three-gpu-pathtracer 0.0.23 → 0.0.25
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 -528
- package/build/index.module.js +319 -66
- package/build/index.module.js.map +1 -1
- package/build/index.umd.cjs +318 -64
- package/build/index.umd.cjs.map +1 -1
- package/package.json +35 -18
- package/src/core/PathTracingSceneGenerator.js +21 -2
- package/src/core/WebGLPathTracer.js +16 -7
- package/src/core/utils/mergeGeometries.js +14 -1
- package/src/core/utils/sceneUpdateUtils.js +8 -12
- package/src/detectors/PrecisionMaterial.js +9 -9
- package/src/index.d.ts +2 -1
- package/src/index.js +1 -0
- package/src/materials/pathtracing/PhysicalPathTracingMaterial.js +2 -1
- package/src/materials/pathtracing/glsl/attenuate_hit_function.glsl.js +2 -1
- package/src/materials/pathtracing/glsl/camera_util_functions.glsl.js +1 -1
- package/src/materials/pathtracing/glsl/get_surface_record_function.glsl.js +2 -1
- package/src/materials/surface/AmbientOcclusionMaterial.js +2 -0
- package/src/objects/PhysicalSpotLight.js +2 -0
- package/src/shader/bsdf/bsdf_functions.glsl.js +0 -1
- package/src/shader/bvh/inside_fog_volume_function.glsl.js +1 -1
- package/src/shader/structs/material_struct.glsl.js +3 -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 +13 -10
- 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,608 @@
|
|
|
1
|
+
import { float } from 'three/tsl';
|
|
2
|
+
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
3
|
+
import { PathtracingMaterial } from './PathtracingMaterial';
|
|
4
|
+
import { specularBrdfFunc, specularBtdfFunc, fresnelMixFunc, conductorFresnelFunc, fresnelCoatFunc, iridescentFresnelFunc, isMatchedIorFunc, thinWallTransmissionRoughnessFunc, transmissionFresnelFunc } from '../nodes/material.wgsl.js';
|
|
5
|
+
import { eonBrdfFunc, eonDirectionFunc, eonPDFFunc } from '../nodes/eon.wgsl.js';
|
|
6
|
+
import { sheenColorFunc, sheenAlbedoScalingFunc } from '../nodes/sheen.wgsl.js';
|
|
7
|
+
import { getLobeWeightsFunc } from '../nodes/sampling.wgsl.js';
|
|
8
|
+
import { ggxDirectionFunc, ggxReflectionAdjustedPDFFunc, ggxRefractionAdjustedPDFFunc } from '../nodes/ggx.wgsl.js';
|
|
9
|
+
import { bxdfContextStruct, scatterRecordStruct, surfaceRecordStruct } from '../nodes/structs.wgsl.js';
|
|
10
|
+
import { rand1, rand2, RNG_INDEX_SCATTER_DIRECTION, RNG_INDEX_SCATTER_TYPE } from '../nodes/random.wgsl.js';
|
|
11
|
+
import { TurquinTexture } from '../TurquinTexture.js';
|
|
12
|
+
import { iorToF0Func, schlickFresnelFunc, schlickFresnelVecFunc, dielectricFresnelFunc } from '../nodes/utils.wgsl.js';
|
|
13
|
+
|
|
14
|
+
const CLEARCOAT_IOR = float( 1.5 );
|
|
15
|
+
const SINGULAR_EVENT_SCALE = float( 1e6 );
|
|
16
|
+
|
|
17
|
+
export class GltfCompliantMaterial extends PathtracingMaterial {
|
|
18
|
+
|
|
19
|
+
constructor( options = {} ) {
|
|
20
|
+
|
|
21
|
+
super();
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
specularBrdf = specularBrdfFunc,
|
|
25
|
+
specularBtdf = specularBtdfFunc,
|
|
26
|
+
diffuseBrdf = eonBrdfFunc,
|
|
27
|
+
fresnelMix = fresnelMixFunc,
|
|
28
|
+
conductorFresnel = conductorFresnelFunc,
|
|
29
|
+
fresnelCoat = fresnelCoatFunc,
|
|
30
|
+
iridescentFresnel = iridescentFresnelFunc,
|
|
31
|
+
} = options;
|
|
32
|
+
|
|
33
|
+
this.turquinTexture = new TurquinTexture();
|
|
34
|
+
this.specularBrdf = specularBrdf;
|
|
35
|
+
this.specularBtdf = specularBtdf;
|
|
36
|
+
this.diffuseBrdf = diffuseBrdf;
|
|
37
|
+
this.fresnelMix = fresnelMix;
|
|
38
|
+
this.conductorFresnel = conductorFresnel;
|
|
39
|
+
this.fresnelCoat = fresnelCoat;
|
|
40
|
+
this.iridescentFresnel = iridescentFresnel;
|
|
41
|
+
this.transmissionFresnel = transmissionFresnelFunc( iridescentFresnel );
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
init( renderer ) {
|
|
46
|
+
|
|
47
|
+
this.turquinTexture.generate( renderer );
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getBsdfNode() {
|
|
52
|
+
|
|
53
|
+
this._bsdfEvalFunc = wgslTagFn/* wgsl */`
|
|
54
|
+
|
|
55
|
+
// The material is organized as one scoped block per lobe in cascade order - clearcoat,
|
|
56
|
+
// sheen, transmission, specular, diffuse - each accumulating into a shared result and
|
|
57
|
+
// guarding its own hemisphere.
|
|
58
|
+
// TODO: energy only cascades downward through front faces, so the layers are always traversed
|
|
59
|
+
// outside-in and cannot reflect energy back into the glass from below.
|
|
60
|
+
fn bsdfEval( ctx: ${ bxdfContextStruct }, surf: ${ surfaceRecordStruct } ) -> vec3f {
|
|
61
|
+
|
|
62
|
+
let NdotV = ctx.V.z;
|
|
63
|
+
let NdotL = ctx.L.z;
|
|
64
|
+
|
|
65
|
+
// Each lobe contributes into "result" within its own scope, evaluated in cascade.
|
|
66
|
+
// "attenuation" carries the fraction of energy each layer passes through to
|
|
67
|
+
// the layers beneath it. Every lobe guards its own hemisphere.
|
|
68
|
+
var result = vec3f( 0.0 );
|
|
69
|
+
var attenuation = vec3f( 1.0 );
|
|
70
|
+
|
|
71
|
+
// clearcoat
|
|
72
|
+
if ( NdotL > 0.0 && surf.clearcoat > 0.0 ) {
|
|
73
|
+
|
|
74
|
+
// the clearcoat evaluates in its own frame
|
|
75
|
+
let toClearcoatMat = surf.clearcoatInvBasis * surf.normalBasis;
|
|
76
|
+
let Vc = normalize( toClearcoatMat * ctx.V );
|
|
77
|
+
let Lc = normalize( toClearcoatMat * ctx.L );
|
|
78
|
+
let Hc = normalize( toClearcoatMat * ctx.H );
|
|
79
|
+
let NdotVc = Vc.z;
|
|
80
|
+
|
|
81
|
+
let clearcoatAlpha = surf.clearcoatRoughness * surf.clearcoatRoughness;
|
|
82
|
+
|
|
83
|
+
// reuse the same pattern for energy conservation used in the dielectric layer
|
|
84
|
+
let clearcoatEnergySS = max( ${ this.turquinTexture.sampleConductorFn }( NdotVc, surf.clearcoatRoughness ), 1e-5 );
|
|
85
|
+
let clearcoatBoost = 1.0 + ${ iorToF0Func }( 1.5 ) * ( 1.0 - clearcoatEnergySS ) / clearcoatEnergySS;
|
|
86
|
+
|
|
87
|
+
let clearcoatSpecular = ${ this.specularBrdf }( Vc, Lc, Hc, vec2( clearcoatAlpha ) ) * clearcoatBoost;
|
|
88
|
+
let clearcoatFresnel = ${ schlickFresnelFunc }( abs( dot( Vc, Hc ) ), ${ iorToF0Func }( 1.5 ) );
|
|
89
|
+
|
|
90
|
+
// retrieve specular energy reflected by this layer
|
|
91
|
+
let clearcoatFresnelEnergySS = ${ this.turquinTexture.sampleDielectricFn }( NdotVc, surf.clearcoatRoughness, 1.5 ) * clearcoatBoost;
|
|
92
|
+
|
|
93
|
+
result += surf.clearcoat * clearcoatFresnel * clearcoatSpecular;
|
|
94
|
+
attenuation *= 1.0 - surf.clearcoat * clearcoatFresnelEnergySS;
|
|
95
|
+
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// sheen
|
|
99
|
+
if ( NdotL > 0.0 && surf.sheen > 0.0 ) {
|
|
100
|
+
|
|
101
|
+
result += attenuation * surf.sheen * ${ sheenColorFunc }( ctx.V, ctx.L, ctx.H, surf );
|
|
102
|
+
attenuation *= mix( 1.0, ${ sheenAlbedoScalingFunc }( ctx.V, ctx.L, surf ), surf.sheen );
|
|
103
|
+
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// transmission
|
|
107
|
+
// handles specular reflection and transmission
|
|
108
|
+
if ( surf.transmission > 0.0 ) {
|
|
109
|
+
|
|
110
|
+
let matchedIor = ${ isMatchedIorFunc }( surf.eta );
|
|
111
|
+
|
|
112
|
+
// anisotropic roughness along tangent, bitangent
|
|
113
|
+
let alphaB = surf.roughness * surf.roughness;
|
|
114
|
+
let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
|
|
115
|
+
let alpha = vec2( alphaT, alphaB );
|
|
116
|
+
|
|
117
|
+
// multiscatter compensation
|
|
118
|
+
var glassBoost = 0.0;
|
|
119
|
+
if ( matchedIor ) {
|
|
120
|
+
|
|
121
|
+
glassBoost = 1.0;
|
|
122
|
+
|
|
123
|
+
} else if ( surf.thinWall ) {
|
|
124
|
+
|
|
125
|
+
// thin wall halves are reflection-shaped so each is compensated with the conductor albedo at its own roughness.
|
|
126
|
+
glassBoost = 1.0 / max( ${ this.turquinTexture.sampleConductorFn }( NdotV, surf.roughness ), 1e-5 );
|
|
127
|
+
|
|
128
|
+
} else {
|
|
129
|
+
|
|
130
|
+
// volumetric bsdf is scaled by its total reflected + refracted energy
|
|
131
|
+
glassBoost = 1.0 / max( ${ this.turquinTexture.sampleTransmissiveFn }( NdotV, surf.roughness, surf.ior, surf.frontFace ), 1e-5 );
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let fresnelCosine = select( ctx.VdotH, NdotV, surf.thinWall );
|
|
136
|
+
let fresnel = ${ this.transmissionFresnel }( fresnelCosine, surf );
|
|
137
|
+
|
|
138
|
+
if ( NdotL < 0.0 ) {
|
|
139
|
+
|
|
140
|
+
var refraction = vec3f( 0.0 );
|
|
141
|
+
if ( surf.thinWall && ! matchedIor ) {
|
|
142
|
+
|
|
143
|
+
// evaluate the flipped reflection, compensated at the remapped roughness
|
|
144
|
+
let wiMirror = vec3f( ctx.L.xy, - ctx.L.z );
|
|
145
|
+
let thinWallAlpha = vec2f( ${ thinWallTransmissionRoughnessFunc }( alphaB, surf.ior ) );
|
|
146
|
+
let thinWallEnergySS = max( ${ this.turquinTexture.sampleConductorFn }( NdotV, sqrt( thinWallAlpha.x ) ), 1e-5 );
|
|
147
|
+
refraction = fresnel.transmittance * ${ this.specularBrdf }( ctx.V, wiMirror, ctx.H, thinWallAlpha ) / thinWallEnergySS;
|
|
148
|
+
|
|
149
|
+
} else if ( ! matchedIor ) {
|
|
150
|
+
|
|
151
|
+
// The default BTDF contains the plain dielectric Fresnel complement. Replace it
|
|
152
|
+
// with the colored thin-film-aware coefficient shared by sampling and the PDF.
|
|
153
|
+
let dielectricTransmittance = 1.0 - ${ dielectricFresnelFunc }( abs( ctx.VdotH ), surf.eta );
|
|
154
|
+
if ( dielectricTransmittance > EPSILON ) {
|
|
155
|
+
|
|
156
|
+
refraction = ${ this.specularBtdf }( ctx.V, ctx.L, ctx.H, alpha, surf.eta ) *
|
|
157
|
+
fresnel.transmittance / dielectricTransmittance * glassBoost;
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// the refracted half is not attenuated by the layers above
|
|
164
|
+
result += ( 1.0 - surf.metalness ) * surf.transmission * refraction * surf.color;
|
|
165
|
+
|
|
166
|
+
} else {
|
|
167
|
+
|
|
168
|
+
// the raw single scatter lobe scaled by the glass compensation boost rather
|
|
169
|
+
// than the opaque dielectric boost
|
|
170
|
+
let specular = ${ this.specularBrdf }( ctx.V, ctx.L, ctx.H, alpha );
|
|
171
|
+
let dielectricSpecular = specular * glassBoost;
|
|
172
|
+
|
|
173
|
+
let reflection = dielectricSpecular * fresnel.reflectance;
|
|
174
|
+
|
|
175
|
+
result += attenuation * ( 1.0 - surf.metalness ) * surf.transmission * reflection;
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// specular
|
|
182
|
+
// metallic + dielectric + iridescence
|
|
183
|
+
if ( NdotL > 0.0 ) {
|
|
184
|
+
|
|
185
|
+
// anisotropic roughness along tangent, bitangent
|
|
186
|
+
let alphaB = surf.roughness * surf.roughness;
|
|
187
|
+
let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
|
|
188
|
+
let alpha = vec2( alphaT, alphaB );
|
|
189
|
+
|
|
190
|
+
// Sample the single scatter energy for specular at the given roughness
|
|
191
|
+
let energySS = max( ${ this.turquinTexture.sampleConductorFn }( NdotV, surf.roughness ), 1e-5 );
|
|
192
|
+
let specular = ${ this.specularBrdf }( ctx.V, ctx.L, ctx.H, alpha );
|
|
193
|
+
|
|
194
|
+
// metallic with the multiscatter comp
|
|
195
|
+
let metallicBoost = 1.0 + surf.color * ( 1.0 - energySS ) / energySS;
|
|
196
|
+
let metallicSpecular = specular * metallicBoost;
|
|
197
|
+
var metallicReflectance = ${ this.conductorFresnel }( ctx.VdotH, surf.color, vec3f( 1.0 ) );
|
|
198
|
+
|
|
199
|
+
// dielectric with the multiscatter comp
|
|
200
|
+
let dielectricBoost = 1.0 + surf.f0 * ( 1.0 - energySS ) / energySS;
|
|
201
|
+
let dielectricSpecular = specular * dielectricBoost;
|
|
202
|
+
|
|
203
|
+
// KHR_materials_specular: fold the specular color and intensity into the dielectric f0.
|
|
204
|
+
// Schlick is used on both hit sides, matching Cycles - the opaque specular carries no
|
|
205
|
+
// TIR so the energy removed from the base always matches the energy paid back
|
|
206
|
+
let dielectricF0 = min( surf.f0 * surf.specularColor, vec3f( 1.0 ) );
|
|
207
|
+
let dielectricFr = ${ schlickFresnelVecFunc }( ctx.VdotH, dielectricF0, vec3f( 1.0 ) );
|
|
208
|
+
var dielectricReflectance = surf.specularIntensity * dielectricFr;
|
|
209
|
+
|
|
210
|
+
// iridescence
|
|
211
|
+
var filmFresnelMax = 0.0;
|
|
212
|
+
if ( surf.iridescence > 0.0 ) {
|
|
213
|
+
|
|
214
|
+
// the media on either side of the film - air outside and the volume interior
|
|
215
|
+
// as the base on front faces, swapped on back faces so TIR can take effect
|
|
216
|
+
let outsideIor = select( surf.ior, 1.0, surf.frontFace );
|
|
217
|
+
let filmBaseIor = select( 1.0, surf.ior, surf.frontFace );
|
|
218
|
+
|
|
219
|
+
let metallicFilmFresnel = ${ this.iridescentFresnel }( ctx.VdotH, surf.color, surf.iridescenceIor, outsideIor, surf.iridescenceThickness );
|
|
220
|
+
metallicReflectance = mix( metallicReflectance, metallicFilmFresnel, surf.iridescence );
|
|
221
|
+
|
|
222
|
+
let dielectricFilmFresnel = ${ this.iridescentFresnel }( ctx.VdotH, vec3f( ${ iorToF0Func }( filmBaseIor ) ), surf.iridescenceIor, outsideIor, surf.iridescenceThickness );
|
|
223
|
+
dielectricReflectance = mix( dielectricReflectance, dielectricFilmFresnel, surf.iridescence );
|
|
224
|
+
filmFresnelMax = max( max( dielectricFilmFresnel.r, dielectricFilmFresnel.g ), dielectricFilmFresnel.b );
|
|
225
|
+
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let metallic = metallicSpecular * metallicReflectance;
|
|
229
|
+
let dielectric = dielectricSpecular * dielectricReflectance;
|
|
230
|
+
|
|
231
|
+
// the energy the specular interface takes from the layers below
|
|
232
|
+
let fresnelEnergySS = ${ this.turquinTexture.sampleDielectricFn }( NdotV, surf.roughness, surf.ior ) * dielectricBoost;
|
|
233
|
+
|
|
234
|
+
result += attenuation * mix( ( 1.0 - surf.transmission ) * dielectric, metallic, surf.metalness );
|
|
235
|
+
attenuation *= ( 1.0 - surf.metalness ) * mix( 1.0 - fresnelEnergySS, 1.0 - filmFresnelMax, surf.iridescence );
|
|
236
|
+
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// diffuse
|
|
240
|
+
if ( NdotL > 0.0 ) {
|
|
241
|
+
|
|
242
|
+
// the dielectric base mixes diffuse with transmission - the transmissive half
|
|
243
|
+
// is carried by the glass lobe above
|
|
244
|
+
let diffuseVdotH = sqrt( saturate( 0.5 * ( 1.0 + dot( ctx.V, ctx.L ) ) ) );
|
|
245
|
+
let reflection = ${ this.diffuseBrdf }( NdotV, NdotL, diffuseVdotH, surf );
|
|
246
|
+
let diffuse = ( 1.0 - surf.transmission ) * reflection;
|
|
247
|
+
|
|
248
|
+
result += attenuation * diffuse;
|
|
249
|
+
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return result;
|
|
253
|
+
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
`;
|
|
257
|
+
|
|
258
|
+
const bsdfEvalPdfFn = this.getBsdfEvalPdfNode();
|
|
259
|
+
|
|
260
|
+
return wgslTagFn/* wgsl */`
|
|
261
|
+
|
|
262
|
+
fn bsdfSample( worldWo: vec3f, surf: ${ surfaceRecordStruct } ) -> ${ scatterRecordStruct } {
|
|
263
|
+
|
|
264
|
+
var result: ${ scatterRecordStruct };
|
|
265
|
+
result.color = vec3f( 0.0 );
|
|
266
|
+
result.isTransmissive = false;
|
|
267
|
+
result.direction = vec3f( 0.0 );
|
|
268
|
+
result.pdf = 0.0;
|
|
269
|
+
|
|
270
|
+
let wo = normalize( surf.normalInvBasis * worldWo );
|
|
271
|
+
let woClearcoat = normalize( surf.clearcoatInvBasis * worldWo );
|
|
272
|
+
|
|
273
|
+
// TODO: mirror bsdfEval's layer structure - the glass layer should own both its
|
|
274
|
+
// reflection and refraction halves
|
|
275
|
+
// lobe selection weights and cumulative bounds in cascade order:
|
|
276
|
+
// clearcoat, specular, transmission, diffuse
|
|
277
|
+
let weights = ${ getLobeWeightsFunc }( wo, wo, woClearcoat, vec3( 0, 0, 1 ), ${ CLEARCOAT_IOR }, surf );
|
|
278
|
+
let cdfClearcoat = weights.clearcoat;
|
|
279
|
+
let cdfSpecular = cdfClearcoat + weights.specular;
|
|
280
|
+
let cdfTransmission = cdfSpecular + weights.transmission;
|
|
281
|
+
let cdfTotal = cdfTransmission + weights.diffuse;
|
|
282
|
+
|
|
283
|
+
// random samples for lobes
|
|
284
|
+
let lobeSample = ${ rand1 }( ${ RNG_INDEX_SCATTER_TYPE } ) * cdfTotal;
|
|
285
|
+
let directionUV = ${ rand2 }( ${ RNG_INDEX_SCATTER_DIRECTION } );
|
|
286
|
+
|
|
287
|
+
// output
|
|
288
|
+
var wi: vec3f;
|
|
289
|
+
var wh: vec3f;
|
|
290
|
+
|
|
291
|
+
// TODO: see if we can clean up these flags so they're not necessary
|
|
292
|
+
var isTransmissionLobe = false;
|
|
293
|
+
var isDeltaTransmission = false;
|
|
294
|
+
var deltaTransmittance = vec3f( 1.0 );
|
|
295
|
+
var deltaProbability = 1.0;
|
|
296
|
+
var isDead = false;
|
|
297
|
+
|
|
298
|
+
if ( lobeSample <= cdfClearcoat ) {
|
|
299
|
+
|
|
300
|
+
// clearcoat
|
|
301
|
+
let clearcoatAlpha = surf.clearcoatRoughness * surf.clearcoatRoughness;
|
|
302
|
+
let whCoat = ${ ggxDirectionFunc }( woClearcoat, vec2( clearcoatAlpha ), directionUV );
|
|
303
|
+
let wiCoat = - normalize( reflect( woClearcoat, whCoat ) );
|
|
304
|
+
|
|
305
|
+
wi = normalize( surf.normalInvBasis * surf.clearcoatBasis * wiCoat );
|
|
306
|
+
wh = normalize( surf.normalInvBasis * surf.clearcoatBasis * whCoat );
|
|
307
|
+
|
|
308
|
+
// reflected rays must leave above the geometry surface - flip rays that land
|
|
309
|
+
// below it due to the shading normal. Rays below the shading hemisphere are
|
|
310
|
+
// left as is since their loss is refunded by the energy compensation tables
|
|
311
|
+
let faceNormal = normalize( surf.normalInvBasis * surf.faceNormal );
|
|
312
|
+
let geomDotDir = dot( wi, faceNormal );
|
|
313
|
+
if ( wi.z > 0.0 && geomDotDir < 0.0 ) {
|
|
314
|
+
|
|
315
|
+
wi = normalize( wi - 2.0 * geomDotDir * faceNormal );
|
|
316
|
+
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
} else if ( lobeSample <= cdfSpecular ) {
|
|
320
|
+
|
|
321
|
+
// specular
|
|
322
|
+
// anisotropic roughness along tangent, bitangent
|
|
323
|
+
let alphaB = surf.roughness * surf.roughness;
|
|
324
|
+
let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
|
|
325
|
+
let alpha = vec2( alphaT, alphaB );
|
|
326
|
+
|
|
327
|
+
wh = ${ ggxDirectionFunc }( wo, alpha, directionUV );
|
|
328
|
+
wi = - normalize( reflect( wo, wh ) );
|
|
329
|
+
|
|
330
|
+
// reflected rays must leave above the geometry surface - flip rays that land
|
|
331
|
+
// below it due to the shading normal. Rays below the shading hemisphere are
|
|
332
|
+
// left as is since their loss is refunded by the energy compensation tables
|
|
333
|
+
let faceNormal = normalize( surf.normalInvBasis * surf.faceNormal );
|
|
334
|
+
let geomDotDir = dot( wi, faceNormal );
|
|
335
|
+
if ( wi.z > 0.0 && geomDotDir < 0.0 ) {
|
|
336
|
+
|
|
337
|
+
wi = normalize( wi - 2.0 * geomDotDir * faceNormal );
|
|
338
|
+
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
} else if ( lobeSample <= cdfTransmission ) {
|
|
342
|
+
|
|
343
|
+
// transmission
|
|
344
|
+
isTransmissionLobe = true;
|
|
345
|
+
|
|
346
|
+
// anisotropic roughness along tangent, bitangent
|
|
347
|
+
let alphaB = surf.roughness * surf.roughness;
|
|
348
|
+
let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
|
|
349
|
+
let alpha = vec2( alphaT, alphaB );
|
|
350
|
+
|
|
351
|
+
// sample the half vector first and select reflection or refraction by the
|
|
352
|
+
// facet fresnel, matching Cycles - total internal reflection drives the
|
|
353
|
+
// fresnel to 1 so TIR facets always reflect with a matching pdf
|
|
354
|
+
wh = ${ ggxDirectionFunc }( wo, alpha, directionUV );
|
|
355
|
+
|
|
356
|
+
let fresnelCosine = select( dot( wo, wh ), wo.z, surf.thinWall );
|
|
357
|
+
let fresnel = ${ this.transmissionFresnel }( fresnelCosine, surf );
|
|
358
|
+
|
|
359
|
+
let fresnelSample = ( lobeSample - cdfSpecular ) / ( cdfTransmission - cdfSpecular );
|
|
360
|
+
let doReflect = fresnelSample < fresnel.reflectProbability;
|
|
361
|
+
if ( doReflect ) {
|
|
362
|
+
|
|
363
|
+
wi = - normalize( reflect( wo, wh ) );
|
|
364
|
+
|
|
365
|
+
} else if ( ${ isMatchedIorFunc }( surf.eta ) ) {
|
|
366
|
+
|
|
367
|
+
// With no IOR boundary, rough transmission collapses to straight-through
|
|
368
|
+
// delta transmission independent of the sampled microfacet.
|
|
369
|
+
wi = - wo;
|
|
370
|
+
wh = vec3f( 0.0, 0.0, 1.0 );
|
|
371
|
+
isDeltaTransmission = true;
|
|
372
|
+
deltaTransmittance = fresnel.transmittance;
|
|
373
|
+
deltaProbability = 1.0 - fresnel.reflectProbability;
|
|
374
|
+
|
|
375
|
+
} else if ( surf.thinWall ) {
|
|
376
|
+
|
|
377
|
+
// model the double refraction as a single reflection flipped through the
|
|
378
|
+
// surface at the remapped thin wall roughness
|
|
379
|
+
let thinWallAlpha = vec2f( ${ thinWallTransmissionRoughnessFunc }( alphaB, surf.ior ) );
|
|
380
|
+
wh = ${ ggxDirectionFunc }( wo, thinWallAlpha, directionUV );
|
|
381
|
+
wi = - normalize( reflect( wo, wh ) );
|
|
382
|
+
wi = vec3f( wi.xy, - wi.z );
|
|
383
|
+
|
|
384
|
+
} else {
|
|
385
|
+
|
|
386
|
+
wi = refract( - wo, wh, surf.eta );
|
|
387
|
+
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// the facet choice must agree with the resulting hemisphere - reflection above,
|
|
391
|
+
// transmission below. Mismatched samples are dropped since their loss is
|
|
392
|
+
// refunded by the energy compensation tables
|
|
393
|
+
isDead = doReflect != ( wi.z > 0.0 );
|
|
394
|
+
|
|
395
|
+
} else if ( lobeSample <= cdfTotal ) {
|
|
396
|
+
|
|
397
|
+
// diffuse
|
|
398
|
+
// all diffuse BRDF variants share the EON sampling proposal
|
|
399
|
+
wi = ${ eonDirectionFunc }( wo, surf.diffuseRoughness, directionUV );
|
|
400
|
+
wh = normalize( wi + wo );
|
|
401
|
+
|
|
402
|
+
// reflected rays must leave above the geometry surface - flip rays that land
|
|
403
|
+
// below it due to the shading normal. Rays below the shading hemisphere are
|
|
404
|
+
// left as is since their loss is refunded by the energy compensation tables
|
|
405
|
+
let faceNormal = normalize( surf.normalInvBasis * surf.faceNormal );
|
|
406
|
+
let geomDotDir = dot( wi, faceNormal );
|
|
407
|
+
if ( wi.z > 0.0 && geomDotDir < 0.0 ) {
|
|
408
|
+
|
|
409
|
+
wi = normalize( wi - 2.0 * geomDotDir * faceNormal );
|
|
410
|
+
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let worldWi = normalize( surf.normalBasis * wi );
|
|
416
|
+
if ( isDeltaTransmission ) {
|
|
417
|
+
|
|
418
|
+
// Cycles represents a singular event with a large matching BSDF and PDF. Their
|
|
419
|
+
// ratio preserves throughput while forward MIS correctly gives the event full weight.
|
|
420
|
+
result.color = ( 1.0 - surf.metalness ) * surf.transmission * surf.color *
|
|
421
|
+
deltaTransmittance * ${ SINGULAR_EVENT_SCALE };
|
|
422
|
+
result.direction = worldWi;
|
|
423
|
+
result.pdf = weights.transmission * deltaProbability * ${ SINGULAR_EVENT_SCALE };
|
|
424
|
+
result.isTransmissive = dot( worldWi, surf.faceNormal ) < 0.0;
|
|
425
|
+
|
|
426
|
+
} else {
|
|
427
|
+
|
|
428
|
+
// evaluate the shared eval / pdf function for the sampled direction so the sample
|
|
429
|
+
// and light-sampling paths can never disagree on color or pdf
|
|
430
|
+
result = ${ bsdfEvalPdfFn }( worldWo, worldWi, surf );
|
|
431
|
+
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// mismatched glass facets and reflection lobe samples that land below the
|
|
435
|
+
// hemisphere carry no energy - their loss is refunded by the compensation tables
|
|
436
|
+
let invalid = isDead || ( ! isTransmissionLobe && wi.z <= 0.0 );
|
|
437
|
+
result.color *= select( 1.0, 0.0, invalid );
|
|
438
|
+
|
|
439
|
+
return result;
|
|
440
|
+
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
`;
|
|
444
|
+
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Evaluates the BSDF and its sampling pdf for an arbitrary light direction ( both world space ).
|
|
448
|
+
// Used by next event estimation to weight a chosen light/environment direction. Shares the same
|
|
449
|
+
// bsdfEval and lobe-mixture pdf as bsdfSample so MIS weights stay consistent.
|
|
450
|
+
getBsdfEvalPdfNode() {
|
|
451
|
+
|
|
452
|
+
// bsdfSample embeds this node so both share a single instance
|
|
453
|
+
if ( this._bsdfEvalPdfNode ) {
|
|
454
|
+
|
|
455
|
+
return this._bsdfEvalPdfNode;
|
|
456
|
+
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if ( ! this._bsdfEvalFunc ) {
|
|
460
|
+
|
|
461
|
+
this.getBsdfNode();
|
|
462
|
+
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const bsdfEvalFunc = this._bsdfEvalFunc;
|
|
466
|
+
|
|
467
|
+
this._bsdfEvalPdfNode = wgslTagFn/* wgsl */`
|
|
468
|
+
|
|
469
|
+
fn bsdfEvalPdf( worldWo: vec3f, worldWi: vec3f, surf: ${ surfaceRecordStruct } ) -> ${ scatterRecordStruct } {
|
|
470
|
+
|
|
471
|
+
var result: ${ scatterRecordStruct };
|
|
472
|
+
result.color = vec3f( 0.0 );
|
|
473
|
+
result.isTransmissive = false;
|
|
474
|
+
result.direction = worldWi;
|
|
475
|
+
result.pdf = 0.0;
|
|
476
|
+
|
|
477
|
+
let wo = normalize( surf.normalInvBasis * worldWo );
|
|
478
|
+
let wi = normalize( surf.normalInvBasis * worldWi );
|
|
479
|
+
let woClearcoat = normalize( surf.clearcoatInvBasis * worldWo );
|
|
480
|
+
|
|
481
|
+
let isTransmission = wi.z < 0.0;
|
|
482
|
+
if ( isTransmission && ${ isMatchedIorFunc }( surf.eta ) ) {
|
|
483
|
+
|
|
484
|
+
// Delta transmission has no finite solid-angle BSDF or PDF. It is handled
|
|
485
|
+
// explicitly by bsdfSample, matching PBRT-v4's eta == 1 path.
|
|
486
|
+
return result;
|
|
487
|
+
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// reconstruct the half vector bsdfSample would have used for this direction -
|
|
491
|
+
// reflections use the standard half vector while refractions use the generalized
|
|
492
|
+
// form scaled by the ior ratio, oriented into the upper hemisphere
|
|
493
|
+
var wh: vec3f;
|
|
494
|
+
if ( ! isTransmission ) {
|
|
495
|
+
|
|
496
|
+
wh = normalize( wo + wi );
|
|
497
|
+
|
|
498
|
+
} else if ( surf.thinWall ) {
|
|
499
|
+
|
|
500
|
+
// thin wall transmission is modeled as a reflection flipped through the surface
|
|
501
|
+
wh = normalize( wo + vec3f( wi.xy, - wi.z ) );
|
|
502
|
+
|
|
503
|
+
} else {
|
|
504
|
+
|
|
505
|
+
wh = normalize( wi + wo * surf.eta );
|
|
506
|
+
wh *= sign( wh.z );
|
|
507
|
+
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let weights = ${ getLobeWeightsFunc }( wo, wo, woClearcoat, vec3( 0, 0, 1 ), ${ CLEARCOAT_IOR }, surf );
|
|
511
|
+
|
|
512
|
+
// TODO: mirror bsdfEval's layer structure - the glass layer should own both its
|
|
513
|
+
// reflection and refraction halves
|
|
514
|
+
// pdf mixture - every lobe that can produce the direction contributes its share,
|
|
515
|
+
// in the same cascade order and with the same terms as bsdfSample
|
|
516
|
+
|
|
517
|
+
// clearcoat
|
|
518
|
+
if ( weights.clearcoat > 0.0 ) {
|
|
519
|
+
|
|
520
|
+
// the clearcoat lobe evaluates in its own frame
|
|
521
|
+
let toClearcoatMat = surf.clearcoatInvBasis * surf.normalBasis;
|
|
522
|
+
let wiClearcoat = normalize( toClearcoatMat * wi );
|
|
523
|
+
if ( wiClearcoat.z > 0.0 ) {
|
|
524
|
+
|
|
525
|
+
let whClearcoat = normalize( toClearcoatMat * wh );
|
|
526
|
+
let clearcoatAlpha = surf.clearcoatRoughness * surf.clearcoatRoughness;
|
|
527
|
+
result.pdf += weights.clearcoat * ${ ggxReflectionAdjustedPDFFunc }( woClearcoat, whClearcoat, vec2( clearcoatAlpha ) );
|
|
528
|
+
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// specular
|
|
534
|
+
if ( weights.specular > 0.0 && wi.z > 0.0 ) {
|
|
535
|
+
|
|
536
|
+
// anisotropic roughness along tangent, bitangent
|
|
537
|
+
let alphaB = surf.roughness * surf.roughness;
|
|
538
|
+
let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
|
|
539
|
+
let alpha = vec2( alphaT, alphaB );
|
|
540
|
+
|
|
541
|
+
result.pdf += weights.specular * ${ ggxReflectionAdjustedPDFFunc }( wo, wh, alpha );
|
|
542
|
+
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// transmission
|
|
546
|
+
if ( weights.transmission > 0.0 ) {
|
|
547
|
+
|
|
548
|
+
// anisotropic roughness along tangent, bitangent
|
|
549
|
+
let alphaB = surf.roughness * surf.roughness;
|
|
550
|
+
let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
|
|
551
|
+
let alpha = vec2( alphaT, alphaB );
|
|
552
|
+
|
|
553
|
+
// the glass lobe selects reflection or refraction by the facet fresnel so
|
|
554
|
+
// each side carries the corresponding share of the transmission pdf
|
|
555
|
+
let fresnelCosine = select( dot( wo, wh ), wo.z, surf.thinWall );
|
|
556
|
+
let fresnel = ${ this.transmissionFresnel }( fresnelCosine, surf );
|
|
557
|
+
|
|
558
|
+
if ( wi.z > 0.0 ) {
|
|
559
|
+
|
|
560
|
+
result.pdf += weights.transmission * fresnel.reflectProbability * ${ ggxReflectionAdjustedPDFFunc }( wo, wh, alpha );
|
|
561
|
+
|
|
562
|
+
} else if ( surf.thinWall ) {
|
|
563
|
+
|
|
564
|
+
// the flipped reflection shares the reflection pdf at the remapped roughness
|
|
565
|
+
let thinWallAlpha = vec2f( ${ thinWallTransmissionRoughnessFunc }( alphaB, surf.ior ) );
|
|
566
|
+
result.pdf += weights.transmission * ( 1.0 - fresnel.reflectProbability ) * ${ ggxReflectionAdjustedPDFFunc }( wo, wh, thinWallAlpha );
|
|
567
|
+
|
|
568
|
+
} else {
|
|
569
|
+
|
|
570
|
+
result.pdf += weights.transmission * ( 1.0 - fresnel.reflectProbability ) * ${ ggxRefractionAdjustedPDFFunc }( wo, wi, wh, alpha, surf.eta );
|
|
571
|
+
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// diffuse
|
|
577
|
+
if ( weights.diffuse > 0.0 ) {
|
|
578
|
+
|
|
579
|
+
result.pdf += weights.diffuse * ${ eonPDFFunc }( wo, wi, surf.diffuseRoughness );
|
|
580
|
+
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
//
|
|
584
|
+
|
|
585
|
+
// construct the scatter context
|
|
586
|
+
var ctx: ${ bxdfContextStruct };
|
|
587
|
+
ctx.V = wo;
|
|
588
|
+
ctx.L = wi;
|
|
589
|
+
ctx.H = wh;
|
|
590
|
+
ctx.VdotH = saturate( dot( wo, wh ) );
|
|
591
|
+
|
|
592
|
+
// evaluate the bsdf for the direction
|
|
593
|
+
result.color = ${ bsdfEvalFunc }( ctx, surf ) * abs( wi.z );
|
|
594
|
+
|
|
595
|
+
// a direction crossing below the surface enters or leaves the volume
|
|
596
|
+
result.isTransmissive = isTransmission && dot( worldWi, surf.faceNormal ) < 0.0;
|
|
597
|
+
|
|
598
|
+
return result;
|
|
599
|
+
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
`;
|
|
603
|
+
|
|
604
|
+
return this._bsdfEvalPdfNode;
|
|
605
|
+
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
}
|