@flighthq/camera 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/basis.d.ts +6 -0
- package/dist/basis.d.ts.map +1 -0
- package/dist/basis.js +63 -0
- package/dist/basis.js.map +1 -0
- package/dist/camera.d.ts +14 -0
- package/dist/camera.d.ts.map +1 -0
- package/dist/camera.js +79 -0
- package/dist/camera.js.map +1 -0
- package/dist/culling.d.ts +6 -0
- package/dist/culling.d.ts.map +1 -0
- package/dist/culling.js +39 -0
- package/dist/culling.js.map +1 -0
- package/dist/depth.d.ts +4 -0
- package/dist/depth.d.ts.map +1 -0
- package/dist/depth.js +39 -0
- package/dist/depth.js.map +1 -0
- package/dist/frustumCorners.d.ts +3 -0
- package/dist/frustumCorners.d.ts.map +1 -0
- package/dist/frustumCorners.js +64 -0
- package/dist/frustumCorners.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/intersection.d.ts +4 -0
- package/dist/intersection.d.ts.map +1 -0
- package/dist/intersection.js +69 -0
- package/dist/intersection.js.map +1 -0
- package/dist/picking.d.ts +4 -0
- package/dist/picking.d.ts.map +1 -0
- package/dist/picking.js +90 -0
- package/dist/picking.js.map +1 -0
- package/dist/projection.d.ts +16 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/projection.js +48 -0
- package/dist/projection.js.map +1 -0
- package/dist/shadowCamera.d.ts +3 -0
- package/dist/shadowCamera.d.ts.map +1 -0
- package/dist/shadowCamera.js +43 -0
- package/dist/shadowCamera.js.map +1 -0
- package/package.json +39 -0
- package/src/basis.test.ts +83 -0
- package/src/camera.test.ts +189 -0
- package/src/culling.test.ts +90 -0
- package/src/depth.test.ts +65 -0
- package/src/frustumCorners.test.ts +75 -0
- package/src/intersection.test.ts +118 -0
- package/src/picking.test.ts +94 -0
- package/src/projection.test.ts +97 -0
- package/src/shadowCamera.test.ts +45 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createVector3 } from '@flighthq/geometry';
|
|
2
|
+
import type { Vector3Like } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { createCamera, setCameraViewMatrix4FromLookAt } from './camera';
|
|
5
|
+
import { getCameraFrustumCorners } from './frustumCorners';
|
|
6
|
+
import { createPerspectiveProjection } from './projection';
|
|
7
|
+
|
|
8
|
+
type FrustumCorners = [
|
|
9
|
+
Vector3Like,
|
|
10
|
+
Vector3Like,
|
|
11
|
+
Vector3Like,
|
|
12
|
+
Vector3Like,
|
|
13
|
+
Vector3Like,
|
|
14
|
+
Vector3Like,
|
|
15
|
+
Vector3Like,
|
|
16
|
+
Vector3Like,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function makeCamera() {
|
|
20
|
+
const camera = createCamera({
|
|
21
|
+
far: 100,
|
|
22
|
+
near: 1,
|
|
23
|
+
projection: createPerspectiveProjection({ aspect: 1, fovY: Math.PI / 2 }),
|
|
24
|
+
});
|
|
25
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 10), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
26
|
+
return camera;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeCorners(): FrustumCorners {
|
|
30
|
+
return [
|
|
31
|
+
createVector3(),
|
|
32
|
+
createVector3(),
|
|
33
|
+
createVector3(),
|
|
34
|
+
createVector3(),
|
|
35
|
+
createVector3(),
|
|
36
|
+
createVector3(),
|
|
37
|
+
createVector3(),
|
|
38
|
+
createVector3(),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('getCameraFrustumCorners', () => {
|
|
43
|
+
it('writes 8 world-space corners and returns true', () => {
|
|
44
|
+
const camera = makeCamera();
|
|
45
|
+
const corners = makeCorners();
|
|
46
|
+
const result = getCameraFrustumCorners(corners, camera, 1);
|
|
47
|
+
expect(result).toBe(true);
|
|
48
|
+
// Near corners (index 0-3) should be closer to camera than far corners (4-7).
|
|
49
|
+
// Camera is at z=10 looking toward z=0, so smaller z values are farther away.
|
|
50
|
+
for (let i = 0; i < 4; i++) {
|
|
51
|
+
expect(corners[i].z).toBeGreaterThan(corners[i + 4].z);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns false when the view-projection is non-invertible', () => {
|
|
56
|
+
const camera = makeCamera();
|
|
57
|
+
camera.near = 1;
|
|
58
|
+
camera.far = 1; // Degenerate: near == far.
|
|
59
|
+
const corners = makeCorners();
|
|
60
|
+
const result = getCameraFrustumCorners(corners, camera, 1);
|
|
61
|
+
expect(result).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('near corners and far corners have opposite symmetry in xy', () => {
|
|
65
|
+
const camera = makeCamera();
|
|
66
|
+
const corners = makeCorners();
|
|
67
|
+
getCameraFrustumCorners(corners, camera, 1);
|
|
68
|
+
// Near corners: 0 (-1,-1,-1), 1 (1,-1,-1), 2 (-1,1,-1), 3 (1,1,-1)
|
|
69
|
+
// With aspect=1 and fovY=PI/2, the near plane half-extent = near * tan(PI/4) = near.
|
|
70
|
+
// Near = 1 → half-extent = 1. Near corner 0 should be (-1,-1, near_z) and corner 3 (1,1, near_z).
|
|
71
|
+
expect(corners[0].x).toBeCloseTo(-corners[1].x, 3);
|
|
72
|
+
expect(corners[0].y).toBeCloseTo(-corners[2].y, 3);
|
|
73
|
+
expect(corners[3].x).toBeCloseTo(-corners[2].x, 3);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createBoundingSphere, createPlane, createRay3D, createVector3 } from '@flighthq/geometry';
|
|
2
|
+
|
|
3
|
+
import { createCamera, setCameraViewMatrix4FromLookAt } from './camera';
|
|
4
|
+
import { getCameraRayThroughBoundingSphere, intersectCameraRayWithPlane } from './intersection';
|
|
5
|
+
import { createPerspectiveProjection } from './projection';
|
|
6
|
+
|
|
7
|
+
function makeCamera() {
|
|
8
|
+
const camera = createCamera({
|
|
9
|
+
far: 100,
|
|
10
|
+
near: 0.1,
|
|
11
|
+
projection: createPerspectiveProjection({ aspect: 1, fovY: Math.PI / 2 }),
|
|
12
|
+
});
|
|
13
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 10), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
14
|
+
return camera;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('getCameraRayThroughBoundingSphere', () => {
|
|
18
|
+
it('returns true for a sphere in front of the camera', () => {
|
|
19
|
+
const camera = makeCamera();
|
|
20
|
+
const sphere = createBoundingSphere(0, 0, 5, 1);
|
|
21
|
+
const ray = createRay3D();
|
|
22
|
+
const result = getCameraRayThroughBoundingSphere(ray, camera, sphere, 1);
|
|
23
|
+
expect(result).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns a ray pointing from the camera toward the sphere center', () => {
|
|
27
|
+
const camera = makeCamera();
|
|
28
|
+
// Sphere centered at origin, camera at (0,0,10) looking toward (0,0,0).
|
|
29
|
+
const sphere = createBoundingSphere(0, 0, 0, 1);
|
|
30
|
+
const ray = createRay3D();
|
|
31
|
+
getCameraRayThroughBoundingSphere(ray, camera, sphere, 1);
|
|
32
|
+
// The direction should point toward -Z (from eye at 0,0,10 toward sphere center 0,0,0).
|
|
33
|
+
expect(ray.direction.x).toBeCloseTo(0, 3);
|
|
34
|
+
expect(ray.direction.y).toBeCloseTo(0, 3);
|
|
35
|
+
expect(ray.direction.z).toBeCloseTo(-1, 3);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns false for an empty sphere (negative radius)', () => {
|
|
39
|
+
const camera = makeCamera();
|
|
40
|
+
const sphere = createBoundingSphere(0, 0, 5, -1);
|
|
41
|
+
const ray = createRay3D();
|
|
42
|
+
expect(getCameraRayThroughBoundingSphere(ray, camera, sphere, 1)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns false when the sphere center is behind the camera', () => {
|
|
46
|
+
const camera = makeCamera();
|
|
47
|
+
// Camera is at z=10 looking toward -Z. A sphere at z=15 is behind the camera.
|
|
48
|
+
const sphere = createBoundingSphere(0, 0, 15, 1);
|
|
49
|
+
const ray = createRay3D();
|
|
50
|
+
expect(getCameraRayThroughBoundingSphere(ray, camera, sphere, 1)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('is alias-safe when out shares fields with sphere center', () => {
|
|
54
|
+
const camera = makeCamera();
|
|
55
|
+
const sphere = createBoundingSphere(0, 0, 5, 1);
|
|
56
|
+
// Create a ray whose origin is the same object as sphere.center (aliasing).
|
|
57
|
+
const ray = createRay3D(sphere.center.x, sphere.center.y, sphere.center.z);
|
|
58
|
+
const result = getCameraRayThroughBoundingSphere(ray, camera, sphere, 1);
|
|
59
|
+
// Should succeed regardless of aliasing.
|
|
60
|
+
expect(result).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('intersectCameraRayWithPlane', () => {
|
|
65
|
+
it('returns the hit point for a ray pointing straight down into a horizontal plane', () => {
|
|
66
|
+
// Ray pointing straight down (-Y) from (0, 5, 0).
|
|
67
|
+
const ray = createRay3D(0, 5, 0, 0, -1, 0);
|
|
68
|
+
// Horizontal ground plane: y = 0 → 0x + 1y + 0z + 0 = 0.
|
|
69
|
+
const plane = createPlane(0, 1, 0, 0);
|
|
70
|
+
const hit = createVector3();
|
|
71
|
+
const result = intersectCameraRayWithPlane(hit, ray, plane);
|
|
72
|
+
expect(result).toBe(true);
|
|
73
|
+
expect(hit.x).toBeCloseTo(0, 5);
|
|
74
|
+
expect(hit.y).toBeCloseTo(0, 5);
|
|
75
|
+
expect(hit.z).toBeCloseTo(0, 5);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns false when the ray is parallel to the plane', () => {
|
|
79
|
+
// Ray pointing horizontally (along X) and a vertical plane (normal = Y).
|
|
80
|
+
const ray = createRay3D(0, 1, 0, 1, 0, 0);
|
|
81
|
+
const plane = createPlane(0, 1, 0, -1); // y = 1
|
|
82
|
+
const hit = createVector3();
|
|
83
|
+
expect(intersectCameraRayWithPlane(hit, ray, plane)).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns false when the intersection is behind the ray origin (t < 0)', () => {
|
|
87
|
+
// Ray pointing UP (+Y) from below a plane at y=0: the intersection t would be negative.
|
|
88
|
+
const ray = createRay3D(0, -5, 0, 0, 1, 0);
|
|
89
|
+
// Plane at y = -10: 0x + 1y + 0z + 10 = 0.
|
|
90
|
+
const plane = createPlane(0, 1, 0, 10);
|
|
91
|
+
const hit = createVector3();
|
|
92
|
+
// t = -(0*0 + 1*(-5) + 0*0 + 10) / (0*0 + 1*1 + 0*0) = -(5) / 1 = -5 < 0 → false.
|
|
93
|
+
expect(intersectCameraRayWithPlane(hit, ray, plane)).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('computes the correct hit point for a diagonal ray against a diagonal plane', () => {
|
|
97
|
+
// Ray from (0,0,0) pointing diagonally (+1, +1, 0).
|
|
98
|
+
const ray = createRay3D(0, 0, 0, 1, 1, 0);
|
|
99
|
+
// Plane at x + y = 4: 1x + 1y + 0z - 4 = 0.
|
|
100
|
+
const plane = createPlane(1, 1, 0, -4);
|
|
101
|
+
const hit = createVector3();
|
|
102
|
+
const result = intersectCameraRayWithPlane(hit, ray, plane);
|
|
103
|
+
expect(result).toBe(true);
|
|
104
|
+
// t = -(0 + 0 - 4) / (1 + 1) = 4/2 = 2 → hit = (2, 2, 0).
|
|
105
|
+
expect(hit.x).toBeCloseTo(2, 5);
|
|
106
|
+
expect(hit.y).toBeCloseTo(2, 5);
|
|
107
|
+
expect(hit.z).toBeCloseTo(0, 5);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('is alias-safe when out is the same object as the ray origin', () => {
|
|
111
|
+
const ray = createRay3D(0, 5, 0, 0, -1, 0);
|
|
112
|
+
const plane = createPlane(0, 1, 0, 0);
|
|
113
|
+
// Alias out = ray.origin.
|
|
114
|
+
const result = intersectCameraRayWithPlane(ray.origin, ray, plane);
|
|
115
|
+
expect(result).toBe(true);
|
|
116
|
+
expect(ray.origin.y).toBeCloseTo(0, 5);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createRay3D, createVector3 } from '@flighthq/geometry';
|
|
2
|
+
|
|
3
|
+
import { createCamera, setCameraViewMatrix4FromLookAt } from './camera';
|
|
4
|
+
import { getCameraScreenToWorldRay, getCameraWorldToScreen } from './picking';
|
|
5
|
+
import { createPerspectiveProjection } from './projection';
|
|
6
|
+
|
|
7
|
+
function makeCamera() {
|
|
8
|
+
return createCamera({
|
|
9
|
+
far: 100,
|
|
10
|
+
near: 0.1,
|
|
11
|
+
projection: createPerspectiveProjection({ aspect: 1, fovY: Math.PI / 2 }),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('getCameraScreenToWorldRay', () => {
|
|
16
|
+
it('returns a ray pointing from near toward far for center NDC (0,0)', () => {
|
|
17
|
+
const camera = makeCamera();
|
|
18
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
19
|
+
const ray = createRay3D();
|
|
20
|
+
const result = getCameraScreenToWorldRay(ray, camera, 0, 0, 1);
|
|
21
|
+
expect(result).toBe(true);
|
|
22
|
+
// Center ray should point along -Z (camera looks toward -Z).
|
|
23
|
+
expect(ray.direction.x).toBeCloseTo(0, 3);
|
|
24
|
+
expect(ray.direction.y).toBeCloseTo(0, 3);
|
|
25
|
+
expect(ray.direction.z).toBeCloseTo(-1, 3);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns false when the view-projection is non-invertible', () => {
|
|
29
|
+
const camera = makeCamera();
|
|
30
|
+
// near == far collapses the depth range, making the matrix singular.
|
|
31
|
+
camera.near = 1;
|
|
32
|
+
camera.far = 1;
|
|
33
|
+
const ray = createRay3D();
|
|
34
|
+
const result = getCameraScreenToWorldRay(ray, camera, 0, 0, 1);
|
|
35
|
+
expect(result).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('round-trips with getCameraWorldToScreen at the center point', () => {
|
|
39
|
+
const camera = makeCamera();
|
|
40
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
41
|
+
// A world point directly ahead of the camera.
|
|
42
|
+
const worldPoint = createVector3(0, 0, 2);
|
|
43
|
+
const ndc = createVector3();
|
|
44
|
+
getCameraWorldToScreen(ndc, camera, worldPoint, 1);
|
|
45
|
+
// Unproject those NDC coordinates back.
|
|
46
|
+
const ray = createRay3D();
|
|
47
|
+
getCameraScreenToWorldRay(ray, camera, ndc.x, ndc.y, 1);
|
|
48
|
+
// The direction of the ray should point from the origin toward (0,0,2).
|
|
49
|
+
// Since the camera is at (0,0,5) looking at origin, forward is -Z.
|
|
50
|
+
expect(ray.direction.x).toBeCloseTo(0, 2);
|
|
51
|
+
expect(ray.direction.y).toBeCloseTo(0, 2);
|
|
52
|
+
expect(ray.direction.z).toBeCloseTo(-1, 2);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('getCameraWorldToScreen', () => {
|
|
57
|
+
it('maps a point in front of the camera to NDC with |x|, |y| <= 1', () => {
|
|
58
|
+
const camera = makeCamera();
|
|
59
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
60
|
+
const ndc = createVector3();
|
|
61
|
+
const result = getCameraWorldToScreen(ndc, camera, createVector3(0, 0, 2), 1);
|
|
62
|
+
expect(result).toBe(true);
|
|
63
|
+
expect(Math.abs(ndc.x)).toBeLessThanOrEqual(1);
|
|
64
|
+
expect(Math.abs(ndc.y)).toBeLessThanOrEqual(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('maps the origin to (0,0) NDC for a camera looking along -Z', () => {
|
|
68
|
+
const camera = makeCamera();
|
|
69
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
70
|
+
const ndc = createVector3();
|
|
71
|
+
getCameraWorldToScreen(ndc, camera, createVector3(0, 0, 0), 1);
|
|
72
|
+
expect(ndc.x).toBeCloseTo(0, 3);
|
|
73
|
+
expect(ndc.y).toBeCloseTo(0, 3);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns false for a point behind the camera', () => {
|
|
77
|
+
const camera = makeCamera();
|
|
78
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
79
|
+
const ndc = createVector3();
|
|
80
|
+
// A point behind the eye (z=10, camera eye is at z=5, looking toward z=0).
|
|
81
|
+
const result = getCameraWorldToScreen(ndc, camera, createVector3(0, 0, 10), 1);
|
|
82
|
+
expect(result).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('is safe when out aliases worldPoint', () => {
|
|
86
|
+
const camera = makeCamera();
|
|
87
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
88
|
+
const point = createVector3(0, 0, 2);
|
|
89
|
+
const result = getCameraWorldToScreen(point, camera, point, 1);
|
|
90
|
+
// Should succeed and write valid NDC into point (which was also worldPoint).
|
|
91
|
+
expect(result).toBe(true);
|
|
92
|
+
expect(Math.abs(point.x)).toBeLessThanOrEqual(1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createMatrix4, createPerspectiveMatrix4, setOrthographicMatrix4 } from '@flighthq/geometry';
|
|
2
|
+
import type { OrthographicProjection, PerspectiveProjection } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createOrthographicProjection,
|
|
6
|
+
createPerspectiveProjection,
|
|
7
|
+
isOrthographicProjection,
|
|
8
|
+
isPerspectiveProjection,
|
|
9
|
+
setProjectionMatrix4,
|
|
10
|
+
} from './projection';
|
|
11
|
+
|
|
12
|
+
describe('createOrthographicProjection', () => {
|
|
13
|
+
it('builds an orthographic descriptor from half-extents', () => {
|
|
14
|
+
const projection = createOrthographicProjection({ halfHeight: 3, halfWidth: 4 });
|
|
15
|
+
expect(projection.kind).toBe('orthographic');
|
|
16
|
+
expect(projection.halfWidth).toBe(4);
|
|
17
|
+
expect(projection.halfHeight).toBe(3);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('createPerspectiveProjection', () => {
|
|
22
|
+
it('builds a perspective descriptor from fovY and aspect', () => {
|
|
23
|
+
const projection = createPerspectiveProjection({ aspect: 1.5, fovY: Math.PI / 3 });
|
|
24
|
+
expect(projection.kind).toBe('perspective');
|
|
25
|
+
expect(projection.fovY).toBeCloseTo(Math.PI / 3);
|
|
26
|
+
expect(projection.aspect).toBe(1.5);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('defaults aspect to 1 when omitted', () => {
|
|
30
|
+
const projection = createPerspectiveProjection({ fovY: 1 });
|
|
31
|
+
expect(projection.aspect).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('isOrthographicProjection', () => {
|
|
36
|
+
it('is true for orthographic and false for perspective', () => {
|
|
37
|
+
const ortho = createOrthographicProjection({ halfHeight: 1, halfWidth: 1 });
|
|
38
|
+
const persp = createPerspectiveProjection({ aspect: 1, fovY: 1 });
|
|
39
|
+
expect(isOrthographicProjection(ortho)).toBe(true);
|
|
40
|
+
expect(isOrthographicProjection(persp)).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('isPerspectiveProjection', () => {
|
|
45
|
+
it('is true for perspective and false for orthographic', () => {
|
|
46
|
+
const persp = createPerspectiveProjection({ aspect: 1, fovY: 1 });
|
|
47
|
+
const ortho = createOrthographicProjection({ halfHeight: 1, halfWidth: 1 });
|
|
48
|
+
expect(isPerspectiveProjection(persp)).toBe(true);
|
|
49
|
+
expect(isPerspectiveProjection(ortho)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('setProjectionMatrix4', () => {
|
|
54
|
+
it('matches geometry perspective builder using tan(fovY/2)', () => {
|
|
55
|
+
const fovY = Math.PI / 2;
|
|
56
|
+
const aspect = 1.6;
|
|
57
|
+
const near = 0.1;
|
|
58
|
+
const far = 100;
|
|
59
|
+
const projection: PerspectiveProjection = createPerspectiveProjection({ aspect, fovY });
|
|
60
|
+
|
|
61
|
+
const out = createMatrix4();
|
|
62
|
+
setProjectionMatrix4(out, projection, aspect, near, far);
|
|
63
|
+
|
|
64
|
+
const expected = createPerspectiveMatrix4(Math.tan(fovY * 0.5), aspect, near, far);
|
|
65
|
+
for (let i = 0; i < 16; i++) {
|
|
66
|
+
expect(out.m[i]).toBeCloseTo(expected.m[i]);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('uses the passed aspect over the projection stored aspect for perspective', () => {
|
|
71
|
+
const fovY = 1;
|
|
72
|
+
const projection = createPerspectiveProjection({ aspect: 1, fovY });
|
|
73
|
+
|
|
74
|
+
const out = createMatrix4();
|
|
75
|
+
setProjectionMatrix4(out, projection, 2, 0.5, 50);
|
|
76
|
+
|
|
77
|
+
const expected = createPerspectiveMatrix4(Math.tan(fovY * 0.5), 2, 0.5, 50);
|
|
78
|
+
for (let i = 0; i < 16; i++) {
|
|
79
|
+
expect(out.m[i]).toBeCloseTo(expected.m[i]);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('matches geometry orthographic builder using symmetric half-extents', () => {
|
|
84
|
+
const projection: OrthographicProjection = createOrthographicProjection({ halfHeight: 3, halfWidth: 5 });
|
|
85
|
+
const near = 1;
|
|
86
|
+
const far = 20;
|
|
87
|
+
|
|
88
|
+
const out = createMatrix4();
|
|
89
|
+
setProjectionMatrix4(out, projection, 99, near, far);
|
|
90
|
+
|
|
91
|
+
const expected = createMatrix4();
|
|
92
|
+
setOrthographicMatrix4(expected, -5, 5, -3, 3, near, far);
|
|
93
|
+
for (let i = 0; i < 16; i++) {
|
|
94
|
+
expect(out.m[i]).toBeCloseTo(expected.m[i]);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createAabb } from '@flighthq/geometry';
|
|
2
|
+
import type { OrthographicProjection } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { createCamera } from './camera';
|
|
5
|
+
import { createPerspectiveProjection } from './projection';
|
|
6
|
+
import { configureDirectionalShadowCamera } from './shadowCamera';
|
|
7
|
+
|
|
8
|
+
function bounds() {
|
|
9
|
+
return createAabb(-1, -1, -1, 1, 1, 1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('configureDirectionalShadowCamera', () => {
|
|
13
|
+
it('switches the camera to an orthographic frustum sized to the scene sphere', () => {
|
|
14
|
+
const camera = createCamera({
|
|
15
|
+
far: 100,
|
|
16
|
+
near: 0.1,
|
|
17
|
+
projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
configureDirectionalShadowCamera(camera, { x: 0, y: -1, z: 0 }, bounds());
|
|
21
|
+
|
|
22
|
+
expect(camera.projection.kind).toBe('orthographic');
|
|
23
|
+
const radius = Math.hypot(1, 1, 1);
|
|
24
|
+
expect((camera.projection as OrthographicProjection).halfWidth).toBeCloseTo(radius);
|
|
25
|
+
expect(camera.near).toBeCloseTo(radius);
|
|
26
|
+
expect(camera.far).toBeCloseTo(radius * 3);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('places the scene centre two radii in front along the light direction', () => {
|
|
30
|
+
const camera = createCamera({
|
|
31
|
+
far: 100,
|
|
32
|
+
near: 0.1,
|
|
33
|
+
projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
configureDirectionalShadowCamera(camera, { x: 0, y: -1, z: 0 }, bounds());
|
|
37
|
+
|
|
38
|
+
// The world origin (= scene centre) maps to (0, 0, -distance) in the shadow camera's view space;
|
|
39
|
+
// the view matrix's translation column carries that view-space position of the world origin.
|
|
40
|
+
const distance = Math.hypot(1, 1, 1) * 2;
|
|
41
|
+
expect(camera.view.m[12]).toBeCloseTo(0);
|
|
42
|
+
expect(camera.view.m[13]).toBeCloseTo(0);
|
|
43
|
+
expect(camera.view.m[14]).toBeCloseTo(-distance);
|
|
44
|
+
});
|
|
45
|
+
});
|