feat: add Cesium semantic inspection mode

This commit is contained in:
2026-08-05 13:24:39 +08:00
parent 400525dd07
commit 607d8fc0b1
18 changed files with 542 additions and 56 deletions

View File

@@ -29,6 +29,16 @@ from osmassets.materials import CESIUM_EXPORT_PROPERTY, link_alpha_clip # noqa:
# mesh's shared slots can recognise its own output and leave it alone.
EXPORT_PREFIX = "Cesium "
# Top-level collections are the authoring scene's stable semantic boundary.
# Their numeric prefixes are draw-order labels in Blender, not part of the
# Cesium asset contract below.
SEMANTIC_ASSETS = (
("roads", "道路", ("03_Roads",)),
("buildings", "建筑", ("04_Buildings",)),
("vegetation", "绿化与设施", ("02_Green", "05_Props")),
("water", "水体", ("01_Water",)),
)
# Fraction of its own albedo a cut-out foliage material emits, to keep the
# shadowed side of a crown off Cesium's near-black ambient floor. Kept well
# under the 0.18 the buildings use: a tree still has to read as lit from one
@@ -544,6 +554,54 @@ def triangulate_mesh(obj):
obj.modifiers.remove(modifier)
def export_glb(filepath, meshes):
"""Export a prepared object subset without changing authoring visibility."""
bpy.ops.object.select_all(action="DESELECT")
for obj in meshes:
obj.select_set(True)
bpy.context.view_layer.objects.active = meshes[0] if meshes else None
os.makedirs(os.path.dirname(filepath), exist_ok=True)
bpy.ops.export_scene.gltf(
filepath=filepath,
export_format="GLB",
use_selection=True,
export_apply=False,
export_texcoords=True,
export_normals=True,
export_materials="EXPORT",
export_image_format="AUTO",
export_tangents=True,
export_extras=True,
export_cameras=False,
export_lights=False,
)
def semantic_asset_specs(glb_path, meshes):
"""Derive optional inspection assets from the scene's named collections."""
stem, extension = os.path.splitext(glb_path)
by_collection = {}
for collection in bpy.context.scene.collection.children:
by_collection[collection.name] = set(collection.all_objects)
specs = []
for asset_id, label, collection_names in SEMANTIC_ASSETS:
objects = set()
for name in collection_names:
objects.update(by_collection.get(name, set()))
subset = [obj for obj in meshes if obj in objects]
if not subset:
continue
path = stem + "-" + asset_id + extension
specs.append({
"id": asset_id,
"label": label,
"path": path,
"meshes": subset,
})
return specs
def export(args):
if not os.path.exists(args["blend"]):
raise FileNotFoundError(args["blend"])
@@ -583,30 +641,10 @@ def export(args):
material_map[source.name] = make_export_material(source)
slot.material = material_map[source.name]
bpy.ops.object.select_all(action="DESELECT")
for obj in meshes:
obj.select_set(True)
bpy.context.view_layer.objects.active = meshes[0] if meshes else None
os.makedirs(os.path.dirname(args["glb"]), exist_ok=True)
bpy.ops.export_scene.gltf(
filepath=args["glb"],
export_format="GLB",
use_selection=True,
export_apply=False,
export_texcoords=True,
export_normals=True,
export_materials="EXPORT",
export_image_format="AUTO",
# The foliage materials carry a normal map, and glTF leaves tangent
# derivation to the renderer when TANGENT is absent. On thin
# double-sided leaf cards that derivation is unreliable, so ship real
# tangents — it costs four floats a vertex on meshes this small.
export_tangents=True,
export_extras=True,
export_cameras=False,
export_lights=False,
)
export_glb(args["glb"], meshes)
semantic_assets = semantic_asset_specs(args["glb"], meshes)
for asset in semantic_assets:
export_glb(asset["path"], asset["meshes"])
scene = bpy.context.scene
try:
@@ -626,7 +664,14 @@ def export(args):
"type": "model",
"url": os.path.basename(args["glb"]),
"enabled": True,
}],
}] + [{
"id": asset["id"],
"label": asset["label"],
"type": "model",
"url": os.path.basename(asset["path"]),
"enabled": False,
"category": "semantic",
} for asset in semantic_assets],
"coordinate_system": "local ENU meters (X east, Y north, Z up)",
"heading_correction_degrees": -90.0,
"anchor": {"longitude": center_lon, "latitude": center_lat, "height": 0.35},
@@ -660,6 +705,7 @@ def export(args):
print("CESIUM_EXPORT_DONE", json.dumps({
"glb": args["glb"], "metadata": args["metadata"],
"meshes": len(meshes), "materials": len(material_map),
"semantic_assets": [asset["id"] for asset in semantic_assets],
"anchor": [center_lon, center_lat],
}, ensure_ascii=True))