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,151 @@
|
|
|
1
|
+
import { Matrix3 } from 'three/webgpu';
|
|
2
|
+
import { texture, sampler, uniform, wgslFn } from 'three/tsl';
|
|
3
|
+
import { EquirectHdrInfoUniform } from '../uniforms/EquirectHdrInfoUniform.js';
|
|
4
|
+
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
|
+
import { constants, environmentSampleStruct } from './nodes/structs.wgsl.js';
|
|
6
|
+
import { equirectDirectionToUvFn, equirectUvToDirectionFn, luminanceFn } from './nodes/sampling.wgsl.js';
|
|
7
|
+
|
|
8
|
+
// totalWeight includes sin( theta ), which cancels in the solid-angle conversion.
|
|
9
|
+
const equirectLuminancePdfFn = wgslFn( /* wgsl */ `
|
|
10
|
+
|
|
11
|
+
fn equirectLuminancePdf( luminance: f32, totalWeight: f32, resolution: vec2u ) -> f32 {
|
|
12
|
+
|
|
13
|
+
return f32( resolution.x * resolution.y ) * luminance / ( 2.0 * PI * PI * totalWeight );
|
|
14
|
+
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
`, [ constants ] );
|
|
18
|
+
|
|
19
|
+
export class EquirectHdrInfoNode extends EquirectHdrInfoUniform {
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
|
|
23
|
+
super();
|
|
24
|
+
|
|
25
|
+
// environment map + importance-sampling CDF textures, each with a sampler
|
|
26
|
+
this.mapNode = texture( this.map );
|
|
27
|
+
this.mapSampler = sampler( this.mapNode );
|
|
28
|
+
this.marginalNode = texture( this.marginalWeights );
|
|
29
|
+
this.marginalSampler = sampler( this.marginalNode );
|
|
30
|
+
this.conditionalNode = texture( this.conditionalWeights );
|
|
31
|
+
this.conditionalSampler = sampler( this.conditionalNode );
|
|
32
|
+
|
|
33
|
+
// scalar parameters that assemble into the EnvironmentInfo struct in the shader
|
|
34
|
+
this.rotationNode = uniform( new Matrix3() );
|
|
35
|
+
this.intensityNode = uniform( 1 );
|
|
36
|
+
this.totalSumNode = uniform( this.totalSum );
|
|
37
|
+
|
|
38
|
+
this._initFns();
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getPixelWeight( r, g, b, row, height ) {
|
|
43
|
+
|
|
44
|
+
// weight the pixel contribution by its spherical solid angle.
|
|
45
|
+
const theta = Math.PI * ( row + 0.5 ) / height;
|
|
46
|
+
return super.getPixelWeight( r, g, b ) * Math.sin( theta );
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
updateFrom( envMap ) {
|
|
51
|
+
|
|
52
|
+
super.updateFrom( envMap );
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
mapNode,
|
|
56
|
+
marginalNode,
|
|
57
|
+
conditionalNode,
|
|
58
|
+
totalSumNode,
|
|
59
|
+
} = this;
|
|
60
|
+
|
|
61
|
+
// refresh values in place on the existing nodes so no rebuild is required
|
|
62
|
+
mapNode.value = this.map;
|
|
63
|
+
marginalNode.value = this.marginalWeights;
|
|
64
|
+
conditionalNode.value = this.conditionalWeights;
|
|
65
|
+
totalSumNode.value = this.totalSum;
|
|
66
|
+
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_initFns() {
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
mapNode,
|
|
73
|
+
mapSampler,
|
|
74
|
+
marginalNode,
|
|
75
|
+
marginalSampler,
|
|
76
|
+
conditionalNode,
|
|
77
|
+
conditionalSampler,
|
|
78
|
+
totalSumNode,
|
|
79
|
+
rotationNode,
|
|
80
|
+
intensityNode,
|
|
81
|
+
} = this;
|
|
82
|
+
|
|
83
|
+
this.sampleColor = wgslTagFn/* wgsl */`
|
|
84
|
+
fn sampleEnv( direction: vec3f ) -> vec4f {
|
|
85
|
+
|
|
86
|
+
let sampleDir = ${ rotationNode } * direction;
|
|
87
|
+
let mapUv = ${ equirectDirectionToUvFn }( sampleDir );
|
|
88
|
+
let col = textureSampleLevel( ${ mapNode }, ${ mapSampler }, mapUv, 0 );
|
|
89
|
+
|
|
90
|
+
return vec4f( ${ intensityNode } * col.rgb, col.a );
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
this.sampleDir = wgslTagFn/* wgsl */`
|
|
96
|
+
fn sampleEnvDir( r: vec2f ) -> ${ environmentSampleStruct } {
|
|
97
|
+
|
|
98
|
+
var result: ${ environmentSampleStruct };
|
|
99
|
+
|
|
100
|
+
// walk the CDF: marginal picks the row (v), conditional picks the column (u)
|
|
101
|
+
let v = textureSampleLevel( ${ marginalNode }, ${ marginalSampler }, vec2f( r.x, 0.0 ), 0 ).x;
|
|
102
|
+
let u = textureSampleLevel( ${ conditionalNode }, ${ conditionalSampler }, vec2f( r.y, v ), 0 ).x;
|
|
103
|
+
let uv = vec2f( u, v );
|
|
104
|
+
let totalSum = ${ totalSumNode };
|
|
105
|
+
|
|
106
|
+
let direction = ${ equirectUvToDirectionFn }( uv );
|
|
107
|
+
let color = textureSampleLevel( ${ mapNode }, ${ mapSampler }, uv, 0 ).rgb;
|
|
108
|
+
|
|
109
|
+
result.direction = transpose( ${ rotationNode } ) * direction;
|
|
110
|
+
result.color = color * ${ intensityNode };
|
|
111
|
+
|
|
112
|
+
if ( totalSum != 0.0 ) {
|
|
113
|
+
|
|
114
|
+
let lum = ${ luminanceFn }( color );
|
|
115
|
+
let resolution = textureDimensions( ${ mapNode } );
|
|
116
|
+
result.pdf = ${ equirectLuminancePdfFn }( lum, totalSum, resolution );
|
|
117
|
+
|
|
118
|
+
} else {
|
|
119
|
+
|
|
120
|
+
result.pdf = 0.0;
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
this.getDirPdf = wgslTagFn/* wgsl */`
|
|
130
|
+
fn getEnvDirPdf( direction: vec3f ) -> f32 {
|
|
131
|
+
|
|
132
|
+
if ( ${ totalSumNode } == 0.0 ) {
|
|
133
|
+
|
|
134
|
+
return 0.0;
|
|
135
|
+
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let rotatedDir = ${ rotationNode } * direction;
|
|
139
|
+
let mapUv = ${ equirectDirectionToUvFn }( rotatedDir );
|
|
140
|
+
let color = textureSampleLevel( ${ mapNode }, ${ mapSampler }, mapUv, 0 ).rgb;
|
|
141
|
+
let lum = ${ luminanceFn }( color );
|
|
142
|
+
let resolution = textureDimensions( ${ mapNode } );
|
|
143
|
+
|
|
144
|
+
return ${ equirectLuminancePdfFn }( lum, ${ totalSumNode }, resolution );
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { storage, uniform, uniformArray, texture } from 'three/tsl';
|
|
2
|
+
import { StorageBufferAttribute, HalfFloatType } from 'three/webgpu';
|
|
3
|
+
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
4
|
+
import { AtlasTexture } from './AtlasTexture.js';
|
|
5
|
+
import { LightsInfoUniformStruct } from '../uniforms/LightsInfoUniformStruct.js';
|
|
6
|
+
import { lightStruct, lightRecordStruct } from './nodes/structs.wgsl.js';
|
|
7
|
+
import { sampleTexelFunc } from './nodes/utils.wgsl.js';
|
|
8
|
+
import {
|
|
9
|
+
RECT_AREA_LIGHT_TYPE,
|
|
10
|
+
CIRC_AREA_LIGHT_TYPE,
|
|
11
|
+
SPOT_LIGHT_TYPE,
|
|
12
|
+
DIR_LIGHT_TYPE,
|
|
13
|
+
POINT_LIGHT_TYPE,
|
|
14
|
+
LIGHT_FAR_DISTANCE,
|
|
15
|
+
intersectsRectangleFn,
|
|
16
|
+
intersectsCircleFn,
|
|
17
|
+
randomAreaLightSampleFn,
|
|
18
|
+
randomSpotLightSampleFn,
|
|
19
|
+
getSpotAttenuationFn,
|
|
20
|
+
} from './nodes/lights.wgsl.js';
|
|
21
|
+
|
|
22
|
+
export class LightsInfoNode extends LightsInfoUniformStruct {
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
|
|
26
|
+
super();
|
|
27
|
+
|
|
28
|
+
// lights packed into a storage buffer of Light structs
|
|
29
|
+
this.countNode = uniform( this.count, 'uint' );
|
|
30
|
+
this.buffer = new StorageBufferAttribute( new Float32Array( 2 * lightStruct.getLength() ), lightStruct.getLength() );
|
|
31
|
+
this.bufferNode = storage( this.buffer, lightStruct ).toReadOnly().setName( 'lights' );
|
|
32
|
+
|
|
33
|
+
// ies profiles packed into an atlas alongside their placement rects
|
|
34
|
+
this.iesAtlas = new AtlasTexture( { type: HalfFloatType } );
|
|
35
|
+
this.iesProfilesNode = texture( this.iesAtlas.texture );
|
|
36
|
+
this.iesInfoNode = uniformArray( this.iesAtlas.textureInfo, 'uvec4' );
|
|
37
|
+
|
|
38
|
+
this._initFns();
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
updateFrom( renderer, lights ) {
|
|
43
|
+
|
|
44
|
+
// the unique set of ies textures referenced by the lights' "iesMap" fields
|
|
45
|
+
const iesTextures = Array.from( new Set( lights.map( l => l.iesMap ).filter( t => t ) ) );
|
|
46
|
+
const changed = super.updateFrom( lights, iesTextures );
|
|
47
|
+
|
|
48
|
+
this.iesAtlas.setTextures( renderer, iesTextures );
|
|
49
|
+
this.iesProfilesNode.value = this.iesAtlas.texture;
|
|
50
|
+
|
|
51
|
+
const stride = lightStruct.getLength();
|
|
52
|
+
const count = this.count;
|
|
53
|
+
const capacity = Math.max( count, 2 );
|
|
54
|
+
|
|
55
|
+
// resize the buffer to the exact light count, keeping the same binding node
|
|
56
|
+
if ( this.buffer.array.length !== capacity * stride ) {
|
|
57
|
+
|
|
58
|
+
this.buffer.dispose();
|
|
59
|
+
this.buffer = new StorageBufferAttribute( new Float32Array( capacity * stride ), stride );
|
|
60
|
+
this.bufferNode.value = this.buffer;
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// the texture's packed float layout already matches lightStruct's std layout, so copy it in
|
|
65
|
+
const src = this.tex.image.data;
|
|
66
|
+
this.buffer.array.set( src.subarray( 0, count * stride ) );
|
|
67
|
+
|
|
68
|
+
// rewrite the int fields ( lightType, iesProfile ) as i32 bits
|
|
69
|
+
const intView = new Int32Array( this.buffer.array.buffer );
|
|
70
|
+
for ( let i = 0; i < count; i ++ ) {
|
|
71
|
+
|
|
72
|
+
const base = i * stride;
|
|
73
|
+
intView[ base + 3 ] = Math.round( src[ base + 3 ] );
|
|
74
|
+
intView[ base + 21 ] = Math.round( src[ base + 21 ] );
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.buffer.needsUpdate = true;
|
|
79
|
+
this.countNode.value = this.count;
|
|
80
|
+
|
|
81
|
+
return changed;
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_initFns() {
|
|
86
|
+
|
|
87
|
+
const { bufferNode, iesProfilesNode, iesInfoNode } = this;
|
|
88
|
+
|
|
89
|
+
// profiles are sampled out of an atlas so filtering must resolve tile-relative wrapping
|
|
90
|
+
const sampleIesTexelFn = sampleTexelFunc( iesInfoNode, iesProfilesNode, 'sampleIesTexel' );
|
|
91
|
+
|
|
92
|
+
// uniformly pick a light and sample it
|
|
93
|
+
this.randomLightSample = wgslTagFn/* wgsl */`
|
|
94
|
+
fn randomLightSample( lightIndex: u32, rayOrigin: vec3f, ruv: vec2f ) -> ${ lightRecordStruct } {
|
|
95
|
+
|
|
96
|
+
let light = ${ bufferNode }[ lightIndex ];
|
|
97
|
+
|
|
98
|
+
var result: ${ lightRecordStruct };
|
|
99
|
+
if ( light.lightType == ${ SPOT_LIGHT_TYPE } ) {
|
|
100
|
+
|
|
101
|
+
result = ${ randomSpotLightSampleFn }( light, rayOrigin, ruv );
|
|
102
|
+
|
|
103
|
+
let spotNormal = normalize( cross( light.u, light.v ) );
|
|
104
|
+
let cosTheta = dot( result.direction, spotNormal );
|
|
105
|
+
var spotAttenuation: f32;
|
|
106
|
+
if ( light.iesProfile >= 0 ) {
|
|
107
|
+
|
|
108
|
+
// half a texel of the profile's tile in the atlas
|
|
109
|
+
let profileInfo = ${ iesInfoNode }[ u32( light.iesProfile ) ];
|
|
110
|
+
let texelInset = 0.5 / vec2f( vec2u( profileInfo.y & 0xFFFFu, profileInfo.y >> 16u ) );
|
|
111
|
+
|
|
112
|
+
// tilt spans [ 0, 180 ]deg inset to the texel centers, twist covers [ 0, 359 ]deg and wraps
|
|
113
|
+
let tiltAngle = mix( texelInset.x, 1.0 - texelInset.x, acos( cosTheta ) / PI );
|
|
114
|
+
let twistAngle = ( atan2( dot( result.direction, light.v ), dot( result.direction, light.u ) ) + PI ) / ( 2.0 * PI ) + texelInset.y;
|
|
115
|
+
let packedProfile = ( 1 << 26 ) | light.iesProfile;
|
|
116
|
+
spotAttenuation = ${ sampleIesTexelFn }( vec2f( tiltAngle, twistAngle ), packedProfile, 0.0 ).r;
|
|
117
|
+
|
|
118
|
+
} else {
|
|
119
|
+
|
|
120
|
+
spotAttenuation = ${ getSpotAttenuationFn }( light.coneCos, light.penumbraCos, cosTheta );
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
result.emission *= spotAttenuation;
|
|
125
|
+
|
|
126
|
+
} else if ( light.lightType == ${ POINT_LIGHT_TYPE } ) {
|
|
127
|
+
|
|
128
|
+
// the point light's world position is packed into the u slot
|
|
129
|
+
let lightRay = light.u - rayOrigin;
|
|
130
|
+
let lightDist = length( lightRay );
|
|
131
|
+
let cutoffDistance = light.distance;
|
|
132
|
+
var distanceFalloff = 1.0 / max( pow( lightDist, light.decay ), 0.01 );
|
|
133
|
+
if ( cutoffDistance > 0.0 ) {
|
|
134
|
+
|
|
135
|
+
let window = clamp( 1.0 - pow( lightDist / cutoffDistance, 4.0 ), 0.0, 1.0 );
|
|
136
|
+
distanceFalloff *= window * window;
|
|
137
|
+
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
result.direction = normalize( lightRay );
|
|
141
|
+
result.dist = lightDist;
|
|
142
|
+
result.pdf = 1.0;
|
|
143
|
+
result.emission = light.color * light.intensity * distanceFalloff;
|
|
144
|
+
result.lightType = light.lightType;
|
|
145
|
+
|
|
146
|
+
} else if ( light.lightType == ${ DIR_LIGHT_TYPE } ) {
|
|
147
|
+
|
|
148
|
+
// the directional light's direction is packed into the u slot
|
|
149
|
+
result.dist = ${ LIGHT_FAR_DISTANCE };
|
|
150
|
+
result.direction = light.u;
|
|
151
|
+
result.pdf = 1.0;
|
|
152
|
+
result.emission = light.color * light.intensity;
|
|
153
|
+
result.lightType = light.lightType;
|
|
154
|
+
|
|
155
|
+
} else {
|
|
156
|
+
|
|
157
|
+
result = ${ randomAreaLightSampleFn }( light, rayOrigin, ruv );
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
// forward intersection of a ray with a single area light ( rect / circ only ), used for MIS
|
|
167
|
+
// TODO: support hitting the spot light disk here and move spot lights into the
|
|
168
|
+
// MIS-weighted set so they appear in sharp reflections
|
|
169
|
+
this.intersectLightAtIndex = wgslTagFn/* wgsl */`
|
|
170
|
+
fn intersectLightAtIndex( rayOrigin: vec3f, rayDirection: vec3f, index: u32, lightRec: ptr<function, ${ lightRecordStruct }> ) -> bool {
|
|
171
|
+
|
|
172
|
+
let light = ${ bufferNode }[ index ];
|
|
173
|
+
|
|
174
|
+
var u = light.u;
|
|
175
|
+
var v = light.v;
|
|
176
|
+
let normal = normalize( cross( u, v ) );
|
|
177
|
+
|
|
178
|
+
// only front-facing area lights can be hit
|
|
179
|
+
if ( dot( normal, rayDirection ) > 0.0 ) {
|
|
180
|
+
|
|
181
|
+
u *= 1.0 / dot( u, u );
|
|
182
|
+
v *= 1.0 / dot( v, v );
|
|
183
|
+
|
|
184
|
+
var dist = - 1.0;
|
|
185
|
+
if ( light.lightType == ${ RECT_AREA_LIGHT_TYPE } ) {
|
|
186
|
+
|
|
187
|
+
dist = ${ intersectsRectangleFn }( light.position, normal, u, v, rayOrigin, rayDirection );
|
|
188
|
+
|
|
189
|
+
} else if ( light.lightType == ${ CIRC_AREA_LIGHT_TYPE } ) {
|
|
190
|
+
|
|
191
|
+
dist = ${ intersectsCircleFn }( light.position, normal, u, v, rayOrigin, rayDirection );
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if ( dist > 0.0 ) {
|
|
196
|
+
|
|
197
|
+
let cosTheta = dot( rayDirection, normal );
|
|
198
|
+
lightRec.dist = dist;
|
|
199
|
+
lightRec.pdf = ( dist * dist ) / ( light.area * cosTheta );
|
|
200
|
+
lightRec.emission = light.color * light.intensity;
|
|
201
|
+
lightRec.direction = rayDirection;
|
|
202
|
+
lightRec.lightType = light.lightType;
|
|
203
|
+
return true;
|
|
204
|
+
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return false;
|
|
210
|
+
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
dispose() {
|
|
217
|
+
|
|
218
|
+
this.tex.dispose();
|
|
219
|
+
this.buffer.dispose();
|
|
220
|
+
this.iesAtlas.dispose();
|
|
221
|
+
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { Matrix4, Vector2 } from 'three/webgpu';
|
|
2
|
+
import { PathTracerMegaKernel } from './compute/PathTracerMegaKernel.js';
|
|
3
|
+
import { EquirectHdrInfoNode } from './EquirectHdrInfoNode.js';
|
|
4
|
+
import { EquirectBackgroundInfo } from './EquirectBackgroundInfo.js';
|
|
5
|
+
import { LightsInfoNode } from './LightsInfoNode.js';
|
|
6
|
+
import { FILTER_GLOSSY_DISABLED } from './nodes/material.wgsl.js';
|
|
7
|
+
import { PathTracerBackend } from './PathTracerBackend.js';
|
|
8
|
+
|
|
9
|
+
const _size = new Vector2();
|
|
10
|
+
|
|
11
|
+
export class MegaKernelPathTracer extends PathTracerBackend {
|
|
12
|
+
|
|
13
|
+
constructor( renderer ) {
|
|
14
|
+
|
|
15
|
+
super( renderer );
|
|
16
|
+
|
|
17
|
+
// options
|
|
18
|
+
this.envInfo = new EquirectHdrInfoNode();
|
|
19
|
+
|
|
20
|
+
// every pixel in a tile finishes its sample in one dispatch, so the counts follow from how
|
|
21
|
+
// far through the tile cycle the render is and need no GPU work to measure
|
|
22
|
+
this._completedSamples = 0;
|
|
23
|
+
this._tileProgress = 0;
|
|
24
|
+
|
|
25
|
+
// kernels
|
|
26
|
+
this.kernel = new PathTracerMegaKernel( ).setWorkgroupSize( 8, 8, 1 );
|
|
27
|
+
this.kernel.envInfo = this.envInfo;
|
|
28
|
+
|
|
29
|
+
this.backgroundInfo = new EquirectBackgroundInfo();
|
|
30
|
+
this.kernel.backgroundInfo = this.backgroundInfo;
|
|
31
|
+
|
|
32
|
+
this.lightsInfo = new LightsInfoNode();
|
|
33
|
+
this.kernel.lightsInfo = this.lightsInfo;
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
resetSeed() {
|
|
38
|
+
|
|
39
|
+
this.kernel.seed = 0;
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
reset() {
|
|
44
|
+
|
|
45
|
+
super.reset();
|
|
46
|
+
this._completedSamples = 0;
|
|
47
|
+
this._tileProgress = 0;
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getSampleCountsAsync() {
|
|
52
|
+
|
|
53
|
+
if ( this.lowResMode ) {
|
|
54
|
+
|
|
55
|
+
return { min: 0, max: 0, avg: 0 };
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const completed = this._completedSamples;
|
|
60
|
+
const progress = this._tileProgress;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
min: progress === 1 ? completed + 1 : completed,
|
|
64
|
+
max: completed + 1,
|
|
65
|
+
avg: completed + progress,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setBVHData( bvhData ) {
|
|
71
|
+
|
|
72
|
+
this.kernel.bvhData = bvhData;
|
|
73
|
+
this.rebuild();
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
rebuild() {
|
|
78
|
+
|
|
79
|
+
super.rebuild();
|
|
80
|
+
this.kernel.needsUpdate = true;
|
|
81
|
+
this.reset();
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setRandom( random ) {
|
|
86
|
+
|
|
87
|
+
this.kernel.context.random = random;
|
|
88
|
+
this.kernel.needsUpdate = true;
|
|
89
|
+
this.reset();
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setMaterial( material ) {
|
|
94
|
+
|
|
95
|
+
this.kernel.material = material.getData();
|
|
96
|
+
this.kernel.needsUpdate = true;
|
|
97
|
+
this.reset();
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setFilterGlossy( value ) {
|
|
102
|
+
|
|
103
|
+
// the kernel takes the inverted threshold, mirroring Cycles
|
|
104
|
+
this.kernel.filterGlossy = value === 0 ? FILTER_GLOSSY_DISABLED : 1 / value;
|
|
105
|
+
this.reset();
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setClamping( direct, indirect ) {
|
|
110
|
+
|
|
111
|
+
this.kernel.clampDirect = direct;
|
|
112
|
+
this.kernel.clampIndirect = indirect;
|
|
113
|
+
this.reset();
|
|
114
|
+
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
setEnvironment( envMap ) {
|
|
118
|
+
|
|
119
|
+
this.envInfo.updateFrom( envMap );
|
|
120
|
+
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
setLights( lights ) {
|
|
124
|
+
|
|
125
|
+
this.lightsInfo.updateFrom( this.renderer, lights );
|
|
126
|
+
this.reset();
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setMultipleImportanceSampling( enabled ) {
|
|
131
|
+
|
|
132
|
+
this.kernel.misEnabled = enabled ? 1 : 0;
|
|
133
|
+
this.reset();
|
|
134
|
+
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setEnvironmentParams( envMapIntensity, envMapRotation ) {
|
|
138
|
+
|
|
139
|
+
const { envInfo } = this;
|
|
140
|
+
const rotationMatrix = new Matrix4().makeRotationFromEuler( envMapRotation ).invert();
|
|
141
|
+
envInfo.rotationNode.value.setFromMatrix4( rotationMatrix );
|
|
142
|
+
envInfo.intensityNode.value = envMapIntensity;
|
|
143
|
+
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
setBackground( background ) {
|
|
147
|
+
|
|
148
|
+
const { backgroundInfo } = this;
|
|
149
|
+
backgroundInfo.dispose();
|
|
150
|
+
backgroundInfo.map = background;
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
setBackgroundParams(
|
|
155
|
+
backgroundIntensity,
|
|
156
|
+
backgroundRotation,
|
|
157
|
+
backgroundBlurriness,
|
|
158
|
+
) {
|
|
159
|
+
|
|
160
|
+
const { backgroundInfo } = this;
|
|
161
|
+
const rotationMatrix = new Matrix4().makeRotationFromEuler( backgroundRotation ).invert();
|
|
162
|
+
backgroundInfo.rotationNode.value.copy( rotationMatrix );
|
|
163
|
+
backgroundInfo.intensity = backgroundIntensity;
|
|
164
|
+
backgroundInfo.blur = backgroundBlurriness;
|
|
165
|
+
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setTransmissiveBackground( value ) {
|
|
169
|
+
|
|
170
|
+
this.kernel.transmissiveBackground = value;
|
|
171
|
+
this.reset();
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
*createRenderTask() {
|
|
176
|
+
|
|
177
|
+
const {
|
|
178
|
+
renderer,
|
|
179
|
+
kernel,
|
|
180
|
+
maxBounces,
|
|
181
|
+
maxTransparentBounces,
|
|
182
|
+
|
|
183
|
+
outputTarget,
|
|
184
|
+
sampleCountTarget,
|
|
185
|
+
lowResMode,
|
|
186
|
+
} = this;
|
|
187
|
+
|
|
188
|
+
const tiles = new Vector2();
|
|
189
|
+
|
|
190
|
+
// init parameters
|
|
191
|
+
kernel.outputTarget = outputTarget;
|
|
192
|
+
kernel.sampleCountTarget = sampleCountTarget;
|
|
193
|
+
|
|
194
|
+
kernel.maxBounces = maxBounces;
|
|
195
|
+
kernel.maxTransparentBounces = maxTransparentBounces;
|
|
196
|
+
|
|
197
|
+
// number of tile cycles that have finished, ie. the sample count every pixel has reached
|
|
198
|
+
let completedSamples = 0;
|
|
199
|
+
|
|
200
|
+
while ( true ) {
|
|
201
|
+
|
|
202
|
+
// skip the tile loop entirely once every pixel is finished
|
|
203
|
+
kernel.maxSamples = this.maxSamples;
|
|
204
|
+
if ( this.maxSamples !== 0 && completedSamples >= this.maxSamples ) {
|
|
205
|
+
|
|
206
|
+
yield;
|
|
207
|
+
continue;
|
|
208
|
+
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// split the frame into a near square grid of tiles no larger than the budget
|
|
212
|
+
this.getSize( _size );
|
|
213
|
+
const budgetTiles = Math.max( 1, Math.ceil( _size.x * _size.y / this.frameBudget ) );
|
|
214
|
+
tiles.x = Math.ceil( Math.sqrt( budgetTiles ) );
|
|
215
|
+
tiles.y = Math.ceil( budgetTiles / tiles.x );
|
|
216
|
+
|
|
217
|
+
// every pixel in a tile finishes its sample in a single dispatch, so the sample counts
|
|
218
|
+
// can be derived from how far through the tile cycle we are without reading anything back
|
|
219
|
+
const tileCount = this.lowResMode ? 1 : tiles.x * tiles.y;
|
|
220
|
+
let completedTiles = 0;
|
|
221
|
+
|
|
222
|
+
const tileSize = kernel.tileSize;
|
|
223
|
+
if ( lowResMode ) {
|
|
224
|
+
|
|
225
|
+
this.getSize( tileSize );
|
|
226
|
+
|
|
227
|
+
} else {
|
|
228
|
+
|
|
229
|
+
this.getSize( tileSize ).divide( tiles ).ceil();
|
|
230
|
+
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const dispatchSize = kernel.getDispatchSize( tileSize.x, tileSize.y );
|
|
234
|
+
kernel.seed += 1;
|
|
235
|
+
|
|
236
|
+
// Swap targets to support devices without <rgba32float, read_write> textures
|
|
237
|
+
// Copy latest data to a new outputTarget to keep the appearance
|
|
238
|
+
renderer.copyTextureToTexture( this.outputTarget, this.prevOutputTarget );
|
|
239
|
+
[ this.outputTarget, this.prevOutputTarget ] = [ this.prevOutputTarget, this.outputTarget ];
|
|
240
|
+
kernel.prevOutputTarget = this.prevOutputTarget;
|
|
241
|
+
kernel.outputTarget = this.outputTarget;
|
|
242
|
+
|
|
243
|
+
for ( let x = 0; x < tiles.x; x ++ ) {
|
|
244
|
+
|
|
245
|
+
for ( let y = 0; y < tiles.y; y ++ ) {
|
|
246
|
+
|
|
247
|
+
kernel.offset.set( x, y ).multiply( tileSize );
|
|
248
|
+
renderer.compute( kernel.kernel, dispatchSize );
|
|
249
|
+
|
|
250
|
+
// in low res mode the tiles past the first fall outside the target and do no work
|
|
251
|
+
completedTiles = Math.min( completedTiles + 1, tileCount );
|
|
252
|
+
this._completedSamples = completedSamples;
|
|
253
|
+
this._tileProgress = completedTiles / tileCount;
|
|
254
|
+
|
|
255
|
+
yield;
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
completedSamples ++;
|
|
262
|
+
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
dispose() {
|
|
268
|
+
|
|
269
|
+
super.dispose();
|
|
270
|
+
|
|
271
|
+
// TODO: dispose of all buffers
|
|
272
|
+
this.envInfo.dispose();
|
|
273
|
+
this.lightsInfo.dispose();
|
|
274
|
+
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
}
|