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,92 @@
|
|
|
1
|
+
export class ComputeKernel {
|
|
2
|
+
|
|
3
|
+
get computeNode() {
|
|
4
|
+
|
|
5
|
+
return this.kernel.computeNode;
|
|
6
|
+
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get workgroupSize() {
|
|
10
|
+
|
|
11
|
+
return this.kernel.workgroupSize;
|
|
12
|
+
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
set needsUpdate( v ) {
|
|
16
|
+
|
|
17
|
+
// TODO: hack to force the kernel to rebuild since "needsUpdate" is not respected
|
|
18
|
+
this.setWorkgroupSize( ...this.workgroupSize );
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor( fn, options = {} ) {
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
workgroupSize = [ 64 ],
|
|
26
|
+
context = {},
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
// this.workgroupSize = [ ...workgroupSize ];
|
|
30
|
+
this._fn = fn;
|
|
31
|
+
this.kernel = null;
|
|
32
|
+
this.context = context;
|
|
33
|
+
|
|
34
|
+
this.setWorkgroupSize( ...workgroupSize );
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
defineUniformAccessors( parameters ) {
|
|
39
|
+
|
|
40
|
+
for ( const key in parameters ) {
|
|
41
|
+
|
|
42
|
+
if ( key in this ) {
|
|
43
|
+
|
|
44
|
+
throw new Error( `ComputeNode: Uniform name ${ key } is already defined.` );
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const node = parameters[ key ];
|
|
49
|
+
if ( 'value' in node ) {
|
|
50
|
+
|
|
51
|
+
Object.defineProperty( this, key, {
|
|
52
|
+
get() {
|
|
53
|
+
|
|
54
|
+
return parameters[ key ].value;
|
|
55
|
+
|
|
56
|
+
},
|
|
57
|
+
set( v ) {
|
|
58
|
+
|
|
59
|
+
parameters[ key ].value = v;
|
|
60
|
+
|
|
61
|
+
},
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setWorkgroupSize( x = 64, y = 1, z = 1 ) {
|
|
71
|
+
|
|
72
|
+
const node = this._fn.context( this.context );
|
|
73
|
+
|
|
74
|
+
// forward the call parameters so they remain accessible via "computeNode.parameters"
|
|
75
|
+
node.parameters = this._fn.parameters;
|
|
76
|
+
this.kernel = node.computeKernel( [ x, y, z ] );
|
|
77
|
+
return this;
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getDispatchSize( tx = 1, ty = 1, tz = 1, target = [] ) {
|
|
82
|
+
|
|
83
|
+
const [ wgx, wgy, wgz ] = this.workgroupSize;
|
|
84
|
+
target.length = 3;
|
|
85
|
+
target[ 0 ] = Math.ceil( tx / wgx );
|
|
86
|
+
target[ 1 ] = Math.ceil( ty / wgy );
|
|
87
|
+
target[ 2 ] = Math.ceil( tz / wgz );
|
|
88
|
+
return target;
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { StorageBufferAttribute } from 'three/webgpu';
|
|
2
|
+
import { uniform, storage, globalId } from 'three/tsl';
|
|
3
|
+
import { ComputeKernel } from './ComputeKernel.js';
|
|
4
|
+
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
|
+
|
|
6
|
+
// Copies the first "count" elements of one storage buffer into another of the same element type,
|
|
7
|
+
// for carrying live state across a buffer reallocation.
|
|
8
|
+
export class CopyBufferKernel extends ComputeKernel {
|
|
9
|
+
|
|
10
|
+
constructor( elementType ) {
|
|
11
|
+
|
|
12
|
+
const params = {
|
|
13
|
+
source: storage( new StorageBufferAttribute( 1, 1 ), elementType ).toReadOnly(),
|
|
14
|
+
target: storage( new StorageBufferAttribute( 1, 1 ), elementType ),
|
|
15
|
+
count: uniform( 0, 'uint' ),
|
|
16
|
+
globalId: globalId,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const fn = wgslTagFn/* wgsl */`
|
|
20
|
+
fn compute( count: u32, globalId: vec3u ) -> void {
|
|
21
|
+
|
|
22
|
+
let index = globalId.x;
|
|
23
|
+
if ( index >= count ) {
|
|
24
|
+
|
|
25
|
+
return;
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
${ params.target }[ index ] = ${ params.source }[ index ];
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
super( fn( params ) );
|
|
35
|
+
|
|
36
|
+
this.defineUniformAccessors( params );
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { DataTexture, Vector2, StorageTexture } from 'three/webgpu';
|
|
2
|
+
import { ComputeKernel } from './ComputeKernel.js';
|
|
3
|
+
import { texture, sampler, uniform, globalId, textureStore } from 'three/tsl';
|
|
4
|
+
import { rngInit, rngNextBounce, rand1, rand2, rand3, RNG_INDEX_RAY_JITTER, RNG_INDEX_BACKGROUND_SAMPLE, RNG_INDEX_DIRECT_LIGHT_SAMPLE, RNG_INDEX_RUSSIAN_ROULETTE, RNG_INDEX_DISPERSION_WAVELENGTH, RNG_INDEX_ALPHA_TEST } from '../nodes/random.wgsl.js';
|
|
5
|
+
import { misHeuristicFn, weightedAlphaBlendFn } from '../nodes/sampling.wgsl.js';
|
|
6
|
+
import { proxy, proxyFn, wgslTagFn, rayStruct } from 'three-mesh-bvh/webgpu';
|
|
7
|
+
import { clampPathContributionFunc, isTerminatingScatterFunc, offsetRayOriginFunc } from '../nodes/utils.wgsl.js';
|
|
8
|
+
import { lightRecordStruct } from '../nodes/structs.wgsl.js';
|
|
9
|
+
import { ENVIRONMENT_LIGHT_TYPE, LIGHT_FAR_DISTANCE, LIGHT_EPSILON, isMISWeightLightFn } from '../nodes/lights.wgsl.js';
|
|
10
|
+
import { applyDispersionFunc, dispersionColorWeightFunc, DISPERSION_MIN_WAVELENGTH, DISPERSION_MAX_WAVELENGTH, transmissionAttenuationFunc } from '../nodes/material.wgsl.js';
|
|
11
|
+
import { TRANSMISSIVE_BACKGROUND_ENVIRONMENT, TRANSMISSIVE_BACKGROUND_OVERLAY, TRANSMISSIVE_BACKGROUND_TRANSPARENT } from '../constants.js';
|
|
12
|
+
|
|
13
|
+
export class PathTracerMegaKernel extends ComputeKernel {
|
|
14
|
+
|
|
15
|
+
constructor( ) {
|
|
16
|
+
|
|
17
|
+
const params = {
|
|
18
|
+
bvhData: { value: null },
|
|
19
|
+
material: { value: null },
|
|
20
|
+
envInfo: { value: null },
|
|
21
|
+
lightsInfo: { value: null },
|
|
22
|
+
|
|
23
|
+
// targets
|
|
24
|
+
prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(),
|
|
25
|
+
outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toWriteOnly(),
|
|
26
|
+
sampleCountTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadWrite(),
|
|
27
|
+
|
|
28
|
+
// tiles
|
|
29
|
+
offset: uniform( new Vector2() ),
|
|
30
|
+
tileSize: uniform( new Vector2() ),
|
|
31
|
+
|
|
32
|
+
// settings
|
|
33
|
+
seed: uniform( 0 ),
|
|
34
|
+
maxBounces: uniform( 5 ),
|
|
35
|
+
maxTransparentBounces: uniform( 5, 'uint' ),
|
|
36
|
+
misEnabled: uniform( 1, 'uint' ),
|
|
37
|
+
maxSamples: uniform( 0, 'uint' ),
|
|
38
|
+
filterGlossy: uniform( 1 ),
|
|
39
|
+
clampDirect: uniform( 0 ),
|
|
40
|
+
clampIndirect: uniform( 10 ),
|
|
41
|
+
|
|
42
|
+
backgroundInfo: { value: null },
|
|
43
|
+
|
|
44
|
+
transmissiveBackground: uniform( TRANSMISSIVE_BACKGROUND_OVERLAY ),
|
|
45
|
+
|
|
46
|
+
textures: texture( new DataTexture() ),
|
|
47
|
+
textureSampler: sampler( new DataTexture() ),
|
|
48
|
+
|
|
49
|
+
// compute variables
|
|
50
|
+
globalId: globalId,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const raycastOutput = proxy( 'bvhData.value.fns.raycastFirstHit.outputType', params );
|
|
54
|
+
const raycastFirstHitFn = proxyFn( 'bvhData.value.fns.raycastFirstHit', params );
|
|
55
|
+
const sampleTrianglePointFn = proxyFn( 'bvhData.value.fns.sampleTrianglePoint', params );
|
|
56
|
+
const getSurfaceRecordFn = proxyFn( 'bvhData.value.fns.getSurfaceRecord', params );
|
|
57
|
+
const getCameraRayFn = proxyFn( 'bvhData.value.fns.getCameraRay', params );
|
|
58
|
+
const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params );
|
|
59
|
+
const bsdfEvalPdfFn = proxyFn( 'material.value.bsdfEvalPdf', params );
|
|
60
|
+
|
|
61
|
+
// environment resources
|
|
62
|
+
const envTotalSumNode = proxy( 'envInfo.value.totalSumNode', params );
|
|
63
|
+
const envIntensityNode = proxy( 'envInfo.value.intensityNode', params );
|
|
64
|
+
const sampleEnvColor = proxy( 'envInfo.value.sampleColor', params );
|
|
65
|
+
const sampleEnvDir = proxy( 'envInfo.value.sampleDir', params );
|
|
66
|
+
const getEnvDirPdf = proxy( 'envInfo.value.getDirPdf', params );
|
|
67
|
+
const sampleBackground = proxy( 'backgroundInfo.value.sampleColor', params );
|
|
68
|
+
|
|
69
|
+
// analytic scene lights pulled off the lightsInfo provider ( LightsInfoNode )
|
|
70
|
+
const lightsCountNode = proxy( 'lightsInfo.value.countNode', params );
|
|
71
|
+
const randomLightSampleFn = proxyFn( 'lightsInfo.value.randomLightSample', params );
|
|
72
|
+
const intersectLightAtIndexFn = proxyFn( 'lightsInfo.value.intersectLightAtIndex', params );
|
|
73
|
+
|
|
74
|
+
const shader = wgslTagFn/* wgsl */`
|
|
75
|
+
|
|
76
|
+
fn compute(
|
|
77
|
+
|
|
78
|
+
// indices and target
|
|
79
|
+
globalId: vec3u,
|
|
80
|
+
|
|
81
|
+
// tiles
|
|
82
|
+
offset: vec2u,
|
|
83
|
+
tileSize: vec2u,
|
|
84
|
+
|
|
85
|
+
// settings
|
|
86
|
+
seed: u32,
|
|
87
|
+
maxBounces: u32,
|
|
88
|
+
maxTransparentBounces: u32,
|
|
89
|
+
misEnabled: u32,
|
|
90
|
+
maxSamples: u32,
|
|
91
|
+
filterGlossy: f32,
|
|
92
|
+
clampDirect: f32,
|
|
93
|
+
clampIndirect: f32,
|
|
94
|
+
|
|
95
|
+
transmissiveBackground: u32,
|
|
96
|
+
|
|
97
|
+
) -> void {
|
|
98
|
+
|
|
99
|
+
let transforms = &${ proxy( 'bvhData.value.storage.transforms', params ) };
|
|
100
|
+
let materials = &${ proxy( 'bvhData.value.storage.materials', params ) };
|
|
101
|
+
|
|
102
|
+
// make sure we don't bleed over the edge of our tile
|
|
103
|
+
if ( globalId.x >= tileSize.x || globalId.y >= tileSize.y ) {
|
|
104
|
+
|
|
105
|
+
return;
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// to screen coordinates
|
|
110
|
+
let indexUV = offset + globalId.xy;
|
|
111
|
+
let targetDimensions = textureDimensions( ${ params.outputTarget } );
|
|
112
|
+
if ( indexUV.x >= targetDimensions.x || indexUV.y >= targetDimensions.y ) {
|
|
113
|
+
|
|
114
|
+
return;
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// skip the pixel once it has hit the sample limit
|
|
119
|
+
let sampleCount = textureLoad( ${ params.sampleCountTarget }, indexUV ).r;
|
|
120
|
+
if ( maxSamples != 0u && sampleCount >= maxSamples ) {
|
|
121
|
+
|
|
122
|
+
return;
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let uv = vec2f( indexUV ) / vec2f( targetDimensions );
|
|
127
|
+
${ rngInit }( indexUV.xy, seed, 0 );
|
|
128
|
+
|
|
129
|
+
// scene ray
|
|
130
|
+
let jitteredUv = uv + ${ rand2 }( ${ RNG_INDEX_RAY_JITTER } ) / vec2f( targetDimensions );
|
|
131
|
+
var ray: ${ rayStruct };
|
|
132
|
+
if ( ! ${ getCameraRayFn }( jitteredUv, vec2f( targetDimensions ), &ray ) ) {
|
|
133
|
+
|
|
134
|
+
return;
|
|
135
|
+
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ray.direction = normalize( ray.direction );
|
|
139
|
+
|
|
140
|
+
var resultColor = vec4f( 0, 0, 0, 1 );
|
|
141
|
+
var throughputColor = vec3f( 1.0 );
|
|
142
|
+
var bsdfPdf = 0.0;
|
|
143
|
+
var isFullyTransmissive = true;
|
|
144
|
+
var minPdf = 1.0;
|
|
145
|
+
var transparentBounce = 0u;
|
|
146
|
+
// A negative value marks a sampled wavelength whose RGB reconstruction weight has not been applied yet.
|
|
147
|
+
var dispersionWavelength = - mix( ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0, ${ rand1 }( ${ RNG_INDEX_DISPERSION_WAVELENGTH } ) );
|
|
148
|
+
|
|
149
|
+
// one-sample NEE selects between the analytic lights and the environment -
|
|
150
|
+
// lightsDenom is the number of options
|
|
151
|
+
let envActive = ${ envTotalSumNode } > 0.0 && ${ envIntensityNode } > 0.0;
|
|
152
|
+
let lightsCount = ${ lightsCountNode };
|
|
153
|
+
var lightsDenom = f32( lightsCount );
|
|
154
|
+
if ( envActive ) {
|
|
155
|
+
|
|
156
|
+
lightsDenom += 1.0;
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for ( var bounce = 0u; bounce < maxBounces; bounce ++ ) {
|
|
161
|
+
|
|
162
|
+
var hitResult: ${ raycastOutput };
|
|
163
|
+
let didHit = ${ raycastFirstHitFn }( ray, &hitResult );
|
|
164
|
+
let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
|
|
165
|
+
|
|
166
|
+
// forward hits: a bsdf-sampled ray that lands on a area light. MIS-weighted
|
|
167
|
+
// only when NEE is also sampling the lights
|
|
168
|
+
if ( bounce > 0u ) {
|
|
169
|
+
|
|
170
|
+
for ( var li = 0u; li < lightsCount; li ++ ) {
|
|
171
|
+
|
|
172
|
+
var lightRec: ${ lightRecordStruct };
|
|
173
|
+
if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
|
|
174
|
+
|
|
175
|
+
var misWeight = 1.0;
|
|
176
|
+
if ( misEnabled != 0u ) {
|
|
177
|
+
|
|
178
|
+
let lightPdf = lightRec.pdf / lightsDenom;
|
|
179
|
+
misWeight = ${ misHeuristicFn }( bsdfPdf, lightPdf );
|
|
180
|
+
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let lightHit = ${ clampPathContributionFunc }( lightRec.emission * throughputColor * misWeight, bounce, clampDirect, clampIndirect );
|
|
184
|
+
resultColor += vec4f( lightHit, 0.0 );
|
|
185
|
+
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if ( didHit ) {
|
|
193
|
+
|
|
194
|
+
let objectInfo = transforms[ hitResult.objectIndex ];
|
|
195
|
+
var materialInfo = ( *materials )[ objectInfo.materialIndex ];
|
|
196
|
+
|
|
197
|
+
// a matte surface hit by the camera ray renders as a fully transparent
|
|
198
|
+
if ( materialInfo.matte != 0 && bounce == 0u ) {
|
|
199
|
+
|
|
200
|
+
resultColor = vec4f( 0.0 );
|
|
201
|
+
break;
|
|
202
|
+
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// apply per-object colors
|
|
206
|
+
materialInfo.color *= objectInfo.color.rgb;
|
|
207
|
+
materialInfo.opacity *= objectInfo.color.a;
|
|
208
|
+
|
|
209
|
+
let view = - ray.direction;
|
|
210
|
+
var vertexData = ${ sampleTrianglePointFn }( hitResult.barycoord, hitResult.indices.xyz );
|
|
211
|
+
vertexData.normal = normalize( transpose( objectInfo.inverseMatrixWorld ) * vertexData.normal );
|
|
212
|
+
vertexData.tangent = vec4f( ( objectInfo.matrixWorld * vec4f( vertexData.tangent.xyz, 0.0 ) ).xyz, vertexData.tangent.w );
|
|
213
|
+
vertexData.position = objectInfo.matrixWorld * vertexData.position;
|
|
214
|
+
|
|
215
|
+
// blur glossy surfaces after low-probability bounces to suppress fireflies,
|
|
216
|
+
// from the Cycles "filter glossy" approach in integrator/surface_shader.h
|
|
217
|
+
// The smallest pdf seen along the path for the glossy filter is tracked below
|
|
218
|
+
let blurRoughness = sqrt( clamp( 1.0 - filterGlossy * minPdf, 0.0, 1.0 ) ) * 0.5;
|
|
219
|
+
|
|
220
|
+
var surface = ${ getSurfaceRecordFn }( materialInfo, vertexData, hitResult.side, hitResult.normal, view, blurRoughness );
|
|
221
|
+
|
|
222
|
+
// Stochastically pass through partially transparent surfaces by restarting
|
|
223
|
+
// the ray at the hit point, advancing the rng but not the bounce count.
|
|
224
|
+
if ( ${ rand1 }( ${ RNG_INDEX_ALPHA_TEST } ) > surface.opacity ) {
|
|
225
|
+
|
|
226
|
+
// stop once the transparent bounces run out
|
|
227
|
+
if ( transparentBounce >= maxTransparentBounces ) {
|
|
228
|
+
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
ray.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, ray.direction, hitResult.normal );
|
|
234
|
+
${ rngNextBounce }();
|
|
235
|
+
transparentBounce ++;
|
|
236
|
+
bounce --;
|
|
237
|
+
continue;
|
|
238
|
+
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let isDispersive = materialInfo.dispersion > 0.0 && surface.ior > 1.0 && surface.transmission > 0.0 && ! surface.thinWall;
|
|
242
|
+
if ( isDispersive ) {
|
|
243
|
+
|
|
244
|
+
let wavelength = abs( dispersionWavelength );
|
|
245
|
+
${ applyDispersionFunc }( &surface, materialInfo.dispersion, wavelength );
|
|
246
|
+
if ( dispersionWavelength < 0.0 ) {
|
|
247
|
+
|
|
248
|
+
dispersionWavelength = wavelength;
|
|
249
|
+
throughputColor *= ${ dispersionColorWeightFunc }( wavelength );
|
|
250
|
+
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// attenuate the light transmitted through the volume when exiting a backface
|
|
256
|
+
if ( hitResult.side < 0.0 && materialInfo.transmission > 0.0 ) {
|
|
257
|
+
|
|
258
|
+
throughputColor *= ${ transmissionAttenuationFunc }( hitResult.dist, materialInfo.attenuationColor, materialInfo.attenuationDistance );
|
|
259
|
+
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// emission, clamped at the depth of the segment that found it, as in Cycles
|
|
263
|
+
let emission = ${ clampPathContributionFunc }( throughputColor * surface.emission, bounce, clampDirect, clampIndirect );
|
|
264
|
+
resultColor += vec4f( emission, 0.0 );
|
|
265
|
+
|
|
266
|
+
// next event estimation: draw one light or the environment, each with
|
|
267
|
+
// probability 1 / lightsDenom
|
|
268
|
+
if ( misEnabled != 0u && lightsDenom > 0.0 ) {
|
|
269
|
+
|
|
270
|
+
// pick one light or the environment with a single sample
|
|
271
|
+
// TODO: importance-sample the selection by light intensity and solid angle
|
|
272
|
+
let ruv = ${ rand3 }( ${ RNG_INDEX_DIRECT_LIGHT_SAMPLE } );
|
|
273
|
+
let lightIndex = min( u32( ruv.x * lightsDenom ), u32( lightsDenom ) - 1u );
|
|
274
|
+
var lightRec: ${ lightRecordStruct };
|
|
275
|
+
if ( envActive && lightIndex == lightsCount ) {
|
|
276
|
+
|
|
277
|
+
// the environment, sampled from its CDF, as a light of kind ENVIRONMENT
|
|
278
|
+
let envSample = ${ sampleEnvDir }( ruv.yz );
|
|
279
|
+
lightRec.direction = envSample.direction;
|
|
280
|
+
lightRec.emission = envSample.color;
|
|
281
|
+
lightRec.pdf = envSample.pdf;
|
|
282
|
+
lightRec.dist = ${ LIGHT_FAR_DISTANCE };
|
|
283
|
+
lightRec.lightType = ${ ENVIRONMENT_LIGHT_TYPE };
|
|
284
|
+
|
|
285
|
+
} else {
|
|
286
|
+
|
|
287
|
+
lightRec = ${ randomLightSampleFn }( lightIndex, vertexData.position.xyz, ruv.yz );
|
|
288
|
+
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if ( lightRec.pdf > 0.0 ) {
|
|
292
|
+
|
|
293
|
+
let evalRec = ${ bsdfEvalPdfFn }( view, lightRec.direction, surface );
|
|
294
|
+
if ( evalRec.pdf > 0.0 ) {
|
|
295
|
+
|
|
296
|
+
var shadowRay: ${ rayStruct };
|
|
297
|
+
shadowRay.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, lightRec.direction, hitResult.normal );
|
|
298
|
+
shadowRay.direction = lightRec.direction;
|
|
299
|
+
shadowRay.maxDist = lightRec.dist - ${ LIGHT_EPSILON };
|
|
300
|
+
|
|
301
|
+
// opaque occlusion up to the light distance. A shadow-specific any hit
|
|
302
|
+
// traversal could support tinted shadows from transmissive and partially
|
|
303
|
+
// opaque objects
|
|
304
|
+
var shadowHit: ${ raycastOutput };
|
|
305
|
+
let occluded = ${ raycastFirstHitFn }( shadowRay, &shadowHit );
|
|
306
|
+
if ( ! occluded ) {
|
|
307
|
+
|
|
308
|
+
var lightPdf = lightRec.pdf;
|
|
309
|
+
lightPdf /= lightsDenom;
|
|
310
|
+
|
|
311
|
+
// env + area lights are also bsdf-sampled, so MIS-weight them - punctual lights take full weight
|
|
312
|
+
let misWeight = select( 1.0, ${ misHeuristicFn }( lightPdf, evalRec.pdf ), ${ isMISWeightLightFn }( lightRec.lightType ) );
|
|
313
|
+
let directLight = throughputColor * lightRec.emission * evalRec.color * misWeight / lightPdf;
|
|
314
|
+
let contribution = ${ clampPathContributionFunc }( directLight, bounce + 1u, clampDirect, clampIndirect );
|
|
315
|
+
resultColor += vec4f( contribution, 0.0 );
|
|
316
|
+
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let scatterRec = ${ bsdfSampleFn }( view, surface );
|
|
326
|
+
if ( ${ isTerminatingScatterFunc }( scatterRec ) ) {
|
|
327
|
+
|
|
328
|
+
break;
|
|
329
|
+
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
isFullyTransmissive = isFullyTransmissive && scatterRec.isTransmissive;
|
|
333
|
+
|
|
334
|
+
// track the smallest pdf seen along the path for the glossy filter
|
|
335
|
+
minPdf = min( minPdf, scatterRec.pdf );
|
|
336
|
+
|
|
337
|
+
// russian roulette early out:
|
|
338
|
+
// Matches Cycles path_state_continuation_probability in integrator/path_state.h
|
|
339
|
+
if ( bounce >= 3u ) {
|
|
340
|
+
|
|
341
|
+
let rrThroughput = throughputColor * scatterRec.color / scatterRec.pdf;
|
|
342
|
+
let rrProb = saturate( sqrt( max( max( rrThroughput.r, rrThroughput.g ), rrThroughput.b ) ) );
|
|
343
|
+
if ( rrProb <= 0.0 || ${ rand1 }( ${ RNG_INDEX_RUSSIAN_ROULETTE } ) > rrProb ) {
|
|
344
|
+
|
|
345
|
+
break;
|
|
346
|
+
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
throughputColor /= rrProb;
|
|
350
|
+
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
throughputColor *= scatterRec.color;
|
|
354
|
+
throughputColor /= scatterRec.pdf;
|
|
355
|
+
bsdfPdf = scatterRec.pdf;
|
|
356
|
+
|
|
357
|
+
// exit if our throughput is 0.0
|
|
358
|
+
if ( all( throughputColor == vec3f( 0.0 ) ) ) {
|
|
359
|
+
|
|
360
|
+
break;
|
|
361
|
+
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
ray.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, scatterRec.direction, hitResult.normal );
|
|
365
|
+
ray.direction = scatterRec.direction;
|
|
366
|
+
ray.maxDist = 0.0;
|
|
367
|
+
|
|
368
|
+
} else {
|
|
369
|
+
|
|
370
|
+
if ( bounce > 0u && ! isFullyTransmissive ) {
|
|
371
|
+
|
|
372
|
+
var misWeight = 1.0;
|
|
373
|
+
if ( misEnabled != 0u && envActive ) {
|
|
374
|
+
|
|
375
|
+
// match the env pdf scaling used by the NEE selection so the two estimators balance
|
|
376
|
+
let envPdf = ${ getEnvDirPdf }( ray.direction ) / lightsDenom;
|
|
377
|
+
misWeight = ${ misHeuristicFn }( bsdfPdf, envPdf );
|
|
378
|
+
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let environment = ${ sampleEnvColor }( ray.direction ).rgb * throughputColor * misWeight;
|
|
382
|
+
let contribution = ${ clampPathContributionFunc }( environment, bounce, clampDirect, clampIndirect );
|
|
383
|
+
resultColor += vec4f( contribution, 0.0 );
|
|
384
|
+
|
|
385
|
+
} else {
|
|
386
|
+
|
|
387
|
+
// hit the background
|
|
388
|
+
// support multiple transparent background blending techniques
|
|
389
|
+
let rng = ${ rand2 }( ${ RNG_INDEX_BACKGROUND_SAMPLE } );
|
|
390
|
+
let bg = ${ sampleBackground }( ray.direction, rng );
|
|
391
|
+
if ( bounce == 0u ) {
|
|
392
|
+
|
|
393
|
+
// sample the background directly if this is the primary ray
|
|
394
|
+
let background = ${ clampPathContributionFunc }( bg.a * bg.rgb, bounce, clampDirect, clampIndirect );
|
|
395
|
+
resultColor = vec4f( background, bg.a );
|
|
396
|
+
|
|
397
|
+
} else {
|
|
398
|
+
|
|
399
|
+
// transmissive ray handling
|
|
400
|
+
let env = ${ sampleEnvColor }( ray.direction );
|
|
401
|
+
let avg = saturate( dot( throughputColor, vec3f( 1.0 / 3.0 ) ) );
|
|
402
|
+
let transparency = ( 1.0 - bg.a ) * avg;
|
|
403
|
+
var envMisWeight = 1.0;
|
|
404
|
+
if ( misEnabled != 0u && ${ envTotalSumNode } > 0.0 ) {
|
|
405
|
+
|
|
406
|
+
let envPdf = ${ getEnvDirPdf }( ray.direction );
|
|
407
|
+
envMisWeight = ${ misHeuristicFn }( bsdfPdf, envPdf );
|
|
408
|
+
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if ( transmissiveBackground == ${ TRANSMISSIVE_BACKGROUND_ENVIRONMENT }u ) {
|
|
412
|
+
|
|
413
|
+
// display the env map through transmissive surfaces
|
|
414
|
+
let background = ${ clampPathContributionFunc }( env.rgb * throughputColor * envMisWeight, bounce, clampDirect, clampIndirect );
|
|
415
|
+
resultColor = vec4f(
|
|
416
|
+
resultColor.rgb + background,
|
|
417
|
+
1.0,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
} else if ( transmissiveBackground == ${ TRANSMISSIVE_BACKGROUND_TRANSPARENT }u ) {
|
|
421
|
+
|
|
422
|
+
// fade the background by the throughput color average
|
|
423
|
+
let background = ${ clampPathContributionFunc }( bg.a * bg.rgb * throughputColor * envMisWeight, bounce, clampDirect, clampIndirect );
|
|
424
|
+
resultColor = vec4f(
|
|
425
|
+
resultColor.rgb + background,
|
|
426
|
+
1.0 - transparency,
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
} else {
|
|
430
|
+
|
|
431
|
+
// fade the background by the throughput color average, mixing in env lighting
|
|
432
|
+
var light = mix( env.rgb, bg.rgb, bg.a ) * envMisWeight;
|
|
433
|
+
let background = ${ clampPathContributionFunc }( light * throughputColor, bounce, clampDirect, clampIndirect );
|
|
434
|
+
resultColor = vec4f(
|
|
435
|
+
resultColor.rgb + background,
|
|
436
|
+
1.0 - transparency,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
break;
|
|
446
|
+
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
${ rngNextBounce }();
|
|
450
|
+
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
let nextSampleCount = sampleCount + 1;
|
|
454
|
+
// store the color rows top down to match a rasterized render target
|
|
455
|
+
let colorIndex = vec2u( indexUV.x, targetDimensions.y - 1u - indexUV.y );
|
|
456
|
+
|
|
457
|
+
let prevColor = textureLoad( ${ params.prevOutputTarget }, colorIndex );
|
|
458
|
+
let blendedColor = ${ weightedAlphaBlendFn }( prevColor, resultColor, 1.0 / f32( nextSampleCount ) );
|
|
459
|
+
textureStore( ${ params.sampleCountTarget }, indexUV, vec4( nextSampleCount ) );
|
|
460
|
+
textureStore( ${ params.outputTarget }, colorIndex, blendedColor );
|
|
461
|
+
|
|
462
|
+
}`;
|
|
463
|
+
|
|
464
|
+
super( shader( params ) );
|
|
465
|
+
|
|
466
|
+
this.defineUniformAccessors( params );
|
|
467
|
+
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { StorageTexture } from 'three/webgpu';
|
|
2
|
+
import { ComputeKernel } from './ComputeKernel.js';
|
|
3
|
+
import { textureStore, wgslFn, globalId, uniform } from 'three/tsl';
|
|
4
|
+
import { SAMPLE_ACTIVE_FLAG, SAMPLE_COUNT_MASK } from '../constants.js';
|
|
5
|
+
|
|
6
|
+
// Kernel for copying count + active flag to an output target for debug visualizations
|
|
7
|
+
export class SampleDebugKernel extends ComputeKernel {
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
|
|
11
|
+
const params = {
|
|
12
|
+
globalId: globalId,
|
|
13
|
+
displaySamples: uniform( true ),
|
|
14
|
+
inputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(),
|
|
15
|
+
outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadWrite(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const fn = wgslFn( /* wgsl */`
|
|
19
|
+
|
|
20
|
+
fn compute(
|
|
21
|
+
globalId: vec3u,
|
|
22
|
+
inputTarget: texture_storage_2d<r32uint, read>,
|
|
23
|
+
outputTarget: texture_storage_2d<rgba8unorm, read_write>,
|
|
24
|
+
displaySamples: u32
|
|
25
|
+
) -> void {
|
|
26
|
+
|
|
27
|
+
let combined = textureLoad( inputTarget, globalId.xy ).r;
|
|
28
|
+
let isActive = ( ${ SAMPLE_ACTIVE_FLAG }u & combined ) != 0u;
|
|
29
|
+
let samples = combined & ${ SAMPLE_COUNT_MASK }u;
|
|
30
|
+
|
|
31
|
+
if ( displaySamples != 0 ) {
|
|
32
|
+
|
|
33
|
+
textureStore( outputTarget, globalId.xy, vec4f( f32( samples ) * 0.01, 0, 0, 1.0 ) );
|
|
34
|
+
|
|
35
|
+
} else {
|
|
36
|
+
|
|
37
|
+
let v = f32( isActive );
|
|
38
|
+
textureStore( outputTarget, globalId.xy, vec4f( v, v, v, 1.0 ) );
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
` )( params );
|
|
44
|
+
|
|
45
|
+
super( fn );
|
|
46
|
+
|
|
47
|
+
this.defineUniformAccessors( params );
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
}
|