THREE.js Orthographic camera aspect must match viewport aspect? - three.js

Please could someone confirm that the following "supposed constraint" is correct?...
In order to render a three.js orthographic camera to a viewport (and to avoid distortion) the camera's frustum left,right,bottom and top planes must define a frontal frustum face (ocWidth, ocHeight) whose aspect ratio (width/height) is the same as the aspect ratio of the viewport?
In the following example the camera width and height are set first and then the viewport height is constrained by the desired viewport width and the given camera aspect ratio. (An alternative approach would be to set the viewport width and height first and then constrain the camera height to the desired camera width and the given viewport aspect ratio.)
//Orthographic Camera
ocWidth = 99000; //... World Units
ocHeight = 33000; //... World Units
var myCamera = new THREE.OrthographicCamera(
ocWidth / - 2, ocWidth / 2,
ocHeight / 2, ocHeight / - 2,
NEAR = 1, FAR = 1000 );
oc_aspect_ratio = ocWidth / ocHeight;
//Viewport
vp_aspect_ratio = oc_aspect_ratio;
vpXwidth = 800; //... pixels
vpYheight = vpXwidth /vp_aspect_ratio; //... pixels, to ensure no distortion
vpXmin = -vpXwidth /2; vpXmax = vpXwidth /2; //... pixels
vpYmin = -vpYheight /2; vpYmax = vpYheight /2; //... pixels
myRenderer.setViewport( vpXmin, vpYmin, vpXwidth, vpYheight );
Thus (in general) the width and height of the RENDERER are irrelevant as far as the orthographic camera is concerned (The exception is when the effective viewport fills the entire renderer, which is the default if no viewport is explicitly defined. In this case the renderer aspect ratio must match the camera aspect ratio).
I have studied
this documentation
this example
this SO question
which are all helpful but do not explicitly confirm the supposed constraint.

If you are rendering your scene with an orthographic camera, and you wish to prevent distortion of the rendered scene, you need to set your viewport aspect ratio to match the aspect ratio of the camera's frustum.

Related

Map Texture to Plane without distortion

In Three.js, how can I change the way in which a texture gets mapped onto a plane?
Let's assume we have a 1x1 plane and a 16:9 image. How can I control the way in which that image gets mapped onto the plane?
By default, the image gets "squished". I would like it to maintain its aspect ratio and have any overlap get "cut off". Is there a way to configure the material or texture to do this, or would I use a shader? If so, what would it need to look like?
const planeMesh = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1, 1),
new THREE.MeshBasicMaterial({
map: texture,
})
);
PS: In future, I would also like to be able to zoom into and out of the image on mouse hover without affecting the size of the plane, so would think a shader might be better?
A Texture already has several properties built-in that can do what you're looking for.
const texture = textureLoader.load("whatever.png");
const planeMesh = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1, 1),
new THREE.MeshBasicMaterial({
map: texture,
})
);
// Sets the pivot point to the center of the texture
texture.center.set(0.5, 0.5);
// Make the texture repeat 0.5625 times in the x-axis to match 16:9 ratio
let ratio = 9 / 16;
texture.repeat.set(ratio, 1);
// Scale texture up to "zoom" into it
let zoom = 0.5;
texture.repeat.set(ratio * zoom, 1 * zoom);
You can read more about the .repeat .center and even .rotation properties in the Texture docs. Just keep in mind that repeating a texture is a bit counter-intuitive because you're doing the inverse of scaling a texture. So to scale a texture by 2, you have to tell it to repeat 1/2 times.

can we show geometries that are out of canvas (3D space) in three.js

