Refactor Cesium material export contract
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -61,12 +61,14 @@ MATERIALS = {
|
||||
"diffuse": "leafy_grass_diff_1k.jpg",
|
||||
"normal": "leafy_grass_nor_gl_1k.jpg",
|
||||
"roughness": 0.92, "scale": 7.0,
|
||||
"tint": (0.12, 0.48, 0.08), "tint_factor": 0.72},
|
||||
"tint": (0.12, 0.48, 0.08), "tint_factor": 0.72,
|
||||
"cesium": {"tint": ((0.12, 0.48, 0.08), 0.72)}},
|
||||
"scrub": {"kind": "textured", "name": "Scrub Ground Cover",
|
||||
"diffuse": "leafy_grass_diff_1k.jpg",
|
||||
"normal": "leafy_grass_nor_gl_1k.jpg",
|
||||
"roughness": 0.96, "scale": 15.0,
|
||||
"tint": (0.085, 0.30, 0.065), "tint_factor": 0.46},
|
||||
"tint": (0.085, 0.30, 0.065), "tint_factor": 0.46,
|
||||
"cesium": {"tint": ((0.08, 0.28, 0.07), 0.28)}},
|
||||
|
||||
"fountain_stone": {"kind": "solid", "name": "Fountain Stone",
|
||||
"color": (0.42, 0.45, 0.43), "roughness": 0.72},
|
||||
@@ -81,25 +83,37 @@ MATERIALS = {
|
||||
"diffuse": "white_plaster_02_diff_1k.jpg",
|
||||
"normal": "white_plaster_02_nor_gl_1k.jpg",
|
||||
"roughness": 0.82, "scale": 4.2, "metallic": 0.0,
|
||||
"tint": (0.92, 0.94, 0.92), "tint_factor": 0.38},
|
||||
"tint": (0.92, 0.94, 0.92), "tint_factor": 0.38,
|
||||
"cesium": {"tint": ((0.92, 0.94, 0.92), 0.38),
|
||||
"metallic": 0.0,
|
||||
"base_color": (0.93, 0.94, 0.91),
|
||||
"emission": ((0.93, 0.94, 0.91), 0.18)}},
|
||||
"building_industrial": {"kind": "textured",
|
||||
"name": "Industrial White Ribbed Facade",
|
||||
"diffuse": "corrugated_iron_03_diff_1k.jpg",
|
||||
"normal": "corrugated_iron_03_nor_gl_1k.jpg",
|
||||
"roughness": 0.56, "scale": 2.4, "metallic": 0.16,
|
||||
"tint": (0.86, 0.92, 0.94), "tint_factor": 0.68},
|
||||
"tint": (0.86, 0.92, 0.94), "tint_factor": 0.68,
|
||||
"cesium": {"tint": ((0.90, 0.93, 0.91), 0.86),
|
||||
"metallic": 0.08,
|
||||
"emission": ((0.90, 0.93, 0.91), 0.18)}},
|
||||
"building_office_roof": {"kind": "textured", "name": "Office Light Flat Roof",
|
||||
"diffuse": "concrete_floor_02_diff_1k.jpg",
|
||||
"normal": "concrete_floor_02_bump_1k.jpg",
|
||||
"roughness": 0.84, "scale": 5.0,
|
||||
"normal_is_bump": True,
|
||||
"tint": (0.82, 0.86, 0.88), "tint_factor": 0.35},
|
||||
"tint": (0.82, 0.86, 0.88), "tint_factor": 0.35,
|
||||
"cesium": {"tint": ((0.82, 0.86, 0.88), 0.35),
|
||||
"base_color": (0.88, 0.90, 0.88),
|
||||
"emission": ((0.88, 0.90, 0.88), 0.14)}},
|
||||
"building_industrial_roof": {"kind": "textured",
|
||||
"name": "Factory Blue Metal Roof",
|
||||
"diffuse": "blue_metal_plate_diff_1k.jpg",
|
||||
"normal": "blue_metal_plate_nor_gl_1k.jpg",
|
||||
"roughness": 0.48, "scale": 3.4, "metallic": 0.28,
|
||||
"tint": (0.03, 0.42, 0.78), "tint_factor": 0.45},
|
||||
"tint": (0.03, 0.42, 0.78), "tint_factor": 0.45,
|
||||
"cesium": {"tint": ((0.08, 0.50, 0.88), 0.58),
|
||||
"emission": ((0.08, 0.50, 0.88), 0.12)}},
|
||||
"building_glass": {"kind": "solid", "name": "Office Blue Gray Glass",
|
||||
"color": (0.12, 0.20, 0.24), "roughness": 0.22,
|
||||
"metallic": 0.10},
|
||||
@@ -117,14 +131,18 @@ MATERIALS = {
|
||||
(0.12, 0.36, 0.08)),
|
||||
"scale": 3.2, "detail": 3.8,
|
||||
"bump_strength": 0.08},
|
||||
"cesium": {"tint": ((0.06, 0.22, 0.05), 0.18)}},
|
||||
"cesium": {"tint": ((0.06, 0.22, 0.05), 0.18),
|
||||
"base_color": (0.065, 0.24, 0.055),
|
||||
"emission": ((0.025, 0.07, 0.02), 0.015)}},
|
||||
"tree_crown_light": {"kind": "solid", "name": "Tree Crown Light",
|
||||
"color": (0.13, 0.42, 0.09), "roughness": 0.88,
|
||||
"procedural": {"colors": ((0.07, 0.25, 0.05),
|
||||
(0.22, 0.56, 0.13)),
|
||||
"scale": 3.6, "detail": 3.4,
|
||||
"bump_strength": 0.07},
|
||||
"cesium": {"tint": ((0.16, 0.42, 0.09), 0.16)}},
|
||||
"cesium": {"tint": ((0.16, 0.42, 0.09), 0.16),
|
||||
"base_color": (0.14, 0.40, 0.085),
|
||||
"emission": ((0.045, 0.12, 0.03), 0.015)}},
|
||||
"tree_crown": {"kind": "solid", "name": "Tree Crown",
|
||||
"color": (0.10, 0.36, 0.08), "roughness": 0.88,
|
||||
"procedural": {"colors": ((0.04, 0.18, 0.04),
|
||||
@@ -136,22 +154,6 @@ MATERIALS = {
|
||||
"emission": ((0.04, 0.11, 0.035), 0.02)}},
|
||||
}
|
||||
|
||||
# Cesium-specific overrides that don't have a home in the material system yet:
|
||||
# metallic overrides (flat values, not materials) and emission overrides for
|
||||
# colours that export_cesium.py hand-tuned separately.
|
||||
CESIUM_EXPORT = {
|
||||
"metallic_overrides": {
|
||||
"Office White Plaster Facade": 0.0,
|
||||
"Industrial White Ribbed Facade": 0.08,
|
||||
},
|
||||
"emission_overrides": {
|
||||
"Office White Plaster Facade": ((0.93, 0.94, 0.91), 0.18),
|
||||
"Office Light Flat Roof": ((0.88, 0.90, 0.88), 0.14),
|
||||
"Industrial White Ribbed Facade": ((0.90, 0.93, 0.91), 0.18),
|
||||
"Factory Blue Metal Roof": ((0.08, 0.50, 0.88), 0.12),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def road_material_specs():
|
||||
"""Road layer materials as MATERIALS-shaped specs, in draw order."""
|
||||
|
||||
@@ -6,11 +6,14 @@ the catalog importable by plain Python, and by anything else that wants to read
|
||||
the scene's material definitions without launching Blender.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
CESIUM_EXPORT_PROPERTY = "cesium_export"
|
||||
|
||||
TEXTURE_ROOT = os.path.abspath(os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "..", "..",
|
||||
"assets", "textures", "polyhaven"
|
||||
@@ -195,15 +198,26 @@ def link_alpha_clip(material, alpha_output, bsdf, cutoff=0.5):
|
||||
material.use_backface_culling = False
|
||||
|
||||
|
||||
def apply_cesium_contract(material, spec):
|
||||
contract = spec.get("cesium")
|
||||
if contract is None:
|
||||
if CESIUM_EXPORT_PROPERTY in material:
|
||||
del material[CESIUM_EXPORT_PROPERTY]
|
||||
return material
|
||||
material[CESIUM_EXPORT_PROPERTY] = json.dumps(contract, sort_keys=True)
|
||||
return material
|
||||
|
||||
|
||||
def from_spec(spec):
|
||||
"""Build a material from a `catalog.MATERIALS` entry."""
|
||||
if spec["kind"] == "textured":
|
||||
return make_textured_material(
|
||||
material = make_textured_material(
|
||||
spec["name"], spec["diffuse"], spec["normal"],
|
||||
roughness=spec.get("roughness", 0.8), scale=spec["scale"],
|
||||
normal_is_bump=spec.get("normal_is_bump", False),
|
||||
metallic=spec.get("metallic", 0.0),
|
||||
tint=spec.get("tint"), tint_factor=spec.get("tint_factor", 0.0))
|
||||
return apply_cesium_contract(material, spec)
|
||||
|
||||
material = make_material(spec["name"], spec["color"],
|
||||
spec.get("roughness", 0.8),
|
||||
@@ -215,4 +229,4 @@ def from_spec(spec):
|
||||
detail=procedural["detail"],
|
||||
bump_strength=procedural["bump_strength"],
|
||||
object_space=procedural.get("object_space", False))
|
||||
return material
|
||||
return apply_cesium_contract(material, spec)
|
||||
|
||||
Reference in New Issue
Block a user