Fit Object3D inside Variable Width Canvas Three.js - 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.

Related

How to accelerate calculations when update messive position from 3d to screen (hud)

I want to update hud positon form 3d position to 2d when mouse moving. Since it may have a large number of 3d objects to project to the screen position, I meet a performance problem.
Are there any way to accelerate calculations? The following is how I calculate 3d object position on 2d screen.
function toScreenPosition(obj) {
var vector = new THREE.Vector3();
//calculate screen half size
var widthHalf = 0.5 * renderer.context.canvas.width;
var heightHalf = 0.5 * renderer.context.canvas.height;
//get 3d object position
obj.updateMatrixWorld();
vector.setFromMatrixPosition(obj.matrixWorld);
vector.project(this.camera);
//get 2d position on screen
vector.x = (vector.x * widthHalf) + widthHalf;
vector.y = -(vector.y * heightHalf) + heightHalf;
return {
x: vector.x,
y: vector.y
};
}
Rather than repositioning your HUD in world space every time your camera moves, add your HUD object(s) to your camera object, and position them only once. Then, when your camera moves, your HUD moves along with it, because the camera's transformation is cascaded to it's children.
yourCamera.add(yourHUD);
yourHUD.position.z = 10;
Note that doing it this way (or even positioning it the way you were) may allow scene objects to clip through your HUD geometry, or even appear between your HUD and the camera, obscuring the HUD. If that's what you want, great! If not, you could move your HUD to a second render pass, allowing it to remain "on top."
First, here is an example of your function rewritten for (almost) optimal performance as written in the comments above, the renderloop is obviously just an example to illustrate where to do which calls:
var width = renderer.context.canvas.width;
var height = renderer.context.canvas.height;
// has to be called whenever the canvas-size changes
function onCanvasResize() {
width = renderer.context.canvas.width;
height = renderer.context.canvas.height;
});
var projMatrix = new THREE.Matrix4();
// renderloop-function, called per animation-frame
function render() {
// just needed once per frame (even better would be
// once per camera-movement)
projMatrix.multiplyMatrices(
camera.projectionMatrix,
projMatrix.getInverse(camera.matrixWorld)
);
hudObjects.forEach(function(obj) {
toScreenPosition(obj, projMatrix);
});
}
// wrapped in IIFE to store the local vector-variable (this pattern
// is used everywhere in three.js)
var toScreenPosition = (function() {
var vector = new THREE.Vector3();
return function __toScreenPosition(obj, projectionMatrix) {
// this could potentially be left away, but isn't too
// expensive as there are 'needsUpdate'-checks in place
obj.updateMatrixWorld();
vector.setFromMatrixPosition(obj.matrixWorld);
vector.applyMatrix4(projectionMatrix);
vector.x = (vector.x + 1) * width / 2;
vector.y = (1 - vector.y) * height / 2;
// might want to consider returning a Vector3-instance
// instead, depends on how the result is used
return {x: vector.x, y: vector.y};
}
}) ();
But, considering you want to render a HUD, it would be better to do that independently of the main-scene, making all of the above computations obsolete and also allowing you to choose a different coordinate-system for sizing and positioning of HUD-elements.
I have an example for this here: https://codepen.io/usefulthink/pen/ZKPvPB. There I used an orthographic camera and a seperate scene to render HUD-Elements on top of the 3d-scene. No extra computations required. Plus I can specify the size and position of HUD-elements conveniently in pixel-units (The same would work using a perspective camera, only requires a bit more trigonometry to get it right).

Nearby culling in Three.js despite camera not being near face

