three-gpu-pathtracer 0.0.26 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +4 -2
  2. package/build/index.module.js +23 -5
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +22 -4
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +8 -8
  7. package/src/core/PathTracingSceneGenerator.js +1 -1
  8. package/src/core/utils/sceneUpdateUtils.js +2 -1
  9. package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
  10. package/src/uniforms/LightsInfoUniformStruct.js +16 -1
  11. package/src/webgpu/API.md +167 -19
  12. package/src/webgpu/AtlasTexture.js +32 -12
  13. package/src/webgpu/EquirectHdrInfoNode.js +97 -46
  14. package/src/webgpu/LightsInfoNode.js +142 -4
  15. package/src/webgpu/MegaKernelPathTracer.js +11 -3
  16. package/src/webgpu/PathTracerBackend.js +174 -21
  17. package/src/webgpu/TurquinTexture.js +2 -0
  18. package/src/webgpu/WaveFrontPathTracer.js +169 -71
  19. package/src/webgpu/WebGPUPathTracer.js +637 -51
  20. package/src/webgpu/compute/ComputeKernel.js +18 -0
  21. package/src/webgpu/compute/PathTracerMegaKernel.js +253 -71
  22. package/src/webgpu/compute/TallySampleCountsKernel.js +10 -10
  23. package/src/webgpu/compute/UnpackAuxKernel.js +48 -0
  24. package/src/webgpu/compute/ZeroOutKernel.js +3 -1
  25. package/src/webgpu/compute/targets.js +55 -0
  26. package/src/webgpu/compute/wavefront/LogicKernel.js +136 -55
  27. package/src/webgpu/compute/wavefront/MaterialKernel.js +153 -66
  28. package/src/webgpu/compute/wavefront/MediumKernel.js +154 -0
  29. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +5 -5
  30. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +5 -5
  31. package/src/webgpu/compute/wavefront/QueueMediumWalksKernel.js +52 -0
  32. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +6 -6
  33. package/src/webgpu/compute/wavefront/TraceRayKernel.js +11 -9
  34. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +18 -23
  35. package/src/webgpu/compute/wavefront/structs.js +63 -35
  36. package/src/webgpu/constants.js +2 -2
  37. package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
  38. package/src/webgpu/index.d.ts +79 -6
  39. package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
  40. package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
  41. package/src/webgpu/nodes/PathtracerBVHComputeData.js +109 -17
  42. package/src/webgpu/nodes/aux.wgsl.js +61 -0
  43. package/src/webgpu/nodes/eon.wgsl.js +1 -1
  44. package/src/webgpu/nodes/lights.wgsl.js +21 -1
  45. package/src/webgpu/nodes/material.wgsl.js +2 -1
  46. package/src/webgpu/nodes/medium.wgsl.js +165 -0
  47. package/src/webgpu/nodes/random.wgsl.js +2 -0
  48. package/src/webgpu/nodes/sampling.wgsl.js +44 -3
  49. package/src/webgpu/nodes/structs.wgsl.js +8 -1
  50. package/src/webgpu/upscale/FSRUpscaler.js +3 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-gpu-pathtracer",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "Path tracing renderer and utilities for three.js built on top of three-mesh-bvh.",
5
5
  "module": "src/index.js",
6
6
  "main": "build/index.umd.cjs",
@@ -43,9 +43,9 @@
43
43
  "devDependencies": {
44
44
  "@lookingglass/webxr": "^0.6.0",
45
45
  "@monogrid/gainmap-js": "^3.0.5",
46
- "@pmndrs/upscaler": "^0.2.0",
46
+ "@pmndrs/upscaler": "^0.3.0",
47
47
  "@types/node": "^24.3.1",
48
- "@types/three": "^0.185.1",
48
+ "@types/three": "^0.186.0",
49
49
  "@typescript-eslint/eslint-plugin": "^8.40.0",
50
50
  "@vitejs/plugin-basic-ssl": "^2.3.0",
51
51
  "@webgpu/types": "^0.1.72",
@@ -56,22 +56,22 @@
56
56
  "jsdoc": "^4.0.5",
57
57
  "mediabunny": "^1.50.9",
58
58
  "node-fetch": "^3.2.9",
59
- "oidn-web": "^0.4.0",
59
+ "oidn-webgpu": "^0.6.0",
60
60
  "pixelmatch": "^5.3.0",
61
61
  "pngjs": "^6.0.0",
62
62
  "process": "^0.11.10",
63
63
  "puppeteer": "^24.42.0",
64
64
  "rollup": "^2.70.0",
65
65
  "simple-git": "^3.10.0",
66
- "three": "^0.185.1",
67
- "three-mesh-bvh": "^0.9.15",
66
+ "three": "^0.187.0",
67
+ "three-mesh-bvh": "^0.9.17",
68
68
  "typescript": "^5.9.2",
69
69
  "vite": "^8.2.2",
70
70
  "yargs": "^17.5.1"
71
71
  },
