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.
- package/README.md +4 -2
- package/build/index.module.js +23 -5
- package/build/index.module.js.map +1 -1
- package/build/index.umd.cjs +22 -4
- package/build/index.umd.cjs.map +1 -1
- package/package.json +8 -8
- package/src/core/PathTracingSceneGenerator.js +1 -1
- package/src/core/utils/sceneUpdateUtils.js +2 -1
- package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
- package/src/uniforms/LightsInfoUniformStruct.js +16 -1
- package/src/webgpu/API.md +167 -19
- package/src/webgpu/AtlasTexture.js +32 -12
- package/src/webgpu/EquirectHdrInfoNode.js +97 -46
- package/src/webgpu/LightsInfoNode.js +142 -4
- package/src/webgpu/MegaKernelPathTracer.js +11 -3
- package/src/webgpu/PathTracerBackend.js +174 -21
- package/src/webgpu/TurquinTexture.js +2 -0
- package/src/webgpu/WaveFrontPathTracer.js +169 -71
- package/src/webgpu/WebGPUPathTracer.js +637 -51
- package/src/webgpu/compute/ComputeKernel.js +18 -0
- package/src/webgpu/compute/PathTracerMegaKernel.js +253 -71
- package/src/webgpu/compute/TallySampleCountsKernel.js +10 -10
- package/src/webgpu/compute/UnpackAuxKernel.js +48 -0
- package/src/webgpu/compute/ZeroOutKernel.js +3 -1
- package/src/webgpu/compute/targets.js +55 -0
- package/src/webgpu/compute/wavefront/LogicKernel.js +136 -55
- package/src/webgpu/compute/wavefront/MaterialKernel.js +153 -66
- package/src/webgpu/compute/wavefront/MediumKernel.js +154 -0
- package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +5 -5
- package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +5 -5
- package/src/webgpu/compute/wavefront/QueueMediumWalksKernel.js +52 -0
- package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +6 -6
- package/src/webgpu/compute/wavefront/TraceRayKernel.js +11 -9
- package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +18 -23
- package/src/webgpu/compute/wavefront/structs.js +63 -35
- package/src/webgpu/constants.js +2 -2
- package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
- package/src/webgpu/index.d.ts +79 -6
- package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
- package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
- package/src/webgpu/nodes/PathtracerBVHComputeData.js +109 -17
- package/src/webgpu/nodes/aux.wgsl.js +61 -0
- package/src/webgpu/nodes/eon.wgsl.js +1 -1
- package/src/webgpu/nodes/lights.wgsl.js +21 -1
- package/src/webgpu/nodes/material.wgsl.js +2 -1
- package/src/webgpu/nodes/medium.wgsl.js +165 -0
- package/src/webgpu/nodes/random.wgsl.js +2 -0
- package/src/webgpu/nodes/sampling.wgsl.js +44 -3
- package/src/webgpu/nodes/structs.wgsl.js +8 -1
- package/src/webgpu/upscale/FSRUpscaler.js +3 -2
|
@@ -2,7 +2,7 @@ import { StorageBufferAttribute } from 'three/webgpu';
|
|
|
2
2
|
import { uniform, storage, globalId } from 'three/tsl';
|
|
3
3
|
import { ComputeKernel } from '../ComputeKernel.js';
|
|
4
4
|
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
|
-
import { rayDataStruct,
|
|
5
|
+
import { rayDataStruct, PIXEL_QUEUE_ELEMENT_COUNT, PIXEL_QUEUE_HEADER } from './structs.js';
|
|
6
6
|
|
|
7
7
|
// Moves pixels between a range of path slots and the queue when the pool is resized. Slots added
|
|
8
8
|
// by a grow take a pixel from the queue tail and start idle, as MaterialKernel expects. Slots
|
|
@@ -14,7 +14,7 @@ export class ResetSlotsKernel extends ComputeKernel {
|
|
|
14
14
|
|
|
15
15
|
const params = {
|
|
16
16
|
rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ),
|
|
17
|
-
pixelQueue: storage( new StorageBufferAttribute( 1, 1 ),
|
|
17
|
+
pixelQueue: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ).toAtomic(),
|
|
18
18
|
start: uniform( 0, 'uint' ),
|
|
19
19
|
end: uniform( 0, 'uint' ),
|
|
20
20
|
addingSlots: uniform( 0, 'uint' ),
|
|
@@ -37,14 +37,14 @@ export class ResetSlotsKernel extends ComputeKernel {
|
|
|
37
37
|
if ( addingSlots == 0u ) {
|
|
38
38
|
|
|
39
39
|
// slots being removed hand their pixel back before the pool is discarded
|
|
40
|
-
let queueIndex = atomicAdd( &pixelQueue
|
|
41
|
-
atomicStore( &pixelQueue
|
|
40
|
+
let queueIndex = atomicAdd( &pixelQueue[ ${ PIXEL_QUEUE_ELEMENT_COUNT }u ], 1u );
|
|
41
|
+
atomicStore( &pixelQueue[ ${ PIXEL_QUEUE_HEADER }u + queueIndex ], rayDataStorage[ index ].pixelIndex );
|
|
42
42
|
|
|
43
43
|
} else {
|
|
44
44
|
|
|
45
45
|
// the pool never exceeds the pixel count, so the queue holds enough for every new slot
|
|
46
|
-
let previousCount = atomicSub( &pixelQueue
|
|
47
|
-
rayDataStorage[ index ].pixelIndex = atomicLoad( &pixelQueue
|
|
46
|
+
let previousCount = atomicSub( &pixelQueue[ ${ PIXEL_QUEUE_ELEMENT_COUNT }u ], 1u );
|
|
47
|
+
rayDataStorage[ index ].pixelIndex = atomicLoad( &pixelQueue[ ${ PIXEL_QUEUE_HEADER }u + previousCount - 1u ] );
|
|
48
48
|
rayDataStorage[ index ].resultColor = vec4f( 0.0 );
|
|
49
49
|
rayDataStorage[ index ].throughputColor = vec3f( 0.0 );
|
|
50
50
|
rayDataStorage[ index ].objectIndex = - 1;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { StorageBufferAttribute } from 'three/webgpu';
|
|
2
2
|
import { ComputeKernel } from '../ComputeKernel.js';
|
|
3
3
|
import { storage, globalId } from 'three/tsl';
|
|
4
|
-
import {
|
|
4
|
+
import { contextProxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
5
|
import { rngInit } from '../../nodes/random.wgsl.js';
|
|
6
|
-
import {
|
|
6
|
+
import { traceQueuedRayStruct, intersectionResultStruct, RAY_QUEUE_LENGTH } from './structs.js';
|
|
7
7
|
|
|
8
8
|
// Pure BVH traversal over the queued bounce rays: one thread per queued ray, writing a compact
|
|
9
9
|
// intersection result at the ray's queue index for LogicKernel to consume next frame.
|
|
@@ -12,34 +12,36 @@ export class TraceRayKernel extends ComputeKernel {
|
|
|
12
12
|
constructor( ) {
|
|
13
13
|
|
|
14
14
|
const params = {
|
|
15
|
-
bvhData: { value: null },
|
|
16
15
|
|
|
17
|
-
rayQueue: storage( new StorageBufferAttribute( 1, 1 ),
|
|
16
|
+
rayQueue: storage( new StorageBufferAttribute( 1, 1 ), traceQueuedRayStruct ),
|
|
17
|
+
queueCounters: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ),
|
|
18
18
|
rayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), intersectionResultStruct ),
|
|
19
19
|
|
|
20
20
|
globalId: globalId,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const raycastOutput =
|
|
24
|
-
const raycastFirstHitFn =
|
|
23
|
+
const raycastOutput = contextProxy( 'bvhData.fns.raycastFirstHit.outputType' );
|
|
24
|
+
const raycastFirstHitFn = contextProxy( 'bvhData.fns.raycastFirstHit' );
|
|
25
25
|
|
|
26
26
|
const fn = wgslTagFn /* wgsl */`
|
|
27
27
|
|
|
28
28
|
fn compute( globalId: vec3u ) -> void {
|
|
29
29
|
|
|
30
30
|
let rayQueue = &${ params.rayQueue };
|
|
31
|
+
let queueCounters = &${ params.queueCounters };
|
|
31
32
|
let rayIntersectionsStorage = &${ params.rayIntersectionsStorage };
|
|
32
33
|
|
|
33
34
|
let index = globalId.x;
|
|
34
|
-
if ( index >=
|
|
35
|
+
if ( index >= queueCounters[ ${ RAY_QUEUE_LENGTH }u ] ) {
|
|
35
36
|
|
|
36
37
|
return;
|
|
37
38
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
// whole structs are read from the buffer, not the pointer alias, which Safari fails to compile
|
|
42
|
+
let queuedRay = ${ params.rayQueue }[ index ];
|
|
41
43
|
let indexUV = vec2u( queuedRay.pixelIndex >> 16, queuedRay.pixelIndex & 0xFFFF );
|
|
42
|
-
${ rngInit }( indexUV, queuedRay.seed, queuedRay.
|
|
44
|
+
${ rngInit }( indexUV, queuedRay.seed, queuedRay.rngIndex );
|
|
43
45
|
|
|
44
46
|
let ray = Ray( queuedRay.origin, queuedRay.direction, queuedRay.maxDist );
|
|
45
47
|
var hitResult: ${ raycastOutput };
|
|
@@ -1,59 +1,54 @@
|
|
|
1
1
|
import { StorageBufferAttribute } from 'three/webgpu';
|
|
2
2
|
import { ComputeKernel } from '../ComputeKernel.js';
|
|
3
3
|
import { storage, globalId } from 'three/tsl';
|
|
4
|
-
import {
|
|
4
|
+
import { contextProxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
5
|
import { rngInit } from '../../nodes/random.wgsl.js';
|
|
6
|
-
import {
|
|
6
|
+
import { traceQueuedRayStruct, shadowTraceResultStruct, SHADOW_RAY_QUEUE_LENGTH } from './structs.js';
|
|
7
7
|
|
|
8
|
-
// Pure BVH traversal over the queued shadow rays. Uses
|
|
9
|
-
//
|
|
10
|
-
// hit distance against the light distance.
|
|
8
|
+
// Pure BVH traversal over the queued shadow rays. Uses an any-hit traversal without face culling that
|
|
9
|
+
// passes partially transparent surfaces, accumulating the light they block.
|
|
11
10
|
export class TraceShadowRayKernel extends ComputeKernel {
|
|
12
11
|
|
|
13
12
|
constructor( ) {
|
|
14
13
|
|
|
15
14
|
const params = {
|
|
16
|
-
bvhData: { value: null },
|
|
17
15
|
|
|
18
|
-
shadowRayQueue: storage( new StorageBufferAttribute( 1, 1 ),
|
|
19
|
-
|
|
16
|
+
shadowRayQueue: storage( new StorageBufferAttribute( 1, 1 ), traceQueuedRayStruct ),
|
|
17
|
+
queueCounters: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ),
|
|
18
|
+
shadowRayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), shadowTraceResultStruct ),
|
|
20
19
|
|
|
21
20
|
globalId: globalId,
|
|
22
21
|
};
|
|
23
22
|
|
|
24
|
-
const raycastOutput =
|
|
25
|
-
const
|
|
23
|
+
const raycastOutput = contextProxy( 'bvhData.fns.raycastFirstHit.outputType' );
|
|
24
|
+
const raycastShadowFn = contextProxy( 'bvhData.fns.raycastShadow' );
|
|
26
25
|
|
|
27
26
|
const fn = wgslTagFn /* wgsl */`
|
|
28
27
|
|
|
29
28
|
fn compute( globalId: vec3u ) -> void {
|
|
30
29
|
|
|
31
30
|
let shadowRayQueue = &${ params.shadowRayQueue };
|
|
31
|
+
let queueCounters = &${ params.queueCounters };
|
|
32
32
|
let shadowRayIntersectionsStorage = &${ params.shadowRayIntersectionsStorage };
|
|
33
33
|
|
|
34
34
|
let index = globalId.x;
|
|
35
|
-
if ( index >=
|
|
35
|
+
if ( index >= queueCounters[ ${ SHADOW_RAY_QUEUE_LENGTH }u ] ) {
|
|
36
36
|
|
|
37
37
|
return;
|
|
38
38
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// whole structs are read from the buffer, not the pointer alias, which Safari fails to compile
|
|
42
|
+
let queuedRay = ${ params.shadowRayQueue }[ index ];
|
|
42
43
|
let indexUV = vec2u( queuedRay.pixelIndex >> 16, queuedRay.pixelIndex & 0xFFFF );
|
|
43
|
-
${ rngInit }( indexUV, queuedRay.seed, queuedRay.
|
|
44
|
+
${ rngInit }( indexUV, queuedRay.seed, queuedRay.rngIndex );
|
|
44
45
|
|
|
45
46
|
let ray = Ray( queuedRay.origin, queuedRay.direction, queuedRay.maxDist );
|
|
46
47
|
var hitResult: ${ raycastOutput };
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} else {
|
|
53
|
-
|
|
54
|
-
shadowRayIntersectionsStorage[ index ].objectIndex = - 1;
|
|
55
|
-
|
|
56
|
-
}
|
|
48
|
+
let occluded = ${ raycastShadowFn }( ray, &hitResult );
|
|
49
|
+
shadowRayIntersectionsStorage[ index ].objectIndex = select( - 1, i32( hitResult.objectIndex ), occluded );
|
|
50
|
+
shadowRayIntersectionsStorage[ index ].skippedOcclusion = hitResult.skippedOcclusion;
|
|
51
|
+
shadowRayIntersectionsStorage[ index ].occlusion = hitResult.occlusion;
|
|
57
52
|
|
|
58
53
|
}
|
|
59
54
|
`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StructTypeNode } from 'three/webgpu';
|
|
2
|
+
import { mediumStateStruct } from '../../nodes/medium.wgsl.js';
|
|
2
3
|
|
|
3
4
|
// StructTypeNode that force-emits dependency structs. TSL can't discover a struct referenced only via
|
|
4
5
|
// a type string, so we build each dependency first, which registers its definition ahead of this struct.
|
|
@@ -28,7 +29,7 @@ class DependentStructTypeNode extends StructTypeNode {
|
|
|
28
29
|
|
|
29
30
|
// Persistent per-path-slot state. One slot per in-flight path, holding everything needed to resolve
|
|
30
31
|
// the previous frame's trace results and stage the next bounce.
|
|
31
|
-
|
|
32
|
+
const rayDataMembers = {
|
|
32
33
|
|
|
33
34
|
throughputColor: 'vec3f',
|
|
34
35
|
currentBounce: 'uint',
|
|
@@ -76,8 +77,10 @@ export const rayDataStruct = new StructTypeNode( {
|
|
|
76
77
|
// the traced segment length, for the transmission attenuation applied by MaterialKernel
|
|
77
78
|
dist: 'float',
|
|
78
79
|
|
|
79
|
-
// alpha test pass throughs, counted separately from the bounce count
|
|
80
|
+
// alpha test pass throughs, counted separately from the bounce count, and how far along the ray they
|
|
81
|
+
// moved the start of its traversal. The ray keeps the origin it scattered from
|
|
80
82
|
alphaDepth: 'uint',
|
|
83
|
+
rayStart: 'float',
|
|
81
84
|
|
|
82
85
|
// the camera segment's maximum trace distance, carried across alpha pass throughs
|
|
83
86
|
maxDist: 'float',
|
|
@@ -86,7 +89,30 @@ export const rayDataStruct = new StructTypeNode( {
|
|
|
86
89
|
// first dispersive interaction
|
|
87
90
|
dispersionWavelength: 'float',
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
// the first scatter's albedo and normal, 8 bits per channel, for the denoiser's auxiliary buffers
|
|
93
|
+
auxAlbedo: 'uint',
|
|
94
|
+
auxNormal: 'uint',
|
|
95
|
+
|
|
96
|
+
// scatter events taken inside media, counted separately from the bounce count
|
|
97
|
+
subsurfaceBounces: 'uint',
|
|
98
|
+
|
|
99
|
+
// whether MediumKernel walks the path's next segment, cleared once the walk reaches a surface or ends
|
|
100
|
+
walkPending: 'uint',
|
|
101
|
+
|
|
102
|
+
// whether the staged segment starts at a medium scatter event, where no light is sampled
|
|
103
|
+
mediumScattered: 'uint',
|
|
104
|
+
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// The path's medium, nested at the end to share the ray data buffer under the storage buffer limit. Sized
|
|
108
|
+
// separately since nested structs aren't.
|
|
109
|
+
export const rayDataStruct = new DependentStructTypeNode( {
|
|
110
|
+
...rayDataMembers,
|
|
111
|
+
medium: mediumStateStruct.name,
|
|
112
|
+
}, 'RayData', [ mediumStateStruct ] );
|
|
113
|
+
|
|
114
|
+
const rayDataLength = new StructTypeNode( rayDataMembers ).getLength();
|
|
115
|
+
rayDataStruct.getLength = () => rayDataLength + mediumStateStruct.getLength();
|
|
90
116
|
|
|
91
117
|
// A ray queued for BVH traversal by the trace kernels. A "maxDist" of zero traces unbounded.
|
|
92
118
|
export const traceQueuedRayStruct = new StructTypeNode( {
|
|
@@ -95,12 +121,14 @@ export const traceQueuedRayStruct = new StructTypeNode( {
|
|
|
95
121
|
pixelIndex: 'uint',
|
|
96
122
|
|
|
97
123
|
direction: 'vec3f',
|
|
98
|
-
|
|
124
|
+
|
|
125
|
+
// the traversal's random sequence index: bounces, alpha pass throughs and subsurface bounces, as in the megakernel
|
|
126
|
+
rngIndex: 'uint',
|
|
99
127
|
|
|
100
128
|
seed: 'uint',
|
|
101
|
-
alphaDepth: 'uint',
|
|
102
129
|
maxDist: 'float',
|
|
103
130
|
_alignment0: 'uint',
|
|
131
|
+
_alignment1: 'uint',
|
|
104
132
|
|
|
105
133
|
}, 'TraceQueuedRay' );
|
|
106
134
|
|
|
@@ -118,38 +146,38 @@ export const intersectionResultStruct = new StructTypeNode( {
|
|
|
118
146
|
side: 'float',
|
|
119
147
|
|
|
120
148
|
indices: 'vec3u',
|
|
121
|
-
_alignment0: 'uint',
|
|
122
149
|
|
|
123
150
|
}, 'TraceResult' );
|
|
124
151
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}, '
|
|
138
|
-
|
|
152
|
+
// Shadow ray trace result, written by TraceShadowRayKernel at the ray's queue index and consumed by
|
|
153
|
+
// LogicKernel the following frame. objectIndex >= 0 encodes an occluded light.
|
|
154
|
+
export const shadowTraceResultStruct = new StructTypeNode( {
|
|
155
|
+
|
|
156
|
+
objectIndex: 'int',
|
|
157
|
+
|
|
158
|
+
// the share of bsdf rays the skipped faces would stop, whose light weight moves to the light sample
|
|
159
|
+
skippedOcclusion: 'float',
|
|
160
|
+
|
|
161
|
+
// the fraction of light blocked by the partially transparent surfaces passed
|
|
162
|
+
occlusion: 'float',
|
|
163
|
+
|
|
164
|
+
}, 'ShadowTraceResult' );
|
|
165
|
+
|
|
166
|
+
// The ray and shadow ray queues are plain arrays of queued rays, with their append-only lengths in a
|
|
167
|
+
// separate counters buffer: storage buffers are always bound as arrays, which can't hold a struct
|
|
168
|
+
// ending in a runtime-sized array.
|
|
169
|
+
export const RAY_QUEUE_LENGTH = 0;
|
|
170
|
+
export const SHADOW_RAY_QUEUE_LENGTH = 1;
|
|
171
|
+
export const QUEUE_COUNTER_COUNT = 2;
|
|
172
|
+
|
|
173
|
+
// The path slots walking through a medium this pass, for MediumKernel. Its length leads the same u32
|
|
174
|
+
// array, ahead of the elements, as MediumKernel is at its storage buffer limit.
|
|
175
|
+
export const MEDIUM_QUEUE_LENGTH = 0;
|
|
176
|
+
export const MEDIUM_QUEUE_HEADER = 1;
|
|
139
177
|
|
|
140
178
|
// Round-robin queue of pixel indices waiting for a free path slot when the output resolution exceeds
|
|
141
|
-
// the ray data pool
|
|
142
|
-
//
|
|
143
|
-
export const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
elements: 'array<atomic<u32>>',
|
|
147
|
-
}, 'PixelQueue' );
|
|
148
|
-
pixelQueueStruct.getLength = () => 2;
|
|
149
|
-
|
|
150
|
-
export const pixelQueueNonAtomicStruct = new DependentStructTypeNode( {
|
|
151
|
-
current: 'uint',
|
|
152
|
-
elementCount: 'uint',
|
|
153
|
-
elements: 'array<u32>',
|
|
154
|
-
}, 'PixelQueue' );
|
|
155
|
-
pixelQueueNonAtomicStruct.getLength = () => 2;
|
|
179
|
+
// the ray data pool, sized to the pixel count so retiring slots can always return their pixel. Its
|
|
180
|
+
// counters lead the same u32 array, ahead of the elements.
|
|
181
|
+
export const PIXEL_QUEUE_CURRENT = 0;
|
|
182
|
+
export const PIXEL_QUEUE_ELEMENT_COUNT = 1;
|
|
183
|
+
export const PIXEL_QUEUE_HEADER = 2;
|
package/src/webgpu/constants.js
CHANGED
|
@@ -13,7 +13,7 @@ import * as bluedither from './nodes/rand/bluedither.wgsl.js';
|
|
|
13
13
|
export const RANDOM_PCG = pcg;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Sobol low discrepancy samples, scrambled per pixel. Converges faster than `RANDOM_PCG`.
|
|
16
|
+
* Sobol low discrepancy samples, scrambled per pixel. Converges faster than `RANDOM_PCG`. The default.
|
|
17
17
|
* @section Random Strategies
|
|
18
18
|
* @type {Object}
|
|
19
19
|
*/
|
|
@@ -21,7 +21,7 @@ export const RANDOM_SOBOL = sobol;
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* The same Sobol sequence for every pixel, offset by a blue noise dither so early frames read as
|
|
24
|
-
* smooth grain rather than clumped noise.
|
|
24
|
+
* smooth grain rather than clumped noise.
|
|
25
25
|
* @section Random Strategies
|
|
26
26
|
* @type {Object}
|
|
27
27
|
*/
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { EventDispatcher, ExternalTexture } from 'three/webgpu';
|
|
2
|
+
import { UnpackAuxKernel } from '../compute/UnpackAuxKernel.js';
|
|
3
|
+
import { createAuxImageTarget } from '../compute/targets.js';
|
|
3
4
|
|
|
4
|
-
/** @import {
|
|
5
|
-
/** @import { DynamicTileSetting } from 'oidn-
|
|
5
|
+
/** @import { StorageArrayTexture, Texture, WebGPURenderer } from 'three/webgpu' */
|
|
6
|
+
/** @import { DynamicTileSetting } from 'oidn-webgpu' */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fired when a pass started by `update` fails, such as the weights failing to download, with the
|
|
10
|
+
* failure as the event's `error`. No other pass starts until a reset.
|
|
11
|
+
* @event OIDNDenoiser#error
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fired as each tile of a pass finishes, with the fraction of the pass done as `progress`.
|
|
16
|
+
* @event OIDNDenoiser#progress
|
|
17
|
+
*/
|
|
6
18
|
|
|
7
19
|
/**
|
|
8
20
|
* Runs Open Image Denoise over a path traced image. Pass one to
|
|
@@ -12,14 +24,14 @@ import { diffuseColor, mrt, normalView, vec4 } from 'three/tsl';
|
|
|
12
24
|
* nor the network files become a dependency.
|
|
13
25
|
*
|
|
14
26
|
* ```js
|
|
15
|
-
* import { initUNetFromURL } from 'oidn-
|
|
27
|
+
* import { initUNetFromURL } from 'oidn-webgpu';
|
|
16
28
|
* pathTracer.setDenoiser( new OIDNDenoiser( { initUNetFromURL, auxWeightsUrl } ) );
|
|
17
29
|
* ```
|
|
18
30
|
*
|
|
19
31
|
* Weights come from the oidn-weights repository, where the "_small" and "_large" variants trade
|
|
20
32
|
* quality against download size and per tile cost.
|
|
21
33
|
*/
|
|
22
|
-
export class OIDNDenoiser {
|
|
34
|
+
export class OIDNDenoiser extends EventDispatcher {
|
|
23
35
|
|
|
24
36
|
/**
|
|
25
37
|
* The denoised result, or null until the first tile has been produced.
|
|
@@ -69,6 +81,8 @@ export class OIDNDenoiser {
|
|
|
69
81
|
*/
|
|
70
82
|
constructor( options = {} ) {
|
|
71
83
|
|
|
84
|
+
super();
|
|
85
|
+
|
|
72
86
|
const {
|
|
73
87
|
initUNetFromURL,
|
|
74
88
|
auxWeightsUrl,
|
|
@@ -80,7 +94,7 @@ export class OIDNDenoiser {
|
|
|
80
94
|
|
|
81
95
|
if ( ! initUNetFromURL ) {
|
|
82
96
|
|
|
83
|
-
throw new Error( 'OIDNDenoiser: "initUNetFromURL" from "oidn-
|
|
97
|
+
throw new Error( 'OIDNDenoiser: "initUNetFromURL" from "oidn-webgpu" must be provided.' );
|
|
84
98
|
|
|
85
99
|
}
|
|
86
100
|
|
|
@@ -88,27 +102,32 @@ export class OIDNDenoiser {
|
|
|
88
102
|
this.auxWeightsUrl = auxWeightsUrl;
|
|
89
103
|
this.colorWeightsUrl = colorWeightsUrl;
|
|
90
104
|
|
|
91
|
-
// `false` falls back to the color only model, which is blurrier but
|
|
105
|
+
// `false` falls back to the color only model, which is blurrier but needs no auxiliary buffers
|
|
92
106
|
this.useAuxiliaryBuffers = useAuxiliaryBuffers;
|
|
93
107
|
|
|
94
|
-
// tiling, passed through to oidn-
|
|
108
|
+
// tiling, passed through to oidn-webgpu. Null keeps its defaults
|
|
95
109
|
this.maxTileSize = maxTileSize;
|
|
96
110
|
this.dynamicTile = dynamicTile;
|
|
97
111
|
|
|
98
112
|
this.renderer = null;
|
|
99
|
-
this.scene = null;
|
|
100
|
-
this.camera = null;
|
|
101
113
|
|
|
102
114
|
this._texture = null;
|
|
103
115
|
this._running = false;
|
|
104
116
|
this._complete = false;
|
|
105
117
|
this._abort = null;
|
|
106
118
|
this._requestId = 0;
|
|
119
|
+
|
|
120
|
+
// set when a pass started by "update" fails, so the render loop doesn't start another until a reset
|
|
121
|
+
this._failed = false;
|
|
107
122
|
this._unets = { aux: null, color: null };
|
|
108
123
|
|
|
109
124
|
this._resultPipeline = null;
|
|
110
125
|
this._rawTexture = null;
|
|
111
|
-
|
|
126
|
+
|
|
127
|
+
// the albedo and normal unpacked from the path tracer's auxiliary target for a pass
|
|
128
|
+
this._unpackAuxKernel = new UnpackAuxKernel().setWorkgroupSize( 8, 8, 1 );
|
|
129
|
+
this._albedoTarget = createAuxImageTarget( 'Denoiser Albedo' );
|
|
130
|
+
this._normalTarget = createAuxImageTarget( 'Denoiser Normal' );
|
|
112
131
|
|
|
113
132
|
}
|
|
114
133
|
|
|
@@ -122,41 +141,40 @@ export class OIDNDenoiser {
|
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*/
|
|
128
|
-
setScene( scene, camera ) {
|
|
129
|
-
|
|
130
|
-
this.scene = scene;
|
|
131
|
-
this.camera = camera;
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Renders the auxiliary buffers and starts a pass. Safe to call every frame.
|
|
144
|
+
* Unpacks the auxiliary buffers the path tracer accumulated and starts a pass. Safe to call
|
|
145
|
+
* every frame.
|
|
137
146
|
*
|
|
138
147
|
* @param {Texture} target - The path traced result, in linear HDR.
|
|
148
|
+
* @param {StorageArrayTexture|null} aux - The packed auxiliary buffers, see "aux.wgsl.js".
|
|
139
149
|
* @returns {Texture|null}
|
|
140
150
|
*/
|
|
141
|
-
update( target ) {
|
|
151
|
+
update( target, aux = null ) {
|
|
142
152
|
|
|
143
|
-
if ( this._complete || this._running ) {
|
|
153
|
+
if ( this._complete || this._running || this._failed ) {
|
|
144
154
|
|
|
145
155
|
return this._texture;
|
|
146
156
|
|
|
147
157
|
}
|
|
148
158
|
|
|
159
|
+
// TODO: denoise the alpha for transparent backgrounds. oidn-webgpu's "denoiseAlpha" runs as a
|
|
160
|
+
// second pass, but its "_updatePasses" doesn't rebuild its shaders when the option changes
|
|
149
161
|
let albedo = null;
|
|
150
162
|
let normal = null;
|
|
151
|
-
if ( this.useAuxiliaryBuffers ) {
|
|
163
|
+
if ( this.useAuxiliaryBuffers && aux ) {
|
|
152
164
|
|
|
153
|
-
|
|
154
|
-
albedo =
|
|
155
|
-
normal =
|
|
165
|
+
this._unpackAux( aux );
|
|
166
|
+
albedo = this._albedoTarget;
|
|
167
|
+
normal = this._normalTarget;
|
|
156
168
|
|
|
157
169
|
}
|
|
158
170
|
|
|
159
|
-
|
|
171
|
+
// a failure, such as the weights failing to download, is reported once rather than every frame
|
|
172
|
+
this.denoise( target, albedo, normal ).catch( error => {
|
|
173
|
+
|
|
174
|
+
this._failed = true;
|
|
175
|
+
this.dispatchEvent( { type: 'error', error } );
|
|
176
|
+
|
|
177
|
+
} );
|
|
160
178
|
|
|
161
179
|
return this._texture;
|
|
162
180
|
|
|
@@ -213,14 +231,22 @@ export class OIDNDenoiser {
|
|
|
213
231
|
const inputs = {
|
|
214
232
|
color: { data: backend.get( color ).texture, width, height },
|
|
215
233
|
|
|
234
|
+
// pace the tiles to display frames so the path tracer keeps rendering between them
|
|
235
|
+
scheduling: 'animation-frame',
|
|
236
|
+
|
|
216
237
|
// the output is seeded with the raw color, so copying per tile shows a progressive wipe
|
|
217
|
-
progress:
|
|
218
|
-
done: output => {
|
|
238
|
+
progress: ( output, tileData, tile, tileIndex, tileCount ) => {
|
|
219
239
|
|
|
220
240
|
this._copyOutputToTexture( output, width, height );
|
|
241
|
+
this.dispatchEvent( { type: 'progress', progress: ( tileIndex + 1 ) / tileCount } );
|
|
242
|
+
|
|
243
|
+
},
|
|
244
|
+
done: output => {
|
|
245
|
+
|
|
221
246
|
this._running = false;
|
|
222
247
|
this._complete = true;
|
|
223
248
|
this._abort = null;
|
|
249
|
+
this._disposeAuxImages();
|
|
224
250
|
|
|
225
251
|
},
|
|
226
252
|
};
|
|
@@ -254,17 +280,18 @@ export class OIDNDenoiser {
|
|
|
254
280
|
this._rawTexture?.destroy();
|
|
255
281
|
this._rawTexture = null;
|
|
256
282
|
|
|
283
|
+
this._disposeAuxImages();
|
|
284
|
+
|
|
257
285
|
this._running = false;
|
|
258
286
|
this._complete = false;
|
|
287
|
+
this._failed = false;
|
|
259
288
|
|
|
260
289
|
}
|
|
261
290
|
|
|
262
291
|
dispose() {
|
|
263
292
|
|
|
264
293
|
this.reset();
|
|
265
|
-
|
|
266
|
-
this._auxTarget?.dispose();
|
|
267
|
-
this._auxTarget = null;
|
|
294
|
+
this._unpackAuxKernel.dispose();
|
|
268
295
|
|
|
269
296
|
// the networks hold the weights on the GPU, and a load may still be in flight
|
|
270
297
|
for ( const key in this._unets ) {
|
|
@@ -276,52 +303,28 @@ export class OIDNDenoiser {
|
|
|
276
303
|
|
|
277
304
|
}
|
|
278
305
|
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
306
|
+
// Unpacks the path tracer's auxiliary target into albedo and normal textures, since oidn-webgpu
|
|
307
|
+
// reads every input as the same kind as the color texture
|
|
308
|
+
_unpackAux( aux ) {
|
|
282
309
|
|
|
283
|
-
const { renderer,
|
|
310
|
+
const { renderer, _unpackAuxKernel, _albedoTarget, _normalTarget } = this;
|
|
311
|
+
const { width, height } = aux.image;
|
|
284
312
|
|
|
285
|
-
|
|
313
|
+
_albedoTarget.setSize( width, height );
|
|
314
|
+
_normalTarget.setSize( width, height );
|
|
286
315
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
type: UnsignedByteType,
|
|
292
|
-
minFilter: NearestFilter,
|
|
293
|
-
magFilter: NearestFilter,
|
|
294
|
-
samples: 4,
|
|
295
|
-
} );
|
|
296
|
-
|
|
297
|
-
// the MRT keys are matched against these names
|
|
298
|
-
this._auxTarget.textures[ 0 ].name = 'output';
|
|
299
|
-
this._auxTarget.textures[ 1 ].name = 'normal';
|
|
300
|
-
|
|
301
|
-
}
|
|
316
|
+
_unpackAuxKernel.auxTarget = aux;
|
|
317
|
+
_unpackAuxKernel.albedoTarget = _albedoTarget;
|
|
318
|
+
_unpackAuxKernel.normalTarget = _normalTarget;
|
|
319
|
+
renderer.compute( _unpackAuxKernel.kernel, _unpackAuxKernel.getDispatchSize( width, height ) );
|
|
302
320
|
|
|
303
|
-
|
|
304
|
-
auxTarget.setSize( width, height );
|
|
305
|
-
|
|
306
|
-
const originalTarget = renderer.getRenderTarget();
|
|
307
|
-
const originalMRT = renderer.getMRT();
|
|
308
|
-
const originalToneMapping = renderer.toneMapping;
|
|
309
|
-
|
|
310
|
-
// the buffers are data rather than an image, so they must not be tone mapped
|
|
311
|
-
renderer.toneMapping = NoToneMapping;
|
|
312
|
-
renderer.setRenderTarget( auxTarget );
|
|
313
|
-
renderer.setMRT( mrt( {
|
|
314
|
-
output: diffuseColor,
|
|
315
|
-
normal: vec4( normalView.mul( 0.5 ).add( 0.5 ), 1.0 ),
|
|
316
|
-
} ) );
|
|
317
|
-
|
|
318
|
-
renderer.render( scene, camera );
|
|
321
|
+
}
|
|
319
322
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
renderer.toneMapping = originalToneMapping;
|
|
323
|
+
// the unpacked images only live for a pass
|
|
324
|
+
_disposeAuxImages() {
|
|
323
325
|
|
|
324
|
-
|
|
326
|
+
this._albedoTarget.dispose();
|
|
327
|
+
this._normalTarget.dispose();
|
|
325
328
|
|
|
326
329
|
}
|
|
327
330
|
|
|
@@ -346,7 +349,7 @@ export class OIDNDenoiser {
|
|
|
346
349
|
if ( this.maxTileSize !== null ) options.maxTileSize = this.maxTileSize;
|
|
347
350
|
if ( this.dynamicTile !== null ) options.dynamicTile = this.dynamicTile;
|
|
348
351
|
|
|
349
|
-
return this.initUNetFromURL( url, { device
|
|
352
|
+
return this.initUNetFromURL( url, { device }, options );
|
|
350
353
|
|
|
351
354
|
} )();
|
|
352
355
|
|
|
@@ -358,7 +361,7 @@ export class OIDNDenoiser {
|
|
|
358
361
|
|
|
359
362
|
// Copies the denoiser's packed output buffer into a sampleable texture, using a compute pass
|
|
360
363
|
// since copyBufferToTexture requires 256 byte row alignment.
|
|
361
|
-
// TODO: remove the copy once oidn-
|
|
364
|
+
// TODO: remove the copy once oidn-webgpu can write into a caller-provided storage texture.
|
|
362
365
|
_copyOutputToTexture( output, width, height ) {
|
|
363
366
|
|
|
364
367
|
const device = this.renderer.backend.device;
|