Skip to content

WebGPURenderer: Introduce ProjectorLight #31022

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 10 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@
"webgpu_lights_ies_spotlight",
"webgpu_lights_phong",
"webgpu_lights_physical",
"webgpu_lights_projector",
"webgpu_lights_rectarealight",
"webgpu_lights_selective",
"webgpu_lights_spotlight",
Expand Down
Binary file added examples/screenshots/webgpu_lights_projector.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
297 changes: 297 additions & 0 deletions examples/webgpu_lights_projector.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgpu - projector light</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 - projector light<br />
</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>

<video id="video" loop muted crossOrigin="anonymous" playsinline style="display:none">
<source src="textures/sintel.ogv" type='video/ogg; codecs="theora, vorbis"'>
<source src="textures/sintel.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
</video>

<script type="module">

import * as THREE from 'three';
import { Fn, color, mx_worley_noise_float, time } from 'three/tsl';

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

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

let renderer, scene, camera;

let projectorLight, lightHelper;

init();

function init() {

// Renderer

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

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;

scene = new THREE.Scene();

camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.set( 7, 4, 1 );

// Controls

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

// Textures

const loader = new THREE.TextureLoader().setPath( 'textures/' );

// Lights

const causticEffect = Fn( ( [ projectorUV ] ) => {

const waterLayer0 = mx_worley_noise_float( projectorUV.mul( 10 ).add( time ) );

const caustic = waterLayer0.mul( color( 0x5abcd8 ) ).mul( 2 );

return caustic;

} );


const ambient = new THREE.HemisphereLight( 0xffffff, 0x8d8d8d, 0.15 );
scene.add( ambient );

projectorLight = new THREE.ProjectorLight( 0xffffff, 100 );
projectorLight.colorNode = causticEffect;
projectorLight.position.set( 2.5, 5, 2.5 );
projectorLight.angle = Math.PI / 6;
projectorLight.penumbra = 1;
projectorLight.decay = 2;
projectorLight.distance = 0;

projectorLight.castShadow = true;
projectorLight.shadow.mapSize.width = 1024;
projectorLight.shadow.mapSize.height = 1024;
projectorLight.shadow.camera.near = 1;
projectorLight.shadow.camera.far = 10;
projectorLight.shadow.focus = 1;
projectorLight.shadow.bias = - .003;
scene.add( projectorLight );

lightHelper = new THREE.SpotLightHelper( projectorLight );
scene.add( lightHelper );

//

const geometry = new THREE.PlaneGeometry( 200, 200 );
const material = new THREE.MeshLambertMaterial( { color: 0xbcbcbc } );

const mesh = new THREE.Mesh( geometry, material );
mesh.position.set( 0, - 1, 0 );
mesh.rotation.x = - Math.PI / 2;
mesh.receiveShadow = true;
scene.add( mesh );

// Models

new PLYLoader().load( 'models/ply/binary/Lucy100k.ply', function ( geometry ) {

geometry.scale( 0.0024, 0.0024, 0.0024 );
geometry.computeVertexNormals();

const material = new THREE.MeshLambertMaterial();

const mesh = new THREE.Mesh( geometry, material );
mesh.rotation.y = - Math.PI / 2;
mesh.position.y = 0.8;
mesh.castShadow = true;
mesh.receiveShadow = true;
scene.add( mesh );

} );

window.addEventListener( 'resize', onWindowResize );

// GUI

const gui = new GUI();

const params = {
type: 'procedural',
color: projectorLight.color.getHex(),
intensity: projectorLight.intensity,
distance: projectorLight.distance,
angle: projectorLight.angle,
penumbra: projectorLight.penumbra,
decay: projectorLight.decay,
focus: projectorLight.shadow.focus,
shadows: true,
};

let videoTexture, mapTexture;

gui.add( params, 'type', [ 'procedural', 'video', 'texture' ] ).onChange( function ( val ) {

projectorLight.colorNode = null;
projectorLight.map = null;

if ( val === 'procedural' ) {

projectorLight.colorNode = causticEffect;

focus.setValue( 1 );

} else if ( val === 'video' ) {

if ( videoTexture === undefined ) {

const video = document.getElementById( 'video' );
video.play();

videoTexture = new THREE.VideoTexture( video );

}

projectorLight.map = videoTexture;

focus.setValue( .46 );

} else if ( val === 'texture' ) {

mapTexture = loader.load( 'colors.png' );
mapTexture.minFilter = THREE.LinearFilter;
mapTexture.magFilter = THREE.LinearFilter;
mapTexture.generateMipmaps = false;
mapTexture.colorSpace = THREE.SRGBColorSpace;

projectorLight.map = mapTexture;

focus.setValue( 1 );

}

} );

gui.addColor( params, 'color' ).onChange( function ( val ) {

projectorLight.color.setHex( val );

} );

gui.add( params, 'intensity', 0, 500 ).onChange( function ( val ) {

projectorLight.intensity = val;

} );


gui.add( params, 'distance', 0, 20 ).onChange( function ( val ) {

projectorLight.distance = val;

} );

gui.add( params, 'angle', 0, Math.PI / 3 ).onChange( function ( val ) {

projectorLight.angle = val;

} );

gui.add( params, 'penumbra', 0, 1 ).onChange( function ( val ) {

projectorLight.penumbra = val;

} );

gui.add( params, 'decay', 1, 2 ).onChange( function ( val ) {

projectorLight.decay = val;

} );

const focus = gui.add( params, 'focus', 0, 1 ).onChange( function ( val ) {

projectorLight.shadow.focus = val;

} );

gui.add( params, 'shadows' ).onChange( function ( val ) {

renderer.shadowMap.enabled = val;

scene.traverse( function ( child ) {

if ( child.material ) {

child.material.needsUpdate = true;

}

} );

} );

gui.open();

}

