OverviewFilesCommitsIssuesDocsReleasesPeers

blender-bridge commits / 7aacf7c

Fix reconnect regression, add per-mesh VT texture mapping (v0.16.1)
Three interacting bugs caused transform sync to break after SHM resize reconnect, and textures never appeared on synced meshes: 1. Sequence mismatch: new ChangeDetector started at seq=0 while Godot's _last_import_sequence retained old value, silently skipping first flush. Fix: reset _last_import_sequence on "hello" and preserve detector across reconnects (reuse instance, clear caches, keep _name_to_id/_sequence). 2. ID loss: new ChangeDetector lost _name_to_id for Blender-origin objects. set_id_map replaced entire map. Fix: merge additively so reconnect's scene_exported handler doesn't overwrite Blender-origin IDs. 3. No per-mesh texture ID: material blob lacked texture_id field, so Godot couldn't distinguish textured meshes from flat-color ones. VT material was applied indiscriminately (blocking StandardMaterial3D) or disabled entirely. Fix: add texture_id (uint32) to material blob format. Blender resolves image name → VT texture_id via lookup callback. Godot applies VT material only when texture_id > 0, StandardMaterial3D otherwise. 4. Texture registration timing: objects flushed before texture_registered response arrived, so texture_id was always 0. Fix: mark_texture_objects_dirty triggers UPDATE_FULL re-flush when texture registration completes. Also: remove debug prints, clean up stale from_blender nodes on reconnect, update scene_import.py to skip new texture_id field. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7aacf7c8868d310d7ee90c0d48d983cf812bdfbb
by plankatron <fleeky@gmail.com> · 2026-03-04T13:52:47.000Z

Changed files (6) +113 -16

