Skip to content

Examples: Add webgpu_reflection_roughness#31294

Merged
sunag merged 3 commits into
mrdoob:devfrom
sunag:dev-reflection-mipmaps
Jun 21, 2025
Merged

Examples: Add webgpu_reflection_roughness#31294
sunag merged 3 commits into
mrdoob:devfrom
sunag:dev-reflection-mipmaps

Conversation

@sunag

@sunag sunag commented Jun 20, 2025

Copy link
Copy Markdown
Collaborator

Description

Adds blurred reflection using mipmaps, which is good to give the reflection roughness effects.

image

@sunag sunag added this to the r178 milestone Jun 20, 2025
@Mugen87

Mugen87 commented Jun 21, 2025

Copy link
Copy Markdown
Collaborator

How about naming the example webgpu_reflection_roughness instead? To me, the reflection looks not dirty in the sense of filthy/smutty. I expected so see something different when reading the term _dirty^^.

@sunag sunag changed the title Examples: Add webgpu_reflection_dirty Examples: Add webgpu_reflection_roughness Jun 21, 2025
@sunag sunag marked this pull request as ready for review June 21, 2025 15:16
@sunag sunag merged commit e9518aa into mrdoob:dev Jun 21, 2025
18 of 19 checks passed
@sunag sunag deleted the dev-reflection-mipmaps branch June 21, 2025 15:16
@mrdoob

mrdoob commented Jun 30, 2025

Copy link
Copy Markdown
Owner

I tried adding the car to the scene and it broke 😇

Screen.Recording.2025-06-30.at.5.23.12.PM.mov

Something to do with the windshield being transmissive...

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgpu - roughness reflection</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> webgpu - roughness reflection
		</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';
			import { Fn, vec2, vec4, texture, uv, textureBicubic, rangeFogFactor, reflector, time } from 'three/tsl';

			import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';

			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
			import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';

			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

			import Stats from 'three/addons/libs/stats.module.js';

			let camera, scene, renderer;
			let controls;
			let stats;

			const wheels = [];

			init();

			async function init() {

				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.1, 100 );
				camera.position.set( 4.25, 1.4, - 4.5 );

				scene = new THREE.Scene();

				//

				new RGBELoader()
					.setPath( 'textures/equirectangular/' )
					.load( 'moonless_golf_1k.hdr', function ( texture ) {

						texture.mapping = THREE.EquirectangularReflectionMapping;

						scene.background = texture;
						scene.environment = texture;

					} );

				// textures

				const textureLoader = new THREE.TextureLoader();

				const uvMap = textureLoader.load( 'textures/uv_grid_directx.jpg' );
				uvMap.colorSpace = THREE.SRGBColorSpace;

				const perlinMap = textureLoader.load( './textures/noises/perlin/rgb-256x256.png' );
				perlinMap.wrapS = THREE.RepeatWrapping;
				perlinMap.wrapT = THREE.RepeatWrapping;
				perlinMap.colorSpace = THREE.SRGBColorSpace;

				// Car

				const dracoLoader = new DRACOLoader();
				dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );

				const loader = new GLTFLoader();
				loader.setDRACOLoader( dracoLoader );

				loader.load( 'models/gltf/ferrari.glb', function ( gltf ) {

					const carModel = gltf.scene.children[ 0 ];

					const bodyMaterial = new THREE.MeshPhysicalMaterial( {
						color: 0xff0000, metalness: 1.0, roughness: 0.5, clearcoat: 1.0, clearcoatRoughness: 0.03
					} );

					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
					} );

					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;

					wheels.push(
						carModel.getObjectByName( 'wheel_fl' ),
						carModel.getObjectByName( 'wheel_fr' ),
						carModel.getObjectByName( 'wheel_rl' ),
						carModel.getObjectByName( 'wheel_rr' )
					);

					scene.add( carModel );

				} );

				// reflection

				const reflection = reflector( { resolution: .5, bounces: false, generateMipmaps: true } ); // 0.5 is half of the rendering view
				reflection.target.rotateX( - Math.PI / 2 );
				scene.add( reflection.target );

				const animatedUV = uv().mul( 10 ).add( vec2( time.mul( .1 ), 0 ) );
				const roughness = texture( perlinMap, animatedUV ).r.mul( 2 ).saturate();

				const floorMaterial = new THREE.MeshStandardNodeMaterial();
				floorMaterial.transparent = true;
				floorMaterial.metalness = 1;
				floorMaterial.roughnessNode = roughness.mul( .2 );
				floorMaterial.colorNode = Fn( () => {

					// blur reflection using textureBicubic()
					const dirtyReflection = textureBicubic( reflection, roughness.mul( .9 ) );

					// falloff opacity by distance like an opacity-fog
					const opacity = rangeFogFactor( 7, 25 ).oneMinus();

					return vec4( dirtyReflection.rgb, opacity );

				} )();

				const floor = new THREE.Mesh( new THREE.BoxGeometry( 50, .001, 50 ), floorMaterial );
				floor.position.set( 0, 0, 0 );
				scene.add( floor );

				// renderer

				renderer = new THREE.WebGPURenderer();
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				renderer.setAnimationLoop( animate );
				renderer.toneMapping = THREE.NeutralToneMapping;
				renderer.toneMappingExposure = 2;
				document.body.appendChild( renderer.domElement );

				stats = new Stats();
				document.body.appendChild( stats.dom );

				controls = new OrbitControls( camera, renderer.domElement );
				controls.maxDistance = 9;
				controls.maxPolarAngle = THREE.MathUtils.degToRad( 90 );
				controls.target.set( 0, 0.5, 0 );
				controls.update();

				// events

				window.addEventListener( 'resize', onWindowResize );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			}

			function animate() {

				stats.update();

				controls.update();

				const time = - performance.now() / 1000;

				for ( let i = 0; i < wheels.length; i ++ ) {

					wheels[ i ].rotation.x = time * Math.PI * 2;

				}

				renderer.render( scene, camera );

			}

		</script>

	</body>
