OverviewFilesCommitsIssuesDocsReleasesPeers

blender-bridge commits / 2a62eb2

fix: garbled meshes — export index buffer, fix transform layout, Blender 5.0 compat
- Export index buffer from Godot meshes (indexed meshes like BoxMesh need it) - Fix transform matrix: translation in column 3, not row 3 - Add has_normals flag to blob format for unambiguous parsing - Remove calc_normals() call (removed in Blender 5.0) - Add "Clear Scene on Import" toggle in Blender UI - Scene auto-clears bridge objects before re-import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2a62eb26f32e7cb640701a94ee03a4bce83744f4
by plankatron <fleeky@gmail.com> · 2026-03-03T00:22:20.000Z

Changed files (4) +110 -32

blender_addon/__init__.py +22 -0
……@@ -50,6 +50,9 @@ def _poll_timer():
5050 if text == "scene_exported":
5151 # Full scene import from Godot
5252 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()
5356 _importer.full_import()
5457 # Share ID mapping with change detector
5558 if _detector is not None:
……@@ -161,6 +164,10 @@ class BRIDGE_OT_import_scene(bpy.types.Operator):
161164 self.report({"ERROR"}, "Not connected")
162165 return {"CANCELLED"}
163166
167 # Clear existing bridge objects if enabled
168 if context.scene.bridge_clear_on_import:
169 _clear_bridge_objects()
170
164171 _importer.full_import()
165172 if _detector is not None:
166173 _detector.set_id_map(_importer._id_to_name)
……@@ -173,6 +180,13 @@ class BRIDGE_OT_import_scene(bpy.types.Operator):
173180 return {"FINISHED"}
174181
175182
183def _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
176190# ── UI Panel ─────────────────────────────────────────────────────────────────
177191
178192
……@@ -195,6 +209,7 @@ class BRIDGE_PT_panel(bpy.types.Panel):
195209 else:
196210 layout.operator("bridge.disconnect", icon="UNLINKED")
197211 layout.separator()
212 layout.prop(context.scene, "bridge_clear_on_import")
198213 layout.operator("bridge.import_scene", icon="IMPORT")
199214
200215 # Status info
……@@ -221,6 +236,11 @@ _classes = (
221236def register():
222237 for cls in _classes:
223238 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 )
224244
225245
226246def unregister():
……@@ -240,6 +260,8 @@ def unregister():
240260 _shm = None
241261 _timer_running = False
242262
263 del bpy.types.Scene.bridge_clear_on_import
264
243265 for cls in reversed(_classes):
244266 bpy.utils.unregister_class(cls)
245267
blender_addon/change_detector.py +1 -2
……@@ -161,8 +161,7 @@ class ChangeDetector:
161161 written = self._shm.write_blob_array(blob_offset, coords)
162162 blob_offset += written
163163
164 # Bulk-read normals
165 mesh.calc_normals()
164 # Bulk-read normals (calc_normals removed in Blender 5.0)
166165 normals = np.empty(vert_count * 3, dtype=np.float32)
167166 mesh.vertices.foreach_get("normal", normals)
168167 written = self._shm.write_blob_array(blob_offset, normals)
blender_addon/scene_import.py +44 -19
……@@ -125,30 +125,38 @@ class SceneImporter:
125125 pos_offset = vert_offset + 4
126126 positions = self._shm.read_blob(pos_offset, vert_count * 3 * 4)
127127 positions = positions.reshape(-1, 3)
128 cursor = pos_offset + vert_count * 3 * 4
128129
129 # Check for normals
130 # Read has_normals flag
130131 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)
135138 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
136145
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
139147 verts = [tuple(v) for v in positions]
140148 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))
143157
144158 mesh = bpy.data.meshes.new(f"bridge_mesh_{entry['object_id']}")
145159 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
152160 mesh.update()
153161 obj = bpy.data.objects.new(mesh.name, mesh)
154162 bpy.context.collection.objects.link(obj)
……@@ -230,19 +238,36 @@ class SceneImporter:
230238 pos_offset = vert_offset + 4
231239 positions = self._shm.read_blob(pos_offset, vert_count * 3 * 4)
232240 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
233254
234255 mesh = obj.data
235256 if len(mesh.vertices) == vert_count:
236 # Same vertex count — update in-place using foreach_set
237257 flat = positions.flatten()
238258 mesh.vertices.foreach_set("co", flat)
239259 mesh.update()
240260 else:
241 # Different vertex count — rebuild
242261 verts = [tuple(v) for v in positions]
243262 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))
246271 mesh.clear_geometry()
247272 mesh.from_pydata(verts, [], faces)
248273 mesh.update()
src/blender_bridge.cpp +43 -11
……@@ -202,28 +202,37 @@ void BlenderBridge::_export_transform(const Transform3D &xform, float *out) {
202202 const Basis &b = xform.basis;
203203 const Vector3 &o = xform.origin;
204204
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)
206215 out[0] = b[0][0];
207216 out[1] = -b[2][0];
208217 out[2] = b[1][0];
209 out[3] = 0.0f;
218 out[3] = o.x;
210219
211 // Row 1 (Blender Y): -Godot Z
220 // Row 1 (Blender Y = -Godot Z)
212221 out[4] = -b[0][2];
213222 out[5] = b[2][2];
214223 out[6] = -b[1][2];
215 out[7] = 0.0f;
224 out[7] = -o.z;
216225
217 // Row 2 (Blender Z): Godot Y
226 // Row 2 (Blender Z = Godot Y)
218227 out[8] = b[0][1];
219228 out[9] = -b[2][1];
220229 out[10] = b[1][1];
221 out[11] = 0.0f;
230 out[11] = o.y;
222231
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;
227236 out[15] = 1.0f;
228237}
229238
……@@ -277,7 +286,13 @@ uint32_t BlenderBridge::_export_node(Node3D *node, uint32_t index, uint8_t *blob
277286 }
278287
279288 // 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) {
281296 PackedVector3Array normals = arrays[Mesh::ARRAY_NORMAL];
282297 const Vector3 *norm_r = normals.ptr();
283298 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
288303 blob_offset += 12;
289304 }
290305 }
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 }
291323 }
292324 }
293325 }