@plasius/gpu-lighting 0.1.18 → 0.2.0

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/src/index.js CHANGED
@@ -136,6 +136,46 @@ export const lightingProfileModeOrder = Object.freeze([
136
136
  "hybrid",
137
137
  "reference",
138
138
  ]);
139
+ export const lightingEnvironmentTimeOfDayNames = Object.freeze([
140
+ "dawn",
141
+ "midday",
142
+ "dusk",
143
+ "night",
144
+ ]);
145
+ export const lightingEnvironmentSceneNames = Object.freeze([
146
+ "studio",
147
+ "harbor",
148
+ "grass-field",
149
+ "forest",
150
+ "warehouse",
151
+ "cavern",
152
+ ]);
153
+ export const lightingEnvironmentLightSourceKinds = Object.freeze([
154
+ "sky",
155
+ "sun",
156
+ "moon",
157
+ "stars",
158
+ "horizon-glow",
159
+ "ground-bounce",
160
+ "studio-softbox",
161
+ "canopy-transmission",
162
+ "window-portal",
163
+ "fluorescent-strip",
164
+ "sodium-door",
165
+ "emergency-beacon",
166
+ "cave-mouth",
167
+ "torch",
168
+ "bioluminescence",
169
+ "lava-fissure",
170
+ "crystal",
171
+ "custom",
172
+ ]);
173
+ export const lightingEnvironmentPortalShapes = Object.freeze(["rectangle"]);
174
+ export const lightingEnvironmentPortalModes = Object.freeze([
175
+ "disabled",
176
+ "guide",
177
+ "guide-and-gate",
178
+ ]);
139
179
  export const defaultAdaptiveLightingProfilePolicy = Object.freeze({
140
180
  preferredProfile: "reference",
141
181
  minimumFrameRate: 30,
@@ -151,6 +191,861 @@ export const lightingDistanceBands = Object.freeze([
151
191
  export const lightingWorkerQueueClass = "lighting";
152
192
  export const lightingDebugOwner = "lighting";
153
193
 
194
+ function freezeVec4(value) {
195
+ return Object.freeze([value[0], value[1], value[2], value[3] ?? 1]);
196
+ }
197
+
198
+ function normalizeVector3(value, fallback) {
199
+ if (!Array.isArray(value) || value.length < 3) {
200
+ return [...fallback];
201
+ }
202
+ const vector = [
203
+ Number.isFinite(value[0]) ? value[0] : fallback[0],
204
+ Number.isFinite(value[1]) ? value[1] : fallback[1],
205
+ Number.isFinite(value[2]) ? value[2] : fallback[2],
206
+ ];
207
+ const length = Math.hypot(vector[0], vector[1], vector[2]);
208
+ if (!Number.isFinite(length) || length <= 0.000001) {
209
+ return [...fallback];
210
+ }
211
+ return vector.map((component) => component / length);
212
+ }
213
+
214
+ function readColor(value, fallback) {
215
+ if (!Array.isArray(value) || value.length < 3) {
216
+ return freezeVec4(fallback);
217
+ }
218
+ return freezeVec4([
219
+ Number.isFinite(value[0]) ? Math.max(0, value[0]) : fallback[0],
220
+ Number.isFinite(value[1]) ? Math.max(0, value[1]) : fallback[1],
221
+ Number.isFinite(value[2]) ? Math.max(0, value[2]) : fallback[2],
222
+ Number.isFinite(value[3]) ? Math.max(0, Math.min(1, value[3])) : fallback[3] ?? 1,
223
+ ]);
224
+ }
225
+
226
+ function colorLuminance(value) {
227
+ return value[0] * 0.2126 + value[1] * 0.7152 + value[2] * 0.0722;
228
+ }
229
+
230
+ function readPositiveColor(value, fallback) {
231
+ const color = readColor(value, fallback);
232
+ const fallbackColor = readColor(fallback, [1, 1, 1, 1]);
233
+ return freezeVec4([
234
+ color[0] > 0 ? color[0] : Math.max(fallbackColor[0], 0.0001),
235
+ color[1] > 0 ? color[1] : Math.max(fallbackColor[1], 0.0001),
236
+ color[2] > 0 ? color[2] : Math.max(fallbackColor[2], 0.0001),
237
+ color[3],
238
+ ]);
239
+ }
240
+
241
+ function ensureNonNullColor(value, fallback = [1, 1, 1, 1]) {
242
+ const color = readColor(value, fallback);
243
+ if (colorLuminance(color) > 0.000001) {
244
+ return color;
245
+ }
246
+ return readPositiveColor(fallback, [1, 1, 1, 1]);
247
+ }
248
+
249
+ function readFinite(value, fallback) {
250
+ return Number.isFinite(value) ? value : fallback;
251
+ }
252
+
253
+ function readVector3(value, fallback) {
254
+ if (!Array.isArray(value) || value.length < 3) {
255
+ return [...fallback];
256
+ }
257
+ return [
258
+ Number.isFinite(value[0]) ? value[0] : fallback[0],
259
+ Number.isFinite(value[1]) ? value[1] : fallback[1],
260
+ Number.isFinite(value[2]) ? value[2] : fallback[2],
261
+ ];
262
+ }
263
+
264
+ function dot3(a, b) {
265
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
266
+ }
267
+
268
+ function cross3(a, b) {
269
+ return [
270
+ a[1] * b[2] - a[2] * b[1],
271
+ a[2] * b[0] - a[0] * b[2],
272
+ a[0] * b[1] - a[1] * b[0],
273
+ ];
274
+ }
275
+
276
+ function normalizeRawVector3(value, fallback) {
277
+ const length = Math.hypot(value[0], value[1], value[2]);
278
+ if (!Number.isFinite(length) || length <= 0.000001) {
279
+ return [...fallback];
280
+ }
281
+ return value.map((component) => component / length);
282
+ }
283
+
284
+ function orthogonalFallback(normal) {
285
+ if (Math.abs(normal[1]) < 0.92) {
286
+ return normalizeRawVector3(cross3([0, 1, 0], normal), [1, 0, 0]);
287
+ }
288
+ return normalizeRawVector3(cross3([1, 0, 0], normal), [0, 0, 1]);
289
+ }
290
+
291
+ function normalizePortalTangent(value, normal) {
292
+ const raw = normalizeVector3(value, orthogonalFallback(normal));
293
+ const projected = [
294
+ raw[0] - normal[0] * dot3(raw, normal),
295
+ raw[1] - normal[1] * dot3(raw, normal),
296
+ raw[2] - normal[2] * dot3(raw, normal),
297
+ ];
298
+ return normalizeRawVector3(projected, orthogonalFallback(normal));
299
+ }
300
+
301
+ function readPositiveFinite(value, fallback) {
302
+ const number = Number(value ?? fallback);
303
+ if (!Number.isFinite(number)) {
304
+ return fallback;
305
+ }
306
+ return Math.max(number, 0.0001);
307
+ }
308
+
309
+ function normalizeEnvironmentPortalMode(value, hasPortals) {
310
+ if (value == null) {
311
+ return hasPortals ? "guide-and-gate" : "disabled";
312
+ }
313
+ if (value === "gate") {
314
+ return "guide-and-gate";
315
+ }
316
+ if (lightingEnvironmentPortalModes.includes(value)) {
317
+ return value;
318
+ }
319
+ throw new Error(
320
+ `environmentPortalMode must be one of: ${lightingEnvironmentPortalModes.join(", ")}.`
321
+ );
322
+ }
323
+
324
+ function normalizeEnvironmentPortal(portal, index) {
325
+ if (!portal || typeof portal !== "object") {
326
+ throw new Error(`environmentPortals[${index}] must be an object.`);
327
+ }
328
+ const shape = portal.shape ?? portal.kind ?? "rectangle";
329
+ if (!lightingEnvironmentPortalShapes.includes(shape)) {
330
+ throw new Error(
331
+ `environmentPortals[${index}].shape must be one of: ${lightingEnvironmentPortalShapes.join(", ")}.`
332
+ );
333
+ }
334
+ const normal = Object.freeze(
335
+ normalizeVector3(portal.normal, [0, 0, 1])
336
+ );
337
+ const tangent = Object.freeze(normalizePortalTangent(portal.tangent, normal));
338
+ const bitangent = Object.freeze(
339
+ normalizeRawVector3(cross3(normal, tangent), [0, 1, 0])
340
+ );
341
+ const width = readPositiveFinite(
342
+ portal.width,
343
+ readPositiveFinite(portal.halfWidth, 0.5) * 2
344
+ );
345
+ const height = readPositiveFinite(
346
+ portal.height,
347
+ readPositiveFinite(portal.halfHeight, 0.5) * 2
348
+ );
349
+ const radianceScale = Math.max(
350
+ 0,
351
+ readFinite(portal.radianceScale ?? portal.intensity, 1)
352
+ );
353
+ return Object.freeze({
354
+ id: typeof portal.id === "string" && portal.id.length > 0
355
+ ? portal.id
356
+ : `environment-portal-${index}`,
357
+ shape,
358
+ position: Object.freeze(readVector3(portal.position ?? portal.center, [0, 0, 0])),
359
+ normal,
360
+ tangent,
361
+ bitangent,
362
+ width,
363
+ height,
364
+ radianceScale,
365
+ color: readColor(portal.color, [1, 1, 1, 1]),
366
+ twoSided: portal.twoSided !== false,
367
+ });
368
+ }
369
+
370
+ function normalizeEnvironmentPortals(value) {
371
+ if (value == null) {
372
+ return Object.freeze([]);
373
+ }
374
+ if (!Array.isArray(value)) {
375
+ throw new Error("environmentPortals must be an array when provided.");
376
+ }
377
+ return Object.freeze(value.map(normalizeEnvironmentPortal));
378
+ }
379
+
380
+ function freezeLightSourceSpec(source) {
381
+ return Object.freeze({
382
+ ...source,
383
+ color: source.color ? freezeVec4(source.color) : undefined,
384
+ direction: source.direction
385
+ ? Object.freeze(normalizeVector3(source.direction, [0, 1, 0]))
386
+ : undefined,
387
+ position: source.position
388
+ ? Object.freeze(readVector3(source.position, [0, 0, 0]))
389
+ : undefined,
390
+ });
391
+ }
392
+
393
+ function defineEnvironmentPreset(spec) {
394
+ return Object.freeze({
395
+ ...spec,
396
+ scene: spec.scene ?? "studio",
397
+ timeOfDay: spec.timeOfDay ?? "midday",
398
+ horizonColor: freezeVec4(spec.horizonColor),
399
+ zenithColor: freezeVec4(spec.zenithColor),
400
+ sunDirection: Object.freeze(normalizeVector3(spec.sunDirection, [0, 1, 0])),
401
+ sunColor: freezeVec4(spec.sunColor),
402
+ ambientColor: freezeVec4(spec.ambientColor),
403
+ environmentLightSources: Object.freeze(
404
+ (spec.environmentLightSources ?? []).map(freezeLightSourceSpec)
405
+ ),
406
+ });
407
+ }
408
+
409
+ function buildEnvironmentLightSourceFallback(config, preset) {
410
+ const firstPresetSource = preset.environmentLightSources[0] ?? {};
411
+ return {
412
+ kind: firstPresetSource.kind ?? "sky",
413
+ role: firstPresetSource.role ?? "fill",
414
+ color: firstPresetSource.color ?? config.sunColor,
415
+ intensity:
416
+ firstPresetSource.intensity ??
417
+ Math.max(config.environmentIntensity, 0.0001),
418
+ direction: firstPresetSource.direction ?? config.sunDirection,
419
+ angularRadiusRadians: firstPresetSource.angularRadiusRadians ?? 0.25,
420
+ reach: firstPresetSource.reach ?? 1000,
421
+ };
422
+ }
423
+
424
+ function normalizeEnvironmentLightSource(source, index, fallback) {
425
+ if (!source || typeof source !== "object") {
426
+ throw new Error(`environmentLightSources[${index}] must be an object.`);
427
+ }
428
+ const requestedKind = source.kind ?? source.type ?? fallback.kind ?? "custom";
429
+ const kind = lightingEnvironmentLightSourceKinds.includes(requestedKind)
430
+ ? requestedKind
431
+ : "custom";
432
+ const color = readPositiveColor(source.color, fallback.color ?? [1, 1, 1, 1]);
433
+ const intensity = readPositiveFinite(
434
+ source.intensity ?? source.radianceScale,
435
+ fallback.intensity ?? 1
436
+ );
437
+ const radiance = freezeVec4([
438
+ color[0] * intensity,
439
+ color[1] * intensity,
440
+ color[2] * intensity,
441
+ color[3],
442
+ ]);
443
+ return Object.freeze({
444
+ id:
445
+ typeof source.id === "string" && source.id.length > 0
446
+ ? source.id
447
+ : `environment-light-source-${index}`,
448
+ kind,
449
+ type: kind,
450
+ role:
451
+ typeof source.role === "string" && source.role.length > 0
452
+ ? source.role
453
+ : fallback.role ?? "fill",
454
+ direction: Object.freeze(
455
+ normalizeVector3(source.direction, fallback.direction ?? [0, 1, 0])
456
+ ),
457
+ position: Object.freeze(
458
+ readVector3(source.position ?? source.origin, fallback.position ?? [0, 0, 0])
459
+ ),
460
+ color,
461
+ intensity,
462
+ radiance,
463
+ luminance: colorLuminance(radiance),
464
+ angularRadiusRadians: readPositiveFinite(
465
+ source.angularRadiusRadians ?? source.angularRadius,
466
+ fallback.angularRadiusRadians ?? 0.25
467
+ ),
468
+ reach: readPositiveFinite(
469
+ source.reach ?? source.distance,
470
+ fallback.reach ?? 1000
471
+ ),
472
+ castsShadows: source.castsShadows !== false,
473
+ contributesToEnvironment: source.contributesToEnvironment !== false,
474
+ });
475
+ }
476
+
477
+ function normalizeEnvironmentLightSources(value, preset, config) {
478
+ const baseSources = value ?? preset.environmentLightSources;
479
+ const fallback = buildEnvironmentLightSourceFallback(config, preset);
480
+ if (!Array.isArray(baseSources)) {
481
+ throw new Error("environmentLightSources must be an array when provided.");
482
+ }
483
+ const normalizedSources = baseSources.length > 0
484
+ ? baseSources.map((source, index) =>
485
+ normalizeEnvironmentLightSource(source, index, fallback)
486
+ )
487
+ : [normalizeEnvironmentLightSource(fallback, 0, fallback)];
488
+ return Object.freeze(normalizedSources);
489
+ }
490
+
491
+ function findDominantEnvironmentLightSource(sources) {
492
+ return sources.reduce((dominant, source) =>
493
+ source.luminance > dominant.luminance ? source : dominant
494
+ );
495
+ }
496
+
497
+ function createEnvironmentMissLighting(source, environmentColor) {
498
+ const fallbackRadiance = readPositiveColor(environmentColor, source.radiance);
499
+ const radiance = ensureNonNullColor(source.radiance, fallbackRadiance);
500
+ const color = readPositiveColor(source.color, environmentColor);
501
+ return Object.freeze({
502
+ sourceId: source.id,
503
+ kind: source.kind,
504
+ role: source.role,
505
+ contribution: "inferred-environment",
506
+ startingPoint: "environment-miss",
507
+ direction: source.direction,
508
+ position: source.position,
509
+ color,
510
+ intensity: Math.max(source.intensity, 0.0001),
511
+ radiance,
512
+ luminance: Math.max(colorLuminance(radiance), 0.0001),
513
+ });
514
+ }
515
+
516
+ const environmentLightingPresets = Object.freeze({
517
+ "moonlit-harbor": defineEnvironmentPreset({
518
+ preset: "moonlit-harbor",
519
+ scene: "harbor",
520
+ timeOfDay: "night",
521
+ environmentMode: 0,
522
+ environmentIntensity: 0.86,
523
+ exposure: 1,
524
+ horizonColor: freezeVec4([0.33, 0.43, 0.53, 1]),
525
+ zenithColor: freezeVec4([0.035, 0.07, 0.14, 1]),
526
+ sunDirection: Object.freeze(normalizeVector3([0.22, 0.88, 0.42], [0, 1, 0])),
527
+ sunColor: freezeVec4([2.1, 2.25, 2.65, 1]),
528
+ ambientColor: freezeVec4([0.018, 0.023, 0.03, 1]),
529
+ environmentLightSources: [
530
+ {
531
+ id: "harbor-moon",
532
+ kind: "moon",
533
+ role: "key",
534
+ direction: [0.22, 0.88, 0.42],
535
+ color: [0.7, 0.76, 0.9, 1],
536
+ intensity: 2.2,
537
+ angularRadiusRadians: 0.018,
538
+ },
539
+ {
540
+ id: "harbor-sky",
541
+ kind: "sky",
542
+ role: "fill",
543
+ direction: [0, 1, 0],
544
+ color: [0.22, 0.31, 0.48, 1],
545
+ intensity: 0.35,
546
+ },
547
+ ],
548
+ }),
549
+ "product-studio": defineEnvironmentPreset({
550
+ preset: "product-studio",
551
+ scene: "studio",
552
+ timeOfDay: "midday",
553
+ environmentMode: 1,
554
+ environmentIntensity: 1.05,
555
+ exposure: 1,
556
+ horizonColor: freezeVec4([0.52, 0.61, 0.65, 1]),
557
+ zenithColor: freezeVec4([0.18, 0.22, 0.26, 1]),
558
+ sunDirection: Object.freeze(normalizeVector3([0.18, 0.93, 0.24], [0, 1, 0])),
559
+ sunColor: freezeVec4([3.8, 3.55, 2.85, 1]),
560
+ ambientColor: freezeVec4([0.024, 0.027, 0.03, 1]),
561
+ environmentLightSources: [
562
+ {
563
+ id: "studio-key-softbox",
564
+ kind: "studio-softbox",
565
+ role: "key",
566
+ direction: [0.18, 0.93, 0.24],
567
+ color: [1, 0.94, 0.82, 1],
568
+ intensity: 4.1,
569
+ angularRadiusRadians: 0.42,
570
+ },
571
+ {
572
+ id: "studio-fill-panel",
573
+ kind: "studio-softbox",
574
+ role: "fill",
575
+ direction: [-0.56, 0.62, -0.2],
576
+ color: [0.75, 0.84, 1, 1],
577
+ intensity: 1.3,
578
+ angularRadiusRadians: 0.55,
579
+ },
580
+ ],
581
+ }),
582
+ "neutral-studio": defineEnvironmentPreset({
583
+ preset: "neutral-studio",
584
+ scene: "studio",
585
+ timeOfDay: "midday",
586
+ environmentMode: 2,
587
+ environmentIntensity: 0.95,
588
+ exposure: 1,
589
+ horizonColor: freezeVec4([0.48, 0.53, 0.55, 1]),
590
+ zenithColor: freezeVec4([0.24, 0.26, 0.29, 1]),
591
+ sunDirection: Object.freeze(normalizeVector3([-0.24, 0.86, 0.36], [0, 1, 0])),
592
+ sunColor: freezeVec4([2.4, 2.35, 2.2, 1]),
593
+ ambientColor: freezeVec4([0.028, 0.029, 0.03, 1]),
594
+ environmentLightSources: [
595
+ {
596
+ id: "neutral-studio-overhead",
597
+ kind: "studio-softbox",
598
+ role: "key",
599
+ direction: [-0.24, 0.86, 0.36],
600
+ color: [0.96, 0.97, 1, 1],
601
+ intensity: 2.5,
602
+ angularRadiusRadians: 0.5,
603
+ },
604
+ {
605
+ id: "neutral-studio-wall-bounce",
606
+ kind: "ground-bounce",
607
+ role: "fill",
608
+ direction: [0.2, 0.3, -0.9],
609
+ color: [0.55, 0.58, 0.62, 1],
610
+ intensity: 0.8,
611
+ },
612
+ ],
613
+ }),
614
+ "grass-field-dawn": defineEnvironmentPreset({
615
+ preset: "grass-field-dawn",
616
+ scene: "grass-field",
617
+ timeOfDay: "dawn",
618
+ environmentMode: 3,
619
+ environmentIntensity: 0.92,
620
+ exposure: 1.06,
621
+ horizonColor: [0.92, 0.54, 0.32, 1],
622
+ zenithColor: [0.16, 0.28, 0.5, 1],
623
+ sunDirection: [0.64, 0.32, 0.18],
624
+ sunColor: [5.6, 3.15, 1.55, 1],
625
+ ambientColor: [0.034, 0.047, 0.032, 1],
626
+ environmentLightSources: [
627
+ { id: "field-dawn-sun", kind: "sun", role: "key", direction: [0.64, 0.32, 0.18], color: [1, 0.58, 0.28, 1], intensity: 5.6, angularRadiusRadians: 0.012 },
628
+ { id: "field-dawn-sky", kind: "sky", role: "fill", direction: [0, 1, 0], color: [0.36, 0.52, 0.82, 1], intensity: 0.9 },
629
+ { id: "field-dawn-grass-bounce", kind: "ground-bounce", role: "bounce", direction: [0, 0.25, 0.1], color: [0.22, 0.44, 0.12, 1], intensity: 0.45 },
630
+ ],
631
+ }),
632
+ "grass-field-midday": defineEnvironmentPreset({
633
+ preset: "grass-field-midday",
634
+ scene: "grass-field",
635
+ timeOfDay: "midday",
636
+ environmentMode: 4,
637
+ environmentIntensity: 1.18,
638
+ exposure: 0.96,
639
+ horizonColor: [0.58, 0.78, 0.96, 1],
640
+ zenithColor: [0.1, 0.34, 0.82, 1],
641
+ sunDirection: [0.18, 0.98, 0.08],
642
+ sunColor: [9.8, 9.4, 8.55, 1],
643
+ ambientColor: [0.048, 0.062, 0.04, 1],
644
+ environmentLightSources: [
645
+ { id: "field-midday-sun", kind: "sun", role: "key", direction: [0.18, 0.98, 0.08], color: [1, 0.96, 0.86, 1], intensity: 9.8, angularRadiusRadians: 0.0093 },
646
+ { id: "field-midday-sky", kind: "sky", role: "fill", direction: [0, 1, 0], color: [0.48, 0.7, 1, 1], intensity: 1.8 },
647
+ { id: "field-midday-ground", kind: "ground-bounce", role: "bounce", direction: [0, 0.35, -0.15], color: [0.28, 0.56, 0.16, 1], intensity: 0.65 },
648
+ ],
649
+ }),
650
+ "grass-field-dusk": defineEnvironmentPreset({
651
+ preset: "grass-field-dusk",
652
+ scene: "grass-field",
653
+ timeOfDay: "dusk",
654
+ environmentMode: 5,
655
+ environmentIntensity: 0.82,
656
+ exposure: 1.12,
657
+ horizonColor: [1.08, 0.42, 0.24, 1],
658
+ zenithColor: [0.09, 0.1, 0.32, 1],
659
+ sunDirection: [-0.76, 0.24, 0.22],
660
+ sunColor: [4.8, 1.65, 0.72, 1],
661
+ ambientColor: [0.026, 0.026, 0.034, 1],
662
+ environmentLightSources: [
663
+ { id: "field-dusk-sun", kind: "sun", role: "key", direction: [-0.76, 0.24, 0.22], color: [1, 0.34, 0.16, 1], intensity: 4.8, angularRadiusRadians: 0.014 },
664
+ { id: "field-dusk-horizon", kind: "horizon-glow", role: "fill", direction: [-0.9, 0.08, 0.1], color: [0.92, 0.28, 0.16, 1], intensity: 1.2 },
665
+ { id: "field-dusk-grass", kind: "ground-bounce", role: "bounce", direction: [0, 0.22, 0.2], color: [0.12, 0.28, 0.11, 1], intensity: 0.35 },
666
+ ],
667
+ }),
668
+ "grass-field-night": defineEnvironmentPreset({
669
+ preset: "grass-field-night",
670
+ scene: "grass-field",
671
+ timeOfDay: "night",
672
+ environmentMode: 6,
673
+ environmentIntensity: 0.48,
674
+ exposure: 1.35,
675
+ horizonColor: [0.08, 0.13, 0.2, 1],
676
+ zenithColor: [0.018, 0.035, 0.09, 1],
677
+ sunDirection: [-0.22, 0.86, -0.34],
678
+ sunColor: [0.72, 0.82, 1.35, 1],
679
+ ambientColor: [0.012, 0.017, 0.026, 1],
680
+ environmentLightSources: [
681
+ { id: "field-night-moon", kind: "moon", role: "key", direction: [-0.22, 0.86, -0.34], color: [0.52, 0.62, 1, 1], intensity: 1.25, angularRadiusRadians: 0.018 },
682
+ { id: "field-night-stars", kind: "stars", role: "fill", direction: [0, 1, 0], color: [0.32, 0.38, 0.6, 1], intensity: 0.24 },
683
+ { id: "field-night-horizon", kind: "horizon-glow", role: "rim", direction: [0.8, 0.08, -0.15], color: [0.08, 0.14, 0.26, 1], intensity: 0.28 },
684
+ ],
685
+ }),
686
+ "forest-dawn": defineEnvironmentPreset({
687
+ preset: "forest-dawn",
688
+ scene: "forest",
689
+ timeOfDay: "dawn",
690
+ environmentMode: 7,
691
+ environmentIntensity: 0.78,
692
+ exposure: 1.14,
693
+ horizonColor: [0.72, 0.48, 0.28, 1],
694
+ zenithColor: [0.08, 0.18, 0.18, 1],
695
+ sunDirection: [0.58, 0.42, -0.24],
696
+ sunColor: [4.4, 2.65, 1.32, 1],
697
+ ambientColor: [0.024, 0.04, 0.026, 1],
698
+ environmentLightSources: [
699
+ { id: "forest-dawn-sun-shaft", kind: "sun", role: "key", direction: [0.58, 0.42, -0.24], color: [1, 0.62, 0.32, 1], intensity: 4.4, angularRadiusRadians: 0.018 },
700
+ { id: "forest-dawn-canopy", kind: "canopy-transmission", role: "filter", direction: [0.12, 0.78, 0.2], color: [0.34, 0.68, 0.24, 1], intensity: 0.86 },
701
+ { id: "forest-dawn-sky-gap", kind: "sky", role: "fill", direction: [-0.18, 0.92, 0.12], color: [0.28, 0.46, 0.62, 1], intensity: 0.48 },
702
+ ],
703
+ }),
704
+ "forest-midday": defineEnvironmentPreset({
705
+ preset: "forest-midday",
706
+ scene: "forest",
707
+ timeOfDay: "midday",
708
+ environmentMode: 8,
709
+ environmentIntensity: 0.96,
710
+ exposure: 1.02,
711
+ horizonColor: [0.38, 0.62, 0.42, 1],
712
+ zenithColor: [0.08, 0.28, 0.32, 1],
713
+ sunDirection: [0.08, 0.96, -0.18],
714
+ sunColor: [7.2, 6.9, 5.25, 1],
715
+ ambientColor: [0.034, 0.055, 0.032, 1],
716
+ environmentLightSources: [
717
+ { id: "forest-midday-sun-gap", kind: "sun", role: "key", direction: [0.08, 0.96, -0.18], color: [1, 0.96, 0.74, 1], intensity: 7.2, angularRadiusRadians: 0.013 },
718
+ { id: "forest-midday-leaves", kind: "canopy-transmission", role: "filter", direction: [0.32, 0.75, 0.12], color: [0.24, 0.72, 0.28, 1], intensity: 1.35 },
719
+ { id: "forest-midday-floor", kind: "ground-bounce", role: "bounce", direction: [-0.1, 0.25, 0.18], color: [0.18, 0.35, 0.13, 1], intensity: 0.42 },
720
+ ],
721
+ }),
722
+ "forest-dusk": defineEnvironmentPreset({
723
+ preset: "forest-dusk",
724
+ scene: "forest",
725
+ timeOfDay: "dusk",
726
+ environmentMode: 9,
727
+ environmentIntensity: 0.68,
728
+ exposure: 1.2,
729
+ horizonColor: [0.72, 0.28, 0.2, 1],
730
+ zenithColor: [0.04, 0.07, 0.18, 1],
731
+ sunDirection: [-0.7, 0.28, -0.18],
732
+ sunColor: [3.2, 1.18, 0.56, 1],
733
+ ambientColor: [0.018, 0.026, 0.024, 1],
734
+ environmentLightSources: [
735
+ { id: "forest-dusk-horizon", kind: "horizon-glow", role: "key", direction: [-0.7, 0.18, -0.18], color: [1, 0.34, 0.2, 1], intensity: 2.2, angularRadiusRadians: 0.1 },
736
+ { id: "forest-dusk-canopy", kind: "canopy-transmission", role: "filter", direction: [0.18, 0.7, 0.26], color: [0.18, 0.38, 0.2, 1], intensity: 0.52 },
737
+ { id: "forest-dusk-sky-gap", kind: "sky", role: "fill", direction: [0, 1, 0], color: [0.12, 0.16, 0.34, 1], intensity: 0.42 },
738
+ ],
739
+ }),
740
+ "forest-night": defineEnvironmentPreset({
741
+ preset: "forest-night",
742
+ scene: "forest",
743
+ timeOfDay: "night",
744
+ environmentMode: 10,
745
+ environmentIntensity: 0.42,
746
+ exposure: 1.42,
747
+ horizonColor: [0.035, 0.08, 0.1, 1],
748
+ zenithColor: [0.012, 0.025, 0.06, 1],
749
+ sunDirection: [0.2, 0.82, -0.46],
750
+ sunColor: [0.42, 0.56, 1.1, 1],
751
+ ambientColor: [0.01, 0.016, 0.02, 1],
752
+ environmentLightSources: [
753
+ { id: "forest-night-moon-gap", kind: "moon", role: "key", direction: [0.2, 0.82, -0.46], color: [0.42, 0.56, 1, 1], intensity: 0.95, angularRadiusRadians: 0.025 },
754
+ { id: "forest-night-canopy", kind: "canopy-transmission", role: "filter", direction: [-0.16, 0.66, 0.1], color: [0.08, 0.18, 0.12, 1], intensity: 0.28 },
755
+ { id: "forest-night-stars", kind: "stars", role: "fill", direction: [0, 1, 0], color: [0.22, 0.28, 0.5, 1], intensity: 0.18 },
756
+ ],
757
+ }),
758
+ "warehouse-dawn": defineEnvironmentPreset({
759
+ preset: "warehouse-dawn",
760
+ scene: "warehouse",
761
+ timeOfDay: "dawn",
762
+ environmentMode: 11,
763
+ environmentIntensity: 0.74,
764
+ exposure: 1.08,
765
+ horizonColor: [0.58, 0.44, 0.34, 1],
766
+ zenithColor: [0.16, 0.19, 0.24, 1],
767
+ sunDirection: [0.82, 0.28, 0.18],
768
+ sunColor: [2.8, 1.7, 0.92, 1],
769
+ ambientColor: [0.028, 0.03, 0.032, 1],
770
+ environmentLightSources: [
771
+ { id: "warehouse-dawn-loading-door", kind: "window-portal", role: "key", direction: [0.82, 0.28, 0.18], color: [1, 0.62, 0.34, 1], intensity: 2.8, angularRadiusRadians: 0.22 },
772
+ { id: "warehouse-dawn-fluorescent", kind: "fluorescent-strip", role: "fill", direction: [0, 1, 0], color: [0.78, 0.9, 1, 1], intensity: 1.1, angularRadiusRadians: 0.35 },
773
+ { id: "warehouse-dawn-concrete-bounce", kind: "ground-bounce", role: "bounce", direction: [0, 0.28, -0.2], color: [0.34, 0.36, 0.38, 1], intensity: 0.42 },
774
+ ],
775
+ }),
776
+ "warehouse-midday": defineEnvironmentPreset({
777
+ preset: "warehouse-midday",
778
+ scene: "warehouse",
779
+ timeOfDay: "midday",
780
+ environmentMode: 12,
781
+ environmentIntensity: 0.92,
782
+ exposure: 0.98,
783
+ horizonColor: [0.64, 0.7, 0.74, 1],
784
+ zenithColor: [0.28, 0.34, 0.42, 1],
785
+ sunDirection: [0.35, 0.86, 0.16],
786
+ sunColor: [4.2, 4, 3.45, 1],
787
+ ambientColor: [0.034, 0.036, 0.038, 1],
788
+ environmentLightSources: [
789
+ { id: "warehouse-midday-skylights", kind: "window-portal", role: "key", direction: [0.35, 0.86, 0.16], color: [0.92, 0.96, 1, 1], intensity: 4.2, angularRadiusRadians: 0.18 },
790
+ { id: "warehouse-midday-fluorescent", kind: "fluorescent-strip", role: "fill", direction: [-0.2, 0.92, 0.1], color: [0.78, 0.92, 1, 1], intensity: 1.6, angularRadiusRadians: 0.45 },
791
+ { id: "warehouse-midday-door-spill", kind: "sodium-door", role: "rim", direction: [-0.82, 0.18, -0.12], color: [1, 0.58, 0.24, 1], intensity: 0.68 },
792
+ ],
793
+ }),
794
+ "warehouse-dusk": defineEnvironmentPreset({
795
+ preset: "warehouse-dusk",
796
+ scene: "warehouse",
797
+ timeOfDay: "dusk",
798
+ environmentMode: 13,
799
+ environmentIntensity: 0.7,
800
+ exposure: 1.16,
801
+ horizonColor: [0.7, 0.32, 0.24, 1],
802
+ zenithColor: [0.08, 0.1, 0.18, 1],
803
+ sunDirection: [-0.78, 0.18, 0.16],
804
+ sunColor: [2.4, 0.94, 0.48, 1],
805
+ ambientColor: [0.022, 0.024, 0.03, 1],
806
+ environmentLightSources: [
807
+ { id: "warehouse-dusk-door-glow", kind: "sodium-door", role: "key", direction: [-0.78, 0.18, 0.16], color: [1, 0.42, 0.2, 1], intensity: 2.4, angularRadiusRadians: 0.18 },
808
+ { id: "warehouse-dusk-fluorescent", kind: "fluorescent-strip", role: "fill", direction: [0, 0.95, -0.08], color: [0.72, 0.88, 1, 1], intensity: 1.35, angularRadiusRadians: 0.4 },
809
+ { id: "warehouse-dusk-emergency", kind: "emergency-beacon", role: "accent", direction: [0.2, 0.35, -0.8], color: [1, 0.08, 0.04, 1], intensity: 0.32 },
810
+ ],
811
+ }),
812
+ "warehouse-night": defineEnvironmentPreset({
813
+ preset: "warehouse-night",
814
+ scene: "warehouse",
815
+ timeOfDay: "night",
816
+ environmentMode: 14,
817
+ environmentIntensity: 0.58,
818
+ exposure: 1.28,
819
+ horizonColor: [0.06, 0.08, 0.12, 1],
820
+ zenithColor: [0.02, 0.03, 0.055, 1],
821
+ sunDirection: [0.1, 0.94, -0.12],
822
+ sunColor: [1.2, 1.65, 2.25, 1],
823
+ ambientColor: [0.014, 0.018, 0.024, 1],
824
+ environmentLightSources: [
825
+ { id: "warehouse-night-fluorescent", kind: "fluorescent-strip", role: "key", direction: [0.1, 0.94, -0.12], color: [0.68, 0.88, 1, 1], intensity: 2.25, angularRadiusRadians: 0.5 },
826
+ { id: "warehouse-night-emergency", kind: "emergency-beacon", role: "accent", direction: [-0.4, 0.3, 0.7], color: [1, 0.05, 0.025, 1], intensity: 0.4 },
827
+ { id: "warehouse-night-door-leak", kind: "window-portal", role: "rim", direction: [0.82, 0.08, -0.2], color: [0.12, 0.22, 0.42, 1], intensity: 0.34 },
828
+ ],
829
+ }),
830
+ "cavern-dawn": defineEnvironmentPreset({
831
+ preset: "cavern-dawn",
832
+ scene: "cavern",
833
+ timeOfDay: "dawn",
834
+ environmentMode: 15,
835
+ environmentIntensity: 0.62,
836
+ exposure: 1.24,
837
+ horizonColor: [0.5, 0.3, 0.2, 1],
838
+ zenithColor: [0.04, 0.07, 0.09, 1],
839
+ sunDirection: [0.72, 0.32, 0.26],
840
+ sunColor: [2.1, 1.22, 0.64, 1],
841
+ ambientColor: [0.018, 0.018, 0.016, 1],
842
+ environmentLightSources: [
843
+ { id: "cavern-dawn-mouth", kind: "cave-mouth", role: "key", direction: [0.72, 0.32, 0.26], color: [1, 0.58, 0.3, 1], intensity: 2.1, angularRadiusRadians: 0.24 },
844
+ { id: "cavern-dawn-torch", kind: "torch", role: "emissive", direction: [-0.35, 0.28, -0.6], color: [1, 0.42, 0.16, 1], intensity: 1.35, reach: 18 },
845
+ { id: "cavern-dawn-crystal", kind: "crystal", role: "accent", direction: [0.08, 0.22, 0.9], color: [0.22, 0.72, 1, 1], intensity: 0.28, reach: 10 },
846
+ ],
847
+ }),
848
+ "cavern-midday": defineEnvironmentPreset({
849
+ preset: "cavern-midday",
850
+ scene: "cavern",
851
+ timeOfDay: "midday",
852
+ environmentMode: 16,
853
+ environmentIntensity: 0.72,
854
+ exposure: 1.16,
855
+ horizonColor: [0.6, 0.56, 0.48, 1],
856
+ zenithColor: [0.08, 0.12, 0.14, 1],
857
+ sunDirection: [0.36, 0.82, 0.14],
858
+ sunColor: [3.4, 3.05, 2.2, 1],
859
+ ambientColor: [0.02, 0.022, 0.02, 1],
860
+ environmentLightSources: [
861
+ { id: "cavern-midday-mouth", kind: "cave-mouth", role: "key", direction: [0.36, 0.82, 0.14], color: [1, 0.9, 0.66, 1], intensity: 3.4, angularRadiusRadians: 0.18 },
862
+ { id: "cavern-midday-biolume", kind: "bioluminescence", role: "fill", direction: [-0.25, 0.25, 0.7], color: [0.1, 0.82, 0.64, 1], intensity: 0.46, reach: 14 },
863
+ { id: "cavern-midday-wet-rock", kind: "ground-bounce", role: "bounce", direction: [0.1, 0.2, -0.3], color: [0.18, 0.2, 0.18, 1], intensity: 0.22 },
864
+ ],
865
+ }),
866
+ "cavern-dusk": defineEnvironmentPreset({
867
+ preset: "cavern-dusk",
868
+ scene: "cavern",
869
+ timeOfDay: "dusk",
870
+ environmentMode: 17,
871
+ environmentIntensity: 0.56,
872
+ exposure: 1.32,
873
+ horizonColor: [0.46, 0.18, 0.14, 1],
874
+ zenithColor: [0.035, 0.045, 0.08, 1],
875
+ sunDirection: [-0.62, 0.22, 0.22],
876
+ sunColor: [1.55, 0.56, 0.28, 1],
877
+ ambientColor: [0.014, 0.014, 0.018, 1],
878
+ environmentLightSources: [
879
+ { id: "cavern-dusk-mouth", kind: "cave-mouth", role: "rim", direction: [-0.62, 0.22, 0.22], color: [1, 0.36, 0.18, 1], intensity: 1.55, angularRadiusRadians: 0.22 },
880
+ { id: "cavern-dusk-torch", kind: "torch", role: "key", direction: [0.32, 0.34, -0.54], color: [1, 0.38, 0.12, 1], intensity: 1.85, reach: 20 },
881
+ { id: "cavern-dusk-biolume", kind: "bioluminescence", role: "fill", direction: [-0.18, 0.18, 0.74], color: [0.08, 0.58, 0.72, 1], intensity: 0.34, reach: 12 },
882
+ ],
883
+ }),
884
+ "cavern-night": defineEnvironmentPreset({
885
+ preset: "cavern-night",
886
+ scene: "cavern",
887
+ timeOfDay: "night",
888
+ environmentMode: 18,
889
+ environmentIntensity: 0.5,
890
+ exposure: 1.45,
891
+ horizonColor: [0.025, 0.035, 0.06, 1],
892
+ zenithColor: [0.008, 0.014, 0.03, 1],
893
+ sunDirection: [0.18, 0.28, -0.68],
894
+ sunColor: [1.9, 0.72, 0.24, 1],
895
+ ambientColor: [0.01, 0.012, 0.018, 1],
896
+ environmentLightSources: [
897
+ { id: "cavern-night-torch", kind: "torch", role: "key", direction: [0.18, 0.28, -0.68], color: [1, 0.36, 0.12, 1], intensity: 1.9, reach: 18 },
898
+ { id: "cavern-night-biolume", kind: "bioluminescence", role: "fill", direction: [-0.32, 0.16, 0.72], color: [0.06, 0.62, 0.76, 1], intensity: 0.52, reach: 16 },
899
+ { id: "cavern-night-lava", kind: "lava-fissure", role: "emissive", direction: [0.42, 0.12, 0.28], color: [1, 0.18, 0.04, 1], intensity: 0.8, reach: 12 },
900
+ ],
901
+ }),
902
+ });
903
+
904
+ export const lightingEnvironmentPresetNames = Object.freeze(
905
+ Object.keys(environmentLightingPresets)
906
+ );
907
+
908
+ function resolveEnvironmentPreset(name, timeOfDay) {
909
+ const presetName = typeof name === "string" && name.length > 0 ? name : "product-studio";
910
+ const preset = environmentLightingPresets[presetName];
911
+ if (!preset) {
912
+ if (lightingEnvironmentSceneNames.includes(presetName)) {
913
+ if (
914
+ timeOfDay != null &&
915
+ !lightingEnvironmentTimeOfDayNames.includes(timeOfDay)
916
+ ) {
917
+ throw new Error(
918
+ `timeOfDay must be one of: ${lightingEnvironmentTimeOfDayNames.join(", ")}.`
919
+ );
920
+ }
921
+ const scenePresetName = `${presetName}-${timeOfDay ?? "midday"}`;
922
+ const scenePreset = environmentLightingPresets[scenePresetName];
923
+ if (scenePreset) {
924
+ return scenePreset;
925
+ }
926
+ }
927
+ throw new Error(
928
+ `Unknown lighting environment preset "${presetName}". Expected one of: ${lightingEnvironmentPresetNames.join(", ")}.`
929
+ );
930
+ }
931
+ return preset;
932
+ }
933
+
934
+ function estimateEnvironmentColor(config) {
935
+ const horizonWeight = 0.58;
936
+ const zenithWeight = 1 - horizonWeight;
937
+ const glowWeight = 0.055;
938
+ const intensity = Math.max(config.environmentIntensity, 0.0001);
939
+ return ensureNonNullColor([
940
+ (config.horizonColor[0] * horizonWeight + config.zenithColor[0] * zenithWeight + config.sunColor[0] * glowWeight) * intensity,
941
+ (config.horizonColor[1] * horizonWeight + config.zenithColor[1] * zenithWeight + config.sunColor[1] * glowWeight) * intensity,
942
+ (config.horizonColor[2] * horizonWeight + config.zenithColor[2] * zenithWeight + config.sunColor[2] * glowWeight) * intensity,
943
+ 1,
944
+ ], config.dominantLightSource?.radiance ?? config.sunColor);
945
+ }
946
+
947
+ export function createEnvironmentLightingConfig(options = {}) {
948
+ const preset = resolveEnvironmentPreset(
949
+ options.preset ?? options.name ?? options.scene,
950
+ options.timeOfDay
951
+ );
952
+ const environmentPortals = normalizeEnvironmentPortals(
953
+ options.environmentPortals ?? options.portals
954
+ );
955
+ const environmentPortalMode = normalizeEnvironmentPortalMode(
956
+ options.environmentPortalMode ?? options.portalMode,
957
+ environmentPortals.length > 0
958
+ );
959
+ const environmentIntensity = Math.max(
960
+ readFinite(options.environmentIntensity ?? options.intensity, preset.environmentIntensity),
961
+ 0.0001
962
+ );
963
+ const baseConfig = {
964
+ preset: preset.preset,
965
+ scene: preset.scene,
966
+ timeOfDay: preset.timeOfDay,
967
+ profile: typeof options.profile === "string" ? options.profile : defaultLightingProfile,
968
+ environmentMode: Math.max(0, Math.trunc(readFinite(options.environmentMode, preset.environmentMode))),
969
+ environmentIntensity,
970
+ exposure: Math.max(0.0001, readFinite(options.exposure, preset.exposure)),
971
+ horizonColor: readColor(options.horizonColor, preset.horizonColor),
972
+ zenithColor: readColor(options.zenithColor, preset.zenithColor),
973
+ sunDirection: Object.freeze(
974
+ normalizeVector3(options.sunDirection, preset.sunDirection)
975
+ ),
976
+ sunColor: readColor(options.sunColor, preset.sunColor),
977
+ ambientColor: readColor(options.ambientColor, preset.ambientColor),
978
+ environmentPortalMode,
979
+ environmentPortals,
980
+ };
981
+ const environmentLightSources = normalizeEnvironmentLightSources(
982
+ options.environmentLightSources ?? options.lightSources,
983
+ preset,
984
+ baseConfig
985
+ );
986
+ const dominantLightSource = findDominantEnvironmentLightSource(
987
+ environmentLightSources
988
+ );
989
+ const config = {
990
+ ...baseConfig,
991
+ environmentLightSources,
992
+ lightSources: environmentLightSources,
993
+ dominantLightSource,
994
+ };
995
+ const environmentColor = estimateEnvironmentColor(config);
996
+ const environmentMissLighting = createEnvironmentMissLighting(
997
+ dominantLightSource,
998
+ environmentColor
999
+ );
1000
+
1001
+ return Object.freeze({
1002
+ ...config,
1003
+ environmentColor,
1004
+ environmentMissLighting,
1005
+ wavefront: Object.freeze({
1006
+ environmentColor,
1007
+ ambientColor: config.ambientColor,
1008
+ environmentPortalMode: config.environmentPortalMode,
1009
+ environmentPortals: config.environmentPortals,
1010
+ environmentLightSources: config.environmentLightSources,
1011
+ lightSources: config.environmentLightSources,
1012
+ dominantLightSource,
1013
+ environmentMissLighting,
1014
+ environmentLighting: Object.freeze({
1015
+ horizonColor: config.horizonColor,
1016
+ zenithColor: config.zenithColor,
1017
+ sunDirection: Object.freeze([...config.sunDirection]),
1018
+ sunColor: config.sunColor,
1019
+ intensity: config.environmentIntensity,
1020
+ mode: config.environmentMode,
1021
+ exposure: config.exposure,
1022
+ environmentPortalMode: config.environmentPortalMode,
1023
+ environmentPortalCount: config.environmentPortals.length,
1024
+ environmentLightSources: config.environmentLightSources,
1025
+ environmentLightSourceCount: config.environmentLightSources.length,
1026
+ dominantLightSource,
1027
+ environmentMissLighting,
1028
+ }),
1029
+ }),
1030
+ });
1031
+ }
1032
+
1033
+ export function createWavefrontEnvironmentLightingOptions(options = {}) {
1034
+ const config = createEnvironmentLightingConfig(options);
1035
+ return Object.freeze({
1036
+ environmentColor: config.wavefront.environmentColor,
1037
+ ambientColor: config.wavefront.ambientColor,
1038
+ environmentPortalMode: config.wavefront.environmentPortalMode,
1039
+ environmentPortals: config.wavefront.environmentPortals,
1040
+ environmentLightSources: config.wavefront.environmentLightSources,
1041
+ lightSources: config.wavefront.environmentLightSources,
1042
+ dominantLightSource: config.wavefront.dominantLightSource,
1043
+ environmentMissLighting: config.wavefront.environmentMissLighting,
1044
+ environmentLighting: config.wavefront.environmentLighting,
1045
+ lightingEnvironment: config,
1046
+ });
1047
+ }
1048
+
154
1049
  const lightingImportanceLevels = Object.freeze([
155
1050
  "low",
156
1051
  "medium",