@flighthq/lighting 0.2.1-next.585.c7219b8 → 0.2.1-next.640.ec2ea9a

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.d.ts CHANGED
@@ -6,6 +6,7 @@ export * from './hemisphereLight';
6
6
  export * from './lightAnalysis';
7
7
  export * from './lightIntensity';
8
8
  export * from './pointLight';
9
+ export * from './sceneForwardLights';
9
10
  export * from './sceneLights';
10
11
  export * from './spotLight';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export * from './hemisphereLight';
6
6
  export * from './lightAnalysis';
7
7
  export * from './lightIntensity';
8
8
  export * from './pointLight';
9
+ export * from './sceneForwardLights';
9
10
  export * from './sceneLights';
10
11
  export * from './spotLight';
11
12
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC"}
@@ -1,6 +1,7 @@
1
- import type { BoundingSphereLike, Light } from '@flighthq/types';
1
+ import type { BoundingSphereLike, Light, PointLight, SpotLight } from '@flighthq/types';
2
+ export declare function getLightContributionAtBoundingSphere(light: Readonly<PointLight | SpotLight>, bounds: Readonly<BoundingSphereLike>): number;
2
3
  export declare function getLightInfluenceBounds(out: BoundingSphereLike, light: Readonly<Light>): void;
3
4
  export declare function getLightLuminance(light: Readonly<Light>): number;
4
5
  export declare function hasLightInfluenceOnBounds(light: Readonly<Light>, bounds: Readonly<BoundingSphereLike>): boolean;
5
- export declare function isLightShadowCasting(light: Readonly<Light>): boolean;
6
+ export declare function isLightCastingShadow(light: Readonly<Light>): boolean;
6
7
  //# sourceMappingURL=lightAnalysis.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lightAnalysis.d.ts","sourceRoot":"","sources":["../src/lightAnalysis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAc,MAAM,iBAAiB,CAAC;AAe7E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,kBAAkB,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,IAAI,CAqC7F;AAMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,MAAM,CAUhE;AAMD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,OAAO,CA0B/G;AAID,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,OAAO,CAMpE"}
