Three.js, Calculating the transformation matrix between two BufferGeometry position state - matrix

I'm working on a AR project, where an objects detection AI return the vertices of the detected object.
On the scene I have the detected object as the parent mesh with a BufferGeometry and the position attributes updated from the AI output, I need to calculate its transformation matrix when the vertices change and apply those transformation to its children.
How can I calculate the transformation matrix (Translation, Scaling, Rotation) from one "detection (position vertices)" to another.
Here is a simplified illustration of my problem, where the blue plane is the detected object and the red one its child, I need to calculate the blue plane transformation from its previous position apply them to red one so they can move together :
https://jsfiddle.net/uv76tj89/1/
Thanks.

If you know that the child geometry is going to be half as big as the parent geometry, just apply child.scale.set(0.5, 0.5, 0.5); and then assign the exact same vertex positions as the parent on update with:
parentGeometry.getAttribute('position').array = parentPositions[posIndex];
parentGeometry.getAttribute('position').needsUpdate = true;
childGeometry.getAttribute('position').array = parentPositions[posIndex];
childGeometry.getAttribute('position').needsUpdate = true;
the Three.js engine will apply the 1/2 scale and +5 to the z axis to the child vertices, so you don't have to worry about manually making these adjusments. See the demo below.
(Notice I created Float32Arrays within parentPositions[] as an optimization so you don't have to make a new array and a new BufferAttribute each time you update them. It's not a noticeable performance boost with just 4 vertices, but it does help when you have 1000's of vertices).
var scene = new THREE.Scene();
var camera = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, 1, 1000 );
var renderer = new THREE.WebGLRenderer({antialias : true});
renderer.setClearColor(0x444444);
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
camera.position.z = 100;
new THREE.OrbitControls( camera, renderer.domElement );
/* Helpers */
// Grid helper
var width = 100;
var height = 100;
var gridHelper = new THREE.GridHelper(width * 2, 10, 0x999999, 0x000000);
gridHelper.position.y = -height / 2;
scene.add(gridHelper)
// Axes helper
scene.add(new THREE.AxesHelper(50));
/* parent Mesh */
var parentGeometry = new THREE.PlaneBufferGeometry(width, height);
var parentMaterial = new THREE.MeshBasicMaterial( { color: 0x209ad6} );
var parent = new THREE.Mesh( parentGeometry, parentMaterial );
scene.add(parent);
/* Child mesh */
var childGeometry = new THREE.PlaneBufferGeometry(width, height);
var childMaterial = new THREE.MeshBasicMaterial( {color: 0xFF0000} );
var child = new THREE.Mesh( childGeometry, childMaterial );
parent.add(child);
// Apply desired transformations to the child Mesh
child.position.z = 5;
child.scale.set(0.5, 0.5, 0.5);
/* Parent positions */
// Make all position arrays Float32Array
// so we don't have to create a new one each frame.
var parentPositions = [
//Reference point
new Float32Array([
-50, 50, 0,
50, 50, 0,
-50, -50, 0,
50, -50, 0
]),
//Variations
new Float32Array([
-50, 50, 50,
50, 50, 50,
-50, -50, -50,
50, -50, -50
]),
new Float32Array([
-75, 75, -25,
75, 75, -25,
-75, -75, 25,
75, -75, 25
]),
new Float32Array([
0, 75, -25,
75, 0, -25,
-75, 0, 25,
0, -75, 25,
]),
//... random positions
];
var lastTime = 0;
var posIndex = 0;
function render(currentTime) {
requestAnimationFrame( render );
// Update position attributes
// Instead of making a new attribute on each update
if (currentTime >= lastTime + 1000) {
parentGeometry.getAttribute('position').array = parentPositions[posIndex];
parentGeometry.getAttribute('position').needsUpdate = true;
childGeometry.getAttribute('position').array = parentPositions[posIndex];
childGeometry.getAttribute('position').needsUpdate = true;
lastTime = currentTime;
posIndex = posIndex === 3 ? 0 : posIndex + 1;
}
renderer.render( scene, camera );
}
render()
html, body {margin: 0; padding: 0;overflow: hidden;}
<script src="https://cdn.jsdelivr.net/npm/three#0.117.1/build/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r110/examples/js/controls/OrbitControls.js"></script>
I'm not sure why you originally made the child a PlaneGeometry, but I had to change it to match the parent PlaneBufferGeometry.
Edit:
I guess I'm just now starting to understand what your problem is, and you'd need to calculate the vector that's perpendicular to your plane face. You can pick any three out of the four vertices to do this:
You can use this answer to figure out which point the triangle is pointing towards, and then you can make the child point in that direction with child.lookAt(x, y, z);
You can read this article for a little more in-depth explanation on how to get that perpendicular.

