Hello everyone,
I’ve been following (at a veeery slow pace) Freya Holmer’s awesome Math for Game Devs course, and I’m at the chapter about 4x4 matrices and cross products.
Trying to replicate heur demonstration into Three.js, I use a rotated mesh with a plane geometry intersecting with a ray cast from the camera.
I’m able to compute the directional vectors using the THREE.Vector3.crossVectors()
method, but when I apply the matrix made from these vectors, the results doesn’t seem right:
matrix.makeBasis(xDirection, yDirection, zDirection);
cubeMesh.setRotationFromMatrix(matrix);
Am I using the correct methods? Am I missing a transformation (local to world, world to local, another…)?
You can also check the sandbox here.
1 Like
The methods you’re using sound plausible. Are you trying to orient a cube to the surface?
Freya is describing the mathematical basis for achieving that, but there are shortcuts you can take in threejs to achieve a similar effect…
specifically… the raycaster returns a surface normal… you can convert the normal to worldspace with
let worldNormal = hit.normal.clone().transformDirection( hit.object.matrixWorld )
add it to the position of the cube..
let lookTarget = worldNormal.add( cube.position );
cube.lookAt( lookTarget );
The techniques you’re learning are great knowledge to have to understand how things work, and there are often higher level constructs/utilities that wrap a lot of the functionality, especially in threejs.
1 Like
Yes, I am indeed trying to orient the cube to match the surface which the ray intersects with. Using THREE.Mesh.lookAt()
method just do that, although, I’m loosing control over cube orientation on x and z axis (which I didn’t have that far, anyway).
I’m still interested to understand the usage of a rotation matrix in Three.js, if someone gets an idea. Here’s the rest of the code:
origin.copy(hits[0].point);
yDirection
.copy(hits[0].normal)
.transformDirection(hits[0].object.matrixWorld);
xDirection.crossVectors(yDirection, raycaster.ray.direction).normalize();
zDirection.crossVectors(yDirection, xDirection);
cubeMesh.position.copy(origin);
cubeMesh.position.add(yDirection.clone().multiplyScalar(0.5));
// That part would be swapped for cubeMesh.lookAt(origin.add(yDirection))
matrix.makeBasis(xDirection, yDirection, zDirection);
cubeMesh.setRotationFromMatrix(matrix);
Or you can look into the sandbox code here.
1 Like
.makeBasis also wipes out the position, and scaling, so you’ll need to at least re-set the position i think.
matrix.makeBasis(xDirection, yDirection, zDirection);
matrix.setPosition(cubeMesh.position)
And if you want it to be placed On the surface… not inside it… you might want to offset that position by the normal * half cube width…
matrix.setPosition(hits[0].normal.clone().multiplyScalar(.5).add(cubeMesh.position))
And then, once you have set up the .matrix, you probably want to decompose it back into:
cubeMesh.matrix.decompose(cubeMesh.position,cubeMesh.quaternion,cubeMesh.scale)
otherwise it will just get overwritten with the old values from cubeMesh.position/quaternion/scale on the next render.
Or smth. hth
1 Like
Thanks, using the transformation matrix is even better than the lookAt()
method: now the x and y axis are taken into account!
But I’m still confused with what I had wrong previously, from what I understand, if I only care about the rotation, and not the position nor scale, the upper 3x3 matrix should have been enough. But I had the rotation wrong, while scale and position were handled correctly through vector math (simple addition to get the position, no scaling).
Edit: Not related, but does somebody know why the OrbitControls in the sandbox don’t work?
try:
labelRenderer.domElement.style.pointerEvents = "none";
css2drenderer is stealing the mouse.
1 Like
Ah indeed, thank you very much for your expert’s advices!
EDIT: Now playing with Group
s, and I’m not understanding how to transform the coordinates. It seems that:
hits[0].point
and hits[0].normal
are in world coordinates
cubeMesh.position
, cubeMesh.scale
and cubeMesh.rotation
are in local coordinates, which are modified by the Group
they belong too.
So my idea was to set both the point and normal in group’s local space before computing the new position and looking direction of the cube:
origin.copy(hits[0].point);
yDirection.copy(hits[0].normal).transformDirection(hits[0].object.matrixWorld);
group.worldToLocal(origin);
group.worldToLocal(yDirection);
cubeMesh.lookAt(origin.add(yDirection));
cubeMesh.position.copy(origin);
cubeMesh.position.add(yDirection.clone().multiplyScalar(0.5));
It turned out that was not the correct computations…
For the transformation matrix, my idea was to transform the vectors with the world matrix from the group before using them as basis for the matrix, which again, didn’t work as expected… (using local matrix instead of world matrix yielded the same results)
group.worldToLocal(origin);
xDirection.transformDirection(group.matrixWorld);
yDirection.transformDirection(group.matrixWorld);
zDirection.transformDirection(group.matrixWorld);
matrix.makeBasis(xDirection, yDirection, zDirection);
matrix.setPosition(yDirection.clone().multiplyScalar(0.5).add(origin));
matrix.decompose(cubeMesh.position, cubeMesh.quaternion, cubeMesh.scale);
The chapter on matrices in the documentation is a little scarce regarding all the transformations that can be done, are there another place this improtant bits of information are shared?
Thank you, that seems a very complete book!
However, ain’t there implementation choices from the Three.js library that would need to be taken into account? Like which methods use local space, and which one use world space coordinates? Or which operation is destructive, and which one is non-destructive?
On my issue with Groups, I wrongly assumed that applying the local matrix to a vector that have previously been transformed from the world matrix would bring back the vector to its original value.
yDirection.copy(hits[0].normal); // Object { x: 0, y: 0, z: 1 }
yDirection.transformDirection(hits[0].object.matrixWorld); // Object { x: 0, y: 0.8660254037844386, z: 0.5000000000000001 }
yDirection.transformDirection(hits[0].object.matrix); // Object { x: 0, y: 0.8660254037844386, z: 0.5000000000000001 }
But the solution was to keep the normal vector unmodified:
yDirection.copy(hits[0].normal);
yDirection.transformDirection(hits[0].object.matrixWorld);
const target = origin.clone().add(yDirection.clone().multiplyScalar(4));
cubeMesh.lookAt(target);
group.worldToLocal(origin);
cubeMesh.position.copy(
origin.clone().add(hits[0].normal.clone().multiplyScalar(0.5))
);
Now I’d like to figure out the way to do it with matrix transformation instead of the lookAt
method, as it would give me control over the y rotation of the cube.