/* Crafter®.Asset Copyright (C) 2026 Catcrafts® catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ export module Crafter.Asset:Mesh; import :Compression; import Crafter.Math; import std; namespace fs = std::filesystem; export namespace Crafter { struct __attribute__((packed)) VertexNormalTangentUVPacked { Vector normal; Vector tangent; Vector uv; }; struct VertexNormalTangentUV { Vector normal; Vector tangent; Vector uv; }; // GDeflate-compressed counterpart of MeshAsset::Save output. Three // regions: [vertex, index, data]. dataCount==0 leaves the data region // empty (zero compressedSize/decompressedSize). dataStride records sizeof(T) // at compress time so consumers can validate. struct CompressedMeshAsset { std::uint32_t vertexCount = 0; std::uint32_t indexCount = 0; std::uint32_t dataCount = 0; std::uint32_t dataStride = 0; Compression::CompressedBlob blob; }; namespace MeshAssetFormat { inline constexpr char magic[4] = {'C', 'G', 'D', 'M'}; inline constexpr std::uint32_t version = 1; } inline CompressedMeshAsset LoadCompressedMesh(fs::path path) { std::ifstream file(path, std::ios::binary); char magic[4]; file.read(magic, 4); if (std::memcmp(magic, MeshAssetFormat::magic, 4) != 0) { throw std::runtime_error("LoadCompressedMesh: bad magic on " + path.string()); } std::uint32_t version = 0; file.read(reinterpret_cast(&version), sizeof(version)); if (version != MeshAssetFormat::version) { throw std::runtime_error("LoadCompressedMesh: unsupported version on " + path.string()); } CompressedMeshAsset out; file.read(reinterpret_cast(&out.vertexCount), sizeof(out.vertexCount)); file.read(reinterpret_cast(&out.indexCount), sizeof(out.indexCount)); file.read(reinterpret_cast(&out.dataCount), sizeof(out.dataCount)); file.read(reinterpret_cast(&out.dataStride), sizeof(out.dataStride)); out.blob = Compression::ReadBlob(file); return out; } template struct MeshAsset { std::vector> vertexes; std::vector indexes; std::vector datas; void Save(fs::path path) { std::ofstream file(path, std::ios::binary); std::uint32_t vertexCount = vertexes.size(); std::uint32_t indexCount = indexes.size(); std::uint32_t dataCount = datas.size(); file.write(reinterpret_cast(&vertexCount), sizeof(vertexCount)); file.write(reinterpret_cast(&indexCount), sizeof(indexCount)); file.write(reinterpret_cast(&dataCount), sizeof(dataCount)); file.write(reinterpret_cast(vertexes.data()), vertexCount * sizeof(Vector)); file.write(reinterpret_cast(indexes.data()), indexCount * sizeof(std::uint32_t)); file.write(reinterpret_cast(datas.data()), dataCount * sizeof(T)); } void SaveCompressed(fs::path path) const { std::array, 3> streams = { std::as_bytes(std::span(vertexes)), std::as_bytes(std::span(indexes)), std::as_bytes(std::span(datas)), }; Compression::CompressedBlob blob = Compression::CompressStreams(streams); std::ofstream file(path, std::ios::binary); file.write(MeshAssetFormat::magic, 4); std::uint32_t version = MeshAssetFormat::version; std::uint32_t vc = static_cast(vertexes.size()); std::uint32_t ic = static_cast(indexes.size()); std::uint32_t dc = static_cast(datas.size()); std::uint32_t stride = static_cast(sizeof(T)); file.write(reinterpret_cast(&version), sizeof(version)); file.write(reinterpret_cast(&vc), sizeof(vc)); file.write(reinterpret_cast(&ic), sizeof(ic)); file.write(reinterpret_cast(&dc), sizeof(dc)); file.write(reinterpret_cast(&stride), sizeof(stride)); Compression::WriteBlob(file, blob); } static MeshAsset Load(fs::path path) { MeshAsset mesh; std::ifstream file(path, std::ios::binary); std::uint32_t vertexCount = 0; std::uint32_t indexCount = 0; std::uint32_t dataCount = 0; file.read(reinterpret_cast(&vertexCount), sizeof(vertexCount)); file.read(reinterpret_cast(&indexCount), sizeof(indexCount)); file.read(reinterpret_cast(&dataCount), sizeof(dataCount)); mesh.vertexes.resize(vertexCount); mesh.indexes.resize(indexCount); mesh.datas.resize(dataCount); file.read(reinterpret_cast(mesh.vertexes.data()), vertexCount * sizeof(Vector)); file.read(reinterpret_cast(mesh.indexes.data()), indexCount * sizeof(std::uint32_t)); file.read(reinterpret_cast(mesh.datas.data()), dataCount * sizeof(T)); return mesh; } static MeshAsset LoadOBJ(fs::path path) requires (std::same_as || std::same_as) { std::ifstream file(path); MeshAsset mesh; std::string line; std::vector> positions; std::vector> normals; std::vector> uvs; while (std::getline(file, line)) { if (line.substr(0, 2) == "vt") { std::istringstream iss(line.substr(3)); Vector uv; iss >> uv.x >> uv.y; uvs.push_back(uv); } else if (line.substr(0, 2) == "vn") { std::istringstream iss(line.substr(3)); Vector normal; iss >> normal.x >> normal.y >> normal.z; normals.push_back(normal); } else if (line.substr(0, 1) == "v") { std::istringstream iss(line.substr(2)); Vector position; iss >> position.x >> position.y >> position.z; positions.push_back(position); } else if (line.substr(0, 1) == "f") { std::istringstream iss(line.substr(2)); std::string vertexGroup; while (iss >> vertexGroup) { std::istringstream vss(vertexGroup); std::string vIndex, vtIndex, vnIndex; std::getline(vss, vIndex, '/'); std::getline(vss, vtIndex, '/'); std::getline(vss, vnIndex, '/'); Vector v = positions[std::stoi(vIndex) - 1]; Vector vt = uvs[std::stoi(vtIndex) - 1]; Vector vn = normals[std::stoi(vnIndex) - 1]; for (std::uint32_t i = 0; i < mesh.datas.size(); i++) { if (mesh.datas[i].normal == vn && mesh.datas[i].uv == vt && mesh.vertexes[i] == v) { mesh.indexes.push_back(i); goto skip; } } mesh.indexes.push_back(mesh.datas.size()); mesh.datas.push_back({vn, {0,0,0}, vt}); mesh.vertexes.push_back(v); skip:; } } } // Accumulate face normals and tangents for (std::uint32_t i = 0; i < mesh.indexes.size(); i += 3) { std::uint32_t i0 = mesh.indexes[i]; std::uint32_t i1 = mesh.indexes[i + 1]; std::uint32_t i2 = mesh.indexes[i + 2]; // Edges of the triangle Vector edge1 = mesh.vertexes[i1] - mesh.vertexes[i0]; Vector edge2 = mesh.vertexes[i2] - mesh.vertexes[i0]; // Texture coordinate deltas Vector deltaUV1 = mesh.datas[i1].uv - mesh.datas[i0].uv; Vector deltaUV2 = mesh.datas[i2].uv - mesh.datas[i0].uv; // Tangent calculation float f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x); Vector tangent; tangent.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x); tangent.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y); tangent.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z); // Normalize tangent tangent.Normalize(); // Accumulate normals and tangents for each vertex mesh.datas[i0].tangent += tangent; mesh.datas[i1].tangent += tangent; mesh.datas[i2].tangent += tangent; } // Normalize vertex normals and tangents for (T& v : mesh.datas) { v.tangent.Normalize(); } return mesh; } }; template <> struct MeshAsset { std::vector> vertexes; std::vector indexes; void Save(fs::path path) { std::ofstream file(path, std::ios::binary); std::uint32_t vertexCount = vertexes.size(); std::uint32_t indexCount = indexes.size(); file.write(reinterpret_cast(&vertexCount), sizeof(vertexCount)); file.write(reinterpret_cast(&indexCount), sizeof(indexCount)); file.write(reinterpret_cast(vertexes.data()), vertexCount * sizeof(Vector)); file.write(reinterpret_cast(indexes.data()), indexCount * sizeof(std::uint32_t)); } void SaveCompressed(fs::path path) const { // Three regions to keep file format identical to the templated // variant; the data region is empty (skipped on the GPU path). std::array, 3> streams = { std::as_bytes(std::span(vertexes)), std::as_bytes(std::span(indexes)), std::span{}, }; Compression::CompressedBlob blob = Compression::CompressStreams(streams); std::ofstream file(path, std::ios::binary); file.write(MeshAssetFormat::magic, 4); std::uint32_t version = MeshAssetFormat::version; std::uint32_t vc = static_cast(vertexes.size()); std::uint32_t ic = static_cast(indexes.size()); std::uint32_t dc = 0; std::uint32_t stride = 0; file.write(reinterpret_cast(&version), sizeof(version)); file.write(reinterpret_cast(&vc), sizeof(vc)); file.write(reinterpret_cast(&ic), sizeof(ic)); file.write(reinterpret_cast(&dc), sizeof(dc)); file.write(reinterpret_cast(&stride), sizeof(stride)); Compression::WriteBlob(file, blob); } static MeshAsset Load(fs::path path) { MeshAsset mesh; std::ifstream file(path, std::ios::binary); std::uint32_t vertexCount = 0; std::uint32_t indexCount = 0; file.read(reinterpret_cast(&vertexCount), sizeof(vertexCount)); file.read(reinterpret_cast(&indexCount), sizeof(indexCount)); mesh.vertexes.resize(vertexCount); mesh.indexes.resize(indexCount); file.read(reinterpret_cast(mesh.vertexes.data()), vertexCount * sizeof(Vector)); file.read(reinterpret_cast(mesh.indexes.data()), indexCount * sizeof(std::uint32_t)); return mesh; } static MeshAsset LoadOBJ(fs::path path) { std::ifstream file(path); MeshAsset mesh; std::string line; while (std::getline(file, line)) { if (line.rfind("v ", 0) == 0) { std::istringstream iss(line.substr(2)); Vector position; iss >> position.x >> position.y >> position.z; mesh.vertexes.push_back(position); } else if (line.rfind("f ", 0) == 0) { std::istringstream iss(line.substr(2)); std::string vertexGroup; while (iss >> vertexGroup) { mesh.indexes.push_back(std::stoi(vertexGroup) - 1); } } } return mesh; } }; }