OverviewFilesCommitsIssuesDocsReleasesPeers

blender-bridge commits / fd3a04e

Finalize command: save .blend + .tscn + .png textures (v0.14.1)
Add Finalize operator in Blender that saves the .blend file and sends a finalize message to Godot. Godot saves CPU-reconstructed textures as .png, converts VT ShaderMaterials to StandardMaterial3D with ImageTexture references, and saves the scene as .tscn. Scenes can be reopened in Godot without Blender connected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fd3a04e20b458ca1b04bf26d6c63099aee49690e
by plankatron <fleeky@gmail.com> · 2026-03-03T22:03:21.000Z

Changed files (4) +153 -3

blender_addon/__init__.py +26 -1
……@@ -11,7 +11,7 @@ Usage:
1111
1212import bpy
1313
14ADDON_VERSION = "0.14.0"
14ADDON_VERSION = "0.14.1"
1515
1616# Support hot-reload (F3 > Reload Scripts)
1717if "shm_protocol" in locals():
……@@ -152,6 +152,10 @@ def _poll_timer_impl(generation: int):
152152 # Other objects (e.g. modifier dependents) still update.
153153 _detector.suppress(updated_names, 5)
154154 got_incoming = True
155 elif text == "finalized":
156 print("BlenderBridge: Godot finalized scene successfully")
157 elif text == "finalize_error":
158 print("BlenderBridge: Godot finalize failed")
155159 elif text.startswith("texture_registered:"):
156160 # Godot registered a texture: texture_registered:name,id
157161 parts = text.split(":", 1)[1].split(",")
……@@ -432,6 +436,25 @@ class BRIDGE_OT_import_scene(bpy.types.Operator):
432436
433437
434438
439class BRIDGE_OT_finalize(bpy.types.Operator):
440 """Save .blend and tell Godot to save .tscn + textures"""
441
442 bl_idname = "bridge.finalize"
443 bl_label = "Finalize Bridge"
444
445 def execute(self, context):
446 global _conn
447 if _conn is None or not _conn.connected:
448 self.report({"ERROR"}, "Not connected to Godot")
449 return {"CANCELLED"}
450 # Save the .blend file
451 bpy.ops.wm.save_mainfile()
452 # Tell Godot to finalize
453 _conn.send_message(b"finalize")
454 self.report({"INFO"}, "Finalize sent — saving .blend and Godot scene")
455 return {"FINISHED"}
456
457
435458# ── UI Panel ─────────────────────────────────────────────────────────────────
436459
437460
……@@ -455,6 +478,7 @@ class BRIDGE_PT_panel(bpy.types.Panel):
455478 layout.operator("bridge.disconnect", icon="UNLINKED")
456479 layout.separator()
457480 layout.operator("bridge.import_scene", icon="FILE_REFRESH")
481 layout.operator("bridge.finalize", icon="EXPORT")
458482
459483 # Status info
460484 box = layout.box()
……@@ -479,6 +503,7 @@ _classes = (
479503 BRIDGE_OT_connect,
480504 BRIDGE_OT_disconnect,
481505 BRIDGE_OT_import_scene,
506 BRIDGE_OT_finalize,
482507 BRIDGE_PT_panel,
483508)
484509
blender_addon/blender_manifest.toml +1 -1
……@@ -2,7 +2,7 @@ schema_version = "1.0.0"
22id = "blender_godot_bridge"
33name = "Blender-Godot Bridge"
44tagline = "Real-time bidirectional scene sync with Godot"
5version = "0.14.0"
5version = "0.14.1"
66blender_version_min = "4.2.0"
77type = "add-on"
88maintainer = "blender-bridge <noreply@localhost>"
src/blender_bridge.cpp +123 -1
……@@ -6,9 +6,14 @@
66#include <string>
77
88#include <godot_cpp/classes/camera3d.hpp>
9#include <godot_cpp/classes/dir_access.hpp>
910#include <godot_cpp/classes/directional_light3d.hpp>
11#include <godot_cpp/classes/image_texture.hpp>
1012#include <godot_cpp/classes/light3d.hpp>
1113#include <godot_cpp/classes/omni_light3d.hpp>
14#include <godot_cpp/classes/packed_scene.hpp>
15#include <godot_cpp/classes/resource_loader.hpp>
16#include <godot_cpp/classes/resource_saver.hpp>
1217#include <godot_cpp/classes/scene_tree.hpp>
1318#include <godot_cpp/classes/spot_light3d.hpp>
1419#include <godot_cpp/classes/window.hpp>
……@@ -72,6 +77,7 @@ void BlenderBridge::_bind_methods() {
7277 ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "virtual_texture_path"), "set_virtual_texture_path", "get_virtual_texture_path");
7378
7479 ClassDB::bind_method(D_METHOD("notify_texture_dirty", "texture_id", "tile_x", "tile_y", "w", "h", "mip"), &BlenderBridge::notify_texture_dirty);
80 ClassDB::bind_method(D_METHOD("finalize_scene", "root", "save_dir"), &BlenderBridge::finalize_scene);
7581
7682 // Signals
7783 ADD_SIGNAL(MethodInfo("client_connected", PropertyInfo(Variant::INT, "client_id")));
……@@ -115,6 +121,101 @@ void BlenderBridge::notify_texture_dirty(int texture_id, int tile_x, int tile_y,
115121 emit_signal("texture_dirty", texture_id, tile_x, tile_y, w, h, mip);
116122}
117123
124// ── Finalize (save textures + scene to disk) ──────────────────────────────────
125
126Error BlenderBridge::finalize_scene(Node *root, const String &save_dir) {
127 if (_virtual_texture_path.is_empty())
128 return ERR_UNCONFIGURED;
129
130 Node *vt_node = get_node_or_null(_virtual_texture_path);
131 VirtualTexture *vt = Object::cast_to<VirtualTexture>(vt_node);
132 if (!vt)
133 return ERR_UNCONFIGURED;
134
135 // Step 1: Save each reconstructed texture as .png
136 TypedArray<Dictionary> textures = vt->get_all_textures();
137 HashMap<int, String> tex_paths; // vt_id → saved .png path
138
139 for (int i = 0; i < textures.size(); i++) {
140 Dictionary tex = textures[i];
141 int tex_id = tex["id"];
142 String tex_name = tex["name"];
143 bool has_image = tex["has_image"];
144
145 if (!has_image)
146 continue;
147
148 Ref<Image> img = vt->get_reconstructed_image(tex_id);
149 if (img.is_null())
150 continue;
151
152 // Sanitize name for filesystem
153 String safe_name = tex_name.replace(" ", "_").replace("/", "_");
154 String png_path = save_dir.path_join(safe_name + ".png");
155 Error err = img->save_png(png_path);
156 if (err == OK) {
157 tex_paths[tex_id] = png_path;
158 UtilityFunctions::print("Finalize: Saved ", png_path);
159 }
160 }
161
162 // Step 2: Convert VT ShaderMaterials to StandardMaterial3D + ImageTexture
163 Ref<Shader> vt_shader = vt->get_vt_shader();
164 std::vector<Node *> stack;
165 stack.push_back(root);
166 while (!stack.empty()) {
167 Node *node = stack.back();
168 stack.pop_back();
169
170 MeshInstance3D *mesh_inst = Object::cast_to<MeshInstance3D>(node);
171 if (mesh_inst) {
172 for (int s = 0; s < mesh_inst->get_surface_override_material_count(); s++) {
173 Ref<Material> mat = mesh_inst->get_surface_override_material(s);
174 Ref<ShaderMaterial> shader_mat = mat;
175 if (shader_mat.is_valid() && vt_shader.is_valid() && shader_mat->get_shader() == vt_shader) {
176 // Extract material properties from VT shader params
177 Color albedo = shader_mat->get_shader_parameter("albedo_color");
178 float rough = shader_mat->get_shader_parameter("roughness");
179 float metal = shader_mat->get_shader_parameter("metallic");
180
181 Ref<StandardMaterial3D> std_mat;
182 std_mat.instantiate();
183 std_mat->set_albedo(albedo);
184 std_mat->set_roughness(rough);
185 std_mat->set_metallic(metal);
186
187 // Set first available texture as albedo
188 for (const KeyValue<int, String> &pair : tex_paths) {
189 Ref<Texture2D> loaded = ResourceLoader::get_singleton()->load(pair.value);
190 if (loaded.is_valid()) {
191 std_mat->set_texture(StandardMaterial3D::TEXTURE_ALBEDO, loaded);
192 break;
193 }
194 }
195
196 mesh_inst->set_surface_override_material(s, std_mat);
197 }
198 }
199 }
200
201 for (int c = 0; c < node->get_child_count(); c++) {
202 stack.push_back(node->get_child(c));
203 }
204 }
205
206 // Step 3: Save the scene as .tscn
207 Ref<PackedScene> packed;
208 packed.instantiate();
209 packed->pack(root);
210 String scene_path = save_dir.path_join("scene.tscn");
211 Error err = ResourceSaver::get_singleton()->save(packed, scene_path);
212 if (err == OK) {
213 UtilityFunctions::print("Finalize: Saved scene to ", scene_path);
214 }
215
216 return err;
217}
218
118219// ── Server lifecycle ──────────────────────────────────────────────────────────
119220
120221Error BlenderBridge::start_server(const String &socket_path) {
……@@ -163,7 +264,7 @@ Error BlenderBridge::start_server(const String &socket_path) {
163264 _known_instance_ids.clear();
164265 _next_object_id = 1;
165266
166 UtilityFunctions::print("BlenderBridge v0.14.0: Server started (shm=", _shm_name, ", sock=", socket_path, ", regions=2x", (int)_region_size, ")");
267 UtilityFunctions::print("BlenderBridge v0.14.1: Server started (shm=", _shm_name, ", sock=", socket_path, ", regions=2x", (int)_region_size, ")");
167268 return OK;
168269}
169270
……@@ -1089,6 +1190,7 @@ void BlenderBridge::_handle_uds_message(const UdsMessage &msg) {
10891190 // "request_texture:name,width,height" — Blender asks Godot to register a texture with VT
10901191 // "texture_dirty:tex_id,tile_x,tile_y,w,h,mip" — texture invalidation
10911192 // "tile_ready:tex_id,tile_x,tile_y,mip,shm_offset,size" — tile data ready in shm
1193 // "finalize" — save textures as .png and scene as .tscn
10921194
10931195 std::string text(reinterpret_cast<const char *>(msg.data.data()), msg.size);
10941196
……@@ -1141,6 +1243,26 @@ void BlenderBridge::_handle_uds_message(const UdsMessage &msg) {
11411243 return;
11421244 }
11431245
1246 if (text == "finalize") {
1247 // Blender requests finalization — save textures + scene
1248 Object *root_obj = ObjectDB::get_instance(_export_root_id);
1249 Node *root = Object::cast_to<Node>(root_obj);
1250 if (root) {
1251 String save_dir = "res://blender_bridge_export/";
1252 Ref<DirAccess> dir = DirAccess::open("res://");
1253 if (dir.is_valid()) {
1254 dir->make_dir_recursive("blender_bridge_export");
1255 }
1256 Error err = finalize_scene(root, save_dir);
1257 const char *reply = (err == OK) ? "finalized" : "finalize_error";
1258 _uds.send_to(msg.client_id, reply, (uint32_t)strlen(reply));
1259 } else {
1260 const char *reply = "finalize_error";
1261 _uds.send_to(msg.client_id, reply, (uint32_t)strlen(reply));
1262 }
1263 return;
1264 }
1265
11441266 if (text.substr(0, 14) == "texture_dirty:") {
11451267 // Parse: texture_dirty:tex_id,tile_x,tile_y,w,h,mip
11461268 int tex_id, tx, ty, w, h, mip;
src/blender_bridge.h +3 -0
……@@ -65,6 +65,9 @@ public:
6565 // Forward texture dirty rect from Blender to VirtualTexture
6666 void notify_texture_dirty(int texture_id, int tile_x, int tile_y, int w, int h, int mip);
6767
68 // ── Finalize (save textures + scene to disk) ────────────────────────
69 Error finalize_scene(Node *root, const String &save_dir);
70
6871 // ── Signals ─────────────────────────────────────────────────────────
6972 // client_connected(client_id: int)
7073 // client_disconnected(client_id: int)