</html>

@mrdoob

mrdoob commented Jun 30, 2025

Copy link
Copy Markdown
Owner

Seems to be pretty delicate...

Screen.Recording.2025-06-30.at.5.49.06.PM.mov
<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgpu - roughness reflection</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> webgpu - roughness reflection
		</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';
			import { Fn, vec2, vec4, texture, uv, textureBicubic, rangeFogFactor, reflector, time } from 'three/tsl';

			import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
			import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';

			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

			import Stats from 'three/addons/libs/stats.module.js';

			let camera, scene, renderer;
			let controls;
			let stats;

			init();

			async function init() {

				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.25, 30 );
				camera.position.set( - 4, 2, 4 );
				camera.lookAt( 0, .4, 0 );

				scene = new THREE.Scene();

				//

				new RGBELoader()
					.setPath( 'textures/equirectangular/' )
					.load( 'moonless_golf_1k.hdr', function ( texture ) {

						texture.mapping = THREE.EquirectangularReflectionMapping;

						scene.background = texture;
						scene.environment = texture;

					} );

				// textures

				const textureLoader = new THREE.TextureLoader();

				const uvMap = textureLoader.load( 'textures/uv_grid_directx.jpg' );
				uvMap.colorSpace = THREE.SRGBColorSpace;

				// const perlinMap = textureLoader.load( './textures/noises/perlin/rgb-256x256.png' );
				const perlinMap = textureLoader.load( './textures/water.jpg' );
				perlinMap.wrapS = THREE.RepeatWrapping;
				perlinMap.wrapT = THREE.RepeatWrapping;
				perlinMap.colorSpace = THREE.SRGBColorSpace;

				// model

				const dracoLoader = new DRACOLoader();
				dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );

				const loader = new GLTFLoader();
				loader.setDRACOLoader( dracoLoader );
				loader.load( 'models/gltf/ShaderBall2.glb', function ( gltf ) {

					const model = gltf.scene;
					model.scale.setScalar( 5 )

					scene.add( model );

				} );

				// reflection

				const reflection = reflector( { resolution: .5, bounces: false, generateMipmaps: true } ); // 0.5 is half of the rendering view
				reflection.target.rotateX( - Math.PI / 2 );
				scene.add( reflection.target );

				const animatedUV = uv().mul( 5 ); // .add( vec2( time.mul( .1 ), 0 ) );
				const roughness = texture( perlinMap, animatedUV ).r.mul( 2 ).saturate();

				const floorMaterial = new THREE.MeshStandardNodeMaterial();
				floorMaterial.transparent = true;
				floorMaterial.metalness = 1;
				floorMaterial.roughnessNode = roughness.mul( .2 );
				floorMaterial.colorNode = Fn( () => {

					// blur reflection using textureBicubic()
					const dirtyReflection = textureBicubic( reflection, roughness.mul( .9 ) );

					// falloff opacity by distance like an opacity-fog
					const opacity = rangeFogFactor( 7, 25 ).oneMinus();

					return vec4( dirtyReflection.rgb, opacity );

				} )();

				const floor = new THREE.Mesh( new THREE.BoxGeometry( 50, .001, 50 ), floorMaterial );
				floor.position.set( 0, 0, 0 );
				scene.add( floor );

				// renderer

				renderer = new THREE.WebGPURenderer( { antialias: true } );
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				renderer.setAnimationLoop( animate );
				renderer.toneMapping = THREE.NeutralToneMapping;
				renderer.toneMappingExposure = 2;
				document.body.appendChild( renderer.domElement );

				stats = new Stats();
				document.body.appendChild( stats.dom );

				controls = new OrbitControls( camera, renderer.domElement );
				controls.minDistance = 1;
				controls.maxDistance = 10;
				controls.maxPolarAngle = Math.PI / 2;
				controls.autoRotate = true;
				controls.autoRotateSpeed = - .1;
				controls.target.set( 0, .5, 0 );
				controls.update();

				// events

				window.addEventListener( 'resize', onWindowResize );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			}

			function animate() {

				stats.update();

				controls.update();

				renderer.render( scene, camera );

			}

		</script>

	</body>
