Browse Source

Examples: Add ground projected env map (#24311)

* Add ground projected env map

* fix: Update screenshot

* fix: Fix imports

* fix: reset build and lock files

* Update webgl_materials_envmaps_ground-projected.html

Update title.

* fix: lint

* feat: Add car to and limit GPU/camera in ground projected env example

* Reset files.json

* add webgl_materials_envmaps_ground-projected to files.json

* fix: glsl formatting in GroundProjectedEnv class

* Update screenshot for webgl_materials_envmaps_ground-projected

* lint fix

* Restrict controls furthur

* Lint GLSL and example

* Add rotation option; Move class to examples/objects

* remove rotation option

Co-authored-by: Michael Herzog <[email protected]>
Faraz Shaikh 3 years ago
parent
commit
da167b1386

+ 1 - 0
examples/files.json

@@ -141,6 +141,7 @@
 		"webgl_materials_displacementmap",
 		"webgl_materials_envmaps",
 		"webgl_materials_envmaps_exr",
+		"webgl_materials_envmaps_ground-projected",
 		"webgl_materials_envmaps_hdr",
 		"webgl_materials_lightmap",
 		"webgl_materials_matcap",

+ 186 - 0
examples/jsm/objects/GroundProjectedEnv.js

@@ -0,0 +1,186 @@
+import { Mesh, IcosahedronGeometry, ShaderMaterial, DoubleSide } from 'three';
+
+/**
+ * Ground projected env map adapted from @react-three/drei.
+ * https://github.com/pmndrs/drei/blob/master/src/core/Environment.tsx
+ */
+export class GroundProjectedEnv extends Mesh {
+
+	constructor( texture, options ) {
+
+		const isCubeMap = texture.isCubeTexture;
+		const w =
+			( isCubeMap ? texture.image[ 0 ]?.width : texture.image.width ) ?? 1024;
+		const cubeSize = w / 4;
+		const _lodMax = Math.floor( Math.log2( cubeSize ) );
+		const _cubeSize = Math.pow( 2, _lodMax );
+		const width = 3 * Math.max( _cubeSize, 16 * 7 );
+		const height = 4 * _cubeSize;
+
+		const defines = [
+			isCubeMap ? '#define ENVMAP_TYPE_CUBE' : '',
+			`#define CUBEUV_TEXEL_WIDTH ${1.0 / width}`,
+			`#define CUBEUV_TEXEL_HEIGHT ${1.0 / height}`,
+			`#define CUBEUV_MAX_MIP ${_lodMax}.0`,
+		];
+
+		const vertexShader = /* glsl */ `
+        varying vec3 vWorldPosition;
+
+        void main() 
+        {
+
+            vec4 worldPosition = ( modelMatrix * vec4( position, 1.0 ) );
+            vWorldPosition = worldPosition.xyz;
+            
+            gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+
+        }
+        `;
+		const fragmentShader = defines.join( '\n' ) + /* glsl */ `
+        #define ENVMAP_TYPE_CUBE_UV
+
+        varying vec3 vWorldPosition;
+
+        uniform float radius;
+        uniform float height;
+        uniform float angle;
+
+        #ifdef ENVMAP_TYPE_CUBE
+
+            uniform samplerCube map;
+
+        #else
+
+            uniform sampler2D map;
+
+        #endif
+
+        // From: https://www.shadertoy.com/view/4tsBD7
+        float diskIntersectWithBackFaceCulling( vec3 ro, vec3 rd, vec3 c, vec3 n, float r ) 
+        {
+
+            float d = dot ( rd, n );
+            
+            if( d > 0.0 ) { return 1e6; }
+            
+            vec3  o = ro - c;
+            float t = - dot( n, o ) / d;
+            vec3  q = o + rd * t;
+            
+            return ( dot( q, q ) < r * r ) ? t : 1e6;
+
+        }
+
+        // From: https://www.iquilezles.org/www/articles/intersectors/intersectors.htm
+        float sphereIntersect( vec3 ro, vec3 rd, vec3 ce, float ra ) 
+        {
+
+            vec3 oc = ro - ce;
+            float b = dot( oc, rd );
+            float c = dot( oc, oc ) - ra * ra;
+            float h = b * b - c;
+            
+            if( h < 0.0 ) { return -1.0; }
+            
+            h = sqrt( h );
+            
+            return - b + h;
+
+        }
+
+        vec3 project() 
+        {
+
+            vec3 p = normalize( vWorldPosition );
+            vec3 camPos = cameraPosition;
+            camPos.y -= height;
+
+            float intersection = sphereIntersect( camPos, p, vec3( 0.0 ), radius );
+            if( intersection > 0.0 ) {
+                
+                vec3 h = vec3( 0.0, - height, 0.0 );
+                float intersection2 = diskIntersectWithBackFaceCulling( camPos, p, h, vec3( 0.0, 1.0, 0.0 ), radius );
+                p = ( camPos + min( intersection, intersection2 ) * p ) / radius;
+
+            } else {
+
+                p = vec3( 0.0, 1.0, 0.0 );
+
+            }
+
+            return p;
+
+        }
+
+        #include <common>
+        #include <cube_uv_reflection_fragment>
+
+        void main() 
+        {
+
+            vec3 projectedWorldPosition = project();
+            
+            #ifdef ENVMAP_TYPE_CUBE
+
+                vec3 outcolor = textureCube( map, projectedWorldPosition ).rgb;
+
+            #else
+
+                vec3 direction = normalize( projectedWorldPosition );
+                vec2 uv = equirectUv( direction );
+                vec3 outcolor = texture2D( map, uv ).rgb;
+
+            #endif
+
+            gl_FragColor = vec4( outcolor, 1.0 );
+
+            #include <tonemapping_fragment>
+            #include <encodings_fragment>
+
+        }
+        `;
+
+		const uniforms = {
+			map: { value: texture },
+			height: { value: options?.height || 15 },
+			radius: { value: options?.radius || 100 },
+		};
+
+		const geometry = new IcosahedronGeometry( 1, 16 );
+		const material = new ShaderMaterial( {
+			uniforms,
+			fragmentShader,
+			vertexShader,
+			side: DoubleSide,
+		} );
+
+		super( geometry, material );
+
+	}
+
+	set radius( radius ) {
+
+		this.material.uniforms.radius.value = radius;
+
+	}
+
+	get radius() {
+
+		return this.material.uniforms.radius.value;
+
+	}
+
+	set height( height ) {
+
+		this.material.uniforms.height.value = height;
+
+	}
+
+	get height() {
+
+		return this.material.uniforms.height.value;
+
+	}
+
+}

BIN
examples/screenshots/webgl_materials_envmaps_ground-projected.jpg


BIN
examples/textures/cube/lake/nx.png


BIN
examples/textures/cube/lake/ny.png


BIN
examples/textures/cube/lake/nz.png


BIN
examples/textures/cube/lake/px.png


BIN
examples/textures/cube/lake/py.png


BIN
examples/textures/cube/lake/pz.png


+ 333 - 0
examples/webgl_materials_envmaps_ground-projected.html

@@ -0,0 +1,333 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>
+			threejs webgl - 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"
+		/>
+		<link type="text/css" rel="stylesheet" href="main.css" />
+	</head>
+	<body>
+		<div id="container"></div>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">threejs</a> -
+			Ground projected environment mapping. By
+			<a href="https://twitter.com/CantBeFaraz" target="_blank" rel="noopener"
+				>Faraz Shaikh</a
+			>.
+			<br> 
+			Ferrari 458 Italia model by <a href="https://sketchfab.com/models/57bf6cc56931426e87494f554df1dab6" target="_blank" rel="noopener">vicent091036</a>
+		</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"
+				}
+			}
+		</script>
+
+		<script type="module">
+			import * as THREE from 'three';
+
+			import Stats from './jsm/libs/stats.module.js';
+
+			import { GUI } from './jsm/libs/lil-gui.module.min.js';
+			import { OrbitControls } from './jsm/controls/OrbitControls.js';
+			import { GroundProjectedEnv } from './jsm/objects/GroundProjectedEnv.js';
+			import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
+			import { DRACOLoader } from './jsm/loaders/DRACOLoader.js';
+			import { FlakesTexture } from './jsm/textures/FlakesTexture.js';
+
+			const params = {
+				height: 34,
+				radius: 440,
+				toneMappingExposure: 1
+			};
+
+			let camera, scene, renderer, stats, env,dirLight;
+
+			init();
+			animate();
+
+			function init() {
+
+				initScene();
+				initMisc();
+
+				document.body.appendChild( renderer.domElement );
+				window.addEventListener( 'resize', onWindowResize );
+			
+			}
+
+			function initScene() {
+
+				camera = new THREE.PerspectiveCamera(
+					45,
+					window.innerWidth / window.innerHeight,
+					1,
+					1000
+				);
+				camera.position.set( - 1, 0.3, 1 ).multiplyScalar( 25 );
+
+				scene = new THREE.Scene();
+
+				dirLight = new THREE.DirectionalLight( 0xffffff, 0.2 );
+				dirLight.position.set( 10, 8, 10 );
+				dirLight.castShadow = true;
+				dirLight.shadow.camera.near = 1;
+				dirLight.shadow.camera.far = 100;
+				dirLight.shadow.camera.right = 150;
+				dirLight.shadow.camera.left = - 150;
+				dirLight.shadow.camera.top = 150;
+				dirLight.shadow.camera.bottom = - 150;
+				dirLight.shadow.mapSize.width = 1024;
+				dirLight.shadow.mapSize.height = 1024;
+				scene.add( dirLight );
+
+				const geometry = new THREE.PlaneGeometry( 1, 1 );
+				const material = new THREE.ShadowMaterial( { opacity: 0.3 } );
+
+				const ground = new THREE.Mesh( geometry, material );
+				ground.scale.setScalar( 1000 );
+				ground.rotation.x = - Math.PI / 2;
+				ground.position.y = - 0.001;
+				ground.castShadow = false;
+				ground.receiveShadow = true;
+				scene.add( ground );
+
+				const cubeLoader = new THREE.CubeTextureLoader();
+				cubeLoader.setPath( 'textures/cube/lake/' );
+
+				const textureCube = cubeLoader.load( [
+					'px.png',
+					'nx.png',
+					'py.png',
+					'ny.png',
+					'pz.png',
+					'nz.png',
+				] );
+
+				env = new GroundProjectedEnv( textureCube );
+				env.scale.setScalar( 100 );
+				scene.add( env );
+
+				scene.background = textureCube;
+				scene.environment = textureCube;
+
+				const dracoLoader = new DRACOLoader();
+				dracoLoader.setDecoderPath( 'js/libs/draco/gltf/' );
+
+				const loader = new GLTFLoader();
+				loader.setDRACOLoader( dracoLoader );
+
+				const normalMap3 = new THREE.CanvasTexture( new FlakesTexture() );
+				normalMap3.wrapS = THREE.RepeatWrapping;
+				normalMap3.wrapT = THREE.RepeatWrapping;
+				normalMap3.repeat.x = 10;
+				normalMap3.repeat.y = 6;
+				normalMap3.anisotropy = 16;
+
+				const bodyMaterial = new THREE.MeshPhysicalMaterial( {
+					clearcoat: 1.0,
+					clearcoatRoughness: 0.1,
+					metalness: 1,
+					roughness: 0.4,
+					color: 0xff2800,
+					normalMap: normalMap3,
+					normalScale: new THREE.Vector2( 0.15, 0.15 ),
+				} );
+
+				const wheelMaterial = new THREE.MeshPhysicalMaterial( {
+					clearcoat: 1.0,
+					clearcoatRoughness: 0.1,
+					metalness: 0.9,
+					roughness: 0.5,
+					color: '#080808',
+					normalMap: normalMap3,
+					normalScale: new THREE.Vector2( 0.15, 0.15 ),
+				} );
+
+				const yellowMaterial = new THREE.MeshPhysicalMaterial( {
+					clearcoat: 1.0,
+					clearcoatRoughness: 0.1,
+					metalness: 1,
+					roughness: 0.2,
+					color: '#e66b00',
+				} );
+
+				const lightsMaterial = new THREE.MeshPhysicalMaterial( {
+					emissive: '#ffffff',
+					color: 'white',
+				} );
+
+				const chromeMaterial = new THREE.MeshPhysicalMaterial( {
+					clearcoat: 1.0,
+					clearcoatRoughness: 0.1,
+					metalness: 0.9,
+					roughness: 0.5,
+					color: 0xffffff,
+				} );
+
+				const detailsMaterial = new THREE.MeshStandardMaterial( {
+					clearcoat: 1.0,
+					clearcoatRoughness: 0.1,
+					metalness: 0.9,
+					roughness: 0.5,
+					color: 0xffffff,
+				} );
+
+				const glassMaterial = new THREE.MeshPhysicalMaterial( {
+					color: 0xffffff,
+					metalness: 0.25,
+					roughness: 0,
+					transmission: 1.0,
+					clearcoat: 1.0,
+					clearcoatRoughness: 0,
+				} );
+
+				const shadow = new THREE.TextureLoader().load(
+					'models/gltf/ferrari_ao.png'
+				);
+
+				loader.load( 'models/gltf/ferrari.glb', function ( gltf ) {
+
+					gltf.scene.scale.setScalar( 6 );
+
+					const box = new THREE.Box3().setFromObject( gltf.scene );
+					gltf.scene.position.y = - box.min.y;
+					gltf.scene.rotation.y = THREE.MathUtils.degToRad( 180 );
+
+					gltf.scene.traverse( ( obj ) => {
+
+						if ( obj.isMesh ) {
+
+							obj.castShadow = obj.recieveShadow = true;
+
+						}
+
+					} );
+
+					gltf.scene.getObjectByName( 'body' ).material = bodyMaterial;
+
+					gltf.scene.getObjectByName( 'rim_fl' ).material = detailsMaterial;
+					gltf.scene.getObjectByName( 'rim_fr' ).material = detailsMaterial;
+					gltf.scene.getObjectByName( 'rim_rr' ).material = detailsMaterial;
+					gltf.scene.getObjectByName( 'rim_rl' ).material = detailsMaterial;
+					gltf.scene.getObjectByName( 'trim' ).material = detailsMaterial;
+
+					gltf.scene.getObjectByName( 'glass' ).material = glassMaterial;
+
+					gltf.scene.getObjectByName( 'wheel' ).material = wheelMaterial;
+					gltf.scene.getObjectByName( 'wheel_1' ).material = wheelMaterial;
+					gltf.scene.getObjectByName( 'wheel_2' ).material = wheelMaterial;
+					gltf.scene.getObjectByName( 'wheel_3' ).material = wheelMaterial;
+					gltf.scene.getObjectByName( 'brake' ).material = wheelMaterial;
+					gltf.scene.getObjectByName( 'interior_dark' ).material = wheelMaterial;
+					gltf.scene.getObjectByName( 'brake_1' ).material = wheelMaterial;
+					gltf.scene.getObjectByName( 'brake_2' ).material = wheelMaterial;
+					gltf.scene.getObjectByName( 'brake_3' ).material = wheelMaterial;
+
+					gltf.scene.getObjectByName( 'yellow_trim' ).material = yellowMaterial;
+					gltf.scene.getObjectByName( 'lights' ).material = lightsMaterial;
+					gltf.scene.getObjectByName( 'chrome' ).material = chromeMaterial;
+
+					gltf.scene.getObjectByName( 'wheel_fl' ).rotation.z =
+						THREE.MathUtils.degToRad( - 30 );
+					gltf.scene.getObjectByName( 'wheel_fr' ).rotation.z =
+						THREE.MathUtils.degToRad( - 30 );
+
+					// 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,
+						} )
+					);
+					mesh.rotation.x = - Math.PI / 2;
+					mesh.renderOrder = 2;
+					gltf.scene.add( mesh );
+
+					scene.add( gltf.scene );
+
+				} );
+
+			}
+
+			function initMisc() {
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.shadowMap.enabled = true;
+				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				
+				// Mouse control
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 0, 0 );
+				controls.maxPolarAngle = THREE.MathUtils.degToRad( 80 );
+				controls.maxDistance = 100;
+				controls.minDistance = 30;
+				controls.enablePan = false;
+				controls.update();
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				const gui = new GUI();
+				gui.add( params, 'height', 20, 50, 0.1 );
+				gui.add( params, 'radius', 200, 600, 0.1 );
+				gui.add( renderer, 'toneMappingExposure', 0, 2, 0.1 ).name( 'exposure' );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+				render();
+
+				stats.update();
+
+			}
+
+			function renderScene() {
+
+				renderer.render( scene, camera );
+
+			}
+
+			function render() {
+
+				renderScene();
+
+				env.radius = params.radius;
+				env.height = params.height;
+
+			}
+		</script>
+	</body>
+</html>