@plasius/gpu-lighting 0.1.17 → 0.1.19

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.
@@ -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
  }
@@ -71,6 +71,21 @@ struct HybridReflectionPixel {
71
71
  hit_normal_distance: vec4<f32>,
72
72
  };
73
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
+
74
89
  fn encode_history_weight(value: f32) -> f32 {
75
90
  return clamp(value, 0.0, 1.0);
76
91
  }
@@ -92,6 +107,14 @@ fn hybrid_fresnel_schlick(cos_theta: f32, f0: vec3<f32>) -> vec3<f32> {
92
107
  return f0 + (vec3<f32>(1.0) - f0) * factor;
93
108
  }
94
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
+
95
118
  fn hybrid_hash_u32(value: u32) -> u32 {
96
119
  var x = value + 0x9e3779b9u;
97
120
  x = (x ^ (x >> 16u)) * 0x85ebca6bu;
@@ -1,3 +1,58 @@
1
- fn process_job() {
2
- // Placeholder radiance cache update stage for indirect lighting reuse.
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> hybridRadianceCacheHistory: array<HybridRadianceCacheEntry>;
5
+ @group(0) @binding(4) var<storage, read_write> hybridRadianceCacheOutput: array<HybridRadianceCacheEntry>;
6
+
7
+ fn radiance_cache_index(pixel: vec2<u32>) -> u32 {
8
+ return pixel.y * max(hybridFrameParams.image_width, 1u) + pixel.x;
9
+ }
10
+
11
+ @compute @workgroup_size(8, 8, 1)
12
+ fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
13
+ if (
14
+ global_id.x >= hybridFrameParams.image_width ||
15
+ global_id.y >= hybridFrameParams.image_height
16
+ ) {
17
+ return;
18
+ }
19
+
20
+ let index = radiance_cache_index(global_id.xy);
21
+ let surface = hybridReflectionSurfaces[index];
22
+ let direct = hybridDirectLightingInput[index];
23
+ let previous = hybridRadianceCacheHistory[index];
24
+ let normal = hybrid_safe_normalize(surface.normal_roughness.xyz);
25
+ let roughness = clamp(surface.normal_roughness.w + hybridFrameParams.roughness_bias, 0.02, 1.0);
26
+ let occlusion = hybrid_saturate(surface.emission_occlusion.w);
27
+ let sky_probe = hybrid_environment(normal, hybridFrameParams.sky_intensity, hybridFrameParams.sky_mode);
28
+ let direct_irradiance = direct.radiance_confidence.xyz * (0.28 + (1.0 - roughness) * 0.12);
29
+ let cache_fill = sky_probe * (0.08 + occlusion * 0.12);
30
+ let current_irradiance = direct_irradiance + cache_fill;
31
+ let bent_normal = hybrid_safe_normalize(
32
+ normal * (0.75 + occlusion * 0.25) +
33
+ previous.bent_normal_depth.xyz * previous.irradiance_validity.w * 0.2
34
+ );
35
+ let history_weight = select(
36
+ 0.0,
37
+ encode_history_weight(hybridFrameParams.history_weight) * previous.irradiance_validity.w,
38
+ hybridFrameParams.reflection_reset == 0u && previous.irradiance_validity.w > 0.0
39
+ );
40
+ let resolved_irradiance =
41
+ previous.irradiance_validity.xyz * history_weight +
42
+ current_irradiance * (1.0 - history_weight);
43
+ let validity = clamp(
44
+ hybrid_luminance(resolved_irradiance) * 0.05 +
45
+ occlusion * 0.35 +
46
+ (1.0 - roughness) * 0.15,
47
+ 0.0,
48
+ 1.0
49
+ );
50
+ let probe_depth =
51
+ previous.bent_normal_depth.w * history_weight +
52
+ length(surface.position.xyz) * (1.0 - history_weight);
53
+
54
+ hybridRadianceCacheOutput[index] = HybridRadianceCacheEntry(
55
+ vec4<f32>(resolved_irradiance, validity),
56
+ vec4<f32>(bent_normal, probe_depth)
57
+ );
3
58
  }
@@ -1,3 +1,220 @@
1
- fn process_job() {
2
- // Placeholder screen-space tracing stage for first-hit reuse.
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> hybridScreenTraceHistory: array<HybridScreenTracePixel>;
5
+ @group(0) @binding(4) var<storage, read> hybridReflectionScene: HybridReflectionSceneMetadata;
6
+ @group(0) @binding(5) var<uniform> hybridGroundPlane: HybridGroundPlane;
7
+ @group(0) @binding(6) var<storage, read> hybridReflectionMaterials: array<HybridReflectionMaterial>;
8
+ @group(0) @binding(7) var<storage, read> hybridReflectionSpheres: array<HybridReflectionSphere>;
9
+ @group(0) @binding(8) var<storage, read_write> hybridScreenTraceOutput: array<HybridScreenTracePixel>;
10
+
11
+ fn screen_trace_index(pixel: vec2<u32>) -> u32 {
12
+ return pixel.y * max(hybridFrameParams.image_width, 1u) + pixel.x;
13
+ }
14
+
15
+ fn unpack_reflection_material(index: u32) -> HybridReflectionMaterial {
16
+ if (hybridReflectionScene.material_count == 0u) {
17
+ return HybridReflectionMaterial(
18
+ vec4<f32>(0.7, 0.72, 0.76, 0.4),
19
+ vec4<f32>(0.0, 0.0, 0.0, 0.0)
20
+ );
21
+ }
22
+
23
+ let safe_index = min(index, hybridReflectionScene.material_count - 1u);
24
+ return hybridReflectionMaterials[safe_index];
25
+ }
26
+
27
+ fn miss_trace() -> HybridReflectionTrace {
28
+ return HybridReflectionTrace(
29
+ 0u,
30
+ hybridReflectionScene.max_trace_distance,
31
+ vec3<f32>(0.0),
32
+ 0u,
33
+ vec3<f32>(0.0, 1.0, 0.0),
34
+ 0u
35
+ );
36
+ }
37
+
38
+ fn set_best_trace(best: ptr<function, HybridReflectionTrace>, candidate: HybridReflectionTrace) {
39
+ if (candidate.hit_mask == 1u && candidate.distance < (*best).distance) {
40
+ *best = candidate;
41
+ }
42
+ }
43
+
44
+ fn trace_ground(
45
+ origin: vec3<f32>,
46
+ direction: vec3<f32>,
47
+ best: ptr<function, HybridReflectionTrace>
48
+ ) {
49
+ if (hybridGroundPlane.enabled == 0u) {
50
+ return;
51
+ }
52
+
53
+ let plane_normal = hybrid_safe_normalize(hybridGroundPlane.normal);
54
+ let denominator = dot(plane_normal, direction);
55
+ if (abs(denominator) <= 0.0005) {
56
+ return;
57
+ }
58
+
59
+ let distance = -(dot(plane_normal, origin) + hybridGroundPlane.height) / denominator;
60
+ if (distance <= 0.0005 || distance >= (*best).distance || distance >= hybridReflectionScene.max_trace_distance) {
61
+ return;
62
+ }
63
+
64
+ set_best_trace(
65
+ best,
66
+ HybridReflectionTrace(
67
+ 1u,
68
+ distance,
69
+ origin + direction * distance,
70
+ hybridGroundPlane.material_index,
71
+ plane_normal,
72
+ 0u
73
+ )
74
+ );
75
+ }
76
+
77
+ fn trace_spheres(
78
+ origin: vec3<f32>,
79
+ direction: vec3<f32>,
80
+ best: ptr<function, HybridReflectionTrace>
81
+ ) {
82
+ var index = 0u;
83
+ loop {
84
+ if (index >= hybridReflectionScene.sphere_count) {
85
+ break;
86
+ }
87
+
88
+ let sphere = hybridReflectionSpheres[index];
89
+ let offset = origin - sphere.center_radius.xyz;
90
+ let a = dot(direction, direction);
91
+ let half_b = dot(offset, direction);
92
+ let c = dot(offset, offset) - sphere.center_radius.w * sphere.center_radius.w;
93
+ let discriminant = half_b * half_b - a * c;
94
+ if (discriminant >= 0.0) {
95
+ let root = sqrt(discriminant);
96
+ var distance = (-half_b - root) / max(a, 0.0005);
97
+ if (distance <= 0.0005) {
98
+ distance = (-half_b + root) / max(a, 0.0005);
99
+ }
100
+ if (distance > 0.0005 && distance < (*best).distance && distance < hybridReflectionScene.max_trace_distance) {
101
+ let position = origin + direction * distance;
102
+ let normal = hybrid_safe_normalize(position - sphere.center_radius.xyz);
103
+ set_best_trace(
104
+ best,
105
+ HybridReflectionTrace(
106
+ 1u,
107
+ distance,
108
+ position,
109
+ sphere.material_index,
110
+ normal,
111
+ 0u
112
+ )
113
+ );
114
+ }
115
+ }
116
+
117
+ index = index + 1u;
118
+ }
119
+ }
120
+
121
+ fn trace_screen_scene(origin: vec3<f32>, direction: vec3<f32>) -> HybridReflectionTrace {
122
+ var best = miss_trace();
123
+ trace_ground(origin, direction, &best);
124
+ trace_spheres(origin, direction, &best);
125
+ return best;
126
+ }
127
+
128
+ fn evaluate_hit_radiance(trace: HybridReflectionTrace, reflection_direction: vec3<f32>) -> vec3<f32> {
129
+ let material = unpack_reflection_material(trace.material_index);
130
+ let normal = hybrid_safe_normalize(trace.normal);
131
+ let view_direction = -reflection_direction;
132
+ let light_direction = hybrid_safe_normalize(vec3<f32>(0.31, 0.92, 0.22));
133
+ let ndotl = hybrid_saturate(dot(normal, light_direction));
134
+ let halfway = hybrid_safe_normalize(light_direction + view_direction);
135
+ let roughness = clamp(material.albedo_roughness.w, 0.02, 1.0);
136
+ let metalness = hybrid_saturate(material.emission_metalness.w);
137
+ let albedo = material.albedo_roughness.xyz;
138
+ let fresnel = hybrid_fresnel_schlick(
139
+ hybrid_saturate(dot(normal, view_direction)),
140
+ hybrid_surface_f0(albedo, metalness)
141
+ );
142
+ let diffuse = albedo * ndotl * 0.3183098861837907 * (1.0 - metalness);
143
+ let specular = fresnel * pow(hybrid_saturate(dot(normal, halfway)), 12.0 + (1.0 - roughness) * 84.0) * ndotl;
144
+ let sky_fill = hybrid_environment(normal, hybridFrameParams.sky_intensity, hybridFrameParams.sky_mode);
145
+ return material.emission_metalness.xyz + (diffuse + specular) * vec3<f32>(4.8, 4.5, 4.1) + sky_fill * 0.1;
146
+ }
147
+
148
+ @compute @workgroup_size(8, 8, 1)
149
+ fn process_job(@builtin(global_invocation_id) global_id: vec3<u32>) {
150
+ if (
151
+ global_id.x >= hybridFrameParams.image_width ||
152
+ global_id.y >= hybridFrameParams.image_height
153
+ ) {
154
+ return;
155
+ }
156
+
157
+ let index = screen_trace_index(global_id.xy);
158
+ let surface = hybridReflectionSurfaces[index];
159
+ let previous = hybridScreenTraceHistory[index];
160
+ let position = surface.position.xyz;
161
+ let normal = hybrid_safe_normalize(surface.normal_roughness.xyz);
162
+ let roughness = clamp(surface.normal_roughness.w + hybridFrameParams.roughness_bias, 0.02, 1.0);
163
+ let albedo = surface.albedo_metalness.xyz;
164
+ let metalness = hybrid_saturate(surface.albedo_metalness.w);
165
+ let occlusion = hybrid_saturate(surface.emission_occlusion.w);
166
+ let view_direction = hybrid_safe_normalize(hybridReflectionCamera.position - position);
167
+ var random_state = hybrid_hash_u32(
168
+ hybridFrameParams.frame_index * 1664525u +
169
+ index * 1013904223u +
170
+ 0x68bc21ebu
171
+ );
172
+ let reflected_direction = hybrid_safe_normalize(
173
+ reflect(-view_direction, normal) +
174
+ hybrid_sample_unit_sphere(&random_state) * roughness * roughness * 0.25
175
+ );
176
+ let trace = trace_screen_scene(
177
+ position + normal * max(hybridFrameParams.thickness, 0.0005),
178
+ reflected_direction
179
+ );
180
+ let sky_fallback = hybrid_environment(
181
+ reflected_direction,
182
+ hybridFrameParams.sky_intensity,
183
+ hybridFrameParams.sky_mode
184
+ );
185
+ let hit_radiance = select(
186
+ sky_fallback,
187
+ evaluate_hit_radiance(trace, reflected_direction),
188
+ trace.hit_mask == 1u
189
+ );
190
+ let reflection_budget = clamp(
191
+ max(hybrid_surface_f0(albedo, metalness).x, max(albedo.y * metalness, albedo.z * metalness)) +
192
+ (1.0 - roughness) * 0.45,
193
+ 0.0,
194
+ 1.0
195
+ );
196
+ let trace_confidence = clamp(
197
+ reflection_budget * occlusion * select(0.35, 0.9, trace.hit_mask == 1u),
198
+ 0.0,
199
+ 1.0
200
+ );
201
+ let history_weight = select(
202
+ 0.0,
203
+ encode_history_weight(hybridFrameParams.history_weight),
204
+ hybridFrameParams.reflection_reset == 0u && previous.radiance_confidence.w > 0.0
205
+ );
206
+ let resolved_radiance =
207
+ previous.radiance_confidence.xyz * history_weight +
208
+ hit_radiance * trace_confidence * (1.0 - history_weight);
209
+ let resolved_normal = select(normal, hybrid_safe_normalize(trace.normal), trace.hit_mask == 1u);
210
+ let resolved_distance = select(
211
+ hybridFrameParams.max_reflection_distance,
212
+ trace.distance,
213
+ trace.hit_mask == 1u
214
+ );
215
+
216
+ hybridScreenTraceOutput[index] = HybridScreenTracePixel(
217
+ vec4<f32>(resolved_radiance * hybridFrameParams.exposure, trace_confidence),
218
+ vec4<f32>(resolved_normal, resolved_distance)
219
+ );
3
220
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/gpu-lighting",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
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
@@ -136,6 +136,11 @@ export const lightingProfileModeOrder = Object.freeze([
136
136
  "hybrid",
137
137
  "reference",
138
138
  ]);
139
+ export const lightingEnvironmentPresetNames = Object.freeze([
140
+ "moonlit-harbor",
141
+ "product-studio",
142
+ "neutral-studio",
143
+ ]);
139
144
  export const defaultAdaptiveLightingProfilePolicy = Object.freeze({
140
145
  preferredProfile: "reference",
141
146
  minimumFrameRate: 30,
@@ -151,6 +156,153 @@ export const lightingDistanceBands = Object.freeze([
151
156
  export const lightingWorkerQueueClass = "lighting";
152
157
  export const lightingDebugOwner = "lighting";
153
158
 
159
+ function freezeVec4(value) {
160
+ return Object.freeze([value[0], value[1], value[2], value[3] ?? 1]);
161
+ }
162
+
163
+ function normalizeVector3(value, fallback) {
164
+ if (!Array.isArray(value) || value.length < 3) {
165
+ return [...fallback];
166
+ }
167
+ const vector = [
168
+ Number.isFinite(value[0]) ? value[0] : fallback[0],
169
+ Number.isFinite(value[1]) ? value[1] : fallback[1],
170
+ Number.isFinite(value[2]) ? value[2] : fallback[2],
171
+ ];
172
+ const length = Math.hypot(vector[0], vector[1], vector[2]);
173
+ if (!Number.isFinite(length) || length <= 0.000001) {
174
+ return [...fallback];
175
+ }
176
+ return vector.map((component) => component / length);
177
+ }
178
+
179
+ function readColor(value, fallback) {
180
+ if (!Array.isArray(value) || value.length < 3) {
181
+ return freezeVec4(fallback);
182
+ }
183
+ return freezeVec4([
184
+ Number.isFinite(value[0]) ? Math.max(0, value[0]) : fallback[0],
185
+ Number.isFinite(value[1]) ? Math.max(0, value[1]) : fallback[1],
186
+ Number.isFinite(value[2]) ? Math.max(0, value[2]) : fallback[2],
187
+ Number.isFinite(value[3]) ? Math.max(0, Math.min(1, value[3])) : fallback[3] ?? 1,
188
+ ]);
189
+ }
190
+
191
+ function readFinite(value, fallback) {
192
+ return Number.isFinite(value) ? value : fallback;
193
+ }
194
+
195
+ const environmentLightingPresets = Object.freeze({
196
+ "moonlit-harbor": Object.freeze({
197
+ preset: "moonlit-harbor",
198
+ environmentMode: 0,
199
+ environmentIntensity: 0.86,
200
+ exposure: 1,
201
+ horizonColor: freezeVec4([0.33, 0.43, 0.53, 1]),
202
+ zenithColor: freezeVec4([0.035, 0.07, 0.14, 1]),
203
+ sunDirection: Object.freeze(normalizeVector3([0.22, 0.88, 0.42], [0, 1, 0])),
204
+ sunColor: freezeVec4([2.1, 2.25, 2.65, 1]),
205
+ ambientColor: freezeVec4([0.018, 0.023, 0.03, 1]),
206
+ }),
207
+ "product-studio": Object.freeze({
208
+ preset: "product-studio",
209
+ environmentMode: 1,
210
+ environmentIntensity: 1.05,
211
+ exposure: 1,
212
+ horizonColor: freezeVec4([0.52, 0.61, 0.65, 1]),
213
+ zenithColor: freezeVec4([0.18, 0.22, 0.26, 1]),
214
+ sunDirection: Object.freeze(normalizeVector3([0.18, 0.93, 0.24], [0, 1, 0])),
215
+ sunColor: freezeVec4([3.8, 3.55, 2.85, 1]),
216
+ ambientColor: freezeVec4([0.024, 0.027, 0.03, 1]),
217
+ }),
218
+ "neutral-studio": Object.freeze({
219
+ preset: "neutral-studio",
220
+ environmentMode: 2,
221
+ environmentIntensity: 0.95,
222
+ exposure: 1,
223
+ horizonColor: freezeVec4([0.48, 0.53, 0.55, 1]),
224
+ zenithColor: freezeVec4([0.24, 0.26, 0.29, 1]),
225
+ sunDirection: Object.freeze(normalizeVector3([-0.24, 0.86, 0.36], [0, 1, 0])),
226
+ sunColor: freezeVec4([2.4, 2.35, 2.2, 1]),
227
+ ambientColor: freezeVec4([0.028, 0.029, 0.03, 1]),
228
+ }),
229
+ });
230
+
231
+ function resolveEnvironmentPreset(name) {
232
+ const presetName = typeof name === "string" && name.length > 0 ? name : "product-studio";
233
+ const preset = environmentLightingPresets[presetName];
234
+ if (!preset) {
235
+ throw new Error(
236
+ `Unknown lighting environment preset "${presetName}". Expected one of: ${lightingEnvironmentPresetNames.join(", ")}.`
237
+ );
238
+ }
239
+ return preset;
240
+ }
241
+
242
+ function estimateEnvironmentColor(config) {
243
+ const horizonWeight = 0.58;
244
+ const zenithWeight = 1 - horizonWeight;
245
+ const glowWeight = 0.055;
246
+ const intensity = Math.max(config.environmentIntensity, 0.0001);
247
+ return freezeVec4([
248
+ (config.horizonColor[0] * horizonWeight + config.zenithColor[0] * zenithWeight + config.sunColor[0] * glowWeight) * intensity,
249
+ (config.horizonColor[1] * horizonWeight + config.zenithColor[1] * zenithWeight + config.sunColor[1] * glowWeight) * intensity,
250
+ (config.horizonColor[2] * horizonWeight + config.zenithColor[2] * zenithWeight + config.sunColor[2] * glowWeight) * intensity,
251
+ 1,
252
+ ]);
253
+ }
254
+
255
+ export function createEnvironmentLightingConfig(options = {}) {
256
+ const preset = resolveEnvironmentPreset(options.preset ?? options.name);
257
+ const environmentIntensity = Math.max(
258
+ readFinite(options.environmentIntensity ?? options.intensity, preset.environmentIntensity),
259
+ 0.0001
260
+ );
261
+ const config = {
262
+ preset: preset.preset,
263
+ profile: typeof options.profile === "string" ? options.profile : defaultLightingProfile,
264
+ environmentMode: Math.max(0, Math.trunc(readFinite(options.environmentMode, preset.environmentMode))),
265
+ environmentIntensity,
266
+ exposure: Math.max(0.0001, readFinite(options.exposure, preset.exposure)),
267
+ horizonColor: readColor(options.horizonColor, preset.horizonColor),
268
+ zenithColor: readColor(options.zenithColor, preset.zenithColor),
269
+ sunDirection: Object.freeze(
270
+ normalizeVector3(options.sunDirection, preset.sunDirection)
271
+ ),
272
+ sunColor: readColor(options.sunColor, preset.sunColor),
273
+ ambientColor: readColor(options.ambientColor, preset.ambientColor),
274
+ };
275
+ const environmentColor = estimateEnvironmentColor(config);
276
+
277
+ return Object.freeze({
278
+ ...config,
279
+ environmentColor,
280
+ wavefront: Object.freeze({
281
+ environmentColor,
282
+ ambientColor: config.ambientColor,
283
+ environmentLighting: Object.freeze({
284
+ horizonColor: config.horizonColor,
285
+ zenithColor: config.zenithColor,
286
+ sunDirection: Object.freeze([...config.sunDirection]),
287
+ sunColor: config.sunColor,
288
+ intensity: config.environmentIntensity,
289
+ mode: config.environmentMode,
290
+ exposure: config.exposure,
291
+ }),
292
+ }),
293
+ });
294
+ }
295
+
296
+ export function createWavefrontEnvironmentLightingOptions(options = {}) {
297
+ const config = createEnvironmentLightingConfig(options);
298
+ return Object.freeze({
299
+ environmentColor: config.wavefront.environmentColor,
300
+ ambientColor: config.wavefront.ambientColor,
301
+ environmentLighting: config.wavefront.environmentLighting,
302
+ lightingEnvironment: config,
303
+ });
304
+ }
305
+
154
306
  const lightingImportanceLevels = Object.freeze([
155
307
  "low",
156
308
  "medium",
@@ -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
  }