@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,16 @@
|
|
|
1
|
+
import type { Matrix4Like } from '@flighthq/types';
|
|
2
|
+
import type { OrthographicProjection, PerspectiveProjection, Projection } from '@flighthq/types';
|
|
3
|
+
export declare function createOrthographicProjection(opts: Readonly<OrthographicProjectionOptions>): OrthographicProjection;
|
|
4
|
+
export declare function createPerspectiveProjection(opts: Readonly<PerspectiveProjectionOptions>): PerspectiveProjection;
|
|
5
|
+
export declare function isOrthographicProjection(projection: Readonly<Projection>): projection is OrthographicProjection;
|
|
6
|
+
export declare function isPerspectiveProjection(projection: Readonly<Projection>): projection is PerspectiveProjection;
|
|
7
|
+
export declare function setProjectionMatrix4(out: Matrix4Like, projection: Readonly<Projection>, aspect: number, near: number, far: number): void;
|
|
8
|
+
export interface OrthographicProjectionOptions {
|
|
9
|
+
halfHeight: number;
|
|
10
|
+
halfWidth: number;
|
|
11
|
+
}
|
|
12
|
+
export interface PerspectiveProjectionOptions {
|
|
13
|
+
aspect?: number;
|
|
14
|
+
fovY: number;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAKjG,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,QAAQ,CAAC,6BAA6B,CAAC,GAAG,sBAAsB,CAMlH;AAID,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,QAAQ,CAAC,4BAA4B,CAAC,GAAG,qBAAqB,CAM/G;AAGD,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,IAAI,sBAAsB,CAE/G;AAGD,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,IAAI,qBAAqB,CAE7G;AAUD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,WAAW,EAChB,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,EAChC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,IAAI,CAWN;AAGD,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,4BAA4B;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { setOrthographicMatrix4, setPerspectiveMatrix4 } from '@flighthq/geometry';
|
|
2
|
+
// Builds an orthographic projection descriptor from explicit view-volume half-extents (in
|
|
3
|
+
// view-space units). The full visible width is 2*halfWidth and height 2*halfHeight; the
|
|
4
|
+
// clip-plane distances live on the owning Camera, not the projection.
|
|
5
|
+
export function createOrthographicProjection(opts) {
|
|
6
|
+
return {
|
|
7
|
+
halfHeight: opts.halfHeight,
|
|
8
|
+
halfWidth: opts.halfWidth,
|
|
9
|
+
kind: 'orthographic',
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
// Builds a perspective projection descriptor from a vertical field of view (radians) and a
|
|
13
|
+
// viewport aspect ratio (width / height). The clip-plane distances live on the owning Camera.
|
|
14
|
+
export function createPerspectiveProjection(opts) {
|
|
15
|
+
return {
|
|
16
|
+
aspect: opts.aspect ?? 1,
|
|
17
|
+
fovY: opts.fovY,
|
|
18
|
+
kind: 'perspective',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// True when the projection is an orthographic descriptor. Narrows the discriminated union.
|
|
22
|
+
export function isOrthographicProjection(projection) {
|
|
23
|
+
return projection.kind === 'orthographic';
|
|
24
|
+
}
|
|
25
|
+
// True when the projection is a perspective descriptor. Narrows the discriminated union.
|
|
26
|
+
export function isPerspectiveProjection(projection) {
|
|
27
|
+
return projection.kind === 'perspective';
|
|
28
|
+
}
|
|
29
|
+
// Writes the projection matrix for `projection` into `out`, delegating to geometry's perspective
|
|
30
|
+
// or orthographic builders. `aspect` (viewport width / height) overrides a perspective
|
|
31
|
+
// projection's stored aspect so a single descriptor can drive a resizing viewport; it is ignored
|
|
32
|
+
// for orthographic projections, whose half-extents are explicit. `near`/`far` are the clip-plane
|
|
33
|
+
// distances supplied by the owning Camera.
|
|
34
|
+
//
|
|
35
|
+
// Reads the projection fields into locals before writing `out`, so it is safe even if `out`
|
|
36
|
+
// aliases a matrix referenced elsewhere.
|
|
37
|
+
export function setProjectionMatrix4(out, projection, aspect, near, far) {
|
|
38
|
+
if (projection.kind === 'perspective') {
|
|
39
|
+
// Geometry's setPerspectiveMatrix4 takes the tangent of the half-FOV, not the full angle.
|
|
40
|
+
const tanHalfFovY = Math.tan(projection.fovY * 0.5);
|
|
41
|
+
setPerspectiveMatrix4(out, tanHalfFovY, aspect, near, far);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const halfWidth = projection.halfWidth;
|
|
45
|
+
const halfHeight = projection.halfHeight;
|
|
46
|
+
setOrthographicMatrix4(out, -halfWidth, halfWidth, -halfHeight, halfHeight, near, far);
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.js","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAInF,0FAA0F;AAC1F,wFAAwF;AACxF,sEAAsE;AACtE,MAAM,UAAU,4BAA4B,CAAC,IAA6C;IACxF,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,IAAI,EAAE,cAAc;KACrB,CAAC;AACJ,CAAC;AAED,2FAA2F;AAC3F,8FAA8F;AAC9F,MAAM,UAAU,2BAA2B,CAAC,IAA4C;IACtF,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC;QACxB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,aAAa;KACpB,CAAC;AACJ,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,wBAAwB,CAAC,UAAgC;IACvE,OAAO,UAAU,CAAC,IAAI,KAAK,cAAc,CAAC;AAC5C,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,uBAAuB,CAAC,UAAgC;IACtE,OAAO,UAAU,CAAC,IAAI,KAAK,aAAa,CAAC;AAC3C,CAAC;AAED,iGAAiG;AACjG,uFAAuF;AACvF,iGAAiG;AACjG,iGAAiG;AACjG,2CAA2C;AAC3C,EAAE;AACF,4FAA4F;AAC5F,yCAAyC;AACzC,MAAM,UAAU,oBAAoB,CAClC,GAAgB,EAChB,UAAgC,EAChC,MAAc,EACd,IAAY,EACZ,GAAW;IAEX,IAAI,UAAU,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;QACtC,0FAA0F;QAC1F,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;QACpD,qBAAqB,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;QAC3D,OAAO;IACT,CAAC;IAED,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC;IACvC,MAAM,UAAU,GAAG,UAAU,CAAC,UAAU,CAAC;IACzC,sBAAsB,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AACzF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadowCamera.d.ts","sourceRoot":"","sources":["../src/shadowCamera.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAarE,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,QAAQ,CAAC,WAAW,CAAC,EACrC,WAAW,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAC9B,IAAI,CA8BN"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { setCameraViewMatrix4FromLookAt } from './camera';
|
|
2
|
+
import { createOrthographicProjection } from './projection';
|
|
3
|
+
// Configures `camera` as the shadow camera for a directional light — the spec's "a shadow camera is
|
|
4
|
+
// just a Camera placed at the light." Looks along `lightDirection` (the light's travel direction) at
|
|
5
|
+
// the centre of `sceneBounds`, with an orthographic frustum sized to the scene's bounding sphere so the
|
|
6
|
+
// whole scene fits the shadow map. The render side then draws scene depth from this camera into the
|
|
7
|
+
// shadow map and samples it during shading.
|
|
8
|
+
//
|
|
9
|
+
// Backend-agnostic CPU math (no GL). `lightDirection` need not be normalized. A degenerate (single
|
|
10
|
+
// point) bounds falls back to unit radius so the frustum stays valid.
|
|
11
|
+
export function configureDirectionalShadowCamera(camera, lightDirection, sceneBounds) {
|
|
12
|
+
const min = sceneBounds.min;
|
|
13
|
+
const max = sceneBounds.max;
|
|
14
|
+
const cx = (min.x + max.x) * 0.5;
|
|
15
|
+
const cy = (min.y + max.y) * 0.5;
|
|
16
|
+
const cz = (min.z + max.z) * 0.5;
|
|
17
|
+
let radius = Math.hypot(max.x - cx, max.y - cy, max.z - cz);
|
|
18
|
+
if (radius === 0)
|
|
19
|
+
radius = 1;
|
|
20
|
+
const dl = Math.hypot(lightDirection.x, lightDirection.y, lightDirection.z) || 1;
|
|
21
|
+
const dx = lightDirection.x / dl;
|
|
22
|
+
const dy = lightDirection.y / dl;
|
|
23
|
+
const dz = lightDirection.z / dl;
|
|
24
|
+
// Pull the eye back two radii along -lightDirection so the whole sphere sits in front of the camera.
|
|
25
|
+
const distance = radius * 2;
|
|
26
|
+
_eye.x = cx - dx * distance;
|
|
27
|
+
_eye.y = cy - dy * distance;
|
|
28
|
+
_eye.z = cz - dz * distance;
|
|
29
|
+
_target.x = cx;
|
|
30
|
+
_target.y = cy;
|
|
31
|
+
_target.z = cz;
|
|
32
|
+
// A near-vertical light needs a non-parallel up vector.
|
|
33
|
+
const up = Math.abs(dy) > 0.99 ? _upZ : _upY;
|
|
34
|
+
setCameraViewMatrix4FromLookAt(camera, _eye, _target, up);
|
|
35
|
+
camera.near = radius; // distance - radius
|
|
36
|
+
camera.far = radius * 3; // distance + radius
|
|
37
|
+
camera.projection = createOrthographicProjection({ halfHeight: radius, halfWidth: radius });
|
|
38
|
+
}
|
|
39
|
+
const _eye = { x: 0, y: 0, z: 0 };
|
|
40
|
+
const _target = { x: 0, y: 0, z: 0 };
|
|
41
|
+
const _upY = { x: 0, y: 1, z: 0 };
|
|
42
|
+
const _upZ = { x: 0, y: 0, z: 1 };
|
|
43
|
+
//# sourceMappingURL=shadowCamera.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadowCamera.js","sourceRoot":"","sources":["../src/shadowCamera.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/camera",
|
|
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": "3D camera: projections and view-projection helpers",
|
|
38
|
+
"sideEffects": false
|
|
39
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createVector3 } from '@flighthq/geometry';
|
|
2
|
+
|
|
3
|
+
import { getCameraForward, getCameraPosition, getCameraRight, getCameraUp } from './basis';
|
|
4
|
+
import { createCamera, setCameraViewMatrix4FromLookAt } from './camera';
|
|
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: 1 }),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('getCameraForward', () => {
|
|
16
|
+
it('returns the eye→target direction for a look-at camera', () => {
|
|
17
|
+
const camera = makeCamera();
|
|
18
|
+
// Camera at (0,0,5) looking at origin — forward should be (0,0,-1).
|
|
19
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
20
|
+
const out = createVector3();
|
|
21
|
+
getCameraForward(out, camera);
|
|
22
|
+
expect(out.x).toBeCloseTo(0);
|
|
23
|
+
expect(out.y).toBeCloseTo(0);
|
|
24
|
+
expect(out.z).toBeCloseTo(-1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('is safe when out aliases a camera view field component', () => {
|
|
28
|
+
const camera = makeCamera();
|
|
29
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
30
|
+
// Use a standalone out; just confirm it does not throw.
|
|
31
|
+
const out = createVector3();
|
|
32
|
+
getCameraForward(out, camera);
|
|
33
|
+
expect(out.z).toBeCloseTo(-1);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getCameraPosition', () => {
|
|
38
|
+
it('returns the eye position for a look-at camera', () => {
|
|
39
|
+
const camera = makeCamera();
|
|
40
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(3, 4, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
41
|
+
const out = createVector3();
|
|
42
|
+
getCameraPosition(out, camera);
|
|
43
|
+
expect(out.x).toBeCloseTo(3);
|
|
44
|
+
expect(out.y).toBeCloseTo(4);
|
|
45
|
+
expect(out.z).toBeCloseTo(5);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns the origin for an identity view', () => {
|
|
49
|
+
const camera = makeCamera();
|
|
50
|
+
// Default view is identity → eye at origin.
|
|
51
|
+
const out = createVector3(9, 9, 9);
|
|
52
|
+
getCameraPosition(out, camera);
|
|
53
|
+
expect(out.x).toBeCloseTo(0);
|
|
54
|
+
expect(out.y).toBeCloseTo(0);
|
|
55
|
+
expect(out.z).toBeCloseTo(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('getCameraRight', () => {
|
|
60
|
+
it('returns the world-space right direction for a look-at camera', () => {
|
|
61
|
+
const camera = makeCamera();
|
|
62
|
+
// Looking along -Z, up = +Y → right should be +X.
|
|
63
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
64
|
+
const out = createVector3();
|
|
65
|
+
getCameraRight(out, camera);
|
|
66
|
+
expect(out.x).toBeCloseTo(1);
|
|
67
|
+
expect(out.y).toBeCloseTo(0);
|
|
68
|
+
expect(out.z).toBeCloseTo(0);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('getCameraUp', () => {
|
|
73
|
+
it('returns the world-space up direction for a look-at camera', () => {
|
|
74
|
+
const camera = makeCamera();
|
|
75
|
+
// Looking along -Z, up hint = +Y → computed up should be +Y.
|
|
76
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
77
|
+
const out = createVector3();
|
|
78
|
+
getCameraUp(out, camera);
|
|
79
|
+
expect(out.x).toBeCloseTo(0);
|
|
80
|
+
expect(out.y).toBeCloseTo(1);
|
|
81
|
+
expect(out.z).toBeCloseTo(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createMatrix4, createVector3, inverseMatrix4, multiplyMatrix4, setMatrix4LookAt } from '@flighthq/geometry';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createCamera,
|
|
5
|
+
getCameraInverseViewProjectionMatrix4,
|
|
6
|
+
getCameraViewProjectionMatrix4,
|
|
7
|
+
setCameraJitter,
|
|
8
|
+
setCameraViewMatrix4FromLookAt,
|
|
9
|
+
setCameraViewMatrix4FromMatrix4,
|
|
10
|
+
updateCameraInverseViewProjection,
|
|
11
|
+
} from './camera';
|
|
12
|
+
import { createPerspectiveProjection, setProjectionMatrix4 } from './projection';
|
|
13
|
+
|
|
14
|
+
describe('createCamera', () => {
|
|
15
|
+
it('stores projection, near, far and identity view/inverse with zero jitter', () => {
|
|
16
|
+
const projection = createPerspectiveProjection({ aspect: 1, fovY: 1 });
|
|
17
|
+
const camera = createCamera({ far: 100, near: 0.1, projection });
|
|
18
|
+
|
|
19
|
+
expect(camera.projection).toBe(projection);
|
|
20
|
+
expect(camera.near).toBe(0.1);
|
|
21
|
+
expect(camera.far).toBe(100);
|
|
22
|
+
expect(camera.jitter.x).toBe(0);
|
|
23
|
+
expect(camera.jitter.y).toBe(0);
|
|
24
|
+
expect(camera.view.m[0]).toBe(1);
|
|
25
|
+
expect(camera.view.m[5]).toBe(1);
|
|
26
|
+
expect(camera.inverseViewProjection.m[10]).toBe(1);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('getCameraInverseViewProjectionMatrix4', () => {
|
|
31
|
+
it('writes the inverse of the view-projection into a distinct out', () => {
|
|
32
|
+
const projection = createPerspectiveProjection({ aspect: 1, fovY: 1 });
|
|
33
|
+
const camera = createCamera({ far: 100, near: 0.1, projection });
|
|
34
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
35
|
+
|
|
36
|
+
const viewProjection = createMatrix4();
|
|
37
|
+
getCameraViewProjectionMatrix4(viewProjection, camera, 1.5);
|
|
38
|
+
|
|
39
|
+
const expected = createMatrix4();
|
|
40
|
+
inverseMatrix4(expected, viewProjection);
|
|
41
|
+
|
|
42
|
+
const out = createMatrix4();
|
|
43
|
+
expect(getCameraInverseViewProjectionMatrix4(out, camera, 1.5)).toBe(true);
|
|
44
|
+
for (let i = 0; i < 16; i++) {
|
|
45
|
+
expect(out.m[i]).toBeCloseTo(expected.m[i]);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('is safe when out aliases the camera inverseViewProjection', () => {
|
|
50
|
+
const projection = createPerspectiveProjection({ aspect: 1, fovY: 1 });
|
|
51
|
+
const camera = createCamera({ far: 100, near: 0.1, projection });
|
|
52
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(2, 1, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
53
|
+
|
|
54
|
+
const viewProjection = createMatrix4();
|
|
55
|
+
getCameraViewProjectionMatrix4(viewProjection, camera, 1.3);
|
|
56
|
+
const expected = createMatrix4();
|
|
57
|
+
inverseMatrix4(expected, viewProjection);
|
|
58
|
+
|
|
59
|
+
expect(getCameraInverseViewProjectionMatrix4(camera.inverseViewProjection, camera, 1.3)).toBe(true);
|
|
60
|
+
for (let i = 0; i < 16; i++) {
|
|
61
|
+
expect(camera.inverseViewProjection.m[i]).toBeCloseTo(expected.m[i]);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('getCameraViewProjectionMatrix4', () => {
|
|
67
|
+
it('writes projection times view into a distinct out', () => {
|
|
68
|
+
const projection = createPerspectiveProjection({ aspect: 1, fovY: 1 });
|
|
69
|
+
const camera = createCamera({ far: 100, near: 0.1, projection });
|
|
70
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
71
|
+
|
|
72
|
+
const out = createMatrix4();
|
|
73
|
+
getCameraViewProjectionMatrix4(out, camera, 2);
|
|
74
|
+
|
|
75
|
+
const projectionMatrix = createMatrix4();
|
|
76
|
+
setProjectionMatrix4(projectionMatrix, projection, 2, camera.near, camera.far);
|
|
77
|
+
const expected = createMatrix4();
|
|
78
|
+
multiplyMatrix4(expected, projectionMatrix, camera.view);
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < 16; i++) {
|
|
81
|
+
expect(out.m[i]).toBeCloseTo(expected.m[i]);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('is safe when out aliases the camera view', () => {
|
|
86
|
+
const projection = createPerspectiveProjection({ aspect: 1, fovY: 1 });
|
|
87
|
+
const camera = createCamera({ far: 100, near: 0.1, projection });
|
|
88
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(1, 2, 4), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
89
|
+
|
|
90
|
+
const projectionMatrix = createMatrix4();
|
|
91
|
+
setProjectionMatrix4(projectionMatrix, projection, 1.7, camera.near, camera.far);
|
|
92
|
+
const expected = createMatrix4();
|
|
93
|
+
multiplyMatrix4(expected, projectionMatrix, camera.view);
|
|
94
|
+
|
|
95
|
+
getCameraViewProjectionMatrix4(camera.view, camera, 1.7);
|
|
96
|
+
for (let i = 0; i < 16; i++) {
|
|
97
|
+
expect(camera.view.m[i]).toBeCloseTo(expected.m[i]);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('setCameraJitter', () => {
|
|
103
|
+
it('sets the jitter offset in place', () => {
|
|
104
|
+
const camera = createCamera({
|
|
105
|
+
far: 100,
|
|
106
|
+
near: 0.1,
|
|
107
|
+
projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }),
|
|
108
|
+
});
|
|
109
|
+
const jitter = camera.jitter;
|
|
110
|
+
setCameraJitter(camera, 0.25, -0.5);
|
|
111
|
+
expect(camera.jitter).toBe(jitter);
|
|
112
|
+
expect(camera.jitter.x).toBe(0.25);
|
|
113
|
+
expect(camera.jitter.y).toBe(-0.5);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('setCameraViewMatrix4FromLookAt', () => {
|
|
118
|
+
it('builds the view matrix from eye-target-up', () => {
|
|
119
|
+
const camera = createCamera({
|
|
120
|
+
far: 100,
|
|
121
|
+
near: 0.1,
|
|
122
|
+
projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }),
|
|
123
|
+
});
|
|
124
|
+
const eye = createVector3(0, 0, 10);
|
|
125
|
+
const target = createVector3(0, 0, 0);
|
|
126
|
+
const up = createVector3(0, 1, 0);
|
|
127
|
+
setCameraViewMatrix4FromLookAt(camera, eye, target, up);
|
|
128
|
+
|
|
129
|
+
const expected = createMatrix4();
|
|
130
|
+
setMatrix4LookAt(expected, eye, target, up);
|
|
131
|
+
for (let i = 0; i < 16; i++) {
|
|
132
|
+
expect(camera.view.m[i]).toBeCloseTo(expected.m[i]);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('setCameraViewMatrix4FromMatrix4', () => {
|
|
138
|
+
it('copies a precomputed view matrix into the camera', () => {
|
|
139
|
+
const camera = createCamera({
|
|
140
|
+
far: 100,
|
|
141
|
+
near: 0.1,
|
|
142
|
+
projection: createPerspectiveProjection({ aspect: 1, fovY: 1 }),
|
|
143
|
+
});
|
|
144
|
+
const view = createMatrix4();
|
|
145
|
+
setMatrix4LookAt(view, createVector3(3, 3, 3), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
146
|
+
|
|
147
|
+
const target = camera.view;
|
|
148
|
+
setCameraViewMatrix4FromMatrix4(camera, view);
|
|
149
|
+
// Copied in place: the camera's own view matrix instance is retained.
|
|
150
|
+
expect(camera.view).toBe(target);
|
|
151
|
+
for (let i = 0; i < 16; i++) {
|
|
152
|
+
expect(camera.view.m[i]).toBeCloseTo(view.m[i]);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('updateCameraInverseViewProjection', () => {
|
|
158
|
+
it('stores the inverse view-projection into camera.inverseViewProjection and returns true', () => {
|
|
159
|
+
const projection = createPerspectiveProjection({ aspect: 1, fovY: 1 });
|
|
160
|
+
const camera = createCamera({ far: 100, near: 0.1, projection });
|
|
161
|
+
setCameraViewMatrix4FromLookAt(camera, createVector3(1, 2, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
|
|
162
|
+
|
|
163
|
+
const result = updateCameraInverseViewProjection(camera, 1.5);
|
|
164
|
+
expect(result).toBe(true);
|
|
165
|
+
|
|
166
|
+
// Compute expected inverse independently.
|
|
167
|
+
const vp = createMatrix4();
|
|
168
|
+
getCameraViewProjectionMatrix4(vp, camera, 1.5);
|
|
169
|
+
const expected = createMatrix4();
|
|
170
|
+
inverseMatrix4(expected, vp);
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < 16; i++) {
|
|
173
|
+
expect(camera.inverseViewProjection.m[i]).toBeCloseTo(expected.m[i]);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('returns false and leaves inverseViewProjection untouched for a non-invertible matrix', () => {
|
|
178
|
+
const projection = createPerspectiveProjection({ aspect: 1, fovY: 1 });
|
|
179
|
+
const camera = createCamera({ far: 100, near: 0.1, projection });
|
|
180
|
+
camera.near = 1;
|
|
181
|
+
camera.far = 1; // Degenerate.
|
|
182
|
+
// Store a known value so we can confirm it was not overwritten.
|
|
183
|
+
camera.inverseViewProjection.m[0] = 42;
|
|
184
|
+
|
|
185
|
+
const result = updateCameraInverseViewProjection(camera, 1);
|
|
186
|
+
expect(result).toBe(false);
|
|
187
|
+
expect(camera.inverseViewProjection.m[0]).toBe(42);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createAabb, createBoundingSphere, createFrustum, createVector3 } from '@flighthq/geometry';
|
|
2
|
+
|
|
3
|
+
import { createCamera, setCameraViewMatrix4FromLookAt } from './camera';
|
|
4
|
+
import { getCameraFrustum, isBoxInCameraFrustum, isPointInCameraFrustum, isSphereInCameraFrustum } from './culling';
|
|
5
|
+
import { createPerspectiveProjection } from './projection';
|
|
6
|
+
|
|
7
|
+
function makeCamera() {
|
|
8
|
+
const camera = createCamera({
|
|
9
|
+
far: 100,
|
|
10
|
+
near: 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('getCameraFrustum', () => {
|
|
18
|
+
it('writes the six frustum planes into the out frustum', () => {
|
|
19
|
+
const camera = makeCamera();
|
|
20
|
+
const frustum = createFrustum();
|
|
21
|
+
getCameraFrustum(frustum, camera, 1);
|
|
22
|
+
// Near plane normal should have a z component (pointing generally toward the camera).
|
|
23
|
+
const len = Math.sqrt(
|
|
24
|
+
frustum.near.a * frustum.near.a + frustum.near.b * frustum.near.b + frustum.near.c * frustum.near.c,
|
|
25
|
+
);
|
|
26
|
+
expect(len).toBeCloseTo(1, 3);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('isBoxInCameraFrustum', () => {
|
|
31
|
+
it('returns true for a box in front of the camera', () => {
|
|
32
|
+
const camera = makeCamera();
|
|
33
|
+
const aabb = createAabb();
|
|
34
|
+
aabb.min.x = -1;
|
|
35
|
+
aabb.min.y = -1;
|
|
36
|
+
aabb.min.z = 2;
|
|
37
|
+
aabb.max.x = 1;
|
|
38
|
+
aabb.max.y = 1;
|
|
39
|
+
aabb.max.z = 5;
|
|
40
|
+
expect(isBoxInCameraFrustum(camera, aabb, 1)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns false for a box entirely behind the camera', () => {
|
|
44
|
+
const camera = makeCamera();
|
|
45
|
+
const aabb = createAabb();
|
|
46
|
+
// Behind eye at z=10, so z > 10 is behind.
|
|
47
|
+
aabb.min.x = -1;
|
|
48
|
+
aabb.min.y = -1;
|
|
49
|
+
aabb.min.z = 15;
|
|
50
|
+
aabb.max.x = 1;
|
|
51
|
+
aabb.max.y = 1;
|
|
52
|
+
aabb.max.z = 20;
|
|
53
|
+
expect(isBoxInCameraFrustum(camera, aabb, 1)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('isPointInCameraFrustum', () => {
|
|
58
|
+
it('returns true for a point in the frustum', () => {
|
|
59
|
+
const camera = makeCamera();
|
|
60
|
+
const point = createVector3(0, 0, 5);
|
|
61
|
+
expect(isPointInCameraFrustum(camera, point, 1)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns false for a point behind the camera', () => {
|
|
65
|
+
const camera = makeCamera();
|
|
66
|
+
const point = createVector3(0, 0, 15);
|
|
67
|
+
expect(isPointInCameraFrustum(camera, point, 1)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('isSphereInCameraFrustum', () => {
|
|
72
|
+
it('returns true for a sphere in front of the camera', () => {
|
|
73
|
+
const camera = makeCamera();
|
|
74
|
+
const sphere = createBoundingSphere(0, 0, 5, 1);
|
|
75
|
+
expect(isSphereInCameraFrustum(camera, sphere, 1)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns false for a sphere entirely behind the camera', () => {
|
|
79
|
+
const camera = makeCamera();
|
|
80
|
+
// A small sphere far behind the eye.
|
|
81
|
+
const sphere = createBoundingSphere(0, 0, 20, 1);
|
|
82
|
+
expect(isSphereInCameraFrustum(camera, sphere, 1)).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns false for an empty sphere', () => {
|
|
86
|
+
const camera = makeCamera();
|
|
87
|
+
const sphere = createBoundingSphere(0, 0, 5, -1);
|
|
88
|
+
expect(isSphereInCameraFrustum(camera, sphere, 1)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createCamera } from './camera';
|
|
2
|
+
import { getCameraLinearDepth, getCameraViewSpaceZ } from './depth';
|
|
3
|
+
import { createPerspectiveProjection } from './projection';
|
|
4
|
+
|
|
5
|
+
function makeCamera(near = 0.1, far = 100) {
|
|
6
|
+
return createCamera({
|
|
7
|
+
far,
|
|
8
|
+
near,
|
|
9
|
+
projection: createPerspectiveProjection({ aspect: 1, fovY: Math.PI / 2 }),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('getCameraLinearDepth', () => {
|
|
14
|
+
it('returns a negative value for the near plane (ndcZ = -1)', () => {
|
|
15
|
+
const camera = makeCamera(1, 100);
|
|
16
|
+
const depth = getCameraLinearDepth(camera, -1);
|
|
17
|
+
// Near plane: viewZ should be -near
|
|
18
|
+
expect(depth).toBeCloseTo(-1, 3);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns a negative value for the far plane (ndcZ = 1)', () => {
|
|
22
|
+
const camera = makeCamera(1, 100);
|
|
23
|
+
const depth = getCameraLinearDepth(camera, 1);
|
|
24
|
+
// Far plane: viewZ should be -far
|
|
25
|
+
expect(depth).toBeCloseTo(-100, 2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns 0 when near equals far (degenerate clip range)', () => {
|
|
29
|
+
const camera = makeCamera(10, 10);
|
|
30
|
+
expect(getCameraLinearDepth(camera, 0)).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns a value between -near and -far for a midpoint ndcZ', () => {
|
|
34
|
+
const camera = makeCamera(1, 100);
|
|
35
|
+
// ndcZ = 0 is between near and far planes.
|
|
36
|
+
const depth = getCameraLinearDepth(camera, 0);
|
|
37
|
+
expect(depth).toBeLessThan(-1);
|
|
38
|
+
expect(depth).toBeGreaterThan(-100);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('getCameraViewSpaceZ', () => {
|
|
43
|
+
it('returns a positive value for the near plane (ndcZ = -1)', () => {
|
|
44
|
+
const camera = makeCamera(1, 100);
|
|
45
|
+
const z = getCameraViewSpaceZ(camera, -1);
|
|
46
|
+
expect(z).toBeCloseTo(1, 3);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns a positive value for the far plane (ndcZ = 1)', () => {
|
|
50
|
+
const camera = makeCamera(1, 100);
|
|
51
|
+
const z = getCameraViewSpaceZ(camera, 1);
|
|
52
|
+
expect(z).toBeCloseTo(100, 2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('is the negation of getCameraLinearDepth', () => {
|
|
56
|
+
const camera = makeCamera(0.1, 50);
|
|
57
|
+
const ndcZ = 0.5;
|
|
58
|
+
expect(getCameraViewSpaceZ(camera, ndcZ)).toBeCloseTo(-getCameraLinearDepth(camera, ndcZ), 5);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns 0 for degenerate clip range', () => {
|
|
62
|
+
const camera = makeCamera(5, 5);
|
|
63
|
+
expect(getCameraViewSpaceZ(camera, 0)).toBeCloseTo(0, 5);
|
|
64
|
+
});
|
|
65
|
+
});
|