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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// The lightmap unwrap of whole GLB files — `lecodes assets unwrap` (commands/unwrap.ts) and `lecodes lightmap bake`'s
|
|
2
|
+
// pre-step (the CLI reaches it through `unwrapGlbFiles` in src/cli.ts). The geometry work is ./lightmapUv.ts.
|
|
3
|
+
//
|
|
4
|
+
// IN PLACE, not `<name>.fixed.glb` like the doctor's passes: scenes reference the original file, so a fixed copy next
|
|
5
|
+
// to it was one the owner copied over the original by hand (and the copies stayed). A file whose every primitive the
|
|
6
|
+
// unwrap would lay out already carries TEXCOORD_1 is left alone (`force` re-unwraps it). The write is atomic: a temp
|
|
7
|
+
// file beside the target, renamed over it — a failed or killed run leaves the original as it was.
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs"
|
|
10
|
+
import { basename, dirname, join } from "node:path"
|
|
11
|
+
import type { Document, Material, TextureInfo } from "@gltf-transform/core"
|
|
12
|
+
import { ensureCodecs, makeIO } from "../doctor/reduceGlbBones"
|
|
13
|
+
import { applyUnwrap, planNeedsUnwrap, planUnwrap, type UnwrapOptions, type UnwrapPlan, type UnwrapResult } from "./lightmapUv"
|
|
14
|
+
|
|
15
|
+
export { applyUnwrap, planNeedsUnwrap, planUnwrap, DEFAULT_UNWRAP_TEXEL, UNWRAP_PADDING } from "./lightmapUv"
|
|
16
|
+
export type { UnwrapGroup, UnwrapOptions, UnwrapPlan, UnwrapResult } from "./lightmapUv"
|
|
17
|
+
|
|
18
|
+
export interface UnwrapFileOptions extends UnwrapOptions {
|
|
19
|
+
/** unwrap again even when every primitive already carries TEXCOORD_1 */
|
|
20
|
+
force?: boolean
|
|
21
|
+
/** write here instead of over the input */
|
|
22
|
+
output?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UnwrapFileReport {
|
|
26
|
+
input: string
|
|
27
|
+
/** where the unwrapped file went; null when nothing was written */
|
|
28
|
+
output: string | null
|
|
29
|
+
status: "unwrapped" | "skipped" | "failed"
|
|
30
|
+
/** why it was skipped or failed */
|
|
31
|
+
reason?: string
|
|
32
|
+
/** one line: what the unwrap made of the file (describeUnwrap) */
|
|
33
|
+
summary: string
|
|
34
|
+
result?: UnwrapResult
|
|
35
|
+
/** primitives the unwrap leaves as they are (skinned, not a triangle list, …) */
|
|
36
|
+
kept: { name: string, reason: string }[]
|
|
37
|
+
/** TEXCOORD_1 sets the unwrap replaced (`force`, or a file that had them on some primitives only) */
|
|
38
|
+
replaced: number
|
|
39
|
+
warnings: string[]
|
|
40
|
+
ms: number
|
|
41
|
+
bytesBefore: number
|
|
42
|
+
bytesAfter: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const message = (e: unknown): string => (e instanceof Error ? e.message : String(e))
|
|
46
|
+
|
|
47
|
+
/** Materials of the planned primitives with a texture on TEXCOORD_1 (an AO map on the second set): they would sample the
|
|
48
|
+
* lightmap layout once the set is replaced. */
|
|
49
|
+
const uv1Samplers = (plan: UnwrapPlan): string[] => {
|
|
50
|
+
const names = new Set<string>()
|
|
51
|
+
const infos = (m: Material): (TextureInfo | null)[] => [
|
|
52
|
+
m.getBaseColorTextureInfo(), m.getNormalTextureInfo(), m.getOcclusionTextureInfo(), m.getEmissiveTextureInfo(), m.getMetallicRoughnessTextureInfo(),
|
|
53
|
+
]
|
|
54
|
+
for (const p of [ ...plan.prims, ...plan.lods ]) {
|
|
55
|
+
const m = p.prim.getMaterial()
|
|
56
|
+
if (m && infos(m).some((i) => i?.getTexCoord() === 1)) names.add(m.getName() || "material")
|
|
57
|
+
}
|
|
58
|
+
return [ ...names ]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Write `bytes` to `target` through a temp file beside it, renamed over it. */
|
|
62
|
+
const writeAtomic = (target: string, bytes: Uint8Array): void => {
|
|
63
|
+
const dir = dirname(target)
|
|
64
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
65
|
+
const tmp = join(dir, `.${basename(target)}.unwrap-${process.pid}.tmp`)
|
|
66
|
+
try {
|
|
67
|
+
writeFileSync(tmp, bytes)
|
|
68
|
+
renameSync(tmp, target)
|
|
69
|
+
} catch (e) {
|
|
70
|
+
try { unlinkSync(tmp) } catch { /* never written */ }
|
|
71
|
+
throw e
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Unwrap one document in memory (no file): the plan's verdict, and the result when it did. */
|
|
76
|
+
export async function unwrapDocument(doc: Document, opts: UnwrapFileOptions = {}): Promise<Omit<UnwrapFileReport, "input" | "output" | "summary" | "ms" | "bytesBefore" | "bytesAfter">> {
|
|
77
|
+
const plan = planUnwrap(doc, opts)
|
|
78
|
+
const kept = plan.skipped
|
|
79
|
+
if (plan.prims.length === 0) return { status: "skipped", reason: kept.length ? "nothing to unwrap" : "no meshes", kept, replaced: 0, warnings: [] }
|
|
80
|
+
if (!opts.force && !planNeedsUnwrap(plan)) {
|
|
81
|
+
return { status: "skipped", reason: `has TEXCOORD_1 on its ${plan.prims.length + plan.lods.length} primitive(s)`, kept, replaced: 0, warnings: [] }
|
|
82
|
+
}
|
|
83
|
+
const replaced = plan.prims.filter((p) => p.hasUv1).length + plan.lods.filter((l) => l.hasUv1).length
|
|
84
|
+
const warnings: string[] = []
|
|
85
|
+
const samplers = replaced ? uv1Samplers(plan) : []
|
|
86
|
+
if (samplers.length) warnings.push(`${samplers.join(", ")} sample${samplers.length === 1 ? "s" : ""} a texture on TEXCOORD_1 — it now reads the lightmap layout`)
|
|
87
|
+
const result = await applyUnwrap(doc, plan)
|
|
88
|
+
// glTF's top-level `scene` is optional and glTF-Transform only writes it when set; strict loaders then load no roots.
|
|
89
|
+
const root = doc.getRoot()
|
|
90
|
+
if (!root.getDefaultScene() && root.listScenes().length > 0) root.setDefaultScene(root.listScenes()[0])
|
|
91
|
+
return { status: "unwrapped", result, kept, replaced, warnings }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Unwrap one GLB file: in place unless `output` names another file. Never throws for the file's sake — a failure is the
|
|
95
|
+
* report's status. */
|
|
96
|
+
export async function unwrapGlbFile(input: string, opts: UnwrapFileOptions = {}): Promise<UnwrapFileReport> {
|
|
97
|
+
const t0 = performance.now()
|
|
98
|
+
const rep: UnwrapFileReport = { input, output: null, status: "failed", summary: "", kept: [], replaced: 0, warnings: [], ms: 0, bytesBefore: 0, bytesAfter: 0 }
|
|
99
|
+
const done = (): UnwrapFileReport => { rep.ms = performance.now() - t0; rep.summary = describeUnwrap(rep); return rep }
|
|
100
|
+
if (!existsSync(input)) { rep.reason = "not found"; return done() }
|
|
101
|
+
rep.bytesBefore = statSync(input).size
|
|
102
|
+
await ensureCodecs()
|
|
103
|
+
const io = makeIO()
|
|
104
|
+
let doc: Document
|
|
105
|
+
try { doc = await io.read(input) } catch (e) { rep.reason = `unreadable: ${message(e)}`; return done() }
|
|
106
|
+
try {
|
|
107
|
+
Object.assign(rep, await unwrapDocument(doc, opts))
|
|
108
|
+
} catch (e) {
|
|
109
|
+
rep.status = "failed"
|
|
110
|
+
rep.reason = message(e)
|
|
111
|
+
return done()
|
|
112
|
+
}
|
|
113
|
+
if (rep.status !== "unwrapped") return done()
|
|
114
|
+
const output = opts.output ?? input
|
|
115
|
+
try {
|
|
116
|
+
const bytes = await io.writeBinary(doc)
|
|
117
|
+
writeAtomic(output, bytes)
|
|
118
|
+
rep.output = output
|
|
119
|
+
rep.bytesAfter = bytes.byteLength
|
|
120
|
+
} catch (e) {
|
|
121
|
+
rep.status = "failed"
|
|
122
|
+
rep.reason = `not written: ${message(e)}`
|
|
123
|
+
}
|
|
124
|
+
return done()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Where `-o` sends an input: none = in place; one input and a path that is not a folder = that file; else the folder. */
|
|
128
|
+
export const unwrapOutputFor = (input: string, out: string | undefined, batch: boolean): string | undefined => {
|
|
129
|
+
if (!out) return undefined
|
|
130
|
+
const norm = out.replace(/\\/g, "/")
|
|
131
|
+
const asDir = batch || norm.endsWith("/") || (existsSync(norm) && statSync(norm).isDirectory())
|
|
132
|
+
return asDir ? join(norm, basename(input)).replace(/\\/g, "/") : out
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Unwrap several GLB files one after the other (xatlas is single-threaded wasm; memory stays one file's). `out` as `-o`;
|
|
136
|
+
* `onFile` hears each report as it lands. */
|
|
137
|
+
export async function unwrapGlbFiles(files: string[], opts: UnwrapFileOptions & { out?: string, onFile?: (r: UnwrapFileReport) => void } = {}): Promise<UnwrapFileReport[]> {
|
|
138
|
+
const { out, onFile, ...rest } = opts
|
|
139
|
+
const reports: UnwrapFileReport[] = []
|
|
140
|
+
for (const input of files) {
|
|
141
|
+
const r = await unwrapGlbFile(input, { ...rest, output: unwrapOutputFor(input, out, files.length > 1) ?? rest.output })
|
|
142
|
+
reports.push(r)
|
|
143
|
+
onFile?.(r)
|
|
144
|
+
}
|
|
145
|
+
return reports
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const kb = (n: number): string => `${Math.round(n / 1024)} KB`
|
|
149
|
+
|
|
150
|
+
/** One line per file: what the unwrap made of it. */
|
|
151
|
+
export const describeUnwrap = (r: UnwrapFileReport): string => {
|
|
152
|
+
if (r.status !== "unwrapped" || !r.result) return `${r.status === "failed" ? "failed" : "skipped"}: ${r.reason ?? "?"}`
|
|
153
|
+
const res = r.result
|
|
154
|
+
const lod = res.lodPrimitives ? `; ${res.lodPrimitives} LOD primitive(s) by projection` : ""
|
|
155
|
+
const groups = res.groups.length ? `, ${res.groups.length} groups${res.splitNodes ? ` (${res.splitNodes} node(s) cut)` : ""}` : ""
|
|
156
|
+
return `${res.charts} charts over ${res.primitives} primitive(s)${groups}, vertices ${res.verticesBefore} → ${res.verticesAfter} (+${res.verticesAfter - res.verticesBefore} on seams)${lod}${r.replaced ? `; ${r.replaced} TEXCOORD_1 set(s) replaced` : ""}, ${(r.ms / 1000).toFixed(1)} s`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The detail lines under a file's line: groups, what was left as it was, warnings, where it went. */
|
|
160
|
+
export const detailUnwrap = (r: UnwrapFileReport): string[] => {
|
|
161
|
+
const lines: string[] = []
|
|
162
|
+
r.result?.groups.forEach((g, k) => lines.push(`group ${k}: ${g.primitives} primitive(s), ${g.area.toFixed(0)} m², ${g.charts} charts${g.texelScale > 1 ? `, texel ×${g.texelScale}` : ""}`))
|
|
163
|
+
if (r.kept.length) lines.push(`left as is: ${r.kept.slice(0, 6).map((s) => `${s.name} (${s.reason})`).join(", ")}${r.kept.length > 6 ? ` … ${r.kept.length - 6} more` : ""}`)
|
|
164
|
+
for (const w of r.warnings) lines.push(`⚠ ${w}`)
|
|
165
|
+
if (r.output) lines.push(`${r.output === r.input ? "written in place" : `written to ${r.output}`} (${kb(r.bytesBefore)} → ${kb(r.bytesAfter)})`)
|
|
166
|
+
return lines
|
|
167
|
+
}
|