mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-08 10:03:51 +00:00
feat(editor): add --smooth-mesh-normals for area-weighted normal recompute
Recomputes per-vertex normals as the area-weighted average of incident face normals. The cross-product magnitude is twice the triangle area, so larger faces contribute more to the local direction — gives a clean smooth-shaded result on curved surfaces. Use cases: - Imported geometry has no normals (--import-obj leaves them zero or face-flat). - Custom transforms have desynced normals from positions. - Faceted-by-construction meshes (cube, stairs) need a smooth re-shade for stylistic reasons. Degenerate verts (unreferenced or with sum that cancels to zero — e.g., the two poles of a UV sphere) fall back to (0,1,0) rather than leaving NaN; reported separately so the user sees how many. Verified: sphere → 219 of 221 normalized + 2 degenerate poles handled cleanly; minimal triangle → 3/3 normalized. Brings command count to 229 (kArgRequired 210).
This commit is contained in:
parent
b38dec7a83
commit
d54440a75b
1 changed files with 84 additions and 0 deletions
|
|
@ -561,6 +561,8 @@ static void printUsage(const char* argv0) {
|
|||
std::printf(" Invert every vertex normal (use for inside-out meshes or two-sided pre-flip)\n");
|
||||
std::printf(" --mirror-mesh <wom-base> <x|y|z>\n");
|
||||
std::printf(" Mirror every vertex + normal across the chosen axis (also flips winding)\n");
|
||||
std::printf(" --smooth-mesh-normals <wom-base>\n");
|
||||
std::printf(" Recompute per-vertex normals as area-weighted averages of incident face normals\n");
|
||||
std::printf(" --merge-meshes <a-base> <b-base> <out-base>\n");
|
||||
std::printf(" Combine two WOMs into one (vertex/index buffers concatenated, batches preserved)\n");
|
||||
std::printf(" --add-item <zoneDir> <name> [id] [quality] [displayId] [itemLevel]\n");
|
||||
|
|
@ -986,6 +988,7 @@ int main(int argc, char* argv[]) {
|
|||
"--scale-mesh", "--translate-mesh", "--strip-mesh",
|
||||
"--gen-texture-noise", "--rotate-mesh",
|
||||
"--center-mesh", "--flip-mesh-normals", "--mirror-mesh",
|
||||
"--smooth-mesh-normals",
|
||||
"--merge-meshes",
|
||||
"--gen-texture-radial", "--gen-texture-stripes",
|
||||
"--validate-glb", "--info-glb", "--info-glb-tree", "--info-glb-bytes",
|
||||
|
|
@ -17189,6 +17192,87 @@ int main(int argc, char* argv[]) {
|
|||
wom.boundMin.x, wom.boundMin.y, wom.boundMin.z,
|
||||
wom.boundMax.x, wom.boundMax.y, wom.boundMax.z);
|
||||
return 0;
|
||||
} else if (std::strcmp(argv[i], "--smooth-mesh-normals") == 0 && i + 1 < argc) {
|
||||
// Recompute per-vertex normals as the area-weighted
|
||||
// average of incident face normals. Useful when:
|
||||
// - Imported geometry has no normals (--import-obj
|
||||
// leaves them zero or face-flat).
|
||||
// - Custom transforms have desynced normals from the
|
||||
// positions (e.g., user post-processed the WOM
|
||||
// externally).
|
||||
// - Faceted-by-construction meshes (cube, stairs) need
|
||||
// a smooth re-shade for stylistic reasons.
|
||||
//
|
||||
// The cross-product magnitude is twice the triangle area,
|
||||
// which weights large faces more — bigger triangles
|
||||
// contribute more to the local surface direction.
|
||||
std::string womBase = argv[++i];
|
||||
if (womBase.size() >= 4 &&
|
||||
womBase.substr(womBase.size() - 4) == ".wom") {
|
||||
womBase = womBase.substr(0, womBase.size() - 4);
|
||||
}
|
||||
if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) {
|
||||
std::fprintf(stderr,
|
||||
"smooth-mesh-normals: %s.wom does not exist\n",
|
||||
womBase.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
|
||||
if (!wom.isValid()) {
|
||||
std::fprintf(stderr,
|
||||
"smooth-mesh-normals: failed to load %s.wom\n",
|
||||
womBase.c_str());
|
||||
return 1;
|
||||
}
|
||||
// Reset vertex normals to zero so the accumulator sums
|
||||
// cleanly.
|
||||
for (auto& v : wom.vertices) v.normal = glm::vec3(0);
|
||||
for (size_t k = 0; k + 2 < wom.indices.size(); k += 3) {
|
||||
uint32_t i0 = wom.indices[k];
|
||||
uint32_t i1 = wom.indices[k + 1];
|
||||
uint32_t i2 = wom.indices[k + 2];
|
||||
if (i0 >= wom.vertices.size() ||
|
||||
i1 >= wom.vertices.size() ||
|
||||
i2 >= wom.vertices.size()) continue;
|
||||
glm::vec3 p0 = wom.vertices[i0].position;
|
||||
glm::vec3 p1 = wom.vertices[i1].position;
|
||||
glm::vec3 p2 = wom.vertices[i2].position;
|
||||
// Cross product magnitude == 2 * triangle area, used
|
||||
// as the weight.
|
||||
glm::vec3 faceN = glm::cross(p1 - p0, p2 - p0);
|
||||
wom.vertices[i0].normal += faceN;
|
||||
wom.vertices[i1].normal += faceN;
|
||||
wom.vertices[i2].normal += faceN;
|
||||
}
|
||||
int normalized = 0, degenerate = 0;
|
||||
for (auto& v : wom.vertices) {
|
||||
float len = glm::length(v.normal);
|
||||
if (len > 1e-6f) {
|
||||
v.normal /= len;
|
||||
normalized++;
|
||||
} else {
|
||||
// Vertex unreferenced or sum cancelled — fall
|
||||
// back to "up" rather than leaving zero so the
|
||||
// shader doesn't get a dark NaN spot.
|
||||
v.normal = glm::vec3(0, 1, 0);
|
||||
degenerate++;
|
||||
}
|
||||
}
|
||||
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
|
||||
std::fprintf(stderr,
|
||||
"smooth-mesh-normals: failed to save %s.wom\n",
|
||||
womBase.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::printf("Smoothed normals on %s.wom\n", womBase.c_str());
|
||||
std::printf(" vertices touched : %zu\n", wom.vertices.size());
|
||||
std::printf(" triangles read : %zu\n", wom.indices.size() / 3);
|
||||
std::printf(" normalized : %d\n", normalized);
|
||||
if (degenerate > 0) {
|
||||
std::printf(" degenerate : %d (set to (0,1,0))\n",
|
||||
degenerate);
|
||||
}
|
||||
return 0;
|
||||
} else if (std::strcmp(argv[i], "--merge-meshes") == 0 && i + 3 < argc) {
|
||||
// Combine two WOMs into one. The second mesh's indices
|
||||
// are offset by the first mesh's vertex count, and its
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue