| … | … | @@ -61,7 +61,7 @@ def _make_poll_timer(generation: int): |
| 61 | 61 | |
| 62 | 62 | |
| 63 | 63 | def _scan_material_textures(): |
| 64 | """Scan Blender scene for materials with textures and request VT registration. | |
| 64 | """Scan Blender scene for materials with textures and request SVT registration. | |
| 65 | 65 | |
| 66 | 66 | Handles image textures (existing pipeline), procedural textures (bake first), |
| 67 | 67 | and ColorRamp chains (trace upstream recursively). |
| … | … | @@ -104,7 +104,7 @@ def _handle_texture_node(obj, mat, from_node, image_requested): |
| 104 | 104 | """Route a texture node to the appropriate handler.""" |
| 105 | 105 | global _conn, _baker, _proc_baker |
| 106 | 106 | |
| 107 | # Image textures → existing VT pipeline (no bake) | |
| 107 | # Image textures → existing SVT pipeline (no bake) | |
| 108 | 108 | if from_node.type == "TEX_IMAGE" and from_node.image is not None: |
| 109 | 109 | img = from_node.image |
| 110 | 110 | img_name = img.name |
| … | … | @@ -193,7 +193,7 @@ def _poll_timer_impl(generation: int): |
| 193 | 193 | _detector.suppress(set(_importer._id_to_name.values()), 5) |
| 194 | 194 | got_incoming = True |
| 195 | 195 | print(f"BlenderBridge: Synced scene from Godot") |
| 196 | # Scan Blender scene for textures (image + procedural) and request VT registration | |
| 196 | # Scan Blender scene for textures (image + procedural) and request SVT registration | |
| 197 | 197 | _scan_material_textures() |
| 198 | 198 | elif text in ("updated", "transform_update"): |
| 199 | 199 | # Incremental update from Godot (region 0) |
| … | … | @@ -304,7 +304,7 @@ def _attempt_reconnect(): |
| 304 | 304 | _detector.set_resize_callback(_on_shm_resized) |
| 305 | 305 | |
| 306 | 306 | # Reuse existing baker but clear stale _texture_ids (Godot restarted; |
| 307 | # its VT has no registrations, so textures must be re-requested). | |
| 307 | # its SVT has no registrations, so textures must be re-requested). | |
| 308 | 308 | tile_ring_offset = _shm._region_size * 2 |
| 309 | 309 | tile_ring_size = _shm.size - tile_ring_offset |
| 310 | 310 | if _baker is not None: |
| … | … | @@ -59,7 +59,7 @@ class TileBaker: |
| 59 | 59 | self._ring_offset = 0 |
| 60 | 60 | |
| 61 | 61 | def register_texture(self, name: str, texture_id: int): |
| 62 | """Register a Blender image name → VT texture ID mapping.""" | |
| 62 | """Register a Blender image name → SVT texture ID mapping.""" | |
| 63 | 63 | self._texture_ids[name] = texture_id |
| 64 | 64 | |
| 65 | 65 | def unregister_texture(self, name: str): |
| … | … | @@ -134,7 +134,7 @@ Error BlenderBridge::finalize_scene(Node *root, const String &save_dir) { |
| 134 | 134 | |
| 135 | 135 | // Step 1: Save each reconstructed texture as .png |
| 136 | 136 | TypedArray<Dictionary> textures = vt->get_all_textures(); |
| 137 | HashMap<int, String> tex_paths; // vt_id → saved .png path | |
| 137 | HashMap<int, String> tex_paths; // svt_id → saved .png path | |
| 138 | 138 | |
| 139 | 139 | for (int i = 0; i < textures.size(); i++) { |
| 140 | 140 | Dictionary tex = textures[i]; |
| … | … | @@ -159,8 +159,8 @@ Error BlenderBridge::finalize_scene(Node *root, const String &save_dir) { |
| 159 | 159 | } |
| 160 | 160 | } |
| 161 | 161 | |
| 162 | // Step 2: Convert VT ShaderMaterials to StandardMaterial3D + ImageTexture | |
| 163 | Ref<Shader> vt_shader = vt->get_vt_shader(); | |
| 162 | // Step 2: Convert SVT ShaderMaterials to StandardMaterial3D + ImageTexture | |
| 163 | Ref<Shader> svt_shader = vt->get_svt_shader(); | |
| 164 | 164 | std::vector<Node *> stack; |
| 165 | 165 | stack.push_back(root); |
| 166 | 166 | while (!stack.empty()) { |
| … | … | @@ -172,8 +172,8 @@ Error BlenderBridge::finalize_scene(Node *root, const String &save_dir) { |
| 172 | 172 | for (int s = 0; s < mesh_inst->get_surface_override_material_count(); s++) { |
| 173 | 173 | Ref<Material> mat = mesh_inst->get_surface_override_material(s); |
| 174 | 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 | |
| 175 | if (shader_mat.is_valid() && svt_shader.is_valid() && shader_mat->get_shader() == svt_shader) { | |
| 176 | // Extract material properties from SVT shader params | |
| 177 | 177 | Color albedo = shader_mat->get_shader_parameter("albedo_color"); |
| 178 | 178 | float rough = shader_mat->get_shader_parameter("roughness"); |
| 179 | 179 | float metal = shader_mat->get_shader_parameter("metallic"); |
| … | … | @@ -1173,7 +1173,7 @@ void BlenderBridge::_handle_uds_message(const UdsMessage &msg) { |
| 1173 | 1173 | |
| 1174 | 1174 | // Messages are simple text-based signaling: |
| 1175 | 1175 | // "hello" — handshake |
| 1176 | // "request_texture:name,width,height" — Blender asks Godot to register a texture with VT | |
| 1176 | // "request_texture:name,width,height" — Blender asks Godot to register a texture with SVT | |
| 1177 | 1177 | // "texture_dirty:tex_id,tile_x,tile_y,w,h,mip" — texture invalidation |
| 1178 | 1178 | // "tile_ready:tex_id,tile_x,tile_y,mip,shm_offset,size" — tile data ready in shm |
| 1179 | 1179 | // "finalize" — save textures as .png and scene as .tscn |
| … | … | @@ -1237,8 +1237,8 @@ void BlenderBridge::_handle_uds_message(const UdsMessage &msg) { |
| 1237 | 1237 | VirtualTexture *vt = Object::cast_to<VirtualTexture>(vt_node); |
| 1238 | 1238 | if (vt) { |
| 1239 | 1239 | String gd_name = String::utf8(tex_name.c_str(), (int)tex_name.size()); |
| 1240 | int vt_id = vt->register_texture(gd_name, img_w, img_h); | |
| 1241 | String reply = "texture_registered:" + gd_name + "," + String::num_int64(vt_id); | |
| 1240 | int svt_id = vt->register_texture(gd_name, img_w, img_h); | |
| 1241 | String reply = "texture_registered:" + gd_name + "," + String::num_int64(svt_id); | |
| 1242 | 1242 | CharString reply_utf8 = reply.utf8(); |
| 1243 | 1243 | _uds.send_to(msg.client_id, reply_utf8.get_data(), (uint32_t)reply_utf8.length()); |
| 1244 | 1244 | } |
| … | … | @@ -0,0 +1,37 @@ |
| 1 | shader_type spatial; | |
| 2 | ||
| 3 | // ── Sparse Virtual Texture atlas and page table ───────────────────────────── | |
| 4 | uniform sampler2D ssvt_atlas : source_color, filter_linear, repeat_disable; | |
| 5 | uniform sampler2D ssvt_page_table : filter_nearest, repeat_disable; | |
| 6 | ||
| 7 | // Cache geometry: 1.0 / cache_grid_size (e.g. 1/32 = 0.03125) | |
| 8 | uniform float tile_size_uv; | |
| 9 | // Virtual tiles per edge at mip 0 (e.g. 128) | |
| 10 | uniform float page_table_size; | |
| 11 | ||
| 12 | // ── Material fallbacks (used when tile not yet resident) ──────────────────── | |
| 13 | uniform vec4 albedo_color : source_color = vec4(0.8, 0.8, 0.8, 1.0); | |
| 14 | uniform float roughness : hint_range(0,1) = 0.5; | |
| 15 | uniform float metallic : hint_range(0,1) = 0.0; | |
| 16 | ||
| 17 | void fragment() { | |
| 18 | // Virtual tile address from UV | |
| 19 | vec2 svt_coord = UV * page_table_size; | |
| 20 | ivec2 tile_id = ivec2(floor(svt_coord)); | |
| 21 | vec2 in_tile_frac = fract(svt_coord); | |
| 22 | ||
| 23 | // Page table texel: R=atlas_x, G=atlas_y, B=valid, A=0 | |
| 24 | vec4 page = texelFetch(svt_page_table, tile_id, 0); | |
| 25 | ||
| 26 | if (page.b > 0.5) { | |
| 27 | // Tile is resident — sample from atlas | |
| 28 | vec2 atlas_uv = (vec2(page.r, page.g) * 255.0 + in_tile_frac) * tile_size_uv; | |
| 29 | ALBEDO = texture(svt_atlas, atlas_uv).rgb; | |
| 30 | } else { | |
| 31 | // Tile not loaded — use fallback color | |
| 32 | ALBEDO = albedo_color.rgb; | |
| 33 | } | |
| 34 | ||
| 35 | ROUGHNESS = roughness; | |
| 36 | METALLIC = metallic; | |
| 37 | } |
| … | … | @@ -33,7 +33,7 @@ void VirtualTexture::_bind_methods() { |
| 33 | 33 | ClassDB::bind_method(D_METHOD("get_page_table_texture"), &VirtualTexture::get_page_table_texture); |
| 34 | 34 | |
| 35 | 35 | // Material factory |
| 36 | ClassDB::bind_method(D_METHOD("create_vt_material", "fallback_albedo", "roughness", "metallic"), &VirtualTexture::create_vt_material); | |
| 36 | ClassDB::bind_method(D_METHOD("create_svt_material", "fallback_albedo", "roughness", "metallic"), &VirtualTexture::create_svt_material); | |
| 37 | 37 | |
| 38 | 38 | // Feedback |
| 39 | 39 | ClassDB::bind_method(D_METHOD("submit_feedback", "feedback_data"), &VirtualTexture::submit_feedback); |
| … | … | @@ -171,22 +171,22 @@ Ref<Texture2DRD> VirtualTexture::get_page_table_texture() const { |
| 171 | 171 | |
| 172 | 172 | // ── Material factory ────────────────────────────────────────────────────── |
| 173 | 173 | |
| 174 | Ref<ShaderMaterial> VirtualTexture::create_vt_material(const Color &fallback, float rough, float metal) { | |
| 174 | Ref<ShaderMaterial> VirtualTexture::create_svt_material(const Color &fallback, float rough, float metal) { | |
| 175 | 175 | // Lazy-load the shader |
| 176 | 176 | if (!_shader_loaded) { |
| 177 | _vt_shader = ResourceLoader::get_singleton()->load("res://addons/blender_bridge/vt_lookup.gdshader"); | |
| 177 | _svt_shader = ResourceLoader::get_singleton()->load("res://addons/blender_bridge/svt_lookup.gdshader"); | |
| 178 | 178 | _shader_loaded = true; |
| 179 | 179 | } |
| 180 | if (_vt_shader.is_null()) { | |
| 181 | UtilityFunctions::push_warning("VirtualTexture: vt_lookup.gdshader not found at res://addons/blender_bridge/"); | |
| 180 | if (_svt_shader.is_null()) { | |
| 181 | UtilityFunctions::push_warning("VirtualTexture: svt_lookup.gdshader not found at res://addons/blender_bridge/"); | |
| 182 | 182 | return Ref<ShaderMaterial>(); |
| 183 | 183 | } |
| 184 | 184 | |
| 185 | 185 | Ref<ShaderMaterial> mat; |
| 186 | 186 | mat.instantiate(); |
| 187 | mat->set_shader(_vt_shader); | |
| 188 | mat->set_shader_parameter("vt_atlas", _atlas_tex); | |
| 189 | mat->set_shader_parameter("vt_page_table", _page_table_tex); | |
| 187 | mat->set_shader(_svt_shader); | |
| 188 | mat->set_shader_parameter("svt_atlas", _atlas_tex); | |
| 189 | mat->set_shader_parameter("svt_page_table", _page_table_tex); | |
| 190 | 190 | mat->set_shader_parameter("tile_size_uv", 1.0f / _cache_grid_size); |
| 191 | 191 | mat->set_shader_parameter("page_table_size", (float)_page_table.tiles_at_mip(0)); |
| 192 | 192 | mat->set_shader_parameter("albedo_color", fallback); |
| … | … | @@ -514,8 +514,8 @@ TypedArray<Dictionary> VirtualTexture::get_all_textures() const { |
| 514 | 514 | return result; |
| 515 | 515 | } |
| 516 | 516 | |
| 517 | Ref<Shader> VirtualTexture::get_vt_shader() const { | |
| 518 | return _vt_shader; | |
| 517 | Ref<Shader> VirtualTexture::get_svt_shader() const { | |
| 518 | return _svt_shader; | |
| 519 | 519 | } |
| 520 | 520 | |
| 521 | 521 | // ── Stats ───────────────────────────────────────────────────────────────────── |
| … | … | @@ -21,7 +21,7 @@ |
| 21 | 21 | |
| 22 | 22 | using namespace godot; |
| 23 | 23 | |
| 24 | // ── VirtualTexture node ─────────────────────────────────────────────────────── | |
| 24 | // ── SparseVirtualTexture node ──────────────────────────────────────────────── | |
| 25 | 25 | // Manages the SVT pipeline: page table, physical cache atlas, feedback analysis, |
| 26 | 26 | // and tile upload. Runs entirely on the global RenderingDevice. |
| 27 | 27 | class VirtualTexture : public Node { |
| … | … | @@ -52,8 +52,8 @@ public: |
| 52 | 52 | Ref<Texture2DRD> get_page_table_texture() const; |
| 53 | 53 | |
| 54 | 54 | // ── Material factory ──────────────────────────────────────────────── |
| 55 | Ref<ShaderMaterial> create_vt_material(const Color &fallback_albedo, float roughness, float metallic); | |
| 56 | Ref<Shader> get_vt_shader() const; | |
| 55 | Ref<ShaderMaterial> create_svt_material(const Color &fallback_albedo, float roughness, float metallic); | |
| 56 | Ref<Shader> get_svt_shader() const; | |
| 57 | 57 | |
| 58 | 58 | // ── Configuration ─────────────────────────────────────────────────── |
| 59 | 59 | void set_tile_size(int size); |
| … | … | @@ -119,7 +119,7 @@ private: |
| 119 | 119 | bool _gpu_initialized = false; |
| 120 | 120 | |
| 121 | 121 | // ── Shader (lazy-loaded) ──────────────────────────────────────────── |
| 122 | Ref<Shader> _vt_shader; | |
| 122 | Ref<Shader> _svt_shader; | |
| 123 | 123 | bool _shader_loaded = false; |
| 124 | 124 | |
| 125 | 125 | // ── Frame tracking ────────────────────────────────────────────────── |
| … | … | @@ -1,37 +0,0 @@ |
| 1 | shader_type spatial; | |
| 2 | ||
| 3 | // ── Virtual Texture atlas and page table ──────────────────────────────────── | |
| 4 | uniform sampler2D vt_atlas : source_color, filter_linear, repeat_disable; | |
| 5 | uniform sampler2D vt_page_table : filter_nearest, repeat_disable; | |
| 6 | ||
| 7 | // Cache geometry: 1.0 / cache_grid_size (e.g. 1/32 = 0.03125) | |
| 8 | uniform float tile_size_uv; | |
| 9 | // Virtual tiles per edge at mip 0 (e.g. 128) | |
| 10 | uniform float page_table_size; | |
| 11 | ||
| 12 | // ── Material fallbacks (used when tile not yet resident) ──────────────────── | |
| 13 | uniform vec4 albedo_color : source_color = vec4(0.8, 0.8, 0.8, 1.0); | |
| 14 | uniform float roughness : hint_range(0,1) = 0.5; | |
| 15 | uniform float metallic : hint_range(0,1) = 0.0; | |
| 16 | ||
| 17 | void fragment() { | |
| 18 | // Virtual tile address from UV | |
| 19 | vec2 vt_coord = UV * page_table_size; | |
| 20 | ivec2 tile_id = ivec2(floor(vt_coord)); | |
| 21 | vec2 in_tile_frac = fract(vt_coord); | |
| 22 | ||
| 23 | // Page table texel: R=atlas_x, G=atlas_y, B=valid, A=0 | |
| 24 | vec4 page = texelFetch(vt_page_table, tile_id, 0); | |
| 25 | ||
| 26 | if (page.b > 0.5) { | |
| 27 | // Tile is resident — sample from atlas | |
| 28 | vec2 atlas_uv = (vec2(page.r, page.g) * 255.0 + in_tile_frac) * tile_size_uv; | |
| 29 | ALBEDO = texture(vt_atlas, atlas_uv).rgb; | |
| 30 | } else { | |
| 31 | // Tile not loaded — use fallback color | |
| 32 | ALBEDO = albedo_color.rgb; | |
| 33 | } | |
| 34 | ||
| 35 | ROUGHNESS = roughness; | |
| 36 | METALLIC = metallic; | |
| 37 | } |
| … | … | @@ -90,7 +90,7 @@ func _init() -> void: |
| 90 | 90 | failed += 1 |
| 91 | 91 | |
| 92 | 92 | # ── Test 7: Invalidation forwarding ────────────────────────────── |
| 93 | # This should forward to VT without crashing | |
| 93 | # This should forward to SVT without crashing | |
| 94 | 94 | bridge.notify_texture_dirty(tex_id1, 0, 0, 2, 2, 0) |
| 95 | 95 | print(" notify_texture_dirty... PASS") |
| 96 | 96 | passed += 1 |
| … | … | @@ -98,10 +98,10 @@ func _init() -> void: |
| 98 | 98 | # ── Test 8: Stats ──────────────────────────────────────────────── |
| 99 | 99 | var stats: Dictionary = vt.get_stats() |
| 100 | 100 | if stats.has("cache_total") and stats["cache_total"] == 16: |
| 101 | print(" vt_stats... PASS") | |
| 101 | print(" svt_stats... PASS") | |
| 102 | 102 | passed += 1 |
| 103 | 103 | else: |
| 104 | print(" vt_stats... FAIL") | |
| 104 | print(" svt_stats... FAIL") | |
| 105 | 105 | failed += 1 |
| 106 | 106 | |
| 107 | 107 | # ── Test 9: Unregister texture ─────────────────────────────────── |