LumixEngine/src/studio/import_asset_dialog.cpp
Mikulas Florek 8837e9178e cleanup
2015-12-01 23:50:31 +01:00

1698 lines
No EOL
43 KiB
C++

#include "import_asset_dialog.h"
#include "assimp/postprocess.h"
#include "assimp/ProgressHandler.hpp"
#include "assimp/scene.h"
#include "core/crc32.h"
#include "core/FS/ifile.h"
#include "core/FS/file_system.h"
#include "core/FS/os_file.h"
#include "core/log.h"
#include "core/MT/task.h"
#include "core/path_utils.h"
#include "core/system.h"
#include "crnlib.h"
#include "debug/floating_points.h"
#include "editor/world_editor.h"
#include "engine/engine.h"
#include "metadata.h"
#include "ocornut-imgui/imgui.h"
#include "platform_interface.h"
#include "physics/physics_geometry_manager.h"
#include "renderer/model.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb/stb_image.h"
#include "utils.h"
typedef StringBuilder<Lumix::MAX_PATH_LENGTH> PathBuilder;
enum class VertexAttributeDef : Lumix::uint32
{
POSITION,
FLOAT1,
FLOAT2,
FLOAT3,
FLOAT4,
INT1,
INT2,
INT3,
INT4,
SHORT2,
SHORT4,
BYTE4,
NONE
};
struct DDSConvertCallbackData
{
ImportAssetDialog* dialog;
const char* dest_path;
};
crn_bool ddsConvertCallback(crn_uint32 phase_index,
crn_uint32 total_phases,
crn_uint32 subphase_index,
crn_uint32 total_subphases,
void* pUser_data_ptr)
{
DDSConvertCallbackData* data = (DDSConvertCallbackData*)pUser_data_ptr;
float fraction = phase_index / float(total_phases) +
(subphase_index / float(total_subphases)) / total_phases;
data->dialog->setImportMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH + 50>("Saving ")
<< data->dest_path
<< "\n"
<< int(fraction*100)
<< "%%");
return true;
}
static bool saveAsRaw(ImportAssetDialog& dialog,
Lumix::FS::FileSystem& fs,
const Lumix::uint8* image_data,
int image_width,
int image_height,
const char* dest_path,
float scale,
Lumix::IAllocator& allocator)
{
ASSERT(image_data);
dialog.setImportMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH + 30>("Saving ")
<< dest_path);
auto* file = fs.open(fs.getDiskDevice(),
dest_path,
Lumix::FS::Mode::WRITE | Lumix::FS::Mode::CREATE);
if (!file)
{
dialog.setMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH + 30>("Could not save ")
<< dest_path);
return false;
}
Lumix::Array<Lumix::uint16> data(allocator);
data.resize(image_width * image_height);
for (int j = 0; j < image_height; ++j)
{
for (int i = 0; i < image_width; ++i)
{
data[i + j * image_width] = Lumix::uint16(scale * image_data[(i + j * image_width) * 4]);
}
}
file->write((const char*)&data[0], data.size() * sizeof(data[0]));
fs.close(*file);
return true;
}
static bool saveAsDDS(ImportAssetDialog& dialog,
Lumix::FS::FileSystem& fs,
const char* source_path,
const Lumix::uint8* image_data,
int image_width,
int image_height,
const char* dest_path)
{
ASSERT(image_data);
dialog.setImportMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH + 30>("Saving ")
<< dest_path);
DDSConvertCallbackData callback_data;
callback_data.dialog = &dialog;
callback_data.dest_path = dest_path;
crn_uint32 size;
crn_comp_params comp_params;
comp_params.m_width = image_width;
comp_params.m_height = image_height;
comp_params.m_file_type = cCRNFileTypeDDS;
comp_params.m_format = cCRNFmtDXT3;
comp_params.m_quality_level = cCRNMinQualityLevel;
comp_params.m_dxt_quality = cCRNDXTQualitySuperFast;
comp_params.m_dxt_compressor_type = cCRNDXTCompressorRYG;
comp_params.m_pProgress_func = ddsConvertCallback;
comp_params.m_pProgress_func_data = &callback_data;
comp_params.m_num_helper_threads = 3;
comp_params.m_pImages[0][0] = (Lumix::uint32*)image_data;
crn_mipmap_params mipmap_params;
mipmap_params.m_mode = cCRNMipModeGenerateMips;
void* data = crn_compress(comp_params, mipmap_params, size);
if (!data)
{
dialog.setMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH + 30>("Could not convert ")
<< source_path);
return false;
}
auto* file = fs.open(fs.getDiskDevice(),
dest_path,
Lumix::FS::Mode::WRITE | Lumix::FS::Mode::CREATE);
if (!file)
{
dialog.setMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH + 30>("Could not save ")
<< dest_path);
crn_free_block(data);
return false;
}
file->write((const char*)data, size);
fs.close(*file);
crn_free_block(data);
return true;
}
struct ImportTextureTask : public Lumix::MT::Task
{
ImportTextureTask(ImportAssetDialog& dialog)
: Task(dialog.m_editor.getAllocator())
, m_dialog(dialog)
{
}
static void getDestinationPath(const char* output_dir,
const char* source,
bool to_dds,
bool to_raw,
char* out,
int max_size)
{
char basename[Lumix::MAX_PATH_LENGTH];
Lumix::PathUtils::getBasename(basename, sizeof(basename), source);
if (to_dds)
{
PathBuilder dest_path(output_dir);
dest_path << "/" << basename << ".dds";
Lumix::copyString(out, max_size, dest_path);
return;
}
if (to_raw)
{
PathBuilder dest_path(output_dir);
dest_path << "/" << basename << ".raw";
Lumix::copyString(out, max_size, dest_path);
return;
}
char ext[Lumix::MAX_PATH_LENGTH];
Lumix::PathUtils::getExtension(ext, sizeof(ext), source);
PathBuilder dest_path(output_dir);
dest_path << "/" << basename << "." << ext;
Lumix::copyString(out, max_size, dest_path);
}
int task() override
{
m_dialog.setImportMessage("Importing texture...");
int image_width;
int image_height;
int image_comp;
auto* data = stbi_load(m_dialog.m_source, &image_width, &image_height, &image_comp, 4);
if (!data)
{
m_dialog.setMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH + 30>("Could not load ") << m_dialog.m_source);
return -1;
}
char dest_path[Lumix::MAX_PATH_LENGTH];
getDestinationPath(m_dialog.m_output_dir,
m_dialog.m_source,
m_dialog.m_convert_to_dds,
m_dialog.m_convert_to_raw,
dest_path,
Lumix::lengthOf(dest_path));
if (m_dialog.m_convert_to_dds)
{
m_dialog.setImportMessage("Converting to DDS...");
saveAsDDS(m_dialog,
m_dialog.m_editor.getEngine().getFileSystem(),
m_dialog.m_source,
data,
image_width,
image_height,
dest_path);
}
else if (m_dialog.m_convert_to_raw)
{
m_dialog.setImportMessage("Converting to RAW...");
saveAsRaw(m_dialog,
m_dialog.m_editor.getEngine().getFileSystem(),
data,
image_width,
image_height,
dest_path,
m_dialog.m_raw_texture_scale,
m_dialog.m_editor.getAllocator());
}
else
{
m_dialog.setImportMessage("Copying...");
if (!Lumix::copyFile(m_dialog.m_source, dest_path))
{
m_dialog.setMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH * 2 + 30>("Could not copy ")
<< m_dialog.m_source
<< " to "
<< dest_path);
}
}
stbi_image_free(data);
return 0;
}
ImportAssetDialog& m_dialog;
}; // struct ImportTextureTask
struct ImportTask : public Lumix::MT::Task
{
struct ProgressHandler : public Assimp::ProgressHandler
{
bool Update(float percentage) override
{
m_task->m_dialog.setImportMessage(
StringBuilder<50>("Importing... ") << int(percentage*100) << "%%");
return true;
}
ImportTask* m_task;
};
ImportTask(ImportAssetDialog& dialog)
: Task(dialog.m_editor.getAllocator())
, m_dialog(dialog)
{
m_dialog.m_importer.SetProgressHandler(&m_progress_handler);
}
~ImportTask()
{
m_dialog.m_importer.SetProgressHandler(nullptr);
}
int task() override
{
m_progress_handler.m_task = this;
Lumix::enableFloatingPointTraps(false);
m_dialog.m_importer.SetPropertyInteger(
AI_CONFIG_PP_RVC_FLAGS, aiComponent_LIGHTS | aiComponent_CAMERAS);
unsigned int flags = aiProcess_JoinIdenticalVertices | aiProcess_RemoveComponent |
aiProcess_GenUVCoords | aiProcess_RemoveRedundantMaterials |
aiProcess_Triangulate | aiProcess_LimitBoneWeights |
aiProcess_OptimizeGraph | aiProcess_CalcTangentSpace;
flags |= m_dialog.m_gen_smooth_normal ? aiProcess_GenSmoothNormals : aiProcess_GenNormals;
flags |= m_dialog.m_optimize_mesh_on_import ? aiProcess_OptimizeMeshes : 0;
const aiScene* scene =
m_dialog.m_importer.ReadFile(m_dialog.m_source, flags);
if (!scene || !scene->mMeshes || !scene->mMeshes[0]->mTangents)
{
m_dialog.setMessage(m_dialog.m_importer.GetErrorString());
Lumix::g_log_error.log("import")
<< m_dialog.m_importer.GetErrorString();
}
else
{
m_dialog.m_mesh_mask.resize(scene->mNumMeshes);
for (int i = 0; i < m_dialog.m_mesh_mask.size(); ++i)
{
m_dialog.m_mesh_mask[i] = true;
}
}
Lumix::enableFloatingPointTraps(true);
return 0;
}
ImportAssetDialog& m_dialog;
ProgressHandler m_progress_handler;
}; // struct ImportTask
struct ConvertTask : public Lumix::MT::Task
{
struct SkinInfo
{
SkinInfo()
{
memset(this, 0, sizeof(SkinInfo));
index = 0;
}
float weights[4];
Lumix::uint16 bone_indices[4];
int index;
};
ConvertTask(ImportAssetDialog& dialog)
: Task(dialog.m_editor.getAllocator())
, m_dialog(dialog)
, m_filtered_meshes(dialog.m_editor.getAllocator())
{
}
bool saveEmbeddedTextures(const aiScene* scene)
{
bool success = true;
m_dialog.m_saved_embedded_textures.clear();
for (unsigned int i = 0; i < scene->mNumTextures; ++i)
{
const aiTexture* texture = scene->mTextures[i];
if (texture->mHeight != 0)
{
m_dialog.setMessage(
"Uncompressed texture embedded. This is not supported.");
return false;
}
PathBuilder texture_name("texture");
texture_name << i << ".dds";
int width, height, comp;
auto data = stbi_load_from_memory((stbi_uc*)texture->pcData, texture->mWidth, &width, &height, &comp, 4);
if (!data) continue;
m_dialog.m_saved_embedded_textures.push(
Lumix::string(texture_name, m_dialog.m_editor.getAllocator()));
PathBuilder dest(m_dialog.m_texture_output_dir[0] ? m_dialog.m_texture_output_dir
: m_dialog.m_output_dir);
dest << "/" << texture_name;
bool saved = saveAsDDS(m_dialog,
m_dialog.m_editor.getEngine().getFileSystem(),
"Embedded texture",
data,
width,
height,
dest);
success = success && saved;
stbi_image_free(data);
}
return success;
}
bool saveTexture(const char* texture_path,
const char* source_mesh_dir,
Lumix::FS::IFile& material_file) const
{
Lumix::string texture_source_path(texture_path,
m_dialog.m_editor.getAllocator());
int mapping_index = m_dialog.m_path_mapping.find(texture_source_path);
if (mapping_index >= 0)
{
texture_source_path = m_dialog.m_path_mapping.at(mapping_index);
}
bool is_embedded = false;
if (texture_source_path[0] == '*')
{
is_embedded = true;
int index;
Lumix::fromCString(texture_source_path.c_str() + 1,
texture_source_path.length() - 1,
&index);
texture_source_path = m_dialog.m_saved_embedded_textures[index];
}
Lumix::PathUtils::FileInfo texture_info(texture_source_path.c_str());
if (!m_dialog.m_texture_output_dir[0])
{
material_file << "\t, \"texture\" : {\n\t\t\"source\" : \"" << texture_info.m_basename
<< ".";
material_file << (m_dialog.m_convert_to_dds ? "dds" : texture_info.m_extension);
material_file << "\"\n }\n";
}
else
{
material_file << "\t, \"texture\" : {\n\t\t\"source\" : \"";
char from_root_path[Lumix::MAX_PATH_LENGTH];
m_dialog.m_editor.getRelativePath(from_root_path,
Lumix::lengthOf(from_root_path),
m_dialog.m_texture_output_dir);
material_file << "/" << from_root_path;
material_file << texture_info.m_basename << ".";
material_file << (m_dialog.m_convert_to_dds ? "dds" : texture_info.m_extension);
material_file << "\"\n }\n";
}
bool is_already_saved =
m_dialog.m_saved_textures.indexOf(texture_source_path) >= 0;
if (is_embedded || is_already_saved) return true;
PathBuilder source_absolute(source_mesh_dir);
source_absolute << "/" << texture_source_path.c_str();
const char* source =
Lumix::PathUtils::isAbsolute(texture_source_path.c_str())
? texture_source_path.c_str()
: source_absolute;
if (m_dialog.m_convert_to_dds &&
Lumix::compareString(texture_info.m_extension, "dds") != 0)
{
PathBuilder dest(m_dialog.m_texture_output_dir[0] ? m_dialog.m_texture_output_dir
: m_dialog.m_output_dir);
dest << "/" << texture_info.m_basename << ".dds";
int image_width, image_height, dummy;
auto data = stbi_load(source, &image_width, &image_height, &dummy, 4);
if (!data)
{
StringBuilder<Lumix::MAX_PATH_LENGTH + 20> error_msg(
"Could not load image ", source);
m_dialog.setMessage(error_msg);
return false;
}
if (!saveAsDDS(m_dialog,
m_dialog.m_editor.getEngine().getFileSystem(),
source,
data,
image_width,
image_height,
dest))
{
stbi_image_free(data);
m_dialog.setMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH * 2 + 20>(
"Error converting ", source, " to ", dest));
return false;
}
stbi_image_free(data);
}
else
{
PathBuilder dest(m_dialog.m_output_dir);
dest << "/" << texture_info.m_basename << "."
<< texture_info.m_extension;
if (Lumix::compareString(source, dest) != 0 && !Lumix::copyFile(source, dest))
{
m_dialog.setMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH * 2 + 20>(
"Error copying ", source, " to ", dest));
return false;
}
}
m_dialog.m_saved_textures.push(texture_source_path);
return true;
}
bool saveLumixMaterials()
{
if (!m_dialog.m_import_materials) return true;
m_dialog.setImportMessage("Importing materials...");
const aiScene* scene = m_dialog.m_importer.GetScene();
if (!saveEmbeddedTextures(scene))
{
m_dialog.setMessage("Failed to import embedded texture");
}
m_dialog.m_saved_textures.clear();
int undefined_count = 0;
char source_mesh_dir[Lumix::MAX_PATH_LENGTH];
Lumix::PathUtils::getDir(
source_mesh_dir, sizeof(source_mesh_dir), m_dialog.m_source);
for (unsigned int i = 0; i < scene->mNumMaterials; ++i)
{
const aiMaterial* material = scene->mMaterials[i];
if (!saveMaterial(material, source_mesh_dir, &undefined_count))
{
return false;
}
}
return true;
}
bool saveMaterial(const aiMaterial* material,
const char* source_mesh_dir,
int* undefined_count) const
{
ASSERT(undefined_count);
aiString material_name;
material->Get(AI_MATKEY_NAME, material_name);
PathBuilder output_material_name(m_dialog.m_output_dir);
output_material_name << "/" << material_name.C_Str() << ".mat";
m_dialog.setImportMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH + 30>("Converting ")
<< output_material_name);
auto& fs = m_dialog.m_editor.getEngine().getFileSystem();
auto* file =
fs.open(fs.getDiskDevice(),
output_material_name,
Lumix::FS::Mode::CREATE | Lumix::FS::Mode::WRITE);
if (!file)
{
m_dialog.setMessage(
StringBuilder<20 + Lumix::MAX_PATH_LENGTH>(
"Could not create ",
output_material_name));
return false;
}
const aiScene* scene = m_dialog.m_importer.GetScene();
*file << "{\n\t\"shader\" : \"shaders/"
<< (isSkinned(scene, material) ? "skinned" : "rigid")
<< ".shd\"\n";
if (material->GetTextureCount(aiTextureType_DIFFUSE) == 1)
{
aiString texture_path;
material->GetTexture(aiTextureType_DIFFUSE, 0, &texture_path);
saveTexture(texture_path.C_Str(), source_mesh_dir, *file);
}
else
{
saveTexture(PathBuilder("undefined") << *undefined_count << ".dds",
source_mesh_dir,
*file);
++*undefined_count;
}
if (material->GetTextureCount(aiTextureType_NORMALS) == 1)
{
aiString texture_path;
material->GetTexture(aiTextureType_NORMALS, 0, &texture_path);
saveTexture(texture_path.C_Str(), source_mesh_dir, *file);
}
else if (material->GetTextureCount(aiTextureType_HEIGHT) == 1)
{
aiString texture_path;
material->GetTexture(aiTextureType_HEIGHT, 0, &texture_path);
saveTexture(texture_path.C_Str(), source_mesh_dir, *file);
}
else if (material->GetTextureCount(aiTextureType_NORMALS) > 1)
{
m_dialog.setMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH + 20>(
"Too many normal maps in ", material_name.C_Str()));
fs.close(*file);
return false;
}
file->write("}", 1);
fs.close(*file);
return true;
}
int task() override
{
if (saveLumixPhysics() && saveLumixModel() && saveLumixMaterials())
{
m_dialog.setMessage("Success.");
}
return 0;
}
static int countNodes(const aiNode* node)
{
int count = 1;
for (unsigned int i = 0; i < node->mNumChildren; ++i)
{
count += countNodes(node->mChildren[i]);
}
return count;
}
static bool isSkinned(const aiMesh* mesh)
{
return mesh->mNumBones > 0;
}
static bool isSkinned(const aiScene* scene, const aiMaterial* material)
{
for (unsigned int i = 0; i < scene->mNumMeshes; ++i)
{
if (scene->mMaterials[scene->mMeshes[i]->mMaterialIndex] == material &&
isSkinned(scene->mMeshes[i]))
{
return true;
}
}
return false;
}
static void getBoneNamesHashes(const aiNode* node, Lumix::Array<int>& node_names)
{
node_names.push(Lumix::crc32(node->mName.C_Str()));
for (unsigned int i = 0; i < node->mNumChildren; ++i)
{
getBoneNamesHashes(node->mChildren[i], node_names);
}
}
void fillSkinInfo(const aiScene* scene,
Lumix::Array<SkinInfo>& infos,
int vertices_count) const
{
Lumix::Array<int> node_names(m_dialog.m_editor.getAllocator());
getBoneNamesHashes(scene->mRootNode, node_names);
infos.resize(vertices_count);
int offset = 0;
for (auto* mesh : m_filtered_meshes)
{
for (unsigned int j = 0; j < mesh->mNumBones; ++j)
{
const aiBone* bone = mesh->mBones[j];
int bone_index = node_names.indexOf(Lumix::crc32(bone->mName.C_Str()));
for (unsigned int k = 0; k < bone->mNumWeights; ++k)
{
auto& info = infos[offset + bone->mWeights[k].mVertexId];
info.weights[info.index] = bone->mWeights[k].mWeight;
info.bone_indices[info.index] = bone_index;
++info.index;
}
}
offset += mesh->mNumVertices;
}
}
static Lumix::uint32 packuint32(Lumix::uint8 _x, Lumix::uint8 _y, Lumix::uint8 _z, Lumix::uint8 _w)
{
union
{
Lumix::uint32 ui32;
Lumix::uint8 arr[4];
} un;
un.arr[0] = _x;
un.arr[1] = _y;
un.arr[2] = _z;
un.arr[3] = _w;
return un.ui32;
}
static Lumix::uint32 packF4u(const aiVector3D& vec)
{
const Lumix::uint8 xx = Lumix::uint8(vec.x * 127.0f + 128.0f);
const Lumix::uint8 yy = Lumix::uint8(vec.y * 127.0f + 128.0f);
const Lumix::uint8 zz = Lumix::uint8(vec.z * 127.0f + 128.0f);
const Lumix::uint8 ww = Lumix::uint8(0);
return packuint32(xx, yy, zz, ww);
}
void writeGeometry(Lumix::FS::IFile& file) const
{
const aiScene* scene = m_dialog.m_importer.GetScene();
Lumix::int32 indices_count = 0;
int vertices_count = 0;
Lumix::int32 vertices_size = 0;
for (auto* mesh : m_filtered_meshes)
{
indices_count += mesh->mNumFaces * 3;
vertices_count += mesh->mNumVertices;
vertices_size += mesh->mNumVertices * getVertexSize(mesh);
}
file.write((const char*)&indices_count, sizeof(indices_count));
Lumix::int32 polygon_idx = 0;
for (auto* mesh : m_filtered_meshes)
{
for (unsigned int j = 0; j < mesh->mNumFaces; ++j)
{
polygon_idx = mesh->mFaces[j].mIndices[0];
file.write((const char*)&polygon_idx, sizeof(polygon_idx));
polygon_idx = mesh->mFaces[j].mIndices[1];
file.write((const char*)&polygon_idx, sizeof(polygon_idx));
polygon_idx = mesh->mFaces[j].mIndices[2];
file.write((const char*)&polygon_idx, sizeof(polygon_idx));
}
}
file.write((const char*)&vertices_size, sizeof(vertices_size));
Lumix::Array<SkinInfo> skin_infos(m_dialog.m_editor.getAllocator());
fillSkinInfo(scene, skin_infos, vertices_count);
int skin_index = 0;
aiMatrix3x3 normal_matrix(scene->mRootNode->mTransformation);
for (auto* mesh : m_filtered_meshes)
{
bool is_skinned = isSkinned(mesh);
for (unsigned int j = 0; j < mesh->mNumVertices; ++j)
{
if (is_skinned)
{
file.write((const char*)skin_infos[skin_index].weights,
sizeof(skin_infos[skin_index].weights));
file.write((const char*)skin_infos[skin_index].bone_indices,
sizeof(skin_infos[skin_index].bone_indices));
}
++skin_index;
auto v = scene->mRootNode->mTransformation * mesh->mVertices[j];
Lumix::Vec3 position(v.x, v.y, v.z);
file.write((const char*)&position, sizeof(position));
if (mesh->mColors[0])
{
auto assimp_color = mesh->mColors[0][j];
Lumix::uint8 color[4];
color[0] = Lumix::uint8(assimp_color.r * 255);
color[1] = Lumix::uint8(assimp_color.g * 255);
color[2] = Lumix::uint8(assimp_color.b * 255);
color[3] = Lumix::uint8(assimp_color.a * 255);
file.write(color, sizeof(color));
}
auto normal = normal_matrix * mesh->mNormals[j];
Lumix::uint32 int_normal = packF4u(normal);
file.write((const char*)&int_normal, sizeof(int_normal));
if (mesh->mTangents)
{
auto tangent = mesh->mTangents[j];
Lumix::uint32 int_tangent = packF4u(tangent);
file.write((const char*)&int_tangent, sizeof(int_tangent));
}
auto uv = mesh->mTextureCoords[0][j];
uv.y = -uv.y;
file.write((const char*)&uv, sizeof(uv.x) + sizeof(uv.y));
}
}
}
static int getAttributeCount(const aiMesh* mesh)
{
int count = 3; // position, normal, uv
if (isSkinned(mesh)) count += 2;
if (mesh->mColors[0]) ++count;
if (mesh->mTangents) ++count;
return count;
}
static int getVertexSize(const aiMesh* mesh)
{
static const int POSITION_SIZE = sizeof(float) * 3;
static const int NORMAL_SIZE = sizeof(Lumix::uint8) * 4;
static const int TANGENT_SIZE = sizeof(Lumix::uint8) * 4;
static const int UV_SIZE = sizeof(float) * 2;
static const int COLOR_SIZE = sizeof(Lumix::uint8) * 4;
static const int BONE_INDICES_WEIGHTS_SIZE =
sizeof(float) * 4 + sizeof(Lumix::uint16) * 4;
int size = POSITION_SIZE + NORMAL_SIZE + UV_SIZE;
if (mesh->mTangents) size += TANGENT_SIZE;
if (mesh->mColors[0]) size += COLOR_SIZE;
if (isSkinned(mesh)) size += BONE_INDICES_WEIGHTS_SIZE;
return size;
}
static const aiNode* getOwner(const aiNode* node, int mesh_index)
{
for (int i = 0; i < (int)node->mNumMeshes; ++i)
{
if (node->mMeshes[i] == mesh_index) return node;
}
for(int i = 0; i < (int)node->mNumChildren; ++i)
{
auto* child = node->mChildren[i];
auto* owner = getOwner(child, mesh_index);
if (owner) return owner;
}
return nullptr;
}
const aiNode* getOwner(const aiMesh* mesh) const
{
const aiScene* scene = m_dialog.m_importer.GetScene();
for (int i = 0; i < int(scene->mNumMeshes); ++i)
{
if (scene->mMeshes[i] == mesh) return getOwner(scene->mRootNode, i);
}
return nullptr;
}
aiString getMeshName(const aiMesh* mesh) const
{
aiString mesh_name = mesh->mName;
int length = Lumix::stringLength(mesh_name.C_Str());
if (length == 0)
{
const auto* node = getOwner(mesh);
if (node)
{
mesh_name = node->mName;
}
}
return mesh_name;
}
void writeMeshes(Lumix::FS::IFile& file) const
{
const aiScene* scene = m_dialog.m_importer.GetScene();
Lumix::int32 mesh_count = 0;
for (int i = 0; i < m_dialog.m_mesh_mask.size(); ++i)
{
if (m_dialog.m_mesh_mask[i]) ++mesh_count;
}
file.write((const char*)&mesh_count, sizeof(mesh_count));
Lumix::int32 attribute_array_offset = 0;
Lumix::int32 indices_offset = 0;
for (auto* mesh : m_filtered_meshes)
{
int vertex_size = getVertexSize(mesh);
aiString material_name;
scene->mMaterials[mesh->mMaterialIndex]->Get(AI_MATKEY_NAME,
material_name);
Lumix::int32 length = Lumix::stringLength(material_name.C_Str());
file.write((const char*)&length, sizeof(length));
file.write((const char*)material_name.C_Str(), length);
file.write((const char*)&attribute_array_offset,
sizeof(attribute_array_offset));
Lumix::int32 attribute_array_size = mesh->mNumVertices * vertex_size;
attribute_array_offset += attribute_array_size;
file.write((const char*)&attribute_array_size,
sizeof(attribute_array_size));
file.write((const char*)&indices_offset, sizeof(indices_offset));
Lumix::int32 mesh_tri_count = mesh->mNumFaces;
indices_offset += mesh->mNumFaces * 3;
file.write((const char*)&mesh_tri_count, sizeof(mesh_tri_count));
aiString mesh_name = getMeshName(mesh);
length = Lumix::stringLength(mesh_name.C_Str());
file.write((const char*)&length, sizeof(length));
file.write((const char*)mesh_name.C_Str(), length);
Lumix::int32 attribute_count = getAttributeCount(mesh);
file.write((const char*)&attribute_count, sizeof(attribute_count));
if (isSkinned(mesh))
{
writeAttribute("in_weights", VertexAttributeDef::FLOAT4, file);
writeAttribute("in_indices", VertexAttributeDef::SHORT4, file);
}
writeAttribute("in_position", VertexAttributeDef::POSITION, file);
if (mesh->mColors[0]) writeAttribute("in_colors", VertexAttributeDef::BYTE4, file);
writeAttribute("in_normal", VertexAttributeDef::BYTE4, file);
if (mesh->mTangents) writeAttribute("in_tangents", VertexAttributeDef::BYTE4, file);
writeAttribute("in_tex_coords", VertexAttributeDef::FLOAT2, file);
}
}
static void writeAttribute(const char* attribute_name,
VertexAttributeDef attribute_type,
Lumix::FS::IFile& file)
{
Lumix::uint32 length = Lumix::stringLength(attribute_name);
file.write((const char*)&length, sizeof(length));
file.write(attribute_name, length);
Lumix::uint32 type = (Lumix::uint32)attribute_type;
file.write((const char*)&type, sizeof(type));
}
static void writeNode(Lumix::FS::IFile& file, const aiNode* node, aiMatrix4x4 parent_transform)
{
Lumix::int32 len = Lumix::stringLength(node->mName.C_Str());
file.write((const char*)&len, sizeof(len));
file.write(node->mName.C_Str(), node->mName.length + 1);
if (node->mParent)
{
Lumix::int32 len = Lumix::stringLength(node->mParent->mName.C_Str());
file.write((const char*)&len, sizeof(len));
file.write(node->mParent->mName.C_Str(), node->mParent->mName.length);
}
else
{
Lumix::int32 len = 0;
file.write((const char*)&len, sizeof(len));
}
aiQuaterniont<float> rot;
aiVector3t<float> pos;
(parent_transform * node->mTransformation).DecomposeNoScaling(rot, pos);
file.write((const char*)&pos, sizeof(pos));
file.write((const char*)&rot.x, sizeof(rot.x));
file.write((const char*)&rot.y, sizeof(rot.y));
file.write((const char*)&rot.z, sizeof(rot.z));
file.write((const char*)&rot.w, sizeof(rot.w));
for (unsigned int i = 0; i < node->mNumChildren; ++i)
{
writeNode(
file, node->mChildren[i], parent_transform * node->mTransformation);
}
}
void writeLods(Lumix::FS::IFile& file) const
{
Lumix::int32 lods[] = { -1, -1, -1, -1, -1, -1, -1, -1 };
Lumix::int32 lod_count = -1;
float factors[8];
for (int i = 0; i < m_filtered_meshes.size(); ++i)
{
int lod = getMeshLOD(&m_filtered_meshes[i]);
if (lod < 0 || lod >= Lumix::lengthOf(lods)) break;
lods[lod] = i;
factors[lod] = getMeshLODFactor(m_filtered_meshes[i]);
lod_count = Lumix::Math::maxValue(lod_count, lod + 1);
}
if (lods[0] < 0)
{
lod_count = 1;
file.write((const char*)&lod_count, sizeof(lod_count));
Lumix::int32 to_mesh = m_filtered_meshes.size() - 1;
file.write((const char*)&to_mesh, sizeof(to_mesh));
float distance = FLT_MAX;
file.write((const char*)&distance, sizeof(distance));
}
else
{
file.write((const char*)&lod_count, sizeof(lod_count));
for (int i = 0; i < lod_count; ++i)
{
Lumix::int32 to_mesh = lods[i];
file.write((const char*)&to_mesh, sizeof(to_mesh));
float factor = i == lod_count - 1 ? FLT_MAX : factors[i];
file.write((const char*)&factor, sizeof(factor));
}
}
}
void writeSkeleton(Lumix::FS::IFile& file) const
{
const aiScene* scene = m_dialog.m_importer.GetScene();
Lumix::int32 count = countNodes(scene->mRootNode);
if (count == 1)
{
count = 0;
}
file.write((const char*)&count, sizeof(count));
if (count > 0)
{
writeNode(file, scene->mRootNode, aiMatrix4x4());
}
}
void writePhysicsHeader(Lumix::FS::IFile& file) const
{
Lumix::PhysicsGeometry::Header header;
header.m_magic = Lumix::PhysicsGeometry::HEADER_MAGIC;
header.m_version = (Lumix::uint32)Lumix::PhysicsGeometry::Versions::LAST;
header.m_convex = (Lumix::uint32)m_dialog.m_make_convex;
file.write((const char*)&header, sizeof(header));
}
bool saveLumixPhysics()
{
if (!m_dialog.m_import_physics) return true;
m_dialog.setImportMessage("Importing physics...");
char filename[Lumix::MAX_PATH_LENGTH];
Lumix::PathUtils::getBasename(filename, sizeof(filename), m_dialog.m_source);
Lumix::catString(filename, ".phy");
auto& fs = m_dialog.m_editor.getEngine().getFileSystem();
PathBuilder phy_path(m_dialog.m_output_dir);
phy_path << "/" << filename;
Lumix::FS::IFile* file =
fs.open(fs.getDiskDevice(), phy_path, Lumix::FS::Mode::CREATE | Lumix::FS::Mode::WRITE);
if (!file)
{
Lumix::g_log_error.log("import") << "Could not create file " << phy_path;
return false;
}
writePhysicsHeader(*file);
Lumix::int32 count = 0;
for (auto* mesh : m_filtered_meshes)
{
count += (Lumix::int32)mesh->mNumVertices;
}
file->write((const char*)&count, sizeof(count));
for (auto* mesh : m_filtered_meshes)
{
file->write(
(const char*)mesh->mVertices, sizeof(mesh->mVertices[0]) * mesh->mNumVertices);
}
if (!m_dialog.m_make_convex) writePhysiscTriMesh(*file);
fs.close(*file);
return true;
}
void writePhysiscTriMesh(Lumix::FS::IFile& file)
{
int count = 0;
for (auto* mesh : m_filtered_meshes)
{
count += (int)mesh->mNumFaces * 3;
}
file.write((const char*)&count, sizeof(count));
int offset = 0;
for (auto* mesh : m_filtered_meshes)
{
for (unsigned int j = 0; j < mesh->mNumFaces; ++j)
{
ASSERT(mesh->mFaces[j].mNumIndices == 3);
Lumix::uint32 index = mesh->mFaces[j].mIndices[0] + offset;
file.write((const char*)&index, sizeof(index));
index = mesh->mFaces[j].mIndices[1] + offset;
file.write((const char*)&index, sizeof(index));
index = mesh->mFaces[j].mIndices[2] + offset;
file.write((const char*)&index, sizeof(index));
}
offset += mesh->mNumVertices;
}
}
bool checkModel() const
{
for (auto* mesh : m_filtered_meshes)
{
if (!mesh->HasNormals())
{
m_dialog.setMessage(StringBuilder<256>(
"Mesh ", getMeshName(mesh).C_Str(), " has no normals."));
return false;
}
if (!mesh->HasPositions())
{
m_dialog.setMessage(StringBuilder<256>(
"Mesh ", getMeshName(mesh).C_Str(), " has no positions."));
return false;
}
if (!mesh->HasTextureCoords(0))
{
m_dialog.setMessage(StringBuilder<256>(
"Mesh ", getMeshName(mesh).C_Str(), " has no texture coords."));
return false;
}
}
return true;
}
static void writeModelHeader(Lumix::FS::IFile& file)
{
Lumix::Model::FileHeader header;
header.m_magic = Lumix::Model::FILE_MAGIC;
header.m_version = (Lumix::uint32)Lumix::Model::FileVersion::LATEST;
file.write((const char*)&header, sizeof(header));
}
float getMeshLODFactor(const aiMesh* mesh) const
{
const char* mesh_name = getMeshName(mesh).C_Str();
int len = Lumix::stringLength(mesh_name);
if (len < 5) return FLT_MAX;
const char* last = mesh_name + len - 1;
while (last > mesh_name && *last >= '0' && *last <= '9')
{
--last;
}
++last;
if (last < mesh_name + 4) FLT_MAX;
if (Lumix::compareStringN(last - 4, "_LOD", 4) != 0) return FLT_MAX;
const char* end_of_factor = last - 4;
const char* begin_factor = end_of_factor - 1;
if (begin_factor <= mesh_name) return FLT_MAX;
while (*begin_factor != '_' && begin_factor > mesh_name)
{
--begin_factor;
}
++begin_factor;
if (begin_factor == end_of_factor) return FLT_MAX;
int factor;
Lumix::fromCString(begin_factor, int(end_of_factor - begin_factor), &factor);
return float(factor);
}
int getMeshLOD(const aiMesh* const * mesh_ptr) const
{
const aiMesh* const mesh = *mesh_ptr;
const char* mesh_name = getMeshName(mesh).C_Str();
int len = Lumix::stringLength(mesh_name);
if (len < 5) return -1;
const char* last = mesh_name + len - 1;
while (last > mesh_name && *last >= '0' && *last <= '9')
{
--last;
}
++last;
if (last < mesh_name + 4) return -1;
if (Lumix::compareStringN(last - 4, "_LOD", 4) != 0) return -1;
int lod;
Lumix::fromCString(last, len - int(last - mesh_name), &lod);
return lod;
}
void filterMeshes()
{
const aiScene* scene = m_dialog.m_importer.GetScene();
m_filtered_meshes.clear();
for (unsigned int i = 0; i < scene->mNumMeshes; ++i)
{
if (m_dialog.m_mesh_mask[i]) m_filtered_meshes.push(scene->mMeshes[i]);
}
static ConvertTask* that = this;
auto cmpMeshes = [](const void* a, const void* b) -> int {
auto a_mesh = static_cast<aiMesh* const *>(a);
auto b_mesh = static_cast<aiMesh* const *>(b);
return that->getMeshLOD(a_mesh) - that->getMeshLOD(b_mesh);
};
qsort(&m_filtered_meshes[0], m_filtered_meshes.size(), sizeof(m_filtered_meshes[0]), cmpMeshes);
}
bool saveLumixModel()
{
ASSERT(m_dialog.m_output_dir[0] != '\0');
if (!m_dialog.m_import_model) return true;
if (!checkModel()) return false;
m_dialog.setImportMessage("Importing model...");
Lumix::makePath(m_dialog.m_output_dir);
if (m_dialog.m_texture_output_dir[0]) Lumix::makePath(m_dialog.m_texture_output_dir);
char basename[Lumix::MAX_PATH_LENGTH];
Lumix::PathUtils::getBasename(basename, sizeof(basename), m_dialog.m_source);
PathBuilder path(m_dialog.m_output_dir);
path << "/" << basename << ".msh";
auto& fs = m_dialog.m_editor.getEngine().getFileSystem();
Lumix::FS::IFile* file =
fs.open(fs.getDiskDevice(),
path,
Lumix::FS::Mode::CREATE | Lumix::FS::Mode::WRITE);
if (!file)
{
m_dialog.setMessage(
StringBuilder<Lumix::MAX_PATH_LENGTH + 15>(
"Failed to open ", path));
return false;
}
filterMeshes();
writeModelHeader(*file);
writeMeshes(*file);
writeGeometry(*file);
writeSkeleton(*file);
writeLods(*file);
fs.close(*file);
return true;
}
Lumix::Array<aiMesh*> m_filtered_meshes;
ImportAssetDialog& m_dialog;
}; // struct ConvertTask
ImportAssetDialog::ImportAssetDialog(Lumix::WorldEditor& editor, Metadata& metadata)
: m_source_exists(false)
, m_metadata(metadata)
, m_task(nullptr)
, m_editor(editor)
, m_import_physics(false)
, m_import_model(true)
, m_is_converting(false)
, m_is_importing(false)
, m_is_importing_texture(false)
, m_mutex(false)
, m_make_convex(false)
, m_saved_textures(editor.getAllocator())
, m_saved_embedded_textures(editor.getAllocator())
, m_path_mapping(editor.getAllocator())
, m_mesh_mask(editor.getAllocator())
, m_convert_to_dds(false)
, m_convert_to_raw(false)
, m_raw_texture_scale(1)
{
m_is_opened = false;
m_message[0] = '\0';
m_import_message[0] = '\0';
m_task = nullptr;
m_source[0] = '\0';
m_output_dir[0] = '\0';
m_texture_output_dir[0] = '\0';
}
ImportAssetDialog::~ImportAssetDialog()
{
if (m_task)
{
m_task->destroy();
LUMIX_DELETE(m_editor.getAllocator(), m_task);
}
}
bool ImportAssetDialog::checkTexture(const char* source_dir, const char* texture_path, const char* message)
{
if (texture_path[0] == '*') return true;
const char* path = Lumix::PathUtils::isAbsolute(texture_path) || !source_dir
? texture_path
: PathBuilder(source_dir) << "/" << texture_path;
if (Lumix::fileExists(path)) return true;
char new_path[Lumix::MAX_PATH_LENGTH];
Lumix::messageBox(message ? message : StringBuilder<Lumix::MAX_PATH_LENGTH + 40>("Texture ")
<< path
<< " not found, please locate it");
if (!PlatformInterface::getOpenFilename(new_path, sizeof(new_path), "All\0*.*\0")) return false;
Lumix::string old_path_str(path, m_editor.getAllocator());
Lumix::string new_path_str(new_path, m_editor.getAllocator());
m_path_mapping.erase(old_path_str);
m_path_mapping.insert(old_path_str, new_path_str);
return true;
}
bool ImportAssetDialog::checkTextures()
{
if (!m_import_materials) return true;
auto scene = m_importer.GetScene();
int undefined_count = 0;
char source_dir[Lumix::MAX_PATH_LENGTH];
Lumix::PathUtils::getDir(source_dir, sizeof(source_dir), m_source);
for (unsigned int i = 0; i < scene->mNumMaterials; ++i)
{
const aiMaterial* material = scene->mMaterials[i];
int types[] = { aiTextureType_DIFFUSE, aiTextureType_NORMALS, aiTextureType_HEIGHT };
for (auto type : types)
{
for (unsigned int j = 0; j < material->GetTextureCount((aiTextureType)type); ++j)
{
aiString texture_path;
material->GetTexture((aiTextureType)type, j, &texture_path);
if (!checkTexture(source_dir, texture_path.C_Str(), nullptr))
{
return false;
}
}
}
if (material->GetTextureCount(aiTextureType_DIFFUSE) != 1)
{
PathBuilder texture_filename("undefined");
texture_filename << undefined_count << ".dds";
aiString material_name;
material->Get(AI_MATKEY_NAME, material_name);
StringBuilder<200> message(
"Please select diffuse texture for material ");
message << material_name.C_Str();
if (!checkTexture(nullptr, texture_filename, message))
{
return false;
}
++undefined_count;
}
}
return true;
}
static bool isImage(const char* path)
{
char ext[10];
Lumix::PathUtils::getExtension(ext, sizeof(ext), path);
static const char* image_extensions[] = {
"jpg", "jpeg", "png", "tga", "bmp", "psd", "gif", "hdr", "pic", "pnm"};
for (auto image_ext : image_extensions)
{
if (Lumix::compareString(ext, image_ext) == 0)
{
return true;
}
}
return false;
}
void ImportAssetDialog::checkSource()
{
if (m_output_dir[0] == '\0')
{
Lumix::PathUtils::getDir(m_output_dir, sizeof(m_output_dir), m_source);
}
m_source_exists = Lumix::fileExists(m_source);
if (!m_source_exists)
{
m_importer.FreeScene();
return;
}
if (isImage(m_source))
{
m_importer.FreeScene();
return;
}
ASSERT(!m_task);
setImportMessage("Importing...");
m_is_importing = true;
m_task = LUMIX_NEW(m_editor.getAllocator(), ImportTask)(*this);
m_task->create("ImportAssetTask");
m_task->run();
}
void ImportAssetDialog::setMessage(const char* message)
{
Lumix::MT::SpinLock lock(m_mutex);
Lumix::copyString(m_message, message);
}
void ImportAssetDialog::setImportMessage(const char* message)
{
Lumix::MT::SpinLock lock(m_mutex);
Lumix::copyString(m_import_message, message);
}
void ImportAssetDialog::getMessage(char* msg, int max_size)
{
Lumix::MT::SpinLock lock(m_mutex);
Lumix::copyString(msg, max_size, m_message);
}
bool ImportAssetDialog::hasMessage()
{
Lumix::MT::SpinLock lock(m_mutex);
return m_message[0] != '\0';
}
void ImportAssetDialog::convert()
{
ASSERT(!m_task);
if (!checkTextures()) return;
setImportMessage("Converting...");
m_is_converting = true;
m_task = LUMIX_NEW(m_editor.getAllocator(), ConvertTask)(*this);
m_task->create("ConvertAssetTask");
m_task->run();
}
void ImportAssetDialog::importTexture()
{
ASSERT(!m_task);
setImportMessage("Importing texture...");
char dest_path[Lumix::MAX_PATH_LENGTH];
ImportTextureTask::getDestinationPath(m_output_dir,
m_source,
m_convert_to_dds,
m_convert_to_raw,
dest_path,
Lumix::lengthOf(dest_path));
char tmp[Lumix::MAX_PATH_LENGTH];
Lumix::PathUtils::normalize(dest_path, tmp, Lumix::lengthOf(tmp));
m_editor.getRelativePath(dest_path, Lumix::lengthOf(dest_path), tmp);
Lumix::uint32 hash = Lumix::crc32(dest_path);
m_metadata.setString(hash, Lumix::crc32("source"), m_source);
m_is_importing_texture = true;
m_task = LUMIX_NEW(m_editor.getAllocator(), ImportTextureTask)(*this);
m_task->create("ImportTextureTask");
m_task->run();
}
bool ImportAssetDialog::isTextureDirValid() const
{
if (!m_texture_output_dir[0]) return true;
char normalized_path[Lumix::MAX_PATH_LENGTH];
Lumix::PathUtils::normalize(
m_texture_output_dir, normalized_path, Lumix::lengthOf(normalized_path));
return (m_editor.isRelativePath(normalized_path));
}
void ImportAssetDialog::onGUI()
{
if (!m_is_opened) return;
if (ImGui::Begin("Import asset", &m_is_opened))
{
if (hasMessage())
{
char msg[1024];
getMessage(msg, sizeof(msg));
ImGui::Text(msg);
if (ImGui::Button("OK"))
{
setMessage("");
}
ImGui::End();
return;
}
if (m_is_converting || m_is_importing || m_is_importing_texture)
{
if (m_task && m_task->isFinished())
{
m_task->destroy();
LUMIX_DELETE(m_editor.getAllocator(), m_task);
m_task = nullptr;
m_is_importing = false;
m_is_converting = false;
m_is_importing_texture = false;
}
{
Lumix::MT::SpinLock lock(m_mutex);
ImGui::Text(m_import_message);
}
ImGui::End();
return;
}
if (ImGui::Checkbox("Optimize meshes", &m_optimize_mesh_on_import)) checkSource();
ImGui::SameLine();
if (ImGui::Checkbox("Smooth normals", &m_gen_smooth_normal)) checkSource();
if (ImGui::InputText("Source", m_source, sizeof(m_source))) checkSource();
ImGui::SameLine();
if (ImGui::Button("..."))
{
PlatformInterface::getOpenFilename(m_source, sizeof(m_source), "All\0*.*\0");
checkSource();
}
if (isImage(m_source))
{
if (ImGui::Checkbox("Convert to raw", &m_convert_to_raw))
{
if (m_convert_to_raw) m_convert_to_dds = false;
}
if (m_convert_to_raw)
{
ImGui::SameLine();
ImGui::DragFloat("Scale", &m_raw_texture_scale, 1.0f, 0.01f, 256.0f);
}
if (ImGui::Checkbox("Convert to DDS", &m_convert_to_dds))
{
if (m_convert_to_dds) m_convert_to_raw = false;
}
ImGui::InputText("Output directory", m_output_dir, sizeof(m_output_dir));
ImGui::SameLine();
if (ImGui::Button("...##browseoutput"))
{
PlatformInterface::getOpenDirectory(m_output_dir, sizeof(m_output_dir));
}
if (ImGui::Button("Import texture"))
{
importTexture();
}
ImGui::End();
return;
}
if (m_importer.GetScene())
{
auto* scene = m_importer.GetScene();
ImGui::Checkbox("Import model", &m_import_model);
if (scene->HasMaterials())
{
ImGui::Checkbox(StringBuilder<50>("Import materials (",
scene->mNumMaterials,
")"),
&m_import_materials);
ImGui::Checkbox("Convert to DDS", &m_convert_to_dds);
}
if (scene->HasAnimations())
{
ImGui::Checkbox(StringBuilder<50>("Import animations (",
scene->mNumAnimations,
")"),
&m_import_animations);
}
ImGui::Checkbox("Import physics", &m_import_physics);
if (m_import_physics)
{
ImGui::SameLine();
ImGui::Checkbox("Make convex", &m_make_convex);
}
if (scene->mNumMeshes > 1)
{
if (ImGui::CollapsingHeader(StringBuilder<30>("Meshes (")
<< scene->mNumMeshes
<< ")##Meshes",
nullptr,
true,
true))
{
for (int i = 0; i < (int)scene->mNumMeshes; ++i)
{
const char* name = scene->mMeshes[i]->mName.C_Str();
bool b = m_mesh_mask[i];
ImGui::Checkbox(name[0] == '\0'
? StringBuilder<30>("N/A###na", (Lumix::uint64)&scene->mMeshes[i])
: name,
&b);
m_mesh_mask[i] = b;
}
}
}
ImGui::InputText("Output directory", m_output_dir, sizeof(m_output_dir));
ImGui::SameLine();
if (ImGui::Button("...##browseoutput"))
{
PlatformInterface::getOpenDirectory(m_output_dir, sizeof(m_output_dir));
}
ImGui::InputText(
"Texture output directory", m_texture_output_dir, sizeof(m_texture_output_dir));
ImGui::SameLine();
if (ImGui::Button("...##browsetextureoutput"))
{
PlatformInterface::getOpenDirectory(m_texture_output_dir, sizeof(m_texture_output_dir));
}
if (m_output_dir[0] != '\0')
{
if (!isTextureDirValid())
{
ImGui::Text("Texture output directory must be an ancestor of the working "
"directory or empty.");
}
else if (ImGui::Button("Convert"))
{
convert();
}
}
}
}
ImGui::End();
}