function onWindowResize() {

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

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

}

function animate() {

const time = performance.now() / 3000;

projectorLight.position.x = Math.cos( time ) * 2.5;
projectorLight.position.z = Math.sin( time ) * 2.5;

lightHelper.update();

renderer.render( scene, camera );

}

</script>

</body>

</html>
35 changes: 0 additions & 35 deletions examples/webgpu_lights_spotlight.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
<script type="module">

import * as THREE from 'three';
import { Fn, vec2, length, uniform, abs, max, min, sub, div, saturate, acos } from 'three/tsl';

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

Expand Down Expand Up @@ -95,30 +94,6 @@
const ambient = new THREE.HemisphereLight( 0xffffff, 0x8d8d8d, 0.15 );
scene.add( ambient );

const boxAttenuationFn = Fn( ( [ lightNode ], builder ) => {

const light = lightNode.light;

const sdBox = Fn( ( [ p, b ] ) => {

const d = vec2( abs( p ).sub( b ) ).toVar();

return length( max( d, 0.0 ) ).add( min( max( d.x, d.y ), 0.0 ) );

} );

const penumbraCos = uniform( 'float' ).onRenderUpdate( () => Math.min( Math.cos( light.angle * ( 1 - light.penumbra ) ), .99999 ) );
const spotLightCoord = lightNode.getSpotLightCoord( builder );
const coord = spotLightCoord.xyz.div( spotLightCoord.w );

const boxDist = sdBox( coord.xy.sub( vec2( 0.5 ) ), vec2( 0.5 ) );
const angleFactor = div( -1.0, sub( 1.0, acos( penumbraCos ) ).sub( 1.0 ) );
const attenuation = saturate( boxDist.mul( - 2.0 ).mul( angleFactor ) );

return attenuation;

} );

spotLight = new THREE.SpotLight( 0xffffff, 100 );
spotLight.map = textures[ 'disturb.jpg' ];
spotLight.position.set( 2.5, 5, 2.5 );
Expand Down Expand Up @@ -252,16 +227,6 @@

} );

gui.add( params, 'customAttenuation' ).name( 'custom attenuation' ).onChange( function ( val ) {

spotLight.attenuationNode = val ? boxAttenuationFn : null;

aspectGUI.setValue( 1 ).enable( val );

} );

const aspectGUI = gui.add( spotLight.shadow, 'aspect', 0, 2 ).enable( false );

gui.open();

}
Expand Down
1 change: 1 addition & 0 deletions src/Three.WebGPU.Nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { default as StorageBufferAttribute } from './renderers/common/StorageBuf
export { default as StorageInstancedBufferAttribute } from './renderers/common/StorageInstancedBufferAttribute.js';
export { default as IndirectStorageBufferAttribute } from './renderers/common/IndirectStorageBufferAttribute.js';
export { default as IESSpotLight } from './lights/webgpu/IESSpotLight.js';
export { default as ProjectorLight } from './lights/webgpu/ProjectorLight.js';
export { default as NodeLoader } from './loaders/nodes/NodeLoader.js';
export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js';
export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoader.js';
Expand Down
1 change: 1 addition & 0 deletions src/Three.WebGPU.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { default as StorageBufferAttribute } from './renderers/common/StorageBuf
export { default as StorageInstancedBufferAttribute } from './renderers/common/StorageInstancedBufferAttribute.js';
export { default as IndirectStorageBufferAttribute } from './renderers/common/IndirectStorageBufferAttribute.js';
export { default as IESSpotLight } from './lights/webgpu/IESSpotLight.js';
export { default as ProjectorLight } from './lights/webgpu/ProjectorLight.js';
export { default as NodeLoader } from './loaders/nodes/NodeLoader.js';
export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js';
export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoader.js';
Expand Down
Loading