通用化项目结构:Blender 脚本去硬编码、config 路径占位符、layerPrefix 配置
- blender/generate_nantaizi.py → blender/generate_scene.py(去硬编码路径, --geojson 改为可选,--office-overrides 替代硬编码 way ID) - blender/export_cesium.py CLI 参数改为必传,修复隐藏物体导出崩溃 - blender/README.md 重写为中文通用文档 - config/default.json 路径改为占位符,template.json 新增 blender 配置块 - scripts/build-osm2streets-qgis.js 新增 layerPrefix 配置驱动 QGIS 图层名 - package.json name → osm-gis-pipeline - README.md 重写并增加实际运行示例
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Export the Nantaizi Blender master scene as a Cesium-ready GLB.
|
||||
"""Export a Blender scene (from generate_scene.py) as a Cesium-ready GLB.
|
||||
|
||||
The authoring scene intentionally uses a few Blender-only nodes (for example
|
||||
the grass tint and procedural tree crown variation). glTF has a smaller
|
||||
@@ -17,70 +17,38 @@ import bpy
|
||||
import numpy as np
|
||||
|
||||
|
||||
DEFAULT_BLEND = (
|
||||
"/Users/que01/osm2streets-qgis-workflow/outputs/"
|
||||
"nantaizi-lake-innovation-valley/nantaizi_lake_innovation_valley.blend"
|
||||
)
|
||||
DEFAULT_GLB = (
|
||||
"/Users/que01/osm2streets-qgis-workflow/outputs/"
|
||||
"nantaizi-lake-innovation-valley/nantaizi_lake_innovation_valley_cesium.glb"
|
||||
)
|
||||
DEFAULT_METADATA = (
|
||||
"/Users/que01/osm2streets-qgis-workflow/outputs/"
|
||||
"nantaizi-lake-innovation-valley/nantaizi_lake_innovation_valley_cesium.json"
|
||||
)
|
||||
|
||||
# Blender 里使用 MixRGB 在 Poly Haven 贴图上叠加颜色修正。
|
||||
# glTF/Cesium 不能稳定保留这类 Blender 专用节点,所以导出前要把
|
||||
# 同样的 tint 烘焙到临时图片里,再写入 GLB。
|
||||
# 这样 Cesium 预览会尽量接近 Blender 视图,也能避免浅色办公楼外墙、
|
||||
# 屋顶在导出后退回偏黑的原始贴图。
|
||||
EXPORT_TINTS = {
|
||||
"Grass": ((0.12, 0.48, 0.08), 0.72),
|
||||
"Office White Plaster Facade": ((0.92, 0.94, 0.92), 0.38),
|
||||
# 兼容旧 .blend:普通办公楼从金属板切回灰泥前生成的文件,
|
||||
# 可能还保留这个旧材质名。
|
||||
"Office White Metal Facade": ((0.92, 0.94, 0.92), 0.68),
|
||||
"Office Light Flat Roof": ((0.82, 0.86, 0.88), 0.35),
|
||||
"Industrial White Ribbed Facade": ((0.90, 0.93, 0.91), 0.86),
|
||||
"Factory Blue Metal Roof": ((0.08, 0.50, 0.88), 0.58),
|
||||
}
|
||||
|
||||
# Cesium 的光照比 Blender 材质预览更“硬”。外墙默认不要保留金属度,
|
||||
# 除非确实是工业金属墙面;否则旧金属墙贴图直接导出时容易发黑。
|
||||
EXPORT_METALLIC_OVERRIDES = {
|
||||
"Office White Plaster Facade": 0.0,
|
||||
"Office White Metal Facade": 0.0,
|
||||
"Industrial White Ribbed Facade": 0.08,
|
||||
}
|
||||
|
||||
# 普通办公楼在 Cesium 里使用稳定浅色展示材质。
|
||||
# 这些建筑不是工业厂房,导出时如果继续采样原始墙面/屋顶贴图,
|
||||
# Cesium 的光照和 mipmap 会把贴图里的暗斑放大,画面就会显得脏黑。
|
||||
# 所以这里直接覆盖 Base Color,只保留几何、窗带和必要法线细节。
|
||||
EXPORT_BASE_COLOR_OVERRIDES = {
|
||||
"Office White Plaster Facade": (0.93, 0.94, 0.91),
|
||||
"Office White Metal Facade": (0.93, 0.94, 0.91),
|
||||
"Office Light Flat Roof": (0.88, 0.90, 0.88),
|
||||
}
|
||||
|
||||
# Cesium 是真实 PBR 光照,侧墙和屋顶会比 Blender 材质预览暗很多。
|
||||
# 给普通建筑加很弱的 emissive 补光,不是做“发光楼”,只是模拟
|
||||
# Blender 预览里的环境光,让浅色办公楼在网页里保持干净明亮。
|
||||
EXPORT_EMISSION_OVERRIDES = {
|
||||
"Office White Plaster Facade": ((0.93, 0.94, 0.91), 0.18),
|
||||
"Office White Metal Facade": ((0.93, 0.94, 0.91), 0.18),
|
||||
"Office Light Flat Roof": ((0.88, 0.90, 0.88), 0.14),
|
||||
# 三栋厂房仍保留金属/波纹质感,但 Cesium 里原贴图会偏暗。
|
||||
# 这里加少量补光,让墙面和蓝色屋顶更接近 Blender 预览。
|
||||
"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 cli_args():
|
||||
values = {"blend": DEFAULT_BLEND, "glb": DEFAULT_GLB,
|
||||
"metadata": DEFAULT_METADATA}
|
||||
values = {"blend": None, "glb": None, "metadata": None}
|
||||
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
@@ -89,11 +57,16 @@ def cli_args():
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
if not values.get("blend"):
|
||||
raise RuntimeError("--blend is required")
|
||||
if not values.get("glb"):
|
||||
raise RuntimeError("--glb is required")
|
||||
if not values.get("metadata"):
|
||||
raise RuntimeError("--metadata is required")
|
||||
return values
|
||||
|
||||
|
||||
def image_for(material, want_normal=False):
|
||||
"""Find a diffuse/normal image from the authoring material nodes."""
|
||||
candidates = []
|
||||
for node in material.node_tree.nodes:
|
||||
if node.type != "TEX_IMAGE" or not node.image:
|
||||
@@ -129,7 +102,6 @@ def source_texture_scale(material):
|
||||
|
||||
|
||||
def tinted_image(source, name, tint, factor):
|
||||
"""Bake Blender's MixRGB tint into a generated image for glTF."""
|
||||
existing = bpy.data.images.get(name)
|
||||
if existing:
|
||||
return existing
|
||||
@@ -148,7 +120,6 @@ def tinted_image(source, name, tint, factor):
|
||||
|
||||
|
||||
def cesium_tinted_image(material, source):
|
||||
"""Return a baked diffuse image matching the Blender authoring tint."""
|
||||
tint = EXPORT_TINTS.get(material.name)
|
||||
if not tint or not source:
|
||||
return source
|
||||
@@ -158,7 +129,6 @@ def cesium_tinted_image(material, source):
|
||||
|
||||
|
||||
def tree_crown_image():
|
||||
"""Bake a compact leafy color variation texture for the web material."""
|
||||
name = "Cesium Tree Crown Baked"
|
||||
existing = bpy.data.images.get(name)
|
||||
if existing:
|
||||
@@ -188,7 +158,6 @@ def tree_crown_image():
|
||||
|
||||
|
||||
def make_export_material(material):
|
||||
"""Build a small Principled + Image Texture material understood by glTF."""
|
||||
result = material.copy()
|
||||
result.name = "Cesium " + material.name
|
||||
result.use_nodes = True
|
||||
@@ -225,9 +194,6 @@ def make_export_material(material):
|
||||
else:
|
||||
diffuse = cesium_tinted_image(material, diffuse)
|
||||
if material.name in EXPORT_BASE_COLOR_OVERRIDES:
|
||||
# 普通办公楼/浅色屋顶使用上面的稳定浅色 Base Color。
|
||||
# 不连接漫反射贴图和法线贴图,避免 Cesium 里重新出现偏黑、
|
||||
# 偏脏的斑驳效果,也避免法线贴图在硬光照下把屋顶压暗。
|
||||
diffuse = None
|
||||
normal = None
|
||||
mapping = None
|
||||
@@ -236,8 +202,6 @@ def make_export_material(material):
|
||||
texcoord.location = (-650, 0)
|
||||
mapping = nodes.new("ShaderNodeMapping")
|
||||
mapping.location = (-450, 0)
|
||||
# The authoring materials use Generated coordinates with these scales.
|
||||
# UVs are used here because Generated coordinates are not part of glTF.
|
||||
mapping.inputs["Scale"].default_value = source_texture_scale(material)
|
||||
links.new(texcoord.outputs["UV"], mapping.inputs["Vector"])
|
||||
|
||||
@@ -262,9 +226,6 @@ def make_export_material(material):
|
||||
links.new(normal_tex.outputs["Color"], normal_map.inputs["Color"])
|
||||
links.new(normal_map.outputs["Normal"], bsdf.inputs["Normal"])
|
||||
|
||||
# These values are deliberately conservative for web rendering. In
|
||||
# particular, avoid transmission/alpha because the source scene has no
|
||||
# transparent geometry and those features are expensive in Cesium.
|
||||
if "Alpha" in bsdf.inputs:
|
||||
bsdf.inputs["Alpha"].default_value = 1.0
|
||||
return result
|
||||
@@ -294,8 +255,6 @@ def apply_mesh_modifiers(obj):
|
||||
try:
|
||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
||||
except RuntimeError:
|
||||
# A failed nonessential modifier should not prevent the rest of
|
||||
# the park from exporting.
|
||||
pass
|
||||
|
||||
|
||||
@@ -304,17 +263,15 @@ def export(args):
|
||||
raise FileNotFoundError(args["blend"])
|
||||
bpy.ops.wm.open_mainfile(filepath=args["blend"])
|
||||
|
||||
# Build one export material per source material and assign it in memory.
|
||||
material_map = {}
|
||||
meshes = []
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
# Cesium supplies the ellipsoid/globe surface. The authoring ground
|
||||
# plane is deliberately omitted so it cannot appear as a large flat
|
||||
# rectangle over the basemap.
|
||||
if obj.name == "Ground Plane":
|
||||
continue
|
||||
if obj.hide_viewport or obj.hide_render:
|
||||
continue
|
||||
meshes.append(obj)
|
||||
apply_mesh_modifiers(obj)
|
||||
unwrap_mesh(obj)
|
||||
@@ -360,8 +317,6 @@ def export(args):
|
||||
"asset": os.path.basename(args["glb"]),
|
||||
"coordinate_system": "local ENU meters (X east, Y north, Z up)",
|
||||
"heading_correction_degrees": -90.0,
|
||||
# A small offset prevents centimeter-high road markings and grass from
|
||||
# fighting with the globe depth buffer at overview distances.
|
||||
"anchor": {"longitude": center_lon, "latitude": center_lat, "height": 0.35},
|
||||
"bounds": bounds,
|
||||
"source_osm": scene.get("source_osm", ""),
|
||||
@@ -390,8 +345,6 @@ def export(args):
|
||||
json.dump(metadata, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
|
||||
# Keep the authoring blend untouched. The temporary export materials are
|
||||
# only present in this Blender process and are not saved.
|
||||
print("CESIUM_EXPORT_DONE", json.dumps({
|
||||
"glb": args["glb"], "metadata": args["metadata"],
|
||||
"meshes": len(meshes), "materials": len(material_map),
|
||||
@@ -400,4 +353,4 @@ def export(args):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
export(cli_args())
|
||||
export(cli_args())
|
||||
Reference in New Issue
Block a user