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,216 @@
|
|
|
1
|
+
import { ColorManagement, FloatType, RGBAFormat } from 'three';
|
|
2
|
+
import { RedIntegerFormat, StorageTexture, UnsignedIntType } from 'three/webgpu';
|
|
3
|
+
import { ZeroOutKernel } from './compute/ZeroOutKernel.js';
|
|
4
|
+
|
|
5
|
+
export class PathTracerBackend {
|
|
6
|
+
|
|
7
|
+
constructor( renderer ) {
|
|
8
|
+
|
|
9
|
+
this.renderer = renderer;
|
|
10
|
+
this.maxBounces = 15;
|
|
11
|
+
|
|
12
|
+
// how much work a single update dispatches, in path slots. Lower values keep frames
|
|
13
|
+
// responsive on slower hardware; the unit is resolution independent.
|
|
14
|
+
this.frameBudget = 250000;
|
|
15
|
+
this.maxTransparentBounces = 15;
|
|
16
|
+
this.lowResMode = false;
|
|
17
|
+
|
|
18
|
+
// stop taking samples once a pixel reaches this count. zero means no limit.
|
|
19
|
+
this.maxSamples = 0;
|
|
20
|
+
|
|
21
|
+
this._renderTask = null;
|
|
22
|
+
|
|
23
|
+
this.outputTarget = new StorageTexture( 1, 1, );
|
|
24
|
+
this.outputTarget.format = RGBAFormat;
|
|
25
|
+
this.outputTarget.type = FloatType;
|
|
26
|
+
this.outputTarget.colorSpace = ColorManagement.workingColorSpace;
|
|
27
|
+
this.outputTarget.name = 'Output #0';
|
|
28
|
+
this.outputTarget.generateMipmaps = false;
|
|
29
|
+
|
|
30
|
+
this.prevOutputTarget = new StorageTexture( 1, 1, );
|
|
31
|
+
this.prevOutputTarget.format = RGBAFormat;
|
|
32
|
+
this.prevOutputTarget.type = FloatType;
|
|
33
|
+
this.prevOutputTarget.colorSpace = ColorManagement.workingColorSpace;
|
|
34
|
+
this.prevOutputTarget.name = 'Output #1';
|
|
35
|
+
this.prevOutputTarget.generateMipmaps = false;
|
|
36
|
+
|
|
37
|
+
this.sampleCountTarget = new StorageTexture( 1, 1, );
|
|
38
|
+
this.sampleCountTarget.format = RedIntegerFormat;
|
|
39
|
+
this.sampleCountTarget.type = UnsignedIntType;
|
|
40
|
+
this.sampleCountTarget.name = 'Sample Count';
|
|
41
|
+
this.sampleCountTarget.generateMipmaps = false;
|
|
42
|
+
|
|
43
|
+
this.sampleCountClearKernel = new ZeroOutKernel().setWorkgroupSize( 8, 8, 1 );
|
|
44
|
+
this.outputTargetClearKernel = new ZeroOutKernel().setWorkgroupSize( 8, 8, 1 );
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setRandom( random ) {
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setBVHData( data ) {
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setTextures( textures ) {
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setMaterial( material ) {
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setFilterGlossy( value ) {
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setClamping( _direct, _indirect ) {
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setEnvironment(
|
|
73
|
+
envMap,
|
|
74
|
+
envMapIntensity,
|
|
75
|
+
envMapRotation,
|
|
76
|
+
) {
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setLights( lights ) {
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setBackground(
|
|
85
|
+
background,
|
|
86
|
+
backgroundIntensity,
|
|
87
|
+
backgroundRotation,
|
|
88
|
+
backgroundBlurriness,
|
|
89
|
+
) {
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Rebuild the render task after a shader-generating function changes.
|
|
94
|
+
rebuild() {
|
|
95
|
+
|
|
96
|
+
this._renderTask = null;
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setSize( w, h ) {
|
|
101
|
+
|
|
102
|
+
w = Math.ceil( w );
|
|
103
|
+
h = Math.ceil( h );
|
|
104
|
+
|
|
105
|
+
const { width, height } = this.outputTarget;
|
|
106
|
+
if ( width === w && height === h ) {
|
|
107
|
+
|
|
108
|
+
return false;
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.outputTarget.dispose();
|
|
113
|
+
this.prevOutputTarget.dispose();
|
|
114
|
+
this.sampleCountTarget.dispose();
|
|
115
|
+
|
|
116
|
+
this.outputTarget = this.outputTarget.clone();
|
|
117
|
+
this.prevOutputTarget = this.outputTarget.clone();
|
|
118
|
+
this.sampleCountTarget = this.sampleCountTarget.clone();
|
|
119
|
+
|
|
120
|
+
this.outputTarget.setSize( w, h );
|
|
121
|
+
this.prevOutputTarget.setSize( w, h );
|
|
122
|
+
this.sampleCountTarget.setSize( w, h );
|
|
123
|
+
|
|
124
|
+
this.reset();
|
|
125
|
+
return true;
|
|
126
|
+
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getSize( target ) {
|
|
130
|
+
|
|
131
|
+
const { width, height } = this.outputTarget;
|
|
132
|
+
target.x = width;
|
|
133
|
+
target.y = height;
|
|
134
|
+
|
|
135
|
+
return target;
|
|
136
|
+
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
update() {
|
|
140
|
+
|
|
141
|
+
const { renderer } = this;
|
|
142
|
+
if ( ! renderer.initialized ) {
|
|
143
|
+
|
|
144
|
+
return;
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if ( ! this._renderTask ) {
|
|
149
|
+
|
|
150
|
+
this._renderTask = this.createRenderTask();
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this._renderTask.next();
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
*createRenderTask() {
|
|
159
|
+
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Measures the current per pixel sample counts. Backends that need GPU work to answer this do
|
|
163
|
+
// it here, so it only happens when asked for rather than every round.
|
|
164
|
+
async getSampleCountsAsync() {
|
|
165
|
+
|
|
166
|
+
return { min: 0, max: 0, avg: 0 };
|
|
167
|
+
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
resetSeed() {}
|
|
171
|
+
|
|
172
|
+
reset() {
|
|
173
|
+
|
|
174
|
+
const {
|
|
175
|
+
renderer,
|
|
176
|
+
outputTargetClearKernel,
|
|
177
|
+
sampleCountClearKernel,
|
|
178
|
+
|
|
179
|
+
outputTarget,
|
|
180
|
+
prevOutputTarget,
|
|
181
|
+
sampleCountTarget,
|
|
182
|
+
} = this;
|
|
183
|
+
|
|
184
|
+
if ( ! renderer.initialized ) {
|
|
185
|
+
|
|
186
|
+
return;
|
|
187
|
+
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { width, height } = outputTarget;
|
|
191
|
+
const dispatchSize = outputTargetClearKernel.getDispatchSize( width, height );
|
|
192
|
+
|
|
193
|
+
outputTargetClearKernel.target = outputTarget;
|
|
194
|
+
renderer.compute( outputTargetClearKernel.kernel, dispatchSize );
|
|
195
|
+
|
|
196
|
+
outputTargetClearKernel.target = prevOutputTarget;
|
|
197
|
+
renderer.compute( outputTargetClearKernel.kernel, dispatchSize );
|
|
198
|
+
|
|
199
|
+
sampleCountClearKernel.target = sampleCountTarget;
|
|
200
|
+
renderer.compute( sampleCountClearKernel.kernel, dispatchSize );
|
|
201
|
+
|
|
202
|
+
this._renderTask = null;
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
dispose() {
|
|
207
|
+
|
|
208
|
+
this.outputTarget.dispose();
|
|
209
|
+
this.prevOutputTarget.dispose();
|
|
210
|
+
this.sampleCountTarget.dispose();
|
|
211
|
+
|
|
212
|
+
this._renderTask = null;
|
|
213
|
+
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Storage3DTexture, RGBAFormat, LinearFilter, HalfFloatType, MathUtils } from 'three/webgpu';
|
|
2
|
+
import { storageTexture3D, globalId, uniform, texture3D, sampler } from 'three/tsl';
|
|
3
|
+
import { turquinIntegralFn } from './nodes/material.wgsl.js';
|
|
4
|
+
import { ComputeKernel } from './compute/ComputeKernel.js';
|
|
5
|
+
import { wgslTagFn, proxyFn } from 'three-mesh-bvh/webgpu';
|
|
6
|
+
|
|
7
|
+
const RESOLUTION = 32;
|
|
8
|
+
const WORKGROUP_SIZE = 16;
|
|
9
|
+
|
|
10
|
+
// A 3D texture of the GGX single-scatter directional albedo E, used for Turquin multiscatter
|
|
11
|
+
// energy compensation (https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf).
|
|
12
|
+
export class TurquinTexture extends Storage3DTexture {
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
|
|
16
|
+
// layer 0: conductor
|
|
17
|
+
// layer 1 - 10: dielectric reflection over the ior range
|
|
18
|
+
// layer 11 - 20: transmissive total energy entering
|
|
19
|
+
// layer 21 - 30: transmissive total energy exiting
|
|
20
|
+
super( RESOLUTION, RESOLUTION, 31 );
|
|
21
|
+
|
|
22
|
+
// rgba16float is the one float format that is both storable and filterable everywhere
|
|
23
|
+
this.type = HalfFloatType;
|
|
24
|
+
this.format = RGBAFormat;
|
|
25
|
+
this.minFilter = LinearFilter;
|
|
26
|
+
this.magFilter = LinearFilter;
|
|
27
|
+
|
|
28
|
+
// TODO: turquin paper suggests 32 layers for dielectric
|
|
29
|
+
this.dielectricLayerCount = 10;
|
|
30
|
+
this.transmissiveLayerCount = 10;
|
|
31
|
+
this.minIoR = 1;
|
|
32
|
+
this.maxIoR = 2.5;
|
|
33
|
+
|
|
34
|
+
// construct the fns
|
|
35
|
+
this.sampleConductorFn = proxyFn( '_sampleConductorFn', this );
|
|
36
|
+
this.sampleDielectricFn = proxyFn( '_sampleDielectricFn', this );
|
|
37
|
+
this.sampleTransmissiveFn = proxyFn( '_sampleTransmissiveFn', this );
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// bakes the albedo table into the texture. Must be called once the renderer is initialized.
|
|
42
|
+
generate( renderer ) {
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
dielectricLayerCount,
|
|
46
|
+
transmissiveLayerCount,
|
|
47
|
+
minIoR,
|
|
48
|
+
maxIoR,
|
|
49
|
+
} = this;
|
|
50
|
+
|
|
51
|
+
const params = {
|
|
52
|
+
outputTarget: storageTexture3D( this ).toWriteOnly(),
|
|
53
|
+
globalId,
|
|
54
|
+
eta: uniform( 1 ),
|
|
55
|
+
layer: uniform( 0, 'uint' ),
|
|
56
|
+
includeFresnel: uniform( 0 ),
|
|
57
|
+
includeRefraction: uniform( 0 ),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const dispatch = [ RESOLUTION / WORKGROUP_SIZE, RESOLUTION / WORKGROUP_SIZE, 1 ];
|
|
61
|
+
const kernel = new ComputeKernel( turquinIntegralFn( params.outputTarget )( params ), { workgroupSize: [ WORKGROUP_SIZE, WORKGROUP_SIZE, 1 ] } );
|
|
62
|
+
|
|
63
|
+
// metallic
|
|
64
|
+
params.layer.value = 0;
|
|
65
|
+
params.eta.value = 1;
|
|
66
|
+
params.includeFresnel.value = 0;
|
|
67
|
+
params.includeRefraction.value = 0;
|
|
68
|
+
renderer.compute( kernel.kernel, dispatch );
|
|
69
|
+
|
|
70
|
+
// dielectric
|
|
71
|
+
for ( let i = 0; i < dielectricLayerCount; i ++ ) {
|
|
72
|
+
|
|
73
|
+
params.layer.value ++;
|
|
74
|
+
params.eta.value = 1 / MathUtils.mapLinear( i, 0, dielectricLayerCount - 1, minIoR, maxIoR );
|
|
75
|
+
params.includeFresnel.value = 1;
|
|
76
|
+
renderer.compute( kernel.kernel, dispatch );
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// transmissive total energy - entering and exiting layers over the same ior range
|
|
81
|
+
params.includeFresnel.value = 1;
|
|
82
|
+
params.includeRefraction.value = 1;
|
|
83
|
+
for ( let i = 0; i < transmissiveLayerCount; i ++ ) {
|
|
84
|
+
|
|
85
|
+
const ior = MathUtils.mapLinear( i, 0, transmissiveLayerCount - 1, minIoR, maxIoR );
|
|
86
|
+
|
|
87
|
+
// entering - air incident
|
|
88
|
+
params.layer.value = 1 + dielectricLayerCount + i;
|
|
89
|
+
params.eta.value = 1 / ior;
|
|
90
|
+
renderer.compute( kernel.kernel, dispatch );
|
|
91
|
+
|
|
92
|
+
// exiting - volume incident, TIR side
|
|
93
|
+
params.layer.value = 1 + dielectricLayerCount + transmissiveLayerCount + i;
|
|
94
|
+
params.eta.value = ior;
|
|
95
|
+
renderer.compute( kernel.kernel, dispatch );
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// fns
|
|
100
|
+
// function that maps the given value from range [a0, a1] to [b0, b1]
|
|
101
|
+
const mapLinearClampedFn = wgslTagFn/* wgsl */`
|
|
102
|
+
fn mapLinearClamped( v: f32, a0: f32, a1: f32, b0: f32, b1: f32 ) -> f32 {
|
|
103
|
+
|
|
104
|
+
let mapped = saturate( ( v - a0 ) / ( a1 - a0 ) );
|
|
105
|
+
return mix( b0, b1, mapped );
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
// texture nodes
|
|
111
|
+
const textureNode = texture3D( this ).setName( 'turquinTexture' );
|
|
112
|
+
const samplerNode = sampler( this ).setName( 'turquinSampler' );
|
|
113
|
+
|
|
114
|
+
// conductor fetch fn
|
|
115
|
+
const layerDepth = 1.0 / ( 1 + dielectricLayerCount + 2 * transmissiveLayerCount );
|
|
116
|
+
this._sampleConductorFn = wgslTagFn/* wgsl */`
|
|
117
|
+
fn sampleConductor( cosTheta0: f32, roughness: f32 ) -> f32 {
|
|
118
|
+
|
|
119
|
+
let layer = 0.5;
|
|
120
|
+
let uvw = vec3f( cosTheta0, roughness, layer * ${ layerDepth } );
|
|
121
|
+
return textureSampleLevel( ${ textureNode }, ${ samplerNode }, uvw, 0 ).r;
|
|
122
|
+
|
|
123
|
+
}
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
// dielectric fetch fn - ior is clamped at the valid range
|
|
127
|
+
this._sampleDielectricFn = wgslTagFn/* wgsl */`
|
|
128
|
+
fn sampleDielectric( cosTheta0: f32, roughness: f32, ior: f32 ) -> f32 {
|
|
129
|
+
|
|
130
|
+
let blockStart = 1.0;
|
|
131
|
+
let layer = ${ mapLinearClampedFn }(
|
|
132
|
+
ior,
|
|
133
|
+
f32( ${ minIoR } ), f32( ${ maxIoR } ),
|
|
134
|
+
0.5, f32( ${ dielectricLayerCount } ) - 0.5,
|
|
135
|
+
) + blockStart;
|
|
136
|
+
|
|
137
|
+
let uvw = vec3f( cosTheta0, roughness, layer * ${ layerDepth } );
|
|
138
|
+
return textureSampleLevel( ${ textureNode }, ${ samplerNode }, uvw, 0 ).r;
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
// transmissive fetch fn - entering selects the air-incident block, otherwise the
|
|
144
|
+
// volume-incident (TIR) block. ior is clamped at the valid range
|
|
145
|
+
const enterBlockStart = 1 + dielectricLayerCount;
|
|
146
|
+
const exitBlockStart = 1 + dielectricLayerCount + transmissiveLayerCount;
|
|
147
|
+
this._sampleTransmissiveFn = wgslTagFn/* wgsl */`
|
|
148
|
+
fn sampleTransmissive( cosTheta0: f32, roughness: f32, ior: f32, entering: bool ) -> f32 {
|
|
149
|
+
|
|
150
|
+
let blockStart = select(
|
|
151
|
+
f32( ${ exitBlockStart } ),
|
|
152
|
+
f32( ${ enterBlockStart } ),
|
|
153
|
+
entering
|
|
154
|
+
);
|
|
155
|
+
let layer = ${ mapLinearClampedFn }(
|
|
156
|
+
ior,
|
|
157
|
+
f32( ${ minIoR } ), f32( ${ maxIoR } ),
|
|
158
|
+
0.5, f32( ${ transmissiveLayerCount } ) - 0.5,
|
|
159
|
+
) + blockStart;
|
|
160
|
+
|
|
161
|
+
let uvw = vec3f( cosTheta0, roughness, layer * ${ layerDepth } );
|
|
162
|
+
return textureSampleLevel( ${ textureNode }, ${ samplerNode }, uvw, 0 ).r;
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
}
|