lecodes-assets 0.2.0 → 2.4.10
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 +738 -449
- package/dist/cli.js +19760 -8612
- package/dist/index.js +19334 -8167
- package/package.json +15 -3
- package/src/bvh/fromDocument.ts +136 -0
- package/src/bvh/toDocument.ts +202 -0
- 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 -0
- 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 +67 -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 +103 -0
- package/src/commands/shared.ts +121 -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 +286 -0
- package/src/doctor/doctor.ts +305 -0
- package/src/doctor/lod.ts +177 -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/normalizeScale.ts +220 -0
- package/src/doctor/reduceGlbBones.ts +218 -0
- package/src/doctor/shading.ts +95 -0
- package/src/doctor/transcodeKtx2.ts +275 -0
- package/src/doctor/transcodeTextures.ts +101 -0
- package/src/doctor/validateGlb.ts +264 -0
- package/src/doctor/vertexAo.ts +321 -0
- package/src/fbx/convert.ts +327 -0
- package/src/fbx/read.ts +111 -0
- package/src/fbx/toDocument.ts +227 -0
- package/src/filmstrip.ts +263 -0
- package/src/geometry.ts +222 -0
- package/src/index.ts +19 -0
- package/src/mergeSkins.ts +117 -0
- package/src/probe.ts +167 -0
- package/src/props.ts +137 -0
- package/src/retarget/automap.ts +276 -0
- package/src/retarget/ghost.ts +23 -0
- package/src/retarget/heading.ts +238 -0
- package/src/retarget/humanoid.ts +572 -0
- package/src/retarget/inPlace.ts +130 -0
- package/src/retarget/marks.ts +581 -0
- package/src/retarget/mirror.ts +0 -0
- package/src/retarget/retarget.ts +2375 -0
- package/src/retarget/rig.ts +545 -0
- package/src/retarget/unityAnim.ts +171 -0
- package/src/retarget/writeMarks.ts +134 -0
- package/src/sky/analyze.ts +136 -0
- package/src/sky/cmgen.ts +71 -0
- package/src/sky/index.ts +200 -0
- package/src/terrainPack.ts +208 -0
- package/src/textures/resolveImage.ts +202 -0
- 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 +81 -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 +381 -0
- package/src/unity/levelTerrain.ts +116 -0
- package/src/unity/matFile.ts +179 -0
- package/src/unity/materialSlots.ts +53 -0
- package/src/unity/materialWriter.ts +306 -0
- package/src/unity/materials.ts +198 -0
- package/src/unity/math.ts +82 -0
- package/src/unity/png.ts +75 -0
- package/src/unity/prefabTree.ts +285 -0
- package/src/unity/prefabs.ts +54 -0
- package/src/unity/propertyPath.ts +43 -0
- package/src/unity/scene.ts +384 -0
- 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 +149 -0
- package/src/unity/yaml.ts +131 -0
- package/src/unreal/anim.ts +284 -0
- package/src/unreal/clipSource.ts +25 -0
- package/src/unreal/convert.ts +67 -0
- package/src/unreal/convertAsset.ts +81 -0
- package/src/unreal/index.ts +115 -0
- package/src/unreal/list.ts +113 -0
- package/src/unreal/materialGraph.ts +224 -0
- package/src/unreal/materials.ts +399 -0
- package/src/unreal/mesh.ts +205 -0
- package/src/unreal/oodle.ts +153 -0
- package/src/unreal/package.ts +677 -0
- package/src/unreal/probe.ts +144 -0
- package/src/unreal/resolve.ts +53 -0
- package/src/unreal/rig.ts +149 -0
- package/src/unreal/sequencer.ts +193 -0
- package/src/unreal/skeleton.ts +74 -0
- package/src/unreal/sound.ts +128 -0
- package/src/unreal/texture.ts +269 -0
- package/src/unreal/toDocument.ts +367 -0
- package/src/unreal/versions.ts +108 -0
- package/src/unwrap/index.ts +167 -0
- package/src/unwrap/lightmapUv.ts +761 -0
- package/src/wasm/fbxReader.js +14 -0
- package/src/wasm/lightmapUv.js +14 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// `unreal ls <dir>` — what a pack holds, before converting anything. Reads only the summary, the
|
|
2
|
+
// name table and the tables (no bulk data, no tracks), so a 796-package pack lists in well under a
|
|
3
|
+
// second; a clip's skeleton comes from its imports, which is enough to group clips by rig.
|
|
4
|
+
//
|
|
5
|
+
// Assets the reader cannot handle are listed with their reason instead of being skipped — the point
|
|
6
|
+
// of listing a pack is to find out what is usable.
|
|
7
|
+
|
|
8
|
+
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
9
|
+
import { dirname, join, relative, resolve } from "node:path"
|
|
10
|
+
import { resolveObjectFile } from "./resolve"
|
|
11
|
+
import { readPackage, engineLabel, classOf, assetExport } from "./package"
|
|
12
|
+
import { skeletonPathOf } from "./index"
|
|
13
|
+
import { dataModelKind, packageKind } from "./anim"
|
|
14
|
+
|
|
15
|
+
export type UnrealAssetRow = {
|
|
16
|
+
file: string
|
|
17
|
+
kind: ReturnType<typeof packageKind>
|
|
18
|
+
engine: string
|
|
19
|
+
/** The `/Game/...` path of the skeleton a clip or mesh names. */
|
|
20
|
+
skeleton: string
|
|
21
|
+
dataModel: string
|
|
22
|
+
note: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const listUassets = (dir: string): string[] => {
|
|
26
|
+
const out: string[] = []
|
|
27
|
+
const walk = (d: string): void => {
|
|
28
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
29
|
+
const p = join(d, e.name)
|
|
30
|
+
if (e.isDirectory()) walk(p)
|
|
31
|
+
else if (/\.uasset$/i.test(e.name)) out.push(p)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (statSync(dir).isDirectory()) walk(dir)
|
|
35
|
+
else out.push(dir)
|
|
36
|
+
return out.sort()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A pack's assets, grouped by the skeleton they belong to. A `Skeleton` asset does not know its own
|
|
41
|
+
* `/Game/...` path in UE 5.0 files (`packageName` is "None" there), so it is matched to the path its
|
|
42
|
+
* clips name by resolving that path back to a file — the same walk-up `--clips` uses.
|
|
43
|
+
*/
|
|
44
|
+
export const scanUnreal = (dir: string): UnrealAssetRow[] => {
|
|
45
|
+
const rows = scanPackages(dir)
|
|
46
|
+
const byFile = new Map(rows.map((r) => [ resolve(r.file), r ]))
|
|
47
|
+
// one referencing file per skeleton path, captured before anything is re-keyed below
|
|
48
|
+
const referrer = new Map<string, string>()
|
|
49
|
+
for (const r of rows) if (r.skeleton.startsWith("/") && !referrer.has(r.skeleton)) referrer.set(r.skeleton, r.file)
|
|
50
|
+
for (const [ path, from ] of referrer) {
|
|
51
|
+
const found = resolveObjectFile(path, dirname(from))
|
|
52
|
+
const row = found ? byFile.get(resolve(found.file)) : undefined
|
|
53
|
+
if (row) row.skeleton = path
|
|
54
|
+
}
|
|
55
|
+
return rows
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const scanPackages = (dir: string): UnrealAssetRow[] =>
|
|
59
|
+
listUassets(dir).map((file): UnrealAssetRow => {
|
|
60
|
+
try {
|
|
61
|
+
const pkg = readPackage(new Uint8Array(readFileSync(file)), file)
|
|
62
|
+
const kind = packageKind(pkg)
|
|
63
|
+
const model = dataModelKind(pkg)
|
|
64
|
+
return {
|
|
65
|
+
file, kind, engine: engineLabel(pkg.summary),
|
|
66
|
+
// a UE 5.0 package does not carry its own path ("None") — `scanUnreal` fills it in
|
|
67
|
+
skeleton: kind === "Skeleton" ? (pkg.summary.packageName.startsWith("/") ? pkg.summary.packageName : "") : skeletonPathOf(pkg),
|
|
68
|
+
dataModel: model === "none" ? "" : model,
|
|
69
|
+
// the class of the export the package IS (an AnimMontage, not the AnimDataModel beside it)
|
|
70
|
+
note: kind === "other" ? (() => { const a = assetExport(pkg); return a ? classOf(pkg, a) : "?" })() : "",
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return { file, kind: "other", engine: "?", skeleton: "", dataModel: "", note: (e as Error).message.replace(/^.*?: /, "") }
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
export const formatUnrealListing = (dir: string, rows: UnrealAssetRow[], opts: { all?: boolean } = {}): string => {
|
|
78
|
+
const lines: string[] = []
|
|
79
|
+
const usable = rows.filter((r) => r.kind !== "other" && r.kind !== "Texture2D")
|
|
80
|
+
lines.push(`${dir}: ${rows.length} package(s) — ${usable.length} readable by this pipeline`)
|
|
81
|
+
const engines = new Map<string, number>()
|
|
82
|
+
for (const r of rows) engines.set(r.engine, (engines.get(r.engine) ?? 0) + 1)
|
|
83
|
+
lines.push(` engines: ${[ ...engines ].map(([ e, n ]) => `${e} ×${n}`).join(", ")}`)
|
|
84
|
+
|
|
85
|
+
// grouped by skeleton, so a pack's rigs and the clips on them read as a unit
|
|
86
|
+
const bySkeleton = new Map<string, UnrealAssetRow[]>()
|
|
87
|
+
for (const r of usable) {
|
|
88
|
+
const key = r.skeleton || "(no skeleton named)"
|
|
89
|
+
const list = bySkeleton.get(key) ?? []
|
|
90
|
+
list.push(r)
|
|
91
|
+
bySkeleton.set(key, list)
|
|
92
|
+
}
|
|
93
|
+
for (const [ skel, list ] of [ ...bySkeleton ].sort((a, b) => b[1].length - a[1].length)) {
|
|
94
|
+
const counts = { AnimSequence: 0, SkeletalMesh: 0, Skeleton: 0, Texture2D: 0 } as Record<string, number>
|
|
95
|
+
for (const r of list) counts[r.kind]++
|
|
96
|
+
const blocked = list.filter((r) => r.note)
|
|
97
|
+
lines.push(` ${skel}: ${counts.Skeleton} skeleton(s), ${counts.SkeletalMesh} mesh(es), ${counts.AnimSequence} clip(s)${blocked.length ? ` ⚠ ${blocked.length} not readable (${blocked[0].note})` : ""}`)
|
|
98
|
+
for (const r of list) {
|
|
99
|
+
if (!opts.all && r.kind === "AnimSequence" && !r.note && list.length > 12) continue
|
|
100
|
+
lines.push(` ${r.kind.padEnd(13)} ${relative(dir, r.file).replace(/\\/g, "/")}${r.note ? ` ⚠ ${r.note}` : ""}`)
|
|
101
|
+
}
|
|
102
|
+
if (!opts.all && counts.AnimSequence > 0 && list.length > 12) lines.push(` (${counts.AnimSequence} clip(s) not listed — pass --all)`)
|
|
103
|
+
}
|
|
104
|
+
const textures = rows.filter((r) => r.kind === "Texture2D")
|
|
105
|
+
if (textures.length) lines.push(` textures: ${textures.length} Texture2D asset(s)`)
|
|
106
|
+
const other = rows.filter((r) => r.kind === "other")
|
|
107
|
+
if (other.length) {
|
|
108
|
+
const kinds = new Map<string, number>()
|
|
109
|
+
for (const r of other) kinds.set(r.note || "?", (kinds.get(r.note || "?") ?? 0) + 1)
|
|
110
|
+
lines.push(` other: ${[ ...kinds ].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([ k, n ]) => `${k} ×${n}`).join(", ")}`)
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n")
|
|
113
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// A Material's EXPRESSION GRAPH — the nodes the artist wired in the material editor, as data.
|
|
2
|
+
//
|
|
3
|
+
// `materials.ts` places textures by what their names suggest, because what a parameter DOES is a
|
|
4
|
+
// property of the graph. This reads the graph itself: every `MaterialExpression*` export with its
|
|
5
|
+
// scalar settings (a Constant's `R`, a ScalarParameter's name and default, a TextureSample's asset)
|
|
6
|
+
// and its inputs as edges (`{ node, output, mask }`), plus the material's own outputs (BaseColor,
|
|
7
|
+
// Opacity, Normal, …) as edges into it. A tool can then EVALUATE the graph — e.g. bake what a decal
|
|
8
|
+
// material computes into a texture a renderer without that shader can use.
|
|
9
|
+
//
|
|
10
|
+
// An input (`FExpressionInput` and its Color/Scalar/Vector subclasses) is a StructProperty whose
|
|
11
|
+
// payload is native, not a tag list: the expression (package index), the output index, the input's
|
|
12
|
+
// name, the mask flag and the four channel masks, then the subclass's own constant. Only the common
|
|
13
|
+
// head is read. Output indices follow each class's own output list — a TextureSample's are RGB, R,
|
|
14
|
+
// G, B, A, RGBA — and `mask` spells the channels the edge takes ("rgb", "a", "" = all).
|
|
15
|
+
//
|
|
16
|
+
// Verified on UE 4.25 packages (the "Bullet Impact VFX" pack's decal materials, read with
|
|
17
|
+
// `{ ue4: true }`). UE5 writes the same input head, but its editor-only data moved to
|
|
18
|
+
// `MaterialEditorOnlyData` — a UE5 Material's outputs are read from that export when present.
|
|
19
|
+
|
|
20
|
+
import { classOf, readFName, readObjectProperties, readProperties, propInt, resolveObject, type PropertyTag, type UePackage, type BinaryReader } from "./package"
|
|
21
|
+
|
|
22
|
+
export type MaterialEdge = {
|
|
23
|
+
/** The expression the edge comes from (its object name, e.g. `MaterialExpressionMultiply_21`). */
|
|
24
|
+
node: string
|
|
25
|
+
/** Which of that expression's outputs. */
|
|
26
|
+
output: number
|
|
27
|
+
/** The channels taken: `"rgb"`, `"a"`, `"r"` … or `""` for the output as it is. */
|
|
28
|
+
mask: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type MaterialExpression = {
|
|
32
|
+
/** `Multiply`, `LinearInterpolate`, `TextureSample`, … (the class without `MaterialExpression`). */
|
|
33
|
+
kind: string
|
|
34
|
+
/** Connected inputs by their property name (`A`, `B`, `Alpha`, `Input`, `Coordinates`, …). */
|
|
35
|
+
inputs: Record<string, MaterialEdge>
|
|
36
|
+
/** Everything scalar the expression carries: numbers, names, enums, booleans, colours, and
|
|
37
|
+
* object references as the referenced asset's path (`Texture` → `/Game/…/T_x`). */
|
|
38
|
+
values: Record<string, number | string | boolean | number[]>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type MaterialGraph = {
|
|
42
|
+
name: string
|
|
43
|
+
/** `MD_Surface`, `MD_DeferredDecal`, …; `BLEND_Opaque`, `BLEND_Translucent`, …; `MSM_DefaultLit`, …. */
|
|
44
|
+
domain: string
|
|
45
|
+
blendMode: string
|
|
46
|
+
shadingModel: string
|
|
47
|
+
/** The material's own pins (`BaseColor`, `Opacity`, `Normal`, `Metallic`, `Roughness`, `EmissiveColor`, …). */
|
|
48
|
+
outputs: Record<string, MaterialEdge>
|
|
49
|
+
/** Every other scalar setting on the material (two-sided, tangent-space normal, …). */
|
|
50
|
+
settings: Record<string, number | string | boolean | number[]>
|
|
51
|
+
expressions: Record<string, MaterialExpression>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Struct types that are an expression input (`FExpressionInput` and its subclasses). */
|
|
55
|
+
const isInputStruct = (s: string | undefined): boolean => !!s && /(^ExpressionInput$|MaterialInput$)/.test(s)
|
|
56
|
+
|
|
57
|
+
const MASK = [ "r", "g", "b", "a" ]
|
|
58
|
+
|
|
59
|
+
/** The native head of an `FExpressionInput`: expression, output index, input name, mask, r/g/b/a. */
|
|
60
|
+
const readEdge = (pkg: UePackage, r: BinaryReader, tag: PropertyTag): MaterialEdge | null => {
|
|
61
|
+
if (isTaggedEdge(pkg, r, tag)) return readTaggedEdge(pkg, r, tag)
|
|
62
|
+
if (tag.size < 36) return null
|
|
63
|
+
r.offset = tag.data
|
|
64
|
+
const expr = r.i32(), output = r.i32()
|
|
65
|
+
readFName(r, pkg.names) // the input's display name
|
|
66
|
+
const masked = r.i32() !== 0
|
|
67
|
+
const channels = [ r.i32(), r.i32(), r.i32(), r.i32() ]
|
|
68
|
+
return edgeTo(pkg, expr, output, masked, channels)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const EDGE_FIELDS = new Set([ "Expression", "OutputIndex", "InputName", "Mask", "MaskR", "MaskG", "MaskB", "MaskA", "ExpressionName", "None" ])
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Old UE4 (seen on 4.10, file version 482) writes an input as an ordinary TAG LIST — `Expression`
|
|
75
|
+
* (ObjectProperty), `OutputIndex`, `Mask`, `MaskR`… each only when it is not the default — instead of
|
|
76
|
+
* the native head. Told apart by its first two names: a field of the struct, then a property type.
|
|
77
|
+
*/
|
|
78
|
+
const isTaggedEdge = (pkg: UePackage, r: BinaryReader, tag: PropertyTag): boolean => {
|
|
79
|
+
if (tag.size < 8 || pkg.summary.ue5 !== 0) return false
|
|
80
|
+
r.offset = tag.data
|
|
81
|
+
const n0 = r.i32()
|
|
82
|
+
if (n0 < 0 || n0 >= pkg.names.length || !EDGE_FIELDS.has(pkg.names[n0])) return false
|
|
83
|
+
if (pkg.names[n0] === "None") return tag.size === 8
|
|
84
|
+
r.i32()
|
|
85
|
+
const n2 = r.i32()
|
|
86
|
+
return n2 >= 0 && n2 < pkg.names.length && pkg.names[n2].endsWith("Property")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const readTaggedEdge = (pkg: UePackage, r: BinaryReader, tag: PropertyTag): MaterialEdge | null => {
|
|
90
|
+
r.offset = tag.data
|
|
91
|
+
const p = readProperties(r, pkg)
|
|
92
|
+
const int = (n: string): number => propInt(r, p.byName.get(n)) ?? 0
|
|
93
|
+
return edgeTo(pkg, int("Expression"), int("OutputIndex"), int("Mask") !== 0, [ int("MaskR"), int("MaskG"), int("MaskB"), int("MaskA") ])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const edgeTo = (pkg: UePackage, expr: number, output: number, masked: boolean, channels: number[]): MaterialEdge | null => {
|
|
97
|
+
if (expr === 0) return null
|
|
98
|
+
const target = resolveObject(pkg, expr)
|
|
99
|
+
if (!target) return null
|
|
100
|
+
return { node: target.name, output, mask: masked ? channels.map((c, i) => (c ? MASK[i] : "")).join("") : "" }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A tag's value when it is a plain setting; undefined for what this reader does not model. */
|
|
104
|
+
const readValue = (pkg: UePackage, r: BinaryReader, t: PropertyTag): number | string | boolean | number[] | undefined => {
|
|
105
|
+
r.offset = t.data
|
|
106
|
+
switch (t.type) {
|
|
107
|
+
case "FloatProperty": return r.f32()
|
|
108
|
+
case "DoubleProperty": return r.f64()
|
|
109
|
+
case "IntProperty": return r.i32()
|
|
110
|
+
case "BoolProperty": return t.bool ?? false
|
|
111
|
+
case "NameProperty": return readFName(r, pkg.names)
|
|
112
|
+
case "StrProperty": return r.fstring()
|
|
113
|
+
case "EnumProperty": return readFName(r, pkg.names)
|
|
114
|
+
case "ByteProperty": return t.enumName && t.enumName !== "None" ? readFName(r, pkg.names) : r.u8()
|
|
115
|
+
case "ObjectProperty": {
|
|
116
|
+
const o = resolveObject(pkg, r.i32())
|
|
117
|
+
if (!o) return undefined
|
|
118
|
+
return o.kind === "import" && o.packagePath ? o.packagePath : o.name
|
|
119
|
+
}
|
|
120
|
+
case "StructProperty":
|
|
121
|
+
if (t.struct === "LinearColor") return [ r.f32(), r.f32(), r.f32(), r.f32() ]
|
|
122
|
+
if (t.struct === "Color") { const b = r.raw(4); return [ b[2] / 255, b[1] / 255, b[0] / 255, b[3] / 255 ] } // BGRA in memory
|
|
123
|
+
return undefined
|
|
124
|
+
default: return undefined
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Bookkeeping every expression carries that says nothing about what it computes.
|
|
129
|
+
const NOISE = /^(MaterialExpressionEditor[XY]|MaterialExpressionGuid|ExpressionGUID|Material|Function|Desc|bCollapsed|bRealtimePreview|bHidePreviewWindow|bShowOutputNameOnPin|bNeedToUpdatePreview|Outputs|SortPriority|Group)$/
|
|
130
|
+
|
|
131
|
+
/** What an edge evaluates to when everything upstream of it is a number — `null` as soon as anything
|
|
132
|
+
* is not (a texture, a vertex colour, UVs, an expression this does not model). Output 0 is the whole
|
|
133
|
+
* value, output k the k-th channel (a VectorParameter's RGB / R / G / B / A); the edge's mask then
|
|
134
|
+
* takes its channels. Properties left at their class default are not serialised, so the defaults
|
|
135
|
+
* are UE's: a Constant's R 0, a Multiply's ConstB 1, a Lerp's ConstAlpha 0.5. */
|
|
136
|
+
export const evaluateConstant = (graph: MaterialGraph, edge: MaterialEdge | undefined, depth = 0): number[] | null => {
|
|
137
|
+
if (!edge || depth > 64) return null
|
|
138
|
+
const ex = graph.expressions[edge.node]
|
|
139
|
+
if (!ex) return null
|
|
140
|
+
const num = (k: string, d = 0): number => (typeof ex.values[k] === "number" ? ex.values[k] as number : d)
|
|
141
|
+
const vec = (k: string, d: number[]): number[] => (Array.isArray(ex.values[k]) ? [ ...(ex.values[k] as number[]) ] : d)
|
|
142
|
+
const arg = (pin: string, constKey: string, d: number): number[] | null =>
|
|
143
|
+
ex.inputs[pin] ? evaluateConstant(graph, ex.inputs[pin], depth + 1) : [ num(constKey, d) ]
|
|
144
|
+
const zip = (a: number[], b: number[], f: (x: number, y: number) => number): number[] => {
|
|
145
|
+
const n = Math.max(a.length, b.length)
|
|
146
|
+
return Array.from({ length: n }, (_, i) => f(a.length === 1 ? a[0] : a[i] ?? 0, b.length === 1 ? b[0] : b[i] ?? 0))
|
|
147
|
+
}
|
|
148
|
+
let v: number[] | null
|
|
149
|
+
switch (ex.kind) {
|
|
150
|
+
case "Constant": v = [ num("R") ]; break
|
|
151
|
+
case "Constant2Vector": v = [ num("R"), num("G") ]; break
|
|
152
|
+
case "Constant3Vector": v = vec("Constant", [ 0, 0, 0, 0 ]).slice(0, 3); break
|
|
153
|
+
case "Constant4Vector": v = vec("Constant", [ 0, 0, 0, 0 ]); break
|
|
154
|
+
case "ScalarParameter": case "StaticBoolParameter": v = [ num("DefaultValue") ]; break
|
|
155
|
+
case "VectorParameter": v = vec("DefaultValue", [ 0, 0, 0, 1 ]); break
|
|
156
|
+
case "Multiply": case "Add": case "Subtract": case "Divide": case "Max": case "Min": {
|
|
157
|
+
const a = arg("A", "ConstA", ex.kind === "Multiply" || ex.kind === "Divide" ? 1 : 0)
|
|
158
|
+
const b = arg("B", "ConstB", ex.kind === "Multiply" || ex.kind === "Add" || ex.kind === "Divide" ? 1 : 0)
|
|
159
|
+
if (!a || !b) return null
|
|
160
|
+
const op = { Multiply: (x: number, y: number) => x * y, Add: (x: number, y: number) => x + y, Subtract: (x: number, y: number) => x - y,
|
|
161
|
+
Divide: (x: number, y: number) => (y ? x / y : 0), Max: Math.max, Min: Math.min }[ex.kind]
|
|
162
|
+
v = zip(a, b, op)
|
|
163
|
+
break
|
|
164
|
+
}
|
|
165
|
+
case "OneMinus": { const a = arg("Input", "", 0); v = a && a.map((x) => 1 - x); break }
|
|
166
|
+
case "LinearInterpolate": {
|
|
167
|
+
const a = arg("A", "ConstA", 0), b = arg("B", "ConstB", 1), t = arg("Alpha", "ConstAlpha", 0.5)
|
|
168
|
+
if (!a || !b || !t) return null
|
|
169
|
+
v = zip(zip(a, b, (x, y) => y - x), t, (d, s) => d * s).map((d, i) => (a.length === 1 ? a[0] : a[i] ?? 0) + d)
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
default: return null
|
|
173
|
+
}
|
|
174
|
+
if (!v) return null
|
|
175
|
+
if (edge.output > 0) v = [ v[edge.output - 1] ?? 0 ]
|
|
176
|
+
if (edge.mask) v = [ ...edge.mask ].map((c) => v![MASK.indexOf(c)] ?? v![0] ?? 0)
|
|
177
|
+
return v
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The expression graph of the package's Material (throws when it holds none). */
|
|
181
|
+
export const readMaterialGraph = (pkg: UePackage): MaterialGraph => {
|
|
182
|
+
const exports = pkg.exports.map((e, i) => ({ e, i, cls: classOf(pkg, e) }))
|
|
183
|
+
const mat = exports.find((x) => x.cls === "Material")
|
|
184
|
+
if (!mat) throw new Error(`${pkg.label}: no Material export (${[ ...new Set(exports.map((x) => x.cls)) ].join(", ")})`)
|
|
185
|
+
const graph: MaterialGraph = { name: mat.e.objectName, domain: "MD_Surface", blendMode: "BLEND_Opaque", shadingModel: "MSM_DefaultLit", outputs: {}, settings: {}, expressions: {} }
|
|
186
|
+
|
|
187
|
+
// the material's own pins: on the Material in UE4, on its MaterialEditorOnlyData in UE5
|
|
188
|
+
const pinsFrom = [ mat, ...exports.filter((x) => x.cls === "MaterialEditorOnlyData") ]
|
|
189
|
+
for (const src of pinsFrom) {
|
|
190
|
+
const { reader: r, props } = readObjectProperties(pkg, src.e)
|
|
191
|
+
for (const t of props.list) {
|
|
192
|
+
if (t.type === "StructProperty" && isInputStruct(t.struct)) {
|
|
193
|
+
const edge = readEdge(pkg, r, t)
|
|
194
|
+
if (edge) graph.outputs[t.name] = edge
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
if (src !== mat || NOISE.test(t.name)) continue
|
|
198
|
+
const v = readValue(pkg, r, t)
|
|
199
|
+
if (v === undefined) continue
|
|
200
|
+
if (t.name === "MaterialDomain") graph.domain = String(v)
|
|
201
|
+
else if (t.name === "BlendMode") graph.blendMode = String(v)
|
|
202
|
+
else if (t.name === "ShadingModel") graph.shadingModel = String(v)
|
|
203
|
+
else graph.settings[t.name] = v
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const x of exports) {
|
|
208
|
+
if (!x.cls.startsWith("MaterialExpression") || x.cls === "MaterialExpressionComment") continue
|
|
209
|
+
const { reader: r, props } = readObjectProperties(pkg, x.e)
|
|
210
|
+
const ex: MaterialExpression = { kind: x.cls.slice("MaterialExpression".length), inputs: {}, values: {} }
|
|
211
|
+
for (const t of props.list) {
|
|
212
|
+
if (NOISE.test(t.name)) continue
|
|
213
|
+
if (t.type === "StructProperty" && isInputStruct(t.struct)) {
|
|
214
|
+
const edge = readEdge(pkg, r, t)
|
|
215
|
+
if (edge) ex.inputs[t.name] = edge
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
const v = readValue(pkg, r, t)
|
|
219
|
+
if (v !== undefined) ex.values[t.name] = v
|
|
220
|
+
}
|
|
221
|
+
graph.expressions[x.e.objectName] = ex
|
|
222
|
+
}
|
|
223
|
+
return graph
|
|
224
|
+
}
|