72
72
  "peerDependencies": {
73
- "three": ">=0.185.0",
74
- "three-mesh-bvh": ">=0.9.15",
73
+ "three": ">=0.187.0",
74
+ "three-mesh-bvh": ">=0.9.17",
75
75
  "xatlas-web": "^0.1.0"
76
76
  },
77
77
  "scripts": {
@@ -177,7 +177,7 @@ export class PathTracingSceneGenerator {
177
177
  // generate the geometry
178
178
  const result = staticGeometryGenerator.generate( geometry );
179
179
  const materials = result.materials;
180
- let needsMaterialIndexUpdate = result.changeType !== NO_CHANGE || this._materialUuids === null || this._materialUuids.length !== length;
180
+ let needsMaterialIndexUpdate = result.changeType !== NO_CHANGE || this._materialUuids === null || this._materialUuids.length !== materials.length;
181
181
  if ( ! needsMaterialIndexUpdate ) {
182
182
 
183
183
  for ( let i = 0, length = materials.length; i < length; i ++ ) {
@@ -78,7 +78,8 @@ export function getLights( scene ) {
78
78
  c.isRectAreaLight ||
79
79
  c.isSpotLight ||
80
80
  c.isPointLight ||
81
- c.isDirectionalLight
81
+ c.isDirectionalLight ||
82
+ c.isSunLight
82
83
  ) {
83
84
 
84
85
  lights.push( c );
@@ -1,4 +1,4 @@
1
- import { DataTexture, RedFormat, LinearFilter, DataUtils, HalfFloatType, Source, RepeatWrapping, RGBAFormat, FloatType, ClampToEdgeWrapping } from 'three';
1
+ import { DataTexture, RedFormat, LinearFilter, DataUtils, HalfFloatType, TextureSource, RepeatWrapping, RGBAFormat, FloatType, ClampToEdgeWrapping } from 'three';
2
2
  import { toHalfFloatArray } from '../utils/TextureUtils.js';
3
3
 
4
4
  function binarySearchFindClosestIndexOf( array, targetValue, offset = 0, count = array.length ) {
@@ -42,7 +42,7 @@ function colorToLuminance( r, g, b ) {
42
42
  function preprocessEnvMap( envMap, targetType = HalfFloatType ) {
43
43
 
44
44
  const map = envMap.clone();
45
- map.source = new Source( { ...map.image } );
45
+ map.source = new TextureSource( { ...map.image } );
46
46
  const { width, height, data } = map.image;
47
47
 
48
48
  // TODO: is there a simple way to avoid cloning and adjusting the env map data here?
@@ -311,6 +311,8 @@ export class EquirectHdrInfoUniform {
311
311
 
312
312
  this.totalSum = totalSumValue;
313
313
  this.map = map;
314
+ this.cdfMarginal = cdfMarginal;
315
+ this.cdfConditional = cdfConditional;
314
316
 
315
317
  }
316
318
 
@@ -2,6 +2,7 @@ import { DataTexture, RGBAFormat, ClampToEdgeWrapping, FloatType, Vector3, Quate
2
2
  import { bufferToHash } from '../utils/bufferToHash.js';
3
3
 
4
4
  const LIGHT_PIXELS = 6;
5
+ const VISIBLE_TO_CAMERA_OFFSET = 11;
5
6
  const RECT_AREA_LIGHT = 0;
6
7
  const CIRC_AREA_LIGHT = 1;
7
8
  const SPOT_LIGHT = 2;
@@ -82,7 +83,7 @@ export class LightsInfoUniformStruct {
82
83
 
83
84
  type = SPOT_LIGHT;
84
85
 
85
- } else if ( l.isDirectionalLight ) {
86
+ } else if ( l.isDirectionalLight || l.isSunLight ) {
86
87
 
87
88
  type = DIR_LIGHT;
88
89
 
@@ -207,8 +208,22 @@ export class LightsInfoUniformStruct {
207
208
  floatArray[ baseIndex + ( index ++ ) ] = target.y;
208
209
  floatArray[ baseIndex + ( index ++ ) ] = target.z;
209
210
 
211
+ } else if ( l.isSunLight ) {
212
+
213
+ // the sun has no target and shines from its position toward the origin
214
+ target.setFromMatrixPosition( l.matrixWorld ).normalize();
215
+
216
+ // sample 3
217
+ // u vector
218
+ floatArray[ baseIndex + ( index ++ ) ] = target.x;
219
+ floatArray[ baseIndex + ( index ++ ) ] = target.y;
220
+ floatArray[ baseIndex + ( index ++ ) ] = target.z;
221
+
210
222
  }
211
223
 
224
+ // whether camera rays see the light
225
+ floatArray[ baseIndex + VISIBLE_TO_CAMERA_OFFSET ] = l.visibleToCameraRays ? 1 : 0;
226
+
212
227
  }
213
228
 
214
229
  this.count = lights.length;
package/src/webgpu/API.md CHANGED
@@ -17,7 +17,7 @@ Uncorrelated pseudo random samples. Converges slowest but never shows structured
17
17
  RANDOM_SOBOL: Object
18
18
  ```
19
19
 
20
- Sobol low discrepancy samples, scrambled per pixel. Converges faster than `RANDOM_PCG`.
20
+ Sobol low discrepancy samples, scrambled per pixel. Converges faster than `RANDOM_PCG`. The default.
21
21
 
22
22
  ### RANDOM_BLUE_DITHER
23
23
 
@@ -26,7 +26,7 @@ RANDOM_BLUE_DITHER: Object
26
26
  ```
27
27
 
28
28
  The same Sobol sequence for every pixel, offset by a blue noise dither so early frames read as
29
- smooth grain rather than clumped noise. The default.
29
+ smooth grain rather than clumped noise.
30
30
 
31
31
  ## Transmissive Background Modes
32
32
 
@@ -143,7 +143,7 @@ Runs Open Image Denoise over a path traced image. Pass one to
143
143
  nor the network files become a dependency.
144
144
 
145
145
  ```js
146
- import { initUNetFromURL } from 'oidn-web';
146
+ import { initUNetFromURL } from 'oidn-webgpu';
147
147
  pathTracer.setDenoiser( new OIDNDenoiser( { initUNetFromURL, auxWeightsUrl } ) );
148
148
  ```
149
149
 
@@ -151,6 +151,17 @@ Weights come from the oidn-weights repository, where the "_small" and "_large" v
151
151
  quality against download size and per tile cost.
152
152
 
153
153
 
154
+ ### events
155
+
156
+ ```js
157
+ // Fired when a pass started by `update` fails, such as the weights failing to download, with the
158
+ // failure as the event's `error`. No other pass starts until a reset.
159
+ { type: 'error' }
160
+
161
+ // Fired as each tile of a pass finishes, with the fraction of the pass done as `progress`.
162
+ { type: 'progress' }
163
+ ```
164
+
154
165
  ### .texture
155
166
 
156
167
  ```js
@@ -210,20 +221,17 @@ init( renderer: WebGPURenderer ): void
210
221
  ```
211
222
 
212
223
 
213
- ### .setScene
214
-
215
- ```js
216
- setScene( scene: Scene, camera: Camera ): void
217
- ```
218
-
219
-
220
224
  ### .update
221
225
 
222
226
  ```js
223
- update( target: Texture ): Texture | null
227
+ update(
228
+ target: Texture,
229
+ aux = null: StorageArrayTexture | null
230
+ ): Texture | null
224
231
  ```
225
232
 
226
- Renders the auxiliary buffers and starts a pass. Safe to call every frame.
233
+ Unpacks the auxiliary buffers the path tracer accumulated and starts a pass. Safe to call
234
+ every frame.
227
235
 
228
236
 
229
237
  ### .denoise
@@ -261,6 +269,19 @@ the environment afterward require the matching `update*` function. Any change th
261
269
  the accumulated image restarts it.
262
270
 
263
271
 
272
+ ### events
273
+
274
+ ```js
275
+ // Fired once every pixel has reached `maxSamples` and an attached denoiser has finished. Fires
276
+ // again after a reset, or after `maxSamples` is raised, once the render has caught up.
277
+ { type: 'complete' }
278
+
279
+ // Fired as the render advances. `stage` is "building", "compiling", "rendering" or "denoising", and
280
+ // `progress` is the fraction of it done, or null when unknown. "rendering" also gives the sample
281
+ // counts as `samples`, reported a few times a second.
282
+ { type: 'progress' }
283
+ ```
284
+
264
285
  ### .maxBounces
265
286
 
266
287
  ```js
@@ -277,20 +298,40 @@ resolve more indirect light at the cost of speed.
277
298
  frameBudget: number = 250000
278
299
  ```
279
300
 
280
- Number of path slots dispatched per [WebGPUPathTracer#renderSample](WebGPUPathTracer#renderSample) call,
301
+ Number of path steps dispatched per [WebGPUPathTracer#renderSample](WebGPUPathTracer#renderSample) call,
281
302
  independent of resolution. Raising it trades frame rate for convergence speed.
282
303
 
283
304
 
305
+ ### .frameTime
306
+
307
+ ```js
308
+ readonly frameTime: number
309
+ ```
310
+
311
+ GPU time in milliseconds of a recent [WebGPUPathTracer#renderSample](WebGPUPathTracer#renderSample) call. Only measured
312
+ when the renderer is created with `trackTimestamp: true`.
313
+
314
+
284
315
  ### .maxTransparentBounces
285
316
 
286
317
  ```js
287
- maxTransparentBounces: number = 5
318
+ maxTransparentBounces: number = 15
288
319
  ```
289
320
 
290
321
  Maximum number of alpha tested surfaces a ray can pass through. Counted separately from
291
322
  [WebGPUPathTracer#maxBounces](WebGPUPathTracer#maxBounces) so foliage and cutouts cannot exhaust the bounce budget.
292
323
 
293
324
 
325
+ ### .maxSubsurfaceBounces
326
+
327
+ ```js
328
+ maxSubsurfaceBounces: number = 256
329
+ ```
330
+
331
+ Maximum number of scatter events a path can take inside a subsurface scattering volume. Counted
332
+ separately from [WebGPUPathTracer#maxBounces](WebGPUPathTracer#maxBounces). Paths that reach it end, darkening dense volumes.
333
+
334
+
294
335
  ### .maxSamples
295
336
 
296
337
  ```js
@@ -303,6 +344,16 @@ render has stopped, so it has no effect while this is `0`.
303
344
  > [!NOTE]
304
345
  > `0` renders indefinitely.
305
346
 
347
+ ### .complete
348
+
349
+ ```js
350
+ complete: boolean
351
+ ```
352
+
353
+ Whether every pixel has reached [WebGPUPathTracer#maxSamples](WebGPUPathTracer#maxSamples) and an attached denoiser
354
+ has finished, as of the last measurement. Always false while rendering indefinitely.
355
+
356
+
306
357
  ### .transmissiveBackground
307
358
 
308
359
  ```js
@@ -395,6 +446,17 @@ readonly lowResMode: boolean
395
446
  Whether the tracer is currently rendering the low resolution preview.
396
447
 
397
448
 
449
+ ### .compiling
450
+
451
+ ```js
452
+ readonly compiling: boolean
453
+ ```
454
+
455
+ Whether the kernels are compiling, such as after the scene or material changes. Nothing new
456
+ renders until they finish and the canvas keeps the last frame, so an app may draw its own
457
+ preview in the meantime.
458
+
459
+
398
460
  ### .textureAtlas
399
461
 
400
462
  ```js
@@ -410,13 +472,13 @@ The atlas every scene texture is packed into for the kernels to sample.
410
472
  minSamples: number = 1
411
473
  ```
412
474
 
413
- Samples every pixel must reach before the full resolution render is faded in.
475
+ Average samples per pixel the full resolution render must reach before it is faded in.
414
476
 
415
477
 
416
478
  ### .renderDelay
417
479
 
418
480
  ```js
419
- renderDelay: number = 500
481
+ renderDelay: number = 16
420
482
  ```
421
483
 
422
484
  Milliseconds to show the low resolution preview for after a reset.
@@ -425,7 +487,7 @@ Milliseconds to show the low resolution preview for after a reset.
425
487
  ### .fadeDuration
426
488
 
427
489
  ```js
428
- fadeDuration: number = 500
490
+ fadeDuration: number = 250
429
491
  ```
430
492
 
431
493
  Milliseconds taken to cross fade from the preview to the full render.
@@ -446,7 +508,39 @@ Whether to render a low resolution preview while the camera is moving.
446
508
  lowResScale: number = 0.1
447
509
  ```
448
510
 
449
- Resolution of the low resolution preview, as a fraction of the render size.
511
+ Resolution of the low resolution preview, as a fraction of the render size. Adjusted when
512
+ [WebGPUPathTracer#adaptiveDynamicRes](WebGPUPathTracer#adaptiveDynamicRes) is set. The preview never has more pixels than a
513
+ frame of [WebGPUPathTracer#frameBudget](WebGPUPathTracer#frameBudget) can render.
514
+
515
+
516
+ ### .adaptiveDynamicRes
517
+
518
+ ```js
519
+ adaptiveDynamicRes: boolean = true
520
+ ```
521
+
522
+ Whether [WebGPUPathTracer#lowResScale](WebGPUPathTracer#lowResScale) is adjusted to the pixels a frame of
523
+ [WebGPUPathTracer#frameBudget](WebGPUPathTracer#frameBudget) can render.
524
+
525
+
526
+ ### .adaptiveFrameBudget
527
+
528
+ ```js
529
+ adaptiveFrameBudget: boolean = true
530
+ ```
531
+
532
+ Whether [WebGPUPathTracer#frameBudget](WebGPUPathTracer#frameBudget) is adjusted to keep each
533
+ [WebGPUPathTracer#renderSample](WebGPUPathTracer#renderSample) call near [WebGPUPathTracer#targetFrameTime](WebGPUPathTracer#targetFrameTime) of
534
+ GPU time. Requires a renderer created with `trackTimestamp: true`.
535
+
536
+
537
+ ### .targetFrameTime
538
+
539
+ ```js
540
+ targetFrameTime: number = 16
541
+ ```
542
+
543
+ GPU time in milliseconds per frame targeted by [WebGPUPathTracer#adaptiveFrameBudget](WebGPUPathTracer#adaptiveFrameBudget).
450
544
 
451
545
 
452
546
  ### .renderScale
@@ -469,6 +563,27 @@ Whether to track the canvas size automatically. Set to `false` to drive the rend
469
563
  size with [WebGPUPathTracer#setSize](WebGPUPathTracer#setSize).
470
564
 
471
565
 
566
+ ### .renderToCanvas
567
+
568
+ ```js
569
+ renderToCanvas: boolean = true
570
+ ```
571
+
572
+ Whether [WebGPUPathTracer#renderSample](WebGPUPathTracer#renderSample) draws the result to the current render
573
+ target, the canvas by default. Set to `false` to only accumulate, as when baking to a
574
+ texture read through [WebGPUPathTracer#target](WebGPUPathTracer#target).
575
+
576
+
577
+ ### .asyncCompile
578
+
579
+ ```js
580
+ asyncCompile: boolean = false
581
+ ```
582
+
583
+ Whether to compile the kernels asynchronously so a compile doesn't stall the page. Nothing
584
+ new renders while [WebGPUPathTracer#compiling](WebGPUPathTracer#compiling) is true.
585
+
586
+
472
587
  ### .generateMissingAttributes
473
588
 
474
589
  ```js
@@ -539,7 +654,8 @@ setDenoiser( denoiser: OIDNDenoiser | null ): void
539
654
  ```
540
655
 
541
656
  Attaches a denoiser, run once the render settles and displayed in place of the raw image.
542
- Settings live on the instance. Pass null to remove it.
657
+ Settings live on the instance. Pass null to remove it. The render restarts when the auxiliary
658
+ buffers it needs are added or dropped.
543
659
 
544
660
 
545
661
  ### .setUpscaler
@@ -564,6 +680,28 @@ against. Call this once, then use the `update*` functions for later changes.
564
680
  > [!NOTE]
565
681
  > Geometry BVHs are built synchronously, so this blocks for large scenes.
566
682
 
683
+ ### .setBVHWorker
684
+
685
+ ```js
686
+ setBVHWorker( WorkerConstructor: function ): void
687
+ ```
688
+
689
+ Sets the worker class [WebGPUPathTracer#setSceneAsync](WebGPUPathTracer#setSceneAsync) builds the mesh geometry BVHs
690
+ with, such as `GenerateMeshBVHWorker` from three-mesh-bvh. Several are created and disposed
691
+ per call so geometries build in parallel.
692
+
693
+
694
+ ### .setSceneAsync
695
+
696
+ ```js
697
+ async setSceneAsync( scene: Scene, camera: Camera ): Promise<void>
698
+ ```
699
+
700
+ Like [WebGPUPathTracer#setScene](WebGPUPathTracer#setScene) but builds the mesh geometry BVHs in the workers set
701
+ with [WebGPUPathTracer#setBVHWorker](WebGPUPathTracer#setBVHWorker) so the page is not blocked. Skinned meshes are
702
+ still built synchronously.
703
+
704
+
567
705
  ### .setRandom
568
706
 
569
707
  ```js
@@ -708,6 +846,16 @@ Draws a heatmap of the per pixel sample counts, for spotting pixels that converg
708
846
  > Measures on every call and draws with the previous result, since the readback lands a
709
847
  > frame later.
710
848
 
849
+ ### .renderAuxBuffer
850
+
851
+ ```js
852
+ renderAuxBuffer( buffer = 'albedo': 'albedo' | 'normal' ): void
853
+ ```
854
+
855
+ Draws the albedo or normal auxiliary buffer accumulated for the denoiser, for checking what
856
+ it is given. Nothing is drawn until a denoiser that uses them is attached.
857
+
858
+
711
859
  ### .dispose
712
860
 
713
861
  ```js
@@ -6,10 +6,11 @@ import {
6
6
  NoBlending,
7
7
  Color,
8
8
  Vector4,
9
+ Matrix3,
9
10
  } from 'three/webgpu';
10
11
 
11
- const MAX_TEXTURE_SIZE = 4096;
12
12
  const _prevClearColor = new Color();
13
+ const _prevMatrix = new Matrix3();
13
14
 
14
15
  function getTextureHash( texture ) {
15
16
 
@@ -241,9 +242,9 @@ export class AtlasTexture {
241
242
 
242
243
  }
243
244
 
244
- // calculate the maximum dimension of the atlas
245
+ // calculate the maximum dimension of the atlas, up to what the device allows
245
246
  let maxDim = textures.reduce( ( v, t ) => Math.max( v, t.width, t.height ), 1 );
246
- maxDim = Math.min( MAX_TEXTURE_SIZE, maxDim );
247
+ maxDim = Math.min( renderer.backend.device.limits.maxTextureDimension2D, maxDim );
247
248
 
248
249
  const width = maxDim;
249
250
  const height = maxDim;
@@ -438,21 +439,40 @@ export class AtlasTexture {
438
439
  for ( let i = 0, l = textures.length; i < l; i ++ ) {
439
440
 
440
441
  const { x, y, w, h, page } = placements[ i ];
441
-
442
- // Clone the source so we get an independent texture handle
443
- const texture = textures[ i ].clone();
444
- texture.matrixAutoUpdate = false;
445
- texture.matrix.identity();
446
-
447
- quadMesh.material.map = texture;
442
+ const texture = textures[ i ];
448
443
 
449
444
  // three.js uses the render target viewport / scissor
450
445
  renderTarget.viewport.set( x, y, w, h );
451
446
  renderTarget.scissor.set( x, y, w, h );
452
447
  renderer.setRenderTarget( renderTarget, page );
453
- quadMesh.render( renderer );
454
448
 
455
- texture.dispose();
449
+ if ( texture.isRenderTargetTexture ) {
450
+
451
+ // a render target's content only lives on the GPU, so draw it directly
452
+ const prevAutoUpdate = texture.matrixAutoUpdate;
453
+ _prevMatrix.copy( texture.matrix );
454
+ texture.matrix.identity();
455
+ texture.matrixAutoUpdate = false;
456
+
457
+ quadMesh.material.map = texture;
458
+ quadMesh.render( renderer );
459
+
460
+ texture.matrixAutoUpdate = prevAutoUpdate;
461
+ texture.matrix.copy( _prevMatrix );
462
+
463
+ } else {
464
+
465
+ // Clone the source so we get an independent texture handle
466
+ const clone = texture.clone();
467
+ clone.matrixAutoUpdate = false;
468
+ clone.matrix.identity();
469
+
470
+ quadMesh.material.map = clone;
471
+ quadMesh.render( renderer );
472
+
473
+ clone.dispose();
474
+
475
+ }
456
476
 
457
477
  }
458
478