Related

Meshes culled after buffer geom vertex positions updated

Some objects are being frustrum culled after I amend their buffer geometry vertex positions. I don't want to set frustrumCulled = false, nor can I (feasibly) change the mesh position to anything other than (0,0,0). What options do I have here? My expectation is that the meshes should behave consistently with each other. I'm obviously missing something here.
Demo:
2 Meshes, one green, one red
Green mesh is initialised with position 0,0,0 with vertices around 0,10,0
Red mesh is initialised with position 0,0,0 with vertices around 0,0,0, but after 2 seconds, vertices are updated to around 0,10,0
Orbit controls allow zooming (Just zoom in see see actual behaviour)
Expected behaviour: Both meshes are visible when I zoom / move the camera. Or at least, they both behave in the same way
Actual behaviour, green mesh behaves as expected, red mesh is sometimes culled
const OrbitControls = THREE.OrbitControls // CDN shim
// Helper to create mesh with buffer geom
const createGeo = (initialY, color) => {
const geometry = new THREE.BufferGeometry()
const vertices = new Float32Array([
0, initialY, 0,
0, initialY + 1, 0,
0, initialY, 1,
0, initialY, 0,
0, initialY + 1, 0,
1, initialY, 0
])
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
const material = new THREE.MeshBasicMaterial({
color: color,
side: THREE.DoubleSide
})
mesh = new THREE.Mesh(geometry, material)
// Note: Setting frustumCulled to false always allows this to be seen, but I don't this object to always be seen
// mesh.frustumCulled = false
scene.add(mesh)
return mesh
}
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.1, 100)
let controls
const greenMesh = createGeo(10, 0x00FF00) // Set Green at 0,10,0
const redMesh = createGeo(0, 0xFF0000) // Set red at 0,0,0, then update to 0,10,0
const renderer = new THREE.WebGLRenderer({antialias: true})
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(() => {
controls.update()
greenMesh.rotation.y += 0.1
redMesh.rotation.y -= 0.1
renderer.render(scene, camera)
})
document.body.appendChild(renderer.domElement)
controls = new OrbitControls(camera, renderer.domElement)
camera.position.set(15, -20, 15)
controls.target.set(0, 10, 0)
window.onresize = function() {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
}
scene.add(new THREE.AxesHelper(100))
// Update red mesh to same position as green mesh
setTimeout(() => {
redMesh.geometry.attributes.position.setXYZ(0, 0, 10, 0)
redMesh.geometry.attributes.position.setXYZ(1, 0, 11, 0)
redMesh.geometry.attributes.position.setXYZ(2, 0, 10, 1)
redMesh.geometry.attributes.position.setXYZ(3, 0, 10, 0)
redMesh.geometry.attributes.position.setXYZ(4, 0, 11, 0)
redMesh.geometry.attributes.position.setXYZ(5, 1, 10, 0)
// As per - https://threejs.org/docs/#manual/en/introduction/How-to-update-things
redMesh.geometry.attributes.position.needsUpdate = true
redMesh.geometry.computeBoundingBox()
// When zooming / panning, red mesh is not always visible
}, 2000)
<script src="https://cdn.jsdelivr.net/npm/three#0.147.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three#0.147.0/examples/js/controls/OrbitControls.min.js"></script>
As mentioned by #kikon, both
mesh.geometry.computeBoundingBox()
mesh.geometry.computeBoundingSphere()
are required. Whilst it doesn't explicitly state these in this docs, they have to both be used when updating vertex positions (I had made the incorrect assumption that you would use either the box or sphere).
The code for frustrum culling uses the bounding sphere, and a bounding sphere is automatically created once first invoked (hence the difference in red / green mesh behaviour), and has to be recomputed and both methods need to be called in this case.

