Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/api/en/cameras/PerspectiveCamera.html
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ <h3>[method:null updateProjectionMatrix]()</h3>
Updates the camera projection matrix. Must be called after any change of parameters.
</p>

<h3>[method:null frameCorners]( [param:Vector3 bottomLeftCorner], [param:Vector3 bottomRightCorner], [param:Vector3 topLeftCorner], [param:boolean estimateViewFrustum] )</h3>
<p>
Set this PerspectiveCamera's projectionMatrix and quaternion to exactly frame the corners of an arbitrary rectangle using [link:https://web.archive.org/web/20191110002841/http://csc.lsu.edu/~kooima/articles/genperspective/index.html Kooima's Generalized Perspective Projection formulation].
NOTE: This function ignores the standard parameters; do not call updateProjectionMatrix() after this! toJSON will also not capture the off-axis matrix generated by this function.
</p>

<h3>[method:Object toJSON]([param:Object meta])</h3>
<p>
meta -- object containing metadata such as textures or images in objects' descendants.<br />
Expand Down
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
"webgl_math_obb",
"webgl_math_orientation_transform",
"webgl_mirror",
"webgl_portal",
"webgl_modifier_curve",
"webgl_modifier_curve_instanced",
"webgl_modifier_edgesplit",
Expand Down
Binary file added examples/screenshots/webgl_portal.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/tags.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"webgl_math_obb": [ "intersection", "bounding" ],
"webgl_math_orientation_transform": [ "rotation" ],
"webgl_mirror": [ "reflection" ],
"webgl_portal": [ "portal", "frameCorners", "renderTarget" ],
"webgl_morphtargets_horse": [ "animation" ],
"webgl_multiple_elements": [ "differential equations", "physics" ],
"webgl_multiple_elements_text": [ "font" ],
Expand Down
252 changes: 252 additions & 0 deletions examples/webgl_portal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - portal</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
<style>
body {
color: #444;
}
a {
color: #08f;
}
</style>
</head>
<body>

<div id="container"></div>
<div id="info"><a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - portal
</div>

<script type="module">

import * as THREE from '../build/three.module.js';

import { OrbitControls } from './jsm/controls/OrbitControls.js';

let camera, scene, renderer;

let cameraControls;

let smallSphereOne, smallSphereTwo;

let portalCamera, leftPortal, rightPortal, leftPortalTexture, reflectedPosition,
rightPortalTexture, bottomLeftCorner, bottomRightCorner, topLeftCorner, frustumHelper;

init();
animate();

function init() {

const container = document.getElementById( 'container' );

// renderer
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
container.appendChild( renderer.domElement );
renderer.localClippingEnabled = true;

// scene
scene = new THREE.Scene();

// camera
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 5000 );
camera.position.set( 0, 75, 160 );

cameraControls = new OrbitControls( camera, renderer.domElement );
cameraControls.target.set( 0, 40, 0 );
cameraControls.maxDistance = 400;
cameraControls.minDistance = 10;
cameraControls.update();

//

const planeGeo = new THREE.PlaneGeometry( 100.1, 100.1 );

let geometry, material;
geometry = new THREE.CylinderGeometry( 0.1, 15 * Math.cos( Math.PI / 180 * 30 ), 0.1, 24, 1 );
material = new THREE.MeshPhongMaterial( { color: 0xffffff, emissive: 0x444444 } );

// bouncing icosphere
const portalPlane = new THREE.Plane( new THREE.Vector3( 0, 0, 1 ), 0.0 );
geometry = new THREE.IcosahedronGeometry( 5, 0 );
material = new THREE.MeshPhongMaterial( {
color: 0xffffff, emissive: 0x333333, flatShading: true,
clippingPlanes: [ portalPlane ], clipShadows: true } );
smallSphereOne = new THREE.Mesh( geometry, material );
scene.add( smallSphereOne );
smallSphereTwo = new THREE.Mesh( geometry, material );
scene.add( smallSphereTwo );

// portals
portalCamera = new THREE.PerspectiveCamera( 45, 1.0, 0.1, 500.0 );
scene.add( portalCamera );
//frustumHelper = new THREE.CameraHelper( portalCamera );
//scene.add( frustumHelper );
bottomLeftCorner = new THREE.Vector3();
bottomRightCorner = new THREE.Vector3();
topLeftCorner = new THREE.Vector3();
reflectedPosition = new THREE.Vector3();

leftPortalTexture = new THREE.WebGLRenderTarget( 256, 256, {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBFormat
} );
leftPortal = new THREE.Mesh( planeGeo, new THREE.MeshBasicMaterial( { map: leftPortalTexture.texture } ) );
leftPortal.position.x = - 30;
leftPortal.position.y = 20;
leftPortal.scale.set( 0.35, 0.35, 0.35 );
scene.add( leftPortal );

