@plasius/gpu-lighting 0.1.16 → 0.1.18

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.
Files changed (28) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +54 -0
  3. package/dist/index.cjs +105 -0
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +102 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/techniques/techniques/hybrid/direct-lighting.job.wgsl +69 -2
  8. package/dist/techniques/techniques/hybrid/final-gather.job.wgsl +62 -2
  9. package/dist/techniques/techniques/hybrid/prelude.wgsl +140 -0
  10. package/dist/techniques/techniques/hybrid/radiance-cache.job.wgsl +57 -2
  11. package/dist/techniques/techniques/hybrid/reflection-resolve.job.wgsl +235 -2
  12. package/dist/techniques/techniques/hybrid/screen-trace.job.wgsl +219 -2
  13. package/dist/techniques/techniques/pathtracer/accumulate.job.wgsl +64 -2
  14. package/dist/techniques/techniques/pathtracer/denoise.job.wgsl +125 -2
  15. package/dist/techniques/techniques/pathtracer/pathtrace.job.wgsl +429 -2
  16. package/dist/techniques/techniques/pathtracer/prelude.wgsl +260 -0
  17. package/package.json +1 -1
  18. package/src/index.js +114 -0
  19. package/src/techniques/hybrid/direct-lighting.job.wgsl +69 -2
  20. package/src/techniques/hybrid/final-gather.job.wgsl +62 -2
  21. package/src/techniques/hybrid/prelude.wgsl +140 -0
  22. package/src/techniques/hybrid/radiance-cache.job.wgsl +57 -2
  23. package/src/techniques/hybrid/reflection-resolve.job.wgsl +235 -2
  24. package/src/techniques/hybrid/screen-trace.job.wgsl +219 -2
  25. package/src/techniques/pathtracer/accumulate.job.wgsl +64 -2
  26. package/src/techniques/pathtracer/denoise.job.wgsl +125 -2
  27. package/src/techniques/pathtracer/pathtrace.job.wgsl +429 -2
  28. package/src/techniques/pathtracer/prelude.wgsl +260 -0
@@ -1,3 +1,65 @@
1
- fn process_job() {
2
- // Placeholder progressive accumulation stage.
1
+ @group(0) @binding(0) var<uniform> pathTracerParams: PathTracerParams;
2
+ @group(0) @binding(1) var<storage, read> pathSampleBuffer: array<PathSamplePixel>;
3
+ @group(0) @binding(2) var<storage, read_write> pathAccumulationBuffer: array<PathAccumulationPixel>;
4
+
5
+ fn accumulation_index(pixel: vec2<u32>) -> u32 {
6
+ return pixel.y * max(pathTracerParams.image_width, 1u) + pixel.x;
7
+ }
8
+
9
+ fn reset_accumulation(sample: PathSamplePixel) -> PathAccumulationPixel {
10
+ let sample_luminance = luminance(sample.radiance_opacity.xyz);
11
+ return PathAccumulationPixel(
12
+ vec4<f32>(sample.radiance_opacity.xyz, max(sample.albedo_sample_count.w, 1.0)),
13
+ vec4<f32>(sample_luminance, 0.0, 0.0, f32(pathTracerParams.frame_index))
14
+ );
15
+ }
16
+
17
+ @compute @workgroup_size(8, 8, 1)
18
+ fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
19
+ if (
20
+ global_id.x >= pathTracerParams.image_width ||
21
+ global_id.y >= pathTracerParams.image_height
22
+ ) {
23
+ return;
24
+ }
25
+
26
+ let index = accumulation_index(global_id.xy);
27
+ let sample = pathSampleBuffer[index];
28
+ let history = pathAccumulationBuffer[index];
29
+ let sample_luminance = luminance(sample.radiance_opacity.xyz);
30
+ let current_samples = max(sample.albedo_sample_count.w, 1.0);
31
+ let history_samples = max(history.integrated_radiance.w, 0.0);
32
+ let should_reset =
33
+ pathTracerParams.accumulation_reset != 0u ||
34
+ history_samples <= 0.0 ||
35
+ history.moments.w <= 0.0;
36
+
37
+ if (should_reset) {
38
+ pathAccumulationBuffer[index] = reset_accumulation(sample);
39
+ return;
40
+ }
41
+
42
+ let combined_samples = min(history_samples + current_samples, 4096.0);
43
+ let progressive_weight = current_samples / max(combined_samples, 1.0);
44
+ let minimum_refresh = 1.0 - clamp(pathTracerParams.history_blend, 0.0, 0.98);
45
+ let blend = clamp(max(progressive_weight, minimum_refresh), 0.02, 1.0);
46
+ let integrated =
47
+ history.integrated_radiance.xyz * (1.0 - blend) +
48
+ sample.radiance_opacity.xyz * blend;
49
+ let mean_luminance =
50
+ history.moments.x * (1.0 - blend) +
51
+ sample_luminance * blend;
52
+ let variance_sample = sample_luminance - mean_luminance;
53
+ let variance =
54
+ max(0.0, history.moments.y * (1.0 - blend) + variance_sample * variance_sample * blend);
55
+
56
+ pathAccumulationBuffer[index] = PathAccumulationPixel(
57
+ vec4<f32>(integrated, combined_samples),
58
+ vec4<f32>(
59
+ mean_luminance,
60
+ variance,
61
+ 1.0 - blend,
62
+ f32(pathTracerParams.frame_index)
63
+ )
64
+ );
3
65
  }
