@aethervtt/shard-gltf 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/accessors.d.ts +20 -0
- package/dist/accessors.js +137 -0
- package/dist/document.d.ts +157 -0
- package/dist/document.js +49 -0
- package/dist/importer.d.ts +12 -0
- package/dist/importer.js +965 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +20 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.js +11 -0
- package/package.json +40 -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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GltfDocument } from './document.js';
|
|
2
|
+
export interface AccessorData {
|
|
3
|
+
values: Float64Array;
|
|
4
|
+
count: number;
|
|
5
|
+
components: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Decodes accessors into flat arrays, handling strides, normalization, and sparse storage. `buffer`
|
|
9
|
+
* returns a buffer's bytes (resolved by the importer: GLB chunk, data URI, or external file).
|
|
10
|
+
*/
|
|
11
|
+
export declare class Accessors {
|
|
12
|
+
private readonly doc;
|
|
13
|
+
private readonly buffers;
|
|
14
|
+
private readonly cache;
|
|
15
|
+
constructor(doc: GltfDocument, buffers: Uint8Array[]);
|
|
16
|
+
/** Accessor `index` as doubles (exact for every glTF component type). */
|
|
17
|
+
read(index: number): AccessorData;
|
|
18
|
+
floats(index: number): Float32Array;
|
|
19
|
+
private viewOf;
|
|
20
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
const COMPONENTS = {
|
|
3
|
+
SCALAR: 1,
|
|
4
|
+
VEC2: 2,
|
|
5
|
+
VEC3: 3,
|
|
6
|
+
VEC4: 4,
|
|
7
|
+
MAT2: 4,
|
|
8
|
+
MAT3: 9,
|
|
9
|
+
MAT4: 16
|
|
10
|
+
};
|
|
11
|
+
const SIZES = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 };
|
|
12
|
+
function reader(view, type, normalized) {
|
|
13
|
+
switch (type) {
|
|
14
|
+
case 5120:
|
|
15
|
+
return normalized ? (o) => Math.max(view.getInt8(o) / 127, -1) : (o) => view.getInt8(o);
|
|
16
|
+
case 5121:
|
|
17
|
+
return normalized ? (o) => view.getUint8(o) / 255 : (o) => view.getUint8(o);
|
|
18
|
+
case 5122:
|
|
19
|
+
return normalized ? (o) => Math.max(view.getInt16(o, true) / 32767, -1) : (o) => view.getInt16(o, true);
|
|
20
|
+
case 5123:
|
|
21
|
+
return normalized ? (o) => view.getUint16(o, true) / 65535 : (o) => view.getUint16(o, true);
|
|
22
|
+
case 5125:
|
|
23
|
+
return (o) => view.getUint32(o, true);
|
|
24
|
+
case 5126:
|
|
25
|
+
return (o) => view.getFloat32(o, true);
|
|
26
|
+
default:
|
|
27
|
+
throw new ShardError("gltf/invalid", `Unknown component type ${type}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
class Accessors {
|
|
31
|
+
doc;
|
|
32
|
+
buffers;
|
|
33
|
+
cache = /* @__PURE__ */ new Map();
|
|
34
|
+
constructor(doc, buffers) {
|
|
35
|
+
this.doc = doc;
|
|
36
|
+
this.buffers = buffers;
|
|
37
|
+
}
|
|
38
|
+
/** Accessor `index` as doubles (exact for every glTF component type). */
|
|
39
|
+
read(index) {
|
|
40
|
+
const cached = this.cache.get(index);
|
|
41
|
+
if (cached) return cached;
|
|
42
|
+
const acc = this.doc.accessors?.[index];
|
|
43
|
+
const ptr = `/accessors/${index}`;
|
|
44
|
+
if (!acc) throw new ShardError("gltf/invalid", `No accessor ${index}`, { path: ptr });
|
|
45
|
+
const components = COMPONENTS[acc.type];
|
|
46
|
+
const size = SIZES[acc.componentType];
|
|
47
|
+
if (!components || !size) {
|
|
48
|
+
throw new ShardError("gltf/invalid", `Accessor ${index} has an unknown type`, { path: ptr });
|
|
49
|
+
}
|
|
50
|
+
const values = new Float64Array(acc.count * components);
|
|
51
|
+
if (acc.bufferView !== void 0) {
|
|
52
|
+
const { view, stride, base } = this.viewOf(
|
|
53
|
+
acc.bufferView,
|
|
54
|
+
acc.byteOffset ?? 0,
|
|
55
|
+
components * size,
|
|
56
|
+
ptr
|
|
57
|
+
);
|
|
58
|
+
const needed = base + (acc.count - 1) * stride + components * size;
|
|
59
|
+
if (acc.count > 0 && needed > view.byteLength) {
|
|
60
|
+
throw new ShardError(
|
|
61
|
+
"gltf/accessor-out-of-range",
|
|
62
|
+
`Accessor ${index} reads past the end of buffer view ${acc.bufferView}`,
|
|
63
|
+
{ path: ptr, hint: "The file is truncated or its byteOffset/count are wrong." }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const read = reader(view, acc.componentType, acc.normalized ?? false);
|
|
67
|
+
for (let i = 0; i < acc.count; i++) {
|
|
68
|
+
const o = base + i * stride;
|
|
69
|
+
for (let c = 0; c < components; c++) values[i * components + c] = read(o + c * size);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (acc.sparse) {
|
|
73
|
+
const s = acc.sparse;
|
|
74
|
+
const idx = this.viewOf(
|
|
75
|
+
s.indices.bufferView,
|
|
76
|
+
s.indices.byteOffset ?? 0,
|
|
77
|
+
SIZES[s.indices.componentType],
|
|
78
|
+
`${ptr}/sparse/indices`
|
|
79
|
+
);
|
|
80
|
+
const val = this.viewOf(
|
|
81
|
+
s.values.bufferView,
|
|
82
|
+
s.values.byteOffset ?? 0,
|
|
83
|
+
components * size,
|
|
84
|
+
`${ptr}/sparse/values`
|
|
85
|
+
);
|
|
86
|
+
const readIndex = reader(idx.view, s.indices.componentType, false);
|
|
87
|
+
const readValue = reader(val.view, acc.componentType, acc.normalized ?? false);
|
|
88
|
+
const indexSize = SIZES[s.indices.componentType];
|
|
89
|
+
for (let i = 0; i < s.count; i++) {
|
|
90
|
+
const target = readIndex(idx.base + i * indexSize);
|
|
91
|
+
if (target >= acc.count) {
|
|
92
|
+
throw new ShardError(
|
|
93
|
+
"gltf/accessor-out-of-range",
|
|
94
|
+
`Sparse index ${target} is past accessor ${index}'s count`,
|
|
95
|
+
{
|
|
96
|
+
path: `${ptr}/sparse`
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
for (let c = 0; c < components; c++) {
|
|
101
|
+
values[target * components + c] = readValue(val.base + (i * components + c) * size);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const data = { values, count: acc.count, components };
|
|
106
|
+
this.cache.set(index, data);
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
floats(index) {
|
|
110
|
+
return Float32Array.from(this.read(index).values);
|
|
111
|
+
}
|
|
112
|
+
viewOf(viewIndex, byteOffset, elementSize, ptr) {
|
|
113
|
+
const bv = this.doc.bufferViews?.[viewIndex];
|
|
114
|
+
if (!bv) {
|
|
115
|
+
throw new ShardError("gltf/invalid", `No buffer view ${viewIndex}`, { path: ptr });
|
|
116
|
+
}
|
|
117
|
+
const buffer = this.buffers[bv.buffer];
|
|
118
|
+
if (!buffer) {
|
|
119
|
+
throw new ShardError("gltf/buffer-missing", `Buffer ${bv.buffer} couldn't be loaded`, {
|
|
120
|
+
path: `/buffers/${bv.buffer}`
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const start = bv.byteOffset ?? 0;
|
|
124
|
+
if (start + bv.byteLength > buffer.byteLength) {
|
|
125
|
+
throw new ShardError(
|
|
126
|
+
"gltf/accessor-out-of-range",
|
|
127
|
+
`Buffer view ${viewIndex} is past the end of buffer ${bv.buffer}`,
|
|
128
|
+
{ path: `/bufferViews/${viewIndex}` }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset + start, bv.byteLength);
|
|
132
|
+
return { view, stride: bv.byteStride ?? elementSize, base: byteOffset };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
Accessors
|
|
137
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/** The parts of the glTF 2.0 JSON the importer reads. */
|
|
2
|
+
export interface GltfDocument {
|
|
3
|
+
asset: {
|
|
4
|
+
version: string;
|
|
5
|
+
};
|
|
6
|
+
scene?: number;
|
|
7
|
+
scenes?: {
|
|
8
|
+
name?: string;
|
|
9
|
+
nodes?: number[];
|
|
10
|
+
}[];
|
|
11
|
+
nodes?: GltfNode[];
|
|
12
|
+
meshes?: {
|
|
13
|
+
name?: string;
|
|
14
|
+
primitives: GltfPrimitive[];
|
|
15
|
+
weights?: number[];
|
|
16
|
+
}[];
|
|
17
|
+
materials?: GltfMaterial[];
|
|
18
|
+
accessors?: GltfAccessor[];
|
|
19
|
+
bufferViews?: {
|
|
20
|
+
buffer: number;
|
|
21
|
+
byteOffset?: number;
|
|
22
|
+
byteLength: number;
|
|
23
|
+
byteStride?: number;
|
|
24
|
+
}[];
|
|
25
|
+
buffers?: {
|
|
26
|
+
uri?: string;
|
|
27
|
+
byteLength: number;
|
|
28
|
+
}[];
|
|
29
|
+
cameras?: {
|
|
30
|
+
type: 'perspective' | 'orthographic';
|
|
31
|
+
perspective?: {
|
|
32
|
+
yfov: number;
|
|
33
|
+
znear: number;
|
|
34
|
+
zfar?: number;
|
|
35
|
+
};
|
|
36
|
+
orthographic?: {
|
|
37
|
+
xmag: number;
|
|
38
|
+
ymag: number;
|
|
39
|
+
znear: number;
|
|
40
|
+
zfar: number;
|
|
41
|
+
};
|
|
42
|
+
}[];
|
|
43
|
+
skins?: {
|
|
44
|
+
name?: string;
|
|
45
|
+
joints: number[];
|
|
46
|
+
inverseBindMatrices?: number;
|
|
47
|
+
skeleton?: number;
|
|
48
|
+
}[];
|
|
49
|
+
animations?: {
|
|
50
|
+
name?: string;
|
|
51
|
+
channels: {
|
|
52
|
+
sampler: number;
|
|
53
|
+
target: {
|
|
54
|
+
node?: number;
|
|
55
|
+
path: string;
|
|
56
|
+
};
|
|
57
|
+
}[];
|
|
58
|
+
samplers: {
|
|
59
|
+
input: number;
|
|
60
|
+
output: number;
|
|
61
|
+
interpolation?: string;
|
|
62
|
+
}[];
|
|
63
|
+
}[];
|
|
64
|
+
textures?: {
|
|
65
|
+
source?: number;
|
|
66
|
+
sampler?: number;
|
|
67
|
+
extensions?: Record<string, unknown>;
|
|
68
|
+
}[];
|
|
69
|
+
images?: {
|
|
70
|
+
uri?: string;
|
|
71
|
+
bufferView?: number;
|
|
72
|
+
mimeType?: string;
|
|
73
|
+
name?: string;
|
|
74
|
+
}[];
|
|
75
|
+
samplers?: {
|
|
76
|
+
magFilter?: number;
|
|
77
|
+
minFilter?: number;
|
|
78
|
+
wrapS?: number;
|
|
79
|
+
wrapT?: number;
|
|
80
|
+
}[];
|
|
81
|
+
extensionsUsed?: string[];
|
|
82
|
+
extensionsRequired?: string[];
|
|
83
|
+
extensions?: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
export interface GltfNode {
|
|
86
|
+
name?: string;
|
|
87
|
+
children?: number[];
|
|
88
|
+
mesh?: number;
|
|
89
|
+
camera?: number;
|
|
90
|
+
skin?: number;
|
|
91
|
+
matrix?: number[];
|
|
92
|
+
translation?: number[];
|
|
93
|
+
rotation?: number[];
|
|
94
|
+
scale?: number[];
|
|
95
|
+
weights?: number[];
|
|
96
|
+
extensions?: Record<string, unknown>;
|
|
97
|
+
extras?: unknown;
|
|
98
|
+
}
|
|
99
|
+
export interface GltfPrimitive {
|
|
100
|
+
attributes: Record<string, number>;
|
|
101
|
+
indices?: number;
|
|
102
|
+
material?: number;
|
|
103
|
+
mode?: number;
|
|
104
|
+
targets?: Record<string, number>[];
|
|
105
|
+
}
|
|
106
|
+
export interface TextureInfo {
|
|
107
|
+
index: number;
|
|
108
|
+
texCoord?: number;
|
|
109
|
+
scale?: number;
|
|
110
|
+
strength?: number;
|
|
111
|
+
extensions?: Record<string, unknown>;
|
|
112
|
+
}
|
|
113
|
+
export interface GltfMaterial {
|
|
114
|
+
name?: string;
|
|
115
|
+
pbrMetallicRoughness?: {
|
|
116
|
+
baseColorFactor?: number[];
|
|
117
|
+
metallicFactor?: number;
|
|
118
|
+
roughnessFactor?: number;
|
|
119
|
+
baseColorTexture?: TextureInfo;
|
|
120
|
+
metallicRoughnessTexture?: TextureInfo;
|
|
121
|
+
};
|
|
122
|
+
normalTexture?: TextureInfo;
|
|
123
|
+
occlusionTexture?: TextureInfo;
|
|
124
|
+
emissiveTexture?: TextureInfo;
|
|
125
|
+
emissiveFactor?: number[];
|
|
126
|
+
alphaMode?: 'OPAQUE' | 'MASK' | 'BLEND';
|
|
127
|
+
alphaCutoff?: number;
|
|
128
|
+
doubleSided?: boolean;
|
|
129
|
+
extensions?: Record<string, unknown>;
|
|
130
|
+
}
|
|
131
|
+
export interface GltfAccessor {
|
|
132
|
+
bufferView?: number;
|
|
133
|
+
byteOffset?: number;
|
|
134
|
+
componentType: number;
|
|
135
|
+
normalized?: boolean;
|
|
136
|
+
count: number;
|
|
137
|
+
type: 'SCALAR' | 'VEC2' | 'VEC3' | 'VEC4' | 'MAT2' | 'MAT3' | 'MAT4';
|
|
138
|
+
sparse?: {
|
|
139
|
+
count: number;
|
|
140
|
+
indices: {
|
|
141
|
+
bufferView: number;
|
|
142
|
+
byteOffset?: number;
|
|
143
|
+
componentType: number;
|
|
144
|
+
};
|
|
145
|
+
values: {
|
|
146
|
+
bufferView: number;
|
|
147
|
+
byteOffset?: number;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export interface ParsedGltf {
|
|
152
|
+
json: GltfDocument;
|
|
153
|
+
/** The GLB binary chunk, if the file is a .glb. */
|
|
154
|
+
bin: Uint8Array | undefined;
|
|
155
|
+
}
|
|
156
|
+
/** Reads a .glb container or .gltf JSON. */
|
|
157
|
+
export declare function parseGltf(bytes: Uint8Array): ParsedGltf;
|
package/dist/document.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
const GLB_MAGIC = 1179937895;
|
|
3
|
+
const CHUNK_JSON = 1313821514;
|
|
4
|
+
const CHUNK_BIN = 5130562;
|
|
5
|
+
function parseGltf(bytes) {
|
|
6
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
7
|
+
let json;
|
|
8
|
+
let bin;
|
|
9
|
+
if (bytes.byteLength >= 12 && view.getUint32(0, true) === GLB_MAGIC) {
|
|
10
|
+
const version = view.getUint32(4, true);
|
|
11
|
+
if (version !== 2) {
|
|
12
|
+
throw new ShardError("gltf/invalid", `GLB version ${version} isn't supported (need 2)`, {
|
|
13
|
+
path: ""
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const length = Math.min(view.getUint32(8, true), bytes.byteLength);
|
|
17
|
+
let offset = 12;
|
|
18
|
+
while (offset + 8 <= length) {
|
|
19
|
+
const chunkLength = view.getUint32(offset, true);
|
|
20
|
+
const type = view.getUint32(offset + 4, true);
|
|
21
|
+
const data = bytes.subarray(offset + 8, offset + 8 + chunkLength);
|
|
22
|
+
if (type === CHUNK_JSON) json = JSON.parse(new TextDecoder().decode(data));
|
|
23
|
+
else if (type === CHUNK_BIN && !bin) bin = data;
|
|
24
|
+
offset += 8 + chunkLength;
|
|
25
|
+
}
|
|
26
|
+
if (json === void 0)
|
|
27
|
+
throw new ShardError("gltf/invalid", "GLB has no JSON chunk", { path: "" });
|
|
28
|
+
} else {
|
|
29
|
+
try {
|
|
30
|
+
json = JSON.parse(new TextDecoder().decode(bytes));
|
|
31
|
+
} catch (cause) {
|
|
32
|
+
throw new ShardError("gltf/invalid", "Not a .glb and not valid glTF JSON", {
|
|
33
|
+
path: "",
|
|
34
|
+
cause
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const doc = json;
|
|
39
|
+
if (!doc?.asset || typeof doc.asset.version !== "string" || !doc.asset.version.startsWith("2")) {
|
|
40
|
+
throw new ShardError("gltf/invalid", 'Missing asset.version "2.x"', {
|
|
41
|
+
path: "/asset/version",
|
|
42
|
+
hint: "Only glTF 2.0 files are supported."
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return { json: doc, bin };
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
parseGltf
|
|
49
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type JsonValue } from '@aethervtt/shard-core';
|
|
2
|
+
export declare const GltfImportSettings: import("@aethervtt/shard-core").ComponentDef<{
|
|
3
|
+
readonly scale: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
4
|
+
readonly forward: import("@aethervtt/shard-core").FieldType<"+z" | "-z", "u8">;
|
|
5
|
+
readonly emissiveLuminance: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
6
|
+
readonly cameras: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
|
|
7
|
+
readonly lights: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
|
|
8
|
+
readonly generateNormals: import("@aethervtt/shard-core").FieldType<"always" | "missing" | "never", "u8">;
|
|
9
|
+
readonly lods: import("@aethervtt/shard-core").FieldType<"auto" | "none", "u8">;
|
|
10
|
+
readonly materialTypes: import("@aethervtt/shard-core").FieldType<JsonValue, "object">;
|
|
11
|
+
}>;
|
|
12
|
+
export declare const GltfImporter: import("@aethervtt/shard-assets").ImporterDef;
|