While this answer may not cover everything, the basic idea is there for a function similar to glFrustum
. The math is solid, but the presentation could use some improvement. Fortunately, Three.js supports this concept through Camera.setViewOffset
.
To utilize Camera.setViewOffset
, you define a larger view and specify which portion of the canvas represents that view. In this scenario, we're focusing on adjusting the smaller view without concerning ourselves with a larger perspective.
In order to align with the user's eye position, you'll need to select a central area within the broader view and adjust the camera accordingly.
The calculation detailing how much to move each element might seem daunting at first glance, but it seems like they should be equal — the only difference being that the camera moves in world units while setViewOffset
operates in pixels. Given that CSS pixels equate to 96 per inch, moving the eye inches results in an opposing shift offsetting by 96 * distance moved.
Furthermore, understanding the user's eye-screen distance along with the screen size enables us to compute an appropriate field of view based on the setup (e.g., a 60-inch TV with the viewer standing 3 feet away versus someone facing an 11-inch notebook from a foot apart).
let mouse = new THREE.Vector2(), INTERSECTED;
let radius = 100, theta = 0;
const camera = new THREE.PerspectiveCamera(70, 1, 1, 10000);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set( 1, 1, 1 ).normalize();
scene.add( light );
const geometry = new THREE.BoxBufferGeometry(1, 1, 1);
for (let z = 0; z < 50; ++z) {
for (let y = -1; y <= 1; ++y) {
for (let x = -1; x <= 1; ++x) {
if (x === 0 && y === 0) {
continue;
}
const object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff}));
object.position.x = x * 1.5;
object position.y = y * 1.5;
object.position.z = -z * 2.5;
scene.add(object);
}
}
}
renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});
resize(true);
document.addEventListener( 'mousemove', onDocumentMouseMove, false );
requestAnimationFrame(render);
function resize(force) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
if (force || canvas.width !== width || canvas.height !== height) {
renderer.setSize(width, height, true);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
}
function onDocumentMouseMove(event) {
event.preventDefault();
const canvas = renderer.domElement;
// compute mouse movement in inches
mouse.x = (event.clientX - canvas.clientWidth / 2) / 96;
mouse.y = (event.clientY - canvas.clientHeight / 2) / 96;
}
function render(time) {
const worldUnits = 1;
const pixelUnits = worldUnits * 96;
const canvas = renderer.domElement;
const fullWidth = canvas.clientWidth;
const fullHeight = canvas.clientHeight;
const aspect = fullWidth / fullHeight;
const eyeOffsetX = mouse.x;
const eyeOffsetY = mouse.y;
const eyeDistFromScreenInches = 12;
const halfHeightOfScreenInches = fullHeight / 96 * .5;
const halfFov = Math.atan2(halfHeightOfScreenInches, eyeDistFromScreenInches) * 180 / Math.PI;
const width = fullWidth;
const height = fullHeight;
const centerX = fullWidth / 2;
const centerY = fullHeight / 2;
const left = centerX - width / 2 - eyeOffsetX * pixelUnits;
const top = centerY - height / 2 - eyeOffsetY * pixelUnits;
camera.fov = halfFov * 2;
camera.position.x = eyeOffsetX * worldUnits;
camera.position.y = -eyeOffsetY * worldUnits;
camera.setViewOffset(fullWidth, fullHeight, left, top, width, height);
camera.updateProjectionMatrix();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.min.js"></script>
<canvas></canvas>