I've run into an issue after switching to a logarithmic depth buffer in Three.js. Everything runs nicely except for nearby culling of the ground as described in the following photos:
As you can see, the camera is elevated above the ground significantly. The character box that is shown is about 2 units above the ground, and my camera is set up as such:
var WIDTH = window.innerWidth
, HEIGHT = window.innerHeight;
var VIEW_ANGLE = 70
, ASPECT = WIDTH / HEIGHT
, NEAR = 1e-6
, FAR = 9000;
var aspect = WIDTH / HEIGHT;
var camera = new THREE.PerspectiveCamera(VIEW_ANGLE, ASPECT, NEAR, FAR);
camera.rotation.order = 'YXZ';
So my NEAR parameter is nowhere near 2, the distance between the camera and the ground. You can see in the second image that I even move up the camera with my PointerLockControls and still run into the issue.
Can anyone diagnose my issue?
I also tested my issue by seeing if this bug occurred with a static camera as well. It does.
Additionally, this problem only happens with the logarithmic depth buffer, as it doesn't happen with the default depth buffer.
I have my camera as a child to a controls object, which is defined as follows:
controls = new THREE.PointerLockControls(camera);
controls.getObject().position.set(strtx, 50, strtz);
scene.add(controls.getObject());
camera.position.z += 2;
camera.position.y += .1;
Here's the relevant code for PointerLockControls:
var pitchObject, yawObject;
var v = new THREE.Vector3(0, 0, -1);
THREE.PointerLockControls = function(camera){
var scope = this;
camera.rotation.set(0, 0, 0);
pitchObject = new THREE.Object3D();
pitchObject.rotation.x -= 0.3;
pitchObject.add(camera);
yawObject = new THREE.Object3D();
yawObject.position.y = 10;
yawObject.add(pitchObject);
var PI_2 = Math.PI / 2;
var onMouseMove = function(event){
if (scope.enabled === false) return;
var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
yawObject.rotation.y -= movementX * 0.002;
pitchObject.rotation.x -= movementY * 0.002;
pitchObject.rotation.x = Math.max( - PI_2, Math.min( PI_2, pitchObject.rotation.x ) );
};
this.dispose = function() {
document.removeEventListener( 'mousemove', onMouseMove, false );
};
document.addEventListener( 'mousemove', onMouseMove, false );
this.enabled = false;
this.getObject = function () {
return yawObject;
};
this.getDirection = function() {
// assumes the camera itself is not rotated
var rotation = new THREE.Euler(0, 0, 0, "YXZ");
var direction = new THREE.Vector3(0, 0, -1);
return function() {
rotation.set(pitchObject.rotation.x, yawObject.rotation.y, 0);
v.copy(direction).applyEuler(rotation);
return v;
};
}();
};
You'll also notice that it's only the ground that is being culled, not other objects
Edit:
I've whipped up an isolated environment that shows the larger issue. In the first image, I have a flat PlaneBufferGeometry that has 400 segments for both width and height, defined by var g = new THREE.PlaneBufferGeometry(380, 380, 400, 400);. Even getting very close to the surface, no clipping is present:
However, if I provide only 1 segment, var g = new THREE.PlaneBufferGeometry(380, 380, 1, 1);, the clipping is present
I'm not sure if this intended in Three.js/WebGL, but it seems that I'll need to do something to work around it.
I don't think this is a bug, I think this is a feature of how the depthbuffer in the different settings works. Look at this example. On the right, the depthbuffer can't make up its mind between the letters in "microscopic" and the sphere. This is because it has lower precision at very small scales and starts doing rounding that oscilates between one object and another, and favoring draw order over z-depth.
It's always a tradeoff. If you want to forgo this issue, you can try raising the scale of your scene overall, so that the 'near' of the camera will never be so close to something that it can round it off - so just work in a number range that won't be rounded in the exponential model of the logarithmic z-buffer.
Also another question - how is the blue defined, because maybe what you're seeing is not clipping from being too close, but confusion between whether blue or the ground is closer. If it's just a blue box encompassing everything, you could try making it bigger and more distant from the ground.
EDIT:
Okay, this looks like it should work. so I would start looking for edge cases. What can you do to change the scene so that it does work? What can you do to make other things start breaking?
try moving the landscape far down/ far up (does the issue persist when looking up instead of down at it, does it persist even when it's unquestionably far away?)
try rotating the landscape
try changing the camera FOV
try changing the camera far plane
try changing the camera near plane from 1e-x notation to .000001, .0001,.01,.1, etc. see what effect it has.
console.log the camera object in your render function, and make sure that the fov, near, far etc, is as you set on setup and that it's not being overwritten and reset to default. check what it prints out in chrome's developer tools, you can browse the whole object, check position, parent name, all that stuff.
basically i don't see a blatant mistake, so I would guess it's something hard to spot, or it's working exactly as it should. Figure out what you can do to improve the effect/ make it worse, and that will clarify a direction to go.
A good rule of thumb for debugging is to try and just take things to an extreme, without trying to fix it, or keep the code true to its purpose, and just see in what way it breaks further/changes. report back when you find something.

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 );

THREE.js Orthographic camera aspect must match viewport aspect?

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.

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