intersect_ray giving inaccurate surface normals

Godot Version

4.4.1.stable

Question

every frame, i am using intersect_ray to find where the mouse hits to place something
the issue is that using the default Godot Physics engine gives inaccurate surface normals
i have tried Jolt Physics and it does report more accurate normals but i can’t use since it causes various glitches in my game.
they are mostly accurate but always slightly off, which is unacceptable for my building system

var c_part_move = null
var c_current_part_hovered = null
var c_silhoutte: MeshInstance3D = (func ():
  var instance = MeshInstance3D.new()
  var mesh = BoxMesh.new()
  var material = StandardMaterial3D.new()
  material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
  material.albedo_color = Color.from_rgba8(74, 255, 74, 67)
  mesh.material = material
  instance.mesh = mesh
  instance.top_level = true
  return instance
).call()
var c_anchor = false
var c_weld_target = null
var c_rotation = Vector3(0, 0, 0)
var c_last_normal = Vector3.ZERO

var move_tool_ray_query_params: PhysicsRayQueryParameters3D = (func ():
  var ray_query = PhysicsRayQueryParameters3D.new()
  ray_query.collide_with_areas = false
  ray_query.collision_mask = 0b101
  return ray_query
).call()

func align_with_y(in_xform, new_y):
  var xform = Transform3D(in_xform)
  xform.basis.y = new_y
  xform.basis.x = -xform.basis.z.cross(new_y)
  xform.basis = xform.basis.orthonormalized()
  return xform

# ... inside a function
  var camera: Camera3D = player.get_node("CameraPivot/Mount").get_child(0)
  var mouse = viewport.get_mouse_position()
  var ray_length = 100
  var from = camera.project_ray_origin(mouse)
  var to = from + camera.project_ray_normal(mouse) * ray_length
  move_tool_ray_query_params.from = from
  move_tool_ray_query_params.to = to
  move_tool_ray_query_params.exclude = [c_part_move.get_rid()]
  var result = space.intersect_ray(move_tool_ray_query_params)
  # hit nothing, don't update position
  if result.size() == 0: return
  var normal: Vector3 = result.normal
  c_silhoutte.position = result.position
  var rotate_pivot = c_silhoutte.position
  var rotate_angle = deg_to_rad(90)
  c_silhoutte.transform = \
    align_with_y(c_silhoutte.transform, normal)
  c_silhoutte.transform = \
    c_silhoutte.transform.translated_local(c_silhoutte.mesh.size / 2)
  if Input.is_action_just_released("tool_build_rotate_x"):
    c_rotation += Vector3(1, 0, 0)
  if Input.is_action_just_released("tool_build_rotate_y"):
    c_rotation += Vector3(0, 1, 0)
  if Input.is_action_just_released("tool_build_rotate_z"):
    c_rotation += Vector3(0, 0, 1)
  c_silhoutte.rotation = normal
  for x in range(c_rotation.x):
    rotate_around_pivot(c_silhoutte, rotate_pivot, Vector3(1, 0, 0), rotate_angle)
  for y in range(c_rotation.y):
    rotate_around_pivot(c_silhoutte, rotate_pivot, Vector3(0, 1, 0), rotate_angle)
  for z in range(c_rotation.z):
    rotate_around_pivot(c_silhoutte, rotate_pivot, Vector3(0, 0, 1), rotate_angle)
  var is_on_terrain = false
  for chunk in root.get_node("Game/Terrain").get_children():
    if chunk.get_instance_id() == result.collider_id:
      is_on_terrain = true
  if not is_on_terrain:
    var collider = result.collider
    if collider.get_parent() != root.get_node("Game/Parts"):
      c_weld_target = null
      return
    c_weld_target = collider
  else: c_weld_target = null
  #c_silhoutte.rotation += c_rotation
  #if c_rotation != Vector3.ZERO: c_silhoutte.rotate_object_local(c_rotation.normalized(), deg_to_rad(900))
  c_last_normal = normal
  c_anchor = is_on_terrain

