@plasius/gpu-lighting 0.1.19 → 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,10 +136,45 @@ 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",
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",
143
178
  ]);
144
179
  export const defaultAdaptiveLightingProfilePolicy = Object.freeze({
145
180
  preferredProfile: "reference",
@@ -188,13 +223,301 @@ function readColor(value, fallback) {
188
223
  ]);
189
224
  }
190
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
+
191
249
  function readFinite(value, fallback) {
192
250
  return Number.isFinite(value) ? value : fallback;
193
251
  }
194
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
+
195
516
  const environmentLightingPresets = Object.freeze({
196
- "moonlit-harbor": Object.freeze({
517
+ "moonlit-harbor": defineEnvironmentPreset({
197
518
  preset: "moonlit-harbor",
519
+ scene: "harbor",
520
+ timeOfDay: "night",
198
521
  environmentMode: 0,
199
522
  environmentIntensity: 0.86,
200
523
  exposure: 1,
@@ -203,9 +526,30 @@ const environmentLightingPresets = Object.freeze({
203
526
  sunDirection: Object.freeze(normalizeVector3([0.22, 0.88, 0.42], [0, 1, 0])),
204
527
  sunColor: freezeVec4([2.1, 2.25, 2.65, 1]),
205
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
+ ],
206
548
  }),
207
- "product-studio": Object.freeze({
549
+ "product-studio": defineEnvironmentPreset({
208
550
  preset: "product-studio",
551
+ scene: "studio",
552
+ timeOfDay: "midday",
209
553
  environmentMode: 1,
210
554
  environmentIntensity: 1.05,
211
555
  exposure: 1,
@@ -214,9 +558,31 @@ const environmentLightingPresets = Object.freeze({
214
558
  sunDirection: Object.freeze(normalizeVector3([0.18, 0.93, 0.24], [0, 1, 0])),
215
559
  sunColor: freezeVec4([3.8, 3.55, 2.85, 1]),
216
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
+ ],
217
581
  }),
218
- "neutral-studio": Object.freeze({
582
+ "neutral-studio": defineEnvironmentPreset({
219
583
  preset: "neutral-studio",
584
+ scene: "studio",
585
+ timeOfDay: "midday",
220
586
  environmentMode: 2,
221
587
  environmentIntensity: 0.95,
222
588
  exposure: 1,
@@ -225,13 +591,339 @@ const environmentLightingPresets = Object.freeze({
225
591
  sunDirection: Object.freeze(normalizeVector3([-0.24, 0.86, 0.36], [0, 1, 0])),
226
592
  sunColor: freezeVec4([2.4, 2.35, 2.2, 1]),
227
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
+ ],
228
901
  }),
229
902
  });
230
903
 
231
- function resolveEnvironmentPreset(name) {
904
+ export const lightingEnvironmentPresetNames = Object.freeze(
905
+ Object.keys(environmentLightingPresets)
906
+ );
907
+
908
+ function resolveEnvironmentPreset(name, timeOfDay) {
232
909
  const presetName = typeof name === "string" && name.length > 0 ? name : "product-studio";
233
910
  const preset = environmentLightingPresets[presetName];
234
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
+ }
235
927
  throw new Error(
236
928
  `Unknown lighting environment preset "${presetName}". Expected one of: ${lightingEnvironmentPresetNames.join(", ")}.`
237
929
  );
@@ -244,22 +936,34 @@ function estimateEnvironmentColor(config) {
244
936
  const zenithWeight = 1 - horizonWeight;
245
937
  const glowWeight = 0.055;
246
938
  const intensity = Math.max(config.environmentIntensity, 0.0001);
247
- return freezeVec4([
939
+ return ensureNonNullColor([
248
940
  (config.horizonColor[0] * horizonWeight + config.zenithColor[0] * zenithWeight + config.sunColor[0] * glowWeight) * intensity,
249
941
  (config.horizonColor[1] * horizonWeight + config.zenithColor[1] * zenithWeight + config.sunColor[1] * glowWeight) * intensity,
250
942
  (config.horizonColor[2] * horizonWeight + config.zenithColor[2] * zenithWeight + config.sunColor[2] * glowWeight) * intensity,
251
943
  1,
252
- ]);
944
+ ], config.dominantLightSource?.radiance ?? config.sunColor);
253
945
  }
254
946
 
255
947
  export function createEnvironmentLightingConfig(options = {}) {
256
- const preset = resolveEnvironmentPreset(options.preset ?? options.name);
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
+ );
257
959
  const environmentIntensity = Math.max(
258
960
  readFinite(options.environmentIntensity ?? options.intensity, preset.environmentIntensity),
259
961
  0.0001
260
962
  );
261
- const config = {
963
+ const baseConfig = {
262
964
  preset: preset.preset,
965
+ scene: preset.scene,
966
+ timeOfDay: preset.timeOfDay,
263
967
  profile: typeof options.profile === "string" ? options.profile : defaultLightingProfile,
264
968
  environmentMode: Math.max(0, Math.trunc(readFinite(options.environmentMode, preset.environmentMode))),
265
969
  environmentIntensity,
@@ -271,15 +975,42 @@ export function createEnvironmentLightingConfig(options = {}) {
271
975
  ),
272
976
  sunColor: readColor(options.sunColor, preset.sunColor),
273
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,
274
994
  };
275
995
  const environmentColor = estimateEnvironmentColor(config);
996
+ const environmentMissLighting = createEnvironmentMissLighting(
997
+ dominantLightSource,
998
+ environmentColor
999
+ );
276
1000
 
277
1001
  return Object.freeze({
278
1002
  ...config,
279
1003
  environmentColor,
1004
+ environmentMissLighting,
280
1005
  wavefront: Object.freeze({
281
1006
  environmentColor,
282
1007
  ambientColor: config.ambientColor,
1008
+ environmentPortalMode: config.environmentPortalMode,
1009
+ environmentPortals: config.environmentPortals,
1010
+ environmentLightSources: config.environmentLightSources,
1011
+ lightSources: config.environmentLightSources,
1012
+ dominantLightSource,
1013
+ environmentMissLighting,
283
1014
  environmentLighting: Object.freeze({
284
1015
  horizonColor: config.horizonColor,
285
1016
  zenithColor: config.zenithColor,
@@ -288,6 +1019,12 @@ export function createEnvironmentLightingConfig(options = {}) {
288
1019
  intensity: config.environmentIntensity,
289
1020
  mode: config.environmentMode,
290
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,
291
1028
  }),
292
1029
  }),
293
1030
  });
@@ -298,6 +1035,12 @@ export function createWavefrontEnvironmentLightingOptions(options = {}) {
298
1035
  return Object.freeze({
299
1036
  environmentColor: config.wavefront.environmentColor,
300
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,
301
1044
  environmentLighting: config.wavefront.environmentLighting,
302
1045
  lightingEnvironment: config,
303
1046
  });