@@ -1,3 +1,126 @@
1
- fn process_job() {
2
- // Placeholder denoise stage for interactive reference previews.
1
+ @group(0) @binding(0) var<uniform> pathTracerParams: PathTracerParams;
2
+ @group(0) @binding(1) var<storage, read> pathAccumulationBuffer: array<PathAccumulationPixel>;
3
+ @group(0) @binding(2) var<storage, read> pathSampleBuffer: array<PathSamplePixel>;
4
+ @group(0) @binding(3) var<storage, read_write> pathDenoiseHistoryBuffer: array<PathDenoisePixel>;
5
+
6
+ fn denoise_index(pixel: vec2<u32>) -> u32 {
7
+ return pixel.y * max(pathTracerParams.image_width, 1u) + pixel.x;
8
+ }
9
+
10
+ fn in_bounds(pixel: vec2<i32>) -> bool {
11
+ return
12
+ pixel.x >= 0 &&
13
+ pixel.y >= 0 &&
14
+ pixel.x < i32(pathTracerParams.image_width) &&
15
+ pixel.y < i32(pathTracerParams.image_height);
16
+ }
17
+
18
+ fn spatial_kernel(offset: vec2<i32>) -> f32 {
19
+ if (offset.x == 0 && offset.y == 0) {
20
+ return 4.0;
21
+ }
22
+ if (offset.x == 0 || offset.y == 0) {
23
+ return 2.0;
24
+ }
25
+ return 1.0;
26
+ }
27
+
28
+ fn bilateral_weight(
29
+ center_sample: PathSamplePixel,
30
+ center_accumulation: PathAccumulationPixel,
31
+ neighbor_sample: PathSamplePixel,
32
+ offset: vec2<i32>
33
+ ) -> f32 {
34
+ let strength = clamp(pathTracerParams.denoise_strength, 0.0, 1.0);
35
+ let center_normal = safe_normalize(center_sample.normal_roughness.xyz);
36
+ let neighbor_normal = safe_normalize(neighbor_sample.normal_roughness.xyz);
37
+ let normal_alignment = pow(
38
+ saturate(dot(center_normal, neighbor_normal)),
39
+ 6.0 + (1.0 - strength) * 24.0
40
+ );
41
+ let depth_delta = abs(center_sample.direction_distance.w - neighbor_sample.direction_distance.w);
42
+ let depth_scale =
43
+ max(center_sample.direction_distance.w, 1.0) *
44
+ (0.01 + strength * 0.04);
45
+ let depth_weight = exp(-depth_delta * safe_rcp(depth_scale));
46
+ let albedo_delta = length(
47
+ center_sample.albedo_sample_count.xyz - neighbor_sample.albedo_sample_count.xyz
48
+ );
49
+ let albedo_weight = exp(-albedo_delta * (2.0 + strength * 2.0));
50
+ let sample_weight = clamp(
51
+ neighbor_sample.albedo_sample_count.w /
52
+ max(center_sample.albedo_sample_count.w, 1.0),
53
+ 0.25,
54
+ 1.5
55
+ );
56
+ let variance_penalty = 1.0 / (1.0 + center_accumulation.moments.y * 0.5);
57
+ return spatial_kernel(offset) *
58
+ max(normal_alignment * depth_weight * albedo_weight * sample_weight * variance_penalty, 0.0001);
59
+ }
60
+
61
+ @compute @workgroup_size(8, 8, 1)
62
+ fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
63
+ if (
64
+ global_id.x >= pathTracerParams.image_width ||
65
+ global_id.y >= pathTracerParams.image_height
66
+ ) {
67
+ return;
68
+ }
69
+
70
+ let pixel = vec2<i32>(i32(global_id.x), i32(global_id.y));
71
+ let index = denoise_index(global_id.xy);
72
+ let center_accumulation = pathAccumulationBuffer[index];
73
+ let center_sample = pathSampleBuffer[index];
74
+ let center_history = pathDenoiseHistoryBuffer[index];
75
+ var filtered = vec3<f32>(0.0);
76
+ var total_weight = 0.0;
77
+
78
+ for (var y = -1; y <= 1; y = y + 1) {
79
+ for (var x = -1; x <= 1; x = x + 1) {
80
+ let neighbor_pixel = pixel + vec2<i32>(x, y);
81
+ if (!in_bounds(neighbor_pixel)) {
82
+ continue;
83
+ }
84
+
85
+ let neighbor_index = denoise_index(vec2<u32>(u32(neighbor_pixel.x), u32(neighbor_pixel.y)));
86
+ let neighbor_accumulation = pathAccumulationBuffer[neighbor_index];
87
+ let neighbor_sample = pathSampleBuffer[neighbor_index];
88
+ let weight = bilateral_weight(
89
+ center_sample,
90
+ center_accumulation,
91
+ neighbor_sample,
92
+ vec2<i32>(x, y)
93
+ );
94
+ filtered = filtered + neighbor_accumulation.integrated_radiance.xyz * weight;
95
+ total_weight = total_weight + weight;
96
+ }
97
+ }
98
+
99
+ let filtered_color = filtered / max(total_weight, PATH_TRACER_EPSILON);
100
+ let sample_confidence = clamp(
101
+ log2(max(center_accumulation.integrated_radiance.w, 1.0) + 1.0) / 6.0,
102
+ 0.0,
103
+ 1.0
104
+ );
105
+ let variance_penalty = 1.0 / (1.0 + center_accumulation.moments.y * 0.75);
106
+ let confidence = clamp(sample_confidence * variance_penalty, 0.0, 1.0);
107
+ let use_history =
108
+ pathTracerParams.accumulation_reset == 0u &&
109
+ center_history.filtered_radiance.w > 0.0;
110
+ let temporal_blend = clamp(0.2 + confidence * 0.6, 0.2, 0.9);
111
+ let resolved_color = select(
112
+ filtered_color,
113
+ center_history.filtered_radiance.xyz * (1.0 - temporal_blend) +
114
+ filtered_color * temporal_blend,
115
+ use_history
116
+ );
117
+
118
+ pathDenoiseHistoryBuffer[index] = PathDenoisePixel(
119
+ vec4<f32>(resolved_color, confidence),
120
+ vec4<f32>(safe_normalize(center_sample.normal_roughness.xyz), center_sample.direction_distance.w),
121
+ vec4<f32>(
122
+ center_sample.albedo_sample_count.xyz,
123
+ center_accumulation.integrated_radiance.w
124
+ )
125
+ );
3
126
  }
