فهرست منبع

Merge pull request #86 from NASA-AMMOS/offscreen-shadows

Add offscreen shadows
Garrett Johnson 5 سال پیش
والد
کامیت
e289a9c3bf

+ 2 - 0
CHANGELOG.md

@@ -6,7 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 
 ## Unreleased
 ### Added
+
 - Basic support for CMPT, PNTS, and I3DM file formats.
+- `autoDisableRendererCulling` field to `TilesRenderer`.
 
 ## [0.1.2] - 2020-06-08
 ### Changed

+ 20 - 1
README.md

@@ -14,6 +14,8 @@ Three.js renderer implementation for the [3D Tiles format](https://github.com/An
 
 [Custom material example here](https://nasa-ammos.github.io/3DTilesRendererJS/example/bundle/customMaterial.html)!
 
+[Rendering shadows from offscreen tiles example here](https://nasa-ammos.github.io/3DTilesRendererJS/example/bundle/offscreenShadows.html)!
+
 # Use
 
 ## Installation
@@ -74,7 +76,6 @@ tilesRenderer.onLoadModel = function ( scene ) {
 
 }
 scene.add( tilesRenderer.group );
-
 ```
 
 # API
@@ -117,6 +118,24 @@ loadSiblings = true : Boolean
 
 If true then all sibling tiles will be loaded, as well, to ensure coherence when moving the camera. If false then only currently viewed tiles will be loaded.
 
+### .displayActiveTiles
+
+```js
+displayActiveTiles = false : Boolean
+```
+
+"Active tiles" are those that are loaded and available but not necessarily visible. If [loadSiblings](#loadSiblings) is true then the tiles loaded up to the extents of the tileset will be considered active even outside the camera view. These tiles are useful for raycasting off camera or for casting shadows.
+
+Active tiles not currently visible in a camera frustum are removed from the scene as an optimization. Setting `displayActiveTiles` to true will keep them in the scene to be rendered from an outside camera view not accounted for by the tiles renderer.
+
+### .autoDisableRendererCulling
+
+```js
+autoDisableRendererCulling = true : Boolean
+```
+
+If true then all tile meshes automatically have their [frustumCulled](https://threejs.org/docs/index.html#api/en/core/Object3D.frustumCulled) field set to false. This is useful particularly when using one camera because the tiles renderer automatically performs it's own frustum culling on visible tiles. If [displayActiveTiles](#displayActiveTiles) is true or multiple cameras are being used then you may consider setting this to false.
+
 ### .lruCache
 
 ```js

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 46124 - 0
example/bundle/offscreenShadows.ce0529e7.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
example/bundle/offscreenShadows.ce0529e7.js.map


+ 28 - 0
example/bundle/offscreenShadows.html

@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+        <meta charset="utf-8">
+
+        <title>3D Tiles Renderer Material Example</title>
+
+        <style>* {
+                margin: 0;
+                padding: 0;
+            }
+
+            html {
+                overflow: hidden;
+                font-family: Arial, Helvetica, sans-serif;
+                user-select: none;
+            }
+
+            canvas {
+                image-rendering: pixelated;
+                outline: none;
+            }</style>
+    </head>
+    <body>
+        <script src="offscreenShadows.ce0529e7.js"></script>
+    </body>
+</html>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 392 - 0
example/bundle/offscreenShadows.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
example/bundle/offscreenShadows.js.map


+ 30 - 0
example/offscreenShadows.html

@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+        <meta charset="utf-8"/>
+
+        <title>3D Tiles Renderer Material Example</title>
+
+        <style>
+            * {
+                margin: 0;
+                padding: 0;
+            }
+
+            html {
+                overflow: hidden;
+                font-family: Arial, Helvetica, sans-serif;
+                user-select: none;
+            }
+
+            canvas {
+                image-rendering: pixelated;
+                outline: none;
+            }
+        </style>
+    </head>
+    <body>
+        <script src="./offscreenShadows.js"></script>
+    </body>
+</html>

+ 231 - 0
example/offscreenShadows.js

@@ -0,0 +1,231 @@
+import { TilesRenderer } from '../src/index.js';
+import {
+	Scene,
+	DirectionalLight,
+	AmbientLight,
+	WebGLRenderer,
+	PerspectiveCamera,
+	Box3,
+	OrthographicCamera,
+	sRGBEncoding,
+	Group,
+	MeshStandardMaterial,
+	PCFSoftShadowMap,
+} from 'three';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import * as dat from 'three/examples/jsm/libs/dat.gui.module.js';
+import Stats from 'three/examples/jsm/libs/stats.module.js';
+
+let camera, controls, scene, renderer, tiles, orthoCamera;
+let offsetParent, box, dirLight;
+let stats;
+
+const NONE = 0;
+const DISPLAY_ACTIVE_TILES = 1;
+const USE_SHADOW_CAMERA = 2;
+const params = {
+
+	'errorTarget': 2,
+	'shadowStrategy': NONE,
+	'orthographic': false,
+
+};
+
+init();
+animate();
+
+function onLoadModel( scene ) {
+
+	scene.traverse( c => {
+
+		if ( c.isMesh ) {
+
+			c.material = new MeshStandardMaterial();
+			c.castShadow = true;
+			c.receiveShadow = true;
+
+		}
+
+	} );
+
+}
+
+function init() {
+
+	scene = new Scene();
+
+	// primary camera view
+	renderer = new WebGLRenderer( { antialias: true } );
+	renderer.setPixelRatio( window.devicePixelRatio );
+	renderer.setSize( window.innerWidth, window.innerHeight );
+	renderer.setClearColor( 0x151c1f );
+	renderer.shadowMap.enabled = true;
+	renderer.shadowMap.type = PCFSoftShadowMap;
+	renderer.outputEncoding = sRGBEncoding;
+
+	document.body.appendChild( renderer.domElement );
+
+	camera = new PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 4000 );
+	camera.position.set( -21, 13, 25 );
+
+	orthoCamera = new OrthographicCamera();
+
+	// controls
+	controls = new OrbitControls( camera, renderer.domElement );
+	controls.screenSpacePanning = false;
+	controls.minDistance = 1;
+	controls.maxDistance = 2000;
+
+	// lights
+	dirLight = new DirectionalLight( 0xffffff, 1.25 );
+	dirLight.position.set( - 100, 40, 10 );
+	dirLight.castShadow = true;
+	dirLight.shadow.bias = - 0.00001;
+	dirLight.shadow.mapSize.setScalar( 2048 );
+
+	const shadowCam = dirLight.shadow.camera;
+	shadowCam.left = - 100;
+	shadowCam.bottom = - 100;
+	shadowCam.right = 100;
+	shadowCam.top = 100;
+	shadowCam.updateProjectionMatrix();
+
+	scene.add( dirLight );
+
+	const ambLight = new AmbientLight( 0xffffff, 0.05 );
+	scene.add( ambLight );
+
+	box = new Box3();
+
+	offsetParent = new Group();
+	scene.add( offsetParent );
+
+	// tiles
+	const url = window.location.hash.replace( /^#/, '' ) || '../data/tileset.json';
+	tiles = new TilesRenderer( url );
+	tiles.onLoadModel = onLoadModel;
+	offsetParent.add( tiles.group );
+
+
+	onWindowResize();
+	window.addEventListener( 'resize', onWindowResize, false );
+
+	// GUI
+	const gui = new dat.GUI();
+	gui.width = 300;
+	gui.add( params, 'orthographic' );
+	gui.add( params, 'errorTarget' ).min( 0 ).max( 25 ).step( 1 );
+	gui.add( params, 'shadowStrategy', { NONE, DISPLAY_ACTIVE_TILES, USE_SHADOW_CAMERA } );
+	gui.open();
+
+	// Stats
+	stats = new Stats();
+	stats.showPanel( 0 );
+	document.body.appendChild( stats.dom );
+
+}
+
+function onWindowResize() {
+
+	camera.aspect = window.innerWidth / window.innerHeight;
+	renderer.setPixelRatio( window.devicePixelRatio );
+	renderer.setSize( window.innerWidth, window.innerHeight );
+	camera.updateProjectionMatrix();
+
+	updateOrthoCamera();
+
+}
+
+function updateOrthoCamera() {
+
+	orthoCamera.position.copy( camera.position );
+	orthoCamera.rotation.copy( camera.rotation );
+
+	const scale = camera.position.distanceTo( controls.target ) / 2.0;
+	let aspect = window.innerWidth / window.innerHeight;
+	orthoCamera.left = - aspect * scale;
+	orthoCamera.right = aspect * scale;
+	orthoCamera.bottom = - scale;
+	orthoCamera.top = scale;
+	orthoCamera.near = camera.near;
+	orthoCamera.far = camera.far;
+	orthoCamera.updateProjectionMatrix();
+
+}
+
+function animate() {
+
+	requestAnimationFrame( animate );
+
+	tiles.errorTarget = params.errorTarget;
+	switch( parseFloat( params.shadowStrategy ) ) {
+
+		case NONE:
+			tiles.displayActiveTiles = false;
+			tiles.autoDisableRendererCulling = true;
+			tiles.deleteCamera( dirLight.shadow.camera );
+			break;
+
+		case DISPLAY_ACTIVE_TILES:
+			tiles.displayActiveTiles = true;
+			tiles.autoDisableRendererCulling = false;
+			tiles.deleteCamera( dirLight.shadow.camera );
+			break;
+
+		case USE_SHADOW_CAMERA:
+			tiles.displayActiveTiles = false;
+			tiles.autoDisableRendererCulling = false;
+			tiles.setCamera( dirLight.shadow.camera );
+			tiles.setResolution( dirLight.shadow.camera, dirLight.shadow.mapSize );
+			break;
+
+	}
+
+	if ( params.orthographic ) {
+
+		tiles.deleteCamera( camera );
+		tiles.setCamera( orthoCamera );
+		tiles.setResolutionFromRenderer( orthoCamera, renderer );
+
+	} else {
+
+		tiles.deleteCamera( orthoCamera );
+		tiles.setCamera( camera );
+		tiles.setResolutionFromRenderer( camera, renderer );
+
+	}
+
+	offsetParent.rotation.set( 0, 0, 0 );
+	if ( params.up === '-Z' ) {
+
+		offsetParent.rotation.x = Math.PI / 2;
+
+	}
+	offsetParent.updateMatrixWorld( true );
+
+	// update tiles center
+	if ( tiles.getBounds( box ) ) {
+
+		box.getCenter( tiles.group.position );
+		tiles.group.position.multiplyScalar( - 1 );
+
+	}
+
+	// update tiles
+	window.tiles = tiles;
+	camera.updateMatrixWorld();
+	orthoCamera.updateMatrixWorld();
+	tiles.update();
+
+	render();
+	stats.update();
+
+}
+
+function render() {
+
+	updateOrthoCamera();
+
+	renderer.render( scene, params.orthographic ? orthoCamera : camera );
+
+}

+ 2 - 0
src/three/TilesRenderer.d.ts

@@ -4,6 +4,8 @@ import { TilesGroup } from './TilesGroup';
 
 export class TilesRenderer extends TilesRendererBase {
 
+	autoDisableRendererCulling : Boolean;
+
 	group : TilesGroup;
 
 	getBounds( box : Box3 ) : Boolean;

+ 43 - 1
src/three/TilesRenderer.js

@@ -19,6 +19,7 @@ import {
 } from 'three';
 import { raycastTraverse, raycastTraverseFirstHit } from './raycastTraverse.js';
 
+const INITIAL_FRUSTUM_CULLED = Symbol( 'INITIAL_FRUSTUM_CULLED' );
 const DEG2RAD = MathUtils.DEG2RAD;
 const tempMat = new Matrix4();
 const tempMat2 = new Matrix4();
@@ -33,8 +34,43 @@ const useImageBitmap = typeof createImageBitmap !== 'undefined';
 
 function emptyRaycast() {}
 
+function updateFrustumCulled( object, toInitialValue ) {
+
+	object.traverse( c => {
+
+		c.frustumCulled = c[ INITIAL_FRUSTUM_CULLED ] && toInitialValue;
+
+	} );
+
+}
+
 export class TilesRenderer extends TilesRendererBase {
 
+	get autoDisableRendererCulling() {
+
+		return this._autoDisableRendererCulling;
+
+	}
+
+	set autoDisableRendererCulling( value ) {
+
+		if ( this._autoDisableRendererCulling !== value ) {
+
+			super._autoDisableRendererCulling = value;
+			this.traverse( tile => {
+
+				if ( tile.scene ) {
+
+					updateFrustumCulled( tile.scene, value );
+
+				}
+
+			} );
+
+		}
+
+	}
+
 	constructor( ...args ) {
 
 		super( ...args );
@@ -44,6 +80,7 @@ export class TilesRenderer extends TilesRendererBase {
 		this.cameraInfo = [];
 		this.activeTiles = new Set();
 		this.visibleTiles = new Set();
+		this._autoDisableRendererCulling = true;
 
 		this.onLoadModel = null;
 
@@ -489,7 +526,12 @@ export class TilesRenderer extends TilesRendererBase {
 
 			scene.matrix.premultiply( cachedTransform );
 			scene.matrix.decompose( scene.position, scene.quaternion, scene.scale );
-			scene.traverse( c => c.frustumCulled = false );
+			scene.traverse( c => {
+
+				c[ INITIAL_FRUSTUM_CULLED ] = c.frustumCulled;
+
+			} );
+			updateFrustumCulled( scene, this.autoDisableRendererCulling );
 
 			cached.scene = scene;