sorry for the messy code, i havent refactored it yet
is there a way to increase surface normal accuracy?
sorry if something is unclear, this is my first post on the forum


to be clear, c_part_move is the wood block there which will be moved and c_silhoutte is the green transparent box. this code runs to update c_silhoutte’s position so that when the mouse is lifted, c_part_move is placed at c_silhoutte position & rotation

There is a lot of non-trivial code and logic here, but at a first glance you should generally avoid manipulating basis properties directly in this way:

You can use look_at(...) for nodes and looking_at(...) for transforms and bases. So, Basis.looking_at(new_y.cross(old_basis.x), new_y) or something similar. I don’t know how much ‘accuracy’ that’ll add, but floating point errors are unavoidable. Also can I ask in what way exactly the results are off? Maybe you can fix that simply by some minute snapping.

thank you, ill try that
the normals are off by ~10 degrees in a random direction (depending on the camera direction i believe) using godot physics and much smaller in jolt physics
i can’t use jolt physics but i’ll do as you said and if it doesnt work ill try snapping

ive modified align_with_y like you said but the normals are still inaccurate

here you can see the little red orb with a grey bit sticking out
its an indicator i’ve made to show the raycast results’ position and normal
its supposed to be exactly aligned with the other thing you’re dragging on top of but here it clearly is not

and the results are even weirder on the side of a box
(i cant upload more than one image but they are misaligned)
sometimes it looks like the normals are parallel to surface instead of perpendicular for some reason when dragging onto the side of a box

they aren’t exactly aligned to terrain either

sorry for the late reply, took a while to take 5 images then realize i can only put 1

Did some testing and googling, and realized that this is actually a bad case for the looking_at function.

Try your luck with quaternions (my solution, not thoroughly tested):

# this modifies only the basis, so you simply set the position separately
# or rewrite the function however you prefer :)
func align_with_normal_quat(input_basis : Basis, normal_vector : Vector3) -> Basis:
	var rot_basis := Basis(Quaternion(input_basis.y, normal_vector))
	return rot_basis * input_basis

And if that fails look at this issue with a more tested solution.

i tried the function but the issue persisted. the problem is in the raycasting i do itself
though this function is very helpful as my original function that i was using was very troublesome and caused me problems in several of my projects
i’ll look at the other solution
thank you for all your effort by the way

yeah, unfortunately, it just seems that Godot’s intersect_ray isn’t accurate enough for my use case. i’ll have to look into another way of calculating normals to align things

Hm, I still think the problem lies on a higher level, surface normals shouldn’t give such errors (10 degrees is way beyond arithmetic errors). Try isolating parts of the code - so, e.g. using the same meshes test only the raycasting code, after that try different mesh.

the red indicator is what i created to debug the raycast specifically
here is some code i excluded from the function for brevity:

  root.get_node("Game/PivotIndicator").position = result.position
  root.get_node("Game/PivotIndicator").rotation = normal

the box and the debug indicator are always perfectly aligned, but the indicator itself is misaligned which means its an issue with the raycast itself.
i agree that surface normals shouldnt be that inaccurate, but switching to jolt physics does make it more accurate, which indicates that it is an issue with the raycasting itself

there are three things i could do now:

  1. switch to jolt physics and fix all issues that occur in my game with it (the issue wont completely disappear though)
  2. manually calculate the normals since i only care about collisions with parts (very simple boxes) or terrain
  3. tweak the raycast parameters or try a Raycast3D instead of intersect_ray

i’m honestly very confused by this

You also forget the collision shape itself, a lot of problems can stem from issues (sometimes obscure) with the way they were created or generated. Beyond that I’m not versed enough with godot physics to offer any more advice, unfortunately.

the wood boxes are a simple CollisionShape3D in the scene with a normal BoxMesh, but the terrain is created by SurfaceTool and does call generate_normals() at the end
i’ll look into it, thanks again for all your help

1 Like

i’ve been completely unable to make any progress on this issue at all
would love if anybody has any ideas to try to help
this bug is major enough that i have to completely pause all work on my game until it is fixed