OverviewFilesCommitsIssuesDocsReleasesPeers

blender-bridge commits / 4afad53

Add per-triangle smooth/flat shading transfer between Blender and Godot
Extends the SHM blob format with per-triangle smooth flags after index data: [uint32 smooth_count][uint8 × smooth_count] (0=flat, 1=smooth). Blender→Godot: change_detector.py exports polygon use_smooth via loop_triangle polygon_index indirection. C++ import duplicates vertices for flat-shaded triangles with computed face normals. Godot→Blender: C++ export detects flat shading by comparing vertex normals to face normals (dot > 0.999 threshold). scene_import.py applies use_smooth per polygon via foreach_set. Also fixes pre-existing winding order bug in _update_mesh_geometry rebuild path (wasn't reversing winding like _import_mesh). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4afad53009ef6e067b4ae7082b2acc59afc8cd61
by plankatron <fleeky@gmail.com> · 2026-03-03T16:19:11.000Z

Changed files (3) +209 -7

blender_addon/change_detector.py +23 -0
……@@ -300,6 +300,29 @@ class ChangeDetector:
300300 blob_abs_offset += len(raw)
301301 blob_rel_offset += len(raw)
302302
303 # Write per-triangle smooth flags
304 self._shm.write_blob(
305 blob_abs_offset, struct.pack("<I", tri_count)
306 )
307 blob_abs_offset += 4
308 blob_rel_offset += 4
309
310 if tri_count > 0:
311 poly_indices = np.empty(tri_count, dtype=np.int32)
312 mesh.loop_triangles.foreach_get(
313 "polygon_index", poly_indices
314 )
315 poly_count = len(mesh.polygons)
316 poly_smooth = np.empty(poly_count, dtype=np.int32)
317 mesh.polygons.foreach_get("use_smooth", poly_smooth)
318 smooth_flags = poly_smooth[poly_indices].astype(
319 np.uint8
320 )
321 raw = smooth_flags.tobytes()
322 self._shm.write_blob(blob_abs_offset, raw)
323 blob_abs_offset += len(raw)
324 blob_rel_offset += len(raw)
325
303326 if eval_obj is not None:
304327 eval_obj.to_mesh_clear()
305328
blender_addon/scene_import.py +47 -6
……@@ -231,14 +231,33 @@ class SceneImporter:
231231 for fi in range(0, idx_count - 2, 3):
232232 # Reverse winding: Y↔Z swap changes handedness
233233 faces.append((int(indices[fi]), int(indices[fi + 2]), int(indices[fi + 1])))
234 cursor += idx_count * 4
234235 else:
235236 for fi in range(0, vert_count - 2, 3):
236237 # Reverse winding for non-indexed triangles too
237238 faces.append((fi, fi + 2, fi + 1))
238239
240 # Read per-triangle smooth flags (after indices)
241 smooth_flags = None
242 data_end = entry["data_offset"] + entry["data_size"]
243 if cursor + 4 <= data_end:
244 smooth_count = struct.unpack("<I", bytes(self._shm._mm[cursor:cursor + 4]))[0]
245 cursor += 4
246 if smooth_count > 0 and cursor + smooth_count <= data_end:
247 smooth_flags = np.frombuffer(
248 bytes(self._shm._mm[cursor:cursor + smooth_count]),
249 dtype=np.uint8,
250 )
251
239252 mesh = bpy.data.meshes.new(obj_name)
240253 mesh.from_pydata(verts, [], faces)
241254 mesh.update()
255
256 # Apply per-face smooth/flat shading
257 if smooth_flags is not None and len(smooth_flags) == len(mesh.polygons):
258 mesh.polygons.foreach_set("use_smooth", smooth_flags.tolist())
259 mesh.update()
260
242261 obj = bpy.data.objects.new(mesh.name, mesh)
243262 bpy.context.collection.objects.link(obj)
244263 return obj
……@@ -346,11 +365,29 @@ class SceneImporter:
346365 if has_normals:
347366 cursor += vert_count * 3 * 4
348367
349 # Read index count
368 # Read index count and indices
350369 idx_count_raw = self._shm.read_blob(cursor, 4)
351370 idx_count = int(struct.unpack("<I", idx_count_raw.tobytes())[0])
352371 cursor += 4
353372
373 indices = None
374 if idx_count > 0:
375 indices_raw = self._shm.read_blob(cursor, idx_count * 4)
376 indices = np.frombuffer(indices_raw.tobytes(), dtype=np.int32)
377 cursor += idx_count * 4
378
379 # Read per-triangle smooth flags (after indices)
380 smooth_flags = None
381 data_end = entry["data_offset"] + entry["data_size"]
382 if cursor + 4 <= data_end:
383 smooth_count = struct.unpack("<I", bytes(self._shm._mm[cursor:cursor + 4]))[0]
384 cursor += 4
385 if smooth_count > 0 and cursor + smooth_count <= data_end:
386 smooth_flags = np.frombuffer(
387 bytes(self._shm._mm[cursor:cursor + smooth_count]),
388 dtype=np.uint8,
389 )
390
354391 mesh = obj.data
355392 if len(mesh.vertices) == vert_count:
356393 flat = positions.flatten()
……@@ -359,14 +396,18 @@ class SceneImporter:
359396 else:
360397 verts = [tuple(v) for v in positions]
361398 faces = []
362 if idx_count > 0:
363 indices_raw = self._shm.read_blob(cursor, idx_count * 4)
364 indices = np.frombuffer(indices_raw.tobytes(), dtype=np.int32)
399 if indices is not None:
365400 for fi in range(0, idx_count - 2, 3):
366 faces.append((int(indices[fi]), int(indices[fi + 1]), int(indices[fi + 2])))
401 # Reverse winding: Y↔Z swap changes handedness
402 faces.append((int(indices[fi]), int(indices[fi + 2]), int(indices[fi + 1])))
367403 else:
368404 for fi in range(0, vert_count - 2, 3):
369 faces.append((fi, fi + 1, fi + 2))
405 faces.append((fi, fi + 2, fi + 1))
370406 mesh.clear_geometry()
371407 mesh.from_pydata(verts, [], faces)
372408 mesh.update()
409
410 # Apply per-face smooth/flat shading
411 if smooth_flags is not None and len(smooth_flags) == len(mesh.polygons):
412 mesh.polygons.foreach_set("use_smooth", smooth_flags.tolist())
413 mesh.update()
src/blender_bridge.cpp +139 -1
……@@ -19,7 +19,10 @@ using namespace godot;
1919// ── Data layout in blob region ────────────────────────────────────────────────
2020// Per object: [name prefix] [4×4 transform (64 bytes)] [optional vertex data]
2121// Name prefix: [uint8 name_len] [name_bytes] [padding to 4-byte align]
22// Vertex data: [uint32 vertex_count] [positions: 3*N floats] [normals: 3*N floats]
22// Vertex data: [uint32 vert_count] [float32×3 × vert_count positions]
23// [uint32 has_normals] [float32×3 × vert_count normals]
24// [uint32 idx_count] [int32 × idx_count indices]
25// [uint32 smooth_count] [uint8 × smooth_count flags] (0=flat, 1=smooth)
2326
2427static constexpr uint32_t TRANSFORM_SIZE = 64; // 4×4 float matrix
2528
……@@ -384,6 +387,41 @@ uint32_t BlenderBridge::_export_node(Node3D *node, uint32_t index, uint8_t *blob
384387 memcpy(blob_base + blob_offset, idx_r, idx_count * 4);
385388 blob_offset += idx_count * 4;
386389 }
390
391 // Write per-triangle smooth flags (0=flat, 1=smooth)
392 uint32_t tri_count = idx_count / 3;
393 memcpy(blob_base + blob_offset, &tri_count, 4);
394 blob_offset += 4;
395
396 if (tri_count > 0) {
397 if (has_normals && has_indices) {
398 PackedVector3Array norm_arr = arrays[Mesh::ARRAY_NORMAL];
399 const Vector3 *nr = norm_arr.ptr();
400 PackedInt32Array idx_arr = arrays[Mesh::ARRAY_INDEX];
401 const int32_t *ir = idx_arr.ptr();
402
403 for (uint32_t t = 0; t < tri_count; t++) {
404 int32_t i0 = ir[t * 3], i1 = ir[t * 3 + 1], i2 = ir[t * 3 + 2];
405 Vector3 fn = (pos_r[i1] - pos_r[i0]).cross(pos_r[i2] - pos_r[i0]);
406 float len_sq = fn.length_squared();
407
408 uint8_t is_smooth = 1;
409 if (len_sq > 1e-12f) {
410 fn /= Math::sqrt(len_sq);
411 if (nr[i0].dot(fn) > 0.999f &&
412 nr[i1].dot(fn) > 0.999f &&
413 nr[i2].dot(fn) > 0.999f) {
414 is_smooth = 0;
415 }
416 }
417 blob_base[blob_offset++] = is_smooth;
418 }
419 } else {
420 // No normals or no indices — assume all smooth
421 memset(blob_base + blob_offset, 1, tri_count);
422 blob_offset += tri_count;
423 }
424 }
387425 }
388426 }
389427 }
……@@ -716,6 +754,7 @@ void BlenderBridge::import_changes(Node *root) {
716754 if (idx_count > 0 && cursor + idx_count * 4 <= entry.data_offset + entry.data_size) {
717755 indices.resize(idx_count);
718756 memcpy(indices.ptrw(), base + cursor, idx_count * 4);
757 cursor += idx_count * 4;
719758 // Reverse triangle winding order: the Y↔Z axis swap changes
720759 // handedness, so Blender's CCW becomes CW in Godot without this fix.
721760 int32_t *idx_w = indices.ptrw();
……@@ -726,6 +765,71 @@ void BlenderBridge::import_changes(Node *root) {
726765 }
727766 }
728767
768 // Read per-triangle smooth flags (after indices)
769 uint32_t smooth_count = 0;
770 if (cursor + 4 <= entry.data_offset + entry.data_size) {
771 memcpy(&smooth_count, base + cursor, 4);
772 cursor += 4;
773 }
774
775 std::vector<uint8_t> smooth_flags;
776 if (smooth_count > 0 && cursor + smooth_count <= entry.data_offset + entry.data_size) {
777 smooth_flags.resize(smooth_count);
778 memcpy(smooth_flags.data(), base + cursor, smooth_count);
779 cursor += smooth_count;
780 }
781
782 // Handle flat shading: duplicate vertices with face normals
783 if (!smooth_flags.empty() && normals.size() > 0 && indices.size() > 0) {
784 uint32_t tri_count = indices.size() / 3;
785 bool has_flat = false;
786 for (uint32_t t = 0; t < tri_count && t < smooth_count; t++) {
787 if (smooth_flags[t] == 0) { has_flat = true; break; }
788 }
789
790 if (has_flat) {
791 PackedVector3Array new_positions = positions;
792 PackedVector3Array new_normals = normals;
793 PackedInt32Array new_indices;
794 const Vector3 *orig_pos = positions.ptr();
795
796 for (uint32_t t = 0; t < tri_count; t++) {
797 int32_t i0 = indices[t * 3];
798 int32_t i1 = indices[t * 3 + 1];
799 int32_t i2 = indices[t * 3 + 2];
800
801 if (t < smooth_count && smooth_flags[t] == 0) {
802 // Flat: duplicate vertices with face normal
803 Vector3 p0 = orig_pos[i0];
804 Vector3 p1 = orig_pos[i1];
805 Vector3 p2 = orig_pos[i2];
806 Vector3 face_n = (p1 - p0).cross(p2 - p0).normalized();
807
808 int32_t base_idx = new_positions.size();
809 new_positions.push_back(p0);
810 new_positions.push_back(p1);
811 new_positions.push_back(p2);
812 new_normals.push_back(face_n);
813 new_normals.push_back(face_n);
814 new_normals.push_back(face_n);
815
816 new_indices.push_back(base_idx);
817 new_indices.push_back(base_idx + 1);
818 new_indices.push_back(base_idx + 2);
819 } else {
820 // Smooth: keep original indices
821 new_indices.push_back(i0);
822 new_indices.push_back(i1);
823 new_indices.push_back(i2);
824 }
825 }
826
827 positions = new_positions;
828 normals = new_normals;
829 indices = new_indices;
830 }
831 }
832
729833 Ref<ArrayMesh> arr_mesh = Object::cast_to<ArrayMesh>(mesh_inst->get_mesh().ptr());
730834 if (!arr_mesh.is_valid()) {
731835 arr_mesh.instantiate();
……@@ -1023,6 +1127,40 @@ void BlenderBridge::_detect_and_push_changes() {
10231127 memcpy(base + blob_offset, idx_r, idx_count * 4);
10241128 blob_offset += idx_count * 4;
10251129 }
1130
1131 // Write per-triangle smooth flags (0=flat, 1=smooth)
1132 uint32_t tri_count = idx_count / 3;
1133 memcpy(base + blob_offset, &tri_count, 4);
1134 blob_offset += 4;
1135
1136 if (tri_count > 0) {
1137 if (has_normals && has_indices) {
1138 PackedVector3Array norm_arr = arrays[Mesh::ARRAY_NORMAL];
1139 const Vector3 *nr = norm_arr.ptr();
1140 PackedInt32Array idx_arr = arrays[Mesh::ARRAY_INDEX];
1141 const int32_t *ir = idx_arr.ptr();
1142
1143 for (uint32_t t = 0; t < tri_count; t++) {
1144 int32_t i0 = ir[t * 3], i1 = ir[t * 3 + 1], i2 = ir[t * 3 + 2];
1145 Vector3 fn = (pos_r[i1] - pos_r[i0]).cross(pos_r[i2] - pos_r[i0]);
1146 float len_sq = fn.length_squared();
1147
1148 uint8_t is_smooth = 1;
1149 if (len_sq > 1e-12f) {
1150 fn /= Math::sqrt(len_sq);
1151 if (nr[i0].dot(fn) > 0.999f &&
1152 nr[i1].dot(fn) > 0.999f &&
1153 nr[i2].dot(fn) > 0.999f) {
1154 is_smooth = 0;
1155 }
1156 }
1157 base[blob_offset++] = is_smooth;
1158 }
1159 } else {
1160 memset(base + blob_offset, 1, tri_count);
1161 blob_offset += tri_count;
1162 }
1163 }
10261164 }
10271165 }
10281166 }