| … | … | @@ -11,7 +11,7 @@ Usage: |
| 11 | 11 | |
| 12 | 12 | import bpy |
| 13 | 13 | |
| 14 | ADDON_VERSION = "0.16.0" | |
| 14 | ADDON_VERSION = "0.16.1" | |
| 15 | 15 | |
| 16 | 16 | # Support hot-reload (F3 > Reload Scripts) |
| 17 | 17 | if "shm_protocol" in locals(): |
| … | … | @@ -219,6 +219,9 @@ def _poll_timer_impl(generation: int): |
| 219 | 219 | # Bake immediately so tiles flow on this tick |
| 220 | 220 | _baker.bake_full_image(img_name) |
| 221 | 221 | 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) | |
| 222 | 225 | |
| 223 | 226 | # Flush accumulated Blender changes to SHM and notify Godot |
| 224 | 227 | if _detector is not None: |
| … | … | @@ -286,13 +289,28 @@ def _attempt_reconnect(): |
| 286 | 289 | _shm = None |
| 287 | 290 | return 2.0 # Retry in 2 seconds |
| 288 | 291 | |
| 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) | |
| 290 | 294 | _importer = SceneImporter(_shm, MAX_OBJECTS) |
| 291 | 295 | if _detector is not None: |
| 292 | 296 | _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 | ) | |
| 296 | 314 | |
| 297 | 315 | # Reuse existing baker to preserve _texture_ids (prevents re-requesting |
| 298 | 316 | # all textures on reconnect). Only update SHM reference and ring buffer. |
| … | … | @@ -454,6 +472,9 @@ class BRIDGE_OT_connect(bpy.types.Operator): |
| 454 | 472 | tile_ring_size = TILE_RING_SIZE |
| 455 | 473 | _baker = TileBaker(_shm, tile_size=128, max_mips=8) |
| 456 | 474 | _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 | ) | |
| 457 | 478 | |
| 458 | 479 | # Set up non-blocking procedural baker |
| 459 | 480 | _proc_baker = ProceduralBaker() |
| … | … | @@ -2,7 +2,7 @@ schema_version = "1.0.0" |
| 2 | 2 | id = "blender_godot_bridge" |
| 3 | 3 | name = "Blender-Godot Bridge" |
| 4 | 4 | tagline = "Real-time bidirectional scene sync with Godot" |
| 5 | version = "0.16.0" | |
| 5 | version = "0.16.1" | |
| 6 | 6 | blender_version_min = "4.2.0" |
| 7 | 7 | type = "add-on" |
| 8 | 8 | maintainer = "blender-bridge <noreply@localhost>" |
| … | … | @@ -104,6 +104,8 @@ class ChangeDetector: |
| 104 | 104 | self._had_shading_update = False |
| 105 | 105 | # Callback for notifying Godot of SHM resize: fn(new_size, new_region_size) |
| 106 | 106 | self._resize_callback = None |
| 107 | # Callback to resolve image name → VT texture_id (0 = not registered) | |
| 108 | self._texture_lookup = None | |
| 107 | 109 | |
| 108 | 110 | def register(self): |
| 109 | 111 | """Register the depsgraph handler.""" |
| … | … | @@ -146,8 +148,10 @@ class ChangeDetector: |
| 146 | 148 | del self._suppressed_objects[name] |
| 147 | 149 | |
| 148 | 150 | 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 | |
| 151 | 155 | if self._name_to_id: |
| 152 | 156 | self._next_id = max(self._name_to_id.values()) + 1 |
| 153 | 157 | |
| … | … | @@ -182,6 +186,35 @@ class ChangeDetector: |
| 182 | 186 | """Set a callback for SHM resize notifications: callback(new_size, new_region_size).""" |
| 183 | 187 | self._resize_callback = callback |
| 184 | 188 | |
| 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 | ||
| 185 | 218 | @staticmethod |
| 186 | 219 | def _hash_transform(mat) -> int: |
| 187 | 220 | """FNV-1a hash of 4×4 matrix floats for change detection.""" |
| … | … | @@ -707,6 +740,20 @@ class ChangeDetector: |
| 707 | 740 | written = self._shm.write_blob_array(blob_abs_offset, mat_data) |
| 708 | 741 | blob_abs_offset += written |
| 709 | 742 | 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 | |
| 710 | 757 | if not has_mat: |
| 711 | 758 | self._shm.write_blob( |
| 712 | 759 | blob_abs_offset, struct.pack("<I", 0) |
| … | … | @@ -296,12 +296,13 @@ class SceneImporter: |
| 296 | 296 | if cursor + 4 <= data_end: |
| 297 | 297 | has_material = struct.unpack("<I", bytes(self._shm._mm[cursor:cursor + 4]))[0] |
| 298 | 298 | cursor += 4 |
| 299 | if has_material and cursor + 44 <= data_end: | |
| 299 | if has_material and cursor + 48 <= data_end: | |
| 300 | 300 | mat_data = np.frombuffer( |
| 301 | 301 | bytes(self._shm._mm[cursor:cursor + 44]), |
| 302 | 302 | dtype=np.float32, |
| 303 | 303 | ) |
| 304 | 304 | cursor += 44 |
| 305 | cursor += 4 # skip texture_id (VT, not used in Blender) | |
| 305 | 306 | self._apply_material_to_mesh(mesh, mat_data) |
| 306 | 307 | |
| 307 | 308 | obj = bpy.data.objects.new(mesh.name, mesh) |
| … | … | @@ -511,10 +512,11 @@ class SceneImporter: |
| 511 | 512 | if cursor + 4 <= data_end: |
| 512 | 513 | has_material = struct.unpack("<I", bytes(self._shm._mm[cursor:cursor + 4]))[0] |
| 513 | 514 | cursor += 4 |
| 514 | if has_material and cursor + 44 <= data_end: | |
| 515 | if has_material and cursor + 48 <= data_end: | |
| 515 | 516 | mat_data = np.frombuffer( |
| 516 | 517 | bytes(self._shm._mm[cursor:cursor + 44]), |
| 517 | 518 | dtype=np.float32, |
| 518 | 519 | ) |
| 519 | 520 | cursor += 44 |
| 521 | cursor += 4 # skip texture_id (VT, not used in Blender) | |
| 520 | 522 | self._apply_material_to_mesh(mesh, mat_data) |
| … | … | @@ -264,7 +264,7 @@ Error BlenderBridge::start_server(const String &socket_path) { |
| 264 | 264 | _known_instance_ids.clear(); |
| 265 | 265 | _next_object_id = 1; |
| 266 | 266 | |
| 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, ")"); | |
| 268 | 268 | return OK; |
| 269 | 269 | } |
| 270 | 270 | |
| … | … | @@ -570,6 +570,10 @@ uint32_t BlenderBridge::_export_node(Node3D *node, uint32_t index, uint8_t *blob |
| 570 | 570 | mat_data[10] = std_mat->get_emission_energy_multiplier(); |
| 571 | 571 | memcpy(blob_base + blob_offset, mat_data, 44); |
| 572 | 572 | 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; | |
| 573 | 577 | } else { |
| 574 | 578 | uint32_t has_material = 0; |
| 575 | 579 | memcpy(blob_base + blob_offset, &has_material, 4); |
| … | … | @@ -1081,22 +1085,25 @@ void BlenderBridge::import_changes(Node *root) { |
| 1081 | 1085 | memcpy(&has_material, base + cursor, 4); |
| 1082 | 1086 | cursor += 4; |
| 1083 | 1087 | } |
| 1084 | ||
| 1085 | if (has_material && cursor + 44 <= data_end) { | |
| 1088 | if (has_material && cursor + 48 <= data_end) { | |
| 1086 | 1089 | float mat_data[11]; |
| 1087 | 1090 | memcpy(mat_data, base + cursor, 44); |
| 1088 | 1091 | cursor += 44; |
| 1089 | 1092 | |
| 1093 | uint32_t texture_id = 0; | |
| 1094 | memcpy(&texture_id, base + cursor, 4); | |
| 1095 | cursor += 4; | |
| 1096 | ||
| 1090 | 1097 | Color albedo(mat_data[0], mat_data[1], mat_data[2], mat_data[3]); |
| 1091 | 1098 | float roughness_val = mat_data[4]; |
| 1092 | 1099 | float metallic_val = mat_data[5]; |
| 1093 | 1100 | |
| 1094 | // Try VT material first (shows Blender image textures via atlas) | |
| 1101 | // Apply VT material if this mesh has a registered texture | |
| 1095 | 1102 | bool used_vt = false; |
| 1096 | if (!_virtual_texture_path.is_empty()) { | |
| 1103 | if (texture_id > 0 && !_virtual_texture_path.is_empty()) { | |
| 1097 | 1104 | Node *vt_node = get_node_or_null(_virtual_texture_path); |
| 1098 | 1105 | VirtualTexture *vt = Object::cast_to<VirtualTexture>(vt_node); |
| 1099 | if (vt && vt->get_texture_count() > 0) { | |
| 1106 | if (vt) { | |
| 1100 | 1107 | Ref<ShaderMaterial> vt_mat = vt->create_vt_material(albedo, roughness_val, metallic_val); |
| 1101 | 1108 | if (vt_mat.is_valid()) { |
| 1102 | 1109 | mesh_inst->set_surface_override_material(0, vt_mat); |
| … | … | @@ -1204,6 +1211,23 @@ void BlenderBridge::_handle_uds_message(const UdsMessage &msg) { |
| 1204 | 1211 | } |
| 1205 | 1212 | |
| 1206 | 1213 | 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 | ||
| 1207 | 1231 | const char *ack = "connected"; |
| 1208 | 1232 | _uds.send_to(msg.client_id, ack, (uint32_t)strlen(ack)); |
| 1209 | 1233 | |
| … | … | @@ -1554,6 +1578,9 @@ void BlenderBridge::_detect_and_push_changes() { |
| 1554 | 1578 | mat_data[10] = std_mat->get_emission_energy_multiplier(); |
| 1555 | 1579 | memcpy(base + blob_offset, mat_data, 44); |
| 1556 | 1580 | blob_offset += 44; |
| 1581 | uint32_t tex_id = 0; | |
| 1582 | memcpy(base + blob_offset, &tex_id, 4); | |
| 1583 | blob_offset += 4; | |
| 1557 | 1584 | } else { |
| 1558 | 1585 | uint32_t has_material = 0; |
| 1559 | 1586 | memcpy(base + blob_offset, &has_material, 4); |