Three.js - Accurate ray casting for collision detection - javascript

I'm working with Three.js, version 68. I'm using the same method for collision detection as this guy is using here, which is great most of the time (A big "thank you" goes out to the author!): http://stemkoski.github.io/Three.js/Collision-Detection.html
Here is a link to the source if you want to download it from github. Just look for Collision-Detection.html: https://github.com/stemkoski/stemkoski.github.com
Here is the code that is important to the collision detection:
var MovingCube;
var collidableMeshList = [];
var wall = new THREE.Mesh(wallGeometry, wallMaterial);
wall.position.set(100, 50, -100);
scene.add(wall);
collidableMeshList.push(wall);
var wall = new THREE.Mesh(wallGeometry, wireMaterial);
wall.position.set(100, 50, -100);
scene.add(wall);
var wall2 = new THREE.Mesh(wallGeometry, wallMaterial);
wall2.position.set(-150, 50, 0);
wall2.rotation.y = 3.14159 / 2;
scene.add(wall2);
collidableMeshList.push(wall2);
var wall2 = new THREE.Mesh(wallGeometry, wireMaterial);
wall2.position.set(-150, 50, 0);
wall2.rotation.y = 3.14159 / 2;
scene.add(wall2);
var cubeGeometry = new THREE.CubeGeometry(50,50,50,1,1,1);
var wireMaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe:true } );
MovingCube = new THREE.Mesh( cubeGeometry, wireMaterial );
MovingCube.position.set(0, 25.1, 0);
// collision detection:
// determines if any of the rays from the cube's origin to each vertex
// intersects any face of a mesh in the array of target meshes
// for increased collision accuracy, add more vertices to the cube;
// for example, new THREE.CubeGeometry( 64, 64, 64, 8, 8, 8, wireMaterial )
// HOWEVER: when the origin of the ray is within the target mesh, collisions do not occur
var originPoint = MovingCube.position.clone();
for (var vertexIndex = 0; vertexIndex < MovingCube.geometry.vertices.length; vertexIndex++)
{
var localVertex = MovingCube.geometry.vertices[vertexIndex].clone();
var globalVertex = localVertex.applyMatrix4( MovingCube.matrix );
var directionVector = globalVertex.sub( MovingCube.position );
var ray = new THREE.Raycaster( originPoint, directionVector.clone().normalize() );
var collisionResults = ray.intersectObjects( collidableMeshList );
if ( collisionResults.length > 0 && collisionResults[0].distance < directionVector.length() )
appendText(" Hit ");
}
This works great most of the time, but there are times when I can move the cube partially into the wall, and it won't register a collision. For example, look at this image:
It should say "Hit" in the top-left corner where there are just a bunch of dots, and it's not.
NOTE: I also tried his suggestion and did the following, but it didn't seem to help much:
THREE.BoxGeometry( 64, 64, 64, 8, 8, 8, wireMaterial ) // BoxGeometry is used in version 68 instead of CubeGeometry
Does anyone know how this method could be more accurate? Another question: Does anyone know what the following if statement is for, i.e. why does the object's distance have to be less than the length of the direction vector?:
if ( collisionResults.length > 0 && collisionResults[0].distance < directionVector.length() )