blender_addon/__init__.py +26 -5
……@@ -11,7 +11,7 @@ Usage:
1111
1212import bpy
1313
14ADDON_VERSION = "0.16.0"
14ADDON_VERSION = "0.16.1"
1515
1616# Support hot-reload (F3 > Reload Scripts)
1717if "shm_protocol" in locals():
……@@ -219,6 +219,9 @@ def _poll_timer_impl(generation: int):
219219 # Bake immediately so tiles flow on this tick
220220 _baker.bake_full_image(img_name)
221221 print(f"BlenderBridge: Baked texture '{img_name}'")
222 # Re-flush objects using this texture so texture_id is sent
223 if _detector is not None:
224 _detector.mark_texture_objects_dirty(img_name)
222225
223226 # Flush accumulated Blender changes to SHM and notify Godot
224227 if _detector is not None:
……@@ -286,13 +289,28 @@ def _attempt_reconnect():
286289 _shm = None
287290 return 2.0 # Retry in 2 seconds
288291
289 # Re-initialize importer and detector
292 # Re-initialize importer; preserve detector across reconnects to keep
293 # _name_to_id and _sequence (prevents ID loss and sequence mismatch)
290294 _importer = SceneImporter(_shm, MAX_OBJECTS)
291295 if _detector is not None:
292296 _detector.unregister()
293 _detector = ChangeDetector(_shm, MAX_OBJECTS)
294 _detector.register()
295 _detector.set_resize_callback(_on_shm_resized)
297 _detector._shm = _shm
298 _detector._pending.clear()
299 _detector._last_depsgraph = None
300 _detector._transform_hashes.clear() # Force re-detect all transforms
301 _detector._geometry_hashes.clear() # Force re-detect all geometry
302 _detector.register()
303 _detector.set_resize_callback(_on_shm_resized)
304 _detector.set_texture_lookup(
305 lambda name: _baker._texture_ids.get(name, 0) if _baker else 0
306 )
307 else:
308 _detector = ChangeDetector(_shm, MAX_OBJECTS)
309 _detector.register()
310 _detector.set_resize_callback(_on_shm_resized)
311 _detector.set_texture_lookup(
312 lambda name: _baker._texture_ids.get(name, 0) if _baker else 0
313 )
296314
297315 # Reuse existing baker to preserve _texture_ids (prevents re-requesting
298316 # all textures on reconnect). Only update SHM reference and ring buffer.
……@@ -454,6 +472,9 @@ class BRIDGE_OT_connect(bpy.types.Operator):
454472 tile_ring_size = TILE_RING_SIZE
455473 _baker = TileBaker(_shm, tile_size=128, max_mips=8)
456474 _baker.init_ring_buffer(tile_ring_offset, tile_ring_size)
475 _detector.set_texture_lookup(
476 lambda name: _baker._texture_ids.get(name, 0) if _baker else 0
477 )
457478
458479 # Set up non-blocking procedural baker
459480 _proc_baker = ProceduralBaker()
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.16.0"
5version = "0.16.1"
66blender_version_min = "4.2.0"
77type = "add-on"
88maintainer = "blender-bridge <noreply@localhost>"
blender_addon/change_detector.py +49 -2
……@@ -104,6 +104,8 @@ class ChangeDetector:
104104 self._had_shading_update = False
105105 # Callback for notifying Godot of SHM resize: fn(new_size, new_region_size)
106106 self._resize_callback = None
107 # Callback to resolve image name → VT texture_id (0 = not registered)
108 self._texture_lookup = None
107109
108110 def register(self):
109111 """Register the depsgraph handler."""
……@@ -146,8 +148,10 @@ class ChangeDetector:
146148 del self._suppressed_objects[name]
147149
148150 def set_id_map(self, id_map: dict[int, str]):
149 """Set the object ID mapping from the scene importer (reversed)."""
150 self._name_to_id = {name: obj_id for obj_id, name in id_map.items()}
151 """Merge object ID mapping from scene importer (additive, not replace)."""
152 for obj_id, name in id_map.items():
153 if name not in self._name_to_id:
154 self._name_to_id[name] = obj_id
151155 if self._name_to_id:
152156 self._next_id = max(self._name_to_id.values()) + 1
153157
……@@ -182,6 +186,35 @@ class ChangeDetector:
182186 """Set a callback for SHM resize notifications: callback(new_size, new_region_size)."""
183187 self._resize_callback = callback
184188
189 def set_texture_lookup(self, callback):
190 """Set a callback to resolve image name → VT texture_id (0 = not registered)."""
191 self._texture_lookup = callback
192
193 def mark_texture_objects_dirty(self, image_name: str):
194 """Mark all objects using a texture image for UPDATE_FULL re-flush.
195
196 Called when a texture_id becomes available after registration,
197 so the material blob includes the correct texture_id.
198 """
199 if bpy is None:
200 return
201 for obj in bpy.data.objects:
202 if obj.type != "MESH" or obj.active_material is None:
203 continue
204 mat = obj.active_material
205 if not mat.use_nodes or not mat.node_tree:
206 continue
207 for node in mat.node_tree.nodes:
208 if node.type != "BSDF_PRINCIPLED":
209 continue
210 bc_input = node.inputs.get("Base Color")
211 if bc_input and bc_input.is_linked:
212 from_node = bc_input.links[0].from_node
213 if (from_node.type == "TEX_IMAGE" and from_node.image
214 and from_node.image.name == image_name):
215 self._pending[obj.name] = UPDATE_FULL
216 break
217
185218 @staticmethod
186219 def _hash_transform(mat) -> int:
187220 """FNV-1a hash of 4×4 matrix floats for change detection."""
……@@ -707,6 +740,20 @@ class ChangeDetector:
707740 written = self._shm.write_blob_array(blob_abs_offset, mat_data)
708741 blob_abs_offset += written
709742 blob_rel_offset += written
743
744 # Write texture_id (uint32): 0 = no texture, >0 = VT texture ID
745 tex_id = 0
746 if self._texture_lookup is not None:
747 bc_input = bsdf.inputs.get("Base Color")
748 if bc_input and bc_input.is_linked:
749 from_node = bc_input.links[0].from_node
750 if from_node.type == "TEX_IMAGE" and from_node.image:
751 tex_id = self._texture_lookup(from_node.image.name)
752 self._shm.write_blob(
753 blob_abs_offset, struct.pack("<I", tex_id)
754 )
755 blob_abs_offset += 4
756 blob_rel_offset += 4
710757 if not has_mat:
711758 self._shm.write_blob(
712759 blob_abs_offset, struct.pack("<I", 0)
blender_addon/scene_import.py +4 -2
……@@ -296,12 +296,13 @@ class SceneImporter:
296296 if cursor + 4 <= data_end:
297297 has_material = struct.unpack("<I", bytes(self._shm._mm[cursor:cursor + 4]))[0]
298298 cursor += 4
299 if has_material and cursor + 44 <= data_end:
299 if has_material and cursor + 48 <= data_end:
300300 mat_data = np.frombuffer(
301301 bytes(self._shm._mm[cursor:cursor + 44]),
302302 dtype=np.float32,
303303 )
304304 cursor += 44
305 cursor += 4 # skip texture_id (VT, not used in Blender)
305306 self._apply_material_to_mesh(mesh, mat_data)
306307
307308 obj = bpy.data.objects.new(mesh.name, mesh)
……@@ -511,10 +512,11 @@ class SceneImporter:
511512 if cursor + 4 <= data_end:
512513 has_material = struct.unpack("<I", bytes(self._shm._mm[cursor:cursor + 4]))[0]
513514 cursor += 4
514 if has_material and cursor + 44 <= data_end:
515 if has_material and cursor + 48 <= data_end:
515516 mat_data = np.frombuffer(
516517 bytes(self._shm._mm[cursor:cursor + 44]),
517518 dtype=np.float32,
518519 )
519520 cursor += 44
521 cursor += 4 # skip texture_id (VT, not used in Blender)
520522 self._apply_material_to_mesh(mesh, mat_data)
blender_bridge_addon.zip +0 -0
(no content changes — binary or rename)
src/blender_bridge.cpp +33 -6
……@@ -264,7 +264,7 @@ Error BlenderBridge::start_server(const String &socket_path) {
264264 _known_instance_ids.clear();
265265 _next_object_id = 1;
266266
267 UtilityFunctions::print("BlenderBridge v0.16.0: Server started (shm=", _shm_name, ", sock=", socket_path, ", regions=2x", (int)_region_size, ")");
267 UtilityFunctions::print("BlenderBridge v0.16.1: Server started (shm=", _shm_name, ", sock=", socket_path, ", regions=2x", (int)_region_size, ")");
268268 return OK;
269269}
270270
……@@ -570,6 +570,10 @@ uint32_t BlenderBridge::_export_node(Node3D *node, uint32_t index, uint8_t *blob
570570 mat_data[10] = std_mat->get_emission_energy_multiplier();
571571 memcpy(blob_base + blob_offset, mat_data, 44);
572572 blob_offset += 44;
573 // texture_id = 0 (Godot-origin meshes have no VT texture)
574 uint32_t tex_id = 0;
575 memcpy(blob_base + blob_offset, &tex_id, 4);
576 blob_offset += 4;
573577 } else {
574578 uint32_t has_material = 0;
575579 memcpy(blob_base + blob_offset, &has_material, 4);
……@@ -1081,22 +1085,25 @@ void BlenderBridge::import_changes(Node *root) {
10811085 memcpy(&has_material, base + cursor, 4);
10821086 cursor += 4;
10831087 }
1084
1085 if (has_material && cursor + 44 <= data_end) {
1088 if (has_material && cursor + 48 <= data_end) {
10861089 float mat_data[11];
10871090 memcpy(mat_data, base + cursor, 44);
10881091 cursor += 44;
10891092
1093 uint32_t texture_id = 0;
1094 memcpy(&texture_id, base + cursor, 4);
1095 cursor += 4;
1096
10901097 Color albedo(mat_data[0], mat_data[1], mat_data[2], mat_data[3]);
10911098 float roughness_val = mat_data[4];
10921099 float metallic_val = mat_data[5];
10931100
1094 // Try VT material first (shows Blender image textures via atlas)
1101 // Apply VT material if this mesh has a registered texture
10951102 bool used_vt = false;
1096 if (!_virtual_texture_path.is_empty()) {
1103 if (texture_id > 0 && !_virtual_texture_path.is_empty()) {
10971104 Node *vt_node = get_node_or_null(_virtual_texture_path);
10981105 VirtualTexture *vt = Object::cast_to<VirtualTexture>(vt_node);
1099 if (vt && vt->get_texture_count() > 0) {
1106 if (vt) {
11001107 Ref<ShaderMaterial> vt_mat = vt->create_vt_material(albedo, roughness_val, metallic_val);
11011108 if (vt_mat.is_valid()) {
11021109 mesh_inst->set_surface_override_material(0, vt_mat);
……@@ -1204,6 +1211,23 @@ void BlenderBridge::_handle_uds_message(const UdsMessage &msg) {
12041211 }
12051212
12061213 if (text.substr(0, 5) == "hello") {
1214 _last_import_sequence = 0; // Reset for new connection
1215
1216 // Clean up stale from_blender tracked objects (will be re-sent)
1217 _tracked_objects.erase(
1218 std::remove_if(_tracked_objects.begin(), _tracked_objects.end(),
1219 [&](const TrackedObject &t) {
1220 if (t.from_blender) {
1221 _known_instance_ids.erase(t.godot_instance_id);
1222 Object *obj = ObjectDB::get_instance(t.godot_instance_id);
1223 Node *n = Object::cast_to<Node>(obj);
1224 if (n) n->queue_free();
1225 return true;
1226 }
1227 return false;
1228 }),
1229 _tracked_objects.end());
1230
12071231 const char *ack = "connected";
12081232 _uds.send_to(msg.client_id, ack, (uint32_t)strlen(ack));
12091233
……@@ -1554,6 +1578,9 @@ void BlenderBridge::_detect_and_push_changes() {
15541578 mat_data[10] = std_mat->get_emission_energy_multiplier();
15551579 memcpy(base + blob_offset, mat_data, 44);
15561580 blob_offset += 44;
1581 uint32_t tex_id = 0;
1582 memcpy(base + blob_offset, &tex_id, 4);
1583 blob_offset += 4;
15571584 } else {
15581585 uint32_t has_material = 0;
15591586 memcpy(base + blob_offset, &has_material, 4);