Skip to content

Examples: Add webgpu_reflection_blurred #31116

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 12 commits into from
May 15, 2025
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@
"webgpu_postprocessing",
"webgpu_procedural_texture",
"webgpu_reflection",
"webgpu_reflection_blurred",
"webgpu_refraction",
"webgpu_rendertarget_2d-array_3d",
"webgpu_rtt",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
234 changes: 234 additions & 0 deletions examples/webgpu_reflection_blurred.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgpu - blurred 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 - blurred 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, vec4, fract, abs, uniform, pow, color, max, length, rangeFogFactor, sub, reflector, normalWorld, hue, time, mix, positionWorld } from 'three/tsl';

import { hashBlur } from 'three/addons/tsl/display/hashBlur.js';

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

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

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

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

let camera, scene, renderer;
let model, mixer, clock;
let controls;
let stats;
let gui;

init();

async function init() {

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

scene = new THREE.Scene();
scene.backgroundNode = hue( normalWorld.y.mix( 0, color( 0x0066ff ) ).mul( .1 ), time );

const waterAmbientLight = new THREE.HemisphereLight( 0xffffff, 0x0066ff, 10 );
scene.add( waterAmbientLight );

clock = new THREE.Clock();

// animated model

const gltfLoader = new GLTFLoader();
gltfLoader.load( 'models/gltf/Michelle.glb', function ( gltf ) {

model = gltf.scene;
model.children[ 0 ].children[ 0 ].castShadow = true;

mixer = new THREE.AnimationMixer( model );

const action = mixer.clipAction( gltf.animations[ 0 ] );
action.play();

scene.add( model );

} );

// textures

const textureLoader = new THREE.TextureLoader();

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

// uv map for debugging

const uvMaterial = new THREE.MeshStandardNodeMaterial( {
map: uvMap,
side: THREE.DoubleSide
} );

const uvMesh = new THREE.Mesh( new THREE.PlaneGeometry( 2, 2 ), uvMaterial );
uvMesh.position.set( 0, 1, - 3 );
scene.add( uvMesh );

// circle effect

const drawCircle = Fn( ( [ pos, radius, width, power, color, timer = time.mul( .5 ) ] ) => {

// https://www.shadertoy.com/view/3tdSRn

const dist1 = length( pos );
dist1.assign( fract( dist1.mul( 5.0 ).sub( fract( timer ) ) ) );
const dist2 = dist1.sub( radius );
const intensity = pow( radius.div( abs( dist2 ) ), width );
const col = color.rgb.mul( intensity ).mul( power ).mul( max( sub( 0.8, abs( dist2 ) ), 0.0 ) );

return col;

} );

const circleFadeY = positionWorld.y.mul( .7 ).oneMinus().max( 0 );
const animatedColor = mix( color( 0x74ccf4 ), color( 0x7f00c5 ), positionWorld.xz.distance( 0 ).div( 10 ).clamp() );
const animatedCircle = hue( drawCircle( positionWorld.xz.mul( .1 ), 0.5, 0.8, .01, animatedColor ).mul( circleFadeY ), time );

const floorLight = new THREE.PointLight( 0xffffff );
floorLight.colorNode = animatedCircle.mul( 50 );
scene.add( floorLight );

// reflection

const roughness = uniform( .9 );
const radius = uniform( 0.2 );

const reflection = reflector( { resolution: 1, depth: true, bounces: false } ); // 0.5 is half of the rendering view
Copy link
Collaborator

@Mugen87 Mugen87 May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example looks awesome!

The only thing I would suggest to change is the default resolution of the reflector. On my macMini M2 Pro, the current example runs around 30 FPS. Using the half resolution for the reflection, the frame rate is back to 60 FPS. Since the reflections are blurred, you barely see a difference in quality anyway (mostly at the plane with the uv texture).

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we set resolution to 0.5 on a non-retina screen the reflection becomes pixelated:

Screenshot 2025-05-16 at 5 04 41 PM

Instead of 0.5 maybe something like 1 / devicePixelRatio would work better?

const reflectionDepth = reflection.getDepthNode();
reflection.target.rotateX( - Math.PI / 2 );
scene.add( reflection.target );

const floorMaterial = new THREE.MeshStandardNodeMaterial();
floorMaterial.transparent = true;
floorMaterial.colorNode = Fn( () => {

// ranges adjustment

const radiusRange = mix( 0.01, 0.1, radius ); // range [ 0.01, 0.1 ]
const roughnessRange = mix( 0.3, 0.03, roughness ); // range [ 0.03, 0.3 ]

// blur the reflection

const reflectionBlurred = hashBlur( reflection, radiusRange, {
mask: reflectionDepth,
premultipliedAlpha: true
} );

// reflection composite

const reflectionMask = reflectionBlurred.a.mul( reflectionDepth ).remapClamp( 0, roughnessRange );
const reflectionItensity = .1;
const reflectionMixFactor = reflectionMask.mul( roughness.mul( 2 ).min( 1 ) );
const reflectionFinal = mix( reflection.rgb, reflectionBlurred.rgb, reflectionMixFactor ).mul( reflectionItensity );

// mix reflection with animated circle

const output = animatedCircle.add( reflectionFinal );

// falloff opacity by distance like an opacity-fog

const opacity = rangeFogFactor( 7, 25 ).oneMinus();

// final output

return vec4( output, 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.3;
document.body.appendChild( renderer.domElement );

gui = new GUI();
gui.add( roughness, 'value', 0, 1 ).name( 'roughness' );
gui.add( radius, 'value', 0, 1 ).name( 'radius' );

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

const delta = clock.getDelta();

if ( model ) {

mixer.update( delta );

}

renderer.render( scene, camera );

}

</script>
</body>
</html>