refactor(tools): group the sixty scripts by what they talk to
One directory with sixty scripts stopped being navigable, and the grouping is also the sandbox boundary:
Tools/unreal/ commandlets and the two map builders, with ue_common.py and ue_placement.py as the
shared imports (they need the engine)
Tools/blender/ builders, exporters, the FBX preset, the MPFB character pipeline, blender_screenshots
(they need the Blender Flatpak)
Tools/review/ the visual loop: render_screenshots.sh, judge_screenshot.py, hero_compare.py,
build_comparison_md.py, e2e_asset_check.py, hero_iterate.sh, visual_loop.py,
visual_targets.json
Tools/gen/ image and texture generation: the ComfyUI driver, deterministic texture writers,
reference fetching
Tools/input/ OS-level input injection
root _common.py, check-lfs.sh, addon_compat_probe.py, sibling_asset_audit.py, tests/
Every moved script now resolves the repository root as ../.. and puts the tools root plus its siblings on
sys.path, so 'import ue_common' still works from unreal/. The test suite bootstrap does the same for the five
folders. render_screenshots.sh takes the folder as a third argument to run_cmdlet, and the e2e driver,
hero_iterate.sh and the comparison generator were repointed.
51 README rows now link to their new location, the README gains a Layout section naming each folder's
sandbox, and the architecture notes and Blender README follow the moves.
Verified: 108 tests pass, prettier is clean, judge_screenshot.py and hero_compare.py run from their new
homes, and the e2e run was aborted mid-way (it had been rebuilding an empty DemoMap for 15 minutes) and is
being rerun against the new layout.
This commit is contained in:
1 parent
f681d83736
commit
4d27109668
168 files changed
+799
-407
No files matched your search
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Procedural character textures: skin detail and hair strands.
|
||||
|
||||
The local ComfyUI instance was tried first for skin, and failed in a way worth
|
||||
recording: every "flat skin swatch" prompt produced an anatomical body part, twice
|
||||
out of three attempts, and the third produced a leather-scale surface. SDXL has no
|
||||
useful notion of "texture swatch without anatomy", and a character texture that
|
||||
needs manual rejection on every seed is worse than noise we control.
|
||||
|
||||
So skin detail is generated here instead: fine tileable noise with a low-frequency
|
||||
break-up, which is what the base colour actually needs (pores, not a pattern). Hair
|
||||
is generated as a strand sheet with alpha, for hair cards.
|
||||
|
||||
python3 Tools/gen/generate_character_textures.py --out Art/textures/generated
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
SKIN = {
|
||||
"name": "T_Character_SkinDetail",
|
||||
"size": 512,
|
||||
# Base tone sits near the material's measured skin albedo so a multiply blend
|
||||
# keeps the character's colour rather than tinting it.
|
||||
"tint": (222, 178, 152),
|
||||
"detail": 9,
|
||||
}
|
||||
|
||||
HAIR = {
|
||||
"name": "T_Character_Hair",
|
||||
"size": 512,
|
||||
"base": (44, 32, 26),
|
||||
"highlight": (96, 74, 58),
|
||||
"strands": 190,
|
||||
}
|
||||
|
||||
|
||||
def write_png(path, width, height, pixels, alpha=True):
|
||||
"""Write RGBA or RGB pixels with zlib, no image library needed."""
|
||||
raw = bytearray()
|
||||
for y in range(height):
|
||||
raw.append(0)
|
||||
start = y * width * (4 if alpha else 3)
|
||||
step = 4 if alpha else 3
|
||||
for x in range(width):
|
||||
i = start + x * step
|
||||
raw += pixels[i:i + step]
|
||||
|
||||
def chunk(tag, data):
|
||||
return (struct.pack(">I", len(data)) + tag + data +
|
||||
struct.pack(">I", zlib.crc32(tag + data) & 0xffffffff))
|
||||
|
||||
colour_type = 6 if alpha else 2
|
||||
ihdr = struct.pack(">IIBBBBB", width, height, 8, colour_type, 0, 0, 0)
|
||||
png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr)
|
||||
png += chunk(b"IDAT", zlib.compress(bytes(raw), 6)) + chunk(b"IEND", b"")
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(png)
|
||||
|
||||
|
||||
def value_noise(size, cells, seed):
|
||||
"""Tileable value noise: a lattice that wraps, bilinearly interpolated."""
|
||||
rnd = random.Random(seed)
|
||||
lattice = [[rnd.random() for _ in range(cells)] for _ in range(cells)]
|
||||
out = [0.0] * (size * size)
|
||||
for y in range(size):
|
||||
fy = y / size * cells
|
||||
y0 = int(fy) % cells
|
||||
y1 = (y0 + 1) % cells
|
||||
ty = fy - int(fy)
|
||||
ty = ty * ty * (3 - 2 * ty)
|
||||
for x in range(size):
|
||||
fx = x / size * cells
|
||||
x0 = int(fx) % cells
|
||||
x1 = (x0 + 1) % cells
|
||||
tx = fx - int(fx)
|
||||
tx = tx * tx * (3 - 2 * tx)
|
||||
a = lattice[y0][x0] * (1 - tx) + lattice[y0][x1] * tx
|
||||
b = lattice[y1][x0] * (1 - tx) + lattice[y1][x1] * tx
|
||||
out[y * size + x] = a * (1 - ty) + b * ty
|
||||
return out
|
||||
|
||||
|
||||
def skin_texture(spec, out_dir):
|
||||
size, tint, detail = spec["size"], spec["tint"], spec["detail"]
|
||||
# Three octaves: fine pores, mid break-up, broad blotch. All tileable.
|
||||
fine = value_noise(size, 96, 7)
|
||||
mid = value_noise(size, 24, 13)
|
||||
broad = value_noise(size, 7, 21)
|
||||
px = bytearray(size * size * 3)
|
||||
for i in range(size * size):
|
||||
n = (fine[i] - 0.5) * detail * 1.0 + (mid[i] - 0.5) * detail * 0.6 + (broad[i] - 0.5) * detail * 0.4
|
||||
for c in range(3):
|
||||
v = tint[c] + n
|
||||
px[i * 3 + c] = max(0, min(255, int(v)))
|
||||
path = os.path.join(out_dir, spec["name"] + ".png")
|
||||
write_png(path, size, size, px, alpha=False)
|
||||
return path, {"octaves": {"fine": 96, "mid": 24, "broad": 7}, "detail": detail, "tint": tint}
|
||||
|
||||
|
||||
def hair_texture(spec, out_dir):
|
||||
size = spec["size"]
|
||||
base, hi = spec["base"], spec["highlight"]
|
||||
rnd = random.Random(5)
|
||||
px = bytearray(size * size * 4)
|
||||
# Vertical strands: each is a line with slight wander, alpha fading at the edges.
|
||||
strands = []
|
||||
for _ in range(spec["strands"]):
|
||||
strands.append({
|
||||
"x": rnd.uniform(0, size),
|
||||
"w": rnd.uniform(0.6, 2.2),
|
||||
"a": rnd.uniform(0.35, 0.95),
|
||||
"wander": rnd.uniform(2.0, 9.0),
|
||||
"phase": rnd.uniform(0, math.tau),
|
||||
"bright": rnd.random(),
|
||||
})
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
i = (y * size + x) * 4
|
||||
alpha = 0.0
|
||||
lift = 0.0
|
||||
for s in strands:
|
||||
sx = (s["x"] + math.sin(y / size * math.tau + s["phase"]) * s["wander"]) % size
|
||||
d = abs(x - sx)
|
||||
d = min(d, size - d)
|
||||
if d < s["w"]:
|
||||
a = (1.0 - d / s["w"]) * s["a"]
|
||||
if a > alpha:
|
||||
alpha = a
|
||||
lift = s["bright"]
|
||||
if alpha <= 0.02:
|
||||
px[i:i + 4] = bytes((0, 0, 0, 0))
|
||||
continue
|
||||
shade = [base[c] + (hi[c] - base[c]) * lift for c in range(3)]
|
||||
px[i:i + 3] = bytes(max(0, min(255, int(v))) for v in shade)
|
||||
px[i + 3] = int(min(1.0, alpha) * 255)
|
||||
path = os.path.join(out_dir, spec["name"] + ".png")
|
||||
write_png(path, size, size, px, alpha=True)
|
||||
return path, {"strands": spec["strands"], "base": base, "highlight": hi}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--out", default="Art/textures/generated")
|
||||
args = ap.parse_args()
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
results = {}
|
||||
for spec, maker in ((SKIN, skin_texture), (HAIR, hair_texture)):
|
||||
path, meta = maker(spec, args.out)
|
||||
sidecar = os.path.splitext(path)[0] + ".json"
|
||||
with open(sidecar, "w") as handle:
|
||||
json.dump({"tool": "Tools/gen/generate_character_textures.py",
|
||||
"asset": spec["name"], "size": spec["size"],
|
||||
"procedural": True, "meta": meta,
|
||||
"note": "generated, not photographed; no third-party licence"},
|
||||
handle, indent=2)
|
||||
results[spec["name"]] = path
|
||||
print(f"wrote {path} (+ sidecar)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user