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.
- package/README.md +203 -26
- package/dist/cli.js +18157 -11441
- package/dist/index.js +16603 -9878
- package/package.json +3 -1
- package/src/cli-kit/command.ts +100 -0
- package/src/cli-kit/help.ts +72 -0
- package/src/cli-kit/index.ts +4 -0
- package/src/cli-kit/parse.ts +72 -0
- package/src/cli-kit/run.ts +52 -0
- package/src/cli.ts +24 -1039
- package/src/commands/avatar.ts +77 -0
- package/src/commands/bvh.ts +26 -0
- package/src/commands/convert.ts +156 -0
- package/src/commands/doctor.ts +79 -0
- package/src/commands/geometry.ts +45 -0
- package/src/commands/index.ts +24 -0
- package/src/commands/marks.ts +38 -0
- package/src/commands/mirror.ts +35 -0
- package/src/commands/probe.ts +43 -0
- package/src/commands/retarget/donor.ts +63 -0
- package/src/commands/retarget/index.ts +169 -0
- package/src/commands/retarget/map.ts +81 -0
- package/src/commands/retarget/report.ts +77 -0
- package/src/commands/scene.ts +108 -0
- package/src/commands/shared.ts +140 -0
- package/src/commands/sky.ts +35 -0
- package/src/commands/terrainPack.ts +40 -0
- package/src/commands/unpack.ts +43 -0
- package/src/commands/unreal.ts +109 -0
- package/src/commands/unwrap.ts +56 -0
- package/src/doctor/cardTrim.ts +288 -0
- package/src/doctor/doctor.ts +94 -30
- package/src/doctor/impostor.ts +614 -0
- package/src/doctor/merge/accessors.ts +30 -0
- package/src/doctor/merge/atlasLayout.ts +94 -0
- package/src/doctor/merge/atlasPixels.ts +114 -0
- package/src/doctor/merge/bindSpace.ts +59 -0
- package/src/doctor/merge/classify.ts +136 -0
- package/src/doctor/merge/geometry.ts +167 -0
- package/src/doctor/merge/index.ts +98 -0
- package/src/doctor/merge/matrix.ts +60 -0
- package/src/doctor/merge/plan.ts +88 -0
- package/src/doctor/merge/textures.ts +60 -0
- package/src/doctor/merge/types.ts +77 -0
- package/src/doctor/transcodeKtx2.ts +186 -31
- package/src/doctor/transcodeTextures.ts +6 -1
- package/src/doctor/validateGlb.ts +2 -2
- package/src/doctor/vertexAo.ts +333 -0
- package/src/fbx/read.ts +2 -0
- package/src/fbx/toDocument.ts +227 -209
- package/src/geometry.ts +222 -0
- package/src/index.ts +2 -0
- package/src/retarget/heading.ts +238 -171
- package/src/retarget/humanoid.ts +572 -561
- package/src/retarget/retarget.ts +2375 -2272
- package/src/retarget/rig.ts +4 -1
- package/src/retarget/writeMarks.ts +134 -132
- package/src/sky/cmgen.ts +3 -3
- package/src/sky/index.ts +57 -8
- package/src/terrainPack.ts +61 -22
- package/src/unity/colliderFit.ts +287 -0
- package/src/unity/fbxModel.ts +194 -0
- package/src/unity/foliage.ts +63 -0
- package/src/unity/gameObjects.ts +90 -0
- package/src/unity/guid.ts +104 -0
- package/src/unity/image.ts +58 -0
- package/src/unity/kindGlb.ts +364 -0
- package/src/unity/lecodesLevel.ts +414 -0
- package/src/unity/levelTerrain.ts +125 -0
- package/src/unity/matFile.ts +179 -0
- package/src/unity/materialSlots.ts +53 -0
- package/src/unity/materialWriter.ts +316 -0
- package/src/unity/materials.ts +34 -429
- package/src/unity/math.ts +82 -0
- package/src/unity/prefabTree.ts +285 -0
- package/src/unity/prefabs.ts +10 -36
- package/src/unity/propertyPath.ts +43 -0
- package/src/unity/scene.ts +339 -304
- package/src/unity/serialized.ts +242 -0
- package/src/unity/terrain.ts +221 -0
- package/src/unity/triBudget.ts +77 -0
- package/src/unity/triplanar.ts +122 -0
- package/src/unity/unityMesh.ts +151 -0
- package/src/unity/unpack.ts +24 -5
- package/src/unity/yaml.ts +131 -0
- package/src/unreal/index.ts +6 -5
- package/src/unreal/materialGraph.ts +224 -0
- package/src/unreal/materials.ts +117 -9
- package/src/unreal/package.ts +30 -12
- package/src/unreal/resolve.ts +6 -4
- package/src/unreal/sound.ts +128 -0
- package/src/unreal/texture.ts +70 -3
- package/src/unreal/toDocument.ts +7 -0
- package/src/unreal/versions.ts +8 -0
- package/src/unwrap/index.ts +167 -0
- package/src/unwrap/lightmapUv.ts +761 -0
- package/src/wasm/fbxReader.js +1 -1
- package/src/wasm/lightmapUv.js +1 -1
- package/src/doctor/lightmapUv.ts +0 -452
package/src/geometry.ts
ADDED
|
@@ -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"
|