To answer your last question first: that line detects whether the collision happened inside your MovingCube. Your raycasting code casts a ray from the MovingCube's position towards each of its vertices. Anything that the ray intersects with is returned, along with the distance from the MovingCube's position at which the intersected object was found (collisionResults[0].distance). That distance is compared with the distance from the MovingCube's position to the relevant vertex. If the distance to the collision is less than the distance to the vertex, the collision happened inside the cube.
Raycasting is a poor method of collision detection because it only detects collisions in the exact directions rays are cast. It also has some additional edge cases. For example, if the ray is cast from inside another object, the other object might not be considered to be colliding. As another example, raycasting in Three.js uses bounding spheres (or, if unavailable, bounding boxes) to calculate ray intersection, so rays can "intersect" with objects even if they wouldn't hit them visually.
If you're only dealing with spheres or upright cuboids, it's straightforward math to check collision. (That's why Three.js uses bounding spheres and bounding boxes - and most applications that need to do collision checking use secondary collision-only geometries that are less complicated than the rendered ones.) Spheres are colliding if the distance between their centers is less than the sum of their radii. Boxes are colliding if the edges overlap (e.g. if the left edge of box 1 is to the left of the right edge of box 2, and the boxes are within a vertical distance the sum of their half-heights and a horizontal distance the sum of their half-lengths).
For certain applications you can also use voxels, e.g. divide the world into cubical units, do box math, and say that two objects are colliding if they overlap with the same cube-unit.
For more complex applications, you'll probably want to use a library like Ammo.js, Cannon.js, or Physi.js.
The reason raycasting is appealing is because it's workable with more complex geometries without using a library. As you've discovered, however, it's less than perfect. :-)
I wrote a book called Game Development with Three.js which goes into this topic in some depth. (I won't link to it here because I'm not here to promote it, but you can Google it if you're interested.) The book comes with sample code that shows how to do basic collision detection, including full code for a 3D capture-the-flag game.

Related

Three.js: Bounding sphere of a scaled object

I have a set of 3D shapes (pyramid, cube, octahedron, prism etc.) and I need to build described sphere around each of them. It is easy to do so using geometry.boundingSphere as it has radius of the described sphere. But if I scale an object the bounding sphere is not being updated. Is it possible to update bounding sphere relatively to the scale?
Using Three.js 0.129.
const { position } = entity.object3D;
const mesh = entity.getObject3D('mesh') as THREE.Mesh;
mesh.geometry.computeBoundingSphere();
const { radius } = mesh.geometry.boundingSphere;
createSphere(radius, position);
The geometry.boundingSphere property represents the geometry. You could technically have two meshes with different scales share the same geometry, so you would want to maintain the geometry's original bounding sphere, and then compute a new one for each mesh, individually.
One problem with scaling the bounding sphere is that you can scale your mesh in x, y, and z separately, and even invert vertex position values given negative scaling values. unequal scale values would lead to it being less of a sphere, and more of a spheroid, which would not help you in math.
What you can do is recompute a bounding sphere for your mesh, given its updated world transformation matrix. I suggest using the world matrix because other ancestors of your mesh could also influence scale in unpredictable ways.
// Given:
// (THREE.Mesh) yourMesh
// Copy the geometry
let geoClone = yourMesh.geometry.clone() // really bad for memory, but it's simple/easy
// Apply the world transformation to the copy (updates vertex positions)
geoClone.applyMatrix4( yourMesh.matrixWorld )
// Convert the vertices into Vector3s (also bad for memeory)
let vertices = []
let pos = geoClone.attributes.position.array
for( let i = 0, l = pos.length; i < l; i += 3 ){
vertices.push( new THREE.Vector3( pos[i], pos[i+1], pos[i+2] ) )
}
// Create and set your mesh's bounding sphere
yourMesh.userData.boundingSphereWorld = new THREE.Sphere()
yourMesh.userData.boundingSphereWorld.setFromPoints( vertices )
This will create a world-aligned bounding sphere for your mesh. If you want one based on local transformations, you can follow the same idea using the local yourMesh.matrix matrix instead. Just know that your sphere's center will then be based on your mesh's local transformation/rotation, not just its scale.

Three.js rope / cable effect - animating thick lines

With Three.js I want to create the effect of an object swinging from a cable or rope. It doesn't require real physics as the "swinging" object simply follows a fixed animation. The easiest solution is using the THREE.Line, however the problem is that THREE.Line can only be 1px thick and looks kinda awful.
In the three.js examples there is a "fat lines" example :
https://threejs.org/examples/?q=lines#webgl_lines_fat
however the problem is that once I have created the line using LineGeometry() I cannot figure out how to animate it.
The only solution I have found so far is to delete then create a new line every single frame, which works but seems like a really uneconomical, poorly optimized way to do it.
Does anyone know of a better way to either animate Line Geometry without having to delete and replace each frame? Or is there another method within three.js which would allow me to create thicker animated lines?
Thanks!!
I actually have a small project where I animate a bulb swinging along some rope. You can access it here, the functions I'm talking about below are in helperFns.js.
Actually, what I basically do is create my attached object separately :
let geometry = new THREE.SphereGeometry( 1, 32, 32 );
var material = new THREE.MeshStandardMaterial({color:0x000000,emissive:0xffffff,emissiveIntensity:lightIntensity});
bulb = new THREE.Mesh( geometry, material );
light = new THREE.PointLight(0xF5DCAF,lightIntensity,Infinity,2)
light.power = lightIntensity*20000
light.position.set(0,length*Math.sin(theta),z0-length*Math.cos(theta))
light.add(bulb)
light.castShadow = true;
hemiLight = new THREE.HemisphereLight( 0xddeeff, 0x0f0e0d, 0.1 );
scene.add(hemiLight)
scene.add(light)
I then add a spline connected to it :
// Create the wire linking the bulb to the roof
var curveObject = drawSpline(light.position,{x:0,y:0,z:z0},0xffffff);
scene.add(curveObject)
Where drawSpline is the following function :
// Build a spline representing the wire between the roof and the bulb. The new middle point is computed as the middle point shifted orthogonally from the lign by shiftRatio
function drawSpline(beginning,end,clr){
// Compute y sign to know which way to bend the wire
let ySign = Math.sign((end.y+beginning.y)/2)
// Compute the bending strength and multiply per Math.abs(beginning.y) to ensure it decreases as the bulb gets closer to the theta = 0 position, and also to ensure
// that the shift is null if thete is null (no discontinuity in the wire movement)
let appliedRatio = -shiftRatio*Math.abs(beginning.y)
// Compute middle line position vector and the direction vector from the roof to the bulb
let midVector = new THREE.Vector3( 0, (end.y+beginning.y)/2, (end.z+beginning.z)/2 )
let positionVector = new THREE.Vector3(0,end.y-beginning.y,end.z-beginning.z)
// Compute the orthogonal vector to the direction vector (opposite sense to the bending shift)
let orthogVector = new THREE.Vector3(0,positionVector.z,-positionVector.y).normalize()
// Compute the curve passing by the three points
var curve = new THREE.CatmullRomCurve3( [
new THREE.Vector3( beginning.x, beginning.y, beginning.z ),
midVector.clone().addScaledVector(orthogVector,ySign*appliedRatio),
new THREE.Vector3( end.x, end.y, end.z ),
]);
// Build the curve line object
var points = curve.getPoints( 20 );
var geometry = new THREE.BufferGeometry().setFromPoints( points );
var material = new THREE.LineBasicMaterial( { color : clr } );
// Create the final object to add to the scene
var curveObject = new THREE.Line( geometry, material );
return curveObject;
}
It creates the CatmullRomCurve3 interpolating the 3 points (one fix end at (0, 0, 0), one middle point to apply the bend, and the bulb position. You can actually start with a straight line, and then try to compute some curve.
To do so, you want to get the vector orthogonal to the line and shift the line (on the good side) along this vector.
And finally, at each animate() call, I redraw the spline for the new position of the bulb :
scene.children[2] = drawSpline(light.position,{x:0,y:0,z:z0},0xffffff)
Tell me if there is a point you do not get, but it should help for your problem.
Just wanted to post a more detailed version of West Langleys great reply. To animate a THREE Line2 you need to use the commands :
line.geometry.attributes.instanceStart.setXYZ( index, x, y, z );
line.geometry.attributes.instanceEnd.setXYZ( index, x, y, z );
What confused me was the index value - rather than thinking about a Line2 as being vertex points (the method used for creating the line) you need to think of a Line2 as being made of separate individual lines between 2 sets of points... so each line has a Start point and and an End point.
A "W" is therefore NOT defined as 5 vertices but by 4 lines. So you can "split" a Line2 by setting a different Start point to the previous lines End point. The index is the number of lines that make up your object. In my case I have two lines forming a V shape... so I set my index to 1 to affect the end of line 0 and the start of line 1, as in West's example :
var index = 1;
line.geometry.attributes.instanceEnd.setXYZ( index - 1, x, y, z );
line.geometry.attributes.instanceStart.setXYZ( index, x, y, z );
And then you just need to update the line using :
line.geometry.attributes.instanceStart.data.needsUpdate = true;
Thanks again to West for this really useful answer. I'd never have guessed this as you cannot see these variables when you look at the Line2 object properties. Very useful info. I hope it helps someone else at some point.

Three.js detect when object is partially and fully occluded

I'm trying to detect when an object in Three.js is partially and fully occluded (hidden behind) another object.
My current simple solution casts a single ray to the the center of the object:
function getScreenPos(object) {
var pos = object.position.clone();
camera.updateMatrixWorld();
pos.project(camera);
return new THREE.Vector2(pos.x, pos.y);
}
function isOccluded(object) {
raycaster.setFromCamera(getScreenPos(object), camera);
var intersects = raycaster.intersectObjects(scene.children);
if (intersects[0] && intersects[0].object === object) {
return false;
} else {
return true;
}
}
However it doesn't account for the object's dimensions (width, height, depth).
Not occluded (because center of object is not behind)
Occluded (because center of object is behind)
View working demo:
https://jsfiddle.net/kmturley/nb9f5gho/57/
Currently thinking I could calculate the object box size, and cast Rays for each corner of the box. But this might still be a little too simple:
var box = new THREE.Box3().setFromObject(object);
var size = box.getSize();
I would like to find a more robust approach which could give partially occluded and fully occluded booleans values or maybe even percentage occluded?
Search Stack Overflow and the Three.js examples for "GPU picking." The concept can be broken down into three basic steps:
Change the material of each shape to a unique flat (MeshBasicMaterial) color.
Render the scene with the unique materials.
Read the pixels of the rendered frame to collect color information.
Your scenario allows you a few caveats.
Give only the shape you're testing a unique color--everything else can be black.
You don't need to render the full scene to test one shape. You could adjust your viewport to render only the area surrounding the shape in question.
Because you only gave a color only to your test part, the rest of the data should be zeroes, making finding pixels matching your unique color much easier.
Now that you have the pixel data, you can determine the following:
If NO pixels matchthe unique color, then the shape is fully occluded.
If SOME pixels match the unique color, then the shape is at least partially visible.
The second bullet says that the shape is "at least partially" visible. This is because you can't test for full visibility with the information you currently have.
What I would do (and someone else might have a better solution) is render the same viewport a second time, but only have the test shape visible, which is the equivalent of the part being fully visible. With this information in hand, compare the pixels against the first render. If both have the same number (perhaps within a tolerance) of pixels of the unique color, then you can say the part is fully visible/not occluded.
I managed to get a working version for WebGL1 based on TheJim01's answer!
First create a second simpler scene to use for calculations:
pickingScene = new THREE.Scene();
pickingTextureOcclusion = new THREE.WebGLRenderTarget(window.innerWidth / 2, window.innerHeight / 2);
pickingMaterial = new THREE.MeshBasicMaterial({ vertexColors: THREE.VertexColors });
pickingScene.add(new THREE.Mesh(BufferGeometryUtils.mergeBufferGeometries([
createBuffer(geometry, mesh),
createBuffer(geometry2, mesh2)
]), pickingMaterial));
Recreate your objects as Buffer Geometry (faster for performance):
function createBuffer(geometry, mesh) {
var buffer = new THREE.SphereBufferGeometry(geometry.parameters.radius, geometry.parameters.widthSegments, geometry.parameters.heightSegments);
quaternion.setFromEuler(mesh.rotation);
matrix.compose(mesh.position, quaternion, mesh.scale);
buffer.applyMatrix4(matrix);
applyVertexColors(buffer, color.setHex(mesh.name));
return buffer;
}
Add a color based on the mesh.name e.g. an id 1, 2, 3, etc
function applyVertexColors(geometry, color) {
var position = geometry.attributes.position;
var colors = [];
for (var i = 0; i < position.count; i ++) {
colors.push(color.r, color.g, color.b);
}
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
}
Then during the render loop check the second scene for that texture, and match pixel data to the mesh name:
function isOccludedBuffer(object) {
renderer.setRenderTarget(pickingTextureOcclusion);
renderer.render(pickingScene, camera);
var pixelBuffer = new Uint8Array(window.innerWidth * window.innerHeight);
renderer.readRenderTargetPixels(pickingTextureOcclusion, 0, 0, window.innerWidth / 2, window.innerHeight / 2, pixelBuffer);
renderer.setRenderTarget(null);
return !pixelBuffer.includes(object.name);
}
You can view the WebGL1 working demo here:
https://jsfiddle.net/kmturley/nb9f5gho/62/
One caveat to note with this approach is that your picking scene needs to stay up-to-date with changes in your main scene. So if your objects move position/rotation etc, they need to be updated in the picking scene too. In my example the camera is moving, not the objects so it doesn't need updating.
For WebGL2 we will have a better solution:
https://tsherif.github.io/webgl2examples/occlusion.html
But this is not supported in all browsers yet:
https://www.caniuse.com/#search=webgl

Three.js - How to get camera position with respect to origin

The title of this question might a bit ambiguous, but I don't know how to phrase it in a line.
Basically I've got this situation: there is a perspective camera in the scene and a mesh. The mesh is NOT centered at the origin of the axis.
The camera points directly to the center of this object, and it's position (I mean literally the "position" property of the Three.js camera object) is the position with respect to the center of the object, not the origin; so it works in another coordinate system.
My question is: how can I get the position of the camera not with respect of the object center but with respect of the origin of the "global" coordinate system?
To clarify, the situation is this. In this image you can see a hand mesh that has a position far away from the origin of the coordinate system. The camera points directly to the center of the hand (so the origin from the point of view of the camera is the center of the hand), and if I print it's position it gives me these values:
x: -53.46980763626004; y: -2.7201492246619283; z: -9.814480359970839
while actually I want the position with respect to the origin of the coordinate stystem (so in this case the values would be different; for example, the y value would be positive).
UPDATE:
I tried #leota's suggestion, so I used the localToWorld method in this way:
var camera = scene.getCamera();
var viewPos = camera.position;
var newView = new THREE.Vector3();
newView.copy(viewPos);
camera.localToWorld(newView);
I did an experiment with this mesh. As you can see this mesh is also not centered on the origin (you can see the axes on the bottom-left corner).
If I print the normal value of the camera's position (so, with respect to the center of the mesh) it gives me these results:
x: 0; y: 0; z: 15
If now I print the resulting values after the code above, the result is:
x: 0; y: 0; z: 30
which is wrong, because as you can see the camera position in the image has x and y values clearly different than 0 (while z = 30 could be true, as far as I can see).
If for example I rotate the camera so that it's very close to the origin, like this (in the image the camera is just behind the origin, so its position in world coordinates should have negative values for x, y, z), the coordinates with respect of the center of the object are:
x: -4.674180744175711; y: -4.8370441591630255; z: -4.877951155147168
while after the code above they become:
x: 3.6176076166961373; y: -4.98753160894295; z: -4.365141278155379
The y and z values might even be accurate at a glance, but the positive value of x tells me that it's totally wrong, and I don't know why.
I'm going to continue looking for a solution, but this might be a step in the right direction. Still, any more suggestions are appreciated!
UPDATE 2:
Found the solution. What #leota said is correct, that is indeed how you would get absolute coordinates for the camera. In my case though, I finally found a single line of code hidden in the project's code that was scaling everything according to some rule (project related). So the solution for me was to take the camera position as it is and then scale it back according to that rule.
Since #leota's answer was indeed the solution to the original question, I'm accepting it as the correct anwser
Not sure I got your question :) if I did then you need to switch between World and Local coordinate systems. The THREE.PerspectiveCamera inherits from THREE.Object3D so you can use the following methods to set your coordinate system:
.localToWorld ( vector )
vector - A local vector.
Updates the vector from local space to world space.
.worldToLocal ( vector )
vector - A world vector.
Updates the vector from world space to local space.
From Three.js Documentation
Update:
First update your camera Matrix:
camera.updateMatrixWorld();
Then:
var vector = camera.position.clone();
vector.applyMatrix( camera.matrixWorld );
The vector should hold the position in world coordinate
I had same question trying to answer I was confused for a while my guess but not sure is
var plot = camera.position.x - mesh.position.x;
var plotb = camera.position.y - mesh.position.y;
var plotc = camera.position.z - mesh.position.z;
mesh.position.x = (camera.position.x + plot) - mesh.position.x;
mesh.position.y = (camera.position.y + plotb) - mesh.position.y;
mesh.position.z = (camera.position.z + plotc) - mesh.position.z;
or
var plot = (camera.position.x * mesh.position.x) / 1000;
var plotb = (camera.position.y * mesh.position.y) / 1000;
var plotc = (camera.position.z * mesh.position.z) / 1000;
mesh.position.x = mesh.position.x + plot;
mesh.position.y = mesh.position.y + plotb;
mesh.position.z = mesh.position.z + plotc;

Three.js: Camera flying around sphere?

In Three.js (which uses JavaScript/ WebGL), how would one create a camera which flies around a sphere at fixed height, fixed forward speed, and fixed orientation in relation to the sphere, with the user only being able to steer left and right?
Imagine an airplane on an invisible string to the center of a globe, flying near ground and always seeing part of the sphere:
(I currently have code which rotates the sphere so to the camera it looks like it's flying -- left and right steering not implemented yet -- but I figure before I go further it might be cleaner to move the camera/ airplane, not the sphere group.)
Thanks!
You mean like in my Ludum Dare 23 game? I found this to be a bit more complicated than I expected. It's not difficult, though.
Here I'm assuming that you know the latitude and longitude of the camera and its distance from the center of the sphere (called radius), and want to create a transformation matrix for the camera.
Create the following objects only once to avoid creating new objects in the game loop:
var rotationY = new Matrix4();
var rotationX = new Matrix4();
var translation = new Matrix4();
var matrix = new Matrix4();
Then every time the camera moves, create the matrix as follows:
rotationY.setRotationY(longitude);
rotationX.setRotationX(-latitude);
translation.setTranslation(0, 0, radius);
matrix.multiply(rotationY, rotationX).multiplySelf(translation);
After this just set the camera matrix (assuming camera is your camera object):
// Clear the camera matrix.
// Strangely, Object3D doesn't have a way to just SET the matrix(?)
camera.matrix.identity();
camera.applyMatrix(matrix);
Thanks for Martin's answer! I've now got it running fine in another approach as follows (Martin's approach may be perfect too; also many thanks to Lmg!):
Set the camera to be a straight line atop the sphere in the beginning (i.e. a high y value, a bit beyond the radius, which was 200 in my case); make it look a bit lower:
camera.position.set(0, 210, 0);
camera.lookAt( new THREE.Vector3(0, 190, -50) );
Create an empty group (an Object3D) and put the camera in:
camGroup = new THREE.Object3D();
camGroup.add(camera);
scene.add(camGroup);
Track the mouse position in percent in relation to the screen half:
var halfWidth = window.innerWidth / 2, halfHeight = window.innerHeight / 2;
app.mouseX = event.pageX - halfWidth;
app.mouseY = event.pageY - halfHeight;
app.mouseXPercent = Math.ceil( (app.mouseX / halfWidth) * 100 );
app.mouseYPercent = Math.ceil( (app.mouseY / halfHeight) * 100 );
In the animation loop, apply this percent to a rotation, while automoving forward:
camGroup.matrix.rotateY(-app.mouseXPercent * .00025);
camGroup.matrix.rotateX(-.0025);
camGroup.rotation.getRotationFromMatrix(camGroup.matrix);
requestAnimationFrame(animate);
renderer.render(scene, camera);

Categories