@flighthq/lighting 0.1.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/ambientLight.d.ts +8 -0
- package/dist/ambientLight.d.ts.map +1 -0
- package/dist/ambientLight.js +16 -0
- package/dist/ambientLight.js.map +1 -0
- package/dist/areaLight.d.ts +18 -0
- package/dist/areaLight.d.ts.map +1 -0
- package/dist/areaLight.js +75 -0
- package/dist/areaLight.js.map +1 -0
- package/dist/colorFromKelvin.d.ts +2 -0
- package/dist/colorFromKelvin.d.ts.map +1 -0
- package/dist/colorFromKelvin.js +44 -0
- package/dist/colorFromKelvin.js.map +1 -0
- package/dist/directionalLight.d.ts +15 -0
- package/dist/directionalLight.d.ts.map +1 -0
- package/dist/directionalLight.js +56 -0
- package/dist/directionalLight.js.map +1 -0
- package/dist/environment.d.ts +8 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +18 -0
- package/dist/environment.js.map +1 -0
- package/dist/hemisphereLight.d.ts +9 -0
- package/dist/hemisphereLight.d.ts.map +1 -0
- package/dist/hemisphereLight.js +22 -0
- package/dist/hemisphereLight.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/lightAnalysis.d.ts +6 -0
- package/dist/lightAnalysis.d.ts.map +1 -0
- package/dist/lightAnalysis.js +91 -0
- package/dist/lightAnalysis.js.map +1 -0
- package/dist/pointLight.d.ts +14 -0
- package/dist/pointLight.d.ts.map +1 -0
- package/dist/pointLight.js +35 -0
- package/dist/pointLight.js.map +1 -0
- package/dist/spotLight.d.ts +25 -0
- package/dist/spotLight.d.ts.map +1 -0
- package/dist/spotLight.js +86 -0
- package/dist/spotLight.js.map +1 -0
- package/package.json +39 -0
- package/src/ambientLight.test.ts +29 -0
- package/src/areaLight.test.ts +100 -0
- package/src/colorFromKelvin.test.ts +48 -0
- package/src/directionalLight.test.ts +89 -0
- package/src/environment.test.ts +49 -0
- package/src/hemisphereLight.test.ts +31 -0
- package/src/lightAnalysis.test.ts +188 -0
- package/src/pointLight.test.ts +55 -0
- package/src/spotLight.test.ts +143 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity';
|
|
2
|
+
import { cloneVector3, createVector3, setVector3 } from '@flighthq/geometry';
|
|
3
|
+
import { SpotLightKind } from '@flighthq/types';
|
|
4
|
+
// Independent copy of a spot light's data, including fresh `position`/`direction` vectors.
|
|
5
|
+
export function cloneSpotLight(source) {
|
|
6
|
+
return createEntity({
|
|
7
|
+
castsShadow: source.castsShadow,
|
|
8
|
+
color: source.color,
|
|
9
|
+
direction: cloneVector3(source.direction),
|
|
10
|
+
innerConeCos: source.innerConeCos,
|
|
11
|
+
intensity: source.intensity,
|
|
12
|
+
kind: SpotLightKind,
|
|
13
|
+
normalBias: source.normalBias,
|
|
14
|
+
outerConeCos: source.outerConeCos,
|
|
15
|
+
pcfRadius: source.pcfRadius,
|
|
16
|
+
position: cloneVector3(source.position),
|
|
17
|
+
range: source.range,
|
|
18
|
+
shadowBias: source.shadowBias,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
// Cone-restricted point light. `position`/`direction` are world-space; the cone is stored as the
|
|
22
|
+
// precomputed cosines of its inner and outer half-angles (innerConeCos >= outerConeCos). Color is
|
|
23
|
+
// packed sRgb-albedo RGBA (0xrrggbbaa); defaults to opaque white at unit intensity, at the origin
|
|
24
|
+
// facing down (0, -1, 0), 0-degree inner / 45-degree outer cone, infinite range, shadows off.
|
|
25
|
+
export function createSpotLight(options) {
|
|
26
|
+
const position = options?.position;
|
|
27
|
+
const direction = options?.direction;
|
|
28
|
+
const light = createEntity({
|
|
29
|
+
castsShadow: options?.castsShadow ?? false,
|
|
30
|
+
color: options?.color ?? 0xffffffff,
|
|
31
|
+
direction: direction ? cloneVector3(direction) : createVector3(0, -1, 0),
|
|
32
|
+
innerConeCos: 1,
|
|
33
|
+
intensity: options?.intensity ?? 1,
|
|
34
|
+
kind: SpotLightKind,
|
|
35
|
+
normalBias: options?.normalBias ?? 0,
|
|
36
|
+
outerConeCos: 1,
|
|
37
|
+
pcfRadius: options?.pcfRadius ?? 0,
|
|
38
|
+
position: position ? cloneVector3(position) : createVector3(0, 0, 0),
|
|
39
|
+
range: options?.range ?? -1,
|
|
40
|
+
shadowBias: options?.shadowBias ?? 0,
|
|
41
|
+
});
|
|
42
|
+
setSpotLightCone(light, options?.innerConeDegrees ?? 0, options?.outerConeDegrees ?? 45);
|
|
43
|
+
return light;
|
|
44
|
+
}
|
|
45
|
+
// Reads the inner and outer cone half-angles from precomputed cosines and writes them as degrees
|
|
46
|
+
// into `out`. Provides the round-trip for `createSpotLight` which accepts degrees but stores
|
|
47
|
+
// cosines. The `out` object is a plain `{ innerDegrees, outerDegrees }` struct.
|
|
48
|
+
export function getSpotLightConeDegrees(out, source) {
|
|
49
|
+
out.innerDegrees = (Math.acos(source.innerConeCos) * 180) / Math.PI;
|
|
50
|
+
out.outerDegrees = (Math.acos(source.outerConeCos) * 180) / Math.PI;
|
|
51
|
+
}
|
|
52
|
+
// Writes the precomputed cone cosines from inner/outer half-angles given in degrees. A larger
|
|
53
|
+
// half-angle yields a smaller cosine, so the stored invariant innerConeCos >= outerConeCos holds
|
|
54
|
+
// exactly when innerDegrees <= outerDegrees; callers are responsible for ordering their inputs.
|
|
55
|
+
export function setSpotLightCone(out, innerDegrees, outerDegrees) {
|
|
56
|
+
out.innerConeCos = Math.cos((innerDegrees * Math.PI) / 180);
|
|
57
|
+
out.outerConeCos = Math.cos((outerDegrees * Math.PI) / 180);
|
|
58
|
+
}
|
|
59
|
+
// Writes a normalized direction into `out.direction`. Normalizes x/y/z before storing.
|
|
60
|
+
// Alias-safe: scalar arguments cannot alias the output vector.
|
|
61
|
+
export function setSpotLightDirection(out, x, y, z) {
|
|
62
|
+
const lx = x;
|
|
63
|
+
const ly = y;
|
|
64
|
+
const lz = z;
|
|
65
|
+
const len = Math.sqrt(lx * lx + ly * ly + lz * lz);
|
|
66
|
+
if (len > 0) {
|
|
67
|
+
setVector3(out.direction, lx / len, ly / len, lz / len);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Writes the normalized direction from `out.position` toward `targetX,targetY,targetZ` into
|
|
71
|
+
// `out.direction`. Derives `direction` from a target point — the most common authoring gesture.
|
|
72
|
+
// If `out.position` equals the target (zero-length direction), `direction` is left unchanged.
|
|
73
|
+
export function setSpotLightTarget(out, targetX, targetY, targetZ) {
|
|
74
|
+
// Read position into locals before writing direction for alias safety.
|
|
75
|
+
const px = out.position.x;
|
|
76
|
+
const py = out.position.y;
|
|
77
|
+
const pz = out.position.z;
|
|
78
|
+
const dx = targetX - px;
|
|
79
|
+
const dy = targetY - py;
|
|
80
|
+
const dz = targetZ - pz;
|
|
81
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
82
|
+
if (len > 0) {
|
|
83
|
+
setVector3(out.direction, dx / len, dy / len, dz / len);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=spotLight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spotLight.js","sourceRoot":"","sources":["../src/spotLight.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/lighting",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/entity": "0.1.0",
|
|
31
|
+
"@flighthq/geometry": "0.1.0",
|
|
32
|
+
"@flighthq/types": "0.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.3.0"
|
|
36
|
+
},
|
|
37
|
+
"description": "Light descriptors: ambient/directional/point/spot/hemisphere/area and environment",
|
|
38
|
+
"sideEffects": false
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AmbientLightKind } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { cloneAmbientLight, createAmbientLight } from './ambientLight';
|
|
4
|
+
|
|
5
|
+
describe('cloneAmbientLight', () => {
|
|
6
|
+
it('creates an independent copy with the same fields', () => {
|
|
7
|
+
const light = createAmbientLight({ color: 0x112233ff, intensity: 0.5 });
|
|
8
|
+
const copy = cloneAmbientLight(light);
|
|
9
|
+
expect(copy).not.toBe(light);
|
|
10
|
+
expect(copy.color).toBe(0x112233ff);
|
|
11
|
+
expect(copy.intensity).toBe(0.5);
|
|
12
|
+
expect(copy.kind).toBe(AmbientLightKind);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('createAmbientLight', () => {
|
|
17
|
+
it('applies opaque-white defaults at unit intensity', () => {
|
|
18
|
+
const light = createAmbientLight();
|
|
19
|
+
expect(light.color).toBe(0xffffffff);
|
|
20
|
+
expect(light.intensity).toBe(1);
|
|
21
|
+
expect(light.kind).toBe(AmbientLightKind);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('overrides color and intensity from options', () => {
|
|
25
|
+
const light = createAmbientLight({ color: 0x00ff00ff, intensity: 2 });
|
|
26
|
+
expect(light.color).toBe(0x00ff00ff);
|
|
27
|
+
expect(light.intensity).toBe(2);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createVector3 } from '@flighthq/geometry';
|
|
2
|
+
import { AreaLightKind } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { cloneAreaLight, createAreaLight, setAreaLightOrientation } from './areaLight';
|
|
5
|
+
|
|
6
|
+
describe('cloneAreaLight', () => {
|
|
7
|
+
it('creates an independent copy with fresh position/direction/right/up vectors', () => {
|
|
8
|
+
const light = createAreaLight({
|
|
9
|
+
castsShadow: true,
|
|
10
|
+
color: 0x112233ff,
|
|
11
|
+
direction: createVector3(0, 0, -1),
|
|
12
|
+
intensity: 0.5,
|
|
13
|
+
normalBias: 0.1,
|
|
14
|
+
pcfRadius: 2,
|
|
15
|
+
position: createVector3(1, 2, 3),
|
|
16
|
+
range: 8,
|
|
17
|
+
right: createVector3(2, 0, 0),
|
|
18
|
+
shadowBias: 0.01,
|
|
19
|
+
up: createVector3(0, 3, 0),
|
|
20
|
+
});
|
|
21
|
+
const copy = cloneAreaLight(light);
|
|
22
|
+
expect(copy).not.toBe(light);
|
|
23
|
+
expect(copy.position).not.toBe(light.position);
|
|
24
|
+
expect(copy.direction).not.toBe(light.direction);
|
|
25
|
+
expect(copy.right).not.toBe(light.right);
|
|
26
|
+
expect(copy.up).not.toBe(light.up);
|
|
27
|
+
expect(copy.castsShadow).toBe(true);
|
|
28
|
+
expect(copy.color).toBe(0x112233ff);
|
|
29
|
+
expect(copy.direction.z).toBe(-1);
|
|
30
|
+
expect(copy.intensity).toBe(0.5);
|
|
31
|
+
expect(copy.normalBias).toBe(0.1);
|
|
32
|
+
expect(copy.pcfRadius).toBe(2);
|
|
33
|
+
expect(copy.position.y).toBe(2);
|
|
34
|
+
expect(copy.range).toBe(8);
|
|
35
|
+
expect(copy.right.x).toBe(2);
|
|
36
|
+
expect(copy.shadowBias).toBe(0.01);
|
|
37
|
+
expect(copy.up.y).toBe(3);
|
|
38
|
+
expect(copy.kind).toBe(AreaLightKind);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('createAreaLight', () => {
|
|
43
|
+
it('applies defaults: white, unit intensity, origin facing down, unit half-extents', () => {
|
|
44
|
+
const light = createAreaLight();
|
|
45
|
+
expect(light.castsShadow).toBe(false);
|
|
46
|
+
expect(light.color).toBe(0xffffffff);
|
|
47
|
+
expect(light.direction.y).toBe(-1);
|
|
48
|
+
expect(light.intensity).toBe(1);
|
|
49
|
+
expect(light.normalBias).toBe(0);
|
|
50
|
+
expect(light.pcfRadius).toBe(0);
|
|
51
|
+
expect(light.position.x).toBe(0);
|
|
52
|
+
expect(light.range).toBe(-1);
|
|
53
|
+
expect(light.right.x).toBe(1);
|
|
54
|
+
expect(light.shadowBias).toBe(0);
|
|
55
|
+
expect(light.up.z).toBe(1);
|
|
56
|
+
expect(light.kind).toBe(AreaLightKind);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('copies the supplied vectors rather than aliasing them', () => {
|
|
60
|
+
const right = createVector3(4, 0, 0);
|
|
61
|
+
const up = createVector3(0, 0, 5);
|
|
62
|
+
const light = createAreaLight({ right, up });
|
|
63
|
+
expect(light.right).not.toBe(right);
|
|
64
|
+
expect(light.up).not.toBe(up);
|
|
65
|
+
expect(light.right.x).toBe(4);
|
|
66
|
+
expect(light.up.z).toBe(5);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('setAreaLightOrientation', () => {
|
|
71
|
+
it('updates the direction, right, and up axes while preserving half-extent lengths', () => {
|
|
72
|
+
// right has length 3, up has length 4 — half-extents should survive the orientation update.
|
|
73
|
+
const light = createAreaLight({
|
|
74
|
+
direction: createVector3(0, -1, 0),
|
|
75
|
+
right: createVector3(3, 0, 0),
|
|
76
|
+
up: createVector3(0, 0, 4),
|
|
77
|
+
});
|
|
78
|
+
setAreaLightOrientation(light, createVector3(0, 0, -1), createVector3(1, 0, 0), createVector3(0, 1, 0));
|
|
79
|
+
// Direction should now point into -z.
|
|
80
|
+
expect(light.direction.z).toBeCloseTo(-1, 6);
|
|
81
|
+
// Right and up half-extent lengths should be preserved.
|
|
82
|
+
const rightLen = Math.sqrt(light.right.x ** 2 + light.right.y ** 2 + light.right.z ** 2);
|
|
83
|
+
const upLen = Math.sqrt(light.up.x ** 2 + light.up.y ** 2 + light.up.z ** 2);
|
|
84
|
+
expect(rightLen).toBeCloseTo(3, 5);
|
|
85
|
+
expect(upLen).toBeCloseTo(4, 5);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('ignores zero-length input vectors', () => {
|
|
89
|
+
const light = createAreaLight({
|
|
90
|
+
direction: createVector3(0, -1, 0),
|
|
91
|
+
right: createVector3(1, 0, 0),
|
|
92
|
+
up: createVector3(0, 0, 1),
|
|
93
|
+
});
|
|
94
|
+
setAreaLightOrientation(light, createVector3(0, 0, 0), createVector3(0, 0, 0), createVector3(0, 0, 0));
|
|
95
|
+
// All vectors unchanged when all inputs are zero.
|
|
96
|
+
expect(light.direction.y).toBeCloseTo(-1, 6);
|
|
97
|
+
expect(light.right.x).toBeCloseTo(1, 6);
|
|
98
|
+
expect(light.up.z).toBeCloseTo(1, 6);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createColorFromKelvin } from './colorFromKelvin';
|
|
2
|
+
|
|
3
|
+
describe('createColorFromKelvin', () => {
|
|
4
|
+
it('returns opaque alpha (0xff) for all temperatures', () => {
|
|
5
|
+
expect(createColorFromKelvin(3000) & 0xff).toBe(0xff);
|
|
6
|
+
expect(createColorFromKelvin(6500) & 0xff).toBe(0xff);
|
|
7
|
+
expect(createColorFromKelvin(10000) & 0xff).toBe(0xff);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns white (0xffffffff) for D65 daylight (~6500 K)', () => {
|
|
11
|
+
const color = createColorFromKelvin(6500);
|
|
12
|
+
const r = (color >>> 24) & 0xff;
|
|
13
|
+
const g = (color >>> 16) & 0xff;
|
|
14
|
+
const b = (color >>> 8) & 0xff;
|
|
15
|
+
// D65 should be close to neutral white — all channels near 255.
|
|
16
|
+
expect(r).toBeGreaterThan(240);
|
|
17
|
+
expect(g).toBeGreaterThan(240);
|
|
18
|
+
expect(b).toBeGreaterThan(230);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('produces warmer (more red, less blue) color for lower temperatures', () => {
|
|
22
|
+
const warm = createColorFromKelvin(2000);
|
|
23
|
+
const cool = createColorFromKelvin(10000);
|
|
24
|
+
const warmR = (warm >>> 24) & 0xff;
|
|
25
|
+
const warmB = (warm >>> 8) & 0xff;
|
|
26
|
+
const coolR = (cool >>> 24) & 0xff;
|
|
27
|
+
const coolB = (cool >>> 8) & 0xff;
|
|
28
|
+
// Warm candlelight should have more red and less blue than cool sky light.
|
|
29
|
+
expect(warmR).toBeGreaterThanOrEqual(coolR);
|
|
30
|
+
expect(warmB).toBeLessThanOrEqual(coolB);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('clamps inputs below 1000 K to 1000 K', () => {
|
|
34
|
+
expect(createColorFromKelvin(500)).toBe(createColorFromKelvin(1000));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('clamps inputs above 40000 K to 40000 K', () => {
|
|
38
|
+
expect(createColorFromKelvin(50000)).toBe(createColorFromKelvin(40000));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns a valid packed RGBA integer (unsigned 32-bit)', () => {
|
|
42
|
+
const color = createColorFromKelvin(5500);
|
|
43
|
+
expect(color).toBeGreaterThanOrEqual(0);
|
|
44
|
+
expect(color).toBeLessThanOrEqual(0xffffffff);
|
|
45
|
+
// Must be an integer.
|
|
46
|
+
expect(color % 1).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createVector3 } from '@flighthq/geometry';
|
|
2
|
+
import { DirectionalLightKind } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
cloneDirectionalLight,
|
|
6
|
+
createDirectionalLight,
|
|
7
|
+
setDirectionalLightDirection,
|
|
8
|
+
setDirectionalLightTarget,
|
|
9
|
+
} from './directionalLight';
|
|
10
|
+
|
|
11
|
+
describe('cloneDirectionalLight', () => {
|
|
12
|
+
it('creates an independent copy with a fresh direction vector', () => {
|
|
13
|
+
const light = createDirectionalLight({
|
|
14
|
+
castsShadow: true,
|
|
15
|
+
color: 0x112233ff,
|
|
16
|
+
direction: createVector3(1, 0, 0),
|
|
17
|
+
intensity: 0.5,
|
|
18
|
+
normalBias: 0.1,
|
|
19
|
+
pcfRadius: 2,
|
|
20
|
+
shadowBias: 0.01,
|
|
21
|
+
});
|
|
22
|
+
const copy = cloneDirectionalLight(light);
|
|
23
|
+
expect(copy).not.toBe(light);
|
|
24
|
+
expect(copy.direction).not.toBe(light.direction);
|
|
25
|
+
expect(copy.castsShadow).toBe(true);
|
|
26
|
+
expect(copy.color).toBe(0x112233ff);
|
|
27
|
+
expect(copy.direction.x).toBe(1);
|
|
28
|
+
expect(copy.intensity).toBe(0.5);
|
|
29
|
+
expect(copy.normalBias).toBe(0.1);
|
|
30
|
+
expect(copy.pcfRadius).toBe(2);
|
|
31
|
+
expect(copy.shadowBias).toBe(0.01);
|
|
32
|
+
expect(copy.kind).toBe(DirectionalLightKind);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('createDirectionalLight', () => {
|
|
37
|
+
it('applies defaults: white, unit intensity, downward, shadows off', () => {
|
|
38
|
+
const light = createDirectionalLight();
|
|
39
|
+
expect(light.castsShadow).toBe(false);
|
|
40
|
+
expect(light.color).toBe(0xffffffff);
|
|
41
|
+
expect(light.direction.x).toBe(0);
|
|
42
|
+
expect(light.direction.y).toBe(-1);
|
|
43
|
+
expect(light.direction.z).toBe(0);
|
|
44
|
+
expect(light.intensity).toBe(1);
|
|
45
|
+
expect(light.normalBias).toBe(0);
|
|
46
|
+
expect(light.pcfRadius).toBe(0);
|
|
47
|
+
expect(light.shadowBias).toBe(0);
|
|
48
|
+
expect(light.kind).toBe(DirectionalLightKind);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('copies the supplied direction rather than aliasing it', () => {
|
|
52
|
+
const direction = createVector3(0, 0, 1);
|
|
53
|
+
const light = createDirectionalLight({ direction });
|
|
54
|
+
expect(light.direction).not.toBe(direction);
|
|
55
|
+
expect(light.direction.z).toBe(1);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('setDirectionalLightDirection', () => {
|
|
60
|
+
it('writes a normalized direction into the light', () => {
|
|
61
|
+
const light = createDirectionalLight();
|
|
62
|
+
setDirectionalLightDirection(light, 0, 0, 3);
|
|
63
|
+
expect(light.direction.x).toBeCloseTo(0, 6);
|
|
64
|
+
expect(light.direction.y).toBeCloseTo(0, 6);
|
|
65
|
+
expect(light.direction.z).toBeCloseTo(1, 6);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('leaves direction unchanged for a zero-length input', () => {
|
|
69
|
+
const light = createDirectionalLight({ direction: createVector3(0, -1, 0) });
|
|
70
|
+
setDirectionalLightDirection(light, 0, 0, 0);
|
|
71
|
+
expect(light.direction.y).toBeCloseTo(-1, 6);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('setDirectionalLightTarget', () => {
|
|
76
|
+
it('sets direction toward the target from the from-point', () => {
|
|
77
|
+
const light = createDirectionalLight();
|
|
78
|
+
setDirectionalLightTarget(light, 0, 0, 0, 0, 0, 5);
|
|
79
|
+
expect(light.direction.x).toBeCloseTo(0, 6);
|
|
80
|
+
expect(light.direction.y).toBeCloseTo(0, 6);
|
|
81
|
+
expect(light.direction.z).toBeCloseTo(1, 6);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('leaves direction unchanged when from equals to', () => {
|
|
85
|
+
const light = createDirectionalLight({ direction: createVector3(0, -1, 0) });
|
|
86
|
+
setDirectionalLightTarget(light, 1, 2, 3, 1, 2, 3);
|
|
87
|
+
expect(light.direction.y).toBeCloseTo(-1, 6);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity';
|
|
2
|
+
import type { CubeTexture, Sampler } from '@flighthq/types';
|
|
3
|
+
import { EnvironmentKind } from '@flighthq/types';
|
|
4
|
+
|
|
5
|
+
import { cloneEnvironment, createEnvironment } from './environment';
|
|
6
|
+
|
|
7
|
+
function createTestCubeTexture(): CubeTexture {
|
|
8
|
+
const sampler: Sampler = createEntity({
|
|
9
|
+
anisotropy: 1,
|
|
10
|
+
magFilter: 'linear',
|
|
11
|
+
minFilter: 'linear',
|
|
12
|
+
mipmaps: false,
|
|
13
|
+
wrapU: 'clamp-to-edge',
|
|
14
|
+
wrapV: 'clamp-to-edge',
|
|
15
|
+
});
|
|
16
|
+
return createEntity({
|
|
17
|
+
colorSpace: 'linear',
|
|
18
|
+
faces: [null, null, null, null, null, null],
|
|
19
|
+
sampler,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('cloneEnvironment', () => {
|
|
24
|
+
it('creates an independent copy that shares the cubemap reference', () => {
|
|
25
|
+
const cube = createTestCubeTexture();
|
|
26
|
+
const environment = createEnvironment({ environment: cube, intensity: 0.5 });
|
|
27
|
+
const copy = cloneEnvironment(environment);
|
|
28
|
+
expect(copy).not.toBe(environment);
|
|
29
|
+
expect(copy.environment).toBe(cube);
|
|
30
|
+
expect(copy.intensity).toBe(0.5);
|
|
31
|
+
expect(copy.kind).toBe(EnvironmentKind);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('createEnvironment', () => {
|
|
36
|
+
it('applies defaults: no cubemap at unit intensity', () => {
|
|
37
|
+
const environment = createEnvironment();
|
|
38
|
+
expect(environment.environment).toBeNull();
|
|
39
|
+
expect(environment.intensity).toBe(1);
|
|
40
|
+
expect(environment.kind).toBe(EnvironmentKind);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('stores the supplied cubemap and intensity', () => {
|
|
44
|
+
const cube = createTestCubeTexture();
|
|
45
|
+
const environment = createEnvironment({ environment: cube, intensity: 2 });
|
|
46
|
+
expect(environment.environment).toBe(cube);
|
|
47
|
+
expect(environment.intensity).toBe(2);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { HemisphereLightKind } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { cloneHemisphereLight, createHemisphereLight } from './hemisphereLight';
|
|
4
|
+
|
|
5
|
+
describe('cloneHemisphereLight', () => {
|
|
6
|
+
it('creates an independent copy with the same fields', () => {
|
|
7
|
+
const light = createHemisphereLight({ groundColor: 0x223344ff, intensity: 0.5, skyColor: 0x8899aaff });
|
|
8
|
+
const copy = cloneHemisphereLight(light);
|
|
9
|
+
expect(copy).not.toBe(light);
|
|
10
|
+
expect(copy.groundColor).toBe(0x223344ff);
|
|
11
|
+
expect(copy.intensity).toBe(0.5);
|
|
12
|
+
expect(copy.skyColor).toBe(0x8899aaff);
|
|
13
|
+
expect(copy.kind).toBe(HemisphereLightKind);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('createHemisphereLight', () => {
|
|
18
|
+
it('applies opaque-white defaults at unit intensity for both colors', () => {
|
|
19
|
+
const light = createHemisphereLight();
|
|
20
|
+
expect(light.groundColor).toBe(0xffffffff);
|
|
21
|
+
expect(light.intensity).toBe(1);
|
|
22
|
+
expect(light.skyColor).toBe(0xffffffff);
|
|
23
|
+
expect(light.kind).toBe(HemisphereLightKind);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('overrides sky and ground colors from options', () => {
|
|
27
|
+
const light = createHemisphereLight({ groundColor: 0x000000ff, skyColor: 0x0000ffff });
|
|
28
|
+
expect(light.groundColor).toBe(0x000000ff);
|
|
29
|
+
expect(light.skyColor).toBe(0x0000ffff);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { createBoundingSphere } from '@flighthq/geometry';
|
|
2
|
+
|
|
3
|
+
import { createAmbientLight } from './ambientLight';
|
|
4
|
+
import { createAreaLight } from './areaLight';
|
|
5
|
+
import { createDirectionalLight } from './directionalLight';
|
|
6
|
+
import { createEnvironment } from './environment';
|
|
7
|
+
import { createHemisphereLight } from './hemisphereLight';
|
|
8
|
+
import {
|
|
9
|
+
getLightInfluenceBounds,
|
|
10
|
+
getLightLuminance,
|
|
11
|
+
hasLightInfluenceOnBounds,
|
|
12
|
+
isLightShadowCasting,
|
|
13
|
+
} from './lightAnalysis';
|
|
14
|
+
import { createPointLight } from './pointLight';
|
|
15
|
+
import { createSpotLight } from './spotLight';
|
|
16
|
+
|
|
17
|
+
describe('getLightInfluenceBounds', () => {
|
|
18
|
+
it('returns sentinel radius (-1) for ambient lights', () => {
|
|
19
|
+
const light = createAmbientLight();
|
|
20
|
+
const out = createBoundingSphere();
|
|
21
|
+
getLightInfluenceBounds(out, light);
|
|
22
|
+
expect(out.radius).toBe(-1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns sentinel radius for hemisphere lights', () => {
|
|
26
|
+
const light = createHemisphereLight();
|
|
27
|
+
const out = createBoundingSphere();
|
|
28
|
+
getLightInfluenceBounds(out, light);
|
|
29
|
+
expect(out.radius).toBe(-1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns sentinel radius for directional lights', () => {
|
|
33
|
+
const light = createDirectionalLight();
|
|
34
|
+
const out = createBoundingSphere();
|
|
35
|
+
getLightInfluenceBounds(out, light);
|
|
36
|
+
expect(out.radius).toBe(-1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns sentinel radius for environment lights', () => {
|
|
40
|
+
const light = createEnvironment();
|
|
41
|
+
const out = createBoundingSphere();
|
|
42
|
+
getLightInfluenceBounds(out, light);
|
|
43
|
+
expect(out.radius).toBe(-1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns sentinel radius for infinite-range point light', () => {
|
|
47
|
+
const light = createPointLight({ range: -1 });
|
|
48
|
+
const out = createBoundingSphere();
|
|
49
|
+
getLightInfluenceBounds(out, light);
|
|
50
|
+
expect(out.radius).toBe(-1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns position + range sphere for finite-range point light', () => {
|
|
54
|
+
const light = createPointLight({ position: { x: 1, y: 2, z: 3 }, range: 5 });
|
|
55
|
+
const out = createBoundingSphere();
|
|
56
|
+
getLightInfluenceBounds(out, light);
|
|
57
|
+
expect(out.center.x).toBe(1);
|
|
58
|
+
expect(out.center.y).toBe(2);
|
|
59
|
+
expect(out.center.z).toBe(3);
|
|
60
|
+
expect(out.radius).toBe(5);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns position + range sphere for finite-range spot light', () => {
|
|
64
|
+
const light = createSpotLight({ position: { x: 0, y: 10, z: 0 }, range: 8 });
|
|
65
|
+
const out = createBoundingSphere();
|
|
66
|
+
getLightInfluenceBounds(out, light);
|
|
67
|
+
expect(out.center.y).toBe(10);
|
|
68
|
+
expect(out.radius).toBe(8);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns position + range sphere for finite-range area light', () => {
|
|
72
|
+
const light = createAreaLight({ position: { x: 5, y: 0, z: 0 }, range: 3 });
|
|
73
|
+
const out = createBoundingSphere();
|
|
74
|
+
getLightInfluenceBounds(out, light);
|
|
75
|
+
expect(out.center.x).toBe(5);
|
|
76
|
+
expect(out.radius).toBe(3);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('getLightLuminance', () => {
|
|
81
|
+
it('returns 0 for lights without a color field', () => {
|
|
82
|
+
const light = createEnvironment({ intensity: 5 });
|
|
83
|
+
// Environment has no color field — luminance should fall back to 0.
|
|
84
|
+
// (environment light has no color field in its type; cast to bypass type)
|
|
85
|
+
expect(getLightLuminance(light)).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns zero for black (0x000000ff)', () => {
|
|
89
|
+
const light = createAmbientLight({ color: 0x000000ff, intensity: 1 });
|
|
90
|
+
expect(getLightLuminance(light)).toBeCloseTo(0, 6);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns nonzero luminance for white at unit intensity', () => {
|
|
94
|
+
const light = createAmbientLight({ color: 0xffffffff, intensity: 1 });
|
|
95
|
+
expect(getLightLuminance(light)).toBeCloseTo(1, 4);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('scales luminance by intensity', () => {
|
|
99
|
+
const a = createAmbientLight({ color: 0xffffffff, intensity: 2 });
|
|
100
|
+
const b = createAmbientLight({ color: 0xffffffff, intensity: 4 });
|
|
101
|
+
expect(getLightLuminance(b) / getLightLuminance(a)).toBeCloseTo(2, 4);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('uses BT.709 green weighting (green > red > blue)', () => {
|
|
105
|
+
const red = createAmbientLight({ color: 0xff0000ff, intensity: 1 });
|
|
106
|
+
const green = createAmbientLight({ color: 0x00ff00ff, intensity: 1 });
|
|
107
|
+
const blue = createAmbientLight({ color: 0x0000ffff, intensity: 1 });
|
|
108
|
+
expect(getLightLuminance(green)).toBeGreaterThan(getLightLuminance(red));
|
|
109
|
+
expect(getLightLuminance(red)).toBeGreaterThan(getLightLuminance(blue));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('hasLightInfluenceOnBounds', () => {
|
|
114
|
+
it('returns true for ambient lights (unlimited reach)', () => {
|
|
115
|
+
const light = createAmbientLight();
|
|
116
|
+
const bounds = createBoundingSphere(100, 0, 0, 1);
|
|
117
|
+
expect(hasLightInfluenceOnBounds(light, bounds)).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns true for directional lights (unlimited reach)', () => {
|
|
121
|
+
const light = createDirectionalLight();
|
|
122
|
+
const bounds = createBoundingSphere(1000, 0, 0, 1);
|
|
123
|
+
expect(hasLightInfluenceOnBounds(light, bounds)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns true for overlapping point light and bounds', () => {
|
|
127
|
+
const light = createPointLight({ position: { x: 0, y: 0, z: 0 }, range: 10 });
|
|
128
|
+
const bounds = createBoundingSphere(5, 0, 0, 2);
|
|
129
|
+
expect(hasLightInfluenceOnBounds(light, bounds)).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('returns false for non-overlapping point light and bounds', () => {
|
|
133
|
+
const light = createPointLight({ position: { x: 0, y: 0, z: 0 }, range: 5 });
|
|
134
|
+
const bounds = createBoundingSphere(20, 0, 0, 1);
|
|
135
|
+
expect(hasLightInfluenceOnBounds(light, bounds)).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns false for empty bounds (radius < 0)', () => {
|
|
139
|
+
const light = createPointLight({ position: { x: 0, y: 0, z: 0 }, range: 10 });
|
|
140
|
+
const bounds = createBoundingSphere(0, 0, 0, -1);
|
|
141
|
+
expect(hasLightInfluenceOnBounds(light, bounds)).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns true for infinite-range point light regardless of distance', () => {
|
|
145
|
+
const light = createPointLight({ position: { x: 0, y: 0, z: 0 }, range: -1 });
|
|
146
|
+
const bounds = createBoundingSphere(999, 0, 0, 1);
|
|
147
|
+
expect(hasLightInfluenceOnBounds(light, bounds)).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('isLightShadowCasting', () => {
|
|
152
|
+
it('returns false for ambient lights', () => {
|
|
153
|
+
expect(isLightShadowCasting(createAmbientLight())).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('returns false for hemisphere lights', () => {
|
|
157
|
+
expect(isLightShadowCasting(createHemisphereLight())).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('returns false for environment lights', () => {
|
|
161
|
+
expect(isLightShadowCasting(createEnvironment())).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns false for directional lights with castsShadow: false', () => {
|
|
165
|
+
const light = createDirectionalLight({ castsShadow: false });
|
|
166
|
+
expect(isLightShadowCasting(light)).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('returns true for directional lights with castsShadow: true', () => {
|
|
170
|
+
const light = createDirectionalLight({ castsShadow: true });
|
|
171
|
+
expect(isLightShadowCasting(light)).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns true for point lights with castsShadow: true', () => {
|
|
175
|
+
const light = createPointLight({ castsShadow: true });
|
|
176
|
+
expect(isLightShadowCasting(light)).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('returns true for spot lights with castsShadow: true', () => {
|
|
180
|
+
const light = createSpotLight({ castsShadow: true });
|
|
181
|
+
expect(isLightShadowCasting(light)).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('returns true for area lights with castsShadow: true', () => {
|
|
185
|
+
const light = createAreaLight({ castsShadow: true });
|
|
186
|
+
expect(isLightShadowCasting(light)).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
});
|