Three.js giving me the wrong normal vectors only sometimes

I am loading a parent gltf model in which I would like to be able to drag a child model around the surface of that model and have the child model orient its rotation automatically to that surface. I have already implemented this and it works, but only for some models. First I’ll demonstrate the code that does most of this:

useFrame((state, delta) => {
    if (!isDraggingModel) return;

    raycaster.setFromCamera(state.mouse, camera);
    const intersects = raycaster.intersectObjects(scene.children, true);
  
    // Filters everything out that isn't our target mesh based on the parent's group name.
    const meshIntersects = intersects.filter(intersect => {
      let object = intersect.object;
      // Traverse up the hierarchy until you find an object with a name or reach the scene
      while (object && object !== scene) {
        if (object.name === "ParentGroup") {
          return true; 
        }
        object = object.parent; 
      }
      return false; 
    });
  
    // Grab the first mesh we intersect (outermost) with and update model transform, as well as add normal vectors for debugging
    if (meshIntersects.length > 0) {
      const intersect = meshIntersects[0];

      // Update the model position and rotation to the intersection point and face normal
      if (modelRef.current) {
        modelRef.current.position.copy(intersect.point);
        modelRef.current.lookAt(
          intersect.point.x - intersect.face.normal.x,
          intersect.point.y - intersect.face.normal.y,
          intersect.point.z - intersect.face.normal.z
        );

        // adding normal vectors for debugging
        const arrowHelper = new THREE.ArrowHelper(intersect.face.normal, intersect.point, 1, 0xff0000);
        scene.add(arrowHelper);
      }
    }
  });

To further explain this code:

  1. It raycasts from the mouse and finds an intersect between the ray and the meshes of the parent model.
  2. It then filters out any intersects that are not in the parent model’s group, such as a child mesh or part of the transform.
  3. It takes the first mesh in the array of intersects (the outermost) and tells the child model to position itself there and rotate itself so that it is “looking at” that intersect.
  4. ArrowHelpers are added to show normal vectors of any intersect for debugging purposes.

This works perfectly on some models. however, on many what happens is the child model rotates in a way that doesn’t make sense. When adding the normal vectors in, I can see this is because threejs has calculated normal vectors as going where they shouldn’t. There is a slight pattern to their wrong direction. You can see that here as I drag a model across a parent “dome” like shape:
image
The correct normal vectors would look like this:
dome correct

These models were modelled in onShape. I am able to get example two to work because I have exported just one single part of the larger complex model. If I export the entire model with all parts together, that is when I get incorrect normal vectors. Similarly, if I export the entire thing as a single mesh OBJ, vectors work great. Any format like GLTF that supports complex scenes/multiple meshes gives me this issue. I figured this meant it wasn’t consistently intersecting with the correct mesh, but upon further inspection it seems that it is, so that is not the issue.

Face normals returned by the raycaster hit are in model space not worldspace, so perhaps you need to transform them to worldspace before adding to hit.position.
Something like:

let worldNormal = hit.face.normal.clone().transformDirection(hit.object.matrixWorld)
3 Likes

You are right and your example was exactly what I needed. Thank you very much!

1 Like