浏览代码

Examples: Add external three-gpu-pathtracer example (#24803)

* Add initial path tracer example

* update pathtracer demo

* add more parameters

* Improve gui options

* skip pathtracer render

* Adjust floor, background color

* adjust defaults

* Add tone mapping option

* Add tags, screenshot, setting updates

* add exception comment

* Update settings

* Header and title fixes
Garrett Johnson 2 年之前
父节点
当前提交
1a8b53fb93
共有 5 个文件被更改,包括 499 次插入0 次删除
  1. 1 0
      examples/files.json
  2. 二进制
      examples/screenshots/webgl_renderer_pathtracer.jpg
  3. 1 0
      examples/tags.json
  4. 496 0
      examples/webgl_renderer_pathtracer.html
  5. 1 0
      test/e2e/puppeteer.js

+ 1 - 0
examples/files.json

@@ -206,6 +206,7 @@
 		"webgl_raycaster_sprite",
 		"webgl_raycaster_sprite",
 		"webgl_raycaster_texture",
 		"webgl_raycaster_texture",
 		"webgl_read_float_buffer",
 		"webgl_read_float_buffer",
+		"webgl_renderer_pathtracer",
 		"webgl_refraction",
 		"webgl_refraction",
 		"webgl_rtt",
 		"webgl_rtt",
 		"webgl_shader",
 		"webgl_shader",

二进制
examples/screenshots/webgl_renderer_pathtracer.jpg


+ 1 - 0
examples/tags.json

@@ -90,6 +90,7 @@
 	"webgl_postprocessing_3dlut": [ "color grading" ],
 	"webgl_postprocessing_3dlut": [ "color grading" ],
 	"webgl_materials_modified": [ "onBeforeCompile" ],
 	"webgl_materials_modified": [ "onBeforeCompile" ],
 	"webgl_raycaster_bvh": [ "external", "query", "bounds", "tree", "accelerate", "performance", "community", "extension", "plugin", "library", "three-mesh-bvh" ],
 	"webgl_raycaster_bvh": [ "external", "query", "bounds", "tree", "accelerate", "performance", "community", "extension", "plugin", "library", "three-mesh-bvh" ],
+	"webgl_renderer_pathtracer": [ "external", "raytracing", "pathtracing", "library", "plugin", "extension", "community", "three-gpu-pathtracer", "three-mesh-bvh" ],
 	"webgl_shadowmap_csm": [ "cascade" ],
 	"webgl_shadowmap_csm": [ "cascade" ],
 	"webgl_shadowmap_pcss": [ "soft" ],
 	"webgl_shadowmap_pcss": [ "soft" ],
 	"webgl_simple_gi": [ "global illumination" ],
 	"webgl_simple_gi": [ "global illumination" ],

+ 496 - 0
examples/webgl_renderer_pathtracer.html

@@ -0,0 +1,496 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - three-gpu-pathtracer</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;
+				background-color: white;
+			}
+
+			a {
+				color: #fb8c00;
+			}
+
+			#samples-container {
+				position: absolute;
+				bottom: 0;
+				right: 0;
+				font-family: 'Courier New', Courier, monospace;
+				color: white;
+				pointer-events: none;
+			}
+
+			#samples {
+
+				background-color: rgba( 0.0, 0.0, 0.0, 0.75 );
+				padding: 5px;
+				display: inline-block;
+
+			}
+
+			.checkerboard {
+				background-image:
+					linear-gradient(45deg, #ddd 25%, transparent 25%),
+					linear-gradient(-45deg, #ddd 25%, transparent 25%),
+					linear-gradient(45deg, transparent 75%, #ddd 75%),
+					linear-gradient(-45deg, transparent 75%, #ddd 75%);
+				background-size: 20px 20px;
+				background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
+			}
+		</style>
+	</head>
+
+	<body>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> pathtracer - <a href="https://github.com/gkjohnson/three-gpu-pathtracer" target="_blank" rel="noopener">three-gpu-pathtracer</a><br/>
+			See <a href="https://github.com/gkjohnson/three-gpu-pathtracer" target="_blank" rel="noopener">main project repository</a> for more information and examples on high fidelity path tracing.
+		</div>
+		<div id="samples-container">
+			<div id="samples">--</div>
+		</div>
+
+		<!-- Import maps polyfill -->
+		<!-- Remove this when import maps will be widely supported -->
+		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/examples/": "./",
+					"three-gpu-pathtracer": "https://unpkg.com/[email protected]/build/index.module.js",
+					"three-mesh-bvh": "https://unpkg.com/three-mesh-bvh@^0.5.10/build/index.module.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
+			import { LDrawLoader } from 'three/addons/loaders/LDrawLoader.js';
+			import { LDrawUtils } from 'three/addons/utils/LDrawUtils.js';
+			import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
+
+			import { PhysicalPathTracingMaterial, PathTracingRenderer, MaterialReducer, BlurredEnvMapGenerator, PathTracingSceneGenerator } from 'three-gpu-pathtracer';
+
+			let container, progressBarDiv, samplesEl;
+			let camera, scene, renderer, controls, gui;
+			let pathTracer, sceneInfo, fsQuad, floor;
+			let delaySamples = 0;
+
+			const params = {
+				enable: true,
+				toneMapping: true,
+				pause: false,
+				tiles: 3,
+				transparentBackground: false,
+				resolutionScale: 1,
+				download: () => {
+
+					const link = document.createElement('a');
+					link.download = 'pathtraced-render.png';
+					link.href = renderer.domElement.toDataURL().replace( 'image/png', 'image/octet-stream' );
+					link.click();
+
+				},
+				roughness: 0.15,
+				metalness: 0.9,
+			};
+
+			init();
+			render();
+
+			function init() {
+
+				samplesEl = document.getElementById( 'samples' );
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 10000 );
+				camera.position.set( 150, 200, 250 );
+
+				// initialize the renderer
+				renderer = new THREE.WebGLRenderer( { antialias: true, preserveDrawingBuffer: true, premultipliedAlpha: false } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.outputEncoding = THREE.sRGBEncoding;
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				renderer.setClearColor( 0xdddddd );
+				document.body.appendChild( renderer.domElement );
+
+				// initialize the pathtracer
+				pathTracer = new PathTracingRenderer( renderer );
+				pathTracer.alpha = true;
+				pathTracer.tiles.set( params.tiles, params.tiles );
+				pathTracer.material = new PhysicalPathTracingMaterial();
+				pathTracer.material.setDefine( 'FEATURE_GRADIENT_BG', 1 );
+				pathTracer.material.setDefine( 'FEATURE_MIS', 1 );
+				pathTracer.material.bgGradientTop.set( 0xeeeeee );
+				pathTracer.material.bgGradientBottom.set( 0xeaeaea );
+				pathTracer.camera = camera;
+
+				fsQuad = new FullScreenQuad( new THREE.MeshBasicMaterial( {
+					map: pathTracer.target.texture,
+					blending: THREE.CustomBlending
+				} ) );
+
+				// scene
+				scene = new THREE.Scene();
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.addEventListener( 'change', () => {
+
+					delaySamples = 5;
+					pathTracer.reset();
+
+				} );
+
+				window.addEventListener( 'resize', onWindowResize );
+				onWindowResize();
+
+				progressBarDiv = document.createElement( 'div' );
+				progressBarDiv.innerText = 'Loading...';
+				progressBarDiv.style.fontSize = '3em';
+				progressBarDiv.style.color = '#888';
+				progressBarDiv.style.display = 'block';
+				progressBarDiv.style.position = 'absolute';
+				progressBarDiv.style.top = '50%';
+				progressBarDiv.style.width = '100%';
+				progressBarDiv.style.textAlign = 'center';
+
+				// load materials and then the model
+				createGUI();
+
+				loadModel();
+
+			}
+
+			async function loadModel() {
+
+				progressBarDiv.innerText = 'Loading...';
+
+				let model = null;
+				let envMap = null;
+
+				updateProgressBar( 0 );
+				showProgressBar();
+				
+				// only smooth when not rendering with flat colors to improve processing time
+				const ldrawPromise = 
+					new LDrawLoader()
+						.setPath( 'models/ldraw/officialLibrary/' )
+						.loadAsync( 'models/7140-1-X-wingFighter.mpd_Packed.mpd', onProgress )
+						.then( function ( legoGroup ) {
+
+							legoGroup = LDrawUtils.mergeObject( legoGroup );
+							legoGroup.rotation.x = Math.PI;
+
+							model = new THREE.Group();
+							model.add( legoGroup );
+
+							// Convert from LDraw coordinates: rotate 180 degrees around OX
+							model.updateMatrixWorld();
+
+						} )
+						.catch( onError );
+
+				const envMapPromise =
+					new RGBELoader()
+						.setPath( 'textures/equirectangular/' )
+						.loadAsync( 'royal_esplanade_1k.hdr' )
+						.then( tex => {
+
+							const envMapGenerator = new BlurredEnvMapGenerator( renderer );
+							const blurredEnvMap = envMapGenerator.generate( tex, 0 );
+
+							scene.environment = blurredEnvMap;
+							envMap = blurredEnvMap;
+
+						} );
+
+				await Promise.all( [ envMapPromise, ldrawPromise ] );
+
+				hideProgressBar();
+				document.body.classList.add( 'checkerboard' );
+
+				// Adjust camera
+				const bbox = new THREE.Box3().setFromObject( model );
+				const size = bbox.getSize( new THREE.Vector3() );
+				const radius = Math.max( size.x, Math.max( size.y, size.z ) ) * 0.4;
+
+				controls.target0.copy( bbox.getCenter( new THREE.Vector3() ) );
+				controls.position0.set( 2.3, 1, 2 ).multiplyScalar( radius ).add( controls.target0 );
+				controls.reset();
+
+				// add floor
+				floor = new THREE.Mesh(
+					new THREE.PlaneGeometry(),
+					new THREE.MeshStandardMaterial( {
+						side: THREE.DoubleSide,
+						roughness: 0.01,
+						metalness: 1,
+						map: generateRadialFloorTexture( 1024 ),
+						transparent: true,
+					} ),
+				);
+				floor.scale.setScalar( 2500 );
+				floor.rotation.x = - Math.PI / 2;
+				floor.position.y = bbox.min.y;
+				model.add( floor );
+				model.updateMatrixWorld();
+
+				// de-duplicate and reduce the number of materials used in place
+				const reducer = new MaterialReducer();
+				reducer.process( model );
+
+				// reset the progress bar to display bvh generation
+				progressBarDiv.innerText = 'Generating BVH...';
+				updateProgressBar( 0 );
+
+				const generator = new PathTracingSceneGenerator();
+				const result = generator.generate( model );
+
+				// add the model to the scene
+				sceneInfo = result;
+				sceneInfo.scene.traverse( c => {
+
+					if ( c.isLineSegments ) {
+
+						c.visible = false;
+
+					}
+
+				} );
+				scene.add( sceneInfo.scene );
+
+				// update the material
+				const { bvh, textures, materials } = result;
+				const geometry = bvh.geometry;
+				const material = pathTracer.material;
+
+				material.bvh.updateFrom( bvh );
+				material.normalAttribute.updateFrom( geometry.attributes.normal );
+				material.tangentAttribute.updateFrom( geometry.attributes.tangent );
+				material.uvAttribute.updateFrom( geometry.attributes.uv );
+				material.materialIndexAttribute.updateFrom( geometry.attributes.materialIndex );
+				// material.colorAttribute.updateFrom( geometry.attributes.color );
+				material.textures.setTextures( renderer, 2048, 2048, textures );
+				material.materials.updateFrom( materials, textures );
+				pathTracer.material.envMapInfo.updateFrom( envMap );
+				pathTracer.reset();
+
+			}
+
+			function onWindowResize() {
+
+				const w = window.innerWidth;
+				const h = window.innerHeight;
+				const scale = params.resolutionScale;
+				const dpr = window.devicePixelRatio;
+
+				pathTracer.setSize( w * scale * dpr, h * scale * dpr );
+				pathTracer.reset();
+
+				renderer.setSize( w, h );
+				renderer.setPixelRatio( window.devicePixelRatio * scale );
+
+				const aspect = w / h;
+				camera.aspect = aspect;
+				camera.updateProjectionMatrix();
+
+			}
+
+			function createGUI() {
+
+				if ( gui ) {
+
+					gui.destroy();
+
+				}
+
+				gui = new GUI();
+				gui.add( params, 'enable' );
+				gui.add( params, 'pause' );
+				gui.add( params, 'toneMapping' );
+				gui.add( params, 'transparentBackground' ).onChange( v => {
+
+					pathTracer.material.backgroundAlpha = v ? 0 : 1;
+					renderer.setClearAlpha( v ? 0 : 1 );
+					pathTracer.reset();
+
+				} );
+				gui.add( params, 'resolutionScale', 0.1, 1.0 ).onChange( onWindowResize );
+				gui.add( params, 'tiles', 1, 3, 1 ).onChange( v => {
+
+					pathTracer.tiles.set( v, v );
+
+				} );
+				gui.add( params, 'roughness', 0, 1 ).name( 'floor roughness' ).onChange( () => {
+
+					pathTracer.reset();
+
+				} );
+				gui.add( params, 'metalness', 0, 1 ).name( 'floor metalness' ).onChange( () => {
+
+					pathTracer.reset();
+
+				} );
+				gui.add( params, 'download' ).name( 'download image' );
+
+
+			}
+
+			//
+
+			function render() {
+
+				requestAnimationFrame( render );
+
+				if ( ! sceneInfo ) {
+
+					return;
+
+				}
+
+				renderer.toneMapping = params.toneMapping ? THREE.ACESFilmicToneMapping : THREE.NoToneMapping;
+
+				if ( pathTracer.samples < 1.0 || ! params.enable ) {
+
+					renderer.render( scene, camera );
+
+				}
+
+				if ( params.enable && delaySamples === 0 ) {
+
+					const samples = Math.floor( pathTracer.samples );
+					samplesEl.innerText = `samples: ${ samples }`;
+
+					floor.material.roughness = params.roughness;
+					floor.material.metalness = params.metalness;
+
+					pathTracer.material.materials.updateFrom( sceneInfo.materials, sceneInfo.textures );
+					pathTracer.material.filterGlossyFactor = 0.5;
+					pathTracer.material.physicalCamera.updateFrom( camera );
+
+					camera.updateMatrixWorld();
+
+					if ( ! params.pause || pathTracer.samples < 1 ) {
+
+						pathTracer.update();
+
+					}
+
+					renderer.autoClear = false;
+					fsQuad.render( renderer );
+					renderer.autoClear = true;
+
+				} else if ( delaySamples > 0 ) {
+
+					delaySamples --;
+
+				}
+
+				samplesEl.innerText = `samples: ${ Math.floor( pathTracer.samples ) }`;
+
+			}
+
+			function onProgress( xhr ) {
+
+				if ( xhr.lengthComputable ) {
+
+					updateProgressBar( xhr.loaded / xhr.total );
+
+					console.log( Math.round( xhr.loaded / xhr.total * 100, 2 ) + '% downloaded' );
+
+				}
+
+			}
+
+			function onError( error ) {
+
+				const message = 'Error loading model';
+				progressBarDiv.innerText = message;
+				console.log( message );
+				console.error( error );
+
+			}
+
+			function showProgressBar() {
+
+				document.body.appendChild( progressBarDiv );
+
+			}
+
+			function hideProgressBar() {
+
+				document.body.removeChild( progressBarDiv );
+
+			}
+
+			function updateProgressBar( fraction ) {
+
+				progressBarDiv.innerText = 'Loading... ' + Math.round( fraction * 100, 2 ) + '%';
+
+			}
+
+			function generateRadialFloorTexture( dim ) {
+
+				const data = new Uint8Array( dim * dim * 4 );
+
+				for ( let x = 0; x < dim; x ++ ) {
+
+					for ( let y = 0; y < dim; y ++ ) {
+
+						const xNorm = x / ( dim - 1 );
+						const yNorm = y / ( dim - 1 );
+
+						const xCent = 2.0 * ( xNorm - 0.5 );
+						const yCent = 2.0 * ( yNorm - 0.5 );
+						let a = Math.max( Math.min( 1.0 - Math.sqrt( xCent ** 2 + yCent ** 2 ), 1.0 ), 0.0 );
+						a = a ** 1.5;
+						a = a * 1.5;
+						a = Math.min( a, 1.0 );
+
+						const i = y * dim + x;
+						data[ i * 4 + 0 ] = 255;
+						data[ i * 4 + 1 ] = 255;
+						data[ i * 4 + 2 ] = 255;
+						data[ i * 4 + 3 ] = a * 255;
+
+					}
+
+				}
+
+				const tex = new THREE.DataTexture( data, dim, dim );
+				tex.format = THREE.RGBAFormat;
+				tex.type = THREE.UnsignedByteType;
+				tex.minFilter = THREE.LinearFilter;
+				tex.magFilter = THREE.LinearFilter;
+				tex.wrapS = THREE.RepeatWrapping;
+				tex.wrapT = THREE.RepeatWrapping;
+				tex.needsUpdate = true;
+				return tex;
+
+			}
+
+		</script>
+
+		<!-- LDraw.org CC BY 2.0 Parts Library attribution -->
+		<div style="display: block; position: absolute; bottom: 8px; left: 8px; width: 160px; padding: 10px; background-color: #F3F7F8;">
+			<center>
+				<a href="http://www.ldraw.org"><img style="width: 145px" src="models/ldraw/ldraw_org_logo/Stamp145.png"></a>
+				<br />
+				<a href="http://www.ldraw.org/">This software uses the LDraw Parts Library</a>
+			</center>
+		</div>
+
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -35,6 +35,7 @@ const exceptionList = [
 	'webgl_nodes_materials_standard', // puppeteer does not support import maps yet
 	'webgl_nodes_materials_standard', // puppeteer does not support import maps yet
 	'webgl_postprocessing_crossfade', // fails for some misterious reason
 	'webgl_postprocessing_crossfade', // fails for some misterious reason
 	'webgl_raymarching_reflect', // exception for Github Actions
 	'webgl_raymarching_reflect', // exception for Github Actions
+	'webgl_renderer_pathtracer', // slow to render
 	'webgl_test_memory2', // gives fatal error in puppeteer
 	'webgl_test_memory2', // gives fatal error in puppeteer
 	'webgl_tiled_forward', // exception for Github Actions
 	'webgl_tiled_forward', // exception for Github Actions
 	'webgl_video_kinect', // video tag not deterministic enough
 	'webgl_video_kinect', // video tag not deterministic enough