feat(hero): sandstone, banners and recessed portals - the material was the missing link
Iterated the loop five times against Art/reference/hero-masonic/. What took the iterations was the material, not the geometry: - M_FacadeStone in /Game/Hollywood/Site is the KIT stone; the hero does not use it, so warming it changed nothing. - The hero's materials come from the FBX and live at /Game/Hollywood/ElCapitan/M_Facade_*. Unreal keeps an existing material asset when an FBX referencing it is re-imported, so a Blender colour change never arrived. - Deleting those materials before the import broke the mesh's slots: one colour, no banners, no windows. The working step is tint_imported_materials(): import normally, then author one plain Constant3Vector material per facade slot with the colour sampled from the reference, and assign it to the matching slot. The elevation is now warm sandstone with ashlar coursing, three red banners, two window bands and three recessed portals with steps. Also: the portal's solid stone reveal z-fought with the door leaf and read as stripes, so the recess is carried by jambs, lintel and shadow; banners are 1.35 x 6.2 m hung from 7.2 m instead of 1.55 x 5.4. Loop iteration 5 verdict is still 'no': it wants the carved inscription, medallion relief, a crimson banner tone and visible window grilles - all recorded as the next iteration. Rows 4-6 are pending review with the new captures and two comparison plates.
This commit is contained in:
1 parent
19ed4fefd4
commit
bf7dfe1052
120 files changed
+276
-158
No files matched your search
@@ -131,8 +131,8 @@ def build():
|
||||
|
||||
# Warm limestone, sampled from 02-masonictemple01.jpg: the reference is a yellow-grey sandstone,
|
||||
# nowhere near the near-white the blockout used.
|
||||
stone = make_material("M_Facade_Stone", (0.66, 0.60, 0.48, 1.0), 0.82)
|
||||
stone_dark = make_material("M_Facade_StoneShadow", (0.52, 0.47, 0.38, 1.0), 0.85)
|
||||
stone = make_material("M_Facade_Stone", (0.34, 0.29, 0.21, 1.0), 0.82)
|
||||
stone_dark = make_material("M_Facade_StoneShadow", (0.22, 0.185, 0.135, 1.0), 0.85)
|
||||
glass = make_material("M_Facade_GlassDark", (0.05, 0.06, 0.07, 1.0), 0.25)
|
||||
banner = make_material("M_Facade_Banner", (0.66, 0.06, 0.05, 1.0), 0.9)
|
||||
|
||||
@@ -306,8 +306,11 @@ def build():
|
||||
# Recessed doorway: a dark reveal set back inside the plinth, 1.2 m deep so
|
||||
# it reads as an opening from any angle.
|
||||
depth = 1.2
|
||||
cube(f"Hero_Portal_{i}", x, plinth_front + depth / 2.0 + 0.05, PORTAL_H / 2.0,
|
||||
PORTAL_W, depth, PORTAL_H, glass)
|
||||
# Door leaf set back inside the opening. A solid stone reveal box at the same volume z-fought
|
||||
# with the leaf and read as stripes, so the recess is carried by the jambs, the lintel and the
|
||||
# shadow instead.
|
||||
cube(f"Hero_PortalLeaf_{i}", x, plinth_front + depth - 0.10, PORTAL_H / 2.0,
|
||||
PORTAL_W - 0.10, 0.08, PORTAL_H - 0.10, glass)
|
||||
# Stone surround, standing 0.1 m proud of the plinth face.
|
||||
for side, sx in (("L", -1.0), ("R", 1.0)):
|
||||
# Jambs stop at the pavement: a 0.15 m overhang below Z 0 would sink the
|
||||
@@ -362,6 +365,26 @@ def build():
|
||||
cube(f"Hero_Window_{rank}_{i}", wx, WINDOW_Y, z, 2.0, 0.08, 1.9, glass)
|
||||
note("Hero_Window_x10", (2.0, 0.08, 1.9), "photographs: window bands behind the colonnade")
|
||||
|
||||
# --- window grilles ------------------------------------------------------
|
||||
# The reference bays are closed with a dark metal trellis, not plain glass. Seven vertical and five
|
||||
# horizontal bars per opening, 4 cm square, read as a grille at street distance and cost 60 cubes.
|
||||
grille = make_material("M_Facade_Grille", (0.10, 0.10, 0.11, 1.0), 0.5)
|
||||
barred = 0
|
||||
for obj in list(bpy.data.objects):
|
||||
if not obj.name.startswith("Hero_Window_"):
|
||||
continue
|
||||
wx, wy, wz, wdx, wdy, wdz = (obj.location.x, obj.location.y, obj.location.z,
|
||||
obj.dimensions.x, obj.dimensions.y, obj.dimensions.z)
|
||||
for v in range(7):
|
||||
x = wx - wdx / 2.0 + (v + 0.5) * wdx / 7.0
|
||||
cube(f"Hero_GrilleV_{barred:02d}", x, wy - 0.02, wz, 0.04, 0.03, wdz * 0.96, grille)
|
||||
barred += 1
|
||||
for hcount in range(5):
|
||||
z = wz - wdz / 2.0 + (hcount + 0.5) * wdz / 5.0
|
||||
cube(f"Hero_GrilleH_{barred:02d}", wx, wy - 0.02, z, wdx * 0.96, 0.03, 0.04, grille)
|
||||
barred += 1
|
||||
print(f"[hero] window grilles: {barred} bars")
|
||||
|
||||
# --- rustication and roof cresting --------------------------------------
|
||||
# The photographs show the ground storey in banded ashlar and a cresting with balusters along
|
||||
# the roof edge; without them the elevation reads as a flat card.
|
||||
|
||||
+11
-5
@@ -20,10 +20,10 @@ REFERENCE="${REFERENCE:-Art/reference/hero-masonic/02-masonictemple01.jpg}"
|
||||
|
||||
step() { printf '\n== %s\n' "$1"; }
|
||||
|
||||
step "1/5 blender files writable"
|
||||
step "1/6 blender files writable"
|
||||
find Art -name '*.blend*' -exec chmod u+w {} \;
|
||||
|
||||
step "2/5 build the facade"
|
||||
step "2/6 build the facade"
|
||||
LD_LIBRARY_PATH="$BLENDER_FILES/blender/lib" "$BLENDER_FILES/blender/blender" -b --factory-startup \
|
||||
--python Tools/hero_facade_build.py -- \
|
||||
--out /tmp/gigline/hero-facade.fbx \
|
||||
@@ -32,16 +32,22 @@ LD_LIBRARY_PATH="$BLENDER_FILES/blender/lib" "$BLENDER_FILES/blender/blender" -b
|
||||
> /tmp/gigline/hero-build.log 2>&1 || { echo "build failed - see /tmp/gigline/hero-build.log"; exit 1; }
|
||||
grep -E 'exported FBX' /tmp/gigline/hero-build.log || { echo "no FBX written"; exit 1; }
|
||||
|
||||
step "3/5 import into Unreal"
|
||||
step "3/6 import into Unreal"
|
||||
"$ENGINE/Engine/Binaries/Linux/UnrealEditor-Cmd" "$UPROJECT" -run=pythonscript \
|
||||
-script="$(pwd)/Tools/ue_hero_facade_import.py" -nullrhi -unattended -nopause -stdout \
|
||||
> /tmp/gigline/hero-import.log 2>&1
|
||||
grep -q 'save_map' /tmp/gigline/hero-facade-import.txt || { echo "import failed - see /tmp/gigline/hero-facade-import.txt"; exit 1; }
|
||||
|
||||
step "4/5 render the capture set"
|
||||
step "4/6 rebuild DemoMap (materials live in the map builder)"
|
||||
"$ENGINE/Engine/Binaries/Linux/UnrealEditor-Cmd" "$UPROJECT" -run=pythonscript \
|
||||
-script="$(pwd)/Tools/ue_build_demo_map.py" -nullrhi -unattended -nopause -stdout \
|
||||
> /tmp/gigline/hero-demomap.log 2>&1
|
||||
grep -q 'save_map' /tmp/gigline/demo-map.txt || { echo "demo map rebuild failed - see /tmp/gigline/demo-map.txt"; exit 1; }
|
||||
|
||||
step "5/6 render the capture set"
|
||||
bash Tools/render_screenshots.sh > /tmp/gigline/hero-render.log 2>&1 || { echo "render failed - see /tmp/gigline/hero-render.log"; exit 1; }
|
||||
|
||||
step "5/5 judge and compare"
|
||||
step "6/6 judge and compare"
|
||||
python3 Tools/judge_screenshot.py --image "Screenshots/review/${SHOT}.png" \
|
||||
--expect "A neoclassical Masonic temple facade: warm yellow-grey sandstone with horizontal ashlar courses, six giant fluted Ionic columns on square pedestals, three tall red vertical banners reading JIMMY KIMMEL LIVE hanging between the columns, dark lattice metal grilles in the window bays behind the colonnade, three recessed dark entrance doorways with steps, above the columns a carved inscription band flanked by two round medallions, topped by a crested parapet." \
|
||||
| tee /tmp/gigline/hero-judge.txt | tail -2
|
||||
|
||||
@@ -472,10 +472,10 @@ def build():
|
||||
# Ashlar coursing at 0.55 m (a 0.10 m band joint), the reading the 2009
|
||||
# facade photograph gives for the hero's stone; roughness breaks up so the
|
||||
# wall is not one specular plane.
|
||||
"M_FacadeStone": dict(dark=(0.62, 0.58, 0.50), bright=(0.76, 0.72, 0.64),
|
||||
"M_FacadeStone": dict(dark=(0.30, 0.26, 0.20), bright=(0.47, 0.41, 0.31),
|
||||
roughness=0.70, specular=0.32, noise_scale=0.005,
|
||||
courses_m=0.55, joint_width=0.11,
|
||||
joint_tint=(0.40, 0.37, 0.32), roughness_variation=0.12,
|
||||
joint_tint=(0.16, 0.14, 0.11), roughness_variation=0.16,
|
||||
texture="T_StoneAshlar", tile_cm=440.0),
|
||||
# Rendered/stucco frontage: no coursing, but a fine tooth and a wider
|
||||
# roughness range than before, because flat stucco is what read as plastic.
|
||||
@@ -484,10 +484,10 @@ def build():
|
||||
roughness_variation=0.20, texture="T_Stucco", tile_cm=260.0),
|
||||
# Stone in shadow: the hero's recessed wall behind the colonnade. Same
|
||||
# coursing as the face, a stop darker, so depth reads without a light.
|
||||
"M_FacadeStoneShadow": dict(dark=(0.40, 0.37, 0.32), bright=(0.52, 0.49, 0.43),
|
||||
"M_FacadeStoneShadow": dict(dark=(0.18, 0.155, 0.12), bright=(0.28, 0.24, 0.18),
|
||||
roughness=0.78, specular=0.26, noise_scale=0.005,
|
||||
courses_m=0.55, joint_width=0.11,
|
||||
joint_tint=(0.28, 0.26, 0.23),
|
||||
joint_tint=(0.10, 0.09, 0.07),
|
||||
roughness_variation=0.10),
|
||||
# Glazing: low roughness, strong specular. A plausible IOR for glass sits
|
||||
# near 1.5, which UE expresses through specular ~0.5-0.6 on an opaque
|
||||
|
||||
@@ -41,6 +41,89 @@ def log(msg):
|
||||
lines.append(str(msg))
|
||||
|
||||
|
||||
FACADE_TINTS = {
|
||||
# Linear values, sampled from the reference photograph: the blockout's near-white stone was the one
|
||||
# thing the judge kept calling out. The FBX import keeps an existing material asset, so the colour has
|
||||
# to be written into the material after the import - dropping the asset instead broke the mesh's slots.
|
||||
"M_Facade_Stone": (0.34, 0.29, 0.21),
|
||||
"M_Facade_StoneShadow": (0.21, 0.175, 0.125),
|
||||
"M_Facade_GlassDark": (0.02, 0.025, 0.03),
|
||||
"M_Facade_Banner": (0.42, 0.035, 0.03),
|
||||
"M_Facade_Grille": (0.06, 0.06, 0.065),
|
||||
}
|
||||
|
||||
|
||||
def tint_imported_materials():
|
||||
"""Author plain facade materials for the imported slots.
|
||||
|
||||
The FBX import produces material *instances*, which cannot carry their own base colour, and the
|
||||
instance's parent keeps the blockout's near-white constant. Instead of fighting that, this creates one
|
||||
simple material per facade surface with the colour sampled from the reference and assigns it to the
|
||||
matching slot. That is the step that finally moves the stone off white; a tint attempt that silently
|
||||
did nothing is what let it survive three rebuilds.
|
||||
"""
|
||||
mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/SM_HeroFacade")
|
||||
if mesh is None:
|
||||
log("no SM_HeroFacade to tint")
|
||||
return
|
||||
library = unreal.MaterialEditingLibrary
|
||||
slots = mesh.get_editor_property("static_materials")
|
||||
assigned = 0
|
||||
for index, slot in enumerate(slots):
|
||||
current = slot.get_editor_property("material_interface")
|
||||
name = current.get_name() if current else ""
|
||||
base = name.split(".")[0].replace("_T", "")
|
||||
rgb = FACADE_TINTS.get(base)
|
||||
if rgb is None:
|
||||
continue
|
||||
path = f"{DEST}/{base}_Ref"
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(path):
|
||||
unreal.EditorAssetLibrary.delete_asset(path)
|
||||
material = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
base + "_Ref", DEST, unreal.Material, unreal.MaterialFactoryNew())
|
||||
colour = library.create_material_expression(
|
||||
material, unreal.MaterialExpressionConstant3Vector, -400, 0)
|
||||
colour.set_editor_property("constant", unreal.LinearColor(rgb[0], rgb[1], rgb[2], 1.0))
|
||||
library.connect_material_property(colour, "", unreal.MaterialProperty.MP_BASE_COLOR)
|
||||
library.recompile_material(material)
|
||||
slot.set_editor_property("material_interface", material)
|
||||
assigned += 1
|
||||
log(f"slot {index} {base}: {material.get_name()} {rgb}")
|
||||
if assigned:
|
||||
mesh.set_editor_property("static_materials", slots)
|
||||
unreal.EditorAssetLibrary.save_loaded_asset(mesh)
|
||||
log(f"assigned {assigned} facade materials")
|
||||
|
||||
|
||||
def drop_stale_mesh():
|
||||
"""Remove the imported mesh so its material slots are rebuilt from the FBX.
|
||||
|
||||
An earlier pass deleted the facade materials without the mesh, and the mesh kept pointing at the deleted
|
||||
objects: every slot fell back and the whole elevation rendered in one colour with no banners or windows.
|
||||
"""
|
||||
path = f"{DEST}/SM_HeroFacade"
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(path):
|
||||
unreal.EditorAssetLibrary.delete_asset(path)
|
||||
log(f"dropped stale mesh {path}")
|
||||
|
||||
|
||||
def drop_stale_materials():
|
||||
"""Delete the imported facade materials so the FBX recreates them with the current colours.
|
||||
|
||||
Unreal keeps an existing material asset when an FBX that references the same name is re-imported, so a
|
||||
colour change in the Blender builder never reached the engine - the wall stayed the blockout's near-white
|
||||
through several rebuilds. Deleting first is what makes the material part of the model.
|
||||
"""
|
||||
dropped = []
|
||||
for name in ("M_Facade_Stone", "M_Facade_StoneShadow", "M_Facade_GlassDark",
|
||||
"M_Facade_Banner", "M_Facade_Grille"):
|
||||
path = f"{DEST}/{name}"
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(path):
|
||||
unreal.EditorAssetLibrary.delete_asset(path)
|
||||
dropped.append(name)
|
||||
log(f"dropped stale materials: {dropped if dropped else 'none'}")
|
||||
|
||||
|
||||
def import_fbx():
|
||||
task = unreal.AssetImportTask()
|
||||
task.filename = FBX
|
||||
@@ -143,7 +226,10 @@ def measure_and_place():
|
||||
|
||||
|
||||
try:
|
||||
drop_stale_mesh()
|
||||
drop_stale_materials()
|
||||
import_fbx()
|
||||
tint_imported_materials()
|
||||
measure_and_place()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
import traceback
|
||||
|
||||
Reference in new issue
Block a user