three-gpu-pathtracer 0.0.24 → 0.0.26
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.
- package/README.md +53 -530
- package/build/index.module.js +262 -50
- package/build/index.module.js.map +1 -1
- package/build/index.umd.cjs +261 -48
- package/build/index.umd.cjs.map +1 -1
- package/package.json +29 -11
- package/src/core/WebGLPathTracer.js +8 -0
- package/src/core/utils/sceneUpdateUtils.js +8 -12
- package/src/detectors/PrecisionMaterial.js +9 -9
- package/src/index.js +1 -0
- package/src/objects/PhysicalSpotLight.js +2 -0
- package/src/shader/bsdf/bsdf_functions.glsl.js +0 -1
- package/src/textures/BlueNoiseTexture.js +6 -6
- package/src/textures/ProceduralEquirectTexture.js +8 -7
- package/src/textures/turquinMetal.png +0 -0
- package/src/uniforms/EquirectHdrInfoUniform.js +9 -4
- package/src/uniforms/FloatAttributeTextureArray.js +11 -11
- package/src/uniforms/MaterialsTexture.js +9 -9
- package/src/webgpu/API.md +762 -0
- package/src/webgpu/AtlasTexture.js +471 -0
- package/src/webgpu/BlurredEnvMapGenerator.js +129 -0
- package/src/webgpu/EquirectBackgroundInfo.js +97 -0
- package/src/webgpu/EquirectHdrInfoNode.js +151 -0
- package/src/webgpu/LightsInfoNode.js +224 -0
- package/src/webgpu/MegaKernelPathTracer.js +277 -0
- package/src/webgpu/PathTracerBackend.js +216 -0
- package/src/webgpu/TurquinTexture.js +169 -0
- package/src/webgpu/WaveFrontPathTracer.js +586 -0
- package/src/webgpu/WebGPUPathTracer.js +1302 -0
- package/src/webgpu/compute/ComputeKernel.js +92 -0
- package/src/webgpu/compute/CopyBufferKernel.js +40 -0
- package/src/webgpu/compute/PathTracerMegaKernel.js +470 -0
- package/src/webgpu/compute/SampleDebugKernel.js +51 -0
- package/src/webgpu/compute/TallySampleCountsKernel.js +111 -0
- package/src/webgpu/compute/ZeroOutBufferKernel.js +35 -0
- package/src/webgpu/compute/ZeroOutKernel.js +31 -0
- package/src/webgpu/compute/wavefront/LogicKernel.js +349 -0
- package/src/webgpu/compute/wavefront/MaterialKernel.js +371 -0
- package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +72 -0
- package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +38 -0
- package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +66 -0
- package/src/webgpu/compute/wavefront/TraceRayKernel.js +71 -0
- package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +67 -0
- package/src/webgpu/compute/wavefront/structs.js +155 -0
- package/src/webgpu/constants.js +62 -0
- package/src/webgpu/denoise/OIDNDenoiser.js +431 -0
- package/src/webgpu/index.d.ts +176 -0
- package/src/webgpu/index.js +11 -0
- package/src/webgpu/materials/GltfCompliantMaterial.js +608 -0
- package/src/webgpu/materials/GraphMaterial.js +331 -0
- package/src/webgpu/materials/PathtracingMaterial.js +89 -0
- package/src/webgpu/materials/RenderToScreenMaterial.js +150 -0
- package/src/webgpu/materials/debug/AtlasDebugMaterial.js +41 -0
- package/src/webgpu/materials/debug/SampleDensityMaterial.js +61 -0
- package/src/webgpu/nodes/PathtracerBVHComputeData.js +1037 -0
- package/src/webgpu/nodes/debugBounds.wgsl.js +228 -0
- package/src/webgpu/nodes/eon.wgsl.js +262 -0
- package/src/webgpu/nodes/ggx.wgsl.js +190 -0
- package/src/webgpu/nodes/lights.wgsl.js +203 -0
- package/src/webgpu/nodes/material.wgsl.js +990 -0
- package/src/webgpu/nodes/rand/bluedither.wgsl.js +92 -0
- package/src/webgpu/nodes/rand/pcg.wgsl.js +81 -0
- package/src/webgpu/nodes/rand/sobol.wgsl.js +287 -0
- package/src/webgpu/nodes/random.wgsl.js +20 -0
- package/src/webgpu/nodes/reset.wgsl.js +21 -0
- package/src/webgpu/nodes/sampling.wgsl.js +193 -0
- package/src/webgpu/nodes/sheen.wgsl.js +148 -0
- package/src/webgpu/nodes/structs.wgsl.js +272 -0
- package/src/webgpu/nodes/utils.wgsl.js +366 -0
- package/src/webgpu/shims/ArrayCameraShim.js +74 -0
- package/src/webgpu/shims/EquirectCameraShim.js +37 -0
- package/src/webgpu/shims/PhysicalCameraShim.js +151 -0
- package/src/webgpu/upscale/FSRUpscaler.js +108 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MeshBasicMaterial,
|
|
3
|
+
RenderTarget,
|
|
4
|
+
NoToneMapping,
|
|
5
|
+
QuadMesh,
|
|
6
|
+
NoBlending,
|
|
7
|
+
Color,
|
|
8
|
+
Vector4,
|
|
9
|
+
} from 'three/webgpu';
|
|
10
|
+
|
|
11
|
+
const MAX_TEXTURE_SIZE = 4096;
|
|
12
|
+
const _prevClearColor = new Color();
|
|
13
|
+
|
|
14
|
+
function getTextureHash( texture ) {
|
|
15
|
+
|
|
16
|
+
return `${ texture.source.uuid }:${ texture.source.version }`;
|
|
17
|
+
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// TODO: consider adding a "waste map"
|
|
21
|
+
// TODO: consider using "best fit"
|
|
22
|
+
|
|
23
|
+
// Skyline rectangle packing (bottom-left, no waste map).
|
|
24
|
+
// A "skyline" is an array of contiguous segments { x, y, width } that always
|
|
25
|
+
// spans [ 0, pageWidth ] and describes the upper contour of placed rectangles.
|
|
26
|
+
// Each rect is placed where its top edge is lowest ( leftmost on ties ).
|
|
27
|
+
//
|
|
28
|
+
// Reference: Jukka Jylänki, "A Thousand Ways to Pack the Bin - A Practical
|
|
29
|
+
// Approach to Two-Dimensional Rectangle Bin Packing" (2010), the Skyline
|
|
30
|
+
// Bottom-Left (SkylineBL) heuristic. See also Sean Barrett's stb_rect_pack.h:
|
|
31
|
+
// https://github.com/nothings/stb/blob/master/stb_rect_pack.h
|
|
32
|
+
|
|
33
|
+
// Resting y for a w-wide rect whose left edge sits at nodes[ i ].x. The rect
|
|
34
|
+
// bridges every segment it spans and rests on the tallest of them. Returns -1
|
|
35
|
+
// if it would run off the right edge.
|
|
36
|
+
function skylineFit( nodes, i, w, pageWidth ) {
|
|
37
|
+
|
|
38
|
+
const x = nodes[ i ].x;
|
|
39
|
+
if ( x + w > pageWidth ) {
|
|
40
|
+
|
|
41
|
+
return - 1;
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let widthLeft = w;
|
|
46
|
+
let y = 0;
|
|
47
|
+
let j = i;
|
|
48
|
+
while ( widthLeft > 0 ) {
|
|
49
|
+
|
|
50
|
+
// defensive: the skyline should always cover the full page width
|
|
51
|
+
if ( j >= nodes.length ) {
|
|
52
|
+
|
|
53
|
+
return - 1;
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
y = Math.max( y, nodes[ j ].y );
|
|
58
|
+
widthLeft -= nodes[ j ].width;
|
|
59
|
+
j ++;
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return y;
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Patch the skyline after placing a rect spanning [ x, x + w ] with top edge y.
|
|
68
|
+
// The left edge x always aligns to an existing segment boundary, so only the
|
|
69
|
+
// right edge can split a segment. Mutates nodes in place.
|
|
70
|
+
function addSkylineNode( nodes, x, y, w ) {
|
|
71
|
+
|
|
72
|
+
const right = x + w;
|
|
73
|
+
const result = [];
|
|
74
|
+
let inserted = false;
|
|
75
|
+
|
|
76
|
+
for ( let i = 0; i < nodes.length; i ++ ) {
|
|
77
|
+
|
|
78
|
+
const node = nodes[ i ];
|
|
79
|
+
const nodeRight = node.x + node.width;
|
|
80
|
+
|
|
81
|
+
if ( nodeRight <= x ) {
|
|
82
|
+
|
|
83
|
+
// entirely left of the new node
|
|
84
|
+
result.push( node );
|
|
85
|
+
|
|
86
|
+
} else if ( node.x >= right ) {
|
|
87
|
+
|
|
88
|
+
// entirely right of the new node
|
|
89
|
+
if ( ! inserted ) {
|
|
90
|
+
|
|
91
|
+
result.push( { x, y, width: w } );
|
|
92
|
+
inserted = true;
|
|
93
|
+
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
result.push( node );
|
|
97
|
+
|
|
98
|
+
} else {
|
|
99
|
+
|
|
100
|
+
// overlapped by the new node
|
|
101
|
+
if ( ! inserted ) {
|
|
102
|
+
|
|
103
|
+
result.push( { x, y, width: w } );
|
|
104
|
+
inserted = true;
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// keep any right-side remainder at its original height
|
|
109
|
+
if ( nodeRight > right ) {
|
|
110
|
+
|
|
111
|
+
result.push( { x: right, y: node.y, width: nodeRight - right } );
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if ( ! inserted ) {
|
|
120
|
+
|
|
121
|
+
result.push( { x, y, width: w } );
|
|
122
|
+
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// merge adjacent segments of equal height and write back into nodes
|
|
126
|
+
nodes.length = 0;
|
|
127
|
+
for ( let i = 0; i < result.length; i ++ ) {
|
|
128
|
+
|
|
129
|
+
const node = result[ i ];
|
|
130
|
+
const last = nodes[ nodes.length - 1 ];
|
|
131
|
+
if ( last && last.y === node.y && last.x + last.width === node.x ) {
|
|
132
|
+
|
|
133
|
+
last.width += node.width;
|
|
134
|
+
|
|
135
|
+
} else {
|
|
136
|
+
|
|
137
|
+
nodes.push( node );
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Try to place a w x h rect into a single page's skyline. Returns the chosen
|
|
146
|
+
// { x, y } or null if it does not fit.
|
|
147
|
+
function skylineInsert( nodes, w, h, pageWidth, pageHeight ) {
|
|
148
|
+
|
|
149
|
+
let bestY = Infinity;
|
|
150
|
+
let bestX = 0;
|
|
151
|
+
let bestIndex = - 1;
|
|
152
|
+
|
|
153
|
+
for ( let i = 0; i < nodes.length; i ++ ) {
|
|
154
|
+
|
|
155
|
+
const y = skylineFit( nodes, i, w, pageWidth );
|
|
156
|
+
if ( y >= 0 && y + h <= pageHeight && y < bestY ) {
|
|
157
|
+
|
|
158
|
+
bestY = y;
|
|
159
|
+
bestX = nodes[ i ].x;
|
|
160
|
+
bestIndex = i;
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if ( bestIndex === - 1 ) {
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
addSkylineNode( nodes, bestX, bestY + h, w );
|
|
173
|
+
return { x: bestX, y: bestY };
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Packs a set of textures into one or more atlas pages and exposes both the
|
|
178
|
+
// packed texture and a Uint32Array of per-texture info ( atlas bounds + page )
|
|
179
|
+
// suitable for upload as a uniform array.
|
|
180
|
+
export class AtlasTexture {
|
|
181
|
+
|
|
182
|
+
get width() {
|
|
183
|
+
|
|
184
|
+
return this.renderTarget.width;
|
|
185
|
+
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
get height() {
|
|
189
|
+
|
|
190
|
+
return this.renderTarget.height;
|
|
191
|
+
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
get pageCount() {
|
|
195
|
+
|
|
196
|
+
return this.renderTarget.depth;
|
|
197
|
+
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get texture() {
|
|
201
|
+
|
|
202
|
+
return this.renderTarget.texture;
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
constructor( options = {} ) {
|
|
207
|
+
|
|
208
|
+
// RenderTarget already defaults to RGBA8, linear filtering, clamp wrapping
|
|
209
|
+
// and no mipmaps, so we only specify the array-target-specific options here.
|
|
210
|
+
this.renderTarget = new RenderTarget( 1, 1, {
|
|
211
|
+
depthBuffer: false,
|
|
212
|
+
depth: 1,
|
|
213
|
+
...options,
|
|
214
|
+
} );
|
|
215
|
+
this.texture.isArrayTexture = true;
|
|
216
|
+
|
|
217
|
+
// per-texture packed info ( one Vector4 each ): see _buildTextureInfo
|
|
218
|
+
this.textureInfo = [ new Vector4() ];
|
|
219
|
+
|
|
220
|
+
this.hashes = [];
|
|
221
|
+
this.placements = [];
|
|
222
|
+
this.quadMesh = new QuadMesh( new MeshBasicMaterial() );
|
|
223
|
+
this.quadMesh.material.blending = NoBlending;
|
|
224
|
+
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setTextures( renderer, textures ) {
|
|
228
|
+
|
|
229
|
+
// The order the textures arrive in follows material order, which can change without the
|
|
230
|
+
// set of textures changing. Pack in a uuid-stable order instead, and map the placements
|
|
231
|
+
// back to the caller's indices afterwards.
|
|
232
|
+
const order = textures.map( ( t, i ) => i );
|
|
233
|
+
order.sort( ( a, b ) => textures[ a ].source.uuid < textures[ b ].source.uuid ? - 1 : 1 );
|
|
234
|
+
textures = order.map( i => textures[ i ] );
|
|
235
|
+
|
|
236
|
+
// nothing to do if the set of textures is unchanged
|
|
237
|
+
if ( this._hashesMatch( textures ) ) {
|
|
238
|
+
|
|
239
|
+
this._buildCallerTextureInfo( order );
|
|
240
|
+
return;
|
|
241
|
+
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// calculate the maximum dimension of the atlas
|
|
245
|
+
let maxDim = textures.reduce( ( v, t ) => Math.max( v, t.width, t.height ), 1 );
|
|
246
|
+
maxDim = Math.min( MAX_TEXTURE_SIZE, maxDim );
|
|
247
|
+
|
|
248
|
+
const width = maxDim;
|
|
249
|
+
const height = maxDim;
|
|
250
|
+
|
|
251
|
+
// full repack of every texture
|
|
252
|
+
const { renderTarget } = this;
|
|
253
|
+
const { placements, pageCount } = this._packTextures( textures, width, height );
|
|
254
|
+
|
|
255
|
+
// RenderTarget.setSize resets isArrayTexture to "image.depth > 1" so
|
|
256
|
+
// we reset it here.
|
|
257
|
+
renderTarget.setSize( width, height, pageCount );
|
|
258
|
+
this.texture.isArrayTexture = true;
|
|
259
|
+
|
|
260
|
+
this._renderTextures( renderer, textures, placements );
|
|
261
|
+
this.placements = placements;
|
|
262
|
+
|
|
263
|
+
// rebuild hashes and the texture -> index lookup
|
|
264
|
+
const hashes = new Array( textures.length );
|
|
265
|
+
for ( let i = 0, l = textures.length; i < l; i ++ ) {
|
|
266
|
+
|
|
267
|
+
const hash = getTextureHash( textures[ i ] );
|
|
268
|
+
hashes[ i ] = hash;
|
|
269
|
+
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.hashes = hashes;
|
|
273
|
+
|
|
274
|
+
this._buildCallerTextureInfo( order );
|
|
275
|
+
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// The atlas packs in its own stable order, the shader indexes textureInfo with the order the
|
|
279
|
+
// caller passed the textures in - "order[ i ]" is where the i-th packed texture came from.
|
|
280
|
+
_buildCallerTextureInfo( order ) {
|
|
281
|
+
|
|
282
|
+
const placements = new Array( order.length );
|
|
283
|
+
for ( let i = 0, l = order.length; i < l; i ++ ) {
|
|
284
|
+
|
|
285
|
+
placements[ order[ i ] ] = this.placements[ i ];
|
|
286
|
+
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this._buildTextureInfo( placements );
|
|
290
|
+
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
dispose() {
|
|
294
|
+
|
|
295
|
+
this.renderTarget.dispose();
|
|
296
|
+
this.quadMesh.material.dispose();
|
|
297
|
+
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_hashesMatch( textures ) {
|
|
301
|
+
|
|
302
|
+
const hashes = this.hashes;
|
|
303
|
+
if ( hashes.length !== textures.length ) {
|
|
304
|
+
|
|
305
|
+
return false;
|
|
306
|
+
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for ( let i = 0, l = textures.length; i < l; i ++ ) {
|
|
310
|
+
|
|
311
|
+
if ( hashes[ i ] !== getTextureHash( textures[ i ] ) ) {
|
|
312
|
+
|
|
313
|
+
return false;
|
|
314
|
+
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return true;
|
|
320
|
+
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
_packTextures( textures, width, height ) {
|
|
324
|
+
|
|
325
|
+
// use each texture's native size, clamped so it can physically fit a page
|
|
326
|
+
// ( anything larger is downsampled into its rect during the blit )
|
|
327
|
+
const items = [];
|
|
328
|
+
for ( let i = 0, l = textures.length; i < l; i ++ ) {
|
|
329
|
+
|
|
330
|
+
const texture = textures[ i ];
|
|
331
|
+
items.push( {
|
|
332
|
+
index: i,
|
|
333
|
+
w: Math.min( texture.width, width ),
|
|
334
|
+
h: Math.min( texture.height, height ),
|
|
335
|
+
} );
|
|
336
|
+
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// place taller rects first for tighter skyline occupancy
|
|
340
|
+
const order = items.sort( ( a, b ) => b.h - a.h );
|
|
341
|
+
|
|
342
|
+
const placements = new Array( textures.length );
|
|
343
|
+
const pages = [];
|
|
344
|
+
|
|
345
|
+
for ( let k = 0, l = order.length; k < l; k ++ ) {
|
|
346
|
+
|
|
347
|
+
const item = order[ k ];
|
|
348
|
+
let placed = null;
|
|
349
|
+
let page = - 1;
|
|
350
|
+
|
|
351
|
+
// try to fit into an existing page
|
|
352
|
+
for ( let p = 0; p < pages.length; p ++ ) {
|
|
353
|
+
|
|
354
|
+
placed = skylineInsert( pages[ p ], item.w, item.h, width, height );
|
|
355
|
+
if ( placed ) {
|
|
356
|
+
|
|
357
|
+
page = p;
|
|
358
|
+
break;
|
|
359
|
+
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// otherwise open a new page
|
|
365
|
+
if ( ! placed ) {
|
|
366
|
+
|
|
367
|
+
const skyline = [ { x: 0, y: 0, width: width } ];
|
|
368
|
+
placed = skylineInsert( skyline, item.w, item.h, width, height );
|
|
369
|
+
pages.push( skyline );
|
|
370
|
+
page = pages.length - 1;
|
|
371
|
+
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
placements[ item.index ] = { x: placed.x, y: placed.y, w: item.w, h: item.h, page };
|
|
375
|
+
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { placements, pageCount: Math.max( pages.length, 1 ) };
|
|
379
|
+
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_buildTextureInfo( placements ) {
|
|
383
|
+
|
|
384
|
+
const info = this.textureInfo;
|
|
385
|
+
info.length = 0;
|
|
386
|
+
for ( let i = 0, l = placements.length; i < l; i ++ ) {
|
|
387
|
+
|
|
388
|
+
// offsetX and Y in pixels (16 bits + 16 bits)
|
|
389
|
+
// width and height in pixels (16 bit + 16 bits)
|
|
390
|
+
// page + reserved (16 bits)
|
|
391
|
+
// reserved
|
|
392
|
+
const { x, y, w, h, page } = placements[ i ];
|
|
393
|
+
const v = new Vector4();
|
|
394
|
+
v.x = ( x & 0xFFFF ) | ( ( y & 0xFFFF ) << 16 );
|
|
395
|
+
v.y = ( w & 0xFFFF ) | ( ( h & 0xFFFF ) << 16 );
|
|
396
|
+
v.z = page & 0xFFFF;
|
|
397
|
+
v.w = 0;
|
|
398
|
+
info.push( v );
|
|
399
|
+
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if ( info.length === 0 ) {
|
|
403
|
+
|
|
404
|
+
info.push( new Vector4( 0, 0, 0, 0 ) );
|
|
405
|
+
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_renderTextures( renderer, textures, placements ) {
|
|
411
|
+
|
|
412
|
+
const { quadMesh, renderTarget, width, height } = this;
|
|
413
|
+
|
|
414
|
+
// Save previous renderer state
|
|
415
|
+
const prevRenderTarget = renderer.getRenderTarget();
|
|
416
|
+
const prevToneMapping = renderer.toneMapping;
|
|
417
|
+
const prevAutoClear = renderer.autoClear;
|
|
418
|
+
const prevScissorTest = renderer.getScissorTest();
|
|
419
|
+
const prevClearAlpha = renderer.getClearAlpha();
|
|
420
|
+
renderer.getClearColor( _prevClearColor );
|
|
421
|
+
|
|
422
|
+
// blit each tile into its sub-rect without clearing the rest of the page
|
|
423
|
+
renderer.toneMapping = NoToneMapping;
|
|
424
|
+
renderer.autoClear = false;
|
|
425
|
+
renderer.setScissorTest( true );
|
|
426
|
+
renderer.setClearColor( 0, 0 );
|
|
427
|
+
|
|
428
|
+
// clear the pages
|
|
429
|
+
for ( let i = 0, l = renderTarget.depth; i < l; i ++ ) {
|
|
430
|
+
|
|
431
|
+
renderTarget.viewport.set( 0, 0, width, height );
|
|
432
|
+
renderTarget.scissor.set( 0, 0, width, height );
|
|
433
|
+
renderer.setRenderTarget( renderTarget, i );
|
|
434
|
+
renderer.clear();
|
|
435
|
+
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for ( let i = 0, l = textures.length; i < l; i ++ ) {
|
|
439
|
+
|
|
440
|
+
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;
|
|
448
|
+
|
|
449
|
+
// three.js uses the render target viewport / scissor
|
|
450
|
+
renderTarget.viewport.set( x, y, w, h );
|
|
451
|
+
renderTarget.scissor.set( x, y, w, h );
|
|
452
|
+
renderer.setRenderTarget( renderTarget, page );
|
|
453
|
+
quadMesh.render( renderer );
|
|
454
|
+
|
|
455
|
+
texture.dispose();
|
|
456
|
+
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// restore the render target's full-size viewport/scissor and the renderer
|
|
460
|
+
renderTarget.viewport.set( 0, 0, width, height );
|
|
461
|
+
renderTarget.scissor.set( 0, 0, width, height );
|
|
462
|
+
quadMesh.material.map = null;
|
|
463
|
+
renderer.setClearColor( _prevClearColor, prevClearAlpha );
|
|
464
|
+
renderer.setScissorTest( prevScissorTest );
|
|
465
|
+
renderer.autoClear = prevAutoClear;
|
|
466
|
+
renderer.setRenderTarget( prevRenderTarget );
|
|
467
|
+
renderer.toneMapping = prevToneMapping;
|
|
468
|
+
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** @import { Texture, WebGPURenderer } from 'three/webgpu' */
|
|
2
|
+
import {
|
|
3
|
+
RenderTarget,
|
|
4
|
+
FloatType,
|
|
5
|
+
HalfFloatType,
|
|
6
|
+
RGBAFormat,
|
|
7
|
+
DataTexture,
|
|
8
|
+
DataUtils,
|
|
9
|
+
EquirectangularReflectionMapping,
|
|
10
|
+
NodeMaterial,
|
|
11
|
+
QuadMesh,
|
|
12
|
+
} from 'three/webgpu';
|
|
13
|
+
import { Fn, pmremTexture, uniform, uv, vec3, float, sin, cos } from 'three/tsl';
|
|
14
|
+
|
|
15
|
+
const PI = Math.PI;
|
|
16
|
+
|
|
17
|
+
// equirect uv -> world direction, matching three.js' env sampling direction
|
|
18
|
+
const equirectUvToDirection = /*@__PURE__*/ Fn( ( [ coord ] ) => {
|
|
19
|
+
|
|
20
|
+
const u = coord.x.sub( 0.5 );
|
|
21
|
+
const v = float( 1.0 ).sub( coord.y );
|
|
22
|
+
|
|
23
|
+
const theta = u.mul( 2.0 * PI );
|
|
24
|
+
const phi = v.mul( PI );
|
|
25
|
+
const sinPhi = sin( phi );
|
|
26
|
+
|
|
27
|
+
return vec3( sinPhi.mul( cos( theta ) ), cos( phi ), sinPhi.mul( sin( theta ) ) );
|
|
28
|
+
|
|
29
|
+
} );
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Produces a PMREM prefiltered, optionally downsampled equirectangular copy of an environment
|
|
33
|
+
* map. Blurring removes the small bright details that make an environment slow to converge.
|
|
34
|
+
*/
|
|
35
|
+
export class BlurredEnvMapGenerator {
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {WebGPURenderer} renderer
|
|
39
|
+
*/
|
|
40
|
+
constructor( renderer ) {
|
|
41
|
+
|
|
42
|
+
this.renderer = renderer;
|
|
43
|
+
|
|
44
|
+
// set up the pmrem sampling material
|
|
45
|
+
this._blur = uniform( 0 );
|
|
46
|
+
this._pmremNode = pmremTexture( null, equirectUvToDirection( uv() ), this._blur );
|
|
47
|
+
|
|
48
|
+
const material = new NodeMaterial();
|
|
49
|
+
material.colorNode = this._pmremNode;
|
|
50
|
+
material.toneMapped = false;
|
|
51
|
+
|
|
52
|
+
this.quad = new QuadMesh( material );
|
|
53
|
+
this.renderTarget = new RenderTarget( 1, 1, { type: FloatType, format: RGBAFormat, depthBuffer: false } );
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Frees every GPU resource held by the generator.
|
|
59
|
+
*/
|
|
60
|
+
dispose() {
|
|
61
|
+
|
|
62
|
+
// disposes the node's internal PMREMGenerator and its prefiltering render targets
|
|
63
|
+
this._pmremNode.value = null;
|
|
64
|
+
this._pmremNode.dispose();
|
|
65
|
+
this.quad.material.dispose();
|
|
66
|
+
this.renderTarget.dispose();
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Renders a blurred copy of the given environment map. The source texture is left untouched.
|
|
72
|
+
*
|
|
73
|
+
* @param {Texture} texture - Equirectangular environment map to blur.
|
|
74
|
+
* @param {number} [blur=0] - Blur amount, from 0 to 1.
|
|
75
|
+
* @param {number} [width=null] - Output width. Defaults to the source width.
|
|
76
|
+
* @param {number} [height=null] - Output height. Defaults to the source height.
|
|
77
|
+
* @returns {Promise<Texture>}
|
|
78
|
+
* @note The returned texture is owned by the caller and must be disposed.
|
|
79
|
+
*/
|
|
80
|
+
async generate( texture, blur = 0, width = null, height = null ) {
|
|
81
|
+
|
|
82
|
+
const { renderTarget, quad, renderer } = this;
|
|
83
|
+
|
|
84
|
+
// clone the texture so we can ensure references are cleaned up
|
|
85
|
+
const cloned = texture.clone();
|
|
86
|
+
|
|
87
|
+
// default to the source resolution
|
|
88
|
+
width = width ?? cloned.image.width;
|
|
89
|
+
height = height ?? cloned.image.height;
|
|
90
|
+
|
|
91
|
+
renderTarget.setSize( width, height );
|
|
92
|
+
renderTarget.texture.colorSpace = cloned.colorSpace;
|
|
93
|
+
|
|
94
|
+
// update the uniforms
|
|
95
|
+
this._pmremNode.value = cloned;
|
|
96
|
+
this._blur.value = blur;
|
|
97
|
+
|
|
98
|
+
// render the blurred equirect to the target
|
|
99
|
+
const prevTarget = renderer.getRenderTarget();
|
|
100
|
+
renderer.setRenderTarget( renderTarget );
|
|
101
|
+
quad.render( renderer );
|
|
102
|
+
renderer.setRenderTarget( prevTarget );
|
|
103
|
+
|
|
104
|
+
// read the data back and pack to half float
|
|
105
|
+
const readBuffer = await renderer.readRenderTargetPixelsAsync( renderTarget, 0, 0, width, height );
|
|
106
|
+
const buffer = new Uint16Array( width * height * 4 );
|
|
107
|
+
for ( let i = 0, l = buffer.length; i < l; i ++ ) {
|
|
108
|
+
|
|
109
|
+
buffer[ i ] = DataUtils.toHalfFloat( readBuffer[ i ] );
|
|
110
|
+
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const result = new DataTexture( buffer, width, height, RGBAFormat, HalfFloatType );
|
|
114
|
+
result.minFilter = cloned.minFilter;
|
|
115
|
+
result.magFilter = cloned.magFilter;
|
|
116
|
+
result.wrapS = cloned.wrapS;
|
|
117
|
+
result.wrapT = cloned.wrapT;
|
|
118
|
+
result.mapping = EquirectangularReflectionMapping;
|
|
119
|
+
result.colorSpace = cloned.colorSpace;
|
|
120
|
+
result.needsUpdate = true;
|
|
121
|
+
|
|
122
|
+
this._pmremNode.value = null;
|
|
123
|
+
cloned.dispose();
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { DataTexture, LinearFilter, Matrix4, RGBAFormat, RepeatWrapping } from 'three';
|
|
2
|
+
import { sampler, texture, uniform } from 'three/tsl';
|
|
3
|
+
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
4
|
+
import { equirectDirectionToUvFn, sampleHemisphereFn } from './nodes/sampling.wgsl.js';
|
|
5
|
+
|
|
6
|
+
export class EquirectBackgroundInfo {
|
|
7
|
+
|
|
8
|
+
get map() {
|
|
9
|
+
|
|
10
|
+
return this.mapNode.value;
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
set map( v ) {
|
|
15
|
+
|
|
16
|
+
this.mapNode.value = v;
|
|
17
|
+
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get blur() {
|
|
21
|
+
|
|
22
|
+
return this.blurNode.value;
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
set blur( v ) {
|
|
27
|
+
|
|
28
|
+
this.blurNode.value = v;
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get rotation() {
|
|
33
|
+
|
|
34
|
+
return this.rotationNode.value;
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get intensity() {
|
|
39
|
+
|
|
40
|
+
return this.intensityNode.value;
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
set intensity( v ) {
|
|
45
|
+
|
|
46
|
+
this.intensityNode.value = v;
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
|
|
52
|
+
const defaultMap = new DataTexture( new Uint8Array( [ 0, 0, 0, 255 ] ), 1, 1, RGBAFormat );
|
|
53
|
+
defaultMap.minFilter = LinearFilter;
|
|
54
|
+
defaultMap.magFilter = LinearFilter;
|
|
55
|
+
defaultMap.wrapS = RepeatWrapping;
|
|
56
|
+
defaultMap.wrapT = RepeatWrapping;
|
|
57
|
+
defaultMap.generateMipmaps = false;
|
|
58
|
+
defaultMap.needsUpdate = true;
|
|
59
|
+
|
|
60
|
+
this.mapNode = texture( defaultMap );
|
|
61
|
+
this.mapSampler = sampler( this.mapNode );
|
|
62
|
+
this.blurNode = uniform( 0.0 );
|
|
63
|
+
this.rotationNode = uniform( new Matrix4() );
|
|
64
|
+
this.intensityNode = uniform( 1.0 );
|
|
65
|
+
|
|
66
|
+
this._initFns();
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
dispose() {
|
|
71
|
+
|
|
72
|
+
this.map.dispose();
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_initFns() {
|
|
77
|
+
|
|
78
|
+
const { mapNode, mapSampler, blurNode, rotationNode, intensityNode } = this;
|
|
79
|
+
|
|
80
|
+
this.sampleColor = wgslTagFn/* wgsl */`
|
|
81
|
+
fn sampleBackground( direction: vec3f, uv: vec2f ) -> vec4f {
|
|
82
|
+
|
|
83
|
+
let rotatedDir = ( ${ rotationNode } * vec4f( direction, 0.0 ) ).xyz;
|
|
84
|
+
let offsetDir = ${ sampleHemisphereFn }( rotatedDir, uv ) * 0.5 * ${ blurNode };
|
|
85
|
+
let sampleDir = normalize( rotatedDir + offsetDir );
|
|
86
|
+
|
|
87
|
+
let mapUv = ${ equirectDirectionToUvFn }( sampleDir );
|
|
88
|
+
let col = textureSampleLevel( ${ mapNode }, ${ mapSampler }, mapUv, 0 );
|
|
89
|
+
|
|
90
|
+
return vec4f( ${ intensityNode } * col.rgb, col.a );
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
}
|