Compare commits
1
Commits
38230f5092
...
0273340e3b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0273340e3b |
+14
-1
@@ -3,7 +3,7 @@ add_executable(TheChef
|
||||
"ShaderCompiler.h" "ShaderCompiler.cpp"
|
||||
)
|
||||
|
||||
target_compile_features(TheChef PRIVATE cxx_std_17)
|
||||
target_compile_features(TheChef PRIVATE cxx_std_20)
|
||||
|
||||
# Good practice for tools
|
||||
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)
|
||||
|
||||
# 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
|
||||
if (MSVC)
|
||||
target_compile_options(TheChef PRIVATE /W4)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -13,6 +13,14 @@
|
||||
|
||||
#include "ShaderCompiler.h"
|
||||
|
||||
#ifdef THECHEF_MODEL_COOKING
|
||||
#include "ModelCooker.h"
|
||||
#endif
|
||||
|
||||
#ifdef THECHEF_TEXTURE_COOKING
|
||||
#include "TextureCooker.h"
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
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) {
|
||||
const std::string ext = to_lower(src.extension().string());
|
||||
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)
|
||||
if (ext == ".vert" || ext == ".frag" || ext == ".comp" || ext == ".glsl") return "shader";
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
fs::path out = dstLikeSrc;
|
||||
// Option A:
|
||||
out.replace_extension(dstLikeSrc.extension().string() + ".spv");
|
||||
// Option B (uncomment to use):
|
||||
// out.replace_extension(".spv");
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -119,32 +122,29 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
fs::create_directories(args.output);
|
||||
|
||||
// ----------------------------
|
||||
// Shader compiler setup
|
||||
// ----------------------------
|
||||
SpirvShaderCompiler::Options scOpt;
|
||||
scOpt.debugInfo = false; // set true if you want debug info in SPIR-V
|
||||
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 = {
|
||||
args.input / "shaders",
|
||||
args.input / "shaders" / "include"
|
||||
};
|
||||
|
||||
// Optional defines:
|
||||
// scOpt.defines = { "MY_DEFINE=1", "USE_FOG" };
|
||||
|
||||
SpirvShaderCompiler shaderCompiler(scOpt);
|
||||
|
||||
// ----------------------------
|
||||
// Manifest init
|
||||
// ----------------------------
|
||||
#ifdef THECHEF_MODEL_COOKING
|
||||
ModelCooker::Options mcOpt;
|
||||
mcOpt.inputRoot = args.input;
|
||||
|
||||
ModelCooker modelCooker(mcOpt);
|
||||
#endif
|
||||
|
||||
#ifdef THECHEF_TEXTURE_COOKING
|
||||
TextureCooker textureCooker;
|
||||
#endif
|
||||
|
||||
json manifest;
|
||||
manifest["version"] = 1;
|
||||
manifest["assets"] = json::array();
|
||||
@@ -191,6 +191,40 @@ int main(int argc, char** argv) {
|
||||
finalOut = dst;
|
||||
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 {
|
||||
// Normal assets: copy if needed.
|
||||
changed = copy_if_needed(src, dst);
|
||||
|
||||
Reference in New Issue
Block a user