I am creating one geometry at location (0,0,0) but projecting at some other location (for ex. #50,50,50). If the point (0,0,0) is going out of canvas, then geometry is hiding.
Is there any way to always render it on canvas?
How far off the edge does the origin need to be?
You could make the canvas larger than you need, than mask the areas on the edge such that only the center area shows. That way when the origin goes off the side, it will still technically be on the canvas, and the projected geometry will be in the visible area. I expect you will only need a buffer space equal to projection offset.
See here for an example of applying a mask: https://jsfiddle.net/shawnoakley/n1368qr0/2/
Example code:
var context = document.getElementById('canvas').getContext('2d');
// Mask color
context.fillStyle = '#000';
// Image proportions
context.fillRect(0,0,600,400);
var unmaskedImage = function(x, y, radius){
context.save();
context.globalCompositeOperation = 'destination-out';
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, false);
context.fill();
context.restore();
};
unmaskedImage(300, 300, 300);

Fit Object3D inside Variable Width Canvas Three.js

I have an object3D of width 100 in my scene centred at the origin. The camera has an FOV of 50 and I would like this to remain constant. I am currently positioning the camera with
var camDistance = (100/2)/Math.tan(50/2 * Math.PI/180);
var camHeight = camDistance * (6/25);
camera.position.set(0,camHeight,camDistance);
camera.lookAt(0,0,0);
This is looks good for larger displays but on mobile the object extends past the edges of the screen. I want to vary the distance from the camera to the object so that the object always occupies the same percentage of the screen horizontally, no matter what size viewport it is loaded on. What I thought should work is
var camDistance = (100/2)/Math.tan(50/2 * Math.PI/180) * (1700/window.innerWidth);
Since the object occupies about 1700px with this fov. This sort of works except the object is now too far away on very small screen widths and too close on very large screen widths.
Is there a way to actually make the object occupy the same horizontal percentage of the viewport instead of the poor approximation that I have come up with? Preferably a solution that avoids the magical-ness of 1700px.
So if I understand you correctly, you are only interested in fitting the width, not the height, of the object and screen.
It would have helped if you had added a HTML snippet of the problem in question, so I could try solutions for your application, but this is what I could come up with:
let dz = objectWidth/(2 * Math.tan(camera.fov/2) * camera.aspect);
camera.position.set(0, camHeight, margin + dz);
Here, margin is some z value that you can specify. You also need to make sure that camera.aspect corresponds to the actual aspect ratio of the window (below is how I would dynamically update it for a fullscreen application):
function onResize() {
let width = window.innerWidth;
let height = window.innerHeight;
camera.aspect = width / height;
renderer.setSize(width, height);
camera.updateProjectionMatrix();
}
This works in a sandbox I set up for myself, but please let me know if it can be applied to your application too or if there is something I haven't taken into account.

LIBGDX / OpenGL : Reducing the size of everything

This could be the worse question ever asked however that would be a cool achievement.
I have created a 3D world made of cubes that are 1x1x1 (think Minecraft), all the maths works great etc. However 1x1x1 nearly fills the whole screen (viewable area)
Is there a way I can change the ViewPort or something so that 1x1x1 is half the size it currently is?
Code for setting up camera
float aspectRatio = Gdx.graphics.getWidth() / Gdx.graphics.getHeight();
camera = new PerspectiveCamera(67, 1.0f * aspectRatio, 1.0f);
camera.near = 0.1f; // 0.5 //todo find out what this is again
camera.far = 1000;
fps = new ControlsController(camera , this, stage);
I am using the FirstPersonCameraController and PerspectiveCamera to try and make a first person game
I guess the problem is:
camera = new PerspectiveCamera(67, 1.0f * aspectRatio, 1.0f);
An standard initialization of your camera could be (based on this tutorial):
camera = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
// ...
Note how the width and height of the camera is nearly (if not the same) of the width and height of the native gdx window dimension. In your case you set this size to 1 (the same size of your mesh). Try with a bigger viewport dimension to allow your mesh be smaller (in perspective), something like:
/** Not too sure since is a perspective view, but play with this values **/
float multiplier = 2; // <- to allow your mesh be a fraction
// of the size of the viewport of the camera
camera = new PerspectiveCamera(67, multiplier * aspectRatio, multiplier );

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