| … | … | @@ -50,6 +50,9 @@ def _poll_timer(): |
| 50 | 50 | if text == "scene_exported": |
| 51 | 51 | # Full scene import from Godot |
| 52 | 52 | if _importer is not None: |
| 53 | scene = bpy.context.scene | |
| 54 | if hasattr(scene, "bridge_clear_on_import") and scene.bridge_clear_on_import: | |
| 55 | _clear_bridge_objects() | |
| 53 | 56 | _importer.full_import() |
| 54 | 57 | # Share ID mapping with change detector |
| 55 | 58 | if _detector is not None: |
| … | … | @@ -161,6 +164,10 @@ class BRIDGE_OT_import_scene(bpy.types.Operator): |
| 161 | 164 | self.report({"ERROR"}, "Not connected") |
| 162 | 165 | return {"CANCELLED"} |
| 163 | 166 | |
| 167 | # Clear existing bridge objects if enabled | |
| 168 | if context.scene.bridge_clear_on_import: | |
| 169 | _clear_bridge_objects() | |
| 170 | ||
| 164 | 171 | _importer.full_import() |
| 165 | 172 | if _detector is not None: |
| 166 | 173 | _detector.set_id_map(_importer._id_to_name) |
| … | … | @@ -173,6 +180,13 @@ class BRIDGE_OT_import_scene(bpy.types.Operator): |
| 173 | 180 | return {"FINISHED"} |
| 174 | 181 | |
| 175 | 182 | |
| 183 | def _clear_bridge_objects(): | |
| 184 | """Remove all objects previously imported by the bridge.""" | |
| 185 | to_remove = [obj for obj in bpy.data.objects if obj.name.startswith("bridge_")] | |
| 186 | for obj in to_remove: | |
| 187 | bpy.data.objects.remove(obj, do_unlink=True) | |
| 188 | ||
| 189 | ||
| 176 | 190 | # ── UI Panel ───────────────────────────────────────────────────────────────── |
| 177 | 191 | |
| 178 | 192 | |
| … | … | @@ -195,6 +209,7 @@ class BRIDGE_PT_panel(bpy.types.Panel): |
| 195 | 209 | else: |
| 196 | 210 | layout.operator("bridge.disconnect", icon="UNLINKED") |
| 197 | 211 | layout.separator() |
| 212 | layout.prop(context.scene, "bridge_clear_on_import") | |
| 198 | 213 | layout.operator("bridge.import_scene", icon="IMPORT") |
| 199 | 214 | |
| 200 | 215 | # Status info |
| … | … | @@ -221,6 +236,11 @@ _classes = ( |
| 221 | 236 | def register(): |
| 222 | 237 | for cls in _classes: |
| 223 | 238 | bpy.utils.register_class(cls) |
| 239 | bpy.types.Scene.bridge_clear_on_import = bpy.props.BoolProperty( | |
| 240 | name="Clear Scene on Import", | |
| 241 | description="Remove existing bridge objects before importing from Godot", | |
| 242 | default=True, | |
| 243 | ) | |
| 224 | 244 | |
| 225 | 245 | |
| 226 | 246 | def unregister(): |
| … | … | @@ -240,6 +260,8 @@ def unregister(): |
| 240 | 260 | _shm = None |
| 241 | 261 | _timer_running = False |
| 242 | 262 | |
| 263 | del bpy.types.Scene.bridge_clear_on_import | |
| 264 | ||
| 243 | 265 | for cls in reversed(_classes): |
| 244 | 266 | bpy.utils.unregister_class(cls) |
| 245 | 267 |
| … | … | @@ -161,8 +161,7 @@ class ChangeDetector: |
| 161 | 161 | written = self._shm.write_blob_array(blob_offset, coords) |
| 162 | 162 | blob_offset += written |
| 163 | 163 | |
| 164 | # Bulk-read normals | |
| 165 | mesh.calc_normals() | |
| 164 | # Bulk-read normals (calc_normals removed in Blender 5.0) | |
| 166 | 165 | normals = np.empty(vert_count * 3, dtype=np.float32) |
| 167 | 166 | mesh.vertices.foreach_get("normal", normals) |
| 168 | 167 | written = self._shm.write_blob_array(blob_offset, normals) |
| … | … | @@ -125,30 +125,38 @@ class SceneImporter: |
| 125 | 125 | pos_offset = vert_offset + 4 |
| 126 | 126 | positions = self._shm.read_blob(pos_offset, vert_count * 3 * 4) |
| 127 | 127 | positions = positions.reshape(-1, 3) |
| 128 | cursor = pos_offset + vert_count * 3 * 4 | |
| 128 | 129 | |
| 129 | # Check for normals | |
| 130 | # Read has_normals flag | |
| 130 | 131 | normals = None |
| 131 | normal_offset = pos_offset + vert_count * 3 * 4 | |
| 132 | expected_end = normal_offset + vert_count * 3 * 4 | |
| 133 | if expected_end <= blob_offset + blob_size: | |
| 134 | normals = self._shm.read_blob(normal_offset, vert_count * 3 * 4) | |
| 132 | normals_flag_raw = self._shm.read_blob(cursor, 4) | |
| 133 | has_normals = int(struct.unpack("<I", normals_flag_raw.tobytes())[0]) | |
| 134 | cursor += 4 | |
| 135 | ||
| 136 | if has_normals: | |
| 137 | normals = self._shm.read_blob(cursor, vert_count * 3 * 4) | |
| 135 | 138 | normals = normals.reshape(-1, 3) |
| 139 | cursor += vert_count * 3 * 4 | |
| 140 | ||
| 141 | # Read index count | |
| 142 | idx_count_raw = self._shm.read_blob(cursor, 4) | |
| 143 | idx_count = int(struct.unpack("<I", idx_count_raw.tobytes())[0]) | |
| 144 | cursor += 4 | |
| 136 | 145 | |
| 137 | # Build mesh from vertices | |
| 138 | # For now, create triangulated mesh (every 3 verts = 1 face) | |
| 146 | # Build faces from index buffer or raw triangle list | |
| 139 | 147 | verts = [tuple(v) for v in positions] |
| 140 | 148 | faces = [] |
| 141 | for fi in range(0, vert_count - 2, 3): | |
| 142 | faces.append((fi, fi + 1, fi + 2)) | |
| 149 | if idx_count > 0: | |
| 150 | indices_raw = self._shm.read_blob(cursor, idx_count * 4) | |
| 151 | indices = np.frombuffer(indices_raw.tobytes(), dtype=np.int32) | |
| 152 | for fi in range(0, idx_count - 2, 3): | |
| 153 | faces.append((int(indices[fi]), int(indices[fi + 1]), int(indices[fi + 2]))) | |
| 154 | else: | |
| 155 | for fi in range(0, vert_count - 2, 3): | |
| 156 | faces.append((fi, fi + 1, fi + 2)) | |
| 143 | 157 | |
| 144 | 158 | mesh = bpy.data.meshes.new(f"bridge_mesh_{entry['object_id']}") |
| 145 | 159 | mesh.from_pydata(verts, [], faces) |
| 146 | ||
| 147 | if normals is not None: | |
| 148 | mesh.normals_split_custom_set_from_vertices( | |
| 149 | [tuple(n) for n in normals] | |
| 150 | ) | |
| 151 | ||
| 152 | 160 | mesh.update() |
| 153 | 161 | obj = bpy.data.objects.new(mesh.name, mesh) |
| 154 | 162 | bpy.context.collection.objects.link(obj) |
| … | … | @@ -230,19 +238,36 @@ class SceneImporter: |
| 230 | 238 | pos_offset = vert_offset + 4 |
| 231 | 239 | positions = self._shm.read_blob(pos_offset, vert_count * 3 * 4) |
| 232 | 240 | positions = positions.reshape(-1, 3) |
| 241 | cursor = pos_offset + vert_count * 3 * 4 | |
| 242 | ||
| 243 | # Skip normals flag + normals data | |
| 244 | normals_flag_raw = self._shm.read_blob(cursor, 4) | |
| 245 | has_normals = int(struct.unpack("<I", normals_flag_raw.tobytes())[0]) | |
| 246 | cursor += 4 | |
| 247 | if has_normals: | |
| 248 | cursor += vert_count * 3 * 4 | |
| 249 | ||
| 250 | # Read index count | |
| 251 | idx_count_raw = self._shm.read_blob(cursor, 4) | |
| 252 | idx_count = int(struct.unpack("<I", idx_count_raw.tobytes())[0]) | |
| 253 | cursor += 4 | |
| 233 | 254 | |
| 234 | 255 | mesh = obj.data |
| 235 | 256 | if len(mesh.vertices) == vert_count: |
| 236 | # Same vertex count — update in-place using foreach_set | |
| 237 | 257 | flat = positions.flatten() |
| 238 | 258 | mesh.vertices.foreach_set("co", flat) |
| 239 | 259 | mesh.update() |
| 240 | 260 | else: |
| 241 | # Different vertex count — rebuild | |
| 242 | 261 | verts = [tuple(v) for v in positions] |
| 243 | 262 | faces = [] |
| 244 | for fi in range(0, vert_count - 2, 3): | |
| 245 | faces.append((fi, fi + 1, fi + 2)) | |
| 263 | if idx_count > 0: | |
| 264 | indices_raw = self._shm.read_blob(cursor, idx_count * 4) | |
| 265 | indices = np.frombuffer(indices_raw.tobytes(), dtype=np.int32) | |
| 266 | for fi in range(0, idx_count - 2, 3): | |
| 267 | faces.append((int(indices[fi]), int(indices[fi + 1]), int(indices[fi + 2]))) | |
| 268 | else: | |
| 269 | for fi in range(0, vert_count - 2, 3): | |
| 270 | faces.append((fi, fi + 1, fi + 2)) | |
| 246 | 271 | mesh.clear_geometry() |
| 247 | 272 | mesh.from_pydata(verts, [], faces) |
| 248 | 273 | mesh.update() |
| … | … | @@ -202,28 +202,37 @@ void BlenderBridge::_export_transform(const Transform3D &xform, float *out) { |
| 202 | 202 | const Basis &b = xform.basis; |
| 203 | 203 | const Vector3 &o = xform.origin; |
| 204 | 204 | |
| 205 | // Row 0 (Blender X): Godot X | |
| 205 | // Blender expects row-major 4×4 with translation in row 3 (column 3 when transposed). | |
| 206 | // Matrix() constructor takes rows, so we write standard row-major: | |
| 207 | // [basis_col0.x basis_col1.x basis_col2.x origin.x] | |
| 208 | // [basis_col0.y basis_col1.y basis_col2.y origin.y] | |
| 209 | // [basis_col0.z basis_col1.z basis_col2.z origin.z] | |
| 210 | // [0 0 0 1 ] | |
| 211 | // | |
| 212 | // With coordinate conversion M applied: M * col, M * origin | |
| 213 | ||
| 214 | // Row 0 (Blender X) | |
| 206 | 215 | out[0] = b[0][0]; |
| 207 | 216 | out[1] = -b[2][0]; |
| 208 | 217 | out[2] = b[1][0]; |
| 209 | out[3] = 0.0f; | |
| 218 | out[3] = o.x; | |
| 210 | 219 | |
| 211 | // Row 1 (Blender Y): -Godot Z | |
| 220 | // Row 1 (Blender Y = -Godot Z) | |
| 212 | 221 | out[4] = -b[0][2]; |
| 213 | 222 | out[5] = b[2][2]; |
| 214 | 223 | out[6] = -b[1][2]; |
| 215 | out[7] = 0.0f; | |
| 224 | out[7] = -o.z; | |
| 216 | 225 | |
| 217 | // Row 2 (Blender Z): Godot Y | |
| 226 | // Row 2 (Blender Z = Godot Y) | |
| 218 | 227 | out[8] = b[0][1]; |
| 219 | 228 | out[9] = -b[2][1]; |
| 220 | 229 | out[10] = b[1][1]; |
| 221 | out[11] = 0.0f; | |
| 230 | out[11] = o.y; | |
| 222 | 231 | |
| 223 | // Row 3: translation | |
| 224 | out[12] = o.x; | |
| 225 | out[13] = -o.z; | |
| 226 | out[14] = o.y; | |
| 232 | // Row 3 | |
| 233 | out[12] = 0.0f; | |
| 234 | out[13] = 0.0f; | |
| 235 | out[14] = 0.0f; | |
| 227 | 236 | out[15] = 1.0f; |
| 228 | 237 | } |
| 229 | 238 | |
| … | … | @@ -277,7 +286,13 @@ uint32_t BlenderBridge::_export_node(Node3D *node, uint32_t index, uint8_t *blob |
| 277 | 286 | } |
| 278 | 287 | |
| 279 | 288 | // Write normals if present |
| 280 | if (arrays.size() > Mesh::ARRAY_NORMAL && arrays[Mesh::ARRAY_NORMAL].get_type() != Variant::NIL) { | |
| 289 | bool has_normals = arrays.size() > Mesh::ARRAY_NORMAL && arrays[Mesh::ARRAY_NORMAL].get_type() != Variant::NIL; | |
| 290 | // Write has_normals flag (uint32) | |
| 291 | uint32_t normals_flag = has_normals ? 1 : 0; | |
| 292 | memcpy(blob_base + blob_offset, &normals_flag, 4); | |
| 293 | blob_offset += 4; | |
| 294 | ||
| 295 | if (has_normals) { | |
| 281 | 296 | PackedVector3Array normals = arrays[Mesh::ARRAY_NORMAL]; |
| 282 | 297 | const Vector3 *norm_r = normals.ptr(); |
| 283 | 298 | for (uint32_t v = 0; v < vert_count; v++) { |
| … | … | @@ -288,6 +303,23 @@ uint32_t BlenderBridge::_export_node(Node3D *node, uint32_t index, uint8_t *blob |
| 288 | 303 | blob_offset += 12; |
| 289 | 304 | } |
| 290 | 305 | } |
| 306 | ||
| 307 | // Write index buffer if present | |
| 308 | bool has_indices = arrays.size() > Mesh::ARRAY_INDEX && arrays[Mesh::ARRAY_INDEX].get_type() != Variant::NIL; | |
| 309 | uint32_t idx_count = 0; | |
| 310 | if (has_indices) { | |
| 311 | PackedInt32Array indices = arrays[Mesh::ARRAY_INDEX]; | |
| 312 | idx_count = indices.size(); | |
| 313 | } | |
| 314 | memcpy(blob_base + blob_offset, &idx_count, 4); | |
| 315 | blob_offset += 4; | |
| 316 | ||
| 317 | if (idx_count > 0) { | |
| 318 | PackedInt32Array indices = arrays[Mesh::ARRAY_INDEX]; | |
| 319 | const int32_t *idx_r = indices.ptr(); | |
| 320 | memcpy(blob_base + blob_offset, idx_r, idx_count * 4); | |
| 321 | blob_offset += idx_count * 4; | |
| 322 | } | |
| 291 | 323 | } |
| 292 | 324 | } |
| 293 | 325 | } |