Default Object.rotation's axis of rotation (Three.js)

Does changing the value of an object's rotation property rotate the object about the world axes or the object's axes? For eg
object.rotation.x += 2
Will this rotate the object about the world X axis or the object's X axis?
I tried the following example :
var materials = [];
materials.push( [ new THREE.MeshBasicMaterial( { color: 0xff0000 } ) ] );
materials.push( [ new THREE.MeshBasicMaterial( { color: 0xff0000 } ) ] );
materials.push( [ new THREE.MeshBasicMaterial( { color: 0x00ff00 } ) ] );
materials.push( [ new THREE.MeshBasicMaterial( { color: 0x00ff00 } ) ] );
materials.push( [ new THREE.MeshBasicMaterial( { color: 0x0000ff } ) ] );
materials.push( [ new THREE.MeshBasicMaterial( { color: 0x0000ff } ) ] );
object = new THREE.Mesh( new THREE.CubeGeometry( 20, 20, 20, 1, 1, 1, materials ), new THREE.MeshFaceMaterial() );
object.position.x = 0;
object.position.y = 0;
object.position.z = 0;
object.rotation.x = 0;
object.rotation.y = 0;
object.rotation.z = 0;
object.scale.x = 1;
object.scale.y = 1;
object.scale.z = 1;
scene.add( object );
After i have added the object to the scene i tried the following -
object.rotation.x += Math.PI/4;
object.rotation.y += Math.PI/4;
object.rotation.z += Math.PI/4;
Until now the object rotates with respect to its own X,Y and Z axes.
but doing:
object.rotation.x += Math.Pi/4;
now rotates the object 45 degrees about the World X axis instead of the object's X axis.
The rotations are in local space. However, they are evaluated in a specific order which you can set yourself. The object.rotation property is an Euler, and as you can read in the description of the order-property:
The order in which to apply rotations. Default is 'XYZ', which means
that the object will first be rotated around its X axis, then its Y
axis and finally its Z axis. Other possibilities are: 'YZX', 'ZXY',
'XZY', 'YXZ' and 'ZYX'. These must be in upper case.
Three.js uses intrinsic Tait-Bryan angles. This means that rotations
are performed with respect to the local coordinate system. That is,
for order 'XYZ', the rotation is first around the local-X axis (which
is the same as the world-X axis), then around local-Y (which may now
be different from the world Y-axis), then local-Z (which may be
different from the world Z-axis).
(Emphasis added by me).
Consider the following example, every cube is updated with the same y-axis rotation. In every case the rotation is happening around the y-axis in local space, but:
The first cube has not been rotated in any other axis, making its
local space axis the same as the world space axis.
The second cube has been rotated around its x-axis, which means that
the y-axis is also rotated.
The third cube has also been rotated, but the order of rotation is
changed so that the y-axis rotation happens first, while it is still the same as the world y-axis, and the rotation around x happens afterward.
const canvas = document.getElementById("canvas");
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(10, 2, 20, 40);
const renderer = new THREE.WebGLRenderer({ canvas });
const light = new THREE.PointLight();
light.position.set(0, 5, 10)
scene.add(light);
camera.position.set(0, 0, 25);
const material = new THREE.MeshLambertMaterial();
const geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
const cube1 = new THREE.Mesh(geometry, material);
const cube2 = new THREE.Mesh(geometry, material);
const cube3 = new THREE.Mesh(geometry, material);
cube1.position.set(-2, 0, 0);
cube2.position.set( 0, 0, 0);
cube3.position.set( 2, 0, 0);
scene.add(cube1);
scene.add(cube2);
scene.add(cube3);
//The first cube is not rotated
cube1.rotation.set(0, 0, 0);
//The second cube is rotated pi/4 rad around X
cube2.rotation.set(Math.PI/4, 0, 0);
//The third cube is rotated pi/4 rad around X
//AND the rotation order is set to YXZ to make the y-rotation happen first
cube3.rotation.set(Math.PI/4, 0, 0);
cube3.rotation.order = "YXZ";
let t0 = performance.now();
function update(){
const t1 = performance.now();
const rotation = 0.001 * (t1 - t0);
cube1.rotation.y += rotation;
cube2.rotation.y += rotation;
cube3.rotation.y += rotation;
renderer.render(scene, camera);
t0 = t1;
requestAnimationFrame(update);
}
update();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/92/three.js"></script>
<canvas id="canvas" width="400" height="200"></canvas>

