feat(hero): generate the stone albedo with the new texture tool, import it, wire it into the material

Used the workflow integration from the previous step for real: generated a warm limestone ashlar albedo
with two profiles compared (AlbedoBase 17.04 vs Krea-2 13.90, AlbedoBase won), saved it as
Art/textures/generated/T_FacadeAshlar_AI.png beside its provenance sidecar, and imported it to
/Game/Hollywood/Textures.

ue_hero_facade_import.py now imports that albedo and, for the two stone slots, builds a material with
TextureSampleParameter2D -> Multiply(tint) -> Base Color, tiling the UVs 6x, so the map carries the coursing
and the tint keeps the value inside the reference's range.

Two real bugs fixed on the way:

- the albedo path was resolved against unreal.Paths.project_dir() (Game/GigLine), not the repository root, so
  the import silently skipped;
- the authored materials were never saved - only the mesh was - so after a reload the slots pointed at
  nothing and the elevation fell back to one default colour.

Honest status: the nodes are verified present and saved (Constant3Vector, TextureSampleParameter2D,
TextureCoordinate, Multiply) and the source mesh has UVs, but the render still shows the flat tint. Recorded
in TODO.md with the next probe: read the compiled Base Color pin instead of the node list.

THIRD_PARTY_NOTICES.md gains the AlbedoBase row (commercial use, credit required).
This commit is contained in:
zwitschi committed 2026-11-10 11:58:39 +01:00
1 parent 70a7e19841
commit f681d83736
126 files changed
+445 -139

No files matched your search

+53 -1
View File
@@ -41,6 +41,12 @@ def log(msg):
lines.append(str(msg))
# Generated with Tools/comfyui_generate.py texture (AlbedoBase XL, see the sidecar beside the PNG). The
# reference facade is coursed rubble-ashlar, not a flat tint, and a texture is what carries that at 45 m.
FACADE_ALBEDO = "Art/textures/generated/T_FacadeAshlar_AI.png"
FACADE_ALBEDO_ASSET = "T_FacadeAshlar_AI"
TEXTURE_DIR = "/Game/Hollywood/Textures"
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
@@ -53,6 +59,28 @@ FACADE_TINTS = {
}
def import_albedo():
"""Import the generated ashlar albedo, if it is on disk. Returns the asset or None."""
# unreal.Paths.project_dir() is Game/GigLine, not the repository root: the textures live beside Art/,
# two levels above this file.
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
path = os.path.join(repo_root, FACADE_ALBEDO)
if not os.path.exists(path):
log(f"albedo missing: {path}")
return None
task = unreal.AssetImportTask()
task.filename = path
task.destination_path = TEXTURE_DIR
task.destination_name = FACADE_ALBEDO_ASSET
task.automated = True
task.save = True
task.replace_existing = True
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
asset = unreal.EditorAssetLibrary.load_asset(f"{TEXTURE_DIR}/{FACADE_ALBEDO_ASSET}")
log(f"albedo imported: {asset.get_path_name() if asset else 'FAILED'}")
return asset
def tint_imported_materials():
"""Author plain facade materials for the imported slots.
@@ -67,6 +95,7 @@ def tint_imported_materials():
log("no SM_HeroFacade to tint")
return
library = unreal.MaterialEditingLibrary
albedo = import_albedo()
slots = mesh.get_editor_property("static_materials")
assigned = 0
for index, slot in enumerate(slots):
@@ -84,8 +113,31 @@ def tint_imported_materials():
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)
source = colour
if albedo is not None and base in ("M_Facade_Stone", "M_Facade_StoneShadow"):
# Texture sample multiplied by the tint: the map carries the coursing and grain, the tint keeps
# the value in the reference's range instead of the generator's brighter one.
sample = library.create_material_expression(
material, unreal.MaterialExpressionTextureSampleParameter2D, -700, 0)
sample.set_editor_property("parameter_name", f"Albedo_{base}")
sample.set_editor_property("texture", albedo)
if hasattr(unreal, "MaterialExpressionTextureCoordinate"):
uvs = library.create_material_expression(
material, unreal.MaterialExpressionTextureCoordinate, -950, 0)
uvs.set_editor_property("u_tiling", 6.0)
uvs.set_editor_property("v_tiling", 6.0)
library.connect_material_expressions(uvs, "", sample, "UVs")
multiply = library.create_material_expression(
material, unreal.MaterialExpressionMultiply, -250, 0)
library.connect_material_expressions(sample, "RGB", multiply, "A")
library.connect_material_expressions(colour, "", multiply, "B")
source = multiply
library.connect_material_property(source, "", unreal.MaterialProperty.MP_BASE_COLOR)
library.recompile_material(material)
# Save the material itself. Saving only the mesh left these assets unsaved, so after a reload the
# slots pointed at nothing and the whole elevation rendered in one default colour.
saved = unreal.EditorAssetLibrary.save_loaded_asset(material)
log(f"{base}_Ref saved: {saved}")
slot.set_editor_property("material_interface", material)
assigned += 1
log(f"slot {index} {base}: {material.get_name()} {rgb}")