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,990 @@
|
|
|
1
|
+
import { wgslFn, mat3 } from 'three/tsl';
|
|
2
|
+
import {
|
|
3
|
+
inverseMat3x3Func,
|
|
4
|
+
getBasisFromNormalFunc,
|
|
5
|
+
iorToF0Func,
|
|
6
|
+
schlickFresnelFunc,
|
|
7
|
+
schlickFresnelVecFunc,
|
|
8
|
+
iorToF0GeneralFunc,
|
|
9
|
+
fresnel0ToIorFunc,
|
|
10
|
+
iorToF0GeneralVecFunc,
|
|
11
|
+
dielectricFresnelFunc,
|
|
12
|
+
totalInternalReflectionFunc,
|
|
13
|
+
totalInternalReflectionVecFunc,
|
|
14
|
+
} from './utils.wgsl.js';
|
|
15
|
+
import {
|
|
16
|
+
ggxSmithVisibilityFunc,
|
|
17
|
+
ggxDistributionFunc,
|
|
18
|
+
ggxDirectionFunc,
|
|
19
|
+
ggxReflectionAdjustedPDFFunc,
|
|
20
|
+
ggxRefractionAdjustedPDFFunc,
|
|
21
|
+
ggxShadowMaskG1Func,
|
|
22
|
+
} from './ggx.wgsl.js';
|
|
23
|
+
import { constants, surfaceRecordStruct, transmissionFresnelStruct } from './structs.wgsl.js';
|
|
24
|
+
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
25
|
+
|
|
26
|
+
// Correct the shading normal to prevent scattering rays below the geometry surface by bending
|
|
27
|
+
// the normal towards the geometry normal such that the perfect reflection ray is just above the
|
|
28
|
+
// geometry surface. Ported verbatim from Blender Cycles "ensure_valid_specular_reflection"
|
|
29
|
+
// (src/kernel/closure/bsdf_util.h), which is derived from from the Iray paper (Keller et al. 2017, A.3).
|
|
30
|
+
//
|
|
31
|
+
// This only guarantees perfect reflection rays are above surface - other diffuse rays, for example,
|
|
32
|
+
// flipped when sampling.
|
|
33
|
+
const ensureValidReflectionNormal = wgslTagFn/* wgsl */`
|
|
34
|
+
|
|
35
|
+
fn ensureValidReflectionNormal( n: vec3f, ng: vec3f, i: vec3f ) -> vec3f {
|
|
36
|
+
|
|
37
|
+
// reflected ray
|
|
38
|
+
let R = 2.0 * dot( n, i ) * n - i;
|
|
39
|
+
|
|
40
|
+
let Iz = dot( i, ng );
|
|
41
|
+
|
|
42
|
+
// reflection rays may always be at least as shallow as the incoming ray
|
|
43
|
+
let threshold = min( 0.9 * Iz, 0.01 );
|
|
44
|
+
if ( dot( ng, R ) >= threshold ) {
|
|
45
|
+
|
|
46
|
+
return n;
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Form coordinate system with Ng as the Z axis and N inside the X-Z-plane.
|
|
51
|
+
// The X axis is found by normalizing the component of N that's orthogonal to Ng.
|
|
52
|
+
// The Y axis isn't actually needed.
|
|
53
|
+
let orthoN = n - dot( n, ng ) * ng;
|
|
54
|
+
let orthoLen = length( orthoN );
|
|
55
|
+
let X = select( n, orthoN / orthoLen, orthoLen != 0.0 );
|
|
56
|
+
|
|
57
|
+
// Calculate N.z and N.x in the local coordinate system.
|
|
58
|
+
//
|
|
59
|
+
// The goal of this computation is to find a N' that is rotated towards Ng just enough
|
|
60
|
+
// to lift R' above the threshold (here called t), therefore dot(R', Ng) = t.
|
|
61
|
+
//
|
|
62
|
+
// See the Blender implementation for a full description of the solution.
|
|
63
|
+
let Ix = dot( i, X );
|
|
64
|
+
|
|
65
|
+
let a = Ix * Ix + Iz * Iz;
|
|
66
|
+
let b = 2.0 * ( a + Iz * threshold );
|
|
67
|
+
let c = ( threshold + Iz ) * ( threshold + Iz );
|
|
68
|
+
|
|
69
|
+
// only one root is valid; the sign of Ix selects it
|
|
70
|
+
var Nz2: f32;
|
|
71
|
+
if ( Ix < 0.0 ) {
|
|
72
|
+
|
|
73
|
+
Nz2 = 0.25 * ( b + sqrt( max( b * b - 4.0 * a * c, 0.0 ) ) ) / a;
|
|
74
|
+
|
|
75
|
+
} else {
|
|
76
|
+
|
|
77
|
+
Nz2 = 0.25 * ( b - sqrt( max( b * b - 4.0 * a * c, 0.0 ) ) ) / a;
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let Nx = sqrt( max( 1.0 - Nz2, 0.0 ) );
|
|
82
|
+
let Nz = sqrt( max( Nz2, 0.0 ) );
|
|
83
|
+
|
|
84
|
+
return Nx * X + Nz * ng;
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
// Adjusts the shading normal such that the provided view normal is guaranteed to be in
|
|
91
|
+
// positive hemisphere by rotating it towards the view direction just enough so view is
|
|
92
|
+
// just glancing the surface as a small epsilon if needed.
|
|
93
|
+
const ensureValidViewNormal = wgslTagFn/* wgsl */`
|
|
94
|
+
|
|
95
|
+
fn ensureValidViewNormal( n: vec3f, ng: vec3f, view: vec3f ) -> vec3f {
|
|
96
|
+
|
|
97
|
+
// ensure the view is at least slightly above the surface normal hemisphere
|
|
98
|
+
const MIN_VIEW_COS = 1e-6;
|
|
99
|
+
|
|
100
|
+
// if we're positive already then early out
|
|
101
|
+
let c = dot( n, view );
|
|
102
|
+
if ( c > MIN_VIEW_COS ) {
|
|
103
|
+
|
|
104
|
+
return n;
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// perpendicular vector to normal and view
|
|
109
|
+
var perp = n - c * view;
|
|
110
|
+
let perpLen = length( perp );
|
|
111
|
+
|
|
112
|
+
// if we reach here that means that he view is nearly the opposite direction and
|
|
113
|
+
// we can't derive plan to rotate on towards the view, so just use the geometry
|
|
114
|
+
// normal. This could likely only really happen with a severe normal map application.
|
|
115
|
+
if ( perpLen <= 1e-6 ) {
|
|
116
|
+
|
|
117
|
+
return ng;
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
perp = perp / perpLen;
|
|
122
|
+
|
|
123
|
+
// rotate the normal to be at larger than the above threshold - pythagorean
|
|
124
|
+
// theorem for generating normal with threshold*view component
|
|
125
|
+
return MIN_VIEW_COS * view + sqrt( 1.0 - MIN_VIEW_COS * MIN_VIEW_COS ) * perp;
|
|
126
|
+
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
// Builds getSurfaceRecord using the given per-instance sampleTexel and uv channel lookup
|
|
132
|
+
// Sentinel for a disabled glossy filter ( FLT_MAX, mirroring Cycles ): the blur term
|
|
133
|
+
// clamps to zero for any path pdf when the inverted filter value is this large.
|
|
134
|
+
export const FILTER_GLOSSY_DISABLED = 3.402823466e38;
|
|
135
|
+
|
|
136
|
+
export const getSurfaceRecordFunc = ( sampleTexel, getUvFromChannel, getColor ) => wgslFn( /* wgsl */ `
|
|
137
|
+
|
|
138
|
+
fn getSurfaceRecord(
|
|
139
|
+
material: Material,
|
|
140
|
+
vertexData: bvh_GeometryStruct,
|
|
141
|
+
side: f32,
|
|
142
|
+
faceNormal: vec3f,
|
|
143
|
+
view: vec3f,
|
|
144
|
+
blurRoughness: f32,
|
|
145
|
+
) -> SurfaceRecord {
|
|
146
|
+
|
|
147
|
+
// "faceNormal" is provided on the hit side so flip it back to the geometric side
|
|
148
|
+
var normal = faceNormal * side;
|
|
149
|
+
if ( material.flatShading == 0 ) {
|
|
150
|
+
|
|
151
|
+
normal = normalize( vertexData.normal.xyz );
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
var baseNormal = normal;
|
|
156
|
+
if ( material.normalMap != -1 ) {
|
|
157
|
+
|
|
158
|
+
// some provided tangents can be malformed (0, 0, 0) causing the normal to be degenerate
|
|
159
|
+
// resulting in NaNs and slow path tracing.
|
|
160
|
+
if ( length( vertexData.tangent.xyz ) > 0.0 && vertexData.tangent.w != 0.0 ) {
|
|
161
|
+
|
|
162
|
+
// TODO: consider re-orthonormalizing against the normal here since attribute
|
|
163
|
+
// interpolation could result in drift.
|
|
164
|
+
let tangent = normalize( vertexData.tangent.xyz );
|
|
165
|
+
let bitangent = normalize( cross( baseNormal, tangent ) * vertexData.tangent.w );
|
|
166
|
+
let vTBN = mat3x3f( tangent, bitangent, baseNormal );
|
|
167
|
+
|
|
168
|
+
let uvPrime = material.normalMapTransform * vec3( getUvFromChannel( vertexData, material.normalMap ), 1.0 );
|
|
169
|
+
var texNormal = sampleTexel( uvPrime.xy, material.normalMap, 0 ).xyz;
|
|
170
|
+
texNormal = texNormal * 2.0 - 1.0;
|
|
171
|
+
texNormal = texNormal * vec3f( material.normalScale, 1.0 );
|
|
172
|
+
normal = normalize( vTBN * texNormal );
|
|
173
|
+
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
normal *= side;
|
|
179
|
+
|
|
180
|
+
normal = ensureValidViewNormal( normal, faceNormal, view );
|
|
181
|
+
|
|
182
|
+
normal = ensureValidReflectionNormal( normal, faceNormal, view );
|
|
183
|
+
|
|
184
|
+
var albedo = vec4( material.color, material.opacity );
|
|
185
|
+
if ( material.vertexColors == 1 ) {
|
|
186
|
+
|
|
187
|
+
albedo *= getColor( vertexData );
|
|
188
|
+
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if ( material.map != -1 ) {
|
|
192
|
+
|
|
193
|
+
let uvPrime = material.mapTransform * vec3f( getUvFromChannel( vertexData, material.map ), 1 );
|
|
194
|
+
albedo *= sampleTexel( uvPrime.xy, material.map, 0 );
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if ( material.alphaMap != -1 ) {
|
|
199
|
+
|
|
200
|
+
let uvPrime = material.alphaMapTransform * vec3f( getUvFromChannel( vertexData, material.alphaMap ), 1 );
|
|
201
|
+
albedo.a *= sampleTexel( uvPrime.xy, material.alphaMap, 0 ).g;
|
|
202
|
+
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
var roughness = material.roughness;
|
|
206
|
+
if ( material.roughnessMap != -1 ) {
|
|
207
|
+
|
|
208
|
+
let uvPrime = material.roughnessMapTransform * vec3f( getUvFromChannel( vertexData, material.roughnessMap ), 1 );
|
|
209
|
+
let texColor = sampleTexel( uvPrime.xy, material.roughnessMap, 0 );
|
|
210
|
+
roughness *= texColor.g;
|
|
211
|
+
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
var metalness = material.metalness;
|
|
215
|
+
if ( material.metalnessMap != -1 ) {
|
|
216
|
+
|
|
217
|
+
let uvPrime = material.metalnessMapTransform * vec3f( getUvFromChannel( vertexData, material.metalnessMap ), 1 );
|
|
218
|
+
let texColor = sampleTexel( uvPrime.xy, material.metalnessMap, 0 );
|
|
219
|
+
metalness *= texColor.b;
|
|
220
|
+
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
var emission = material.emissiveIntensity * material.emissive;
|
|
224
|
+
if ( material.emissiveMap != -1 ) {
|
|
225
|
+
|
|
226
|
+
let uvPrime = material.emissiveMapTransform * vec3f( getUvFromChannel( vertexData, material.emissiveMap ), 1 );
|
|
227
|
+
let texColor = sampleTexel( uvPrime.xy, material.emissiveMap, 0 );
|
|
228
|
+
emission *= texColor.rgb;
|
|
229
|
+
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
var transmission = material.transmission;
|
|
233
|
+
if ( material.transmissionMap != -1 ) {
|
|
234
|
+
|
|
235
|
+
let uvPrime = material.transmissionMapTransform * vec3f( getUvFromChannel( vertexData, material.transmissionMap ), 1 );
|
|
236
|
+
let texColor = sampleTexel( uvPrime.xy, material.transmissionMap, 0 );
|
|
237
|
+
transmission *= texColor.r;
|
|
238
|
+
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
var clearcoat = material.clearcoat;
|
|
242
|
+
if ( material.clearcoatMap != -1 ) {
|
|
243
|
+
|
|
244
|
+
let uvPrime = material.clearcoatMapTransform * vec3f( getUvFromChannel( vertexData, material.clearcoatMap ), 1 );
|
|
245
|
+
let texColor = sampleTexel( uvPrime.xy, material.clearcoatMap, 0 );
|
|
246
|
+
clearcoat *= texColor.r;
|
|
247
|
+
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
var clearcoatRoughness = material.clearcoatRoughness;
|
|
251
|
+
if ( material.clearcoatRoughnessMap != -1 ) {
|
|
252
|
+
|
|
253
|
+
let uvPrime = material.clearcoatRoughnessMapTransform * vec3f( getUvFromChannel( vertexData, material.clearcoatRoughnessMap ), 1 );
|
|
254
|
+
let texColor = sampleTexel( uvPrime.xy, material.clearcoatRoughnessMap, 0 );
|
|
255
|
+
clearcoatRoughness *= texColor.g;
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var clearcoatNormal = baseNormal;
|
|
260
|
+
if ( material.clearcoatNormalMap != -1 ) {
|
|
261
|
+
|
|
262
|
+
// some provided tangents can be malformed (0, 0, 0) causing the normal to be degenerate
|
|
263
|
+
// resulting in NaNs and slow path tracing.
|
|
264
|
+
if ( length( vertexData.tangent.xyz ) > 0.0 && vertexData.tangent.w != 0.0 ) {
|
|
265
|
+
|
|
266
|
+
// TODO: consider re-orthonormalizing against the normal here since attribute
|
|
267
|
+
// interpolation could result in drift.
|
|
268
|
+
let tangent = normalize( vertexData.tangent.xyz );
|
|
269
|
+
let bitangent = normalize( cross( baseNormal, tangent ) * vertexData.tangent.w );
|
|
270
|
+
let vTBN = mat3x3f( tangent, bitangent, baseNormal );
|
|
271
|
+
|
|
272
|
+
let uvPrime = material.clearcoatNormalMapTransform * vec3( getUvFromChannel( vertexData, material.clearcoatNormalMap ), 1.0 );
|
|
273
|
+
var texNormal = sampleTexel( uvPrime.xy, material.clearcoatNormalMap, 0 ).xyz;
|
|
274
|
+
texNormal = texNormal * 2.0 - 1.0;
|
|
275
|
+
texNormal = texNormal * vec3f( material.clearcoatNormalScale, 1.0 );
|
|
276
|
+
clearcoatNormal = normalize( vTBN * texNormal );
|
|
277
|
+
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
clearcoatNormal *= side;
|
|
283
|
+
|
|
284
|
+
clearcoatNormal = ensureValidViewNormal( clearcoatNormal, faceNormal, view );
|
|
285
|
+
|
|
286
|
+
clearcoatNormal = ensureValidReflectionNormal( clearcoatNormal, faceNormal, view );
|
|
287
|
+
|
|
288
|
+
var sheenColor = material.sheenColor;
|
|
289
|
+
if ( material.sheenColorMap != -1 ) {
|
|
290
|
+
|
|
291
|
+
let uvPrime = material.sheenColorMapTransform * vec3f( getUvFromChannel( vertexData, material.sheenColorMap ), 1 );
|
|
292
|
+
let texColor = sampleTexel( uvPrime.xy, material.sheenColorMap, 0 );
|
|
293
|
+
sheenColor *= texColor.rgb;
|
|
294
|
+
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
var sheenRoughness = material.sheenRoughness;
|
|
298
|
+
if ( material.sheenRoughnessMap != -1 ) {
|
|
299
|
+
|
|
300
|
+
let uvPrime = material.sheenRoughnessMapTransform * vec3f( getUvFromChannel( vertexData, material.sheenRoughnessMap ), 1 );
|
|
301
|
+
let texColor = sampleTexel( uvPrime.xy, material.sheenRoughnessMap, 0 );
|
|
302
|
+
sheenRoughness *= texColor.r;
|
|
303
|
+
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
var iridescence = material.iridescence;
|
|
307
|
+
if ( material.iridescenceMap != -1 ) {
|
|
308
|
+
|
|
309
|
+
let uvPrime = material.iridescenceMapTransform * vec3f( getUvFromChannel( vertexData, material.iridescenceMap ), 1 );
|
|
310
|
+
let texColor = sampleTexel( uvPrime.xy, material.iridescenceMap, 0 );
|
|
311
|
+
iridescence *= texColor.r;
|
|
312
|
+
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
var iridescenceThickness = material.iridescenceThicknessMaximum;
|
|
316
|
+
if ( material.iridescenceThicknessMap != -1 ) {
|
|
317
|
+
|
|
318
|
+
let uvPrime = material.iridescenceThicknessMapTransform * vec3f( getUvFromChannel( vertexData, material.iridescenceThicknessMap ), 1 );
|
|
319
|
+
let texColor = sampleTexel( uvPrime.xy, material.iridescenceThicknessMap, 0 );
|
|
320
|
+
|
|
321
|
+
iridescenceThickness = mix(
|
|
322
|
+
material.iridescenceThicknessMinimum,
|
|
323
|
+
material.iridescenceThicknessMaximum,
|
|
324
|
+
texColor.g,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
var specularColor = material.specularColor;
|
|
330
|
+
if ( material.specularColorMap != -1 ) {
|
|
331
|
+
|
|
332
|
+
let uvPrime = material.specularColorMapTransform * vec3f( getUvFromChannel( vertexData, material.specularColorMap ), 1 );
|
|
333
|
+
let texColor = sampleTexel( uvPrime.xy, material.specularColorMap, 0 );
|
|
334
|
+
specularColor *= texColor.rgb;
|
|
335
|
+
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
var specularIntensity = material.specularIntensity;
|
|
339
|
+
if ( material.specularIntensityMap != -1 ) {
|
|
340
|
+
|
|
341
|
+
let uvPrime = material.specularIntensityMapTransform * vec3f( getUvFromChannel( vertexData, material.specularIntensityMap ), 1 );
|
|
342
|
+
let texColor = sampleTexel( uvPrime.xy, material.specularIntensityMap, 0 );
|
|
343
|
+
specularIntensity *= texColor.a;
|
|
344
|
+
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// extract the anisotropy magnitude vector in tangent space
|
|
348
|
+
var anisotropyDirVec = material.anisotropy * vec2f( cos( material.anisotropyRotation ), sin( material.anisotropyRotation ) );
|
|
349
|
+
if ( material.anisotropyMap != -1 ) {
|
|
350
|
+
|
|
351
|
+
let uvPrime = material.anisotropyMapTransform * vec3f( getUvFromChannel( vertexData, material.anisotropyMap ), 1.0 );
|
|
352
|
+
let aniTex = sampleTexel( uvPrime.xy, material.anisotropyMap, 0 );
|
|
353
|
+
|
|
354
|
+
// map rg encode the direction ([-1,1]), b the strength; rotate + scale by the material anisotropy.
|
|
355
|
+
let mapDir = aniTex.rg * 2.0 - vec2f( 1.0 );
|
|
356
|
+
let mapStr = aniTex.b;
|
|
357
|
+
let mapLen = length( mapDir );
|
|
358
|
+
if ( mapLen > EPSILON ) {
|
|
359
|
+
|
|
360
|
+
anisotropyDirVec = mat2x2f(
|
|
361
|
+
anisotropyDirVec.x, anisotropyDirVec.y,
|
|
362
|
+
- anisotropyDirVec.y, anisotropyDirVec.x
|
|
363
|
+
) * mapStr * mapDir / mapLen;
|
|
364
|
+
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// adjust the surface basis to be oriented along the anisotropic vector
|
|
370
|
+
let anisotropyStrength = length( anisotropyDirVec );
|
|
371
|
+
var surfaceBasis = getBasisFromNormal( normal );
|
|
372
|
+
if ( anisotropyStrength > 0.0 && length( vertexData.tangent.xyz ) > 0.0 && vertexData.tangent.w != 0.0 ) {
|
|
373
|
+
|
|
374
|
+
let anisotropyDir = anisotropyDirVec / anisotropyStrength;
|
|
375
|
+
|
|
376
|
+
// re-orthonormalize the tangent against the shading normal so the frame stays orthonormal -
|
|
377
|
+
// normal map adjustments will cause the normal and tangent to become non-orthonormal.
|
|
378
|
+
let tangent = normalize( vertexData.tangent.xyz - normal * dot( normal, vertexData.tangent.xyz ) );
|
|
379
|
+
let bitangent = cross( normal, tangent ) * vertexData.tangent.w;
|
|
380
|
+
|
|
381
|
+
surfaceBasis = mat3x3f(
|
|
382
|
+
tangent * anisotropyDir.x + bitangent * anisotropyDir.y,
|
|
383
|
+
bitangent * anisotropyDir.x - tangent * anisotropyDir.y,
|
|
384
|
+
normal
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
var surf: SurfaceRecord;
|
|
390
|
+
|
|
391
|
+
surf.volumeParticle = false;
|
|
392
|
+
surf.faceNormal = faceNormal;
|
|
393
|
+
surf.normal = normal;
|
|
394
|
+
|
|
395
|
+
surf.metalness = metalness;
|
|
396
|
+
surf.diffuseRoughness = material.diffuseRoughness;
|
|
397
|
+
surf.color = albedo.rgb;
|
|
398
|
+
surf.emission = emission;
|
|
399
|
+
|
|
400
|
+
surf.opacity = select( 1.0, albedo.a, material.transparent != 0 );
|
|
401
|
+
|
|
402
|
+
surf.ior = material.ior;
|
|
403
|
+
surf.transmission = transmission;
|
|
404
|
+
surf.thinWall = material.thinWall == 1;
|
|
405
|
+
surf.attenuationColor = material.attenuationColor;
|
|
406
|
+
surf.attenuationDistance = material.attenuationDistance;
|
|
407
|
+
|
|
408
|
+
surf.clearcoatNormal = clearcoatNormal;
|
|
409
|
+
surf.clearcoat = clearcoat;
|
|
410
|
+
|
|
411
|
+
surf.iridescence = iridescence;
|
|
412
|
+
surf.iridescenceIor = material.iridescenceIor;
|
|
413
|
+
surf.iridescenceThickness = iridescenceThickness;
|
|
414
|
+
|
|
415
|
+
surf.specularColor = specularColor;
|
|
416
|
+
surf.specularIntensity = specularIntensity;
|
|
417
|
+
|
|
418
|
+
surf.sheen = material.sheen;
|
|
419
|
+
surf.sheenColor = sheenColor;
|
|
420
|
+
|
|
421
|
+
let minRoughness = max( MIN_ROUGHNESS, blurRoughness );
|
|
422
|
+
surf.roughness = clamp( roughness, minRoughness, 1.0 );
|
|
423
|
+
surf.clearcoatRoughness = clamp( clearcoatRoughness, minRoughness, 1.0 );
|
|
424
|
+
surf.sheenRoughness = clamp( sheenRoughness, minRoughness, 1.0 );
|
|
425
|
+
surf.anisotropy = anisotropyStrength;
|
|
426
|
+
|
|
427
|
+
// frontFace is used to determine transmissive properties and PDF. If no transmission is used
|
|
428
|
+
// then we can just always assume this is a front face.
|
|
429
|
+
surf.frontFace = side == 1.0 || transmission == 0.0;
|
|
430
|
+
if ( material.thinWall == 1 || surf.frontFace ) {
|
|
431
|
+
|
|
432
|
+
surf.eta = 1.0 / material.ior;
|
|
433
|
+
|
|
434
|
+
} else {
|
|
435
|
+
|
|
436
|
+
surf.eta = material.ior;
|
|
437
|
+
|
|
438
|
+
}
|
|
439
|
+
surf.f0 = iorToF0( surf.eta );
|
|
440
|
+
|
|
441
|
+
// get the normal frames
|
|
442
|
+
surf.normalBasis = surfaceBasis;
|
|
443
|
+
surf.normalInvBasis = inverse( surf.normalBasis );
|
|
444
|
+
|
|
445
|
+
surf.clearcoatBasis = getBasisFromNormal( surf.clearcoatNormal );
|
|
446
|
+
surf.clearcoatInvBasis = inverse( surf.clearcoatBasis );
|
|
447
|
+
|
|
448
|
+
return surf;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
`, [
|
|
452
|
+
inverseMat3x3Func,
|
|
453
|
+
iorToF0Func,
|
|
454
|
+
getBasisFromNormalFunc,
|
|
455
|
+
sampleTexel,
|
|
456
|
+
getUvFromChannel,
|
|
457
|
+
getColor,
|
|
458
|
+
surfaceRecordStruct,
|
|
459
|
+
constants,
|
|
460
|
+
ensureValidReflectionNormal,
|
|
461
|
+
ensureValidViewNormal,
|
|
462
|
+
] );
|
|
463
|
+
|
|
464
|
+
/*
|
|
465
|
+
*
|
|
466
|
+
* N : Macronormal of the surface
|
|
467
|
+
* V ( wo ) : View direction
|
|
468
|
+
* L ( wi ) : Light direction
|
|
469
|
+
* H ( wh ) : Halfvector between V and L, micronormal of the surface in ggx
|
|
470
|
+
* f0 : Amount of light reflected when looking at a surface head on - "fresnel 0"
|
|
471
|
+
* f90 : Amount of light reflected at grazing angles
|
|
472
|
+
*
|
|
473
|
+
*/
|
|
474
|
+
|
|
475
|
+
// Disney Diffuse BRDF without subsurface approximation
|
|
476
|
+
export const diffuseBrdfFunc = wgslFn( /* wgsl */ `
|
|
477
|
+
|
|
478
|
+
fn diffuseBrdf( NdotV: f32, NdotL: f32, VdotH: f32, surf: SurfaceRecord ) -> vec3f {
|
|
479
|
+
|
|
480
|
+
// https://blog.selfshadow.com/publications/s2015-shading-course/burley/s2015_pbs_disney_bsdf_notes.pdf
|
|
481
|
+
// See equation (4)
|
|
482
|
+
|
|
483
|
+
let fl = schlickFresnel( NdotL, 0.0 );
|
|
484
|
+
let fv = schlickFresnel( NdotV, 0.0 );
|
|
485
|
+
|
|
486
|
+
let alpha = surf.roughness * surf.roughness;
|
|
487
|
+
let bias = mix( 0.0, 0.5, alpha ) - 1;
|
|
488
|
+
let energyFactor = mix( 1.0, 1.0 / 1.51, alpha );
|
|
489
|
+
|
|
490
|
+
let rr = 2.0 * alpha * VdotH * VdotH;
|
|
491
|
+
let retro = rr * ( fl + fv + fl * fv * ( rr + 2.0 * bias ) );
|
|
492
|
+
let fresnel = ( 1.0 + bias * fl ) * ( 1.0f + bias * fv );
|
|
493
|
+
|
|
494
|
+
// TODO: subsurface approx?
|
|
495
|
+
|
|
496
|
+
return energyFactor * ( surf.color / PI ) * ( retro + fresnel );
|
|
497
|
+
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
`, [ constants, schlickFresnelFunc, surfaceRecordStruct ] );
|
|
501
|
+
|
|
502
|
+
// Lambertian diffuse BRDF with cosine distribution
|
|
503
|
+
export const lambertBrdfFunc = wgslFn( /* wgsl */ `
|
|
504
|
+
|
|
505
|
+
fn lambertBrdf( NdotV: f32, NdotL: f32, VdotH: f32, surf: SurfaceRecord ) -> vec3f {
|
|
506
|
+
|
|
507
|
+
return surf.color / PI;
|
|
508
|
+
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
`, [ constants, surfaceRecordStruct ] );
|
|
512
|
+
|
|
513
|
+
export const specularBrdfFunc = wgslFn( /* wgsl */ `
|
|
514
|
+
|
|
515
|
+
fn specularBrdf( V: vec3f, L: vec3f, H: vec3f, alpha: vec2f ) -> vec3f {
|
|
516
|
+
|
|
517
|
+
let alphaT = alpha.x;
|
|
518
|
+
let alphaB = alpha.y;
|
|
519
|
+
|
|
520
|
+
let Vis = ggxSmithVisibility( V, L, alpha );
|
|
521
|
+
let D = ggxDistribution( H, alpha );
|
|
522
|
+
|
|
523
|
+
return vec3f( D * Vis );
|
|
524
|
+
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
`, [ ggxSmithVisibilityFunc, ggxDistributionFunc ] );
|
|
528
|
+
|
|
529
|
+
// PBRT-v4 treats matched media as perfect specular transmission because the rough dielectric
|
|
530
|
+
// half-vector and its Jacobian are undefined when eta is one:
|
|
531
|
+
// https://github.com/mmp/pbrt-v4/blob/master/src/pbrt/bxdfs.cpp
|
|
532
|
+
export const isMatchedIorFunc = wgslFn( /* wgsl */ `
|
|
533
|
+
|
|
534
|
+
fn isMatchedIor( eta: f32 ) -> bool {
|
|
535
|
+
|
|
536
|
+
return abs( eta - 1.0 ) < EPSILON;
|
|
537
|
+
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
`, [ constants ] );
|
|
541
|
+
|
|
542
|
+
export const specularBtdfFunc = wgslFn( /* wgsl */`
|
|
543
|
+
|
|
544
|
+
fn specularBtdf( V: vec3f, L: vec3f, H: vec3f, alpha: vec2f, eta: f32 ) -> vec3f {
|
|
545
|
+
|
|
546
|
+
let NdotV = V.z;
|
|
547
|
+
let NdotL = L.z;
|
|
548
|
+
let HdotV = dot( V, H );
|
|
549
|
+
let HdotL = dot( L, H );
|
|
550
|
+
|
|
551
|
+
// Heaviside function for G term
|
|
552
|
+
if ( NdotV * HdotV < 0.0 || NdotL * HdotL < 0.0 || HdotV * HdotL > 0.0 ) {
|
|
553
|
+
|
|
554
|
+
return vec3f( 0.0 );
|
|
555
|
+
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let G1_i = ggxShadowMaskG1( V, alpha );
|
|
559
|
+
let G1_o = ggxShadowMaskG1( L, alpha );
|
|
560
|
+
|
|
561
|
+
// separable G2 product, no height correlation
|
|
562
|
+
let denom = eta * HdotV + HdotL;
|
|
563
|
+
let Vis = G1_i * G1_o * abs( HdotV ) * abs( HdotL ) /
|
|
564
|
+
( abs( NdotV ) * abs( NdotL ) * denom * denom );
|
|
565
|
+
|
|
566
|
+
let F = dielectricFresnel( abs( HdotV ), eta );
|
|
567
|
+
|
|
568
|
+
let D = ggxDistribution( H, alpha );
|
|
569
|
+
|
|
570
|
+
return vec3f( ( 1 - F ) * D * Vis );
|
|
571
|
+
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
`, [ ggxShadowMaskG1Func, ggxDistributionFunc, dielectricFresnelFunc ] );
|
|
575
|
+
|
|
576
|
+
// Effective roughness for thin walled transmission, from page 40 (note the slides' 3.7 is a typo for 3.4) -
|
|
577
|
+
// referenced from Blender Cycles:
|
|
578
|
+
// https://blog.selfshadow.com/publications/s2017-shading-course/imageworks/s2017_pbs_imageworks_slides_v2.pdf
|
|
579
|
+
export const thinWallTransmissionRoughnessFunc = wgslFn( /* wgsl */ `
|
|
580
|
+
|
|
581
|
+
fn thinWallTransmissionRoughness( alpha: f32, eta: f32 ) -> f32 {
|
|
582
|
+
|
|
583
|
+
let t = 3.4 * ( eta - 1.0 ) * ( eta - 0.5 ) * ( eta - 0.5 ) / ( eta * eta * eta );
|
|
584
|
+
return saturate( alpha * sqrt( max( t, 0.0 ) ) );
|
|
585
|
+
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
` );
|
|
589
|
+
|
|
590
|
+
// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_volume/README.md#attenuation
|
|
591
|
+
export const transmissionAttenuationFunc = wgslFn( /* wgsl */ `
|
|
592
|
+
|
|
593
|
+
fn transmissionAttenuation( dist: f32, attColor: vec3f, attDist: f32 ) -> vec3f {
|
|
594
|
+
|
|
595
|
+
return pow( attColor, vec3f( dist / attDist ) );
|
|
596
|
+
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
` );
|
|
600
|
+
|
|
601
|
+
// Continuous Cauchy IOR approximation derived from the Fraunhofer C, d, and F spectral lines:
|
|
602
|
+
// https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_dispersion
|
|
603
|
+
// https://github.com/bsdorra/slang-pbr/blob/c12046ec3b9628bcfc384ed2c275c92d007e5d7e/src/models/enterprise/components/transmission.slang#L20-L27
|
|
604
|
+
export const DISPERSION_MIN_WAVELENGTH = 360;
|
|
605
|
+
export const DISPERSION_MAX_WAVELENGTH = 830;
|
|
606
|
+
|
|
607
|
+
export const dispersionIorFunc = wgslFn( /* wgsl */ `
|
|
608
|
+
|
|
609
|
+
fn dispersionIor( ior: f32, dispersion: f32, wavelength: f32 ) -> f32 {
|
|
610
|
+
|
|
611
|
+
let nd = max( ior, 1.0 );
|
|
612
|
+
let scale = ( nd - 1.0 ) * max( dispersion, 0.0 ) / 20.0;
|
|
613
|
+
let lambda = clamp( wavelength, ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0 );
|
|
614
|
+
return max( nd + scale * ( 523655.0 / ( lambda * lambda ) - 1.5168 ), 1.0 );
|
|
615
|
+
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
` );
|
|
619
|
+
|
|
620
|
+
export const applyDispersionFunc = wgslTagFn/* wgsl */ `
|
|
621
|
+
|
|
622
|
+
fn applyDispersion( surf: ptr<function, ${ surfaceRecordStruct }>, dispersion: f32, wavelength: f32 ) -> void {
|
|
623
|
+
|
|
624
|
+
surf.ior = ${ dispersionIorFunc }( surf.ior, dispersion, wavelength );
|
|
625
|
+
surf.eta = select( surf.ior, 1.0 / surf.ior, surf.thinWall || surf.frontFace );
|
|
626
|
+
surf.f0 = ${ iorToF0Func }( surf.eta );
|
|
627
|
+
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
`;
|
|
631
|
+
|
|
632
|
+
// A path keeps one uniformly sampled hero wavelength after its first dispersive interaction.
|
|
633
|
+
// The CIE 1931 fit is converted to linear Rec. 709, clamped to the RGB gamut, and normalized so
|
|
634
|
+
// every channel has an expected weight of one over the uniformly sampled wavelength interval.
|
|
635
|
+
// This preserves the expected value of the renderer's existing RGB throughput while providing
|
|
636
|
+
// continuous hues. The renderer still uses RGB material and light data rather than spectral data.
|
|
637
|
+
// CIE fit: https://jcgt.org/published/0002/02/01/
|
|
638
|
+
// Hero wavelength sampling:
|
|
639
|
+
// https://pbr-book.org/4ed/Textures_and_Materials/Material_Interface_and_Implementations.html#sec:dielectric-material
|
|
640
|
+
// https://doi.org/10.1111/cgf.12419
|
|
641
|
+
export const dispersionColorWeightFunc = wgslFn( /* wgsl */ `
|
|
642
|
+
|
|
643
|
+
fn dispersionColorWeight( wavelength: f32 ) -> vec3f {
|
|
644
|
+
|
|
645
|
+
let lambda = clamp( wavelength, ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0 );
|
|
646
|
+
|
|
647
|
+
let x1 = ( lambda - 442.0 ) * select( 0.0374, 0.0624, lambda < 442.0 );
|
|
648
|
+
let x2 = ( lambda - 599.8 ) * select( 0.0323, 0.0264, lambda < 599.8 );
|
|
649
|
+
let x3 = ( lambda - 501.1 ) * select( 0.0382, 0.0490, lambda < 501.1 );
|
|
650
|
+
let x = 0.362 * exp( - 0.5 * x1 * x1 ) + 1.056 * exp( - 0.5 * x2 * x2 ) - 0.065 * exp( - 0.5 * x3 * x3 );
|
|
651
|
+
|
|
652
|
+
let y1 = ( lambda - 568.8 ) * select( 0.0247, 0.0213, lambda < 568.8 );
|
|
653
|
+
let y2 = ( lambda - 530.9 ) * select( 0.0322, 0.0613, lambda < 530.9 );
|
|
654
|
+
let y = 0.821 * exp( - 0.5 * y1 * y1 ) + 0.286 * exp( - 0.5 * y2 * y2 );
|
|
655
|
+
|
|
656
|
+
let z1 = ( lambda - 437.0 ) * select( 0.0278, 0.0845, lambda < 437.0 );
|
|
657
|
+
let z2 = ( lambda - 459.0 ) * select( 0.0725, 0.0385, lambda < 459.0 );
|
|
658
|
+
let z = 1.217 * exp( - 0.5 * z1 * z1 ) + 0.681 * exp( - 0.5 * z2 * z2 );
|
|
659
|
+
|
|
660
|
+
let linearRgb = max( vec3f(
|
|
661
|
+
3.2404542 * x - 1.5371385 * y - 0.4985314 * z,
|
|
662
|
+
- 0.9692660 * x + 1.8760108 * y + 0.0415560 * z,
|
|
663
|
+
0.0556434 * x - 0.2040259 * y + 1.0572252 * z,
|
|
664
|
+
), vec3f( 0.0 ) );
|
|
665
|
+
|
|
666
|
+
return linearRgb * vec3f( 2.6699448, 4.0708066, 4.2980494 );
|
|
667
|
+
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
` );
|
|
671
|
+
|
|
672
|
+
// Dielectric layer fresnel operator that supports custom f0 color, specular weight.
|
|
673
|
+
// Based on the specular color specification:
|
|
674
|
+
// https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_specular
|
|
675
|
+
export const fresnelMixFunc = wgslFn( /* wgsl */ `
|
|
676
|
+
|
|
677
|
+
fn fresnelMix( VdotH: f32, f0Color: vec3f, ior: f32, eta: f32, weight: f32, base: vec3f, layer: vec3f ) -> vec3f {
|
|
678
|
+
|
|
679
|
+
var f0 = iorToF0( ior ) * f0Color;
|
|
680
|
+
f0 = min( f0, vec3f( 1.0 ) );
|
|
681
|
+
|
|
682
|
+
// reflect all light on total internal reflection, matching the WebGL evaluateFresnel function
|
|
683
|
+
var fr = vec3f( 1.0 );
|
|
684
|
+
if ( ! totalInternalReflection( abs( VdotH ), eta ) ) {
|
|
685
|
+
|
|
686
|
+
fr = schlickFresnelVec( abs( VdotH ), f0, vec3f( 1.0 ) );
|
|
687
|
+
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
let maxFr = max( max( fr.r, fr.g ), fr.b );
|
|
691
|
+
|
|
692
|
+
return ( 1.0 - weight * maxFr ) * base + weight * fr * layer;
|
|
693
|
+
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
`, [ schlickFresnelVecFunc, iorToF0Func, totalInternalReflectionFunc ] );
|
|
697
|
+
|
|
698
|
+
const XYZ_TO_REC709 = mat3(
|
|
699
|
+
3.2404542, - 0.9692660, 0.0556434,
|
|
700
|
+
- 1.5371385, 1.8760108, - 0.2040259,
|
|
701
|
+
- 0.4985314, 0.0415560, 1.0572252,
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
const evalSensitivityFunc = wgslTagFn/* wgsl */`
|
|
705
|
+
|
|
706
|
+
fn evalSensitivity( OPD: f32, shift: vec3f ) -> vec3f {
|
|
707
|
+
|
|
708
|
+
let phase = 2.0 * ${ Math.PI } * OPD * 1.0e-9;
|
|
709
|
+
const val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13);
|
|
710
|
+
const pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06);
|
|
711
|
+
const _var = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09);
|
|
712
|
+
|
|
713
|
+
var xyz = val * sqrt(2.0 * ${ Math.PI } * _var) * cos(pos * phase + shift) * exp(-phase * phase * _var);
|
|
714
|
+
xyz.x += 9.7470e-14 * sqrt(2.0 * ${ Math.PI } * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase * phase);
|
|
715
|
+
xyz /= 1.0685e-7;
|
|
716
|
+
|
|
717
|
+
let rgb = ${ XYZ_TO_REC709 } * xyz;
|
|
718
|
+
return rgb;
|
|
719
|
+
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
`;
|
|
723
|
+
|
|
724
|
+
// Reference: Belcour/Barla, 2017
|
|
725
|
+
// https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html
|
|
726
|
+
// This is a simplified model that ignores light polarization and uses fresnel approximation
|
|
727
|
+
export const iridescentFresnelFunc = wgslFn( /* wgsl */ `
|
|
728
|
+
|
|
729
|
+
fn iridescentFresnel(
|
|
730
|
+
cosTheta1: f32, baseF0: vec3f, iridescenceIor: f32,
|
|
731
|
+
outsideIor: f32, iridescenceThickness: f32,
|
|
732
|
+
) -> vec3f {
|
|
733
|
+
|
|
734
|
+
let sinTheta2Sq = pow( outsideIor / iridescenceIor, 2.0 ) * ( 1.0 - pow( cosTheta1, 2.0 ) );
|
|
735
|
+
let cosTheta2Sq = 1.0 - sinTheta2Sq;
|
|
736
|
+
|
|
737
|
+
// Handle total internal reflection
|
|
738
|
+
if ( cosTheta2Sq < 0.0 ) {
|
|
739
|
+
|
|
740
|
+
return vec3( 1.0 );
|
|
741
|
+
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
let cosTheta2 = sqrt( cosTheta2Sq );
|
|
745
|
+
|
|
746
|
+
// First interface: air -> iridescent thin film
|
|
747
|
+
let R0 = iorToF0General( iridescenceIor, outsideIor );
|
|
748
|
+
let R12 = schlickFresnel( cosTheta1, R0 );
|
|
749
|
+
let R21 = R12;
|
|
750
|
+
let T121 = 1.0 - R12;
|
|
751
|
+
let phi12 = select( 0.0, PI, iridescenceIor < outsideIor );
|
|
752
|
+
let phi21 = PI - phi12;
|
|
753
|
+
|
|
754
|
+
// Second interface: iridescent thin film -> base material
|
|
755
|
+
let baseIor = fresnel0ToIor( clamp( baseF0, vec3( 0.0 ), vec3( 0.9999 ) ) ); // guard against 1.0
|
|
756
|
+
let R1 = iorToF0GeneralVec( baseIor, vec3( iridescenceIor ) );
|
|
757
|
+
var R23 = schlickFresnelVec( cosTheta2, R1, vec3( 1.0 ) );
|
|
758
|
+
|
|
759
|
+
// Handle total internal reflection at the film -> base interface
|
|
760
|
+
// NOTE: Added separately from the original implementation. Is this correct?
|
|
761
|
+
R23 = select( R23, vec3( 1.0 ), totalInternalReflectionVec( cosTheta2, vec3( iridescenceIor ) / baseIor ) );
|
|
762
|
+
let phi23 = select( vec3( 0.0 ), vec3( PI ), baseIor < vec3( iridescenceIor ) );
|
|
763
|
+
|
|
764
|
+
// Phase shift
|
|
765
|
+
let OPD = 2.0 * iridescenceIor * iridescenceThickness * cosTheta2;
|
|
766
|
+
let phi = vec3( phi21 ) + phi23;
|
|
767
|
+
|
|
768
|
+
// Analytical integration
|
|
769
|
+
// Compound terms
|
|
770
|
+
let R123 = clamp( R12 * R23, vec3( 1e-5 ), vec3( 0.9999 ) );
|
|
771
|
+
let r123 = sqrt( R123 );
|
|
772
|
+
let Rs = T121 * T121 * R23 / ( vec3( 1.0 ) - R123 );
|
|
773
|
+
|
|
774
|
+
// Reflectance term for m = 0 (DC term amplitude)
|
|
775
|
+
let C0 = R12 + Rs;
|
|
776
|
+
var I = C0;
|
|
777
|
+
|
|
778
|
+
// Reflectance term for m > 0 (pairs of diracs)
|
|
779
|
+
var Cm = Rs - T121;
|
|
780
|
+
for (var m = 1; m <= 2; m += 1) {
|
|
781
|
+
|
|
782
|
+
Cm *= r123;
|
|
783
|
+
let Sm = 2.0 * evalSensitivity( f32( m ) * OPD, f32( m ) * phi );
|
|
784
|
+
I += Cm * Sm;
|
|
785
|
+
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return max( I, vec3(0.0) );
|
|
789
|
+
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
`, [ iorToF0GeneralFunc, iorToF0GeneralVecFunc, schlickFresnelFunc, fresnel0ToIorFunc, evalSensitivityFunc, totalInternalReflectionVecFunc ] );
|
|
793
|
+
|
|
794
|
+
// Reflection and transmission coefficients for the transmissive dielectric layer. The scalar
|
|
795
|
+
// branch probability follows Cycles: choose between the two colored coefficients by their mean
|
|
796
|
+
// energy while retaining the full RGB coefficient in the path throughput.
|
|
797
|
+
export const transmissionFresnelFunc = ( iridescentFresnel = iridescentFresnelFunc ) => wgslTagFn/* wgsl */ `
|
|
798
|
+
|
|
799
|
+
fn transmissionFresnel( cosTheta: f32, surf: ${ surfaceRecordStruct } ) -> ${ transmissionFresnelStruct } {
|
|
800
|
+
|
|
801
|
+
var result: ${ transmissionFresnelStruct };
|
|
802
|
+
let cosine = saturate( abs( cosTheta ) );
|
|
803
|
+
let matchedIor = ${ isMatchedIorFunc }( surf.eta );
|
|
804
|
+
let airIncident = surf.thinWall || surf.frontFace;
|
|
805
|
+
let dielectricFresnel = ${ dielectricFresnelFunc }( cosine, surf.eta );
|
|
806
|
+
|
|
807
|
+
// Keep matched media transparent unless a real thin-film layer is present. This avoids
|
|
808
|
+
// introducing the non-physical Schlick F90 reflection produced by KHR_materials_specular
|
|
809
|
+
// when the substrate itself has no IOR boundary.
|
|
810
|
+
var reflectance = vec3f( 0.0 );
|
|
811
|
+
if ( ! matchedIor ) {
|
|
812
|
+
|
|
813
|
+
let specularF0 = min( surf.f0 * surf.specularColor, vec3f( 1.0 ) );
|
|
814
|
+
let dielectricReflectance = select(
|
|
815
|
+
vec3f( dielectricFresnel ),
|
|
816
|
+
${ schlickFresnelVecFunc }( cosine, specularF0, vec3f( 1.0 ) ),
|
|
817
|
+
airIncident,
|
|
818
|
+
);
|
|
819
|
+
reflectance = surf.specularIntensity * dielectricReflectance;
|
|
820
|
+
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
reflectance = clamp( reflectance, vec3f( 0.0 ), vec3f( 1.0 ) );
|
|
824
|
+
var transmittance = vec3f( 1.0 ) - reflectance;
|
|
825
|
+
|
|
826
|
+
if ( surf.iridescence > 0.0 ) {
|
|
827
|
+
|
|
828
|
+
// The media on either side of the film are air outside and the volume interior as
|
|
829
|
+
// the base. Swap them on interior hits so film-interface TIR remains possible.
|
|
830
|
+
let outsideIor = select( surf.ior, 1.0, airIncident );
|
|
831
|
+
let filmBaseIor = select( 1.0, surf.ior, airIncident );
|
|
832
|
+
let filmReflectance = clamp( ${ iridescentFresnel }(
|
|
833
|
+
cosine,
|
|
834
|
+
vec3f( ${ iorToF0Func }( filmBaseIor ) ),
|
|
835
|
+
surf.iridescenceIor,
|
|
836
|
+
outsideIor,
|
|
837
|
+
surf.iridescenceThickness,
|
|
838
|
+
), vec3f( 0.0 ), vec3f( 1.0 ) );
|
|
839
|
+
|
|
840
|
+
reflectance = mix( reflectance, filmReflectance, surf.iridescence );
|
|
841
|
+
transmittance = mix( transmittance, vec3f( 1.0 ) - filmReflectance, surf.iridescence );
|
|
842
|
+
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
result.reflectance = reflectance;
|
|
846
|
+
result.transmittance = transmittance;
|
|
847
|
+
|
|
848
|
+
// Include the base-color tint in the transmission energy, as Cycles does when it
|
|
849
|
+
// computes the glass reflection probability from its two tinted coefficients.
|
|
850
|
+
let reflectEnergy = dot( reflectance, vec3f( 1.0 / 3.0 ) );
|
|
851
|
+
let transmitEnergy = dot( transmittance * surf.color, vec3f( 1.0 / 3.0 ) );
|
|
852
|
+
let totalEnergy = reflectEnergy + transmitEnergy;
|
|
853
|
+
result.reflectProbability = select( 0.0, reflectEnergy / max( totalEnergy, EPSILON ), totalEnergy > EPSILON );
|
|
854
|
+
|
|
855
|
+
return result;
|
|
856
|
+
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
`;
|
|
860
|
+
|
|
861
|
+
export const conductorFresnelFunc = wgslFn( /* wgsl */ `
|
|
862
|
+
|
|
863
|
+
fn conductorFresnel( VdotH: f32, f0: vec3f, specular: vec3f ) -> vec3f {
|
|
864
|
+
|
|
865
|
+
return specular * schlickFresnelVec( abs( VdotH ), f0, vec3f( 1 ) );
|
|
866
|
+
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
`, [ schlickFresnelVecFunc ] );
|
|
870
|
+
|
|
871
|
+
export const fresnelCoatFunc = wgslFn( /* wgsl */ `
|
|
872
|
+
|
|
873
|
+
fn fresnelCoat( VdotNc: f32, ior: f32, base: vec3f, layer: vec3f, weight: f32 ) -> vec3f {
|
|
874
|
+
|
|
875
|
+
let f0 = iorToF0( ior );
|
|
876
|
+
let F = schlickFresnel( abs( VdotNc ), f0 );
|
|
877
|
+
|
|
878
|
+
return mix( base, layer, weight * F );
|
|
879
|
+
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
`, [ iorToF0Func, schlickFresnelFunc ] );
|
|
883
|
+
|
|
884
|
+
// GGX Multibounce compensation using Turquin's method. Bakes the directional albedo of the
|
|
885
|
+
// reflection lobe, optionally fresnel-weighted, and optionally including the refraction lobe
|
|
886
|
+
// for the full transmissive bsdf. "eta" is the incident over transmitted ior ratio.
|
|
887
|
+
export const turquinIntegralFn = outputTarget => wgslTagFn/* wgsl */ `
|
|
888
|
+
|
|
889
|
+
fn albedo(
|
|
890
|
+
eta: f32,
|
|
891
|
+
includeFresnel: bool,
|
|
892
|
+
includeRefraction: bool,
|
|
893
|
+
globalId: vec3u,
|
|
894
|
+
layer: u32,
|
|
895
|
+
) -> void {
|
|
896
|
+
|
|
897
|
+
// The matched-IOR limit is a unit-energy delta transmission event, so it cannot
|
|
898
|
+
// be integrated with the rough BTDF below.
|
|
899
|
+
if ( includeRefraction && ${ isMatchedIorFunc }( eta ) ) {
|
|
900
|
+
|
|
901
|
+
textureStore( ${ outputTarget }, vec3( globalId.xy, layer ), vec4( 1.0 ) );
|
|
902
|
+
return;
|
|
903
|
+
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// sample the brdf directions in a grid pattern
|
|
907
|
+
const GRID_SIZE = 64u;
|
|
908
|
+
|
|
909
|
+
// TODO: this sampling means that energy at 0.0 & 1.0 roughness (and 0 and 90deg cos) are never
|
|
910
|
+
// written to the texture due to the half texel inset, resulting in small, though possibly noticeable,
|
|
911
|
+
// error in common cases.
|
|
912
|
+
let dimensions = textureDimensions( ${ outputTarget } ).xy;
|
|
913
|
+
let uv = ( vec2f( globalId.xy ) + vec2f( 0.5 ) ) / vec2f( dimensions );
|
|
914
|
+
|
|
915
|
+
let cosThetaO = uv.x;
|
|
916
|
+
let roughness = uv.y;
|
|
917
|
+
let alpha = roughness * roughness;
|
|
918
|
+
|
|
919
|
+
let wo = vec3( sqrt( 1 - cosThetaO * cosThetaO ), 0 , cosThetaO );
|
|
920
|
+
|
|
921
|
+
let f0 = ${ iorToF0Func }( eta );
|
|
922
|
+
|
|
923
|
+
var result = 0.0;
|
|
924
|
+
for ( var x = 0u; x < GRID_SIZE; x++ ) {
|
|
925
|
+
|
|
926
|
+
for ( var y = 0u; y < GRID_SIZE; y++ ) {
|
|
927
|
+
|
|
928
|
+
// calculate the half vector to sample
|
|
929
|
+
let gridPoint = vec2f( vec2u( x, y ) ) + vec2f( 0.5 );
|
|
930
|
+
let sampleUv = gridPoint / f32( GRID_SIZE );
|
|
931
|
+
let wh = ${ ggxDirectionFunc }( wo, vec2( alpha ), sampleUv );
|
|
932
|
+
|
|
933
|
+
var f = 1.0;
|
|
934
|
+
if ( includeFresnel ) {
|
|
935
|
+
|
|
936
|
+
// use the exact dielectric fresnel so the table agrees with the fresnel used
|
|
937
|
+
// for lobe sampling
|
|
938
|
+
f = ${ dielectricFresnelFunc }( dot( wo, wh ), eta );
|
|
939
|
+
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// air-incident reflection is evaluated with schlick in bsdfEval - only the
|
|
943
|
+
// volume-incident ( TIR ) glass reflection uses the exact fresnel
|
|
944
|
+
var reflectionF = f;
|
|
945
|
+
if ( eta < 1.0 ) {
|
|
946
|
+
|
|
947
|
+
reflectionF = ${ schlickFresnelFunc }( dot( wo, wh ), f0 );
|
|
948
|
+
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// reflection lobe - samples landing below the surface are lost
|
|
952
|
+
let wi = - reflect( wo, wh );
|
|
953
|
+
let reflectionPdf = ${ ggxReflectionAdjustedPDFFunc }( wo, wh, vec2f( alpha ) );
|
|
954
|
+
if ( wi.z > 0.0 && reflectionPdf != 0.0 ) {
|
|
955
|
+
|
|
956
|
+
let specular = ${ specularBrdfFunc }( wo, wi, wh, vec2f( alpha ) );
|
|
957
|
+
result += reflectionF * specular.x * wi.z / reflectionPdf;
|
|
958
|
+
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// refraction lobe - the btdf carries its own ( 1 - F ) fresnel weight, and
|
|
962
|
+
// samples landing back above the surface are lost
|
|
963
|
+
if ( includeRefraction ) {
|
|
964
|
+
|
|
965
|
+
let wiRefract = refract( - wo, wh, eta );
|
|
966
|
+
if ( wiRefract.z < 0.0 ) {
|
|
967
|
+
|
|
968
|
+
let transmission = ${ specularBtdfFunc }( wo, wiRefract, wh, vec2f( alpha ), eta );
|
|
969
|
+
let pdf = ${ ggxRefractionAdjustedPDFFunc }( wo, wiRefract, wh, vec2f( alpha ), eta );
|
|
970
|
+
if ( pdf != 0.0 ) {
|
|
971
|
+
|
|
972
|
+
result += transmission.x * abs( wiRefract.z ) / pdf;
|
|
973
|
+
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
result /= f32( GRID_SIZE * GRID_SIZE );
|
|
985
|
+
|
|
986
|
+
textureStore( ${ outputTarget }, vec3( globalId.xy, layer ), vec4( result ) );
|
|
987
|
+
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
`;
|