This question already has an answer here:
-
Three.js - Width of view
1 answer
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;