three-gpu-pathtracer 0.0.27 → 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 +0 -1
- package/build/index.module.js +5 -3
- package/build/index.module.js.map +1 -1
- package/build/index.umd.cjs +4 -2
- package/build/index.umd.cjs.map +1 -1
- package/package.json +7 -7
- package/src/core/PathTracingSceneGenerator.js +1 -1
- package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
- package/src/webgpu/API.md +167 -19
- package/src/webgpu/AtlasTexture.js +2 -3
- package/src/webgpu/EquirectHdrInfoNode.js +89 -48
- package/src/webgpu/LightsInfoNode.js +1 -1
- package/src/webgpu/MegaKernelPathTracer.js +10 -3
- package/src/webgpu/PathTracerBackend.js +172 -21
- package/src/webgpu/WaveFrontPathTracer.js +156 -71
- package/src/webgpu/WebGPUPathTracer.js +628 -48
- package/src/webgpu/compute/ComputeKernel.js +8 -0
- package/src/webgpu/compute/PathTracerMegaKernel.js +187 -36
- 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 +67 -20
- 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 -25
- package/src/webgpu/compute/wavefront/structs.js +63 -37
- package/src/webgpu/constants.js +2 -2
- package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
- package/src/webgpu/index.d.ts +41 -6
- package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
- package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
- package/src/webgpu/nodes/PathtracerBVHComputeData.js +35 -13
- package/src/webgpu/nodes/aux.wgsl.js +61 -0
- package/src/webgpu/nodes/eon.wgsl.js +1 -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 +7 -1
- package/src/webgpu/upscale/FSRUpscaler.js +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
2
|
+
import { scatterRecordStruct, surfaceRecordStruct } from './structs.wgsl.js';
|
|
3
|
+
|
|
4
|
+
// The denoiser's auxiliary images, the first hit albedo and normal, accumulate in an "r32uint" array
|
|
5
|
+
// texture, the one kind of storage texture core WebGPU can both read and write. Each layer packs two
|
|
6
|
+
// halves: albedo.rg, then albedo.b and normal.z, then normal.xy. Normals are mapped to [0,1].
|
|
7
|
+
export const AUX_LAYERS = 3;
|
|
8
|
+
|
|
9
|
+
// the smooth metals the auxiliary images look past to what they reflect, as in mutracer
|
|
10
|
+
const AUX_MIRROR_MAX_ROUGHNESS = 0.02;
|
|
11
|
+
const AUX_MIRROR_MIN_METALNESS = 0.98;
|
|
12
|
+
|
|
13
|
+
// whether a scatter shows its surface in the auxiliary images, rather than looking past it through a
|
|
14
|
+
// transmission or a mirror
|
|
15
|
+
export const isAuxSurfaceFn = wgslTagFn/* wgsl */`
|
|
16
|
+
|
|
17
|
+
fn isAuxSurface( surface: ${ surfaceRecordStruct }, scatterRec: ${ scatterRecordStruct } ) -> bool {
|
|
18
|
+
|
|
19
|
+
let isMirror = surface.roughness <= ${ AUX_MIRROR_MAX_ROUGHNESS } && surface.metalness >= ${ AUX_MIRROR_MIN_METALNESS };
|
|
20
|
+
return ! scatterRec.isTransmissive && ! isMirror;
|
|
21
|
+
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export const unpackAuxAlbedoFn = wgslTagFn/* wgsl */`
|
|
27
|
+
|
|
28
|
+
fn unpackAuxAlbedo( packed: vec3u ) -> vec3f {
|
|
29
|
+
|
|
30
|
+
return vec3f( unpack2x16float( packed.x ), unpack2x16float( packed.y ).x );
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
export const unpackAuxNormalFn = wgslTagFn/* wgsl */`
|
|
37
|
+
|
|
38
|
+
fn unpackAuxNormal( packed: vec3u ) -> vec3f {
|
|
39
|
+
|
|
40
|
+
return vec3f( unpack2x16float( packed.z ), unpack2x16float( packed.y ).y );
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
// blends a sample's albedo and normal into the packed running means
|
|
47
|
+
export const blendAuxFn = wgslTagFn/* wgsl */`
|
|
48
|
+
|
|
49
|
+
fn blendAux( packed: vec3u, albedo: vec3f, normal: vec3f, weight: f32 ) -> vec3u {
|
|
50
|
+
|
|
51
|
+
let meanAlbedo = mix( ${ unpackAuxAlbedoFn }( packed ), albedo, weight );
|
|
52
|
+
let meanNormal = mix( ${ unpackAuxNormalFn }( packed ), normal * 0.5 + 0.5, weight );
|
|
53
|
+
return vec3u(
|
|
54
|
+
pack2x16float( meanAlbedo.rg ),
|
|
55
|
+
pack2x16float( vec2f( meanAlbedo.b, meanNormal.z ) ),
|
|
56
|
+
pack2x16float( meanNormal.xy ),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
`;
|
|
@@ -119,7 +119,7 @@ const eonLtcBasisFunc = wgslFn( /* wgsl */ `
|
|
|
119
119
|
fn eonLtcBasis( wo: vec3f ) -> mat3x3f {
|
|
120
120
|
|
|
121
121
|
let lenSqr = dot( wo.xy, wo.xy );
|
|
122
|
-
let invLen = inverseSqrt(
|
|
122
|
+
let invLen = inverseSqrt( lenSqr );
|
|
123
123
|
let x = select(
|
|
124
124
|
vec3f( 1.0, 0.0, 0.0 ),
|
|
125
125
|
vec3f( wo.x, wo.y, 0.0 ) * invLen,
|
|
@@ -848,7 +848,8 @@ export const transmissionFresnelFunc = ( iridescentFresnel = iridescentFresnelFu
|
|
|
848
848
|
// Include the base-color tint in the transmission energy, as Cycles does when it
|
|
849
849
|
// computes the glass reflection probability from its two tinted coefficients.
|
|
850
850
|
let reflectEnergy = dot( reflectance, vec3f( 1.0 / 3.0 ) );
|
|
851
|
-
let
|
|
851
|
+
let transmissionTint = select( sqrt( surf.color ), surf.color, surf.thinWall );
|
|
852
|
+
let transmitEnergy = dot( transmittance * transmissionTint, vec3f( 1.0 / 3.0 ) );
|
|
852
853
|
let totalEnergy = reflectEnergy + transmitEnergy;
|
|
853
854
|
result.reflectProbability = select( 0.0, reflectEnergy / max( totalEnergy, EPSILON ), totalEnergy > EPSILON );
|
|
854
855
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { StructTypeNode } from 'three/webgpu';
|
|
2
|
+
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
3
|
+
import { LIGHT_FAR_DISTANCE } from './lights.wgsl.js';
|
|
4
|
+
import { materialStruct } from './structs.wgsl.js';
|
|
5
|
+
|
|
6
|
+
// Random walk subsurface scattering, following Cycles' subsurface_random_walk.h
|
|
7
|
+
// https://github.com/blender/cycles/blob/a456b761034dda42c32eef9f4aae0fa5a5c9f604/src/kernel/integrator/subsurface_random_walk.h
|
|
8
|
+
|
|
9
|
+
// the smallest attenuation color used to derive a medium's extinction, keeping the log finite
|
|
10
|
+
const MEDIUM_MIN_ATTENUATION = 1e-6;
|
|
11
|
+
|
|
12
|
+
// the throughput below which a walk stops, matching Cycles' VOLUME_THROUGHPUT_EPSILON
|
|
13
|
+
export const MEDIUM_THROUGHPUT_EPSILON = 1e-6;
|
|
14
|
+
|
|
15
|
+
// the largest phase anisotropy used, short of the +/- 1 delta phase function the scatter albedo
|
|
16
|
+
// inversion and the Henyey-Greenstein pdf divide by zero at
|
|
17
|
+
const MEDIUM_MAX_ANISOTROPY = 0.999;
|
|
18
|
+
|
|
19
|
+
export const mediumStruct = new StructTypeNode( {
|
|
20
|
+
inside: 'bool',
|
|
21
|
+
object: 'uint',
|
|
22
|
+
extinction: 'vec3f',
|
|
23
|
+
albedo: 'vec3f',
|
|
24
|
+
anisotropy: 'float',
|
|
25
|
+
}, 'Medium' );
|
|
26
|
+
|
|
27
|
+
// a medium in a storage buffer, which can't hold bools, for walks that span passes
|
|
28
|
+
export const mediumStateStruct = new StructTypeNode( {
|
|
29
|
+
extinction: 'vec3f',
|
|
30
|
+
object: 'uint',
|
|
31
|
+
|
|
32
|
+
albedo: 'vec3f',
|
|
33
|
+
anisotropy: 'float',
|
|
34
|
+
|
|
35
|
+
inside: 'uint',
|
|
36
|
+
}, 'MediumState' );
|
|
37
|
+
|
|
38
|
+
export const storeMediumFn = wgslTagFn/* wgsl */`
|
|
39
|
+
|
|
40
|
+
fn storeMedium( medium: ${ mediumStruct } ) -> ${ mediumStateStruct } {
|
|
41
|
+
|
|
42
|
+
var state: ${ mediumStateStruct };
|
|
43
|
+
state.extinction = medium.extinction;
|
|
44
|
+
state.object = medium.object;
|
|
45
|
+
state.albedo = medium.albedo;
|
|
46
|
+
state.anisotropy = medium.anisotropy;
|
|
47
|
+
state.inside = select( 0u, 1u, medium.inside );
|
|
48
|
+
return state;
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
export const loadMediumFn = wgslTagFn/* wgsl */`
|
|
55
|
+
|
|
56
|
+
fn loadMedium( state: ${ mediumStateStruct } ) -> ${ mediumStruct } {
|
|
57
|
+
|
|
58
|
+
var medium: ${ mediumStruct };
|
|
59
|
+
medium.inside = state.inside != 0u;
|
|
60
|
+
medium.object = state.object;
|
|
61
|
+
medium.extinction = state.extinction;
|
|
62
|
+
medium.albedo = state.albedo;
|
|
63
|
+
medium.anisotropy = state.anisotropy;
|
|
64
|
+
return medium;
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
// The single scatter albedo of a volume from its perceived multi scatter albedo and phase anisotropy, using the
|
|
71
|
+
// inversion from "KHR_materials_scatter" (after Kulla and Conty 2017). A zero multi scatter albedo maps to exactly zero
|
|
72
|
+
// https://github.com/KhronosGroup/glTF/pull/2579
|
|
73
|
+
const singleScatterAlbedoFn = wgslTagFn/* wgsl */`
|
|
74
|
+
|
|
75
|
+
fn singleScatterAlbedo( multiscatterAlbedo: vec3f, anisotropy: f32 ) -> vec3f {
|
|
76
|
+
|
|
77
|
+
let a = multiscatterAlbedo;
|
|
78
|
+
let s = clamp( 4.09712 + 4.20863 * a - sqrt( 9.59217 + 41.6808 * a + 17.7126 * a * a ), vec3f( 0.0 ), vec3f( 1.0 ) );
|
|
79
|
+
let s2 = select( s * s, vec3f( 1.0 ), a <= vec3f( 0.0 ) );
|
|
80
|
+
return ( 1.0 - s2 ) / ( 1.0 - anisotropy * s2 );
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
// The medium entered by refracting into a material, not "inside" if the material doesn't scatter
|
|
87
|
+
export const enterMediumFn = wgslTagFn/* wgsl */`
|
|
88
|
+
|
|
89
|
+
fn enterMedium( material: ${ materialStruct }, object: u32 ) -> ${ mediumStruct } {
|
|
90
|
+
|
|
91
|
+
let anisotropy = clamp( material.scatterAnisotropy, - ${ MEDIUM_MAX_ANISOTROPY }, ${ MEDIUM_MAX_ANISOTROPY } );
|
|
92
|
+
|
|
93
|
+
var medium: ${ mediumStruct };
|
|
94
|
+
medium.extinction = - log( max( material.attenuationColor, vec3f( ${ MEDIUM_MIN_ATTENUATION } ) ) ) / material.attenuationDistance;
|
|
95
|
+
medium.albedo = ${ singleScatterAlbedoFn }( material.multiscatterColor * material.scatterStrength, anisotropy );
|
|
96
|
+
medium.inside = any( medium.albedo > vec3f( 0.0 ) ) && any( medium.extinction > vec3f( 0.0 ) );
|
|
97
|
+
medium.object = object;
|
|
98
|
+
medium.anisotropy = anisotropy;
|
|
99
|
+
return medium;
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
// The probability of sampling each color channel's distance, in proportion to throughput times albedo
|
|
106
|
+
// https://github.com/blender/cycles/blob/a456b761034dda42c32eef9f4aae0fa5a5c9f604/src/kernel/closure/volume.h#L157
|
|
107
|
+
export const mediumChannelPdfFn = wgslTagFn/* wgsl */`
|
|
108
|
+
|
|
109
|
+
fn mediumChannelPdf( throughput: vec3f, albedo: vec3f ) -> vec3f {
|
|
110
|
+
|
|
111
|
+
let weights = abs( throughput * albedo );
|
|
112
|
+
let weightSum = weights.r + weights.g + weights.b;
|
|
113
|
+
if ( weightSum > 0.0 ) {
|
|
114
|
+
|
|
115
|
+
return weights / weightSum;
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return vec3f( 1.0 / 3.0 );
|
|
120
|
+
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
// Samples the distance to the next scatter event using one color channel's extinction
|
|
126
|
+
export const sampleMediumDistanceFn = wgslTagFn/* wgsl */`
|
|
127
|
+
|
|
128
|
+
fn sampleMediumDistance( medium: ${ mediumStruct }, channelPdf: vec3f, u: vec2f ) -> f32 {
|
|
129
|
+
|
|
130
|
+
var channel = 2u;
|
|
131
|
+
if ( u.x < channelPdf.r ) {
|
|
132
|
+
|
|
133
|
+
channel = 0u;
|
|
134
|
+
|
|
135
|
+
} else if ( u.x < channelPdf.r + channelPdf.g ) {
|
|
136
|
+
|
|
137
|
+
channel = 1u;
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// a channel with no extinction never scatters, and WGSL doesn't guarantee an infinite quotient
|
|
142
|
+
let extinction = medium.extinction[ channel ];
|
|
143
|
+
if ( extinction <= 0.0 ) {
|
|
144
|
+
|
|
145
|
+
return ${ LIGHT_FAR_DISTANCE };
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return - log( 1.0 - u.y ) / extinction;
|
|
150
|
+
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
// Per channel pdf of a segment: of scattering at "dist", or of passing it when it didn't scatter
|
|
156
|
+
export const mediumDistancePdfFn = wgslTagFn/* wgsl */`
|
|
157
|
+
|
|
158
|
+
fn mediumDistancePdf( medium: ${ mediumStruct }, dist: f32, scattered: bool ) -> vec3f {
|
|
159
|
+
|
|
160
|
+
let transmittance = exp( - medium.extinction * dist );
|
|
161
|
+
return select( transmittance, medium.extinction * transmittance, scattered );
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
`;
|
|
@@ -6,6 +6,8 @@ export const RNG_INDEX_APERTURE_SAMPLE = 4;
|
|
|
6
6
|
export const RNG_INDEX_DIRECT_LIGHT_SAMPLE = 5;
|
|
7
7
|
export const RNG_INDEX_RUSSIAN_ROULETTE = 6;
|
|
8
8
|
export const RNG_INDEX_DISPERSION_WAVELENGTH = 7;
|
|
9
|
+
export const RNG_INDEX_MEDIUM_DISTANCE = 8;
|
|
10
|
+
export const RNG_INDEX_MEDIUM_DIRECTION = 9;
|
|
9
11
|
export const RNG_INDEX_ALPHA_TEST = 50;
|
|
10
12
|
import { contextProxyFn } from 'three-mesh-bvh/webgpu';
|
|
11
13
|
import * as sobol from './rand/sobol.wgsl.js';
|
|
@@ -108,14 +108,55 @@ export const sampleHemisphereFn = wgslFn( /* wgsl */ `
|
|
|
108
108
|
|
|
109
109
|
`, [ constants ] );
|
|
110
110
|
|
|
111
|
+
// below this anisotropy the Henyey-Greenstein phase function is sampled as isotropic, avoiding the
|
|
112
|
+
// division by zero in its inverted CDF
|
|
113
|
+
const HG_ISOTROPIC_EPSILON = 1e-3;
|
|
114
|
+
|
|
115
|
+
// Samples a new direction from the Henyey-Greenstein phase function around the propagation direction
|
|
116
|
+
// "d". Positive "g" scatters forward. The sample is exact, so the phase function over its pdf is one.
|
|
117
|
+
// https://github.com/blender/cycles/blob/a456b761034dda42c32eef9f4aae0fa5a5c9f604/src/kernel/closure/volume_util.h#L36
|
|
118
|
+
export const sampleHenyeyGreensteinFn = wgslFn( /* wgsl */ `
|
|
119
|
+
|
|
120
|
+
fn sampleHenyeyGreenstein( d: vec3f, g: f32, uv: vec2f ) -> vec3f {
|
|
121
|
+
|
|
122
|
+
var cosTheta = 1.0 - 2.0 * uv.x;
|
|
123
|
+
if ( abs( g ) > ${ HG_ISOTROPIC_EPSILON } ) {
|
|
124
|
+
|
|
125
|
+
let sqrTerm = ( 1.0 - g * g ) / ( 1.0 - g + 2.0 * g * uv.x );
|
|
126
|
+
cosTheta = clamp( ( 1.0 + g * g - sqrTerm * sqrTerm ) / ( 2.0 * g ), - 1.0, 1.0 );
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// orthonormal basis around "d", from https://graphics.pixar.com/library/OrthonormalB/paper.pdf
|
|
131
|
+
let sign = select( sign( d.z ), 1.0, d.z == 0.0 );
|
|
132
|
+
let a = - 1.0 / ( sign + d.z );
|
|
133
|
+
let b = d.x * d.y * a;
|
|
134
|
+
let b1 = vec3( 1.0 + sign * d.x * d.x * a, sign * b, - sign * d.x );
|
|
135
|
+
let b2 = vec3( b, sign + d.y * d.y * a, - d.y );
|
|
136
|
+
|
|
137
|
+
let sinTheta = sqrt( max( 0.0, 1.0 - cosTheta * cosTheta ) );
|
|
138
|
+
let phi = 2.0 * PI * uv.y;
|
|
139
|
+
return normalize( sinTheta * cos( phi ) * b1 + sinTheta * sin( phi ) * b2 + cosTheta * d );
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
`, [ constants ] );
|
|
144
|
+
|
|
111
145
|
// power heuristic for multiple importance sampling
|
|
112
146
|
export const misHeuristicFn = wgslFn( /* wgsl */ `
|
|
113
147
|
|
|
114
148
|
fn misHeuristic( a: f32, b: f32 ) -> f32 {
|
|
115
149
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
150
|
+
// computed as a ratio so squaring large pdfs can't overflow to Inf / Inf
|
|
151
|
+
if ( a >= b ) {
|
|
152
|
+
|
|
153
|
+
let r = b / a;
|
|
154
|
+
return 1.0 / ( 1.0 + r * r );
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let r = a / b;
|
|
159
|
+
return r * r / ( 1.0 + r * r );
|
|
119
160
|
|
|
120
161
|
}
|
|
121
162
|
|
|
@@ -156,7 +156,13 @@ export const materialStruct = new StructTypeNode( {
|
|
|
156
156
|
|
|
157
157
|
// offset 264 floats
|
|
158
158
|
anisotropyMapTransform: 'mat3',
|
|
159
|
-
|
|
159
|
+
|
|
160
|
+
// offset 276 floats
|
|
161
|
+
// the volume's scattering, from "KHR_materials_scatter"
|
|
162
|
+
multiscatterColor: 'vec3',
|
|
163
|
+
scatterStrength: 'float',
|
|
164
|
+
scatterAnisotropy: 'float',
|
|
165
|
+
// total size = 284
|
|
160
166
|
}, 'Material' );
|
|
161
167
|
|
|
162
168
|
export const surfaceRecordStruct = new StructTypeNode( {
|