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