Skip to content

Commit 3b687a2

Browse files
authored
TSL: ShadowNode: prevent memory leaks. Add webgpu_test_memory (#32395)
1 parent 656a31b commit 3b687a2

File tree

11 files changed

+378
-2
lines changed

11 files changed

+378
-2
lines changed

examples/files.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@
455455
"webgpu_sprites",
456456
"webgpu_storage_buffer",
457457
"webgpu_struct_drawindirect",
458+
"webgpu_test_memory",
458459
"webgpu_texturegrad",
459460
"webgpu_textures_2d-array",
460461
"webgpu_textures_2d-array_compressed",
10.6 KB
Loading

examples/webgpu_test_memory.html

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>three.js webgpu - memory test I</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="example.css">
8+
</head>
9+
10+
<body>
11+
<div id="info" class="invert">
12+
<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
13+
14+
<div class="title-wrapper">
15+
<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>WebGPU Memory Test I</span>
16+
</div>
17+
18+
<small>
19+
This example tests memory management with WebGPU renderer.
20+
<br /> Spheres are created, rendered, and disposed in each frame.
21+
</small>
22+
</div>
23+
24+
<script type="importmap">
25+
{
26+
"imports": {
27+
"three": "../build/three.webgpu.js",
28+
"three/tsl": "../build/three.tsl.js",
29+
"three/webgpu": "../build/three.webgpu.js",
30+
"three/addons/": "./jsm/"
31+
}
32+
}
33+
</script>
34+
35+
<script type="module">
36+
37+
import * as THREE from 'three/webgpu';
38+
import { pass, mrt, directionToColor, normalView, screenUV, context, sample, colorToDirection } from 'three/tsl';
39+
import { outline } from 'three/addons/tsl/display/OutlineNode.js';
40+
import { ao } from 'three/addons/tsl/display/GTAONode.js';
41+
42+
import { Inspector } from 'three/addons/inspector/Inspector.js';
43+
44+
let camera, scene, renderer, light;
45+
let generateMeshes = true;
46+
let mesh;
47+
let postProcessing;
48+
let aoNode, outlineNode, scenePass, prePass;
49+
const selectedObjects = [];
50+
51+
const params = {
52+
castShadow: true,
53+
enablePP: false,
54+
enableOutline: true,
55+
enableAO: true,
56+
recreateLight: () => {
57+
58+
// Remove existing light
59+
if ( light ) {
60+
61+
scene.remove( light );
62+
light.dispose();
63+
light = null;
64+
65+
}
66+
67+
// Create new directional light
68+
light = new THREE.DirectionalLight( 0xffffff, 1 );
69+
light.position.set( Math.random() * 200 - 100, 100, Math.random() * 200 - 100 );
70+
light.castShadow = params.castShadow;
71+
scene.add( light );
72+
73+
},
74+
start: () => {
75+
76+
generateMeshes = true;
77+
78+
},
79+
stop: () => {
80+
81+
generateMeshes = false;
82+
83+
}
84+
};
85+
86+
init();
87+
88+
async function init() {
89+
90+
const container = document.createElement( 'div' );
91+
document.body.appendChild( container );
92+
93+
camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 10000 );
94+
camera.position.z = 200;
95+
96+
scene = new THREE.Scene();
97+
scene.background = new THREE.Color( 0xffffff );
98+
99+
renderer = new THREE.WebGPURenderer();
100+
renderer.shadowMap.enabled = true;
101+
renderer.setPixelRatio( window.devicePixelRatio );
102+
renderer.setSize( window.innerWidth, window.innerHeight );
103+
renderer.setAnimationLoop( animate );
104+
renderer.inspector = new Inspector();
105+
container.appendChild( renderer.domElement );
106+
107+
await renderer.init();
108+
109+
light = new THREE.DirectionalLight( 0xffffff, 1 );
110+
light.position.set( 0, 100, 0 );
111+
light.castShadow = true;
112+
scene.add( light );
113+
114+
const planeGeometry = new THREE.PlaneGeometry( 1000, 1000 );
115+
const planeMaterial = new THREE.MeshLambertMaterial( { color: 0xcccccc } );
116+
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
117+
plane.rotation.x = - Math.PI / 2;
118+
plane.position.y = - 100;
119+
plane.receiveShadow = true;
120+
scene.add( plane );
121+
122+
// Inspector UI
123+
const gui = renderer.inspector.createParameters( 'Settings' );
124+
125+
gui.add( params, 'castShadow' ).name( 'Cast Shadow' ).onChange( ( value ) => {
126+
127+
if ( light ) light.castShadow = value;
128+
129+
} );
130+
131+
const ppFolder = gui.addFolder( 'Post Processing' );
132+
ppFolder.add( params, 'enablePP' ).name( 'Enable' ).onChange( updatePostProcessing );
133+
ppFolder.add( params, 'enableOutline' ).name( 'Outline' ).onChange( updatePostProcessing );
134+
ppFolder.add( params, 'enableAO' ).name( 'AO' ).onChange( updatePostProcessing );
135+
136+
gui.add( params, 'recreateLight' ).name( 'Recreate Directional Light' );
137+
gui.add( params, 'start' ).name( 'Start Creating Meshes' );
138+
gui.add( params, 'stop' ).name( 'Stop Creating Meshes' );
139+
140+
window.addEventListener( 'resize', onWindowResize );
141+
142+
}
143+
144+
function updatePostProcessing() {
145+
146+
if ( postProcessing ) {
147+
148+
postProcessing.dispose();
149+
postProcessing = null;
150+
151+
}
152+
153+
if ( scenePass ) {
154+
155+
scenePass.dispose();
156+
scenePass = null;
157+
158+
}
159+
160+
if ( prePass ) {
161+
162+
prePass.dispose();
163+
prePass = null;
164+
165+
}
166+
167+
if ( aoNode ) {
168+
169+
aoNode.dispose();
170+
aoNode = null;
171+
172+
}
173+
174+
if ( outlineNode ) {
175+
176+
outlineNode.dispose();
177+
outlineNode = null;
178+
179+
}
180+
181+
if ( params.enablePP ) {
182+
183+
postProcessing = new THREE.PostProcessing( renderer );
184+
185+
scenePass = pass( scene, camera );
186+
let colorNode = scenePass;
187+
188+
if ( params.enableAO ) {
189+
190+
prePass = pass( scene, camera );
191+
prePass.setMRT( mrt( {
192+
output: directionToColor( normalView )
193+
} ) );
194+
195+
const prePassNormal = sample( ( uv ) => {
196+
197+
return colorToDirection( prePass.getTextureNode().sample( uv ) );
198+
199+
} );
200+
const prePassDepth = prePass.getTextureNode( 'depth' );
201+
202+
aoNode = ao( prePassDepth, prePassNormal, camera );
203+
204+
scenePass.contextNode = context( {
205+
ao: aoNode.getTextureNode().sample( screenUV ).r
206+
} );
207+
208+
}
209+
210+
if ( params.enableOutline ) {
211+
212+
outlineNode = outline( scene, camera, {
213+
selectedObjects: selectedObjects
214+
} );
215+
colorNode = colorNode.add( outlineNode );
216+
217+
}
218+
219+
postProcessing.outputNode = colorNode;
220+
221+
}
222+
223+
}
224+
225+
function onWindowResize() {
226+
227+
const width = window.innerWidth;
228+
const height = window.innerHeight;
229+
230+
camera.aspect = width / height;
231+
camera.updateProjectionMatrix();
232+
233+
renderer.setSize( width, height );
234+
235+
}
236+
237+
function createImage() {
238+
239+
const canvas = document.createElement( 'canvas' );
240+
canvas.width = 256;
241+
canvas.height = 256;
242+
243+
const canvas2DContext = canvas.getContext( '2d' );
244+
canvas2DContext.fillStyle = 'rgb(' + Math.floor( Math.random() * 256 ) + ',' + Math.floor( Math.random() * 256 ) + ',' + Math.floor( Math.random() * 256 ) + ')';
245+
canvas2DContext.fillRect( 0, 0, 256, 256 );
246+
247+
return canvas;
248+
249+
}
250+
251+
//
252+
253+
function animate() {
254+
255+
if ( generateMeshes ) {
256+
257+
if ( mesh ) {
258+
259+
scene.remove( mesh );
260+
261+
mesh.geometry.dispose();
262+
mesh.material.map.dispose();
263+
mesh.material.dispose();
264+
265+
}
266+
267+
const geometry = new THREE.SphereGeometry( 50, Math.random() * 64, Math.random() * 32 );
268+
269+
const texture = new THREE.CanvasTexture( createImage() );
270+
271+
const material = new THREE.MeshLambertMaterial( { map: texture } );
272+
273+
mesh = new THREE.Mesh( geometry, material );
274+
mesh.castShadow = true;
275+
276+
scene.add( mesh );
277+
278+
if ( outlineNode ) {
279+
280+
selectedObjects[ 0 ] = mesh;
281+
282+
}
283+
284+
}
285+
286+
if ( postProcessing ) {
287+
288+
postProcessing.render();
289+
290+
} else {
291+
292+
renderer.render( scene, camera );
293+
294+
}
295+
296+
}
297+
298+
</script>
299+
300+
</body>
301+
</html>

