diff options
| author | Brad Kittenbrink <brad@lindenlab.com> | 2022-04-27 10:27:48 -0700 | 
|---|---|---|
| committer | Brad Kittenbrink <brad@lindenlab.com> | 2022-04-27 10:27:48 -0700 | 
| commit | a3ffa9f006b008b5faad248f700c8c2fbc6b74fd (patch) | |
| tree | f674d4d4e3ab2d4ef4a7419594ea507506238232 /indra/newview/llmodelpreview.cpp | |
| parent | 030d61ac58be38f9f8aafeb68b383d88d670080b (diff) | |
| parent | bafa869c21cb8b329f94be6fa930a43d11699082 (diff) | |
Merge remote-tracking branch 'origin/DRTVWR-546' into DRTVWR-559
Diffstat (limited to 'indra/newview/llmodelpreview.cpp')
| -rw-r--r-- | indra/newview/llmodelpreview.cpp | 873 | 
1 files changed, 567 insertions, 306 deletions
| diff --git a/indra/newview/llmodelpreview.cpp b/indra/newview/llmodelpreview.cpp index fef169cbbc..859d987fc3 100644 --- a/indra/newview/llmodelpreview.cpp +++ b/indra/newview/llmodelpreview.cpp @@ -41,6 +41,7 @@  #include "lliconctrl.h"  #include "llmatrix4a.h"  #include "llmeshrepository.h" +#include "llmeshoptimizer.h"  #include "llrender.h"  #include "llsdutil_math.h"  #include "llskinningutil.h" @@ -66,7 +67,6 @@  #include "lltabcontainer.h"  #include "lltextbox.h" -#include "glod/glod.h"  #include <boost/algorithm/string.hpp>  bool LLModelPreview::sIgnoreLoadedCallback = false; @@ -89,19 +89,6 @@ static const F32 PREVIEW_ZOOM_LIMIT(10.f);  const F32 SKIN_WEIGHT_CAMERA_DISTANCE = 16.f; -BOOL stop_gloderror() -{ -    GLuint error = glodGetError(); - -    if (error != GLOD_NO_ERROR) -    { -        LL_WARNS() << "GLOD error detected, cannot generate LOD: " << std::hex << error << LL_ENDL; -        return TRUE; -    } - -    return FALSE; -} -  LLViewerFetchedTexture* bindMaterialDiffuseTexture(const LLImportMaterial& material)  {      LLViewerFetchedTexture *texture = LLViewerTextureManager::getFetchedTexture(material.getDiffuseMap(), FTT_DEFAULT, TRUE, LLGLTexture::BOOST_PREVIEW); @@ -200,10 +187,6 @@ LLModelPreview::LLModelPreview(S32 width, S32 height, LLFloater* fmp)      mLoadState = LLModelLoader::STARTING;      mGroup = 0;      mLODFrozen = false; -    mBuildShareTolerance = 0.f; -    mBuildQueueMode = GLOD_QUEUE_GREEDY; -    mBuildBorderMode = GLOD_BORDER_UNLOCK; -    mBuildOperator = GLOD_OPERATOR_EDGE_COLLAPSE;      for (U32 i = 0; i < LLModel::NUM_LODS; ++i)      { @@ -211,10 +194,6 @@ LLModelPreview::LLModelPreview(S32 width, S32 height, LLFloater* fmp)          mRequestedCreaseAngle[i] = -1.f;          mRequestedLoDMode[i] = 0;          mRequestedErrorThreshold[i] = 0.f; -        mRequestedBuildOperator[i] = 0; -        mRequestedQueueMode[i] = 0; -        mRequestedBorderMode[i] = 0; -        mRequestedShareTolerance[i] = 0.f;      }      mViewOption["show_textures"] = false; @@ -224,23 +203,11 @@ LLModelPreview::LLModelPreview(S32 width, S32 height, LLFloater* fmp)      mHasPivot = false;      mModelPivot = LLVector3(0.0f, 0.0f, 0.0f); -    glodInit(); -      createPreviewAvatar();  }  LLModelPreview::~LLModelPreview()  { -    // glod apparently has internal mem alignment issues that are angering -    // the heap-check code in windows, these should be hunted down in that -    // TP code, if possible -    // -    // kernel32.dll!HeapFree()  + 0x14 bytes	 -    // msvcr100.dll!free(void * pBlock)  Line 51	C -    // glod.dll!glodGetGroupParameteriv()  + 0x119 bytes	 -    // glod.dll!glodShutdown()  + 0x77 bytes	 -    // -    //glodShutdown();      if (mModelLoader)      {          mModelLoader->shutdown(); @@ -527,7 +494,7 @@ void LLModelPreview::rebuildUploadData()                  bool upload_skinweights = fmp && fmp->childGetValue("upload_skin").asBoolean();                  if (upload_skinweights && high_lod_model->mSkinInfo.mJointNames.size() > 0)                  { -                    LLQuaternion bind_rot = LLSkinningUtil::getUnscaledQuaternion(high_lod_model->mSkinInfo.mBindShapeMatrix); +                    LLQuaternion bind_rot = LLSkinningUtil::getUnscaledQuaternion(LLMatrix4(high_lod_model->mSkinInfo.mBindShapeMatrix));                      LLQuaternion identity;                      if (!bind_rot.isEqualEps(identity, 0.01))                      { @@ -762,11 +729,6 @@ void LLModelPreview::loadModel(std::string filename, S32 lod, bool force_disable      mLODFile[lod] = filename; -    if (lod == LLModel::LOD_HIGH) -    { -        clearGLODGroup(); -    } -      std::map<std::string, std::string> joint_alias_map;      getJointAliases(joint_alias_map); @@ -867,7 +829,6 @@ void LLModelPreview::clearIncompatible(S32 lod)                  if (i == LLModel::LOD_HIGH)                  {                      mBaseModel = mModel[lod]; -                    clearGLODGroup();                      mBaseScene = mScene[lod];                      mVertexBuffer[5].clear();                  } @@ -876,23 +837,6 @@ void LLModelPreview::clearIncompatible(S32 lod)      }  } -void LLModelPreview::clearGLODGroup() -{ -    if (mGroup) -    { -        for (std::map<LLPointer<LLModel>, U32>::iterator iter = mObject.begin(); iter != mObject.end(); ++iter) -        { -            glodDeleteObject(iter->second); -            stop_gloderror(); -        } -        mObject.clear(); - -        glodDeleteGroup(mGroup); -        stop_gloderror(); -        mGroup = 0; -    } -} -  void LLModelPreview::loadModelCallback(S32 loaded_lod)  {      assert_main_thread(); @@ -1044,7 +988,6 @@ void LLModelPreview::loadModelCallback(S32 loaded_lod)              }              mBaseModel = mModel[loaded_lod]; -            clearGLODGroup();              mBaseScene = mScene[loaded_lod];              mVertexBuffer[5].clear(); @@ -1275,240 +1218,483 @@ void LLModelPreview::restoreNormals()      updateStatusMessages();  } -void LLModelPreview::genLODs(S32 which_lod, U32 decimation, bool enforce_tri_limit) +// 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, bool sloppy)  { -    // Allow LoD from -1 to LLModel::LOD_PHYSICS -    if (which_lod < -1 || which_lod > LLModel::NUM_LODS - 1) +    // Figure out buffer size +    S32 size_indices = 0; +    S32 size_vertices = 0; + +    for (U32 face_idx = 0; face_idx < base_model->getNumVolumeFaces(); ++face_idx)      { -        std::ostringstream out; -        out << "Invalid level of detail: " << which_lod; -        LL_WARNS() << out.str() << LL_ENDL; -        LLFloaterModelPreview::addStringToLog(out, false); -        assert(which_lod >= -1 && which_lod < LLModel::NUM_LODS); -        return; +        const LLVolumeFace &face = base_model->getVolumeFace(face_idx); +        size_indices += face.mNumIndices; +        size_vertices += face.mNumVertices;      } -    if (mBaseModel.empty()) +    if (size_indices < 3)      { -        return; +        return -1;      } -    LLVertexBuffer::unbind(); +    // 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)); -    bool no_ff = LLGLSLShader::sNoFixedFunction; -    LLGLSLShader* shader = LLGLSLShader::sCurBoundShaderPtr; -    LLGLSLShader::sNoFixedFunction = false; +    // 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) * 2 * size_vertices + tc_bytes_size); +    LLVector4a* combined_normals = combined_positions + size_vertices; +    LLVector2* combined_tex_coords = (LLVector2*)(combined_normals + size_vertices); -    if (shader) +    // copy indices and vertices into new buffers +    S32 combined_positions_shift = 0; +    S32 indices_idx_shift = 0; +    S32 combined_indices_shift = 0; +    for (U32 face_idx = 0; face_idx < base_model->getNumVolumeFaces(); ++face_idx)      { -        shader->unbind(); -    } +        const LLVolumeFace &face = base_model->getVolumeFace(face_idx); -    stop_gloderror(); -    static U32 cur_name = 1; +        // vertices +        S32 copy_bytes = face.mNumVertices * sizeof(LLVector4a); +        LLVector4a::memcpyNonAliased16((F32*)(combined_positions + combined_positions_shift), (F32*)face.mPositions, copy_bytes); -    S32 limit = -1; +        // normals +        LLVector4a::memcpyNonAliased16((F32*)(combined_normals + combined_positions_shift), (F32*)face.mNormals, copy_bytes); -    U32 triangle_count = 0; +        // tex coords +        copy_bytes = face.mNumVertices * sizeof(LLVector2); +        memcpy((void*)(combined_tex_coords + combined_positions_shift), (void*)face.mTexCoords, copy_bytes); -    U32 instanced_triangle_count = 0; +        combined_positions_shift += face.mNumVertices; -    //get the triangle count for the whole scene -    for (LLModelLoader::scene::iterator iter = mBaseScene.begin(), endIter = mBaseScene.end(); iter != endIter; ++iter) -    { -        for (LLModelLoader::model_instance_list::iterator instance = iter->second.begin(), end_instance = iter->second.end(); instance != end_instance; ++instance) +        // indices, sadly can't do dumb memcpy for indices, need to adjust each value +        for (S32 i = 0; i < face.mNumIndices; ++i)          { -            LLModel* mdl = instance->mModel; -            if (mdl) -            { -                instanced_triangle_count += mdl->getNumTriangles(); -            } +            U16 idx = face.mIndices[i]; + +            combined_indices[combined_indices_shift] = idx + indices_idx_shift; +            combined_indices_shift++;          } +        indices_idx_shift += face.mNumVertices;      } -    //get the triangle count for the non-instanced set of models -    for (U32 i = 0; i < mBaseModel.size(); ++i) +    // Now that we have buffers, optimize +    S32 target_indices = 0; +    F32 result_error = 0; // how far from original the model is, 1 == 100% +    S32 new_indices = 0; + +    if (indices_decimator > 0)      { -        triangle_count += mBaseModel[i]->getNumTriangles(); +        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; +    } +    new_indices = LLMeshOptimizer::simplifyU32( +        output_indices, +        combined_indices, +        size_indices, +        combined_positions, +        size_vertices, +        LLVertexBuffer::sTypeSize[LLVertexBuffer::TYPE_VERTEX], +        target_indices, +        error_threshold, +        sloppy, +        &result_error); -    //get ratio of uninstanced triangles to instanced triangles -    F32 triangle_ratio = (F32)triangle_count / (F32)instanced_triangle_count; -    U32 base_triangle_count = triangle_count; +    if (result_error < 0) +    { +        LL_WARNS() << "Negative result error from meshoptimizer for model " << target_model->mLabel +            << " target Indices: " << target_indices +            << " new Indices: " << new_indices +            << " original count: " << size_indices << LL_ENDL; +    } -    U32 type_mask = LLVertexBuffer::MAP_VERTEX | LLVertexBuffer::MAP_NORMAL | LLVertexBuffer::MAP_TEXCOORD0; +    if (new_indices < 3) +    { +        // Model should have at least one visible triangle +        ll_aligned_free<64>(combined_positions); +        ll_aligned_free_32(output_indices); +        ll_aligned_free_32(combined_indices); -    U32 lod_mode = 0; +        return -1; +    } -    F32 lod_error_threshold = 0; +    // repack back into individual faces -    // The LoD should be in range from Lowest to High -    if (which_lod > -1 && which_lod < NUM_LOD) +    LLVector4a* buffer_positions = (LLVector4a*)ll_aligned_malloc<64>(sizeof(LLVector4a) * 2 * 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 (U32 face_idx = 0; face_idx < base_model->getNumVolumeFaces(); ++face_idx)      { -        LLCtrlSelectionInterface* iface = mFMP->childGetSelectionInterface("lod_mode_" + lod_name[which_lod]); -        if (iface) +        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++)          { -            lod_mode = iface->getFirstSelectedIndex(); +            old_to_new_positions_map[i] = -1;          } -        lod_error_threshold = mFMP->childGetValue("lod_error_threshold_" + lod_name[which_lod]).asReal(); +        // Copy relevant indices and vertices +        for (S32 i = 0; i < new_indices; ++i) +        { +            U32 idx = 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: " << new_indices +                            << " original count: " << size_indices +                            << " error treshold: " << error_threshold +                            << LL_ENDL; + +                        // U16 vertices overflow shouldn't happen, but just in case +                        new_indices = 0; +                        valid_faces = 0; +                        for (U32 face_idx = 0; face_idx < base_model->getNumVolumeFaces(); ++face_idx) +                        { +                            genMeshOptimizerPerFace(base_model, target_model, face_idx, indices_decimator, error_threshold, false); +                            const LLVolumeFace &face = target_model->getVolumeFace(face_idx); +                            new_indices += face.mNumIndices; +                            if (face.mNumIndices >= 3) +                            { +                                valid_faces++; +                            } +                        } +                        if (valid_faces) +                        { +                            return (F32)size_indices / (F32)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); + +            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;      } -    if (which_lod != -1) +    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); +    ll_aligned_free_32(combined_indices); + +    if (new_indices < 3 || valid_faces == 0)      { -        mRequestedLoDMode[which_lod] = lod_mode; +        // Model should have at least one visible triangle +        return -1;      } -    if (lod_mode == 0) +    return (F32)size_indices / (F32)new_indices; +} + +F32 LLModelPreview::genMeshOptimizerPerFace(LLModel *base_model, LLModel *target_model, U32 face_idx, F32 indices_decimator, F32 error_threshold, bool sloppy) +{ +    const LLVolumeFace &face = base_model->getVolumeFace(face_idx); +    S32 size_indices = face.mNumIndices; +    if (size_indices < 3)      { -        lod_mode = GLOD_TRIANGLE_BUDGET; +        return -1; +    } +    // todo: do not allocate per each face, add one large buffer somewhere +    // faces have limited amount of indices +    S32 size = (size_indices * sizeof(U16) + 0xF) & ~0xF; +    U16* output = (U16*)ll_aligned_malloc_16(size); -        // The LoD should be in range from Lowest to High -        if (which_lod > -1 && which_lod < NUM_LOD) +    S32 target_indices = 0; +    F32 result_error = 0; // how far from original the model is, 1 == 100% +    S32 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; +    } +    new_indices = LLMeshOptimizer::simplify( +        output, +        face.mIndices, +        size_indices, +        face.mPositions, +        face.mNumVertices, +        LLVertexBuffer::sTypeSize[LLVertexBuffer::TYPE_VERTEX], +        target_indices, +        error_threshold, +        sloppy, +        &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: " << 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 (new_indices < 3) +    { +        if (!sloppy)          { -            limit = mFMP->childGetValue("lod_triangle_limit_" + lod_name[which_lod]).asInteger(); -            //convert from "scene wide" to "non-instanced" triangle limit -            limit = (S32)((F32)limit*triangle_ratio); +            // 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      { -        lod_mode = GLOD_ERROR_THRESHOLD; -    } +        // Assign new values +        new_face.resizeIndices(new_indices); // will wipe out mIndices, so new_face can't substitute output +        S32 idx_size = (new_indices * sizeof(U16) + 0xF) & ~0xF; +        LLVector4a::memcpyNonAliased16((F32*)new_face.mIndices, (F32*)output, idx_size); -    bool object_dirty = false; +        // clear unused values +        new_face.optimize(); +    } -    if (mGroup == 0) +    ll_aligned_free_16(output); +      +    if (new_indices < 3)      { -        object_dirty = true; -        mGroup = cur_name++; -        glodNewGroup(mGroup); +        // At least one triangle is needed +        return -1;      } -    if (object_dirty) +    return (F32)size_indices / (F32)new_indices; +} + +void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 decimation, bool enforce_tri_limit) +{ +    LL_INFOS() << "Generating lod " << which_lod << " using meshoptimizer" << LL_ENDL; +    // Allow LoD from -1 to LLModel::LOD_PHYSICS +    if (which_lod < -1 || which_lod > LLModel::NUM_LODS - 1)      { -        for (LLModelLoader::model_list::iterator iter = mBaseModel.begin(); iter != mBaseModel.end(); ++iter) -        { //build GLOD objects for each model in base model list -            LLModel* mdl = *iter; +        std::ostringstream out; +        out << "Invalid level of detail: " << which_lod; +        LL_WARNS() << out.str() << LL_ENDL; +        LLFloaterModelPreview::addStringToLog(out, false); +        assert(lod >= -1 && lod < LLModel::NUM_LODS); +        return; +    } -            if (mObject[mdl] != 0) -            { -                glodDeleteObject(mObject[mdl]); -            } +    if (mBaseModel.empty()) +    { +        return; +    } -            mObject[mdl] = cur_name++; +    //get the triangle count for all base models +    S32 base_triangle_count = 0; +    for (S32 i = 0; i < mBaseModel.size(); ++i) +    { +        base_triangle_count += mBaseModel[i]->getNumTriangles(); +    } -            glodNewObject(mObject[mdl], mGroup, GLOD_DISCRETE); -            stop_gloderror(); +    // Urgh... +    // TODO: add interface to mFMP to get error treshold or let mFMP write one into LLModelPreview +    // We should not be accesing views from other class! +    U32 lod_mode = LIMIT_TRIANGLES; +    F32 indices_decimator = 0; +    F32 triangle_limit = 0; +    F32 lod_error_threshold = 1; //100% -            if (iter == mBaseModel.begin() && !mdl->mSkinWeights.empty()) -            { //regenerate vertex buffer for skinned models to prevent animation feedback during LOD generation -                mVertexBuffer[5].clear(); -            } +    // If requesting a single lod +    if (which_lod > -1 && which_lod < NUM_LOD) +    { +        LLCtrlSelectionInterface* iface = mFMP->childGetSelectionInterface("lod_mode_" + lod_name[which_lod]); +        if (iface) +        { +            lod_mode = iface->getFirstSelectedIndex(); +        } -            if (mVertexBuffer[5].empty()) +        if (lod_mode == LIMIT_TRIANGLES) +        { +            if (!enforce_tri_limit)              { -                genBuffers(5, false); -            } +                triangle_limit = base_triangle_count; +                // reset to default value for this lod +                F32 pw = pow((F32)decimation, (F32)(LLModel::LOD_HIGH - which_lod)); -            U32 tri_count = 0; -            for (U32 i = 0; i < mVertexBuffer[5][mdl].size(); ++i) +                triangle_limit /= pw; //indices_ratio can be 1/pw +            } +            else              { -                LLVertexBuffer* buff = mVertexBuffer[5][mdl][i]; -                buff->setBuffer(type_mask & buff->getTypeMask()); -                U32 num_indices = mVertexBuffer[5][mdl][i]->getNumIndices(); -                if (num_indices > 2) -                { -                    glodInsertElements(mObject[mdl], i, GL_TRIANGLES, num_indices, GL_UNSIGNED_SHORT, (U8*)mVertexBuffer[5][mdl][i]->getIndicesPointer(), 0, 0.f); -                } -                tri_count += num_indices / 3; -                stop_gloderror(); -            } +                // UI spacifies limit for all models of single lod +                triangle_limit = mFMP->childGetValue("lod_triangle_limit_" + lod_name[which_lod]).asInteger(); -            glodBuildObject(mObject[mdl]); -            stop_gloderror(); +            } +            // meshoptimizer doesn't use triangle limit, it uses indices limit, so convert it to aproximate ratio +            // triangle_limit can be 0. +            indices_decimator = (F32)base_triangle_count / llmax(triangle_limit, 1.f);          } +        else +        { +            // UI shows 0 to 100%, but meshoptimizer works with 0 to 1 +            lod_error_threshold = mFMP->childGetValue("lod_error_threshold_" + lod_name[which_lod]).asReal() / 100.f; +        } +    } +    else +    { +        // we are genrating all lods and each lod will get own indices_decimator +        indices_decimator = 1; +        triangle_limit = base_triangle_count;      } +    mMaxTriangleLimit = base_triangle_count; + +    // Build models      S32 start = LLModel::LOD_HIGH;      S32 end = 0;      if (which_lod != -1)      { -        start = end = which_lod; +        start = which_lod; +        end = which_lod;      } -    mMaxTriangleLimit = base_triangle_count; -      for (S32 lod = start; lod >= end; --lod)      {          if (which_lod == -1)          { +            // we are genrating all lods and each lod gets own indices_ratio              if (lod < start)              { -                triangle_count /= decimation; -            } -        } -        else -        { -            if (enforce_tri_limit) -            { -                triangle_count = limit; -            } -            else -            { -                for (S32 j = LLModel::LOD_HIGH; j>which_lod; --j) -                { -                    triangle_count /= decimation; -                } +                indices_decimator *= decimation; +                triangle_limit /= decimation;              }          } +        mRequestedTriangleCount[lod] = triangle_limit; +        mRequestedErrorThreshold[lod] = lod_error_threshold * 100; +        mRequestedLoDMode[lod] = lod_mode; +          mModel[lod].clear();          mModel[lod].resize(mBaseModel.size());          mVertexBuffer[lod].clear(); -        U32 actual_tris = 0; -        U32 actual_verts = 0; -        U32 submeshes = 0; - -        mRequestedTriangleCount[lod] = (S32)((F32)triangle_count / triangle_ratio); -        mRequestedErrorThreshold[lod] = lod_error_threshold; - -        glodGroupParameteri(mGroup, GLOD_ADAPT_MODE, lod_mode); -        stop_gloderror(); - -        glodGroupParameteri(mGroup, GLOD_ERROR_MODE, GLOD_OBJECT_SPACE_ERROR); -        stop_gloderror(); - -        glodGroupParameterf(mGroup, GLOD_OBJECT_SPACE_ERROR_THRESHOLD, lod_error_threshold); -        stop_gloderror(); - -        if (lod_mode != GLOD_TRIANGLE_BUDGET) -        { -            glodGroupParameteri(mGroup, GLOD_MAX_TRIANGLES, 0); -        } -        else -        { -            //SH-632: always add 1 to desired amount to avoid decimating below desired amount -            glodGroupParameteri(mGroup, GLOD_MAX_TRIANGLES, triangle_count + 1); -        } - -        stop_gloderror(); -        glodAdaptGroup(mGroup); -        stop_gloderror();          for (U32 mdl_idx = 0; mdl_idx < mBaseModel.size(); ++mdl_idx)          {              LLModel* base = mBaseModel[mdl_idx]; -            GLint patch_count = 0; -            glodGetObjectParameteriv(mObject[base], GLOD_NUM_PATCHES, &patch_count); -            stop_gloderror(); -              LLVolumeParams volume_params;              volume_params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE);              mModel[lod][mdl_idx] = new LLModel(volume_params, 0.f); @@ -1517,74 +1703,159 @@ void LLModelPreview::genLODs(S32 which_lod, U32 decimation, bool enforce_tri_lim              mModel[lod][mdl_idx]->mLabel = name;              mModel[lod][mdl_idx]->mSubmodelID = base->mSubmodelID; - -            GLint* sizes = new GLint[patch_count * 2]; -            glodGetObjectParameteriv(mObject[base], GLOD_PATCH_SIZES, sizes); -            stop_gloderror(); - -            GLint* names = new GLint[patch_count]; -            glodGetObjectParameteriv(mObject[base], GLOD_PATCH_NAMES, names); -            stop_gloderror(); - -            mModel[lod][mdl_idx]->setNumVolumeFaces(patch_count); +            mModel[lod][mdl_idx]->setNumVolumeFaces(base->getNumVolumeFaces());              LLModel* target_model = mModel[lod][mdl_idx]; -            for (GLint i = 0; i < patch_count; ++i) +            S32 model_meshopt_mode = meshopt_mode; + +            // Ideally this should run not per model, +            // but combine all submodels with origin model as well +            if (model_meshopt_mode == MESH_OPTIMIZER_COMBINE)              { -                type_mask = mVertexBuffer[5][base][i]->getTypeMask(); +                // Run meshoptimizer for each model/object, up to 8 faces in one model. -                LLPointer<LLVertexBuffer> buff = new LLVertexBuffer(type_mask, 0); +                // Ideally this should run not per model, +                // but combine all submodels with origin model as well +                F32 res = genMeshOptimizerPerModel(base, target_model, indices_decimator, lod_error_threshold, false); +                if (res < 0) +                { +                    target_model->copyVolumeFaces(base); +                } +            } -                if (sizes[i * 2 + 1] > 0 && sizes[i * 2] > 0) +            if (model_meshopt_mode == MESH_OPTIMIZER_SLOPPY) +            { +                // Run meshoptimizer for each face +                for (U32 face_idx = 0; face_idx < base->getNumVolumeFaces(); ++face_idx)                  { -                    if (!buff->allocateBuffer(sizes[i * 2 + 1], sizes[i * 2], true)) +                    if (genMeshOptimizerPerFace(base, target_model, face_idx, indices_decimator, lod_error_threshold, true) < 0)                      { -                        // Todo: find a way to stop preview in this case instead of crashing -                        LL_ERRS() << "Failed buffer allocation during preview LOD generation." -                            << " Vertices: " << sizes[i * 2 + 1] -                            << " Indices: " << sizes[i * 2] << LL_ENDL; +                        // Sloppy failed and returned an invalid model +                        genMeshOptimizerPerFace(base, target_model, face_idx, indices_decimator, lod_error_threshold, false);                      } -                    buff->setBuffer(type_mask); -                    glodFillElements(mObject[base], names[i], GL_UNSIGNED_SHORT, (U8*)buff->getIndicesPointer()); -                    stop_gloderror();                  } -                else +            } + +            if (model_meshopt_mode == MESH_OPTIMIZER_AUTO) +            { +                // Switches between 'combine' method and 'sloppy' based on combine's result. +                F32 allowed_ratio_drift = 2.f; +                F32 precise_ratio = genMeshOptimizerPerModel(base, target_model, indices_decimator, lod_error_threshold, false); +                 +                if (precise_ratio < 0 || (precise_ratio * allowed_ratio_drift < indices_decimator))                  { -                    // This face was eliminated or we failed to allocate buffer, -                    // attempt to create a dummy triangle (one vertex, 3 indices, all 0) -                    buff->allocateBuffer(1, 3, true); -                    memset((U8*)buff->getMappedData(), 0, buff->getSize()); -                    memset((U8*)buff->getIndicesPointer(), 0, buff->getIndicesSize()); -                } +                    // Try sloppy variant if normal one failed to simplify model enough. +                    // Sloppy variant can fail entirely and has issues with precision, +                    // so code needs to do multiple attempts with different decimators. +                    // Todo: this is a bit of a mess, needs to be refined and improved +                    F32 last_working_decimator = 0.f; +                    F32 last_working_ratio = F32_MAX; -                buff->validateRange(0, buff->getNumVerts() - 1, buff->getNumIndices(), 0); +                    F32 sloppy_ratio = genMeshOptimizerPerModel(base, target_model, indices_decimator, lod_error_threshold, true); -                LLStrider<LLVector3> pos; -                LLStrider<LLVector3> norm; -                LLStrider<LLVector2> tc; -                LLStrider<U16> index; +                    if (sloppy_ratio > 0) +                    { +                        // Would be better to do a copy of target_model here, but if +                        // we need to use sloppy decimation, model should be cheap +                        // and fast to generate and it won't affect end result +                        last_working_decimator = indices_decimator; +                        last_working_ratio = sloppy_ratio; +                    } -                buff->getVertexStrider(pos); -                if (type_mask & LLVertexBuffer::MAP_NORMAL) -                { -                    buff->getNormalStrider(norm); -                } -                if (type_mask & LLVertexBuffer::MAP_TEXCOORD0) -                { -                    buff->getTexCoord0Strider(tc); -                } +                    // Sloppy has a tendecy to error into lower side, so a request for 100 +                    // triangles turns into ~70, so check for significant difference from target decimation +                    F32 sloppy_ratio_drift = 1.4f; +                    if (lod_mode == LIMIT_TRIANGLES +                        && (sloppy_ratio > indices_decimator * sloppy_ratio_drift || sloppy_ratio < 0)) +                    { +                        // Apply a correction to compensate. + +                        // (indices_decimator / res_ratio) by itself is likely to overshoot to a differend +                        // 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, true); +                    } + +                    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, true); +                    } + +                    if (sloppy_ratio < 0) +                    { +                        // Sloppy method didn't work, try with smaller decimation values +                        S32 size_vertices = 0; + +                        for (U32 face_idx = 0; face_idx < base->getNumVolumeFaces(); ++face_idx) +                        { +                            const LLVolumeFace &face = base->getVolumeFace(face_idx); +                            size_vertices += face.mNumVertices; +                        } + +                        // Complex models aren't supposed to get here, they are supposed +                        // to work on a first try of sloppy due to having more viggle room. +                        // If they didn't, something is likely wrong, no point locking the +                        // thread in a long calculation that will fail. +                        const U32 too_many_vertices = 27000; +                        if (size_vertices > too_many_vertices) +                        { +                            LL_WARNS() << "Sloppy optimization method failed for a complex model " << target_model->getName() << LL_ENDL; +                        } +                        else +                        { +                            // Find a decimator that does work +                            F32 sloppy_decimation_step = sqrt((F32)decimation); // example: 27->15->9->5->3 +                            F32 sloppy_decimator = indices_decimator / sloppy_decimation_step; -                buff->getIndexStrider(index); +                            while (sloppy_ratio < 0 +                                && sloppy_decimator > precise_ratio +                                && sloppy_decimator > 1)// precise_ratio isn't supposed to be below 1, but check just in case +                            { +                                sloppy_ratio = genMeshOptimizerPerModel(base, target_model, sloppy_decimator, lod_error_threshold, true); +                                sloppy_decimator = sloppy_decimator / sloppy_decimation_step; +                            } +                        } +                    } -                target_model->setVolumeFaceData(names[i], pos, norm, tc, index, buff->getNumVerts(), buff->getNumIndices()); -                actual_tris += buff->getNumIndices() / 3; -                actual_verts += buff->getNumVerts(); -                ++submeshes; +                    if (sloppy_ratio < 0 || sloppy_ratio < precise_ratio) +                    { +                        // Sloppy variant failed to generate triangles or is worse. +                        // Can happen with models that are too simple as is. -                if (!validate_face(target_model->getVolumeFace(names[i]))) +                        if (precise_ratio < 0) +                        { +                            // Precise method failed as well, just copy face over +                            target_model->copyVolumeFaces(base); +                            precise_ratio = 1.f; +                        } +                        else +                        { +                            // Fallback to normal method +                            precise_ratio = genMeshOptimizerPerModel(base, target_model, indices_decimator, lod_error_threshold, false); +                        } + +                        LL_INFOS() << "Model " << target_model->getName() +                            << " lod " << which_lod +                            << " resulting ratio " << precise_ratio +                            << " simplified using per model method." << LL_ENDL; +                    } +                    else +                    { +                        LL_INFOS() << "Model " << target_model->getName() +                            << " lod " << which_lod +                            << " resulting ratio " << sloppy_ratio +                            << " sloppily simplified using per model method." << LL_ENDL; +                    } +                } +                else                  { -                    LL_ERRS() << "Invalid face generated during LOD generation." << LL_ENDL; +                    LL_INFOS() << "Model " << target_model->getName() +                        << " lod " << which_lod +                        << " resulting ratio " << precise_ratio +                        << " simplified using per model method." << LL_ENDL;                  }              } @@ -1594,6 +1865,7 @@ void LLModelPreview::genLODs(S32 which_lod, U32 decimation, bool enforce_tri_lim              target_model->mPosition = base->mPosition;              target_model->mSkinWeights = base->mSkinWeights;              target_model->mSkinInfo = base->mSkinInfo; +              //copy material list              target_model->mMaterialList = base->mMaterialList; @@ -1601,9 +1873,6 @@ void LLModelPreview::genLODs(S32 which_lod, U32 decimation, bool enforce_tri_lim              {                  LL_ERRS() << "Invalid model generated when creating LODs" << LL_ENDL;              } - -            delete[] sizes; -            delete[] names;          }          //rebuild scene based on mBaseScene @@ -1629,13 +1898,6 @@ void LLModelPreview::genLODs(S32 which_lod, U32 decimation, bool enforce_tri_lim              }          }      } - -    LLVertexBuffer::unbind(); -    LLGLSLShader::sNoFixedFunction = no_ff; -    if (shader) -    { -        shader->bind(); -    }  }  void LLModelPreview::updateStatusMessages() @@ -2198,7 +2460,7 @@ void LLModelPreview::updateLodControls(S32 lod)      S32 lod_mode = lod_combo->getCurrentIndex();      if (lod_mode == LOD_FROM_FILE) // LoD from file      { -        fmp->mLODMode[lod] = 0; +        fmp->mLODMode[lod] = LOD_FROM_FILE;          for (U32 i = 0; i < num_file_controls; ++i)          {              mFMP->childSetVisible(file_controls[i] + lod_name[lod], true); @@ -2211,7 +2473,7 @@ void LLModelPreview::updateLodControls(S32 lod)      }      else if (lod_mode == USE_LOD_ABOVE) // use LoD above      { -        fmp->mLODMode[lod] = 2; +        fmp->mLODMode[lod] = USE_LOD_ABOVE;          for (U32 i = 0; i < num_file_controls; ++i)          {              mFMP->childSetVisible(file_controls[i] + lod_name[lod], false); @@ -2237,7 +2499,7 @@ void LLModelPreview::updateLodControls(S32 lod)      }      else // auto generate, the default case for all LoDs except High      { -        fmp->mLODMode[lod] = 1; +        fmp->mLODMode[lod] = MESH_OPTIMIZER_AUTO;          //don't actually regenerate lod when refreshing UI          mLODFrozen = true; @@ -2269,7 +2531,7 @@ void LLModelPreview::updateLodControls(S32 lod)              threshold->setVisible(false);              limit->setMaxValue(mMaxTriangleLimit); -            limit->setIncrement(mMaxTriangleLimit / 32); +            limit->setIncrement(llmax((U32)1, mMaxTriangleLimit / 32));          }          else          { @@ -2433,6 +2695,8 @@ void LLModelPreview::genBuffers(S32 lod, bool include_skin_weights)                  *(index_strider++) = vf.mIndices[i];              } +            vb->flush(); +              mVertexBuffer[lod][mdl].push_back(vb);              vertex_count += num_vertices; @@ -2659,8 +2923,6 @@ BOOL LLModelPreview::render()      LLMutexLock lock(this);      mNeedsUpdate = FALSE; -    bool use_shaders = LLGLSLShader::sNoFixedFunction; -      bool edges = mViewOption["show_edges"];      bool joint_overrides = mViewOption["show_joint_overrides"];      bool joint_positions = mViewOption["show_joint_positions"]; @@ -2678,10 +2940,8 @@ BOOL LLModelPreview::render()      LLGLDisable fog(GL_FOG);      { -        if (use_shaders) -        { -            gUIProgram.bind(); -        } +        gUIProgram.bind(); +          //clear background to grey          gGL.matrixMode(LLRender::MM_PROJECTION);          gGL.pushMatrix(); @@ -2700,10 +2960,7 @@ BOOL LLModelPreview::render()          gGL.matrixMode(LLRender::MM_MODELVIEW);          gGL.popMatrix(); -        if (use_shaders) -        { -            gUIProgram.unbind(); -        } +        gUIProgram.unbind();      }      LLFloaterModelPreview* fmp = LLFloaterModelPreview::sInstance; @@ -2854,10 +3111,7 @@ BOOL LLModelPreview::render()          refresh();      } -    if (use_shaders) -    { -        gObjectPreviewProgram.bind(); -    } +    gObjectPreviewProgram.bind();      gGL.loadIdentity();      gPipeline.enableLightsPreview(); @@ -2890,7 +3144,6 @@ BOOL LLModelPreview::render()      {          genBuffers(-1, skin_weight);          //genBuffers(3); -        //genLODs();      }      if (!mModel[mPreviewLOD].empty()) @@ -2918,6 +3171,11 @@ BOOL LLModelPreview::render()              genBuffers(mPreviewLOD, skin_weight);          } +        if (physics && mVertexBuffer[LLModel::LOD_PHYSICS].empty()) +        { +            genBuffers(LLModel::LOD_PHYSICS, false); +        } +          if (!skin_weight)          {              for (LLMeshUploadThread::instance_list::iterator iter = mUploadData.begin(); iter != mUploadData.end(); ++iter) @@ -2979,6 +3237,7 @@ BOOL LLModelPreview::render()                          glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);                          glLineWidth(1.f);                      } +                    buffer->flush();                  }                  gGL.popMatrix();              } @@ -3039,6 +3298,14 @@ BOOL LLModelPreview::render()                                  if (!physics.mMesh.empty())                                  { //render hull instead of mesh +                                    // SL-16993 physics.mMesh[i].mNormals were being used to light the exploded +                                    // analyzed physics shape but the drawArrays() interface changed +                                    //  causing normal data <0,0,0> to be passed to the shader. +                                    // The Phyics Preview shader uses plain vertex coloring so the physics hull is full lit. +                                    // We could also use interface/ui shaders. +                                    gObjectPreviewProgram.unbind(); +                                    gPhysicsPreviewProgram.bind(); +                                      for (U32 i = 0; i < physics.mMesh.size(); ++i)                                      {                                          if (explode > 0.f) @@ -3059,24 +3326,22 @@ BOOL LLModelPreview::render()                                          }                                          gGL.diffuseColor4ubv(hull_colors[i].mV); -                                        LLVertexBuffer::drawArrays(LLRender::TRIANGLES, physics.mMesh[i].mPositions, physics.mMesh[i].mNormals); +                                        LLVertexBuffer::drawArrays(LLRender::TRIANGLES, physics.mMesh[i].mPositions);                                          if (explode > 0.f)                                          {                                              gGL.popMatrix();                                          }                                      } + +                                    gPhysicsPreviewProgram.unbind(); +                                    gObjectPreviewProgram.bind();                                  }                              }                          }                          if (render_mesh)                          { -                            if (mVertexBuffer[LLModel::LOD_PHYSICS].empty()) -                            { -                                genBuffers(LLModel::LOD_PHYSICS, false); -                            } -                              U32 num_models = mVertexBuffer[LLModel::LOD_PHYSICS][model].size();                              if (pass > 0){                                  for (U32 i = 0; i < num_models; ++i) @@ -3096,6 +3361,8 @@ BOOL LLModelPreview::render()                                      glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);                                      glLineWidth(1.f); + +                                    buffer->flush();                                  }                              }                          } @@ -3140,11 +3407,6 @@ BOOL LLModelPreview::render()                                  if (physics.mHull.empty())                                  { -                                    if (mVertexBuffer[LLModel::LOD_PHYSICS].empty()) -                                    { -                                        genBuffers(LLModel::LOD_PHYSICS, false); -                                    } -                                      U32 num_models = mVertexBuffer[LLModel::LOD_PHYSICS][model].size();                                      for (U32 v = 0; v < num_models; ++v)                                      { @@ -3171,6 +3433,8 @@ BOOL LLModelPreview::render()                                                  buffer->draw(LLRender::POINTS, 3, i);                                              }                                          } + +                                        buffer->flush();                                      }                                  }                              } @@ -3231,7 +3495,7 @@ BOOL LLModelPreview::render()                                  LLJoint *joint = getPreviewAvatar()->getJoint(skin->mJointNums[j]);                                  if (joint)                                  { -                                    const LLVector3& jointPos = skin->mAlternateBindMatrix[j].getTranslation(); +                                    const LLVector3& jointPos = LLVector3(skin->mAlternateBindMatrix[j].getTranslation());                                      if (joint->aboveJointPosThreshold(jointPos))                                      {                                          bool override_changed; @@ -3273,11 +3537,10 @@ BOOL LLModelPreview::render()                              //build matrix palette                              LLMatrix4a mat[LL_MAX_JOINTS_PER_MESH_OBJECT]; -                            LLSkinningUtil::initSkinningMatrixPalette((LLMatrix4*)mat, joint_count, +                            LLSkinningUtil::initSkinningMatrixPalette(mat, joint_count,                                  skin, getPreviewAvatar()); -                            LLMatrix4a bind_shape_matrix; -                            bind_shape_matrix.loadu(skin->mBindShapeMatrix); +                            const LLMatrix4a& bind_shape_matrix = skin->mBindShapeMatrix;                              U32 max_joints = LLSkinningUtil::getMaxJointCount();                              for (U32 j = 0; j < buffer->getNumVerts(); ++j)                              { @@ -3360,10 +3623,7 @@ BOOL LLModelPreview::render()          }      } -    if (use_shaders) -    { -        gObjectPreviewProgram.unbind(); -    } +    gObjectPreviewProgram.unbind();      gGL.popMatrix(); @@ -3477,7 +3737,7 @@ bool LLModelPreview::lodQueryCallback()          {              S32 lod = preview->mLodsQuery.back();              preview->mLodsQuery.pop_back(); -            preview->genLODs(lod); +            preview->genMeshOptimizerLODs(lod, MESH_OPTIMIZER_AUTO);              if (preview->mLookUpLodFiles && (lod == LLModel::LOD_HIGH))              { @@ -3492,12 +3752,13 @@ bool LLModelPreview::lodQueryCallback()      return true;  } -void LLModelPreview::onLODParamCommit(S32 lod, bool enforce_tri_limit) +void LLModelPreview::onLODMeshOptimizerParamCommit(S32 requested_lod, bool enforce_tri_limit, S32 mode)  {      if (!mLODFrozen)      { -        genLODs(lod, 3, enforce_tri_limit); +        genMeshOptimizerLODs(requested_lod, mode, 3, enforce_tri_limit);          refresh(); +        mDirty = true;      }  } | 
