@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 ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -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
+ }
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
@@ -0,0 +1,2 @@
1
+ export * from "./types.ts";
2
+ export * from "./Parser.ts";
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
+ }