@flighthq/render 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/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/renderAppearance.d.ts +3 -0
- package/dist/renderAppearance.d.ts.map +1 -0
- package/dist/renderAppearance.js +35 -0
- package/dist/renderAppearance.js.map +1 -0
- package/dist/renderCache.d.ts +36 -0
- package/dist/renderCache.d.ts.map +1 -0
- package/dist/renderCache.js +83 -0
- package/dist/renderCache.js.map +1 -0
- package/dist/renderColor.d.ts +3 -0
- package/dist/renderColor.d.ts.map +1 -0
- package/dist/renderColor.js +17 -0
- package/dist/renderColor.js.map +1 -0
- package/dist/renderMaterial.d.ts +3 -0
- package/dist/renderMaterial.d.ts.map +1 -0
- package/dist/renderMaterial.js +9 -0
- package/dist/renderMaterial.js.map +1 -0
- package/dist/renderProxy.d.ts +19 -0
- package/dist/renderProxy.d.ts.map +1 -0
- package/dist/renderProxy.js +203 -0
- package/dist/renderProxy.js.map +1 -0
- package/dist/renderProxyAdapter.d.ts +7 -0
- package/dist/renderProxyAdapter.d.ts.map +1 -0
- package/dist/renderProxyAdapter.js +32 -0
- package/dist/renderProxyAdapter.js.map +1 -0
- package/dist/renderQueue.d.ts +9 -0
- package/dist/renderQueue.d.ts.map +1 -0
- package/dist/renderQueue.js +97 -0
- package/dist/renderQueue.js.map +1 -0
- package/dist/renderState.d.ts +5 -0
- package/dist/renderState.d.ts.map +1 -0
- package/dist/renderState.js +42 -0
- package/dist/renderState.js.map +1 -0
- package/dist/renderTarget.d.ts +21 -0
- package/dist/renderTarget.d.ts.map +1 -0
- package/dist/renderTarget.js +39 -0
- package/dist/renderTarget.js.map +1 -0
- package/dist/renderTransform2d.d.ts +3 -0
- package/dist/renderTransform2d.d.ts.map +1 -0
- package/dist/renderTransform2d.js +26 -0
- package/dist/renderTransform2d.js.map +1 -0
- package/dist/renderViewport.d.ts +6 -0
- package/dist/renderViewport.d.ts.map +1 -0
- package/dist/renderViewport.js +57 -0
- package/dist/renderViewport.js.map +1 -0
- package/dist/renderer.d.ts +6 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +24 -0
- package/dist/renderer.js.map +1 -0
- package/dist/sceneRender.d.ts +4 -0
- package/dist/sceneRender.d.ts.map +1 -0
- package/dist/sceneRender.js +175 -0
- package/dist/sceneRender.js.map +1 -0
- package/package.json +48 -0
- package/src/renderAppearance.test.ts +109 -0
- package/src/renderCache.test.ts +151 -0
- package/src/renderColor.test.ts +10 -0
- package/src/renderMaterial.test.ts +30 -0
- package/src/renderProxy.test.ts +531 -0
- package/src/renderProxyAdapter.test.ts +101 -0
- package/src/renderQueue.test.ts +210 -0
- package/src/renderState.test.ts +93 -0
- package/src/renderTarget.test.ts +84 -0
- package/src/renderTransform2d.test.ts +59 -0
- package/src/renderViewport.test.ts +187 -0
- package/src/renderer.test.ts +128 -0
- package/src/sceneRender.test.ts +207 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { DisplayObjectClipHooks, Renderer, RenderState } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
copyAllRenderersFromRenderState,
|
|
5
|
+
copyRenderersFromRenderState,
|
|
6
|
+
noopRendererData,
|
|
7
|
+
registerRenderer,
|
|
8
|
+
} from './renderer';
|
|
9
|
+
import { createRenderState, getRenderStateRuntime } from './renderState';
|
|
10
|
+
|
|
11
|
+
describe('copyAllRenderersFromRenderState', () => {
|
|
12
|
+
it('copies all registrations and the clip hooks from source to target', () => {
|
|
13
|
+
const source = createRenderState();
|
|
14
|
+
const target = createRenderState();
|
|
15
|
+
const kind = 'kind';
|
|
16
|
+
const renderer = { createData: vi.fn(), submit: vi.fn() } as unknown as Renderer;
|
|
17
|
+
const hooks = {
|
|
18
|
+
finalize: vi.fn(),
|
|
19
|
+
popClip: vi.fn(),
|
|
20
|
+
pushClip: vi.fn(),
|
|
21
|
+
} as unknown as DisplayObjectClipHooks;
|
|
22
|
+
registerRenderer(source, kind, renderer);
|
|
23
|
+
source.displayObjectClipHooks = hooks;
|
|
24
|
+
|
|
25
|
+
copyAllRenderersFromRenderState(target, source);
|
|
26
|
+
|
|
27
|
+
expect(getRenderStateRuntime(target).rendererMap.get(kind)).toBe(renderer);
|
|
28
|
+
expect(target.displayObjectClipHooks).toBe(hooks);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('is a no-op when source has no registrations', () => {
|
|
32
|
+
const source = createRenderState();
|
|
33
|
+
const target = createRenderState();
|
|
34
|
+
copyAllRenderersFromRenderState(target, source);
|
|
35
|
+
expect(getRenderStateRuntime(target).rendererMap.size).toBe(0);
|
|
36
|
+
expect(target.displayObjectClipHooks).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('copyRenderersFromRenderState', () => {
|
|
41
|
+
it('copies all renderer registrations from source to target', () => {
|
|
42
|
+
const source = createRenderState();
|
|
43
|
+
const target = createRenderState();
|
|
44
|
+
const kind = 'kind';
|
|
45
|
+
const renderer = { createData: vi.fn(), submit: vi.fn() } as unknown as Renderer;
|
|
46
|
+
registerRenderer(source, kind, renderer);
|
|
47
|
+
copyRenderersFromRenderState(target, source);
|
|
48
|
+
expect(getRenderStateRuntime(target).rendererMap.get(kind)).toBe(renderer);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('is a no-op when source has no renderer registrations', () => {
|
|
52
|
+
const source = createRenderState();
|
|
53
|
+
const target = createRenderState();
|
|
54
|
+
copyRenderersFromRenderState(target, source);
|
|
55
|
+
expect(getRenderStateRuntime(target).rendererMap.size).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not affect source rendererMapId', () => {
|
|
59
|
+
const source = createRenderState();
|
|
60
|
+
const target = createRenderState();
|
|
61
|
+
const kind = 'kind';
|
|
62
|
+
const renderer = { createData: vi.fn(), submit: vi.fn() } as unknown as Renderer;
|
|
63
|
+
registerRenderer(source, kind, renderer);
|
|
64
|
+
const sourceIdBeforeCopy = getRenderStateRuntime(source).rendererMapId;
|
|
65
|
+
copyRenderersFromRenderState(target, source);
|
|
66
|
+
expect(getRenderStateRuntime(source).rendererMapId).toBe(sourceIdBeforeCopy);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('noopRendererData', () => {
|
|
71
|
+
it('returns null', () => {
|
|
72
|
+
const state = createRenderState();
|
|
73
|
+
expect(noopRendererData(state, {} as any)).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('registerRenderer', () => {
|
|
78
|
+
let state: RenderState;
|
|
79
|
+
let kindA: string;
|
|
80
|
+
let kindB: string;
|
|
81
|
+
let renderer1: Renderer;
|
|
82
|
+
let renderer2: Renderer;
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
kindA = 'kindA';
|
|
86
|
+
kindB = 'kindB';
|
|
87
|
+
renderer1 = { render: vi.fn() } as unknown as Renderer;
|
|
88
|
+
renderer2 = { render: vi.fn() } as unknown as Renderer;
|
|
89
|
+
state = createRenderState();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should register a new renderer', () => {
|
|
93
|
+
expect(getRenderStateRuntime(state).rendererMap.has(kindA)).toBe(false);
|
|
94
|
+
registerRenderer(state, kindA, renderer1);
|
|
95
|
+
expect(getRenderStateRuntime(state).rendererMap.get(kindA)).toBe(renderer1);
|
|
96
|
+
expect(getRenderStateRuntime(state).rendererMapId).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should increment rendererMapId for each new renderer', () => {
|
|
100
|
+
registerRenderer(state, kindA, renderer1);
|
|
101
|
+
const idAfterFirst = getRenderStateRuntime(state).rendererMapId;
|
|
102
|
+
registerRenderer(state, kindB, renderer2);
|
|
103
|
+
expect(getRenderStateRuntime(state).rendererMap.get(kindB)).toBe(renderer2);
|
|
104
|
+
expect(getRenderStateRuntime(state).rendererMapId).toBe(idAfterFirst + 1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should not increment rendererMapId if the same renderer is registered', () => {
|
|
108
|
+
registerRenderer(state, kindA, renderer1);
|
|
109
|
+
const idBefore = getRenderStateRuntime(state).rendererMapId;
|
|
110
|
+
registerRenderer(state, kindA, renderer1);
|
|
111
|
+
expect(getRenderStateRuntime(state).rendererMap.get(kindA)).toBe(renderer1);
|
|
112
|
+
expect(getRenderStateRuntime(state).rendererMapId).toBe(idBefore);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should update renderer and increment rendererMapId if different renderer is registered', () => {
|
|
116
|
+
registerRenderer(state, kindA, renderer1);
|
|
117
|
+
const idBefore = getRenderStateRuntime(state).rendererMapId;
|
|
118
|
+
registerRenderer(state, kindA, renderer2);
|
|
119
|
+
expect(getRenderStateRuntime(state).rendererMap.get(kindA)).toBe(renderer2);
|
|
120
|
+
expect(getRenderStateRuntime(state).rendererMapId).toBe(idBefore + 1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should wrap around rendererMapId correctly using >>> 0', () => {
|
|
124
|
+
getRenderStateRuntime(state).rendererMapId = 0xffffffff;
|
|
125
|
+
registerRenderer(state, kindA, renderer1);
|
|
126
|
+
expect(getRenderStateRuntime(state).rendererMapId).toBe(0);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createCamera, createPerspectiveProjection, setCameraViewMatrix4FromLookAt } from '@flighthq/camera';
|
|
2
|
+
import { createAabb } from '@flighthq/geometry';
|
|
3
|
+
import { createAmbientLight, createDirectionalLight } from '@flighthq/lighting';
|
|
4
|
+
import { unpackColorToLinear } from '@flighthq/materials';
|
|
5
|
+
import { computeMeshGeometryBounds, createBoxMeshGeometry } from '@flighthq/mesh';
|
|
6
|
+
import { addNodeChild, invalidateNodeLocalTransform } from '@flighthq/node';
|
|
7
|
+
import { createMesh, createScene } from '@flighthq/scene';
|
|
8
|
+
import type { Camera, Material, MeshGeometry, SceneLightBlock, SceneLights } from '@flighthq/types';
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
|
|
11
|
+
import { createRenderState } from './renderState';
|
|
12
|
+
import { packSceneLightBlock, prepareSceneRender } from './sceneRender';
|
|
13
|
+
|
|
14
|
+
function boundedBox(): MeshGeometry {
|
|
15
|
+
const geometry = createBoxMeshGeometry(2, 2, 2);
|
|
16
|
+
const bounds = createAabb();
|
|
17
|
+
computeMeshGeometryBounds(bounds, geometry);
|
|
18
|
+
geometry.bounds = bounds;
|
|
19
|
+
return geometry;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function frontCamera(): Camera {
|
|
23
|
+
const camera = createCamera({
|
|
24
|
+
far: 100,
|
|
25
|
+
near: 0.1,
|
|
26
|
+
projection: createPerspectiveProjection({ aspect: 1, fovY: Math.PI / 3 }),
|
|
27
|
+
});
|
|
28
|
+
setCameraViewMatrix4FromLookAt(camera, { x: 0, y: 0, z: 10 }, { x: 0, y: 0, z: 0 }, { x: 0, y: 1, z: 0 });
|
|
29
|
+
return camera;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function emptyLights(): SceneLights {
|
|
33
|
+
return { ambient: null, directional: null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function newLightBlock(): SceneLightBlock {
|
|
37
|
+
return {
|
|
38
|
+
ambientCount: 0,
|
|
39
|
+
data: new Float32Array(12),
|
|
40
|
+
directionalCount: 0,
|
|
41
|
+
hemisphereCount: 0,
|
|
42
|
+
pointCount: 0,
|
|
43
|
+
spotCount: 0,
|
|
44
|
+
version: 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('packSceneLightBlock', () => {
|
|
49
|
+
it('zeroes the block and clears counts when no lights are present', () => {
|
|
50
|
+
const block = newLightBlock();
|
|
51
|
+
block.data[0] = 5;
|
|
52
|
+
packSceneLightBlock(block, emptyLights());
|
|
53
|
+
expect(block.directionalCount).toBe(0);
|
|
54
|
+
expect(block.ambientCount).toBe(0);
|
|
55
|
+
expect(block.data[0]).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('packs the directional direction and linear, premultiplied radiance', () => {
|
|
59
|
+
const block = newLightBlock();
|
|
60
|
+
const directional = createDirectionalLight({ color: 0xffffffff, direction: { x: 0, y: -1, z: 0 }, intensity: 2 });
|
|
61
|
+
packSceneLightBlock(block, { ambient: null, directional });
|
|
62
|
+
expect(block.directionalCount).toBe(1);
|
|
63
|
+
expect(block.data[0]).toBeCloseTo(0);
|
|
64
|
+
expect(block.data[1]).toBeCloseTo(-1);
|
|
65
|
+
expect(block.data[2]).toBeCloseTo(0);
|
|
66
|
+
const expected = unpackColorToLinear([0, 0, 0, 0], 0xffffffff);
|
|
67
|
+
expect(block.data[4]).toBeCloseTo(expected[0] * 2);
|
|
68
|
+
expect(block.data[5]).toBeCloseTo(expected[1] * 2);
|
|
69
|
+
expect(block.data[6]).toBeCloseTo(expected[2] * 2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('packs the ambient radiance into the ambient slot', () => {
|
|
73
|
+
const block = newLightBlock();
|
|
74
|
+
const ambient = createAmbientLight({ color: 0xffffffff, intensity: 0.5 });
|
|
75
|
+
packSceneLightBlock(block, { ambient, directional: null });
|
|
76
|
+
expect(block.ambientCount).toBe(1);
|
|
77
|
+
const expected = unpackColorToLinear([0, 0, 0, 0], 0xffffffff);
|
|
78
|
+
expect(block.data[8]).toBeCloseTo(expected[0] * 0.5);
|
|
79
|
+
expect(block.data[9]).toBeCloseTo(expected[1] * 0.5);
|
|
80
|
+
expect(block.data[10]).toBeCloseTo(expected[2] * 0.5);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('bumps version when the packed block changes', () => {
|
|
84
|
+
const block = newLightBlock();
|
|
85
|
+
packSceneLightBlock(block, emptyLights());
|
|
86
|
+
const v = block.version;
|
|
87
|
+
packSceneLightBlock(block, {
|
|
88
|
+
ambient: createAmbientLight({ color: 0xffffffff, intensity: 0.5 }),
|
|
89
|
+
directional: null,
|
|
90
|
+
});
|
|
91
|
+
expect(block.version).toBe(v + 1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('does not bump version when re-packed with identical lights', () => {
|
|
95
|
+
const block = newLightBlock();
|
|
96
|
+
const lights = { ambient: createAmbientLight({ color: 0xffffffff, intensity: 0.5 }), directional: null };
|
|
97
|
+
packSceneLightBlock(block, lights);
|
|
98
|
+
const v = block.version;
|
|
99
|
+
packSceneLightBlock(block, lights);
|
|
100
|
+
expect(block.version).toBe(v);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('decodes sRgb color to linear (a mid-gray channel is darker in linear)', () => {
|
|
104
|
+
const block = newLightBlock();
|
|
105
|
+
const ambient = createAmbientLight({ color: 0x808080ff, intensity: 1 });
|
|
106
|
+
packSceneLightBlock(block, { ambient, directional: null });
|
|
107
|
+
expect(block.data[8]).toBeLessThan(0x80 / 0xff);
|
|
108
|
+
expect(block.data[8]).toBeGreaterThan(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('prepareSceneRender', () => {
|
|
113
|
+
it('returns the lit, view-projected frame with the visible mesh', () => {
|
|
114
|
+
const state = createRenderState();
|
|
115
|
+
const scene = createScene();
|
|
116
|
+
const mesh = createMesh(boundedBox(), [null]);
|
|
117
|
+
addNodeChild(scene, mesh);
|
|
118
|
+
|
|
119
|
+
const list = prepareSceneRender(state, scene, frontCamera(), {
|
|
120
|
+
ambient: createAmbientLight(),
|
|
121
|
+
directional: createDirectionalLight(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(list.meshCount).toBe(1);
|
|
125
|
+
expect(list.visibleMeshes[0]).toBe(mesh);
|
|
126
|
+
expect(list.lights.directionalCount).toBe(1);
|
|
127
|
+
expect(list.lights.ambientCount).toBe(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('computes a non-identity view-projection', () => {
|
|
131
|
+
const state = createRenderState();
|
|
132
|
+
const scene = createScene();
|
|
133
|
+
const list = prepareSceneRender(state, scene, frontCamera(), emptyLights());
|
|
134
|
+
// A perspective view-projection is not the identity matrix.
|
|
135
|
+
expect(list.viewProjection.m[15]).not.toBe(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('culls a mesh placed far behind the camera', () => {
|
|
139
|
+
const state = createRenderState();
|
|
140
|
+
const scene = createScene();
|
|
141
|
+
const mesh = createMesh(boundedBox(), [null]);
|
|
142
|
+
mesh.localMatrix.m[14] = 1000;
|
|
143
|
+
invalidateNodeLocalTransform(mesh);
|
|
144
|
+
addNodeChild(scene, mesh);
|
|
145
|
+
|
|
146
|
+
const list = prepareSceneRender(state, scene, frontCamera(), emptyLights());
|
|
147
|
+
expect(list.meshCount).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('keeps a mesh whose geometry has no cached bounds', () => {
|
|
151
|
+
const state = createRenderState();
|
|
152
|
+
const scene = createScene();
|
|
153
|
+
const mesh = createMesh(createBoxMeshGeometry(), [null]);
|
|
154
|
+
mesh.geometry.bounds = null;
|
|
155
|
+
mesh.localMatrix.m[14] = 1000;
|
|
156
|
+
invalidateNodeLocalTransform(mesh);
|
|
157
|
+
addNodeChild(scene, mesh);
|
|
158
|
+
|
|
159
|
+
const list = prepareSceneRender(state, scene, frontCamera(), emptyLights());
|
|
160
|
+
expect(list.meshCount).toBe(1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('skips disabled subtrees', () => {
|
|
164
|
+
const state = createRenderState();
|
|
165
|
+
const scene = createScene();
|
|
166
|
+
const group = createScene({ enabled: false });
|
|
167
|
+
const mesh = createMesh(boundedBox(), [null]);
|
|
168
|
+
addNodeChild(group, mesh);
|
|
169
|
+
addNodeChild(scene, group);
|
|
170
|
+
|
|
171
|
+
const list = prepareSceneRender(state, scene, frontCamera(), emptyLights());
|
|
172
|
+
expect(list.meshCount).toBe(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('resolves world transforms through nested groups', () => {
|
|
176
|
+
const state = createRenderState();
|
|
177
|
+
const scene = createScene();
|
|
178
|
+
const group = createScene();
|
|
179
|
+
const mesh = createMesh(boundedBox(), [null]);
|
|
180
|
+
addNodeChild(group, mesh);
|
|
181
|
+
addNodeChild(scene, group);
|
|
182
|
+
group.localMatrix.m[12] = 1;
|
|
183
|
+
invalidateNodeLocalTransform(group);
|
|
184
|
+
|
|
185
|
+
const list = prepareSceneRender(state, scene, frontCamera(), emptyLights());
|
|
186
|
+
expect(list.meshCount).toBe(1);
|
|
187
|
+
expect(list.visibleMeshes[0]).toBe(mesh);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('reuses the same list per render state across calls', () => {
|
|
191
|
+
const state = createRenderState();
|
|
192
|
+
const scene = createScene();
|
|
193
|
+
const first = prepareSceneRender(state, scene, frontCamera(), emptyLights());
|
|
194
|
+
const second = prepareSceneRender(state, scene, frontCamera(), emptyLights());
|
|
195
|
+
expect(second).toBe(first);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('honors a positional material on a mesh', () => {
|
|
199
|
+
const state = createRenderState();
|
|
200
|
+
const scene = createScene();
|
|
201
|
+
const material = { kind: 'TestMaterial' } as unknown as Material;
|
|
202
|
+
const mesh = createMesh(boundedBox(), [material]);
|
|
203
|
+
addNodeChild(scene, mesh);
|
|
204
|
+
const list = prepareSceneRender(state, scene, frontCamera(), emptyLights());
|
|
205
|
+
expect(list.visibleMeshes[0].materials[0]).toBe(material);
|
|
206
|
+
});
|
|
207
|
+
});
|