| … | … | @@ -6,9 +6,14 @@ |
| 6 | 6 | #include <string> |
| 7 | 7 | |
| 8 | 8 | #include <godot_cpp/classes/camera3d.hpp> |
| 9 | #include <godot_cpp/classes/dir_access.hpp> |
| 9 | 10 | #include <godot_cpp/classes/directional_light3d.hpp> |
| 11 | #include <godot_cpp/classes/image_texture.hpp> |
| 10 | 12 | #include <godot_cpp/classes/light3d.hpp> |
| 11 | 13 | #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> |
| 12 | 17 | #include <godot_cpp/classes/scene_tree.hpp> |
| 13 | 18 | #include <godot_cpp/classes/spot_light3d.hpp> |
| 14 | 19 | #include <godot_cpp/classes/window.hpp> |
| … | … | @@ -72,6 +77,7 @@ void BlenderBridge::_bind_methods() { |
| 72 | 77 | ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "virtual_texture_path"), "set_virtual_texture_path", "get_virtual_texture_path"); |
| 73 | 78 | |
| 74 | 79 | 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); |
| 75 | 81 | |
| 76 | 82 | // Signals |
| 77 | 83 | 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, |
| 115 | 121 | emit_signal("texture_dirty", texture_id, tile_x, tile_y, w, h, mip); |
| 116 | 122 | } |
| 117 | 123 | |
| 124 | // ── Finalize (save textures + scene to disk) ────────────────────────────────── |
| 125 | |
| 126 | Error 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 | |
| 118 | 219 | // ── Server lifecycle ────────────────────────────────────────────────────────── |
| 119 | 220 | |
| 120 | 221 | Error BlenderBridge::start_server(const String &socket_path) { |
| … | … | @@ -163,7 +264,7 @@ Error BlenderBridge::start_server(const String &socket_path) { |
| 163 | 264 | _known_instance_ids.clear(); |
| 164 | 265 | _next_object_id = 1; |
| 165 | 266 | |
| 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, ")"); |
| 167 | 268 | return OK; |
| 168 | 269 | } |
| 169 | 270 | |
| … | … | @@ -1089,6 +1190,7 @@ void BlenderBridge::_handle_uds_message(const UdsMessage &msg) { |
| 1089 | 1190 | // "request_texture:name,width,height" — Blender asks Godot to register a texture with VT |
| 1090 | 1191 | // "texture_dirty:tex_id,tile_x,tile_y,w,h,mip" — texture invalidation |
| 1091 | 1192 | // "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 |
| 1092 | 1194 | |
| 1093 | 1195 | std::string text(reinterpret_cast<const char *>(msg.data.data()), msg.size); |
| 1094 | 1196 | |
| … | … | @@ -1141,6 +1243,26 @@ void BlenderBridge::_handle_uds_message(const UdsMessage &msg) { |
| 1141 | 1243 | return; |
| 1142 | 1244 | } |
| 1143 | 1245 | |
| 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 | |
| 1144 | 1266 | if (text.substr(0, 14) == "texture_dirty:") { |
| 1145 | 1267 | // Parse: texture_dirty:tex_id,tile_x,tile_y,w,h,mip |
| 1146 | 1268 | int tex_id, tx, ty, w, h, mip; |