THREE.JS: Get object size with respect to camera and object position on screen - three.js

I am newbie to 3D programming, I did started to explore the 3D world from WebGL with Three.JS.
I want to predetermine object size while I change the camera.position.z and object's "Z" position.
For example:
i have a cube mesh at size of 100x100x100.
cube = new THREE.Mesh(
new THREE.CubeGeometry(100, 100, 100, 1,1,1, materials),
new THREE.MeshFaceMaterial()
);
and cam with aspect ratio of 1.8311874
camera = new THREE.PerspectiveCamera( 45, aspect_ratio, 1, 30000 );
I want to know size (2D width & height) of that cube object on screen when,
camera.position.z = 750;
cube.position.z = 500;
Is there is any way to find it/predetermine it?

You can compute the visible height for a given distance from the camera using the formulas explained in Three.js - Width of view.
var vFOV = camera.fov * Math.PI / 180; // convert vertical fov to radians
var height = 2 * Math.tan( vFOV / 2 ) * dist; // visible height
In your case the camera FOV is 45 degrees, so
vFOV = PI/4.
(Note: in three.js the camera field-of-view FOV is the vertical one, not the horizontal one.)
The distance from the camera to the front face (important!) of the cube is 750 - 500 - 50 = 200. Therefore, the visible height in your case is
height = 2 * tan( PI/8 ) * 200 = 165.69.
Since the front face of the cube is 100 x 100, the fraction of the visible height represented by the cube is
fraction = 100 / 165.69 = 0.60.
So if you know the canvas height in pixels, then the height of the cube in pixels is 0.60 times that value.
The link I provided shows how to compute the visible width, so you can do that calculation in a similar fashion if you need it.

Related

Bounding box that is parallel to the camera

My problem is how to define the camera location, given a lookAt vector, when the camera is not on the z axis, so it captures all objects according to its fov and aspect.
I think I need to get a bounding box of my objects that is perpendicular to the camera's lookAt and top and bottom front and back edges are parallel to the xz plane. Then the back of the bounding box is the 'far' plane and I can calculate the distance from it (or fov) and set the camera accordingly.
My question is, how to get such a bounding box (Box3 instance), given some objects on the scene and the lookAt vector ?
My question is, how to get such a bounding box (Box3 instance), given some objects on the scene and the lookAt vector ?
Instances of THREE.Box3 are axis-aligned bounding boxes. No matter how the camera is rotated, it is not possible to generate a different bounding box for a given set of 3D objects.
Maybe you can use a quite common approach 3D viewers which ensures to always display an imported 3D object in the viewport. Exemplary code from the open source glTF viewer looks like this:
const aabb = new THREE.Box3().setFromObject( object );
const center = aabb.getCenter( new THREE.Vector3() );
const size = aabb.getSize( new THREE.Vector3() ).length();
// centering object
object.position.x += ( object.position.x - center.x );
object.position.y += ( object.position.y - center.y );
object.position.z += ( object.position.z - center.z );
// update camera
camera.near = size / 100;
camera.far = size * 100;
camera.updateProjectionMatrix();
camera.position.copy( center );
camera.position.x += size / 2.0;
camera.position.y += size / 5.0;
camera.position.z += size / 2.0;
camera.lookAt( center );

How to preserve threejs texture scale while applying texture rotation