src/lights/DirectionalLight.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ class DirectionalLight extends Light {
8080

8181
dispose() {
8282

83+
super.dispose();
84+
8385
this.shadow.dispose();
8486

8587
}

src/lights/Light.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Light extends Object3D {
5454
*/
5555
dispose() {
5656

57-
// Empty here in base class; some subclasses override.
57+
this.dispatchEvent( { type: 'dispose' } );
5858

5959
}
6060

src/lights/PointLight.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ class PointLight extends Light {
9494

9595
dispose() {
9696

97+
super.dispose();
98+
9799
this.shadow.dispose();
98100

99101
}

src/lights/SpotLight.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ class SpotLight extends Light {
147147

148148
dispose() {
149149

150+
super.dispose();
151+
150152
this.shadow.dispose();
151153

152154
}

src/nodes/lighting/AnalyticLightNode.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,53 @@ class AnalyticLightNode extends LightingNode {
9696
*/
9797
this.updateType = NodeUpdateType.FRAME;
9898

99+
if ( light && light.shadow ) {
100+
101+
this._shadowDisposeListener = () => {
102+
103+
this.disposeShadow();
104+
105+
};
106+
107+
light.addEventListener( 'dispose', this._shadowDisposeListener );
108+
109+
}
110+
111+
}
112+
113+
dispose() {
114+
115+
if ( this._shadowDisposeListener ) {
116+
117+
this.light.removeEventListener( 'dispose', this._shadowDisposeListener );
118+
119+
}
120+
121+
super.dispose();
122+
123+
}
124+
125+
/**
126+
* Frees internal resources related to shadows.
127+
*/
128+
disposeShadow() {
129+
130+
if ( this.shadowNode !== null ) {
131+
132+
this.shadowNode.dispose();
133+
this.shadowNode = null;
134+
135+
}
136+
137+
this.shadowColorNode = null;
138+
139+
if ( this.baseColorNode !== null ) {
140+
141+
this.colorNode = this.baseColorNode;
142+
this.baseColorNode = null;
143+
144+
}
145+
99146
}
100147

101148
getHash() {

0 commit comments

Comments
 (0)