Refactor Cesium material export contract

This commit is contained in:
2026-08-03 11:52:38 +08:00
parent 50dc5f4e5a
commit 0635c09458
16 changed files with 583 additions and 95 deletions

View File

@@ -22,7 +22,7 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
from osmassets.materials import link_alpha_clip # noqa: E402
from osmassets.materials import CESIUM_EXPORT_PROPERTY, link_alpha_clip # noqa: E402
# Marks a material this exporter produced, so a second pass over an instanced
@@ -41,11 +41,11 @@ FOLIAGE_EMISSION = 0.25
# exists because the apple atlas is genuinely dark: its green texels average
# sRGB (0.249, 0.35, 0.12), a deep forest green, and the bark is darker still.
# Rendered at true albedo that is correct — but nothing else in this scene is
# at true albedo. Every other material goes through EXPORT_TINTS (grass mixes
# 72% toward a bright green, the ribbed facade 86% toward white) and
# EXPORT_EMISSION_OVERRIDES (0.18 on the buildings), all hand-tuned against
# Cesium's washed-out default lighting. A new asset dropped in untuned is the
# one thing rendering honestly, and next to the rest it reads as black.
# at true albedo. Every other material goes through Cesium export contracts
# (grass mixes 72% toward a bright green, the ribbed facade 86% toward white,
# 0.18 emission on the buildings), all hand-tuned against Cesium's washed-out
# default lighting. A new asset dropped in untuned is the one thing rendering
# honestly, and next to the rest it reads as black.
#
# A gain rather than a tint, because a tint is what the other materials use and
# it is wrong here: they are single-surface, this is an atlas holding leaves,
@@ -65,6 +65,9 @@ FOLIAGE_ALBEDO_GAIN = 2.1
# force on the trunk — bark just becomes a warmer brown, which it should be.
FOLIAGE_SATURATION = 1.75
# Legacy fallback for .blend files created before materials carried their own
# `cesium_export` custom property. New scenes should get these values from
# catalog.MATERIALS[*]["cesium"], serialized by osmassets.materials.from_spec().
EXPORT_TINTS = {
"Grass": ((0.12, 0.48, 0.08), 0.72),
"Tree Crown Dark": ((0.06, 0.22, 0.05), 0.18),
@@ -142,6 +145,18 @@ def source_color(material):
return color
def cesium_contract(material):
payload = material.get(CESIUM_EXPORT_PROPERTY)
if not payload:
return {}
try:
if isinstance(payload, str):
payload = json.loads(payload)
except (TypeError, ValueError):
return {}
return payload if isinstance(payload, dict) else {}
def principled_bsdf(material):
if not material.use_nodes:
return None
@@ -225,8 +240,7 @@ def tinted_image(source, name, tint, factor):
return result
def cesium_tinted_image(material, source):
tint = EXPORT_TINTS.get(material.name)
def cesium_tinted_image(material, source, tint):
if not tint or not source:
return source
color, factor = tint
@@ -328,8 +342,11 @@ def tree_crown_image():
def make_export_material(material):
contract = cesium_contract(material)
result = material.copy()
result.name = EXPORT_PREFIX + material.name
if CESIUM_EXPORT_PROPERTY in result:
del result[CESIUM_EXPORT_PROPERTY]
result.use_nodes = True
nodes = result.node_tree.nodes
links = result.node_tree.links
@@ -339,14 +356,20 @@ def make_export_material(material):
output.location = (520, 0)
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (250, 0)
base_color = EXPORT_BASE_COLOR_OVERRIDES.get(
material.name, source_color(material))
has_base_color_override = (
"base_color" in contract or
material.name in EXPORT_BASE_COLOR_OVERRIDES
)
base_color = contract.get("base_color", EXPORT_BASE_COLOR_OVERRIDES.get(
material.name, source_color(material)))
bsdf.inputs["Base Color"].default_value = (*base_color, 1.0)
bsdf.inputs["Roughness"].default_value = source_principled_value(
material, "Roughness", 0.8)
bsdf.inputs["Metallic"].default_value = EXPORT_METALLIC_OVERRIDES.get(
material.name, source_principled_value(material, "Metallic", 0.0))
emission = EXPORT_EMISSION_OVERRIDES.get(material.name)
bsdf.inputs["Metallic"].default_value = contract.get(
"metallic", EXPORT_METALLIC_OVERRIDES.get(
material.name, source_principled_value(material, "Metallic", 0.0)))
emission = contract.get(
"emission", EXPORT_EMISSION_OVERRIDES.get(material.name))
if emission:
emission_color, emission_strength = emission
if "Emission Color" in bsdf.inputs:
@@ -365,13 +388,14 @@ def make_export_material(material):
if material.name == "Tree Crown":
diffuse = tree_crown_image()
else:
diffuse = cesium_tinted_image(material, diffuse)
tint = contract.get("tint", EXPORT_TINTS.get(material.name))
diffuse = cesium_tinted_image(material, diffuse, tint)
if alpha_clipped and diffuse is not None:
safe_name = material.name.replace(" ", "_")
diffuse = alpha_dilated_image(
diffuse, f"{EXPORT_PREFIX}{safe_name} Dilated",
gain=FOLIAGE_ALBEDO_GAIN, saturation=FOLIAGE_SATURATION)
if material.name in EXPORT_BASE_COLOR_OVERRIDES:
if has_base_color_override:
diffuse = None
normal = None
mapping = None
@@ -416,10 +440,9 @@ def make_export_material(material):
# Lift the crown out of Cesium's ambient. The preview configures no
# environment map, so anything the sun does not hit directly falls to a
# weak default spherical-harmonic term — which is why every other
# material here carries an EXPORT_EMISSION_OVERRIDES entry. A crown is
# mostly self-shadowed leaf cards facing away from the sun, so at
# distance it collapses into one dark mass while a sunlit close-up
# still reads fine.
# material here carries an emission override. A crown is mostly
# self-shadowed leaf cards facing away from the sun, so at distance it
# collapses into one dark mass while a sunlit close-up still reads fine.
#
# Feed the diffuse back in as the emissive texture rather than using a
# flat colour: a constant would wash the bark with leaf green, whereas