Crafter.Build/implementations/Crafter.Build-Asset.cpp
Jorijn van der Graaf 7ff426b2f0
Some checks failed
CI / build-test-release (push) Failing after 5m36s
SPDX license update
Canonical LGPL-3.0-only text, GPL-3.0 companion, SPDX headers on all
first-party sources, MIT for examples/. Vendored lib/ untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:19:17 +02:00

104 lines
4.4 KiB
C++

//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module Crafter.Build:Asset_impl;
import :Asset;
import std;
#ifdef CRAFTER_BUILD_HAS_ASSET
import Crafter.Asset;
import Crafter.Math;
#endif
namespace fs = std::filesystem;
namespace Crafter {
std::string CompressAsset(const fs::path& input, const fs::path& output) {
std::string ext = input.extension().string();
// Case-insensitive extension match — Sponza's textures dir mixes
// case (.tga and .TGA both appear in the wild).
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
#ifdef CRAFTER_BUILD_HAS_ASSET
try {
// stb_image (the loader behind LoadPNG) handles all of these,
// so a single branch covers every raster format we ship. The
// function is misnamed for historical reasons.
if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") {
auto tex = TextureAsset<Vector<std::uint8_t, 4, 4>>::LoadPNG<std::uint8_t>(input);
tex.SaveCompressed(output);
} else if (ext == ".obj") {
auto mesh = MeshAsset<VertexNormalTangentUVPacked>::LoadOBJ(input);
mesh.SaveCompressed(output);
} else {
return std::format("{}: unsupported asset extension '{}'", input.string(), ext);
}
} catch (const std::exception& e) {
return std::format("{}: {}", input.string(), e.what());
}
return {};
#else
return std::format(
"{}: crafter-build was bootstrapped without Crafter.Asset linkage; "
"rebuild via `./bin/crafter-build` so the self-host pass picks up the "
"Crafter.Asset dep declared in project.cpp",
input.string());
#endif
}
std::string BuildOBJBundle(
const fs::path& objPath,
const fs::path& mtlPath,
const fs::path& outDir,
std::uint16_t albedoSize)
{
#ifdef CRAFTER_BUILD_HAS_ASSET
const fs::path manifest = outDir / "scene.txt";
if (fs::exists(manifest)) return {};
try {
fs::create_directories(outDir);
auto materials = LoadMTL(mtlPath);
auto meshes = MeshAsset<VertexNormalTangentUVPacked>::LoadOBJSplit(objPath);
std::vector<std::string> uniqueAlbedos;
std::unordered_map<std::string, std::uint32_t> albedoIndex;
for (auto& [matName, _] : meshes) {
auto matIt = materials.find(matName);
if (matIt == materials.end() || matIt->second.mapKd.empty()) continue;
const std::string& kd = matIt->second.mapKd;
if (!albedoIndex.contains(kd)) {
albedoIndex.emplace(kd, static_cast<std::uint32_t>(uniqueAlbedos.size()));
uniqueAlbedos.push_back(kd);
}
}
std::vector<std::int32_t> meshAlbedoIdx;
std::uint32_t emitted = 0;
for (auto& [matName, mesh] : meshes) {
if (mesh.vertexes.empty() || mesh.indexes.empty()) continue;
mesh.SaveCompressed(outDir / std::format("mesh_{}.cmesh", emitted));
std::int32_t a = -1;
if (auto it = materials.find(matName);
it != materials.end() && !it->second.mapKd.empty())
a = static_cast<std::int32_t>(albedoIndex.at(it->second.mapKd));
meshAlbedoIdx.push_back(a);
++emitted;
}
for (std::uint32_t i = 0; i < uniqueAlbedos.size(); ++i) {
const fs::path texPath = mtlPath.parent_path() / uniqueAlbedos[i];
auto tex = TextureAsset<Vector<std::uint8_t, 4, 4>>::LoadPNG<std::uint8_t>(texPath);
tex.Resize(albedoSize, albedoSize);
tex.SaveCompressed(outDir / std::format("tex_{}.ctex", i));
}
std::ofstream m(manifest);
m << uniqueAlbedos.size() << "\n" << emitted << "\n";
for (std::int32_t a : meshAlbedoIdx) m << a << "\n";
} catch (const std::exception& e) {
return std::format("BuildOBJBundle: {}", e.what());
}
return {};
#else
return "BuildOBJBundle: crafter-build was bootstrapped without Crafter.Asset linkage; "
"rebuild via crafter-build itself to pick up the Crafter.Asset dep";
#endif
}
}