From fef855d951034424cb2f33a4bd7f11aee39d3d88 Mon Sep 17 00:00:00 2001 From: Jonas Haugesen Date: Sun, 5 Apr 2026 09:30:12 +0200 Subject: [PATCH] blender export plugin --- blender/addons/snow_trail_export.py | 218 ++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 blender/addons/snow_trail_export.py diff --git a/blender/addons/snow_trail_export.py b/blender/addons/snow_trail_export.py new file mode 100644 index 0000000..759a734 --- /dev/null +++ b/blender/addons/snow_trail_export.py @@ -0,0 +1,218 @@ +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()