I understand that I can use body.position.set(x, y, z) to instantaneously move a body, but how can I move it smoothly in an animated manner where it's movement will adhere to the physics and collide with any other bodies on its journey? Using body.velocity.set(x, y, z) will set its velocity, and using body.linearDamping = v, will provide some friction/resistance... but it's still not good enough to allow me to specify exactly where I want the body to stop.
It sounds like you're looking for a kinematic body. With kinematic bodies, you have full control over the movement, and it will push away other bodies in its path. However, the body has infinite mass and is not affected by other bodies colliding with it.
Start off by defining the start and end positions of your body.
var startPosition = new CANNON.Vec3(5, 0, 2);
var endPosition = new CANNON.Vec3(-5, 0, 2);
var tweenTime = 3; // seconds
Then create your kinematic body. In this example we'll add a Box shape to it.
var body = new CANNON.Body({
mass: 0,
type: CANNON.Body.KINEMATIC,
position: startPosition
});
body.addShape(new CANNON.Box(new CANNON.Vec3(1,1,1)));
world.add(body);
Compute the direction vector and get total length of the tween path.
var direction = new CANNON.Vec3();
endPosition.vsub(startPosition, direction);
var totalLength = direction.length();
direction.normalize();
The speed and velocity can be calculated using the formula v = s / t.
var speed = totalLength / tweenTime;
direction.scale(speed, body.velocity);
For each update, compute the tween progress: a number between 0 and 1 where 0 i start position and 1 is end position. Using this number you can calculate the current body position.
var progress = (world.time - startTime) / tweenTime;
if(progress < 1){
// Calculate current position
direction.scale(progress * totalLength, offset);
startPosition.vadd(offset, body.position);
} else {
// We passed the end position! Stop.
body.velocity.set(0,0,0);
body.position.copy(endPosition);
}
See full code below. You can duplicate one of the cannon.js demos and just paste this code.
var demo = new CANNON.Demo();
var postStepHandler;
demo.addScene("Tween box",function(){
var world = demo.getWorld();
// Inputs
var startPosition = new CANNON.Vec3(5, 0, 2);
var endPosition = new CANNON.Vec3(-5, 0, 2);
var tweenTime = 3; // seconds
var body = new CANNON.Body({
mass: 0,
type: CANNON.Body.KINEMATIC,
position: startPosition
});
body.addShape(new CANNON.Box(new CANNON.Vec3(1,1,1)));
world.add(body);
demo.addVisual(body);
if(postStepHandler){
world.removeEventListener('postStep', postStepHandler);
}
// Compute direction vector and get total length of the path
var direction = new CANNON.Vec3();
endPosition.vsub(startPosition, direction);
var totalLength = direction.length();
direction.normalize();
var speed = totalLength / tweenTime;
direction.scale(speed, body.velocity);
// Save the start time
var startTime = world.time;
var offset = new CANNON.Vec3();
postStepHandler = function(){
// Progress is a number where 0 is at start position and 1 is at end position
var progress = (world.time - startTime) / tweenTime;
if(progress < 1){
direction.scale(progress * totalLength, offset);
startPosition.vadd(offset, body.position);
} else {
body.velocity.set(0,0,0);
body.position.copy(endPosition);
world.removeEventListener('postStep', postStepHandler);
postStepHandler = null;
}
}
world.addEventListener('postStep', postStepHandler);
});
demo.start();
You need to use a physics library for this, such as Physijs. It works easily with Three.js. Googling for "Physijs Three.js" will provide examples.
Related
I am creating a simple THREE.PlaneBufferGeometry using Threejs. The surface is a geologic surface in the earth.
This surface has local gaps or 'holes' in it represented by NaN's. I have read another similar, but older, post where the suggestion was to fill the position Z component with 'undefined' rather than NaN. I tried that but get this error:
THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values.
PlaneBufferGeometry {uuid: "8D8EFFBF-7F10-4ED5-956D-5AE1EAD4DD41", name: "", type: "PlaneBufferGeometry", index: Uint16BufferAttribute, attributes: Object, …}
Here is the TypeScript function that builds the surface:
AddSurfaces(result) {
let surfaces: Surface[] = result;
if (this.surfaceGroup == null) {
this.surfaceGroup = new THREE.Group();
this.globalGroup.add(this.surfaceGroup);
}
surfaces.forEach(surface => {
var material = new THREE.MeshPhongMaterial({ color: 'blue', side: THREE.DoubleSide });
let mesh: Mesh2D = surface.arealMesh;
let values: number[][] = surface.values;
let geometry: PlaneBufferGeometry = new THREE.PlaneBufferGeometry(mesh.width, mesh.height, mesh.nx - 1, mesh.ny - 1);
var positions = geometry.getAttribute('position');
let node: number = 0;
// Surfaces in Three JS are ordered from top left corner x going fastest left to right
// and then Y ('j') going from top to bottom. This is backwards in Y from how we do the
// modelling in the backend.
for (let j = mesh.ny - 1; j >= 0; j--) {
for (let i = 0; i < mesh.nx; i++) {
let value: number = values[i][j];
if(!isNaN(values[i][j])) {
positions.setZ(node, -values[i][j]);
}
else {
positions.setZ(node, undefined); /// This does not work? Any ideas?
}
node++;
}
}
geometry.computeVertexNormals();
var plane = new THREE.Mesh(geometry, material);
plane.receiveShadow = true;
plane.castShadow = true;
let xOrigin: number = mesh.xOrigin;
let yOrigin: number = mesh.yOrigin;
let cx: number = xOrigin + (mesh.width / 2.0);
let cy: number = yOrigin + (mesh.height / 2.0);
// translate point to origin
let tempX: number = xOrigin - cx;
let tempY: number = yOrigin - cy;
let azi: number = mesh.azimuth;
let aziRad = azi * Math.PI / 180.0;
// now apply rotation
let rotatedX: number = tempX * Math.cos(aziRad) - tempY * Math.sin(aziRad);
let rotatedY: number = tempX * Math.sin(aziRad) + tempY * Math.cos(aziRad);
cx += (tempX - rotatedX);
cy += (tempY - rotatedY);
plane.position.set(cx, cy, 0.0);
plane.rotateZ(aziRad);
this.surfaceGroup.add(plane);
});
this.UpdateCamera();
this.animate();
}
Thanks!
I have read another similar, but older, post where the suggestion was to fill the position Z component with 'undefined' rather than NaN.
Using undefined will fail in the same way like using NaN. BufferGeometry.computeBoundingSphere() computes the radius based on Vector3.distanceToSquared(). If you call this method with a vector that contains no valid numerical data, NaN will be returned.
Hence, you can't represent the gaps in a geometry with NaN or undefined position data. The better way is to generate a geometry which actually represents the geometry of your geologic surface. Using ShapeBufferGeometry might be a better candidate since shapes do support the concept of holes.
three.js r117
THREE.PlaneBufferGeometry:: parameters: {
width: number;
height: number;
widthSegments: number;
heightSegments: number;
};
widthSegments or heightSegments should be greater 1 ,if widthSegments < 1 ,widthSegments may be equal 0 or nan.
In my case, it was happening when I tried to create a beveled shape based on a single vector or a bunch of identical vectors - so there was only a single point. Filtering out such shapes solved the issue.
I need to orient one node to point its Z-axis at another node in 3D. Yeah, the perfect job for the LookAtConstraint. And for most of my work LookAt is fine. But when I apply LookAt to a particular node, I can no longer animate that node's translation with SCNAction. Picture a hydrogen atom leaving a molecule as it ionizes. The orientation is needed to properly rotate the bond (a cylinder) bewteen the hydrogen and an oxygen atom on the molecule.
I can orient the bond FROM the oxygen TO the hydrogen and animate. But this disorients most of the other bonds which were getting by just fine with LookAt's.
I gave this a mighty try before realizing it answers a somewhat different question:
Calculate rotations to look at a 3D point?
I had a similar issue with a project. What I eventually realized was that I need to use multiple constraints. One for translation (movement) and the other using the look at constraint.
I would move the object and then apply the look at constraint; in this case, it was a camera following an objects being moved using actions. Code snippet follows:
let targetNodeConstraint = SCNLookAtConstraint(target: someObject)
targetNodeConstraint.gimbalLockEnabled = true
let followObjectConstraint = SCNTransformConstraint(inWorldSpace: true, withBlock: { (node, matrix) -> SCNMatrix4 in
let transformMatrix = SCNMatrix4MakeTranslation(
self.someObject.position.x - 1.0,
self.someObject.position.y, self.someObject.position.z + 1.0)
return transformMatrix
})
// Position the object behind the other object & rotate it to
roadCamera.constraints = [followObjectConstraint, targetNodeConstraint]
The important thing to note is the order in which the constraints are added to the object using an array. In the code above, I am ignoring the current matrix before I apply a transform matrix (I should re-write this code someday)
The complete source code of this "experiment" is on GitHub as I try things out.
https://github.com/ManjitBedi/CubeTrip
Hopefully, this is helpful.
My solution here. Deal with situation that node continuously translate in space and should always toward a position.
#discardableResult
func yew(_ node:SCNNode, toPosition position:SCNVector3) -> Float
{
var eularAngle = SCNVector3Zero
let tranform = node.transform
var forward = GLKVector3Make(tranform.m31, tranform.m32, tranform.m33)
var toWard = GLKVector3Make(position.x - node.position.x, position.y - node.position.y, position.z - node.position.z)
forward = GLKVector3Normalize(GLKVector3Make(forward.x, 0, forward.z))
toWard = GLKVector3Normalize(GLKVector3Make(toWard.x, 0, toWard.z))
var dotProduct = GLKVector3DotProduct(forward,toWard)
dotProduct = (dotProduct > 1) ? 1 : ((dotProduct < -1) ? -1 : dotProduct)
var yew = acos(dotProduct)
if yew < 0 {
assert(false)
}
//toward is clockwise of forward
let isCW = GLKVector3CrossProduct(forward, toWard).y < 0
if isCW {
yew = -yew
}
eularAngle.y = yew
node.eulerAngles = SCNVector3Make(eularAngle.x + wrapperNode.eulerAngles.x,
eularAngle.y + wrapperNode.eulerAngles.y,
eularAngle.z + wrapperNode.eulerAngles.z)
return yew
}
#discardableResult
func pitch(_ node:SCNNode, toPosition position:SCNVector3) -> Float{
var eularAngle = SCNVector3Zero
let tranform = node.transform
var toWard = GLKVector3Make(position.x - node.position.x, position.y - node.position.y, position.z - node.position.z)
var forward = GLKVector3Make(tranform.m31, tranform.m32, tranform.m33)
forward = GLKVector3Normalize(forward)
toWard = GLKVector3Normalize(toWard)
var dotProduct = GLKVector3DotProduct(forward,toWard)
dotProduct = (dotProduct > 1) ? 1 : ((dotProduct < -1) ? -1 : dotProduct)
var pitch = acos(dotProduct)
//toward is clockwise of forward, if right vector of model and crossProfuct.x has same direction
let crossProduct = GLKVector3CrossProduct(forward, toWard)
let isCW = (crossProduct.x <= 0) != (tranform.m11 <= 0)
if isCW {
pitch = -pitch
}
eularAngle.x = pitch
node.eulerAngles = SCNVector3Make(eularAngle.x + node.eulerAngles.x,
eularAngle.y + node.eulerAngles.y,
eularAngle.z + node.eulerAngles.z)
return pitch
}
func orient(_ node:SCNNode, toPosition position:SCNVector3) {
self.yew(node, toPosition: position)
self.pitch(node, toPosition: position)
}
So I have a heightmap system which works well enough, however since the THREE.js has updated to r60 which removed the Face4 object, I am having issues.
My code is something like this:
this.buildGeometry = function(){
var geo, len, i, f, y;
geo = new THREE.PlaneGeometry(3000, 3000, 128, 128);
geo.dynamic = true;
geo.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI / 2));
this.getHeightData('heightmap.png', function (data) {
len = geo.faces.length;
for(i=0;i<len;i++){
f = geo.faces[i];
if( f ){
y = (data[i].r + data[i].g + data[i].b) / 2;
geo.vertices[f.a].y = y;
geo.vertices[f.b].y = y;
geo.vertices[f.c].y = y;
geo.vertices[f.d].y = y;
}
}
geo.computeFaceNormals();
geo.computeCentroids();
mesh = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({color:0xff0000}) );
scene.add(mesh);
});
};
This works well since a pixel represents each face. How is this done now that the faces are all triangulated?
Similarly I use image maps for model positioning as well. Each pixel matches to the respective Face4 and a desired mesh is placed at its centroid. How can this be accomplished now?
I really miss being able to update the library and do not want to be stuck in r59 anymore =[
This approach works fine on the recent versions (tested on r66).
Notice that the genFn returns the height y given current col and row, maxCol and maxRow (for testing purposes, you can of course replace it with a proper array lookup or from a grayscale image... 64x64 determines the mesh resolution and 1x1 the real world dimensions.
var genFn = function(x, y, X, Y) {
var dx = x/X;
var dy = y/Y;
return (Math.sin(dx*15) + Math.cos(dy * 5) ) * 0.05 + 0.025;
};
var geo = new THREE.PlaneGeometry(1, 1, 64, 64);
geo.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI / 2));
var iz, ix,
gridZ1 = geo.widthSegments +1,
gridX1 = geo.heightSegments+1;
for (iz = 0; iz < gridZ1; ++iz) {
for (ix = 0; ix < gridX1; ++ix) {
geo.vertices[ ix + gridX1*iz ].y = genFn(ix, iz, gridX1, gridZ1);
}
}
geo.computeFaceNormals();
geo.computeVertexNormals();
geo.computeCentroids();
var mesh = new THREE.Mesh(
geo,
mtl
);
scene.add(mesh);
Is the intent of the TrackballControl to have a "border" outside the trackball that induces roll? I personally dislike it. It is a bit discontinuous, and does't really have a lot of purpose (imho).
If not, the function getMouseProjectionOnBall can be changed similar to the following. This does two things (not necessarily "correctly"):
Normalize the radius to fill both axis
Map z values outside of the ball (ie where z was previously 0)
I find this a lot more natural, personally.
Thoughts?
this.getMouseProjectionOnBall = function(clientX, clientY) {
var xnormalized = (clientX - _this.screen.width * 0.5 - _this.screen.offsetLeft) / (_this.screen.width / 2.0);
var ynormalized = (_this.screen.height * 0.5 + _this.screen.offsetTop - clientY) / (_this.screen.height / 2.0);
var mouseOnBall = new THREE.Vector3(
xnormalized,
ynormalized,
0.0
);
var length = mouseOnBall.length();
var ballRadius = 1.0; // As a fraction of the screen
if (length > ballRadius * 0.70710678118654752440) {
var temp = ballRadius / 1.41421356237309504880;
mouseOnBall.z = temp * temp / length;
// Remove old method.
// This Left z = 0, which meant rotation axis
// becomes z, which is a roll
//mouseOnBall.normalize();
} else {
mouseOnBall.z = Math.sqrt(1.0 - length * length);
}
_eye.copy(_this.object.position).sub(_this.target);
var projection = _this.object.up.clone().setLength(mouseOnBall.y);
projection.add(_this.object.up.clone().cross(_eye).setLength(mouseOnBall.x));
projection.add(_eye.setLength(mouseOnBall.z));
return projection;
};
I'm having a little trouble generating/displaying a terrain using Three.JS without major FPS drops. Here's the code I wrote to create each block and set the correct position:
var TO_METERS = 10;
var testOb = [];
var blockGeometry = new THREE.CubeGeometry(TO_METERS, TO_METERS, TO_METERS);
var blockMat = new THREE.MeshLambertMaterial({color: 0xFFFFFF, wrapAround: true, side: THREE.FrontSide, shading: THREE.FlatShading});
function loadChunk(startX, startY, startZ) {
var yVar = 0;
var zVar = 0;
var blockCo = 0;
var combinedGeometry = new THREE.CubeGeometry(0, 0, 0);
for (var x = 0; x <= 4999; x++) {
testOb[x] = new THREE.Mesh();
testOb[x].geometry = blockGeometry;
if (blockCo == 10) {
blockCo = 0;
if (zVar == 90) {
yVar += TO_METERS;
zVar = 0;
}
else {
zVar += TO_METERS;
}
}
testOb[x].position.x = (blockCo * TO_METERS) + startX;
testOb[x].position.y = (yVar - 500) + startY;
testOb[x].position.z = zVar + startZ;
testOb[x].castShadow = true;
blockCo++;
THREE.GeometryUtils.merge(combinedGeometry, testOb[x]);
}
var cMesh = new Physijs.BoxMesh(combinedGeometry, blockMat, 0);
scene.add(cMesh);
}
Basically it creates each block, sets the position and merges them together using THREE.GeometryUtils.merge to make up a "chunk" (a rectangle) MineCraft style.
I'm pretty sure the large number of individual blocks that make up each chunk is causing the low FPS. With only 10 chunks the FPS is fine. If I add any more the FPS drops drastically.
One thought I had was to use a WebWorker to do the processing, but of cause that isn't possible as I can't add the chunks or even use Three.JS within it. That also would only help the load time, not the FPS problem I'm having.
If anyone has any ideas how I would go about fixing this problem, I would really appreciate it. :) Maybe it would be possible to hide blocks which the camera can't see? Or I might just totally be doing it the wrong way. Thanks!