@aethervtt/shard-mesh 0.0.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/codec.d.ts +4 -0
- package/dist/codec.js +100 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +24 -0
- package/dist/mesh.d.ts +120 -0
- package/dist/mesh.js +202 -0
- package/dist/primitives.d.ts +48 -0
- package/dist/primitives.js +241 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Dias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/codec.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Mesh, type MeshData } from './mesh.js';
|
|
2
|
+
export declare function encodeMesh(data: MeshData | Mesh): Uint8Array;
|
|
3
|
+
/** Decodes a mesh artifact. Arrays are views over `bytes` (copied only if misaligned). */
|
|
4
|
+
export declare function decodeMesh(bytes: Uint8Array): Mesh;
|
package/dist/codec.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import {
|
|
3
|
+
MESH_ATTRIBUTE_WIDTH,
|
|
4
|
+
MESH_ATTRIBUTES,
|
|
5
|
+
Mesh
|
|
6
|
+
} from "./mesh.js";
|
|
7
|
+
const MAGIC = 1397573715;
|
|
8
|
+
const HEADER = 32;
|
|
9
|
+
const align4 = (n) => n + 3 & ~3;
|
|
10
|
+
function encodeMesh(data) {
|
|
11
|
+
const d = data instanceof Mesh ? data.data() : data;
|
|
12
|
+
const vertices = d.positions.length / 3;
|
|
13
|
+
const arrays = [d.positions];
|
|
14
|
+
let mask = 0;
|
|
15
|
+
MESH_ATTRIBUTES.forEach((a, i) => {
|
|
16
|
+
const arr = d[a];
|
|
17
|
+
if (arr) {
|
|
18
|
+
mask |= 1 << i;
|
|
19
|
+
arrays.push(arr);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const indexType = !d.indices ? 0 : d.indices instanceof Uint16Array ? 16 : 32;
|
|
23
|
+
if (d.indices) arrays.push(d.indices);
|
|
24
|
+
const targets = d.targets ?? [];
|
|
25
|
+
const morphMask = (targets.some((t) => t.normals) ? 1 : 0) | (targets.some((t) => t.tangents) ? 2 : 0);
|
|
26
|
+
for (const t of targets) {
|
|
27
|
+
arrays.push(t.positions);
|
|
28
|
+
if (morphMask & 1) arrays.push(t.normals ?? new Float32Array(vertices * 3));
|
|
29
|
+
if (morphMask & 2) arrays.push(t.tangents ?? new Float32Array(vertices * 3));
|
|
30
|
+
}
|
|
31
|
+
let size = HEADER;
|
|
32
|
+
for (const a of arrays) size += align4(a.byteLength);
|
|
33
|
+
const out = new Uint8Array(size);
|
|
34
|
+
const view = new DataView(out.buffer);
|
|
35
|
+
view.setUint32(0, MAGIC, true);
|
|
36
|
+
view.setUint32(4, targets.length ? 2 : 1, true);
|
|
37
|
+
view.setUint32(8, vertices, true);
|
|
38
|
+
view.setUint32(12, d.indices?.length ?? 0, true);
|
|
39
|
+
view.setUint32(16, indexType, true);
|
|
40
|
+
view.setUint32(20, mask, true);
|
|
41
|
+
view.setUint32(24, targets.length, true);
|
|
42
|
+
view.setUint32(28, morphMask, true);
|
|
43
|
+
let offset = HEADER;
|
|
44
|
+
for (const a of arrays) {
|
|
45
|
+
out.set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), offset);
|
|
46
|
+
offset += align4(a.byteLength);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function decodeMesh(bytes) {
|
|
51
|
+
if (bytes.byteOffset % 4 !== 0) bytes = bytes.slice();
|
|
52
|
+
if (bytes.byteLength < HEADER) throw invalid("is shorter than its header");
|
|
53
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
54
|
+
if (view.getUint32(0, true) !== MAGIC) throw invalid("has the wrong magic number");
|
|
55
|
+
const version = view.getUint32(4, true);
|
|
56
|
+
if (version !== 1 && version !== 2)
|
|
57
|
+
throw invalid(`has version ${version}; this engine reads 1 and 2`);
|
|
58
|
+
const vertices = view.getUint32(8, true);
|
|
59
|
+
const indexCount = view.getUint32(12, true);
|
|
60
|
+
const indexType = view.getUint32(16, true);
|
|
61
|
+
const mask = view.getUint32(20, true);
|
|
62
|
+
let offset = bytes.byteOffset + HEADER;
|
|
63
|
+
const end = bytes.byteOffset + bytes.byteLength;
|
|
64
|
+
const take = (ctor, n) => {
|
|
65
|
+
const byteLength = n * ctor.BYTES_PER_ELEMENT;
|
|
66
|
+
if (offset + byteLength > end) throw invalid("is truncated");
|
|
67
|
+
const arr = new ctor(bytes.buffer, offset, n);
|
|
68
|
+
offset += align4(byteLength);
|
|
69
|
+
return arr;
|
|
70
|
+
};
|
|
71
|
+
const data = { positions: take(Float32Array, vertices * 3) };
|
|
72
|
+
MESH_ATTRIBUTES.forEach((a, i) => {
|
|
73
|
+
if (!(mask & 1 << i)) return;
|
|
74
|
+
const n = vertices * MESH_ATTRIBUTE_WIDTH[a];
|
|
75
|
+
data[a] = a === "joints" ? take(Uint16Array, n) : take(Float32Array, n);
|
|
76
|
+
});
|
|
77
|
+
if (indexType === 16) data.indices = take(Uint16Array, indexCount);
|
|
78
|
+
else if (indexType === 32) data.indices = take(Uint32Array, indexCount);
|
|
79
|
+
if (version === 2) {
|
|
80
|
+
const count = view.getUint32(24, true);
|
|
81
|
+
const morphMask = view.getUint32(28, true);
|
|
82
|
+
data.targets = [];
|
|
83
|
+
for (let k = 0; k < count; k++) {
|
|
84
|
+
const target = { positions: take(Float32Array, vertices * 3) };
|
|
85
|
+
if (morphMask & 1) target.normals = take(Float32Array, vertices * 3);
|
|
86
|
+
if (morphMask & 2) target.tangents = take(Float32Array, vertices * 3);
|
|
87
|
+
data.targets.push(target);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return Mesh.trusted(data);
|
|
91
|
+
}
|
|
92
|
+
function invalid(what) {
|
|
93
|
+
return new ShardError("mesh/invalid-artifact", `Mesh artifact ${what}`, {
|
|
94
|
+
hint: "Re-import the source (`shard import --force`)."
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
decodeMesh,
|
|
99
|
+
encodeMesh
|
|
100
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { decodeMesh, encodeMesh } from './codec.js';
|
|
2
|
+
export { type GpuMeshDescriptor, MAX_MORPH_TARGETS, MESH_ATTRIBUTE_WIDTH, MESH_ATTRIBUTES, Mesh, type MeshAttribute, type MeshData, type MorphTarget, } from './mesh.js';
|
|
3
|
+
export { box, capsule, cone, cube, cylinder, plane, sphere, torus } from './primitives.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { decodeMesh, encodeMesh } from "./codec.js";
|
|
2
|
+
import {
|
|
3
|
+
MAX_MORPH_TARGETS,
|
|
4
|
+
MESH_ATTRIBUTE_WIDTH,
|
|
5
|
+
MESH_ATTRIBUTES,
|
|
6
|
+
Mesh
|
|
7
|
+
} from "./mesh.js";
|
|
8
|
+
import { box, capsule, cone, cube, cylinder, plane, sphere, torus } from "./primitives.js";
|
|
9
|
+
export {
|
|
10
|
+
MAX_MORPH_TARGETS,
|
|
11
|
+
MESH_ATTRIBUTES,
|
|
12
|
+
MESH_ATTRIBUTE_WIDTH,
|
|
13
|
+
Mesh,
|
|
14
|
+
box,
|
|
15
|
+
capsule,
|
|
16
|
+
cone,
|
|
17
|
+
cube,
|
|
18
|
+
cylinder,
|
|
19
|
+
decodeMesh,
|
|
20
|
+
encodeMesh,
|
|
21
|
+
plane,
|
|
22
|
+
sphere,
|
|
23
|
+
torus
|
|
24
|
+
};
|
package/dist/mesh.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export interface MeshData {
|
|
2
|
+
/** xyz per vertex. */
|
|
3
|
+
positions: Float32Array;
|
|
4
|
+
/** xyz per vertex, unit length. */
|
|
5
|
+
normals?: Float32Array;
|
|
6
|
+
/** uv per vertex. */
|
|
7
|
+
uvs?: Float32Array;
|
|
8
|
+
/** rgba per vertex, linear. */
|
|
9
|
+
colors?: Float32Array;
|
|
10
|
+
/** xyzw per vertex: tangent direction plus handedness in w (glTF / MikkTSpace). */
|
|
11
|
+
tangents?: Float32Array;
|
|
12
|
+
/** Second uv set, per vertex. */
|
|
13
|
+
uvs1?: Float32Array;
|
|
14
|
+
/** Four joint indices per vertex (skinning). */
|
|
15
|
+
joints?: Uint16Array;
|
|
16
|
+
/** Four joint weights per vertex, summing to 1 (skinning). */
|
|
17
|
+
weights?: Float32Array;
|
|
18
|
+
indices?: Uint16Array | Uint32Array;
|
|
19
|
+
/** Morph targets (blend shapes): per-vertex deltas added by `render/MorphWeights`. */
|
|
20
|
+
targets?: MorphTarget[];
|
|
21
|
+
}
|
|
22
|
+
/** One morph target: deltas from the base mesh, xyz per vertex. */
|
|
23
|
+
export interface MorphTarget {
|
|
24
|
+
positions: Float32Array;
|
|
25
|
+
normals?: Float32Array;
|
|
26
|
+
tangents?: Float32Array;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A mesh the GPU writes (compute shaders fill its vertex buffers): sizes and bounds, no CPU data.
|
|
30
|
+
* Every mesh passing the same `indices` array shares one GPU index buffer.
|
|
31
|
+
*/
|
|
32
|
+
export interface GpuMeshDescriptor {
|
|
33
|
+
vertexCount: number;
|
|
34
|
+
/** Triangles, shared: the renderer uploads each array once. */
|
|
35
|
+
indices: Uint16Array | Uint32Array;
|
|
36
|
+
/** Indices drawn (default: all of them), e.g. a prefix without skirt triangles. */
|
|
37
|
+
indexCount?: number;
|
|
38
|
+
/** Local-space bounds `[minX, minY, minZ, maxX, maxY, maxZ]` (update them when known). */
|
|
39
|
+
bounds: ArrayLike<number>;
|
|
40
|
+
/**
|
|
41
|
+
* Draw another GPU mesh's vertex buffers with these indices (a subset of its triangles) instead
|
|
42
|
+
* of owning buffers.
|
|
43
|
+
*/
|
|
44
|
+
share?: Mesh;
|
|
45
|
+
/**
|
|
46
|
+
* Where this mesh's vertices start in `share`'s buffers (default: where `share`'s do), so many
|
|
47
|
+
* meshes can live in one set of buffers and draw without rebinding them.
|
|
48
|
+
*/
|
|
49
|
+
baseVertex?: number;
|
|
50
|
+
}
|
|
51
|
+
/** Morph targets a mesh can blend at once (the vertex stage adds the heaviest ones). */
|
|
52
|
+
export declare const MAX_MORPH_TARGETS = 8;
|
|
53
|
+
export declare const MESH_ATTRIBUTES: readonly ['normals', 'uvs', 'colors', 'tangents', 'uvs1', 'joints', 'weights'];
|
|
54
|
+
export type MeshAttribute = (typeof MESH_ATTRIBUTES)[number];
|
|
55
|
+
export declare const MESH_ATTRIBUTE_WIDTH: Record<MeshAttribute, number>;
|
|
56
|
+
/**
|
|
57
|
+
* Geometry on the CPU: separate attribute arrays plus optional indices. The renderer uploads it
|
|
58
|
+
* once per version; replace a mesh's data with `update` and the version bumps.
|
|
59
|
+
*/
|
|
60
|
+
export declare class Mesh {
|
|
61
|
+
positions: Float32Array;
|
|
62
|
+
normals: Float32Array | undefined;
|
|
63
|
+
uvs: Float32Array | undefined;
|
|
64
|
+
colors: Float32Array | undefined;
|
|
65
|
+
tangents: Float32Array | undefined;
|
|
66
|
+
uvs1: Float32Array | undefined;
|
|
67
|
+
joints: Uint16Array | undefined;
|
|
68
|
+
weights: Float32Array | undefined;
|
|
69
|
+
indices: Uint16Array | Uint32Array | undefined;
|
|
70
|
+
targets: MorphTarget[] | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Local-space bounds: `[minX, minY, minZ, maxX, maxY, maxZ]`. With morph targets, they cover
|
|
73
|
+
* every blend of weights in [0, 1].
|
|
74
|
+
*/
|
|
75
|
+
readonly bounds: Float32Array<ArrayBufferLike>;
|
|
76
|
+
/** Increments on `update`, so GPU copies know to re-upload. */
|
|
77
|
+
version: number;
|
|
78
|
+
/**
|
|
79
|
+
* Set for meshes the GPU writes (`Mesh.gpu`): their vertex buffers are storage buffers with no
|
|
80
|
+
* CPU copy; `positions` is empty.
|
|
81
|
+
*/
|
|
82
|
+
gpu: Readonly<{
|
|
83
|
+
vertexCount: number;
|
|
84
|
+
indexCount: number;
|
|
85
|
+
indices: Uint16Array | Uint32Array;
|
|
86
|
+
share: Mesh | undefined;
|
|
87
|
+
baseVertex: number;
|
|
88
|
+
}> | undefined;
|
|
89
|
+
private constructor();
|
|
90
|
+
static create(data: MeshData): Mesh;
|
|
91
|
+
/**
|
|
92
|
+
* A mesh whose vertex buffers the GPU fills: positions, normals, uvs, uvs1, and tangents as
|
|
93
|
+
* separate storage + vertex buffers (f32, 3/3/2/2/4 per vertex), written by a compute pass and
|
|
94
|
+
* drawn like any mesh. Terrain chunks (0043) and GPU foliage (0045) use it.
|
|
95
|
+
*/
|
|
96
|
+
static gpu(desc: GpuMeshDescriptor): Mesh;
|
|
97
|
+
/**
|
|
98
|
+
* Draws a GPU mesh (`Mesh.gpu`) with other triangles over the same vertices: another shared
|
|
99
|
+
* index array (and count). Cheap: the vertex buffers stay, and its batch stays the same.
|
|
100
|
+
*/
|
|
101
|
+
setIndices(indices: Uint16Array | Uint32Array, indexCount?: number): void;
|
|
102
|
+
/**
|
|
103
|
+
* A mesh from data already validated (artifacts the importer checked): attribute lengths are
|
|
104
|
+
* still checked, but the per-index range scan is skipped, which dominates load time for big meshes.
|
|
105
|
+
*/
|
|
106
|
+
static trusted(data: MeshData): Mesh;
|
|
107
|
+
private static skipIndexCheck;
|
|
108
|
+
get vertexCount(): number;
|
|
109
|
+
/** Whether draws are indexed: CPU meshes with indices, and every GPU mesh. */
|
|
110
|
+
get indexed(): boolean;
|
|
111
|
+
/** Where the mesh's vertices start in its vertex buffers (non-zero only for shared GPU meshes). */
|
|
112
|
+
get baseVertex(): number;
|
|
113
|
+
/** Vertices drawn: the index count, or the vertex count for non-indexed meshes. */
|
|
114
|
+
get drawCount(): number;
|
|
115
|
+
update(data: MeshData): void;
|
|
116
|
+
private assign;
|
|
117
|
+
/** The mesh's data as plain arrays (shared, not copied). */
|
|
118
|
+
data(): MeshData;
|
|
119
|
+
private validate;
|
|
120
|
+
}
|
package/dist/mesh.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { aabb, ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
const MAX_MORPH_TARGETS = 8;
|
|
3
|
+
const MESH_ATTRIBUTES = [
|
|
4
|
+
"normals",
|
|
5
|
+
"uvs",
|
|
6
|
+
"colors",
|
|
7
|
+
"tangents",
|
|
8
|
+
"uvs1",
|
|
9
|
+
"joints",
|
|
10
|
+
"weights"
|
|
11
|
+
];
|
|
12
|
+
const MESH_ATTRIBUTE_WIDTH = {
|
|
13
|
+
normals: 3,
|
|
14
|
+
uvs: 2,
|
|
15
|
+
colors: 4,
|
|
16
|
+
tangents: 4,
|
|
17
|
+
uvs1: 2,
|
|
18
|
+
joints: 4,
|
|
19
|
+
weights: 4
|
|
20
|
+
};
|
|
21
|
+
class Mesh {
|
|
22
|
+
positions;
|
|
23
|
+
normals;
|
|
24
|
+
uvs;
|
|
25
|
+
colors;
|
|
26
|
+
tangents;
|
|
27
|
+
uvs1;
|
|
28
|
+
joints;
|
|
29
|
+
weights;
|
|
30
|
+
indices;
|
|
31
|
+
targets;
|
|
32
|
+
/**
|
|
33
|
+
* Local-space bounds: `[minX, minY, minZ, maxX, maxY, maxZ]`. With morph targets, they cover
|
|
34
|
+
* every blend of weights in [0, 1].
|
|
35
|
+
*/
|
|
36
|
+
bounds = aabb.create();
|
|
37
|
+
/** Increments on `update`, so GPU copies know to re-upload. */
|
|
38
|
+
version = 0;
|
|
39
|
+
/**
|
|
40
|
+
* Set for meshes the GPU writes (`Mesh.gpu`): their vertex buffers are storage buffers with no
|
|
41
|
+
* CPU copy; `positions` is empty.
|
|
42
|
+
*/
|
|
43
|
+
gpu;
|
|
44
|
+
constructor(data) {
|
|
45
|
+
if (data) this.assign(data);
|
|
46
|
+
}
|
|
47
|
+
static create(data) {
|
|
48
|
+
return new Mesh(data);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A mesh whose vertex buffers the GPU fills: positions, normals, uvs, uvs1, and tangents as
|
|
52
|
+
* separate storage + vertex buffers (f32, 3/3/2/2/4 per vertex), written by a compute pass and
|
|
53
|
+
* drawn like any mesh. Terrain chunks (0043) and GPU foliage (0045) use it.
|
|
54
|
+
*/
|
|
55
|
+
static gpu(desc) {
|
|
56
|
+
const mesh = new Mesh(void 0);
|
|
57
|
+
mesh.positions = new Float32Array(0);
|
|
58
|
+
mesh.indices = void 0;
|
|
59
|
+
mesh.gpu = {
|
|
60
|
+
vertexCount: desc.vertexCount,
|
|
61
|
+
indexCount: desc.indexCount ?? desc.indices.length,
|
|
62
|
+
indices: desc.indices,
|
|
63
|
+
share: desc.share,
|
|
64
|
+
baseVertex: desc.baseVertex ?? desc.share?.baseVertex ?? 0
|
|
65
|
+
};
|
|
66
|
+
for (let i = 0; i < 6; i++) mesh.bounds[i] = desc.bounds[i];
|
|
67
|
+
return mesh;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Draws a GPU mesh (`Mesh.gpu`) with other triangles over the same vertices: another shared
|
|
71
|
+
* index array (and count). Cheap: the vertex buffers stay, and its batch stays the same.
|
|
72
|
+
*/
|
|
73
|
+
setIndices(indices, indexCount = indices.length) {
|
|
74
|
+
const g = this.gpu;
|
|
75
|
+
if (!g) {
|
|
76
|
+
throw new ShardError("mesh/not-gpu", "setIndices is for GPU meshes (Mesh.gpu)", {
|
|
77
|
+
hint: "Change a CPU mesh with update()."
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (g.indices === indices && g.indexCount === indexCount) return;
|
|
81
|
+
this.gpu = { ...g, indices, indexCount };
|
|
82
|
+
this.version++;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A mesh from data already validated (artifacts the importer checked): attribute lengths are
|
|
86
|
+
* still checked, but the per-index range scan is skipped, which dominates load time for big meshes.
|
|
87
|
+
*/
|
|
88
|
+
static trusted(data) {
|
|
89
|
+
Mesh.skipIndexCheck = true;
|
|
90
|
+
try {
|
|
91
|
+
return new Mesh(data);
|
|
92
|
+
} finally {
|
|
93
|
+
Mesh.skipIndexCheck = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
static skipIndexCheck = false;
|
|
97
|
+
get vertexCount() {
|
|
98
|
+
return this.gpu ? this.gpu.vertexCount : this.positions.length / 3;
|
|
99
|
+
}
|
|
100
|
+
/** Whether draws are indexed: CPU meshes with indices, and every GPU mesh. */
|
|
101
|
+
get indexed() {
|
|
102
|
+
return this.gpu !== void 0 || this.indices !== void 0;
|
|
103
|
+
}
|
|
104
|
+
/** Where the mesh's vertices start in its vertex buffers (non-zero only for shared GPU meshes). */
|
|
105
|
+
get baseVertex() {
|
|
106
|
+
return this.gpu ? this.gpu.baseVertex : 0;
|
|
107
|
+
}
|
|
108
|
+
/** Vertices drawn: the index count, or the vertex count for non-indexed meshes. */
|
|
109
|
+
get drawCount() {
|
|
110
|
+
if (this.gpu) return this.gpu.indexCount;
|
|
111
|
+
return this.indices ? this.indices.length : this.vertexCount;
|
|
112
|
+
}
|
|
113
|
+
update(data) {
|
|
114
|
+
this.gpu = void 0;
|
|
115
|
+
this.assign(data);
|
|
116
|
+
this.version++;
|
|
117
|
+
}
|
|
118
|
+
assign(data) {
|
|
119
|
+
this.positions = data.positions;
|
|
120
|
+
this.normals = data.normals;
|
|
121
|
+
this.uvs = data.uvs;
|
|
122
|
+
this.colors = data.colors;
|
|
123
|
+
this.tangents = data.tangents;
|
|
124
|
+
this.uvs1 = data.uvs1;
|
|
125
|
+
this.joints = data.joints;
|
|
126
|
+
this.weights = data.weights;
|
|
127
|
+
this.indices = data.indices;
|
|
128
|
+
this.targets = data.targets?.length ? data.targets : void 0;
|
|
129
|
+
this.validate();
|
|
130
|
+
}
|
|
131
|
+
/** The mesh's data as plain arrays (shared, not copied). */
|
|
132
|
+
data() {
|
|
133
|
+
const out = { positions: this.positions };
|
|
134
|
+
for (const a of MESH_ATTRIBUTES)
|
|
135
|
+
if (this[a]) out[a] = this[a];
|
|
136
|
+
if (this.indices) out.indices = this.indices;
|
|
137
|
+
if (this.targets) out.targets = this.targets;
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
validate() {
|
|
141
|
+
const n = this.positions.length / 3;
|
|
142
|
+
const bad = (what) => new ShardError("mesh/invalid", `Mesh ${what}`, {
|
|
143
|
+
hint: "positions/normals: 3 per vertex, uvs/uvs1: 2, colors/tangents/joints/weights: 4; indices must be < vertex count."
|
|
144
|
+
});
|
|
145
|
+
if (!Number.isInteger(n) || n === 0)
|
|
146
|
+
throw bad(`has ${this.positions.length} position floats (need a multiple of 3, > 0)`);
|
|
147
|
+
for (const a of MESH_ATTRIBUTES) {
|
|
148
|
+
const arr = this[a];
|
|
149
|
+
if (arr && arr.length !== n * MESH_ATTRIBUTE_WIDTH[a])
|
|
150
|
+
throw bad(`has ${arr.length / MESH_ATTRIBUTE_WIDTH[a]} ${a} for ${n} vertices`);
|
|
151
|
+
}
|
|
152
|
+
if (this.indices) {
|
|
153
|
+
if (this.indices.length % 3 !== 0)
|
|
154
|
+
throw bad(`has ${this.indices.length} indices (need triangles)`);
|
|
155
|
+
for (let i = 0; !Mesh.skipIndexCheck && i < this.indices.length; i++) {
|
|
156
|
+
if (this.indices[i] >= n)
|
|
157
|
+
throw bad(`index ${this.indices[i]} is out of range (${n} vertices)`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const [k, target] of (this.targets ?? []).entries()) {
|
|
161
|
+
for (const a of ["positions", "normals", "tangents"]) {
|
|
162
|
+
const arr = target[a];
|
|
163
|
+
if (arr && arr.length !== n * 3)
|
|
164
|
+
throw bad(`morph target ${k} has ${arr.length / 3} ${a} for ${n} vertices`);
|
|
165
|
+
}
|
|
166
|
+
if (!target.positions) throw bad(`morph target ${k} has no positions`);
|
|
167
|
+
}
|
|
168
|
+
aabb.fromPoints(this.bounds, this.positions);
|
|
169
|
+
if (this.targets) {
|
|
170
|
+
const b = this.bounds;
|
|
171
|
+
for (const target of this.targets) {
|
|
172
|
+
const d = target.positions;
|
|
173
|
+
let lx = 0;
|
|
174
|
+
let ly = 0;
|
|
175
|
+
let lz = 0;
|
|
176
|
+
let hx = 0;
|
|
177
|
+
let hy = 0;
|
|
178
|
+
let hz = 0;
|
|
179
|
+
for (let i = 0; i < d.length; i += 3) {
|
|
180
|
+
lx = Math.min(lx, d[i]);
|
|
181
|
+
ly = Math.min(ly, d[i + 1]);
|
|
182
|
+
lz = Math.min(lz, d[i + 2]);
|
|
183
|
+
hx = Math.max(hx, d[i]);
|
|
184
|
+
hy = Math.max(hy, d[i + 1]);
|
|
185
|
+
hz = Math.max(hz, d[i + 2]);
|
|
186
|
+
}
|
|
187
|
+
b[0] = b[0] + lx;
|
|
188
|
+
b[1] = b[1] + ly;
|
|
189
|
+
b[2] = b[2] + lz;
|
|
190
|
+
b[3] = b[3] + hx;
|
|
191
|
+
b[4] = b[4] + hy;
|
|
192
|
+
b[5] = b[5] + hz;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export {
|
|
198
|
+
MAX_MORPH_TARGETS,
|
|
199
|
+
MESH_ATTRIBUTES,
|
|
200
|
+
MESH_ATTRIBUTE_WIDTH,
|
|
201
|
+
Mesh
|
|
202
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Mesh } from './mesh.js';
|
|
2
|
+
/** Axis-aligned box with the given full extents. */
|
|
3
|
+
export declare function box(options?: {
|
|
4
|
+
x?: number;
|
|
5
|
+
y?: number;
|
|
6
|
+
z?: number;
|
|
7
|
+
}): Mesh;
|
|
8
|
+
export declare function cube(options?: {
|
|
9
|
+
size?: number;
|
|
10
|
+
}): Mesh;
|
|
11
|
+
/** UV sphere. The seam column is duplicated so UVs wrap cleanly; normals match across it. */
|
|
12
|
+
export declare function sphere(options?: {
|
|
13
|
+
radius?: number;
|
|
14
|
+
segments?: number;
|
|
15
|
+
rings?: number;
|
|
16
|
+
}): Mesh;
|
|
17
|
+
/** Flat square in the XZ plane, facing +Y, optionally subdivided (terrain, water, floors). */
|
|
18
|
+
export declare function plane(options?: {
|
|
19
|
+
size?: number;
|
|
20
|
+
subdivisions?: number;
|
|
21
|
+
}): Mesh;
|
|
22
|
+
/** Cylinder (or cone, with `radiusTop: 0`) along Y, with caps. */
|
|
23
|
+
export declare function cylinder(options?: {
|
|
24
|
+
radius?: number;
|
|
25
|
+
radiusTop?: number;
|
|
26
|
+
height?: number;
|
|
27
|
+
segments?: number;
|
|
28
|
+
caps?: boolean;
|
|
29
|
+
}): Mesh;
|
|
30
|
+
export declare function cone(options?: {
|
|
31
|
+
radius?: number;
|
|
32
|
+
height?: number;
|
|
33
|
+
segments?: number;
|
|
34
|
+
}): Mesh;
|
|
35
|
+
/** Capsule along Y: `height` is the full height including the hemispherical ends. */
|
|
36
|
+
export declare function capsule(options?: {
|
|
37
|
+
radius?: number;
|
|
38
|
+
height?: number;
|
|
39
|
+
segments?: number;
|
|
40
|
+
rings?: number;
|
|
41
|
+
}): Mesh;
|
|
42
|
+
/** Torus in the XZ plane. */
|
|
43
|
+
export declare function torus(options?: {
|
|
44
|
+
radius?: number;
|
|
45
|
+
tube?: number;
|
|
46
|
+
radialSegments?: number;
|
|
47
|
+
tubularSegments?: number;
|
|
48
|
+
}): Mesh;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { Mesh } from "./mesh.js";
|
|
2
|
+
class Builder {
|
|
3
|
+
positions = [];
|
|
4
|
+
normals = [];
|
|
5
|
+
uvs = [];
|
|
6
|
+
indices = [];
|
|
7
|
+
vertex(p, n, uv) {
|
|
8
|
+
this.positions.push(p[0], p[1], p[2]);
|
|
9
|
+
this.normals.push(n[0], n[1], n[2]);
|
|
10
|
+
this.uvs.push(uv[0], uv[1]);
|
|
11
|
+
return this.positions.length / 3 - 1;
|
|
12
|
+
}
|
|
13
|
+
triangle(a, b, c) {
|
|
14
|
+
this.indices.push(a, b, c);
|
|
15
|
+
}
|
|
16
|
+
quad(a, b, c, d) {
|
|
17
|
+
this.indices.push(a, b, c, a, c, d);
|
|
18
|
+
}
|
|
19
|
+
build() {
|
|
20
|
+
const count = this.positions.length / 3;
|
|
21
|
+
return Mesh.create({
|
|
22
|
+
positions: new Float32Array(this.positions),
|
|
23
|
+
normals: new Float32Array(this.normals),
|
|
24
|
+
uvs: new Float32Array(this.uvs),
|
|
25
|
+
indices: count > 65535 ? new Uint32Array(this.indices) : new Uint16Array(this.indices)
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function box(options = {}) {
|
|
30
|
+
const hx = (options.x ?? 1) / 2;
|
|
31
|
+
const hy = (options.y ?? 1) / 2;
|
|
32
|
+
const hz = (options.z ?? 1) / 2;
|
|
33
|
+
const b = new Builder();
|
|
34
|
+
const faces = [
|
|
35
|
+
[
|
|
36
|
+
[1, 0, 0],
|
|
37
|
+
[0, 0, -1],
|
|
38
|
+
[0, 1, 0]
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
[-1, 0, 0],
|
|
42
|
+
[0, 0, 1],
|
|
43
|
+
[0, 1, 0]
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
[0, 1, 0],
|
|
47
|
+
[1, 0, 0],
|
|
48
|
+
[0, 0, -1]
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
[0, -1, 0],
|
|
52
|
+
[1, 0, 0],
|
|
53
|
+
[0, 0, 1]
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
[0, 0, 1],
|
|
57
|
+
[1, 0, 0],
|
|
58
|
+
[0, 1, 0]
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
[0, 0, -1],
|
|
62
|
+
[-1, 0, 0],
|
|
63
|
+
[0, 1, 0]
|
|
64
|
+
]
|
|
65
|
+
];
|
|
66
|
+
const h = [hx, hy, hz];
|
|
67
|
+
for (const [n, u, v] of faces) {
|
|
68
|
+
const corner = (su, sv) => [0, 1, 2].map((k) => (n[k] + u[k] * su + v[k] * sv) * h[k]);
|
|
69
|
+
const a = b.vertex(corner(-1, -1), n, [0, 1]);
|
|
70
|
+
const c = b.vertex(corner(1, -1), n, [1, 1]);
|
|
71
|
+
const d = b.vertex(corner(1, 1), n, [1, 0]);
|
|
72
|
+
const e = b.vertex(corner(-1, 1), n, [0, 0]);
|
|
73
|
+
b.quad(a, c, d, e);
|
|
74
|
+
}
|
|
75
|
+
return b.build();
|
|
76
|
+
}
|
|
77
|
+
function cube(options = {}) {
|
|
78
|
+
const s = options.size ?? 1;
|
|
79
|
+
return box({ x: s, y: s, z: s });
|
|
80
|
+
}
|
|
81
|
+
function sphere(options = {}) {
|
|
82
|
+
const radius = options.radius ?? 0.5;
|
|
83
|
+
const segments = Math.max(3, options.segments ?? 32);
|
|
84
|
+
const rings = Math.max(2, options.rings ?? Math.ceil(segments / 2));
|
|
85
|
+
const b = new Builder();
|
|
86
|
+
for (let r = 0; r <= rings; r++) {
|
|
87
|
+
const v = r / rings;
|
|
88
|
+
const phi = v * Math.PI;
|
|
89
|
+
for (let s = 0; s <= segments; s++) {
|
|
90
|
+
const u = s / segments;
|
|
91
|
+
const theta = u * Math.PI * 2;
|
|
92
|
+
const n = [Math.sin(phi) * Math.sin(theta), Math.cos(phi), Math.sin(phi) * Math.cos(theta)];
|
|
93
|
+
b.vertex([n[0] * radius, n[1] * radius, n[2] * radius], n, [u, v]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const row = segments + 1;
|
|
97
|
+
for (let r = 0; r < rings; r++) {
|
|
98
|
+
for (let s = 0; s < segments; s++) {
|
|
99
|
+
const a = r * row + s;
|
|
100
|
+
const c = a + row;
|
|
101
|
+
if (r !== 0) b.triangle(a, c, a + 1);
|
|
102
|
+
if (r !== rings - 1) b.triangle(a + 1, c, c + 1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return b.build();
|
|
106
|
+
}
|
|
107
|
+
function plane(options = {}) {
|
|
108
|
+
const size = options.size ?? 1;
|
|
109
|
+
const n = Math.max(1, options.subdivisions ?? 1);
|
|
110
|
+
const b = new Builder();
|
|
111
|
+
for (let z = 0; z <= n; z++) {
|
|
112
|
+
for (let x = 0; x <= n; x++) {
|
|
113
|
+
b.vertex([(x / n - 0.5) * size, 0, (z / n - 0.5) * size], [0, 1, 0], [x / n, z / n]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (let z = 0; z < n; z++) {
|
|
117
|
+
for (let x = 0; x < n; x++) {
|
|
118
|
+
const a = z * (n + 1) + x;
|
|
119
|
+
b.quad(a, a + n + 1, a + n + 2, a + 1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return b.build();
|
|
123
|
+
}
|
|
124
|
+
function cylinder(options = {}) {
|
|
125
|
+
const r0 = options.radius ?? 0.5;
|
|
126
|
+
const r1 = options.radiusTop ?? r0;
|
|
127
|
+
const h = options.height ?? 1;
|
|
128
|
+
const segments = Math.max(3, options.segments ?? 32);
|
|
129
|
+
const b = new Builder();
|
|
130
|
+
const slope = (r0 - r1) / h;
|
|
131
|
+
const ny = slope / Math.sqrt(1 + slope * slope);
|
|
132
|
+
const nr = 1 / Math.sqrt(1 + slope * slope);
|
|
133
|
+
for (let s = 0; s <= segments; s++) {
|
|
134
|
+
const u = s / segments;
|
|
135
|
+
const t = u * Math.PI * 2;
|
|
136
|
+
const sin = Math.sin(t);
|
|
137
|
+
const cos = Math.cos(t);
|
|
138
|
+
const n = [sin * nr, ny, cos * nr];
|
|
139
|
+
b.vertex([sin * r0, -h / 2, cos * r0], n, [u, 1]);
|
|
140
|
+
b.vertex([sin * r1, h / 2, cos * r1], n, [u, 0]);
|
|
141
|
+
}
|
|
142
|
+
for (let s = 0; s < segments; s++) {
|
|
143
|
+
const a = s * 2;
|
|
144
|
+
if (r1 === 0) b.triangle(a, a + 2, a + 1);
|
|
145
|
+
else b.quad(a, a + 2, a + 3, a + 1);
|
|
146
|
+
}
|
|
147
|
+
if (options.caps ?? true) {
|
|
148
|
+
for (const [y, sign, radius] of [
|
|
149
|
+
[h / 2, 1, r1],
|
|
150
|
+
[-h / 2, -1, r0]
|
|
151
|
+
]) {
|
|
152
|
+
if (radius === 0) continue;
|
|
153
|
+
const center = b.vertex([0, y, 0], [0, sign, 0], [0.5, 0.5]);
|
|
154
|
+
const first = b.positions.length / 3;
|
|
155
|
+
for (let s = 0; s <= segments; s++) {
|
|
156
|
+
const t = s / segments * Math.PI * 2;
|
|
157
|
+
b.vertex(
|
|
158
|
+
[Math.sin(t) * radius, y, Math.cos(t) * radius],
|
|
159
|
+
[0, sign, 0],
|
|
160
|
+
[0.5 + Math.sin(t) / 2, 0.5 - Math.cos(t) / 2]
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
for (let s = 0; s < segments; s++) {
|
|
164
|
+
if (sign > 0) b.triangle(center, first + s, first + s + 1);
|
|
165
|
+
else b.triangle(center, first + s + 1, first + s);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return b.build();
|
|
170
|
+
}
|
|
171
|
+
function cone(options = {}) {
|
|
172
|
+
return cylinder({ ...options, radiusTop: 0 });
|
|
173
|
+
}
|
|
174
|
+
function capsule(options = {}) {
|
|
175
|
+
const radius = options.radius ?? 0.5;
|
|
176
|
+
const height = Math.max(options.height ?? 2, radius * 2);
|
|
177
|
+
const segments = Math.max(3, options.segments ?? 32);
|
|
178
|
+
const ringsPerCap = Math.max(2, options.rings ?? 8);
|
|
179
|
+
const half = height / 2 - radius;
|
|
180
|
+
const b = new Builder();
|
|
181
|
+
const rows = [];
|
|
182
|
+
for (let r = 0; r <= ringsPerCap; r++) rows.push([r / ringsPerCap * (Math.PI / 2), half]);
|
|
183
|
+
for (let r = 0; r <= ringsPerCap; r++)
|
|
184
|
+
rows.push([Math.PI / 2 + r / ringsPerCap * (Math.PI / 2), -half]);
|
|
185
|
+
rows.forEach(([phi, offset], r) => {
|
|
186
|
+
for (let s = 0; s <= segments; s++) {
|
|
187
|
+
const u = s / segments;
|
|
188
|
+
const theta = u * Math.PI * 2;
|
|
189
|
+
const n = [Math.sin(phi) * Math.sin(theta), Math.cos(phi), Math.sin(phi) * Math.cos(theta)];
|
|
190
|
+
b.vertex([n[0] * radius, n[1] * radius + offset, n[2] * radius], n, [
|
|
191
|
+
u,
|
|
192
|
+
r / (rows.length - 1)
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
const row = segments + 1;
|
|
197
|
+
for (let r = 0; r < rows.length - 1; r++) {
|
|
198
|
+
for (let s = 0; s < segments; s++) {
|
|
199
|
+
const a = r * row + s;
|
|
200
|
+
const c = a + row;
|
|
201
|
+
if (r !== rows.length - 2) b.triangle(a, c, c + 1);
|
|
202
|
+
if (r !== 0) b.triangle(a, c + 1, a + 1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return b.build();
|
|
206
|
+
}
|
|
207
|
+
function torus(options = {}) {
|
|
208
|
+
const R = options.radius ?? 0.5;
|
|
209
|
+
const r = options.tube ?? 0.2;
|
|
210
|
+
const radial = Math.max(3, options.radialSegments ?? 16);
|
|
211
|
+
const tubular = Math.max(3, options.tubularSegments ?? 48);
|
|
212
|
+
const b = new Builder();
|
|
213
|
+
for (let j = 0; j <= radial; j++) {
|
|
214
|
+
const v = j / radial * Math.PI * 2;
|
|
215
|
+
for (let i = 0; i <= tubular; i++) {
|
|
216
|
+
const u = i / tubular * Math.PI * 2;
|
|
217
|
+
const cx = Math.sin(u) * R;
|
|
218
|
+
const cz = Math.cos(u) * R;
|
|
219
|
+
const n = [Math.sin(u) * Math.cos(v), Math.sin(v), Math.cos(u) * Math.cos(v)];
|
|
220
|
+
b.vertex([cx + n[0] * r, n[1] * r, cz + n[2] * r], n, [i / tubular, j / radial]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const row = tubular + 1;
|
|
224
|
+
for (let j = 0; j < radial; j++) {
|
|
225
|
+
for (let i = 0; i < tubular; i++) {
|
|
226
|
+
const a = j * row + i;
|
|
227
|
+
b.quad(a, a + 1, a + row + 1, a + row);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return b.build();
|
|
231
|
+
}
|
|
232
|
+
export {
|
|
233
|
+
box,
|
|
234
|
+
capsule,
|
|
235
|
+
cone,
|
|
236
|
+
cube,
|
|
237
|
+
cylinder,
|
|
238
|
+
plane,
|
|
239
|
+
sphere,
|
|
240
|
+
torus
|
|
241
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aethervtt/shard-mesh",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@aethervtt/shard-core": "0.0.1"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/aethervtt/shard.git",
|
|
24
|
+
"directory": "packages/mesh"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"provenance": true
|
|
29
|
+
},
|
|
30
|
+
"shard": {
|
|
31
|
+
"buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
|
|
32
|
+
}
|
|
33
|
+
}
|