diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-06 07:09:40 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-06 07:09:40 +0100 |
| commit | 1ec5d1ae48d1dd3290992ba63a8ec581af02b9dd (patch) | |
| tree | 3545f40ac2fafaf3471063902f3a627b4ab36d8d /tools | |
| parent | 96378529abe1f53e47b733d1d17d589d7b3c5424 (diff) | |
fix(assets): Auto-generate smooth normals for OBJ meshes if missing
Updated asset_packer to detect missing normals in OBJ files.
Implemented a 2-pass parser:
1. Read geometry and connectivity.
2. If normals are missing:
- Compute face normals using cross product.
- Accumulate and average normals per vertex (Smooth Shading).
- Update face indices to map normals 1:1 with positions.
This fixes the 'no shading' issue for assets like dodecahedron.obj which lack normals.
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/asset_packer.cc | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/tools/asset_packer.cc b/tools/asset_packer.cc index 04b74a4..79a6ce6 100644 --- a/tools/asset_packer.cc +++ b/tools/asset_packer.cc @@ -5,10 +5,13 @@ #include <cstdio> // for simplicity, use fprintf() for output generation #include <fstream> #include <map> +#include <algorithm> // For std::count +#include <cstring> // For std::memcpy #include <regex> // For std::regex #include <stdexcept> // For std::stof exceptions #include <string> #include <vector> +#include <cmath> #define STB_IMAGE_IMPLEMENTATION #define STBI_NO_LINEAR // Don't apply gamma correction, we want raw bytes @@ -39,6 +42,11 @@ static bool HasImageExtension(const std::string& filename) { return false; } +static bool HasMeshExtension(const std::string& filename) { + std::string ext = filename.substr(filename.find_last_of(".") + 1); + return ext == "obj"; +} + // Helper struct to hold all information about an asset during parsing struct AssetBuildInfo { std::string name; @@ -53,6 +61,22 @@ struct AssetBuildInfo { std::string func_name_str_name; // ASSET_PROC_FUNC_STR_xxx for procedural }; +struct Vec3 { + float x, y, z; + Vec3 operator+(const Vec3& o) const { return {x + o.x, y + o.y, z + o.z}; } + Vec3 operator+=(const Vec3& o) { x += o.x; y += o.y; z += o.z; return *this; } + Vec3 operator-(const Vec3& o) const { return {x - o.x, y - o.y, z - o.z}; } + Vec3 operator*(float s) const { return {x * s, y * s, z * s}; } + static Vec3 cross(const Vec3& a, const Vec3& b) { + return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; + } + Vec3 normalize() const { + float len = std::sqrt(x * x + y * y + z * z); + if (len > 1e-6f) return {x / len, y / len, z / len}; + return {0, 0, 0}; + } +}; + int main(int argc, char* argv[]) { if (argc != 4) { fprintf(stderr, @@ -230,6 +254,7 @@ int main(int argc, char* argv[]) { std::vector<uint8_t> buffer; bool is_image = HasImageExtension(info.filename); + bool is_mesh = HasMeshExtension(info.filename); if (is_image) { int w, h, channels; @@ -251,6 +276,166 @@ int main(int argc, char* argv[]) { stbi_image_free(img_data); printf("Processed image asset %s: %dx%d RGBA\n", info.name.c_str(), w, h); + } else if (is_mesh) { + std::ifstream obj_file(full_path); + if (!obj_file.is_open()) { + fprintf(stderr, "Error: Could not open mesh file: %s\n", + full_path.c_str()); + return 1; + } + + std::vector<float> v_pos; + std::vector<float> v_norm; + std::vector<float> v_uv; + + struct RawFace { + int v[3]; + int vt[3]; + int vn[3]; + }; + std::vector<RawFace> raw_faces; + + struct Vertex { + float p[3], n[3], u[2]; + }; + std::vector<Vertex> final_vertices; + std::vector<uint32_t> final_indices; + std::map<std::string, uint32_t> vertex_map; + + std::string obj_line; + while (std::getline(obj_file, obj_line)) { + if (obj_line.compare(0, 2, "v ") == 0) { + float x, y, z; + std::sscanf(obj_line.c_str(), "v %f %f %f", &x, &y, &z); + v_pos.push_back(x); + v_pos.push_back(y); + v_pos.push_back(z); + } else if (obj_line.compare(0, 3, "vn ") == 0) { + float x, y, z; + std::sscanf(obj_line.c_str(), "vn %f %f %f", &x, &y, &z); + v_norm.push_back(x); + v_norm.push_back(y); + v_norm.push_back(z); + } else if (obj_line.compare(0, 3, "vt ") == 0) { + float u, v; + std::sscanf(obj_line.c_str(), "vt %f %f", &u, &v); + v_uv.push_back(u); + v_uv.push_back(v); + } else if (obj_line.compare(0, 2, "f ") == 0) { + char s1[64], s2[64], s3[64]; + if (std::sscanf(obj_line.c_str(), "f %s %s %s", s1, s2, s3) == 3) { + std::string parts[3] = {s1, s2, s3}; + RawFace face = {}; + for (int i = 0; i < 3; ++i) { + int v_idx = 0, vt_idx = 0, vn_idx = 0; + if (parts[i].find("//") != std::string::npos) { + std::sscanf(parts[i].c_str(), "%d//%d", &v_idx, &vn_idx); + } else if (std::count(parts[i].begin(), parts[i].end(), '/') == + 2) { + std::sscanf(parts[i].c_str(), "%d/%d/%d", &v_idx, &vt_idx, + &vn_idx); + } else if (std::count(parts[i].begin(), parts[i].end(), '/') == + 1) { + std::sscanf(parts[i].c_str(), "%d/%d", &v_idx, &vt_idx); + } else { + std::sscanf(parts[i].c_str(), "%d", &v_idx); + } + face.v[i] = v_idx; + face.vt[i] = vt_idx; + face.vn[i] = vn_idx; + } + raw_faces.push_back(face); + } + } + } + + // Generate normals if missing + if (v_norm.empty() && !v_pos.empty()) { + printf("Generating normals for %s...\n", info.name.c_str()); + std::vector<Vec3> temp_normals(v_pos.size() / 3, {0, 0, 0}); + for (auto& face : raw_faces) { + // Indices are 1-based in OBJ + int idx0 = face.v[0] - 1; + int idx1 = face.v[1] - 1; + int idx2 = face.v[2] - 1; + + if (idx0 >= 0 && idx1 >= 0 && idx2 >= 0) { + Vec3 p0 = {v_pos[idx0 * 3], v_pos[idx0 * 3 + 1], v_pos[idx0 * 3 + 2]}; + Vec3 p1 = {v_pos[idx1 * 3], v_pos[idx1 * 3 + 1], v_pos[idx1 * 3 + 2]}; + Vec3 p2 = {v_pos[idx2 * 3], v_pos[idx2 * 3 + 1], v_pos[idx2 * 3 + 2]}; + + Vec3 normal = Vec3::cross(p1 - p0, p2 - p0).normalize(); + temp_normals[idx0] += normal; + temp_normals[idx1] += normal; + temp_normals[idx2] += normal; + } + } + + for (const auto& n : temp_normals) { + Vec3 normalized = n.normalize(); + v_norm.push_back(normalized.x); + v_norm.push_back(normalized.y); + v_norm.push_back(normalized.z); + } + + // Assign generated normals to faces + for (auto& face : raw_faces) { + face.vn[0] = face.v[0]; + face.vn[1] = face.v[1]; + face.vn[2] = face.v[2]; + } + } + + // Build final vertices + for (const auto& face : raw_faces) { + for (int i = 0; i < 3; ++i) { + // Reconstruct key string for uniqueness check + char key_buf[128]; + std::sprintf(key_buf, "%d/%d/%d", face.v[i], face.vt[i], face.vn[i]); + std::string key = key_buf; + + if (vertex_map.find(key) == vertex_map.end()) { + vertex_map[key] = (uint32_t)final_vertices.size(); + + Vertex v = {}; + if (face.v[i] > 0) { + v.p[0] = v_pos[(face.v[i] - 1) * 3]; + v.p[1] = v_pos[(face.v[i] - 1) * 3 + 1]; + v.p[2] = v_pos[(face.v[i] - 1) * 3 + 2]; + } + if (face.vn[i] > 0) { + v.n[0] = v_norm[(face.vn[i] - 1) * 3]; + v.n[1] = v_norm[(face.vn[i] - 1) * 3 + 1]; + v.n[2] = v_norm[(face.vn[i] - 1) * 3 + 2]; + } + if (face.vt[i] > 0) { + v.u[0] = v_uv[(face.vt[i] - 1) * 2]; + v.u[1] = v_uv[(face.vt[i] - 1) * 2 + 1]; + } + final_vertices.push_back(v); + } + final_indices.push_back(vertex_map[key]); + } + } + + // Format: [num_vertices(u32)][Vertex * num_vertices][num_indices(u32)][uint32_t + // * num_indices] + buffer.resize(sizeof(uint32_t) + final_vertices.size() * sizeof(Vertex) + + sizeof(uint32_t) + + final_indices.size() * sizeof(uint32_t)); + uint8_t* out_ptr = buffer.data(); + *reinterpret_cast<uint32_t*>(out_ptr) = (uint32_t)final_vertices.size(); + out_ptr += sizeof(uint32_t); + std::memcpy(out_ptr, final_vertices.data(), + final_vertices.size() * sizeof(Vertex)); + out_ptr += final_vertices.size() * sizeof(Vertex); + *reinterpret_cast<uint32_t*>(out_ptr) = (uint32_t)final_indices.size(); + out_ptr += sizeof(uint32_t); + std::memcpy(out_ptr, final_indices.data(), + final_indices.size() * sizeof(uint32_t)); + + printf("Processed mesh asset %s: %zu vertices, %zu indices\n", + info.name.c_str(), final_vertices.size(), final_indices.size()); } else { std::ifstream asset_file(full_path, std::ios::binary); if (!asset_file.is_open()) { |
