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()