diff --git a/CMakeLists.txt b/CMakeLists.txt index 354ec4c..23905b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,13 +6,14 @@ set(APPS apps/voxmerge.cpp apps/voxseparate.cpp demo/demo_vox.cpp + demo/tests.cpp ) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED on) foreach (app ${APPS}) - get_filename_component(APP_NAME ${app} NAME_WE) + get_filename_component(APP_NAME ${app} NAME_WE) add_executable(${APP_NAME} ${app}) target_compile_options(${APP_NAME} PRIVATE $<$:/W4> @@ -23,3 +24,5 @@ endforeach() include(CTest) add_test(NAME test_multiple_model_scene COMMAND $ ${CMAKE_CURRENT_SOURCE_DIR}/demo/vox/test_multiple_model_scene.vox) + +add_test(NAME tests COMMAND $ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/demo/vox) diff --git a/apps/vox2obj.cpp b/apps/vox2obj.cpp index 7236255..954ed34 100644 --- a/apps/vox2obj.cpp +++ b/apps/vox2obj.cpp @@ -12,7 +12,7 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN -// if min/max are not already defined, windows.h defines them, which breaks std::min/std::max. +// if min/max are not already defined, windows.h defines them, which breaks std::min/std::max. // we can safely undefine them after we've included windows.h #define max #define min @@ -300,7 +300,7 @@ bool export_scene_anim_as_obj(const ogt_vox_scene* scene, const std::string& out return false; } printf("writing file %s\n", out_material_name.c_str()); - fprintf(fout, "# opengametools vox2animobj - see source code at https://github.com/jpaver/opengametools/tree/master/apps/vox2animobj.cpp\r\n"); // TODO(jpaver) proper header + fprintf(fout, "# opengametools vox2obj - see source code at https://github.com/jpaver/opengametools/tree/master/apps/vox2obj.cpp\r\n"); // TODO(jpaver) proper header fprintf(fout, "\r\n"); fprintf(fout, "newmtl palette\r\n"); fprintf(fout, "illum 1\r\n"); @@ -439,12 +439,12 @@ bool export_scene_anim_as_obj(const ogt_vox_scene* scene, const std::string& out void print_help() { printf( - "vox2animobj v2.0 by Justin Paver - source code available here: http://github.com/jpaver/opengametools \n" + "vox2obj v2.0 by Justin Paver - source code available here: http://github.com/jpaver/opengametools \n" "\n" "This tool can extract frames out of a given MagicaVoxel.vox and save them either as separate .obj files,\n" " or as a single .obj file with separate internal objects for each frame.\n" "\n" - " usage: vox2animobj [optional args] \n" + " usage: vox2obj [optional args] \n" "\n" " [optional args] can be one or multiple of:\n" " --mesh_algorithm : (default: polygon) sets the meshing mode where is one of: simple, greedy or polygon\n" @@ -455,7 +455,7 @@ void print_help() " --output_vox : (default: disabled): if specified will output .vox files for each frame instead of .obj" "\n" "example:\n" - " vox2animobj --mesh_algorithm polygon --output_name test --frames 0 119 --scale scene.vox\n" + " vox2obj --mesh_algorithm polygon --output_name test --frames 0 119 --scale scene.vox\n" "\n" "The above example uses polygon tessellation. will generate test.mtl/test.tga and the test.obj will contain 120 objects,\n" "with each object representing a mesh of the entire frame within scene.vox\n" diff --git a/demo/tests.cpp b/demo/tests.cpp new file mode 100644 index 0000000..5593253 --- /dev/null +++ b/demo/tests.cpp @@ -0,0 +1,77 @@ +#include "tests.h" + +TESTS_GLOBALS(); + +static void testLoadScene(void) { + const ogt_vox_scene *scene = load_vox_scene_with_groups("test_meta_chunk.vox"); + ASSERT_NE_NULLPTR(scene); + EXPECT_EQ_UINT(7u, scene->anim_range_start); + EXPECT_EQ_UINT(36u, scene->anim_range_end); + EXPECT_EQ_UINT(200u, scene->file_version); + + EXPECT_EQ_UINT(10u, scene->num_cameras); + ASSERT_NE_NULLPTR(scene->cameras); + + ASSERT_EQ_UINT(1u, scene->num_models); + ASSERT_NE_NULLPTR(scene->models); + const ogt_vox_model *model = scene->models[0]; + EXPECT_EQ_UINT(64000u, count_solid_voxels_in_model(model)); + EXPECT_EQ_UINT(40u, model->size_x); + EXPECT_EQ_UINT(40u, model->size_y); + EXPECT_EQ_UINT(40u, model->size_z); + + EXPECT_EQ_UINT(1u, scene->num_instances); + ASSERT_NE_NULLPTR(scene->instances); + + EXPECT_EQ_UINT(16u, scene->num_layers); + ASSERT_NE_NULLPTR(scene->layers); + const ogt_vox_layer &layer = scene->layers[0]; + EXPECT_EQ_NULLPTR(layer.name); + EXPECT_FALSE(layer.hidden); + EXPECT_EQ_UINT(255u, layer.color.r); + EXPECT_EQ_UINT(204u, layer.color.g); + EXPECT_EQ_UINT(153u, layer.color.b); + EXPECT_EQ_UINT(255u, layer.color.a); + + EXPECT_EQ_UINT(1u, scene->num_groups); + ASSERT_NE_NULLPTR(scene->groups); + const ogt_vox_group &group = scene->groups[0]; + EXPECT_EQ_NULLPTR(group.name); + EXPECT_EQ_UINT(k_invalid_group_index, group.parent_group_index); + EXPECT_EQ_UINT(k_invalid_layer_index, group.layer_index); + EXPECT_FALSE(group.hidden); + + EXPECT_EQ_UINT(32u, scene->num_color_names); + ASSERT_NE_NULLPTR(scene->color_names); + EXPECT_EQ_STRING("NOTE", scene->color_names[0]); +} + +static void testGroups(void) { + const ogt_vox_scene *scene = load_vox_scene_with_groups("test_groups.vox"); + ASSERT_NE_NULLPTR(scene); + ASSERT_EQ_INT(5, (int)scene->num_groups); + ASSERT_EQ_INT(150, (int)scene->file_version); + ASSERT_NE_NULLPTR(scene->groups); + EXPECT_EQ_STRING("characters", scene->groups[3].name); + EXPECT_EQ_STRING("text", scene->groups[4].name); +} + +static void testMetaChunk(void) { + const char *filename = loadsave_vox_scene("test_meta_chunk.vox"); + ASSERT_NE_NULLPTR(filename); + const ogt_vox_scene *scene = load_vox_scene_with_groups(filename); + ASSERT_NE_NULLPTR(scene); + EXPECT_EQ_INT(7, (int)scene->anim_range_start); + EXPECT_EQ_INT(36, (int)scene->anim_range_end); + EXPECT_EQ_INT(200, (int)scene->file_version); +} + +int main(int argc, char *argv[]) { + TESTS_INIT(); + + ADD_TEST(testLoadScene); + ADD_TEST(testGroups); + ADD_TEST(testMetaChunk); + + TESTS_SHUTDOWN(); +} diff --git a/demo/tests.h b/demo/tests.h new file mode 100644 index 0000000..0dad4f0 --- /dev/null +++ b/demo/tests.h @@ -0,0 +1,379 @@ +#ifndef TEST_SHARED_H +#define TEST_SHARED_H + +#define OGT_VOX_IMPLEMENTATION +#include "../src/ogt_vox.h" + +#if defined(_MSC_VER) +#include +#endif +#include + +inline uint32_t count_solid_voxels_in_model(const ogt_vox_model *model) { + uint32_t solid_voxel_count = 0; + uint32_t voxel_index = 0; + for (uint32_t z = 0; z < model->size_z; z++) { + for (uint32_t y = 0; y < model->size_y; y++) { + for (uint32_t x = 0; x < model->size_x; x++, voxel_index++) { + uint32_t color_index = model->voxel_data[voxel_index]; + bool is_voxel_solid = (color_index != 0); + solid_voxel_count += (is_voxel_solid ? 1 : 0); + } + } + } + return solid_voxel_count; +} + +// a helper function to load a magica voxel scene given a filename. +inline const ogt_vox_scene *load_vox_scene(const char *filename, + uint32_t scene_read_flags = 0) { + if (filename == nullptr) { + fprintf(stderr, "No filename given\n"); + return nullptr; + } + // open the file +#if defined(_MSC_VER) && _MSC_VER >= 1400 + FILE *fp; + if (0 != fopen_s(&fp, filename, "rb")) + fp = 0; +#else + FILE *fp = fopen(filename, "rb"); +#endif + if (!fp) { + fprintf(stderr, "Failed to load %s\n", filename); + return nullptr; + } + + // get the buffer size which matches the size of the file + fseek(fp, 0, SEEK_END); + uint32_t buffer_size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + // load the file into a memory buffer + uint8_t *buffer = new uint8_t[buffer_size]; + fread(buffer, buffer_size, 1, fp); + fclose(fp); + + // construct the scene from the buffer + const ogt_vox_scene *scene = + ogt_vox_read_scene_with_flags(buffer, buffer_size, scene_read_flags); + + // the buffer can be safely deleted once the scene is instantiated. + delete[] buffer; + + return scene; +} + +inline const ogt_vox_scene *load_vox_scene_with_groups(const char *filename) { + return load_vox_scene(filename, k_read_scene_flags_groups); +} + +// a helper function to save a magica voxel scene to disk. +inline bool save_vox_scene(const char *pcFilename, const ogt_vox_scene *scene) { + if (pcFilename == nullptr) { + fprintf(stderr, "No filename for saving given\n"); + return false; + } + if (scene == nullptr) { + fprintf(stderr, "Failed to save vox scene to %s\n", pcFilename); + return false; + } + // save the scene back out. + uint32_t buffersize = 0; + uint8_t *buffer = ogt_vox_write_scene(scene, &buffersize); + if (buffer == nullptr) { + fprintf(stderr, "Failed to write vox scene to %s\n", pcFilename); + return false; + } + + // open the file for write +#if defined(_MSC_VER) && _MSC_VER >= 1400 + FILE *fp; + if (0 != fopen_s(&fp, pcFilename, "wb")) + fp = 0; +#else + FILE *fp = fopen(pcFilename, "wb"); +#endif + if (!fp) { + fprintf(stderr, "Failed to open %s for writing\n", pcFilename); + ogt_vox_free(buffer); + return false; + } + + fwrite(buffer, buffersize, 1, fp); + fclose(fp); + ogt_vox_free(buffer); + return true; +} + +inline const char *loadsave_vox_scene(const char *filename) { + static const char *target_filename = "test.vox"; + const ogt_vox_scene *scene = load_vox_scene_with_groups(filename); + if (!save_vox_scene(target_filename, scene)) { + return nullptr; + } + return target_filename; +} + +#define TEST_STRINGIFY(arg) #arg + +#define ADD_TEST(func) \ + prevFailed = failed; \ + errorBuf[0] = '\0'; \ + printf("Testing %-30s...", TEST_STRINGIFY(func)); \ + (func)(); \ + if (prevFailed == failed) { \ + printf(" [success]\n"); \ + } else { \ + printf(" [failed]\n"); \ + printf("%s", errorBuf); \ + } \ + ++tests + +#define ADD_DISABLED_TEST(func) \ + prevFailed = failed; \ + errorBuf[0] = '\0'; \ + if (!runDisabled) { \ + printf("Skipping %-30s...", TEST_STRINGIFY(func)); \ + printf(" [skip]\n"); \ + } else { \ + printf("Testing %-30s...", TEST_STRINGIFY(func)); \ + (func)(); \ + if (prevFailed == failed) { \ + printf(" [success]\n"); \ + } else { \ + printf(" [failed]\n"); \ + printf("%s", errorBuf); \ + } \ + } \ + ++tests + +#define TESTS_GLOBALS() \ + static int failed = 0; \ + static int tests = 0; \ + static int prevFailed = 0; \ + static char errorBuf[4096] = ""; \ + static int lastExpectedInt = 0; \ + static unsigned int lastExpectedUInt = 0; \ + static float lastExpectedFloat = 0.0f; \ + static const char *lastExpectedString = nullptr; \ + static bool lastExpectedBool = false; \ + static int runDisabled = 0; + +#define TESTS_SHUTDOWN() \ + printf("\nfailed tests: %i out of %i\n", failed, tests); \ + if (failed != 0) { \ + return 1; \ + } \ + return 0 + +#define TESTS_INIT() \ + (void)lastExpectedInt; \ + (void)lastExpectedUInt; \ + (void)lastExpectedFloat; \ + (void)lastExpectedString; \ + (void)lastExpectedBool; \ + (void)runDisabled; \ + (void)failed; \ + (void)tests; \ + (void)prevFailed; \ + if (argc > 1) { \ + if (!strcmp(argv[1], "--also_run_disabled_tests")) { \ + runDisabled = 1; \ + } else if (!strcmp(argv[1], "--help")) { \ + printf("--also_run_disabled_tests : also run disabled tests"); \ + return 0; \ + } \ + } + +#define EXPECT_TRUE(actual) \ + if (lastExpectedBool = (actual), \ + lastExpectedBool == false) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY( \ + actual) ": expected true (line %i)\n", __LINE__); \ + ++failed; \ + } + +#define EXPECT_FALSE(actual) \ + if (lastExpectedBool = (actual), \ + lastExpectedBool == true) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY( \ + actual) ": expected false (line %i)\n", __LINE__); \ + ++failed; \ + } + +#define ASSERT_EQ_FLOAT(exp, actual) \ + if (lastExpectedFloat = (actual), \ + abs((exp) - lastExpectedFloat) > (epsilon)) { \ + snprintf( \ + errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected %f, but got %f (line %i)\n", \ + exp, lastExpectedFloat, __LINE__); \ + ++failed; \ + return; \ + } + +#define EXPECT_EQ_FLOAT(exp, actual, epsilon) \ + if (lastExpectedFloat = (actual), \ + abs((exp) - lastExpectedFloat) > (epsilon)) { \ + snprintf( \ + errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected %f, but got %f (line %i)\n", \ + exp, lastExpectedFloat, __LINE__); \ + ++failed; \ + } + +#define ASSERT_EQ_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) != lastExpectedInt) { \ + snprintf( \ + errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected %i, but got %i (line %i)\n", \ + exp, lastExpectedInt, __LINE__); \ + ++failed; \ + return; \ + } + +#define EXPECT_EQ_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) != lastExpectedInt) { \ + snprintf( \ + errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected %i, but got %i (line %i)\n", \ + exp, lastExpectedInt, __LINE__); \ + ++failed; \ + } + +#define ASSERT_EQ_UINT(exp, actual) \ + if (lastExpectedUInt = (actual), (exp) != lastExpectedUInt) { \ + snprintf( \ + errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected %u, but got %u (line %i)\n", \ + exp, lastExpectedUInt, __LINE__); \ + ++failed; \ + return; \ + } + +#define EXPECT_EQ_UINT(exp, actual) \ + if (lastExpectedUInt = (actual), (exp) != lastExpectedUInt) { \ + snprintf( \ + errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected %u, but got %u (line %i)\n", \ + exp, lastExpectedUInt, __LINE__); \ + ++failed; \ + } + +#define EXPECT_BETWEEN_INT(minv, maxv, actual) \ + if (lastExpectedInt = (actual), \ + lastExpectedInt < (minv) || lastExpectedInt > (maxv)) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY( \ + actual) ": expected %i to in range of [%i:%i] (line %i)\n", \ + lastExpectedInt, minv, maxv, __LINE__); \ + ++failed; \ + } + +#define EXPECT_GT_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) >= lastExpectedInt) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected to be greater than %i, " \ + "but got %i (line %i)\n", \ + exp, lastExpectedInt, __LINE__); \ + ++failed; \ + } + +#define EXPECT_GE_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) > lastExpectedInt) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected to be greater or equal " \ + "to %i, but got %i (line %i)\n", \ + exp, lastExpectedInt, __LINE__); \ + ++failed; \ + } + +#define EXPECT_LT_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) <= lastExpectedInt) { \ + snprintf( \ + errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY( \ + actual) ": expected to be less than %i, but got %i (line %i)\n", \ + exp, lastExpectedInt, __LINE__); \ + ++failed; \ + } + +#define EXPECT_LE_INT(exp, actual) \ + if (lastExpectedInt = (actual), (exp) < lastExpectedInt) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected to be less or equal to " \ + "%i, but got %i (line %i)\n", \ + exp, lastExpectedInt, __LINE__); \ + ++failed; \ + } + +#define EXPECT_NE_NULLPTR(actual) \ + if ((actual) == nullptr) { \ + snprintf( \ + errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected to be not null (line %i)\n", \ + __LINE__); \ + ++failed; \ + } + +#define ASSERT_NE_NULLPTR(actual) \ + if ((actual) == nullptr) { \ + snprintf( \ + errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected to be not null (line %i)\n", \ + __LINE__); \ + ++failed; \ + return; \ + } + +#define EXPECT_EQ_NULLPTR(actual) \ + if ((actual) != nullptr) { \ + snprintf( \ + errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected to be null (line %i)\n", \ + __LINE__); \ + ++failed; \ + } + +#define ASSERT_EQ_NULLPTR(actual) \ + if ((actual) != nullptr) { \ + snprintf( \ + errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY(actual) ": expected to be null (line %i)\n", \ + __LINE__); \ + ++failed; \ + return; \ + } + +#define ASSERT_EQ_STRING(exp, actual) \ + if (lastExpectedString = (actual), strcmp(exp, lastExpectedString) != 0) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY( \ + actual) ": expected '%s', but got '%s' (line %i)\n", \ + exp, lastExpectedString, __LINE__); \ + ++failed; \ + return; \ + } + +#define EXPECT_EQ_STRING(exp, actual) \ + if (lastExpectedString = (actual), strcmp(exp, lastExpectedString) != 0) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY( \ + actual) ": expected '%s', but got '%s' (line %i)\n", \ + exp, lastExpectedString, __LINE__); \ + ++failed; \ + } + +#define EXPECT_NE_STRING(exp, actual) \ + if (lastExpectedString = (actual), strcmp(exp, lastExpectedString) == 0) { \ + snprintf(errorBuf + strlen(errorBuf), sizeof(errorBuf) - strlen(errorBuf), \ + " - " TEST_STRINGIFY( \ + actual) ": expected '%s', but got '%s' (line %i)\n", \ + exp, lastExpectedString, __LINE__); \ + ++failed; \ + } + +#endif diff --git a/demo/vox/.gitignore b/demo/vox/.gitignore new file mode 100644 index 0000000..094b0c0 --- /dev/null +++ b/demo/vox/.gitignore @@ -0,0 +1 @@ +test.vox diff --git a/demo/vox/test_meta_chunk.vox b/demo/vox/test_meta_chunk.vox new file mode 100644 index 0000000..9b566a0 Binary files /dev/null and b/demo/vox/test_meta_chunk.vox differ diff --git a/src/ogt_vox.h b/src/ogt_vox.h index d370546..fbf24ee 100644 --- a/src/ogt_vox.h +++ b/src/ogt_vox.h @@ -95,6 +95,9 @@ instance has a transform that determines its position and orientation within the scene, but it also has an index that specifies which model the instance uses for its shape. It is expected that there is a many-to-one mapping of instances to models. + Instances can overlap. For file version 200 (or perhaps higher) lower instance numbers are + more important and would overwrite existing voxels on merging, for version < 200 the higher + numbers are more important. An ogt_vox_layer is used to conceptually group instances. Each instance indexes the layer that it belongs to, but the layer itself has its own name and hidden/shown state. @@ -173,6 +176,7 @@ eg. #include "my_assert.h" #define ogt_assert(condition, message_str) my_assert(condition, message_str) + #define ogt_assert_warn(condition, message_str) my_assert(condition, message_str) #define OGT_VOX_IMPLEMENTATION #include "path/to/ogt_vox.h" @@ -229,6 +233,8 @@ // denotes an invalid group index. Usually this is only applicable to the scene's root group's parent. static const uint32_t k_invalid_group_index = UINT32_MAX; + // denotes an invalid layer index. Can happen for instances and groups at least. + static const uint32_t k_invalid_layer_index = UINT32_MAX; // color typedef struct ogt_vox_rgba @@ -332,10 +338,10 @@ uint32_t camera_id; ogt_cam_mode mode; float focus[3]; // the target position - float angle[3]; // rotation in degree - float radius; - float frustum; - int fov; // angle in degree + float angle[3]; // rotation in degree - pitch (-180 to +180), yaw (0 to 360), roll (0 to 360) + float radius; // distance of camera position from target position, also controls frustum in MV for orthographic/isometric modes + float frustum; // 'height' of near plane of frustum, either orthographic height in voxels or tan( fov/2.0f ) + int fov; // angle in degrees for height of field of view, ensure to set frustum as only used when changed in MV UI } ogt_vox_cam; typedef struct ogt_vox_sun @@ -418,21 +424,24 @@ // the scene parsed from a .vox file. typedef struct ogt_vox_scene { - uint32_t num_models; // number of models within the scene. - uint32_t num_instances; // number of instances in the scene (on anim frame 0) - uint32_t num_layers; // number of layers in the scene - uint32_t num_groups; // number of groups in the scene - uint32_t num_color_names;// number of color names in the scene - const char** color_names; // array of color names. size is num_color_names - const ogt_vox_model** models; // array of models. size is num_models - const ogt_vox_instance* instances; // array of instances. size is num_instances - const ogt_vox_layer* layers; // array of layers. size is num_layers - const ogt_vox_group* groups; // array of groups. size is num_groups - ogt_vox_palette palette; // the palette for this scene - ogt_vox_matl_array materials; // the extended materials for this scene - uint32_t num_cameras; // number of cameras for this scene - const ogt_vox_cam* cameras; // the cameras for this scene - ogt_vox_sun* sun; // sun - primary light at infinity + uint32_t file_version; // version of the .vox file format. + uint32_t num_models; // number of models within the scene. + uint32_t num_instances; // number of instances in the scene (on anim frame 0) + uint32_t num_layers; // number of layers in the scene + uint32_t num_groups; // number of groups in the scene + uint32_t num_color_names; // number of color names in the scene + const char** color_names; // array of color names. size is num_color_names + const ogt_vox_model** models; // array of models. size is num_models + const ogt_vox_instance* instances; // array of instances. size is num_instances + const ogt_vox_layer* layers; // array of layers. size is num_layers + const ogt_vox_group* groups; // array of groups. size is num_groups + ogt_vox_palette palette; // the palette for this scene + ogt_vox_matl_array materials; // the extended materials for this scene + uint32_t num_cameras; // number of cameras for this scene + const ogt_vox_cam* cameras; // the cameras for this scene + ogt_vox_sun* sun; // sun - primary light at infinity + uint32_t anim_range_start; // the start frame of the animation range for this scene (META chunk since 0.99.7.2) + uint32_t anim_range_end; // the end frame of the animation range for this scene (META chunk since 0.99.7.2) } ogt_vox_scene; // allocate memory function interface. pass in size, and get a pointer to memory with at least that size available. @@ -507,6 +516,9 @@ #ifndef ogt_assert #include #define ogt_assert(x, msg_str) do { assert((x) && (msg_str)); } while(0) +#endif +#ifndef ogt_assert_warn + #define ogt_assert_warn(x, msg_str) ogt_assert(x, msg_str) #endif #include #include @@ -532,6 +544,7 @@ static const uint32_t CHUNK_ID_rOBJ = MAKE_VOX_CHUNK_ID('r','O','B','J'); static const uint32_t CHUNK_ID_rCAM = MAKE_VOX_CHUNK_ID('r','C','A','M'); static const uint32_t CHUNK_ID_NOTE = MAKE_VOX_CHUNK_ID('N','O','T','E'); + static const uint32_t CHUNK_ID_META = MAKE_VOX_CHUNK_ID('M','E','T','A'); static const uint32_t NAME_MAX_LEN = 256; // max name len = 255 plus 1 for null terminator static const uint32_t CHUNK_HEADER_LEN = 12; // 4 bytes for each of: chunk_id, chunk_size, chunk_child_size @@ -1273,7 +1286,7 @@ } default: { - ogt_assert(0, "unhandled node type"); + ogt_assert_warn(0, "unhandled node type"); } } } @@ -1473,6 +1486,8 @@ bool found_index_map_chunk = false; ogt_vox_sun sun; bool found_sun = false; + uint32_t anim_range_start = 0; + uint32_t anim_range_end = 30; // size some of our arrays to prevent resizing during the parsing for smallish cases. model_ptrs.reserve(64); @@ -1566,8 +1581,9 @@ uint8_t y = packed_voxel_data[i * 4 + 1]; uint8_t z = packed_voxel_data[i * 4 + 2]; uint8_t color_index = packed_voxel_data[i * 4 + 3]; - ogt_assert(x < size_x && y < size_y && z < size_z, "invalid data in XYZI chunk"); - if(x < size_x && y < size_y && z < size_z ) { + const bool inside_region = x < size_x && y < size_y && z < size_z; + ogt_assert_warn(inside_region, "invalid data in XYZI chunk"); + if (inside_region) { voxel_data[(x * k_stride_x) + (y * k_stride_y) + (z * k_stride_z)] = color_index; } } @@ -1607,8 +1623,8 @@ _vox_file_read_uint32(fp, &reserved_id); _vox_file_read_uint32(fp, &layer_id); _vox_file_read_uint32(fp, &num_frames); - ogt_assert(reserved_id == UINT32_MAX, "unexpected values for reserved_id in nTRN chunk"); - ogt_assert(num_frames > 0, "must have at least 1 frame in nTRN chunk"); + ogt_assert_warn(reserved_id == UINT32_MAX, "unexpected values for reserved_id in nTRN chunk"); + ogt_assert_warn(num_frames > 0, "must have at least 1 frame in nTRN chunk"); // make space in misc_data array for the number of transforms we'll need for this node ogt_vox_keyframe_transform* keyframes = misc_data.alloc_many(num_frames); @@ -1722,7 +1738,7 @@ _vox_file_read_int32(fp, &layer_id); _vox_file_read_dict(&dict, fp); _vox_file_read_int32(fp, &reserved_id); - ogt_assert(reserved_id == -1, "unexpected value for reserved_id in LAYR chunk"); + ogt_assert_warn(reserved_id == -1, "unexpected value for reserved_id in LAYR chunk"); layers.grow_to_fit_index(layer_id); layers[layer_id].name = NULL; @@ -1920,6 +1936,25 @@ _vox_file_seek_forwards(fp, remaining); break; } + case CHUNK_ID_META: + { + _vox_file_read_dict(&dict, fp); + const char* anim_range_string = _vox_dict_get_value_as_string(&dict, "_anim_range", NULL); + if (anim_range_string) { + // parse the animation range string, which is of the form "start_frame end_frame" + int start_frame, end_frame; + if (_vox_str_scanf(anim_range_string, "%d %d", &start_frame, &end_frame) == 2) { + if (start_frame < 0 || end_frame < start_frame) { + ogt_assert(false, "invalid animation range in META chunk"); + } else { + // set the animation range + anim_range_start = (uint32_t)start_frame; + anim_range_end = (uint32_t)end_frame; + } + } + } + break; + } case CHUNK_ID_NOTE: { uint32_t num_names; @@ -2026,8 +2061,8 @@ if (g_progress_callback_func) { // we indicate progress as 0.8f * amount of buffer read + 0.2f at end after processing - if (!g_progress_callback_func(0.8f*(float)(fp->offset)/(float)(fp->buffer_size), g_progress_callback_user_data)) - { + const float progress = 0.8f * (float)(fp->offset) / (float)(fp->buffer_size); + if (!g_progress_callback_func(progress, g_progress_callback_user_data)) { return 0; } } @@ -2382,6 +2417,10 @@ } } + scene->anim_range_start = anim_range_start; + scene->anim_range_end = anim_range_end; + scene->file_version = file_version; + if (g_progress_callback_func) { // we indicate progress as complete, but don't check for cancel as finished g_progress_callback_func(1.0f, g_progress_callback_user_data); @@ -2449,10 +2488,10 @@ is_negative = f[i] < 0.0f ? true : false; } else { - ogt_assert(f[i] == 0.0f, "rotation vector should contain only 0.0f, 1.0f, or -1.0f"); + ogt_assert_warn(f[i] == 0.0f, "rotation vector should contain only 0.0f, 1.0f, or -1.0f"); } } - ogt_assert(out_index != 3, "rotation vector was all zeroes but it should be a cardinal axis vector"); + ogt_assert_warn(out_index != 3, "rotation vector was all zeroes but it should be a cardinal axis vector"); return is_negative; } @@ -2465,7 +2504,7 @@ bool row0_negative = _vox_get_vec3_rotation_bits(row0, row0_index); bool row1_negative = _vox_get_vec3_rotation_bits(row1, row1_index); bool row2_negative = _vox_get_vec3_rotation_bits(row2, row2_index); - ogt_assert(((1 << row0_index) | (1 << row1_index) | (1 << row2_index)) == 7, "non orthogonal rows found in transform"); // check that rows are orthogonal. There must be a non-zero entry in column 0, 1 and 2 across these 3 rows. + ogt_assert_warn(((1 << row0_index) | (1 << row1_index) | (1 << row2_index)) == 7, "non orthogonal rows found in transform"); // check that rows are orthogonal. There must be a non-zero entry in column 0, 1 and 2 across these 3 rows. return (row0_index) | (row1_index << 2) | (row0_negative ? 1 << 4 : 0) | (row1_negative ? 1 << 5 : 0) | (row2_negative ? 1 << 6 : 0); } @@ -2584,7 +2623,8 @@ // write file header and file version _vox_file_write_uint32(fp, CHUNK_ID_VOX_); - _vox_file_write_uint32(fp, 150); + uint32_t version = scene->file_version > 0u ? scene->file_version : 150u; // default to 150 if not specified + _vox_file_write_uint32(fp, version); // write the main chunk _vox_file_write_uint32(fp, CHUNK_ID_MAIN); @@ -2594,6 +2634,25 @@ // we need to know how to patch up the main chunk size after we've written everything const uint32_t offset_post_main_chunk = _vox_file_get_offset(fp); + // write out the META chunk + if (version >= 200u) { + char anim_range[64] = ""; + _vox_sprintf(anim_range, sizeof(anim_range), "%d %d", (int)scene->anim_range_start, (int)scene->anim_range_end); + + uint32_t offset_of_chunk_header = _vox_file_get_offset(fp); + // write the META header + _vox_file_write_uint32(fp, CHUNK_ID_META); + _vox_file_write_uint32(fp, 0); // chunk_size will get patched up later + _vox_file_write_uint32(fp, 0); + + _vox_file_write_uint32(fp, 1); // num key values + _vox_file_write_dict_key_value(fp, "_anim_range", anim_range); + + // compute and patch up the chunk size in the chunk header + uint32_t chunk_size = _vox_file_get_offset(fp) - offset_of_chunk_header - CHUNK_HEADER_LEN; + _vox_file_write_uint32_at_offset(fp, offset_of_chunk_header + 4, &chunk_size); + } + // write out all model chunks for (uint32_t i = 0; i < scene->num_models; i++) { const ogt_vox_model* model = scene->models[i]; @@ -2804,7 +2863,6 @@ // write out the sun chunk if (scene->sun) { - ogt_vox_sun* sun = scene->sun; char sun_intensity[32] = ""; char sun_area[32] = ""; @@ -3001,7 +3059,7 @@ // check that the buffer is not larger than the maximum file size, return nothing if would overflow if (fp->data.count > UINT32_MAX || (fp->data.count - offset_post_main_chunk) > UINT32_MAX) { - ogt_assert(0, "Generated file size exceeded 4GiB, which is too large for Magicavoxel to parse."); + ogt_assert_warn(0, "Generated file size exceeded 4GiB, which is too large for Magicavoxel to parse."); *buffer_size = 0; return NULL; // note: fp will be freed in dtor on exit }