From 815ccfe6aa8ab6a563fd8e24e62cd84069cdb181 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 25 Oct 2026 06:55:35 -0400 Subject: [PATCH 1/3] simplifyModel leaves its target as it was when it returns -1 The header says target's faces are as they were where simplifyModel returns -1. Its fallback for a face that would take more vertices than 16-bit indices reach did not keep that: it ran simplifyFace on every face, which empties a face it cannot simplify, and the faces compacted before the one that did not fit were already written. A fallback that kept no triangle returned -1 with the target changed. simplifyModel now counts each face's vertices before it writes any face, and the fallback simplifies the faces into index lists first, through simplifyOwnFace, the body simplifyFace now shares: nothing is written unless a triangle is kept. No caller depends on either behaviour, and the fallback's faces are still simplifyFace's. The fallback had no test. A sloppy simplification, whose grid can take a triangle's vertices from other faces, is what reaches it, and the new case builds one: a sheet whose second face lies on its first, simplified as one mesh, puts 90,000 vertices under the first face. Each face of the result must be the face simplifyFace makes of it. With the fallback switched off the case fails on the compaction assert. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/almeshsimplifier.cpp | 210 +++++++++++-------- indra/llmath/almeshsimplifier.h | 3 +- indra/llmath/tests/almeshsimplifier_test.cpp | 99 +++++++++ 3 files changed, 218 insertions(+), 94 deletions(-) diff --git a/indra/llmath/almeshsimplifier.cpp b/indra/llmath/almeshsimplifier.cpp index 8a6b5451a3..57c0a7dd33 100644 --- a/indra/llmath/almeshsimplifier.cpp +++ b/indra/llmath/almeshsimplifier.cpp @@ -450,6 +450,103 @@ namespace return options; } + // mesh's face simplified on its own, as ALMeshSimplifier::simplifyFace + // makes it: its triangles, numbered as mesh's vertices are, in + // simplified. Returns their index count, 0 where no triangle is left. + size_t simplifyOwnFace(const Mesh& mesh, S32 face, const ALMeshSimplifier::Target& goal, ALMeshSimplifier::Seams seams, + std::vector& simplified) + { + using Seams = ALMeshSimplifier::Seams; + const U32 first = mesh.first[face]; + const U32 vertex_count = mesh.first[face + 1] - first; + const size_t index_count = mesh.first_index[face + 1] - mesh.first_index[face]; + simplified.clear(); + if (index_count < 3) + { + return 0; + } + + // The face's own triangles, numbered from its first vertex. + std::vector indices(mesh.indices.begin() + mesh.first_index[face], mesh.indices.begin() + mesh.first_index[face + 1]); + for (U32& index : indices) + { + index -= first; + } + + // Error is the model's, relative to its size; the simplifier takes it + // relative to the face's. + const F32 face_extent = meshopt_simplifyScale(mesh.positions[first].getF32ptr(), vertex_count, sizeof(LLVector4a)); + const F32 to_face = face_extent > 0.f ? mesh.extent / face_extent : 1.f; + const F32 error = goal.error * to_face; + + const size_t target_count = targetCount(index_count, goal.decimator); + simplified.resize(index_count); + F32 result_error = 0.f; + size_t count = 0; + if (seams == Seams::SLOPPY) + { + count = meshopt_simplifySloppy(simplified.data(), indices.data(), index_count, mesh.positions[first].getF32ptr(), vertex_count, + sizeof(LLVector4a), nullptr, target_count, llmin(error, 1.f), &result_error); + } + else + { + // A face that meets another keeps its vertices where it meets it, + // which the other's simplification keeps as well, and all of its + // parts, as a part of one face may be joined to another face's. A + // face meets another along a border, one's or the other's; its + // borders that meet no face are free, and so is its inside where + // another face lies over it. + const bool meets = std::any_of(mesh.shared.begin() + first, mesh.shared.begin() + first + vertex_count, + [](U8 shared) { return shared != 0; }); + std::vector locks; + if (seams != Seams::ALL) + { + protect(mesh, seams, first, vertex_count, locks); + } + if (meets) + { + locks.resize(vertex_count, 0); + for (U32 v = 0; v < vertex_count; ++v) + { + if (mesh.held[first + v]) + { + locks[v] |= (U8)meshopt_SimplifyVertex_Lock; + } + } + } + count = simplifyMesh(mesh, first, vertex_count, indices.data(), index_count, target_count, error, seamOptions(seams, goal), locks, + goal.distant && !meets, simplified.data(), result_error); + } + if (count < 3) + { + simplified.clear(); + return 0; + } + + simplified.resize(count); + for (U32& index : simplified) + { + index += first; + } + return count; + } + + // The vertices the triangles name. + size_t namedVertexCount(const std::vector& indices, size_t vertex_count) + { + std::vector named(vertex_count, 0); + size_t count = 0; + for (U32 index : indices) + { + if (!named[index]) + { + named[index] = 1; + ++count; + } + } + return count; + } + // What a simplified face takes from the face it is made from besides // its vertices and triangles. void copyLayout(const LLVolumeFace& layout, LLVolumeFace& face) @@ -641,33 +738,29 @@ F32 ALMeshSimplifier::simplifyModel(LLVolume& target, const Target& goal, Seams to.insert(to.end(), simplified.begin() + i, simplified.begin() + i + 3); } - for (S32 f = 0; f < faces; ++f) + // Nothing is written to target until every face is known to fit. + if (std::any_of(face_indices.begin(), face_indices.end(), + [&](const std::vector& indices) { return namedVertexCount(indices, mesh.vertexCount()) > U16_MAX; })) { - if (face_indices[f].size() < 3) + LL_WARNS("MeshSimplifier") << "A face of a simplified model takes more vertices than 16-bit indices reach; " + << "simplifying each face on its own" << LL_ENDL; + count = 0; + for (S32 f = 0; f < faces; ++f) { - continue; + count += simplifyOwnFace(mesh, f, goal, seams, face_indices[f]); } - if (!compact(base, f, face_indices[f].data(), face_indices[f].size(), target.getVolumeFace(f))) + if (count < 3) { - LL_WARNS("MeshSimplifier") << "A face of a simplified model takes more vertices than 16-bit indices reach; " - << "simplifying each face on its own" << LL_ENDL; - size_t face_count = 0; - S32 kept = 0; - for (S32 g = 0; g < faces; ++g) - { - if (simplifyFace(g, target.getVolumeFace(g), goal, seams) > 0.f) - { - face_count += target.getVolumeFace(g).mNumIndices; - ++kept; - } - } - return kept ? (F32)index_count / (F32)face_count : -1.f; + return -1.f; } } + for (S32 f = 0; f < faces; ++f) { - if (face_indices[f].size() < 3) + if (face_indices[f].size() < 3 || !compact(base, f, face_indices[f].data(), face_indices[f].size(), target.getVolumeFace(f))) { + // Every face fits, as counted above. + llassert(face_indices[f].size() < 3); makeEmpty(base.getVolumeFace(f), target.getVolumeFace(f)); } } @@ -676,84 +769,15 @@ F32 ALMeshSimplifier::simplifyModel(LLVolume& target, const Target& goal, Seams F32 ALMeshSimplifier::simplifyFace(S32 face, LLVolumeFace& target, const Target& goal, Seams seams) const { - const LLVolume& base = mBase; const Mesh& mesh = *mMesh; - const U32 first = mesh.first[face]; - const U32 vertex_count = mesh.first[face + 1] - first; - const size_t index_count = mesh.first_index[face + 1] - mesh.first_index[face]; - if (index_count < 3) - { - makeEmpty(base.getVolumeFace(face), target); - return -1.f; - } - - // The face's own triangles, numbered from its first vertex. - std::vector indices(mesh.indices.begin() + mesh.first_index[face], mesh.indices.begin() + mesh.first_index[face + 1]); - for (U32& index : indices) - { - index -= first; - } - - // Error is the model's, relative to its size; the simplifier takes it - // relative to the face's. - const F32 face_extent = meshopt_simplifyScale(mesh.positions[first].getF32ptr(), vertex_count, sizeof(LLVector4a)); - const F32 to_face = face_extent > 0.f ? mesh.extent / face_extent : 1.f; - const F32 error = goal.error * to_face; - - const size_t target_count = targetCount(index_count, goal.decimator); - std::vector simplified(index_count); - F32 result_error = 0.f; - size_t count = 0; - if (seams == Seams::SLOPPY) - { - count = meshopt_simplifySloppy(simplified.data(), indices.data(), index_count, mesh.positions[first].getF32ptr(), vertex_count, - sizeof(LLVector4a), nullptr, target_count, llmin(error, 1.f), &result_error); - } - else - { - // A face that meets another keeps its vertices where it meets it, - // which the other's simplification keeps as well, and all of its - // parts, as a part of one face may be joined to another face's. A - // face meets another along a border, one's or the other's; its - // borders that meet no face are free, and so is its inside where - // another face lies over it. - const bool meets = std::any_of(mesh.shared.begin() + first, mesh.shared.begin() + first + vertex_count, - [](U8 shared) { return shared != 0; }); - std::vector locks; - if (seams != Seams::ALL) - { - protect(mesh, seams, first, vertex_count, locks); - } - if (meets) - { - locks.resize(vertex_count, 0); - for (U32 v = 0; v < vertex_count; ++v) - { - if (mesh.held[first + v]) - { - locks[v] |= (U8)meshopt_SimplifyVertex_Lock; - } - } - } - count = simplifyMesh(mesh, first, vertex_count, indices.data(), index_count, target_count, error, seamOptions(seams, goal), locks, - goal.distant && !meets, simplified.data(), result_error); - } - if (count < 3) - { - makeEmpty(base.getVolumeFace(face), target); - return -1.f; - } - - for (size_t i = 0; i < count; ++i) - { - simplified[i] += first; - } - if (!compact(base, face, simplified.data(), count, target)) + std::vector simplified; + const size_t count = simplifyOwnFace(mesh, face, goal, seams, simplified); + if (count < 3 || !compact(mBase, face, simplified.data(), count, target)) { // A face's own vertices are never more than 16-bit indices reach. - llassert(false); - makeEmpty(base.getVolumeFace(face), target); + llassert(count < 3); + makeEmpty(mBase.getVolumeFace(face), target); return -1.f; } - return (F32)index_count / (F32)count; + return (F32)(mesh.first_index[face + 1] - mesh.first_index[face]) / (F32)count; } diff --git a/indra/llmath/almeshsimplifier.h b/indra/llmath/almeshsimplifier.h index 59fe36181b..cb5ab7ee97 100644 --- a/indra/llmath/almeshsimplifier.h +++ b/indra/llmath/almeshsimplifier.h @@ -90,7 +90,8 @@ class ALMeshSimplifier // or -1 where no triangle was left, in which case target's faces are as // they were. A face left with no triangle is one of three indices to a // vertex at the origin. Where a face would take more vertices than 16-bit - // indices reach, each face is simplified on its own instead. + // indices reach, each face is simplified on its own instead, as + // simplifyFace simplifies it. F32 simplifyModel(LLVolume& target, const Target& goal, Seams seams) const; // Simplifies base's face on its own into target. A face that shares a diff --git a/indra/llmath/tests/almeshsimplifier_test.cpp b/indra/llmath/tests/almeshsimplifier_test.cpp index 1594fed00e..e8b8770067 100644 --- a/indra/llmath/tests/almeshsimplifier_test.cpp +++ b/indra/llmath/tests/almeshsimplifier_test.cpp @@ -1091,6 +1091,105 @@ namespace CHECK(differ == 0); } + // A flat sheet of 600 columns by 200 rows of points as two faces of + // 60,000 vertices: the first has every other column's points, two + // columns a quad, and the second every point of the first 300 columns, + // a column a quad. A sloppy simplification that keeps every triangle + // takes the first face's vertex where the second's lies on it, so that + // the second's triangles with their first corner there are the first + // face's, and name half of the second's vertices besides its own. + LLPointer sheetOverColumns() + { + constexpr S32 COLUMNS = 600; + constexpr S32 ROWS = 200; + auto point = [](S32 column, S32 row, Geometry& g) + { + const F32 x = (F32)column / (COLUMNS - 1); + const F32 y = (F32)row / (ROWS - 1); + g.add(LLVector3(x - 0.5f, y - 0.5f, 0.f), LLVector3(0.f, 0.f, 1.f), LLVector2(x, y)); + }; + auto grid = [](S32 columns, Geometry& g) + { + for (S32 j = 0; j + 1 < ROWS; ++j) + { + for (S32 i = 0; i + 1 < columns; ++i) + { + const U16 c0 = (U16)(i + j * columns); + const U16 c1 = (U16)(c0 + 1); + const U16 c2 = (U16)(c0 + columns); + const U16 c3 = (U16)(c2 + 1); + g.triangle(c0, c1, c3); + g.triangle(c0, c3, c2); + } + } + }; + Geometry first, second; + for (S32 row = 0; row < ROWS; ++row) + { + for (S32 column = 0; column < COLUMNS; column += 2) + { + point(column, row, first); + } + for (S32 column = 0; column < COLUMNS / 2; ++column) + { + point(column, row, second); + } + } + grid(COLUMNS / 2, first); + grid(COLUMNS / 2, second); + return volumeOf({ first, second }); + } + + TEST_CASE("almeshsimplifier: simplified together, a face past 16-bit indices makes each face simplify on its own", "[almeshsimplifier]") + { + ArenaInstalled installed; + LLPointer base = sheetOverColumns(); + const ALMeshSimplifier simplifier(*base); + const ALMeshSimplifier::Target goal = toCount(1.f); + + // The scene: the sheet simplified as one mesh, as simplifyModel + // simplifies it, leaves the first face's triangles naming more + // vertices than 16-bit indices reach. + const LLVolumeFace& first = base->getVolumeFace(0); + const LLVolumeFace& second = base->getVolumeFace(1); + std::vector positions(first.mPositions, first.mPositions + first.mNumVertices); + positions.insert(positions.end(), second.mPositions, second.mPositions + second.mNumVertices); + std::vector indices(first.mIndices, first.mIndices + first.mNumIndices); + for (S32 i = 0; i < second.mNumIndices; ++i) + { + indices.push_back(second.mIndices[i] + (U32)first.mNumVertices); + } + std::vector together(indices.size()); + const size_t together_count = meshopt_simplifySloppy(together.data(), indices.data(), indices.size(), positions[0].getF32ptr(), + positions.size(), sizeof(LLVector4a), nullptr, indices.size(), 1.f, nullptr); + std::set named; + for (size_t i = 0; i < together_count; i += 3) + { + if (together[i] < (U32)first.mNumVertices) + { + named.insert(together.begin() + i, together.begin() + i + 3); + } + } + INFO(fmt::format("simplified as one mesh, the first face's triangles name {} vertices", named.size())); + REQUIRE(named.size() > U16_MAX); + + LLPointer target = targetFor(*base); + REQUIRE(simplifier.simplifyModel(*target, goal, Seams::SLOPPY) > 0.f); + for (S32 f = 0; f < 2; ++f) + { + INFO(fmt::format("face {}", f)); + LLVolumeFace alone; + REQUIRE(simplifier.simplifyFace(f, alone, goal, Seams::SLOPPY) > 0.f); + const LLVolumeFace& face = target->getVolumeFace(f); + REQUIRE(wellFormed(face)); + CHECK(fromBase(face, *base)); + REQUIRE(face.mNumVertices == alone.mNumVertices); + REQUIRE(face.mNumIndices == alone.mNumIndices); + CHECK(std::equal(face.mIndices, face.mIndices + face.mNumIndices, alone.mIndices)); + CHECK(memcmp(face.mPositions, alone.mPositions, sizeof(LLVector4a) * face.mNumVertices) == 0); + } + } + // ------------------------------------------------------------------ // Normals From 8f11d19fffe8eeea74feea1ee50f4bec2df31498 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 25 Oct 2026 06:56:12 -0400 Subject: [PATCH 2/3] Auto mode copies the closest mode's faces back instead of simplifying again When the sloppy pass fails or keeps more than the precise modes did, Auto falls back to the closest of its ALL, UV and NONE passes. It got that result back by running simplify_model again in that mode. On a distant LOD with a triangle limit, where the prune check can run each mode's simplification twice, that was two of the eight attribute-aware simplifications Auto made of the model, where develop before the prim generation stack made four position-only ones. The mode loop now copies the closest mode's faces when a later pass is about to replace them, and the fallback assigns them back: the same faces bit for bit, as meshoptimizer is deterministic. Measured with the cascade replayed outside the viewer, develop's against this one, RelWithDebInfo, best of three: - A model Auto falls back on, two clusters of tiny cubes, each cluster as two faces at the same positions, 82,944 triangles: LOD 2 from 4 to 3 attribute-aware simplifications, 64.9 to 52.3 ms; LODs 1 and 0 from 8 to 6, 122.9 to 97.5 ms and 124.0 to 96.5 ms. - The mesh cache never takes the fallback: 1,285 models of 1,000 triangles or more, 3,855 LOD cases, 7,364 simplifications either way, 30.8 s against 30.6 s, every result identical. 389 of the cases made six, three modes each run twice by the prune check, and then took sloppy; the copy does not reach those. - Large uploads, bodies of 117,106 triangles and a garment of 78,807, never fall back either, and take the same time. Co-Authored-By: Claude Opus 5.5 --- indra/newview/llmodelpreview.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/indra/newview/llmodelpreview.cpp b/indra/newview/llmodelpreview.cpp index 4155835374..bec580ff53 100644 --- a/indra/newview/llmodelpreview.cpp +++ b/indra/newview/llmodelpreview.cpp @@ -1602,23 +1602,28 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d { // Remove progressively more data if we can't reach the target, // keeping the closest a mode came: target_model holds the - // last mode's. + // last mode's, and closest a copy of the faces of the closest + // that fell short, as the passes after it replace them. F32 allowed_ratio_drift = 1.8f; using Seams = ALMeshSimplifier::Seams; F32 precise_ratio = -1.f; - Seams precise_seams = Seams::ALL; + std::vector closest; for (Seams seams : { Seams::ALL, Seams::UV, Seams::NONE }) { const F32 ratio = simplify_model(indices_decimator, seams); - if (ratio > precise_ratio) + const bool closer = ratio > precise_ratio; + if (closer) { precise_ratio = ratio; - precise_seams = seams; } if (ratio >= 0 && ratio * allowed_ratio_drift >= indices_decimator) { break; } + if (closer) + { + target_model->copyFacesTo(closest); + } } if (precise_ratio < 0 || (precise_ratio * allowed_ratio_drift < indices_decimator)) @@ -1699,8 +1704,12 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d } else { - // Fallback to normal method, in the mode that came closest - precise_ratio = simplify_model(indices_decimator, precise_seams); + // Fallback to normal method: the faces of the mode that came closest + llassert(closest.size() == (size_t)target_model->getNumVolumeFaces()); + for (S32 face_idx = 0; face_idx < (S32)closest.size(); ++face_idx) + { + target_model->getVolumeFace(face_idx) = closest[face_idx]; + } } LL_DEBUGS("Upload") << "Model " << target_model->getName() From d315437ad0cd556534e93525281ea77991a110bf Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 25 Oct 2026 06:56:14 -0400 Subject: [PATCH 3/3] GLTF primitives no longer make tangents nothing reads Primitive::prep unrolled each primitive the file gives no tangents, made MikkTSpace tangents for it with meshopt_generateTangents and welded it again with the tangents as a stream. Only the uploader and local meshes load GLTF now, through LLGLTFLoader, which reads positions, normals, texture coordinates, joints and weights and never mTangents. The tangents were most of prep's geometry work, and their stream split vertices the upload then carried. - A primitive without normals is still unrolled for its flat normals and welded again, now without tangents. - Any other surface primitive the file gives no tangents is welded in place, on the streams the unrolled weld used less the tangents: the same vertices in the same order, without the unrolled copy. Its strips and fans still become the triangle list the loader reads. - Points and lines get no tangents. Measured with prep replayed outside the viewer on the same accessor data, this commit's helpers copied in verbatim: every triangle is develop's, corner by corner in position, normal and texture coordinate, for every primitive as its file gives it, with its normals dropped, and for strips, fans and lists made up for it, indexed or not. Vertices as uploaded, develop against this: - CarConcept 173,510 to 162,766, AntiqueCamera 21,233 to 21,170, MosquitoInAmber 18,680 to 18,678. - A Blender export of 293,495 triangles 159,515 to 159,510; one of 1.9 million triangles, and the files that give their own tangents, unchanged. Prep's geometry work: the 1.9 million triangle file 613 to 58 ms, the 293,495 triangle one 95 to 7.6 ms, CarConcept 46 to 4.6 ms; flat normals, the large file with its normals dropped, 936 to 519 ms. Should GLTF scenes be rendered again, their tangents need making again. The compatible mode removed here also gave a triangle of no UV area a different w from the Blender MikkTSpace it replaced. Co-Authored-By: Claude Opus 5.5 --- indra/newview/gltf/primitive.cpp | 275 ++++++++++++++++--------------- 1 file changed, 140 insertions(+), 135 deletions(-) diff --git a/indra/newview/gltf/primitive.cpp b/indra/newview/gltf/primitive.cpp index d0e8c8810f..9c4e52a0d4 100644 --- a/indra/newview/gltf/primitive.cpp +++ b/indra/newview/gltf/primitive.cpp @@ -36,12 +36,64 @@ using namespace LL::GLTF; -// A primitive unrolled into a triangle list, for its tangents and flat normals -struct MikktMesh +// get the vertex index of each corner of a primitive's triangles, as a triangle list +// returns false if the Primitive cannot be converted to a triangle list +static bool get_triangle_corners(const Primitive* prim, std::vector& corners) +{ + bool indexed = !prim->mIndexArray.empty(); + size_t count = indexed ? prim->mIndexArray.size() : prim->mPositions.size(); + llassert(count <= size_t(U32_MAX)); + + corners.clear(); + + if (prim->mMode == Primitive::Mode::TRIANGLES) + { + for (U32 i = 0; i < U32(count / 3 * 3); ++i) + { + corners.push_back(i); + } + } + else if (prim->mMode == Primitive::Mode::TRIANGLE_STRIP || + prim->mMode == Primitive::Mode::TRIANGLE_FAN) + { + for (U32 tri_idx = 0; size_t(tri_idx) + 2 < count; ++tri_idx) + { + U32 idx[3] = { tri_idx, tri_idx + 1, tri_idx + 2 }; + + if (prim->mMode == Primitive::Mode::TRIANGLE_FAN) + { + idx[0] = 0; + } + else if (tri_idx % 2 != 0) + { + std::swap(idx[1], idx[2]); + } + + corners.insert(corners.end(), idx, idx + 3); + } + } + else + { + LL_WARNS("GLTF") << "Unsupported primitive mode for conversion to triangles: " << (S32)prim->mMode << LL_ENDL; + return false; + } + + if (indexed) + { + for (U32& corner : corners) + { + corner = prim->mIndexArray[corner]; + } + } + + return true; +} + +// A primitive unrolled into a triangle list, for its flat normals +struct TriangleList { std::vector p; //positions std::vector n; //normals - std::vector t; //tangents std::vector tc0; //texcoords 0 std::vector tc1; //texcoords 1 std::vector c; //colors @@ -52,28 +104,13 @@ struct MikktMesh // returns false if the Primitive cannot be converted to a triangle list bool copy(const Primitive* prim) { - bool indexed = !prim->mIndexArray.empty(); - size_t vert_count = indexed ? prim->mIndexArray.size() : prim->mPositions.size(); - - size_t triangle_count = 0; - - if (prim->mMode == Primitive::Mode::TRIANGLE_STRIP || - prim->mMode == Primitive::Mode::TRIANGLE_FAN) + std::vector corners; + if (!get_triangle_corners(prim, corners)) { - triangle_count = vert_count - 2; - } - else if (prim->mMode == Primitive::Mode::TRIANGLES) - { - triangle_count = vert_count / 3; - } - else - { - LL_WARNS("GLTF") << "Unsupported primitive mode for conversion to triangles: " << (S32)prim->mMode << LL_ENDL; return false; } - vert_count = triangle_count * 3; - llassert(vert_count <= size_t(U32_MAX)); // triangle_count will also naturally be under the limit + size_t vert_count = corners.size(); p.resize(vert_count); n.resize(vert_count); @@ -81,15 +118,6 @@ struct MikktMesh c.resize(vert_count); bool has_normals = !prim->mNormals.empty(); - if (has_normals) - { - n.resize(vert_count); - } - bool has_tangents = !prim->mTangents.empty(); - if (has_tangents) - { - t.resize(vert_count); - } bool rigged = !prim->mWeights.empty(); if (rigged) @@ -104,63 +132,27 @@ struct MikktMesh tc1.resize(vert_count); } - for (U32 tri_idx = 0; tri_idx < U32(triangle_count); ++tri_idx) + for (size_t i = 0; i < vert_count; ++i) { - U32 idx[3] = {0, 0, 0}; + U32 idx = corners[i]; + p[i].set(prim->mPositions[idx].getF32ptr()); + tc0[i].set(prim->mTexCoords0[idx]); + c[i] = prim->mColors[idx]; - if (prim->mMode == Primitive::Mode::TRIANGLES) + if (multi_uv) { - idx[0] = tri_idx * 3; - idx[1] = tri_idx * 3 + 1; - idx[2] = tri_idx * 3 + 2; - } - else if (prim->mMode == Primitive::Mode::TRIANGLE_STRIP) - { - idx[0] = tri_idx; - idx[1] = tri_idx + 1; - idx[2] = tri_idx + 2; - - if (tri_idx % 2 != 0) - { - std::swap(idx[1], idx[2]); - } - } - else if (prim->mMode == Primitive::Mode::TRIANGLE_FAN) - { - idx[0] = 0; - idx[1] = tri_idx + 1; - idx[2] = tri_idx + 2; + tc1[i].set(prim->mTexCoords1[idx]); } - if (indexed) + if (has_normals) { - idx[0] = prim->mIndexArray[idx[0]]; - idx[1] = prim->mIndexArray[idx[1]]; - idx[2] = prim->mIndexArray[idx[2]]; + n[i].set(prim->mNormals[idx].getF32ptr()); } - for (U32 v = 0; v < 3; ++v) + if (rigged) { - U32 i = tri_idx * 3 + v; - p[i].set(prim->mPositions[idx[v]].getF32ptr()); - tc0[i].set(prim->mTexCoords0[idx[v]]); - c[i] = prim->mColors[idx[v]]; - - if (multi_uv) - { - tc1[i].set(prim->mTexCoords1[idx[v]]); - } - - if (has_normals) - { - n[i].set(prim->mNormals[idx[v]].getF32ptr()); - } - - if (rigged) - { - w[i].set(prim->mWeights[idx[v]].getF32ptr()); - j[i] = prim->mJoints[idx[v]]; - } + w[i].set(prim->mWeights[idx].getF32ptr()); + j[i] = prim->mJoints[idx]; } } @@ -185,25 +177,6 @@ struct MikktMesh } } - // MikkTSpace's tangents, as meshoptimizer's compatible mode computes - // them. GLTF's V runs down the texture, the other way from the V a - // tangent's handedness is taken against, so the tangents are those of - // the UVs as they are with w the other way. - void genTangents() - { - t.resize(p.size()); - if (p.empty()) - { - return; - } - meshopt_generateTangents(t[0].mV, nullptr, p.size(), p[0].mV, p.size(), sizeof(LLVector3), n[0].mV, sizeof(LLVector3), tc0[0].mV, - sizeof(LLVector2), meshopt_TangentCompatible); - for (LLVector4& tangent : t) - { - tangent.mV[3] = -tangent.mV[3]; - } - } - // write to target primitive as an indexed triangle list // Only modifies runtime data, does not modify the original GLTF data void write(Primitive* prim) const @@ -213,7 +186,6 @@ struct MikktMesh { { &p[0], sizeof(LLVector3), sizeof(LLVector3) }, { &n[0], sizeof(LLVector3), sizeof(LLVector3) }, - { &t[0], sizeof(LLVector4), sizeof(LLVector4) }, { &tc0[0], sizeof(LLVector2), sizeof(LLVector2) }, { &c[0], sizeof(LLColor4U), sizeof(LLColor4U) } }; @@ -238,7 +210,6 @@ struct MikktMesh prim->mTexCoords0.resize(vert_count); prim->mNormals.resize(vert_count); - prim->mTangents.resize(vert_count); prim->mPositions.resize(vert_count); prim->mColors.resize(vert_count); if (!w.empty()) @@ -263,7 +234,6 @@ struct MikktMesh prim->mPositions[dst_idx].load3(p[src_idx].mV); prim->mNormals[dst_idx].load3(n[src_idx].mV); prim->mTexCoords0[dst_idx] = tc0[src_idx]; - prim->mTangents[dst_idx].loadua(t[src_idx].mV); prim->mColors[dst_idx] = c[src_idx]; if (!w.empty()) @@ -282,6 +252,70 @@ struct MikktMesh } }; +template +static void remap_vertices(std::vector& vertices, const std::vector& remap, size_t vert_count) +{ + std::vector remapped(vert_count); + meshopt_remapVertexBuffer(remapped.data(), vertices.data(), remap.size(), sizeof(T), remap.data()); + vertices.swap(remapped); +} + +// weld a primitive in place into an indexed triangle list of the vertices its triangles use +// Only modifies runtime data, does not modify the original GLTF data +static bool weld(Primitive* prim) +{ + std::vector corners; + if (!get_triangle_corners(prim, corners)) + { + return false; + } + + std::vector mos = + { + { prim->mPositions.data(), sizeof(LLVector3), sizeof(LLVector4a) }, + { prim->mNormals.data(), sizeof(LLVector3), sizeof(LLVector4a) }, + { prim->mTexCoords0.data(), sizeof(LLVector2), sizeof(LLVector2) }, + { prim->mColors.data(), sizeof(LLColor4U), sizeof(LLColor4U) } + }; + + bool rigged = !prim->mWeights.empty(); + if (rigged) + { + mos.push_back({ prim->mWeights.data(), sizeof(LLVector4a), sizeof(LLVector4a) }); + mos.push_back({ prim->mJoints.data(), sizeof(U64), sizeof(U64) }); + } + + bool multi_uv = !prim->mTexCoords1.empty(); + if (multi_uv) + { + mos.push_back({ prim->mTexCoords1.data(), sizeof(LLVector2), sizeof(LLVector2) }); + } + + // only the vertices the corners name are read, and those no corner names are dropped + std::vector remap(prim->mPositions.size()); + size_t vert_count = meshopt_generateVertexRemapMulti(remap.data(), corners.data(), corners.size(), remap.size(), mos.data(), mos.size()); + + prim->mIndexArray.resize(corners.size()); + meshopt_remapIndexBuffer(prim->mIndexArray.data(), corners.data(), corners.size(), remap.data()); + + remap_vertices(prim->mPositions, remap, vert_count); + remap_vertices(prim->mNormals, remap, vert_count); + remap_vertices(prim->mTexCoords0, remap, vert_count); + remap_vertices(prim->mColors, remap, vert_count); + if (rigged) + { + remap_vertices(prim->mWeights, remap, vert_count); + remap_vertices(prim->mJoints, remap, vert_count); + } + if (multi_uv) + { + remap_vertices(prim->mTexCoords1, remap, vert_count); + } + + prim->mGLMode = LLRender::TRIANGLES; + return true; +} + static void vertical_flip(std::vector& texcoords) { @@ -415,56 +449,27 @@ bool Primitive::prep(Asset& asset) if (mMode == Mode::POINTS || mMode == Mode::LINES || mMode == Mode::LINE_LOOP || mMode == Mode::LINE_STRIP) { //no normals and no surfaces, this primitive is unlit - mTangents.clear(); unlit = true; } else { // unroll into non-indexed array of flat shaded triangles - MikktMesh data; + TriangleList data; if (!data.copy(this)) { return false; } data.genNormals(); - data.genTangents(); data.write(this); } } - - if (mTangents.empty() && !unlit) - { // NOTE: must be done last because tangent generation rewrites the other arrays - if (mMode == Mode::POINTS || mMode == Mode::LINES || mMode == Mode::LINE_LOOP || mMode == Mode::LINE_STRIP) - { - // for points and lines, just make sure tangent is perpendicular to normal - mTangents.resize(mNormals.size()); - LLVector4a up(0.f, 0.f, 1.f, 0.f); - LLVector4a left(1.f, 0.f, 0.f, 0.f); - for (U32 i = 0; i < mNormals.size(); ++i) - { - if (fabsf(mNormals[i].getF32ptr()[2]) < 0.999f) - { - mTangents[i] = up.cross3(mNormals[i]); - } - else - { - mTangents[i] = left.cross3(mNormals[i]); - } - - mTangents[i].getF32ptr()[3] = 1.f; - } - } - else + else if (mTangents.empty() && !unlit && + mMode != Mode::POINTS && mMode != Mode::LINES && mMode != Mode::LINE_LOOP && mMode != Mode::LINE_STRIP) + { + if (!weld(this)) { - MikktMesh data; - if (!data.copy(this)) - { - return false; - } - - data.genTangents(); - data.write(this); + return false; } }