lecodes-assets 0.2.1 → 2.5.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.
Files changed (99) hide show
  1. package/README.md +203 -26
  2. package/dist/cli.js +18157 -11441
  3. package/dist/index.js +16603 -9878
  4. package/package.json +3 -1
  5. package/src/cli-kit/command.ts +100 -0
  6. package/src/cli-kit/help.ts +72 -0
  7. package/src/cli-kit/index.ts +4 -0
  8. package/src/cli-kit/parse.ts +72 -0
  9. package/src/cli-kit/run.ts +52 -0
  10. package/src/cli.ts +24 -1039
  11. package/src/commands/avatar.ts +77 -0
  12. package/src/commands/bvh.ts +26 -0
  13. package/src/commands/convert.ts +156 -0
  14. package/src/commands/doctor.ts +79 -0
  15. package/src/commands/geometry.ts +45 -0
  16. package/src/commands/index.ts +24 -0
  17. package/src/commands/marks.ts +38 -0
  18. package/src/commands/mirror.ts +35 -0
  19. package/src/commands/probe.ts +43 -0
  20. package/src/commands/retarget/donor.ts +63 -0
  21. package/src/commands/retarget/index.ts +169 -0
  22. package/src/commands/retarget/map.ts +81 -0
  23. package/src/commands/retarget/report.ts +77 -0
  24. package/src/commands/scene.ts +108 -0
  25. package/src/commands/shared.ts +140 -0
  26. package/src/commands/sky.ts +35 -0
  27. package/src/commands/terrainPack.ts +40 -0
  28. package/src/commands/unpack.ts +43 -0
  29. package/src/commands/unreal.ts +109 -0
  30. package/src/commands/unwrap.ts +56 -0
  31. package/src/doctor/cardTrim.ts +288 -0
  32. package/src/doctor/doctor.ts +94 -30
  33. package/src/doctor/impostor.ts +614 -0
  34. package/src/doctor/merge/accessors.ts +30 -0
  35. package/src/doctor/merge/atlasLayout.ts +94 -0
  36. package/src/doctor/merge/atlasPixels.ts +114 -0
  37. package/src/doctor/merge/bindSpace.ts +59 -0
  38. package/src/doctor/merge/classify.ts +136 -0
  39. package/src/doctor/merge/geometry.ts +167 -0
  40. package/src/doctor/merge/index.ts +98 -0
  41. package/src/doctor/merge/matrix.ts +60 -0
  42. package/src/doctor/merge/plan.ts +88 -0
  43. package/src/doctor/merge/textures.ts +60 -0
  44. package/src/doctor/merge/types.ts +77 -0
  45. package/src/doctor/transcodeKtx2.ts +186 -31
  46. package/src/doctor/transcodeTextures.ts +6 -1
  47. package/src/doctor/validateGlb.ts +2 -2
  48. package/src/doctor/vertexAo.ts +333 -0
  49. package/src/fbx/read.ts +2 -0
  50. package/src/fbx/toDocument.ts +227 -209
  51. package/src/geometry.ts +222 -0
  52. package/src/index.ts +2 -0
  53. package/src/retarget/heading.ts +238 -171
  54. package/src/retarget/humanoid.ts +572 -561
  55. package/src/retarget/retarget.ts +2375 -2272
  56. package/src/retarget/rig.ts +4 -1
  57. package/src/retarget/writeMarks.ts +134 -132
  58. package/src/sky/cmgen.ts +3 -3
  59. package/src/sky/index.ts +57 -8
  60. package/src/terrainPack.ts +61 -22
  61. package/src/unity/colliderFit.ts +287 -0
  62. package/src/unity/fbxModel.ts +194 -0
  63. package/src/unity/foliage.ts +63 -0
  64. package/src/unity/gameObjects.ts +90 -0
  65. package/src/unity/guid.ts +104 -0
  66. package/src/unity/image.ts +58 -0
  67. package/src/unity/kindGlb.ts +364 -0
  68. package/src/unity/lecodesLevel.ts +414 -0
  69. package/src/unity/levelTerrain.ts +125 -0
  70. package/src/unity/matFile.ts +179 -0
  71. package/src/unity/materialSlots.ts +53 -0
  72. package/src/unity/materialWriter.ts +316 -0
  73. package/src/unity/materials.ts +34 -429
  74. package/src/unity/math.ts +82 -0
  75. package/src/unity/prefabTree.ts +285 -0
  76. package/src/unity/prefabs.ts +10 -36
  77. package/src/unity/propertyPath.ts +43 -0
  78. package/src/unity/scene.ts +339 -304
  79. package/src/unity/serialized.ts +242 -0
  80. package/src/unity/terrain.ts +221 -0
  81. package/src/unity/triBudget.ts +77 -0
  82. package/src/unity/triplanar.ts +122 -0
  83. package/src/unity/unityMesh.ts +151 -0
  84. package/src/unity/unpack.ts +24 -5
  85. package/src/unity/yaml.ts +131 -0
  86. package/src/unreal/index.ts +6 -5
  87. package/src/unreal/materialGraph.ts +224 -0
  88. package/src/unreal/materials.ts +117 -9
  89. package/src/unreal/package.ts +30 -12
  90. package/src/unreal/resolve.ts +6 -4
  91. package/src/unreal/sound.ts +128 -0
  92. package/src/unreal/texture.ts +70 -3
  93. package/src/unreal/toDocument.ts +7 -0
  94. package/src/unreal/versions.ts +8 -0
  95. package/src/unwrap/index.ts +167 -0
  96. package/src/unwrap/lightmapUv.ts +761 -0
  97. package/src/wasm/fbxReader.js +1 -1
  98. package/src/wasm/lightmapUv.js +1 -1
  99. package/src/doctor/lightmapUv.ts +0 -452
