Skip to content

Instantly share code, notes, and snippets.

@tahatorabpour
Last active November 19, 2025 11:23
Show Gist options
  • Select an option

  • Save tahatorabpour/1fae8aa20fbfac8424723d748698e3c5 to your computer and use it in GitHub Desktop.

Select an option

Save tahatorabpour/1fae8aa20fbfac8424723d748698e3c5 to your computer and use it in GitHub Desktop.
Blender, high performance Multithreaded Exporter (Rift exporter)

Article:

https://lotusspring.substack.com

Disclaimer!

This code was written without the intention of being publicly shared. Not much effort was put into beautification or anything like that, one big file that does it all! Some effort is requried on your part to make this compile.

Python Disclaimer!

I heavily dislike python and consider the code wasteful slop. I have very little python experience, so there are likely much better ways of writing the python portion. Exercise caution!

With that said, I hope you find this helpful!
-Taha

#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <cstdio>
#include <vector>
#include <map>
#include <array>
#include <tuple>
#include <string>
#include <cmath>
#include <assert.h>
#include "BLI_path_utils.hh"
#include "BLI_fileops.h"
#include "BLI_math_vector.h"
#include "BLI_math_matrix.h"
#include "BLI_math_rotation.h"
#include "BLI_string.h"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_bounds_types.hh"
#include "BLI_bounds.hh"
#include "BLI_time.h"
#include "BKE_context.hh"
#include "BKE_main.hh"
#include "BKE_scene.hh"
#include "BKE_object.hh"
#include "BKE_mesh.hh"
#include "BKE_material.hh"
#include "BKE_image.hh"
#include "BKE_node.hh"
#include "BKE_customdata.hh"
#include "BKE_idprop.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_attribute.hh"
#include "BKE_deform.hh"
#include "BKE_mesh_tangent.hh"
#include "BKE_attribute.hh"
#include "BKE_node_legacy_types.hh"
#include "BKE_image_save.hh"
#include "BKE_image_format.hh"
#include "BKE_editmesh.hh"
#include "BKE_modifier.hh"
#include "BKE_mesh_wrapper.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "WM_api.hh"
#include "WM_types.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_object_types.h"
#include "DNA_meshdata_types.h"
#include "DNA_modifier_types.h"
#include "DNA_mesh_types.h"
#include "DNA_material_types.h"
#include "DNA_scene_types.h"
#include "DNA_image_types.h"
#include "bmesh.hh"
#include "bmesh_tools.hh"
#include "meow_hash_x64_aesni.h"
// What?
using blender::StringRefNull;
using namespace blender;
/////////////////////////////////////////////////////////////////////////////////
typedef uint32_t u32;
typedef uint64_t u64;
typedef char u8;
#define int int32_t
typedef struct Filedata {
u32 size;
char *data;
} Filedata;
typedef struct t_string {
const char *data;
u32 length;
} t_string;
#define T_STR(a) {a, sizeof(a) - 1}
#define T_STRING(a) (t_string) T_STR(a)
#define ARRAY_SIZE(a) sizeof(a) / sizeof(a[0])
#define ARRAY(type, name) \
struct { \
type *data; \
uint count; \
} name \
#define ARRAY_ADD(array, element) \
(array.data[array.count++] = element, array.count - 1) \
// @Note: It's better to have the vertex data separated and not interleaved!
struct Vertex {
float pos[3];
float norm[3];
float uv[2];
float tan[4];
};
struct TextureData {
int type;
t_string name;
};
struct TextureHashItem {
TextureData data;
u32 hash;
};
typedef enum MaterialTextureIds {
TEXTURE_DIFFUSE = 0,
TEXTURE_NORMALMAP,
MAX_TEXTURES_COUNT,
} MaterialTextureIds;
typedef struct vec3 {
float x, y, z;
} vec3;
struct PrimitiveMaterial {
t_string name;
float color[3];
t_string shader;
bool tri_planar;
ARRAY(TextureData, textures);
int flags;
};
struct SceneMaterial {
PrimitiveMaterial pMat;
Material *mat;
};
struct Primitive {
ARRAY(Vertex, vertices);
ARRAY(u32, indices);
u32 materialIdx;
};
struct Entity {
t_string name;
ARRAY(u32, children);
float translation[3];
float rotation[4];
float scale[3];
int type;
int flags;
t_string asset;
ARRAY(Primitive, primitives);
vec3 boundingBoxVertices[8];
float breaking_force;
float fog_falloff;
float fog_color[3];
float fade_begin;
float fade_end;
t_string sky_shader;
float sun_intensity;
float sun_color[3];
float ambient_intensity;
float ambient_color[3];
int light_enabled;
float light_color[3];
float light_intensity;
float light_falloff;
t_string portal_level;
t_string portal_target_entity;
int gravityswitch_enabled;
int platform_target_entity;
int platform_target_goal;
int platform_looping;
float platform_timer;
int diamond_difficulty;
ARRAY(u32, button_target_entities);
float cable_target_color[3];
int movementSwitch_mode;
};
typedef enum EntityFlags {
ENTITY_FLAG_NONE = 0,
ENTITY_INVISIBLE = (1 << 0),
ENTITY_NO_COLLISION = (1 << 1),
ENTITY_TRIGGER = (1 << 2),
ENTITY_DEV = (1 << 3),
ENTITY_MATERIAL_OVERRIDE = (1 << 4),
ENTITY_BREAKABLE = (1 << 6),
} EntityFlags;
t_string entity_type_strings[] = {
T_STR("None"),
T_STR("Sun"),
T_STR("Light"),
T_STR("Portal"),
T_STR("Cloud"),
T_STR("GravitySwitch"),
T_STR("Platform"),
T_STR("Button"),
T_STR("PlayerStart"),
T_STR("Diamond"),
T_STR("LampPost"),
T_STR("Cable"),
T_STR("MovementSwitch"),
};
typedef enum EntityType {
ENTITY_NONE = 0,
ENTITY_SUN,
ENTITY_LIGHT,
ENTITY_PORTAL,
ENTITY_CLOUD,
ENTITY_GRAVITYSWITCH,
ENTITY_PLATFORM,
ENTITY_BUTTON,
ENTITY_PLAYERSTART,
ENTITY_DIAMOND,
ENTITY_LAMPPOST,
ENTITY_CABLE,
ENTITY_MOVEMENTSWITCH,
ENTITY_TYPES_COUNT,
} EntityType;
void _serialize(char *fileData, u32 *fileDataSize, void *data, u32 size, u32 count) {
memcpy(fileData + *fileDataSize, data, size * count);
*fileDataSize = *fileDataSize + size * count;
}
#define serialize(data, size, count) _serialize(fileData, &fileDataSize, &data, size, count)
#define serialize_int(data, count) _serialize(fileData, &fileDataSize, &data, sizeof(int), count)
#define serialize_float(data, count) _serialize(fileData, &fileDataSize, &data, sizeof(float), count)
#define serialize_string(_data) \
_serialize(fileData, &fileDataSize, &_data.length, sizeof(u32), 1); \
_serialize(fileData, &fileDataSize, (char *) _data.data, _data.length, 1) \
#define Bytes(n) (n)
#define Kilobytes(n) (n << 10)
#define Megabytes(n) (n << 20)
#define Gigabytes(n) (((u64)n) << 30)
typedef struct Arena {
u8 *data;
size_t capacity;
size_t size;
} Arena;
Arena arena = {};
static Arena memory_createArena (size_t size) {
Arena arena = {0};
arena.data = (u8 *) MEM_mallocN(size, "Arena");
arena.capacity = size;
memset(arena.data, 0, size);
return arena;
}
#define memory_allocate(arena, type, count) (type *) memory_allocateAligned(arena, sizeof(type) * count, alignof(type))
static void *memory_allocateAligned (Arena *arena, size_t allocationSize, size_t alignment) {
if (alignment == 0) alignment = 1;
// Check that alignment is a power of two:
assert((alignment & (alignment - 1)) == 0);
// When aligning, we will align by at most `alignment - 1` bytes:
size_t max_align_inc = alignment - 1;
// Since `alignment` is a power of two, we want to have zero bits
// whenever we have a one bit in `alignment - 1`. Thus we need to
// mask off the bits in
size_t align_mask = ~(alignment - 1);
char *current = arena->data + arena->size;
char *current_plus_max_alignment = current + max_align_inc;
uintptr_t current_plus_max_alignment_as_uint = (uintptr_t)current_plus_max_alignment;
uintptr_t aligned_current_as_uint = current_plus_max_alignment_as_uint & align_mask;
void *result = (void*)aligned_current_as_uint;
size_t alignedSize = (u8*)result - arena->data;
size_t newSize = alignedSize + allocationSize;
assert(newSize < arena->capacity);
arena->size = newSize;
return result;
}
static t_string t_stringCreate (Arena *arena, char *src, u32 length) {
t_string result = {};
result.data = memory_allocate(arena, char, length);
result.length = length;
strncpy((char *) result.data, src, length);
return result;
}
static int t_stringCompare (t_string str1, t_string str2) {
uint i;
if (str1.length != str2.length)
return 0;
for (i = 0; i < str1.length; i++) {
if (str1.data[i] != str2.data[i])
return 0;
}
return 1; // Strings are equal
}
u64 getFileLastWriteTime (char *filename) {
FILETIME time = {0};
u64 result = 0;
WIN32_FILE_ATTRIBUTE_DATA data;
if (GetFileAttributesExA(filename, GetFileExInfoStandard, &data)) {
time = data.ftLastWriteTime;
result = ((u64)time.dwHighDateTime << 32) | time.dwLowDateTime;
}
return result;
}
Filedata readEntireFile (Arena *arena, char *filename) {
Filedata result = {0};
HANDLE hFile = CreateFileA(
filename,
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
0,
NULL
);
if (hFile != INVALID_HANDLE_VALUE) {
result.size = GetFileSize(hFile, NULL);
result.data = memory_allocate(arena, char, result.size);
DWORD bytesRead;
ReadFile(hFile, result.data, result.size, &bytesRead, NULL);
CloseHandle(hFile);
assert(bytesRead == result.size);
}
return result;
}
int areFilesDifferent (Arena *arena, char *filepath1, char *filepath2) {
int result = 1;
size_t marker = arena->size;
HANDLE hFile1 = CreateFileA(filepath1, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL);
HANDLE hFile2 = CreateFileA(filepath2, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL);
if (hFile1 != INVALID_HANDLE_VALUE && hFile2 != INVALID_HANDLE_VALUE) {
Filedata data1 = {};
Filedata data2 = {};
data1.size = GetFileSize(hFile1, NULL);
data2.size = GetFileSize(hFile2, NULL);
if (data1.size == data2.size) {
data1.data = memory_allocate(arena, char, data1.size);
data2.data = memory_allocate(arena, char, data2.size);
DWORD bytesRead1, bytesRead2;
ReadFile(hFile1, data1.data, data1.size, &bytesRead1, NULL);
ReadFile(hFile2, data2.data, data2.size, &bytesRead2, NULL);
__m128i hash1 = MeowHash(MeowDefaultSeed, data1.size, data1.data);
__m128i hash2 = MeowHash(MeowDefaultSeed, data2.size, data2.data);
if (MeowHashesAreEqual(hash1, hash2)) {
result = 0;
}
CloseHandle(hFile1);
CloseHandle(hFile2);
}
}
arena->size = marker;
return result;
}
static u32 t_strhash (char *str) {
u32 hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c;
}
return hash;
}
//////////////////////////////////////////////////////////////////////////////////////
void get_custom_float (const ID* id, float *target, const char* name) {
if (id->properties) {
IDProperty *prop = IDP_GetPropertyFromGroup(id->properties, name);
if (prop) {
*target = IDP_Float(prop);
}
}
}
void get_custom_float3 (const ID* id, float *data, const char* name) {
if (id->properties) {
IDProperty* prop = IDP_GetPropertyFromGroup(id->properties, name);
if (prop && prop->type == IDP_ARRAY && prop->subtype == IDP_FLOAT) {
float *value = (float *) IDP_Array(prop);
memcpy(data, value, sizeof(float) * 3);
}
}
}
void get_custom_int (const ID* id, int *target, const char* name) {
if (id->properties) {
IDProperty *prop = IDP_GetPropertyFromGroup(id->properties, name);
if (prop) {
*target = IDP_Int(prop);
}
}
}
void get_custom_bool (const ID* id, bool *target, const char* name) {
if (id->properties) {
IDProperty *prop = IDP_GetPropertyFromGroup(id->properties, name);
if (prop) {
*target = IDP_Bool(prop);
}
}
}
t_string get_custom_string (Arena *arena, const ID *id, const char *name) {
t_string result = {};
if (id->properties) {
IDProperty *prop = IDP_GetPropertyFromGroup(id->properties, name);
if (prop) {
char *str = IDP_String(prop);
if (str) {
u32 length = strlen(str);
result = t_stringCreate(arena, str, length);
}
}
}
return result;
}
Object *get_custom_object (const ID *id, const char *name) {
Object *result = nullptr;
if (id->properties) {
IDProperty *prop = IDP_GetPropertyFromGroup(id->properties, name);
if (prop && prop->type == IDP_ID) {
ID *target_id = (ID *) prop->data.pointer;
if (target_id && target_id->name[0]) {
result = (Object *) target_id;
}
}
}
return result;
}
u32 findEntityIndex (Object *object, Object **objectsArray, u32 objectsArrayCount) {
u32 result = 0;
for (u32 i = 0; i < objectsArrayCount; i++) {
if (object == objectsArray[i]) {
result = i + 1; // +1 Because of the root entity
break;
}
}
return result;
}
//////////////////////////////////////////////////////////////////////////////////////
void rift_process_object (Arena *arena, Depsgraph *depsgraph, Object **allObjects, u32 allObjectsCount, Object *obj, Entity *target_entity, SceneMaterial *sceneMaterials, u32 sceneMaterialsCount);
void rift_save_png (Arena *arena, char *source, char *dest);
void rift_export_image (Main *bmain, Scene *scene, Image *image, char *source, char *dest);
//////////////////////////////////////////////////////////////////////////////////////
#define MAX_THREADS 16
#define MAX_WORK_ITEMS 4096
enum WorkType {
WORK_process_object,
WORK_export_image,
WORK_copy_png,
};
struct WorkItem {
WorkType type;
// Object
Object *object;
Entity *target_entity;
// Image export
char from_path[FILE_MAX];
char dest_path[FILE_MAX];
Image *image;
};
struct WorkQueue {
WorkItem items[MAX_WORK_ITEMS];
volatile LONG head;
volatile LONG tail;
volatile LONG remaining;
};
struct ThreadData {
int thread_id;
HANDLE wake_event;
Arena * arena;
Depsgraph * depsgraph;
Main * bmain;
Scene * scene;
Object ** allObjects;
u32 allObjectsCount;
SceneMaterial * sceneMaterials;
u32 sceneMaterialsCount;
Entity * entitiesArray;
};
Arena thread_arenas[MAX_THREADS] = {};
HANDLE worker_threads[MAX_THREADS] = {};
ThreadData threads_data[MAX_THREADS] = {};
WorkQueue work_queue = {};
int work_queue_push (WorkItem item) {
while (1) {
LONG current_tail = work_queue.tail;
LONG next_tail = (current_tail + 1) % MAX_WORK_ITEMS;
LONG current_head = work_queue.head;
if (next_tail == current_head) {
return 0;
}
if (InterlockedCompareExchange(&work_queue.tail, next_tail, current_tail) == current_tail) {
work_queue.items[current_tail] = item;
InterlockedIncrement(&work_queue.remaining);
return 1;
}
}
}
int work_queue_pop (WorkItem *item) {
while (1) {
LONG current_head = work_queue.head;
LONG current_tail = work_queue.tail;
if (current_head == current_tail) {
return 0;
}
*item = work_queue.items[current_head];
LONG next_head = (current_head + 1) % MAX_WORK_ITEMS;
if (InterlockedCompareExchange(&work_queue.head, next_head, current_head) == current_head) {
return 1;
}
}
}
DWORD WINAPI rift_worker_thread (void *data) {
ThreadData *thread_data = (ThreadData *) data;
while (1) {
// @Note: This will wait only for the first time the exporter in invoked.
// I never put the thread back to sleep, which means it's always rapidly
// checking for new jobs. This, in theory is a bad thing to do, but I don't
// have to care or worry about it. And it seems the Windows is doing some
// intelligent behind the scenes and is lot letting the CPU melt away. So
// your milage may vary here, based on your CPU and OS setup.
WaitForSingleObject(thread_data->wake_event, INFINITE);
WorkItem work_item;
while (work_queue_pop(&work_item)) {
switch (work_item.type) {
case WORK_process_object: {
rift_process_object(
thread_data->arena,
thread_data->depsgraph,
thread_data->allObjects,
thread_data->allObjectsCount,
work_item.object,
work_item.target_entity,
thread_data->sceneMaterials,
thread_data->sceneMaterialsCount
);
} break;
case WORK_copy_png: {
rift_save_png(
thread_data->arena,
work_item.from_path,
work_item.dest_path
);
} break;
case WORK_export_image: {
rift_export_image(
thread_data->bmain,
thread_data->scene,
work_item.image,
work_item.from_path,
work_item.dest_path
);
} break;
}
InterlockedDecrement(&work_queue.remaining);
}
}
return 0;
}
//////////////////////////////////////////////////////////////////////////////////////
void rift_export_image (Main *bmain, Scene *scene, Image *image, char *source, char *dest) {
int save_image = 0;
u64 source_time = getFileLastWriteTime(source);
u64 dest_time = getFileLastWriteTime(dest);
if (!BLI_exists(dest)) save_image = 1;
else if (source_time > dest_time) save_image = 1;
else if (BKE_image_is_dirty(image)) save_image = 1;
if (save_image) {
ImageSaveOptions opts;
BKE_image_save_options_init(&opts, bmain, scene, image, NULL, false, true);
opts.im_format.imtype = R_IMF_IMTYPE_PNG;
opts.im_format.compress = 0;
opts.im_format.depth = R_IMF_CHAN_DEPTH_8;
BLI_strncpy(opts.filepath, dest, sizeof(opts.filepath));
BKE_image_save(NULL, bmain, image, NULL, &opts);
BKE_image_save_options_free(&opts);
}
}
void rift_save_png (Arena *arena, char *source, char *dest) {
if (areFilesDifferent(arena, source, dest)) {
CopyFileA(source, dest, FALSE);
}
}
void rift_process_material (Arena *arena, Material *material, SceneMaterial *sceneMaterial, TextureHashItem *textureHashes, u32 *textureHashesCount, char *fileBlendDir, char *texturesDir) {
char *material_name = material->id.name + 2;
SceneMaterial sceneMat = {};
sceneMat.mat = material;
sceneMat.pMat.color[0] = 1.0f;
sceneMat.pMat.color[1] = 1.0f;
sceneMat.pMat.color[2] = 1.0f;
sceneMat.pMat.shader = T_STR("default");
sceneMat.pMat.tri_planar = true;
sceneMat.pMat.textures.data = memory_allocate(arena, TextureData, MAX_TEXTURES_COUNT);
sceneMat.pMat.name = t_stringCreate(arena, material_name, strlen(material_name));
if (material->use_nodes && material->nodetree) {
LISTBASE_FOREACH(bNode *, node, &material->nodetree->nodes) {
if (node->type_legacy == SH_NODE_BSDF_PRINCIPLED) {
LISTBASE_FOREACH(bNodeSocket *, input, &node->inputs) {
if (STREQ(input->name, "Base Color")) {
bNodeSocketValueRGBA *color_val = (bNodeSocketValueRGBA *) input->default_value;
if (color_val) {
sceneMat.pMat.color[0] = color_val->value[0];
sceneMat.pMat.color[1] = color_val->value[1];
sceneMat.pMat.color[2] = color_val->value[2];
}
break;
}
}
break;
}
}
LISTBASE_FOREACH(bNode*, node, &material->nodetree->nodes) {
if (node->type_legacy == SH_NODE_TEX_IMAGE) {
Image *image = (Image *) node->id;
if (!image) continue;
int texture_type = 0;
LISTBASE_FOREACH(bNodeLink*, link, &material->nodetree->links) {
if (link->fromnode == node && link->tonode->type_legacy == SH_NODE_BSDF_PRINCIPLED) {
if (STREQ(link->tosock->name, "Normal")) {
texture_type = 1;
break;
}
}
}
char abs_path[FILE_MAX] = {};
char dest_path[FILE_MAX] = {};
if (strlen(image->filepath)) {
if (BLI_path_is_rel(image->filepath)) {
BLI_path_join(abs_path, FILE_MAX, fileBlendDir, image->filepath + 2);
}
else {
BLI_strncpy(abs_path, image->filepath, FILE_MAX);
}
}
else {
BLI_strncpy(abs_path, image->id.name + 2, FILE_MAX);
}
const char *basename = BLI_path_basename(abs_path);
const char *ext = BLI_path_extension(abs_path);
u32 hash = t_strhash(abs_path);
int found = 0;
for (int t = 0; t < *textureHashesCount; t++) {
TextureHashItem *item = &textureHashes[t];
if (item->hash == hash) {
found = 1;
TextureData tex_data = item->data;
tex_data.type = texture_type;
ARRAY_ADD(sceneMat.pMat.textures, tex_data);
break;
}
}
if (!found) {
TextureHashItem item = {};
item.hash = hash;
item.data.type = texture_type;
// Copy the PNG, or export the image as PNG.
if (ext && BLI_strcasecmp(ext, ".png") == 0) {
item.data.name = t_stringCreate(arena, (char *) basename, strlen(basename));
BLI_path_join(dest_path, FILE_MAX, texturesDir, basename);
WorkItem item = {};
item.type = WORK_copy_png;
BLI_strncpy(item.from_path, abs_path, FILE_MAX);
BLI_strncpy(item.dest_path, dest_path, FILE_MAX);
work_queue_push(item);
}
else {
char final_tex_name[FILE_MAX] = {};
char basename_copy[FILE_MAX] = {};
BLI_strncpy(basename_copy, basename, FILE_MAX);
BLI_path_extension_strip((char *) basename_copy);
BLI_snprintf(final_tex_name, FILE_MAX, "%s.png", basename_copy);
item.data.name = t_stringCreate(arena, final_tex_name, strlen(final_tex_name));
BLI_path_join(dest_path, FILE_MAX, texturesDir, final_tex_name);
WorkItem item = {};
item.type = WORK_export_image;
item.image = image;
BLI_strncpy(item.from_path, abs_path, FILE_MAX);
BLI_strncpy(item.dest_path, dest_path, FILE_MAX);
work_queue_push(item);
}
textureHashes[*textureHashesCount] = item;
*textureHashesCount = *textureHashesCount + 1;
ARRAY_ADD(sceneMat.pMat.textures, item.data);
}
}
}
if (material->id.properties) {
IDProperty *prop = IDP_GetPropertyFromGroup(material->id.properties, "tri_planar");
if (prop) {
sceneMat.pMat.tri_planar = IDP_Bool(prop);
}
}
}
*sceneMaterial = sceneMat;
}
void rift_process_object (Arena *arena, Depsgraph *depsgraph, Object **allObjects, u32 allObjectsCount, Object *obj, Entity *target_entity, SceneMaterial *sceneMaterials, u32 sceneMaterialsCount) {
Entity ent = {};
char *name = obj->id.name + 2;
ent.name = t_stringCreate(arena, name, strlen(name));
// Process the children indices
size_t arena_marker = arena->size;
ent.children.data = memory_allocate(arena, u32, allObjectsCount);
for (int i = 0; i < allObjectsCount; i++) {
Object *child = allObjects[i];
if (child->parent == obj) {
u32 idx = i + 1;
ARRAY_ADD(ent.children, idx);
}
}
arena->size = arena_marker;
ent.children.data = memory_allocate(arena, u32, ent.children.count);
// Transform to engine coords
float local_mat[4][4];
copy_m4_m4(local_mat, obj->object_to_world().ptr());
if (obj->parent) {
float parent_inv[4][4];
invert_m4_m4(parent_inv, obj->parent->object_to_world().ptr());
mul_m4_m4m4(local_mat, parent_inv, obj->object_to_world().ptr());
}
float translation[3], rotationMatrix[3][3], scale[3];
mat4_to_loc_rot_size(translation, rotationMatrix, scale, local_mat);
float quat[4];
mat3_to_quat(quat, rotationMatrix);
normalize_qt(quat);
ent.translation[0] = translation[0];
ent.translation[1] = translation[2];
ent.translation[2] = -translation[1];
ent.rotation[0] = quat[1]; // x
ent.rotation[1] = quat[3]; // z
ent.rotation[2] = -quat[2]; // -y
ent.rotation[3] = quat[0]; // w
ent.scale[0] = scale[0];
ent.scale[1] = scale[2];
ent.scale[2] = scale[1];
// Types and flags
t_string type_name = get_custom_string(arena, &obj->id, "entity_type");
if (type_name.length) {
for (int i = 0; i < ARRAY_SIZE(entity_type_strings); i++) {
if (t_stringCompare(type_name, entity_type_strings[i])) {
ent.type = i;
break;
}
}
}
bool invisible = 0, no_collision = 0, trigger_zone = 0, developer = 0, breakable = 0;
get_custom_bool(&obj->id, &invisible, "invisible");
get_custom_bool(&obj->id, &no_collision, "no_collision");
get_custom_bool(&obj->id, &trigger_zone, "trigger_zone");
get_custom_bool(&obj->id, &developer, "developer");
get_custom_bool(&obj->id, &breakable, "breakable");
if (invisible) ent.flags |= ENTITY_INVISIBLE;
if (no_collision) ent.flags |= ENTITY_NO_COLLISION;
if (trigger_zone) ent.flags |= ENTITY_TRIGGER;
if (developer) ent.flags |= ENTITY_DEV;
if (breakable) ent.flags |= ENTITY_BREAKABLE;
ent.asset = get_custom_string(arena, &obj->id, "asset_name");
get_custom_float(&obj->id, &ent.breaking_force, "breaking_force");
if (obj->type == OB_MESH) {
Object *obj_eval = DEG_get_evaluated(depsgraph, obj);
Mesh *mesh = BKE_object_get_evaluated_mesh(obj_eval);
if (mesh) { // Kind of janky.
BKE_mesh_wrapper_ensure_mdata(mesh);
}
if (mesh && mesh->verts_num) {
float min[3] = {FLT_MAX, FLT_MAX, FLT_MAX};
float max[3] = {-FLT_MAX, -FLT_MAX, -FLT_MAX};
Span <float3> temp_vert_positions = mesh->vert_positions();
if (temp_vert_positions.data()) {
// @Note: Redundant loop. Can be processed in the vertex processing loop.
for (float3 pos : temp_vert_positions) {
minmax_v3v3_v3(min, max, pos);
}
ent.boundingBoxVertices[0] = {min[0], min[2], -min[1]};
ent.boundingBoxVertices[1] = {min[0], max[2], -min[1]};
ent.boundingBoxVertices[2] = {min[0], max[2], -max[1]};
ent.boundingBoxVertices[3] = {min[0], min[2], -max[1]};
ent.boundingBoxVertices[4] = {max[0], min[2], -min[1]};
ent.boundingBoxVertices[5] = {max[0], max[2], -min[1]};
ent.boundingBoxVertices[6] = {max[0], max[2], -max[1]};
ent.boundingBoxVertices[7] = {max[0], min[2], -max[1]};
}
bool needs_tri = false;
OffsetIndices <int> faces = mesh->faces();
for (int i : faces.index_range()) {
if (faces[i].size() != 3) {
needs_tri = true;
break;
}
}
if (needs_tri) {
BMeshCreateParams bm_create_params = {false};
BMeshFromMeshParams bm_convert_params = {};
bm_convert_params.calc_face_normal = true;
bm_convert_params.calc_vert_normal = true;
BMesh *bmesh = BKE_mesh_to_bmesh_ex(mesh, &bm_create_params, &bm_convert_params);
BM_mesh_triangulate(
bmesh,
MOD_TRIANGULATE_NGON_BEAUTY,
MOD_TRIANGULATE_QUAD_SHORTEDGE,
4,
false,
nullptr, nullptr, nullptr
);
Mesh *triangulated = BKE_mesh_from_bmesh_for_eval_nomain(bmesh, nullptr, mesh);
BM_mesh_free(bmesh);
mesh = triangulated;
}
char uv_name[MAX_CUSTOMDATA_LAYER_NAME] = {};
if (CustomData_number_of_layers(&mesh->corner_data, CD_PROP_FLOAT2) > 0) {
int active_uv = CustomData_get_active_layer_index(&mesh->corner_data, CD_PROP_FLOAT2);
BLI_strncpy(uv_name, mesh->corner_data.layers[active_uv].name, MAX_CUSTOMDATA_LAYER_NAME);
}
else {
CustomData_add_layer_named(&mesh->corner_data, CD_PROP_FLOAT2, CD_SET_DEFAULT, mesh->corners_num, "DefaultUVMap");
BLI_strncpy(uv_name, "DefaultUVMap", MAX_CUSTOMDATA_LAYER_NAME);
}
float (* uv_layer)[2] = (float (*)[2]) CustomData_get_layer_named(&mesh->corner_data, CD_PROP_FLOAT2, "DefaultUVMap");
if (!uv_layer) {
uv_layer = (float (*)[2]) CustomData_get_layer(&mesh->corner_data, CD_PROP_FLOAT2);
}
float(*loop_tangents)[4];
if (CustomData_has_layer(&mesh->corner_data, CD_MLOOPTANGENT)) {
loop_tangents = (float(*)[4]) CustomData_get_layer_for_write(&mesh->corner_data, CD_MLOOPTANGENT, mesh->corners_num);
memset(loop_tangents, 0, sizeof(float[4]) * mesh->corners_num);
}
else {
loop_tangents = (float(*)[4]) (CustomData_add_layer(&mesh->corner_data, CD_MLOOPTANGENT, CD_SET_DEFAULT, mesh->corners_num));
CustomData_set_layer_flag(&mesh->corner_data, CD_MLOOPTANGENT, CD_FLAG_TEMPORARY);
}
if (!loop_tangents) {
__debugbreak();
}
BKE_mesh_calc_loop_tangent_single(mesh, uv_name, loop_tangents, nullptr);
Span <float3> vert_positions = mesh->vert_positions();
Span <int> corner_verts = mesh->corner_verts();
Span <float3> corner_normals = mesh->corner_normals();
Span <int3> corner_tris = mesh->corner_tris();
const int * corner_tri_faces = mesh->corner_tri_faces().data();
bke::AttributeAccessor attributes = mesh->attributes();
VArray <int> material_indices = *attributes.lookup_or_default <int> ("material_index", bke::AttrDomain::Face, 0);
typedef struct MaterialStats {
int tri_count;
int estimated_vertex_count;
} MaterialStats;
ent.primitives.data = memory_allocate(arena, Primitive, obj_eval->totcol);
MaterialStats *mat_stats = memory_allocate(arena, MaterialStats, obj_eval->totcol);
memset(mat_stats, 0, sizeof(MaterialStats) * obj_eval->totcol);
// Single pass to count
for (int tri_idx = 0; tri_idx < corner_tris.size(); tri_idx++) {
int face_idx = corner_tri_faces[tri_idx];
int mat_idx = material_indices[face_idx];
if (mat_idx >= 0 && mat_idx < obj_eval->totcol) {
mat_stats[mat_idx].tri_count++;
mat_stats[mat_idx].estimated_vertex_count += 3; // Conservative estimate
}
}
// @Note: The following loop will create duplicate vertices. So a cube that can be
// represented in 24 vertices and 36 indices will end up having 36 vertices. I didn't
// bother to de-duplicate the vertices, but you would very likely want to actually do it!
// @Note: There is likely a much better way of doing the loop of sad here... proceed
// with caution!
for (int mat_idx = 0; mat_idx < obj_eval->totcol; mat_idx++) {
Material *material = BKE_object_material_get(obj_eval, mat_idx + 1);
if (!material) continue;
// Skip materials with no triangles
if (mat_stats[mat_idx].tri_count == 0) continue;
Primitive primitive = {};
int index_capacity = mat_stats[mat_idx].tri_count * 3;
int vertex_capacity = mat_stats[mat_idx].estimated_vertex_count;
primitive.vertices.data = memory_allocate(arena, Vertex, vertex_capacity);
primitive.indices.data = memory_allocate(arena, u32, index_capacity);
for (int tri_idx = 0; tri_idx < corner_tris.size(); tri_idx++) {
int3 tri = corner_tris[tri_idx];
int face_idx = corner_tri_faces[tri_idx];
if (material_indices[face_idx] != mat_idx) continue;
for (int j = 0; j < 3; j++) {
int loop_idx = tri[j];
int vert_idx = corner_verts[loop_idx];
float pos_engine[3] = {
vert_positions[vert_idx].x,
vert_positions[vert_idx].z,
-vert_positions[vert_idx].y
};
float norm_engine[3] = {
corner_normals[loop_idx].x,
corner_normals[loop_idx].z,
-corner_normals[loop_idx].y
};
normalize_v3(norm_engine);
float uv[2] = {0.0f, 0.0f};
if (uv_layer) {
uv[0] = uv_layer[loop_idx][0];
uv[1] = 1.0f - uv_layer[loop_idx][1];
}
float4 tan = loop_tangents[loop_idx];
float tan_engine[3] = {tan.x, tan.z, -tan.y};
float bitan_engine[3];
cross_v3_v3v3(bitan_engine, norm_engine, tan_engine);
float tan_w[3] = {tan.w, tan.w, tan.w};
float w = (dot_v3v3(bitan_engine, tan_w) > 0.0f) ? 1.0f : -1.0f;
int found_vertex_idx = -1;
if (found_vertex_idx == -1) {
// Should never happen
if (primitive.vertices.count >= vertex_capacity) {
__debugbreak();
}
Vertex *vertex = &primitive.vertices.data[primitive.vertices.count];
copy_v3_v3(vertex->pos, pos_engine);
copy_v3_v3(vertex->norm, norm_engine);
copy_v2_v2(vertex->uv, uv);
copy_v3_v3(vertex->tan, tan_engine);
vertex->tan[3] = w;
found_vertex_idx = primitive.vertices.count;
primitive.vertices.count++;
}
if (primitive.indices.count >= index_capacity) {
// Should never happen
__debugbreak();
}
ARRAY_ADD(primitive.indices, found_vertex_idx);
}
}
if (primitive.vertices.count == 0 || primitive.indices.count == 0) {
// This should never be true now
__debugbreak();
}
char *material_name = material->id.name + 2;
t_string material_name_str = t_stringCreate(arena, material_name, strlen(material_name));
for (int m = 0; m < sceneMaterialsCount; m++) {
SceneMaterial *sceneMat = &sceneMaterials[m];
if (t_stringCompare(material_name_str, sceneMat->pMat.name)) {
primitive.materialIdx = m;
}
}
ARRAY_ADD(ent.primitives, primitive);
}
if (needs_tri) {
BKE_id_free(NULL, mesh);
}
}
}
switch (ent.type) {
case ENTITY_SUN: {
// Default Values, because IDP, doesn't retrieve any values if on default value
ent.sun_color[0] = 1.0f;
ent.sun_color[1] = 1.0f;
ent.sun_color[2] = 1.0f;
ent.sun_intensity = 1.0f;
ent.ambient_color[0] = 1.0f;
ent.ambient_color[1] = 1.0f;
ent.ambient_color[2] = 1.0f;
ent.ambient_intensity = 0.6f;
get_custom_float(&obj->id, &ent.fog_falloff, "fog_falloff");
get_custom_float3(&obj->id, ent.fog_color, "fog_color");
get_custom_float(&obj->id, &ent.fade_begin, "fade_begin");
get_custom_float(&obj->id, &ent.fade_end, "fade_end");
get_custom_float(&obj->id, &ent.sun_intensity, "sun_intensity");
get_custom_float3(&obj->id, ent.sun_color, "sun_color");
get_custom_float(&obj->id, &ent.ambient_intensity, "ambient_intensity");
get_custom_float3(&obj->id, ent.ambient_color, "ambient_color");
ent.sky_shader = get_custom_string(arena, &obj->id, "sky_shader");
} break;
case ENTITY_LIGHT: {
get_custom_int(&obj->id, &ent.light_enabled , "light_enabled");
get_custom_float3(&obj->id, ent.light_color, "light_color");
get_custom_float(&obj->id, &ent.light_intensity, "light_intensity");
get_custom_float(&obj->id, &ent.light_falloff, "light_falloff");
} break;
case ENTITY_PORTAL: {
ent.portal_level = get_custom_string(arena, &obj->id, "portal_level");
ent.portal_target_entity = get_custom_string(arena, &obj->id, "portal_target_entity");
} break;
case ENTITY_GRAVITYSWITCH : {
get_custom_int(&obj->id, &ent.gravityswitch_enabled, "gravityswitch_enabled");
} break;
case ENTITY_MOVEMENTSWITCH: {
get_custom_int(&obj->id, &ent.movementSwitch_mode, "movementSwitch_mode");
} break;
case ENTITY_PLATFORM: {
Object *target_object = get_custom_object(&obj->id, "platform_target_entity");
Object *target_goal = get_custom_object(&obj->id, "platform_target_goal");
if (target_object) {
ent.platform_target_entity = findEntityIndex(target_object, allObjects, allObjectsCount);
}
if (target_goal) {
ent.platform_target_goal = findEntityIndex(target_goal, allObjects, allObjectsCount);
}
get_custom_int(&obj->id, &ent.platform_looping, "platform_looping");
get_custom_float(&obj->id, &ent.platform_timer, "platform_timer");
} break;
case ENTITY_DIAMOND: {
get_custom_int(&obj->id, &ent.diamond_difficulty, "diamond_difficulty");
} break;
case ENTITY_BUTTON: {
size_t arena_size = arena->size;
ent.button_target_entities.data = memory_allocate(arena, u32, 64);
PropertyRNA *collection_prop;
PropertyRNA *obj_prop;
CollectionPropertyIterator iter;
PointerRNA obj_ptr = RNA_id_pointer_create(&obj->id);
collection_prop = RNA_struct_find_property(&obj_ptr, "button_target_entities");
if (collection_prop) {
RNA_PROP_BEGIN(&obj_ptr, itemptr, collection_prop) {
obj_prop = RNA_struct_find_property(&itemptr, "obj");
if (obj_prop) {
PointerRNA target_ptr = RNA_property_pointer_get(&itemptr, obj_prop);
Object *target_obj = (Object *) target_ptr.data;
if (target_obj) {
u32 idx = findEntityIndex(target_obj, allObjects, allObjectsCount);
ARRAY_ADD(ent.button_target_entities, idx);
}
}
} RNA_PROP_END;
}
arena->size = arena_size;
ent.button_target_entities.data = memory_allocate(arena, u32, ent.button_target_entities.count);
} break;
case ENTITY_CABLE: {
// Default Values, because IDP, doesn't retrieve any values if on default value
ent.cable_target_color[0] = 1.0f;
ent.cable_target_color[1] = 1.0f;
ent.cable_target_color[2] = 1.0f;
get_custom_float3(&obj->id, ent.cable_target_color, "cable_target_color");
} break;
}
*target_entity = ent;
}
//
// Exporter Main
//
static void rift_exporter_main(bContext *C, const char *filepath, const char *texturesDir) {
arena.size = 0;
work_queue.head = 0;
work_queue.tail = 0;
work_queue.remaining = 0;
Scene *scene = CTX_data_scene(C);
Main *bmain = CTX_data_main(C);
Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
// This makes it so that blender doesn't change the colorspace of images when exporting them
STRNCPY(scene->view_settings.view_transform, "Standard");
// Collect the scene objects
// @Waste, this does an extra loop over all the objects!
u32 all_objects_capacity = BLI_listbase_count(&bmain->objects) + 1;
ARRAY(Object *, all_objects);
all_objects.data = memory_allocate(&arena, Object *, all_objects_capacity);
all_objects.count = 0;
LISTBASE_FOREACH(Object *, obj, &bmain->objects) {
ARRAY_ADD(all_objects, obj);
}
//
// Process the materials upfront
//
ARRAY(SceneMaterial, sceneMaterials);
ARRAY(TextureHashItem, texture_hashes);
sceneMaterials.data = memory_allocate(&arena, SceneMaterial, 128);
sceneMaterials.count = 0;
texture_hashes.data = memory_allocate(&arena, TextureHashItem, 128);
texture_hashes.count = 0;
// Pre-calculate blend file directory once
char blend_dir[FILE_MAX] = {};
if (strlen(bmain->filepath) > 0) {
BLI_path_split_dir_part(bmain->filepath, blend_dir, FILE_MAX);
}
LISTBASE_FOREACH(Material *, material, &bmain->materials) {
// @Note: This adds jobs to the job queue system
rift_process_material(
&arena,
material,
&sceneMaterials.data[sceneMaterials.count],
texture_hashes.data,
&texture_hashes.count,
blend_dir,
(char *) texturesDir
);
sceneMaterials.count++;
}
// Prepare entities
ARRAY(Entity, entities);
entities.data = memory_allocate(&arena, Entity, (all_objects.count + 1));
entities.count = 0;
Entity root = {};
root.name = T_STR("Root");
root.rotation[3] = 1.0f; // w = 1.0
root.scale[0] = 1.0f;
root.scale[1] = 1.0f;
root.scale[2] = 1.0f;
size_t arena_marker = arena.size;
root.children.data = memory_allocate(&arena, u32, all_objects.count);
u32 idx = 0;
LISTBASE_FOREACH(Object *, obj, &bmain->objects) {
if (!obj->parent) {
ARRAY_ADD(root.children, idx + 1);
}
idx++;
}
arena.size = arena_marker;
root.children.data = memory_allocate(&arena, u32, root.children.count);
ARRAY_ADD(entities, root);
entities.count = all_objects.count + 1;
// Setup the worker threads ... and go!
for (int i = 0; i < MAX_THREADS; i++) {
threads_data[i].arena->size = 0;
threads_data[i].bmain = bmain;
threads_data[i].scene = scene;
threads_data[i].depsgraph = depsgraph;
threads_data[i].allObjects = all_objects.data;
threads_data[i].allObjectsCount = all_objects.count;
threads_data[i].sceneMaterials = sceneMaterials.data;
threads_data[i].sceneMaterialsCount = sceneMaterials.count;
}
for (int i = 0; i < all_objects.count; i++) {
WorkItem item = {};
item.type = WORK_process_object;
item.object = all_objects.data[i];
item.target_entity = &entities.data[i + 1];
work_queue_push(item);
}
for (int i = 0; i < MAX_THREADS; i++) {
SetEvent(threads_data[i].wake_event);
}
// Wait till all threads are donezo
while (1) {
if (work_queue.remaining == 0) break;
}
// Single threaded version:
// for (int i = 0; i < all_objects.count; i++) {
// Object *obj = all_objects.data[i];
// Entity *entity = &entities.data[i + 1];
// rift_process_object(&arena, depsgraph, all_objects.data, all_objects.count, obj, entity, sceneMaterials.data, sceneMaterials.count);
// }
//
// Serialize to disk
//
char *fileData = memory_allocate(&arena, char, Megabytes(500));
u32 fileDataSize = 0;
HANDLE hFile = CreateFileA(filepath, GENERIC_WRITE, 0 , NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile != INVALID_HANDLE_VALUE) {
int lisa = 0x115A;
serialize_int(lisa, 1);
//
// Materials
//
serialize_int(sceneMaterials.count, 1);
for (int i = 0; i < sceneMaterials.count; i++) {
PrimitiveMaterial *pMat = &sceneMaterials.data[i].pMat;
serialize_string(pMat->name);
serialize_float(pMat->color[0], 3);
serialize_int(pMat->tri_planar, 1);
serialize_string(pMat->shader);
serialize_int(pMat->textures.count, 1);
for (int t = 0; t < pMat->textures.count; t++) {
TextureData *tex_data = &pMat->textures.data[t];
serialize_string(tex_data->name);
serialize_int(tex_data->type, 1);
}
serialize_int(pMat->flags, 1);
}
//
// Entities
//
serialize_int(entities.count, 1);
for (int i = 0; i < entities.count; i++) {
Entity *ent = &entities.data[i];
serialize_string(ent->name);
serialize_int(ent->children.count, 1);
serialize_int(ent->children.data[0], ent->children.count);
serialize_float(ent->translation[0], 3);
serialize_float(ent->rotation[0], 4);
serialize_float(ent->scale[0], 3);
serialize_int(ent->type, 1);
serialize_int(ent->flags, 1);
serialize_float(ent->breaking_force, 1);
switch (ent->type) {
case ENTITY_SUN: {
serialize_float(ent->fog_falloff, 1);
serialize_float(ent->fog_color[0], 3);
serialize_float(ent->fade_begin, 1);
serialize_float(ent->fade_end, 1);
serialize_string(ent->sky_shader);
serialize_float(ent->sun_intensity, 1);
serialize_float(ent->sun_color[0], 3);
serialize_float(ent->ambient_intensity, 1);
serialize_float(ent->ambient_color[0], 3);
} break;
case ENTITY_LIGHT: {
serialize_int(ent->light_enabled, 1);
serialize_float(ent->light_color, 3);
serialize_float(ent->light_intensity, 1);
serialize_float(ent->light_falloff, 1);
} break;
case ENTITY_PORTAL: {
serialize_string(ent->portal_level);
serialize_string(ent->portal_target_entity);
} break;
case ENTITY_GRAVITYSWITCH: {
serialize_int(ent->gravityswitch_enabled, 1);
} break;
case ENTITY_MOVEMENTSWITCH: {
serialize_int(ent->movementSwitch_mode, 1);
} break;
case ENTITY_PLATFORM: {
serialize_int(ent->platform_target_entity, 1);
serialize_int(ent->platform_target_goal, 1);
serialize_int(ent->platform_looping, 1);
serialize_float(ent->platform_timer, 1);
} break;
case ENTITY_DIAMOND: {
serialize_int(ent->diamond_difficulty, 1);
} break;
case ENTITY_BUTTON: {
serialize_int(ent->button_target_entities.count, 1);
serialize_int(ent->button_target_entities.data[0], ent->button_target_entities.count);
} break;
case ENTITY_CABLE: {
serialize_float(ent->cable_target_color[0], 3);
} break;
}
serialize_string(ent->asset);
serialize(ent->boundingBoxVertices[0], sizeof(vec3), 8);
serialize_int(ent->primitives.count, 1);
for (int j = 0; j < ent->primitives.count; j++) {
Primitive *prim = &ent->primitives.data[j];
serialize_int(prim->vertices.count, 1);
// @Note: It's better to have the vertex data separated and not interleaved!
for (int v = 0; v < prim->vertices.count; v++) {
Vertex *vert = &prim->vertices.data[v];
serialize_float(vert->pos[0], 3);
serialize_float(vert->norm[0], 3);
serialize_float(vert->uv[0], 2);
serialize_float(vert->tan[0], 4);
}
serialize_int(prim->indices.count, 1);
serialize_int(prim->indices.data[0], prim->indices.count);
serialize_int(prim->materialIdx, 1);
}
}
}
DWORD bytesWritten;
WriteFile(hFile, fileData, fileDataSize, &bytesWritten, NULL);
CloseHandle(hFile);
}
//
// Exporter function, triggered from python
//
static wmOperatorStatus rift_export_exec(bContext *C, wmOperator *op) {
char filepath[FILE_MAX];
char texturesDir[FILE_MAX];
RNA_string_get(op->ptr, "filepath", filepath);
RNA_string_get(op->ptr, "texturesDir", texturesDir);
rift_exporter_main(C, filepath, texturesDir);
return OPERATOR_FINISHED;
}
static wmOperatorStatus rift_export_invoke(bContext *C, wmOperator *op, const wmEvent *) {
WM_event_add_fileselect(C, op);
return OPERATOR_RUNNING_MODAL;
}
//
// Init function, registered on startup
//
extern "C" void EXPORT_SCENE_OT_rift(wmOperatorType * ot) {
arena = memory_createArena(Gigabytes(2));
work_queue.head = 0;
work_queue.tail = 0;
LPCWSTR thread_names[] = {
L"RiftWorker1", L"RiftWorker2", L"RiftWorker3", L"RiftWorker4",
L"RiftWorker5", L"RiftWorker6", L"RiftWorker7", L"RiftWorker8",
L"RiftWorker9", L"RiftWorker10", L"RiftWorker11", L"RiftWorker12",
L"RiftWorker13", L"RiftWorker14", L"RiftWorker15", L"RiftWorker16",
L"RiftWorker17", L"RiftWorker18", L"RiftWorker19", L"RiftWorker20",
L"RiftWorker21", L"RiftWorker22", L"RiftWorker23", L"RiftWorker24",
};
for (int i = 0; i < MAX_THREADS; i++) {
thread_arenas[i] = memory_createArena(Megabytes(200));
threads_data[i].thread_id = i;
threads_data[i].arena = &thread_arenas[i];
threads_data[i].wake_event = CreateEvent(NULL, FALSE, FALSE, NULL);
worker_threads[i] = CreateThread(0, 0, rift_worker_thread, &threads_data[i], 0, 0);
SetThreadDescription(worker_threads[i], thread_names[i]);
}
ot->name = "Export to Rift";
ot->idname = "EXPORT_SCENE_OT_rift";
ot->description = "Export scene to .rift format";
ot->invoke = rift_export_invoke;
ot->exec = rift_export_exec;
ot->poll = WM_operator_winactive;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
RNA_def_string_file_path(ot->srna, "filepath", nullptr, FILE_MAX, "File Path", "Destination .rift file");
RNA_def_string_dir_path(ot->srna, "texturesDir", nullptr, FILE_MAX, "Textures Directory", "Folder to which textures will exported to");
}
import bpy
import os
import bpy
import os
import shutil
import time
import struct
import bmesh
from bpy.props import FloatVectorProperty, IntProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, CollectionProperty
from bpy.types import Operator, UIList, PropertyGroup
from mathutils import Matrix, Quaternion, Vector
from pathlib import Path
# ////////////////////////////////////////////////////////////////////////////////////////////////
# Custom properties
#
def update_entity_type(self, context):
obj = context.active_object
if obj and obj.is_game_entity:
if obj.entity_type == "":
obj.empty_display_type = 'PLAIN_AXES'
obj.empty_display_size = 1.0
elif obj.entity_type == "Sun":
obj.empty_display_type = 'SINGLE_ARROW'
obj.empty_display_size = 10.0
elif obj.entity_type == "Light":
obj.empty_display_type = 'SPHERE'
obj.empty_display_size = 1.0
elif obj.entity_type == "Portal":
obj.empty_display_type = 'SPHERE'
obj.empty_display_size = 10.0
def find_all_entity_types(self, context, edit_text):
items = [
"Sun",
"Light",
"Portal",
"Asset",
"Cloud",
"GravitySwitch",
"Platform",
"Button",
"PlayerStart",
"Diamond",
"LampPost",
"Cable",
"MovementSwitch",
]
if edit_text:
items = [
item for item in items
if edit_text.lower() in item[0].lower()
]
return items
class ButtonTargetItem(PropertyGroup):
obj: PointerProperty(
name="Button Target",
description="Object to be activated when the button is pressed",
type=bpy.types.Object,
poll=lambda self, obj: obj is not None and obj != self.id_data
)
class BUTTON_TARGET_UL_list(UIList):
bl_idname = "BUTTON_TARGET_UL_list"
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if self.layout_type in {'DEFAULT', 'COMPACT'}:
# Display an object picker for each item
row = layout.row(align=True)
row.prop(item, "obj", text="", icon='OBJECT_DATA', emboss=True)
elif self.layout_type == 'GRID':
layout.alignment = 'CENTER'
layout.label(text="", icon='OBJECT_DATA')
# Operator to add a new item to button_target_entities
class BUTTON_TARGET_OT_add(Operator):
bl_idname = "button_target.add"
bl_label = "Add Target"
bl_description = "Add a new target object to the button"
def execute(self, context):
obj = context.active_object
if obj:
obj.button_target_entities.add()
return {'FINISHED'}
return {'CANCELLED'}
# Operator to remove an item from button_target_entities
class BUTTON_TARGET_OT_remove(Operator):
bl_idname = "button_target.remove"
bl_label = "Remove Target"
bl_description = "Remove the selected target object"
index: bpy.props.IntProperty()
def execute(self, context):
obj = context.active_object
if obj and 0 <= self.index < len(obj.button_target_entities):
obj.button_target_entities.remove(self.index)
# Ensure active index stays valid
if obj.active_button_target_index >= len(obj.button_target_entities):
obj.active_button_target_index = max(0, len(obj.button_target_entities) - 1)
return {'FINISHED'}
return {'CANCELLED'}
def find_all_levels(self, context, edit_text):
wm = get_wm()
export_path = wm["rift_export_path"]
items = []
for file in Path(export_path).glob("*.rift"):
items.append(file.stem)
if edit_text:
items = [
item for item in items
if edit_text.lower() in item[0].lower()
]
return items
def register_custom_properties():
bpy.types.Object.is_game_entity = BoolProperty(name="Is Entity", description="Mark object as a game entity", default=False)
#
# Entity flags
#
bpy.types.Object.no_collision = BoolProperty(name="No Collision", description="Entity will not be collidable", default=False)
bpy.types.Object.invisible = BoolProperty(name="Invisible", description="Entity will be invisible", default=False)
bpy.types.Object.developer = BoolProperty(name="Developer", description="Entity will be only get drawn in developer mode in wireframe", default=False)
bpy.types.Object.trigger_zone = BoolProperty(name="Trigger Zone", description="Entity is a trigger zone", default=False)
bpy.types.Object.breakable = BoolProperty(name="Breakable", description="Entity can break", default=False)
bpy.types.Object.breaking_force = FloatProperty(name="Breaking Force", description="Force required to break object", default=0.0, step=1, min=0.0, max=1000.0)
bpy.types.Object.entity_type = StringProperty(name="Entity Type", default="", search=find_all_entity_types, update=update_entity_type, search_options={'SUGGESTION'},)
#
# Sun
#
bpy.types.Object.fog_color = FloatVectorProperty(name="Fog Color", description="RGB color of the fog", subtype='COLOR', default=(1.0, 1.0, 1.0), min=0.0, max=1.0)
bpy.types.Object.fog_falloff = FloatProperty(name="Fog Falloff", description="Fog falloff", default=0.0, step=1, min=0.0, max=1.0)
bpy.types.Object.fade_begin = FloatProperty(name="World Fade begin", description="Vertical Distance at which the world begins to fade", default=50.0, min=0.0, max=1000.0)
bpy.types.Object.fade_end = FloatProperty(name = "World Fade begin", description = "Vertical Distance at which the world begins to fade", default= 100.0, min= 0.0, max= 1000.0)
bpy.types.Object.sky_shader = StringProperty(name = "Sky Shader", description = "Sky shader name", default= "")
bpy.types.Object.sun_intensity = FloatProperty(name = "Sun intensity", description = "Sun light intensity", default = 1.0, min = 0.0, max = 20.0, step = 0.1,)
bpy.types.Object.sun_color = FloatVectorProperty(name = "Sun Color", description = "RGB color of the sun", subtype = 'COLOR', default = (1.0, 1.0, 1.0), min = 0.0, max = 1.0,)
bpy.types.Object.ambient_intensity = FloatProperty(name = "Ambient intensity", description = "Ambient light intensity", default = 0.6, min = 0.0, max = 20.0, step = 0.1,)
bpy.types.Object.ambient_color = FloatVectorProperty(name = "Ambient Color", description = "RGB color of the ambient light", subtype = 'COLOR', default = (1.0, 1.0, 1.0), min = 0.0, max = 1.0,)
#
# Light
#
bpy.types.Object.light_enabled = BoolProperty(name="Light Enabled", description="Whether the light is on or off by default", default=False)
bpy.types.Object.light_color = FloatVectorProperty(name="Light Color", description="RGB color of the light", subtype='COLOR', default=(1.0, 1.0, 1.0), min=0.0, max=1.0)
bpy.types.Object.light_intensity = FloatProperty(name="Light Intensity", description="Intensity of the light", default=1.0, step=1, min=0.0, max=1000.0)
bpy.types.Object.light_falloff = FloatProperty(name="Light Falloff", description="How quickly the light brightness decreases moving from the light center outwards", default=0.02, step=1, min=0.0, max=10.0)
#
# Shared
#
bpy.types.Object.asset_name = StringProperty(name="Asset Name", description="Name of the asset to be referenced from the Museum level", default="")
bpy.types.Object.target_entity = PointerProperty(name="Target Entity", description="The entity this object targets", type=bpy.types.Object, poll=lambda self, obj: obj != self and obj.is_game_entity)
#
# GravitySwitch
#
bpy.types.Object.gravityswitch_enabled = BoolProperty(name="Gravity Switch Enabled", description="Whether the switch is on or off by default", default=False)
#
# Portal
#
bpy.types.Object.portal_level = StringProperty(name="Target Level", description="Destination Level name", default="", search=find_all_levels, search_options={'SUGGESTION'},)
bpy.types.Object.portal_target_entity = StringProperty(name="Target Entity", description="Target entity to teleport to in the target level", default="")
# //@Move
bpy.types.Scene.rift_realtime_export = bpy.props.BoolProperty(name="Realtime Export", description="Automatically export when scene changes", default=False, update=lambda self, context: toggle_realtime_export(context))
#
# MovementSwitch
#
bpy.types.Object.movementSwitch_mode = IntProperty(name = "Movement Switch Mode", description = "0 = Torque. 1 = Force.", default = 0, min = 0, max = 1)
#
# Material
#
bpy.types.Material.tri_planar = BoolProperty(name="Tri-Planar", description="Tri-Planar UV mapping", default=True)
bpy.types.Material.shader_name = StringProperty(name="Shader", description="Shader name", default="default", maxlen=32)
#
# Platform
#
bpy.types.Object.platform_target_entity = PointerProperty(name="Platform Target", description="Platform target mesh/entity that would get moved", type=bpy.types.Object, poll=lambda self, obj: obj != self)
bpy.types.Object.platform_target_goal = PointerProperty(name="Goal", description="Platform move goal", type=bpy.types.Object, poll=lambda self, obj: obj != self and obj != self.platform_target_entity)
bpy.types.Object.platform_looping = BoolProperty(name="Looping", description="The platform animation is looping", default=True)
bpy.types.Object.platform_timer = FloatProperty(name="Timer Delay", description="Timer delay in seconds, after which the animation plays", default=0.0, step=0.1, min=0.0, max=1000.0)
#
# Diamond
#
bpy.types.Object.diamond_difficulty = IntProperty(name = "Difficulty", description = "The difficulty level of obtaining this diamond", default = 0, min = 0, max = 5)
#
# Cable
#
bpy.types.Object.cable_target_color = FloatVectorProperty(name = "Cable Target Color", description = "Color of the Cable, when it is switched on", subtype = 'COLOR', default = (1.0, 1.0, 1.0), min = 0.0, max = 1.0,)
#
# Button
#
bpy.types.Object.button_target_entities = CollectionProperty(type=ButtonTargetItem, name="Button Targets", description="Collection of entities that would would get activated as a result of button activation",)
# //////////////////////////////////////////
# Draw the UI
#
class OBJECT_PT_GameEntityProperties(bpy.types.Panel):
bl_label = "Game Entity Properties"
bl_idname = "OBJECT_PT_game_entity_properties"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
obj = context.active_object
if obj:
box = layout.box()
box.label(text="Entity Flags")
box.prop(obj, "no_collision")
box.prop(obj, "invisible")
box.prop(obj, "developer")
box.prop(obj, "trigger_zone")
box.prop(obj, "breakable")
box.separator()
if (obj.breakable):
box.prop(obj, "breaking_force")
box = layout.box()
box.prop(obj, "is_game_entity")
if obj.is_game_entity:
box.prop(obj, "entity_type")
if obj.entity_type == "Sun":
box.prop(obj, "sun_color")
box.prop(obj, "sun_intensity")
box.prop(obj, "ambient_color")
box.prop(obj, "ambient_intensity")
box.prop(obj, "sky_shader")
box.prop(obj, "fog_falloff")
box.prop(obj, "fog_color")
box.prop(obj, "fade_begin")
box.prop(obj, "fade_end")
elif obj.entity_type == "Light":
box.prop(obj, "light_enabled")
box.prop(obj, "light_color")
box.prop(obj, "light_intensity")
box.prop(obj, "light_falloff")
elif obj.entity_type == "Portal":
box.prop(obj, "portal_level")
box.prop(obj, "portal_target_entity")
elif obj.entity_type == "GravitySwitch":
box.prop(obj, "gravityswitch_enabled")
elif obj.entity_type == "MovementSwitch":
box.prop(obj, "movementSwitch_mode")
elif obj.entity_type == "Platform":
box.prop(obj, "platform_target_entity")
box.prop(obj, "platform_target_goal")
box.prop(obj, "platform_looping")
box.prop(obj, "platform_timer")
elif obj.entity_type == "Diamond":
box.prop(obj, "diamond_difficulty")
elif obj.entity_type == "Cable":
box.prop(obj, "cable_target_color")
elif obj.entity_type == "Button":
box.label(text="Button Targets", icon='OUTLINER_OB_MESH')
# List for button_target_entities
box.template_list(
"BUTTON_TARGET_UL_list", "",
obj, "button_target_entities",
obj, "active_button_target_index",
rows=3 # Default number of rows to display
)
# Add/Remove buttons
row = box.row(align=True)
row.operator("button_target.add", text="Add Target", icon='ADD')
row.operator("button_target.remove", text="Remove Target", icon='REMOVE').index = obj.active_button_target_index
box.prop(obj, "asset_name")
box.prop(obj, "target_entity")
class OBJECT_PT_MaterialProperties(bpy.types.Panel):
bl_label = "Material Properties"
bl_idname = "OBJECT_PT_material_properties"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "material"
def draw(self, context):
layout = self.layout
obj = context.active_object
if not obj or not obj.material_slots:
layout.label(text="No materials found")
return
layout.label(text="Material Options")
for mat_slot in obj.material_slots:
if mat_slot.material:
box = layout.box()
box.label(text=mat_slot.material.name)
box.separator()
box.prop(mat_slot.material, "shader_name")
box.prop(mat_slot.material, "tri_planar")
# ////////////////////////////////////////////////////////////////////////////////////////////////
# Exporter UI Section
#
RIFT_OP_IDNAME = "EXPORT_SCENE_OT_rift"
DEFAULT_EXPORT_PATH = "D:\\Blue\\data\\levels\\"
DEFAULT_TEXTURES_DIR = "D:\\Blue\\data\\textures\\"
save_pending = False
last_save_time = 0
def get_wm():
return bpy.context.window_manager
def ensure_prefs():
wm = get_wm()
if "rift_export_path" not in wm:
wm["rift_export_path"] = DEFAULT_EXPORT_PATH
if "rift_textures_dir" not in wm:
wm["rift_textures_dir"] = DEFAULT_TEXTURES_DIR
if "rift_show_settings" not in wm:
wm["rift_show_settings"] = False
def get_export_path(context, filename):
ensure_prefs()
export_dir = get_wm()["rift_export_path"]
if not export_dir.endswith(os.sep):
export_dir += os.sep
return os.path.join(export_dir, filename + ".rift")
class VIEW3D_PT_GameExporter(bpy.types.Panel):
bl_label = "Game Exporter"
bl_idname = "VIEW3D_PT_game_exporter"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Game"
def draw(self, context):
ensure_prefs()
wm = get_wm()
layout = self.layout
layout.label(text="Rift Exporter v1.0 for Blender 4.5", icon='INFO')
layout.label(text="Exports current blend to Rift format.")
layout.separator()
layout.operator("object.rift_export_auto", text="Export to Rift", icon='EXPORT')
layout.operator("object.rift_export_manual", text="Manual Export...", icon='FILE_FOLDER')
layout.separator()
# Debug info
layout.separator()
layout.label(text=f"Handler registered: {depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post}")
layout.label(text=f"Timer registered: {bpy.app.timers.is_registered(realtime_export_handler)}")
if hasattr(context.scene, 'rift_realtime_export'):
layout.label(text=f"Realtime export: {context.scene.rift_realtime_export}")
layout.prop(context.scene, "rift_realtime_export", text="Realtime Export", icon='TIME')
row = layout.row()
row.prop(wm, '["rift_show_settings"]', text="Settings", emboss=True, icon='PREFERENCES')
if wm["rift_show_settings"]:
box = layout.box()
box.prop(wm, '["rift_export_path"]', text="Export Path")
box.prop(wm, '["rift_textures_dir"]', text="Textures Dir")
box.operator("object.rift_export_settings_reset", text="Reset to Defaults", icon='FILE_REFRESH')
blend_path = bpy.context.blend_data.filepath
filename = bpy.path.basename(blend_path).replace('.blend', '') or "untitled"
export_path = get_export_path(context, filename)
box.label(text=f"Export will go to: {export_path}")
class OBJECT_OT_rift_export_auto(bpy.types.Operator):
bl_idname = "object.rift_export_auto"
bl_label = "Auto Export to Rift"
bl_description = "Export current blend to Rift format using settings"
def execute(self, context):
blend_path = bpy.context.blend_data.filepath
filename = bpy.path.basename(blend_path).replace('.blend', '') or "untitled"
export_path = get_export_path(context, filename)
textures_dir = get_wm()["rift_textures_dir"]
result = bpy.ops.export_scene.rift(filepath=export_path, texturesDir=textures_dir)
if 'FINISHED' in result:
self.report({'INFO'}, f"Exported to {export_path}")
return {'FINISHED'}
else:
self.report({'WARNING'}, "Export failed")
return {'CANCELLED'}
class OBJECT_OT_rift_export_manual(bpy.types.Operator):
bl_idname = "object.rift_export_manual"
bl_label = "Manual Export to Rift"
bl_description = "Manually export to Rift format with file selector"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
path = self.filepath
if not path:
self.report({'WARNING'}, "No export path selected")
return {'CANCELLED'}
textures_dir = get_wm()["rift_textures_dir"]
result = bpy.ops.export_scene.rift(filepath=path, texturesDir=textures_dir)
if 'FINISHED' in result:
self.report({'INFO'}, f"Exported to {path}")
return {'FINISHED'}
else:
self.report({'WARNING'}, "Export failed")
return {'CANCELLED'}
def invoke(self, context, event):
blend_path = bpy.context.blend_data.filepath
filename = bpy.path.basename(blend_path).replace('.blend', '') or "untitled"
default_path = get_export_path(context, filename)
context.window_manager.fileselect_add(self)
self.filepath = default_path
return {'RUNNING_MODAL'}
class OBJECT_OT_rift_export_settings_reset(bpy.types.Operator):
bl_idname = "object.rift_export_settings_reset"
bl_label = "Reset Rift Export Settings"
bl_description = "Reset export paths to default values"
def execute(self, context):
get_wm()["rift_export_path"] = DEFAULT_EXPORT_PATH
get_wm()["rift_textures_dir"] = DEFAULT_TEXTURES_DIR
self.report({'INFO'}, "Rift Export settings reset to defaults")
return {'FINISHED'}
def register_hotkey():
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc:
km = kc.keymaps.new(name='3D View', space_type='VIEW_3D')
kmi = km.keymap_items.new("object.rift_export_auto", 'E', 'PRESS', ctrl=True, shift=True)
kmi.active = True
def unregister_hotkey():
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc:
km = kc.keymaps.get('3D View')
if km:
for kmi in km.keymap_items:
if kmi.idname == "object.rift_export_auto":
km.keymap_items.remove(kmi)
def realtime_export_handler():
global save_pending, last_save_time
try:
scene = bpy.context.scene
except:
return 0.1 # Retry if context is not available
if not hasattr(scene, 'rift_realtime_export') or not scene.rift_realtime_export or not save_pending:
save_pending = False
return None
current_time = time.time()
if current_time - last_save_time < 0.016:
return 0.016
save_pending = False
try:
blend_path = bpy.context.blend_data.filepath
filename = bpy.path.basename(blend_path).replace('.blend', '') or "untitled"
export_path = get_export_path(bpy.context, filename)
textures_dir = get_wm()["rift_textures_dir"]
bpy.ops.export_scene.rift(filepath=export_path, texturesDir=textures_dir)
print(f"Realtime export saved to {export_path}")
except Exception as e:
print(f"Realtime export failed: {e}")
last_save_time = current_time
return None
def depsgraph_update_handler(scene, depsgraph):
global save_pending
if not (hasattr(scene, 'rift_realtime_export') and scene.rift_realtime_export):
return
if depsgraph.id_type_updated('OBJECT') or depsgraph.id_type_updated('MESH') or depsgraph.id_type_updated('MATERIAL'):
save_pending = True
if not bpy.app.timers.is_registered(realtime_export_handler):
bpy.app.timers.register(realtime_export_handler, first_interval=0.016)
def toggle_realtime_export(context):
if context.scene.rift_realtime_export:
if depsgraph_update_handler not in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_handler)
print("Registered depsgraph handler")
else:
if depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_handler)
print("Unregistered depsgraph handler")
if bpy.app.timers.is_registered(realtime_export_handler):
bpy.app.timers.unregister(realtime_export_handler)
print("Unregistered timer")
classes = (
VIEW3D_PT_GameExporter,
OBJECT_PT_GameEntityProperties,
OBJECT_PT_MaterialProperties,
ButtonTargetItem,
BUTTON_TARGET_UL_list,
BUTTON_TARGET_OT_add,
BUTTON_TARGET_OT_remove,
OBJECT_OT_rift_export_auto,
OBJECT_OT_rift_export_manual,
OBJECT_OT_rift_export_settings_reset,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
register_custom_properties()
bpy.types.Object.active_button_target_index = bpy.props.IntProperty()
ensure_prefs()
register_hotkey()
def unregister_custom_properties():
if hasattr(bpy.types.Scene, 'rift_realtime_export'):
del bpy.types.Scene.rift_realtime_export
def unregister():
unregister_hotkey()
# Clean up handlers
if depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_handler)
if bpy.app.timers.is_registered(realtime_export_handler):
bpy.app.timers.unregister(realtime_export_handler)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment