Skip to content

Commit 6224432

Browse files
authored
WebGPURenderer: Introduce ProjectorLight (#31022)
* Texture: Introduce `width`, `height`, `depth` * Fix `lightShadowMatrix()` if `renderer.shadowMap.enabled` is `false` * Remove `spotLight.attenuationNode` * Introduce `ProjectorLight` * add `webgpu_lights_projector` example * Update webgpu_lights_projector.jpg * improve cache key * optimize for mobile * Update webgpu_lights_projector.jpg
1 parent f00d971 commit 6224432

15 files changed

+467
-48
lines changed

examples/files.json

+1
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@
342342
"webgpu_lights_ies_spotlight",
343343
"webgpu_lights_phong",
344344
"webgpu_lights_physical",
345+
"webgpu_lights_projector",
345346
"webgpu_lights_rectarealight",
346347
"webgpu_lights_selective",
347348
"webgpu_lights_spotlight",
62.7 KB
Loading

examples/webgpu_lights_projector.html

+297
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>three.js webgpu - projector light</title>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
7+
<link type="text/css" rel="stylesheet" href="main.css">
8+
</head>
9+
<body>
10+
11+
<div id="info">
12+
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - projector light<br />
13+
</div>
14+
15+
<script type="importmap">
16+
{
17+
"imports": {
18+
"three": "../build/three.webgpu.js",
19+
"three/webgpu": "../build/three.webgpu.js",
20+
"three/tsl": "../build/three.tsl.js",
21+
"three/addons/": "./jsm/"
22+
}
23+
}
24+
</script>
25+
26+
<video id="video" loop muted crossOrigin="anonymous" playsinline style="display:none">
27+
<source src="textures/sintel.ogv" type='video/ogg; codecs="theora, vorbis"'>
28+
<source src="textures/sintel.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
29+
</video>
30+
31+
<script type="module">
32+
33+
import * as THREE from 'three';
34+
import { Fn, color, mx_worley_noise_float, time } from 'three/tsl';
35+
36+
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
37+
38+
import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
39+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
40+
41+
let renderer, scene, camera;
42+
43+
let projectorLight, lightHelper;
44+
45+
init();
46+
47+
function init() {
48+
49+
// Renderer
50+
51+
renderer = new THREE.WebGPURenderer( { antialias: true } );
52+
renderer.setPixelRatio( window.devicePixelRatio );
53+
renderer.setSize( window.innerWidth, window.innerHeight );
54+
renderer.setAnimationLoop( animate );
55+
document.body.appendChild( renderer.domElement );
56+
57+
renderer.shadowMap.enabled = true;
58+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
59+
60+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
61+
renderer.toneMappingExposure = 1;
62+
63+
scene = new THREE.Scene();
64+
65+
camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.1, 100 );
66+
camera.position.set( 7, 4, 1 );
67+
68+
// Controls
69+
70+
const controls = new OrbitControls( camera, renderer.domElement );
71+
controls.minDistance = 2;
72+
controls.maxDistance = 10;
73+
controls.maxPolarAngle = Math.PI / 2;
74+
controls.target.set( 0, 1, 0 );
75+
controls.update();
76+
77+
// Textures
78+
79+
const loader = new THREE.TextureLoader().setPath( 'textures/' );
80+
81+
// Lights
82+
83+
const causticEffect = Fn( ( [ projectorUV ] ) => {
84+
85+
const waterLayer0 = mx_worley_noise_float( projectorUV.mul( 10 ).add( time ) );
86+
87+
const caustic = waterLayer0.mul( color( 0x5abcd8 ) ).mul( 2 );
88+
89+
return caustic;
90+
91+
} );
92+
93+
94+
const ambient = new THREE.HemisphereLight( 0xffffff, 0x8d8d8d, 0.15 );
95+
scene.add( ambient );
96+
97+
projectorLight = new THREE.ProjectorLight( 0xffffff, 100 );
98+
projectorLight.colorNode = causticEffect;
99+
projectorLight.position.set( 2.5, 5, 2.5 );
100+
projectorLight.angle = Math.PI / 6;
101+
projectorLight.penumbra = 1;
102+
projectorLight.decay = 2;
103+
projectorLight.distance = 0;
104+
105+
projectorLight.castShadow = true;
106+
projectorLight.shadow.mapSize.width = 1024;
107+
projectorLight.shadow.mapSize.height = 1024;
108+
projectorLight.shadow.camera.near = 1;
109+
projectorLight.shadow.camera.far = 10;
110+
projectorLight.shadow.focus = 1;
111+
projectorLight.shadow.bias = - .003;
112+
scene.add( projectorLight );
113+
114+
lightHelper = new THREE.SpotLightHelper( projectorLight );
115+
scene.add( lightHelper );
116+
117+
//
118+
119+
const geometry = new THREE.PlaneGeometry( 200, 200 );
120+
const material = new THREE.MeshLambertMaterial( { color: 0xbcbcbc } );
121+
122+
const mesh = new THREE.Mesh( geometry, material );
123+
mesh.position.set( 0, - 1, 0 );
124+
mesh.rotation.x = - Math.PI / 2;
125+
mesh.receiveShadow = true;
126+
scene.add( mesh );
127+
128+
// Models
129+
130+
new PLYLoader().load( 'models/ply/binary/Lucy100k.ply', function ( geometry ) {
131+
132+
geometry.scale( 0.0024, 0.0024, 0.0024 );
133+
geometry.computeVertexNormals();
134+
135+
const material = new THREE.MeshLambertMaterial();
136+
137+
const mesh = new THREE.Mesh( geometry, material );
138+
mesh.rotation.y = - Math.PI / 2;
139+
mesh.position.y = 0.8;
140+
mesh.castShadow = true;
141+
mesh.receiveShadow = true;
142+
scene.add( mesh );
143+
144+
} );
145+
146+
window.addEventListener( 'resize', onWindowResize );
147+
148+
// GUI
149+
150+
const gui = new GUI();
151+
152+
const params = {
153+
type: 'procedural',
154+
color: projectorLight.color.getHex(),
155+
intensity: projectorLight.intensity,
156+
distance: projectorLight.distance,
157+
angle: projectorLight.angle,
158+
penumbra: projectorLight.penumbra,
159+
decay: projectorLight.decay,
160+
focus: projectorLight.shadow.focus,
161+
shadows: true,
162+
};
163+
164+
let videoTexture, mapTexture;
165+
166+
gui.add( params, 'type', [ 'procedural', 'video', 'texture' ] ).onChange( function ( val ) {
167+
168+
projectorLight.colorNode = null;
169+
projectorLight.map = null;
170+
171+
if ( val === 'procedural' ) {
172+
173+
projectorLight.colorNode = causticEffect;
174+
175+
focus.setValue( 1 );
176+
177+
} else if ( val === 'video' ) {
178+
179+
if ( videoTexture === undefined ) {
180+
181+
const video = document.getElementById( 'video' );
182+
video.play();
183+
184+
videoTexture = new THREE.VideoTexture( video );
185+
186+
}
187+
188+
projectorLight.map = videoTexture;
189+
190+
focus.setValue( .46 );
191+
192+
} else if ( val === 'texture' ) {
193+
194+
mapTexture = loader.load( 'colors.png' );
195+
mapTexture.minFilter = THREE.LinearFilter;
196+
mapTexture.magFilter = THREE.LinearFilter;
197+
mapTexture.generateMipmaps = false;
198+
mapTexture.colorSpace = THREE.SRGBColorSpace;
199+
200+
projectorLight.map = mapTexture;
201+
202+
focus.setValue( 1 );
203+
204+
}
205+
206+
} );
207+
208+
gui.addColor( params, 'color' ).onChange( function ( val ) {
209+
210+
projectorLight.color.setHex( val );
211+
212+
} );
213+
214+
gui.add( params, 'intensity', 0, 500 ).onChange( function ( val ) {
215+
216+
projectorLight.intensity = val;
217+
218+
} );
219+
220+
221+
gui.add( params, 'distance', 0, 20 ).onChange( function ( val ) {
222+
223+
projectorLight.distance = val;
224+
225+
} );
226+
227+
gui.add( params, 'angle', 0, Math.PI / 3 ).onChange( function ( val ) {
228+
229+
projectorLight.angle = val;
230+
231+
} );
232+
233+
gui.add( params, 'penumbra', 0, 1 ).onChange( function ( val ) {
234+
235+
projectorLight.penumbra = val;
236+
237+
} );
238+
239+
gui.add( params, 'decay', 1, 2 ).onChange( function ( val ) {
240+
241+
projectorLight.decay = val;
242+
243+
} );
244+
245+
const focus = gui.add( params, 'focus', 0, 1 ).onChange( function ( val ) {
246+
247+
projectorLight.shadow.focus = val;
248+
249+
} );
250+
251+
gui.add( params, 'shadows' ).onChange( function ( val ) {
252+
253+
renderer.shadowMap.enabled = val;
254+
255+
scene.traverse( function ( child ) {
256+
257+
if ( child.material ) {
258+
259+
child.material.needsUpdate = true;
260+
261+
}
262+
263+
} );
264+
265+
} );
266+
267+
gui.open();
268+
269+
}
270+
271+
function onWindowResize() {
272+
273+
camera.aspect = window.innerWidth / window.innerHeight;
274+
camera.updateProjectionMatrix();
275+
276+
renderer.setSize( window.innerWidth, window.innerHeight );
277+
278+
}
279+
280+
function animate() {
281+
282+
const time = performance.now() / 3000;
283+
284+
projectorLight.position.x = Math.cos( time ) * 2.5;
285+
projectorLight.position.z = Math.sin( time ) * 2.5;
286+
287+
lightHelper.update();
288+
289+
renderer.render( scene, camera );
290+
291+
}
292+
293+
</script>
294+
295+
</body>
296+
297+
</html>

examples/webgpu_lights_spotlight.html

-35
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
<script type="module">
2727

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

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

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

98-
const boxAttenuationFn = Fn( ( [ lightNode ], builder ) => {
99-
100-
const light = lightNode.light;
101-
102-
const sdBox = Fn( ( [ p, b ] ) => {
103-
104-
const d = vec2( abs( p ).sub( b ) ).toVar();
105-
106-
return length( max( d, 0.0 ) ).add( min( max( d.x, d.y ), 0.0 ) );
107-
108-
} );
109-
110-
const penumbraCos = uniform( 'float' ).onRenderUpdate( () => Math.min( Math.cos( light.angle * ( 1 - light.penumbra ) ), .99999 ) );
111-
const spotLightCoord = lightNode.getSpotLightCoord( builder );
112-
const coord = spotLightCoord.xyz.div( spotLightCoord.w );
113-
114-
const boxDist = sdBox( coord.xy.sub( vec2( 0.5 ) ), vec2( 0.5 ) );
115-
const angleFactor = div( -1.0, sub( 1.0, acos( penumbraCos ) ).sub( 1.0 ) );
116-
const attenuation = saturate( boxDist.mul( - 2.0 ).mul( angleFactor ) );
117-
118-
return attenuation;
119-
120-
} );
121-
12297
spotLight = new THREE.SpotLight( 0xffffff, 100 );
12398
spotLight.map = textures[ 'disturb.jpg' ];
12499
spotLight.position.set( 2.5, 5, 2.5 );
@@ -252,16 +227,6 @@
252227

253228
} );
254229

255-
gui.add( params, 'customAttenuation' ).name( 'custom attenuation' ).onChange( function ( val ) {
256-
257-
spotLight.attenuationNode = val ? boxAttenuationFn : null;
258-
259-
aspectGUI.setValue( 1 ).enable( val );
260-
261-
} );
262-
263-
const aspectGUI = gui.add( spotLight.shadow, 'aspect', 0, 2 ).enable( false );
264-
265230
gui.open();
266231

267232
}

src/Three.WebGPU.Nodes.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { default as StorageBufferAttribute } from './renderers/common/StorageBuf
1414
export { default as StorageInstancedBufferAttribute } from './renderers/common/StorageInstancedBufferAttribute.js';
1515
export { default as IndirectStorageBufferAttribute } from './renderers/common/IndirectStorageBufferAttribute.js';
1616
export { default as IESSpotLight } from './lights/webgpu/IESSpotLight.js';
17+
export { default as ProjectorLight } from './lights/webgpu/ProjectorLight.js';
1718
export { default as NodeLoader } from './loaders/nodes/NodeLoader.js';
1819
export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js';
1920
export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoader.js';

src/Three.WebGPU.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { default as StorageBufferAttribute } from './renderers/common/StorageBuf
1414
export { default as StorageInstancedBufferAttribute } from './renderers/common/StorageInstancedBufferAttribute.js';
1515
export { default as IndirectStorageBufferAttribute } from './renderers/common/IndirectStorageBufferAttribute.js';
1616
export { default as IESSpotLight } from './lights/webgpu/IESSpotLight.js';
17+
export { default as ProjectorLight } from './lights/webgpu/ProjectorLight.js';
1718
export { default as NodeLoader } from './loaders/nodes/NodeLoader.js';
1819
export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js';
1920
export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoader.js';

0 commit comments

Comments
 (0)