Related
I'm trying to create a textured surface along a path in three.js. I want the texture to tile/repeat along the direction of the path, like this example created in blender:
One way to accomplish this is to create each of the faces "by hand" and to apply a material/texture to each. That works out fine for simple paths (e.g. straight lines) but gets complicated for more elaborate paths.
One of the tools three.js provides is ExtrudeGeometry. Applying a texture to a mesh created this way looks like this with the default UV mapping:
So clearly I need to write a custom UVGenerator function to pass to ExtrudeGeometry. Unfortunately, this doesn't appear to be something for which there is documentation, and previous questions that show up in search results are either out of date (the most complete answers involve a different API for the UVGenerator functions: example) or have no answers (an example).
Here's a jsFiddle example that illustrates the undesired/default behavior. The code is reproduced below. The uvGenerator() function in it is functionally identical to the default three.js uvGenerator, THREE.WorldUVGenerator. It's in the example just to make it easier to fiddle with.
window.onload = function() {
var camera, dataURI, renderer, scene, surface;
var texture, uvGenerator;
var width = 800, height = 600;
dataURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAi0lEQVRIx83WQQ6AIAwEwMXwb/p0b4SI1CJsS08e2k0nKDGVUsCsDEBESOkikusTIx3ABXJdJEEN9BLsRbRRjoJdiEeIr2Ad0Y+7C1YQr4MRgn+I0UiQYBahNMcJ7Ai9LVRgQXw2RAv0HS2HdIBgtKnxHTtD0O9r/86PEbRbT11TdEHecukrldi/7zd8KDG/sdhdzgAAAABJRU5ErkJggg==';
function initCamera() {
camera = new THREE.PerspectiveCamera(60, width/height, 1, 1000);
camera.position.set(0, 0, 0);
scene.add(camera);
}
function initLights() {
var lights;
lights = [
new THREE.PointLight(0xffffff, 1, 0),
new THREE.PointLight(0xffffff, 1, 0),
new THREE.PointLight(0xffffff, 1, 0),
];
lights[0].position.set(0, 200, 0);
lights[1].position.set(100, 200, 100);
lights[2].position.set(-100, -200, -100);
scene.add(lights[0]);
scene.add(lights[1]);
scene.add(lights[2]);
}
function initRenderer() {
var canvas;
canvas = document.getElementById('canvas');
renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
canvas.appendChild(renderer.domElement);
}
function initScene() {
scene = new THREE.Scene();
}
function initSurface() {
surface = extrudeSurface();
surface.position.set(50, -50, 50);
scene.add(surface);
}
function generalInit() {
initScene();
initCamera();
initLights();
initSurface();
initRenderer();
animate();
}
uvGenerator = {
generateTopUV: function(geometry, vertices, idxA, idxB, idxC) {
var ax, ay, bx, by, cx, cy;
ax = vertices[idxA * 3];
ay = vertices[(idxA * 3) + 1];
bx = vertices[idxB * 3];
by = vertices[(idxB * 3) + 1];
cx = vertices[idxC * 3];
cy = vertices[(idxC * 3) + 1];
return([
new THREE.Vector2(ax, ay),
new THREE.Vector2(bx, by),
new THREE.Vector2(cx, cy),
]);
},
generateSideWallUV: function(geometry, vertices,
idxA, idxB, idxC, idxD) {
var ax, ay, az, bx, by, bz, cx, cy, cz;
var dx, dy, dz, bb, bbx, bby, bbz;
geometry.computeBoundingBox();
bb = geometry.boundingBox;
bbx = bb.max.x - bb.min.x;
bby = bb.max.y - bb.min.y;
bbz = bb.max.z - bb.min.z;
ax = vertices[idxA * 3];
ay = vertices[(idxA * 3) + 1];
az = vertices[(idxA * 3) + 2];
bx = vertices[idxB * 3];
by = vertices[(idxB * 3) + 1];
bz = vertices[(idxB * 3) + 2];
cx = vertices[idxC * 3];
cy = vertices[(idxC * 3) + 1];
cz = vertices[(idxC * 3) + 2];
dx = vertices[idxD * 3];
dy = vertices[(idxD * 3) + 1];
dz = vertices[(idxD * 3) + 2];
if(Math.abs(ay - by) < 0.01) {
return([
new THREE.Vector2(ax, 1 - az),
new THREE.Vector2(bx, 1 - bz),
new THREE.Vector2(cx, 1 - cz),
new THREE.Vector2(dx, 1 - dz),
]);
} else {
return([
new THREE.Vector2(ay, 1 - az),
new THREE.Vector2(by, 1 - bz),
new THREE.Vector2(cy, 1 - cz),
new THREE.Vector2(dy, 1 - dz),
]);
}
},
}
function extrudeSurface() {
var extrudeCfg, geometry, material, mesh, size, shape, curve;
size = 20;
curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(-50, 0, -25),
new THREE.Vector3(50, 0, 75),
]);
extrudeCfg = {
steps: 200,
bevelEnabled: false,
extrudePath: curve,
UVGenerator: uvGenerator,
//UVGenerator: THREE.WorldUVGenerator,
};
shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.lineTo(0, size);
shape.lineTo(0.1, size);
shape.lineTo(0.1, 0);
shape.lineTo(0, 0);
geometry = new THREE.ExtrudeGeometry(shape, extrudeCfg);
geometry.computeBoundingBox();
geometry.computeVertexNormals(true);
material = new THREE.MeshBasicMaterial({ map: texture });
mesh = new THREE.Mesh(geometry, material);
new THREE.Vector3(0, 0, 0),
return(mesh);
}
function animate() {
if(!scene)
return;
animID = requestAnimationFrame(animate);
render();
update();
}
function render() {
if(!scene || !camera || !renderer)
return;
renderer.render(scene, camera);
}
function update() {
if(!scene || !camera || !surface)
return;
camera.lookAt(surface.position);
//surface.rotation.x += 0.01;
surface.rotation.y += 0.01;
}
function loadTexture() {
var loader;
loader = new THREE.TextureLoader();
loader.load(dataURI,
function(t) {
t.wrapS = THREE.RepeatWrapping;
t.wrapT = THREE.RepeatWrapping;
t.magFilter = THREE.NearestFilter;
t.minFilter = THREE.NearestFilter;
t.repeat.set(1/20, 1/20);
texture = t;
generalInit();
}
);
}
loadTexture();
};
I've tried converting the vertex coords into a UV-ish 0-1 range by dividing by the size of the geometry's bounding box, but this does not produce the desired result. E.g. something of the form (here only shown for one of the return values):
generateSideWallUV: function(geometry, vertices, idxA, idxB, idxC, idxD) {
var ax = vertices[idxA * 3];
geometry.computeBoundingBox();
var bb = geometry.boundingBox;
var bbx = bb.max.x - bb.min.x;
var bbz = bb.max.z - bb.min.z;
...
return([
new THREE.Vector2(ax / bbx, 1 - (az / bbz),
...
]);
}
}
This approach not working makes sense, because what we care about is the position of the vertex as a fraction of the length of the extruded path, not as a fraction of the bounding box. But I don't know and have not been able to look up how to get that information out of the geometry, vertices, or anything else documented in THREE.
Any help/pointers would be appreciated.
Correction: Dividing the vertex coords by the size of the bounding box also doesn't work in this case because when ExtrudeGeometry calls generateSideWallUV() the bounding box is always
min":{"x":null,"y":null,"z":null},"max":{"x":null,"y":null,"z":null}}
...which means anything/(max.x - min.x) will always evaluate as infinite.
So now I'm even more confused about what we can hope to accomplish in a custom UV generator.
If I'm missing something obvious (for example, if I shouldn't be using ExtrudeGeometry for this sort of thing at all) I'd love to be educated.
Answering my own question:
Here's a link to a jsFiddle of the solution.
Here's the interesting parts. First, instead of using THREE.RepeatWrapping we use ClampToEdgeWrapping and get rid of the repeat setting:
t.wrapS = THREE.ClampToEdgeWrapping;
t.wrapT = THREE.ClampToEdgeWrapping;
//t.repeat.set(1 / 20, 1/20);
Then when we create the config object to pass to ExtrudeGeometry we set the steps to be exactly the number of faces we want. This is kinda a kludge because it seems like we shouldn't have to make a decision about the geometry of the object just to get our UVs right--there are plenty of cases where we might have bends/twists were we probably want more vertices to avoid folding/tearing/just looking wonky. But eh, I'm willing to worry about that later. Anyway, our updated extrudeCfg (in extrudeSurface() in the example) becomes:
extrudgeCfg = {
steps: 20,
bevelEnabled: false,
extrudePath: curve,
UVGenerator: uvGenerator,
};
And then finally we re-write our UVGenerator. And since we're now clamping the texture and using a smaller number of faces, we can do something painfully simple like:
generateSideWallUV: function(geometry, vertices, idxA, idxB, idxC, idxD) {
return([
new THREE.Vector2(0, 0),
new THREE.Vector2(1, 0),
new THREE.Vector2(1, 1),
new THREE.Vector2(0, 1),
]);
}
...which is to say we just stretch a copy of the texture across each face (with the only complication being because the sides of the extruded geometry are a rectangle consisting of two triangles instead of a single quad).
Et voilà:
My three.js scene is completely distorted until I move the mouse somewhere on the site.
You can see the nature of the distortion on the image below:
When I move the mouse, the scene suddenly pops and everything is fine. It doesn't seem to matter where exactly the cursor is within the site, it doesn't have to be over the canvas where my scene is rendered.
This is how the scene looks after moving the mouse:
The following three.js related dependencies are used:
"three": "^0.108.0"
"three-orbitcontrols": "^2.102.2"
"three.meshline": "^1.2.0"
I tried updating three to the latest version (0.116.1), but that didn't solve the issue either. I managed to replicate this issue on Firefox and Edge, but not on Chrome.
Some extra context: we use OffscreenCanvas for better performance, the mouse positions are sent from the main thread to the web worker on mousemove event, we use that information to slightly move the camera and the background (with offsets). I temporarily removed to mousemove handler logic from the web worker code and the issue still popped up, so it's probably unrelated. We use tween.js to make the camera animations smooth.
Relevant code snippets:
Scene setup:
const {scene, camera} = makeScene(elem, cameraPosX, 0, 60, 45);
const supportsWebp = (browser !== 'Safari');
imageLoader.load(backgroundImage, mapImage => {
const texture = new THREE.CanvasTexture(mapImage);
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
texture.minFilter = THREE.LinearFilter;
// Repeat background so we don't run out of it during offset changes on mousemove
texture.wrapS = THREE.MirroredRepeatWrapping;
texture.wrapT = THREE.MirroredRepeatWrapping;
scene.background = texture;
});
// Creating objects in the scene
let orbitingPlanet = getPlanet(0xffffff, true, 1 * mobilePlanetSizeAdjustment);
scene.add(orbitingPlanet);
// Ellipse class, which extends the virtual base class Curve
let curveMain = new THREE.EllipseCurve(
0, 0, // ax, aY
80, 30, // xRadius, yRadius
0, 2 * Math.PI, // aStartAngle, aEndAngle
false, // aClockwise
0.2 // aRotation
);
let ellipseMainGeometry = new THREE.Path(curveMain.getPoints(100)).createPointsGeometry(100);
let ellipseMainMaterial = new MeshLine.MeshLineMaterial({
color: new THREE.Color(0xffffff),
opacity: 0.2,
transparent: true,
});
let ellipseMain = new MeshLine.MeshLine();
ellipseMain.setGeometry(ellipseMainGeometry, function(p) {
return 0.2; // Line width
});
const ellipseMainMesh = new THREE.Mesh(ellipseMain.geometry, ellipseMainMaterial );
scene.add(ellipseMainMesh);
// Create a halfish curve on which one of the orbiting planets will move
let curveMainCut = new THREE.EllipseCurve(
0, 0, // ax, aY
80, 30, // xRadius, yRadius
0.5 * Math.PI, 1.15 * Math.PI, // aStartAngle, aEndAngle
false, // aClockwise
0.2 // aRotation
);
let lastTweenRendered = Date.now();
let startRotation = new THREE.Vector3(
camera.rotation.x,
camera.rotation.y,
camera.rotation.z);
let tweenie;
return (time, rect) => {
camera.aspect = state.width / state.height;
camera.updateProjectionMatrix();
let pt1 = curveMainCut.getPointAt(t_top_faster);
orbitingPlanet.position.set(pt1.x, pt1.y, 1);
t_top_faster = (t_top_faster >= 1) ? 0 : t_top_faster += 0.001;
// Slightly rotate the background on mouse move
if (scene && scene.background) {
// The rotation mush be between 0 and 0.01
scene.background.rotation =
Math.max(-0.001,Math.min(0.01, scene.background.rotation + 0.00005 * target.x));
let offsetX = scene.background.offset.x + 0.00015 * target.x;
let offsetY = scene.background.offset.y + 0.00015 * target.y;
scene.background.offset = new THREE.Vector2(
(offsetX > -0.05 && offsetX < 0.05) ? offsetX : scene.background.offset.x,
(offsetY > -0.05 && offsetY < 0.05) ? offsetY : scene.background.offset.y);
}
lastTweenRendered = tweenAnimateCamera(lastTweenRendered, tweenie, camera, startRotation, 200);
renderer.render(scene, camera);
};
makeScene function:
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(fieldOfView, state.width / state.height, 0.1, 100000000);
camera.position.set(camPosX, camPosY, camPosZ);
camera.lookAt(0, 0, 0);
scene.add(camera);
return {scene, camera};
Camera animation based on mouse positions:
function tweenAnimateCamera(lastTweenRendered, tween, camera, startRotation, period) {
target.x = (1 - mouse.x) * 0.002;
target.y = (1 - mouse.y) * 0.002;
let now = Date.now();
if ((
// Don't let the camera go too far
startRotation.x > -0.01 && startRotation.x < 0.01) &&
now - lastTweenRendered > (period / 2)) {
if (tween) {
tween.stop();
}
lastTweenRendered = now;
let endRotation = new THREE.Vector3(
camera.rotation.x + 0.005 * (target.y - camera.rotation.x),
camera.rotation.y + 0.005 * (target.x - camera.rotation.y),
camera.rotation.z);
tween = new TWEEN.Tween(startRotation)
.to(endRotation, period * 2)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(function (v) {
camera.rotation.set(v.x, v.y, v.z);
})
.onComplete(function(v) {
startRotation = v.clone();
});
tween.start();
}
TWEEN.update();
return lastTweenRendered
}
Mouse position receiver logic:
if (e.data.type === 'mousePosUpdate') {
if (e.data.x !== -100000 && e.data.y !== -100000) {
mouse.x = ( e.data.x - state.width / 2 );
mouse.y = ( e.data.y - state.height / 2 );
target.x = ( 1 - mouse.x ) * 0.002;
target.y = ( 1 - mouse.y ) * 0.002;
}
}
Render loop:
function render(time) {
time *= 0.001;
for (const {elem, fn, ctx} of sceneElements) {
// get the viewport relative position of this element
canvasesUpdatedPos.forEach( canvasUpdate => {
if (canvasUpdate.id === elem.id) {
elem.rect = canvasUpdate.rect;
}
});
const rect = elem.rect;
const bottom = rect.bottom;
const height = rect.height;
const left = rect.left;
const right = rect.right;
const top = rect.top;
const width = rect.width;
const rendererCanvas = renderer.domElement;
const isOffscreen =
bottom < 0 ||
top > state.height ||
right < 0 ||
left > state.width;
if (!isOffscreen && width !== 0 && height !== 0) {
// make sure the renderer's canvas is big enough
let isResize = resizeRendererToDisplaySize(renderer, height, width);
// make sure the canvas for this area is the same size as the area
if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
ctx.canvas.width = width;
ctx.canvas.height = height;
state.width = width;
state.height = height;
}
renderer.setScissor(0, 0, width, height);
renderer.setViewport(0, 0, width, height);
fn(time, rect);
// copy the rendered scene to this element's canvas
ctx.globalCompositeOperation = 'copy';
ctx.drawImage(
rendererCanvas,
0, rendererCanvas.height - height, width, height, // src rect
0, 0, width, height); // dst rect
}
}
// Limiting to 35 FPS.
setTimeout(function() {
if (!stopAnimating) {
requestAnimationFrame(render);
}
}, 1000 / 35);
}
I don't see where you're initiating target and mouse anywhere. My best guess is that target.x, target.y or mouse.x, mouse.y are undefined or 0, and it's probably causing a division by 0, or a calculation that returns NaN, which is giving you that infinitely stretched texture. You should be able to fix this if you initiate those vectors:
var target = new THREE.Vector2();
var mouse = new THREE.Vector2();
First of anyone running into similar issues please take a look at Marquizzo's answer and Ethan Hermsey's comment (on the question) as well, since they provided a good possible cause for this issue, although the issue was different in my case.
The distortion was related to the OffscreenCanvas in our case. When the application starts we send the OffscreenCanvas along with it's size to the web worker:
const rect = element.getBoundingClientRect();
const offScreenCanvas = element.transferControlToOffscreen();
worker.post({
type: 'canvas',
newCanvas: offScreenCanvas,
width: rect.width,
height: rect.height
}, [offScreenCanvas]);
The cause of the issue was the height, which was incorrect in certain cases, 1px to be precise in the example pictured in the question. The incorrect height popped up because of a race condition, in a separate script we used to set up the height of certain canvas container elements with the following script:
$(".fullscreen-block").css({ height: var wh = $(window).height() });
However, we usually sent the size of the canvas to the worker before this happened. Substituting this JS code with a simple CSS rule solved this issue:
.fullscreen-block {
height: 100vh;
}
So, in the end, the issue was not related to the mouse event's handled by us, I can only guess why moving the mouse fixed the distortion. I'd say Firefox probably revalidates/recalculates the size of DOM elements when the mouse is moved and we were notified about size changes, causing the animation to pop to the correct size and state.
The short version: How can one make a camera follow an object controlled by physics within a Three.js scene?
The long version: I'm working on a Three.js scene in which the W,A,S,D keys move a sphere along a plane. So far, however, I haven't figured out how to make the camera follow behind the sphere.
In the example below, the camera follows the sphere perfectly if one only presses the W key. However, if one presses A or D, the sphere starts to turn, and the camera is no longer behind the ball. If the sphere starts to turn, I want the camera to turn with it, so the camera is always following just behind the sphere, and is always a constant distance from the sphere. As users continue to press W, the ball will continue rolling forward relative to the camera.
In a previous scene [demo], I was able to implement this behavior by creating the sphere, adding that sphere to a group, then using the following bit of code each frame:
var relativeCameraOffset = new THREE.Vector3(0,50,200);
var cameraOffset = relativeCameraOffset.applyMatrix4(sphereGroup.matrixWorld);
camera.position.x = cameraOffset.x;
camera.position.y = cameraOffset.y;
camera.position.z = cameraOffset.z;
camera.lookAt(sphereGroup.position);
The key in the demo above was to rotate the sphere while keeping the sphereGroup unrotated, so I could compute the cameraOffset on the un-rotated sphereGroup.
In the demo below, the sphere's position is controlled by the Cannon.js physics library, which translates and rotates the sphere as forces are applied to the body. Does anyone know how I can make the camera follow behind the sphere in the scene below?
/**
* Generate a scene object with a background color
**/
function getScene() {
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
return scene;
}
/**
* Generate the camera to be used in the scene. Camera args:
* [0] field of view: identifies the portion of the scene
* visible at any time (in degrees)
* [1] aspect ratio: identifies the aspect ratio of the
* scene in width/height
* [2] near clipping plane: objects closer than the near
* clipping plane are culled from the scene
* [3] far clipping plane: objects farther than the far
* clipping plane are culled from the scene
**/
function getCamera() {
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 10000);
camera.position.set(0, 2000, -5000);
camera.lookAt(scene.position);
return camera;
}
/**
* Generate the light to be used in the scene. Light args:
* [0]: Hexadecimal color of the light
* [1]: Numeric value of the light's strength/intensity
* [2]: The distance from the light where the intensity is 0
* #param {obj} scene: the current scene object
**/
function getLight(scene) {
var light = new THREE.PointLight( 0xffffff, 0.6, 0, 0 )
light.position.set( -2000, 1000, -2100 );
scene.add( light );
var light = new THREE.PointLight( 0xffffff, 0.15, 0, 0 )
light.position.set( -190, 275, -1801 );
light.castShadow = true;
scene.add( light );
// create some ambient light for the scene
var ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
return light;
}
/**
* Generate the renderer to be used in the scene
**/
function getRenderer() {
// Create the canvas with a renderer
var renderer = new THREE.WebGLRenderer({antialias: true});
// Add support for retina displays
renderer.setPixelRatio(window.devicePixelRatio);
// Specify the size of the canvas
renderer.setSize(window.innerWidth, window.innerHeight);
// Enable shadows
renderer.shadowMap.enabled = true;
// Specify the shadow type; default = THREE.PCFShadowMap
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Add the canvas to the DOM
document.body.appendChild(renderer.domElement);
return renderer;
}
/**
* Generate the controls to be used in the scene
* #param {obj} camera: the three.js camera for the scene
* #param {obj} renderer: the three.js renderer for the scene
**/
function getControls(camera, renderer) {
var controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.zoomSpeed = 0.4;
controls.panSpeed = 0.4;
return controls;
}
/**
* Get stats
**/
function getStats() {
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0px';
stats.domElement.style.right = '0px';
document.body.appendChild( stats.domElement );
return stats;
}
/**
* Get grass
**/
function getGrass() {
var texture = loader.load('http://4.bp.blogspot.com/-JiJEc7lH1Is/UHJs3kn261I/AAAAAAAADYA/gQRAxHK2q_w/s1600/tileable_old_school_video_game_grass.jpg');
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(10, 10);
var material = new THREE.MeshLambertMaterial({
map: texture,
side: THREE.DoubleSide,
});
return material;
}
function getPlanes(scene, loader) {
var planes = [];
var material = getGrass();
[ [4000, 2000, 0, 0, -1000, 0] ].map(function(p) {
var geometry = new THREE.PlaneGeometry(p[0], p[1]);
var plane = new THREE.Mesh(geometry, material);
plane.position.x = p[2];
plane.position.y = p[3];
plane.position.z = p[4];
plane.rotation.y = p[5];
plane.rotation.x = Math.PI / 2;
plane.receiveShadow = true;
planes.push(plane);
scene.add(plane);
})
return planes;
}
/**
* Add background
**/
function getBackground(scene, loader) {
var imagePrefix = 'sky-parts/';
var directions = ['right', 'left', 'top', 'bottom', 'front', 'back'];
var imageSuffix = '.bmp';
var geometry = new THREE.BoxGeometry( 4000, 4000, 4000 );
// Add each of the images for the background cube
var materialArray = [];
for (var i = 0; i < 6; i++)
materialArray.push( new THREE.MeshBasicMaterial({
//map: loader.load(imagePrefix + directions[i] + imageSuffix),
color: 0xff0000,
side: THREE.BackSide
}));
var sky = new THREE.Mesh( geometry, materialArray );
scene.add(sky);
return sky;
}
/**
* Add a character
**/
function getSphere(scene) {
var geometry = new THREE.SphereGeometry( 30, 12, 9 );
var material = new THREE.MeshPhongMaterial({
color: 0xd0901d,
emissive: 0xaa0000,
side: THREE.DoubleSide,
flatShading: true
});
var sphere = new THREE.Mesh( geometry, material );
// allow the sphere to cast a shadow
sphere.castShadow = true;
sphere.receiveShadow = false;
// create a group for translations and rotations
var sphereGroup = new THREE.Group();
sphereGroup.add(sphere)
sphereGroup.castShadow = true;
sphereGroup.receiveShadow = false;
scene.add(sphereGroup);
return [sphere, sphereGroup];
}
/**
* Initialize physics engine
**/
function getPhysics() {
world = new CANNON.World();
world.gravity.set(0, -400, 0); // earth = -9.82 m/s
world.broadphase = new CANNON.NaiveBroadphase();
world.broadphase.useBoundingBoxes = true;
var solver = new CANNON.GSSolver();
solver.iterations = 7;
solver.tolerance = 0.1;
world.solver = solver;
world.quatNormalizeSkip = 0;
world.quatNormalizeFast = false;
world.defaultContactMaterial.contactEquationStiffness = 1e9;
world.defaultContactMaterial.contactEquationRelaxation = 4;
return world;
}
/**
* Generate the materials to be used for contacts
**/
function getPhysicsMaterial() {
var physicsMaterial = new CANNON.Material('slipperyMaterial');
var physicsContactMaterial = new CANNON.ContactMaterial(
physicsMaterial, physicsMaterial, 0.0, 0.3)
world.addContactMaterial(physicsContactMaterial);
return physicsMaterial;
}
/**
* Add objects to the world
**/
function addObjectPhysics() {
addFloorPhysics()
addSpherePhysics()
}
function addFloorPhysics() {
floors.map(function(floor) {
var q = floor.quaternion;
floorBody = new CANNON.Body({
mass: 0, // mass = 0 makes the body static
material: physicsMaterial,
shape: new CANNON.Plane(),
quaternion: new CANNON.Quaternion(-q._x, q._y, q._z, q._w)
});
world.addBody(floorBody);
})
}
function addSpherePhysics() {
sphereBody = new CANNON.Body({
mass: 1,
material: physicsMaterial,
shape: new CANNON.Sphere(30),
linearDamping: 0.5,
position: new CANNON.Vec3(1000, 500, -2000)
});
world.addBody(sphereBody);
}
/**
* Store all currently pressed keys & handle window resize
**/
function addListeners() {
window.addEventListener('keydown', function(e) {
pressed[e.key.toUpperCase()] = true;
})
window.addEventListener('keyup', function(e) {
pressed[e.key.toUpperCase()] = false;
})
window.addEventListener('resize', function(e) {
windowHalfX = window.innerWidth / 2;
windowHalfY = window.innerHeight / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
if (typeof(controls) != 'undefined') controls.handleResize();
})
}
/**
* Update the sphere's position
**/
function moveSphere() {
var delta = clock.getDelta(); // seconds
var moveDistance = 500 * delta; // n pixels per second
var rotateAngle = Math.PI / 2 * delta; // 90 deg per second
// move forwards, backwards, left, or right
if (pressed['W'] || pressed['ARROWUP']) {
sphereBody.velocity.z += moveDistance;
}
if (pressed['S'] || pressed['ARROWDOWN']) {
sphereBody.velocity.z -= moveDistance;
}
if (pressed['A'] || pressed['ARROWLEFT']) {
sphereBody.velocity.x += moveDistance;
}
if (pressed['D'] || pressed['ARROWRIGHT']) {
sphereBody.velocity.x -= moveDistance;
}
}
/**
* Follow the sphere
**/
function moveCamera() {
camera.position.x = sphereBody.position.x + 0;
camera.position.y = sphereBody.position.y + 50;
camera.position.z = sphereBody.position.z + -200;
camera.lookAt(sphereGroup.position);
}
function updatePhysics() {
world.step(1/60);
sphereGroup.position.copy(sphereBody.position);
sphereGroup.quaternion.copy(sphereBody.quaternion);
}
// Render loop
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
moveSphere();
updatePhysics();
if (typeof(controls) === 'undefined') moveCamera();
if (typeof(controls) !== 'undefined') controls.update();
if (typeof(stats) !== 'undefined') stats.update();
};
// state
var pressed = {};
var clock = new THREE.Clock();
// globals
var scene = getScene();
var camera = getCamera();
var light = getLight(scene);
var renderer = getRenderer();
var world = getPhysics();
var physicsMaterial = getPhysicsMaterial();
//var stats = getStats();
//var controls = getControls(camera, renderer);
// global body references
var sphereBody, floorBody;
// add meshes
var loader = new THREE.TextureLoader();
var floors = getPlanes(scene, loader);
var background = getBackground(scene, loader);
var sphereData = getSphere(scene);
var sphere = sphereData[0];
var sphereGroup = sphereData[1];
addObjectPhysics();
addListeners();
render();
body { margin: 0; overflow: hidden; }
canvas { width: 100%; height: 100%; }
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.js'></script>
Answers to comment questions
#jparimaa I think the most intuitive implementation would make W add forward momentum, S add backward momentum, and A and D rotate the camera around the ball. Is that possible?
#HariV The controls you link to are the ones I used in the demo without physics above. Is it possible to get that logic working with physics?
I think it's most intuitive for users if the W key always moves the ball "forward" relative to the camera
One option would be to calculate the direction between the ball and the camera and add velocity to that direction. In this case if you push the ball forward then you could rotate the camera without it affecting the velocity of the ball. Only after you press W/S after the rotation it would change the direction. I'm not sure if that is what you want but maybe this will give you some ideas.
I tried the following code (rotation is global variable initialized to 0)
function moveSphere() {
var delta = clock.getDelta(); // seconds
var moveDistance = 500 * delta; // n pixels per second
var dir = new THREE.Vector3(sphereBody.position.x, sphereBody.position.y, sphereBody.position.z);
dir.sub(camera.position).normalize(); // direction vector between the camera and the ball
if (pressed['W'] || pressed['ARROWUP']) {
sphereBody.velocity.x += moveDistance * dir.x;
sphereBody.velocity.z += moveDistance * dir.z;
}
if (pressed['S'] || pressed['ARROWDOWN']) {
sphereBody.velocity.x -= moveDistance * dir.x;
sphereBody.velocity.z -= moveDistance * dir.z;
}
}
function moveCamera() {
var delta = clock.getDelta();
var sensitivity = 150;
var rotateAngle = Math.PI / 2 * delta * sensitivity;
if (pressed['A'] || pressed['ARROWLEFT']) {
rotation -= rotateAngle;
}
if (pressed['D'] || pressed['ARROWRIGHT']) {
rotation += rotateAngle;
}
var rotZ = Math.cos(rotation)
var rotX = Math.sin(rotation)
var distance = 200;
camera.position.x = sphereBody.position.x - (distance * rotX);
camera.position.y = sphereBody.position.y + 50;
camera.position.z = sphereBody.position.z - (distance * rotZ);
camera.lookAt(sphereGroup.position);
}
This question already has answers here:
Three.js - Width of view
(2 answers)
Closed 5 years ago.
I made a small three.js app that moves a bunch of circles from the bottom of the canvas to the top:
let renderer, scene, light, circles, camera;
initialize();
animate();
function initialize() {
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
scene = new THREE.Scene();
light = new THREE.AmbientLight();
scene.add(light);
circles = new THREE.Group();
scene.add(circles);
camera = new THREE.PerspectiveCamera(45, renderer.domElement.clientWidth / renderer.domElement.clientHeight, 1);
camera.position.z = circles.position.z + 500;
}
function animate() {
// Update each circle.
Array.from(circles.children).forEach(circle => {
if (circle.position.y < visibleBox(circle.position.z).max.y) {
circle.position.y += 4;
} else {
circles.remove(circle);
}
});
// Create a new circle.
let circle = new THREE.Mesh();
circle.geometry = new THREE.CircleGeometry(30, 30);
circle.material = new THREE.MeshToonMaterial({ color: randomColor(), transparent: true, opacity: 0.5 });
circle.position.z = _.random(camera.position.z - camera.far, camera.position.z - (camera.far / 10));
circle.position.x = _.random(visibleBox(circle.position.z).min.x, visibleBox(circle.position.z).max.x);
circle.position.y = visibleBox(circle.position.z).min.y;
circles.add(circle);
// Render the scene.
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
function visibleBox(z) {
return new THREE.Box2(
new THREE.Vector2(-1000, -1000),
new THREE.Vector2(1000, 1000)
);
}
function randomColor() {
return `#${ _.sampleSize("abcdef0123456789", 6).join("")}`;
}
body {
width: 100%;
height: 100%;
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js">
</script>
I use the function visibleBox(z) to determine where to create and destroy each circle. I've hard-coded a return value for this function, but instead I would like it to compute the size of the rectangle that is visible to the camera at a given depth, z.
In other words, I want each circle to be created exactly at the bottom of the camera frustum (the bottom edge of the red rectangle in the image above), and destroyed exactly when it reaches the top of the frustum (the top edge of the red rectangle).
So, how I do compute this rectangle?
Change the function like this:
function visibleBox(z) {
var t = Math.tan( THREE.Math.degToRad( camera.fov ) / 2 )
var h = t * 2 * z;
var w = h * camera.aspect;
return new THREE.Box2(new THREE.Vector2(-w, h), new THREE.Vector2(w, -h));
}
And set up the circle position like this:
circle.position.z = _.random(-camera.near, -camera.far);
var visBox = visibleBox(circle.position.z)
circle.position.x = _.random(visBox.min.x, visBox.max.x);
circle.position.y = visBox.min.y;
Code demonstration:
let renderer, scene, light, circles, camera;
initialize();
animate();
function initialize() {
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
scene = new THREE.Scene();
light = new THREE.AmbientLight();
scene.add(light);
circles = new THREE.Group();
scene.add(circles);
camera = new THREE.PerspectiveCamera(45, renderer.domElement.clientWidth / renderer.domElement.clientHeight, 1);
camera.position.z = circles.position.z + 500;
}
function animate() {
// Update each circle.
Array.from(circles.children).forEach(circle => {
if (circle.position.y < visibleBox(circle.position.z).max.y) {
circle.position.y += 4;
} else {
circles.remove(circle);
}
});
// Create a new circle.
let circle = new THREE.Mesh();
circle.geometry = new THREE.CircleGeometry(30, 30);
circle.material = new THREE.MeshToonMaterial({ color: randomColor(), transparent: true, opacity: 0.5 });
circle.position.z = _.random(-(camera.near+(camera.far-camera.near)/5), -camera.far);
var visBox = visibleBox(circle.position.z)
circle.position.x = _.random(visBox.min.x, visBox.max.x);
circle.position.y = visBox.min.y;
circles.add(circle);
// Render the scene.
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
function visibleBox(z) {
var t = Math.tan( THREE.Math.degToRad( camera.fov ) / 2 )
var h = t * 2 * z;
var w = h * camera.aspect;
return new THREE.Box2(new THREE.Vector2(-w, h), new THREE.Vector2(w, -h));
}
function randomColor() {
return `#${ _.sampleSize("abcdef0123456789", 6).join("")}`;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js">
</script>
Explanation
The projection matrix describes the mapping from 3D points of a scene, to 2D points of the viewport. It transforms from eye space to the clip space, and the coordinates in the clip space are transformed to the normalized device coordinates (NDC) by dividing with the w component of the clip coordinates. The NDC are in range (-1,-1,-1) to (1,1,1).
In the perspective projection the relation between the depth value and the z distance to the camera is not linear.
A perspective projection matrix looks like this:
r = right, l = left, b = bottom, t = top, n = near, f = far
2*n/(r-l) 0 0 0
0 2*n/(t-b) 0 0
(r+l)/(r-l) (t+b)/(t-b) -(f+n)/(f-n) -1
0 0 -2*f*n/(f-n) 0
From this follows the relation between the z coordinate in view space and the normalized device coordinates z component and the depth.:
z_ndc = ( -z_eye * (f+n)/(f-n) - 2*f*n/(f-n) ) / -z_eye
depth = (z_ndc + 1.0) / 2.0
The reverse operation looks like this:
n = near, f = far
z_ndc = 2.0 * depth - 1.0;
z_eye = 2.0 * n * f / (f + n - z_ndc * (f - n));
If the perspective projection matrix is known this can be done as follows:
A = prj_mat[2][2]
B = prj_mat[3][2]
z_eye = B / (A + z_ndc)
See How to render depth linearly in modern OpenGL with gl_FragCoord.z in fragment shader?
The realtion between the projected area in view space and the Z coordinate of the view space is linear. It dpends on the field of view angle and the aspect ratio.
The normaized dievice size can be transformed to a size in view space like this:
aspect = w / h
tanFov = tan( fov_y * 0.5 );
size_x = ndx_size_x * (tanFov * aspect) * z_eye;
size_y = ndx_size_y * tanFov * z_eye;
if the perspective projection matrix is known and the projection is symmetrically (the line of sight is in the center of the viewport and the field of view is not displaced), this can be done as follows:
size_x = ndx_size_x * / (prj_mat[0][0] * z_eye);
size_y = ndx_size_y * / (prj_mat[1][1] * z_eye);
See Field of view + Aspect Ratio + View Matrix from Projection Matrix (HMD OST Calibration)
Note each position in normalized device coordinates can be transformed to view space coordinates by the inverse projection matrix:
mat4 inversePrjMat = inverse( prjMat );
vec4 viewPosH = inversePrjMat * vec3( ndc_x, ndc_y, 2.0 * depth - 1.0, 1.0 );
vec3 viewPos = viewPos.xyz / viewPos.w;
See How to recover view space position given view space depth value and ndc xy
This means the unprojected rectangle with a specific depth, can be calculated like this:
vec4 viewLowerLeftH = inversePrjMat * vec3( -1.0, -1.0, 2.0 * depth - 1.0, 1.0 );
vec4 viewUpperRightH = inversePrjMat * vec3( 1.0, 1.0, 2.0 * depth - 1.0, 1.0 );
vec3 viewLowerLeft = viewLowerLeftH.xyz / viewLowerLeftH.w;
vec3 viewUpperRight = viewUpperRightH.xyz / viewUpperRightH.w;
I am trying to input some known acr values from another program and reproduce them in three.js
Right now I am using the following code I found on this site. It draw the arc fine, although it may not be the best option.
function DRAWarc(){
// smooth my curve over this many points
var numPoints = 100;
spline = new THREE.SplineCurve3([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 200, 0),
new THREE.Vector3(150, 150, 0)
]);
var material = new THREE.LineBasicMaterial({
color: 0xff00f0,
});
var geometry = new THREE.Geometry();
var splinePoints = spline.getPoints(numPoints);
for(var i = 0; i < splinePoints.length; i++){
geometry.vertices.push(splinePoints[i]);
}
var line = new THREE.Line(geometry, material);
scene.add(line);
}
The following are the known variables.
Center point (X,Y) (if the are was a complete circle, the center of the circle)
radius (if it were a circle)
start angle (I'm not positive, but I think this is the degree, if it were a circle, going counter-clockwise, with 0 being to the right of the circle)
end angle (see above)
more code!
///////////
// SCENE //
///////////
scene = new THREE.Scene();
////////////
// CAMERA //
////////////
var SCREEN_WIDTH = window.innerWidth, SCREEN_HEIGHT = window.innerHeight;
viewsize = 900;
camera = new THREE.OrthographicCamera(
SCREEN_WIDTH / - 2, SCREEN_WIDTH / 2,
SCREEN_HEIGHT / 2, SCREEN_HEIGHT / - 2,
1, 1e6 );
camera.position.z = 2000;
scene.add(camera);
camera.lookAt(new THREE.Vector3(2100, 3600, 0));
//////////////
// RENDERER //
//////////////
// create and start the renderer
if ( Detector.webgl ){
renderer = new THREE.WebGLRenderer();
//alert('no problem.');
}else{
renderer = new THREE.CanvasRenderer();
alert('problem.');
}
renderer.setClearColor("white", 1);
renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
container = document.body;
container.appendChild( renderer.domElement );
////////////
// EVENTS //
////////////
// automatically resize renderer
THREEx.WindowResize(renderer, camera);
// toggle full-screen on given key press
THREEx.FullScreen.bindKey({ charCode : 'm'.charCodeAt(0) });
///////////
// STATS //
///////////
// displays current and past frames per second attained by scene
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.bottom = '0px';
stats.domElement.style.zIndex = 100;
container.appendChild( stats.domElement );
///////////
// LIGHT //
///////////
// create a light
var light = new THREE.PointLight(0xffffff);
light.position.set(0,250,0);
scene.add(light);
var ambientLight = new THREE.AmbientLight(0x111111);
// scene.add(ambientLight);
//////////////
// GEOMETRY //
//////////////
// most objects displayed are a "mesh":
// a collection of points ("geometry") and
// a set of surface parameters ("material")
doWork();
}
function animate()
{
requestAnimationFrame( animate );
render();
update();
}
function update()
{
// delta = change in time since last call (in seconds)
var delta = clock.getDelta();
// functionality provided by THREEx.KeyboardState.js
if ( keyboard.pressed("1") )
document.getElementById('message').innerHTML = ' Have a nice day! - 1';
if ( keyboard.pressed("2") )
document.getElementById('message').innerHTML = ' Have a nice day! - 2 ';
//controls.update();
stats.update();
}
function render()
{
renderer.render( scene, camera );
}`
You can draw arc with the circle geometry
// compute angle between p1 and p2
var angle = Math.acos(p1.dot(p2)/(p1.length()*p2.length()));
// create arc
var geometry = new THREE.CircleGeometry(radius, nbSegments, 0, angle);
// remove center vertex
geometry.vertices.splice(0,1);
// TODO: move the arc to the good place in the scene
// add arc to the scene
scene.add(new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xff00f0 }));
So after a little research I found the following post.
How do I calculate a point on a circle’s circumference?
which led me to this bit of math that can be adapted to any language:
x = cx + r * cos(a)
y = cy + r * sin(a)
Where r is the radius, cx,cy the origin, and a the angle from 0..2PI radians or 0..360 degrees.
and heres some fun reading material!
http://en.wikipedia.org/wiki/Circle#Equations
EDIT: just completed the rough draft for this project. enjoi!
i does not draw a spline, instead it draws a line with 102 points. the start of the arc, the end, and 100 evenly spaced points in between. it works well, and i will add a variable to the number of lines to reduce memory if needed.
function getARC(x, y, r, a){
a = a * (Math.PI/180);
var ax = +x + +r * Math.cos(+a),
ay = +y + +r * Math.sin(+a),
res = [];
res['x'] = ax,
res['y'] = ay;
return res;
}
function DRAWarc(cx, cy, ra, sa, ea){
var cx = '2473.5737';
var cy = '3145.1300';
var ra = '47.5538';
var sa = '2';
var ea = '91';
var material = new THREE.LineBasicMaterial({
color: 0xff00f0,
});
var geometry = new THREE.Geometry();
var s = getARC(cx, cy, ra, sa);
geometry.vertices.push(new THREE.Vector3(s['x'], s['y'], 0));
var step = (ea - sa)/100;
for(var i=1;i<=100;i++){
var t = getARC(cx, cy, ra, (+sa + (+step * +i)));
geometry.vertices.push(new THREE.Vector3(t['x'], t['y'], 0));
//alert((+sa + (+step * +i)));
}
var f = getARC(cx, cy, ra, ea);
geometry.vertices.push(new THREE.Vector3(f['x'], f['y'], 0));
var line = new THREE.Line(geometry, material);
scene.add(line);
}