docs(review): the Blender-vs-in-game comparison lives in Screenshots/README.md
Merged COMPARISON.md into the review README as a generated section between <!-- blender-comparison:begin/end --> markers, so there is one place to look and one place to sign off. Tools/build_comparison_md.py rewrites only that block - the hand-written sections around it are untouched - and formats the file with prettier afterwards, because a generated table is never byte-identical to prettier's output and the repo gates on --check. Run it after every capture or Blender render; it is the routine that keeps the page current with new images rather than a one-off export.
This commit is contained in:
1 parent
cb94438e0d
commit
046e2c8b9e
12 files changed
+317
-61
No files matched your search
@@ -55,6 +55,102 @@ SYSTEM_RULES = (
|
||||
)
|
||||
|
||||
|
||||
def _colour_rank(colour):
|
||||
"""Sort key for picking a paint colour: saturation first, then brightness."""
|
||||
return (max(colour) - min(colour), sum(colour))
|
||||
|
||||
|
||||
def read_material(material):
|
||||
"""Base colour, roughness and metallic for a material, Principled or legacy.
|
||||
|
||||
The template's car paint is a **legacy node graph** - mix shaders over glossy and
|
||||
diffuse BSDFs - not a Principled shader. Reading only Principled skipped it, which is
|
||||
exactly why the car arrived in Unreal near-black: the paint colour never left Blender.
|
||||
For those graphs the colour is taken from the mix nodes, which is where the red lives.
|
||||
"""
|
||||
if not material.use_nodes:
|
||||
return None
|
||||
nodes = material.node_tree.nodes
|
||||
bsdf = nodes.get("Principled BSDF")
|
||||
if bsdf:
|
||||
colour = bsdf.inputs["Base Color"].default_value
|
||||
return {
|
||||
"base_color": [round(colour[0], 4), round(colour[1], 4), round(colour[2], 4)],
|
||||
"roughness": round(bsdf.inputs["Roughness"].default_value, 4),
|
||||
"metallic": round(bsdf.inputs["Metallic"].default_value, 4),
|
||||
"source": "principled",
|
||||
}
|
||||
# Legacy graph: the largest colour found on a mix or diffuse node is the paint.
|
||||
best = None
|
||||
roughness = 0.5
|
||||
metallic = 0.0
|
||||
for node in nodes:
|
||||
if node.type in ("MIX", "MIX_RGB"):
|
||||
for socket in node.inputs:
|
||||
try:
|
||||
value = socket.default_value
|
||||
except Exception: # noqa: BLE001 - not every socket has a default
|
||||
continue
|
||||
if not hasattr(value, "__len__") or len(value) < 3:
|
||||
continue
|
||||
candidate = (value[0], value[1], value[2])
|
||||
if max(candidate) < 0.03:
|
||||
continue
|
||||
# Prefer the most saturated colour, not the brightest: the paint's red
|
||||
# (0.49, 0, 0) is darker than the 0.8 grey diffuse beside it, and a
|
||||
# "brightest wins" rule picked the grey - which is how a red car reaches
|
||||
# Unreal as a grey one.
|
||||
if best is None or _colour_rank(candidate) > _colour_rank(best):
|
||||
best = candidate
|
||||
elif node.type == "BSDF_DIFFUSE":
|
||||
colour = node.inputs["Color"].default_value
|
||||
candidate = (colour[0], colour[1], colour[2])
|
||||
if best is None or _colour_rank(candidate) > _colour_rank(best):
|
||||
best = candidate
|
||||
elif node.type == "BSDF_GLOSSY":
|
||||
try:
|
||||
roughness = round(node.inputs["Roughness"].default_value, 4)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
if best is None:
|
||||
return None
|
||||
return {
|
||||
"base_color": [round(best[0], 4), round(best[1], 4), round(best[2], 4)],
|
||||
"roughness": roughness,
|
||||
"metallic": metallic,
|
||||
"source": "legacy graph",
|
||||
}
|
||||
|
||||
|
||||
def ensure_principled(material):
|
||||
"""Rewire a legacy graph to a Principled BSDF so the FBX export carries a colour.
|
||||
|
||||
The template's car paint is a mix-shader graph, and the FBX exporter cannot map that -
|
||||
which is why SM_Vehicle_Body arrived in Unreal holding `WorldGridMaterial` (the default
|
||||
fallback) and rendered black while Blender showed red. Converting in place keeps the
|
||||
colour and makes the export uniform.
|
||||
"""
|
||||
if not material.use_nodes:
|
||||
return False
|
||||
nodes = material.node_tree.nodes
|
||||
if nodes.get("Principled BSDF"):
|
||||
return False
|
||||
spec = read_material(material)
|
||||
if not spec:
|
||||
return False
|
||||
nodes.clear()
|
||||
output = nodes.new("ShaderNodeOutputMaterial")
|
||||
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
|
||||
colour = spec["base_color"]
|
||||
bsdf.inputs["Base Color"].default_value = (colour[0], colour[1], colour[2], 1.0)
|
||||
bsdf.inputs["Roughness"].default_value = max(0.05, spec.get("roughness", 0.5))
|
||||
bsdf.inputs["Metallic"].default_value = spec.get("metallic", 0.0)
|
||||
material.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
|
||||
log(f"converted {material.name} to Principled, base "
|
||||
f"{tuple(round(c, 3) for c in colour)}")
|
||||
return True
|
||||
|
||||
|
||||
def classify(obj):
|
||||
"""Assign an object to a system by its materials first, then by its name."""
|
||||
materials = [m.name.lower() for m in obj.data.materials if m]
|
||||
@@ -234,8 +330,22 @@ def main(argv=None) -> int:
|
||||
log(f"SM_Vehicle_Body_LOD{index}: {tris} triangles")
|
||||
bpy.data.objects.remove(lod, do_unlink=True)
|
||||
|
||||
converted = sum(1 for m in bpy.data.materials if ensure_principled(m))
|
||||
log(f"legacy materials converted for export: {converted}")
|
||||
|
||||
materials = {}
|
||||
for material in bpy.data.materials:
|
||||
found = read_material(material)
|
||||
if found:
|
||||
materials[material.name] = found
|
||||
principled = sum(1 for m in bpy.data.materials
|
||||
if m.use_nodes and m.node_tree.nodes.get("Principled BSDF"))
|
||||
log(f"materials: {len(materials)} read, {principled} of them Principled, "
|
||||
f"{len(materials) - principled} from legacy graphs")
|
||||
|
||||
report = {
|
||||
"template": bpy.data.filepath,
|
||||
"materials": materials,
|
||||
"target_dimensions_m": {"length": TARGET_LENGTH_M, "width": TARGET_WIDTH_M,
|
||||
"height": TARGET_HEIGHT_M},
|
||||
"systems": {system: {"object": joined[system].name,
|
||||
|
||||
Reference in new issue
Block a user