pelican-engine 0.0.0-stage → 0.3.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.
Files changed (192) hide show
  1. package/AGENTS.md +147 -0
  2. package/LICENSE +22 -0
  3. package/README.md +129 -2
  4. package/distribution.json +197 -0
  5. package/docs/AI-WORKFLOW.md +517 -0
  6. package/docs/ARCHITECTURE.md +268 -0
  7. package/docs/CAPABILITIES.md +264 -0
  8. package/docs/DECISIONS.md +107 -0
  9. package/docs/M3-USAGE.md +192 -0
  10. package/docs/PELICAN-MIGRATION.md +38 -0
  11. package/docs/RENDER-BACKENDS.md +50 -0
  12. package/docs/ROADMAP.md +239 -0
  13. package/docs/THIRD-PARTY-NOTICES.md +151 -0
  14. package/docs/api/REFERENCE.md +1435 -0
  15. package/docs/api/index.json +1139 -0
  16. package/docs/npm/DISTRIBUTION.md +130 -0
  17. package/docs/npm/GAME-AUTHOR.md +147 -0
  18. package/engine.manifest.json +15 -0
  19. package/lib/assets/audio.js +67 -0
  20. package/lib/assets/errors.js +36 -0
  21. package/lib/assets/glb.js +277 -0
  22. package/lib/assets/index.js +53 -0
  23. package/lib/assets/jpeg.js +199 -0
  24. package/lib/assets/model.js +558 -0
  25. package/lib/assets/optimize.js +95 -0
  26. package/lib/assets/texture.js +187 -0
  27. package/lib/compiler/distribution.js +17 -0
  28. package/lib/compiler/extract.js +425 -0
  29. package/lib/compiler/graph.js +366 -0
  30. package/lib/compiler/index.js +574 -0
  31. package/lib/compiler/normalize.js +1194 -0
  32. package/lib/compiler/source-map.js +25 -0
  33. package/lib/compiler/types.js +2 -0
  34. package/lib/compiler/version.js +93 -0
  35. package/lib/experiments/simulation/index.js +2 -0
  36. package/lib/experiments/simulation/math.js +41 -0
  37. package/lib/experiments/simulation/probe.js +215 -0
  38. package/lib/experiments/simulation/scenes.js +192 -0
  39. package/lib/experiments/simulation/solver.js +346 -0
  40. package/lib/experiments/simulation/types.js +1 -0
  41. package/lib/experiments/world/adapters.js +30 -0
  42. package/lib/experiments/world/geometry.js +180 -0
  43. package/lib/experiments/world/index.js +2 -0
  44. package/lib/experiments/world/probe.js +288 -0
  45. package/lib/experiments/world/types.js +1 -0
  46. package/lib/experiments/world/world.js +599 -0
  47. package/lib/navigation/index.js +157 -0
  48. package/lib/physics/index.js +909 -0
  49. package/lib/protocol/index.js +92 -0
  50. package/lib/renderer/acceleration.js +609 -0
  51. package/lib/renderer/browser.js +2015 -0
  52. package/lib/renderer/bsdf.js +260 -0
  53. package/lib/renderer/checkpoint.js +88 -0
  54. package/lib/renderer/compute-executor.js +1034 -0
  55. package/lib/renderer/compute-texture-assets.js +49 -0
  56. package/lib/renderer/compute-view-contract.js +1 -0
  57. package/lib/renderer/denoiser.js +361 -0
  58. package/lib/renderer/dielectric.js +360 -0
  59. package/lib/renderer/embedded-image-pixels.js +368 -0
  60. package/lib/renderer/emissive-lights.js +67 -0
  61. package/lib/renderer/environment-importance.js +55 -0
  62. package/lib/renderer/environment.js +346 -0
  63. package/lib/renderer/gameplay.js +464 -0
  64. package/lib/renderer/geometry.js +103 -0
  65. package/lib/renderer/graph-expressions.js +485 -0
  66. package/lib/renderer/graph-tsl.js +141 -0
  67. package/lib/renderer/graph.js +214 -0
  68. package/lib/renderer/managed-texture-sampling.js +29 -0
  69. package/lib/renderer/material-graphs.js +297 -0
  70. package/lib/renderer/medium.js +348 -0
  71. package/lib/renderer/models.js +1053 -0
  72. package/lib/renderer/motion-shader.js +40 -0
  73. package/lib/renderer/path-shader.js +196 -0
  74. package/lib/renderer/pathtrace.js +324 -0
  75. package/lib/renderer/postprocess-contract.js +1 -0
  76. package/lib/renderer/postprocess.js +494 -0
  77. package/lib/renderer/render-buffers.js +132 -0
  78. package/lib/renderer/render-textures.js +155 -0
  79. package/lib/renderer/startup.js +50 -0
  80. package/lib/renderer/surface-capture.js +199 -0
  81. package/lib/renderer/surface-gpu.js +270 -0
  82. package/lib/renderer/surface-motion.js +84 -0
  83. package/lib/renderer/surface-ray-shader.js +91 -0
  84. package/lib/renderer/surface-shader.js +88 -0
  85. package/lib/renderer/surface-tracer.js +1720 -0
  86. package/lib/renderer/surface.js +1425 -0
  87. package/lib/renderer/temporal-filter.js +512 -0
  88. package/lib/renderer/texture-pixels.js +207 -0
  89. package/lib/renderer/traversal-nodes.js +54 -0
  90. package/lib/renderer/ui.js +446 -0
  91. package/lib/renderer/vertex-bounds.js +127 -0
  92. package/lib/renderer/vertex-contract.js +1 -0
  93. package/lib/renderer/vertex-graphs.js +197 -0
  94. package/lib/renderer/volume-shader.js +50 -0
  95. package/lib/runtime/compute-contract.js +1 -0
  96. package/lib/runtime/compute-service.js +674 -0
  97. package/lib/runtime/contracts.js +1 -0
  98. package/lib/runtime/error.js +37 -0
  99. package/lib/runtime/events.js +189 -0
  100. package/lib/runtime/game-services.js +772 -0
  101. package/lib/runtime/host.js +839 -0
  102. package/lib/runtime/material-parameters.js +145 -0
  103. package/lib/runtime/plugins.js +1 -0
  104. package/lib/runtime/postprocess-contract.js +1 -0
  105. package/lib/runtime/postprocess.js +189 -0
  106. package/lib/runtime/prefabs.js +13 -0
  107. package/lib/runtime/random.js +88 -0
  108. package/lib/runtime/render-buffer-contract.js +1 -0
  109. package/lib/runtime/render-buffers.js +243 -0
  110. package/lib/runtime/render-capabilities.js +17 -0
  111. package/lib/runtime/render-settings.js +249 -0
  112. package/lib/runtime/render-texture-contract.js +1 -0
  113. package/lib/runtime/render-textures.js +271 -0
  114. package/lib/runtime/resources.js +648 -0
  115. package/lib/runtime/scheduler.js +765 -0
  116. package/lib/runtime/storage.js +169 -0
  117. package/lib/runtime/world.js +594 -0
  118. package/lib/schema/index.js +24 -0
  119. package/lib/sdk/graph-values.js +16 -0
  120. package/lib/sdk/index.d.ts +800 -0
  121. package/lib/sdk/index.js +82 -0
  122. package/lib/tools/api-index.js +113 -0
  123. package/lib/tools/browser-session.js +89 -0
  124. package/lib/tools/cli.js +513 -0
  125. package/lib/tools/compress-dist.js +59 -0
  126. package/lib/tools/control-client.js +153 -0
  127. package/lib/tools/control-server.js +633 -0
  128. package/lib/tools/diagnostic-code.js +3 -0
  129. package/lib/tools/doctor.js +496 -0
  130. package/lib/tools/npm/browser-cli.js +6 -0
  131. package/lib/tools/npm/entry.js +40 -0
  132. package/lib/tools/npm/layout.js +195 -0
  133. package/lib/tools/setup.js +153 -0
  134. package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
  135. package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
  136. package/licenses/@gltf-transform/core/LICENSE.md +21 -0
  137. package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
  138. package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
  139. package/licenses/@img/colour/LICENSE.md +82 -0
  140. package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
  141. package/licenses/@oxc-project/types/LICENSE +22 -0
  142. package/licenses/@rolldown/pluginutils/LICENSE +21 -0
  143. package/licenses/@tweenjs/tween.js/LICENSE +23 -0
  144. package/licenses/@types/ndarray/LICENSE +21 -0
  145. package/licenses/@types/node/LICENSE +21 -0
  146. package/licenses/@types/stats.js/LICENSE +21 -0
  147. package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
  148. package/licenses/@types/three/LICENSE +21 -0
  149. package/licenses/@types/webxr/LICENSE +21 -0
  150. package/licenses/@webgpu/types/LICENSE +26 -0
  151. package/licenses/THIRD-PARTY-NOTICES.md +139 -0
  152. package/licenses/cwise-compiler/LICENSE +22 -0
  153. package/licenses/detect-libc/LICENSE +201 -0
  154. package/licenses/esbuild/LICENSE.md +21 -0
  155. package/licenses/fast-png/LICENSE +21 -0
  156. package/licenses/fdir/LICENSE +7 -0
  157. package/licenses/fflate/LICENSE +21 -0
  158. package/licenses/iobuffer/LICENSE +22 -0
  159. package/licenses/iota-array/LICENSE +22 -0
  160. package/licenses/is-buffer/LICENSE +21 -0
  161. package/licenses/ktx-parse/LICENSE +21 -0
  162. package/licenses/lightningcss/LICENSE +373 -0
  163. package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
  164. package/licenses/meshoptimizer/LICENSE.md +21 -0
  165. package/licenses/nanoid/LICENSE +20 -0
  166. package/licenses/ndarray/LICENSE +22 -0
  167. package/licenses/ndarray-lanczos/LICENSE +21 -0
  168. package/licenses/ndarray-ops/LICENSE +22 -0
  169. package/licenses/ndarray-pixels/LICENSE +21 -0
  170. package/licenses/ngraph.path/LICENSE +21 -0
  171. package/licenses/picocolors/LICENSE +15 -0
  172. package/licenses/picomatch/LICENSE +21 -0
  173. package/licenses/playwright/LICENSE +202 -0
  174. package/licenses/playwright/NOTICE +5 -0
  175. package/licenses/playwright-core/LICENSE +202 -0
  176. package/licenses/playwright-core/NOTICE +5 -0
  177. package/licenses/postcss/LICENSE +20 -0
  178. package/licenses/prettier/LICENSE +7 -0
  179. package/licenses/property-graph/LICENSE +21 -0
  180. package/licenses/rolldown/LICENSE +25 -0
  181. package/licenses/semver/LICENSE +15 -0
  182. package/licenses/sharp/LICENSE +191 -0
  183. package/licenses/source-map-js/LICENSE +28 -0
  184. package/licenses/three/LICENSE +21 -0
  185. package/licenses/tinyglobby/LICENSE +21 -0
  186. package/licenses/tsx/LICENSE +21 -0
  187. package/licenses/typescript/LICENSE.txt +55 -0
  188. package/licenses/undici-types/LICENSE +21 -0
  189. package/licenses/uniq/LICENSE +22 -0
  190. package/licenses/vite/LICENSE.md +2084 -0
  191. package/package.json +49 -4
  192. package/web/index.html +34 -0
