Skip to content

Examples: Add webgpu_reflection_roughness #31294

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 21, 2025

Conversation

sunag
Copy link
Collaborator

@sunag sunag commented Jun 20, 2025

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
Copy link
Collaborator

Mugen87 commented Jun 21, 2025

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
Copy link
Owner

mrdoob commented Jun 30, 2025

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
Copy link
Owner

mrdoob commented Jun 30, 2025

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
Copy link
Owner

mrdoob commented Jun 30, 2025

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
Copy link
Collaborator Author

sunag commented Jun 30, 2025

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
Copy link
Collaborator Author

sunag commented Jun 30, 2025

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
Copy link
Owner

mrdoob commented Jun 30, 2025

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