@domgell/gltf-parser 1.0.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/.gitattributes +2 -0
- package/gltf-importer.iml +9 -0
- package/index.html +14 -0
- package/package.json +18 -0
- package/res/suzanne.glb +0 -0
- package/src/Parser.ts +330 -0
- package/src/index.ts +2 -0
- package/src/types.ts +244 -0
- package/src/util.ts +67 -0
- package/tsconfig.json +36 -0
package/.gitattributes
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="GENERAL_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
4
|
+
<exclude-output />
|
|
5
|
+
<content url="file://$MODULE_DIR$" />
|
|
6
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
7
|
+
<orderEntry type="module" module-name="gltf-types" />
|
|
8
|
+
</component>
|
|
9
|
+
</module>
|
package/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<!-- Get rid of "http://localhost:5173/favicon.ico not found" -->
|
|
5
|
+
<link rel="shortcut icon" href="#" />
|
|
6
|
+
|
|
7
|
+
<meta charset="UTF-8"/>
|
|
8
|
+
<script type="module" src="src/index.ts"></script>
|
|
9
|
+
<title></title>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<canvas></canvas>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@domgell/gltf-parser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"exports": "./src/index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": ["gltf", "glb", "mesh", "parser"],
|
|
10
|
+
"author": "domgell",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"description": "Parse .GLTF/.GLF files into JS objects",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"gltf-types": "file:../gltf-types",
|
|
15
|
+
"dom-game-math": "*"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {}
|
|
18
|
+
}
|
package/res/suzanne.glb
ADDED
|
Binary file
|
package/src/Parser.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccessorArrayType,
|
|
3
|
+
AccessorConstructorType,
|
|
4
|
+
assert,
|
|
5
|
+
chunk,
|
|
6
|
+
fail,
|
|
7
|
+
getTransform,
|
|
8
|
+
} from "./util.ts";
|
|
9
|
+
import * as Parsed from "./types.ts";
|
|
10
|
+
import * as GLTF from "gltf-types";
|
|
11
|
+
import {mat4, TransformOrder} from "dom-game-math";
|
|
12
|
+
import {Matrix4x4, TextureFilterMap, TextureWrapModeMap} from "./types.ts";
|
|
13
|
+
|
|
14
|
+
export type ParserOptions = {
|
|
15
|
+
transformOrder: TransformOrder,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Parser {
|
|
19
|
+
private constructor(
|
|
20
|
+
readonly path: string,
|
|
21
|
+
readonly options: ParserOptions,
|
|
22
|
+
readonly header: GLTF.GLTF,
|
|
23
|
+
readonly binary: ArrayBuffer,
|
|
24
|
+
) {
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static async create(path: string, options: Partial<ParserOptions> = {}) {
|
|
28
|
+
const r = await fetch(path);
|
|
29
|
+
assert(r.ok, `Failed to import at path: '${path}'`);
|
|
30
|
+
|
|
31
|
+
const data = await r.arrayBuffer();
|
|
32
|
+
const dataView = new DataView(data);
|
|
33
|
+
|
|
34
|
+
let offset = 12;
|
|
35
|
+
|
|
36
|
+
// Get JSON Header
|
|
37
|
+
const jsonChunkLength = dataView.getUint32(offset, true);
|
|
38
|
+
const jsonChunkData = new Uint8Array(data, offset + 8, jsonChunkLength);
|
|
39
|
+
const textDecoder = new TextDecoder("utf-8");
|
|
40
|
+
const json = textDecoder.decode(jsonChunkData);
|
|
41
|
+
const header = JSON.parse(json);
|
|
42
|
+
|
|
43
|
+
offset += 8 + jsonChunkLength;
|
|
44
|
+
|
|
45
|
+
// Get binary data
|
|
46
|
+
const binaryChunkLength = dataView.getUint32(offset, true);
|
|
47
|
+
const binary = new ArrayBuffer(binaryChunkLength);
|
|
48
|
+
const binaryChunkView = new Uint8Array(binary);
|
|
49
|
+
binaryChunkView.set(new Uint8Array(data, offset + 8, binaryChunkLength));
|
|
50
|
+
|
|
51
|
+
return new Parser(path, {transformOrder: options.transformOrder ?? "TRS"}, header, binary);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ----------------------------------- Accessor ------------------------------------
|
|
55
|
+
|
|
56
|
+
accessor<T extends AccessorArrayType, I extends number | undefined>(index: I, type: T): I extends number ? AccessorConstructorType<T> : AccessorConstructorType<T> | undefined {
|
|
57
|
+
if (index === undefined) return undefined as any;
|
|
58
|
+
|
|
59
|
+
assert(this.header.accessors !== undefined, "Accessors are undefined");
|
|
60
|
+
assert(this.header.bufferViews !== undefined, "BufferViews are undefined");
|
|
61
|
+
|
|
62
|
+
const accessor = this.header.accessors[index]
|
|
63
|
+
?? fail(`Accessor at index ${index} not found`);
|
|
64
|
+
|
|
65
|
+
assert(accessor.bufferView !== undefined, "Accessor bufferView is undefined");
|
|
66
|
+
|
|
67
|
+
const bufferView = this.header.bufferViews[accessor.bufferView];
|
|
68
|
+
const byteOffset = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
|
|
69
|
+
|
|
70
|
+
const numComponents = {
|
|
71
|
+
"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT2": 4, "MAT3": 9, "MAT4": 16,
|
|
72
|
+
}[accessor.type];
|
|
73
|
+
|
|
74
|
+
const assertType = {
|
|
75
|
+
5120: Int8Array,
|
|
76
|
+
5121: Uint8Array,
|
|
77
|
+
5122: Int16Array,
|
|
78
|
+
5123: Uint16Array,
|
|
79
|
+
5125: Uint32Array,
|
|
80
|
+
5126: Float32Array,
|
|
81
|
+
}[accessor.componentType];
|
|
82
|
+
assert(assertType.name === type.name, `Mismatched types: requested '${type.name}' but got '${assertType.name}'`);
|
|
83
|
+
|
|
84
|
+
const byteLength = numComponents * accessor.count * type.BYTES_PER_ELEMENT;
|
|
85
|
+
return new type(this.binary.slice(byteOffset, byteOffset + byteLength)) as any;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ------------------------------------- Mesh --------------------------------------
|
|
89
|
+
|
|
90
|
+
async mesh(gltfMesh: GLTF.Mesh) {
|
|
91
|
+
const primitives = new Array<Parsed.MeshPrimitive>(gltfMesh.primitives.length);
|
|
92
|
+
for (let i = 0; i < primitives.length; i++) {
|
|
93
|
+
primitives[i] = await this.primitive(gltfMesh.primitives[i]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {primitives, name: gltfMesh.name};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async primitive(gltfPrimitive: GLTF.MeshPrimitive): Promise<Parsed.MeshPrimitive> {
|
|
100
|
+
const positions = this.accessor(gltfPrimitive.attributes.POSITION, Float32Array);
|
|
101
|
+
const uvs = this.accessor(gltfPrimitive.attributes.TEXCOORD_0, Float32Array);
|
|
102
|
+
const normals = this.accessor(gltfPrimitive.attributes.NORMAL, Float32Array);
|
|
103
|
+
const colors = this.accessor(gltfPrimitive.attributes.COLOR_0, Float32Array); // TODO: Convert to RGBA
|
|
104
|
+
const weights = this.accessor(gltfPrimitive.attributes.WEIGHTS_0, Float32Array); // TODO: Max 4
|
|
105
|
+
const joints = this.accessor(gltfPrimitive.attributes.JOINTS_0, Uint8Array); // TODO: Max 4
|
|
106
|
+
|
|
107
|
+
const attributes: Parsed.MeshPrimitiveAttributes = {
|
|
108
|
+
positions: chunk(positions, 3),
|
|
109
|
+
uvs: uvs && chunk(uvs, 2),
|
|
110
|
+
normals: normals && chunk(normals, 3),
|
|
111
|
+
colors: colors && chunk(colors, 4),
|
|
112
|
+
weights: weights && chunk(weights, 4),
|
|
113
|
+
joints: joints && chunk(joints, 4),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
let indices: Uint32Array | undefined;
|
|
117
|
+
if (gltfPrimitive.indices !== undefined) {
|
|
118
|
+
const gltfIndices = this.accessor(gltfPrimitive.indices, Uint16Array);
|
|
119
|
+
indices = new Uint32Array(gltfIndices);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let material: Parsed.Material | undefined;
|
|
123
|
+
if (gltfPrimitive.material !== undefined && this.header.materials !== undefined) {
|
|
124
|
+
const gltfMaterial = this.header.materials[gltfPrimitive.material];
|
|
125
|
+
material = await this.material(gltfMaterial);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {attributes, indices, material};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ------------------------------------- Skin --------------------------------------
|
|
132
|
+
|
|
133
|
+
skin(gltfSkin: GLTF.Skin) {
|
|
134
|
+
assert(this.header.nodes !== undefined, "`Nodes` is undefined");
|
|
135
|
+
|
|
136
|
+
// Default: Array of identity matrices with length = number of joints
|
|
137
|
+
const inverseBindMatrices = gltfSkin.inverseBindMatrices
|
|
138
|
+
? chunk(this.accessor(gltfSkin.inverseBindMatrices, Float32Array), 16)
|
|
139
|
+
: gltfSkin.joints.map(() => Array.from(mat4.idt) as Matrix4x4);
|
|
140
|
+
|
|
141
|
+
const joints: Record<string, Parsed.Joint> = {};
|
|
142
|
+
|
|
143
|
+
let i = 0;
|
|
144
|
+
for (let jointNodeIndex of gltfSkin.joints) {
|
|
145
|
+
const gltfJoint = this.header.nodes[jointNodeIndex];
|
|
146
|
+
assert(gltfJoint.name !== undefined, "Joint node has no `name` property");
|
|
147
|
+
|
|
148
|
+
// Get joint transform as matrix
|
|
149
|
+
const transform = gltfJoint.matrix
|
|
150
|
+
? [...gltfJoint.matrix] as Matrix4x4
|
|
151
|
+
: getTransform(gltfJoint, this.options.transformOrder);
|
|
152
|
+
|
|
153
|
+
const children = gltfJoint.children?.map(i => this.header.nodes![i].name!) ?? [];
|
|
154
|
+
|
|
155
|
+
//const inverseBindTransform = inverseBindMatrices[jointIndex];
|
|
156
|
+
const inverseBindTransform = inverseBindMatrices[i];
|
|
157
|
+
|
|
158
|
+
joints[gltfJoint.name] = {name: gltfJoint.name, transform, children, inverseBindTransform};
|
|
159
|
+
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const gltfRoot = this.header.nodes[gltfSkin.joints[0]];
|
|
164
|
+
const root = joints[gltfRoot.name!] ?? fail("Invalid root node");
|
|
165
|
+
|
|
166
|
+
// Set parents
|
|
167
|
+
for (let name in joints) {
|
|
168
|
+
const joint = joints[name];
|
|
169
|
+
for (let childName of joint.children) {
|
|
170
|
+
joints[childName].parent = name;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Animations targeting joints of this skin
|
|
175
|
+
const gltfAnimations = this.header.animations?.filter(animation => {
|
|
176
|
+
return animation.channels.some(channel => {
|
|
177
|
+
const target = this.header.nodes![channel.target.node!];
|
|
178
|
+
return joints[target.name!] !== undefined;
|
|
179
|
+
});
|
|
180
|
+
}) ?? [];
|
|
181
|
+
|
|
182
|
+
const animations = gltfAnimations.map(a => this.animation(a));
|
|
183
|
+
|
|
184
|
+
// TEMP: Calculate global inverse transform
|
|
185
|
+
const skinParentNode = this.header.nodes.find(n => n.children?.includes(gltfSkin.joints[0]));
|
|
186
|
+
const globalInverseTransform = skinParentNode
|
|
187
|
+
? getTransform(skinParentNode, this.options.transformOrder)
|
|
188
|
+
: Array.from(mat4.idt) as Matrix4x4;
|
|
189
|
+
|
|
190
|
+
return {joints, root, animations, name: gltfSkin.name, globalInverseTransform};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
animation(gltfAnimation: GLTF.Animation): Parsed.Animation {
|
|
194
|
+
assert(this.header.nodes !== undefined, "`Nodes` is undefined");
|
|
195
|
+
const channels: Record<string, Parsed.AnimationChannel> = {};
|
|
196
|
+
let duration = 0;
|
|
197
|
+
|
|
198
|
+
for (let gltfChannel of gltfAnimation.channels) {
|
|
199
|
+
const gltfTargetNode = this.header.nodes![gltfChannel.target.node!];
|
|
200
|
+
assert(gltfTargetNode !== undefined && gltfTargetNode.name !== undefined, "Invalid target node");
|
|
201
|
+
|
|
202
|
+
// Init channel if not already present
|
|
203
|
+
channels[gltfTargetNode.name] ??= {translation: [], rotation: [], scale: []};
|
|
204
|
+
|
|
205
|
+
const sampler = gltfAnimation.samplers[gltfChannel.sampler];
|
|
206
|
+
const times = this.accessor(sampler.input, Float32Array);
|
|
207
|
+
const values = this.accessor(sampler.output, Float32Array);
|
|
208
|
+
|
|
209
|
+
const numComponents = values.length / times.length;
|
|
210
|
+
assert(numComponents === 3 || numComponents === 4, `Invalid number of components: '${numComponents}'`);
|
|
211
|
+
|
|
212
|
+
// Set keyframes array of current 'path', e.g. 'translation'
|
|
213
|
+
const keyframes = chunk(values, numComponents).map((value, i) => ({time: times[i], value}));
|
|
214
|
+
channels[gltfTargetNode.name][gltfChannel.target.path] = keyframes as any;
|
|
215
|
+
|
|
216
|
+
duration = Math.max(duration, ...keyframes.map(k => k.time));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {channels, name: gltfAnimation.name, duration};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ----------------------------------- Material ------------------------------------
|
|
223
|
+
|
|
224
|
+
async material(gltfMaterial: GLTF.Material): Promise<Parsed.Material> {
|
|
225
|
+
const gltfPbr = gltfMaterial.pbrMetallicRoughness;
|
|
226
|
+
const metallicRoughness: Parsed.MaterialMetallicRoughnessInfo = {
|
|
227
|
+
baseColor: gltfPbr?.baseColorFactor ?? [1, 1, 1, 1],
|
|
228
|
+
baseColorTexture: gltfPbr?.baseColorTexture && await this.texture(gltfPbr.baseColorTexture),
|
|
229
|
+
metallicFactor: gltfPbr?.metallicFactor ?? 1,
|
|
230
|
+
roughnessFactor: gltfPbr?.roughnessFactor ?? 1,
|
|
231
|
+
metallicRoughnessTexture: gltfPbr?.metallicRoughnessTexture && await this.texture(gltfPbr.metallicRoughnessTexture),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
let normal: Parsed.MaterialNormalInfo | undefined;
|
|
235
|
+
if (gltfMaterial.normalTexture !== undefined) {
|
|
236
|
+
fail("TODO");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let emissive: Parsed.MaterialEmissiveInfo | undefined;
|
|
240
|
+
if (gltfMaterial.emissiveTexture !== undefined) {
|
|
241
|
+
fail("TODO");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let ambientOcclusion: Parsed.MaterialAmbientOcclusionInfo | undefined;
|
|
245
|
+
if (gltfMaterial.occlusionTexture !== undefined) {
|
|
246
|
+
fail("TODO");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const alpha: Parsed.MaterialAlphaInfo = {
|
|
250
|
+
mode: gltfMaterial.alphaMode ?? "OPAQUE",
|
|
251
|
+
cutoff: gltfMaterial.alphaCutoff ?? 0.5,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const doubleSided = gltfMaterial.doubleSided ?? false;
|
|
255
|
+
|
|
256
|
+
return {metallicRoughness, normal, emissive, ambientOcclusion, alpha, doubleSided};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async texture(gltfTextureInfo: GLTF.TextureInfo): Promise<Parsed.Texture | undefined> {
|
|
260
|
+
// Get texture from textureInfo
|
|
261
|
+
assert(this.header.textures !== undefined, "Header `Textures` property is undefined");
|
|
262
|
+
const gltfTexture = this.header.textures[gltfTextureInfo.index];
|
|
263
|
+
|
|
264
|
+
// No textures without source
|
|
265
|
+
if (gltfTexture.source === undefined) return undefined;
|
|
266
|
+
|
|
267
|
+
// Load texture image
|
|
268
|
+
assert(this.header.images !== undefined, "Header `Images` property is undefined");
|
|
269
|
+
const gltfImage = this.header.images[gltfTexture.source];
|
|
270
|
+
const source = await this.image(gltfImage);
|
|
271
|
+
|
|
272
|
+
// Parse sampler
|
|
273
|
+
const sampler: Parsed.Sampler = {wrap: {s: "repeat", t: "repeat"}, filter: {min: "linear", mag: "linear"}};
|
|
274
|
+
if (this.header.samplers !== undefined && gltfTexture.sampler !== undefined) {
|
|
275
|
+
const gltfSampler = this.header.samplers[gltfTexture.sampler];
|
|
276
|
+
|
|
277
|
+
if (gltfSampler.minFilter !== undefined) sampler.filter.min = TextureFilterMap[gltfSampler.minFilter];
|
|
278
|
+
if (gltfSampler.magFilter !== undefined) sampler.filter.mag = TextureFilterMap[gltfSampler.magFilter];
|
|
279
|
+
if (gltfSampler.wrapS !== undefined) sampler.wrap.s = TextureWrapModeMap[gltfSampler.wrapS];
|
|
280
|
+
if (gltfSampler.wrapT !== undefined) sampler.wrap.t = TextureWrapModeMap[gltfSampler.wrapT];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {source, sampler, name: gltfTexture.name};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async image(gltfImage: GLTF.Image): Promise<ImageBitmap> {
|
|
287
|
+
// Image from uri
|
|
288
|
+
if ("uri" in gltfImage) {
|
|
289
|
+
// TODO: URI is relative to GLTF file path
|
|
290
|
+
const response = await fetch(this.path + gltfImage.uri);
|
|
291
|
+
assert(response.ok, `Failed to fetch image at URI: '${gltfImage.uri}'`);
|
|
292
|
+
const blob = await response.blob();
|
|
293
|
+
return await createImageBitmap(blob);
|
|
294
|
+
}
|
|
295
|
+
// Image from buffer
|
|
296
|
+
else {
|
|
297
|
+
assert(this.header.bufferViews !== undefined, "Header `BufferViews` property is undefined");
|
|
298
|
+
assert(this.header.buffers !== undefined, "Header `Buffers` property is undefined");
|
|
299
|
+
const bufferView = this.header.bufferViews[gltfImage.bufferView];
|
|
300
|
+
const buffer = this.header.buffers[bufferView.buffer];
|
|
301
|
+
|
|
302
|
+
if (buffer.uri !== undefined) {
|
|
303
|
+
fail("TODO");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// TODO: buffer length + bufferView offset (?)
|
|
307
|
+
const data = this.binary.slice(bufferView.byteOffset ?? 0, (bufferView.byteOffset ?? 0) + bufferView.byteLength);
|
|
308
|
+
const blob = new Blob([new Uint8Array(data)]);
|
|
309
|
+
return await createImageBitmap(blob);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ------------------------------------- Scene -------------------------------------
|
|
314
|
+
|
|
315
|
+
async scene(): Promise<Parsed.Scene> {
|
|
316
|
+
const nodes = []; // TODO
|
|
317
|
+
|
|
318
|
+
const cameras = []; // TODO
|
|
319
|
+
|
|
320
|
+
const meshes = this.header.meshes
|
|
321
|
+
? await Promise.all(this.header.meshes.map(m => this.mesh(m)))
|
|
322
|
+
: [];
|
|
323
|
+
|
|
324
|
+
const skins = this.header.skins
|
|
325
|
+
? this.header.skins.map(s => this.skin(s))
|
|
326
|
+
: [];
|
|
327
|
+
|
|
328
|
+
return {nodes, cameras, meshes, skins};
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import {Tuple} from "./util.ts";
|
|
2
|
+
|
|
3
|
+
export type Matrix4x4 = Tuple<number, 16>
|
|
4
|
+
export type Vector2 = Tuple<number, 2>
|
|
5
|
+
export type Vector3 = Tuple<number, 3>
|
|
6
|
+
export type Vector4 = Tuple<number, 4>
|
|
7
|
+
|
|
8
|
+
// ------------------------------- Scene -------------------------------
|
|
9
|
+
|
|
10
|
+
export type Scene = {
|
|
11
|
+
/**
|
|
12
|
+
* All nodes in scene
|
|
13
|
+
*/
|
|
14
|
+
nodes: Node[]
|
|
15
|
+
cameras: Camera[],
|
|
16
|
+
meshes: Mesh[],
|
|
17
|
+
skins: Skin[],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
// ------------------------------- Node --------------------------------
|
|
22
|
+
|
|
23
|
+
export type Node = {
|
|
24
|
+
transform: Matrix4x4,
|
|
25
|
+
name?: string,
|
|
26
|
+
children: Node[],
|
|
27
|
+
parent?: Node,
|
|
28
|
+
} & ({ mesh: Mesh, skin?: undefined, camera?: undefined }
|
|
29
|
+
| { skin: Skin, mesh?: undefined, camera?: undefined }
|
|
30
|
+
| { camera: Camera, mesh?: undefined, skin?: undefined })
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
// ------------------------------ Camera -------------------------------
|
|
34
|
+
|
|
35
|
+
export type OrthographicCamera = {
|
|
36
|
+
type: "Orthographic"
|
|
37
|
+
xmag: number,
|
|
38
|
+
ymag: number,
|
|
39
|
+
zfar: number,
|
|
40
|
+
znear: number,
|
|
41
|
+
view: Matrix4x4,
|
|
42
|
+
projection: Matrix4x4,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type PerspectiveCamera = {
|
|
46
|
+
type: "Perspective"
|
|
47
|
+
aspectRatio: number,
|
|
48
|
+
yfov: number,
|
|
49
|
+
zfar: number,
|
|
50
|
+
znear: number,
|
|
51
|
+
view: Matrix4x4,
|
|
52
|
+
projection: Matrix4x4,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type Camera = OrthographicCamera | PerspectiveCamera
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// ------------------------------- Mesh --------------------------------
|
|
59
|
+
|
|
60
|
+
export type MeshPrimitiveAttributes = {
|
|
61
|
+
positions: Vector3[],
|
|
62
|
+
uvs?: Vector2[],
|
|
63
|
+
normals?: Vector3[],
|
|
64
|
+
colors?: Vector4[]
|
|
65
|
+
weights?: Vector4[],
|
|
66
|
+
joints?: Vector4[],
|
|
67
|
+
tangents?: Vector4[], // TODO
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type MeshPrimitive = {
|
|
71
|
+
/**
|
|
72
|
+
* Vertex attributes of the mesh primitive
|
|
73
|
+
*/
|
|
74
|
+
attributes: MeshPrimitiveAttributes,
|
|
75
|
+
indices?: Uint32Array,
|
|
76
|
+
material?: Material,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type Mesh = {
|
|
80
|
+
primitives: MeshPrimitive[],
|
|
81
|
+
name?: string,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
// -------------------------------- Skin -------------------------------
|
|
86
|
+
|
|
87
|
+
export type Joint = {
|
|
88
|
+
name: string,
|
|
89
|
+
transform: Matrix4x4,
|
|
90
|
+
inverseBindTransform: Matrix4x4,
|
|
91
|
+
children: string[],
|
|
92
|
+
parent?: string,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type Skin = {
|
|
96
|
+
root: Joint,
|
|
97
|
+
joints: Record<string, Joint>,
|
|
98
|
+
animations: Animation[],
|
|
99
|
+
globalInverseTransform: Matrix4x4,
|
|
100
|
+
name?: string,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ----------------------------- Animation -----------------------------
|
|
104
|
+
|
|
105
|
+
export type Animation = {
|
|
106
|
+
duration: number,
|
|
107
|
+
name?: string,
|
|
108
|
+
channels: Record<string, AnimationChannel>,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type AnimationChannel = {
|
|
112
|
+
translation: AnimationKeyframe<Vector3>[]
|
|
113
|
+
rotation: AnimationKeyframe<Vector4>[]
|
|
114
|
+
scale: AnimationKeyframe<Vector3>[]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type AnimationKeyframe<T> = { time: number, value: T }
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
// ----------------------------- Texture -------------------------------
|
|
121
|
+
|
|
122
|
+
// TODO: If source is undefined, don't create texture, if sampler is undefined create default sampler
|
|
123
|
+
export type Texture = {
|
|
124
|
+
source: ImageBitmap,
|
|
125
|
+
sampler: Sampler,
|
|
126
|
+
name?: string,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type Sampler = {
|
|
130
|
+
wrap: { s: TextureWrapMode, t: TextureWrapMode },
|
|
131
|
+
filter: { min: TextureMinFilter, mag: TextureMagFilter },
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type TextureMagFilter = "nearest" | "linear"
|
|
135
|
+
|
|
136
|
+
export type TextureMinFilter =
|
|
137
|
+
"nearest"
|
|
138
|
+
| "linear"
|
|
139
|
+
| "nearest-mipmap-nearest"
|
|
140
|
+
| "linear-mipmap-nearest"
|
|
141
|
+
| "nearest-mipmap-linear"
|
|
142
|
+
| "linear-mipmap-linear"
|
|
143
|
+
|
|
144
|
+
export type TextureWrapMode =
|
|
145
|
+
| "clamp-to-edge"
|
|
146
|
+
| "mirrored-repeat"
|
|
147
|
+
| "repeat"
|
|
148
|
+
|
|
149
|
+
export const TextureWrapModeMap = {
|
|
150
|
+
[33071]: "clamp-to-edge",
|
|
151
|
+
[33648]: "mirrored-repeat",
|
|
152
|
+
[10497]: "repeat",
|
|
153
|
+
} as const;
|
|
154
|
+
|
|
155
|
+
export const TextureFilterMap = {
|
|
156
|
+
[9728]: "nearest",
|
|
157
|
+
[9729]: "linear",
|
|
158
|
+
[9984]: "nearest-mipmap-nearest",
|
|
159
|
+
[9985]: "linear-mipmap-nearest",
|
|
160
|
+
[9986]: "nearest-mipmap-linear",
|
|
161
|
+
[9987]: "linear-mipmap-linear",
|
|
162
|
+
} as const;
|
|
163
|
+
|
|
164
|
+
// ----------------------------- Material ------------------------------
|
|
165
|
+
|
|
166
|
+
export type Material = {
|
|
167
|
+
metallicRoughness: MaterialMetallicRoughnessInfo,
|
|
168
|
+
normal?: MaterialNormalInfo,
|
|
169
|
+
emissive?: MaterialEmissiveInfo,
|
|
170
|
+
ambientOcclusion?: MaterialAmbientOcclusionInfo,
|
|
171
|
+
alpha: MaterialAlphaInfo,
|
|
172
|
+
/**
|
|
173
|
+
* Whether the material is double sided. Default is `false`.
|
|
174
|
+
*/
|
|
175
|
+
doubleSided: boolean,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export type MaterialAlphaInfo = {
|
|
179
|
+
/**
|
|
180
|
+
* The alpha rendering mode of the material. Default is `"OPAQUE"`.
|
|
181
|
+
*/
|
|
182
|
+
mode: "OPAQUE" | "MASK" | "BLEND",
|
|
183
|
+
/**
|
|
184
|
+
* The alpha cutoff value of the material. Default is `0.5`.
|
|
185
|
+
*/
|
|
186
|
+
cutoff: number,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type MaterialMetallicRoughnessInfo = {
|
|
190
|
+
/**
|
|
191
|
+
* The base color factor of the material. Default is `[1, 1, 1, 1]`.
|
|
192
|
+
*/
|
|
193
|
+
baseColor: Vector4,
|
|
194
|
+
/**
|
|
195
|
+
* The base color texture of the material.
|
|
196
|
+
*/
|
|
197
|
+
baseColorTexture?: Texture,
|
|
198
|
+
/**
|
|
199
|
+
* The factor for the metalness of the material. Default is `1`.
|
|
200
|
+
*/
|
|
201
|
+
metallicFactor: number,
|
|
202
|
+
/**
|
|
203
|
+
* The factor for the roughness of the material. Default is `1`.
|
|
204
|
+
*/
|
|
205
|
+
roughnessFactor: number,
|
|
206
|
+
/**
|
|
207
|
+
* The metallic roughness texture of the material.
|
|
208
|
+
*/
|
|
209
|
+
metallicRoughnessTexture?: Texture,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export type MaterialNormalInfo = {
|
|
213
|
+
/**
|
|
214
|
+
* The normal texture of the material.
|
|
215
|
+
*/
|
|
216
|
+
texture: Texture,
|
|
217
|
+
/**
|
|
218
|
+
* The scale applied to each vector of the normal map. Default is `1`.
|
|
219
|
+
*/
|
|
220
|
+
scale: number,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type MaterialEmissiveInfo = {
|
|
224
|
+
/**
|
|
225
|
+
* The emissive texture of the material.
|
|
226
|
+
*/
|
|
227
|
+
texture: Texture,
|
|
228
|
+
/**
|
|
229
|
+
* The emissive factor of the material. Default is `[0, 0, 0]`.
|
|
230
|
+
*/
|
|
231
|
+
factor: Vector3,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export type MaterialAmbientOcclusionInfo = {
|
|
235
|
+
/**
|
|
236
|
+
* The occlusion texture of the material.
|
|
237
|
+
*/
|
|
238
|
+
texture: Texture,
|
|
239
|
+
/**
|
|
240
|
+
* The occlusion strength of the material. Default is `1`.
|
|
241
|
+
*/
|
|
242
|
+
strength: number,
|
|
243
|
+
}
|
|
244
|
+
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {TransformOrder, mat4, vec3, quat, Matrix4} from "dom-game-math";
|
|
2
|
+
import {Matrix4x4} from "./types.ts";
|
|
3
|
+
|
|
4
|
+
// ----------------------------------- fail & assert -----------------------------------
|
|
5
|
+
|
|
6
|
+
export function fail(msg: string): never {
|
|
7
|
+
throw new Error(msg);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function assert(condition: boolean, msg: string): asserts condition {
|
|
11
|
+
if (!condition) {
|
|
12
|
+
throw new Error(msg);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ------------------------------------- Accessor --------------------------------------
|
|
17
|
+
|
|
18
|
+
export type AccessorArrayType =
|
|
19
|
+
| Float32ArrayConstructor
|
|
20
|
+
| Uint32ArrayConstructor
|
|
21
|
+
| Uint16ArrayConstructor
|
|
22
|
+
| Int16ArrayConstructor
|
|
23
|
+
| Uint8ArrayConstructor
|
|
24
|
+
| Int8ArrayConstructor
|
|
25
|
+
|
|
26
|
+
export type AccessorConstructorType<T extends AccessorArrayType> = ReturnType<T["from"]>
|
|
27
|
+
|
|
28
|
+
// --------------------------------------- Tuple ---------------------------------------
|
|
29
|
+
|
|
30
|
+
export type Tuple<T, N extends number> = T[] & { length: N };
|
|
31
|
+
|
|
32
|
+
export function chunk<T, N extends number>(array: Iterable<T>, size: N): Tuple<T, N>[] {
|
|
33
|
+
const result = new Array<T[]>;
|
|
34
|
+
let chunk = new Array<T>();
|
|
35
|
+
|
|
36
|
+
for (const value of Array.from(array)) {
|
|
37
|
+
chunk.push(value);
|
|
38
|
+
if (chunk.length === size) {
|
|
39
|
+
result.push(chunk);
|
|
40
|
+
chunk = [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (chunk.length) {
|
|
45
|
+
result.push(chunk);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result as Tuple<T, N>[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getTransform(transform: {
|
|
52
|
+
translation?: number[],
|
|
53
|
+
rotation?: number[],
|
|
54
|
+
scale?: number[]
|
|
55
|
+
}, order: TransformOrder, out?: Tuple<number, 16>) {
|
|
56
|
+
out ??= Array.from(mat4.idt) as Tuple<number, 16>;
|
|
57
|
+
|
|
58
|
+
mat4.compose({
|
|
59
|
+
translation: transform.translation && vec3.fromArray(transform.translation),
|
|
60
|
+
rotation: transform.rotation && quat.fromArray(transform.rotation),
|
|
61
|
+
scale: transform.scale && vec3.fromArray(transform.scale),
|
|
62
|
+
order,
|
|
63
|
+
}, out as Matrix4);
|
|
64
|
+
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2023",
|
|
7
|
+
"DOM",
|
|
8
|
+
"DOM.Iterable"
|
|
9
|
+
],
|
|
10
|
+
"useDefineForClassFields": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
/* Bundler mode */
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": false,
|
|
17
|
+
"noEmit": true,
|
|
18
|
+
"noImplicitAny": false,
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": false,
|
|
22
|
+
"noUnusedParameters": false,
|
|
23
|
+
"noFallthroughCasesInSwitch": true,
|
|
24
|
+
"declaration": true,
|
|
25
|
+
"accessor-pairs": [
|
|
26
|
+
"error",
|
|
27
|
+
{
|
|
28
|
+
"setWithoutGet": true,
|
|
29
|
+
"getWithoutSet": true
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"include": [
|
|
34
|
+
"src"
|
|
35
|
+
]
|
|
36
|
+
}
|