Meshes culled after buffer geom vertex positions updated - three.js

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.

Related

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

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.

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>

Object with a higher renderOrder being clipped by rotated element

A rotated object (cylinder in this case) cuts off objects (a triangle made by lines in this case) even though the renderOrder of the second object is higher. See this jsfiddle demo for the effect.
The triangle should be rendered completely on top of the cylinder but is cut off where the outside of the cylinder intersects with it. It's easier to understand what's happening when a texture is used, but jsfiddle is bad at using external images.
var mesh, renderer, scene, camera, controls;
init();
animate();
function init() {
renderer = new THREE.WebGLRenderer({
antialias: true,
preserveDrawingBuffer: true
});
renderer.setClearColor(0x24132E, 1);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.set(0, 0, 7);
camera.lookAt(scene.position)
scene.add(camera);
var geometry = new THREE.CylinderGeometry(1, 1, 100, 32, 1, true);
var material = new THREE.MeshBasicMaterial({
color: 0x0000ff
});
material.side = THREE.DoubleSide;
mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = Math.PI / 2;
scene.add(mesh);
var c = 3, // Side length of the triangle
a = c / 2,
b = Math.sqrt(c * c - a * a),
yOffset = -b / 3; // The vertical offset (if 0, triangle is on x axis)
// Draw the red triangle
var geo = new THREE.Geometry();
geo.vertices.push(
new THREE.Vector3(0, b + yOffset, 0),
new THREE.Vector3(-a, 0 + yOffset, 0),
new THREE.Vector3(a, 0 + yOffset, 0),
new THREE.Vector3(0, b + yOffset, 0)
);
var lineMaterial = new THREE.LineBasicMaterial({
color: 0xff0000,
linewidth: 5,
linejoin: "miter"
});
plane = new THREE.Line(geo, lineMaterial);
// Place it on top of the cylinder
plane.renderOrder = 2; // This should override any clipping, right?
scene.add(plane);
}
function animate() {
requestAnimationFrame(animate);
render();
}
function render() {
renderer.render(scene, camera);
}
Am I doing something wrong or is this a bug?
for the effect that you want use a second scene and render it onto the first one
function init(){
.....
renderer.autoClear = false;
scene.add(tube);
overlayScene.add(triangle);
.....
}
function render() {
renderer.clear();
renderer.render(scene, camera);
renderer.clearDepth();
renderer.render(overlayScene, camera);
}
renderOrder does not mean what you think it means, look at the implementation in WebGLRenderer
objects are sorted by the order, if it meant what you anticipated from it, there would always be some fixed rendering order and colliding objects would be seen through each other, renderOrder is AFAIK used when you have issues with order of transparent/ not opaque objects
I worte a little plugin for three.js for flares for my game. Three.js built-in flares plugin is slow and I preferred not to run another rendering pass which was cutting framerate in half. Here's how I got flares visible on top of objects which were actually in front of them.
Material parameters:
{
side: THREE.FrontSide,
blending: THREE.AdditiveBlending,
transparent: true,
map: flareMap,
depthWrite: false,
polygonOffset: true,
polygonOffsetFactor: -200
}
depthWrite - set to false
polygonOffset - set to true
polygonOffsetFactor - give negative number to get object in front of others. Give it some really high value to be really on top of everything i.e. -10000
Ignore other params, they are needed for my flares

Superimposing of color

I want to paint cubes red color by means of a mouse. But thus the green cube (at the left) becomes not red, but black. The white cube (on the right) is colored normally. What to do?
example here
// init
var material = new THREE.MeshLambertMaterial({
color: 0x00ff00,
side: THREE.DoubleSide,
vertexColors: THREE.FaceColors
});
var geometry = new THREE.BoxGeometry(100, 100, 100, 4, 4, 4);
var Cube = new THREE.Mesh(geometry, material);
Cube.position.x = -100;
scene.add(Cube);
objects.push(Cube);
var material = new THREE.MeshLambertMaterial({
color: 0xffffff,
side: THREE.DoubleSide,
vertexColors: THREE.FaceColors
});
var geometry = new THREE.BoxGeometry(100, 100, 100, 4, 4, 4);
var Cube = new THREE.Mesh(geometry, material);
Cube.position.x = 100;
scene.add(Cube);
objects.push(Cube);
document.addEventListener('mousedown', onDocumentMouseDown, false);
//
function onDocumentMouseDown(event) {
var vector = new THREE.Vector3(
(event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1, 0.5);
vector.unproject(camera);
raycaster.set(camera.position, vector.sub(camera.position).normalize());
var intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
var index = intersects[0].faceIndex;
// change the color of the closest face.
intersects[0].face.color = color;
intersects[0].object.geometry.colorsNeedUpdate = true;
}
}
In your example, the final color is the component-wise product of the material color ( 0x00ff00 ) and the face color ( 0xff0000 ), which results in black ( 0x000000 ).
For that reason, when you have face colors, it is a good idea to set the material color to white.
three.js r.69
I suspect your lighting model is the cause of this. If you try painting the dark sides of the white cube, you will also see black faces. There is a large difference between white ffffff and green 00ff00. Your white cube even appears blue due to the hemi light.
Try using a point light instead of your hemi light and see if it makes a difference.