@@ -0,0 +1,558 @@
1
+ import { NodeIO } from "@gltf-transform/core";
2
+ import { InspectionContext } from "./errors.js";
3
+ import { KHRMaterialsIOR, KHRMaterialsSpecular } from "@gltf-transform/extensions";
4
+ import { parseGLB } from "./glb.js";
5
+ import { inspectTexture } from "./texture.js";
6
+ export async function inspectModel(data, ctx) {
7
+ const { json, bin, accessors } = parseGLB(data, ctx);
8
+ const ref = (list, value, at) => {
9
+ const i = ctx.integer(value, at, 0, (list?.length ?? 0) - 1);
10
+ return list[i];
11
+ };
12
+ const vector = (value, length, at, min = -Infinity, max = Infinity) => {
13
+ ctx.check(Array.isArray(value) &&
14
+ value.length === length &&
15
+ value.every((v) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max), at, `Expected ${length} finite components in [${min}, ${max}].`);
16
+ return value;
17
+ };
18
+ const scalar = (value, at, min = 0, max = 1) => {
19
+ ctx.check(typeof value === "number" &&
20
+ Number.isFinite(value) &&
21
+ value >= min &&
22
+ value <= max, at, `Expected a number in [${min}, ${max}].`);
23
+ return value;
24
+ };
25
+ const accessor = (index, at, type, componentTypes) => {
26
+ const a = ref(accessors, index, at);
27
+ ctx.check((!type || a.definition.type === type) &&
28
+ (!componentTypes ||
29
+ componentTypes.includes(a.definition.componentType)), at, "Accessor has incompatible type or componentType.");
30
+ return a;
31
+ };
32
+ if (json.cameras?.length)
33
+ ctx.unsupported("cameras", "Camera import is not implemented by this inspection subset.");
34
+ const images = (json.images ?? []).map((image, i) => {
35
+ const at = `images/${i}`;
36
+ if (image.uri !== undefined)
37
+ ctx.unsupported(at + "/uri", `External or URI image resource ${String(image.uri)} is not supported; embed image bytes in a buffer view.`);
38
+ const view = ref(json.bufferViews, image.bufferView, at + "/bufferView");
39
+ ctx.check(view.byteStride === undefined && view.target === undefined, at, "Image buffer view must not have vertex stride or target.");
40
+ if (!["image/png", "image/jpeg"].includes(image.mimeType ?? ""))
41
+ ctx.unsupported(at + "/mimeType", `Embedded image MIME ${String(image.mimeType)} is not implemented; use image/png or image/jpeg.`);
42
+ const start = view.byteOffset ?? 0;
43
+ const result = inspectTexture(bin.subarray(start, start + view.byteLength), new InspectionContext(`${ctx.file}#${at}`));
44
+ ctx.check(result.format === (image.mimeType === "image/png" ? "png" : "jpeg"), at + "/mimeType", "Declared image MIME does not match encoded bytes.");
45
+ return {
46
+ index: i,
47
+ name: image.name ?? "",
48
+ bytes: view.byteLength,
49
+ ...result,
50
+ };
51
+ });
52
+ for (const [i, s] of (json.samplers ?? []).entries()) {
53
+ if (s.magFilter !== undefined)
54
+ ctx.check([9728, 9729].includes(s.magFilter), `samplers/${i}/magFilter`, "Invalid magnification filter.");
55
+ if (s.minFilter !== undefined)
56
+ ctx.check([9728, 9729, 9984, 9985, 9986, 9987].includes(s.minFilter), `samplers/${i}/minFilter`, "Invalid minification filter.");
57
+ for (const key of ["wrapS", "wrapT"])
58
+ if (s[key] !== undefined)
59
+ ctx.check([33071, 33648, 10497].includes(s[key]), `samplers/${i}/${key}`, "Invalid wrapping mode.");
60
+ }
61
+ const textures = (json.textures ?? []).map((t, i) => {
62
+ ref(images, t.source, `textures/${i}/source`);
63
+ if (t.sampler !== undefined)
64
+ ref(json.samplers, t.sampler, `textures/${i}/sampler`);
65
+ return {
66
+ index: i,
67
+ name: t.name ?? "",
68
+ image: t.source,
69
+ sampler: t.sampler ?? null,
70
+ };
71
+ });
72
+ const textureInfo = (info, at) => {
73
+ if (info === undefined)
74
+ return null;
75
+ ctx.check(info && typeof info === "object" && !Array.isArray(info), at, "Expected texture info object.");
76
+ ref(textures, info.index, at + "/index");
77
+ ctx.integer(info.texCoord ?? 0, at + "/texCoord");
78
+ return info.index;
79
+ };
80
+ const materials = (json.materials ?? []).map((m, i) => {
81
+ const at = `materials/${i}`, p = m.pbrMetallicRoughness;
82
+ const specular = m.extensions?.KHR_materials_specular;
83
+ const specularTexture = (info, field) => {
84
+ const location = `${at}/extensions/KHR_materials_specular/${field}`;
85
+ const index = textureInfo(info, location);
86
+ if (info && info.texCoord !== undefined && info.texCoord !== 0)
87
+ ctx.unsupported(location + "/texCoord", "Specular textures only support TEXCOORD_0.");
88
+ return index;
89
+ };
90
+ if (p !== undefined)
91
+ ctx.check(p && typeof p === "object" && !Array.isArray(p), at + "/pbrMetallicRoughness", "Expected a PBR object.");
92
+ const slots = {
93
+ baseColor: textureInfo(p?.baseColorTexture, at + "/baseColorTexture"),
94
+ metallicRoughness: textureInfo(p?.metallicRoughnessTexture, at + "/metallicRoughnessTexture"),
95
+ normal: textureInfo(m.normalTexture, at + "/normalTexture"),
96
+ occlusion: textureInfo(m.occlusionTexture, at + "/occlusionTexture"),
97
+ emissive: textureInfo(m.emissiveTexture, at + "/emissiveTexture"),
98
+ specular: specularTexture(specular?.specularTexture, "specularTexture"),
99
+ specularColor: specularTexture(specular?.specularColorTexture, "specularColorTexture"),
100
+ };
101
+ if (m.normalTexture?.scale !== undefined)
102
+ scalar(m.normalTexture.scale, at + "/normalTexture/scale", -Infinity, Infinity);
103
+ if (m.occlusionTexture?.strength !== undefined)
104
+ scalar(m.occlusionTexture.strength, at + "/occlusionTexture/strength");
105
+ if (m.alphaMode !== undefined)
106
+ ctx.check(["OPAQUE", "MASK", "BLEND"].includes(m.alphaMode), at + "/alphaMode", "Invalid alpha mode.");
107
+ if (m.alphaCutoff !== undefined)
108
+ scalar(m.alphaCutoff, at + "/alphaCutoff", 0, Infinity);
109
+ if (m.doubleSided !== undefined)
110
+ ctx.check(typeof m.doubleSided === "boolean", at + "/doubleSided", "Expected boolean.");
111
+ return {
112
+ index: i,
113
+ name: m.name ?? "",
114
+ ...(m.extensions?.KHR_materials_specular ? { specular: m.extensions.KHR_materials_specular } : {}),
115
+ ...(m.extensions?.KHR_materials_ior ? { ior: m.extensions.KHR_materials_ior } : {}),
116
+ pbr: {
117
+ baseColorFactor: vector(p?.baseColorFactor ?? [1, 1, 1, 1], 4, at + "/baseColorFactor", 0, 1),
118
+ metallicFactor: scalar(p?.metallicFactor ?? 1, at + "/metallicFactor"),
119
+ roughnessFactor: scalar(p?.roughnessFactor ?? 1, at + "/roughnessFactor"),
120
+ },
121
+ emissiveFactor: vector(m.emissiveFactor ?? [0, 0, 0], 3, at + "/emissiveFactor", 0, 1),
122
+ textures: slots,
123
+ alphaMode: m.alphaMode ?? "OPAQUE",
124
+ doubleSided: m.doubleSided ?? false,
125
+ };
126
+ });
127
+ let vertexCount = 0, primitiveCount = 0, triangleCount = 0;
128
+ const meshes = (json.meshes ?? []).map((mesh, i) => {
129
+ const at = `meshes/${i}`;
130
+ ctx.check(Array.isArray(mesh.primitives) && mesh.primitives.length > 0, at + "/primitives", "Mesh requires primitives.");
131
+ const primitives = mesh.primitives.map((p, k) => {
132
+ const loc = `${at}/primitives/${k}`;
133
+ ctx.check(p &&
134
+ typeof p === "object" &&
135
+ p.attributes &&
136
+ typeof p.attributes === "object" &&
137
+ !Array.isArray(p.attributes), loc, "Invalid primitive or attributes.");
138
+ const position = accessor(p.attributes.POSITION, loc + "/POSITION", "VEC3", [5126]);
139
+ ctx.check(!position.definition.normalized &&
140
+ position.definition.min &&
141
+ position.definition.max, loc + "/POSITION", "POSITION requires non-normalized floats and verified min/max bounds.");
142
+ const count = position.definition.count;
143
+ for (const [semantic, index] of Object.entries(p.attributes)) {
144
+ const a = accessor(index, loc + "/" + semantic);
145
+ ctx.check(a.definition.count === count, loc + "/" + semantic, "Vertex attributes have different element counts.");
146
+ const rules = {
147
+ NORMAL: ["VEC3", [5126]],
148
+ TANGENT: ["VEC4", [5126]],
149
+ JOINTS_0: ["VEC4", [5121, 5123]],
150
+ WEIGHTS_0: ["VEC4", [5126, 5121, 5123]],
151
+ };
152
+ if (rules[semantic]) {
153
+ accessor(index, loc + "/" + semantic, ...rules[semantic]);
154
+ ctx.check(semantic === "WEIGHTS_0"
155
+ ? a.definition.componentType === 5126
156
+ ? !a.definition.normalized
157
+ : a.definition.normalized === true
158
+ : !a.definition.normalized, loc + "/" + semantic, "Invalid attribute normalization.");
159
+ }
160
+ else if (/^TEXCOORD_\d+$/.test(semantic)) {
161
+ accessor(index, loc + "/" + semantic, "VEC2", [5126, 5121, 5123]);
162
+ ctx.check(a.definition.componentType === 5126
163
+ ? !a.definition.normalized
164
+ : a.definition.normalized === true, loc + "/" + semantic, "Integer texture coordinates must be normalized.");
165
+ }
166
+ else if (/^COLOR_\d+$/.test(semantic)) {
167
+ ctx.check(["VEC3", "VEC4"].includes(a.definition.type) &&
168
+ [5126, 5121, 5123].includes(a.definition.componentType) &&
169
+ (a.definition.componentType === 5126
170
+ ? !a.definition.normalized
171
+ : a.definition.normalized === true), loc + "/" + semantic, "Invalid color attribute.");
172
+ }
173
+ else if (semantic !== "POSITION" && !semantic.startsWith("_"))
174
+ ctx.unsupported(loc + "/" + semantic, "Vertex semantic is outside the implemented subset (only joint/weight set 0 is supported).");
175
+ if (semantic === "NORMAL" || semantic === "TANGENT") {
176
+ for (let v = 0; v < a.definition.count; v++) {
177
+ const start = v * a.components;
178
+ ctx.check(Math.abs(a.values
179
+ .slice(start, start + 3)
180
+ .reduce((sum, n) => sum + n * n, 0) - 1) <= 0.01, loc + "/" + semantic, "Normal/tangent vectors must have unit length.");
181
+ if (semantic === "TANGENT")
182
+ ctx.check(Math.abs(a.values[start + 3]) === 1, loc + "/TANGENT", "Tangent handedness must be +1 or -1.");
183
+ }
184
+ }
185
+ if (/^COLOR_/.test(semantic) && a.definition.componentType === 5126)
186
+ ctx.check(a.values.every((v) => v >= 0 && v <= 1), loc + "/" + semantic, "Vertex colors must be in [0,1].");
187
+ const view = a.definition.bufferView === undefined
188
+ ? undefined
189
+ : json.bufferViews[a.definition.bufferView];
190
+ if (view?.target !== undefined)
191
+ ctx.check(view.target === 34962, loc + "/" + semantic, "Vertex attribute buffer view must target ARRAY_BUFFER.");
192
+ if (view)
193
+ ctx.check(((view.byteOffset ?? 0) + (a.definition.byteOffset ?? 0)) % 4 ===
194
+ 0 &&
195
+ (view.byteStride ??
196
+ a.components *
197
+ {
198
+ 5120: 1,
199
+ 5121: 1,
200
+ 5122: 2,
201
+ 5123: 2,
202
+ 5125: 4,
203
+ 5126: 4,
204
+ }[a.definition.componentType]) %
205
+ 4 ===
206
+ 0, loc + "/" + semantic, "Vertex attribute offset and stride must be four-byte aligned.");
207
+ }
208
+ ctx.check((p.attributes.JOINTS_0 === undefined) ===
209
+ (p.attributes.WEIGHTS_0 === undefined), loc, "JOINTS_0 and WEIGHTS_0 must occur together.");
210
+ let elementCount = count;
211
+ if (p.indices !== undefined) {
212
+ const indices = accessor(p.indices, loc + "/indices", "SCALAR", [5121, 5123, 5125]);
213
+ ctx.check(!indices.definition.normalized &&
214
+ indices.values.every((v) => v < count &&
215
+ v !==
216
+ { 5121: 255, 5123: 65535, 5125: 4294967295 }[indices.definition.componentType]), loc + "/indices", "Index exceeds vertex count or uses forbidden primitive restart value.");
217
+ if (indices.definition.bufferView !== undefined) {
218
+ const view = json.bufferViews[indices.definition.bufferView];
219
+ ctx.check(view.byteStride === undefined &&
220
+ (view.target === undefined || view.target === 34963), loc + "/indices", "Index buffer view must not have stride and must target ELEMENT_ARRAY_BUFFER if target is specified.");
221
+ }
222
+ elementCount = indices.definition.count;
223
+ }
224
+ const mode = ctx.integer(p.mode ?? 4, loc + "/mode", 0, 6);
225
+ ctx.check(mode !== 4 || elementCount % 3 === 0, loc, "Triangle list element count must be divisible by three.");
226
+ ctx.check(mode !== 1 || elementCount % 2 === 0, loc, "Line list element count must be divisible by two.");
227
+ ctx.check(![2, 3].includes(mode) || elementCount >= 2, loc, "Line loop/strip requires at least two elements.");
228
+ ctx.check(![5, 6].includes(mode) || elementCount >= 3, loc, "Triangle strip/fan requires at least three elements.");
229
+ const triangles = mode === 4
230
+ ? elementCount / 3
231
+ : [5, 6].includes(mode)
232
+ ? elementCount - 2
233
+ : 0;
234
+ if (p.material !== undefined) {
235
+ ref(materials, p.material, loc + "/material");
236
+ const m = json.materials[p.material];
237
+ const specular = m.extensions?.KHR_materials_specular;
238
+ for (const info of [
239
+ m.pbrMetallicRoughness?.baseColorTexture,
240
+ m.pbrMetallicRoughness?.metallicRoughnessTexture,
241
+ m.normalTexture,
242
+ m.occlusionTexture,
243
+ m.emissiveTexture,
244
+ specular?.specularTexture,
245
+ specular?.specularColorTexture,
246
+ ])
247
+ if (info)
248
+ ctx.check(p.attributes[`TEXCOORD_${info.texCoord ?? 0}`] !== undefined, loc + "/material", "Textured material references a missing texture coordinate set.");
249
+ }
250
+ if (p.targets !== undefined) {
251
+ ctx.check(Array.isArray(p.targets) && p.targets.length > 0, loc + "/targets", "Expected morph targets.");
252
+ for (const [t, target] of p.targets.entries()) {
253
+ ctx.check(target &&
254
+ typeof target === "object" &&
255
+ Object.keys(target).length > 0, loc + `/targets/${t}`, "Empty morph target.");
256
+ for (const [semantic, index] of Object.entries(target)) {
257
+ ctx.check(["POSITION", "NORMAL", "TANGENT"].includes(semantic), loc + `/targets/${t}`, "Invalid morph target semantic.");
258
+ const a = accessor(index, loc + `/targets/${t}/${semantic}`, "VEC3", [5126]);
259
+ ctx.check(!a.definition.normalized &&
260
+ a.definition.count === count &&
261
+ p.attributes[semantic] !== undefined, loc + `/targets/${t}`, "Morph target count/type or base attribute is invalid.");
262
+ }
263
+ }
264
+ }
265
+ vertexCount += count;
266
+ primitiveCount++;
267
+ triangleCount += triangles;
268
+ return {
269
+ vertexCount: count,
270
+ elementCount,
271
+ triangleCount: triangles,
272
+ mode,
273
+ material: p.material ?? null,
274
+ morphTargetCount: p.targets?.length ?? 0,
275
+ bounds: { min: position.min, max: position.max },
276
+ };
277
+ });
278
+ ctx.check(primitives.every((p) => p.morphTargetCount === primitives[0].morphTargetCount), at, "All mesh primitives must have the same number of morph targets.");
279
+ if (mesh.weights !== undefined) {
280
+ ctx.check(primitives[0].morphTargetCount > 0, at + "/weights", "Mesh weights require morph targets.");
281
+ vector(mesh.weights, primitives[0].morphTargetCount, at + "/weights");
282
+ }
283
+ return {
284
+ index: i,
285
+ name: mesh.name ?? "",
286
+ primitives,
287
+ morphTargetCount: primitives[0].morphTargetCount,
288
+ };
289
+ });
290
+ const parents = new Map();
291
+ for (const [i, n] of (json.nodes ?? []).entries()) {
292
+ const at = `nodes/${i}`;
293
+ if (n.mesh !== undefined)
294
+ ref(meshes, n.mesh, at + "/mesh");
295
+ if (n.camera !== undefined)
296
+ ctx.unsupported(at + "/camera", "Camera nodes are outside the implemented subset.");
297
+ ctx.check(n.matrix === undefined ||
298
+ (n.translation === undefined &&
299
+ n.rotation === undefined &&
300
+ n.scale === undefined), at, "Node cannot combine matrix with TRS transforms.");
301
+ if (n.matrix !== undefined) {
302
+ const m = vector(n.matrix, 16, at + "/matrix");
303
+ ctx.check(m[3] === 0 && m[7] === 0 && m[11] === 0 && m[15] === 1, at + "/matrix", "Node transform must be affine.");
304
+ for (const [a, b] of [
305
+ [0, 4],
306
+ [0, 8],
307
+ [4, 8],
308
+ ]) {
309
+ const lengthA = Math.hypot(...m.slice(a, a + 3)), lengthB = Math.hypot(...m.slice(b, b + 3));
310
+ if (lengthA && lengthB)
311
+ ctx.check(Math.abs((m[a] * m[b] + m[a + 1] * m[b + 1] + m[a + 2] * m[b + 2]) /
312
+ lengthA /
313
+ lengthB) <= 1e-5, at + "/matrix", "Node matrix contains shear and cannot be decomposed to TRS.");
314
+ }
315
+ }
316
+ if (n.translation !== undefined)
317
+ vector(n.translation, 3, at + "/translation");
318
+ if (n.scale !== undefined)
319
+ vector(n.scale, 3, at + "/scale");
320
+ if (n.rotation !== undefined) {
321
+ const q = vector(n.rotation, 4, at + "/rotation", -1, 1);
322
+ ctx.check(Math.abs(q.reduce((s, v) => s + v * v, 0) - 1) < 1e-4, at + "/rotation", "Node quaternion is not normalized.");
323
+ }
324
+ if (n.weights !== undefined) {
325
+ const mesh = ref(meshes, n.mesh, at + "/mesh");
326
+ ctx.check(mesh.morphTargetCount > 0, at + "/weights", "Node weights require morph targets.");
327
+ vector(n.weights, mesh.morphTargetCount, at + "/weights");
328
+ }
329
+ if (n.children !== undefined) {
330
+ ctx.check(Array.isArray(n.children) && n.children.length > 0, at + "/children", "Expected nonempty child node references.");
331
+ for (const child of n.children) {
332
+ ref(json.nodes, child, at + "/children");
333
+ ctx.check(child !== i && !parents.has(child), at + "/children", "Node has itself or a duplicate/multiple parent.");
334
+ parents.set(child, i);
335
+ }
336
+ }
337
+ }
338
+ // Iterative parent walk avoids stack overflow on cyclic or very deep graphs.
339
+ const visited = new Set();
340
+ for (let i = 0; i < (json.nodes?.length ?? 0); i++) {
341
+ const chain = new Set();
342
+ let node = i;
343
+ while (node !== undefined && !visited.has(node)) {
344
+ ctx.check(!chain.has(node), `nodes/${i}`, "Node hierarchy contains a cycle.");
345
+ chain.add(node);
346
+ node = parents.get(node);
347
+ }
348
+ for (const v of chain)
349
+ visited.add(v);
350
+ }
351
+ const skins = (json.skins ?? []).map((s, i) => {
352
+ const at = `skins/${i}`;
353
+ ctx.check(Array.isArray(s.joints) &&
354
+ s.joints.length > 0 &&
355
+ new Set(s.joints).size === s.joints.length, at + "/joints", "Skin requires unique joint references.");
356
+ for (const j of s.joints)
357
+ ref(json.nodes, j, at + "/joints");
358
+ const rootOf = (j) => {
359
+ while (parents.has(j))
360
+ j = parents.get(j);
361
+ return j;
362
+ };
363
+ ctx.check(s.joints.every((j) => rootOf(j) === rootOf(s.joints[0])), at, "Skin joints do not share a common root.");
364
+ if (s.skeleton !== undefined) {
365
+ ref(json.nodes, s.skeleton, at + "/skeleton");
366
+ ctx.check(s.joints.every((j) => {
367
+ while (j !== s.skeleton && parents.has(j))
368
+ j = parents.get(j);
369
+ return j === s.skeleton;
370
+ }), at + "/skeleton", "Skeleton root is not an ancestor of all joints.");
371
+ }
372
+ if (s.inverseBindMatrices !== undefined) {
373
+ const a = accessor(s.inverseBindMatrices, at + "/inverseBindMatrices", "MAT4", [5126]);
374
+ ctx.check(!a.definition.normalized && a.definition.count === s.joints.length, at + "/inverseBindMatrices", "Inverse bind matrix count does not match joint count.");
375
+ }
376
+ return {
377
+ index: i,
378
+ name: s.name ?? "",
379
+ jointCount: s.joints.length,
380
+ joints: [...s.joints],
381
+ skeleton: s.skeleton ?? null,
382
+ inverseBindMatrices: s.inverseBindMatrices ?? null,
383
+ };
384
+ });
385
+ for (const [i, n] of (json.nodes ?? []).entries())
386
+ if (n.skin !== undefined) {
387
+ const at = `nodes/${i}/skin`, skin = ref(skins, n.skin, at);
388
+ const mesh = ref(json.meshes, n.mesh, `nodes/${i}/mesh`);
389
+ for (const [k, p] of mesh.primitives.entries()) {
390
+ const joints = accessor(p.attributes.JOINTS_0, at + `/primitives/${k}/JOINTS_0`);
391
+ const weights = accessor(p.attributes.WEIGHTS_0, at + `/primitives/${k}/WEIGHTS_0`);
392
+ ctx.check(joints.values.every((j) => j < skin.jointCount), at, "Vertex references a joint outside the skin.");
393
+ const denominator = weights.definition.componentType === 5121
394
+ ? 255
395
+ : weights.definition.componentType === 5123
396
+ ? 65535
397
+ : 1;
398
+ for (let v = 0; v < weights.definition.count; v++) {
399
+ let sum = 0;
400
+ for (let c = 0; c < 4; c++) {
401
+ const w = weights.values[v * 4 + c] / denominator;
402
+ ctx.check(w >= 0 && w <= 1, at, "Skin weights must be in [0,1].");
403
+ sum += w;
404
+ }
405
+ ctx.check(Math.abs(sum - 1) <= 0.01, at, "Skin vertex weights do not sum to one.");
406
+ }
407
+ }
408
+ }
409
+ for (const [i, s] of (json.scenes ?? []).entries())
410
+ if (s.nodes !== undefined) {
411
+ ctx.check(Array.isArray(s.nodes) &&
412
+ s.nodes.length > 0 &&
413
+ new Set(s.nodes).size === s.nodes.length, `scenes/${i}/nodes`, "Scene roots must be unique node references.");
414
+ for (const n of s.nodes) {
415
+ ref(json.nodes, n, `scenes/${i}/nodes`);
416
+ ctx.check(!parents.has(n), `scenes/${i}/nodes`, "Scene root node has a parent.");
417
+ }
418
+ }
419
+ if (json.scene !== undefined)
420
+ ref(json.scenes, json.scene, "scene");
421
+ const animations = (json.animations ?? []).map((a, i) => {
422
+ const at = `animations/${i}`;
423
+ ctx.check(Array.isArray(a.samplers) &&
424
+ a.samplers.length > 0 &&
425
+ Array.isArray(a.channels) &&
426
+ a.channels.length > 0, at, "Animation requires samplers and channels.");
427
+ const samplers = a.samplers.map((s, k) => {
428
+ ctx.check(s && typeof s === "object", at + `/samplers/${k}`, "Invalid animation sampler.");
429
+ const input = accessor(s.input, at + `/samplers/${k}/input`, "SCALAR", [
430
+ 5126,
431
+ ]);
432
+ ctx.check(!input.definition.normalized &&
433
+ input.definition.min &&
434
+ input.definition.max &&
435
+ input.values.every((v, j) => v >= 0 && (j === 0 || v > input.values[j - 1])), at + `/samplers/${k}/input`, "Animation times must be nonnegative, strictly increasing floats with verified bounds.");
436
+ const output = accessor(s.output, at + `/samplers/${k}/output`, undefined, [5126]);
437
+ const interpolation = s.interpolation ?? "LINEAR";
438
+ ctx.check(["LINEAR", "STEP", "CUBICSPLINE"].includes(interpolation), at + `/samplers/${k}/interpolation`, "Unknown animation interpolation.");
439
+ ctx.check(!output.definition.normalized, at + `/samplers/${k}/output`, "Animation outputs must not be normalized.");
440
+ return { input, output, interpolation };
441
+ });
442
+ const targets = new Set();
443
+ let start = Infinity, end = -Infinity;
444
+ const channels = a.channels.map((c, k) => {
445
+ const loc = at + `/channels/${k}`;
446
+ ctx.check(c && typeof c === "object" && c.target && typeof c.target === "object", loc, "Invalid animation channel target.");
447
+ const node = ref(json.nodes, c.target.node, loc + "/target/node");
448
+ const s = ref(samplers, c.sampler, loc + "/sampler"), path = c.target.path;
449
+ ctx.check(["translation", "rotation", "scale", "weights"].includes(path), loc + "/target/path", "Unknown animation target path.");
450
+ const target = `${c.target.node}/${path}`;
451
+ ctx.check(!targets.has(target), loc, "Duplicate animation channel for the same node/path.");
452
+ targets.add(target);
453
+ ctx.check(node.matrix === undefined, loc, "Animation target node must use TRS, not matrix.");
454
+ let targetCount = 1;
455
+ if (path === "weights") {
456
+ const mesh = ref(meshes, node.mesh, loc + "/target/mesh");
457
+ targetCount = mesh.morphTargetCount;
458
+ ctx.check(targetCount > 0, loc, "Weights animation requires morph targets.");
459
+ }
460
+ const type = path === "weights" ? "SCALAR" : path === "rotation" ? "VEC4" : "VEC3";
461
+ ctx.check(s.output.definition.type === type &&
462
+ s.output.definition.componentType === 5126 &&
463
+ s.output.definition.count ===
464
+ s.input.definition.count *
465
+ targetCount *
466
+ (s.interpolation === "CUBICSPLINE" ? 3 : 1), loc + "/output", "Animation output type/count does not match input, target and interpolation.");
467
+ if (path === "rotation") {
468
+ const step = s.interpolation === "CUBICSPLINE" ? 12 : 4, first = s.interpolation === "CUBICSPLINE" ? 4 : 0;
469
+ for (let p = first; p < s.output.values.length; p += step)
470
+ ctx.check(Math.abs(s.output.values
471
+ .slice(p, p + 4)
472
+ .reduce((sum, v) => sum + v * v, 0) - 1) < 1e-3, loc + "/rotation", "Animation rotation quaternion is not normalized.");
473
+ }
474
+ start = Math.min(start, s.input.values[0]);
475
+ end = Math.max(end, s.input.values.at(-1));
476
+ return {
477
+ node: c.target.node,
478
+ path,
479
+ sampler: c.sampler,
480
+ interpolation: s.interpolation,
481
+ keyframeCount: s.input.definition.count,
482
+ };
483
+ });
484
+ return {
485
+ index: i,
486
+ name: a.name || `animation_${i}`,
487
+ channelCount: channels.length,
488
+ samplerCount: samplers.length,
489
+ keyframeCount: samplers.reduce((n, s) => n + s.input.definition.count, 0),
490
+ start,
491
+ end,
492
+ duration: end - start,
493
+ channels,
494
+ };
495
+ });
496
+ // The mature reader is used only after resource and semantic validation; no URI is read.
497
+ const document = await new NodeIO()
498
+ .registerExtensions([KHRMaterialsIOR, KHRMaterialsSpecular])
499
+ .setStrictResources(true).readBinary(data);
500
+ const sceneIndex = json.scene ?? (json.scenes?.length ? 0 : null);
501
+ let bounds = null;
502
+ if (sceneIndex !== null) {
503
+ const stack = [
504
+ ...document.getRoot().listScenes()[sceneIndex].listChildren(),
505
+ ];
506
+ while (stack.length) {
507
+ const node = stack.pop();
508
+ stack.push(...node.listChildren());
509
+ const m = node.getWorldMatrix();
510
+ for (const primitive of node.getMesh()?.listPrimitives() ?? []) {
511
+ const position = primitive.getAttribute("POSITION");
512
+ const v = [];
513
+ for (let i = 0; i < position.getCount(); i++) {
514
+ position.getElement(i, v);
515
+ const point = [
516
+ m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12],
517
+ m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13],
518
+ m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14],
519
+ ];
520
+ ctx.check(point.every(Number.isFinite), "bounds", "World transform produces non-finite geometry bounds.");
521
+ if (!bounds)
522
+ bounds = { min: [...point], max: [...point] };
523
+ else
524
+ for (let c = 0; c < 3; c++) {
525
+ bounds.min[c] = Math.min(bounds.min[c], point[c]);
526
+ bounds.max[c] = Math.max(bounds.max[c], point[c]);
527
+ }
528
+ }
529
+ }
530
+ }
531
+ }
532
+ return {
533
+ format: "glb",
534
+ version: "2.0",
535
+ meshCount: meshes.length,
536
+ primitiveCount,
537
+ vertexCount,
538
+ triangleCount,
539
+ nodeCount: json.nodes?.length ?? 0,
540
+ sceneCount: json.scenes?.length ?? 0,
541
+ materialCount: materials.length,
542
+ textureCount: textures.length,
543
+ imageCount: images.length,
544
+ skinCount: skins.length,
545
+ jointCount: skins.reduce((n, s) => n + s.jointCount, 0),
546
+ animationCount: animations.length,
547
+ meshes,
548
+ materials,
549
+ textures,
550
+ images,
551
+ skins,
552
+ animations,
553
+ bounds,
554
+ boundsScene: sceneIndex,
555
+ boundsSpace: "world-static-default-pose",
556
+ bufferBytes: bin.length,
557
+ };
558
+ }
@@ -0,0 +1,95 @@
1
+ import { NodeIO, Logger } from "@gltf-transform/core";
2
+ import { KHRMaterialsIOR, KHRMaterialsSpecular } from "@gltf-transform/extensions";
3
+ import { dedup, weld } from "@gltf-transform/functions";
4
+ import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { inspectAsset } from "./index.js";
7
+ import { createHash } from "node:crypto";
8
+ import { parseGLB } from "./glb.js";
9
+ import { InspectionContext } from "./errors.js";
10
+ /** Explicit lossless preprocessing. Source files and existing outputs are never overwritten. */
11
+ export async function optimizeModel(input, output) {
12
+ if (path.resolve(input).toLowerCase() === path.resolve(output).toLowerCase())
13
+ throw new Error("TC_ASSET_OPTIMIZE_PATH: Choose a new GLB output path.");
14
+ if (path.extname(output).toLowerCase() !== ".glb")
15
+ throw new Error("TC_ASSET_OPTIMIZE_PATH: Output must use .glb.");
16
+ const before = await inspectAsset(input, "model");
17
+ const sourceBytes = await readFile(input);
18
+ const io = new NodeIO().registerExtensions([KHRMaterialsIOR, KHRMaterialsSpecular]);
19
+ const document = await io.readBinary(sourceBytes);
20
+ document.setLogger(new Logger(Logger.Verbosity.SILENT));
21
+ // Weld compares the complete vertex stream bit-for-bit; names remain stable for animations and author references.
22
+ await document.transform(dedup({ keepUniqueNames: true }), weld());
23
+ const content = await io.writeBinary(document);
24
+ await mkdir(path.dirname(path.resolve(output)), { recursive: true });
25
+ try {
26
+ await writeFile(output, content, { flag: "wx" });
27
+ }
28
+ catch (error) {
29
+ if (error.code === "EEXIST")
30
+ throw new Error("TC_ASSET_OPTIMIZE_EXISTS: Output already exists; choose a new path.");
31
+ throw error;
32
+ }
33
+ try {
34
+ const after = await inspectAsset(output, "model");
35
+ const physicalExtensions = (metadata) => {
36
+ const materials = metadata.materials;
37
+ return [...new Set(materials.filter(m => m.specular || m.ior).map(m => JSON.stringify({ name: m.name,
38
+ ...(m.specular ? { specular: { intensity: m.specular.specularFactor ?? 1, color: m.specular.specularColorFactor ?? [1, 1, 1] } } : {}),
39
+ ...(m.ior ? { ior: m.ior.ior ?? 1.5 } : {}) })))].sort();
40
+ };
41
+ const specularTextures = (bytes) => {
42
+ const { json, bin } = parseGLB(bytes, new InspectionContext("optimization.glb"));
43
+ const binding = (info) => {
44
+ if (!info)
45
+ return null;
46
+ const texture = json.textures[info.index];
47
+ const image = json.images[texture.source];
48
+ const view = json.bufferViews[image.bufferView];
49
+ const sampler = json.samplers?.[texture.sampler];
50
+ const start = view.byteOffset ?? 0;
51
+ return {
52
+ hash: createHash("sha256").update(bin.subarray(start, start + view.byteLength)).digest("hex"),
53
+ mimeType: image.mimeType,
54
+ texCoord: info.texCoord ?? 0,
55
+ magFilter: sampler?.magFilter ?? null,
56
+ minFilter: sampler?.minFilter ?? null,
57
+ wrapS: sampler?.wrapS ?? 10497,
58
+ wrapT: sampler?.wrapT ?? 10497,
59
+ };
60
+ };
61
+ return [...new Set((json.materials ?? []).filter(m => m.extensions?.KHR_materials_specular).map(m => {
62
+ const specular = m.extensions.KHR_materials_specular;
63
+ return JSON.stringify({ name: m.name ?? "", intensity: binding(specular.specularTexture), color: binding(specular.specularColorTexture) });
64
+ }))].sort();
65
+ };
66
+ if (before.triangleCount !== after.triangleCount ||
67
+ JSON.stringify(before.bounds) !== JSON.stringify(after.bounds) ||
68
+ JSON.stringify(before.animations) !== JSON.stringify(after.animations) ||
69
+ before.skinCount !== after.skinCount ||
70
+ before.jointCount !== after.jointCount ||
71
+ JSON.stringify(physicalExtensions(before)) !== JSON.stringify(physicalExtensions(after)) ||
72
+ JSON.stringify(specularTextures(sourceBytes)) !== JSON.stringify(specularTextures(Buffer.from(content))))
73
+ throw new Error("TC_ASSET_OPTIMIZE_INVARIANT: Geometry bounds, triangles, skin, animation or physical material extension metadata changed.");
74
+ return {
75
+ format: "pelican.asset-optimization",
76
+ version: 1,
77
+ operations: [
78
+ "dedup-unique-names-preserved",
79
+ "weld-bitwise-identical-vertex-streams",
80
+ ],
81
+ before,
82
+ after,
83
+ bytesSaved: Number(before.bytes) - Number(after.bytes),
84
+ ratio: Number(after.bytes) / Number(before.bytes),
85
+ limitations: [
86
+ "No quantization, mesh compression or texture transcoding. Explicit lossless preprocessing only.",
87
+ "The source is unchanged. Update the game asset path explicitly to adopt this output.",
88
+ ],
89
+ };
90
+ }
91
+ catch (error) {
92
+ await rm(output);
93
+ throw error;
94
+ }
95
+ }