@@ -0,0 +1,222 @@
1
+ // geometry — a GLB's triangles as ONE plain vertex buffer the SDK's `Geometry` can hold
2
+ // (`lecodes assets geometry casing.glb` → casing.geometry.json → `Geometry.load(asset(...))`).
3
+ // Made for small debris the particle system draws per particle (shell casings, rubble, coins):
4
+ // every mesh node's world transform is baked in, primitives are merged, and the result is what
5
+ // `Geometry` carries — positions, normals, one UV set, uint16 indices. Materials, skins and
6
+ // animations are dropped on purpose: the particle material is the SDK's.
7
+
8
+ import type { Document, Node, Primitive } from "@gltf-transform/core"
9
+
10
+ export type GeometryFile = {
11
+ format: "lecodes-geometry"
12
+ version: 1
13
+ /** xyz per vertex, metres, Y-up. */
14
+ vertices: number[]
15
+ /** xyz per vertex, unit length. */
16
+ normals: number[]
17
+ /** uv per vertex (zeros when the source has no TEXCOORD_0). */
18
+ uv: number[]
19
+ /** Triangle list, 3 per face, counter-clockwise front faces. */
20
+ indices: number[]
21
+ bounds: { min: [number, number, number], max: [number, number, number] }
22
+ /** What went in — for the reader's eyes only. */
23
+ source?: { file?: string, nodes: string[] }
24
+ }
25
+
26
+ export type ExtractOptions = {
27
+ /** Take only this node's subtree (name, exact match). Default: every mesh in the scene. */
28
+ node?: string
29
+ /** Re-origin the result: `center` = bounds centre at 0, `bottom` = footprint centre at X/Z = 0
30
+ * and the lowest point at Y = 0. Default: keep the file's placement. */
31
+ origin?: "keep" | "center" | "bottom"
32
+ /** Uniform scale applied after the origin move (a casing modelled in cm → 0.01). */
33
+ scale?: number
34
+ /** Significant digits written to the file (default 6). */
35
+ precision?: number
36
+ }
37
+
38
+ export type ExtractReport = {
39
+ nodes: string[]
40
+ vertices: number
41
+ triangles: number
42
+ size: [number, number, number]
43
+ warnings: string[]
44
+ }
45
+
46
+ /** The SDK caps a Geometry's index buffer at uint16, and the particle vertex stream at ~1 M
47
+ * vertices per system — a debris mesh past this is the wrong asset for the job anyway. */
48
+ export const MAX_VERTICES = 65535
49
+
50
+ type Mat4 = number[]
51
+
52
+ const mul3 = (m: Mat4, x: number, y: number, z: number, w: number): [number, number, number] => [
53
+ m[0] * x + m[4] * y + m[8] * z + m[12] * w,
54
+ m[1] * x + m[5] * y + m[9] * z + m[13] * w,
55
+ m[2] * x + m[6] * y + m[10] * z + m[14] * w,
56
+ ]
57
+
58
+ /** Inverse-transpose of the upper 3×3 (row-major 3×3 for `mulN`), null when singular. */
59
+ const normalMatrix = (m: Mat4): number[] | null => {
60
+ const a = m[0], b = m[4], c = m[8], d = m[1], e = m[5], f = m[9], g = m[2], h = m[6], i = m[10]
61
+ const det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)
62
+ if (Math.abs(det) < 1e-20) return null
63
+ const inv = [
64
+ (e * i - f * h) / det, (c * h - b * i) / det, (b * f - c * e) / det,
65
+ (f * g - d * i) / det, (a * i - c * g) / det, (c * d - a * f) / det,
66
+ (d * h - e * g) / det, (b * g - a * h) / det, (a * e - b * d) / det,
67
+ ]
68
+ // transpose(inverse): n' = inv^T · n → n'[r] = Σ inv[c*3 + r] · n[c]
69
+ return [ inv[0], inv[3], inv[6], inv[1], inv[4], inv[7], inv[2], inv[5], inv[8] ]
70
+ }
71
+
72
+ const det3 = (m: Mat4): number =>
73
+ m[0] * (m[5] * m[10] - m[6] * m[9]) - m[4] * (m[1] * m[10] - m[2] * m[9]) + m[8] * (m[1] * m[6] - m[2] * m[5])
74
+
75
+ const meshNodes = (doc: Document, only?: string): { nodes: Node[], missing: boolean } => {
76
+ const all = doc.getRoot().listNodes()
77
+ if (!only) return { nodes: all.filter((n) => n.getMesh()), missing: false }
78
+ const root = all.find((n) => n.getName() === only)
79
+ if (!root) return { nodes: [], missing: true }
80
+ const out: Node[] = []
81
+ root.traverse((n) => { if (n.getMesh()) out.push(n) })
82
+ return { nodes: out, missing: false }
83
+ }
84
+
85
+ const round = (v: number, precision: number): number => {
86
+ const r = Number(v.toPrecision(precision))
87
+ return r === 0 ? 0 : r // no "-0" in the file
88
+ }
89
+
90
+ /** Smooth, area-weighted vertex normals for a primitive that ships none. */
91
+ const computeNormals = (pos: Float32Array, idx: Uint32Array): Float32Array => {
92
+ const n = new Float32Array(pos.length)
93
+ for (let t = 0; t < idx.length; t += 3) {
94
+ const a = idx[t] * 3, b = idx[t + 1] * 3, c = idx[t + 2] * 3
95
+ const ux = pos[b] - pos[a], uy = pos[b + 1] - pos[a + 1], uz = pos[b + 2] - pos[a + 2]
96
+ const vx = pos[c] - pos[a], vy = pos[c + 1] - pos[a + 1], vz = pos[c + 2] - pos[a + 2]
97
+ const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx
98
+ for (const k of [ a, b, c ]) { n[k] += nx; n[k + 1] += ny; n[k + 2] += nz }
99
+ }
100
+ for (let i = 0; i < n.length; i += 3) {
101
+ const l = Math.hypot(n[i], n[i + 1], n[i + 2]) || 1
102
+ n[i] /= l; n[i + 1] /= l; n[i + 2] /= l
103
+ }
104
+ return n
105
+ }
106
+
107
+ const primitiveIndices = (prim: Primitive, count: number): Uint32Array => {
108
+ const acc = prim.getIndices()
109
+ if (acc) return Uint32Array.from(acc.getArray() as ArrayLike<number>)
110
+ const seq = new Uint32Array(count)
111
+ for (let i = 0; i < count; i++) seq[i] = i
112
+ return seq
113
+ }
114
+
115
+ /** The merged, world-space triangle buffer of `doc` (or of one node's subtree). */
116
+ export const extractGeometry = (doc: Document, opts: ExtractOptions = {}): { data: GeometryFile, report: ExtractReport } => {
117
+ const precision = opts.precision ?? 6
118
+ const warnings: string[] = []
119
+ const { nodes, missing } = meshNodes(doc, opts.node)
120
+ if (missing) throw new Error(`no node "${opts.node}" in the file (--list-nodes prints them)`)
121
+ if (nodes.length === 0) throw new Error(opts.node ? `node "${opts.node}" has no mesh` : "the file has no mesh")
122
+
123
+ const V: number[] = [], N: number[] = [], UV: number[] = [], I: number[] = []
124
+ const used: string[] = []
125
+ for (const node of nodes) {
126
+ const mesh = node.getMesh()!
127
+ const world = Array.from(node.getWorldMatrix()) as Mat4
128
+ const nm = normalMatrix(world)
129
+ if (!nm) { warnings.push(`${node.getName() || "(unnamed)"}: zero-scale transform, skipped`); continue }
130
+ const flip = det3(world) < 0
131
+ if (node.getSkin()) warnings.push(`${node.getName() || "(unnamed)"}: skinned — baked in its bind pose`)
132
+ let took = false
133
+ for (const prim of mesh.listPrimitives()) {
134
+ const mode = prim.getMode()
135
+ if (mode !== 4) { warnings.push(`${node.getName() || "(unnamed)"}: primitive mode ${mode} (not TRIANGLES) skipped`); continue }
136
+ const posAcc = prim.getAttribute("POSITION")
137
+ if (!posAcc) continue
138
+ const pos = Float32Array.from(posAcc.getArray() as ArrayLike<number>)
139
+ const count = pos.length / 3
140
+ const idx = primitiveIndices(prim, count)
141
+ const nrmAcc = prim.getAttribute("NORMAL")
142
+ const nrm = nrmAcc ? Float32Array.from(nrmAcc.getArray() as ArrayLike<number>) : computeNormals(pos, idx)
143
+ const uvAcc = prim.getAttribute("TEXCOORD_0")
144
+ const uv = uvAcc ? Float32Array.from(uvAcc.getArray() as ArrayLike<number>) : new Float32Array(count * 2)
145
+ if (uvAcc && uvAcc.getNormalized()) warnings.push(`${node.getName() || "(unnamed)"}: TEXCOORD_0 is a normalized integer set, read as floats`)
146
+ const base = V.length / 3
147
+ for (let i = 0; i < count; i++) {
148
+ const p = mul3(world, pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2], 1)
149
+ V.push(p[0], p[1], p[2])
150
+ const nx = nrm[i * 3], ny = nrm[i * 3 + 1], nz = nrm[i * 3 + 2]
151
+ let tx = nm[0] * nx + nm[1] * ny + nm[2] * nz
152
+ let ty = nm[3] * nx + nm[4] * ny + nm[5] * nz
153
+ let tz = nm[6] * nx + nm[7] * ny + nm[8] * nz
154
+ const l = Math.hypot(tx, ty, tz) || 1
155
+ tx /= l; ty /= l; tz /= l
156
+ N.push(tx, ty, tz)
157
+ UV.push(uv[i * 2], uv[i * 2 + 1])
158
+ }
159
+ for (let t = 0; t < idx.length; t += 3) {
160
+ // a mirrored transform turns the winding inside out — swap two corners to keep front faces
161
+ if (flip) I.push(base + idx[t], base + idx[t + 2], base + idx[t + 1])
162
+ else I.push(base + idx[t], base + idx[t + 1], base + idx[t + 2])
163
+ }
164
+ took = true
165
+ }
166
+ if (took) used.push(node.getName() || "(unnamed)")
167
+ }
168
+ const vertexCount = V.length / 3
169
+ if (vertexCount === 0) throw new Error("no triangles to take")
170
+ if (vertexCount > MAX_VERTICES) throw new Error(`${vertexCount} vertices — a Geometry holds at most ${MAX_VERTICES} (uint16 indices). Pick one node (--node) or decimate the asset first`)
171
+
172
+ // origin + scale, then bounds
173
+ const min: [number, number, number] = [ Infinity, Infinity, Infinity ], max: [number, number, number] = [ -Infinity, -Infinity, -Infinity ]
174
+ const bounds = (): void => {
175
+ min.fill(Infinity); max.fill(-Infinity)
176
+ for (let i = 0; i < V.length; i += 3) for (let k = 0; k < 3; k++) { min[k] = Math.min(min[k], V[i + k]); max[k] = Math.max(max[k], V[i + k]) }
177
+ }
178
+ bounds()
179
+ const origin = opts.origin ?? "keep"
180
+ if (origin !== "keep") {
181
+ const dx = (min[0] + max[0]) / 2, dz = (min[2] + max[2]) / 2
182
+ const dy = origin === "center" ? (min[1] + max[1]) / 2 : min[1]
183
+ for (let i = 0; i < V.length; i += 3) { V[i] -= dx; V[i + 1] -= dy; V[i + 2] -= dz }
184
+ }
185
+ const scale = opts.scale ?? 1
186
+ if (scale !== 1) for (let i = 0; i < V.length; i++) V[i] *= scale
187
+ bounds()
188
+
189
+ const data: GeometryFile = {
190
+ format: "lecodes-geometry",
191
+ version: 1,
192
+ vertices: V.map((v) => round(v, precision)),
193
+ normals: N.map((v) => round(v, 4)),
194
+ uv: UV.map((v) => round(v, precision)),
195
+ indices: I,
196
+ bounds: { min: min.map((v) => round(v, precision)) as typeof min, max: max.map((v) => round(v, precision)) as typeof max },
197
+ source: { nodes: used },
198
+ }
199
+ const report: ExtractReport = {
200
+ nodes: used, vertices: vertexCount, triangles: I.length / 3,
201
+ size: [ max[0] - min[0], max[1] - min[1], max[2] - min[2] ], warnings,
202
+ }
203
+ return { data, report }
204
+ }
205
+
206
+ /** Compact JSON: one number list per line, so the file diffs and reads like an asset, not a dump. */
207
+ export const formatGeometryFile = (g: GeometryFile): string => {
208
+ const list = (a: number[]): string => `[${a.join(",")}]`
209
+ return [
210
+ "{",
211
+ ` "format": ${JSON.stringify(g.format)},`,
212
+ ` "version": ${g.version},`,
213
+ ` "bounds": ${JSON.stringify(g.bounds)},`,
214
+ ...(g.source ? [ ` "source": ${JSON.stringify(g.source)},` ] : []),
215
+ ` "vertices": ${list(g.vertices)},`,
216
+ ` "normals": ${list(g.normals)},`,
217
+ ` "uv": ${list(g.uv)},`,
218
+ ` "indices": ${list(g.indices)}`,
219
+ "}",
220
+ "",
221
+ ].join("\n")
222
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@ export { convertFbx, mergeClipScene, writeGlb, makeIO, type ConvertOptions, type
6
6
  export { probeFile, probeFbx, probeDocument } from "./probe"
7
7
  export { analyzeDoc, applyPlan, reportPlan, dumpTextures, type DoctorPlan, type DoctorOptions } from "./doctor/doctor"
8
8
  export { validateGlb, formatReport } from "./doctor/validateGlb"
9
+ export { unwrapGlbFile, unwrapGlbFiles, unwrapDocument, planUnwrap, applyUnwrap, type UnwrapOptions, type UnwrapFileOptions, type UnwrapFileReport, type UnwrapResult } from "./unwrap"
10
+ export { planMerge, applyMerge, findKtxTool, type MergeOptions, type MergePlan, type MergeResult } from "./doctor/merge"
9
11
  export { measureScale, applyScale, sceneBounds, type ScaleInfo } from "./doctor/normalizeScale"
10
12
  export { loadRig, rigFromFbx, rigFromDocument, checkUniformScale, type Rig, type DocRig } from "./retarget/rig"
11
13
  export { retarget, analyzeFacing, facingMismatch, bindAccessors, type BoneMap, type RetargetOptions, type RetargetReport, type AlignPolicy, type DonorRef, type LegReport, type ClipContacts, type ContactSpan, type RetargetEvent } from "./retarget/retarget"