how to draw a flat shape in 3-space in three.js?

I'm trying to construct a collection of flat shapes in three.js. Each one is defined as a series of coplanar Vector3 points, but the shapes are not all coplanar. Imagine two flat rectangles as the roof of a house, but with much more complex shapes.
I can make flat Shape objects and then rotate and position them, but since my shapes are conceived in 3d coordinates, it would be much simpler to keep it all in 3-space, which the Shape object doesn't like.
Is there some much more direct way to simply specify an array of coplanar Vector3's, and let three.js do the rest of the work?
I thought about this problem and came up with the idea, when you have a set of co-planar points and you know the normal of the plane (let's name it normal), which your points belong to.
We need to rotate our set of points to make it parallel to the xy-plane, thus the normal of that plane is [0, 0, 1] (let's name it normalZ). To do it, we find quaternions with .setFromUnitVectors() of THREE.Quaternion():
var quaternion = new THREE.Quaternion().setFromUnitVectors(normal, normalZ);
var quaternionBack = new THREE.Quaternion().setFromUnitVectors(normalZ, normal);
Apply quaternion to our set of points
As it's parallel to xy-plane now, z-coordinates of points don't matter, so we can now create a THREE.Shape() object of them. And then create THREE.ShapeGeometry() (name it shapeGeom) from given shape, which will triangulate our shape.
We need to put our points back to their original positions, so we'll apply quaternionBack to them.
After all, we'll assign our set of points to the .vertices property of the shapeGeom.
That's it. If it'll work for you, let me know ;)
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 20, 40);
camera.lookAt(scene.position);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.target = new THREE.Vector3(10, 0, 10);
controls.update();
var grid = new THREE.GridHelper(50, 50, 0x808080, 0x202020); // xy-grid
grid.geometry.rotateX(Math.PI * 0.5);
scene.add(grid);
var points = [ // all of them are on the xz-plane
new THREE.Vector3(5, 0, 5),
new THREE.Vector3(25, 0, 5),
new THREE.Vector3(25, 0, 15),
new THREE.Vector3(15, 0, 15),
new THREE.Vector3(15, 0, 25),
new THREE.Vector3(5, 0, 25),
new THREE.Vector3(5, 0, 5)
]
var geom = new THREE.BufferGeometry().setFromPoints(points);
var pointsObj = new THREE.Points(geom, new THREE.PointsMaterial({
color: "red"
}));
scene.add(pointsObj);
var line = new THREE.LineLoop(geom, new THREE.LineBasicMaterial({
color: "aqua"
}));
scene.add(line);
// normals
var normal = new THREE.Vector3(0, 1, 0); // I already know the normal of xz-plane ;)
scene.add(new THREE.ArrowHelper(normal, new THREE.Vector3(10, 0, 10), 5, 0xffff00)); //yellow
var normalZ = new THREE.Vector3(0, 0, 1); // base normal of xy-plane
scene.add(new THREE.ArrowHelper(normalZ, scene.position, 5, 0x00ffff)); // aqua
// 1 quaternions
var quaternion = new THREE.Quaternion().setFromUnitVectors(normal, normalZ);
var quaternionBack = new THREE.Quaternion().setFromUnitVectors(normalZ, normal);
// 2 make it parallel to xy-plane
points.forEach(p => {
p.applyQuaternion(quaternion)
});
// 3 create shape and shapeGeometry
var shape = new THREE.Shape(points);
var shapeGeom = new THREE.ShapeGeometry(shape);
// 4 put our points back to their origins
points.forEach(p => {
p.applyQuaternion(quaternionBack)
});
// 5 assign points to .vertices
shapeGeom.vertices = points;
var shapeMesh = new THREE.Mesh(shapeGeom, new THREE.MeshBasicMaterial({
color: 0x404040
}));
scene.add(shapeMesh);
render();
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
}
body {
overflow: hidden;
margin: 0;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.90.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three#0.90.0/examples/js/controls/OrbitControls.js"></script>

Smooth Ring 3D in three.js

I want to draw a ring with the help of ExtrudeGeometry.
Ring3D = function(innerRadius, outerRadius, heigth, Segments) {
var extrudeSettings = {
amount: heigth,
bevelEnabled: false,
curveSegments: Segments
};
var arcShape = new THREE.Shape();
arcShape.moveTo(outerRadius, 0);
arcShape.absarc(0, 0, outerRadius, 0, Math.PI * 2, false);
var holePath = new THREE.Path();
holePath.moveTo(innerRadius, 0);
holePath.absarc(0, 0, innerRadius, 0, Math.PI * 2, true);
arcShape.holes.push(holePath);
var geometry = new THREE.ExtrudeGeometry(arcShape, extrudeSettings);
var material = new THREE.MeshPhongMaterial({
color: 0x00ffff
});
var mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = Math.PI / 2;
mesh.position.y = heigth / 2;
var object = new THREE.Object3D;
object.add(mesh);
return object;
}
The resulting figure has visible scars. And the cylinder and torus such scars not. How to get rid of them? Example here.
with geometry.computeVertexNormals();
var shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.lineTo(0, 10);
shape.quadraticCurveTo(35, 30, 0, 50);
shape.lineTo(0, 60);
shape.quadraticCurveTo(48, 30, 0, 0);
var extrudeSettings = {
amount : 20,
steps : 10,
bevelEnabled: false,
curveSegments: 8
};
var geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings );
var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial( { color: '0x804000' ,transparent: true,opacity: 0.2} ) );
scene.add( mesh );
You need to .computeVertexNormals() from your geometry. But it seems there is some issue (with a solution) explained here: https://github.com/mrdoob/three.js/issues/323. I am not sure if it will work in your case.
I have found a comment in the code of ExtrudeGeometry:
this.computeFaceNormals();
// can't really use automatic vertex normals
// as then front and back sides get smoothed too
// should do separate smoothing just for sides
//this.computeVertexNormals();
So it seems it is not supported now :(

How do I construct a hollow cylinder in Three.js

I'm having difficulties constructing a hollow cylinder in Three.js.
Should I go and construct it using CSG or by stitching the vertices together?
var extrudeSettings = {
amount : 2,
steps : 1,
bevelEnabled: false,
curveSegments: 8
};
var arcShape = new THREE.Shape();
arcShape.absarc(0, 0, 1, 0, Math.PI * 2, 0, false);
var holePath = new THREE.Path();
holePath.absarc(0, 0, 0.8, 0, Math.PI * 2, true);
arcShape.holes.push(holePath);
var geometry = new THREE.ExtrudeGeometry(arcShape, extrudeSettings);
This solution uses ChandlerPrall's ThreeCSG.js project: http://github.com/chandlerprall/ThreeCSG
(For now, I recommend using the experimental version that supports materials - the uv branch - http://github.com/chandlerprall/ThreeCSG/tree/uvs)
Here's the code you will need:
// Cylinder constructor parameters:
// radiusAtTop, radiusAtBottom, height, segmentsAroundRadius, segmentsAlongHeight
var smallCylinderGeom = new THREE.CylinderGeometry( 30, 30, 80, 20, 4 );
var largeCylinderGeom = new THREE.CylinderGeometry( 40, 40, 80, 20, 4 );
var smallCylinderBSP = new ThreeBSP(smallCylinderGeom);
var largeCylinderBSP = new ThreeBSP(largeCylinderGeom);
var intersectionBSP = largeCylinderBSP.subtract(smallCylinderBSP);
var redMaterial = new THREE.MeshLambertMaterial( { color: 0xff0000 } );
var hollowCylinder = intersectionBSP.toMesh( redMaterial );
scene.add( hollowCylinder );
It is unlikely that you would have to stitch vertices together. If your cylinder has no thickness, you can use THREE.CylinderGeometry(). If it does have thickness, you can use CSG.
Use SVGloader to load a circle of desired radius (as an SVG). Set the geometry to ExtrudeBufferGeometry and give it your desired height as depth in the extrude settings object.

Resources