#include "blender_bridge.h"
#include "virtual_texture.h"
#include <algorithm>
#include <cstdio>
#include <string>
#include <godot_cpp/classes/camera3d.hpp>
#include <godot_cpp/classes/dir_access.hpp>
#include <godot_cpp/classes/directional_light3d.hpp>
#include <godot_cpp/classes/image_texture.hpp>
#include <godot_cpp/classes/light3d.hpp>
#include <godot_cpp/classes/omni_light3d.hpp>
#include <godot_cpp/classes/packed_scene.hpp>
#include <godot_cpp/classes/resource_loader.hpp>
#include <godot_cpp/classes/resource_saver.hpp>
#include <godot_cpp/classes/scene_tree.hpp>
#include <godot_cpp/classes/spot_light3d.hpp>
#include <godot_cpp/classes/window.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
using namespace godot;
// ── Data layout in blob region ────────────────────────────────────────────────
// Per object: [name prefix] [4×4 transform (64 bytes)] [optional vertex data]
// Name prefix: [uint8 name_len] [name_bytes] [padding to 4-byte align]
// Vertex data: [uint32 vert_count] [float32×3 × vert_count positions]
// [uint32 has_normals] [float32×3 × vert_count normals]
// [uint32 idx_count] [int32 × idx_count indices]
// [uint32 smooth_count] [uint8 × smooth_count flags] (0=flat, 1=smooth)
// [uint32 uv_count] [float32×2 × uv_count uvs]
// [uint32 has_material] [44 bytes material data if has_material]
static constexpr uint32_t TRANSFORM_SIZE = 64; // 4×4 float matrix
BlenderBridge::BlenderBridge() {
}
BlenderBridge::~BlenderBridge() {
stop_server();
}
// ── Bind methods ──────────────────────────────────────────────────────────────
void BlenderBridge::_bind_methods() {
// Server lifecycle
ClassDB::bind_method(D_METHOD("start_server", "socket_path"), &BlenderBridge::start_server, DEFVAL("/tmp/blender_bridge.sock"));
ClassDB::bind_method(D_METHOD("stop_server"), &BlenderBridge::stop_server);
ClassDB::bind_method(D_METHOD("is_connected"), &BlenderBridge::is_connected);
ClassDB::bind_method(D_METHOD("get_client_count"), &BlenderBridge::get_client_count);
// Scene export/import
ClassDB::bind_method(D_METHOD("export_scene", "root"), &BlenderBridge::export_scene);
ClassDB::bind_method(D_METHOD("get_export_info"), &BlenderBridge::get_export_info);
ClassDB::bind_method(D_METHOD("import_changes", "root"), &BlenderBridge::import_changes);
// Properties
ClassDB::bind_method(D_METHOD("set_shm_name", "name"), &BlenderBridge::set_shm_name);
ClassDB::bind_method(D_METHOD("get_shm_name"), &BlenderBridge::get_shm_name);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "shm_name"), "set_shm_name", "get_shm_name");
ClassDB::bind_method(D_METHOD("set_shm_size", "size"), &BlenderBridge::set_shm_size);
ClassDB::bind_method(D_METHOD("get_shm_size"), &BlenderBridge::get_shm_size);
ADD_PROPERTY(PropertyInfo(Variant::INT, "shm_size"), "set_shm_size", "get_shm_size");
ClassDB::bind_method(D_METHOD("set_max_objects", "count"), &BlenderBridge::set_max_objects);
ClassDB::bind_method(D_METHOD("get_max_objects"), &BlenderBridge::get_max_objects);
ADD_PROPERTY(PropertyInfo(Variant::INT, "max_objects"), "set_max_objects", "get_max_objects");
ClassDB::bind_method(D_METHOD("set_auto_sync", "enabled"), &BlenderBridge::set_auto_sync);
ClassDB::bind_method(D_METHOD("get_auto_sync"), &BlenderBridge::get_auto_sync);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_sync"), "set_auto_sync", "get_auto_sync");
// VirtualTexture integration
ClassDB::bind_method(D_METHOD("set_virtual_texture_path", "path"), &BlenderBridge::set_virtual_texture_path);
ClassDB::bind_method(D_METHOD("get_virtual_texture_path"), &BlenderBridge::get_virtual_texture_path);
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "virtual_texture_path"), "set_virtual_texture_path", "get_virtual_texture_path");
ClassDB::bind_method(D_METHOD("notify_texture_dirty", "texture_id", "tile_x", "tile_y", "w", "h", "mip"), &BlenderBridge::notify_texture_dirty);
ClassDB::bind_method(D_METHOD("finalize_scene", "root", "save_dir"), &BlenderBridge::finalize_scene);
// Signals
ADD_SIGNAL(MethodInfo("client_connected", PropertyInfo(Variant::INT, "client_id")));
ADD_SIGNAL(MethodInfo("client_disconnected", PropertyInfo(Variant::INT, "client_id")));
ADD_SIGNAL(MethodInfo("scene_exported", PropertyInfo(Variant::INT, "object_count")));
ADD_SIGNAL(MethodInfo("changes_imported", PropertyInfo(Variant::INT, "object_count")));
ADD_SIGNAL(MethodInfo("texture_dirty",
PropertyInfo(Variant::INT, "texture_id"),
PropertyInfo(Variant::INT, "tile_x"),
PropertyInfo(Variant::INT, "tile_y"),
PropertyInfo(Variant::INT, "w"),
PropertyInfo(Variant::INT, "h"),
PropertyInfo(Variant::INT, "mip")));
}
// ── Properties ────────────────────────────────────────────────────────────────
void BlenderBridge::set_shm_name(const String &name) { _shm_name = name; }
String BlenderBridge::get_shm_name() const { return _shm_name; }
void BlenderBridge::set_shm_size(int size) { _shm_size = size; }
int BlenderBridge::get_shm_size() const { return _shm_size; }
void BlenderBridge::set_max_objects(int count) { _max_objects = count; }
int BlenderBridge::get_max_objects() const { return _max_objects; }
void BlenderBridge::set_auto_sync(bool enabled) { _auto_sync = enabled; }
bool BlenderBridge::get_auto_sync() const { return _auto_sync; }
void BlenderBridge::set_virtual_texture_path(const NodePath &path) { _virtual_texture_path = path; }
NodePath BlenderBridge::get_virtual_texture_path() const { return _virtual_texture_path; }
// ── VirtualTexture integration ────────────────────────────────────────────────
void BlenderBridge::notify_texture_dirty(int texture_id, int tile_x, int tile_y, int w, int h, int mip) {
// Forward to VirtualTexture node if configured
if (!_virtual_texture_path.is_empty()) {
Node *vt_node = get_node_or_null(_virtual_texture_path);
VirtualTexture *vt = Object::cast_to<VirtualTexture>(vt_node);
if (vt) {
vt->invalidate_region(texture_id, tile_x, tile_y, w, h, mip);
}
}
emit_signal("texture_dirty", texture_id, tile_x, tile_y, w, h, mip);
}
// ── Finalize (save textures + scene to disk) ──────────────────────────────────
Error BlenderBridge::finalize_scene(Node *root, const String &save_dir) {
if (_virtual_texture_path.is_empty())
return ERR_UNCONFIGURED;
Node *vt_node = get_node_or_null(_virtual_texture_path);
VirtualTexture *vt = Object::cast_to<VirtualTexture>(vt_node);
if (!vt)
return ERR_UNCONFIGURED;
// Step 1: Save each reconstructed texture as .png
TypedArray<Dictionary> textures = vt->get_all_textures();
HashMap<int, String> tex_paths; // svt_id → saved .png path
for (int i = 0; i < textures.size(); i++) {
Dictionary tex = textures[i];
int tex_id = tex["id"];
String tex_name = tex["name"];
bool has_image = tex["has_image"];
if (!has_image)
continue;
Ref<Image> img = vt->get_reconstructed_image(tex_id);
if (img.is_null())
continue;
// Sanitize name for filesystem
String safe_name = tex_name.replace(" ", "_").replace("/", "_");
String png_path = save_dir.path_join(safe_name + ".png");
Error err = img->save_png(png_path);
if (err == OK) {
tex_paths[tex_id] = png_path;
UtilityFunctions::print("Finalize: Saved ", png_path);
}
}
// Step 2: Convert SVT ShaderMaterials to StandardMaterial3D + ImageTexture
Ref<Shader> svt_shader = vt->get_svt_shader();
std::vector<Node *> stack;
stack.push_back(root);
while (!stack.empty()) {
Node *node = stack.back();
stack.pop_back();
MeshInstance3D *mesh_inst = Object::cast_to<MeshInstance3D>(node);
if (mesh_inst) {
for (int s = 0; s < mesh_inst->get_surface_override_material_count(); s++) {
Ref<Material> mat = mesh_inst->get_surface_override_material(s);
Ref<ShaderMaterial> shader_mat = mat;
if (shader_mat.is_valid() && svt_shader.is_valid() && shader_mat->get_shader() == svt_shader) {
// Extract material properties from SVT shader params
Color albedo = shader_mat->get_shader_parameter("albedo_color");
float rough = shader_mat->get_shader_parameter("roughness");
float metal = shader_mat->get_shader_parameter("metallic");
Ref<StandardMaterial3D> std_mat;
std_mat.instantiate();
std_mat->set_albedo(albedo);
std_mat->set_roughness(rough);
std_mat->set_metallic(metal);
// Set first available texture as albedo
for (const KeyValue<int, String> &pair : tex_paths) {
Ref<Texture2D> loaded = ResourceLoader::get_singleton()->load(pair.value);
if (loaded.is_valid()) {
std_mat->set_texture(StandardMaterial3D::TEXTURE_ALBEDO, loaded);
break;
}
}
mesh_inst->set_surface_override_material(s, std_mat);
}
}
}
for (int c = 0; c < node->get_child_count(); c++) {
stack.push_back(node->get_child(c));
}
}
// Step 3: Save the scene as .tscn
Ref<PackedScene> packed;
packed.instantiate();
packed->pack(root);
String scene_path = save_dir.path_join("scene.tscn");
Error err = ResourceSaver::get_singleton()->save(packed, scene_path);
if (err == OK) {
UtilityFunctions::print("Finalize: Saved scene to ", scene_path);
}
return err;
}
// ── Server lifecycle ──────────────────────────────────────────────────────────
Error BlenderBridge::start_server(const String &socket_path) {
if (_server_running) {
return ERR_ALREADY_IN_USE;
}
// Create shared memory
CharString shm_cs = _shm_name.utf8();
if (!_shm.create(shm_cs.get_data(), _shm_size)) {
UtilityFunctions::printerr("BlenderBridge: Failed to create shared memory: ", _shm_name);
return ERR_CANT_CREATE;
}
// Set up split-region pointers
_region_size = REGION_SIZE;
_write_base = _shm.ptr(); // Region 0: Godot writes
_read_base = region_base(_shm.ptr(), 1, _region_size); // Region 1: Blender writes
// Initialize BOTH region headers
BridgeHeader hdr = {};
hdr.magic[0] = 'B';
hdr.magic[1] = 'G';
hdr.magic[2] = 'O';
hdr.magic[3] = 'D';
hdr.version = 1;
hdr.direction = 0;
hdr.sequence = 0;
hdr.object_count = 0;
hdr.frame_number = 0;
write_header(_write_base, hdr);
write_header(_read_base, hdr);
// Start UDS server
CharString sock_cs = socket_path.utf8();
if (!_uds.start(sock_cs.get_data())) {
UtilityFunctions::printerr("BlenderBridge: Failed to start UDS server: ", socket_path);
_shm.destroy();
return ERR_CANT_CREATE;
}
_server_running = true;
_export_sequence = 0;
_last_import_sequence = 0;
_tracked_objects.clear();
_known_instance_ids.clear();
_next_object_id = 1;
UtilityFunctions::print("BlenderBridge v0.18.2: Server started (shm=", _shm_name, ", sock=", socket_path, ", regions=2x", (int)_region_size, ")");
return OK;
}
void BlenderBridge::stop_server() {
if (!_server_running)
return;
_uds.stop();
_shm.destroy();
_server_running = false;
_tracked_objects.clear();
_known_instance_ids.clear();
_write_base = nullptr;
_read_base = nullptr;
_region_size = 0;
UtilityFunctions::print("BlenderBridge: Server stopped");
}
bool BlenderBridge::is_connected() const {
return _server_running && _uds.client_count() > 0;
}
int BlenderBridge::get_client_count() const {
return _uds.client_count();
}
// ── Scene export (Godot → shared memory) ──────────────────────────────────────
void BlenderBridge::_collect_exportable_nodes(Node *node, std::vector<Node3D *> &out) {
Node3D *n3d = Object::cast_to<Node3D>(node);
if (n3d) {
// Include MeshInstance3D, Camera3D, Light3D
if (Object::cast_to<MeshInstance3D>(n3d) ||
Object::cast_to<Camera3D>(n3d) ||
Object::cast_to<Light3D>(n3d)) {
out.push_back(n3d);
}
}
for (int i = 0; i < node->get_child_count(); i++) {
_collect_exportable_nodes(node->get_child(i), out);
}
}
void BlenderBridge::_export_transform(const Transform3D &xform, float *out) {
// Godot Transform3D → 4×4 column-major matrix in Blender coordinate space
// Godot: Y-up (x, y, z) → Blender: Z-up (x, -z, y)
//
// Conversion matrix M (applied to both basis columns and origin):
// [1, 0, 0]
// [0, 0, -1]
// [0, 1, 0]
//
// Result = M * GodotBasis * M^-1 (for basis), M * origin (for translation)
const Basis &b = xform.basis;
const Vector3 &o = xform.origin;
// Blender expects row-major 4×4 with translation in row 3 (column 3 when transposed).
// Matrix() constructor takes rows, so we write standard row-major:
// [basis_col0.x basis_col1.x basis_col2.x origin.x]
// [basis_col0.y basis_col1.y basis_col2.y origin.y]
// [basis_col0.z basis_col1.z basis_col2.z origin.z]
// [0 0 0 1 ]
//
// With coordinate conversion M applied: M * col, M * origin
// M * B * M^-1 (similarity transform, NOT transposed)
// Row 0 (Blender X)
out[0] = b[0][0];
out[1] = -b[0][2];
out[2] = b[0][1];
out[3] = o.x;
// Row 1 (Blender Y = -Godot Z)
out[4] = -b[2][0];
out[5] = b[2][2];
out[6] = -b[2][1];
out[7] = -o.z;
// Row 2 (Blender Z = Godot Y)
out[8] = b[1][0];
out[9] = -b[1][2];
out[10] = b[1][1];
out[11] = o.y;
// Row 3
out[12] = 0.0f;
out[13] = 0.0f;
out[14] = 0.0f;
out[15] = 1.0f;
}
void BlenderBridge::_write_name_prefix(const String &name, uint8_t *blob_base, uint32_t &blob_offset) {
CharString utf8 = name.utf8();
uint8_t name_len = (uint8_t)std::min((int)utf8.length(), 255);
blob_base[blob_offset] = name_len;
blob_offset += 1;
if (name_len > 0) {
memcpy(blob_base + blob_offset, utf8.get_data(), name_len);
blob_offset += name_len;
}
// Pad to 4-byte alignment
uint32_t total = 1 + name_len;
uint32_t pad = (4 - (total % 4)) % 4;
if (pad > 0) {
memset(blob_base + blob_offset, 0, pad);
blob_offset += pad;
}
}
uint32_t BlenderBridge::_export_node(Node3D *node, uint32_t index, uint8_t *blob_base, uint32_t &blob_offset) {
ObjectEntry entry = {};
entry.object_id = _next_object_id++;
entry.update_type = UPDATE_FULL;
entry.data_offset = blob_offset;
entry.sub_type = 0;
// Determine type and subtype
MeshInstance3D *mesh_inst = Object::cast_to<MeshInstance3D>(node);
Camera3D *camera = Object::cast_to<Camera3D>(node);
Light3D *light = Object::cast_to<Light3D>(node);
bool needs_look_compensation = false;
if (mesh_inst) {
entry.object_type = OBJ_MESH;
} else if (camera) {
entry.object_type = OBJ_CAMERA;
needs_look_compensation = true;
} else if (light) {
entry.object_type = OBJ_LIGHT;
// Detect specific light type
if (Object::cast_to<DirectionalLight3D>(node)) {
entry.sub_type = LIGHT_SUN;
needs_look_compensation = true;
} else if (Object::cast_to<SpotLight3D>(node)) {
entry.sub_type = LIGHT_SPOT;
needs_look_compensation = true;
} else {
entry.sub_type = LIGHT_POINT;
}
} else {
entry.object_type = OBJ_EMPTY;
}
// Write object name: [uint8 name_len] [name_bytes] [pad to 4-byte alignment]
_write_name_prefix(node->get_name(), blob_base, blob_offset);
// Write transform (always present after name prefix)
float xform_data[16];
_export_transform(node->get_global_transform(), xform_data);
// Camera/directional/spot lights: post-multiply by R_x(90°) to compensate
// for the -Z_local look direction convention across coordinate systems.
// Without this, cameras/lights point in the wrong direction in Blender.
if (needs_look_compensation) {
// R_x(90°): new col1 = old col2, new col2 = -old col1
float old1[3] = {xform_data[1], xform_data[5], xform_data[9]};
float old2[3] = {xform_data[2], xform_data[6], xform_data[10]};
xform_data[1] = old2[0]; xform_data[5] = old2[1]; xform_data[9] = old2[2];
xform_data[2] = -old1[0]; xform_data[6] = -old1[1]; xform_data[10] = -old1[2];
}
memcpy(blob_base + blob_offset, xform_data, TRANSFORM_SIZE);
blob_offset += TRANSFORM_SIZE;
// Write vertex data for meshes
if (mesh_inst && mesh_inst->get_mesh().is_valid()) {
Ref<Mesh> mesh = mesh_inst->get_mesh();
if (mesh->get_surface_count() > 0) {
Array arrays = mesh->surface_get_arrays(0);
if (arrays.size() > Mesh::ARRAY_VERTEX) {
PackedVector3Array positions = arrays[Mesh::ARRAY_VERTEX];
uint32_t vert_count = positions.size();
// Write vertex count
memcpy(blob_base + blob_offset, &vert_count, 4);
blob_offset += 4;
// Write positions (converted to Blender coords)
const Vector3 *pos_r = positions.ptr();
for (uint32_t v = 0; v < vert_count; v++) {
float in[3] = { pos_r[v].x, pos_r[v].y, pos_r[v].z };
float out[3];
_godot_to_blender_vec3(in, out);
memcpy(blob_base + blob_offset, out, 12);
blob_offset += 12;
}
// Write normals if present
bool has_normals = arrays.size() > Mesh::ARRAY_NORMAL && arrays[Mesh::ARRAY_NORMAL].get_type() != Variant::NIL;
// Write has_normals flag (uint32)
uint32_t normals_flag = has_normals ? 1 : 0;
memcpy(blob_base + blob_offset, &normals_flag, 4);
blob_offset += 4;
if (has_normals) {
PackedVector3Array normals = arrays[Mesh::ARRAY_NORMAL];
const Vector3 *norm_r = normals.ptr();
for (uint32_t v = 0; v < vert_count; v++) {
float in[3] = { norm_r[v].x, norm_r[v].y, norm_r[v].z };
float out[3];
_godot_to_blender_vec3(in, out);
memcpy(blob_base + blob_offset, out, 12);
blob_offset += 12;
}
}
// Write index buffer if present
bool has_indices = arrays.size() > Mesh::ARRAY_INDEX && arrays[Mesh::ARRAY_INDEX].get_type() != Variant::NIL;
uint32_t idx_count = 0;
if (has_indices) {
PackedInt32Array indices = arrays[Mesh::ARRAY_INDEX];
idx_count = indices.size();
}
memcpy(blob_base + blob_offset, &idx_count, 4);
blob_offset += 4;
if (idx_count > 0) {
PackedInt32Array indices = arrays[Mesh::ARRAY_INDEX];
const int32_t *idx_r = indices.ptr();
memcpy(blob_base + blob_offset, idx_r, idx_count * 4);
blob_offset += idx_count * 4;
}
// Write per-triangle smooth flags (0=flat, 1=smooth)
uint32_t tri_count = idx_count / 3;
memcpy(blob_base + blob_offset, &tri_count, 4);
blob_offset += 4;
if (tri_count > 0) {
if (has_normals && has_indices) {
PackedVector3Array norm_arr = arrays[Mesh::ARRAY_NORMAL];
const Vector3 *nr = norm_arr.ptr();
PackedInt32Array idx_arr = arrays[Mesh::ARRAY_INDEX];
const int32_t *ir = idx_arr.ptr();
for (uint32_t t = 0; t < tri_count; t++) {
int32_t i0 = ir[t * 3], i1 = ir[t * 3 + 1], i2 = ir[t * 3 + 2];
Vector3 fn = (pos_r[i1] - pos_r[i0]).cross(pos_r[i2] - pos_r[i0]);
float len_sq = fn.length_squared();
uint8_t is_smooth = 1;
if (len_sq > 1e-12f) {
fn /= Math::sqrt(len_sq);
if (nr[i0].dot(fn) > 0.999f &&
nr[i1].dot(fn) > 0.999f &&
nr[i2].dot(fn) > 0.999f) {
is_smooth = 0;
}
}
blob_base[blob_offset++] = is_smooth;
}
} else {
// No normals or no indices — assume all smooth
memset(blob_base + blob_offset, 1, tri_count);
blob_offset += tri_count;
}
}
}
// Write UV coordinates (float32×2 per vertex)
bool has_uvs = arrays.size() > Mesh::ARRAY_TEX_UV && arrays[Mesh::ARRAY_TEX_UV].get_type() != Variant::NIL;
uint32_t uv_count = 0;
if (has_uvs) {
PackedVector2Array uvs = arrays[Mesh::ARRAY_TEX_UV];
uv_count = uvs.size();
}
memcpy(blob_base + blob_offset, &uv_count, 4);
blob_offset += 4;
if (uv_count > 0) {
PackedVector2Array uvs = arrays[Mesh::ARRAY_TEX_UV];
const Vector2 *uv_r = uvs.ptr();
for (uint32_t v = 0; v < uv_count; v++) {
float uv[2] = { uv_r[v].x, uv_r[v].y };
memcpy(blob_base + blob_offset, uv, 8);
blob_offset += 8;
}
}
}
}
// Write material data (only for meshes)
if (entry.object_type == OBJ_MESH && mesh_inst) {
Ref<Material> mat_ref = mesh_inst->get_surface_override_material(0);
if (!mat_ref.is_valid() && mesh_inst->get_mesh().is_valid() && mesh_inst->get_mesh()->get_surface_count() > 0) {
mat_ref = mesh_inst->get_mesh()->surface_get_material(0);
}
Ref<StandardMaterial3D> std_mat = Object::cast_to<StandardMaterial3D>(mat_ref.ptr());
if (std_mat.is_valid()) {
uint32_t has_material = 1;
memcpy(blob_base + blob_offset, &has_material, 4);
blob_offset += 4;
Color albedo = std_mat->get_albedo();
float mat_data[11];
mat_data[0] = albedo.r; mat_data[1] = albedo.g; mat_data[2] = albedo.b; mat_data[3] = albedo.a;
mat_data[4] = std_mat->get_roughness();
mat_data[5] = std_mat->get_metallic();
Color emission = std_mat->get_emission();
mat_data[6] = emission.r; mat_data[7] = emission.g; mat_data[8] = emission.b; mat_data[9] = emission.a;
mat_data[10] = std_mat->get_emission_energy_multiplier();
memcpy(blob_base + blob_offset, mat_data, 44);
blob_offset += 44;
uint32_t tex_id = 0; // Godot-side meshes don't carry bridge texture IDs
memcpy(blob_base + blob_offset, &tex_id, 4);
blob_offset += 4;
} else {
uint32_t has_material = 0;
memcpy(blob_base + blob_offset, &has_material, 4);
blob_offset += 4;
}
} else if (entry.object_type == OBJ_MESH) {
uint32_t has_material = 0;
memcpy(blob_base + blob_offset, &has_material, 4);
blob_offset += 4;
}
entry.data_size = blob_offset - entry.data_offset;
entry.sequence = _export_sequence;
write_object_entry(_write_base, index, entry);
// Track the object with initial transform hash
TrackedObject tracked;
tracked.object_id = entry.object_id;
tracked.godot_instance_id = node->get_instance_id();
tracked.name = node->get_name();
tracked.type = (ObjectType)entry.object_type;
tracked.transform_hash = _hash_transform(node->get_global_transform());
tracked.from_blender = false;
_tracked_objects.push_back(tracked);
return entry.object_id;
}
Error BlenderBridge::export_scene(Node *root) {
if (!_server_running || !_shm.valid()) {
return ERR_UNCONFIGURED;
}
// Collect exportable nodes
std::vector<Node3D *> nodes;
_collect_exportable_nodes(root, nodes);
if (nodes.size() > (size_t)_max_objects) {
UtilityFunctions::printerr("BlenderBridge: Too many objects (", (int)nodes.size(), " > ", _max_objects, ")");
return ERR_OUT_OF_MEMORY;
}
// Clear only Godot-origin tracked objects (preserve Blender-origin ones)
_tracked_objects.erase(
std::remove_if(_tracked_objects.begin(), _tracked_objects.end(),
[](const TrackedObject &t) { return !t.from_blender; }),
_tracked_objects.end());
_next_object_id = 1;
// Ensure IDs don't collide with Blender-origin objects
for (const auto &t : _tracked_objects) {
if (t.object_id >= _next_object_id) {
_next_object_id = t.object_id + 1;
}
}
_export_sequence++;
_export_root_id = root->get_instance_id();
_last_export_node_count = (int)nodes.size();
// Blob data starts after header + object table — within region 0
uint32_t blob_start = (uint32_t)data_blob_offset(_max_objects);
uint32_t blob_offset = blob_start;
uint8_t *base = static_cast<uint8_t *>(_write_base);
// Export each node
for (size_t i = 0; i < nodes.size(); i++) {
_export_node(nodes[i], (uint32_t)i, base, blob_offset);
}
// Populate _known_instance_ids for incremental change detection
_known_instance_ids.clear();
for (const auto &t : _tracked_objects) {
_known_instance_ids.insert(t.godot_instance_id);
}
// Update header in region 0 (Godot→Blender)
BridgeHeader hdr = read_header(_write_base);
hdr.direction = 0; // godot→blender (unused in split-region, kept for compat)
hdr.sequence = _export_sequence;
hdr.object_count = (uint32_t)nodes.size();
hdr.frame_number++;
write_header(_write_base, hdr);
// Signal Blender via UDS
if (_uds.client_count() > 0) {
const char *msg = "scene_exported";
_uds.broadcast(msg, (uint32_t)strlen(msg));
}
emit_signal("scene_exported", (int)nodes.size());
return OK;
}
Dictionary BlenderBridge::get_export_info() const {
Dictionary info;
if (!_shm.valid()) {
info["object_count"] = 0;
return info;
}
BridgeHeader hdr = read_header(_write_base);
info["object_count"] = (int)hdr.object_count;
info["sequence"] = (int)hdr.sequence;
info["direction"] = (int)hdr.direction;
Array objects;
for (const auto &tracked : _tracked_objects) {
Dictionary obj;
obj["object_id"] = (int)tracked.object_id;
obj["name"] = tracked.name;
obj["type"] = (int)tracked.type;
objects.push_back(obj);
}
info["objects"] = objects;
return info;
}
// ── Scene import (shared memory → Godot) ──────────────────────────────────────
void BlenderBridge::import_changes(Node *root) {
if (!_shm.valid() || !_read_base)
return;
// Read from region 1 (Blender→Godot) — no direction check needed
BridgeHeader hdr = read_header(_read_base);
// Only process if new data
if (hdr.sequence <= _last_import_sequence)
return;
_last_import_sequence = hdr.sequence;
uint8_t *base = static_cast<uint8_t *>(_read_base);
int imported = 0;
UtilityFunctions::print("BlenderBridge: import_changes seq=", (int)hdr.sequence,
" count=", (int)hdr.object_count, " tracked=", (int)_tracked_objects.size());
for (uint32_t i = 0; i < hdr.object_count; i++) {
ObjectEntry entry = read_object_entry(_read_base, i);
// Read name from blob: [uint8 name_len] [name_bytes] [padding to 4-byte align]
uint32_t cursor = entry.data_offset;
String blender_name;
if (entry.data_size > 0) {
uint8_t name_len = base[cursor];
cursor += 1;
if (name_len > 0 && cursor + name_len <= entry.data_offset + entry.data_size) {
blender_name = String::utf8(reinterpret_cast<const char *>(base + cursor), name_len);
cursor += name_len;
}
// Skip padding to 4-byte alignment
uint32_t total = 1 + name_len;
uint32_t pad = (4 - (total % 4)) % 4;
cursor += pad;
}
// Find matching tracked object
Node3D *target = nullptr;
for (const auto &tracked : _tracked_objects) {
if (tracked.object_id == entry.object_id) {
Object *obj = ObjectDB::get_instance(tracked.godot_instance_id);
target = Object::cast_to<Node3D>(obj);
break;
}
}
if (!target) {
// Unknown object — Blender created it. Create a new node in Godot.
UtilityFunctions::print("BlenderBridge: NEW obj id=", (int)entry.object_id,
" name=", blender_name,
" type=", (int)entry.object_type, " update=", (int)entry.update_type,
" size=", (int)entry.data_size);
if (entry.update_type == UPDATE_DELETE)
continue;
Node *export_root = Object::cast_to<Node>(ObjectDB::get_instance(_export_root_id));
if (!export_root)
export_root = root;
Node3D *new_node = nullptr;
if (entry.object_type == OBJ_MESH) {
MeshInstance3D *mi = memnew(MeshInstance3D);
Ref<ArrayMesh> mesh;
mesh.instantiate();
mi->set_mesh(mesh);
new_node = mi;
} else if (entry.object_type == OBJ_CAMERA) {
new_node = memnew(Camera3D);
} else if (entry.object_type == OBJ_LIGHT) {
if (entry.sub_type == LIGHT_SUN) {
new_node = memnew(DirectionalLight3D);
} else if (entry.sub_type == LIGHT_SPOT) {
new_node = memnew(SpotLight3D);
} else {
new_node = memnew(OmniLight3D);
}
} else {
new_node = memnew(Node3D);
}
// Use actual Blender name, fallback to "blender_obj_N"
String name = blender_name.is_empty()
? String("blender_obj_") + String::num_int64(entry.object_id)
: blender_name;
new_node->set_name(name);
export_root->add_child(new_node);
new_node->set_owner(export_root);
TrackedObject tracked;
tracked.object_id = entry.object_id;
tracked.godot_instance_id = new_node->get_instance_id();
tracked.name = name;
tracked.type = (ObjectType)entry.object_type;
tracked.transform_hash = 0;
tracked.from_blender = true;
_tracked_objects.push_back(tracked);
_known_instance_ids.insert(new_node->get_instance_id());
_last_export_node_count++;
target = new_node;
}
// cursor now points past the name to the transform data
uint32_t transform_offset = cursor;
// Update name on existing tracked objects if Blender renamed them
if (!blender_name.is_empty()) {
for (auto &tracked : _tracked_objects) {
if (tracked.object_id == entry.object_id && tracked.name != blender_name) {
tracked.name = blender_name;
target->set_name(blender_name);
break;
}
}
}
if (entry.update_type == UPDATE_TRANSFORM || entry.update_type == UPDATE_FULL) {
// Read transform from blob (after name prefix)
float xform_data[16];
memcpy(xform_data, base + transform_offset, TRANSFORM_SIZE);
// Reverse of _export_transform: M^-1 * B' * M
// where B' is the Blender-space 4x4 from SHM
Transform3D xform;
Basis &b = xform.basis;
Vector3 &o = xform.origin;
b[0][0] = xform_data[0];
b[0][1] = xform_data[2];
b[0][2] = -xform_data[1];
b[1][0] = xform_data[8];
b[1][1] = xform_data[10];
b[1][2] = -xform_data[9];
b[2][0] = -xform_data[4];
b[2][1] = -xform_data[6];
b[2][2] = xform_data[5];
o.x = xform_data[3];
o.y = xform_data[11];
o.z = -xform_data[7];
// Camera/directional/spot lights: post-multiply by R_x(-90°)
// to undo the look-direction compensation applied during export.
bool needs_look_compensation = (entry.object_type == OBJ_CAMERA) ||
(entry.object_type == OBJ_LIGHT && entry.sub_type != LIGHT_POINT);
if (needs_look_compensation) {
// R_x(-90°): new col1 = -old col2, new col2 = old col1
real_t old_col1[3] = {b[0][1], b[1][1], b[2][1]};
real_t old_col2[3] = {b[0][2], b[1][2], b[2][2]};
b[0][1] = -old_col2[0]; b[1][1] = -old_col2[1]; b[2][1] = -old_col2[2];
b[0][2] = old_col1[0]; b[1][2] = old_col1[1]; b[2][2] = old_col1[2];
}
target->set_global_transform(xform);
// Update tracked hash so _detect_and_push_changes won't echo this back
for (auto &tracked : _tracked_objects) {
if (tracked.object_id == entry.object_id) {
tracked.transform_hash = _hash_transform(xform);
break;
}
}
}
if (entry.update_type == UPDATE_VERTEX || entry.update_type == UPDATE_FULL) {
MeshInstance3D *mesh_inst = Object::cast_to<MeshInstance3D>(target);
if (mesh_inst && entry.data_size > TRANSFORM_SIZE + 4) {
uint32_t cursor = transform_offset + TRANSFORM_SIZE;
uint32_t data_end = entry.data_offset + entry.data_size;
uint32_t vert_count;
memcpy(&vert_count, base + cursor, 4);
cursor += 4;
// Bounds check: ensure position data fits within the entry
if (vert_count == 0) {
// Empty mesh — no geometry data in blob, skip to material
} else if (cursor + vert_count * 12 > data_end) {
UtilityFunctions::printerr("BlenderBridge: vert_count out of bounds: ", (int)vert_count);
continue;
} else {
PackedVector3Array positions;
positions.resize(vert_count);
Vector3 *pos_w = positions.ptrw();
for (uint32_t v = 0; v < vert_count; v++) {
float in[3];
memcpy(in, base + cursor, 12);
cursor += 12;
pos_w[v].x = in[0];
pos_w[v].y = in[2];
pos_w[v].z = -in[1];
}
// Read has_normals flag
PackedVector3Array normals;
uint32_t has_normals = 0;
if (cursor + 4 <= data_end) {
memcpy(&has_normals, base + cursor, 4);
cursor += 4;
}
if (has_normals && cursor + vert_count * 12 <= data_end) {
normals.resize(vert_count);
Vector3 *norm_w = normals.ptrw();
for (uint32_t v = 0; v < vert_count; v++) {
float in[3];
memcpy(in, base + cursor, 12);
cursor += 12;
norm_w[v].x = in[0];
norm_w[v].y = in[2];
norm_w[v].z = -in[1];
}
}
// Read index buffer
PackedInt32Array indices;
uint32_t idx_count = 0;
if (cursor + 4 <= data_end) {
memcpy(&idx_count, base + cursor, 4);
cursor += 4;
}
if (idx_count > 0 && cursor + idx_count * 4 <= data_end) {
indices.resize(idx_count);
memcpy(indices.ptrw(), base + cursor, idx_count * 4);
cursor += idx_count * 4;
// Reverse triangle winding order: the Y↔Z axis swap changes
// handedness, so Blender's CCW becomes CW in Godot without this fix.
int32_t *idx_w = indices.ptrw();
for (uint32_t t = 0; t + 2 < idx_count; t += 3) {
int32_t tmp = idx_w[t + 1];
idx_w[t + 1] = idx_w[t + 2];
idx_w[t + 2] = tmp;
}
} else if (idx_count == 0 && vert_count >= 3) {
// Per-corner data (no index buffer): reverse winding by
// swapping vertices 1 and 2 of each triangle in-place.
Vector3 *pw = positions.ptrw();
Vector3 *nw = normals.size() > 0 ? normals.ptrw() : nullptr;
for (uint32_t t = 0; t + 2 < vert_count; t += 3) {
Vector3 tmp = pw[t + 1];
pw[t + 1] = pw[t + 2];
pw[t + 2] = tmp;
if (nw) {
tmp = nw[t + 1];
nw[t + 1] = nw[t + 2];
nw[t + 2] = tmp;
}
}
}
// Read per-triangle smooth flags (after indices)
uint32_t smooth_count = 0;
if (cursor + 4 <= entry.data_offset + entry.data_size) {
memcpy(&smooth_count, base + cursor, 4);
cursor += 4;
}
std::vector<uint8_t> smooth_flags;
if (smooth_count > 0 && cursor + smooth_count <= entry.data_offset + entry.data_size) {
smooth_flags.resize(smooth_count);
memcpy(smooth_flags.data(), base + cursor, smooth_count);
cursor += smooth_count;
}
// Read UV coordinates (after smooth flags)
PackedVector2Array uvs;
uint32_t uv_count = 0;
if (cursor + 4 <= data_end) {
memcpy(&uv_count, base + cursor, 4);
cursor += 4;
}
if (uv_count > 0 && cursor + uv_count * 8 <= data_end) {
uvs.resize(uv_count);
Vector2 *uv_w = uvs.ptrw();
for (uint32_t v = 0; v < uv_count; v++) {
float uv[2];
memcpy(uv, base + cursor, 8);
cursor += 8;
uv_w[v].x = uv[0];
uv_w[v].y = uv[1];
}
// Reverse UV winding for per-corner data (same as positions/normals)
if (idx_count == 0 && uv_count >= 3) {
Vector2 *uvw = uvs.ptrw();
for (uint32_t t = 0; t + 2 < uv_count; t += 3) {
Vector2 tmp = uvw[t + 1];
uvw[t + 1] = uvw[t + 2];
uvw[t + 2] = tmp;
}
}
}
// Handle flat shading: duplicate vertices with face normals
if (!smooth_flags.empty() && normals.size() > 0 && indices.size() > 0) {
uint32_t tri_count = indices.size() / 3;
bool has_flat = false;
for (uint32_t t = 0; t < tri_count && t < smooth_count; t++) {
if (smooth_flags[t] == 0) { has_flat = true; break; }
}
if (has_flat) {
PackedVector3Array new_positions = positions;
PackedVector3Array new_normals = normals;
PackedVector2Array new_uvs = uvs;
PackedInt32Array new_indices;
const Vector3 *orig_pos = positions.ptr();
const Vector2 *orig_uv = uvs.size() > 0 ? uvs.ptr() : nullptr;
for (uint32_t t = 0; t < tri_count; t++) {
int32_t i0 = indices[t * 3];
int32_t i1 = indices[t * 3 + 1];
int32_t i2 = indices[t * 3 + 2];
if (t < smooth_count && smooth_flags[t] == 0) {
// Flat: duplicate vertices with face normal
Vector3 p0 = orig_pos[i0];
Vector3 p1 = orig_pos[i1];
Vector3 p2 = orig_pos[i2];
Vector3 face_n = (p2 - p0).cross(p1 - p0).normalized();
int32_t base_idx = new_positions.size();
new_positions.push_back(p0);
new_positions.push_back(p1);
new_positions.push_back(p2);
new_normals.push_back(face_n);
new_normals.push_back(face_n);
new_normals.push_back(face_n);
if (orig_uv) {
new_uvs.push_back(orig_uv[i0]);
new_uvs.push_back(orig_uv[i1]);
new_uvs.push_back(orig_uv[i2]);
}
new_indices.push_back(base_idx);
new_indices.push_back(base_idx + 1);
new_indices.push_back(base_idx + 2);
} else {
// Smooth: keep original indices
new_indices.push_back(i0);
new_indices.push_back(i1);
new_indices.push_back(i2);
}
}
positions = new_positions;
normals = new_normals;
uvs = new_uvs;
indices = new_indices;
}
}
Ref<ArrayMesh> arr_mesh = Object::cast_to<ArrayMesh>(mesh_inst->get_mesh().ptr());
if (!arr_mesh.is_valid()) {
arr_mesh.instantiate();
mesh_inst->set_mesh(arr_mesh);
}
// Always do full rebuild — surface_update_vertex_region requires
// matching Godot's internal interleaved vertex format which differs
// from our raw float layout, causing buffer overflow errors.
{
Array arrays;
arrays.resize(Mesh::ARRAY_MAX);
arrays[Mesh::ARRAY_VERTEX] = positions;
if (normals.size() > 0) {
arrays[Mesh::ARRAY_NORMAL] = normals;
}
if (indices.size() > 0) {
arrays[Mesh::ARRAY_INDEX] = indices;
}
if (uvs.size() > 0) {
arrays[Mesh::ARRAY_TEX_UV] = uvs;
}
arr_mesh->clear_surfaces();
arr_mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrays);
// Read material data (after UV data): 44 bytes PBR + 4 bytes tex_id
uint32_t has_material = 0;
if (cursor + 4 <= data_end) {
memcpy(&has_material, base + cursor, 4);
cursor += 4;
}
if (has_material && cursor + 48 <= data_end) {
float mat_data[11];
memcpy(mat_data, base + cursor, 44);
cursor += 44;
uint32_t tex_id = 0;
memcpy(&tex_id, base + cursor, 4);
cursor += 4;
Color albedo(mat_data[0], mat_data[1], mat_data[2], mat_data[3]);
float roughness_val = mat_data[4];
float metallic_val = mat_data[5];
// StandardMaterial3D with Blender PBR properties
Ref<StandardMaterial3D> std_mat;
std_mat.instantiate();
std_mat->set_albedo(albedo);
std_mat->set_roughness(roughness_val);
std_mat->set_metallic(metallic_val);
Color emission(mat_data[6], mat_data[7], mat_data[8], mat_data[9]);
if (emission.r > 0.0f || emission.g > 0.0f || emission.b > 0.0f) {
std_mat->set_feature(BaseMaterial3D::FEATURE_EMISSION, true);
std_mat->set_emission(emission);
std_mat->set_emission_energy_multiplier(mat_data[10]);
}
// Apply image texture if available
if (tex_id > 0 && _image_textures.has(tex_id)) {
std_mat->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, _image_textures[tex_id]);
}
mesh_inst->set_surface_override_material(0, std_mat);
}
}
} // end else (vert_count > 0)
}
}
if (entry.update_type == UPDATE_DELETE) {
// Remove from tracking before freeing
if (target) {
uint64_t del_id = target->get_instance_id();
_known_instance_ids.erase(del_id);
target->queue_free();
}
_tracked_objects.erase(
std::remove_if(_tracked_objects.begin(), _tracked_objects.end(),
[&](const TrackedObject &t) { return t.object_id == entry.object_id; }),
_tracked_objects.end());
_last_export_node_count--;
}
imported++;
}
if (imported > 0) {
emit_signal("changes_imported", imported);
}
}
// ── Frame processing ──────────────────────────────────────────────────────────
void BlenderBridge::_process(double delta) {
if (!_server_running)
return;
// Poll UDS for new connections and messages
_uds.poll();
// Process incoming UDS messages
while (_uds.has_messages()) {
UdsMessage msg = _uds.next_message();
_handle_uds_message(msg);
}
// Import Blender changes when notified via UDS
if (_import_pending && _shm.valid()) {
_import_pending = false;
Node *root = get_tree() ? get_tree()->get_root() : nullptr;
if (root) {
import_changes(root);
_import_cooldown = 10; // Skip change detection for 10 frames after import
}
}
// Auto-sync: push outgoing Godot changes (skip during import cooldown)
if (_import_cooldown > 0) {
_import_cooldown--;
} else if (_auto_sync && _shm.valid()) {
_detect_and_push_changes();
}
}
// ── UDS message handling ──────────────────────────────────────────────────────
void BlenderBridge::_handle_uds_message(const UdsMessage &msg) {
if (msg.size < 1)
return;
// Messages are simple text-based signaling:
// "hello" — handshake
// "request_texture:name,width,height" — Blender asks Godot to register a texture with SVT
// "texture_dirty:tex_id,tile_x,tile_y,w,h,mip" — texture invalidation
// "tile_ready:tex_id,tile_x,tile_y,mip,shm_offset,size" — tile data ready in shm
// "finalize" — save textures as .png and scene as .tscn
std::string text(reinterpret_cast<const char *>(msg.data.data()), msg.size);
if (text == "updated") {
// Blender flushed changes to region 1 — import on next frame
_import_pending = true;
return;
}
if (text.substr(0, 5) == "hello") {
_last_import_sequence = 0; // Reset for new connection
// Clean up stale from_blender tracked objects (will be re-sent)
_tracked_objects.erase(
std::remove_if(_tracked_objects.begin(), _tracked_objects.end(),
[&](const TrackedObject &t) {
if (t.from_blender) {
_known_instance_ids.erase(t.godot_instance_id);
Object *obj = ObjectDB::get_instance(t.godot_instance_id);
Node *n = Object::cast_to<Node>(obj);
if (n) n->queue_free();
return true;
}
return false;
}),
_tracked_objects.end());
const char *ack = "connected";
_uds.send_to(msg.client_id, ack, (uint32_t)strlen(ack));
// Notify the new client that scene data is already in SHM
if (!_tracked_objects.empty()) {
const char *notify = "scene_exported";
_uds.send_to(msg.client_id, notify, (uint32_t)strlen(notify));
}
return;
}
if (text.substr(0, 16) == "request_texture:") {
// Parse: request_texture:name,width,height
std::string payload = text.substr(16);
std::string tex_name;
int img_w = 0, img_h = 0;
size_t comma1 = payload.find(',');
if (comma1 != std::string::npos) {
tex_name = payload.substr(0, comma1);
size_t comma2 = payload.find(',', comma1 + 1);
if (comma2 != std::string::npos) {
img_w = std::atoi(payload.substr(comma1 + 1, comma2 - comma1 - 1).c_str());
img_h = std::atoi(payload.substr(comma2 + 1).c_str());
}
} else {
tex_name = payload; // backward compat: no dimensions
}
if (!tex_name.empty() && !_virtual_texture_path.is_empty()) {
Node *vt_node = get_node_or_null(_virtual_texture_path);
VirtualTexture *vt = Object::cast_to<VirtualTexture>(vt_node);
if (vt) {
String gd_name = String::utf8(tex_name.c_str(), (int)tex_name.size());
int svt_id = vt->register_texture(gd_name, img_w, img_h);
String reply = "texture_registered:" + gd_name + "," + String::num_int64(svt_id);
CharString reply_utf8 = reply.utf8();
_uds.send_to(msg.client_id, reply_utf8.get_data(), (uint32_t)reply_utf8.length());
}
}
return;
}
if (text.substr(0, 12) == "shm_resized:") {
// Blender grew the SHM: shm_resized:new_size,new_region_size
size_t new_size = 0, new_region_size = 0;
if (sscanf(text.c_str() + 12, "%zu,%zu", &new_size, &new_region_size) == 2) {
if (_shm.resize(new_size)) {
_shm_size = (int)new_size;
_region_size = new_region_size;
_write_base = _shm.ptr();
_read_base = region_base(_shm.ptr(), 1, _region_size);
UtilityFunctions::print("BlenderBridge: SHM resized to ", (int)new_size,
" bytes (region=", (int)new_region_size, ")");
} else {
UtilityFunctions::printerr("BlenderBridge: SHM resize failed!");
}
}
return;
}
if (text == "finalize") {
// Blender requests finalization — save textures + scene
Object *root_obj = ObjectDB::get_instance(_export_root_id);
Node *root = Object::cast_to<Node>(root_obj);
if (root) {
String save_dir = "res://blender_bridge_export/";
Ref<DirAccess> dir = DirAccess::open("res://");
if (dir.is_valid()) {
dir->make_dir_recursive("blender_bridge_export");
}
Error err = finalize_scene(root, save_dir);
const char *reply = (err == OK) ? "finalized" : "finalize_error";
_uds.send_to(msg.client_id, reply, (uint32_t)strlen(reply));
} else {
const char *reply = "finalize_error";
_uds.send_to(msg.client_id, reply, (uint32_t)strlen(reply));
}
return;
}
if (text.substr(0, 14) == "texture_dirty:") {
// Parse: texture_dirty:tex_id,tile_x,tile_y,w,h,mip
int tex_id, tx, ty, w, h, mip;
if (sscanf(text.c_str() + 14, "%d,%d,%d,%d,%d,%d", &tex_id, &tx, &ty, &w, &h, &mip) == 6) {
notify_texture_dirty(tex_id, tx, ty, w, h, mip);
}
return;
}
if (text.substr(0, 11) == "tile_ready:") {
// Parse: tile_ready:tex_id,tile_x,tile_y,mip,shm_offset,size
int tex_id, tx, ty, mip;
uint32_t offset, size;
if (sscanf(text.c_str() + 11, "%d,%d,%d,%d,%u,%u", &tex_id, &tx, &ty, &mip, &offset, &size) == 6) {
// Read tile data from shared memory and upload to VirtualTexture
if (_shm.valid() && !_virtual_texture_path.is_empty()) {
Node *vt_node = get_node_or_null(_virtual_texture_path);
VirtualTexture *vt = Object::cast_to<VirtualTexture>(vt_node);
if (vt) {
uint8_t *base = static_cast<uint8_t *>(_shm.ptr());
PackedByteArray tile_data;
tile_data.resize(size);
memcpy(tile_data.ptrw(), base + offset, size);
vt->upload_tile(tex_id, tx, ty, mip, tile_data);
}
}
}
return;
}
if (text.substr(0, 11) == "image_file:") {
// Parse: image_file:tex_id,w,h,filepath
// Blender saved a full image as raw RGBA8 and sent the path
size_t c1 = text.find(',', 11);
size_t c2 = (c1 != std::string::npos) ? text.find(',', c1 + 1) : std::string::npos;
size_t c3 = (c2 != std::string::npos) ? text.find(',', c2 + 1) : std::string::npos;
if (c3 != std::string::npos) {
int tex_id = std::atoi(text.substr(11, c1 - 11).c_str());
int w = std::atoi(text.substr(c1 + 1, c2 - c1 - 1).c_str());
int h = std::atoi(text.substr(c2 + 1, c3 - c2 - 1).c_str());
std::string filepath = text.substr(c3 + 1);
// Read raw RGBA8 file
String gd_path = String::utf8(filepath.c_str(), (int)filepath.size());
Ref<FileAccess> f = FileAccess::open(gd_path, FileAccess::READ);
if (f.is_valid()) {
int64_t expected = (int64_t)w * h * 4;
PackedByteArray data = f->get_buffer(expected);
if (data.size() == expected) {
Ref<Image> img = Image::create_from_data(w, h, false, Image::FORMAT_RGBA8, data);
Ref<ImageTexture> tex = ImageTexture::create_from_image(img);
_image_textures[tex_id] = tex;
UtilityFunctions::print("BlenderBridge: Loaded image texture id=", tex_id,
" (", w, "x", h, ") from ", gd_path);
}
}
}
return;
}
}
// ── Change detection ──────────────────────────────────────────────────────────
uint64_t BlenderBridge::_hash_transform(const Transform3D &xform) {
// FNV-1a hash of the 12 floats in Basis + Origin
uint64_t hash = 14695981039346656037ULL;
auto hash_float = [&](float f) {
uint32_t bits;
memcpy(&bits, &f, 4);
hash ^= bits;
hash *= 1099511628211ULL;
};
const Basis &b = xform.basis;
hash_float(b[0][0]);
hash_float(b[0][1]);
hash_float(b[0][2]);
hash_float(b[1][0]);
hash_float(b[1][1]);
hash_float(b[1][2]);
hash_float(b[2][0]);
hash_float(b[2][1]);
hash_float(b[2][2]);
hash_float(xform.origin.x);
hash_float(xform.origin.y);
hash_float(xform.origin.z);
return hash;
}
void BlenderBridge::_detect_and_push_changes() {
if (!_shm.valid() || !_write_base)
return;
Object *root_obj = ObjectDB::get_instance(_export_root_id);
Node *export_root = Object::cast_to<Node>(root_obj);
if (!export_root)
return;
uint8_t *base = static_cast<uint8_t *>(_write_base);
uint32_t blob_start = (uint32_t)data_blob_offset(_max_objects);
uint32_t blob_offset = blob_start;
uint32_t changed_count = 0;
// ── Detect new/removed Godot nodes (incremental, no full re-export) ──
std::vector<Node3D *> current_nodes;
_collect_exportable_nodes(export_root, current_nodes);
std::set<uint64_t> current_ids;
for (auto *n : current_nodes) {
current_ids.insert(n->get_instance_id());
}
// New nodes: in scene but not tracked
for (auto *node : current_nodes) {
uint64_t nid = node->get_instance_id();
if (_known_instance_ids.count(nid) > 0)
continue;
// New Godot node — export as UPDATE_FULL
uint32_t obj_id = _next_object_id++;
ObjectEntry entry = {};
entry.object_id = obj_id;
entry.update_type = UPDATE_FULL;
entry.data_offset = blob_offset;
entry.sub_type = 0;
MeshInstance3D *mesh_inst = Object::cast_to<MeshInstance3D>(node);
Camera3D *camera = Object::cast_to<Camera3D>(node);
Light3D *light = Object::cast_to<Light3D>(node);
bool needs_look_compensation = false;
if (mesh_inst) {
entry.object_type = OBJ_MESH;
} else if (camera) {
entry.object_type = OBJ_CAMERA;
needs_look_compensation = true;
} else if (light) {
entry.object_type = OBJ_LIGHT;
if (Object::cast_to<DirectionalLight3D>(node)) {
entry.sub_type = LIGHT_SUN;
needs_look_compensation = true;
} else if (Object::cast_to<SpotLight3D>(node)) {
entry.sub_type = LIGHT_SPOT;
needs_look_compensation = true;
} else {
entry.sub_type = LIGHT_POINT;
}
} else {
entry.object_type = OBJ_EMPTY;
}
// Write name prefix
_write_name_prefix(node->get_name(), base, blob_offset);
// Write transform
float xform_data[16];
_export_transform(node->get_global_transform(), xform_data);
if (needs_look_compensation) {
float old1[3] = {xform_data[1], xform_data[5], xform_data[9]};
float old2[3] = {xform_data[2], xform_data[6], xform_data[10]};
xform_data[1] = old2[0]; xform_data[5] = old2[1]; xform_data[9] = old2[2];
xform_data[2] = -old1[0]; xform_data[6] = -old1[1]; xform_data[10] = -old1[2];
}
memcpy(base + blob_offset, xform_data, TRANSFORM_SIZE);
blob_offset += TRANSFORM_SIZE;
// Write vertex data for meshes
if (mesh_inst && mesh_inst->get_mesh().is_valid()) {
Ref<Mesh> mesh = mesh_inst->get_mesh();
if (mesh->get_surface_count() > 0) {
Array arrays = mesh->surface_get_arrays(0);
if (arrays.size() > Mesh::ARRAY_VERTEX) {
PackedVector3Array positions = arrays[Mesh::ARRAY_VERTEX];
uint32_t vert_count = positions.size();
memcpy(base + blob_offset, &vert_count, 4);
blob_offset += 4;
const Vector3 *pos_r = positions.ptr();
for (uint32_t v = 0; v < vert_count; v++) {
float in[3] = { pos_r[v].x, pos_r[v].y, pos_r[v].z };
float out[3];
_godot_to_blender_vec3(in, out);
memcpy(base + blob_offset, out, 12);
blob_offset += 12;
}
bool has_normals = arrays.size() > Mesh::ARRAY_NORMAL && arrays[Mesh::ARRAY_NORMAL].get_type() != Variant::NIL;
uint32_t normals_flag = has_normals ? 1 : 0;
memcpy(base + blob_offset, &normals_flag, 4);
blob_offset += 4;
if (has_normals) {
PackedVector3Array normals = arrays[Mesh::ARRAY_NORMAL];
const Vector3 *norm_r = normals.ptr();
for (uint32_t v = 0; v < vert_count; v++) {
float in[3] = { norm_r[v].x, norm_r[v].y, norm_r[v].z };
float out[3];
_godot_to_blender_vec3(in, out);
memcpy(base + blob_offset, out, 12);
blob_offset += 12;
}
}
bool has_indices = arrays.size() > Mesh::ARRAY_INDEX && arrays[Mesh::ARRAY_INDEX].get_type() != Variant::NIL;
uint32_t idx_count = 0;
if (has_indices) {
PackedInt32Array indices = arrays[Mesh::ARRAY_INDEX];
idx_count = indices.size();
}
memcpy(base + blob_offset, &idx_count, 4);
blob_offset += 4;
if (idx_count > 0) {
PackedInt32Array indices = arrays[Mesh::ARRAY_INDEX];
const int32_t *idx_r = indices.ptr();
memcpy(base + blob_offset, idx_r, idx_count * 4);
blob_offset += idx_count * 4;
}
// Write per-triangle smooth flags (0=flat, 1=smooth)
uint32_t tri_count = idx_count / 3;
memcpy(base + blob_offset, &tri_count, 4);
blob_offset += 4;
if (tri_count > 0) {
if (has_normals && has_indices) {
PackedVector3Array norm_arr = arrays[Mesh::ARRAY_NORMAL];
const Vector3 *nr = norm_arr.ptr();
PackedInt32Array idx_arr = arrays[Mesh::ARRAY_INDEX];
const int32_t *ir = idx_arr.ptr();
for (uint32_t t = 0; t < tri_count; t++) {
int32_t i0 = ir[t * 3], i1 = ir[t * 3 + 1], i2 = ir[t * 3 + 2];
Vector3 fn = (pos_r[i1] - pos_r[i0]).cross(pos_r[i2] - pos_r[i0]);
float len_sq = fn.length_squared();
uint8_t is_smooth = 1;
if (len_sq > 1e-12f) {
fn /= Math::sqrt(len_sq);
if (nr[i0].dot(fn) > 0.999f &&
nr[i1].dot(fn) > 0.999f &&
nr[i2].dot(fn) > 0.999f) {
is_smooth = 0;
}
}
base[blob_offset++] = is_smooth;
}
} else {
memset(base + blob_offset, 1, tri_count);
blob_offset += tri_count;
}
}
// Write UV coordinates (float32×2 per vertex)
bool has_uvs = arrays.size() > Mesh::ARRAY_TEX_UV && arrays[Mesh::ARRAY_TEX_UV].get_type() != Variant::NIL;
uint32_t uv_count = 0;
if (has_uvs) {
PackedVector2Array uvs = arrays[Mesh::ARRAY_TEX_UV];
uv_count = uvs.size();
}
memcpy(base + blob_offset, &uv_count, 4);
blob_offset += 4;
if (uv_count > 0) {
PackedVector2Array uvs = arrays[Mesh::ARRAY_TEX_UV];
const Vector2 *uv_r = uvs.ptr();
for (uint32_t v = 0; v < uv_count; v++) {
float uv[2] = { uv_r[v].x, uv_r[v].y };
memcpy(base + blob_offset, uv, 8);
blob_offset += 8;
}
}
}
}
}
// Write material data (only for meshes): 44 mat + 4 tex_id
if (entry.object_type == OBJ_MESH && mesh_inst) {
Ref<Material> mat_ref = mesh_inst->get_surface_override_material(0);
if (!mat_ref.is_valid() && mesh_inst->get_mesh().is_valid() && mesh_inst->get_mesh()->get_surface_count() > 0) {
mat_ref = mesh_inst->get_mesh()->surface_get_material(0);
}
Ref<StandardMaterial3D> std_mat = Object::cast_to<StandardMaterial3D>(mat_ref.ptr());
if (std_mat.is_valid()) {
uint32_t has_material = 1;
memcpy(base + blob_offset, &has_material, 4);
blob_offset += 4;
Color albedo = std_mat->get_albedo();
float mat_data[11];
mat_data[0] = albedo.r; mat_data[1] = albedo.g; mat_data[2] = albedo.b; mat_data[3] = albedo.a;
mat_data[4] = std_mat->get_roughness();
mat_data[5] = std_mat->get_metallic();
Color emission = std_mat->get_emission();
mat_data[6] = emission.r; mat_data[7] = emission.g; mat_data[8] = emission.b; mat_data[9] = emission.a;
mat_data[10] = std_mat->get_emission_energy_multiplier();
memcpy(base + blob_offset, mat_data, 44);
blob_offset += 44;
uint32_t tex_id = 0;
memcpy(base + blob_offset, &tex_id, 4);
blob_offset += 4;
} else {
uint32_t has_material = 0;
memcpy(base + blob_offset, &has_material, 4);
blob_offset += 4;
}
} else if (entry.object_type == OBJ_MESH) {
uint32_t has_material = 0;
memcpy(base + blob_offset, &has_material, 4);
blob_offset += 4;
}
entry.data_size = blob_offset - entry.data_offset;
entry.sequence = (uint32_t)(_export_sequence + 1);
write_object_entry(_write_base, changed_count, entry);
changed_count++;
// Track the new node
TrackedObject tracked;
tracked.object_id = obj_id;
tracked.godot_instance_id = nid;
tracked.name = node->get_name();
tracked.type = (ObjectType)entry.object_type;
tracked.transform_hash = _hash_transform(node->get_global_transform());
tracked.from_blender = false;
_tracked_objects.push_back(tracked);
}
// Removed nodes: tracked but not in scene
for (auto it = _tracked_objects.begin(); it != _tracked_objects.end(); ) {
if (it->from_blender) {
++it;
continue; // Don't delete Blender-origin objects from Godot side
}
if (current_ids.count(it->godot_instance_id) == 0) {
// Node was removed from Godot — send DELETE
ObjectEntry entry = {};
entry.object_id = it->object_id;
entry.object_type = it->type;
entry.update_type = UPDATE_DELETE;
entry.data_offset = 0;
entry.data_size = 0;
entry.sequence = (uint32_t)(_export_sequence + 1);
write_object_entry(_write_base, changed_count, entry);
changed_count++;
_known_instance_ids.erase(it->godot_instance_id);
it = _tracked_objects.erase(it);
} else {
++it;
}
}
// ── Detect transform changes on existing tracked objects ──
for (auto &tracked : _tracked_objects) {
Object *obj = ObjectDB::get_instance(tracked.godot_instance_id);
Node3D *node = Object::cast_to<Node3D>(obj);
if (!node)
continue;
uint64_t new_hash = _hash_transform(node->get_global_transform());
if (new_hash == tracked.transform_hash)
continue;
// Transform changed — write delta to region 0
tracked.transform_hash = new_hash;
ObjectEntry entry = {};
entry.object_id = tracked.object_id;
entry.object_type = tracked.type;
entry.update_type = UPDATE_TRANSFORM;
entry.data_offset = blob_offset;
entry.sequence = (uint32_t)(_export_sequence + 1);
// Write name prefix
_write_name_prefix(tracked.name, base, blob_offset);
float xform_data[16];
_export_transform(node->get_global_transform(), xform_data);
// Camera/directional/spot lights: apply look-direction compensation
bool needs_look_compensation = (tracked.type == OBJ_CAMERA) ||
(tracked.type == OBJ_LIGHT && Object::cast_to<DirectionalLight3D>(node)) ||
(tracked.type == OBJ_LIGHT && Object::cast_to<SpotLight3D>(node));
if (needs_look_compensation) {
float old1[3] = {xform_data[1], xform_data[5], xform_data[9]};
float old2[3] = {xform_data[2], xform_data[6], xform_data[10]};
xform_data[1] = old2[0]; xform_data[5] = old2[1]; xform_data[9] = old2[2];
xform_data[2] = -old1[0]; xform_data[6] = -old1[1]; xform_data[10] = -old1[2];
}
memcpy(base + blob_offset, xform_data, TRANSFORM_SIZE);
blob_offset += TRANSFORM_SIZE;
entry.data_size = blob_offset - entry.data_offset;
write_object_entry(_write_base, changed_count, entry);
changed_count++;
}
// Update _known_instance_ids
_known_instance_ids = current_ids;
_last_export_node_count = (int)current_nodes.size();
if (changed_count > 0) {
_export_sequence++;
BridgeHeader hdr = read_header(_write_base);
hdr.direction = 0;
hdr.sequence = _export_sequence;
hdr.object_count = changed_count;
hdr.frame_number++;
write_header(_write_base, hdr);
// Signal Blender
if (_uds.client_count() > 0) {
const char *msg = "updated";
_uds.broadcast(msg, (uint32_t)strlen(msg));
}
}
}