1
+ {"version":3,"file":"lightAnalysis.d.ts","sourceRoot":"","sources":["../src/lightAnalysis.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAiBxF,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,QAAQ,CAAC,UAAU,GAAG,SAAS,CAAC,EACvC,MAAM,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GACnC,MAAM,CA+BR;AAMD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,kBAAkB,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,IAAI,CAqC7F;AAMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,MAAM,CAMhE;AAMD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,OAAO,CA0B/G;AAID,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,OAAO,CAMpE"}
@@ -1,4 +1,39 @@
1
+ import { getColorLuminance } from '@flighthq/color';
1
2
  import { AmbientLightKind, AreaLightKind, DirectionalLightKind, EnvironmentKind, HemisphereLightKind, PointLightKind, SpotLightKind, } from '@flighthq/types';
3
+ // Estimates the radiance contribution of one punctual light at a world-space bounding sphere.
4
+ // Distance uses the nearest sphere surface (`centerDistance - radius`) so large objects receive
5
+ // radius slack instead of being ranked only at their centre. The attenuation matches the forward
6
+ // shaders: inverse-square falloff multiplied by the squared glTF/UE4 range window; spot lights also
7
+ // apply the same smoothstep cone at the sphere centre. Multiplying by getLightLuminance makes the
8
+ // result suitable for deterministic forward-budget ranking rather than material-specific shading.
9
+ export function getLightContributionAtBoundingSphere(light, bounds) {
10
+ if (bounds.radius < 0)
11
+ return 0;
12
+ const centerDx = bounds.center.x - light.position.x;
13
+ const centerDy = bounds.center.y - light.position.y;
14
+ const centerDz = bounds.center.z - light.position.z;
15
+ const centerDistance = Math.hypot(centerDx, centerDy, centerDz);
16
+ const distance = Math.max(centerDistance - bounds.radius, 0);
17
+ const distanceSquared = distance * distance;
18
+ let window = 1;
19
+ if (light.range > 0) {
20
+ const factor = distanceSquared / (light.range * light.range);
21
+ const windowed = Math.max(0, Math.min(1, 1 - factor * factor));
22
+ window = windowed * windowed;
23
+ }
24
+ let contribution = (getLightLuminance(light) * window) / Math.max(distanceSquared, 1e-4);
25
+ if (light.kind === SpotLightKind) {
26
+ const spot = light;
27
+ const directionLength = Math.hypot(spot.direction.x, spot.direction.y, spot.direction.z);
28
+ const inverseRayLength = centerDistance > 0 ? 1 / centerDistance : 0;
29
+ const inverseDirectionLength = directionLength > 0 ? 1 / directionLength : 0;
30
+ const cosine = (spot.direction.x * centerDx + spot.direction.y * centerDy + spot.direction.z * centerDz) *
31
+ inverseRayLength *
32
+ inverseDirectionLength;
33
+ contribution *= smoothstep(spot.outerConeCos, spot.innerConeCos, centerDistance > 0 ? cosine : 1);
34
+ }
35
+ return contribution;
36
+ }
2
37
  // Writes the world-space influence bounding sphere of a light into `out`. The sphere bounds the
3
38
  // region the light can illuminate. Lights with a finite `range` produce a sphere centered at
4
39
  // `position` with `radius = range`. Non-spatial lights (ambient, hemisphere, environment) and
@@ -39,21 +74,17 @@ export function getLightInfluenceBounds(out, light) {
39
74
  out.center.z = 0;
40
75
  out.radius = -1;
41
76
  }
42
- // Returns the perceptual luminance of a light's color × intensity. The luminance is computed
43
- // from the packed sRgb-albedo RGBA color using the standard ITU-R BT.709 coefficients, then
44
- // scaled by `intensity`. Useful for ranking lights by visual importance when prioritizing a
45
- // forward-light budget.
77
+ // Returns the linear-light luminance of a light's color × intensity. Packed sRGB channels are
78
+ // gamma-decoded through the shared color primitive before applying the Rec. 709 weights, matching
79
+ // the radiance that packSceneLightBlock sends to the shader. Useful for ranking lights by their
80
+ // rendered contribution when prioritizing a forward-light budget.
46
81
  export function getLightLuminance(light) {
47
82
  const colored = light;
48
83
  const color = colored.color;
49
84
  if (color === undefined)
50
85
  return 0;
51
- const r = ((color >>> 24) & 0xff) / 255;
52
- const g = ((color >>> 16) & 0xff) / 255;
53
- const b = ((color >>> 8) & 0xff) / 255;
54
- const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
55
86
  const intensity = colored.intensity ?? 1;
56
- return luma * intensity;
87
+ return getColorLuminance(color) * intensity;
57
88
  }
58
89
  // Returns true when `light` could influence a point within `bounds`. Non-spatial lights (ambient,
59
90
  // hemisphere, directional, environment) and infinite-range lights always return true. For spatial
@@ -87,11 +118,17 @@ export function hasLightInfluenceOnBounds(light, bounds) {
87
118
  }
88
119
  // Returns true when the light is configured to cast shadows. Non-shadow-capable light types
89
120
  // (AmbientLight, HemisphereLight, Environment) always return false.
90
- export function isLightShadowCasting(light) {
121
+ export function isLightCastingShadow(light) {
91
122
  const kind = light.kind;
92
123
  if (kind === AmbientLightKind || kind === HemisphereLightKind || kind === EnvironmentKind) {
93
124
  return false;
94
125
  }
95
126
  return light.castsShadow;
96
127
  }
128
+ function smoothstep(edge0, edge1, value) {
129
+ if (edge0 === edge1)
130
+ return value < edge0 ? 0 : 1;
131
+ const t = Math.max(0, Math.min(1, (value - edge0) / (edge1 - edge0)));
132
+ return t * t * (3 - 2 * t);
133
+ }
97
134
  //# sourceMappingURL=lightAnalysis.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lightAnalysis.js","sourceRoot":"","sources":["../src/lightAnalysis.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lightAnalysis.js","sourceRoot":"","sources":["../src/lightAnalysis.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { BoundingSphereLike, SceneForwardLightSelection, SceneLightsLike } from '@flighthq/types';
2
+ export declare function selectSceneForwardLights(out: SceneForwardLightSelection, lights: Readonly<SceneLightsLike>, bounds: Readonly<BoundingSphereLike>): void;
3
+ //# sourceMappingURL=sceneForwardLights.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sceneForwardLights.d.ts","sourceRoot":"","sources":["../src/sceneForwardLights.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAElB,0BAA0B,EAC1B,eAAe,EAEhB,MAAM,iBAAiB,CAAC;AAazB,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,0BAA0B,EAC/B,MAAM,EAAE,QAAQ,CAAC,eAAe,CAAC,EACjC,MAAM,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GACnC,IAAI,CAkCN"}
@@ -0,0 +1,72 @@
1
+ import { MAX_FORWARD_LIGHTS } from '@flighthq/types';
2
+ import { getLightContributionAtBoundingSphere } from './lightAnalysis';
3
+ // Selects the strongest point and spot contributors for one object's forward-light budget. Each
4
+ // family has its own MAX_FORWARD_LIGHTS budget, matching the shader's separate fixed arrays and
5
+ // counts. Ranking uses descriptor-side shader-equivalent attenuation at `bounds`; equal scores
6
+ // retain their family input order, making captures reproducible. Zero-contribution lights are
7
+ // omitted.
8
+ //
9
+ // `out.point` and `out.spot` are reused mutable arrays. All inputs are read into fixed-size scratch
10
+ // before either output array is changed, so `out` may alias `lights`.
11
+ export function selectSceneForwardLights(out, lights, bounds) {
12
+ const points = lights.point;
13
+ const spots = lights.spot;
14
+ const pointCount = selectStrongestLights(points, bounds, scratchSelectedPointLights, scratchSelectedPointIndices, scratchSelectedPointScores);
15
+ const spotCount = selectStrongestLights(spots, bounds, scratchSelectedSpotLights, scratchSelectedSpotIndices, scratchSelectedSpotScores);
16
+ const outPoints = out.point;
17
+ const outSpots = out.spot;
18
+ const outIndices = out.indices;
19
+ outIndices.length = 0;
20
+ outPoints.length = 0;
21
+ outSpots.length = 0;
22
+ for (let i = 0; i < pointCount; i++) {
23
+ outIndices.push(scratchSelectedPointIndices[i]);
24
+ outPoints.push(scratchSelectedPointLights[i]);
25
+ }
26
+ for (let i = 0; i < spotCount; i++) {
27
+ // Bitwise complement makes every spot key negative and every point key non-negative, so two
28
+ // selections with the same family-local index cannot collide during block deduplication.
29
+ outIndices.push(~scratchSelectedSpotIndices[i]);
30
+ outSpots.push(scratchSelectedSpotLights[i]);
31
+ }
32
+ }
33
+ function selectStrongestLights(lights, bounds, selectedLights, selectedIndices, selectedScores) {
34
+ let selectedCount = 0;
35
+ if (lights === undefined)
36
+ return selectedCount;
37
+ for (let inputIndex = 0; inputIndex < lights.length; inputIndex++) {
38
+ const light = lights[inputIndex];
39
+ const score = getLightContributionAtBoundingSphere(light, bounds);
40
+ if (!(score > 0))
41
+ continue;
42
+ let insertAt = selectedCount;
43
+ while (insertAt > 0) {
44
+ const previous = insertAt - 1;
45
+ if (score < selectedScores[previous])
46
+ break;
47
+ if (score === selectedScores[previous] && inputIndex > selectedIndices[previous])
48
+ break;
49
+ insertAt--;
50
+ }
51
+ if (insertAt >= MAX_FORWARD_LIGHTS)
52
+ continue;
53
+ const nextCount = Math.min(selectedCount + 1, MAX_FORWARD_LIGHTS);
54
+ for (let i = nextCount - 1; i > insertAt; i--) {
55
+ selectedLights[i] = selectedLights[i - 1];
56
+ selectedIndices[i] = selectedIndices[i - 1];
57
+ selectedScores[i] = selectedScores[i - 1];
58
+ }
59
+ selectedLights[insertAt] = light;
60
+ selectedIndices[insertAt] = inputIndex;
61
+ selectedScores[insertAt] = score;
62
+ selectedCount = nextCount;
63
+ }
64
+ return selectedCount;
65
+ }
66
+ const scratchSelectedPointIndices = new Int32Array(MAX_FORWARD_LIGHTS);
67
+ const scratchSelectedPointLights = new Array(MAX_FORWARD_LIGHTS);
68
+ const scratchSelectedPointScores = new Float64Array(MAX_FORWARD_LIGHTS);
69
+ const scratchSelectedSpotIndices = new Int32Array(MAX_FORWARD_LIGHTS);
70
+ const scratchSelectedSpotLights = new Array(MAX_FORWARD_LIGHTS);
71
+ const scratchSelectedSpotScores = new Float64Array(MAX_FORWARD_LIGHTS);
72
+ //# sourceMappingURL=sceneForwardLights.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sceneForwardLights.js","sourceRoot":"","sources":["../src/sceneForwardLights.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/lighting",
3
- "version": "0.2.1-next.585.c7219b8",
3
+ "version": "0.2.1-next.640.ec2ea9a",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/flighthq/flight.git",
@@ -32,9 +32,10 @@
32
32
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
33
33
  },
34
34
  "dependencies": {
35
- "@flighthq/entity": "0.2.1-next.585.c7219b8",
36
- "@flighthq/geometry": "0.2.1-next.585.c7219b8",
37
- "@flighthq/types": "0.2.1-next.585.c7219b8"
35
+ "@flighthq/color": "0.2.1-next.640.ec2ea9a",
36
+ "@flighthq/entity": "0.2.1-next.640.ec2ea9a",
37
+ "@flighthq/geometry": "0.2.1-next.640.ec2ea9a",
38
+ "@flighthq/types": "0.2.1-next.640.ec2ea9a"
38
39
  },
39
40
  "devDependencies": {
40
41
  "typescript": "^5.3.0"
@@ -6,14 +6,57 @@ import { createDirectionalLight } from './directionalLight';
6
6
  import { createEnvironment } from './environment';
7
7
  import { createHemisphereLight } from './hemisphereLight';
8
8
  import {
9
+ getLightContributionAtBoundingSphere,
9
10
  getLightInfluenceBounds,
10
11
  getLightLuminance,
11
12
  hasLightInfluenceOnBounds,
12
- isLightShadowCasting,
13
+ isLightCastingShadow,
13
14
  } from './lightAnalysis';
14
15
  import { createPointLight } from './pointLight';
15
16
  import { createSpotLight } from './spotLight';
16
17
 
18
+ describe('getLightContributionAtBoundingSphere', () => {
19
+ it('matches inverse-square attenuation for an infinite-range point light', () => {
20
+ const light = createPointLight({ intensity: 1, position: { x: 0, y: 0, z: 0 }, range: -1 });
21
+ const near = getLightContributionAtBoundingSphere(light, createBoundingSphere(2, 0, 0, 0));
22
+ const far = getLightContributionAtBoundingSphere(light, createBoundingSphere(4, 0, 0, 0));
23
+ expect(near / far).toBeCloseTo(4);
24
+ });
25
+
26
+ it('applies radius slack at the nearest sphere surface', () => {
27
+ const light = createPointLight({ position: { x: 0, y: 0, z: 0 }, range: -1 });
28
+ const point = getLightContributionAtBoundingSphere(light, createBoundingSphere(4, 0, 0, 0));
29
+ const sphere = getLightContributionAtBoundingSphere(light, createBoundingSphere(4, 0, 0, 2));
30
+ expect(sphere / point).toBeCloseTo(4);
31
+ });
32
+
33
+ it('applies the squared smooth range window and reaches zero at the range', () => {
34
+ const light = createPointLight({ position: { x: 0, y: 0, z: 0 }, range: 4 });
35
+ const inside = getLightContributionAtBoundingSphere(light, createBoundingSphere(2, 0, 0, 0));
36
+ const edge = getLightContributionAtBoundingSphere(light, createBoundingSphere(4, 0, 0, 0));
37
+ expect(inside).toBeGreaterThan(0);
38
+ expect(edge).toBe(0);
39
+ });
40
+
41
+ it('applies spot smoothstep cone attenuation', () => {
42
+ const light = createSpotLight({
43
+ direction: { x: 1, y: 0, z: 0 },
44
+ innerConeDegrees: 10,
45
+ outerConeDegrees: 30,
46
+ position: { x: 0, y: 0, z: 0 },
47
+ range: -1,
48
+ });
49
+ const inside = getLightContributionAtBoundingSphere(light, createBoundingSphere(4, 0, 0, 0));
50
+ const outside = getLightContributionAtBoundingSphere(light, createBoundingSphere(0, 4, 0, 0));
51
+ expect(inside).toBeGreaterThan(0);
52
+ expect(outside).toBe(0);
53
+ });
54
+
55
+ it('returns zero for empty bounds', () => {
56
+ expect(getLightContributionAtBoundingSphere(createPointLight(), createBoundingSphere(0, 0, 0, -1))).toBe(0);
57
+ });
58
+ });
59
+
17
60
  describe('getLightInfluenceBounds', () => {
18
61
  it('returns sentinel radius (-1) for ambient lights', () => {
19
62
  const light = createAmbientLight();
@@ -95,6 +138,11 @@ describe('getLightLuminance', () => {
95
138
  expect(getLightLuminance(light)).toBeCloseTo(1, 4);
96
139
  });
97
140
 
141
+ it('gamma-decodes packed sRGB before computing luminance', () => {
142
+ const light = createAmbientLight({ color: 0x808080ff, intensity: 1 });
143
+ expect(getLightLuminance(light)).toBeCloseTo(0.21586, 5);
144
+ });
145
+
98
146
  it('scales luminance by intensity', () => {
99
147
  const a = createAmbientLight({ color: 0xffffffff, intensity: 2 });
100
148
  const b = createAmbientLight({ color: 0xffffffff, intensity: 4 });
@@ -148,41 +196,41 @@ describe('hasLightInfluenceOnBounds', () => {
148
196
  });
149
197
  });
150
198
 
151
- describe('isLightShadowCasting', () => {
199
+ describe('isLightCastingShadow', () => {
152
200
  it('returns false for ambient lights', () => {
153
- expect(isLightShadowCasting(createAmbientLight())).toBe(false);
201
+ expect(isLightCastingShadow(createAmbientLight())).toBe(false);
154
202
  });
155
203
 
156
204
  it('returns false for hemisphere lights', () => {
157
- expect(isLightShadowCasting(createHemisphereLight())).toBe(false);
205
+ expect(isLightCastingShadow(createHemisphereLight())).toBe(false);
158
206
  });
159
207
 
160
208
  it('returns false for environment lights', () => {
161
- expect(isLightShadowCasting(createEnvironment())).toBe(false);
209
+ expect(isLightCastingShadow(createEnvironment())).toBe(false);
162
210
  });
163
211
 
164
212
  it('returns false for directional lights with castsShadow: false', () => {
165
213
  const light = createDirectionalLight({ castsShadow: false });
166
- expect(isLightShadowCasting(light)).toBe(false);
214
+ expect(isLightCastingShadow(light)).toBe(false);
167
215
  });
168
216
 
169
217
  it('returns true for directional lights with castsShadow: true', () => {
170
218
  const light = createDirectionalLight({ castsShadow: true });
171
- expect(isLightShadowCasting(light)).toBe(true);
219
+ expect(isLightCastingShadow(light)).toBe(true);
172
220
  });
173
221
 
174
222
  it('returns true for point lights with castsShadow: true', () => {
175
223
  const light = createPointLight({ castsShadow: true });
176
- expect(isLightShadowCasting(light)).toBe(true);
224
+ expect(isLightCastingShadow(light)).toBe(true);
177
225
  });
178
226
 
179
227
  it('returns true for spot lights with castsShadow: true', () => {
180
228
  const light = createSpotLight({ castsShadow: true });
181
- expect(isLightShadowCasting(light)).toBe(true);
229
+ expect(isLightCastingShadow(light)).toBe(true);
182
230
  });
183
231
 
184
232
  it('returns true for area lights with castsShadow: true', () => {
185
233
  const light = createAreaLight({ castsShadow: true });
186
- expect(isLightShadowCasting(light)).toBe(true);
234
+ expect(isLightCastingShadow(light)).toBe(true);
187
235
  });
188
236
  });
@@ -0,0 +1,120 @@
1
+ import { createBoundingSphere } from '@flighthq/geometry';
2
+ import type { SceneForwardLightSelection, SceneLightsLike } from '@flighthq/types';
3
+
4
+ import { createPointLight } from './pointLight';
5
+ import { selectSceneForwardLights } from './sceneForwardLights';
6
+ import { createSpotLight } from './spotLight';
7
+
8
+ function selection(): SceneForwardLightSelection {
9
+ return { indices: [], point: [], spot: [] };
10
+ }
11
+
12
+ function spotLights(xs: readonly number[]) {
13
+ return xs.map((x) =>
14
+ createSpotLight({
15
+ direction: { x: -1, y: 0, z: 0 },
16
+ innerConeDegrees: 45,
17
+ outerConeDegrees: 60,
18
+ position: { x, y: 0, z: 0 },
19
+ range: -1,
20
+ }),
21
+ );
22
+ }
23
+
24
+ describe('selectSceneForwardLights', () => {
25
+ it('keeps all three points and three spots when both families are within budget', () => {
26
+ const points = [8, 7, 6].map((x) => createPointLight({ position: { x, y: 0, z: 0 }, range: -1 }));
27
+ const spots = spotLights([5, 4, 3]);
28
+ const out = selection();
29
+ selectSceneForwardLights(
30
+ out,
31
+ { ambient: null, directional: null, point: points, spot: spots },
32
+ createBoundingSphere(0, 0, 0, 0),
33
+ );
34
+ expect(out.point).toHaveLength(3);
35
+ expect(out.spot).toHaveLength(3);
36
+ expect(new Set(out.point)).toEqual(new Set(points));
37
+ expect(new Set(out.spot)).toEqual(new Set(spots));
38
+ });
39
+
40
+ it('ranks the family cutoff by linear-light radiance instead of packed sRGB channels', () => {
41
+ const gray = Array.from({ length: 4 }, () =>
42
+ createPointLight({
43
+ color: 0x808080ff,
44
+ intensity: 1,
45
+ position: { x: 2, y: 0, z: 0 },
46
+ range: -1,
47
+ }),
48
+ );
49
+ const white = createPointLight({
50
+ color: 0xffffffff,
51
+ intensity: 0.3,
52
+ position: { x: 2, y: 0, z: 0 },
53
+ range: -1,
54
+ });
55
+ const out = selection();
56
+ selectSceneForwardLights(
57
+ out,
58
+ { ambient: null, directional: null, point: [...gray, white] },
59
+ createBoundingSphere(0, 0, 0, 0),
60
+ );
61
+ expect(out.point).toContain(white);
62
+ expect(out.point).toHaveLength(4);
63
+ });
64
+
65
+ it('selects the strongest four points and strongest four spots independently', () => {
66
+ const points = [8, 7, 6, 5, 4, 3].map((x) => createPointLight({ position: { x, y: 0, z: 0 }, range: -1 }));
67
+ const spots = spotLights([8, 7, 6, 5, 4, 3]);
68
+ const out = selection();
69
+ selectSceneForwardLights(
70
+ out,
71
+ { ambient: null, directional: null, point: points, spot: spots },
72
+ createBoundingSphere(0, 0, 0, 0),
73
+ );
74
+ expect(out.point).toEqual(points.slice(2).reverse());
75
+ expect(out.spot).toEqual(spots.slice(2).reverse());
76
+ expect(out.indices).toEqual([5, 4, 3, 2, ~5, ~4, ~3, ~2]);
77
+ });
78
+
79
+ it('uses stable input order to break equal-contribution ties', () => {
80
+ const points = Array.from({ length: 6 }, () => createPointLight({ position: { x: 2, y: 0, z: 0 }, range: -1 }));
81
+ const out = selection();
82
+ selectSceneForwardLights(
83
+ out,
84
+ { ambient: null, directional: null, point: points },
85
+ createBoundingSphere(0, 0, 0, 0),
86
+ );
87
+ expect(out.point).toEqual(points.slice(0, 4));
88
+ expect(out.indices).toEqual([0, 1, 2, 3]);
89
+ });
90
+
91
+ it('omits zero-contribution lights', () => {
92
+ const out = selection();
93
+ selectSceneForwardLights(
94
+ out,
95
+ {
96
+ ambient: null,
97
+ directional: null,
98
+ point: [createPointLight({ position: { x: 10, y: 0, z: 0 }, range: 2 })],
99
+ },
100
+ createBoundingSphere(0, 0, 0, 0),
101
+ );
102
+ expect(out.point).toHaveLength(0);
103
+ expect(out.indices).toHaveLength(0);
104
+ });
105
+
106
+ it('reads all inputs before writing an aliased output', () => {
107
+ const points = [
108
+ createPointLight({ position: { x: 4, y: 0, z: 0 }, range: -1 }),
109
+ createPointLight({ position: { x: 1, y: 0, z: 0 }, range: -1 }),
110
+ ];
111
+ const far = points[0];
112
+ const near = points[1];
113
+ const lights: SceneLightsLike = { ambient: null, directional: null, point: points, spot: [] };
114
+ const out = lights as SceneLightsLike & SceneForwardLightSelection;
115
+ out.indices = [];
116
+ selectSceneForwardLights(out, lights, createBoundingSphere(0, 0, 0, 0));
117
+ expect(out.point).toEqual([near, far]);
118
+ expect(out.indices).toEqual([1, 0]);
119
+ });
120
+ });