@@ -1,3 +1,430 @@
1
- fn process_job() {
2
- // Placeholder path tracing kernel for reference rendering.
1
+ @group(0) @binding(0) var<uniform> pathTracerParams: PathTracerParams;
2
+ @group(0) @binding(1) var<uniform> pathTracerCamera: PathTracerCamera;
3
+ @group(0) @binding(2) var<storage, read> pathTracerScene: PathTracerSceneMetadata;
4
+ @group(0) @binding(3) var<uniform> pathTracerGroundPlane: PathTracerGroundPlane;
5
+ @group(0) @binding(4) var<storage, read> pathTracerMaterials: array<PathTracerMaterial>;
6
+ @group(0) @binding(5) var<storage, read> pathTracerSpheres: array<PathTracerSphere>;
7
+ @group(0) @binding(6) var<storage, read> pathTracerTriangles: array<PathTracerTriangle>;
8
+ @group(0) @binding(7) var<storage, read_write> pathStateBuffer: array<PathState>;
9
+ @group(0) @binding(8) var<storage, read_write> pathSampleBuffer: array<PathSamplePixel>;
10
+
11
+ fn pixel_index(pixel: vec2<u32>) -> u32 {
12
+ return pixel.y * max(pathTracerParams.image_width, 1u) + pixel.x;
13
+ }
14
+
15
+ fn load_path_material(index: u32) -> MaterialSample {
16
+ if (pathTracerScene.material_count == 0u) {
17
+ return unpack_material(make_default_material());
18
+ }
19
+
20
+ let safe_index = min(index, pathTracerScene.material_count - 1u);
21
+ return unpack_material(pathTracerMaterials[safe_index]);
22
+ }
23
+
24
+ fn set_best_hit(best: ptr<function, PathHit>, candidate: PathHit) {
25
+ if (candidate.hit == 1u && candidate.distance < (*best).distance) {
26
+ *best = candidate;
27
+ }
28
+ }
29
+
30
+ fn trace_ground(ray: Ray, best: ptr<function, PathHit>) {
31
+ if (pathTracerGroundPlane.enabled == 0u) {
32
+ return;
33
+ }
34
+
35
+ let plane_normal = safe_normalize(pathTracerGroundPlane.normal);
36
+ let denominator = dot(plane_normal, ray.direction);
37
+ if (abs(denominator) <= PATH_TRACER_EPSILON) {
38
+ return;
39
+ }
40
+
41
+ let distance = -(dot(plane_normal, ray.origin) + pathTracerGroundPlane.height) / denominator;
42
+ if (distance <= ray.t_min || distance >= (*best).distance || distance >= ray.t_max) {
43
+ return;
44
+ }
45
+
46
+ set_best_hit(
47
+ best,
48
+ PathHit(
49
+ 1u,
50
+ distance,
51
+ ray.origin + ray.direction * distance,
52
+ pathTracerGroundPlane.material_index,
53
+ plane_normal,
54
+ 1u,
55
+ vec3<f32>(1.0, 0.0, 0.0),
56
+ 0u
57
+ )
58
+ );
59
+ }
60
+
61
+ fn trace_spheres(ray: Ray, best: ptr<function, PathHit>) {
62
+ var index = 0u;
63
+ loop {
64
+ if (index >= pathTracerScene.sphere_count) {
65
+ break;
66
+ }
67
+
68
+ let sphere = pathTracerSpheres[index];
69
+ let offset = ray.origin - sphere.center_radius.xyz;
70
+ let a = dot(ray.direction, ray.direction);
71
+ let half_b = dot(offset, ray.direction);
72
+ let c = dot(offset, offset) - sphere.center_radius.w * sphere.center_radius.w;
73
+ let discriminant = half_b * half_b - a * c;
74
+ if (discriminant >= 0.0) {
75
+ let root = sqrt(discriminant);
76
+ var distance = (-half_b - root) / max(a, PATH_TRACER_EPSILON);
77
+ if (distance <= ray.t_min) {
78
+ distance = (-half_b + root) / max(a, PATH_TRACER_EPSILON);
79
+ }
80
+ if (distance > ray.t_min && distance < (*best).distance && distance < ray.t_max) {
81
+ let position = ray.origin + ray.direction * distance;
82
+ let normal = safe_normalize(position - sphere.center_radius.xyz);
83
+ set_best_hit(
84
+ best,
85
+ PathHit(
86
+ 1u,
87
+ distance,
88
+ position,
89
+ sphere.material_index,
90
+ normal,
91
+ 2u,
92
+ vec3<f32>(0.0),
93
+ 0u
94
+ )
95
+ );
96
+ }
97
+ }
98
+
99
+ index = index + 1u;
100
+ }
101
+ }
102
+
103
+ fn trace_triangles(ray: Ray, best: ptr<function, PathHit>) {
104
+ var index = 0u;
105
+ loop {
106
+ if (index >= pathTracerScene.triangle_count) {
107
+ break;
108
+ }
109
+
110
+ let triangle = pathTracerTriangles[index];
111
+ let p0 = triangle.position0.xyz;
112
+ let p1 = triangle.position1.xyz;
113
+ let p2 = triangle.position2.xyz;
114
+ let edge1 = p1 - p0;
115
+ let edge2 = p2 - p0;
116
+ let p_vec = cross(ray.direction, edge2);
117
+ let determinant = dot(edge1, p_vec);
118
+ if (abs(determinant) > PATH_TRACER_EPSILON) {
119
+ let inverse_determinant = 1.0 / determinant;
120
+ let t_vec = ray.origin - p0;
121
+ let u = dot(t_vec, p_vec) * inverse_determinant;
122
+ if (u >= 0.0 && u <= 1.0) {
123
+ let q_vec = cross(t_vec, edge1);
124
+ let v = dot(ray.direction, q_vec) * inverse_determinant;
125
+ if (v >= 0.0 && u + v <= 1.0) {
126
+ let distance = dot(edge2, q_vec) * inverse_determinant;
127
+ if (distance > ray.t_min && distance < (*best).distance && distance < ray.t_max) {
128
+ let w = 1.0 - u - v;
129
+ let interpolated_normal =
130
+ triangle.normal0.xyz * w +
131
+ triangle.normal1.xyz * u +
132
+ triangle.normal2.xyz * v;
133
+ let geometric_normal = cross(edge1, edge2);
134
+ let resolved_normal = select(
135
+ safe_normalize(geometric_normal),
136
+ safe_normalize(interpolated_normal),
137
+ dot(interpolated_normal, interpolated_normal) > PATH_TRACER_EPSILON
138
+ );
139
+ set_best_hit(
140
+ best,
141
+ PathHit(
142
+ 1u,
143
+ distance,
144
+ ray.origin + ray.direction * distance,
145
+ triangle.material_index,
146
+ resolved_normal,
147
+ 3u,
148
+ vec3<f32>(w, u, v),
149
+ 0u
150
+ )
151
+ );
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ index = index + 1u;
158
+ }
159
+ }
160
+
161
+ fn trace_scene(ray: Ray) -> PathHit {
162
+ var best = miss_hit(ray);
163
+ trace_ground(ray, &best);
164
+ trace_spheres(ray, &best);
165
+ trace_triangles(ray, &best);
166
+ return best;
167
+ }
168
+
169
+ fn trace_shadow(origin: vec3<f32>, direction: vec3<f32>, max_distance: f32) -> bool {
170
+ let shadow_ray = Ray(
171
+ origin + direction * PATH_TRACER_EPSILON * 8.0,
172
+ PATH_TRACER_EPSILON,
173
+ direction,
174
+ max_distance
175
+ );
176
+ return trace_scene(shadow_ray).hit == 1u;
177
+ }
178
+
179
+ fn evaluate_direct_sun(
180
+ hit: PathHit,
181
+ material: MaterialSample,
182
+ view_direction: vec3<f32>
183
+ ) -> vec3<f32> {
184
+ let surface_normal = faceforward_normal(hit.normal, -view_direction);
185
+ let sun_direction = safe_normalize(vec3<f32>(0.32, 0.91, 0.21));
186
+ let ndotl = saturate(dot(surface_normal, sun_direction));
187
+ if (ndotl <= 0.0) {
188
+ return vec3<f32>(0.0);
189
+ }
190
+
191
+ let shadowed = trace_shadow(hit.position + surface_normal * PATH_TRACER_EPSILON * 12.0, sun_direction, pathTracerScene.max_trace_distance);
192
+ if (shadowed) {
193
+ return vec3<f32>(0.0);
194
+ }
195
+
196
+ let halfway = safe_normalize(sun_direction + view_direction);
197
+ let base_reflectance =
198
+ vec3<f32>(0.04) * (1.0 - material.metalness) +
199
+ material.albedo * material.metalness;
200
+ let fresnel = fresnel_schlick(saturate(dot(surface_normal, view_direction)), base_reflectance);
201
+ let specular_power = 8.0 + (1.0 - material.roughness) * 120.0;
202
+ let specular = fresnel * pow(saturate(dot(surface_normal, halfway)), specular_power) * ndotl;
203
+ let diffuse =
204
+ material.albedo *
205
+ ndotl *
206
+ PATH_TRACER_INV_PI *
207
+ (1.0 - material.metalness);
208
+ let sun_color = vec3<f32>(10.0, 9.4, 8.6) * max(pathTracerParams.environment_intensity, 0.0001);
209
+ return (diffuse + specular) * sun_color;
210
+ }
211
+
212
+ fn sample_scatter(
213
+ surface_normal: vec3<f32>,
214
+ material: MaterialSample,
215
+ view_direction: vec3<f32>,
216
+ random_state: ptr<function, u32>
217
+ ) -> PathScatter {
218
+ let specular_chance = clamp(
219
+ material.metalness * 0.75 + (1.0 - material.roughness) * 0.35,
220
+ 0.05,
221
+ 0.95
222
+ );
223
+ let choose_specular = random_f32(random_state) < specular_chance;
224
+ let base_reflectance =
225
+ vec3<f32>(0.04) * (1.0 - material.metalness) +
226
+ material.albedo * material.metalness;
227
+
228
+ if (choose_specular) {
229
+ let reflected = reflect(-view_direction, surface_normal);
230
+ let glossy_direction = safe_normalize(
231
+ reflected + sample_unit_sphere(random_state) * material.roughness * 0.35
232
+ );
233
+ let attenuation = fresnel_schlick(
234
+ saturate(dot(surface_normal, view_direction)),
235
+ base_reflectance
236
+ ) / specular_chance;
237
+ return PathScatter(glossy_direction, 1.0, attenuation, 1.0);
238
+ }
239
+
240
+ let diffuse_direction = sample_cosine_hemisphere(surface_normal, random_state);
241
+ let cosine = saturate(dot(surface_normal, diffuse_direction));
242
+ let pdf = max(cosine * PATH_TRACER_INV_PI, PATH_TRACER_EPSILON);
243
+ let attenuation =
244
+ material.albedo *
245
+ (1.0 - material.metalness) *
246
+ cosine /
247
+ max((1.0 - specular_chance) * pdf, PATH_TRACER_EPSILON);
248
+ return PathScatter(diffuse_direction, pdf, attenuation, 0.0);
249
+ }
250
+
251
+ fn make_camera_ray(pixel: vec2<u32>, jitter: vec2<f32>, random_state: ptr<function, u32>) -> Ray {
252
+ let image_size = vec2<f32>(
253
+ max(f32(pathTracerParams.image_width), 1.0),
254
+ max(f32(pathTracerParams.image_height), 1.0)
255
+ );
256
+ let uv = ((vec2<f32>(pixel) + jitter) / image_size) * 2.0 - vec2<f32>(1.0, 1.0);
257
+ let tan_half_fov = tan(pathTracerCamera.vertical_fov_radians * 0.5);
258
+ let sensor_offset =
259
+ pathTracerCamera.right * uv.x * pathTracerCamera.aspect_ratio * tan_half_fov +
260
+ pathTracerCamera.up * (-uv.y) * tan_half_fov;
261
+ let focal_point =
262
+ pathTracerCamera.origin +
263
+ (pathTracerCamera.forward + sensor_offset) * pathTracerCamera.focus_distance;
264
+ var origin = pathTracerCamera.origin;
265
+ if (pathTracerCamera.aperture_radius > 0.0) {
266
+ let lens = sample_unit_disk(random_state) * pathTracerCamera.aperture_radius;
267
+ origin =
268
+ origin +
269
+ pathTracerCamera.right * lens.x +
270
+ pathTracerCamera.up * lens.y;
271
+ }
272
+ return Ray(
273
+ origin,
274
+ PATH_TRACER_EPSILON,
275
+ safe_normalize(focal_point - origin),
276
+ pathTracerScene.max_trace_distance
277
+ );
278
+ }
279
+
280
+ @compute @workgroup_size(8, 8, 1)
281
+ fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
282
+ if (
283
+ global_id.x >= pathTracerParams.image_width ||
284
+ global_id.y >= pathTracerParams.image_height
285
+ ) {
286
+ return;
287
+ }
288
+
289
+ let pixel = global_id.xy;
290
+ let index = pixel_index(pixel);
291
+ let sample_count = max(pathTracerParams.samples_per_pixel, 1u);
292
+ let bounce_count = max(pathTracerParams.max_bounces, 1u);
293
+ var random_state = hash_u32(
294
+ pathTracerParams.frame_index * 1664525u +
295
+ index * 1013904223u +
296
+ 0x6d2b79f5u
297
+ );
298
+ var accumulated_radiance = vec3<f32>(0.0);
299
+ var accumulated_normal = vec3<f32>(0.0);
300
+ var accumulated_albedo = vec3<f32>(0.0);
301
+ var accumulated_distance = 0.0;
302
+ var accumulated_roughness = 0.0;
303
+ var primary_direction = vec3<f32>(0.0, 0.0, 1.0);
304
+ var terminal_direction = primary_direction;
305
+ var terminal_origin = pathTracerCamera.origin;
306
+ var terminal_throughput = vec3<f32>(1.0);
307
+ var last_hit_kind = 0u;
308
+ var hit_samples = 0.0;
309
+
310
+ for (var sample_index = 0u; sample_index < sample_count; sample_index = sample_index + 1u) {
311
+ let jitter = vec2<f32>(random_f32(&random_state), random_f32(&random_state));
312
+ var ray = make_camera_ray(pixel, jitter, &random_state);
313
+ primary_direction = primary_direction + ray.direction;
314
+ var throughput = vec3<f32>(1.0);
315
+ var sample_radiance = vec3<f32>(0.0);
316
+ var captured_primary_hit = false;
317
+ var current_hit_kind = 0u;
318
+
319
+ for (var bounce_index = 0u; bounce_index < bounce_count; bounce_index = bounce_index + 1u) {
320
+ let hit = trace_scene(ray);
321
+ if (hit.hit == 0u) {
322
+ sample_radiance = sample_radiance + throughput * environment_radiance(
323
+ ray.direction,
324
+ pathTracerParams.environment_intensity,
325
+ pathTracerParams.environment_mode
326
+ );
327
+ terminal_origin = ray.origin;
328
+ terminal_direction = ray.direction;
329
+ current_hit_kind = 0u;
330
+ break;
331
+ }
332
+
333
+ let material = load_path_material(hit.material_index);
334
+ let surface_normal = faceforward_normal(hit.normal, ray.direction);
335
+
336
+ if (!captured_primary_hit) {
337
+ accumulated_normal = accumulated_normal + surface_normal;
338
+ accumulated_distance = accumulated_distance + hit.distance;
339
+ accumulated_albedo = accumulated_albedo + material.albedo;
340
+ accumulated_roughness = accumulated_roughness + material.roughness;
341
+ hit_samples = hit_samples + 1.0;
342
+ captured_primary_hit = true;
343
+ }
344
+
345
+ sample_radiance = sample_radiance + throughput * material.emission;
346
+ if (pathTracerParams.enable_next_event_estimation != 0u) {
347
+ sample_radiance = sample_radiance + throughput * evaluate_direct_sun(
348
+ hit,
349
+ material,
350
+ -ray.direction
351
+ );
352
+ }
353
+
354
+ let scatter = sample_scatter(surface_normal, material, -ray.direction, &random_state);
355
+ throughput = throughput * scatter.attenuation;
356
+ if (luminance(throughput) <= 0.0001) {
357
+ terminal_origin = hit.position;
358
+ terminal_direction = scatter.direction;
359
+ current_hit_kind = hit.primitive_kind;
360
+ break;
361
+ }
362
+
363
+ if (bounce_index >= 2u) {
364
+ let russian_roulette = clamp(luminance(throughput), 0.05, 0.95);
365
+ if (random_f32(&random_state) > russian_roulette) {
366
+ terminal_origin = hit.position;
367
+ terminal_direction = scatter.direction;
368
+ current_hit_kind = hit.primitive_kind;
369
+ break;
370
+ }
371
+ throughput = throughput / russian_roulette;
372
+ }
373
+
374
+ ray = Ray(
375
+ hit.position + scatter.direction * PATH_TRACER_EPSILON * 8.0,
376
+ PATH_TRACER_EPSILON,
377
+ scatter.direction,
378
+ pathTracerScene.max_trace_distance
379
+ );
380
+ terminal_origin = ray.origin;
381
+ terminal_direction = ray.direction;
382
+ current_hit_kind = hit.primitive_kind;
383
+ }
384
+
385
+ terminal_throughput = throughput;
386
+ last_hit_kind = max(last_hit_kind, current_hit_kind);
387
+ accumulated_radiance = accumulated_radiance + sample_radiance;
388
+ }
389
+
390
+ let sample_count_f32 = f32(sample_count);
391
+ let averaged_radiance = accumulated_radiance / sample_count_f32;
392
+ let averaged_direction = safe_normalize(primary_direction / sample_count_f32);
393
+ let averaged_normal = select(
394
+ vec3<f32>(0.0, 1.0, 0.0),
395
+ safe_normalize(accumulated_normal / max(hit_samples, 1.0)),
396
+ hit_samples > 0.0
397
+ );
398
+ let averaged_distance = select(
399
+ pathTracerScene.max_trace_distance,
400
+ accumulated_distance / max(hit_samples, 1.0),
401
+ hit_samples > 0.0
402
+ );
403
+ let averaged_albedo = select(
404
+ vec3<f32>(0.0),
405
+ accumulated_albedo / max(hit_samples, 1.0),
406
+ hit_samples > 0.0
407
+ );
408
+ let averaged_roughness = select(
409
+ 1.0,
410
+ accumulated_roughness / max(hit_samples, 1.0),
411
+ hit_samples > 0.0
412
+ );
413
+ let hit_mask = select(0.0, 1.0, hit_samples > 0.0);
414
+
415
+ pathStateBuffer[index] = PathState(
416
+ vec4<f32>(terminal_throughput, f32(bounce_count)),
417
+ vec4<f32>(terminal_origin, hit_mask),
418
+ vec4<f32>(terminal_direction, 1.0),
419
+ random_state,
420
+ last_hit_kind,
421
+ vec2<u32>(0u)
422
+ );
423
+
424
+ pathSampleBuffer[index] = PathSamplePixel(
425
+ vec4<f32>(averaged_radiance * pathTracerParams.exposure, hit_mask),
426
+ vec4<f32>(averaged_direction, averaged_distance),
427
+ vec4<f32>(averaged_normal, averaged_roughness),
428
+ vec4<f32>(averaged_albedo, sample_count_f32)
429
+ );
3
430
  }