</html>

@mrdoob

mrdoob commented Jun 30, 2025

Copy link
Copy Markdown
Owner

And another issue: The cube in the reflection animates but the actual cube doesn't...

Screen.Recording.2025-06-30.at.6.14.58.PM.mov
<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgpu - roughness reflection</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> webgpu - roughness reflection
		</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';
			import { Fn, vec2, vec4, texture, uv, textureBicubic, rangeFogFactor, reflector, time } from 'three/tsl';

			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
			import { UltraHDRLoader } from 'three/addons/loaders/UltraHDRLoader.js';

			import Stats from 'three/addons/libs/stats.module.js';

			let camera, scene, renderer;
			let controls;
			let stats;

			init();

			async function init() {

				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.25, 30 );
				camera.position.set( - 4, 1, 4 );

				scene = new THREE.Scene();

				const loader = new UltraHDRLoader();
				loader.setDataType( THREE.HalfFloatType );
				loader.load( `textures/equirectangular/spruit_sunrise_2k.hdr.jpg`, function ( texture ) {

					texture.mapping = THREE.EquirectangularReflectionMapping;
					texture.needsUpdate = true;

					scene.background = texture;
					scene.environment = texture;

				} );

				// textures

				const textureLoader = new THREE.TextureLoader();

				const uvMap = textureLoader.load( 'textures/uv_grid_directx.jpg' );
				uvMap.colorSpace = THREE.SRGBColorSpace;

				const perlinMap = textureLoader.load( './textures/noises/perlin/rgb-256x256.png' );
				perlinMap.wrapS = THREE.RepeatWrapping;
				perlinMap.wrapT = THREE.RepeatWrapping;
				perlinMap.colorSpace = THREE.SRGBColorSpace;

				// uv box for debugging
				
				const mesh = new THREE.Mesh(
					new THREE.BoxGeometry( 1, 1, 1 ),
					new THREE.MeshStandardNodeMaterial( {
						map: uvMap,
						roughnessMap: uvMap,
						emissiveMap: uvMap,
						emissive: 0xffffff
					} )
				);
				mesh.position.set( 0, 1.5, 0 );
				mesh.scale.setScalar( 2 );
				scene.add( mesh );

				// reflection

				const reflection = reflector( { resolution: .5, bounces: false, generateMipmaps: true } ); // 0.5 is half of the rendering view
				reflection.target.rotateX( - Math.PI / 2 );
				scene.add( reflection.target );

				const animatedUV = uv().mul( 10 ).add( vec2( time.mul( .1 ), 0 ) );
				const roughness = texture( perlinMap, animatedUV ).r.mul( 2 ).saturate();

				const floorMaterial = new THREE.MeshStandardNodeMaterial();
				floorMaterial.transparent = true;
				floorMaterial.metalness = 1;
				floorMaterial.roughnessNode = roughness.mul( .2 );
				floorMaterial.colorNode = Fn( () => {

					// blur reflection using textureBicubic()
					const dirtyReflection = textureBicubic( reflection, roughness.mul( .9 ) );

					// falloff opacity by distance like an opacity-fog
					const opacity = rangeFogFactor( 7, 25 ).oneMinus();

					return vec4( dirtyReflection.rgb, opacity );

				} )();

				const floor = new THREE.Mesh( new THREE.BoxGeometry( 50, .001, 50 ), floorMaterial );
				floor.position.set( 0, 0, 0 );
				scene.add( floor );

				// renderer

				renderer = new THREE.WebGPURenderer( { antialias: true } );
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				renderer.setAnimationLoop( animate );
				renderer.toneMapping = THREE.NeutralToneMapping;
				renderer.toneMappingExposure = 1.5;
				document.body.appendChild( renderer.domElement );

				stats = new Stats();
				document.body.appendChild( stats.dom );

				controls = new OrbitControls( camera, renderer.domElement );
				controls.minDistance = 1;
				controls.maxDistance = 10;
				controls.maxPolarAngle = Math.PI / 2;
				controls.autoRotate = true;
				controls.autoRotateSpeed = - .1;
				controls.target.set( 0, .75, 0 );
				controls.update();

				// events

				window.addEventListener( 'resize', onWindowResize );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			}

			function animate( time ) {

				stats.update();

				controls.update();

				const mesh = scene.children[ 1 ];
				mesh.position.y = Math.sin( time * .0005 );

				renderer.render( scene, camera );

			}

		</script>

	</body>
</html>

@sunag

sunag commented Jun 30, 2025

Copy link
Copy Markdown
Collaborator Author

I think it's related with roughness.mul( .9 )? Maybe we can lower it to .5 until we have a more accurate formula.

// blur reflection using textureBicubic()
const roughnessStrength = .5;
const dirtyReflection = textureBicubic( reflection, roughness.mul( roughnessStrength ) );

@sunag

sunag commented Jun 30, 2025

Copy link
Copy Markdown
Collaborator Author

reflector() seems to have an incompatibility with materials that use transmission. I set the resolution to 1 for the reflector and it worked, except for transmission. I suspect that it is caching the texture used in the reflector transmission.

image

@mrdoob

mrdoob commented Jun 30, 2025

Copy link
Copy Markdown
Owner

Could ssr() support roughness too?

This example would look much better:
https://threejs.org/examples/#webgpu_postprocessing_ssr

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants