@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,8 +1,20 @@
1
+ const PATH_TRACER_PI: f32 = 3.141592653589793;
2
+ const PATH_TRACER_INV_PI: f32 = 0.3183098861837907;
3
+ const PATH_TRACER_EPSILON: f32 = 0.0005;
4
+
1
5
  struct PathTracerParams {
2
6
  frame_index: u32,
3
7
  max_bounces: u32,
4
8
  samples_per_pixel: u32,
5
9
  enable_next_event_estimation: u32,
10
+ image_width: u32,
11
+ image_height: u32,
12
+ accumulation_reset: u32,
13
+ environment_mode: u32,
14
+ environment_intensity: f32,
15
+ exposure: f32,
16
+ history_blend: f32,
17
+ denoise_strength: f32,
6
18
  };
7
19
 
8
20
  struct PathSample {
@@ -10,6 +22,254 @@ struct PathSample {
10
22
  throughput: vec3<f32>,
11
23
  };
12
24
 
25
+ struct PathTracerCamera {
26
+ origin: vec3<f32>,
27
+ aspect_ratio: f32,
28
+ forward: vec3<f32>,
29
+ vertical_fov_radians: f32,
30
+ right: vec3<f32>,
31
+ focus_distance: f32,
32
+ up: vec3<f32>,
33
+ aperture_radius: f32,
34
+ };
35
+
36
+ struct PathTracerSceneMetadata {
37
+ sphere_count: u32,
38
+ triangle_count: u32,
39
+ material_count: u32,
40
+ max_trace_distance: f32,
41
+ };
42
+
43
+ struct PathTracerGroundPlane {
44
+ normal: vec3<f32>,
45
+ height: f32,
46
+ material_index: u32,
47
+ enabled: u32,
48
+ _padding0: vec2<u32>,
49
+ };
50
+
51
+ struct PathTracerMaterial {
52
+ albedo_roughness: vec4<f32>,
53
+ emission_metalness: vec4<f32>,
54
+ transmittance_ior: vec4<f32>,
55
+ };
56
+
57
+ struct PathTracerSphere {
58
+ center_radius: vec4<f32>,
59
+ material_index: u32,
60
+ flags: u32,
61
+ _padding0: vec2<u32>,
62
+ };
63
+
64
+ struct PathTracerTriangle {
65
+ position0: vec4<f32>,
66
+ position1: vec4<f32>,
67
+ position2: vec4<f32>,
68
+ normal0: vec4<f32>,
69
+ normal1: vec4<f32>,
70
+ normal2: vec4<f32>,
71
+ material_index: u32,
72
+ flags: u32,
73
+ _padding0: vec2<u32>,
74
+ };
75
+
76
+ struct Ray {
77
+ origin: vec3<f32>,
78
+ t_min: f32,
79
+ direction: vec3<f32>,
80
+ t_max: f32,
81
+ };
82
+
83
+ struct PathHit {
84
+ hit: u32,
85
+ distance: f32,
86
+ position: vec3<f32>,
87
+ material_index: u32,
88
+ normal: vec3<f32>,
89
+ primitive_kind: u32,
90
+ barycentric: vec3<f32>,
91
+ _padding1: u32,
92
+ };
93
+
94
+ struct MaterialSample {
95
+ albedo: vec3<f32>,
96
+ roughness: f32,
97
+ emission: vec3<f32>,
98
+ metalness: f32,
99
+ transmittance: vec3<f32>,
100
+ refractive_index: f32,
101
+ };
102
+
103
+ struct PathScatter {
104
+ direction: vec3<f32>,
105
+ pdf: f32,
106
+ attenuation: vec3<f32>,
107
+ event_kind: f32,
108
+ };
109
+
110
+ struct PathState {
111
+ throughput_bounce: vec4<f32>,
112
+ origin_active: vec4<f32>,
113
+ direction_pdf: vec4<f32>,
114
+ random_state: u32,
115
+ last_hit_kind: u32,
116
+ _padding0: vec2<u32>,
117
+ };
118
+
119
+ struct PathSamplePixel {
120
+ radiance_opacity: vec4<f32>,
121
+ direction_distance: vec4<f32>,
122
+ normal_roughness: vec4<f32>,
123
+ albedo_sample_count: vec4<f32>,
124
+ };
125
+
126
+ struct PathAccumulationPixel {
127
+ integrated_radiance: vec4<f32>,
128
+ moments: vec4<f32>,
129
+ };
130
+
131
+ struct PathDenoisePixel {
132
+ filtered_radiance: vec4<f32>,
133
+ normal_depth: vec4<f32>,
134
+ albedo_history: vec4<f32>,
135
+ };
136
+
13
137
  fn luminance(value: vec3<f32>) -> f32 {
14
138
  return dot(value, vec3<f32>(0.2126, 0.7152, 0.0722));
15
139
  }
140
+
141
+ fn saturate(value: f32) -> f32 {
142
+ return clamp(value, 0.0, 1.0);
143
+ }
144
+
145
+ fn safe_rcp(value: f32) -> f32 {
146
+ if (abs(value) <= PATH_TRACER_EPSILON) {
147
+ return 0.0;
148
+ }
149
+
150
+ return 1.0 / value;
151
+ }
152
+
153
+ fn safe_normalize(value: vec3<f32>) -> vec3<f32> {
154
+ if (dot(value, value) <= PATH_TRACER_EPSILON) {
155
+ return vec3<f32>(0.0, 1.0, 0.0);
156
+ }
157
+
158
+ return normalize(value);
159
+ }
160
+
161
+ fn make_default_material() -> PathTracerMaterial {
162
+ return PathTracerMaterial(
163
+ vec4<f32>(0.72, 0.74, 0.76, 0.5),
164
+ vec4<f32>(0.0, 0.0, 0.0, 0.0),
165
+ vec4<f32>(0.0, 0.0, 0.0, 1.45)
166
+ );
167
+ }
168
+
169
+ fn unpack_material(material: PathTracerMaterial) -> MaterialSample {
170
+ return MaterialSample(
171
+ material.albedo_roughness.xyz,
172
+ clamp(material.albedo_roughness.w, 0.02, 1.0),
173
+ material.emission_metalness.xyz,
174
+ saturate(material.emission_metalness.w),
175
+ material.transmittance_ior.xyz,
176
+ max(material.transmittance_ior.w, 1.0)
177
+ );
178
+ }
179
+
180
+ fn hash_u32(value: u32) -> u32 {
181
+ var x = value + 0x9e3779b9u;
182
+ x = (x ^ (x >> 16u)) * 0x85ebca6bu;
183
+ x = (x ^ (x >> 13u)) * 0xc2b2ae35u;
184
+ return x ^ (x >> 16u);
185
+ }
186
+
187
+ fn random_f32(state: ptr<function, u32>) -> f32 {
188
+ let next = hash_u32((*state) ^ 0x68bc21ebu);
189
+ *state = next;
190
+ return f32(next) * 2.3283064365386963e-10;
191
+ }
192
+
193
+ fn sample_unit_disk(state: ptr<function, u32>) -> vec2<f32> {
194
+ let radius = sqrt(random_f32(state));
195
+ let angle = 2.0 * PATH_TRACER_PI * random_f32(state);
196
+ return vec2<f32>(cos(angle), sin(angle)) * radius;
197
+ }
198
+
199
+ fn sample_unit_sphere(state: ptr<function, u32>) -> vec3<f32> {
200
+ let z = random_f32(state) * 2.0 - 1.0;
201
+ let angle = 2.0 * PATH_TRACER_PI * random_f32(state);
202
+ let radius = sqrt(max(1.0 - z * z, 0.0));
203
+ return vec3<f32>(radius * cos(angle), radius * sin(angle), z);
204
+ }
205
+
206
+ fn build_tangent(normal: vec3<f32>) -> vec3<f32> {
207
+ let basis = select(
208
+ vec3<f32>(0.0, 1.0, 0.0),
209
+ vec3<f32>(1.0, 0.0, 0.0),
210
+ abs(normal.y) > 0.999
211
+ );
212
+ return safe_normalize(cross(basis, normal));
213
+ }
214
+
215
+ fn build_bitangent(normal: vec3<f32>, tangent: vec3<f32>) -> vec3<f32> {
216
+ return safe_normalize(cross(normal, tangent));
217
+ }
218
+
219
+ fn sample_cosine_hemisphere(normal: vec3<f32>, state: ptr<function, u32>) -> vec3<f32> {
220
+ let disk = sample_unit_disk(state);
221
+ let z = sqrt(max(1.0 - dot(disk, disk), 0.0));
222
+ let tangent = build_tangent(normal);
223
+ let bitangent = build_bitangent(normal, tangent);
224
+ return safe_normalize(tangent * disk.x + bitangent * disk.y + normal * z);
225
+ }
226
+
227
+ fn fresnel_schlick(cos_theta: f32, f0: vec3<f32>) -> vec3<f32> {
228
+ let factor = pow(1.0 - saturate(cos_theta), 5.0);
229
+ return f0 + (vec3<f32>(1.0) - f0) * factor;
230
+ }
231
+
232
+ fn environment_radiance(
233
+ direction: vec3<f32>,
234
+ intensity: f32,
235
+ mode: u32
236
+ ) -> vec3<f32> {
237
+ let up_factor = saturate(direction.y * 0.5 + 0.5);
238
+ let horizon_color = vec3<f32>(0.65, 0.74, 0.86);
239
+ let zenith_color = select(
240
+ vec3<f32>(0.05, 0.12, 0.24),
241
+ vec3<f32>(0.11, 0.08, 0.18),
242
+ mode == 1u
243
+ );
244
+ let sunset_color = vec3<f32>(1.1, 0.64, 0.32);
245
+ let sun_direction = safe_normalize(vec3<f32>(0.35, 0.92, 0.18));
246
+ let moon_direction = safe_normalize(vec3<f32>(-0.2, 0.98, -0.1));
247
+ let sun_glow = pow(saturate(dot(direction, sun_direction)), 256.0);
248
+ let moon_glow = pow(saturate(dot(direction, moon_direction)), 512.0);
249
+ var sky = horizon_color * (1.0 - up_factor) + zenith_color * up_factor;
250
+ if (mode == 1u) {
251
+ sky = sky * (1.0 - up_factor * 0.4) + sunset_color * sun_glow * 0.25;
252
+ }
253
+ return (
254
+ sky +
255
+ vec3<f32>(8.0, 7.6, 6.8) * sun_glow +
256
+ vec3<f32>(0.7, 0.76, 0.9) * moon_glow * 0.3
257
+ ) * max(intensity, 0.0001);
258
+ }
259
+
260
+ fn miss_hit(ray: Ray) -> PathHit {
261
+ return PathHit(
262
+ 0u,
263
+ ray.t_max,
264
+ ray.origin + ray.direction * ray.t_max,
265
+ 0u,
266
+ vec3<f32>(0.0, 1.0, 0.0),
267
+ 0u,
268
+ vec3<f32>(0.0),
269
+ 0u
270
+ );
271
+ }
272
+
273
+ fn faceforward_normal(normal: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
274
+ return select(normal, -normal, dot(normal, direction) > 0.0);
275
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/gpu-lighting",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Advanced lighting WGSL modules and planning profiles for @plasius/gpu-worker.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
package/src/index.js CHANGED
@@ -131,6 +131,16 @@ export const lightingProfiles = Object.freeze(
131
131
  export const lightingProfileNames = Object.freeze(Object.keys(lightingProfiles));
132
132
 
133
133
  export const defaultLightingProfile = "realtime";
134
+ export const lightingProfileModeOrder = Object.freeze([
135
+ "realtime",
136
+ "hybrid",
137
+ "reference",
138
+ ]);
139
+ export const defaultAdaptiveLightingProfilePolicy = Object.freeze({
140
+ preferredProfile: "reference",
141
+ minimumFrameRate: 30,
142
+ sampleWindowSize: 4,
143
+ });
134
144
  export const lightingDistanceBands = Object.freeze([
135
145
  "near",
136
146
  "mid",
@@ -199,6 +209,23 @@ function assertLightingImportance(name, value) {
199
209
  return value;
200
210
  }
201
211
 
212
+ function readPositiveIntegerOption(name, value, fallback) {
213
+ if (value === undefined) {
214
+ return fallback;
215
+ }
216
+
217
+ if (
218
+ typeof value !== "number" ||
219
+ !Number.isFinite(value) ||
220
+ value <= 0 ||
221
+ Math.round(value) !== value
222
+ ) {
223
+ throw new Error(`${name} must be a positive integer.`);
224
+ }
225
+
226
+ return value;
227
+ }
228
+
202
229
  function resolveBandParticipation(profileName, band, importance) {
203
230
  const referenceProfile = profileName === "reference";
204
231
  const premiumImportance =
@@ -276,6 +303,93 @@ export function createLightingBandPlan(options = {}) {
276
303
  });
277
304
  }
278
305
 
306
+ const lightingProfileModeEstimatedCostMs = Object.freeze({
307
+ realtime: 4.5,
308
+ hybrid: 7.5,
309
+ reference: 12.5,
310
+ });
311
+
312
+ export function createLightingProfileModeLadder(options = {}) {
313
+ const moduleId =
314
+ typeof options.id === "string" && options.id.trim().length > 0
315
+ ? options.id.trim()
316
+ : "lighting-profile-mode";
317
+ const preferredProfile = getLightingProfile(
318
+ options.preferredProfile ?? defaultAdaptiveLightingProfilePolicy.preferredProfile
319
+ ).name;
320
+ const initialProfile = getLightingProfile(
321
+ options.initialProfile ?? preferredProfile
322
+ ).name;
323
+ const minimumFrameRate = readPositiveIntegerOption(
324
+ "minimumFrameRate",
325
+ options.minimumFrameRate,
326
+ defaultAdaptiveLightingProfilePolicy.minimumFrameRate
327
+ );
328
+ const sampleWindowSize = readPositiveIntegerOption(
329
+ "sampleWindowSize",
330
+ options.sampleWindowSize,
331
+ defaultAdaptiveLightingProfilePolicy.sampleWindowSize
332
+ );
333
+ const importance = assertLightingImportance(
334
+ "importance",
335
+ options.importance ?? "high"
336
+ );
337
+ const moduleImportance = assertLightingImportance(
338
+ "moduleImportance",
339
+ options.moduleImportance ?? "critical"
340
+ );
341
+
342
+ const levels = Object.freeze(
343
+ lightingProfileModeOrder.map((profileName) => {
344
+ const profile = getLightingProfile(profileName);
345
+ return Object.freeze({
346
+ id: profile.name,
347
+ estimatedCostMs: lightingProfileModeEstimatedCostMs[profile.name],
348
+ config: Object.freeze({
349
+ profile: profile.name,
350
+ description: profile.description,
351
+ techniques: Object.freeze([...profile.techniques]),
352
+ lightingBandPlan: createLightingBandPlan({
353
+ profile: profile.name,
354
+ importance,
355
+ }),
356
+ policy: Object.freeze({
357
+ preferredProfile,
358
+ minimumFrameRate,
359
+ sampleWindowSize,
360
+ }),
361
+ }),
362
+ });
363
+ })
364
+ );
365
+
366
+ return Object.freeze({
367
+ id: moduleId,
368
+ domain: "lighting",
369
+ authority: "visual",
370
+ importance: moduleImportance,
371
+ initialLevel: initialProfile,
372
+ levels,
373
+ target: Object.freeze({
374
+ minimumFrameRate,
375
+ maximumFrameRate: minimumFrameRate,
376
+ preferredFrameRates: Object.freeze([minimumFrameRate]),
377
+ }),
378
+ adaptation: Object.freeze({
379
+ sampleWindowSize,
380
+ minimumSamplesBeforeAdjustment: sampleWindowSize,
381
+ degradeCooldownFrames: 1,
382
+ upgradeCooldownFrames: sampleWindowSize,
383
+ minStableFramesForRecovery: sampleWindowSize,
384
+ }),
385
+ policy: Object.freeze({
386
+ preferredProfile,
387
+ minimumFrameRate,
388
+ sampleWindowSize,
389
+ }),
390
+ });
391
+ }
392
+
279
393
  function buildWorkerBudgetLevels(jobType, queueClass, presets) {
280
394
  return Object.freeze([
281
395
  Object.freeze({
@@ -1,3 +1,70 @@
1
- fn process_job() {
2
- // Placeholder direct lighting resolve for the hybrid technique.
1
+ @group(0) @binding(0) var<uniform> hybridFrameParams: HybridFrameParams;
2
+ @group(0) @binding(1) var<uniform> hybridReflectionCamera: HybridReflectionCamera;
3
+ @group(0) @binding(2) var<storage, read> hybridReflectionSurfaces: array<HybridReflectionSurface>;
4
+ @group(0) @binding(3) var<storage, read_write> hybridDirectLightingOutput: array<HybridLightingPixel>;
5
+
6
+ fn direct_lighting_index(pixel: vec2<u32>) -> u32 {
7
+ return pixel.y * max(hybridFrameParams.image_width, 1u) + pixel.x;
8
+ }
9
+
10
+ fn evaluate_direct_sun(
11
+ normal: vec3<f32>,
12
+ view_direction: vec3<f32>,
13
+ albedo: vec3<f32>,
14
+ roughness: f32,
15
+ metalness: f32
16
+ ) -> vec3<f32> {
17
+ let sun_direction = hybrid_safe_normalize(vec3<f32>(0.31, 0.92, 0.22));
18
+ let ndotl = hybrid_saturate(dot(normal, sun_direction));
19
+ if (ndotl <= 0.0) {
20
+ return vec3<f32>(0.0);
21
+ }
22
+
23
+ let halfway = hybrid_safe_normalize(sun_direction + view_direction);
24
+ let f0 = hybrid_surface_f0(albedo, metalness);
25
+ let fresnel = hybrid_fresnel_schlick(
26
+ hybrid_saturate(dot(normal, view_direction)),
27
+ f0
28
+ );
29
+ let diffuse = albedo * ndotl * 0.3183098861837907 * (1.0 - metalness);
30
+ let specular_power = 10.0 + (1.0 - roughness) * 112.0;
31
+ let specular = fresnel * pow(hybrid_saturate(dot(normal, halfway)), specular_power) * ndotl;
32
+ let sun_color = vec3<f32>(6.2, 5.8, 5.1) * max(hybridFrameParams.sky_intensity, 0.0001);
33
+ return (diffuse + specular) * sun_color;
34
+ }
35
+
36
+ @compute @workgroup_size(8, 8, 1)
37
+ fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
38
+ if (
39
+ global_id.x >= hybridFrameParams.image_width ||
40
+ global_id.y >= hybridFrameParams.image_height
41
+ ) {
42
+ return;
43
+ }
44
+
45
+ let index = direct_lighting_index(global_id.xy);
46
+ let surface = hybridReflectionSurfaces[index];
47
+ let position = surface.position.xyz;
48
+ let normal = hybrid_safe_normalize(surface.normal_roughness.xyz);
49
+ let roughness = clamp(surface.normal_roughness.w + hybridFrameParams.roughness_bias, 0.02, 1.0);
50
+ let albedo = surface.albedo_metalness.xyz;
51
+ let metalness = hybrid_saturate(surface.albedo_metalness.w);
52
+ let emission = surface.emission_occlusion.xyz;
53
+ let occlusion = hybrid_saturate(surface.emission_occlusion.w);
54
+ let view_direction = hybrid_safe_normalize(hybridReflectionCamera.position - position);
55
+ let direct_sun = evaluate_direct_sun(normal, view_direction, albedo, roughness, metalness);
56
+ let sky_fill = hybrid_environment(normal, hybridFrameParams.sky_intensity, hybridFrameParams.sky_mode);
57
+ let grazing = pow(1.0 - hybrid_saturate(dot(normal, view_direction)), 4.0);
58
+ let ambient = sky_fill * (0.08 + grazing * 0.06) * occlusion;
59
+ let radiance = emission + direct_sun * occlusion + ambient;
60
+ let confidence = clamp(
61
+ hybrid_luminance(radiance) * 0.045 + occlusion * 0.35 + (1.0 - roughness) * 0.15,
62
+ 0.0,
63
+ 1.0
64
+ );
65
+
66
+ hybridDirectLightingOutput[index] = HybridLightingPixel(
67
+ vec4<f32>(radiance * hybridFrameParams.exposure, confidence),
68
+ vec4<f32>(normal, occlusion)
69
+ );
3
70
  }
@@ -1,3 +1,63 @@
1
- fn process_job() {
2
- // Placeholder final gather stage that combines cache data and traces.
1
+ @group(0) @binding(0) var<uniform> hybridFrameParams: HybridFrameParams;
2
+ @group(0) @binding(1) var<storage, read> hybridReflectionSurfaces: array<HybridReflectionSurface>;
3
+ @group(0) @binding(2) var<storage, read> hybridDirectLightingInput: array<HybridLightingPixel>;
4
+ @group(0) @binding(3) var<storage, read> hybridScreenTraceInput: array<HybridScreenTracePixel>;
5
+ @group(0) @binding(4) var<storage, read> hybridRadianceCacheInput: array<HybridRadianceCacheEntry>;
6
+ @group(0) @binding(5) var<storage, read_write> hybridFinalGatherOutput: array<HybridLightingPixel>;
7
+
8
+ fn final_gather_index(pixel: vec2<u32>) -> u32 {
9
+ return pixel.y * max(hybridFrameParams.image_width, 1u) + pixel.x;
10
+ }
11
+
12
+ @compute @workgroup_size(8, 8, 1)
13
+ fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
14
+ if (
15
+ global_id.x >= hybridFrameParams.image_width ||
16
+ global_id.y >= hybridFrameParams.image_height
17
+ ) {
18
+ return;
19
+ }
20
+
21
+ let index = final_gather_index(global_id.xy);
22
+ let surface = hybridReflectionSurfaces[index];
23
+ let direct = hybridDirectLightingInput[index];
24
+ let trace = hybridScreenTraceInput[index];
25
+ let cache = hybridRadianceCacheInput[index];
26
+ let previous = hybridFinalGatherOutput[index];
27
+ let normal = hybrid_safe_normalize(surface.normal_roughness.xyz);
28
+ let roughness = clamp(surface.normal_roughness.w + hybridFrameParams.roughness_bias, 0.02, 1.0);
29
+ let metalness = hybrid_saturate(surface.albedo_metalness.w);
30
+ let emission = surface.emission_occlusion.xyz;
31
+ let occlusion = hybrid_saturate(surface.emission_occlusion.w);
32
+ let direct_radiance = direct.radiance_confidence.xyz;
33
+ let trace_radiance = trace.radiance_confidence.xyz;
34
+ let cache_irradiance = cache.irradiance_validity.xyz;
35
+ let indirect_gi =
36
+ cache_irradiance * occlusion * (0.32 + (1.0 - roughness) * 0.28);
37
+ let reflection_term =
38
+ trace_radiance *
39
+ trace.radiance_confidence.w *
40
+ (0.18 + (1.0 - roughness) * 0.42 + metalness * 0.25);
41
+ let ambient = hybrid_environment(normal, hybridFrameParams.sky_intensity, hybridFrameParams.sky_mode) * 0.05 * occlusion;
42
+ let current_radiance = direct_radiance + indirect_gi + reflection_term + emission + ambient;
43
+ let history_weight = select(
44
+ 0.0,
45
+ encode_history_weight(hybridFrameParams.history_weight),
46
+ hybridFrameParams.reflection_reset == 0u && previous.radiance_confidence.w > 0.0
47
+ );
48
+ let resolved_radiance =
49
+ previous.radiance_confidence.xyz * history_weight +
50
+ current_radiance * (1.0 - history_weight);
51
+ let confidence = clamp(
52
+ direct.radiance_confidence.w * 0.4 +
53
+ cache.irradiance_validity.w * 0.3 +
54
+ trace.radiance_confidence.w * 0.3,
55
+ 0.0,
56
+ 1.0
57
+ );
58
+
59
+ hybridFinalGatherOutput[index] = HybridLightingPixel(
60
+ vec4<f32>(resolved_radiance, confidence),
61
+ vec4<f32>(normal, occlusion)
62
+ );
3
63
  }
@@ -3,6 +3,14 @@ struct HybridFrameParams {
3
3
  max_trace_steps: u32,
4
4
  history_weight: f32,
5
5
  exposure: f32,
6
+ image_width: u32,
7
+ image_height: u32,
8
+ reflection_reset: u32,
9
+ sky_mode: u32,
10
+ sky_intensity: f32,
11
+ max_reflection_distance: f32,
12
+ roughness_bias: f32,
13
+ thickness: f32,
6
14
  };
7
15
 
8
16
  struct HybridHit {
@@ -10,6 +18,138 @@ struct HybridHit {
10
18
  hit_distance: f32,
11
19
  };
12
20
 
21
+ struct HybridReflectionCamera {
22
+ position: vec3<f32>,
23
+ _padding0: f32,
24
+ };
25
+
26
+ struct HybridReflectionSurface {
27
+ position: vec4<f32>,
28
+ normal_roughness: vec4<f32>,
29
+ albedo_metalness: vec4<f32>,
30
+ emission_occlusion: vec4<f32>,
31
+ };
32
+
33
+ struct HybridGroundPlane {
34
+ normal: vec3<f32>,
35
+ height: f32,
36
+ material_index: u32,
37
+ enabled: u32,
38
+ _padding0: vec2<u32>,
39
+ };
40
+
41
+ struct HybridReflectionSceneMetadata {
42
+ sphere_count: u32,
43
+ material_count: u32,
44
+ _padding0: vec2<u32>,
45
+ max_trace_distance: f32,
46
+ };
47
+
48
+ struct HybridReflectionMaterial {
49
+ albedo_roughness: vec4<f32>,
50
+ emission_metalness: vec4<f32>,
51
+ };
52
+
53
+ struct HybridReflectionSphere {
54
+ center_radius: vec4<f32>,
55
+ material_index: u32,
56
+ flags: u32,
57
+ _padding0: vec2<u32>,
58
+ };
59
+
60
+ struct HybridReflectionTrace {
61
+ hit_mask: u32,
62
+ distance: f32,
63
+ position: vec3<f32>,
64
+ material_index: u32,
65
+ normal: vec3<f32>,
66
+ _padding1: u32,
67
+ };
68
+
69
+ struct HybridReflectionPixel {
70
+ reflection_confidence: vec4<f32>,
71
+ hit_normal_distance: vec4<f32>,
72
+ };
73
+
74
+ struct HybridLightingPixel {
75
+ radiance_confidence: vec4<f32>,
76
+ normal_occlusion: vec4<f32>,
77
+ };
78
+
79
+ struct HybridScreenTracePixel {
80
+ radiance_confidence: vec4<f32>,
81
+ hit_normal_distance: vec4<f32>,
82
+ };
83
+
84
+ struct HybridRadianceCacheEntry {
85
+ irradiance_validity: vec4<f32>,
86
+ bent_normal_depth: vec4<f32>,
87
+ };
88
+
13
89
  fn encode_history_weight(value: f32) -> f32 {
14
90
  return clamp(value, 0.0, 1.0);
15
91
  }
92
+
93
+ fn hybrid_saturate(value: f32) -> f32 {
94
+ return clamp(value, 0.0, 1.0);
95
+ }
96
+
97
+ fn hybrid_safe_normalize(value: vec3<f32>) -> vec3<f32> {
98
+ if (dot(value, value) <= 0.000001) {
99
+ return vec3<f32>(0.0, 1.0, 0.0);
100
+ }
101
+
102
+ return normalize(value);
103
+ }
104
+
105
+ fn hybrid_fresnel_schlick(cos_theta: f32, f0: vec3<f32>) -> vec3<f32> {
106
+ let factor = pow(1.0 - hybrid_saturate(cos_theta), 5.0);
107
+ return f0 + (vec3<f32>(1.0) - f0) * factor;
108
+ }
109
+
110
+ fn hybrid_surface_f0(albedo: vec3<f32>, metalness: f32) -> vec3<f32> {
111
+ return vec3<f32>(0.04) * (1.0 - metalness) + albedo * metalness;
112
+ }
113
+
114
+ fn hybrid_luminance(color: vec3<f32>) -> f32 {
115
+ return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));
116
+ }
117
+
118
+ fn hybrid_hash_u32(value: u32) -> u32 {
119
+ var x = value + 0x9e3779b9u;
120
+ x = (x ^ (x >> 16u)) * 0x85ebca6bu;
121
+ x = (x ^ (x >> 13u)) * 0xc2b2ae35u;
122
+ return x ^ (x >> 16u);
123
+ }
124
+
125
+ fn hybrid_random_f32(state: ptr<function, u32>) -> f32 {
126
+ let next = hybrid_hash_u32((*state) ^ 0x7f4a7c15u);
127
+ *state = next;
128
+ return f32(next) * 2.3283064365386963e-10;
129
+ }
130
+
131
+ fn hybrid_sample_unit_sphere(state: ptr<function, u32>) -> vec3<f32> {
132
+ let z = hybrid_random_f32(state) * 2.0 - 1.0;
133
+ let angle = 6.283185307179586 * hybrid_random_f32(state);
134
+ let radius = sqrt(max(1.0 - z * z, 0.0));
135
+ return vec3<f32>(radius * cos(angle), radius * sin(angle), z);
136
+ }
137
+
138
+ fn hybrid_environment(direction: vec3<f32>, intensity: f32, mode: u32) -> vec3<f32> {
139
+ let up_factor = hybrid_saturate(direction.y * 0.5 + 0.5);
140
+ let horizon_color = vec3<f32>(0.54, 0.64, 0.77);
141
+ let zenith_color = select(
142
+ vec3<f32>(0.04, 0.09, 0.18),
143
+ vec3<f32>(0.09, 0.07, 0.16),
144
+ mode == 1u
145
+ );
146
+ let sun_direction = hybrid_safe_normalize(vec3<f32>(0.31, 0.92, 0.22));
147
+ let sun_glow = pow(hybrid_saturate(dot(direction, sun_direction)), 192.0);
148
+ let sunset_bias = select(vec3<f32>(0.0), vec3<f32>(0.55, 0.24, 0.08) * (1.0 - up_factor), mode == 1u);
149
+ return (
150
+ horizon_color * (1.0 - up_factor) +
151
+ zenith_color * up_factor +
152
+ sunset_bias +
153
+ vec3<f32>(5.8, 5.5, 5.0) * sun_glow
154
+ ) * max(intensity, 0.0001);
155
+ }