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,226 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the facade texture maps the demo materials sample.
|
||||
|
||||
The materials started as procedural graphs only (noise + coursing), which reads
|
||||
as tinted stone but has no grain. These maps add the grain: an ashlar stone
|
||||
course pattern, a rendered stucco tooth, asphalt aggregate and concrete pores,
|
||||
each as base colour plus a roughness map so the specular breaks up.
|
||||
|
||||
Standard library only — the PNG encoder is the same pure-zlib writer used for the
|
||||
MPFB skin, so this runs on the host without numpy or Pillow.
|
||||
|
||||
python3 Tools/gen/generate_facade_textures.py # write to /tmp/gigline/textures
|
||||
python3 Tools/gen/generate_facade_textures.py --size 1024
|
||||
python3 Tools/gen/generate_facade_textures.py --out Art/blender/textures
|
||||
|
||||
Output names match the Unreal import destination `/Game/Hollywood/Textures`:
|
||||
|
||||
T_StoneAshlar_BC.png T_StoneAshlar_R.png
|
||||
T_Stucco_BC.png T_Stucco_R.png
|
||||
T_Asphalt_BC.png T_Asphalt_R.png
|
||||
T_Concrete_BC.png T_Concrete_R.png
|
||||
|
||||
Determinism: every pattern is a deterministic hash of the pixel coordinate, so a
|
||||
re-run reproduces the same files byte for byte (no RNG state, no seam drift).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
# --- PNG -------------------------------------------------------------------
|
||||
|
||||
|
||||
def write_png(path, width, height, rgb_rows):
|
||||
"""Write 8-bit RGB rows (bytes) to a PNG. `rgb_rows` is one bytes per row."""
|
||||
raw = bytearray()
|
||||
for y in range(height):
|
||||
raw.append(0) # filter type 0
|
||||
raw += rgb_rows[y]
|
||||
|
||||
def chunk(tag, data):
|
||||
c = struct.pack(">I", len(data)) + tag + data
|
||||
c += struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||
return c
|
||||
|
||||
ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0) # colour type 2 = RGB
|
||||
png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr)
|
||||
png += chunk(b"IDAT", zlib.compress(bytes(raw), 6)) + chunk(b"IEND", b"")
|
||||
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(png)
|
||||
return path
|
||||
|
||||
|
||||
# --- deterministic value noise --------------------------------------------
|
||||
|
||||
|
||||
def _hash2(x, y, seed=0):
|
||||
"""Deterministic 0..1 hash of an integer cell. No RNG, fully reproducible."""
|
||||
n = (x * 374761393 + y * 668265263 + seed * 1442695040888963407) & 0xFFFFFFFF
|
||||
n = (n ^ (n >> 13)) * 1274126177 & 0xFFFFFFFF
|
||||
return ((n ^ (n >> 16)) & 0xFFFF) / 65535.0
|
||||
|
||||
|
||||
def value_noise(x, y, seed=0):
|
||||
"""Smooth bilinear value noise at continuous (x, y) in cell units."""
|
||||
xi, yi = math.floor(x), math.floor(y)
|
||||
fx, fy = x - xi, y - yi
|
||||
# smoothstep weights so cells blend without visible lattice lines
|
||||
sx = fx * fx * (3.0 - 2.0 * fx)
|
||||
sy = fy * fy * (3.0 - 2.0 * fy)
|
||||
n00 = _hash2(xi, yi, seed)
|
||||
n10 = _hash2(xi + 1, yi, seed)
|
||||
n01 = _hash2(xi, yi + 1, seed)
|
||||
n11 = _hash2(xi + 1, yi + 1, seed)
|
||||
return (n00 * (1 - sx) + n10 * sx) * (1 - sy) + (n01 * (1 - sx) + n11 * sx) * sy
|
||||
|
||||
|
||||
def fractal(x, y, octaves=4, seed=0):
|
||||
"""Fractal sum of value noise, normalised to 0..1."""
|
||||
total, amp, freq, norm = 0.0, 1.0, 1.0, 0.0
|
||||
for _ in range(octaves):
|
||||
total += value_noise(x * freq, y * freq, seed) * amp
|
||||
norm += amp
|
||||
amp *= 0.5
|
||||
freq *= 2.0
|
||||
return total / norm
|
||||
|
||||
|
||||
def clamp8(value):
|
||||
return max(0, min(255, int(value * 255.0 + 0.5)))
|
||||
|
||||
|
||||
# --- patterns --------------------------------------------------------------
|
||||
# Each returns (base_rgb_function, roughness_function) over u, v in 0..1.
|
||||
|
||||
|
||||
def stone_ashlar(courses=8, per_course=4):
|
||||
"""Coursed ashlar: horizontal beds, staggered vertical joints, stone grain."""
|
||||
course_h = 1.0 / courses
|
||||
|
||||
def block_uv(u, v):
|
||||
row = int(v / course_h)
|
||||
shift = 0.5 * (row % 2) / per_course # stagger alternate courses
|
||||
col = int(((u + shift) % 1.0) * per_course)
|
||||
return row, col, (v % course_h) / course_h, ((u + shift) % 1.0) * per_course - col
|
||||
|
||||
def base(u, v, px, py):
|
||||
row, col, fj, fi = block_uv(u, v)
|
||||
tone = 0.62 + 0.10 * _hash2(row, col, 11) # each block a little different
|
||||
grain = 0.06 * (fractal(px * 0.15, py * 0.15, 5, 3) - 0.5) * 2.0
|
||||
# joint: a dark band along the bed and the head of each block
|
||||
joint = 0.0
|
||||
if fj < 0.055 or fi < 0.055:
|
||||
joint = -0.22
|
||||
value = tone + grain + joint
|
||||
return (value * 1.02, value, value * 0.94) # faintly warm stone
|
||||
|
||||
def rough(u, v, px, py):
|
||||
row, col, fj, fi = block_uv(u, v)
|
||||
base_r = 0.66 + 0.10 * _hash2(row, col, 29)
|
||||
if fj < 0.055 or fi < 0.055:
|
||||
base_r = 0.86 # joints are matte
|
||||
return base_r + 0.06 * fractal(px * 0.2, py * 0.2, 3, 7)
|
||||
|
||||
return base, rough
|
||||
|
||||
|
||||
def stucco():
|
||||
def base(u, v, px, py):
|
||||
blot = fractal(px * 0.03, py * 0.03, 4, 21)
|
||||
tooth = fractal(px * 0.9, py * 0.9, 3, 22)
|
||||
value = 0.58 + 0.12 * (blot - 0.5) + 0.05 * (tooth - 0.5)
|
||||
return (value * 1.03, value, value * 0.96)
|
||||
|
||||
def rough(u, v, px, py):
|
||||
return 0.74 + 0.18 * fractal(px * 0.06, py * 0.06, 3, 31)
|
||||
|
||||
return base, rough
|
||||
|
||||
|
||||
def asphalt():
|
||||
def base(u, v, px, py):
|
||||
agg = fractal(px * 1.4, py * 1.4, 3, 41) # fine aggregate
|
||||
patch = fractal(px * 0.02, py * 0.02, 3, 42) # repair patches
|
||||
value = 0.055 + 0.05 * (patch - 0.5) + 0.06 * (agg - 0.5)
|
||||
return (value, value, value * 1.05) # cool bitumen
|
||||
|
||||
def rough(u, v, px, py):
|
||||
return 0.90 + 0.08 * fractal(px * 0.5, py * 0.5, 2, 43)
|
||||
|
||||
return base, rough
|
||||
|
||||
|
||||
def concrete():
|
||||
def base(u, v, px, py):
|
||||
pores = fractal(px * 0.8, py * 0.8, 3, 51)
|
||||
tone = fractal(px * 0.04, py * 0.04, 4, 52)
|
||||
value = 0.50 + 0.10 * (tone - 0.5) + 0.05 * (pores - 0.5)
|
||||
return (value * 1.01, value, value * 0.97)
|
||||
|
||||
def rough(u, v, px, py):
|
||||
return 0.84 + 0.10 * fractal(px * 0.15, py * 0.15, 3, 53)
|
||||
|
||||
return base, rough
|
||||
|
||||
|
||||
TEXTURES = {
|
||||
"T_StoneAshlar": stone_ashlar(),
|
||||
"T_Stucco": stucco(),
|
||||
"T_Asphalt": asphalt(),
|
||||
"T_Concrete": concrete(),
|
||||
}
|
||||
|
||||
|
||||
def render(name, pattern, size):
|
||||
"""Render one texture; returns {suffix: path} for _BC and _R."""
|
||||
base_fn, rough_fn = pattern
|
||||
paths = {}
|
||||
for suffix, fn in (("BC", base_fn), ("R", rough_fn)):
|
||||
# Wrap in a function so the inner loop stays tight.
|
||||
rows = []
|
||||
for y in range(size):
|
||||
v = y / size
|
||||
row = bytearray()
|
||||
for x in range(size):
|
||||
u = x / size
|
||||
value = fn(u, v, x / size * size / 64.0, y / size * size / 64.0)
|
||||
if isinstance(value, tuple):
|
||||
row += bytes((clamp8(value[0]), clamp8(value[1]), clamp8(value[2])))
|
||||
else:
|
||||
grey = clamp8(value)
|
||||
row += bytes((grey, grey, grey))
|
||||
rows.append(bytes(row))
|
||||
path = os.path.join(OUT_DIR, f"{name}_{suffix}.png")
|
||||
write_png(path, size, size, rows)
|
||||
paths[suffix] = path
|
||||
return paths
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
global OUT_DIR
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--out", default="/tmp/gigline/textures",
|
||||
help="output directory (default /tmp/gigline/textures)")
|
||||
ap.add_argument("--size", type=int, default=512, help="edge length in pixels")
|
||||
args = ap.parse_args(argv)
|
||||
OUT_DIR = args.out
|
||||
|
||||
print(f"writing {len(TEXTURES) * 2} maps at {args.size}² to {OUT_DIR}")
|
||||
for name, pattern in TEXTURES.items():
|
||||
paths = render(name, pattern, args.size)
|
||||
for suffix, path in paths.items():
|
||||
print(f" {os.path.basename(path):28} {os.path.getsize(path):>8} bytes")
|
||||
return 0
|
||||
|
||||
|
||||
OUT_DIR = "/tmp/gigline/textures"
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user