how is the final color of a rendered mesh is determined?

We give color while initializing a material. We also specify a color while initializing ambient and directional light sources. How is the final color of the mesh is determined.
I see no change in the final color of mesh when i change the color of the material. However the rendered color of the mesh is changing while i change the color of light sources (ambient or directional).
So
1) what is the use of specifying a color, while initializing a material ?, and
2) How is the final color of the mesh is determined
darkMaterial = new THREE.MeshBasicMaterial( { color: 0xff0000 } );
darkMaterialL = new THREE.MeshLambertMaterial( { color: 0xffff00 } );
darkMaterialP = new THREE.MeshPhongMaterial( { color: 0xffff00 } );
var ambientLight = new THREE.AmbientLight(0x00ff00);
var light = new THREE.PointLight(0x000000);
light.position.set(0,150,100);
scene.add(ambientLight);
scene.add(light);
The above are the lights and materials i used.
I wrote a jsfiddle for you to take a look at: http://jsfiddle.net/fnR4E/
var camera, scene, renderer;
var geometry = new Array();
var material = new Array();
var mesh = new Array();
var light;
var angle = 0.1;
init();
render();
function init() {
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.z = 5;
camera.position.y = 5;
scene = new THREE.Scene();
geometry[0] = new THREE.SphereGeometry(1, 8, 6, 0, Math.PI * 2, 0, Math.PI);
geometry[1] = new THREE.SphereGeometry(1, 8, 6, 0, Math.PI * 2, 0, Math.PI);
geometry[2] = new THREE.SphereGeometry(1, 8, 6, 0, Math.PI * 2, 0, Math.PI);
material[0] = new THREE.MeshBasicMaterial({ color: 0xff0000 });
material[1] = new THREE.MeshLambertMaterial({ ambient: 0xffffff, color: 0x00FF00 });
material[2] = new THREE.MeshPhongMaterial({ ambient: 0xffffff, color: 0xdddddd, specular: 0xFFFFFF, shininess: 15 });
mesh[0] = new THREE.Mesh(geometry[0], material[0]);
mesh[1] = new THREE.Mesh(geometry[1], material[1]);
mesh[2] = new THREE.Mesh(geometry[2], material[2]);
var ambientLight = new THREE.AmbientLight(0x007700);
var light = new THREE.PointLight(0xFFFFFF);
light.position.set(0, 2, 0);
scene.add(ambientLight);
scene.add(light);
mesh[0].position.set(-2, 0, 0);
mesh[2].position.set(2, 0, 0);
scene.add(mesh[0]);
scene.add(mesh[1]);
scene.add(mesh[2]);
renderer = new THREE.CanvasRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
}
function render() {
requestAnimationFrame(render);
camera.position.x = 5 * Math.cos(angle);
camera.position.z = 5 * Math.sin(angle);
camera.lookAt(new THREE.Vector3(0, 0, 0));
angle += 0.01;
renderer.render(scene, camera);
}
The first mesh is using MeshBasicMaterial which essentially means it is lit by material color alone, for proof you can change the values of ambientLight and light to whatever you want and it won't effect the rendered color of this mesh.
The following two meshes (the first is MeshLambertMaterial and the second is MeshPhongMaterial) use both lights. For additional reading on the theory behind each of the shading models (Lambertian and Phong) check out these excellent wikipedia articles:
http://en.wikipedia.org/wiki/Lambertian_reflectance
http://en.wikipedia.org/wiki/Phong_reflection_model
Here is a more "practical" explanation of what is going on (but you'll probably at least want to refer to the wiki articles for the equations that are discussed below):
The ambientLight is multiplied by the material 'ambient' value to produce the mesh ambient color. This color only gets used up to the amount specified by the diffuse color of the material. For example, if material ambient value is 0xFFFFFF and AmbientLight is 0x00FF00 then the mesh has a fully green ambient light - but, if the diffuse color of the material ('color') contains NO green color channel (e.g. 0xFF00FF) then there is no ambient light applied to the mesh. Alternatively, if there is a diffuse color of 0x007700 (half of the full green channel) then you will see ambient light on the object of the color 0x007700.
The diffuse color is denoted by the material 'color' value. This is the perceived color of the mesh. In both the Lambert and BlinnPhong shading models this color is multiplied by the dot product of the vertex or fragment normal with the light vector. In essence, this means that the more directly lit a vertex or fragment is - the closer to the full diffuse color it will be. A vertex or fragment that is not directly lit by a light source at all is black. AmbientLight sources are not included in this dot product calculation.
NOTE: Occluding meshes are not accounted for in this dot product calculation. Only the angle between the light source and the vertex or fragment is considered.
Finally, the MeshPhongMaterial uses an additional property called specular. This is the reflective light that produces the "shiny" spot on a mesh. This comes from calculating the angle of reflection against the normal from the light source. The material property 'specular' determines the color of this reflection spot. Once again, AmbientLight sources are not included in this lighting calculation.
NOTE: Once again, occluding meshes are not accounted for in this calculation.
Fixed the problem.

Resources