diff --git a/indra/llmath/CMakeLists.txt b/indra/llmath/CMakeLists.txt index 2128d7ee74..e0787a35ed 100644 --- a/indra/llmath/CMakeLists.txt +++ b/indra/llmath/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(llmath STATIC) target_sources( llmath PRIVATE + almeshsimplifier.cpp alplaneset.cpp alprojection.cpp alsimdkernels.cpp @@ -47,6 +48,7 @@ target_sources( PUBLIC FILE_SET HEADERS FILES + almeshsimplifier.h alplaneset.h alprojection.h alsimdkernels.h @@ -127,6 +129,7 @@ if(AL_BUILD_TESTS) PROJECT llmath SOURCES tests/alignment_test.cpp + tests/almeshsimplifier_test.cpp tests/alplaneset_test.cpp tests/alsimdkernels_test.cpp tests/llbbox_test.cpp diff --git a/indra/llmath/almeshsimplifier.cpp b/indra/llmath/almeshsimplifier.cpp new file mode 100644 index 0000000000..a540fcd03d --- /dev/null +++ b/indra/llmath/almeshsimplifier.cpp @@ -0,0 +1,677 @@ +/** + * @file almeshsimplifier.cpp + * @brief A model's levels of detail, made from its faces by meshoptimizer's + * simplifier + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * 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 + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "almeshsimplifier.h" + +#include "llvolume.h" +#include "llvector4a.h" +#include "v2math.h" + +#include + +#include +#include +#include + +namespace +{ + // What the simplifier weighs at a vertex besides its position: its + // normal, as a direction in the authored shape, and its texture + // coordinate over its face's density. + constexpr size_t ATTRIBUTE_COUNT = 5; + + // A change of 1/weight in an attribute across a distance d costs as + // much as moving the surface by d, in a model scaled to a size of 1. + // A normal that turns through a right angle across a triangle then + // costs about a third of the triangle's width in displacement. + constexpr F32 NORMAL_WEIGHT = 0.25f; + + // Texture coordinates are taken over the square root of their face's + // mean triangle area in texture space, at any texture scale: a + // coordinate that slides by a triangle's width in the texture, across a + // triangle, costs a tenth of that triangle's width in displacement. + constexpr F32 UV_WEIGHT = 0.1f; + + constexpr F32 ATTRIBUTE_WEIGHTS[ATTRIBUTE_COUNT] = { NORMAL_WEIGHT, NORMAL_WEIGHT, NORMAL_WEIGHT, UV_WEIGHT, UV_WEIGHT }; + + // base's faces as meshoptimizer takes them, their vertices and indices + // numbered face after face. + struct Mesh + { + std::vector first; // each face's first vertex, then the vertex count + std::vector first_index; // each face's first index, then the index count + std::vector indices; // each face's triangles, those of its indices that name its vertices + std::vector positions; // in the authored shape + std::vector attributes; // ATTRIBUTE_COUNT a vertex + std::vector shared; // at a position another face has a vertex at too + std::vector uv_seam; // at a position its face has a vertex with another texture coordinate at + F32 extent = 0.f; // the largest extent of the positions + + size_t vertexCount() const { return first.back(); } + size_t indexCount() const { return first_index.back(); } + + S32 faceOf(U32 vertex) const + { + return (S32)(std::upper_bound(first.begin(), first.end(), vertex) - first.begin()) - 1; + } + }; + + LLVector4a unscale(const LLVector3& normalized_scale) + { + // A loader keeps a size of nothing as 1, and so does this. + LLVector4a scale; + scale.set(normalized_scale.mV[0] != 0.f ? normalized_scale.mV[0] : 1.f, + normalized_scale.mV[1] != 0.f ? normalized_scale.mV[1] : 1.f, + normalized_scale.mV[2] != 0.f ? normalized_scale.mV[2] : 1.f, 1.f); + return scale; + } + + void prepare(const LLVolume& base, Mesh& mesh) + { + const S32 faces = base.getNumVolumeFaces(); + mesh.first.assign(faces + 1, 0); + mesh.first_index.assign(faces + 1, 0); + size_t index_capacity = 0; + for (S32 f = 0; f < faces; ++f) + { + const LLVolumeFace& face = base.getVolumeFace(f); + mesh.first[f + 1] = mesh.first[f] + (face.mPositions && face.mNumVertices > 0 ? (U32)face.mNumVertices : 0); + index_capacity += face.mIndices && face.mNumIndices > 0 ? (size_t)face.mNumIndices : 0; + } + + const size_t vertex_count = mesh.vertexCount(); + mesh.indices.clear(); + mesh.indices.reserve(index_capacity); + mesh.positions.resize(vertex_count); + mesh.attributes.assign(vertex_count * ATTRIBUTE_COUNT, 0.f); + for (S32 f = 0; f < faces; ++f) + { + const LLVolumeFace& face = base.getVolumeFace(f); + const U32 first = mesh.first[f]; + const U32 vertices = mesh.first[f + 1] - first; + const U32 indices = vertices && face.mIndices && face.mNumIndices > 0 ? (U32)(face.mNumIndices - face.mNumIndices % 3) : 0; + + // A triangle that names a vertex the face does not have is left + // out. + for (U32 i = 0; i < indices; i += 3) + { + const U16* corner = face.mIndices + i; + if (corner[0] < vertices && corner[1] < vertices && corner[2] < vertices) + { + mesh.indices.push_back(first + corner[0]); + mesh.indices.push_back(first + corner[1]); + mesh.indices.push_back(first + corner[2]); + } + } + mesh.first_index[f + 1] = (U32)mesh.indices.size(); + + const LLVector4a scale = unscale(face.mNormalizedScale); + const LLVector4a inv_scale(1.f / scale[0], 1.f / scale[1], 1.f / scale[2], 1.f); + for (U32 v = 0; v < vertices; ++v) + { + mesh.positions[first + v].setMul(face.mPositions[v], scale); + } + + if (face.mNormals) + { + for (U32 v = 0; v < vertices; ++v) + { + // The face keeps the authored normal times the scale. + LLVector4a normal; + normal.setMul(face.mNormals[v], inv_scale); + const F32 length = sqrtf(normal.dot3(normal).getF32()); + F32* attribute = &mesh.attributes[(first + v) * ATTRIBUTE_COUNT]; + if (length > 0.f && std::isfinite(length)) + { + attribute[0] = normal[0] / length; + attribute[1] = normal[1] / length; + attribute[2] = normal[2] / length; + } + } + } + + if (face.mTexCoords && vertices) + { + F64 area = 0.0; + size_t triangles = 0; + for (U32 i = mesh.first_index[f]; i < mesh.first_index[f + 1]; i += 3) + { + const LLVector2& a = face.mTexCoords[mesh.indices[i] - first]; + const LLVector2& b = face.mTexCoords[mesh.indices[i + 1] - first]; + const LLVector2& c = face.mTexCoords[mesh.indices[i + 2] - first]; + area += fabs(0.5 * ((F64)(b.mV[0] - a.mV[0]) * (c.mV[1] - a.mV[1]) - (F64)(c.mV[0] - a.mV[0]) * (b.mV[1] - a.mV[1]))); + ++triangles; + } + const F64 mean = triangles ? area / (F64)triangles : 0.0; + const F32 density = mean > 0.0 && std::isfinite(mean) ? (F32)(1.0 / sqrt(mean)) : 0.f; + + // Measured from the face's least coordinates, so that their + // size stays near the density's whatever the texture's offset. + LLVector2 least = face.mTexCoords[0]; + for (U32 v = 1; v < vertices; ++v) + { + least.mV[0] = llmin(least.mV[0], face.mTexCoords[v].mV[0]); + least.mV[1] = llmin(least.mV[1], face.mTexCoords[v].mV[1]); + } + for (U32 v = 0; v < vertices; ++v) + { + F32* attribute = &mesh.attributes[(first + v) * ATTRIBUTE_COUNT]; + attribute[3] = (face.mTexCoords[v].mV[0] - least.mV[0]) * density; + attribute[4] = (face.mTexCoords[v].mV[1] - least.mV[1]) * density; + } + } + } + + // Each vertex at the first vertex at its position, so that a + // position's vertices are known together, as the simplifier knows + // them. + std::vector remap(vertex_count); + if (vertex_count) + { + meshopt_generatePositionRemap(remap.data(), mesh.positions[0].getF32ptr(), vertex_count, sizeof(LLVector4a)); + } + std::vector spans(vertex_count, 0); + std::vector seam(vertex_count, 0); + for (U32 v = 0; v < vertex_count; ++v) + { + const U32 r = remap[v]; + if (r == v) + { + continue; + } + const S32 face = mesh.faceOf(v); + if (face != mesh.faceOf(r)) + { + spans[r] = 1; + } + else + { + const LLVolumeFace& volume_face = base.getVolumeFace(face); + const LLVector2* uv = volume_face.mTexCoords; + if (uv && uv[v - mesh.first[face]] != uv[r - mesh.first[face]]) + { + seam[r] = 1; + } + } + } + mesh.shared.resize(vertex_count); + mesh.uv_seam.resize(vertex_count); + for (U32 v = 0; v < vertex_count; ++v) + { + mesh.shared[v] = spans[remap[v]]; + mesh.uv_seam[v] = seam[remap[v]]; + } + + mesh.extent = vertex_count ? meshopt_simplifyScale(mesh.positions[0].getF32ptr(), vertex_count, sizeof(LLVector4a)) : 0.f; + } + + // The vertex flags for a simplification free to cross seams: the + // borders between faces held, and the texture coordinate seams too for + // Seams::UV. + void protect(const Mesh& mesh, ALMeshSimplifier::Seams seams, U32 first, U32 count, std::vector& locks) + { + locks.assign(count, 0); + for (U32 v = 0; v < count; ++v) + { + const bool held = mesh.shared[first + v] || (seams == ALMeshSimplifier::Seams::UV && mesh.uv_seam[first + v]); + locks[v] = held ? (U8)meshopt_SimplifyVertex_Protect : 0; + } + } + + size_t targetCount(size_t index_count, F32 decimator) + { + if (decimator > 0.f) + { + return llclamp((size_t)llfloor((F32)index_count / decimator), (size_t)3, index_count); + } + return 3; + } + + // The largest detached part a simplification may drop: one whose + // vertices all lie within this distance of their mean, relative to the + // model's largest extent. + constexpr F32 PRUNE_LIMIT = 0.05f; + + // The radius of the largest part, connected by shared positions, that + // indices has and simplified has none of, as meshoptimizer measures a + // part: its vertices' farthest from their mean, over the extent. + F32 largestDropped(const LLVector4a* positions, size_t vertex_count, const U32* indices, size_t index_count, const U32* simplified, + size_t simplified_count, F32 extent) + { + std::vector remap(vertex_count); + meshopt_generatePositionRemap(remap.data(), positions[0].getF32ptr(), vertex_count, sizeof(LLVector4a)); + std::vector parent(vertex_count); + for (U32 v = 0; v < vertex_count; ++v) + { + parent[v] = v; + } + auto root = [&](U32 v) + { + while (parent[v] != v) + { + v = parent[v] = parent[parent[v]]; + } + return v; + }; + for (size_t i = 0; i < index_count; i += 3) + { + const U32 a = root(remap[indices[i]]); + for (S32 k = 1; k < 3; ++k) + { + const U32 b = root(remap[indices[i + k]]); + if (a != b) + { + parent[llmax(a, b)] = llmin(a, b); + } + } + } + + std::vector kept(vertex_count, 0); + for (size_t i = 0; i < simplified_count; ++i) + { + kept[root(remap[simplified[i]])] = 1; + } + std::vector used(vertex_count, 0); + for (size_t i = 0; i < index_count; ++i) + { + used[indices[i]] = 1; + } + + // Each dropped part's mean, then its radius. + std::vector mean(vertex_count); + std::vector weight(vertex_count, 0.f); + for (U32 v = 0; v < vertex_count; ++v) + { + mean[v].clear(); + } + for (U32 v = 0; v < vertex_count; ++v) + { + const U32 r = root(remap[v]); + if (used[v] && !kept[r]) + { + mean[r].add(positions[v]); + weight[r] += 1.f; + } + } + F32 largest = 0.f; + for (U32 v = 0; v < vertex_count; ++v) + { + const U32 r = root(remap[v]); + if (used[v] && !kept[r]) + { + LLVector4a centre = mean[r]; + centre.mul(1.f / weight[r]); + LLVector4a offset; + offset.setSub(positions[v], centre); + largest = llmax(largest, sqrtf(offset.dot3(offset).getF32())); + } + } + return extent > 0.f ? largest / extent : 0.f; + } + + // A simplification of vertices [first, first + count) of mesh, by + // indices numbered from first. Where prune is set, it may drop detached + // parts as its error grows past their size, none larger than + // PRUNE_LIMIT; where it would have dropped a larger one to reach its + // target, it drops each part within PRUNE_LIMIT first and simplifies + // what is left without dropping any more. + size_t simplifyMesh(const Mesh& mesh, U32 first, U32 count, const U32* indices, size_t index_count, size_t target_count, F32 error, + U32 options, const std::vector& locks, bool prune, U32* destination, F32& result_error) + { + const LLVector4a* vertices = &mesh.positions[first]; + const F32* positions = vertices[0].getF32ptr(); + const F32* attributes = &mesh.attributes[(size_t)first * ATTRIBUTE_COUNT]; + const U8* lock = locks.empty() ? nullptr : locks.data(); + auto simplify = [&](const U32* from, size_t from_count, U32 with) + { + result_error = 0.f; + return meshopt_simplifyWithAttributes(destination, from, from_count, positions, count, sizeof(LLVector4a), attributes, + ATTRIBUTE_COUNT * sizeof(F32), ATTRIBUTE_WEIGHTS, ATTRIBUTE_COUNT, lock, + llmin(target_count, from_count), error, with, &result_error); + }; + if (!prune) + { + return simplify(indices, index_count, options); + } + + const size_t simplified = simplify(indices, index_count, options | meshopt_SimplifyPrune); + const F32 extent = meshopt_simplifyScale(positions, count, sizeof(LLVector4a)); + if (largestDropped(vertices, count, indices, index_count, destination, simplified, extent) <= PRUNE_LIMIT) + { + return simplified; + } + + std::vector small_parts_gone(index_count); + const size_t left = meshopt_simplifyPrune(small_parts_gone.data(), indices, index_count, positions, count, sizeof(LLVector4a), + llmin(error, PRUNE_LIMIT)); + if (left < 3) + { + return simplify(indices, index_count, options); + } + return simplify(small_parts_gone.data(), left, options); + } + + U32 seamOptions(ALMeshSimplifier::Seams seams, const ALMeshSimplifier::Target& goal) + { + U32 options = goal.distant ? meshopt_SimplifyRegularizeLight : 0; + if (seams == ALMeshSimplifier::Seams::UV || seams == ALMeshSimplifier::Seams::NONE) + { + options |= meshopt_SimplifyPermissive; + } + return options; + } + + // What a simplified face takes from the face it is made from besides + // its vertices and triangles. + void copyLayout(const LLVolumeFace& layout, LLVolumeFace& face) + { + face.mID = layout.mID; + face.mTypeMask = layout.mTypeMask; + face.mExtents[0] = layout.mExtents[0]; + face.mExtents[1] = layout.mExtents[1]; + *face.mCenter = *layout.mCenter; + face.mTexCoordExtents[0] = layout.mTexCoordExtents[0]; + face.mTexCoordExtents[1] = layout.mTexCoordExtents[1]; + face.mNormalizedScale = layout.mNormalizedScale; + face.mOptimized = false; + } + + // The face of one vertex at the origin and one triangle of it, that a + // face left with no triangle becomes. + void makeEmpty(const LLVolumeFace& layout, LLVolumeFace& face) + { + copyLayout(layout, face); + ll_aligned_free_16(face.mWeights); + face.mWeights = nullptr; + face.resizeVertices(1); + face.resizeIndices(3); + memset(face.mIndices, 0, sizeof(U16) * 3); + face.mPositions[0].clear(); + face.mNormals[0].clear(); + face.mTexCoords[0].setZero(); + } +} // namespace + +// static +bool ALMeshSimplifier::compact(const LLVolume& base, S32 face, const U32* indices, size_t index_count, LLVolumeFace& target) +{ + const S32 faces = base.getNumVolumeFaces(); + std::vector first(faces + 1, 0); + for (S32 f = 0; f < faces; ++f) + { + const LLVolumeFace& from = base.getVolumeFace(f); + first[f + 1] = first[f] + (from.mPositions && from.mNumVertices > 0 ? (U32)from.mNumVertices : 0); + } + const size_t vertex_count = first.back(); + + // Each vertex's place in target, in the order the triangles first name + // it; one they do not name has none. + std::vector place(vertex_count); + const size_t kept = vertex_count ? meshopt_optimizeVertexFetchRemap(place.data(), indices, index_count, vertex_count) : 0; + if (kept > U16_MAX) + { + return false; + } + + const LLVolumeFace& layout = base.getVolumeFace(face); + copyLayout(layout, target); + target.resizeVertices((S32)kept); + target.resizeIndices((S32)index_count); + ll_aligned_free_16(target.mWeights); + target.mWeights = nullptr; + if (layout.mWeights) + { + target.allocateWeights((S32)kept); + } + if (layout.mTangents) + { + target.allocateTangents((S32)kept); + } + + for (S32 f = 0; f < faces; ++f) + { + const LLVolumeFace& from = base.getVolumeFace(f); + for (U32 v = first[f]; v < first[f + 1]; ++v) + { + const U32 to = place[v]; + if (to == U32_MAX) + { + continue; + } + const U32 at = v - first[f]; + target.mPositions[to] = from.mPositions[at]; + if (from.mNormals) + { + target.mNormals[to] = from.mNormals[at]; + } + else + { + target.mNormals[to].clear(); + } + if (from.mTexCoords) + { + target.mTexCoords[to] = from.mTexCoords[at]; + } + else + { + target.mTexCoords[to].setZero(); + } + if (target.mWeights) + { + if (from.mWeights) + { + target.mWeights[to] = from.mWeights[at]; + } + else + { + target.mWeights[to].clear(); + } + } + if (target.mTangents) + { + if (from.mTangents) + { + target.mTangents[to] = from.mTangents[at]; + } + else + { + target.mTangents[to].clear(); + } + } + } + } + + for (size_t i = 0; i < index_count; ++i) + { + target.mIndices[i] = (U16)place[indices[i]]; + } + + if (!layout.mNormals) + { + target.mNormals = nullptr; + } + if (!layout.mTexCoords) + { + target.mTexCoords = nullptr; + } + return true; +} + +// static +F32 ALMeshSimplifier::simplifyModel(const LLVolume& base, LLVolume& target, const Target& goal, Seams seams) +{ + Mesh mesh; + prepare(base, mesh); + const size_t index_count = mesh.indices.size(); + if (index_count < 3) + { + return -1.f; + } + + 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(), mesh.indices.data(), index_count, mesh.positions[0].getF32ptr(), + mesh.vertexCount(), sizeof(LLVector4a), nullptr, target_count, goal.error, &result_error); + } + else + { + // The simplifier keeps a seam between faces closed as it keeps any + // other; free to cross seams, it is held at the faces' borders. + std::vector locks; + if (seams != Seams::ALL) + { + protect(mesh, seams, 0, (U32)mesh.vertexCount(), locks); + } + count = simplifyMesh(mesh, 0, (U32)mesh.vertexCount(), mesh.indices.data(), index_count, target_count, goal.error, + seamOptions(seams, goal), locks, goal.distant, simplified.data(), result_error); + } + if (count < 3) + { + return -1.f; + } + + // Each face's triangles, those whose first vertex is one of its own. + const S32 faces = base.getNumVolumeFaces(); + std::vector> face_indices(faces); + for (size_t i = 0; i < count; i += 3) + { + std::vector& to = face_indices[mesh.faceOf(simplified[i])]; + to.insert(to.end(), simplified.begin() + i, simplified.begin() + i + 3); + } + + for (S32 f = 0; f < faces; ++f) + { + if (face_indices[f].size() < 3) + { + continue; + } + if (!compact(base, f, face_indices[f].data(), face_indices[f].size(), target.getVolumeFace(f))) + { + 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(base, 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; + } + } + for (S32 f = 0; f < faces; ++f) + { + if (face_indices[f].size() < 3) + { + makeEmpty(base.getVolumeFace(f), target.getVolumeFace(f)); + } + } + return (F32)index_count / (F32)count; +} + +// static +F32 ALMeshSimplifier::simplifyFace(const LLVolume& base, S32 face, LLVolumeFace& target, const Target& goal, Seams seams) +{ + Mesh mesh; + prepare(base, mesh); + 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 border, 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. + const bool meets = std::any_of(mesh.shared.begin() + first, mesh.shared.begin() + first + vertex_count, + [](U8 shared) { return shared != 0; }); + U32 options = seamOptions(seams, goal); + if (meets) + { + options |= meshopt_SimplifyLockBorder; + } + std::vector locks; + if (seams != Seams::ALL) + { + protect(mesh, seams, first, vertex_count, locks); + } + count = simplifyMesh(mesh, first, vertex_count, indices.data(), index_count, target_count, error, options, 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)) + { + // A face's own vertices are never more than 16-bit indices reach. + llassert(false); + makeEmpty(base.getVolumeFace(face), target); + return -1.f; + } + return (F32)index_count / (F32)count; +} diff --git a/indra/llmath/almeshsimplifier.h b/indra/llmath/almeshsimplifier.h new file mode 100644 index 0000000000..7928473f57 --- /dev/null +++ b/indra/llmath/almeshsimplifier.h @@ -0,0 +1,102 @@ +/** + * @file almeshsimplifier.h + * @brief A model's levels of detail, made from its faces by meshoptimizer's + * simplifier + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * 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 + * $/LicenseInfo$ + */ + +#ifndef AL_MESHSIMPLIFIER_H +#define AL_MESHSIMPLIFIER_H + +#include "stdtypes.h" + +class LLVolume; +class LLVolumeFace; + +// The mesh uploader's simplification of a model: the faces of one volume +// simplified into the faces of another, all of them together or one at a +// time. The error the simplifier weighs is the model's in the shape it was +// authored in, its positions scaled back out of the unit cube by each face's +// mNormalizedScale, and it counts how far the normals and texture +// coordinates drift as well as the positions. A simplified face takes its +// vertices from the base face unchanged, each attribute as it was: its +// position too, so whatever is keyed by position, as a model's skin weights +// are, still finds it. +class ALMeshSimplifier +{ +public: + // Which seams a simplification keeps in place. A seam is where one + // position is several vertices, that differ in normal or texture + // coordinate; a simplification free to cross one moves or removes it + // where the error allows. The borders between a model's faces stay + // where they are in every mode but SLOPPY. + enum class Seams + { + ALL, // every seam + UV, // texture coordinate seams; normal seams are free + NONE, // none but the borders between faces + SLOPPY, // none: vertices merge by place, whatever joins them + }; + + struct Target + { + // The share of the base's triangles to keep, as the base's index + // count over this; 0 keeps a triangle and leaves the error to stop + // the simplification. + F32 decimator = 0.f; + + // The most error to accept, relative to the model's size: 0.01 is + // a deviation of one hundredth of its largest extent. + F32 error = 1.f; + + // A level of detail seen from far away: its triangles are of more + // even size and shape, at a little cost in error, and it may drop + // detached parts that are small next to the model. + bool distant = false; + }; + + // Simplifies base's faces together, as one mesh, into target's, which + // has as many faces. A triangle belongs to the face its first vertex + // came from. A part detached from the rest is dropped only with all of + // its faces. Returns the ratio of base's indices to the simplification's, + // 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. + static F32 simplifyModel(const LLVolume& base, LLVolume& target, const Target& goal, Seams seams); + + // Simplifies base's face on its own into target. A face that shares a + // position with any other face of base keeps its border where it is, so + // that no gap opens between the two, and keeps all of its parts; only + // one that shares none may lose its small detached parts. Returns as simplifyModel does; where no triangle + // is left, target is the face of three indices to a vertex at the origin. + static F32 simplifyFace(const LLVolume& base, S32 face, LLVolumeFace& target, const Target& goal, Seams seams); + + // The face of base's face's layout that the given triangles make: the + // vertices they name and no others, each with every attribute it has in + // base, in the order the triangles first name them. The indices number + // base's vertices face after face, so that a triangle may take vertices + // from any face. Returns false, leaving target as it was, where the + // triangles name more vertices than 16-bit indices reach. + static bool compact(const LLVolume& base, S32 face, const U32* indices, size_t index_count, LLVolumeFace& target); +}; + +#endif // AL_MESHSIMPLIFIER_H diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index e12b13914d..e2db948e76 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -5701,6 +5701,188 @@ bool LLVolumeFace::cacheOptimize(bool gen_tangents) return true; } +// meshoptimizer's normals, which join two triangles' corners across the edge +// they share where their own normals are within the crease angle, weighting +// each by its angle and the lengths of its two edges. They are made in the +// shape the mesh was authored in, its positions scaled back out of the unit +// cube by mNormalizedScale, so that the crease angle is the one a user sees, +// and come back as the face's normals, the authored ones times the scale. +// A corner of triangles with no area takes the normal of another corner of +// its vertex, or the vertex's own where it has none, and then the axis +// along z. A vertex whose corners take different normals becomes one vertex +// for each, and vertices then the same in every attribute become one; every +// vertex keeps its position, texture coordinate and weights. The tangents go, +// as they no longer fit the normals. +bool LLVolumeFace::generateNormals(F32 crease_angle, F32 smoothing) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; + + if (!mPositions || !mIndices || mNumVertices <= 0 || mNumIndices < 3 || mNumIndices % 3 != 0) + { + return false; + } + + const size_t index_count = mNumIndices; + const size_t vertex_count = mNumVertices; + std::vector indices(index_count); + for (size_t i = 0; i < index_count; ++i) + { + indices[i] = mIndices[i]; + if (indices[i] >= vertex_count) + { + LL_WARNS_ONCE("LLVolume") << "Out of bounds index detected: mIndices[" << i << "]=" << indices[i] + << " >= mNumVertices=" << mNumVertices << LL_ENDL; + return false; + } + } + + const LLVector4a scale(mNormalizedScale.mV[0] != 0.f ? mNormalizedScale.mV[0] : 1.f, + mNormalizedScale.mV[1] != 0.f ? mNormalizedScale.mV[1] : 1.f, + mNormalizedScale.mV[2] != 0.f ? mNormalizedScale.mV[2] : 1.f, 1.f); + std::vector positions(vertex_count); + for (size_t v = 0; v < vertex_count; ++v) + { + positions[v].setMul(mPositions[v], scale); + } + + std::vector generated(index_count * 3); + meshopt_generateNormals(generated.data(), indices.data(), index_count, positions[0].getF32ptr(), vertex_count, sizeof(LLVector4a), + llclamp(crease_angle, 0.f, F_PI), llmax(smoothing, 0.f)); + + // Each corner's normal in the face's frame, and for each vertex the + // first of its corners to have one. + std::vector corner_normals(index_count); + std::vector first_corner(vertex_count, U32_MAX); + for (size_t c = 0; c < index_count; ++c) + { + LLVector4a& normal = corner_normals[c]; + normal.set(generated[c * 3 + 0], generated[c * 3 + 1], generated[c * 3 + 2], 0.f); + normal.mul(scale); + unit3_or_zero(normal); + normal.getF32ptr()[3] = 0.f; + if (first_corner[indices[c]] == U32_MAX && normal.dot3(normal).getF32() > 0.f) + { + first_corner[indices[c]] = (U32)c; + } + } + for (size_t c = 0; c < index_count; ++c) + { + LLVector4a& normal = corner_normals[c]; + if (normal.dot3(normal).getF32() > 0.f) + { + continue; + } + const U32 v = indices[c]; + if (first_corner[v] != U32_MAX) + { + normal = corner_normals[first_corner[v]]; + continue; + } + if (mNormals) + { + normal.set(mNormals[v][0], mNormals[v][1], mNormals[v][2], 0.f); + unit3_or_zero(normal); + } + if (normal.dot3(normal).getF32() == 0.f) + { + normal.set(0.f, 0.f, 1.f, 0.f); + } + } + + // A vertex the face holds more than once, the same in all but its + // normal, is one vertex; then each corner is that vertex with its own + // normal, and corners the same in both are one. + std::vector canonical(vertex_count); + const meshopt_Stream attributes[] = { + { mPositions, sizeof(F32) * 3, sizeof(LLVector4a) }, + { mTexCoords, sizeof(LLVector2), sizeof(LLVector2) }, + { mWeights, sizeof(LLVector4a), sizeof(LLVector4a) }, + }; + meshopt_Stream used[3]; + size_t used_count = 0; + for (const meshopt_Stream& stream : attributes) + { + if (stream.data) + { + used[used_count++] = stream; + } + } + meshopt_generateVertexRemapMulti(canonical.data(), indices.data(), index_count, vertex_count, used, used_count); + std::vector corner_vertices(index_count); + for (size_t c = 0; c < index_count; ++c) + { + corner_vertices[c] = canonical[indices[c]]; + } + std::vector remap(index_count); + const meshopt_Stream corners[] = { + { corner_vertices.data(), sizeof(U32), sizeof(U32) }, + { corner_normals.data(), sizeof(F32) * 3, sizeof(LLVector4a) }, + }; + const size_t unique = + meshopt_generateVertexRemapMulti(remap.data(), nullptr, index_count, index_count, corners, sizeof(corners) / sizeof(corners[0])); + if (unique > U16_MAX) + { + LL_WARNS("LLVolume") << "Normals for a face of " << mNumVertices << " vertices and " << index_count / 3 + << " triangles take more vertices than 16-bit indices reach; keeping its own" << LL_ENDL; + return false; + } + + // Each new vertex from the first corner that names it. + std::vector from_corner(unique); + for (size_t c = index_count; c-- > 0;) + { + from_corner[remap[c]] = (U32)c; + } + + LLVector4a* old_positions = mPositions; + const LLVector2* old_tex_coords = mTexCoords; + LLVector4a* old_weights = mWeights; + mPositions = nullptr; + mWeights = nullptr; + resizeVertices((S32)unique); + if (!mPositions) + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("LLVolume") << "Failed to allocate memory for resizeVertices(" << unique << ")" << LL_ENDL; + } + if (old_weights) + { + allocateWeights((S32)unique); + } + for (size_t u = 0; u < unique; ++u) + { + const U32 corner = from_corner[u]; + const U32 from = indices[corner]; + mPositions[u] = old_positions[from]; + mNormals[u] = corner_normals[corner]; + if (old_tex_coords) + { + mTexCoords[u] = old_tex_coords[from]; + } + else + { + mTexCoords[u].setZero(); + } + if (old_weights) + { + mWeights[u] = old_weights[from]; + } + } + for (size_t i = 0; i < index_count; ++i) + { + mIndices[i] = (U16)remap[i]; + } + if (!old_tex_coords) + { + mTexCoords = nullptr; + } + mOptimized = false; + + ll_aligned_free<64>(old_positions); + ll_aligned_free_16(old_weights); + return true; +} + void LLVolumeFace::createOctree(F32 scaler, const LLVector4a& center, const LLVector4a& size) { LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index 7f465f9997..0454a539e0 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -938,6 +938,13 @@ class LLVolumeFace void optimize(F32 angle_cutoff = 2.f); bool cacheOptimize(bool gen_tangents = false); + // Normals made from the face's shape: smooth across an edge whose two + // triangles meet at less than crease_angle radians and hard across one + // where they meet at more, evened out over their neighbours by smoothing + // passes. Returns false, leaving the face as it was, where it has no + // triangle or would need more vertices than 16-bit indices reach. + bool generateNormals(F32 crease_angle, F32 smoothing); + void createOctree(F32 scaler = 0.25f, const LLVector4a& center = LLVector4a(0,0,0), const LLVector4a& size = LLVector4a(0.5f,0.5f,0.5f)); void destroyOctree(); // Get a reference to the octree, which may be null diff --git a/indra/llmath/tests/almeshsimplifier_test.cpp b/indra/llmath/tests/almeshsimplifier_test.cpp new file mode 100644 index 0000000000..25185acde7 --- /dev/null +++ b/indra/llmath/tests/almeshsimplifier_test.cpp @@ -0,0 +1,1286 @@ +/** + * @file almeshsimplifier_test.cpp + * @brief The mesh uploader's simplification and normals, against what each + * mode promises to keep. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * 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 + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "almeshsimplifier.h" +#include "llmath.h" +#include "llmeshoptimizer.h" +#include "llvector4a.h" +#include "llvolume.h" +#include "v2math.h" +#include "v3math.h" +#include "almeshcorpus.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + using Seams = ALMeshSimplifier::Seams; + + // The uploader's own levels of detail: a third of the triangles a level. + constexpr F32 DECIMATORS[] = { 3.f, 9.f, 27.f }; + + // How far over its target the uploader's Auto mode lets a simplification + // come and still take it. + constexpr F32 ALLOWED_DRIFT = 1.8f; + + struct ArenaInstalled + { + static void* MESHOPTIMIZER_ALLOC_CALLCONV newBlock(size_t bytes) { return ::operator new(bytes); } + static void MESHOPTIMIZER_ALLOC_CALLCONV deleteBlock(void* block) { ::operator delete(block); } + + ArenaInstalled() { LLMeshOptimizer::initClass(); } + ~ArenaInstalled() { meshopt_setAllocator(newBlock, deleteBlock); } + }; + + LLVector3 v3(const LLVector4a& v) + { + return LLVector3(v.getF32ptr()); + } + + // ------------------------------------------------------------------ + // Geometry + + // A face as a test writes it. + struct Geometry + { + std::vector positions; + std::vector normals; + std::vector uvs; + std::vector indices; + LLVector3 scale = LLVector3(1.f, 1.f, 1.f); + + U16 add(const LLVector3& p, const LLVector3& n, const LLVector2& uv) + { + positions.push_back(p); + normals.push_back(n); + uvs.push_back(uv); + return (U16)(positions.size() - 1); + } + + void triangle(U16 a, U16 b, U16 c) { indices.insert(indices.end(), { a, b, c }); } + + void write(LLVolumeFace& f) const + { + f.resizeVertices((S32)positions.size()); + f.resizeIndices((S32)indices.size()); + for (size_t v = 0; v < positions.size(); ++v) + { + f.mPositions[v].load3(positions[v].mV); + f.mNormals[v].load3(normals[v].mV); + f.mTexCoords[v] = uvs[v]; + } + std::copy(indices.begin(), indices.end(), f.mIndices); + f.mNormalizedScale = scale; + LLVector4a& min = f.mExtents[0]; + LLVector4a& max = f.mExtents[1]; + min = max = f.mPositions[0]; + for (S32 v = 1; v < f.mNumVertices; ++v) + { + min.setMin(min, f.mPositions[v]); + max.setMax(max, f.mPositions[v]); + } + f.mCenter->setAdd(min, max); + f.mCenter->mul(0.5f); + } + }; + + LLPointer volumeOf(const std::vector& faces) + { + LLVolumeParams params; + params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE); + params.setSculptID(LLUUID::null, LL_SCULPT_TYPE_MESH); + LLPointer volume = new LLVolume(params, 1.f); + volume->getVolumeFaces().resize(faces.size()); + for (size_t f = 0; f < faces.size(); ++f) + { + faces[f].write(volume->getVolumeFace((S32)f)); + } + return volume; + } + + // A volume of as many faces as base, for a simplification to fill. + LLPointer targetFor(const LLVolume& base) + { + LLVolumeParams params; + params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE); + params.setSculptID(LLUUID::null, LL_SCULPT_TYPE_MESH); + LLPointer volume = new LLVolume(params, 1.f); + volume->getVolumeFaces().resize(base.getNumVolumeFaces()); + return volume; + } + + // Rows first_row to last_row of a latitude-longitude sphere as an + // exporter writes one: the seam column twice, at u = 0 and u = 1, and + // each pole a row of points with one triangle a column. The seam's two + // columns, a pole's points, and the row two calls share are each the + // same position to the bit. + Geometry sphereRows(S32 columns, S32 rows, S32 first_row, S32 last_row, F32 u_offset = 0.f) + { + Geometry g; + for (S32 r = first_row; r <= last_row; ++r) + { + for (S32 c = 0; c <= columns; ++c) + { + const F32 theta = F_PI * r / rows; + const F32 phi = F_TWO_PI * (c % columns) / columns; + const F32 ring = (r == 0 || r == rows) ? 0.f : sinf(theta); + const F32 height = r == 0 ? 1.f : (r == rows ? -1.f : cosf(theta)); + const LLVector3 n(ring * cosf(phi), ring * sinf(phi), height); + g.add(n * 0.5f, n, LLVector2(u_offset + (F32)c / columns, 1.f - (F32)r / rows)); + } + } + auto at = [&](S32 c, S32 r) { return (U16)(c + (r - first_row) * (columns + 1)); }; + for (S32 r = first_row; r < last_row; ++r) + { + for (S32 c = 0; c < columns; ++c) + { + if (r > 0) + { + g.triangle(at(c, r), at(c, r + 1), at(c + 1, r)); + } + if (r < rows - 1) + { + g.triangle(at(c + 1, r), at(c, r + 1), at(c + 1, r + 1)); + } + } + } + return g; + } + + Geometry sphere(S32 columns, S32 rows) + { + return sphereRows(columns, rows, 0, rows); + } + + // Each triangle its own three vertices, with the triangle's own normal: + // a model exported flat shaded. + Geometry flatShaded(const Geometry& from) + { + Geometry g; + for (size_t i = 0; i < from.indices.size(); i += 3) + { + const LLVector3& a = from.positions[from.indices[i]]; + const LLVector3& b = from.positions[from.indices[i + 1]]; + const LLVector3& c = from.positions[from.indices[i + 2]]; + LLVector3 n = (b - a) % (c - a); + n.normalize(); + const U16 first = g.add(a, n, from.uvs[from.indices[i]]); + g.add(b, n, from.uvs[from.indices[i + 1]]); + g.add(c, n, from.uvs[from.indices[i + 2]]); + g.triangle(first, (U16)(first + 1), (U16)(first + 2)); + } + return g; + } + + // A cube of six sides, each a grid of cells by cells quads with its own + // normal. Each side is a texture island of its own, or with shared_uvs + // every side takes its coordinates from x and y alike, so that its only + // seams are its normals'. + Geometry cube(S32 cells, bool shared_uvs = false) + { + Geometry g; + for (S32 axis = 0; axis < 3; ++axis) + { + for (F32 sign : { 1.f, -1.f }) + { + LLVector3 n, a; + n.mV[axis] = sign; + a.mV[(axis + 1) % 3] = 1.f; + const LLVector3 b = n % a; + const U16 first = (U16)g.positions.size(); + for (S32 j = 0; j <= cells; ++j) + { + for (S32 i = 0; i <= cells; ++i) + { + const F32 s = 2.f * i / cells - 1.f; + const F32 t = 2.f * j / cells - 1.f; + const LLVector3 p = (n + a * s + b * t) * 0.5f; + const LLVector2 uv = + shared_uvs ? LLVector2(p.mV[0] + 0.5f, p.mV[1] + 0.5f) : LLVector2(s * 0.5f + 0.5f, t * 0.5f + 0.5f); + g.add(p, n, uv); + } + } + for (S32 j = 0; j < cells; ++j) + { + for (S32 i = 0; i < cells; ++i) + { + const U16 c0 = (U16)(first + i + j * (cells + 1)); + const U16 c1 = (U16)(c0 + 1); + const U16 c2 = (U16)(c0 + cells + 1); + const U16 c3 = (U16)(c2 + 1); + g.triangle(c0, c1, c3); + g.triangle(c0, c3, c2); + } + } + } + } + return g; + } + + // A flat square of cells by cells quads in z = 0, x from x0, facing up. + Geometry plane(S32 cells, F32 x0 = -0.5f) + { + Geometry g; + for (S32 j = 0; j <= cells; ++j) + { + for (S32 i = 0; i <= cells; ++i) + { + const F32 u = (F32)i / cells; + const F32 v = (F32)j / cells; + g.add(LLVector3(x0 + u, v - 0.5f, 0.f), LLVector3(0.f, 0.f, 1.f), LLVector2(u, v)); + } + } + for (S32 j = 0; j < cells; ++j) + { + for (S32 i = 0; i < cells; ++i) + { + const U16 c0 = (U16)(i + j * (cells + 1)); + const U16 c1 = (U16)(c0 + 1); + const U16 c2 = (U16)(c0 + cells + 1); + const U16 c3 = (U16)(c2 + 1); + g.triangle(c0, c1, c3); + g.triangle(c0, c3, c2); + } + } + return g; + } + + // ------------------------------------------------------------------ + // Measures + + using PositionKey = std::array; + using EdgeKey = std::pair; + + PositionKey keyOf(const LLVector4a& p) + { + PositionKey key; + for (S32 k = 0; k < 3; ++k) + { + const F32 c = p[k] == 0.f ? 0.f : p[k]; + std::memcpy(&key[k], &c, sizeof(F32)); + } + return key; + } + + EdgeKey edgeOf(const PositionKey& a, const PositionKey& b) + { + return a < b ? EdgeKey(a, b) : EdgeKey(b, a); + } + + // The edges of a face, by position, that one triangle of it has: where + // it ends, as far as its positions go. + std::set borderOf(const LLVolumeFace& face) + { + std::map uses; + for (S32 i = 0; i < face.mNumIndices; i += 3) + { + for (S32 k = 0; k < 3; ++k) + { + ++uses[edgeOf(keyOf(face.mPositions[face.mIndices[i + k]]), keyOf(face.mPositions[face.mIndices[i + (k + 1) % 3]]))]; + } + } + std::set border; + for (const auto& [edge, count] : uses) + { + if (count == 1) + { + border.insert(edge); + } + } + return border; + } + + std::set positionsOf(const LLVolumeFace& face) + { + std::set keys; + for (S32 v = 0; v < face.mNumVertices; ++v) + { + keys.insert(keyOf(face.mPositions[v])); + } + return keys; + } + + // The edges of a's border on positions b had, that b's border does not + // have: a gap between the two. + size_t gaps(const LLVolumeFace& a, const LLVolumeFace& b, const std::set& shared) + { + const std::set border_b = borderOf(b); + size_t open = 0; + for (const EdgeKey& edge : borderOf(a)) + { + if (shared.count(edge.first) && shared.count(edge.second) && !border_b.count(edge)) + { + ++open; + } + } + return open; + } + + // Every index names a vertex, and there are whole triangles. + bool wellFormed(const LLVolumeFace& face) + { + if (face.mNumIndices < 3 || face.mNumIndices % 3 != 0 || face.mNumVertices <= 0) + { + return false; + } + return std::all_of(face.mIndices, face.mIndices + face.mNumIndices, [&](U16 i) { return i < face.mNumVertices; }); + } + + // Every vertex of face is one of base's, the same in each attribute. + bool fromBase(const LLVolumeFace& face, const LLVolume& base) + { + std::set> vertices; + auto bits = [](const LLVolumeFace& f, S32 v) + { + std::vector out(8); + std::memcpy(&out[0], f.mPositions[v].getF32ptr(), 12); + std::memcpy(&out[3], f.mNormals[v].getF32ptr(), 12); + std::memcpy(&out[6], f.mTexCoords[v].mV, 8); + return out; + }; + for (S32 f = 0; f < base.getNumVolumeFaces(); ++f) + { + const LLVolumeFace& from = base.getVolumeFace(f); + for (S32 v = 0; v < from.mNumVertices; ++v) + { + vertices.insert(bits(from, v)); + } + } + for (S32 v = 0; v < face.mNumVertices; ++v) + { + if (!vertices.count(bits(face, v))) + { + return false; + } + } + return true; + } + + LLVector3 closestOnTriangle(const LLVector3& p, const LLVector3& a, const LLVector3& b, const LLVector3& c) + { + const LLVector3 ab = b - a, ac = c - a, ap = p - a; + const F32 d1 = ab * ap, d2 = ac * ap; + if (d1 <= 0.f && d2 <= 0.f) + { + return a; + } + const LLVector3 bp = p - b; + const F32 d3 = ab * bp, d4 = ac * bp; + if (d3 >= 0.f && d4 <= d3) + { + return b; + } + const F32 vc = d1 * d4 - d3 * d2; + if (vc <= 0.f && d1 >= 0.f && d3 <= 0.f) + { + return a + ab * (d1 / (d1 - d3)); + } + const LLVector3 cp = p - c; + const F32 d5 = ab * cp, d6 = ac * cp; + if (d6 >= 0.f && d5 <= d6) + { + return c; + } + const F32 vb = d5 * d2 - d1 * d6; + if (vb <= 0.f && d2 >= 0.f && d6 <= 0.f) + { + return a + ac * (d2 / (d2 - d6)); + } + const F32 va = d3 * d6 - d5 * d4; + if (va <= 0.f && (d4 - d3) >= 0.f && (d5 - d6) >= 0.f) + { + return b + (c - b) * ((d4 - d3) / ((d4 - d3) + (d5 - d6))); + } + const F32 denom = 1.f / (va + vb + vc); + return a + ab * (vb * denom) + ac * (vc * denom); + } + + // The farthest any vertex of base lies from the simplified surface, over + // base's largest extent. + F32 deviation(const LLVolume& base, const LLVolume& simplified) + { + std::vector> triangles; + for (S32 f = 0; f < simplified.getNumVolumeFaces(); ++f) + { + const LLVolumeFace& face = simplified.getVolumeFace(f); + for (S32 i = 0; i < face.mNumIndices; i += 3) + { + triangles.push_back({ v3(face.mPositions[face.mIndices[i]]), v3(face.mPositions[face.mIndices[i + 1]]), + v3(face.mPositions[face.mIndices[i + 2]]) }); + } + } + LLVector3 least(F32_MAX, F32_MAX, F32_MAX), most(-F32_MAX, -F32_MAX, -F32_MAX); + F32 farthest = 0.f; + for (S32 f = 0; f < base.getNumVolumeFaces(); ++f) + { + const LLVolumeFace& face = base.getVolumeFace(f); + for (S32 v = 0; v < face.mNumVertices; ++v) + { + const LLVector3 p = v3(face.mPositions[v]); + for (S32 k = 0; k < 3; ++k) + { + least.mV[k] = llmin(least.mV[k], p.mV[k]); + most.mV[k] = llmax(most.mV[k], p.mV[k]); + } + F32 nearest = F32_MAX; + for (const auto& t : triangles) + { + nearest = llmin(nearest, (closestOnTriangle(p, t[0], t[1], t[2]) - p).lengthSquared()); + } + farthest = llmax(farthest, nearest); + } + } + const LLVector3 size = most - least; + return sqrtf(farthest) / llmax(size.mV[0], llmax(size.mV[1], size.mV[2])); + } + + S32 triangleCount(const LLVolume& volume) + { + S32 count = 0; + for (S32 f = 0; f < volume.getNumVolumeFaces(); ++f) + { + count += volume.getVolumeFace(f).mNumIndices / 3; + } + return count; + } + + ALMeshSimplifier::Target toCount(F32 decimator) + { + ALMeshSimplifier::Target goal; + goal.decimator = decimator; + goal.error = 1.f; + return goal; + } + + const char* nameOf(Seams seams) + { + switch (seams) + { + case Seams::ALL: return "every seam held"; + case Seams::UV: return "texture seams held"; + case Seams::NONE: return "no seam held"; + case Seams::SLOPPY: return "sloppy"; + } + return "?"; + } + + // ------------------------------------------------------------------ + // Targets and error + + TEST_CASE("almeshsimplifier: a model reaches the uploader's triangle targets", "[almeshsimplifier]") + { + ArenaInstalled installed; + const std::pair models[] = { + { "UV sphere 64 by 32", sphere(64, 32) }, + { "cube of 16 by 16 cells a side", cube(16) }, + }; + for (const auto& [name, geometry] : models) + { + LLPointer base = volumeOf({ geometry }); + const S32 base_triangles = triangleCount(*base); + for (F32 decimator : DECIMATORS) + { + for (Seams seams : { Seams::ALL, Seams::UV, Seams::NONE }) + { + INFO(fmt::format("{}, a {}th of {} triangles, {}", name, decimator, base_triangles, nameOf(seams))); + LLPointer target = targetFor(*base); + const F32 ratio = ALMeshSimplifier::simplifyModel(*base, *target, toCount(decimator), seams); + REQUIRE(ratio > 0.f); + REQUIRE(wellFormed(target->getVolumeFace(0))); + CHECK(fromBase(target->getVolumeFace(0), *base)); + const S32 triangles = triangleCount(*target); + INFO(fmt::format("{} triangles", triangles)); + // Holding every seam, the simplifier runs out of edges it + // may collapse near the target, where the rounding of + // its errors decides which are left: the UV sphere's + // 27th, a target of 147, ends at 146 triangles as MSVC + // builds it and at 150 as GCC does. Free to move a seam, + // it reaches the target. + const F32 over = seams == Seams::ALL ? 1.05f : 1.f; + CHECK(triangles <= (S32)ceilf(base_triangles / decimator * over)); + CHECK(ratio * ALLOWED_DRIFT >= decimator); + } + } + } + } + + // A flat shaded model is a seam at every edge. Held to them, its + // simplification can hardly start; free to cross them, it reaches the + // target. + TEST_CASE("almeshsimplifier: free to cross normal seams, a flat shaded model reaches its target", "[almeshsimplifier]") + { + ArenaInstalled installed; + LLPointer base = volumeOf({ flatShaded(sphere(32, 16)) }); + const F32 decimator = 9.f; + + LLPointer held = targetFor(*base); + const F32 held_ratio = ALMeshSimplifier::simplifyModel(*base, *held, toCount(decimator), Seams::ALL); + INFO(fmt::format("every seam held: a ratio of {}", held_ratio)); + CHECK(held_ratio * ALLOWED_DRIFT < decimator); + + for (Seams seams : { Seams::UV, Seams::NONE }) + { + LLPointer free = targetFor(*base); + const F32 ratio = ALMeshSimplifier::simplifyModel(*base, *free, toCount(decimator), seams); + INFO(fmt::format("{}: a ratio of {}", nameOf(seams), ratio)); + REQUIRE(ratio > 0.f); + REQUIRE(wellFormed(free->getVolumeFace(0))); + CHECK(ratio * ALLOWED_DRIFT >= decimator); + } + } + + // The error the simplifier holds to is its estimate, each vertex's + // distance from the planes of the triangles it has taken over; a vertex + // of the base left out in the middle of a curved region lies a little + // further than that from the simplified surface. How much further + // depends on the collapses rounding leads it to: 1.37 times the error + // as MSVC builds it, 1.65 as Clang does with fused multiply-adds. + TEST_CASE("almeshsimplifier: a simplification to an error stays within it", "[almeshsimplifier]") + { + ArenaInstalled installed; + LLPointer base = volumeOf({ sphere(48, 24) }); + S32 coarser = 0; + for (F32 error : { 0.02f, 0.005f }) + { + ALMeshSimplifier::Target goal; + goal.error = error; + LLPointer target = targetFor(*base); + const F32 ratio = ALMeshSimplifier::simplifyModel(*base, *target, goal, Seams::ALL); + const F32 measured = deviation(*base, *target); + const S32 triangles = triangleCount(*target); + INFO(fmt::format("an error of {}: {} of {} triangles, a ratio of {}, {} measured", error, triangles, triangleCount(*base), + ratio, measured)); + CHECK(ratio > 1.25f); + CHECK(measured <= 2.f * error); + CHECK(triangles > coarser); + coarser = triangles; + } + } + + // ------------------------------------------------------------------ + // Borders between faces + + // A sphere's two halves, each its own face: they share the equator's + // positions, and each vertex's weight names its face, so that a vertex of + // one in the other shows. With flat_unmapped the upper half is flat + // shaded, a seam at every edge, and neither has texture coordinates, so + // that a simplification free to cross seams has vertices of the upper + // half's own to move the equator's onto at no cost in texture. + LLPointer halves(bool flat_unmapped = false) + { + Geometry upper = sphereRows(48, 24, 0, 12); + Geometry lower = sphereRows(48, 24, 12, 24, 10.f); + if (flat_unmapped) + { + upper = flatShaded(upper); + std::fill(upper.uvs.begin(), upper.uvs.end(), LLVector2(0.f, 0.f)); + std::fill(lower.uvs.begin(), lower.uvs.end(), LLVector2(0.f, 0.f)); + } + LLPointer volume = volumeOf({ upper, lower }); + for (S32 f = 0; f < 2; ++f) + { + LLVolumeFace& face = volume->getVolumeFace(f); + face.allocateWeights(face.mNumVertices); + for (S32 v = 0; v < face.mNumVertices; ++v) + { + face.mWeights[v].set((F32)f, 0.f, 0.f, 0.f); + } + } + return volume; + } + + // Every vertex of the face is one of base's face's own. + bool ownVertices(const LLVolumeFace& face, S32 f) + { + return face.mWeights && + std::all_of(face.mWeights, face.mWeights + face.mNumVertices, [&](const LLVector4a& w) { return w[0] == (F32)f; }); + } + + std::set sharedPositions(const LLVolume& base) + { + const std::set a = positionsOf(base.getVolumeFace(0)); + const std::set b = positionsOf(base.getVolumeFace(1)); + std::set shared; + std::set_intersection(a.begin(), a.end(), b.begin(), b.end(), std::inserter(shared, shared.begin())); + return shared; + } + + // Each half on its own, as a position-only simplification without a + // border lock took them: their equators come out differently. + LLPointer unlockedHalves(const LLVolume& base, F32 decimator) + { + LLPointer target = targetFor(base); + for (S32 f = 0; f < 2; ++f) + { + const LLVolumeFace& face = base.getVolumeFace(f); + std::vector indices(face.mIndices, face.mIndices + face.mNumIndices); + std::vector simplified(indices.size()); + const size_t target_count = (size_t)(indices.size() / decimator); + const size_t count = meshopt_simplify(simplified.data(), indices.data(), indices.size(), face.mPositions[0].getF32ptr(), + face.mNumVertices, sizeof(LLVector4a), target_count, 1.f, 0, nullptr); + for (U32& index : simplified) + { + index += f ? (U32)base.getVolumeFace(0).mNumVertices : 0; + } + REQUIRE(ALMeshSimplifier::compact(base, f, simplified.data(), count, target->getVolumeFace(f))); + } + return target; + } + + TEST_CASE("almeshsimplifier: simplified face by face, two faces that meet still meet", "[almeshsimplifier]") + { + ArenaInstalled installed; + LLPointer base = halves(); + const std::set shared = sharedPositions(*base); + REQUIRE(shared.size() == 48); + + // The scene holds the gap open where the border is not locked. + LLPointer unlocked = unlockedHalves(*base, 9.f); + const size_t unlocked_gaps = gaps(unlocked->getVolumeFace(0), unlocked->getVolumeFace(1), shared) + + gaps(unlocked->getVolumeFace(1), unlocked->getVolumeFace(0), shared); + INFO(fmt::format("{} edges open without the lock", unlocked_gaps)); + CHECK(unlocked_gaps > 0); + + for (F32 decimator : DECIMATORS) + { + for (Seams seams : { Seams::ALL, Seams::UV, Seams::NONE }) + { + INFO(fmt::format("a {}th, {}", decimator, nameOf(seams))); + LLPointer target = targetFor(*base); + for (S32 f = 0; f < 2; ++f) + { + CHECK(ALMeshSimplifier::simplifyFace(*base, f, target->getVolumeFace(f), toCount(decimator), seams) > 0.f); + REQUIRE(wellFormed(target->getVolumeFace(f))); + CHECK(fromBase(target->getVolumeFace(f), *base)); + } + CHECK(gaps(target->getVolumeFace(0), target->getVolumeFace(1), shared) == 0); + CHECK(gaps(target->getVolumeFace(1), target->getVolumeFace(0), shared) == 0); + } + } + } + + TEST_CASE("almeshsimplifier: simplified together, two faces meet and keep their own vertices", "[almeshsimplifier]") + { + ArenaInstalled installed; + for (bool flat_unmapped : { false, true }) + { + LLPointer base = halves(flat_unmapped); + const std::set shared = sharedPositions(*base); + for (F32 decimator : DECIMATORS) + { + for (Seams seams : { Seams::ALL, Seams::UV, Seams::NONE }) + { + INFO(fmt::format("{} halves, a {}th, {}", flat_unmapped ? "flat shaded, unmapped" : "smooth", decimator, + nameOf(seams))); + LLPointer target = targetFor(*base); + REQUIRE(ALMeshSimplifier::simplifyModel(*base, *target, toCount(decimator), seams) > 0.f); + for (S32 f = 0; f < 2; ++f) + { + const LLVolumeFace& face = target->getVolumeFace(f); + REQUIRE(wellFormed(face)); + CHECK(fromBase(face, *base)); + CHECK(ownVertices(face, f)); + } + CHECK(gaps(target->getVolumeFace(0), target->getVolumeFace(1), shared) == 0); + CHECK(gaps(target->getVolumeFace(1), target->getVolumeFace(0), shared) == 0); + } + } + } + } + + // A face that meets no other is free at its border, where one that + // meets another is held: the held square keeps the 128 edges of its + // border, more than a hundredth of its triangles can make. + TEST_CASE("almeshsimplifier: a face is held at its border only where it meets another", "[almeshsimplifier]") + { + ArenaInstalled installed; + const F32 decimator = 100.f; + + LLPointer alone = volumeOf({ plane(32) }); + LLPointer alone_target = targetFor(*alone); + const F32 alone_ratio = ALMeshSimplifier::simplifyFace(*alone, 0, alone_target->getVolumeFace(0), toCount(decimator), Seams::ALL); + INFO(fmt::format("on its own: a ratio of {}", alone_ratio)); + CHECK(alone_ratio * ALLOWED_DRIFT >= decimator); + + // The same square beside another, which it meets along x = 0.5. + LLPointer pair = volumeOf({ plane(32), plane(32, 0.5f) }); + LLPointer pair_target = targetFor(*pair); + const F32 pair_ratio = ALMeshSimplifier::simplifyFace(*pair, 0, pair_target->getVolumeFace(0), toCount(decimator), Seams::ALL); + INFO(fmt::format("beside another: a ratio of {}", pair_ratio)); + CHECK(pair_ratio * ALLOWED_DRIFT < decimator); + CHECK(borderOf(pair_target->getVolumeFace(0)).size() == 4 * 32); + } + + // ------------------------------------------------------------------ + // Seams + + // The meridian where u runs from 1 back to 0 is a texture seam; a + // triangle across it spans most of the texture. + TEST_CASE("almeshsimplifier: texture seams stay where the mode holds them", "[almeshsimplifier]") + { + ArenaInstalled installed; + LLPointer base = volumeOf({ sphere(48, 24) }); + for (Seams seams : { Seams::ALL, Seams::UV }) + { + for (F32 decimator : DECIMATORS) + { + INFO(fmt::format("a {}th, {}", decimator, nameOf(seams))); + LLPointer target = targetFor(*base); + REQUIRE(ALMeshSimplifier::simplifyModel(*base, *target, toCount(decimator), seams) > 0.f); + const LLVolumeFace& face = target->getVolumeFace(0); + REQUIRE(wellFormed(face)); + size_t across = 0; + for (S32 i = 0; i < face.mNumIndices; i += 3) + { + const F32 u0 = face.mTexCoords[face.mIndices[i]].mV[0]; + const F32 u1 = face.mTexCoords[face.mIndices[i + 1]].mV[0]; + const F32 u2 = face.mTexCoords[face.mIndices[i + 2]].mV[0]; + across += llmax(u0, llmax(u1, u2)) - llmin(u0, llmin(u1, u2)) > 0.5f; + } + CHECK(across == 0); + } + } + } + + // A square of tiles, each quad its own texture island a little apart + // from its neighbours', so that every inner vertex is a texture seam. + // Held, no triangle spans two islands, which would smear the texture + // between them; free, the simplification crosses them. + TEST_CASE("almeshsimplifier: a model of texture islands keeps them apart where the mode holds its texture seams", "[almeshsimplifier]") + { + ArenaInstalled installed; + constexpr S32 TILES = 16; + constexpr F32 PITCH = 1.f / TILES; + Geometry tiles; + for (S32 j = 0; j < TILES; ++j) + { + for (S32 i = 0; i < TILES; ++i) + { + U16 corner[4]; + for (S32 k = 0; k < 4; ++k) + { + const S32 x = i + (k & 1), y = j + (k >> 1); + const LLVector2 uv(PITCH * (i + 0.1f + 0.8f * (k & 1)), PITCH * (j + 0.1f + 0.8f * (k >> 1))); + corner[k] = tiles.add(LLVector3(PITCH * x - 0.5f, PITCH * y - 0.5f, 0.f), LLVector3(0.f, 0.f, 1.f), uv); + } + tiles.triangle(corner[0], corner[1], corner[3]); + tiles.triangle(corner[0], corner[3], corner[2]); + } + } + LLPointer base = volumeOf({ tiles }); + auto spanning = [](const LLVolumeFace& face) + { + size_t count = 0; + for (S32 i = 0; i < face.mNumIndices; i += 3) + { + const LLVector2& a = face.mTexCoords[face.mIndices[i]]; + bool apart = false; + for (S32 k = 1; k < 3; ++k) + { + const LLVector2& b = face.mTexCoords[face.mIndices[i + k]]; + apart |= llfloor(a.mV[0] * TILES) != llfloor(b.mV[0] * TILES) || llfloor(a.mV[1] * TILES) != llfloor(b.mV[1] * TILES); + } + count += apart; + } + return count; + }; + for (F32 decimator : DECIMATORS) + { + INFO(fmt::format("a {}th", decimator)); + for (Seams seams : { Seams::ALL, Seams::UV }) + { + LLPointer held = targetFor(*base); + REQUIRE(ALMeshSimplifier::simplifyModel(*base, *held, toCount(decimator), seams) > 0.f); + CHECK(spanning(held->getVolumeFace(0)) == 0); + } + LLPointer free = targetFor(*base); + REQUIRE(ALMeshSimplifier::simplifyModel(*base, *free, toCount(decimator), Seams::NONE) > 0.f); + CHECK(spanning(free->getVolumeFace(0)) > 0); + } + } + + // A cube whose sides share their texture coordinates is seamed only by + // its normals. Held, every triangle keeps its side's normal at all three + // corners; free, the simplification crosses the edges between sides. + TEST_CASE("almeshsimplifier: normal seams stay where the mode holds them", "[almeshsimplifier]") + { + ArenaInstalled installed; + LLPointer base = volumeOf({ cube(8, true) }); + auto mixed = [](const LLVolumeFace& face) + { + size_t count = 0; + for (S32 i = 0; i < face.mNumIndices; i += 3) + { + const LLVector3 n0 = v3(face.mNormals[face.mIndices[i]]); + count += n0 != v3(face.mNormals[face.mIndices[i + 1]]) || n0 != v3(face.mNormals[face.mIndices[i + 2]]); + } + return count; + }; + for (F32 decimator : DECIMATORS) + { + INFO(fmt::format("a {}th", decimator)); + LLPointer held = targetFor(*base); + REQUIRE(ALMeshSimplifier::simplifyModel(*base, *held, toCount(decimator), Seams::ALL) > 0.f); + CHECK(mixed(held->getVolumeFace(0)) == 0); + } + + // Free to cross them, at a level of detail the held one cannot + // reach, it does. + LLPointer free = targetFor(*base); + REQUIRE(ALMeshSimplifier::simplifyModel(*base, *free, toCount(100.f), Seams::UV) > 0.f); + CHECK(mixed(free->getVolumeFace(0)) > 0); + } + + // ------------------------------------------------------------------ + // Small parts + + TEST_CASE("almeshsimplifier: a distant level of detail short of its target loses its small detached parts first", "[almeshsimplifier]") + { + ArenaInstalled installed; + Geometry model = sphere(32, 16); + // Bolts: cubes of split normals, which no simplification that keeps + // seams can reduce, a fiftieth of the sphere's size. + for (S32 bolt = 0; bolt < 16; ++bolt) + { + const Geometry part = cube(1); + const F32 phi = F_TWO_PI * bolt / 16; + const LLVector3 at(0.6f * cosf(phi), 0.6f * sinf(phi), 0.f); + const U16 first = (U16)model.positions.size(); + for (size_t v = 0; v < part.positions.size(); ++v) + { + model.add(at + part.positions[v] * 0.02f, part.normals[v], part.uvs[v]); + } + for (U16 index : part.indices) + { + model.indices.push_back((U16)(first + index)); + } + } + LLPointer base = volumeOf({ model }); + const S32 bolt_triangles = 16 * 12; + const S32 target_triangles = (S32)(triangleCount(*base) / 9.f); + REQUIRE(target_triangles < bolt_triangles); + + LLPointer target = targetFor(*base); + ALMeshSimplifier::Target goal = toCount(9.f); + goal.distant = true; + REQUIRE(ALMeshSimplifier::simplifyModel(*base, *target, goal, Seams::ALL) > 0.f); + const LLVolumeFace& face = target->getVolumeFace(0); + REQUIRE(wellFormed(face)); + S32 bolt_vertices = 0; + for (S32 v = 0; v < face.mNumVertices; ++v) + { + bolt_vertices += v3(face.mPositions[v]).length() > 0.55f; + } + INFO(fmt::format("{} triangles for a target of {}, {} vertices left on the bolts", triangleCount(*target), target_triangles, + bolt_vertices)); + CHECK(bolt_vertices == 0); + CHECK(triangleCount(*target) <= target_triangles); + + // A level of detail seen from near keeps them, short of its target. + LLPointer near_target = targetFor(*base); + REQUIRE(ALMeshSimplifier::simplifyModel(*base, *near_target, toCount(9.f), Seams::ALL) > 0.f); + CHECK(triangleCount(*near_target) > target_triangles); + } + + // ------------------------------------------------------------------ + // Compaction + + TEST_CASE("almeshsimplifier: compaction keeps each vertex the triangles name, and no other", "[almeshsimplifier]") + { + LLPointer base = volumeOf({ sphere(16, 8), cube(2) }); + LLVolumeFace& rigged = base->getVolumeFace(1); + rigged.allocateWeights(rigged.mNumVertices); + for (S32 v = 0; v < rigged.mNumVertices; ++v) + { + rigged.mWeights[v].set(1.25f + v % 7, 0.5f, 0.f, 0.f); + } + + // Every other triangle of each face, numbered face after face, and + // some that take vertices from both, as a sloppy simplification's + // may. + const U32 second = (U32)base->getVolumeFace(0).mNumVertices; + std::vector indices; + for (S32 f = 0; f < 2; ++f) + { + const LLVolumeFace& face = base->getVolumeFace(f); + for (S32 i = 0; i < face.mNumIndices; i += 6) + { + for (S32 k = 0; k < 3; ++k) + { + indices.push_back(face.mIndices[i + k] + (f ? second : 0)); + } + } + } + indices.insert(indices.end(), { 3, second + 5, 7, second + 1, 9, second + 2 }); + + LLVolumeFace compacted; + REQUIRE(ALMeshSimplifier::compact(*base, 1, indices.data(), indices.size(), compacted)); + REQUIRE(wellFormed(compacted)); + REQUIRE((size_t)compacted.mNumIndices == indices.size()); + REQUIRE(compacted.mWeights); + CHECK((size_t)compacted.mNumVertices == std::set(indices.begin(), indices.end()).size()); + + auto from = [&](U32 index) -> std::pair + { + return index < second ? std::make_pair(&base->getVolumeFace(0), (S32)index) + : std::make_pair(&base->getVolumeFace(1), (S32)(index - second)); + }; + size_t differ = 0; + for (size_t i = 0; i < indices.size(); ++i) + { + const auto [face, v] = from(indices[i]); + const S32 c = compacted.mIndices[i]; + differ += memcmp(face->mPositions[v].getF32ptr(), compacted.mPositions[c].getF32ptr(), 12) != 0; + differ += memcmp(face->mNormals[v].getF32ptr(), compacted.mNormals[c].getF32ptr(), 12) != 0; + differ += face->mTexCoords[v] != compacted.mTexCoords[c]; + const F32 weight = face->mWeights ? face->mWeights[v][0] : 0.f; + differ += weight != compacted.mWeights[c][0]; + } + CHECK(differ == 0); + } + + // ------------------------------------------------------------------ + // Normals + + // A cube of eight corners and one vertex each, whose sides meet at right + // angles, and a triangle of no area. + Geometry cornerCube() + { + Geometry g; + for (S32 corner = 0; corner < 8; ++corner) + { + const LLVector3 p((corner & 1) ? 0.5f : -0.5f, (corner & 2) ? 0.5f : -0.5f, (corner & 4) ? 0.5f : -0.5f); + g.add(p, LLVector3(0.f, 0.f, 1.f), LLVector2(0.f, 0.f)); + } + const U16 quads[6][4] = { { 0, 2, 3, 1 }, { 4, 5, 7, 6 }, { 0, 1, 5, 4 }, { 2, 6, 7, 3 }, { 0, 4, 6, 2 }, { 1, 3, 7, 5 } }; + for (const auto& q : quads) + { + g.triangle(q[0], q[1], q[2]); + g.triangle(q[0], q[2], q[3]); + } + g.triangle(0, 1, 1); + return g; + } + + LLVector3 faceNormal(const LLVolumeFace& face, S32 i) + { + const LLVector3 a = v3(face.mPositions[face.mIndices[i]]); + LLVector3 n = (v3(face.mPositions[face.mIndices[i + 1]]) - a) % (v3(face.mPositions[face.mIndices[i + 2]]) - a); + n.normalize(); + return n; + } + + TEST_CASE("almeshsimplifier: generated normals are hard across a crease and smooth within one", "[almeshsimplifier]") + { + ArenaInstalled installed; + LLPointer base = volumeOf({ cornerCube() }); + + LLVolumeFace hard = base->getVolumeFace(0); + REQUIRE(hard.generateNormals(75.f * DEG_TO_RAD, 0.f)); + REQUIRE(wellFormed(hard)); + CHECK(hard.mNumVertices == 24); + CHECK(positionsOf(hard) == positionsOf(base->getVolumeFace(0))); + for (S32 i = 0; i + 3 < hard.mNumIndices; i += 3) + { + const LLVector3 n = faceNormal(hard, i); + for (S32 k = 0; k < 3; ++k) + { + CHECK(v3(hard.mNormals[hard.mIndices[i + k]]) * n > 0.9999f); + } + } + // The triangle of no area takes a normal its corners already have. + const S32 last = hard.mNumIndices - 3; + for (S32 k = 0; k < 3; ++k) + { + CHECK(fabsf(v3(hard.mNormals[hard.mIndices[last + k]]).length() - 1.f) < 1e-4f); + } + + // Past a right angle, each corner is one vertex whose normal leans + // out of the cube. + LLVolumeFace smooth = base->getVolumeFace(0); + REQUIRE(smooth.generateNormals(100.f * DEG_TO_RAD, 0.f)); + CHECK(smooth.mNumVertices == 8); + for (S32 v = 0; v < smooth.mNumVertices; ++v) + { + const LLVector3 p = v3(smooth.mPositions[v]); + const LLVector3 n = v3(smooth.mNormals[v]); + CHECK(n.mV[0] * p.mV[0] > 0.f); + CHECK(n.mV[1] * p.mV[1] > 0.f); + CHECK(n.mV[2] * p.mV[2] > 0.f); + } + } + + TEST_CASE("almeshsimplifier: a sphere's generated normals point out from its centre", "[almeshsimplifier]") + { + ArenaInstalled installed; + LLPointer base = volumeOf({ sphere(32, 16) }); + LLVolumeFace face = base->getVolumeFace(0); + REQUIRE(face.generateNormals(75.f * DEG_TO_RAD, 0.f)); + REQUIRE(wellFormed(face)); + // Its seam and its poles are texture seams, and stay so: each vertex + // a triangle names stays one vertex. + const LLVolumeFace& before = base->getVolumeFace(0); + CHECK((size_t)face.mNumVertices == std::set(before.mIndices, before.mIndices + before.mNumIndices).size()); + F32 worst = 0.f; + for (S32 v = 0; v < face.mNumVertices; ++v) + { + LLVector3 radial = v3(face.mPositions[v]); + radial.normalize(); + const LLVector3 n = v3(face.mNormals[v]); + worst = llmax(worst, acosf(llclamp(n * radial, -1.f, 1.f)) * RAD_TO_DEG); + } + // A facet's own normal would be up to 8 degrees off; the corners of + // the thin triangles around a pole take a degree or two. + INFO(fmt::format("{} degrees off radial at worst", worst)); + CHECK(worst < 2.f); + } + + // A roof whose slopes meet at 60 degrees, kept, as the uploader keeps a + // model, in the unit cube with its size in mNormalizedScale. Squeezed + // into the cube the slopes meet at 127 degrees, past the crease angle; + // in the shape it was made in they meet within it, and its ridge is + // smooth. + TEST_CASE("almeshsimplifier: the crease angle is taken in the shape the model was made in", "[almeshsimplifier]") + { + ArenaInstalled installed; + constexpr F32 WIDTH = 4.f; // eave to eave + const F32 height = 0.5f * WIDTH * tanf(30.f * DEG_TO_RAD); // each slope 30 degrees off level + Geometry roof; + roof.scale = LLVector3(1.f, WIDTH, height); + for (S32 row = 0; row < 3; ++row) + { + const F32 x = 0.5f * row - 0.5f; + roof.add(LLVector3(x, -0.5f, -0.5f), LLVector3(0.f, 0.f, 1.f), LLVector2(0.f, 0.f)); + roof.add(LLVector3(x, 0.f, 0.5f), LLVector3(0.f, 0.f, 1.f), LLVector2(0.f, 0.f)); + roof.add(LLVector3(x, 0.5f, -0.5f), LLVector3(0.f, 0.f, 1.f), LLVector2(0.f, 0.f)); + } + for (S32 row = 0; row < 2; ++row) + { + for (S32 slope = 0; slope < 2; ++slope) + { + const U16 a = (U16)(row * 3 + slope); + roof.triangle(a, (U16)(a + 3), (U16)(a + 4)); + roof.triangle(a, (U16)(a + 4), (U16)(a + 1)); + } + } + LLPointer base = volumeOf({ roof }); + LLVolumeFace face = base->getVolumeFace(0); + REQUIRE(face.generateNormals(75.f * DEG_TO_RAD, 0.f)); + CHECK(face.mNumVertices == 9); + + // A slope's normal as the face keeps it is its own, made in the + // authored shape, times the scale. + LLVector3 slope_normal(0.f, -sinf(30.f * DEG_TO_RAD), cosf(30.f * DEG_TO_RAD)); + slope_normal.scaleVec(roof.scale); + slope_normal.normalize(); + LLVector3 other_slope = slope_normal; + other_slope.mV[1] = -other_slope.mV[1]; + for (S32 v = 0; v < face.mNumVertices; ++v) + { + const LLVector3 p = v3(face.mPositions[v]); + const LLVector3 n = v3(face.mNormals[v]); + if (p.mV[2] > 0.f) + { + // The ridge's normals lie between the slopes'. + CHECK(n * slope_normal > 0.f); + CHECK(n * other_slope > 0.f); + CHECK(n.mV[2] > 0.9f); + } + else + { + // An eave's is its slope's own. + CHECK(n * (p.mV[1] < 0.f ? slope_normal : other_slope) > 0.9999f); + } + } + } + + // ------------------------------------------------------------------ + // The real mesh corpus + + struct Tally + { + size_t simplified = 0; + size_t failed = 0; + size_t short_of_target = 0; // more than the drift over its target + size_t malformed = 0; // an index past its vertices, or no whole triangles + size_t foreign = 0; // a vertex not one of the base's + size_t crossed = 0; // a face holding another's vertex + F64 reached = 0.0; // the sum of each simplification's triangles over its target's + }; + + // The real mesh corpus (almeshcorpus.h), where AL_MESH_CORPUS_DIR names + // one: each mesh's high LOD simplified to the uploader's three lower + // levels in each mode that keeps the borders between faces, and its + // normals made again, keep every face well formed and every vertex one + // of the base's, and each face's vertices its own. How often each mode + // stops short of its target is printed, and is what the uploader's Auto + // mode works through. + TEST_CASE("almeshsimplifier: the real mesh corpus simplifies into well formed faces", "[almeshsimplifier][corpus][.]") + { + std::optional real = ALMeshCorpus::fromEnvironment(); + if (!real) + { + SKIP("AL_MESH_CORPUS_DIR names no mesh cache"); + } + ArenaInstalled installed; + std::map, Tally> tallies; + size_t meshes = 0, normals_made = 0, normals_kept = 0, normals_bad = 0; + real->forEachLOD( + [&](const CorpusLOD& lod) + { + if (lod.lod != 3) + { + return; + } + const LLVolume& base = *lod.volume; + if (triangleCount(base) < 3) + { + return; + } + ++meshes; + + // Each face's texture coordinates as a sign of whose vertex it is. + std::vector>> own(base.getNumVolumeFaces()); + for (S32 f = 0; f < base.getNumVolumeFaces(); ++f) + { + const LLVolumeFace& face = base.getVolumeFace(f); + for (S32 v = 0; v < face.mNumVertices; ++v) + { + std::vector bits(8); + std::memcpy(&bits[0], face.mPositions[v].getF32ptr(), 12); + std::memcpy(&bits[3], face.mNormals[v].getF32ptr(), 12); + std::memcpy(&bits[6], face.mTexCoords[v].mV, 8); + own[f].insert(bits); + } + } + + for (Seams seams : { Seams::ALL, Seams::UV, Seams::NONE }) + { + for (F32 decimator : DECIMATORS) + { + Tally& tally = tallies[{ (S32)seams, decimator }]; + LLPointer target = targetFor(base); + const F32 ratio = ALMeshSimplifier::simplifyModel(base, *target, toCount(decimator), seams); + if (ratio < 0.f) + { + ++tally.failed; + continue; + } + ++tally.simplified; + tally.short_of_target += ratio * ALLOWED_DRIFT < decimator; + tally.reached += decimator / ratio; + for (S32 f = 0; f < target->getNumVolumeFaces(); ++f) + { + const LLVolumeFace& face = target->getVolumeFace(f); + if (!wellFormed(face)) + { + ++tally.malformed; + continue; + } + if (face.mNumVertices == 1) + { + continue; + } + bool foreign = false, crossed = false; + for (S32 v = 0; v < face.mNumVertices; ++v) + { + std::vector bits(8); + std::memcpy(&bits[0], face.mPositions[v].getF32ptr(), 12); + std::memcpy(&bits[3], face.mNormals[v].getF32ptr(), 12); + std::memcpy(&bits[6], face.mTexCoords[v].mV, 8); + if (!own[f].count(bits)) + { + const bool anywhere = + std::any_of(own.begin(), own.end(), [&](const auto& set) { return set.count(bits) != 0; }); + foreign |= !anywhere; + crossed |= anywhere; + } + } + tally.foreign += foreign; + tally.crossed += crossed; + } + } + } + + for (S32 f = 0; f < base.getNumVolumeFaces(); ++f) + { + LLVolumeFace face = base.getVolumeFace(f); + if (face.mNumIndices < 3) + { + continue; + } + if (!face.generateNormals(75.f * DEG_TO_RAD, 0.f)) + { + ++normals_kept; + continue; + } + ++normals_made; + const std::set before = positionsOf(base.getVolumeFace(f)); + bool bad = !wellFormed(face) || positionsOf(face) != before; + for (S32 v = 0; v < face.mNumVertices && !bad; ++v) + { + const F32 length = v3(face.mNormals[v]).length(); + bad = !(fabsf(length - 1.f) < 1e-3f); + } + normals_bad += bad; + } + }); + + INFO(real->describe()); + std::string table = fmt::format("{} meshes' high LODs\n", meshes); + size_t problems = 0; + for (const auto& [key, tally] : tallies) + { + table += fmt::format(" {:<18} a {:>2}th: {} simplified, {} failed, {} short of the target, " + "triangles at {:.2f} of the target on average; " + "{} malformed, {} with a foreign vertex, {} with another face's\n", + nameOf((Seams)key.first), key.second, tally.simplified, tally.failed, tally.short_of_target, + tally.simplified ? tally.reached / tally.simplified : 0.0, tally.malformed, tally.foreign, tally.crossed); + problems += tally.malformed + tally.foreign + tally.crossed; + } + table += fmt::format("normals: {} faces made again, {} kept, {} with a bad normal or a moved position\n", normals_made, + normals_kept, normals_bad); + INFO(table); + CHECK(meshes > 0); + CHECK(problems == 0); + CHECK(normals_bad == 0); + WARN(table); + } +} diff --git a/indra/llprimitive/CMakeLists.txt b/indra/llprimitive/CMakeLists.txt index a5236ea781..ed11105a92 100644 --- a/indra/llprimitive/CMakeLists.txt +++ b/indra/llprimitive/CMakeLists.txt @@ -67,6 +67,7 @@ if(AL_BUILD_TESTS) tests/llmaterial_test.cpp tests/llmaterialid_test.cpp tests/llmediaentry_test.cpp + tests/llmodel_test.cpp LIBRARIES llprimitive ) # This one stubs LLMaterialID and LLPrimTextureList, which the library diff --git a/indra/llprimitive/llmodel.cpp b/indra/llprimitive/llmodel.cpp index 253dbfafbb..17456882c0 100644 --- a/indra/llprimitive/llmodel.cpp +++ b/indra/llprimitive/llmodel.cpp @@ -597,207 +597,21 @@ void LLModel::addFace(const LLVolumeFace& face) } -void LLModel::generateNormals(F32 angle_cutoff) +namespace { - //generate normals for all faces by: - // 1 - Create faceted copy of face with no texture coordinates - // 2 - Weld vertices in faceted copy that are shared between triangles with less than "angle_cutoff" difference between normals - // 3 - Generate smoothed set of normals based on welding results - // 4 - Create faceted copy of face with texture coordinates - // 5 - Copy smoothed normals to faceted copy, using closest normal to triangle normal where more than one normal exists for a given position - // 6 - Remove redundant vertices from new faceted (now smooth) copy + // Passes of meshoptimizer's normal smoothing after the crease angle has + // joined each smooth region's corners. + constexpr F32 NORMAL_SMOOTHING = 0.f; +} - angle_cutoff = cosf(angle_cutoff); - for (U32 j = 0; j < mVolumeFaces.size(); ++j) +void LLModel::generateNormals(F32 angle_cutoff) +{ + for (LLVolumeFace& face : mVolumeFaces) { - LLVolumeFace& vol_face = mVolumeFaces[j]; - - if (vol_face.mNumIndices > 65535) - { - LL_WARNS("MESHSKININFO") << "Too many vertices for normal generation to work." << LL_ENDL; - continue; - } - - //create faceted copy of current face with no texture coordinates (step 1) - LLVolumeFace faceted; - - LLVector4a* src_pos = (LLVector4a*) vol_face.mPositions; - //LLVector4a* src_norm = (LLVector4a*) vol_face.mNormals; - - - faceted.resizeVertices(vol_face.mNumIndices); - faceted.resizeIndices(vol_face.mNumIndices); - //bake out triangles into temporary face, clearing texture coordinates - for (S32 i = 0; i < vol_face.mNumIndices; ++i) - { - U32 idx = vol_face.mIndices[i]; - - faceted.mPositions[i] = src_pos[idx]; - faceted.mTexCoords[i] = LLVector2(0,0); - faceted.mIndices[i] = i; - } - - //generate normals for temporary face - for (S32 i = 0; i < faceted.mNumIndices; i += 3) - { //for each triangle - U16 i0 = faceted.mIndices[i+0]; - U16 i1 = faceted.mIndices[i+1]; - U16 i2 = faceted.mIndices[i+2]; - - LLVector4a& p0 = faceted.mPositions[i0]; - LLVector4a& p1 = faceted.mPositions[i1]; - LLVector4a& p2 = faceted.mPositions[i2]; - - LLVector4a& n0 = faceted.mNormals[i0]; - LLVector4a& n1 = faceted.mNormals[i1]; - LLVector4a& n2 = faceted.mNormals[i2]; - - LLVector4a lhs, rhs; - lhs.setSub(p1, p0); - rhs.setSub(p2, p0); - - n0.setCross3(lhs, rhs); - n0.normalize3(); - n1 = n0; - n2 = n0; - } - - //weld vertices in temporary face, respecting angle_cutoff (step 2) - faceted.optimize(angle_cutoff); - - //generate normals for welded face based on new topology (step 3) - - for (S32 i = 0; i < faceted.mNumVertices; i++) - { - faceted.mNormals[i].clear(); - } - - for (S32 i = 0; i < faceted.mNumIndices; i += 3) - { //for each triangle - U16 i0 = faceted.mIndices[i+0]; - U16 i1 = faceted.mIndices[i+1]; - U16 i2 = faceted.mIndices[i+2]; - - LLVector4a& p0 = faceted.mPositions[i0]; - LLVector4a& p1 = faceted.mPositions[i1]; - LLVector4a& p2 = faceted.mPositions[i2]; - - LLVector4a& n0 = faceted.mNormals[i0]; - LLVector4a& n1 = faceted.mNormals[i1]; - LLVector4a& n2 = faceted.mNormals[i2]; - - LLVector4a lhs, rhs; - lhs.setSub(p1, p0); - rhs.setSub(p2, p0); - - LLVector4a n; - n.setCross3(lhs, rhs); - - n0.add(n); - n1.add(n); - n2.add(n); - } - - //normalize normals and build point map - LLVolumeFace::VertexMapData::PointMap point_map; - - for (S32 i = 0; i < faceted.mNumVertices; ++i) - { - faceted.mNormals[i].normalize3(); - - LLVolumeFace::VertexMapData v; - v.setPosition(faceted.mPositions[i]); - v.setNormal(faceted.mNormals[i]); - - point_map[LLVector3(v.getPosition().getF32ptr())].push_back(v); - } - - //create faceted copy of current face with texture coordinates (step 4) - LLVolumeFace new_face; - - //bake out triangles into new face - new_face.resizeIndices(vol_face.mNumIndices); - new_face.resizeVertices(vol_face.mNumIndices); - - for (S32 i = 0; i < vol_face.mNumIndices; ++i) - { - U32 idx = vol_face.mIndices[i]; - LLVolumeFace::VertexData v; - new_face.mPositions[i] = vol_face.mPositions[idx]; - new_face.mNormals[i].clear(); - new_face.mIndices[i] = i; - } - - if (vol_face.mTexCoords) - { - for (S32 i = 0; i < vol_face.mNumIndices; i++) - { - U32 idx = vol_face.mIndices[i]; - new_face.mTexCoords[i] = vol_face.mTexCoords[idx]; - } - } - else - { - //ll_aligned_free_16(new_face.mTexCoords); - new_face.mTexCoords = NULL; - } - - //generate normals for new face - for (S32 i = 0; i < new_face.mNumIndices; i += 3) - { //for each triangle - U16 i0 = new_face.mIndices[i+0]; - U16 i1 = new_face.mIndices[i+1]; - U16 i2 = new_face.mIndices[i+2]; - - LLVector4a& p0 = new_face.mPositions[i0]; - LLVector4a& p1 = new_face.mPositions[i1]; - LLVector4a& p2 = new_face.mPositions[i2]; - - LLVector4a& n0 = new_face.mNormals[i0]; - LLVector4a& n1 = new_face.mNormals[i1]; - LLVector4a& n2 = new_face.mNormals[i2]; - - LLVector4a lhs, rhs; - lhs.setSub(p1, p0); - rhs.setSub(p2, p0); - - n0.setCross3(lhs, rhs); - n0.normalize3(); - n1 = n0; - n2 = n0; - } - - //swap out normals in new_face with best match from point map (step 5) - for (S32 i = 0; i < new_face.mNumVertices; ++i) + if (face.mNumIndices >= 3 && !face.generateNormals(angle_cutoff, NORMAL_SMOOTHING)) { - //LLVolumeFace::VertexData v = new_face.mVertices[i]; - - LLVector4a ref_norm = new_face.mNormals[i]; - - LLVolumeFace::VertexMapData::PointMap::iterator iter = point_map.find(LLVector3(new_face.mPositions[i].getF32ptr())); - - if (iter != point_map.end()) - { - F32 best = -2.f; - for (U32 k = 0; k < iter->second.size(); ++k) - { - LLVector4a& n = iter->second[k].getNormal(); - - F32 cur = n.dot3(ref_norm).getF32(); - - if (cur > best) - { - best = cur; - new_face.mNormals[i] = n; - } - } - } + LL_WARNS("MESHSKININFO") << "Kept the normals of a face of " << mLabel << " that normal generation could not make" << LL_ENDL; } - - //remove redundant vertices from new face (step 6) - new_face.optimize(); - - mVolumeFaces[j] = new_face; } } diff --git a/indra/llprimitive/tests/llmodel_test.cpp b/indra/llprimitive/tests/llmodel_test.cpp new file mode 100644 index 0000000000..0a112d9d99 --- /dev/null +++ b/indra/llprimitive/tests/llmodel_test.cpp @@ -0,0 +1,133 @@ +/** + * @file llmodel_test.cpp + * @brief A model's generated normals and levels of detail against its skin + * weights. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * 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 + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llmodel.h" +#include "almeshsimplifier.h" +#include "llmath.h" + +#include + +namespace +{ + LLVolumeParams meshParams() + { + LLVolumeParams params; + params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE); + return params; + } + + // A latitude-longitude sphere of one face, its seam column twice, and a + // skin weight at each of its positions, as a loader keys them. + LLPointer riggedSphere(S32 columns, S32 rows) + { + LLPointer model = new LLModel(meshParams(), 0.f); + model->setNumVolumeFaces(1); + LLVolumeFace& face = model->getVolumeFace(0); + const S32 vertices = (columns + 1) * (rows + 1); + face.resizeVertices(vertices); + for (S32 r = 0; r <= rows; ++r) + { + for (S32 c = 0; c <= columns; ++c) + { + const F32 theta = F_PI * r / rows; + const F32 phi = F_TWO_PI * (c % columns) / columns; + const F32 ring = (r == 0 || r == rows) ? 0.f : sinf(theta); + const F32 height = r == 0 ? 1.f : (r == rows ? -1.f : cosf(theta)); + const S32 v = c + r * (columns + 1); + face.mPositions[v].set(0.5f * ring * cosf(phi), 0.5f * ring * sinf(phi), 0.5f * height, 0.f); + face.mNormals[v].set(ring * cosf(phi), ring * sinf(phi), height, 0.f); + face.mTexCoords[v].set((F32)c / columns, 1.f - (F32)r / rows); + } + } + std::vector indices; + auto at = [&](S32 c, S32 r) { return (U16)(c + r * (columns + 1)); }; + for (S32 r = 0; r < rows; ++r) + { + for (S32 c = 0; c < columns; ++c) + { + if (r > 0) + { + indices.insert(indices.end(), { at(c, r), at(c, r + 1), at(c + 1, r) }); + } + if (r < rows - 1) + { + indices.insert(indices.end(), { at(c + 1, r), at(c, r + 1), at(c + 1, r + 1) }); + } + } + } + face.resizeIndices((S32)indices.size()); + std::copy(indices.begin(), indices.end(), face.mIndices); + + for (S32 v = 0; v < vertices; ++v) + { + const LLVector3 position(face.mPositions[v].getF32ptr()); + const F32 lean = 0.5f + position.mV[2]; + model->mSkinWeights[position] = { LLModel::JointWeight(0, 1.f - lean), LLModel::JointWeight(1, lean) }; + } + return model; + } + + // Every vertex finds its weights by its position as it is, without the + // nearest-point search that a moved vertex would fall back on. + bool weightsFound(LLModel& model) + { + for (S32 f = 0; f < model.getNumVolumeFaces(); ++f) + { + const LLVolumeFace& face = model.getVolumeFace(f); + for (S32 v = 0; v < face.mNumVertices; ++v) + { + if (model.mSkinWeights.find(LLVector3(face.mPositions[v].getF32ptr())) == model.mSkinWeights.end()) + { + return false; + } + } + } + return true; + } + + TEST_CASE("llmodel: generated normals and a simplified level of detail keep each vertex's skin weights", "[llmodel]") + { + LLPointer model = riggedSphere(32, 16); + REQUIRE(weightsFound(*model)); + + model->generateNormals(30.f * DEG_TO_RAD); + CHECK(model->getVolumeFace(0).mNumVertices > 0); + CHECK(weightsFound(*model)); + + using Seams = ALMeshSimplifier::Seams; + for (Seams seams : { Seams::ALL, Seams::NONE, Seams::SLOPPY }) + { + LLPointer lod = new LLModel(meshParams(), 0.f); + lod->setNumVolumeFaces(1); + ALMeshSimplifier::Target goal; + goal.decimator = 9.f; + REQUIRE(ALMeshSimplifier::simplifyModel(*model, *lod, goal, seams) > 0.f); + lod->mSkinWeights = model->mSkinWeights; + CHECK(weightsFound(*lod)); + } + } +} diff --git a/indra/newview/llmodelpreview.cpp b/indra/newview/llmodelpreview.cpp index e13fc7e09d..bd67480d1b 100644 --- a/indra/newview/llmodelpreview.cpp +++ b/indra/newview/llmodelpreview.cpp @@ -33,6 +33,7 @@ #include "gltf/llgltfloader.h" #include "llfloatermodelpreview.h" +#include "almeshsimplifier.h" #include "llagent.h" #include "llanimationstates.h" #include "llcallbacklist.h" @@ -43,7 +44,6 @@ #include "lljointdata.h" #include "llmatrix4a.h" #include "llmeshrepository.h" -#include "llmeshoptimizer.h" #include "llrender.h" #include "llsdutil_math.h" #include "llskinningutil.h" @@ -1392,432 +1392,6 @@ void LLModelPreview::restoreNormals() updateStatusMessages(); } -// Runs per object, but likely it is a better way to run per model+submodels -// returns a ratio of base model indices to resulting indices -// returns -1 in case of failure -F32 LLModelPreview::genMeshOptimizerPerModel(LLModel *base_model, LLModel *target_model, F32 indices_decimator, F32 error_threshold, eSimplificationMode simplification_mode) -{ - // I. Weld faces together - // Figure out buffer size - S32 size_indices = 0; - S32 size_vertices = 0; - - for (S32 face_idx = 0; face_idx < base_model->getNumVolumeFaces(); ++face_idx) - { - const LLVolumeFace &face = base_model->getVolumeFace(face_idx); - size_indices += face.mNumIndices; - size_vertices += face.mNumVertices; - } - - if (size_indices < 3) - { - return -1; - } - - // Allocate buffers, note that we are using U32 buffer instead of U16 - U32* combined_indices = (U32*)ll_aligned_malloc_32(size_indices * sizeof(U32)); - U32* output_indices = (U32*)ll_aligned_malloc_32(size_indices * sizeof(U32)); - - // extra space for normals and text coords - S32 tc_bytes_size = ((size_vertices * sizeof(LLVector2)) + 0xF) & ~0xF; - LLVector4a* combined_positions = (LLVector4a*)ll_aligned_malloc<64>(sizeof(LLVector4a) * 3 * size_vertices + tc_bytes_size); - LLVector4a* combined_normals = combined_positions + size_vertices; - LLVector2* combined_tex_coords = (LLVector2*)(combined_normals + size_vertices); - - // copy indices and vertices into new buffers - S32 combined_positions_shift = 0; - S32 indices_idx_shift = 0; - S32 combined_indices_shift = 0; - for (S32 face_idx = 0; face_idx < base_model->getNumVolumeFaces(); ++face_idx) - { - const LLVolumeFace &face = base_model->getVolumeFace(face_idx); - - // Vertices - S32 copy_bytes = face.mNumVertices * sizeof(LLVector4a); - LLVector4a::memcpyNonAliased16((F32*)(combined_positions + combined_positions_shift), (F32*)face.mPositions, copy_bytes); - - // Normals - LLVector4a::memcpyNonAliased16((F32*)(combined_normals + combined_positions_shift), (F32*)face.mNormals, copy_bytes); - - // Tex coords - copy_bytes = face.mNumVertices * sizeof(LLVector2); - memcpy((void*)(combined_tex_coords + combined_positions_shift), (void*)face.mTexCoords, copy_bytes); - - combined_positions_shift += face.mNumVertices; - - // Indices - // Sadly can't do dumb memcpy for indices, need to adjust each value - for (S32 i = 0; i < face.mNumIndices; ++i) - { - U16 idx = face.mIndices[i]; - - combined_indices[combined_indices_shift] = idx + indices_idx_shift; - combined_indices_shift++; - } - indices_idx_shift += face.mNumVertices; - } - - // II. Generate a shadow buffer if nessesary. - // Welds together vertices if possible - - U32* shadow_indices = NULL; - // if MESH_OPTIMIZER_FULL, just leave as is, since generateShadowIndexBufferU32 - // won't do anything new, model was remaped on a per face basis. - // Similar for MESH_OPTIMIZER_NO_TOPOLOGY, it's pointless - // since 'simplifySloppy' ignores all topology, including normals and uvs. - // Note: simplifySloppy can affect UVs significantly. - if (simplification_mode == MESH_OPTIMIZER_NO_NORMALS) - { - // strip normals, reflections should restore relatively correctly - shadow_indices = (U32*)ll_aligned_malloc_32(size_indices * sizeof(U32)); - LLMeshOptimizer::generateShadowIndexBufferU32(shadow_indices, combined_indices, size_indices, combined_positions, NULL, combined_tex_coords, size_vertices); - } - if (simplification_mode == MESH_OPTIMIZER_NO_UVS) - { - // strip uvs, can heavily affect textures - shadow_indices = (U32*)ll_aligned_malloc_32(size_indices * sizeof(U32)); - LLMeshOptimizer::generateShadowIndexBufferU32(shadow_indices, combined_indices, size_indices, combined_positions, NULL, NULL, size_vertices); - } - - U32* source_indices = NULL; - if (shadow_indices) - { - source_indices = shadow_indices; - } - else - { - source_indices = combined_indices; - } - - // III. Simplify - S32 target_indices = 0; - F32 result_error = 0; // how far from original the model is, 1 == 100% - S32 size_new_indices = 0; - - if (indices_decimator > 0) - { - target_indices = llclamp(llfloor(size_indices / indices_decimator), 3, (S32)size_indices); // leave at least one triangle - } - else // indices_decimator can be zero for error_threshold based calculations - { - target_indices = 3; - } - - size_new_indices = (S32)LLMeshOptimizer::simplifyU32( - output_indices, - source_indices, - size_indices, - combined_positions, - size_vertices, - LLVertexBuffer::sTypeSize[LLVertexBuffer::TYPE_VERTEX], - target_indices, - error_threshold, - simplification_mode == MESH_OPTIMIZER_NO_TOPOLOGY, - &result_error); - - if (result_error < 0) - { - LL_WARNS() << "Negative result error from meshoptimizer for model " << target_model->mLabel - << " target Indices: " << target_indices - << " new Indices: " << size_new_indices - << " original count: " << size_indices << LL_ENDL; - } - - // free unused buffers - ll_aligned_free_32(combined_indices); - ll_aligned_free_32(shadow_indices); - combined_indices = NULL; - shadow_indices = NULL; - - if (size_new_indices < 3) - { - // Model should have at least one visible triangle - ll_aligned_free<64>(combined_positions); - ll_aligned_free_32(output_indices); - - return -1; - } - - // IV. Repack back into individual faces - - LLVector4a* buffer_positions = (LLVector4a*)ll_aligned_malloc<64>(sizeof(LLVector4a) * 3 * size_vertices + tc_bytes_size); - LLVector4a* buffer_normals = buffer_positions + size_vertices; - LLVector2* buffer_tex_coords = (LLVector2*)(buffer_normals + size_vertices); - S32 buffer_idx_size = (size_indices * sizeof(U16) + 0xF) & ~0xF; - U16* buffer_indices = (U16*)ll_aligned_malloc_16(buffer_idx_size); - S32* old_to_new_positions_map = new S32[size_vertices]; - - S32 buf_positions_copied = 0; - S32 buf_indices_copied = 0; - indices_idx_shift = 0; - S32 valid_faces = 0; - - // Crude method to copy indices back into face - for (S32 face_idx = 0; face_idx < base_model->getNumVolumeFaces(); ++face_idx) - { - const LLVolumeFace &face = base_model->getVolumeFace(face_idx); - - // reset data for new run - buf_positions_copied = 0; - buf_indices_copied = 0; - bool copy_triangle = false; - S32 range = indices_idx_shift + face.mNumVertices; - - for (S32 i = 0; i < size_vertices; i++) - { - old_to_new_positions_map[i] = -1; - } - - // Copy relevant indices and vertices - for (S32 i = 0; i < size_new_indices; ++i) - { - S32 idx = (S32)output_indices[i]; - - if ((i % 3) == 0) - { - copy_triangle = idx >= indices_idx_shift && idx < range; - } - - if (copy_triangle) - { - if (old_to_new_positions_map[idx] == -1) - { - // New position, need to copy it - // Validate size - if (buf_positions_copied >= U16_MAX) - { - // Normally this shouldn't happen since the whole point is to reduce amount of vertices - // but it might happen if user tries to run optimization with too large triangle or error value - // so fallback to 'per face' mode or verify requested limits and copy base model as is. - LL_WARNS() << "Over triangle limit. Failed to optimize in 'per object' mode, falling back to per face variant for" - << " model " << target_model->mLabel - << " target Indices: " << target_indices - << " new Indices: " << size_new_indices - << " original count: " << size_indices - << " error treshold: " << error_threshold - << LL_ENDL; - - // U16 vertices overflow shouldn't happen, but just in case - size_new_indices = 0; - valid_faces = 0; - for (S32 face_idx = 0; face_idx < base_model->getNumVolumeFaces(); ++face_idx) - { - genMeshOptimizerPerFace(base_model, target_model, face_idx, indices_decimator, error_threshold, simplification_mode); - const LLVolumeFace &face = target_model->getVolumeFace(face_idx); - size_new_indices += face.mNumIndices; - if (face.mNumIndices >= 3) - { - valid_faces++; - } - } - if (valid_faces) - { - return (F32)size_indices / (F32)size_new_indices; - } - else - { - return -1; - } - } - - // Copy vertice, normals, tcs - buffer_positions[buf_positions_copied] = combined_positions[idx]; - buffer_normals[buf_positions_copied] = combined_normals[idx]; - buffer_tex_coords[buf_positions_copied] = combined_tex_coords[idx]; - - old_to_new_positions_map[idx] = buf_positions_copied; - - buffer_indices[buf_indices_copied] = (U16)buf_positions_copied; - buf_positions_copied++; - } - else - { - // existing position - buffer_indices[buf_indices_copied] = (U16)old_to_new_positions_map[idx]; - } - buf_indices_copied++; - } - } - - if (buf_positions_copied >= U16_MAX) - { - break; - } - - LLVolumeFace &new_face = target_model->getVolumeFace(face_idx); - //new_face = face; //temp - - if (buf_indices_copied < 3) - { - // face was optimized away - new_face.resizeIndices(3); - new_face.resizeVertices(1); - memset(new_face.mIndices, 0, sizeof(U16) * 3); - new_face.mPositions[0].clear(); // set first vertice to 0 - new_face.mNormals[0].clear(); - new_face.mTexCoords[0].setZero(); - } - else - { - new_face.resizeIndices(buf_indices_copied); - new_face.resizeVertices(buf_positions_copied); - new_face.allocateTangents(buf_positions_copied); - S32 idx_size = (buf_indices_copied * sizeof(U16) + 0xF) & ~0xF; - LLVector4a::memcpyNonAliased16((F32*)new_face.mIndices, (F32*)buffer_indices, idx_size); - - LLVector4a::memcpyNonAliased16((F32*)new_face.mPositions, (F32*)buffer_positions, buf_positions_copied * sizeof(LLVector4a)); - LLVector4a::memcpyNonAliased16((F32*)new_face.mNormals, (F32*)buffer_normals, buf_positions_copied * sizeof(LLVector4a)); - - U32 tex_size = (buf_positions_copied * sizeof(LLVector2) + 0xF)&~0xF; - LLVector4a::memcpyNonAliased16((F32*)new_face.mTexCoords, (F32*)buffer_tex_coords, tex_size); - - valid_faces++; - } - - indices_idx_shift += face.mNumVertices; - } - - delete[]old_to_new_positions_map; - ll_aligned_free<64>(combined_positions); - ll_aligned_free<64>(buffer_positions); - ll_aligned_free_32(output_indices); - ll_aligned_free_16(buffer_indices); - - if (size_new_indices < 3 || valid_faces == 0) - { - // Model should have at least one visible triangle - return -1; - } - - return (F32)size_indices / (F32)size_new_indices; -} - -F32 LLModelPreview::genMeshOptimizerPerFace(LLModel *base_model, LLModel *target_model, U32 face_idx, F32 indices_decimator, F32 error_threshold, eSimplificationMode simplification_mode) -{ - const LLVolumeFace &face = base_model->getVolumeFace(face_idx); - S32 size_indices = face.mNumIndices; - if (size_indices < 3) - { - return -1; - } - - S32 size = (size_indices * sizeof(U16) + 0xF) & ~0xF; - U16* output_indices = (U16*)ll_aligned_malloc_16(size); - - U16* shadow_indices = NULL; - // if MESH_OPTIMIZER_FULL, just leave as is, since generateShadowIndexBufferU32 - // won't do anything new, model was remaped on a per face basis. - // Similar for MESH_OPTIMIZER_NO_TOPOLOGY, it's pointless - // since 'simplifySloppy' ignores all topology, including normals and uvs. - if (simplification_mode == MESH_OPTIMIZER_NO_NORMALS) - { - U16* shadow_indices = (U16*)ll_aligned_malloc_16(size); - LLMeshOptimizer::generateShadowIndexBufferU16(shadow_indices, face.mIndices, size_indices, face.mPositions, NULL, face.mTexCoords, face.mNumVertices); - } - if (simplification_mode == MESH_OPTIMIZER_NO_UVS) - { - U16* shadow_indices = (U16*)ll_aligned_malloc_16(size); - LLMeshOptimizer::generateShadowIndexBufferU16(shadow_indices, face.mIndices, size_indices, face.mPositions, NULL, NULL, face.mNumVertices); - } - // Don't run ShadowIndexBuffer for MESH_OPTIMIZER_NO_TOPOLOGY, it's pointless - - U16* source_indices = NULL; - if (shadow_indices) - { - source_indices = shadow_indices; - } - else - { - source_indices = face.mIndices; - } - - S32 target_indices = 0; - F32 result_error = 0; // how far from original the model is, 1 == 100% - S32 size_new_indices = 0; - - if (indices_decimator > 0) - { - target_indices = llclamp(llfloor(size_indices / indices_decimator), 3, (S32)size_indices); // leave at least one triangle - } - else - { - target_indices = 3; - } - - size_new_indices = (S32)LLMeshOptimizer::simplify( - output_indices, - source_indices, - size_indices, - face.mPositions, - face.mNumVertices, - LLVertexBuffer::sTypeSize[LLVertexBuffer::TYPE_VERTEX], - target_indices, - error_threshold, - simplification_mode == MESH_OPTIMIZER_NO_TOPOLOGY, - &result_error); - - if (result_error < 0) - { - LL_WARNS() << "Negative result error from meshoptimizer for face " << face_idx - << " of model " << target_model->mLabel - << " target Indices: " << target_indices - << " new Indices: " << size_new_indices - << " original count: " << size_indices - << " error treshold: " << error_threshold - << LL_ENDL; - } - - LLVolumeFace &new_face = target_model->getVolumeFace(face_idx); - - // Copy old values - new_face = face; - - if (size_new_indices < 3) - { - if (simplification_mode != MESH_OPTIMIZER_NO_TOPOLOGY) - { - // meshopt_optimizeSloppy() can optimize triangles away even if target_indices is > 2, - // but optimize() isn't supposed to - LL_INFOS() << "No indices generated by meshoptimizer for face " << face_idx - << " of model " << target_model->mLabel - << " target Indices: " << target_indices - << " original count: " << size_indices - << " error treshold: " << error_threshold - << LL_ENDL; - } - - // Face got optimized away - // Generate empty triangle - new_face.resizeIndices(3); - new_face.resizeVertices(1); - memset(new_face.mIndices, 0, sizeof(U16) * 3); - new_face.mPositions[0].clear(); // set first vertice to 0 - new_face.mNormals[0].clear(); - new_face.mTexCoords[0].setZero(); - } - else - { - // Assign new values - new_face.resizeIndices(size_new_indices); // will wipe out mIndices, so new_face can't substitute output - S32 idx_size = (size_new_indices * sizeof(U16) + 0xF) & ~0xF; - LLVector4a::memcpyNonAliased16((F32*)new_face.mIndices, (F32*)output_indices, idx_size); - - // Clear unused values - new_face.optimize(); - } - - ll_aligned_free_16(output_indices); - ll_aligned_free_16(shadow_indices); - - if (size_new_indices < 3) - { - // At least one triangle is needed - return -1; - } - - return (F32)size_indices / (F32)size_new_indices; -} - void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 decimation, bool enforce_tri_limit) { LL_DEBUGS("Upload") << "Generating lod " << which_lod << " using meshoptimizer" << LL_ENDL; @@ -1961,6 +1535,21 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d S32 model_meshopt_mode = meshopt_mode; + ALMeshSimplifier::Target goal; + goal.decimator = indices_decimator; + goal.error = lod_error_threshold; + goal.distant = lod <= LLModel::LOD_LOW; + auto simplify_model = [&](F32 decimator, ALMeshSimplifier::Seams seams) + { + ALMeshSimplifier::Target at = goal; + at.decimator = decimator; + return ALMeshSimplifier::simplifyModel(*base, *target_model, at, seams); + }; + auto simplify_face = [&](S32 face_idx, ALMeshSimplifier::Seams seams) + { + return ALMeshSimplifier::simplifyFace(*base, face_idx, target_model->getVolumeFace(face_idx), goal, seams); + }; + // Ideally this should run not per model, // but combine all submodels with origin model as well if (model_meshopt_mode == MESH_OPTIMIZER_PRECISE) @@ -1968,7 +1557,7 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d // Run meshoptimizer for each face for (S32 face_idx = 0; face_idx < base->getNumVolumeFaces(); ++face_idx) { - F32 res = genMeshOptimizerPerFace(base, target_model, face_idx, indices_decimator, lod_error_threshold, MESH_OPTIMIZER_FULL); + F32 res = simplify_face(face_idx, ALMeshSimplifier::Seams::ALL); if (res < 0) { // Mesh optimizer failed and returned an invalid model @@ -1989,10 +1578,10 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d // Run meshoptimizer for each face for (S32 face_idx = 0; face_idx < base->getNumVolumeFaces(); ++face_idx) { - if (genMeshOptimizerPerFace(base, target_model, face_idx, indices_decimator, lod_error_threshold, MESH_OPTIMIZER_NO_TOPOLOGY) < 0) + if (simplify_face(face_idx, ALMeshSimplifier::Seams::SLOPPY) < 0) { // Sloppy failed and returned an invalid model - if (genMeshOptimizerPerFace(base, target_model, face_idx, indices_decimator, lod_error_threshold, MESH_OPTIMIZER_FULL) < 0) + if (simplify_face(face_idx, ALMeshSimplifier::Seams::ALL) < 0) { meshes_fail_count++; } @@ -2012,16 +1601,16 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d { // Remove progressively more data if we can't reach the target. F32 allowed_ratio_drift = 1.8f; - F32 precise_ratio = genMeshOptimizerPerModel(base, target_model, indices_decimator, lod_error_threshold, MESH_OPTIMIZER_FULL); + F32 precise_ratio = simplify_model(indices_decimator, ALMeshSimplifier::Seams::ALL); if (precise_ratio < 0 || (precise_ratio * allowed_ratio_drift < indices_decimator)) { - precise_ratio = genMeshOptimizerPerModel(base, target_model, indices_decimator, lod_error_threshold, MESH_OPTIMIZER_NO_NORMALS); + precise_ratio = simplify_model(indices_decimator, ALMeshSimplifier::Seams::UV); } if (precise_ratio < 0 || (precise_ratio * allowed_ratio_drift < indices_decimator)) { - precise_ratio = genMeshOptimizerPerModel(base, target_model, indices_decimator, lod_error_threshold, MESH_OPTIMIZER_NO_UVS); + precise_ratio = simplify_model(indices_decimator, ALMeshSimplifier::Seams::NONE); } if (precise_ratio < 0 || (precise_ratio * allowed_ratio_drift < indices_decimator)) @@ -2034,7 +1623,7 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d F32 last_working_decimator = 0.f; F32 last_working_ratio = F32_MAX; - F32 sloppy_ratio = genMeshOptimizerPerModel(base, target_model, indices_decimator, lod_error_threshold, MESH_OPTIMIZER_NO_TOPOLOGY); + F32 sloppy_ratio = simplify_model(indices_decimator, ALMeshSimplifier::Seams::SLOPPY); if (sloppy_ratio > 0) { @@ -2057,13 +1646,13 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d // side due to overal lack of precision, and we don't need an ideal result, which // likely does not exist, just a better one, so a partial correction is enough. F32 sloppy_decimator = indices_decimator * (indices_decimator / sloppy_ratio + 1) / 2; - sloppy_ratio = genMeshOptimizerPerModel(base, target_model, sloppy_decimator, lod_error_threshold, MESH_OPTIMIZER_NO_TOPOLOGY); + sloppy_ratio = simplify_model(sloppy_decimator, ALMeshSimplifier::Seams::SLOPPY); } if (last_working_decimator > 0 && sloppy_ratio < last_working_ratio) { // Compensation didn't work, return back to previous decimator - sloppy_ratio = genMeshOptimizerPerModel(base, target_model, indices_decimator, lod_error_threshold, MESH_OPTIMIZER_NO_TOPOLOGY); + sloppy_ratio = simplify_model(indices_decimator, ALMeshSimplifier::Seams::SLOPPY); } if (sloppy_ratio < 0) @@ -2080,7 +1669,7 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d && sloppy_decimator > 1 // precise_ratio isn't supposed to be below 1, but check just in case && end_time > LLTimer::getTotalTime()) { - sloppy_ratio = genMeshOptimizerPerModel(base, target_model, sloppy_decimator, lod_error_threshold, MESH_OPTIMIZER_NO_TOPOLOGY); + sloppy_ratio = simplify_model(sloppy_decimator, ALMeshSimplifier::Seams::SLOPPY); sloppy_decimator = sloppy_decimator / sloppy_decimation_step; } } @@ -2100,7 +1689,7 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d else { // Fallback to normal method - precise_ratio = genMeshOptimizerPerModel(base, target_model, indices_decimator, lod_error_threshold, MESH_OPTIMIZER_FULL); + precise_ratio = simplify_model(indices_decimator, ALMeshSimplifier::Seams::ALL); } LL_DEBUGS("Upload") << "Model " << target_model->getName() diff --git a/indra/newview/llmodelpreview.h b/indra/newview/llmodelpreview.h index aa25c79f97..fa831e151f 100644 --- a/indra/newview/llmodelpreview.h +++ b/indra/newview/llmodelpreview.h @@ -115,8 +115,8 @@ class LLModelPreview : public LLViewerDynamicTexture, public LLMutex, public LLH typedef enum { LOD_FROM_FILE = 0, - MESH_OPTIMIZER_AUTO, // automatically selects method based on model or face - MESH_OPTIMIZER_PRECISE, // combines faces into a single model, simplifies, then splits back into faces + MESH_OPTIMIZER_AUTO, // simplifies a model's faces together, crossing more seams until it reaches the target + MESH_OPTIMIZER_PRECISE, // simplifies each face on its own, keeping every seam MESH_OPTIMIZER_SLOPPY, // uses sloppy method, works per face USE_LOD_ABOVE, } eLoDMode; @@ -238,22 +238,6 @@ class LLModelPreview : public LLViewerDynamicTexture, public LLMutex, public LLH S32 mNumOfFetchingTextures; bool mTexturesNeedScaling; - typedef enum - { - MESH_OPTIMIZER_FULL, - MESH_OPTIMIZER_NO_NORMALS, - MESH_OPTIMIZER_NO_UVS, - MESH_OPTIMIZER_NO_TOPOLOGY, - } eSimplificationMode; - - // Merges faces into single mesh, simplifies using mesh optimizer, - // then splits back into faces. - // Returns reached simplification ratio. -1 in case of a failure. - F32 genMeshOptimizerPerModel(LLModel *base_model, LLModel *target_model, F32 indices_ratio, F32 error_threshold, eSimplificationMode simplification_mode); - // Simplifies specified face using mesh optimizer. - // Returns reached simplification ratio. -1 in case of a failure. - F32 genMeshOptimizerPerFace(LLModel *base_model, LLModel *target_model, U32 face_idx, F32 indices_ratio, F32 error_threshold, eSimplificationMode simplification_mode); - protected: friend class LLModelLoader; friend class LLFloaterModelPreview;