Trying to create interactive GUI for my app using threejs.
I've found this tutorial:
http://zachberry.com/blog/tracking-3d-objects-in-2d-with-three-js/
which explains exactly what I need, but uses some old release.
function getCoordinates(element, camera) {
var p, v, percX, percY, left, top;
var projector = new THREE.Projector();
// this will give us position relative to the world
p = element.position.clone();
console.log('project p', p);
// projectVector will translate position to 2d
v = p.project(camera);
console.log('project v', v);
// translate our vector so that percX=0 represents
// the left edge, percX=1 is the right edge,
// percY=0 is the top edge, and percY=1 is the bottom edge.
percX = (v.x + 1) / 2;
percY = (-v.y + 1) / 2;
// scale these values to our viewport size
left = percX * window.innerWidth;
top = percY * window.innerHeight;
console.log('2d coords left', left);
console.log('2d coords top', top);
}
I had to change the projector to vector.project and matrixWorld.getPosition().clone() to position.clone().
passing position (0,0,0) results with v = {x: NaN, y: NaN, z: -Infinity}, which is not what expected
camera which im passing is camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 10000 )
function getCoordinates( element, camera ) {
var screenVector = new THREE.Vector3();
element.localToWorld( screenVector );
screenVector.project( camera );
var posx = Math.round(( screenVector.x + 1 ) * renderer.domElement.offsetWidth / 2 );
var posy = Math.round(( 1 - screenVector.y ) * renderer.domElement.offsetHeight / 2 );
console.log( posx, posy );
}
http://jsfiddle.net/L0rdzbej/122/
Related
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.
This is a general WebGL issue but for the sake of clarity I'll be using three.js to demonstrate my problem here.
Let's say I have a plane and a perspective camera. I'm trying to get the bounding rectangle of the plane relative to the viewport/window.
This is how I'm doing it so far:
First, get the modelViewProjectionMatrix by multiplying the camera
projectionMatrix with the plane matrix.
Apply that modelViewProjectionMatrix to the plane 4 corners vertices.
Get the min/max values of the result and convert them back to viewport coordinates.
It works well until the plane gets clipped by the camera near plane (usually when using a high field of view), messing up my results.
Is there any way I can get correct values even if the camera near plane is clipping parts of my plane? Maybe by getting the intersection between the plane and the camera near plane?
Edit:
One idea I can think of would be to get the two normalized vectors v1 and v2 as shown on this schema: intersections between a plane and the camera near plane schema.
I'd then have to get the length of those vectors so that they go from the plane's corner to the intersection point (knowing the near plane Z position), but I'm still struggling on that last part.
Anyway, here's the three.js code and the according jsfiddle (uncomment line 109 to show erronate coordinates): https://jsfiddle.net/fbao9jp7/1/
let scene = new THREE.Scene();
let ww = window.innerWidth;
let wh = window.innerHeight;
// camera
const nearPlane = 0.1;
const farPlane = 200;
let camera = new THREE.PerspectiveCamera(45, ww / wh, nearPlane, farPlane);
scene.add(camera);
// renderer
let renderer = new THREE.WebGLRenderer();
renderer.setSize(ww, wh);
document.getElementById("canvas").appendChild(renderer.domElement);
// basic plane
let plane = new THREE.Mesh(
new THREE.PlaneGeometry(0.75, 0.5),
new THREE.MeshBasicMaterial({
map: new THREE.TextureLoader().load('https://source.unsplash.com/EqFjlsOZULo/1280x720'),
side: THREE.DoubleSide,
})
);
scene.add(plane);
function displayBoundingRectangle() {
camera.updateProjectionMatrix();
// keep the plane at a constant position along Z axis based on camera FOV
plane.position.z = -1 / (Math.tan((Math.PI / 180) * 0.5 * camera.fov) * 2.0);
plane.updateMatrix();
// get the plane model view projection matrix
let modelViewProjectionMatrix = new THREE.Matrix4();
modelViewProjectionMatrix = modelViewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, plane.matrix);
let vertices = plane.geometry.vertices;
// apply modelViewProjectionMatrix to our 4 vertices
let projectedPoints = [];
for (let i = 0; i < vertices.length; i++) {
projectedPoints.push(vertices[i].applyMatrix4(modelViewProjectionMatrix));
}
// get our min/max values
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
for (let i = 0; i < projectedPoints.length; i++) {
let corner = projectedPoints[i];
if (corner.x < minX) {
minX = corner.x;
}
if (corner.x > maxX) {
maxX = corner.x;
}
if (corner.y < minY) {
minY = corner.y;
}
if (corner.y > maxY) {
maxY = corner.y;
}
}
// we have our four coordinates
let worldBoundingRect = {
top: maxY,
right: maxX,
bottom: minY,
left: minX,
};
// convert coordinates from [-1, 1] to [0, 1]
let screenBoundingRect = {
top: 1 - (worldBoundingRect.top + 1) / 2,
right: (worldBoundingRect.right + 1) / 2,
bottom: 1 - (worldBoundingRect.bottom + 1) / 2,
left: (worldBoundingRect.left + 1) / 2,
};
// add width and height
screenBoundingRect.width = screenBoundingRect.right - screenBoundingRect.left;
screenBoundingRect.height = screenBoundingRect.bottom - screenBoundingRect.top;
var boundingRectEl = document.getElementById("plane-bounding-rectangle");
// apply to our bounding rectangle div using window width and height
boundingRectEl.style.top = screenBoundingRect.top * wh + "px";
boundingRectEl.style.left = screenBoundingRect.left * ww + "px";
boundingRectEl.style.height = screenBoundingRect.height * wh + "px";
boundingRectEl.style.width = screenBoundingRect.width * ww + "px";
}
// rotate the plane
plane.rotation.x = -2;
plane.rotation.y = -0.8;
/* UNCOMMENT THIS LINE TO SHOW HOW NEAR PLANE CLIPPING AFFECTS OUR BOUNDING RECTANGLE VALUES */
//camera.fov = 150;
// render scene
render();
// show our bounding rectangle
displayBoundingRectangle();
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
body {
margin: 0;
}
#canvas {
width: 100vw;
height: 100vh;
}
#plane-bounding-rectangle {
position: fixed;
pointer-events: none;
background: red;
opacity: 0.2;
}
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.min.js"></script>
<div id="canvas"></div>
<div id="plane-bounding-rectangle"></div>
Many thanks,
Does this show the issue?
let scene = new THREE.Scene();
let ww = window.innerWidth;
let wh = window.innerHeight;
// camera
const nearPlane = 0.1;
const farPlane = 200;
let camera = new THREE.PerspectiveCamera(45, ww / wh, nearPlane, farPlane);
scene.add(camera);
// renderer
let renderer = new THREE.WebGLRenderer();
renderer.setSize(ww, wh);
document.getElementById("canvas").appendChild(renderer.domElement);
// basic plane
let plane = new THREE.Mesh(
new THREE.PlaneGeometry(0.75, 0.5),
new THREE.MeshBasicMaterial({
map: new THREE.TextureLoader().load('https://source.unsplash.com/EqFjlsOZULo/1280x720'),
side: THREE.DoubleSide,
})
);
scene.add(plane);
function displayBoundingRectangle() {
camera.updateProjectionMatrix();
// keep the plane at a constant position along Z axis based on camera FOV
plane.position.z = -1 / (Math.tan((Math.PI / 180) * 0.5 * camera.fov) * 2.0);
plane.updateMatrix();
// get the plane model view projection matrix
let modelViewProjectionMatrix = new THREE.Matrix4();
modelViewProjectionMatrix = modelViewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, plane.matrix);
let vertices = plane.geometry.vertices;
// get our min/max values
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
// apply modelViewProjectionMatrix to our 4 vertices
let corner = new THREE.Vector3();
for (let i = 0; i < vertices.length; i++) {
corner.copy(vertices[i]);
corner.applyMatrix4(modelViewProjectionMatrix);
minX = Math.min(corner.x, minX);
maxX = Math.max(corner.x, maxX);
minY = Math.min(corner.y, minY);
maxY = Math.max(corner.y, maxY);
}
// we have our four coordinates
let worldBoundingRect = {
top: maxY,
right: maxX,
bottom: minY,
left: minX,
};
document.querySelector('#info').textContent = `${minX.toFixed(2)}, ${maxX.toFixed(2)}, ${minY.toFixed(2)}, ${minY.toFixed(2)}`;
// convert coordinates from [-1, 1] to [0, 1]
let screenBoundingRect = {
top: 1 - (worldBoundingRect.top + 1) / 2,
right: (worldBoundingRect.right + 1) / 2,
bottom: 1 - (worldBoundingRect.bottom + 1) / 2,
left: (worldBoundingRect.left + 1) / 2,
};
// add width and height
screenBoundingRect.width = screenBoundingRect.right - screenBoundingRect.left;
screenBoundingRect.height = screenBoundingRect.bottom - screenBoundingRect.top;
var boundingRectEl = document.getElementById("plane-bounding-rectangle");
// apply to our bounding rectangle div using window width and height
boundingRectEl.style.top = screenBoundingRect.top * wh + "px";
boundingRectEl.style.left = screenBoundingRect.left * ww + "px";
boundingRectEl.style.height = screenBoundingRect.height * wh + "px";
boundingRectEl.style.width = screenBoundingRect.width * ww + "px";
}
// rotate the plane
plane.rotation.x = -2;
plane.rotation.y = -0.8;
/* UNCOMMENT THIS LINE TO SHOW HOW NEAR PLANE CLIPPING AFFECTS OUR BOUNDING RECTANGLE VALUES */
//camera.fov = 150;
// render scene
render();
function render(time) {
camera.fov = THREE.MathUtils.lerp(45, 150, Math.sin(time / 1000) * 0.5 + 0.5);
// show our bounding rectangle
displayBoundingRectangle();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
body {
margin: 0;
}
#canvas {
width: 100vw;
height: 100vh;
}
#plane-bounding-rectangle {
position: fixed;
pointer-events: none;
background: red;
opacity: 0.2;
}
#info {
position: absolute;
left: 0;
top: 0;
pointer-events: none;
color: white;
}
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.min.js"></script>
<div id="canvas"></div>
<div id="plane-bounding-rectangle"></div>
<pre id="info"></pre>
If it's not clear the issue is one of your points is going behind the camera frustum cone. The frustum defines a kind of cutoff pyramid from near to far but the math past near (toward the camera) eventually goes to a single point, anything behind that point start starts expanding back out
Based on my schema in the initial question, I've managed to solve my issue.
I won't post the whole snippet here because it's kinda long and verbose (and it also feels a bit like a dirty hack), but this is the main idea:
Get the clipped and non clipped corners.
Use the non clipped corners coordinates and a really small vector going from that corner to the clipped one and recursively add it to our non clipped corner coordinate until we reached the near plane Z position: we found the intersection with the near plane.
Let's say the top left corner of the plane is not clipped but the bottom left corner is clipped. To find the intersection between the camera near plane and the plane's left side, we'll do something like this:
// find the intersection by adding a vector starting from a corner till we reach the near plane
function getIntersection(refPoint, secondPoint) {
// direction vector to add
let vector = secondPoint.sub(refPoint);
// copy our corner refpoint
var intersection = refPoint.clone();
// iterate till we reach near plane
while(intersection.z > -1) {
intersection.add(vector);
}
return intersection;
}
// get our top left corner projected coordinates
let topLeftCorner = vertices[0].applyMatrix4(modelViewProjectionMatrix);
// get a vector parallel to our left plane side toward the bottom left corner and project its coordinates as well
let directionVector = vertices[0].clone().sub(new THREE.Vector3(0, -0.05, 0)).applyMatrix4(modelViewProjectionMatrix);
// get the intersection with the near plane
let bottomLeftIntersection = getIntersection(topLeftCorner, directionVector);
I'm sure there would be a more analytical approach to solve this problem but this works, so I'm gonna stick with it for now.
I am trying to draw a circle exactly where mouse is clicked, but circle is drawn not at exact position. Please let me know what needs to be corrected in the code:
var camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 10000 );
camera.position.set( 0, 0, 1000 );
document.addEventListener( 'mousedown', onDocumentMouseDown, false );
function onDocumentMouseDown( event ) {
event.preventDefault();
if( event.which == 1) { // left mouse click
// x = ( event.clientX / renderer.domElement.clientWidth ) * 2 - 1;
// y = - ( event.clientY / renderer.domElement.clientWidth ) * 2 + 1;
var x = event.clientX; // x coordinate of a mouse pointer
var y = event.clientY; // y coordinate of a mouse pointer
var rect = event.target.getBoundingClientRect();
x = ((x - rect.left) - window.innerWidth/2)/(window.innerWidth/2);
y = (window.innerHeight/2 - (y - rect.top))/(window.innerHeight/2);
var geometry = new THREE.CircleGeometry( 20, 32 );
var material = new THREE.MeshBasicMaterial( { color: 0x65A8FF } );
circle = new THREE.Mesh( geometry, material );
//circle.position.x = x*window.innerWidth*1.23;
//circle.position.y = y*765;
circle.position.x = x*window.innerWidth;
circle.position.y = y*window.innerHeight;
scene.add( circle );
}
}
The problem with your approach is that it's assuming 3D x y coordinates are connected to pixel coordinates without taking the camera's perspective or depth into account. With a perspective camera, the deeper the point goes, the greater its x, y coords will be. You need to set a depth (z) for the points to stop at. Additionally, if your camera moves or rotates, your X, Y coordinates will not work.
I recommend you use a THREE.Raycaster, which performs all those camera projection calculations for you (the docs have an example of its usage at the top of the page). You can see that approach in action in this example.
The way it works is:
You create a plane for the raycaster to hit (this is your depth).
Create a raycast with Raycaster.setFromCamera()
Read back the position where the ray hits the plane.
Use this position to create your geometry.
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 have created an inset trackball camera controller for a 3D scene, using three.js. Currently, this uses a tiny cube, a circle and an orthographic camera placed at the origin of the world. However, these three objects are still visible in the scene itself, as viewed through the main camera. (In my demo code below, I have deliberately made the cube 10x10x10 so that it is clearly visible, but it could be made much smaller.)
Also, elements that are part of the main scene that pass through the origin are visible in the inset. For example: the AxisHelper that belongs to the main scene can be seen in the inset.
Is it possible in three.js/webgl to make certain objects visible to only certain cameras?
If not, then a workaround would be to place the objects required for the trackball feature way off out into deep space, where the main camera cannot see them, but I would prefer a purer solution if possible.
Demo: http://codepen.io/anon/pen/MKWrOr
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r73/three.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="WebGL-output"></div>
<script>
function init() {
var scene = new THREE.Scene()
var renderer = new THREE.WebGLRenderer()
var camera
var cameras = []
var WIDTH = window.innerWidth
var HEIGHT = window.innerHeight
;(function createPerspectiveCamera(){
var FOV = 45
var ASPECT = WIDTH / HEIGHT
var NEAR = 1
var FAR = 360
camera = new THREE.PerspectiveCamera(FOV, ASPECT, NEAR, FAR)
camera.position.x = 100
camera.position.y = 100
camera.position.z = 100
camera.viewport = { x: 0, y: 0, width: WIDTH, height: HEIGHT }
camera.lookAt(scene.position)
cameras.push(camera)
})()
;(function initializeRenderer(){
renderer.setClearColor(new THREE.Color(0xEEEEFF))
renderer.setSize(WIDTH, HEIGHT)
renderer.autoClear = false;
document.getElementById("WebGL-output").appendChild(renderer.domElement)
;(function render() {
var viewport
renderer.setViewport( 0, 0, WIDTH, HEIGHT );
renderer.clear();
cameras.forEach(function (camera) {
viewport = camera.viewport // custom property
renderer.setViewport(
viewport.x
, viewport.y
, viewport.width
, viewport.height
)
renderer.render(scene, camera)
})
requestAnimationFrame(render)
})()
})()
;(function createCameraController(){
var viewport = {
x: WIDTH - 100
, y: HEIGHT - 100
, width: 100
, height: 100
}
var circle = {
x: WIDTH - 50
, y: 50
, radius: 50
}
var settings = {
viewport: viewport
, circle: circle
}
addCameraController(scene, camera, cameras, settings)
})()
// Something to look at
scene.add(new THREE.AxisHelper(70))
}
function addCameraController(scene, camera, cameras, settings) {
var controlCamera
var viewport = settings.viewport
// For mouse interactions
var centreX = settings.circle.x
var centreY = settings.circle.y
var radius = settings.circle.radius
var radius2 = radius * radius
var rotationMatrix = new THREE.Matrix4()
var pivotMatrix = new THREE.Matrix4()
var startMatrix = new THREE.Matrix4()
var start = new THREE.Vector3()
var end = new THREE.Vector3()
var angle
camera.matrixAutoUpdate = false /** takes control of main camera **/
;(function createControlCameraCubeAndCircle(){
var side = 10
var radius = Math.sqrt(side/2 * side/2 * 3)
;(function createCamera(){
controlCamera = new THREE.OrthographicCamera(
-radius, radius
, radius, -radius
, -radius, radius
);
controlCamera.viewport = viewport
controlCamera.rotation.copy(camera.rotation)
// If matrixAutoUpdate is set immediately, the camera rotation is
// not applied
setTimeout(function () {
controlCamera.matrixAutoUpdate = false
}, 1)
scene.add(controlCamera)
cameras.push( controlCamera )
})()
;(function createCompanionCube(){
var cube = new THREE.Object3D()
var cubeGeometry = new THREE.BoxGeometry( side, side, side )
var lineMaterial = new THREE.LineBasicMaterial({
color: 0xffffff
, transparent: true
, opacity: 0.5
})
var faceMaterial = new THREE.MeshPhongMaterial({
color: 0x006699
, emissive: 0x006699
, shading: THREE.FlatShading
, transparent: true
, opacity: 0.2
})
cube.add(
new THREE.LineSegments(
new THREE.WireframeGeometry( cubeGeometry )
, lineMaterial
)
)
cube.add(
new THREE.Mesh(
cubeGeometry
, faceMaterial
)
)
// cube.add(new THREE.AxisHelper(radius))
scene.add(cube);
})()
;(function createCircle(){
var circleGeometry = new THREE.CircleGeometry( radius, 36 );
var material = new THREE.MeshBasicMaterial( {
color: 0xccccff
} );
var circle = new THREE.Mesh( circleGeometry, material );
controlCamera.add( circle );
circle.translateZ(-radius)
})()
})()
window.addEventListener("mousedown", startDrag, false)
function startDrag(event) {
var x = event.clientX - centreX
var y = centreY - event.clientY
var delta2 = x * x + y * y
if (delta2 > radius2) {
return
}
var z = Math.sqrt(radius2 - delta2)
start.set(x, y, z)
window.addEventListener("mousemove", drag, false)
window.addEventListener("mouseup", stopDrag, false)
function drag(event) {
var delta
x = event.clientX - centreX
y = centreY - event.clientY
delta2 = x * x + y * y
if (delta2 > radius2) {
// constrain to adge of sphere
delta = Math.sqrt(delta2)
x = x / delta * radius
y = y / delta * radius
z = 0
} else {
z = Math.sqrt(radius2 - delta2)
}
end.set(x, y, z)
angle = start.angleTo(end)
start.cross(end).normalize()
rotationMatrix.makeRotationAxis(start, -angle)
controlCamera.matrix.multiply(rotationMatrix)
controlCamera.matrixWorldNeedsUpdate = true
rotationMatrix.extractRotation(camera.matrixWorld)
start.applyMatrix4(rotationMatrix).normalize()
rotationMatrix.makeRotationAxis(start, -angle)
camera.applyMatrix(rotationMatrix)
camera.matrixWorldNeedsUpdate = true
start.copy(end)
}
function stopDrag(event) {
window.removeEventListener("mousemove", drag, false)
window.removeEventListener("mouseup", stopDrag, false)
}
}
}
window.onload = init
</script>
</body>
</html>
three.js supports layers.
An object is visible to a camera if the object and the camera share a common layer. The camera and and all objects are by default in layer 0.
For example,
camera.layers.enable( 1 ); // camera now sees default layer 0 and layer 1
camera.layers.set( 1 ); // camera now sees only layer 1
mesh.layers.set( 1 ); // mesh is in layer 1
three.js r.75