Compare commits

...
1 Commits
Author SHA1 Message Date
brammie15 0273340e3b feat: add model / texture cooking 2026-10-13 00:28:34 +02:00
6 changed files with 249 additions and 21 deletions
+14 -1
View File
@@ -3,7 +3,7 @@ add_executable(TheChef
"ShaderCompiler.h" "ShaderCompiler.cpp" "ShaderCompiler.h" "ShaderCompiler.cpp"
) )
target_compile_features(TheChef PRIVATE cxx_std_17) target_compile_features(TheChef PRIVATE cxx_std_20)
# Good practice for tools # Good practice for tools
target_compile_definitions(TheChef PRIVATE target_compile_definitions(TheChef PRIVATE
@@ -45,6 +45,19 @@ add_subdirectory(third_party/glslang)
target_link_libraries(TheChef PRIVATE nlohmann_json::nlohmann_json spdlog::spdlog glslang glslang-default-resource-limits) target_link_libraries(TheChef PRIVATE nlohmann_json::nlohmann_json spdlog::spdlog glslang glslang-default-resource-limits)
# Model cooking needs the engine's asset types, so it's only available when built inside Destrum
if (TARGET destrum::assets)
target_sources(TheChef PRIVATE "ModelCooker.h" "ModelCooker.cpp")
target_link_libraries(TheChef PRIVATE destrum::assets)
target_compile_definitions(TheChef PRIVATE THECHEF_MODEL_COOKING)
endif()
if (TARGET destrum::assets AND TARGET tinyexr)
target_sources(TheChef PRIVATE "TextureCooker.h" "TextureCooker.cpp")
target_link_libraries(TheChef PRIVATE destrum::assets tinyexr)
target_compile_definitions(TheChef PRIVATE THECHEF_TEXTURE_COOKING)
endif()
# Optional: warnings # Optional: warnings
if (MSVC) if (MSVC)
target_compile_options(TheChef PRIVATE /W4) target_compile_options(TheChef PRIVATE /W4)
+79
View File
@@ -0,0 +1,79 @@
#include "ModelCooker.h"
#include <string>
#include <utility>
#include <destrum/Assets/ModelFile.h>
ModelCooker::ModelCooker(Options opt)
: m_opt(std::move(opt))
{
}
bool ModelCooker::CookFileIfNeeded(const fs::path& source, const fs::path& output) const
{
if (!NeedsRebuild(source, output))
return false;
ModelDoc::ModelAsset model = ModelDoc::LoadModel(source.generic_string(), ModelFile::CookOptions);
MakePathsPortable(model, source);
fs::create_directories(output.parent_path());
ModelFile::WriteToFile(model, output);
return true;
}
fs::path ModelCooker::MakeOutputPath(const fs::path& dstLikeSrc)
{
fs::path out = dstLikeSrc;
out.replace_extension(dstLikeSrc.extension().string() + std::string(ModelFile::Extension));
return out;
}
bool ModelCooker::NeedsRebuild(const fs::path& source, const fs::path& output)
{
if (!fs::exists(output))
return true;
if (fs::last_write_time(source) > fs::last_write_time(output))
return true;
return ModelFile::ReadVersionFromFile(output) != ModelFile::Version;
}
void ModelCooker::MakePathsPortable(ModelDoc::ModelAsset& model, const fs::path& source) const
{
const fs::path inputRoot = fs::absolute(m_opt.inputRoot).lexically_normal();
const fs::path modelPath = fs::absolute(source).lexically_normal();
const fs::path modelDir = modelPath.parent_path();
const std::string reference = modelPath.lexically_relative(inputRoot).generic_string();
model.sourcePath = reference;
model.skeleton.assetReference.path = reference;
for (ModelDoc::MeshPrimitive& primitive : model.primitives)
{
primitive.mesh.assetReference.path = reference;
if (primitive.mesh.skeleton)
primitive.mesh.skeleton->assetReference.path = reference;
}
for (ModelDoc::MaterialInfo& material : model.materials)
{
material.assetReference.path = reference;
for (ModelDoc::TextureInfo& texture : material.textures)
{
if (texture.embedded || texture.path.empty())
continue;
const fs::path texturePath = fs::path(texture.path).lexically_normal();
const fs::path fromRoot = texturePath.lexically_relative(inputRoot);
if (fromRoot.empty() || *fromRoot.begin() == "..")
continue;
texture.path = texturePath.lexically_relative(modelDir).generic_string();
}
}
for (SkeletalAnimation& animation : model.animations)
animation.assetReference.path = reference;
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <filesystem>
#include <destrum/Util/ModelDoc.h>
namespace fs = std::filesystem;
class ModelCooker
{
public:
struct Options
{
fs::path inputRoot;
};
explicit ModelCooker(Options opt);
bool CookFileIfNeeded(const fs::path& source, const fs::path& output) const;
static fs::path MakeOutputPath(const fs::path& dstLikeSrc);
private:
Options m_opt;
static bool NeedsRebuild(const fs::path& source, const fs::path& output);
void MakePathsPortable(ModelDoc::ModelAsset& model, const fs::path& source) const;
};
+57
View File
@@ -0,0 +1,57 @@
#include "TextureCooker.h"
#include <cstdlib>
#include <memory>
#include <span>
#include <stdexcept>
#include <string>
#include <tinyexr.h>
#include <destrum/Assets/TextureFile.h>
bool TextureCooker::CookFileIfNeeded(const fs::path& source, const fs::path& output) const
{
if (!NeedsRebuild(source, output))
return false;
float* rgba = nullptr;
int width = 0;
int height = 0;
const char* error = nullptr;
if (LoadEXR(&rgba, &width, &height, source.string().c_str(), &error) != TINYEXR_SUCCESS)
{
const std::string message = error ? error : "unknown error";
FreeEXRErrorMessage(error);
throw std::runtime_error("TinyEXR failed to load '" + source.string() + "': " + message);
}
const std::unique_ptr<float, decltype(&std::free)> pixels(rgba, &std::free);
const std::size_t floatCount = static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4;
const TextureFile::Texture texture = TextureFile::FromRGBA32Float(
static_cast<std::uint32_t>(width),
static_cast<std::uint32_t>(height),
std::span<const float>(pixels.get(), floatCount));
fs::create_directories(output.parent_path());
TextureFile::WriteToFile(texture, output);
return true;
}
fs::path TextureCooker::MakeOutputPath(const fs::path& dstLikeSrc)
{
fs::path out = dstLikeSrc;
out.replace_extension(dstLikeSrc.extension().string() + std::string(TextureFile::Extension));
return out;
}
bool TextureCooker::NeedsRebuild(const fs::path& source, const fs::path& output)
{
if (!fs::exists(output))
return true;
if (fs::last_write_time(source) > fs::last_write_time(output))
return true;
return TextureFile::ReadVersionFromFile(output) != TextureFile::Version;
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <filesystem>
namespace fs = std::filesystem;
class TextureCooker
{
public:
// Returns true if rebuilt. Throws when the image can't be decoded.
bool CookFileIfNeeded(const fs::path& source, const fs::path& output) const;
static fs::path MakeOutputPath(const fs::path& dstLikeSrc);
private:
static bool NeedsRebuild(const fs::path& source, const fs::path& output);
};
+54 -20
View File
@@ -13,6 +13,14 @@
#include "ShaderCompiler.h" #include "ShaderCompiler.h"
#ifdef THECHEF_MODEL_COOKING
#include "ModelCooker.h"
#endif
#ifdef THECHEF_TEXTURE_COOKING
#include "TextureCooker.h"
#endif
namespace fs = std::filesystem; namespace fs = std::filesystem;
using json = nlohmann::json; using json = nlohmann::json;
@@ -60,6 +68,7 @@ static Args parse_args(int argc, char** argv) {
static std::string classify_type(const fs::path& src) { static std::string classify_type(const fs::path& src) {
const std::string ext = to_lower(src.extension().string()); const std::string ext = to_lower(src.extension().string());
if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".tga") return "texture"; if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".tga") return "texture";
if (ext == ".exr") return "hdr_texture";
// Treat these as "shader family" (entry: .vert/.frag/.comp, include-only: .glsl) // Treat these as "shader family" (entry: .vert/.frag/.comp, include-only: .glsl)
if (ext == ".vert" || ext == ".frag" || ext == ".comp" || ext == ".glsl") return "shader"; if (ext == ".vert" || ext == ".frag" || ext == ".comp" || ext == ".glsl") return "shader";
if (ext == ".wav" || ext == ".ogg" || ext == ".mp3") return "audio"; if (ext == ".wav" || ext == ".ogg" || ext == ".mp3") return "audio";
@@ -79,15 +88,9 @@ static bool copy_if_needed(const fs::path& src, const fs::path& dst) {
return true; return true;
} }
// Choose your SPV output naming scheme.
// Option A: shader.vert -> shader.vert.spv (keeps original extension)
// Option B: shader.vert -> shader.spv (replace with .spv)
static fs::path make_spv_output_path(const fs::path& dstLikeSrc) { static fs::path make_spv_output_path(const fs::path& dstLikeSrc) {
fs::path out = dstLikeSrc; fs::path out = dstLikeSrc;
// Option A:
out.replace_extension(dstLikeSrc.extension().string() + ".spv"); out.replace_extension(dstLikeSrc.extension().string() + ".spv");
// Option B (uncomment to use):
// out.replace_extension(".spv");
return out; return out;
} }
@@ -119,32 +122,29 @@ int main(int argc, char** argv) {
} }
fs::create_directories(args.output); fs::create_directories(args.output);
// ----------------------------
// Shader compiler setup
// ----------------------------
SpirvShaderCompiler::Options scOpt; SpirvShaderCompiler::Options scOpt;
scOpt.debugInfo = false; // set true if you want debug info in SPIR-V scOpt.debugInfo = false; // set true if you want debug info in SPIR-V
scOpt.defines = { "DEBUG=1" }; scOpt.defines = { "DEBUG=1" };
// Common conventions (adjust to your folder layout):
// - shaders live under <input>/shaders
// - includes live under <input>/shaders/include
//
// If you don't have these folders, either remove these or point them elsewhere.
scOpt.includeDirs = { scOpt.includeDirs = {
args.input / "shaders", args.input / "shaders",
args.input / "shaders" / "include" args.input / "shaders" / "include"
}; };
// Optional defines:
// scOpt.defines = { "MY_DEFINE=1", "USE_FOG" };
SpirvShaderCompiler shaderCompiler(scOpt); SpirvShaderCompiler shaderCompiler(scOpt);
// ---------------------------- #ifdef THECHEF_MODEL_COOKING
// Manifest init ModelCooker::Options mcOpt;
// ---------------------------- mcOpt.inputRoot = args.input;
ModelCooker modelCooker(mcOpt);
#endif
#ifdef THECHEF_TEXTURE_COOKING
TextureCooker textureCooker;
#endif
json manifest; json manifest;
manifest["version"] = 1; manifest["version"] = 1;
manifest["assets"] = json::array(); manifest["assets"] = json::array();
@@ -191,6 +191,40 @@ int main(int argc, char** argv) {
finalOut = dst; finalOut = dst;
finalType = "shader_raw"; finalType = "shader_raw";
} }
} else if (type == "mesh") {
// The source model is still copied for code that imports it directly.
changed = copy_if_needed(src, dst);
finalOut = dst;
#ifdef THECHEF_MODEL_COOKING
// Models that can't be imported on their own (e.g. animation-only files) keep using the source.
const fs::path modelOut = ModelCooker::MakeOutputPath(dst);
try {
changed = modelCooker.CookFileIfNeeded(src, modelOut) || changed;
finalOut = modelOut;
finalType = "model";
} catch (const std::exception& e) {
spdlog::warn("Not cooking {}, using the source model instead: {}", rel.generic_string(), e.what());
}
#endif
} else if (type == "hdr_texture") {
#ifdef THECHEF_TEXTURE_COOKING
const fs::path textureOut = TextureCooker::MakeOutputPath(dst);
try {
changed = textureCooker.CookFileIfNeeded(src, textureOut);
finalOut = textureOut;
finalType = "texture_cooked";
} catch (const std::exception& e) {
spdlog::warn("Not cooking {}, copying the source image instead: {}", rel.generic_string(), e.what());
changed = copy_if_needed(src, dst);
finalOut = dst;
}
#else
changed = copy_if_needed(src, dst);
finalOut = dst;
#endif
} else { } else {
// Normal assets: copy if needed. // Normal assets: copy if needed.
changed = copy_if_needed(src, dst); changed = copy_if_needed(src, dst);