| … | … | @@ -11,7 +11,7 @@ Usage: |
| 11 | 11 | |
| 12 | 12 | import bpy |
| 13 | 13 | |
| 14 | ADDON_VERSION = "0.13.4" | |
| 14 | ADDON_VERSION = "0.14.0" | |
| 15 | 15 | |
| 16 | 16 | # Support hot-reload (F3 > Reload Scripts) |
| 17 | 17 | if "shm_protocol" in locals(): |
| … | … | @@ -82,12 +82,14 @@ def _request_texture_registrations(): |
| 82 | 82 | for link in base_color_input.links: |
| 83 | 83 | from_node = link.from_node |
| 84 | 84 | if from_node.type == "TEX_IMAGE" and from_node.image is not None: |
| 85 | img_name = from_node.image.name | |
| 85 | img = from_node.image | |
| 86 | img_name = img.name | |
| 86 | 87 | if img_name not in requested and img_name not in _baker._texture_ids: |
| 87 | 88 | requested.add(img_name) |
| 88 | msg = f"request_texture:{img_name}" | |
| 89 | img_w, img_h = img.size[0], img.size[1] | |
| 90 | msg = f"request_texture:{img_name},{img_w},{img_h}" | |
| 89 | 91 | _conn.send_message(msg.encode()) |
| 90 | print(f"BlenderBridge: Requesting texture registration for '{img_name}'") | |
| 92 | print(f"BlenderBridge: Requesting texture registration for '{img_name}' ({img_w}x{img_h})") | |
| 91 | 93 | |
| 92 | 94 | if requested: |
| 93 | 95 | print(f"BlenderBridge: Requested {len(requested)} texture(s)") |
| … | … | @@ -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.13.4" | |
| 5 | version = "0.14.0" | |
| 6 | 6 | blender_version_min = "4.2.0" |
| 7 | 7 | type = "add-on" |
| 8 | 8 | maintainer = "blender-bridge <noreply@localhost>" |
| … | … | @@ -163,7 +163,7 @@ Error BlenderBridge::start_server(const String &socket_path) { |
| 163 | 163 | _known_instance_ids.clear(); |
| 164 | 164 | _next_object_id = 1; |
| 165 | 165 | |
| 166 | UtilityFunctions::print("BlenderBridge v0.13.4: Server started (shm=", _shm_name, ", sock=", socket_path, ", regions=2x", (int)_region_size, ")"); | |
| 166 | UtilityFunctions::print("BlenderBridge v0.14.0: Server started (shm=", _shm_name, ", sock=", socket_path, ", regions=2x", (int)_region_size, ")"); | |
| 167 | 167 | return OK; |
| 168 | 168 | } |
| 169 | 169 | |
| … | … | @@ -1086,7 +1086,7 @@ void BlenderBridge::_handle_uds_message(const UdsMessage &msg) { |
| 1086 | 1086 | |
| 1087 | 1087 | // Messages are simple text-based signaling: |
| 1088 | 1088 | // "hello" — handshake |
| 1089 | // "request_texture:name" — Blender asks Godot to register a texture with VT | |
| 1089 | // "request_texture:name,width,height" — Blender asks Godot to register a texture with VT | |
| 1090 | 1090 | // "texture_dirty:tex_id,tile_x,tile_y,w,h,mip" — texture invalidation |
| 1091 | 1091 | // "tile_ready:tex_id,tile_x,tile_y,mip,shm_offset,size" — tile data ready in shm |
| 1092 | 1092 | |
| … | … | @@ -1111,15 +1111,28 @@ void BlenderBridge::_handle_uds_message(const UdsMessage &msg) { |
| 1111 | 1111 | } |
| 1112 | 1112 | |
| 1113 | 1113 | if (text.substr(0, 16) == "request_texture:") { |
| 1114 | // Blender discovered an image texture and wants Godot to register it with VT | |
| 1115 | std::string tex_name = text.substr(16); | |
| 1114 | // Parse: request_texture:name,width,height | |
| 1115 | std::string payload = text.substr(16); | |
| 1116 | std::string tex_name; | |
| 1117 | int img_w = 0, img_h = 0; | |
| 1118 | size_t comma1 = payload.find(','); | |
| 1119 | if (comma1 != std::string::npos) { | |
| 1120 | tex_name = payload.substr(0, comma1); | |
| 1121 | size_t comma2 = payload.find(',', comma1 + 1); | |
| 1122 | if (comma2 != std::string::npos) { | |
| 1123 | img_w = std::atoi(payload.substr(comma1 + 1, comma2 - comma1 - 1).c_str()); | |
| 1124 | img_h = std::atoi(payload.substr(comma2 + 1).c_str()); | |
| 1125 | } | |
| 1126 | } else { | |
| 1127 | tex_name = payload; // backward compat: no dimensions | |
| 1128 | } | |
| 1129 | ||
| 1116 | 1130 | if (!tex_name.empty() && !_virtual_texture_path.is_empty()) { |
| 1117 | 1131 | Node *vt_node = get_node_or_null(_virtual_texture_path); |
| 1118 | 1132 | VirtualTexture *vt = Object::cast_to<VirtualTexture>(vt_node); |
| 1119 | 1133 | if (vt) { |
| 1120 | 1134 | String gd_name = String::utf8(tex_name.c_str(), (int)tex_name.size()); |
| 1121 | int vt_id = vt->register_texture(gd_name); | |
| 1122 | // Reply so Blender's baker knows the ID | |
| 1135 | int vt_id = vt->register_texture(gd_name, img_w, img_h); | |
| 1123 | 1136 | String reply = "texture_registered:" + gd_name + "," + String::num_int64(vt_id); |
| 1124 | 1137 | CharString reply_utf8 = reply.utf8(); |
| 1125 | 1138 | _uds.send_to(msg.client_id, reply_utf8.get_data(), (uint32_t)reply_utf8.length()); |
| … | … | @@ -19,7 +19,8 @@ VirtualTexture::~VirtualTexture() { |
| 19 | 19 | |
| 20 | 20 | void VirtualTexture::_bind_methods() { |
| 21 | 21 | // Texture registration |
| 22 | ClassDB::bind_method(D_METHOD("register_texture", "name"), &VirtualTexture::register_texture); | |
| 22 | ClassDB::bind_method(D_METHOD("register_texture", "name", "width", "height"), | |
| 23 | &VirtualTexture::register_texture, DEFVAL(0), DEFVAL(0)); | |
| 23 | 24 | ClassDB::bind_method(D_METHOD("unregister_texture", "vt_id"), &VirtualTexture::unregister_texture); |
| 24 | 25 | ClassDB::bind_method(D_METHOD("get_texture_count"), &VirtualTexture::get_texture_count); |
| 25 | 26 | |
| … | … | @@ -40,6 +41,10 @@ void VirtualTexture::_bind_methods() { |
| 40 | 41 | // Tile upload |
| 41 | 42 | ClassDB::bind_method(D_METHOD("upload_tile", "texture_id", "tile_x", "tile_y", "mip", "rgba_data"), &VirtualTexture::upload_tile); |
| 42 | 43 | |
| 44 | // CPU image reconstruction | |
| 45 | ClassDB::bind_method(D_METHOD("get_reconstructed_image", "vt_id"), &VirtualTexture::get_reconstructed_image); | |
| 46 | ClassDB::bind_method(D_METHOD("get_all_textures"), &VirtualTexture::get_all_textures); | |
| 47 | ||
| 43 | 48 | // Stats |
| 44 | 49 | ClassDB::bind_method(D_METHOD("get_stats"), &VirtualTexture::get_stats); |
| 45 | 50 | |
| … | … | @@ -86,10 +91,22 @@ int VirtualTexture::get_mip_levels() const { return _mip_levels; } |
| 86 | 91 | |
| 87 | 92 | // ── Texture registration ────────────────────────────────────────────────────── |
| 88 | 93 | |
| 89 | int VirtualTexture::register_texture(const String &name) { | |
| 94 | int VirtualTexture::register_texture(const String &name, int width, int height) { | |
| 90 | 95 | int id = _next_texture_id++; |
| 91 | _textures.push_back({ id, name, true }); | |
| 92 | UtilityFunctions::print("VirtualTexture: Registered texture '", name, "' as id=", id); | |
| 96 | RegisteredTexture tex; | |
| 97 | tex.id = id; | |
| 98 | tex.name = name; | |
| 99 | tex.active = true; | |
| 100 | tex.width = width; | |
| 101 | tex.height = height; | |
| 102 | // Pre-allocate CPU image if dimensions known | |
| 103 | if (width > 0 && height > 0) { | |
| 104 | tex.cpu_image = Image::create(width, height, false, Image::FORMAT_RGBA8); | |
| 105 | tex.cpu_image->fill(Color(0, 0, 0, 0)); | |
| 106 | } | |
| 107 | _textures.push_back(tex); | |
| 108 | UtilityFunctions::print("VirtualTexture: Registered texture '", name, "' as id=", id, | |
| 109 | " (", width, "x", height, ")"); | |
| 93 | 110 | return id; |
| 94 | 111 | } |
| 95 | 112 | |
| … | … | @@ -202,6 +219,25 @@ Error VirtualTexture::upload_tile(int texture_id, int tile_x, int tile_y, int mi |
| 202 | 219 | _tile_cache.lock(alloc.location); |
| 203 | 220 | } |
| 204 | 221 | |
| 222 | // Blit mip-0 tiles into the CPU reconstruction image | |
| 223 | if (mip == 0) { | |
| 224 | for (auto &tex : _textures) { | |
| 225 | if (tex.id == texture_id && tex.cpu_image.is_valid()) { | |
| 226 | Ref<Image> tile_img = Image::create_from_data( | |
| 227 | _tile_size, _tile_size, false, Image::FORMAT_RGBA8, rgba_data); | |
| 228 | int dst_x = tile_x * _tile_size; | |
| 229 | int dst_y = tile_y * _tile_size; | |
| 230 | int copy_w = MIN(_tile_size, tex.width - dst_x); | |
| 231 | int copy_h = MIN(_tile_size, tex.height - dst_y); | |
| 232 | if (copy_w > 0 && copy_h > 0) { | |
| 233 | tex.cpu_image->blit_rect(tile_img, Rect2i(0, 0, copy_w, copy_h), | |
| 234 | Vector2i(dst_x, dst_y)); | |
| 235 | } | |
| 236 | break; | |
| 237 | } | |
| 238 | } | |
| 239 | } | |
| 240 | ||
| 205 | 241 | emit_signal("tile_loaded", texture_id, tile_x, tile_y, mip); |
| 206 | 242 | _tiles_uploaded_this_frame++; |
| 207 | 243 | return OK; |
| … | … | @@ -443,6 +479,37 @@ void VirtualTexture::_process_tile_requests() { |
| 443 | 479 | } |
| 444 | 480 | } |
| 445 | 481 | |
| 482 | // ── CPU image reconstruction ────────────────────────────────────────────────── | |
| 483 | ||
| 484 | Ref<Image> VirtualTexture::get_reconstructed_image(int vt_id) const { | |
| 485 | for (const auto &tex : _textures) { | |
| 486 | if (tex.id == vt_id && tex.active) { | |
| 487 | return tex.cpu_image; | |
| 488 | } | |
| 489 | } | |
| 490 | return Ref<Image>(); | |
| 491 | } | |
| 492 | ||
| 493 | TypedArray<Dictionary> VirtualTexture::get_all_textures() const { | |
| 494 | TypedArray<Dictionary> result; | |
| 495 | for (const auto &tex : _textures) { | |
| 496 | if (!tex.active) | |
| 497 | continue; | |
| 498 | Dictionary d; | |
| 499 | d["id"] = tex.id; | |
| 500 | d["name"] = tex.name; | |
| 501 | d["width"] = tex.width; | |
| 502 | d["height"] = tex.height; | |
| 503 | d["has_image"] = tex.cpu_image.is_valid(); | |
| 504 | result.push_back(d); | |
| 505 | } | |
| 506 | return result; | |
| 507 | } | |
| 508 | ||
| 509 | Ref<Shader> VirtualTexture::get_vt_shader() const { | |
| 510 | return _vt_shader; | |
| 511 | } | |
| 512 | ||
| 446 | 513 | // ── Stats ───────────────────────────────────────────────────────────────────── |
| 447 | 514 | |
| 448 | 515 | Dictionary VirtualTexture::get_stats() const { |
| … | … | @@ -1,5 +1,6 @@ |
| 1 | 1 | #pragma once |
| 2 | 2 | |
| 3 | #include <godot_cpp/classes/image.hpp> | |
| 3 | 4 | #include <godot_cpp/classes/node.hpp> |
| 4 | 5 | #include <godot_cpp/classes/rendering_device.hpp> |
| 5 | 6 | #include <godot_cpp/classes/rendering_server.hpp> |
| … | … | @@ -12,6 +13,7 @@ |
| 12 | 13 | #include <godot_cpp/variant/packed_byte_array.hpp> |
| 13 | 14 | #include <godot_cpp/variant/rid.hpp> |
| 14 | 15 | #include <godot_cpp/variant/string.hpp> |
| 16 | #include <godot_cpp/variant/typed_array.hpp> | |
| 15 | 17 | |
| 16 | 18 | #include "page_table.h" |
| 17 | 19 | #include "tile_cache.h" |
| … | … | @@ -33,10 +35,14 @@ public: |
| 33 | 35 | ~VirtualTexture(); |
| 34 | 36 | |
| 35 | 37 | // ── Texture registration ──────────────────────────────────────────── |
| 36 | int register_texture(const String &name); | |
| 38 | int register_texture(const String &name, int width = 0, int height = 0); | |
| 37 | 39 | void unregister_texture(int vt_id); |
| 38 | 40 | int get_texture_count() const; |
| 39 | 41 | |
| 42 | // ── CPU image reconstruction ──────────────────────────────────────── | |
| 43 | Ref<Image> get_reconstructed_image(int vt_id) const; | |
| 44 | TypedArray<Dictionary> get_all_textures() const; | |
| 45 | ||
| 40 | 46 | // ── Invalidation (called by BlenderBridge on texture edits) ───────── |
| 41 | 47 | void invalidate_region(int texture_id, int tile_x, int tile_y, int w, int h, int mip); |
| 42 | 48 | void invalidate_all(int texture_id); |
| … | … | @@ -47,6 +53,7 @@ public: |
| 47 | 53 | |
| 48 | 54 | // ── Material factory ──────────────────────────────────────────────── |
| 49 | 55 | Ref<ShaderMaterial> create_vt_material(const Color &fallback_albedo, float roughness, float metallic); |
| 56 | Ref<Shader> get_vt_shader() const; | |
| 50 | 57 | |
| 51 | 58 | // ── Configuration ─────────────────────────────────────────────────── |
| 52 | 59 | void set_tile_size(int size); |
| … | … | @@ -97,6 +104,9 @@ private: |
| 97 | 104 | int id; |
| 98 | 105 | String name; |
| 99 | 106 | bool active; |
| 107 | int width = 0; // Source image width in pixels | |
| 108 | int height = 0; // Source image height in pixels | |
| 109 | Ref<Image> cpu_image; // CPU-side reconstructed image | |
| 100 | 110 | }; |
| 101 | 111 | std::vector<RegisteredTexture> _textures; |
| 102 | 112 | int _next_texture_id = 1; |