ソースを参照

Addons: Add `OutlineNode`. (#29575)

* OutlineNode: Initial setup.

* OutlineNode: Finalize implementation.

* E2E: Update screenshot.
Michael Herzog 1 年間 前
コミット
702b69897e

+ 1 - 0
examples/files.json

@@ -394,6 +394,7 @@
 		"webgpu_postprocessing_fxaa",
 		"webgpu_postprocessing_masking",
 		"webgpu_postprocessing_motion_blur",
+		"webgpu_postprocessing_outline",
 		"webgpu_postprocessing_smaa",
 		"webgpu_postprocessing_sobel",
 		"webgpu_postprocessing_ssaa",

+ 435 - 0
examples/jsm/tsl/display/OutlineNode.js

@@ -0,0 +1,435 @@
+import { Color, DepthTexture, FloatType, RenderTarget, Vector2 } from 'three';
+import { add, Loop, int, exp, min, float, mul, uv, vec2, Fn, textureSize, orthographicDepthToViewZ, QuadMesh, screenUV, TempNode, nodeObject, NodeUpdateType, uniform, vec4, NodeMaterial, passTexture, texture, perspectiveDepthToViewZ, positionView } from 'three/tsl';
+
+const _quadMesh = /*@__PURE__*/ new QuadMesh();
+const _currentClearColor = /*@__PURE__*/ new Color();
+const _size = /*@__PURE__*/ new Vector2();
+const _BLUR_DIRECTION_X = /*@__PURE__*/ new Vector2( 1.0, 0.0 );
+const _BLUR_DIRECTION_Y = /*@__PURE__*/ new Vector2( 0.0, 1.0 );
+
+class OutlineNode extends TempNode {
+
+	static get type() {
+
+		return 'OutlineNode';
+
+	}
+
+	constructor( scene, camera, selectedObjects = [] ) {
+
+		super( 'vec4' );
+
+		this.scene = scene;
+		this.camera = camera;
+		this.selectedObjects = selectedObjects;
+		this.downSampleRatio = 2;
+		this.visibleEdgeColor = new Color( 1, 1, 1 );
+		this.hiddenEdgeColor = new Color( 0.1, 0.04, 0.02 );
+		this.edgeThickness = 1;
+		this.edgeStrength = 3.0;
+		this.edgeGlow = 0;
+		this.pulsePeriod = 0;
+
+		this.updateBeforeType = NodeUpdateType.FRAME;
+
+		// render targets
+
+		this._renderTargetDepthBuffer = new RenderTarget();
+		this._renderTargetDepthBuffer.depthTexture = new DepthTexture();
+		this._renderTargetDepthBuffer.depthTexture.type = FloatType;
+
+		this._renderTargetMaskBuffer = new RenderTarget();
+		this._renderTargetMaskDownSampleBuffer = new RenderTarget( 1, 1, { depthBuffer: false } );
+		this._renderTargetEdgeBuffer1 = new RenderTarget( 1, 1, { depthBuffer: false } );
+		this._renderTargetEdgeBuffer2 = new RenderTarget( 1, 1, { depthBuffer: false } );
+		this._renderTargetBlurBuffer1 = new RenderTarget( 1, 1, { depthBuffer: false } );
+		this._renderTargetBlurBuffer2 = new RenderTarget( 1, 1, { depthBuffer: false } );
+		this._renderTargetComposite = new RenderTarget( 1, 1, { depthBuffer: false } );
+
+		// uniforms
+
+		this._cameraNear = uniform( camera.near );
+		this._cameraFar = uniform( camera.far );
+		this._resolution = uniform( new Vector2() );
+		this._visibleEdgeColor = uniform( new Color() );
+		this._hiddenEdgeColor = uniform( new Color() );
+		this._blurDirection = uniform( new Vector2() );
+		this._kernelRadius = uniform( 1 );
+		this._edgeGlow = uniform( 0 );
+		this._edgeStrength = uniform( 0 );
+
+		this._depthTextureUniform = texture( this._renderTargetDepthBuffer.depthTexture );
+		this._maskTextureUniform = texture( this._renderTargetMaskBuffer.texture );
+		this._maskTextureDownsSampleUniform = texture( this._renderTargetMaskDownSampleBuffer.texture );
+		this._edge1TextureUniform = texture( this._renderTargetEdgeBuffer1.texture );
+		this._edge2TextureUniform = texture( this._renderTargetEdgeBuffer2.texture );
+		this._blurColorTextureUniform = texture( this._renderTargetEdgeBuffer1.texture );
+
+		// materials
+
+		this._depthMaterial = new NodeMaterial();
+		this._depthMaterial.fragmentNode = vec4( 0, 0, 0, 1 );
+		this._depthMaterial.name = 'OutlineNode.depth';
+
+		this._prepareMaskMaterial = new NodeMaterial();
+		this._prepareMaskMaterial.name = 'OutlineNode.prepareMask';
+
+		this._materialCopy = new NodeMaterial();
+		this._materialCopy.name = 'OutlineNode.copy';
+
+		this._edgeDetectionMaterial = new NodeMaterial();
+		this._edgeDetectionMaterial.name = 'OutlineNode.edgeDetection';
+
+		this._separableBlurMaterial = new NodeMaterial();
+		this._separableBlurMaterial.name = 'OutlineNode.separableBlur';
+
+		this._compositeMaterial = new NodeMaterial();
+		this._compositeMaterial.name = 'OutlineNode.composite';
+
+		//
+
+		this._selectionCache = new Set();
+		this._tempPulseColor1 = new Color();
+		this._tempPulseColor2 = new Color();
+
+		//
+
+		this._textureNode = passTexture( this, this._renderTargetComposite.texture );
+
+	}
+
+	getTextureNode() {
+
+		return this._textureNode;
+
+	}
+
+	setSize( width, height ) {
+
+		this._resolution.value.set( width, height );
+
+		this._renderTargetDepthBuffer.setSize( width, height );
+		this._renderTargetMaskBuffer.setSize( width, height );
+		this._renderTargetComposite.setSize( width, height );
+
+		// downsample 1
+
+		let resx = Math.round( width / this.downSampleRatio );
+		let resy = Math.round( height / this.downSampleRatio );
+
+		this._renderTargetMaskDownSampleBuffer.setSize( resx, resy );
+		this._renderTargetEdgeBuffer1.setSize( resx, resy );
+		this._renderTargetBlurBuffer1.setSize( resx, resy );
+
+		// downsample 2
+
+		resx = Math.round( resx / 2 );
+		resy = Math.round( resy / 2 );
+
+		this._renderTargetEdgeBuffer2.setSize( resx, resy );
+		this._renderTargetBlurBuffer2.setSize( resx, resy );
+
+	}
+
+	updateBefore( frame ) {
+
+		const { renderer } = frame;
+		const { camera, scene } = this;
+
+		const size = renderer.getDrawingBufferSize( _size );
+		this.setSize( size.width, size.height );
+
+		renderer.getClearColor( _currentClearColor );
+		const currentClearAlpha = renderer.getClearAlpha();
+		const currentRenderTarget = renderer.getRenderTarget();
+		const currentMRT = renderer.getMRT();
+		const currentBackground = scene.background;
+		const currentRenderObjectFunction = renderer.getRenderObjectFunction();
+
+		//
+
+		renderer.setClearColor( 0xffffff, 1 );
+		renderer.setMRT( null );
+
+		this._updateSelectionCache();
+
+		scene.background = null;
+
+		// 1. Draw non-selected objects in the depth buffer
+
+		scene.overrideMaterial = this._depthMaterial;
+		renderer.setRenderTarget( this._renderTargetDepthBuffer );
+		renderer.setRenderObjectFunction( ( object, ...params ) => {
+
+			if ( this._selectionCache.has( object ) === false ) {
+
+				renderer.renderObject( object, ...params );
+
+			}
+
+		} );
+		renderer.render( scene, camera );
+
+		// 2. Draw only the selected objects by comparing the depth buffer of non-selected objects
+
+		scene.overrideMaterial = this._prepareMaskMaterial;
+		renderer.setRenderTarget( this._renderTargetMaskBuffer );
+		renderer.setRenderObjectFunction( ( object, ...params ) => {
+
+			if ( this._selectionCache.has( object ) === true ) {
+
+				renderer.renderObject( object, ...params );
+
+			}
+
+		} );
+		renderer.render( scene, camera );
+
+		scene.overrideMaterial = null;
+		scene.background = currentBackground;
+		renderer.setRenderObjectFunction( currentRenderObjectFunction );
+
+		this._selectionCache.clear();
+
+		// 3. Downsample to (at least) half resolution
+
+		_quadMesh.material = this._materialCopy;
+		renderer.setRenderTarget( this._renderTargetMaskDownSampleBuffer );
+		_quadMesh.render( renderer );
+
+		// 4. Perform edge detection (half resolution)
+
+		this._tempPulseColor1.copy( this.visibleEdgeColor );
+		this._tempPulseColor2.copy( this.hiddenEdgeColor );
+
+		if ( this.pulsePeriod > 0 ) {
+
+			const scalar = ( 1 + 0.25 ) / 2 + Math.cos( performance.now() * 0.01 / this.pulsePeriod ) * ( 1.0 - 0.25 ) / 2;
+			this._tempPulseColor1.multiplyScalar( scalar );
+			this._tempPulseColor2.multiplyScalar( scalar );
+
+		}
+
+		this._visibleEdgeColor.value.copy( this._tempPulseColor1 );
+		this._hiddenEdgeColor.value.copy( this._tempPulseColor2 );
+
+		_quadMesh.material = this._edgeDetectionMaterial;
+		renderer.setRenderTarget( this._renderTargetEdgeBuffer1 );
+		_quadMesh.render( renderer );
+
+		// 5. Apply blur (half resolution)
+
+		this._blurColorTextureUniform.value = this._renderTargetEdgeBuffer1.texture;
+		this._blurDirection.value.copy( _BLUR_DIRECTION_X );
+		this._kernelRadius.value = this.edgeThickness;
+
+		_quadMesh.material = this._separableBlurMaterial;
+		renderer.setRenderTarget( this._renderTargetBlurBuffer1 );
+		_quadMesh.render( renderer );
+
+		this._blurColorTextureUniform.value = this._renderTargetBlurBuffer1.texture;
+		this._blurDirection.value.copy( _BLUR_DIRECTION_Y );
+
+		renderer.setRenderTarget( this._renderTargetEdgeBuffer1 );
+		_quadMesh.render( renderer );
+
+		// 6. Apply blur (quarter resolution)
+
+		this._blurColorTextureUniform.value = this._renderTargetEdgeBuffer1.texture;
+		this._blurDirection.value.copy( _BLUR_DIRECTION_X );
+		this._kernelRadius.value = this.edgeThickness;
+
+		renderer.setRenderTarget( this._renderTargetBlurBuffer2 );
+		_quadMesh.render( renderer );
+
+		this._blurColorTextureUniform.value = this._renderTargetBlurBuffer2.texture;
+		this._blurDirection.value.copy( _BLUR_DIRECTION_Y );
+
+		renderer.setRenderTarget( this._renderTargetEdgeBuffer2 );
+		_quadMesh.render( renderer );
+
+		// 7. Composite
+
+		this._edgeGlow.value = this.edgeGlow;
+		this._edgeStrength.value = this.edgeStrength;
+
+		_quadMesh.material = this._compositeMaterial;
+		renderer.setRenderTarget( this._renderTargetComposite );
+		_quadMesh.render( renderer );
+
+		// restore
+
+		renderer.setRenderTarget( currentRenderTarget );
+		renderer.setMRT( currentMRT );
+		renderer.setClearColor( _currentClearColor, currentClearAlpha );
+
+	}
+
+	setup() {
+
+		// prepare mask material
+
+		const prepareMask = () => {
+
+			const depth = this._depthTextureUniform.uv( screenUV );
+
+			let viewZNode;
+
+			if ( this.camera.isPerspectiveCamera ) {
+
+				viewZNode = perspectiveDepthToViewZ( depth, this._cameraNear, this._cameraFar );
+
+			} else {
+
+				viewZNode = orthographicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
+
+			}
+
+			const depthTest = positionView.z.lessThanEqual( viewZNode ).select( 1, 0 );
+			return vec4( 0.0, depthTest, 1.0, 1.0 );
+
+		};
+
+		this._prepareMaskMaterial.fragmentNode = prepareMask();
+		this._prepareMaskMaterial.needsUpdate = true;
+
+		// copy material
+
+		this._materialCopy.fragmentNode = this._maskTextureUniform;
+		this._materialCopy.needsUpdate = true;
+
+		// edge detection material
+
+		const edgeDetection = Fn( () => {
+
+			const resolution = textureSize( this._maskTextureDownsSampleUniform );
+			const invSize = vec2( 1 ).div( resolution ).toVar();
+			const uvOffset = vec4( 1.0, 0.0, 0.0, 1.0 ).mul( vec4( invSize, invSize ) );
+
+			const uvNode = uv();
+			const c1 = this._maskTextureDownsSampleUniform.uv( uvNode.add( uvOffset.xy ) ).toVar();
+			const c2 = this._maskTextureDownsSampleUniform.uv( uvNode.sub( uvOffset.xy ) ).toVar();
+			const c3 = this._maskTextureDownsSampleUniform.uv( uvNode.add( uvOffset.yw ) ).toVar();
+			const c4 = this._maskTextureDownsSampleUniform.uv( uvNode.sub( uvOffset.yw ) ).toVar();
+
+			const diff1 = mul( c1.r.sub( c2.r ), float( 0.5 ) );
+			const diff2 = mul( c3.r.sub( c4.r ), float( 0.5 ) );
+			const d = vec2( diff1, diff2 ).length();
+			const a1 = min( c1.g, c2.g );
+			const a2 = min( c3.g, c4.g );
+			const visibilityFactor = min( a1, a2 );
+			const edgeColor = float( 1.0 ).sub( visibilityFactor ).greaterThan( float( 0.001 ) ).select( this._visibleEdgeColor, this._hiddenEdgeColor );
+			return vec4( edgeColor, 1 ).mul( d );
+
+		} );
+
+		this._edgeDetectionMaterial.fragmentNode = edgeDetection();
+		this._edgeDetectionMaterial.needsUpdate = true;
+
+		// seperable blur material
+
+		const gaussianPdf = Fn( ( [ x, sigma ] ) => {
+
+			return float( 0.39894 ).mul( exp( float( - 0.5 ).mul( x ).mul( x ).div( sigma.mul( sigma ) ) ).div( sigma ) );
+
+		} );
+
+		const seperableBlur = Fn( () => {
+
+			const MAX_RADIUS = 4;
+
+			const resolution = textureSize( this._maskTextureDownsSampleUniform );
+			const invSize = vec2( 1 ).div( resolution ).toVar();
+			const uvNode = uv();
+
+			const sigma = this._kernelRadius.div( 2 ).toVar();
+			const weightSum = gaussianPdf( 0, sigma ).toVar();
+			const diffuseSum = this._blurColorTextureUniform.uv( uvNode ).mul( weightSum ).toVar();
+			const delta = this._blurDirection.mul( invSize ).mul( this._kernelRadius ).div( MAX_RADIUS ).toVar();
+
+			const uvOffset = delta.toVar();
+
+			Loop( { start: int( 0 ), end: int( MAX_RADIUS ), type: 'int', condition: '<=' }, ( { i } ) => {
+
+				const x = this._kernelRadius.mul( float( i ) ).div( MAX_RADIUS );
+				const w = gaussianPdf( x, sigma );
+				const sample1 = this._blurColorTextureUniform.uv( uvNode.add( uvOffset ) );
+				const sample2 = this._blurColorTextureUniform.uv( uvNode.sub( uvOffset ) );
+
+				diffuseSum.addAssign( add( sample1, sample2 ).mul( w ) );
+				weightSum.addAssign( float( 2 ).mul( w ) );
+				uvOffset.addAssign( delta );
+
+			} );
+
+			return diffuseSum.div( weightSum );
+
+		} );
+
+		this._separableBlurMaterial.fragmentNode = seperableBlur();
+		this._separableBlurMaterial.needsUpdate = true;
+
+		// composite material
+
+		const composite = Fn( () => {
+
+			const edgeValue1 = this._edge1TextureUniform;
+			const edgeValue2 = this._edge2TextureUniform;
+			const maskColor = this._maskTextureUniform;
+
+			const edgeValue = edgeValue1.add( edgeValue2.mul( this._edgeGlow ) );
+			return this._edgeStrength.mul( maskColor.r ).mul( edgeValue );
+
+		} );
+
+		this._compositeMaterial.fragmentNode = composite();
+		this._compositeMaterial.needsUpdate = true;
+
+		return this._textureNode;
+
+	}
+
+	dispose() {
+
+		this.selectedObjects.length = 0;
+
+		this._renderTargetDepthBuffer.dispose();
+		this._renderTargetMaskBuffer.dispose();
+		this._renderTargetMaskDownSampleBuffer.dispose();
+		this._renderTargetEdgeBuffer1.dispose();
+		this._renderTargetEdgeBuffer2.dispose();
+		this._renderTargetBlurBuffer1.dispose();
+		this._renderTargetBlurBuffer2.dispose();
+		this._renderTargetComposite.dispose();
+
+		this._depthMaterial.dispose();
+		this._prepareMaskMaterial.dispose();
+		this._materialCopy.dispose();
+		this._edgeDetectionMaterial.dispose();
+		this._separableBlurMaterial.dispose();
+		this._compositeMaterial.dispose();
+
+	}
+
+	//
+
+	_updateSelectionCache() {
+
+		for ( let i = 0; i < this.selectedObjects.length; i ++ ) {
+
+			const selectedObject = this.selectedObjects[ i ];
+			selectedObject.traverse( ( object ) => {
+
+				if ( object.isMesh ) this._selectionCache.add( object );
+
+			} );
+
+		}
+
+	}
+
+}
+
+export default OutlineNode;
+
+export const outline = ( scene, camera, selectedObjects ) => nodeObject( new OutlineNode( scene, camera, selectedObjects ) );

BIN
examples/screenshots/webgpu_postprocessing_outline.jpg


+ 311 - 0
examples/webgpu_postprocessing_outline.html

@@ -0,0 +1,311 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - post processing - Outline Pass</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">
+	</head>
+	<body>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Outline Pass by <a href="http://eduperiment.com" target="_blank" rel="noopener">Prashant Sharma</a> and <a href="https://clara.io" target="_blank" rel="noopener">Ben Houston</a><br/><br/>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.webgpu.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { pass } from 'three/tsl';
+			import { outline } from 'three/addons/tsl/display/OutlineNode.js';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
+
+			let container, stats;
+			let camera, scene, renderer, controls;
+			let postProcessing, outlinePass;
+
+			let selectedObjects = [];
+
+			const raycaster = new THREE.Raycaster();
+			const mouse = new THREE.Vector2();
+
+			const obj3d = new THREE.Object3D();
+			const group = new THREE.Group();
+
+			const params = {
+				edgeStrength: 3.0,
+				edgeGlow: 0.0,
+				edgeThickness: 1.0,
+				pulsePeriod: 0
+			};
+
+			// Init gui
+
+			const gui = new GUI( { width: 280 } );
+
+			gui.add( params, 'edgeStrength', 0.01, 10 ).onChange( function ( value ) {
+
+				outlinePass.edgeStrength = Number( value );
+
+			} );
+
+			gui.add( params, 'edgeGlow', 0.0, 1 ).onChange( function ( value ) {
+
+				outlinePass.edgeGlow = Number( value );
+
+			} );
+
+			gui.add( params, 'edgeThickness', 1, 4 ).onChange( function ( value ) {
+
+				outlinePass.edgeThickness = Number( value );
+
+			} );
+
+			gui.add( params, 'pulsePeriod', 0.0, 5 ).onChange( function ( value ) {
+
+				outlinePass.pulsePeriod = Number( value );
+
+			} );
+
+			function Configuration() {
+
+				this.visibleEdgeColor = '#ffffff';
+				this.hiddenEdgeColor = '#190a05';
+
+			}
+
+			const conf = new Configuration();
+
+			gui.addColor( conf, 'visibleEdgeColor' ).onChange( function ( value ) {
+
+				outlinePass.visibleEdgeColor.set( value );
+
+			} );
+
+			gui.addColor( conf, 'hiddenEdgeColor' ).onChange( function ( value ) {
+
+				outlinePass.hiddenEdgeColor.set( value );
+
+			} );
+
+			init();
+
+			function init() {
+
+				container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				const width = window.innerWidth;
+				const height = window.innerHeight;
+
+				renderer = new THREE.WebGPURenderer();
+				renderer.shadowMap.enabled = true;
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( width, height );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+
+				scene = new THREE.Scene();
+
+				camera = new THREE.PerspectiveCamera( 45, width / height, 0.1, 100 );
+				camera.position.set( 0, 0, 8 );
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 5;
+				controls.maxDistance = 20;
+				controls.enablePan = false;
+				controls.enableDamping = true;
+				controls.dampingFactor = 0.05;
+
+				//
+
+				scene.add( new THREE.AmbientLight( 0xaaaaaa, 0.6 ) );
+
+				const light = new THREE.DirectionalLight( 0xddffdd, 2 );
+				light.position.set( 5, 5, 5 );
+				light.castShadow = true;
+				light.shadow.mapSize.width = 2048;
+				light.shadow.mapSize.height = 2048;
+				light.shadow.bias = - 0.005;
+
+				const d = 10;
+
+				light.shadow.camera.left = - d;
+				light.shadow.camera.right = d;
+				light.shadow.camera.top = d;
+				light.shadow.camera.bottom = - d;
+				light.shadow.camera.far = 25;
+
+				scene.add( light );
+
+				// model
+
+				const loader = new OBJLoader();
+				loader.load( 'models/obj/tree.obj', function ( object ) {
+
+					let scale = 1.0;
+
+					object.traverse( function ( child ) {
+
+						if ( child instanceof THREE.Mesh ) {
+
+							child.geometry.center();
+							child.geometry.computeBoundingSphere();
+							scale = 0.2 * child.geometry.boundingSphere.radius;
+
+							const phongMaterial = new THREE.MeshPhongMaterial( { color: 0xffffff, specular: 0x111111, shininess: 5 } );
+							child.material = phongMaterial;
+							child.receiveShadow = true;
+							child.castShadow = true;
+
+						}
+
+					} );
+
+					object.position.y = 1;
+					object.scale.divideScalar( scale );
+					obj3d.add( object );
+
+				} );
+
+				scene.add( group );
+
+				group.add( obj3d );
+
+				//
+
+				const geometry = new THREE.SphereGeometry( 3, 48, 24 );
+
+				for ( let i = 0; i < 20; i ++ ) {
+
+					const material = new THREE.MeshLambertMaterial();
+					material.color.setHSL( Math.random(), 1.0, 0.3 );
+
+					const mesh = new THREE.Mesh( geometry, material );
+					mesh.position.x = Math.random() * 4 - 2;
+					mesh.position.y = Math.random() * 4 - 2;
+					mesh.position.z = Math.random() * 4 - 2;
+					mesh.receiveShadow = true;
+					mesh.castShadow = true;
+					mesh.scale.multiplyScalar( Math.random() * 0.3 + 0.1 );
+					group.add( mesh );
+
+				}
+
+				const floorMaterial = new THREE.MeshLambertMaterial( { side: THREE.DoubleSide } );
+
+				const floorGeometry = new THREE.PlaneGeometry( 12, 12 );
+				const floorMesh = new THREE.Mesh( floorGeometry, floorMaterial );
+				floorMesh.rotation.x -= Math.PI * 0.5;
+				floorMesh.position.y -= 1.5;
+				group.add( floorMesh );
+				floorMesh.receiveShadow = true;
+
+				const torusGeometry = new THREE.TorusGeometry( 1, 0.3, 16, 100 );
+				const torusMaterial = new THREE.MeshPhongMaterial( { color: 0xffaaff } );
+				const torus = new THREE.Mesh( torusGeometry, torusMaterial );
+				torus.position.z = - 4;
+				group.add( torus );
+				torus.receiveShadow = true;
+				torus.castShadow = true;
+
+				//
+
+				stats = new Stats();
+				container.appendChild( stats.dom );
+
+				// postprocessing
+
+				postProcessing = new THREE.PostProcessing( renderer );
+
+				const scenePass = pass( scene, camera );
+				const scenePassColor = scenePass.getTextureNode( 'output' );
+				outlinePass = outline( scene, camera, scene );
+
+				postProcessing.outputNode = outlinePass.getTextureNode().add( scenePassColor );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				renderer.domElement.style.touchAction = 'none';
+				renderer.domElement.addEventListener( 'pointermove', onPointerMove );
+
+				function onPointerMove( event ) {
+
+					if ( event.isPrimary === false ) return;
+
+					mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
+					mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
+
+					checkIntersection();
+
+				}
+
+				function addSelectedObject( object ) {
+
+					selectedObjects = [];
+					selectedObjects.push( object );
+
+				}
+
+				function checkIntersection() {
+
+					raycaster.setFromCamera( mouse, camera );
+
+					const intersects = raycaster.intersectObject( scene, true );
+
+					if ( intersects.length > 0 ) {
+
+						const selectedObject = intersects[ 0 ].object;
+						addSelectedObject( selectedObject );
+						outlinePass.selectedObjects = selectedObjects;
+
+					} else {
+
+						// outlinePass.selectedObjects = [];
+
+					}
+
+				}
+
+			}
+
+			function onWindowResize() {
+
+				const width = window.innerWidth;
+				const height = window.innerHeight;
+
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( width, height );
+
+			}
+
+			function animate() {
+
+				stats.begin();
+
+				controls.update();
+
+				postProcessing.render();
+
+				stats.end();
+
+			}
+
+		</script>
+	</body>
+</html>

粤ICP备19079148号