rightPortalTexture = new THREE.WebGLRenderTarget( 256, 256, {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBFormat
} );
rightPortal = new THREE.Mesh( planeGeo, new THREE.MeshBasicMaterial( { map: rightPortalTexture.texture } ) );
rightPortal.position.x = 30;
rightPortal.position.y = 20;
rightPortal.scale.set( 0.35, 0.35, 0.35 );
scene.add( rightPortal );

// walls
const planeTop = new THREE.Mesh( planeGeo, new THREE.MeshPhongMaterial( { color: 0xffffff } ) );
planeTop.position.y = 100;
planeTop.rotateX( Math.PI / 2 );
scene.add( planeTop );

const planeBottom = new THREE.Mesh( planeGeo, new THREE.MeshPhongMaterial( { color: 0xffffff } ) );
planeBottom.rotateX( - Math.PI / 2 );
scene.add( planeBottom );

const planeFront = new THREE.Mesh( planeGeo, new THREE.MeshPhongMaterial( { color: 0x7f7fff } ) );
planeFront.position.z = 50;
planeFront.position.y = 50;
planeFront.rotateY( Math.PI );
scene.add( planeFront );

const planeBack = new THREE.Mesh( planeGeo, new THREE.MeshPhongMaterial( { color: 0xff7fff } ) );
planeBack.position.z = - 50;
planeBack.position.y = 50;
//planeBack.rotateY( Math.PI );
scene.add( planeBack );

const planeRight = new THREE.Mesh( planeGeo, new THREE.MeshPhongMaterial( { color: 0x00ff00 } ) );
planeRight.position.x = 50;
planeRight.position.y = 50;
planeRight.rotateY( - Math.PI / 2 );
scene.add( planeRight );

const planeLeft = new THREE.Mesh( planeGeo, new THREE.MeshPhongMaterial( { color: 0xff0000 } ) );
planeLeft.position.x = - 50;
planeLeft.position.y = 50;
planeLeft.rotateY( Math.PI / 2 );
scene.add( planeLeft );

// lights
const mainLight = new THREE.PointLight( 0xcccccc, 1.5, 250 );
mainLight.position.y = 60;
scene.add( mainLight );

const greenLight = new THREE.PointLight( 0x00ff00, 0.25, 1000 );
greenLight.position.set( 550, 50, 0 );
scene.add( greenLight );

const redLight = new THREE.PointLight( 0xff0000, 0.25, 1000 );
redLight.position.set( - 550, 50, 0 );
scene.add( redLight );

const blueLight = new THREE.PointLight( 0x7f7fff, 0.25, 1000 );
blueLight.position.set( 0, 50, 550 );
scene.add( blueLight );

window.addEventListener( 'resize', onWindowResize, false );

}

function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

renderer.setSize( window.innerWidth, window.innerHeight );

}

function renderPortal( thisPortalMesh, otherPortalMesh, thisPortalTexture ) {

// set the portal camera position to be reflected about the portal plane
thisPortalMesh.worldToLocal( reflectedPosition.copy( camera.position ) );
reflectedPosition.x *= - 1.0; reflectedPosition.z *= - 1.0;
otherPortalMesh.localToWorld( reflectedPosition );
portalCamera.position.copy( reflectedPosition );

// grab the corners of the other portal
// - note: the portal is viewed backwards; flip the left/right coordinates
otherPortalMesh.localToWorld( bottomLeftCorner.set( 50.05, - 50.05, 0.0 ) );
otherPortalMesh.localToWorld( bottomRightCorner.set( - 50.05, - 50.05, 0.0 ) );
otherPortalMesh.localToWorld( topLeftCorner.set( 50.05, 50.05, 0.0 ) );
// set the projection matrix to encompass the portal's frame
portalCamera.frameCorners( bottomLeftCorner, bottomRightCorner, topLeftCorner, false );

// render the portal
thisPortalTexture.texture.encoding = renderer.outputEncoding;
renderer.setRenderTarget( thisPortalTexture );
renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897
if ( renderer.autoClear === false ) renderer.clear();
renderer.render( scene, portalCamera );

}

function animate() {

requestAnimationFrame( animate );

// move the bouncing sphere(s)
const timerOne = Date.now() * 0.01;
const timerTwo = timerOne + Math.PI * 10.0;

smallSphereOne.position.set(
Math.cos( timerOne * 0.1 ) * 30,
Math.abs( Math.cos( timerOne * 0.2 ) ) * 20 + 5,
Math.sin( timerOne * 0.1 ) * 30
);
smallSphereOne.rotation.y = ( Math.PI / 2 ) - timerOne * 0.1;
smallSphereOne.rotation.z = timerOne * 0.8;

smallSphereTwo.position.set(
Math.cos( timerTwo * 0.1 ) * 30,
Math.abs( Math.cos( timerTwo * 0.2 ) ) * 20 + 5,
Math.sin( timerTwo * 0.1 ) * 30
);
smallSphereTwo.rotation.y = ( Math.PI / 2 ) - timerTwo * 0.1;
smallSphereTwo.rotation.z = timerTwo * 0.8;

// save the original camera properties
let currentRenderTarget = renderer.getRenderTarget();
let currentXrEnabled = renderer.xr.enabled;
let currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
renderer.xr.enabled = false; // Avoid camera modification
renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows

// render the portal effect
renderPortal( leftPortal, rightPortal, leftPortalTexture );
renderPortal( rightPortal, leftPortal, rightPortalTexture );

// restore the original rendering properties
renderer.xr.enabled = currentXrEnabled;
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
renderer.setRenderTarget( currentRenderTarget );

// render the main scene
renderer.render( scene, camera );

}

