@forgeax/engine-geometry 0.1.28 → 0.1.30
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/README.md +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +33 -37
- package/dist/index.mjs.map +1 -1
- package/dist/vertex-attribute-layout.d.ts +14 -0
- package/dist/vertex-attribute-layout.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/vertex-attribute-layout-owner.unit.test.ts +51 -0
- package/src/index.ts +3 -0
- package/src/vertex-attribute-layout.ts +56 -55
|
@@ -25,6 +25,10 @@ export interface GpuVertexBufferLayoutEntry {
|
|
|
25
25
|
}>;
|
|
26
26
|
readonly stepMode?: 'vertex' | 'instance' | undefined;
|
|
27
27
|
}
|
|
28
|
+
/** Canonical position/normal/uv/tangent map for ordinary interleaved meshes. */
|
|
29
|
+
export declare const DEFAULT_VERTEX_ATTRIBUTE_MAP: VertexAttributeMap;
|
|
30
|
+
/** Canonical six-attribute map for skinned interleaved meshes. */
|
|
31
|
+
export declare const SKIN_VERTEX_ATTRIBUTE_MAP: VertexAttributeMap;
|
|
28
32
|
/**
|
|
29
33
|
* Build a non-skin `VertexAttributeMap` carrying exactly `uvSetCount` UV sets
|
|
30
34
|
* (set 0 = `uv`, then `uv1..uv{uvSetCount-1}`). `deriveVertexBufferLayout`
|
|
@@ -61,6 +65,16 @@ export interface VertexLayoutProjection {
|
|
|
61
65
|
readonly arrayStride: number;
|
|
62
66
|
readonly digest: string;
|
|
63
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Derive a vertex count from the geometry-owned projection.
|
|
70
|
+
*
|
|
71
|
+
* Every upload, morph write, and draw range must use this same byte-level
|
|
72
|
+
* calculation. Returning `undefined` keeps malformed payloads out of GPU
|
|
73
|
+
* resource construction instead of silently truncating a fractional count.
|
|
74
|
+
*/
|
|
75
|
+
export declare function deriveVertexCount(vertices: {
|
|
76
|
+
readonly byteLength: number;
|
|
77
|
+
}, projection: VertexLayoutProjection): number | undefined;
|
|
64
78
|
/** Derive the one canonical immutable layout projection for a vertex map. */
|
|
65
79
|
export declare function deriveVertexLayoutProjection(map: VertexAttributeMap): VertexLayoutProjection;
|
|
66
80
|
export interface PackedVertexAttributes {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vertex-attribute-layout.d.ts","sourceRoot":"","sources":["../src/vertex-attribute-layout.ts"],"names":[],"mappings":"AA+BA,OAAO,EAEL,UAAU,
|
|
1
|
+
{"version":3,"file":"vertex-attribute-layout.d.ts","sourceRoot":"","sources":["../src/vertex-attribute-layout.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-geometry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@forgeax/engine-ecs": "0.1.
|
|
26
|
-
"@forgeax/engine-math": "0.1.
|
|
27
|
-
"@forgeax/engine-pack": "0.1.
|
|
28
|
-
"@forgeax/engine-types": "0.1.
|
|
25
|
+
"@forgeax/engine-ecs": "0.1.30",
|
|
26
|
+
"@forgeax/engine-math": "0.1.30",
|
|
27
|
+
"@forgeax/engine-pack": "0.1.30",
|
|
28
|
+
"@forgeax/engine-types": "0.1.30"
|
|
29
29
|
},
|
|
30
30
|
"forgeax": {
|
|
31
31
|
"metrics": {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildMeshAttributeMapForUvSets,
|
|
3
|
+
DEFAULT_VERTEX_ATTRIBUTE_MAP,
|
|
3
4
|
deriveVertexBufferLayout,
|
|
5
|
+
deriveVertexBufferLayoutFromProjection,
|
|
6
|
+
deriveVertexCount,
|
|
4
7
|
deriveVertexLayoutProjection,
|
|
5
8
|
deriveVertexLayoutProjectionFromMask,
|
|
6
9
|
packInterleavedVertexAttributes,
|
|
10
|
+
SKIN_VERTEX_ATTRIBUTE_MAP,
|
|
7
11
|
} from '@forgeax/engine-geometry';
|
|
8
12
|
import type { VertexAttributeMap } from '@forgeax/engine-types';
|
|
9
13
|
import { describe, expect, it } from 'vitest';
|
|
@@ -29,6 +33,34 @@ function makeCompleteAttributeMap(): VertexAttributeMap {
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
describe('vertex attribute layout owner', () => {
|
|
36
|
+
it('publishes the ordinary and skinned maps from the same geometry owner', () => {
|
|
37
|
+
expect(Object.keys(DEFAULT_VERTEX_ATTRIBUTE_MAP)).toEqual([
|
|
38
|
+
'position',
|
|
39
|
+
'normal',
|
|
40
|
+
'uv',
|
|
41
|
+
'tangent',
|
|
42
|
+
]);
|
|
43
|
+
expect(Object.keys(SKIN_VERTEX_ATTRIBUTE_MAP)).toEqual([
|
|
44
|
+
'position',
|
|
45
|
+
'normal',
|
|
46
|
+
'uv',
|
|
47
|
+
'tangent',
|
|
48
|
+
'skinIndex',
|
|
49
|
+
'skinWeight',
|
|
50
|
+
]);
|
|
51
|
+
expect(deriveVertexLayoutProjection(DEFAULT_VERTEX_ATTRIBUTE_MAP).arrayStride).toBe(48);
|
|
52
|
+
expect(deriveVertexLayoutProjection(SKIN_VERTEX_ATTRIBUTE_MAP).arrayStride).toBe(72);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('derives counts only when vertex bytes match the projected stride', () => {
|
|
56
|
+
const plain = deriveVertexLayoutProjection(DEFAULT_VERTEX_ATTRIBUTE_MAP);
|
|
57
|
+
const skinned = deriveVertexLayoutProjection(SKIN_VERTEX_ATTRIBUTE_MAP);
|
|
58
|
+
expect(deriveVertexCount(new Float32Array(24), plain)).toBe(2);
|
|
59
|
+
expect(deriveVertexCount(new Float32Array(18), skinned)).toBe(1);
|
|
60
|
+
expect(deriveVertexCount(new Float32Array(23), plain)).toBeUndefined();
|
|
61
|
+
expect(deriveVertexCount(new Float32Array(17), skinned)).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
32
64
|
it('derives the complete 13-key layout from the format owner', () => {
|
|
33
65
|
expect(deriveVertexBufferLayout(makeCompleteAttributeMap())).toEqual([
|
|
34
66
|
{
|
|
@@ -178,4 +210,23 @@ describe('vertex attribute layout owner', () => {
|
|
|
178
210
|
},
|
|
179
211
|
]);
|
|
180
212
|
});
|
|
213
|
+
|
|
214
|
+
it('aliases sparse UV holes without duplicating authored shader locations', () => {
|
|
215
|
+
const map = { position: makeBuffer(), uv: makeBuffer(), uv7: makeBuffer() };
|
|
216
|
+
const projection = deriveVertexLayoutProjection(map);
|
|
217
|
+
const fromMap = deriveVertexBufferLayout(map, { shaderUvSetCount: 8 });
|
|
218
|
+
const fromProjection = deriveVertexBufferLayoutFromProjection(projection, {
|
|
219
|
+
shaderUvSetCount: 8,
|
|
220
|
+
});
|
|
221
|
+
expect(fromMap).toEqual(fromProjection);
|
|
222
|
+
const layout = fromProjection[0];
|
|
223
|
+
expect(layout?.arrayStride).toBe(28);
|
|
224
|
+
expect(layout?.attributes).toHaveLength(9);
|
|
225
|
+
expect(new Set(layout?.attributes.map((entry) => entry.shaderLocation)).size).toBe(9);
|
|
226
|
+
expect(
|
|
227
|
+
layout?.attributes
|
|
228
|
+
.filter((entry) => entry.shaderLocation >= 6)
|
|
229
|
+
.every((entry) => entry.offset === 20),
|
|
230
|
+
).toBe(true);
|
|
231
|
+
});
|
|
181
232
|
});
|
package/src/index.ts
CHANGED
|
@@ -56,13 +56,16 @@ export {
|
|
|
56
56
|
export { createTorusGeometry } from './torus';
|
|
57
57
|
export {
|
|
58
58
|
buildMeshAttributeMapForUvSets,
|
|
59
|
+
DEFAULT_VERTEX_ATTRIBUTE_MAP,
|
|
59
60
|
deriveVertexBufferLayout,
|
|
60
61
|
deriveVertexBufferLayoutFromProjection,
|
|
62
|
+
deriveVertexCount,
|
|
61
63
|
deriveVertexLayoutProjection,
|
|
62
64
|
deriveVertexLayoutProjectionFromMask,
|
|
63
65
|
type GpuVertexBufferLayoutEntry,
|
|
64
66
|
type PackedVertexAttributes,
|
|
65
67
|
packInterleavedVertexAttributes,
|
|
68
|
+
SKIN_VERTEX_ATTRIBUTE_MAP,
|
|
66
69
|
VertexAttributePackError,
|
|
67
70
|
type VertexLayoutProjection,
|
|
68
71
|
type VertexLayoutProjectionAttribute,
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
import {
|
|
33
33
|
ASSET_ERROR_HINTS,
|
|
34
34
|
AssetError,
|
|
35
|
-
countUvSets,
|
|
36
35
|
err,
|
|
37
36
|
ok,
|
|
38
37
|
type Result,
|
|
@@ -95,6 +94,27 @@ const UV_KEYS: readonly AttributeKey[] = CANONICAL_KEYS.filter(
|
|
|
95
94
|
(key) => key === 'uv' || key.startsWith('uv'),
|
|
96
95
|
);
|
|
97
96
|
|
|
97
|
+
// Empty typed-array values are presence sentinels for layout derivation. Keep
|
|
98
|
+
// the common mesh maps here so renderers, recovery, and points/lines cannot
|
|
99
|
+
// grow a second copy of the canonical attribute vocabulary.
|
|
100
|
+
const EMPTY_FLOAT32 = new Float32Array(0);
|
|
101
|
+
const EMPTY_UINT16 = new Uint16Array(0);
|
|
102
|
+
|
|
103
|
+
/** Canonical position/normal/uv/tangent map for ordinary interleaved meshes. */
|
|
104
|
+
export const DEFAULT_VERTEX_ATTRIBUTE_MAP: VertexAttributeMap = Object.freeze({
|
|
105
|
+
position: EMPTY_FLOAT32,
|
|
106
|
+
normal: EMPTY_FLOAT32,
|
|
107
|
+
uv: EMPTY_FLOAT32,
|
|
108
|
+
tangent: EMPTY_FLOAT32,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** Canonical six-attribute map for skinned interleaved meshes. */
|
|
112
|
+
export const SKIN_VERTEX_ATTRIBUTE_MAP: VertexAttributeMap = Object.freeze({
|
|
113
|
+
...DEFAULT_VERTEX_ATTRIBUTE_MAP,
|
|
114
|
+
skinIndex: EMPTY_UINT16,
|
|
115
|
+
skinWeight: EMPTY_FLOAT32,
|
|
116
|
+
});
|
|
117
|
+
|
|
98
118
|
type Entry = {
|
|
99
119
|
readonly key: AttributeKey;
|
|
100
120
|
readonly shaderLocation: number;
|
|
@@ -102,16 +122,13 @@ type Entry = {
|
|
|
102
122
|
readonly format: string;
|
|
103
123
|
};
|
|
104
124
|
|
|
105
|
-
function emitAliasEntries(
|
|
106
|
-
entries
|
|
107
|
-
|
|
108
|
-
toIndex
|
|
109
|
-
aliasOffset: number,
|
|
110
|
-
currentStride: number,
|
|
111
|
-
): number {
|
|
112
|
-
for (let k = fromIndex; k < toIndex && k < UV_KEYS.length; k++) {
|
|
125
|
+
function emitAliasEntries(entries: Entry[], toIndex: number, currentStride: number): number {
|
|
126
|
+
const lastUv = entries.filter((entry) => UV_KEYS.includes(entry.key)).at(-1);
|
|
127
|
+
const aliasOffset = lastUv?.offset ?? currentStride;
|
|
128
|
+
for (let k = 0; k < toIndex && k < UV_KEYS.length; k++) {
|
|
113
129
|
// biome-ignore lint/style/noNonNullAssertion: bounded index on const array
|
|
114
130
|
const uvKey = UV_KEYS[k]!;
|
|
131
|
+
if (entries.some((entry) => entry.key === uvKey)) continue;
|
|
115
132
|
entries.push({
|
|
116
133
|
key: uvKey,
|
|
117
134
|
shaderLocation: CANONICAL_KEYS.indexOf(uvKey),
|
|
@@ -121,7 +138,7 @@ function emitAliasEntries(
|
|
|
121
138
|
}
|
|
122
139
|
// When meshUvSetCount===0, allocate 8 bytes for the zero UV area.
|
|
123
140
|
// Otherwise no stride increase (aliased to existing offset).
|
|
124
|
-
return
|
|
141
|
+
return lastUv === undefined ? currentStride + ATTRIBUTE_BYTE_STRIDE.uv : currentStride;
|
|
125
142
|
}
|
|
126
143
|
|
|
127
144
|
/**
|
|
@@ -138,12 +155,7 @@ function emitAliasEntries(
|
|
|
138
155
|
* off-screen). uvSetCount <= 1 yields the canonical 4-attribute single-UV map.
|
|
139
156
|
*/
|
|
140
157
|
export function buildMeshAttributeMapForUvSets(uvSetCount: number): VertexAttributeMap {
|
|
141
|
-
const map
|
|
142
|
-
position: new Float32Array(0),
|
|
143
|
-
normal: new Float32Array(0),
|
|
144
|
-
uv: new Float32Array(0),
|
|
145
|
-
tangent: new Float32Array(0),
|
|
146
|
-
};
|
|
158
|
+
const map = { ...DEFAULT_VERTEX_ATTRIBUTE_MAP } as unknown as Record<string, Float32Array>;
|
|
147
159
|
for (let set = 1; set < uvSetCount && set < UV_KEYS.length; set++) {
|
|
148
160
|
// biome-ignore lint/style/noNonNullAssertion: bounded index on const array
|
|
149
161
|
map[UV_KEYS[set]!] = new Float32Array(0);
|
|
@@ -176,21 +188,7 @@ export function deriveVertexBufferLayout(
|
|
|
176
188
|
|
|
177
189
|
// ── clamp-to-last alias (plan-strategy D-1) ──
|
|
178
190
|
if (shaderUvSetCount > 0) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (shaderUvSetCount > meshUvSetCount) {
|
|
182
|
-
const lastUvIndex = meshUvSetCount - 1;
|
|
183
|
-
const lastUvKey =
|
|
184
|
-
lastUvIndex >= 0 && lastUvIndex < UV_KEYS.length ? UV_KEYS[lastUvIndex] : undefined;
|
|
185
|
-
const aliasOffset = lastUvKey !== undefined ? CANONICAL_KEYS.indexOf(lastUvKey) : -1;
|
|
186
|
-
|
|
187
|
-
const aliasByteOffset =
|
|
188
|
-
aliasOffset >= 0
|
|
189
|
-
? (entries.find((e) => e.shaderLocation === aliasOffset)?.offset ?? 0)
|
|
190
|
-
: offset;
|
|
191
|
-
|
|
192
|
-
offset = emitAliasEntries(entries, meshUvSetCount, shaderUvSetCount, aliasByteOffset, offset);
|
|
193
|
-
}
|
|
191
|
+
offset = emitAliasEntries(entries, shaderUvSetCount, offset);
|
|
194
192
|
}
|
|
195
193
|
|
|
196
194
|
if (present === 0 && shaderUvSetCount === 0) return [];
|
|
@@ -224,30 +222,7 @@ export function deriveVertexBufferLayoutFromProjection(
|
|
|
224
222
|
let arrayStride = projection.arrayStride;
|
|
225
223
|
const shaderUvSetCount = opts?.shaderUvSetCount ?? 0;
|
|
226
224
|
if (shaderUvSetCount > 0) {
|
|
227
|
-
|
|
228
|
-
(entry) => entry.key === 'uv' || entry.key.startsWith('uv'),
|
|
229
|
-
).length;
|
|
230
|
-
if (shaderUvSetCount > meshUvSetCount) {
|
|
231
|
-
const lastUv = entries
|
|
232
|
-
.filter((entry) => entry.key === 'uv' || entry.key.startsWith('uv'))
|
|
233
|
-
.at(-1);
|
|
234
|
-
const aliasOffset = lastUv?.offset ?? arrayStride;
|
|
235
|
-
for (
|
|
236
|
-
let index = meshUvSetCount;
|
|
237
|
-
index < shaderUvSetCount && index < UV_KEYS.length;
|
|
238
|
-
index += 1
|
|
239
|
-
) {
|
|
240
|
-
const key = UV_KEYS[index];
|
|
241
|
-
if (key === undefined) continue;
|
|
242
|
-
entries.push({
|
|
243
|
-
key,
|
|
244
|
-
shaderLocation: CANONICAL_KEYS.indexOf(key),
|
|
245
|
-
offset: aliasOffset,
|
|
246
|
-
format: ATTRIBUTE_FORMAT_MAP[key],
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
if (meshUvSetCount === 0) arrayStride += ATTRIBUTE_BYTE_STRIDE.uv;
|
|
250
|
-
}
|
|
225
|
+
arrayStride = emitAliasEntries(entries, shaderUvSetCount, arrayStride);
|
|
251
226
|
}
|
|
252
227
|
entries.sort((a, b) => a.shaderLocation - b.shaderLocation);
|
|
253
228
|
return projection.attributes.length === 0
|
|
@@ -281,6 +256,32 @@ export interface VertexLayoutProjection {
|
|
|
281
256
|
readonly digest: string;
|
|
282
257
|
}
|
|
283
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Derive a vertex count from the geometry-owned projection.
|
|
261
|
+
*
|
|
262
|
+
* Every upload, morph write, and draw range must use this same byte-level
|
|
263
|
+
* calculation. Returning `undefined` keeps malformed payloads out of GPU
|
|
264
|
+
* resource construction instead of silently truncating a fractional count.
|
|
265
|
+
*/
|
|
266
|
+
export function deriveVertexCount(
|
|
267
|
+
vertices: { readonly byteLength: number },
|
|
268
|
+
projection: VertexLayoutProjection,
|
|
269
|
+
): number | undefined {
|
|
270
|
+
const stride = projection.arrayStride;
|
|
271
|
+
const byteLength = vertices.byteLength;
|
|
272
|
+
if (
|
|
273
|
+
!Number.isSafeInteger(stride) ||
|
|
274
|
+
stride <= 0 ||
|
|
275
|
+
!Number.isSafeInteger(byteLength) ||
|
|
276
|
+
byteLength < 0 ||
|
|
277
|
+
byteLength % stride !== 0
|
|
278
|
+
) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
const count = byteLength / stride;
|
|
282
|
+
return Number.isSafeInteger(count) ? count : undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
284
285
|
function bytesForFormat(format: string): number {
|
|
285
286
|
if (format === 'uint16x4' || format === 'float32x2') return 8;
|
|
286
287
|
if (format === 'float32x3') return 12;
|