I'd like to enable a user to rotate a texture on a rectangle while keeping the aspect ratio of the texture image intact. I'm doing the rotation of a 1:1 aspect ratio image on a surface that is rectangular (say width: 2 and length: 1)
Steps to reproduce:
In the below texture rotation example
https://threejs.org/examples/?q=rotation#webgl_materials_texture_rotation
If we change one of the faces of the geometry like below:
https://github.com/mrdoob/three.js/blob/master/examples/webgl_materials_texture_rotation.html#L57
var geometry = new THREE.BoxBufferGeometry( 20, 10, 10 );
Then you can see that as you play around with the rotation control, the image aspect ratio is distorted. (form a square to a weird shape)
At 0 degree:
At some angle between 0 and 90:
I understand that by changing the repeatX and repeatY factor I can control this. It's also easy to see what the values would be at 0 degree, 90 degree rotations.
But I'm struggling to come up with the formula for repeatX and repeatY that works for any texture rotation given length and width of the rectangular face.
Unfortunately when stretching geometry like that, you'll get a distortion in 3D space, not UV space. In this example, one UV.x unit occupies twice as much 3D space as one UV.y unit:
This is giving you those horizontally-skewed diamonds when in between rotations:
Sadly, there's no way to solve this with texture matrix transforms. The horizontal stretching will be applied after the texture transform, in 3D space, so texture.repeat won't help you avoid this. The only way to solve this is by modifying the UVs so the UV.x units take up as much 3D space as UV.y units:
With complex models, you'd do this kind of "equalizing" in a 3D editor, but since the geometry is simple enough, we can do it via code. See the example below. I'm using a width/height ratio variable to use in my UV.y remapping, that way the UV transformations will match up, regardless of how much wider it is.
//////// Boilerplate Three setup
const renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});
const camera = new THREE.PerspectiveCamera(50, 1, 1, 100);
camera.position.z = 3;
const scene = new THREE.Scene();
/////////////////// CREATE GEOM & MATERIAL
const width = 2;
const height = 1;
const ratio= width / height; // <- magic number that will help with UV remapping
const geometry = new THREE.BoxBufferGeometry(width, height, width);
let uvY;
const uvArray = geometry.getAttribute("uv").array;
// Re-map UVs to avoid distortion
for (let i2 = 0; i2 < uvArray.length; i2 += 2){
uvY = uvArray[i2 + 1]; // Extract Y value,
uvY -= 0.5; // center around 0
uvY /= ratio; // divide by w/h ratio
uvY += 0.5; // remove center around 0
uvArray[i2 + 1] = uvY;
}
geometry.getAttribute("uv").needsUpdate = true;
const uvMap = new THREE.TextureLoader().load("https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/uv_grid_opengl.jpg");
// Now we can apply texture transformations as expected
uvMap.center.set(0.5, 0.5);
uvMap.repeat.set(0.25, 0.5);
uvMap.anisotropy = 16;
const material = new THREE.MeshBasicMaterial({map: uvMap});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("resize", resize);
// Add rotation on mousemove
function onMouseMove(ev) {
uvMap.rotation = (ev.clientX / window.innerWidth) * Math.PI * 2;
}
function resize() {
const width = window.innerWidth;
const height = window.innerHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
function animate(time) {
mesh.rotation.y = Math.cos(time/ 3000) * 2;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
resize();
requestAnimationFrame(animate);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://threejs.org/build/three.js"></script>
<canvas></canvas>
First of all, I agree with the solution #Marquizzo provided to your problem. And setting UV explicitly to the geometry should be the easiest way to solve your problem.
But #Marquizzo did not answer why changing the matrix of the texture (set repeatX and repeatY) does not work.
We all know the 2D rotation matrix R
cos -sin
sin cos
UVs are calculated in the shader with a transform matrix T, which is the texture matrix from your question.
T * UV = new UV
To simplify the question, we only consider rotation. And assume we have another additional matrix X for calculating the new UV. Then we have
X * R * UV = new UV
The question now is whether we can find a solution ofX, so that with any rotation, new UV of any points in your question can be calculated correctly. If there is a solution of X, then we can simply use
var X = new Matrix3();
//X.set(x,y,z,...)
texture.matrix.premultiply(X);
Otherwise, we can't find the approach you expected.
Let's create several equations to figure out X.
In the pic below, ABCD is one face of your geometry, and the transparent green is the texture. The UV of point A is (0,1), point B is (0,0), and (1,0), (1,1) for C and D respectively.
The first equation comes from the consideration, without any rotation, the original UV should never be changed (UV for A is always (0,1)). So we should have
X * I * (0, 1) = (0, 1) // I is the identity matrix
From here we can see X should also be an identity matrix.
Then let's see whether the identity matrix X can satisfy the second equation. What's the second equation? Simplify again, let B be the rotation centre(origin) and rotate the texture 90 degrees(counterclockwise). We use -90 to calculate UV though we rotate 90 degrees.
The new UV for point A after rotating the texture 90 degrees should be the current value of E. The value of E is (a/b, 0). Then we have
From this equation we can see X should not be an identity matrix, which means, WE ARE NOT ABLE TO FIND A SOLUTION OF X TO SOLVE YOUR PROBLEM WITH
X * R * UV = new UV
Certainly, you can change the shader of calculating new UVs, but it's even harder than the way #Marquizzo provided.

How to calculate fov for the Perspective camera in three js?

I want to set CubeGeometry touch to the canvas and I used this fovFormula but it didn't work out. This CubeGeometry is going out of canvas.
var height = 500;
var distance = 1000;
var fov = 2 * Math.atan((height) / (2 * distance)) * (180 / Math.PI);
itsLeftCamera = new THREE.PerspectiveCamera(fov , 400 / 500, 1.0, 1000);
If I am calculating wrong so, please guide me how to overcome this problem? and I want to set this in generalize way so at any position of Perspective camera, this geometry would perfectly touch to my canvas and this geometry should be in center of the canvas.
IMO you should calculate for the diagonal instead of the height in the fov calculator because when doing for height you focus on height thereby cutting off width portion greater than height.... When you do for diagonal your camera focus on the entire rectangle...so code imo should be
var height = 500; //Height of the viewport
var width = 400; //Width of the viewPort
var distance = 1000; //Distance of the viewer from the viewport
var diag = Math.sqrt((height*height)+(width*width))
var fov = 2 * Math.atan((diag) / (2 * distance)) * (180 / Math.PI);
itsLeftCamera = new THREE.PerspectiveCamera(fov , width / height, 0.1, distance);

90 degree field of view without distortion in THREE.PerspectiveCamera

I am building a website running on THREE.js to generate a 3D world. From experience with video games, I know they usually use a camera field of view angle of about 90 degrees. When I set PerspectiveCamera in THREE.js to such a high FOV value, however, the scene is severely distorted. This distortion is somehow removed in games while preserving the large field of view. How is this done? Can I do this in THREE.js, too? Thanks!
This is how the camera is created:
new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
100,
10000000
);
The resulting image is this. See how the earth is stretched in the horizontal direction? That's what I am trying to get rid of.
In three.js, camera.fov is the vertical field-of-view in degrees.
The horizontal field-of-view is determined by the vertical field-of-view and the aspect ratio of the display image.
hFOV = 2 * Math.atan( Math.tan( camera.fov * Math.PI / 180 / 2 ) * camera.aspect ) * 180 / Math.PI; // degrees
A reasonable value for camera.fov is 40 to 50 degrees. This yields minimal distortion, and depending on the aspect ratio of the display, yields a horizontal FOV of 80 or 90 degrees.
In your example, you have specified a vertical FOV of 75 degrees, which implies a horizontal FOV of about 110 degrees.
three.js r.69
Based on WestLangley's awesome answer, here is how to get a fixed horizontal fov in three.js:
var horizontalFov = 90;
camera.fov = (Math.atan(Math.tan(((horizontalFov / 2) * Math.PI) / 180) / camera.aspect) * 2 * 180) / Math.PI;

Calculating frame and aspect ratio guides to match cameras

I'm trying to visualize film camera crop and aspect ratio in Three.js. Please bear with me, it's a math problem, and I can't describe it in lesser words...
Instead of just using CameraHelper, I'm using three slightly modified CameraHelper objects for each camera. The helper lines can be seen when looking at a camera (cone), or when looking through a camera, the helper lines effectively create guide lines for the current camera.
Frame helper (bluish one with sides rendered). This is configured and supposed to be what an actual camera sees considering it's focal length and sensor or film dimensions. Calculated in getFOVFrame.
Monitor helper (white). Our frame aspect ratio here is 1.5. For example, if we plan to do a 2.35 (cinemascope) aspect ratio film with a camera of aspect ratio 1.5, this shows the crop area of the frame. So it needs to exactly fit the frame, with extra space either up and down or at the sides, but not both. Calculated in getFOVMonitor.
Screen helper (purple). We want full thing visible in the browser, and if the browser window dimensions/aspect ratio is different, we adjust the actual rendered Three.js camera so that it fits into the browser window and dimensions. So this helper always has the aspect ratio of current browser window, and focal length so that it fits the frame and monitor helper. Calculated in getFOVScreen
So based on our actual preferred camera (frame helper), we need to calculate the monitor camera and adjust it's fov that it exactly fits inside frame camera. Then we also need to calculate the screen camera and adjust it's fov that the frame camera exactly fits inside.
My current solution appears almost correct, but there is something wrong. With long lenses (small fov, big focal length) it seems correct:
Looking through, looks correct:
Both the current camera, and the camera in front look about correct:
Looking through, looks correct:
But at wide lenses (big fov, small focal length) the solution starts to break, there is extra space around the white monitor helper, for example:
Looking through, the white box should touch the bluish one from the sides:
Both the current camera, and the camera in front look wrong, the white boxes should touch the sides of blue box (both have very wide lens):
Looking through (very wide lens), looks wrong, white box should touch blue box and blue box should touch purple box:
So I think I'm calculating the various cameras wrong, although the result seems almost "close enough".
Here's the code that returns the vertical FOV, horizontal HFOV and aspect ratio, which are then used to configure the cameras and helpers:
// BLUE camera fov, based on physical camera settings (sensor dimensions and focal length)
var getFOVFrame = function() {
var fov = 2 * Math.atan( sensor_height / ( focal_length * 2 ) ) * ( 180 / Math.PI );
return fov;
}
var getHFOVFrame = function() {
return getFOVFrame() * getAspectFrame();
}
// PURPLE screen fov, should be able to contain the frame
var getFOVScreen = function() {
var fov = getFOVFrame();
var hfov = fov * getAspectScreen();
if (hfov < getHFOVFrame()) {
hfov = getHFOVFrame();
fov = hfov / getAspectScreen();
}
return fov;
}
var getHFOVScreen = function() {
return getFOVScreen() * getAspectScreen();
}
// WHITE crop area fov, should fit inside blue frame camera
var getFOVMonitor = function() {
var fov = getFOVFrame();
var hfov = fov * getAspectMonitor();
if (hfov > getHFOVFrame()) {
hfov = getHFOVFrame();
fov = hfov / getAspectMonitor();
}
return fov;
}
var getHFOVMonitor = function() {
return getFOVMonitor() * getAspectMonitor();
}
var getAspectScreen = function() {
return screen_width / screen_height;
}
var getAspectFrame = function() {
return sensor_width / sensor_height;
}
var getAspectMonitor = function() {
return monitor_aspect;
}
Why does this produce incorrect results when using large FOV / wide lenses? getFOVScreen and especially getFOVMonitor are the suspects.
Your equation var hfov = fov * getAspectScreen(); is not correct.
The relationship between the vertical FOV (vFOV) and the horizontal FOV (hFOV) are given by the following equations:
hFOV = 2 * Math.atan( Math.tan( vFOV / 2 ) * aspectRatio );
and likewise,
vFOV = 2 * Math.atan( Math.tan( hFOV / 2 ) / aspectRatio );
In these equations, vFOV and hFOV are in radians; aspectRatio = width / height.
In three.js, the PerspectiveCamera.fov is the vertical one, and is in degrees.
three.js r.59

Resources