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.
Files changed (128) hide show
  1. package/README.md +738 -449
  2. package/dist/cli.js +19760 -8612
  3. package/dist/index.js +19334 -8167
  4. package/package.json +15 -3
  5. package/src/bvh/fromDocument.ts +136 -0
  6. package/src/bvh/toDocument.ts +202 -0
  7. package/src/cli-kit/command.ts +100 -0
  8. package/src/cli-kit/help.ts +72 -0
  9. package/src/cli-kit/index.ts +4 -0
  10. package/src/cli-kit/parse.ts +72 -0
  11. package/src/cli-kit/run.ts +52 -0
  12. package/src/cli.ts +24 -0
  13. package/src/commands/avatar.ts +77 -0
  14. package/src/commands/bvh.ts +26 -0
  15. package/src/commands/convert.ts +156 -0
  16. package/src/commands/doctor.ts +67 -0
  17. package/src/commands/geometry.ts +45 -0
  18. package/src/commands/index.ts +24 -0
  19. package/src/commands/marks.ts +38 -0
  20. package/src/commands/mirror.ts +35 -0
  21. package/src/commands/probe.ts +43 -0
  22. package/src/commands/retarget/donor.ts +63 -0
  23. package/src/commands/retarget/index.ts +169 -0
  24. package/src/commands/retarget/map.ts +81 -0
  25. package/src/commands/retarget/report.ts +77 -0
  26. package/src/commands/scene.ts +103 -0
  27. package/src/commands/shared.ts +121 -0
  28. package/src/commands/sky.ts +35 -0
  29. package/src/commands/terrainPack.ts +40 -0
  30. package/src/commands/unpack.ts +43 -0
  31. package/src/commands/unreal.ts +109 -0
  32. package/src/commands/unwrap.ts +56 -0
  33. package/src/doctor/cardTrim.ts +286 -0
  34. package/src/doctor/doctor.ts +305 -0
  35. package/src/doctor/lod.ts +177 -0
  36. package/src/doctor/merge/accessors.ts +30 -0
  37. package/src/doctor/merge/atlasLayout.ts +94 -0
  38. package/src/doctor/merge/atlasPixels.ts +114 -0
  39. package/src/doctor/merge/bindSpace.ts +59 -0
  40. package/src/doctor/merge/classify.ts +136 -0
  41. package/src/doctor/merge/geometry.ts +167 -0
  42. package/src/doctor/merge/index.ts +98 -0
  43. package/src/doctor/merge/matrix.ts +60 -0
  44. package/src/doctor/merge/plan.ts +88 -0
  45. package/src/doctor/merge/textures.ts +60 -0
  46. package/src/doctor/merge/types.ts +77 -0
  47. package/src/doctor/normalizeScale.ts +220 -0
  48. package/src/doctor/reduceGlbBones.ts +218 -0
  49. package/src/doctor/shading.ts +95 -0
  50. package/src/doctor/transcodeKtx2.ts +275 -0
  51. package/src/doctor/transcodeTextures.ts +101 -0
  52. package/src/doctor/validateGlb.ts +264 -0
  53. package/src/doctor/vertexAo.ts +321 -0
  54. package/src/fbx/convert.ts +327 -0
  55. package/src/fbx/read.ts +111 -0
  56. package/src/fbx/toDocument.ts +227 -0
  57. package/src/filmstrip.ts +263 -0
  58. package/src/geometry.ts +222 -0
  59. package/src/index.ts +19 -0
  60. package/src/mergeSkins.ts +117 -0
  61. package/src/probe.ts +167 -0
  62. package/src/props.ts +137 -0
  63. package/src/retarget/automap.ts +276 -0
  64. package/src/retarget/ghost.ts +23 -0
  65. package/src/retarget/heading.ts +238 -0
  66. package/src/retarget/humanoid.ts +572 -0
  67. package/src/retarget/inPlace.ts +130 -0
  68. package/src/retarget/marks.ts +581 -0
  69. package/src/retarget/mirror.ts +0 -0
  70. package/src/retarget/retarget.ts +2375 -0
  71. package/src/retarget/rig.ts +545 -0
  72. package/src/retarget/unityAnim.ts +171 -0
  73. package/src/retarget/writeMarks.ts +134 -0
  74. package/src/sky/analyze.ts +136 -0
  75. package/src/sky/cmgen.ts +71 -0
  76. package/src/sky/index.ts +200 -0
  77. package/src/terrainPack.ts +208 -0
  78. package/src/textures/resolveImage.ts +202 -0
  79. package/src/unity/colliderFit.ts +287 -0
  80. package/src/unity/fbxModel.ts +194 -0
  81. package/src/unity/foliage.ts +63 -0
  82. package/src/unity/gameObjects.ts +81 -0
  83. package/src/unity/guid.ts +104 -0
  84. package/src/unity/image.ts +58 -0
  85. package/src/unity/kindGlb.ts +364 -0
  86. package/src/unity/lecodesLevel.ts +381 -0
  87. package/src/unity/levelTerrain.ts +116 -0
  88. package/src/unity/matFile.ts +179 -0
  89. package/src/unity/materialSlots.ts +53 -0
  90. package/src/unity/materialWriter.ts +306 -0
  91. package/src/unity/materials.ts +198 -0
  92. package/src/unity/math.ts +82 -0
  93. package/src/unity/png.ts +75 -0
  94. package/src/unity/prefabTree.ts +285 -0
  95. package/src/unity/prefabs.ts +54 -0
  96. package/src/unity/propertyPath.ts +43 -0
  97. package/src/unity/scene.ts +384 -0
  98. package/src/unity/serialized.ts +242 -0
  99. package/src/unity/terrain.ts +221 -0
  100. package/src/unity/triBudget.ts +77 -0
  101. package/src/unity/triplanar.ts +122 -0
  102. package/src/unity/unityMesh.ts +151 -0
  103. package/src/unity/unpack.ts +149 -0
  104. package/src/unity/yaml.ts +131 -0
  105. package/src/unreal/anim.ts +284 -0
  106. package/src/unreal/clipSource.ts +25 -0
  107. package/src/unreal/convert.ts +67 -0
  108. package/src/unreal/convertAsset.ts +81 -0
  109. package/src/unreal/index.ts +115 -0
  110. package/src/unreal/list.ts +113 -0
  111. package/src/unreal/materialGraph.ts +224 -0
  112. package/src/unreal/materials.ts +399 -0
  113. package/src/unreal/mesh.ts +205 -0
  114. package/src/unreal/oodle.ts +153 -0
  115. package/src/unreal/package.ts +677 -0
  116. package/src/unreal/probe.ts +144 -0
  117. package/src/unreal/resolve.ts +53 -0
  118. package/src/unreal/rig.ts +149 -0
  119. package/src/unreal/sequencer.ts +193 -0
  120. package/src/unreal/skeleton.ts +74 -0
  121. package/src/unreal/sound.ts +128 -0
  122. package/src/unreal/texture.ts +269 -0
  123. package/src/unreal/toDocument.ts +367 -0
  124. package/src/unreal/versions.ts +108 -0
  125. package/src/unwrap/index.ts +167 -0
  126. package/src/unwrap/lightmapUv.ts +761 -0
  127. package/src/wasm/fbxReader.js +14 -0
  128. 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
+ }