Files
snow_trail/blender/addons/snow_trail_export.py
2026-04-05 09:30:12 +02:00

219 lines
6.9 KiB
Python

bl_info = {
"name": "Snow Trail Export",
"author": "Snow Trail",
"version": (1, 0, 0),
"blender": (5, 0, 0),
"location": "3D Viewport > Sidebar > Snow Trail",
"description": "One-click glTF export to project assets folder",
"category": "Import-Export",
}
import bpy
from pathlib import Path
def find_project_root():
blend_path = Path(bpy.data.filepath)
if not blend_path.exists():
return None
candidate = blend_path.parent
while candidate != candidate.parent:
if (candidate / "assets").is_dir() and (candidate / "blender").is_dir():
return candidate
candidate = candidate.parent
return None
def get_export_path():
blend_path = Path(bpy.data.filepath)
project_root = find_project_root()
if project_root is None:
return None
stem = blend_path.stem
return project_root / "assets" / "meshes" / f"{stem}.gltf"
class SNOWTRAIL_OT_export_gltf(bpy.types.Operator):
bl_idname = "snow_trail.export_gltf"
bl_label = "Export to Project"
bl_description = "Export selected objects as glTF to assets/meshes/"
bl_options = {'REGISTER'}
def execute(self, context):
if not bpy.data.filepath:
self.report({'ERROR'}, "Save the .blend file first")
return {'CANCELLED'}
export_path = get_export_path()
if export_path is None:
self.report({'ERROR'}, "Could not find project root (looking for assets/ + blender/ dirs)")
return {'CANCELLED'}
export_path.parent.mkdir(parents=True, exist_ok=True)
selected = [obj for obj in context.selected_objects if obj.type == 'MESH']
if not selected:
self.report({'ERROR'}, "No mesh objects selected")
return {'CANCELLED'}
props = context.scene.snow_trail_export
bpy.ops.export_scene.gltf(
filepath=str(export_path),
export_format='GLTF_SEPARATE',
use_selection=True,
export_apply=props.apply_modifiers,
export_yup=True,
export_texcoords=True,
export_normals=True,
export_colors=props.export_vertex_colors,
export_materials='NONE' if not props.export_materials else 'EXPORT',
export_animations=props.export_animations,
)
rel_path = export_path.relative_to(find_project_root())
self.report({'INFO'}, f"Exported → {rel_path}")
return {'FINISHED'}
class SNOWTRAIL_OT_export_heightmap(bpy.types.Operator):
bl_idname = "snow_trail.export_heightmap"
bl_label = "Bake Heightmap"
bl_description = "Run heightmap bake script for selected terrain"
bl_options = {'REGISTER'}
def execute(self, context):
project_root = find_project_root()
if project_root is None:
self.report({'ERROR'}, "Could not find project root")
return {'CANCELLED'}
script_path = project_root / "blender" / "scripts" / "generate_heightmap.py"
if not script_path.exists():
self.report({'ERROR'}, f"Script not found: {script_path}")
return {'CANCELLED'}
import importlib.util
spec = importlib.util.spec_from_file_location("generate_heightmap", str(script_path))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
terrain_obj = context.active_object
if terrain_obj is None or terrain_obj.type != 'MESH':
self.report({'ERROR'}, "Select a mesh object first")
return {'CANCELLED'}
output_path = project_root / "assets" / "textures" / "terrain_heightmap.exr"
mod.bake_heightmap(
terrain_obj=terrain_obj,
resolution=context.scene.snow_trail_export.heightmap_resolution,
output_path=str(output_path),
)
self.report({'INFO'}, f"Heightmap → assets/textures/terrain_heightmap.exr")
return {'FINISHED'}
class SNOWTRAIL_ExportProperties(bpy.types.PropertyGroup):
apply_modifiers: bpy.props.BoolProperty(
name="Apply Modifiers",
default=True,
description="Apply modifiers before export",
)
export_vertex_colors: bpy.props.BoolProperty(
name="Vertex Colors",
default=True,
description="Export vertex colors",
)
export_materials: bpy.props.BoolProperty(
name="Materials",
default=False,
description="Export materials (usually not needed for retro aesthetic)",
)
export_animations: bpy.props.BoolProperty(
name="Animations",
default=False,
description="Export animations",
)
heightmap_resolution: bpy.props.IntProperty(
name="Heightmap Resolution",
default=1024,
min=128,
max=4096,
description="Resolution for heightmap bake",
)
class SNOWTRAIL_PT_export_panel(bpy.types.Panel):
bl_label = "Snow Trail Export"
bl_idname = "SNOWTRAIL_PT_export_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Snow Trail"
def draw(self, context):
layout = self.layout
props = context.scene.snow_trail_export
export_path = get_export_path()
if export_path:
project_root = find_project_root()
rel = export_path.relative_to(project_root)
layout.label(text=f"Target: {rel}", icon='FILE')
elif not bpy.data.filepath:
layout.label(text="Save .blend file first!", icon='ERROR')
else:
layout.label(text="Project root not found!", icon='ERROR')
layout.separator()
box = layout.box()
box.label(text="glTF Options:", icon='MESH_DATA')
box.prop(props, "apply_modifiers")
box.prop(props, "export_vertex_colors")
box.prop(props, "export_materials")
box.prop(props, "export_animations")
selected_meshes = [o for o in context.selected_objects if o.type == 'MESH']
row = layout.row()
row.scale_y = 1.5
row.operator("snow_trail.export_gltf", icon='EXPORT')
if not selected_meshes:
row.enabled = False
if selected_meshes:
layout.label(text=f"{len(selected_meshes)} mesh(es) selected")
else:
layout.label(text="Select mesh objects to export", icon='INFO')
layout.separator()
box = layout.box()
box.label(text="Terrain Tools:", icon='WORLD')
box.prop(props, "heightmap_resolution")
box.operator("snow_trail.export_heightmap", icon='IMAGE_DATA')
classes = (
SNOWTRAIL_ExportProperties,
SNOWTRAIL_OT_export_gltf,
SNOWTRAIL_OT_export_heightmap,
SNOWTRAIL_PT_export_panel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.snow_trail_export = bpy.props.PointerProperty(type=SNOWTRAIL_ExportProperties)
def unregister():
del bpy.types.Scene.snow_trail_export
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()