</script>
</body>
</html>
67 changes: 67 additions & 0 deletions src/cameras/PerspectiveCamera.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Camera } from './Camera.js';
import { Vector3 } from '../math/Vector3.js';
import { Quaternion } from '../math/Quaternion.js';
import * as MathUtils from '../math/MathUtils.js';

class PerspectiveCamera extends Camera {
Expand Down Expand Up @@ -204,6 +206,62 @@ class PerspectiveCamera extends Camera {

}

/** Set this PerspectiveCamera's projectionMatrix and quaternion
* to exactly frame the corners of an arbitrary rectangle.
* NOTE: This function ignores the standard parameters;
* do not call updateProjectionMatrix() after this!
* @param {Vector3} bottomLeftCorner
* @param {Vector3} bottomRightCorner
* @param {Vector3} topLeftCorner
* @param {boolean} estimateViewFrustum */
frameCorners( bottomLeftCorner, bottomRightCorner, topLeftCorner, estimateViewFrustum = false ) {

const pa = bottomLeftCorner, pb = bottomRightCorner, pc = topLeftCorner;
const pe = this.position; // eye position
const n = this.near; // distance of near clipping plane
const f = this.far; //distance of far clipping plane

_vr.copy( pb ).sub( pa ).normalize();
_vu.copy( pc ).sub( pa ).normalize();
_vn.crossVectors( _vr, _vu ).normalize();

_va.copy( pa ).sub( pe ); // from pe to pa
_vb.copy( pb ).sub( pe ); // from pe to pb
_vc.copy( pc ).sub( pe ); // from pe to pc

const d = - _va.dot( _vn ); // distance from eye to screen
const l = _vr.dot( _va ) * n / d; // distance to left screen edge
const r = _vr.dot( _vb ) * n / d; // distance to right screen edge
const b = _vu.dot( _va ) * n / d; // distance to bottom screen edge
const t = _vu.dot( _vc ) * n / d; // distance to top screen edge

// Set the camera rotation to match the focal plane to the corners' plane
_quat.setFromUnitVectors( _vec.set( 0, 1, 0 ), _vu );
this.quaternion.setFromUnitVectors( _vec.set( 0, 0, 1 ).applyQuaternion( _quat ), _vn ).multiply( _quat );

// Set the off-axis projection matrix to match the corners
this.projectionMatrix.set( 2.0 * n / ( r - l ), 0.0,
( r + l ) / ( r - l ), 0.0, 0.0,
2.0 * n / ( t - b ),
( t + b ) / ( t - b ), 0.0, 0.0, 0.0,
( f + n ) / ( n - f ),
2.0 * f * n / ( n - f ), 0.0, 0.0, - 1.0, 0.0 );
this.projectionMatrixInverse.copy( this.projectionMatrix ).invert();

// FoV estimation to fix frustum culling
if ( estimateViewFrustum ) {

// Set fieldOfView to a conservative estimate
// to make frustum tall/wide enough to encompass it
this.fov =
MathUtils.RAD2DEG / Math.min( 1.0, this.aspect ) *
Math.atan( ( _vec.copy( pb ).sub( pa ).length() +
( _vec.copy( pc ).sub( pa ).length() ) ) / _va.length() );

}

}

toJSON( meta ) {

const data = super.toJSON( meta );
Expand All @@ -230,4 +288,13 @@ class PerspectiveCamera extends Camera {

PerspectiveCamera.prototype.isPerspectiveCamera = true;

const _va = /*@__PURE__*/ new Vector3(), // from pe to pa
_vb = /*@__PURE__*/ new Vector3(), // from pe to pb
_vc = /*@__PURE__*/ new Vector3(), // from pe to pc
_vr = /*@__PURE__*/ new Vector3(), // right axis of screen
_vu = /*@__PURE__*/ new Vector3(), // up axis of screen
_vn = /*@__PURE__*/ new Vector3(), // normal vector of screen
_vec = /*@__PURE__*/ new Vector3(), // temporary vector
_quat = /*@__PURE__*/ new Quaternion(); // temporary quaternion

export { PerspectiveCamera };