Kaynağa Gözat

Examples: Add TSL function for grounded skyboxes. (#33611)

Michael Herzog 1 ay önce
ebeveyn
işleme
cd8eb0c8a7

+ 1 - 0
examples/files.json

@@ -386,6 +386,7 @@
 		"webgpu_materials_cubemap_mipmaps",
 		"webgpu_materials_displacementmap",
 		"webgpu_materials_envmaps_bpcem",
+		"webgpu_materials_envmaps_groundprojected",
 		"webgpu_materials_envmaps",
 		"webgpu_materials_lightmap",
 		"webgpu_materials_matcap",

+ 62 - 0
examples/jsm/tsl/utils/GroundedSkybox.js

@@ -0,0 +1,62 @@
+import { Fn, If, vec3, float, min, cameraPosition, positionWorld } from 'three/tsl';
+
+/**
+ * @module GroundedSkybox
+ * @three_import import { getGroundProjectedNormal } from 'three/addons/tsl/utils/GroundedSkybox.js';
+ */
+
+/**
+ * Projects the world position onto a sphere whose bottom is clipped by a ground disk,
+ * then returns a vector usable for sampling an environment cube map.
+ *
+ * @tsl
+ * @function
+ * @param {Node<float>} radiusNode - The radius of the projection sphere. Must be large enough to ensure the scene's camera stays inside.
+ * @param {Node<float>} heightNode - The height is how far the camera that took the photo was above the ground. A larger value will magnify the downward part of the image.
+ * @return {Node<vec3>} A direction vector for sampling the environment cube map.
+ */
+export const getGroundProjectedNormal = Fn( ( [ radiusNode, heightNode ] ) => {
+
+	const p = positionWorld.sub( cameraPosition ).normalize().toConst();
+	const camPos = cameraPosition.toVar();
+	camPos.y.subAssign( heightNode );
+
+	// sphereIntersect( camPos, p, vec3( 0 ), radius )
+	const b = camPos.dot( p ).toConst();
+	const c = camPos.dot( camPos ).sub( radiusNode.mul( radiusNode ) ).toConst();
+	const h = b.mul( b ).sub( c ).toConst();
+
+	const intersection = h.greaterThanEqual( 0 ).select( h.sqrt().sub( b ), - 1 );
+
+	const projected = vec3( 0, 1, 0 ).toVar();
+
+	If( intersection.greaterThan( 0 ), () => {
+
+		// diskIntersectWithBackFaceCulling( camPos, p, diskCenter, n, radius )
+		const diskCenter = vec3( 0, heightNode.negate(), 0 ).toConst();
+		const n = vec3( 0, 1, 0 ).toConst();
+		const d = p.dot( n ).toConst();
+
+		const intersection2 = float( 1e6 ).toVar();
+
+		If( d.lessThanEqual( 0 ), () => {
+
+			const o = camPos.sub( diskCenter ).toConst();
+			const t = n.dot( o ).negate().div( d ).toConst();
+			const q = o.add( p.mul( t ) ).toConst();
+
+			If( q.dot( q ).lessThan( radiusNode.mul( radiusNode ) ), () => {
+
+				intersection2.assign( t );
+
+			} );
+
+		} );
+
+		projected.assign( camPos.add( p.mul( min( intersection, intersection2 ) ) ).div( radiusNode ) );
+
+	} );
+
+	return projected;
+
+} );

BIN
examples/screenshots/webgpu_materials_envmaps_groundprojected.jpg


+ 208 - 0
examples/webgpu_materials_envmaps_groundprojected.html

@@ -0,0 +1,208 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>threejs webgpu - materials - ground projected environment mapping</title>
+		<meta charset="utf-8" />
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"/>
+		<meta property="og:title" content="threejs webgpu - materials - ground projected environment mapping" />
+		<meta property="og:type" content="website" />
+		<meta property="og:url" content="https://threejs.org/examples/webgpu_materials_envmaps_groundprojected.html" />
+		<meta property="og:image" content="https://threejs.org/examples/screenshots/webgpu_materials_envmaps_groundprojected.jpg" />
+		<link type="text/css" rel="stylesheet" href="example.css">
+	</head>
+	<body>
+		<div id="info">
+			<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
+
+			<div class="title-wrapper">
+				<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>Ground Projected Env Maps</span>
+			</div>
+
+			<small>
+				Ferrari 458 Italia model by <a href="https://sketchfab.com/models/57bf6cc56931426e87494f554df1dab6" target="_blank" rel="noopener">vicent091036</a><br>
+				<a href="https://polyhaven.com/a/blouberg_sunrise_2" target="_blank" rel="noopener">Blouberg Sunrise 2</a> by <a href="https://gregzaal.com/" target="_blank" rel="noopener">Greg Zaal</a>
+			</small>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+			import * as THREE from 'three/webgpu';
+
+			import { Inspector } from 'three/addons/inspector/Inspector.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+			import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
+			import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
+			import { getGroundProjectedNormal } from 'three/addons/tsl/utils/GroundedSkybox.js';
+
+			import { cubeTexture, float } from 'three/tsl';
+
+			const params = {
+				height: 15,
+				radius: 100,
+				enabled: true,
+			};
+
+			let camera, scene, renderer;
+
+			init();
+
+			async function init() {
+
+				camera = new THREE.PerspectiveCamera(
+					40,
+					window.innerWidth / window.innerHeight,
+					1,
+					1000
+				);
+				camera.position.set( - 20, 7, 20 );
+				camera.lookAt( 0, 4, 0 );
+
+				scene = new THREE.Scene();
+
+				const hdrLoader = new HDRLoader();
+				const envMap = await hdrLoader.loadAsync( 'textures/equirectangular/blouberg_sunrise_2_1k.hdr' );
+				envMap.mapping = THREE.EquirectangularReflectionMapping;
+
+				scene.environment = envMap;
+
+				const dracoLoader = new DRACOLoader();
+				dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );
+
+				const loader = new GLTFLoader();
+				loader.setDRACOLoader( dracoLoader );
+
+				const shadow = new THREE.TextureLoader().load( 'models/gltf/ferrari_ao.png' );
+
+				loader.load( 'models/gltf/ferrari.glb', function ( gltf ) {
+
+					const bodyMaterial = new THREE.MeshPhysicalMaterial( {
+						color: 0x000000, metalness: 1.0, roughness: 0.8,
+						clearcoat: 1.0, clearcoatRoughness: 0.2
+					} );
+
+					const detailsMaterial = new THREE.MeshStandardMaterial( {
+						color: 0xffffff, metalness: 1.0, roughness: 0.5
+					} );
+
+					const glassMaterial = new THREE.MeshPhysicalMaterial( {
+						color: 0xffffff, metalness: 0.25, roughness: 0, transmission: 1.0
+					} );
+
+					const carModel = gltf.scene.children[ 0 ];
+					carModel.scale.multiplyScalar( 4 );
+					carModel.rotation.y = Math.PI;
+
+					carModel.getObjectByName( 'body' ).material = bodyMaterial;
+
+					carModel.getObjectByName( 'rim_fl' ).material = detailsMaterial;
+					carModel.getObjectByName( 'rim_fr' ).material = detailsMaterial;
+					carModel.getObjectByName( 'rim_rr' ).material = detailsMaterial;
+					carModel.getObjectByName( 'rim_rl' ).material = detailsMaterial;
+					carModel.getObjectByName( 'trim' ).material = detailsMaterial;
+
+					carModel.getObjectByName( 'glass' ).material = glassMaterial;
+
+					// shadow
+					const mesh = new THREE.Mesh(
+						new THREE.PlaneGeometry( 0.655 * 4, 1.3 * 4 ),
+						new THREE.MeshBasicMaterial( {
+							map: shadow, blending: THREE.MultiplyBlending, toneMapped: false, transparent: true, premultipliedAlpha: true
+						} )
+					);
+					mesh.rotation.x = - Math.PI / 2;
+					carModel.add( mesh );
+
+					scene.add( carModel );
+
+				} );
+
+				//
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.inspector = new Inspector();
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				document.body.appendChild( renderer.domElement );
+
+				await renderer.init();
+
+				// use a cube map to avoid visual artifacts at the skybox's poles
+
+				const size = envMap.image.height;
+				const cubeRenderTarget = new THREE.CubeRenderTarget( size );
+				cubeRenderTarget.fromEquirectangularTexture( renderer, envMap );
+				const cubeMap = cubeRenderTarget.texture;
+
+				// grounded skybox
+
+				const geometry = new THREE.IcosahedronGeometry( 1, 16 );
+				const material = new THREE.MeshBasicNodeMaterial( { side: THREE.DoubleSide } );
+				material.colorNode = cubeTexture( cubeMap, getGroundProjectedNormal( float( params.radius ), float( params.height ) ) );
+
+				const skybox = new THREE.Mesh( geometry, material );
+				skybox.scale.setScalar( params.radius );
+				scene.add( skybox );
+
+				//
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 2, 0 );
+				controls.maxPolarAngle = THREE.MathUtils.degToRad( 90 );
+				controls.maxDistance = 80;
+				controls.minDistance = 20;
+				controls.enablePan = false;
+				controls.update();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				const gui = renderer.inspector.createParameters( 'Parameters' );
+				gui.add( params, 'enabled' ).name( 'Grounded' ).onChange( function ( value ) {
+
+					if ( value ) {
+
+						scene.add( skybox );
+						scene.background = null;
+
+					} else {
+
+						scene.remove( skybox );
+						scene.background = scene.environment;
+
+					}
+
+				} );
+
+			
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				renderer.render( scene, camera );
+
+			}
+		</script>
+	</body>
+</html>

粤ICP备19079148号