363 lines
13 KiB
C++
363 lines
13 KiB
C++
#include "vox_mesh_importer.h"
|
|
#include "../../constants/voxel_string_names.h"
|
|
#include "../../meshers/cubes/voxel_mesher_cubes.h"
|
|
#include "../../storage/voxel_buffer_internal.h"
|
|
#include "../../storage/voxel_memory_pool.h"
|
|
#include "../../streams/vox_data.h"
|
|
#include "../../util/macros.h"
|
|
#include "../../util/profiling.h"
|
|
#include "vox_import_funcs.h"
|
|
|
|
String VoxelVoxMeshImporter::get_importer_name() const {
|
|
return "VoxelVoxMeshImporter";
|
|
}
|
|
|
|
String VoxelVoxMeshImporter::get_visible_name() const {
|
|
return "VoxelVoxMeshImporter";
|
|
}
|
|
|
|
void VoxelVoxMeshImporter::get_recognized_extensions(List<String> *p_extensions) const {
|
|
p_extensions->push_back("vox");
|
|
}
|
|
|
|
String VoxelVoxMeshImporter::get_preset_name(int p_idx) const {
|
|
return "Default";
|
|
}
|
|
|
|
int VoxelVoxMeshImporter::get_preset_count() const {
|
|
return 1;
|
|
}
|
|
|
|
String VoxelVoxMeshImporter::get_save_extension() const {
|
|
return "mesh";
|
|
}
|
|
|
|
String VoxelVoxMeshImporter::get_resource_type() const {
|
|
return "ArrayMesh";
|
|
}
|
|
|
|
float VoxelVoxMeshImporter::get_priority() const {
|
|
// Higher import priority means the importer is preferred over another.
|
|
return 0.0;
|
|
}
|
|
|
|
// int VoxelVoxMeshImporter::get_import_order() const {
|
|
// }
|
|
|
|
void VoxelVoxMeshImporter::get_import_options(List<ImportOption> *r_options, int p_preset) const {
|
|
VoxelStringNames *sn = VoxelStringNames::get_singleton();
|
|
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, sn->store_colors_in_texture), false));
|
|
r_options->push_back(ImportOption(PropertyInfo(Variant::REAL, sn->scale), 1.f));
|
|
r_options->push_back(ImportOption(
|
|
PropertyInfo(Variant::INT, sn->pivot_mode, PROPERTY_HINT_ENUM, "LowerCorner,SceneOrigin,Center"), 1));
|
|
}
|
|
|
|
bool VoxelVoxMeshImporter::get_option_visibility(const String &p_option,
|
|
const Map<StringName, Variant> &p_options) const {
|
|
return true;
|
|
}
|
|
|
|
struct ForEachModelInstanceArgs {
|
|
const vox::Model *model;
|
|
// Pivot position, which turns out to be at the center in MagicaVoxel
|
|
Vector3i position;
|
|
Basis basis;
|
|
};
|
|
|
|
template <typename F>
|
|
static Error for_each_model_instance_in_scene_graph(
|
|
const vox::Data &data, int node_id, Transform transform, int depth, F f) {
|
|
//
|
|
ERR_FAIL_COND_V(depth > 10, ERR_INVALID_DATA);
|
|
const vox::Node *vox_node = data.get_node(node_id);
|
|
|
|
switch (vox_node->type) {
|
|
case vox::Node::TYPE_TRANSFORM: {
|
|
const vox::TransformNode *vox_transform_node = reinterpret_cast<const vox::TransformNode *>(vox_node);
|
|
// Calculate global transform of the child
|
|
const Transform child_trans(
|
|
transform.basis * vox_transform_node->rotation.basis,
|
|
transform.xform(vox_transform_node->position.to_vec3()));
|
|
for_each_model_instance_in_scene_graph(data, vox_transform_node->child_node_id, child_trans, depth + 1, f);
|
|
} break;
|
|
|
|
case vox::Node::TYPE_GROUP: {
|
|
const vox::GroupNode *vox_group_node = reinterpret_cast<const vox::GroupNode *>(vox_node);
|
|
for (unsigned int i = 0; i < vox_group_node->child_node_ids.size(); ++i) {
|
|
const int child_node_id = vox_group_node->child_node_ids[i];
|
|
for_each_model_instance_in_scene_graph(data, child_node_id, transform, depth + 1, f);
|
|
}
|
|
} break;
|
|
|
|
case vox::Node::TYPE_SHAPE: {
|
|
const vox::ShapeNode *vox_shape_node = reinterpret_cast<const vox::ShapeNode *>(vox_node);
|
|
ForEachModelInstanceArgs args;
|
|
args.model = &data.get_model(vox_shape_node->model_id);
|
|
args.position = Vector3i::from_rounded(transform.origin);
|
|
args.basis = transform.basis;
|
|
f(args);
|
|
} break;
|
|
|
|
default:
|
|
ERR_FAIL_V(ERR_INVALID_DATA);
|
|
break;
|
|
}
|
|
|
|
return OK;
|
|
}
|
|
|
|
template <typename F>
|
|
void for_each_model_instance(const vox::Data &vox_data, F f) {
|
|
if (vox_data.get_model_count() == 0) {
|
|
return;
|
|
}
|
|
if (vox_data.get_root_node_id() == -1) {
|
|
// No scene graph
|
|
ForEachModelInstanceArgs args;
|
|
args.model = &vox_data.get_model(0);
|
|
// Put at center to match what MagicaVoxel would do
|
|
args.position = args.model->size / 2;
|
|
args.basis = Basis();
|
|
f(args);
|
|
return;
|
|
}
|
|
for_each_model_instance_in_scene_graph(vox_data, vox_data.get_root_node_id(), Transform(), 0, f);
|
|
}
|
|
|
|
// Find intersecting or touching models, merge their voxels into the same grid, mesh the result, then combine meshes.
|
|
|
|
struct ModelInstance {
|
|
// Model with baked rotation
|
|
std::unique_ptr<VoxelBufferInternal> voxels;
|
|
// Lowest corner position
|
|
Vector3i position;
|
|
};
|
|
|
|
static void extract_model_instances(const vox::Data &vox_data, std::vector<ModelInstance> &out_instances) {
|
|
// Gather all models and bake their rotations
|
|
for_each_model_instance(vox_data, [&out_instances](ForEachModelInstanceArgs args) {
|
|
ERR_FAIL_COND(args.model == nullptr);
|
|
const vox::Model &model = *args.model;
|
|
|
|
Span<const uint8_t> src_color_indices;
|
|
Vector3i dst_size = model.size;
|
|
|
|
// Using temporary copy to rotate the data
|
|
std::vector<uint8_t> temp_voxels;
|
|
|
|
if (args.basis == Basis()) {
|
|
// No transformation
|
|
src_color_indices = to_span_const(model.color_indexes);
|
|
} else {
|
|
IntBasis basis;
|
|
basis.x = args.basis.get_axis(Vector3::AXIS_X);
|
|
basis.y = args.basis.get_axis(Vector3::AXIS_Y);
|
|
basis.z = args.basis.get_axis(Vector3::AXIS_Z);
|
|
temp_voxels.resize(model.color_indexes.size());
|
|
dst_size = transform_3d_array_zxy(
|
|
to_span_const(model.color_indexes), to_span(temp_voxels), model.size, basis);
|
|
src_color_indices = to_span_const(temp_voxels);
|
|
}
|
|
|
|
// TODO Optimization: implement transformation for VoxelBuffers so we can avoid using a temporary copy.
|
|
// Didn't do it yet because VoxelBuffers also have metadata and the `transform_3d_array_zxy` function only works on arrays.
|
|
std::unique_ptr<VoxelBufferInternal> voxels = std::make_unique<VoxelBufferInternal>();
|
|
voxels->create(dst_size);
|
|
voxels->decompress_channel(VoxelBufferInternal::CHANNEL_COLOR);
|
|
|
|
Span<uint8_t> dst_color_indices;
|
|
ERR_FAIL_COND(!voxels->get_channel_raw(VoxelBufferInternal::CHANNEL_COLOR, dst_color_indices));
|
|
|
|
CRASH_COND(src_color_indices.size() != dst_color_indices.size());
|
|
memcpy(dst_color_indices.data(), src_color_indices.data(), dst_color_indices.size() * sizeof(uint8_t));
|
|
|
|
ModelInstance mi;
|
|
mi.voxels = std::move(voxels);
|
|
mi.position = args.position - mi.voxels->get_size() / 2;
|
|
out_instances.push_back(std::move(mi));
|
|
});
|
|
}
|
|
|
|
static bool make_single_voxel_grid(Span<const ModelInstance> instances, Vector3i &out_origin,
|
|
VoxelBufferInternal &out_voxels) {
|
|
// Determine total size
|
|
const ModelInstance &first_instance = instances[0];
|
|
Box3i bounding_box(first_instance.position, first_instance.voxels->get_size());
|
|
for (unsigned int instance_index = 1; instance_index < instances.size(); ++instance_index) {
|
|
const ModelInstance &mi = instances[instance_index];
|
|
bounding_box.merge_with(Box3i(mi.position, mi.voxels->get_size()));
|
|
}
|
|
|
|
// Extra sanity check
|
|
// 3 gigabytes
|
|
const size_t limit = 3'000'000'000ull;
|
|
const size_t volume = bounding_box.size.volume();
|
|
ERR_FAIL_COND_V_MSG(volume > limit, false,
|
|
String("Vox data is too big to be meshed as a single mesh ({0}: {0} bytes)")
|
|
.format(varray(bounding_box.size.to_vec3(), SIZE_T_TO_VARIANT(volume))));
|
|
|
|
out_voxels.create(bounding_box.size + Vector3i(VoxelMesherCubes::PADDING * 2));
|
|
out_voxels.set_channel_depth(VoxelBufferInternal::CHANNEL_COLOR, VoxelBufferInternal::DEPTH_8_BIT);
|
|
out_voxels.decompress_channel(VoxelBufferInternal::CHANNEL_COLOR);
|
|
|
|
for (unsigned int instance_index = 0; instance_index < instances.size(); ++instance_index) {
|
|
const ModelInstance &mi = instances[instance_index];
|
|
ERR_FAIL_COND_V(mi.voxels == nullptr, false);
|
|
out_voxels.copy_from(*mi.voxels, Vector3i(), mi.voxels->get_size(),
|
|
mi.position - bounding_box.pos + Vector3i(VoxelMesherCubes::PADDING),
|
|
VoxelBufferInternal::CHANNEL_COLOR);
|
|
}
|
|
|
|
out_origin = bounding_box.pos;
|
|
return true;
|
|
}
|
|
|
|
Error VoxelVoxMeshImporter::import(const String &p_source_file, const String &p_save_path,
|
|
const Map<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files,
|
|
Variant *r_metadata) {
|
|
//
|
|
const bool p_store_colors_in_textures = p_options[VoxelStringNames::get_singleton()->store_colors_in_texture];
|
|
const float p_scale = p_options[VoxelStringNames::get_singleton()->scale];
|
|
const int p_pivot_mode = p_options[VoxelStringNames::get_singleton()->pivot_mode];
|
|
|
|
ERR_FAIL_INDEX_V(p_pivot_mode, PIVOT_MODES_COUNT, ERR_INVALID_PARAMETER);
|
|
|
|
vox::Data vox_data;
|
|
const Error load_err = vox_data.load_from_file(p_source_file);
|
|
ERR_FAIL_COND_V(load_err != OK, load_err);
|
|
|
|
// Get color palette
|
|
Ref<VoxelColorPalette> palette;
|
|
palette.instance();
|
|
for (unsigned int i = 0; i < vox_data.get_palette().size(); ++i) {
|
|
const Color8 color = vox_data.get_palette()[i];
|
|
palette->set_color8(i, color);
|
|
}
|
|
|
|
if (vox_data.get_model_count() == 0) {
|
|
// wut
|
|
return ERR_CANT_CREATE;
|
|
}
|
|
|
|
Ref<Image> atlas;
|
|
Ref<Mesh> mesh;
|
|
std::vector<unsigned int> surface_index_to_material;
|
|
{
|
|
std::vector<ModelInstance> model_instances;
|
|
extract_model_instances(vox_data, model_instances);
|
|
|
|
// From this point we no longer need vox data so we can free some memory
|
|
vox_data.clear();
|
|
|
|
// TODO Optimization: this approach uses a lot of memory, might fail on scenes with a large bounding box.
|
|
// One workaround would be to mesh the scene incrementally in chunks, giving up greedy meshing beyond 256 or so.
|
|
Vector3i bounding_box_origin;
|
|
VoxelBufferInternal voxels;
|
|
const bool single_grid_succeeded =
|
|
make_single_voxel_grid(to_span_const(model_instances), bounding_box_origin, voxels);
|
|
ERR_FAIL_COND_V(!single_grid_succeeded, ERR_CANT_CREATE);
|
|
|
|
// We no longer need these
|
|
model_instances.clear();
|
|
|
|
Ref<VoxelMesherCubes> mesher;
|
|
mesher.instance();
|
|
mesher->set_color_mode(VoxelMesherCubes::COLOR_MESHER_PALETTE);
|
|
mesher->set_palette(palette);
|
|
mesher->set_greedy_meshing_enabled(true);
|
|
mesher->set_store_colors_in_texture(p_store_colors_in_textures);
|
|
|
|
Vector3 offset;
|
|
switch (p_pivot_mode) {
|
|
case PIVOT_LOWER_CORNER:
|
|
break;
|
|
case PIVOT_SCENE_ORIGIN:
|
|
offset = bounding_box_origin.to_vec3();
|
|
break;
|
|
case PIVOT_CENTER:
|
|
offset = -((voxels.get_size() - Vector3i(1)) / 2).to_vec3();
|
|
break;
|
|
default:
|
|
ERR_FAIL_V(ERR_BUG);
|
|
break;
|
|
};
|
|
|
|
mesh = VoxImportUtils::build_mesh(voxels, **mesher, surface_index_to_material, atlas, p_scale, offset);
|
|
// Deallocate large temporary memory to free space.
|
|
// This is a workaround because VoxelBuffer uses this by default, however it doesn't fit the present use case.
|
|
// Eventually we should avoid using this pool here.
|
|
VoxelMemoryPool::get_singleton()->clear_unused_blocks();
|
|
}
|
|
|
|
if (mesh.is_null()) {
|
|
// wut
|
|
return ERR_CANT_CREATE;
|
|
}
|
|
|
|
// Save atlas
|
|
// TODO Saving atlases separately is impossible because of https://github.com/godotengine/godot/issues/51163
|
|
// Instead, I do like ResourceImporterScene: I leave them UNCOMPRESSED inside the materials...
|
|
/*String atlas_path;
|
|
if (atlas.is_valid()) {
|
|
atlas_path = String("{0}.atlas{1}.stex").format(varray(p_save_path, model_index));
|
|
const Error save_stex_err = save_stex(atlas, atlas_path, false, 0, true, true, true);
|
|
ERR_FAIL_COND_V_MSG(save_stex_err != OK, save_stex_err,
|
|
String("Failed to save {0}").format(varray(atlas_path)));
|
|
}*/
|
|
|
|
// DEBUG
|
|
// if (atlas.is_valid()) {
|
|
// atlas->save_png(String("debug_atlas{0}.png").format(varray(model_index)));
|
|
// }
|
|
|
|
FixedArray<Ref<SpatialMaterial>, 2> materials;
|
|
for (unsigned int i = 0; i < materials.size(); ++i) {
|
|
Ref<SpatialMaterial> &mat = materials[i];
|
|
mat.instance();
|
|
mat->set_roughness(1.f);
|
|
if (!p_store_colors_in_textures) {
|
|
// In this case we store colors in vertices
|
|
mat->set_flag(SpatialMaterial::FLAG_ALBEDO_FROM_VERTEX_COLOR, true);
|
|
}
|
|
}
|
|
materials[1]->set_feature(SpatialMaterial::FEATURE_TRANSPARENT, true);
|
|
|
|
// Assign materials
|
|
if (p_store_colors_in_textures) {
|
|
// Can't share materials at the moment, because each atlas is specific to its mesh
|
|
for (unsigned int surface_index = 0; surface_index < surface_index_to_material.size(); ++surface_index) {
|
|
const unsigned int material_index = surface_index_to_material[surface_index];
|
|
CRASH_COND(material_index >= materials.size());
|
|
Ref<SpatialMaterial> material = materials[material_index]->duplicate();
|
|
if (atlas.is_valid()) {
|
|
// TODO Do I absolutely HAVE to load this texture back to memory AND renderer just so import works??
|
|
//Ref<Texture> texture = ResourceLoader::load(atlas_path);
|
|
// TODO THIS IS A WORKAROUND, it is not supposed to be an ImageTexture...
|
|
// See earlier code, I could not find any way to reference a separate StreamTexture.
|
|
Ref<ImageTexture> texture;
|
|
texture.instance();
|
|
texture->create_from_image(atlas, 0);
|
|
material->set_texture(SpatialMaterial::TEXTURE_ALBEDO, texture);
|
|
}
|
|
mesh->surface_set_material(surface_index, material);
|
|
}
|
|
} else {
|
|
for (unsigned int surface_index = 0; surface_index < surface_index_to_material.size(); ++surface_index) {
|
|
const unsigned int material_index = surface_index_to_material[surface_index];
|
|
CRASH_COND(material_index >= materials.size());
|
|
mesh->surface_set_material(surface_index, materials[material_index]);
|
|
}
|
|
}
|
|
|
|
// Save mesh
|
|
{
|
|
VOXEL_PROFILE_SCOPE();
|
|
String mesh_save_path = String("{0}.mesh").format(varray(p_save_path));
|
|
const Error mesh_save_err = ResourceSaver::save(mesh_save_path, mesh);
|
|
ERR_FAIL_COND_V_MSG(mesh_save_err != OK, mesh_save_err,
|
|
String("Failed to save {0}").format(varray(mesh_save_path)));
|
|
}
|
|
|
|
return OK;
|
|
}
|