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