Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions indra/llmath/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ add_library(llmath STATIC)
target_sources(
llmath
PRIVATE
almeshsimplifier.cpp
alplaneset.cpp
alprojection.cpp
alsimdkernels.cpp
Expand Down Expand Up @@ -47,6 +48,7 @@ target_sources(
PUBLIC
FILE_SET HEADERS
FILES
almeshsimplifier.h
alplaneset.h
alprojection.h
alsimdkernels.h
Expand Down Expand Up @@ -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
Expand Down
677 changes: 677 additions & 0 deletions indra/llmath/almeshsimplifier.cpp

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions indra/llmath/almeshsimplifier.h
Original file line number Diff line number Diff line change
@@ -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 <rye@alchemyviewer.org>
*
* 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
182 changes: 182 additions & 0 deletions indra/llmath/llvolume.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<U32> 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<LLVector4a> positions(vertex_count);
for (size_t v = 0; v < vertex_count; ++v)
{
positions[v].setMul(mPositions[v], scale);
}

std::vector<F32> 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<LLVector4a> corner_normals(index_count);
std::vector<U32> 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<U32> 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<U32> corner_vertices(index_count);
for (size_t c = 0; c < index_count; ++c)
{
corner_vertices[c] = canonical[indices[c]];
}
std::vector<U32> 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<U32> 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;
Expand Down
7 changes: 7 additions & 0 deletions indra/llmath/llvolume.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading