2026-02-19 03:00:13 +01:00
|
|
|
/*
|
|
|
|
|
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;
|
2026-05-11 18:37:30 +02:00
|
|
|
import :Compression;
|
2026-02-19 03:00:13 +01:00
|
|
|
import Crafter.Math;
|
|
|
|
|
import std;
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
export namespace Crafter {
|
2026-05-18 22:31:28 +02:00
|
|
|
// Minimal Wavefront .mtl record. Only diffuse-map path is captured;
|
|
|
|
|
// normal/specular/etc. would extend this in a backward-compatible way.
|
|
|
|
|
// `mapKd` is verbatim from the file (relative to the .mtl's directory)
|
|
|
|
|
// and empty if the material defined no diffuse texture.
|
|
|
|
|
struct MtlMaterial {
|
|
|
|
|
std::string mapKd;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Parse a .mtl file into { materialName → MtlMaterial }. Strips trailing
|
|
|
|
|
// \r/\n/spaces on names + paths so DOS-line-ending files match the
|
|
|
|
|
// `usemtl` tokens in their sibling .obj. Materials without `map_Kd`
|
|
|
|
|
// are still present in the map with an empty `mapKd`.
|
|
|
|
|
inline std::unordered_map<std::string, MtlMaterial> LoadMTL(fs::path path) {
|
|
|
|
|
std::ifstream file(path);
|
|
|
|
|
std::unordered_map<std::string, MtlMaterial> result;
|
|
|
|
|
std::string current;
|
|
|
|
|
std::string line;
|
|
|
|
|
auto trim = [](std::string& s) {
|
|
|
|
|
while (!s.empty() && (s.back() == '\r' || s.back() == '\n' || s.back() == ' ' || s.back() == '\t')) s.pop_back();
|
|
|
|
|
while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) s.erase(s.begin());
|
|
|
|
|
};
|
|
|
|
|
while (std::getline(file, line)) {
|
|
|
|
|
if (line.starts_with("newmtl ")) {
|
|
|
|
|
current = line.substr(7);
|
|
|
|
|
trim(current);
|
|
|
|
|
result.try_emplace(current);
|
|
|
|
|
} else if (line.starts_with("map_Kd ") && !current.empty()) {
|
|
|
|
|
std::string val = line.substr(7);
|
|
|
|
|
trim(val);
|
|
|
|
|
result[current].mapKd = std::move(val);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 03:00:13 +01:00
|
|
|
struct __attribute__((packed)) VertexNormalTangentUVPacked {
|
|
|
|
|
Vector<float, 3, 4> normal;
|
|
|
|
|
Vector<float, 3, 4> tangent;
|
|
|
|
|
Vector<float, 2, 4> uv;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct VertexNormalTangentUV {
|
|
|
|
|
Vector<float, 3, 0> normal;
|
|
|
|
|
Vector<float, 3, 0> tangent;
|
|
|
|
|
Vector<float, 2, 0> uv;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-11 18:37:30 +02:00
|
|
|
// GDeflate-compressed counterpart of MeshAsset<T>::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) {
|
2026-05-18 22:31:28 +02:00
|
|
|
Compression::Fatal("LoadCompressedMesh: bad magic on " + path.string());
|
2026-05-11 18:37:30 +02:00
|
|
|
}
|
|
|
|
|
std::uint32_t version = 0;
|
|
|
|
|
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
|
|
|
|
if (version != MeshAssetFormat::version) {
|
2026-05-18 22:31:28 +02:00
|
|
|
Compression::Fatal("LoadCompressedMesh: unsupported version on " + path.string());
|
2026-05-11 18:37:30 +02:00
|
|
|
}
|
|
|
|
|
CompressedMeshAsset out;
|
|
|
|
|
file.read(reinterpret_cast<char*>(&out.vertexCount), sizeof(out.vertexCount));
|
|
|
|
|
file.read(reinterpret_cast<char*>(&out.indexCount), sizeof(out.indexCount));
|
|
|
|
|
file.read(reinterpret_cast<char*>(&out.dataCount), sizeof(out.dataCount));
|
|
|
|
|
file.read(reinterpret_cast<char*>(&out.dataStride), sizeof(out.dataStride));
|
|
|
|
|
out.blob = Compression::ReadBlob(file);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 03:00:13 +01:00
|
|
|
template <typename T>
|
|
|
|
|
struct MeshAsset {
|
|
|
|
|
std::vector<Vector<float, 3, 3>> vertexes;
|
|
|
|
|
std::vector<std::uint32_t> indexes;
|
|
|
|
|
std::vector<T> 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<char*>(&vertexCount), sizeof(vertexCount));
|
|
|
|
|
file.write(reinterpret_cast<char*>(&indexCount), sizeof(indexCount));
|
|
|
|
|
file.write(reinterpret_cast<char*>(&dataCount), sizeof(dataCount));
|
|
|
|
|
|
|
|
|
|
file.write(reinterpret_cast<char*>(vertexes.data()), vertexCount * sizeof(Vector<float, 3, 3>));
|
|
|
|
|
file.write(reinterpret_cast<char*>(indexes.data()), indexCount * sizeof(std::uint32_t));
|
|
|
|
|
file.write(reinterpret_cast<char*>(datas.data()), dataCount * sizeof(T));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 18:37:30 +02:00
|
|
|
void SaveCompressed(fs::path path) const {
|
|
|
|
|
std::array<std::span<const std::byte>, 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<std::uint32_t>(vertexes.size());
|
|
|
|
|
std::uint32_t ic = static_cast<std::uint32_t>(indexes.size());
|
|
|
|
|
std::uint32_t dc = static_cast<std::uint32_t>(datas.size());
|
|
|
|
|
std::uint32_t stride = static_cast<std::uint32_t>(sizeof(T));
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&vc), sizeof(vc));
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&ic), sizeof(ic));
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&dc), sizeof(dc));
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&stride), sizeof(stride));
|
|
|
|
|
Compression::WriteBlob(file, blob);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 03:00:13 +01:00
|
|
|
static MeshAsset<T> Load(fs::path path) {
|
|
|
|
|
MeshAsset<T> 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<char*>(&vertexCount), sizeof(vertexCount));
|
|
|
|
|
file.read(reinterpret_cast<char*>(&indexCount), sizeof(indexCount));
|
|
|
|
|
file.read(reinterpret_cast<char*>(&dataCount), sizeof(dataCount));
|
|
|
|
|
|
|
|
|
|
mesh.vertexes.resize(vertexCount);
|
|
|
|
|
mesh.indexes.resize(indexCount);
|
|
|
|
|
mesh.datas.resize(dataCount);
|
|
|
|
|
|
|
|
|
|
file.read(reinterpret_cast<char*>(mesh.vertexes.data()), vertexCount * sizeof(Vector<float, 3, 3>));
|
|
|
|
|
file.read(reinterpret_cast<char*>(mesh.indexes.data()), indexCount * sizeof(std::uint32_t));
|
|
|
|
|
file.read(reinterpret_cast<char*>(mesh.datas.data()), dataCount * sizeof(T));
|
|
|
|
|
|
|
|
|
|
return mesh;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 22:31:28 +02:00
|
|
|
// Parse a .obj split by `usemtl` directives. Returns one
|
|
|
|
|
// MeshAsset<T> per material group (in encounter order), each with
|
|
|
|
|
// its own deduplicated vertex/index/data arrays — i.e. shared
|
|
|
|
|
// vertices across materials are duplicated so each MeshAsset is
|
|
|
|
|
// self-contained and can become a standalone BLAS. Dedup is
|
|
|
|
|
// O(1)-per-vertex via a hash map keyed on (vIdx, vtIdx, vnIdx),
|
|
|
|
|
// unlike LoadOBJ's linear-scan dedup (which is O(N²) and gets
|
|
|
|
|
// pathological on Sponza-class inputs).
|
|
|
|
|
//
|
|
|
|
|
// Faces appearing before any `usemtl` accumulate under the empty
|
|
|
|
|
// string material name; consumers can decide whether to drop them
|
|
|
|
|
// or treat them as a default. Non-triangle faces are fan-triangulated
|
|
|
|
|
// around their first vertex.
|
|
|
|
|
static std::vector<std::pair<std::string, MeshAsset<T>>> LoadOBJSplit(fs::path path)
|
|
|
|
|
requires (std::same_as<T, VertexNormalTangentUVPacked> || std::same_as<T, VertexNormalTangentUV>) {
|
|
|
|
|
std::ifstream file(path);
|
|
|
|
|
|
|
|
|
|
std::vector<Vector<float, 3, 0>> positions;
|
|
|
|
|
std::vector<Vector<float, 3, 0>> normals;
|
|
|
|
|
std::vector<Vector<float, 2, 0>> uvs;
|
|
|
|
|
|
|
|
|
|
std::vector<std::pair<std::string, MeshAsset<T>>> result;
|
|
|
|
|
std::unordered_map<std::string, std::size_t> matIndex;
|
|
|
|
|
std::vector<std::unordered_map<std::uint64_t, std::uint32_t>> dedupPerMat;
|
|
|
|
|
|
|
|
|
|
std::size_t curMat = static_cast<std::size_t>(-1);
|
|
|
|
|
|
|
|
|
|
auto useMaterial = [&](std::string name) {
|
|
|
|
|
while (!name.empty() && (name.back() == '\r' || name.back() == '\n' || name.back() == ' ' || name.back() == '\t')) name.pop_back();
|
|
|
|
|
auto it = matIndex.find(name);
|
|
|
|
|
if (it != matIndex.end()) { curMat = it->second; return; }
|
|
|
|
|
matIndex.emplace(name, result.size());
|
|
|
|
|
result.emplace_back(std::move(name), MeshAsset<T>{});
|
|
|
|
|
dedupPerMat.emplace_back();
|
|
|
|
|
curMat = result.size() - 1;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::string line;
|
|
|
|
|
while (std::getline(file, line)) {
|
|
|
|
|
if (line.starts_with("usemtl ")) {
|
|
|
|
|
useMaterial(line.substr(7));
|
|
|
|
|
} else if (line.starts_with("vt ")) {
|
|
|
|
|
std::istringstream iss(line.substr(3));
|
|
|
|
|
Vector<float, 2, 0> uv;
|
|
|
|
|
iss >> uv.x >> uv.y;
|
|
|
|
|
uvs.push_back(uv);
|
|
|
|
|
} else if (line.starts_with("vn ")) {
|
|
|
|
|
std::istringstream iss(line.substr(3));
|
|
|
|
|
Vector<float, 3, 0> n;
|
|
|
|
|
iss >> n.x >> n.y >> n.z;
|
|
|
|
|
normals.push_back(n);
|
|
|
|
|
} else if (line.starts_with("v ")) {
|
|
|
|
|
std::istringstream iss(line.substr(2));
|
|
|
|
|
Vector<float, 3, 3> p;
|
|
|
|
|
iss >> p.x >> p.y >> p.z;
|
|
|
|
|
positions.push_back(Vector<float, 3, 0>{p.x, p.y, p.z});
|
|
|
|
|
} else if (line.starts_with("f ")) {
|
|
|
|
|
if (curMat == static_cast<std::size_t>(-1)) useMaterial(std::string{});
|
|
|
|
|
MeshAsset<T>& mesh = result[curMat].second;
|
|
|
|
|
auto& dedup = dedupPerMat[curMat];
|
|
|
|
|
std::istringstream iss(line.substr(2));
|
|
|
|
|
std::string vg;
|
|
|
|
|
std::vector<std::uint32_t> faceIndices;
|
|
|
|
|
while (iss >> vg) {
|
|
|
|
|
std::istringstream vss(vg);
|
|
|
|
|
std::string vs, vts, vns;
|
|
|
|
|
std::getline(vss, vs, '/');
|
|
|
|
|
std::getline(vss, vts, '/');
|
|
|
|
|
std::getline(vss, vns, '/');
|
|
|
|
|
std::uint32_t vi = vs.empty() ? 0u : static_cast<std::uint32_t>(std::stoi(vs) - 1);
|
|
|
|
|
std::uint32_t vti = vts.empty() ? 0u : static_cast<std::uint32_t>(std::stoi(vts) - 1);
|
|
|
|
|
std::uint32_t vni = vns.empty() ? 0u : static_cast<std::uint32_t>(std::stoi(vns) - 1);
|
|
|
|
|
std::uint64_t key = (static_cast<std::uint64_t>(vi) << 42)
|
|
|
|
|
| (static_cast<std::uint64_t>(vti) << 21)
|
|
|
|
|
| static_cast<std::uint64_t>(vni);
|
|
|
|
|
if (auto it = dedup.find(key); it != dedup.end()) {
|
|
|
|
|
faceIndices.push_back(it->second);
|
|
|
|
|
} else {
|
|
|
|
|
std::uint32_t newIdx = static_cast<std::uint32_t>(mesh.vertexes.size());
|
|
|
|
|
const Vector<float, 3, 0>& p = positions[vi];
|
|
|
|
|
const Vector<float, 3, 0>& n = normals[vni];
|
|
|
|
|
const Vector<float, 2, 0>& uv = uvs[vti];
|
|
|
|
|
mesh.vertexes.push_back(Vector<float, 3, 3>{p.x, p.y, p.z});
|
|
|
|
|
mesh.datas.push_back(T{n, {0,0,0}, uv});
|
|
|
|
|
dedup.emplace(key, newIdx);
|
|
|
|
|
faceIndices.push_back(newIdx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (std::size_t i = 1; i + 1 < faceIndices.size(); ++i) {
|
|
|
|
|
mesh.indexes.push_back(faceIndices[0]);
|
|
|
|
|
mesh.indexes.push_back(faceIndices[i]);
|
|
|
|
|
mesh.indexes.push_back(faceIndices[i + 1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Accumulate face tangents into each vertex's tangent slot,
|
|
|
|
|
// then normalize. Mirrors LoadOBJ's tangent calculation but
|
|
|
|
|
// guarded against degenerate UVs (the LoadOBJ form NaNs out
|
|
|
|
|
// silently — fine for the cube but blows up some Sponza tris).
|
|
|
|
|
for (auto& entry : result) {
|
|
|
|
|
MeshAsset<T>& mesh = entry.second;
|
|
|
|
|
for (std::uint32_t i = 0; i + 2 < 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];
|
|
|
|
|
Vector<float, 3, 0> edge1 = mesh.vertexes[i1] - mesh.vertexes[i0];
|
|
|
|
|
Vector<float, 3, 0> edge2 = mesh.vertexes[i2] - mesh.vertexes[i0];
|
|
|
|
|
Vector<float, 2, 0> dUV1 = mesh.datas[i1].uv - mesh.datas[i0].uv;
|
|
|
|
|
Vector<float, 2, 0> dUV2 = mesh.datas[i2].uv - mesh.datas[i0].uv;
|
|
|
|
|
float denom = dUV1.x * dUV2.y - dUV1.y * dUV2.x;
|
|
|
|
|
if (denom == 0.0f) continue;
|
|
|
|
|
float f = 1.0f / denom;
|
|
|
|
|
Vector<float, 3, 0> tangent;
|
|
|
|
|
tangent.x = f * (dUV2.y * edge1.x - dUV1.y * edge2.x);
|
|
|
|
|
tangent.y = f * (dUV2.y * edge1.y - dUV1.y * edge2.y);
|
|
|
|
|
tangent.z = f * (dUV2.y * edge1.z - dUV1.y * edge2.z);
|
|
|
|
|
tangent.Normalize();
|
|
|
|
|
mesh.datas[i0].tangent += tangent;
|
|
|
|
|
mesh.datas[i1].tangent += tangent;
|
|
|
|
|
mesh.datas[i2].tangent += tangent;
|
|
|
|
|
}
|
|
|
|
|
for (T& v : mesh.datas) {
|
|
|
|
|
v.tangent.Normalize();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 03:00:13 +01:00
|
|
|
static MeshAsset<T> LoadOBJ(fs::path path) requires (std::same_as<T, VertexNormalTangentUVPacked> || std::same_as<T, VertexNormalTangentUV>) {
|
|
|
|
|
std::ifstream file(path);
|
|
|
|
|
|
|
|
|
|
MeshAsset<T> mesh;
|
|
|
|
|
|
|
|
|
|
std::string line;
|
|
|
|
|
|
|
|
|
|
std::vector<Vector<float, 3, 0>> positions;
|
|
|
|
|
std::vector<Vector<float, 3, 0>> normals;
|
|
|
|
|
std::vector<Vector<float, 2, 0>> uvs;
|
|
|
|
|
|
|
|
|
|
while (std::getline(file, line)) {
|
|
|
|
|
if (line.substr(0, 2) == "vt") {
|
|
|
|
|
std::istringstream iss(line.substr(3));
|
|
|
|
|
Vector<float,2, 0> uv;
|
|
|
|
|
iss >> uv.x >> uv.y;
|
|
|
|
|
uvs.push_back(uv);
|
|
|
|
|
} else if (line.substr(0, 2) == "vn") {
|
|
|
|
|
std::istringstream iss(line.substr(3));
|
|
|
|
|
Vector<float, 3, 0> 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<float,3, 3> 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<float, 3, 0> v = positions[std::stoi(vIndex) - 1];
|
|
|
|
|
Vector<float, 2, 0> vt = uvs[std::stoi(vtIndex) - 1];
|
|
|
|
|
Vector<float, 3, 0> 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<float, 3, 0> edge1 = mesh.vertexes[i1] - mesh.vertexes[i0];
|
|
|
|
|
Vector<float, 3, 0> edge2 = mesh.vertexes[i2] - mesh.vertexes[i0];
|
|
|
|
|
|
|
|
|
|
// Texture coordinate deltas
|
|
|
|
|
Vector<float, 2, 0> deltaUV1 = mesh.datas[i1].uv - mesh.datas[i0].uv;
|
|
|
|
|
Vector<float, 2, 0> deltaUV2 = mesh.datas[i2].uv - mesh.datas[i0].uv;
|
|
|
|
|
|
|
|
|
|
// Tangent calculation
|
|
|
|
|
float f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);
|
|
|
|
|
Vector<float, 3, 0> 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<void> {
|
|
|
|
|
std::vector<Vector<float, 3, 3>> vertexes;
|
|
|
|
|
std::vector<std::uint32_t> 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<char*>(&vertexCount), sizeof(vertexCount));
|
|
|
|
|
file.write(reinterpret_cast<char*>(&indexCount), sizeof(indexCount));
|
|
|
|
|
|
|
|
|
|
file.write(reinterpret_cast<char*>(vertexes.data()), vertexCount * sizeof(Vector<float, 3, 3>));
|
|
|
|
|
file.write(reinterpret_cast<char*>(indexes.data()), indexCount * sizeof(std::uint32_t));
|
|
|
|
|
}
|
2026-05-11 18:37:30 +02:00
|
|
|
|
|
|
|
|
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<std::span<const std::byte>, 3> streams = {
|
|
|
|
|
std::as_bytes(std::span(vertexes)),
|
|
|
|
|
std::as_bytes(std::span(indexes)),
|
|
|
|
|
std::span<const std::byte>{},
|
|
|
|
|
};
|
|
|
|
|
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<std::uint32_t>(vertexes.size());
|
|
|
|
|
std::uint32_t ic = static_cast<std::uint32_t>(indexes.size());
|
|
|
|
|
std::uint32_t dc = 0;
|
|
|
|
|
std::uint32_t stride = 0;
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&vc), sizeof(vc));
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&ic), sizeof(ic));
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&dc), sizeof(dc));
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&stride), sizeof(stride));
|
|
|
|
|
Compression::WriteBlob(file, blob);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 03:00:13 +01:00
|
|
|
static MeshAsset<void> Load(fs::path path) {
|
|
|
|
|
MeshAsset<void> mesh;
|
|
|
|
|
|
|
|
|
|
std::ifstream file(path, std::ios::binary);
|
|
|
|
|
|
|
|
|
|
std::uint32_t vertexCount = 0;
|
|
|
|
|
std::uint32_t indexCount = 0;
|
|
|
|
|
|
|
|
|
|
file.read(reinterpret_cast<char*>(&vertexCount), sizeof(vertexCount));
|
|
|
|
|
file.read(reinterpret_cast<char*>(&indexCount), sizeof(indexCount));
|
|
|
|
|
|
|
|
|
|
mesh.vertexes.resize(vertexCount);
|
|
|
|
|
mesh.indexes.resize(indexCount);
|
|
|
|
|
|
|
|
|
|
file.read(reinterpret_cast<char*>(mesh.vertexes.data()), vertexCount * sizeof(Vector<float, 3, 3>));
|
|
|
|
|
file.read(reinterpret_cast<char*>(mesh.indexes.data()), indexCount * sizeof(std::uint32_t));
|
|
|
|
|
|
|
|
|
|
return mesh;
|
|
|
|
|
}
|
|
|
|
|
static MeshAsset<void> LoadOBJ(fs::path path) {
|
|
|
|
|
std::ifstream file(path);
|
|
|
|
|
|
|
|
|
|
MeshAsset<void> mesh;
|
|
|
|
|
|
|
|
|
|
std::string line;
|
|
|
|
|
|
|
|
|
|
while (std::getline(file, line)) {
|
|
|
|
|
if (line.rfind("v ", 0) == 0) {
|
|
|
|
|
std::istringstream iss(line.substr(2));
|
|
|
|
|
Vector<float,3, 3> 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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|