diff --git a/indra/llappearanceutility/llprocesstexture.cpp b/indra/llappearanceutility/llprocesstexture.cpp index c9dbbd542c..fad23b7920 100644 --- a/indra/llappearanceutility/llprocesstexture.cpp +++ b/indra/llappearanceutility/llprocesstexture.cpp @@ -251,7 +251,7 @@ void LLProcessTexture::process(std::ostream& output) avatar.updateVisualParams(); // Prepare gl for avatar baking - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + gGL.setClearColor(LLColor4::transparent); gGL.setSceneBlendType(LLRender::BT_ALPHA); EBakedTextureIndex bake_type = LLAvatarAppearance::getDictionary()->findBakedByImageName(mInputData["slot_id"].asString()); diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 6493d58a79..630317f637 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -59,10 +59,6 @@ #include -#define DEBUG_SILHOUETTE_BINORMALS 0 -#define DEBUG_SILHOUETTE_NORMALS 0 // TomY: Use this to display normals using the silhouette -#define DEBUG_SILHOUETTE_EDGE_MAP 0 // DaveP: Use this to display edge map using the silhouette - constexpr F32 MIN_CUT_DELTA = 0.02f; constexpr F32 HOLLOW_MIN = 0.f; @@ -3797,490 +3793,6 @@ S32 LLVolume::getNumTriangles(S32* vcount) const return triangle_count; } -void LLVolumeFace::generateSilhouetteEdge(const LLVolume* volume, std::vector& edge) const -{ - llassert(edge.empty()); // edge is supposed to be a scratch array - - if (volume->isMeshAssetLoaded()) { return; } - - if (mTypeMask & CAP_MASK) - { - // Logic copied from LLVolumeFace::createCap - indicates a face created via - // createUnCutCubeCap. - if (!(mTypeMask & HOLLOW_MASK) && - !(mTypeMask & OPEN_MASK) && - ((volume->getParams().getPathParams().getBegin()==0.0f)&& - (volume->getParams().getPathParams().getEnd()==1.0f))&& - (volume->getParams().getProfileParams().getCurveType()==LL_PCODE_PROFILE_SQUARE && - volume->getParams().getPathParams().getCurveType()==LL_PCODE_PATH_LINE) - ) - { - LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("llvfgse - CAP_MASK"); - - const LLAlignedArray& profile = volume->getProfile().mProfile; - S32 grid_size = (profile.size()-1)/4; - edge.resize(mNumIndices); - llassert(edge.size() == 6*grid_size*grid_size); - - S32 cur_edge = 0; - for(S32 gx = 0;gx 0) - { - edge[cur_edge++] = edge_value; - } - else - { - edge[cur_edge++] = -1; // Mark face to higlight it - } - - if (gy < grid_size - 1) - { - edge[cur_edge++] = edge_value; - } - else - { - edge[cur_edge++] = -1; - } - - edge[cur_edge++] = edge_value; - - if (gx < grid_size - 1) - { - edge[cur_edge++] = edge_value; - } - else - { - edge[cur_edge++] = -1; - } - - if (gy > 0) - { - edge[cur_edge++] = edge_value; - } - else - { - edge[cur_edge++] = -1; - } - - edge[cur_edge++] = edge_value; - } - else - { - S32 edge_value = grid_size * 2 * gy + gx * 2; - - if (gy > 0) - { - edge[cur_edge++] = edge_value; - } - else - { - edge[cur_edge++] = -1; - } - - if (gx < grid_size - 1) - { - edge[cur_edge++] = edge_value; - } - else - { - edge[cur_edge++] = -1; - } - - edge[cur_edge++] = edge_value; - - if (gy < grid_size - 1) - { - edge[cur_edge++] = edge_value; - } - else - { - edge[cur_edge++] = -1; - } - - if (gx > 0) - { - edge[cur_edge++] = edge_value; - } - else - { - edge[cur_edge++] = -1; - } - - edge[cur_edge++] = edge_value; - } - } - } - } - } - else if ((mTypeMask & END_MASK) || (mTypeMask & SIDE_MASK)) - { - LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("llvfgse - END_MASK or SIDE_MASK"); - - edge.resize(mNumIndices); - llassert(edge.size() == 6*(mNumS-1)*(mNumT-1)); - - S32 cur_edge = 0; - const bool flat_face = mTypeMask & FLAT_MASK; - for (S32 t = 0; t < (mNumT-1); t++) - { - for (S32 s = 0; s < (mNumS-1); s++) - { - // bottom left/top right neighbor face - edge[cur_edge++] = (mNumS-1)*2*t+s*2+1; - - if (t < mNumT-2) - { // top right/top left neighbor face - edge[cur_edge++] = (mNumS-1)*2*(t+1)+s*2+1; - } - else if (mNumT <= 3 || volume->getPath().isOpen()) - { // no neighbor - edge[cur_edge++] = -1; - } - else - { // wrap on T - edge[cur_edge++] = s*2+1; - } - - if (s > 0) - { // top left/bottom left neighbor face - edge[cur_edge++] = (mNumS-1)*2*t+s*2-1; - } - else if (flat_face || volume->getProfile().isOpen()) - { // no neighbor - edge[cur_edge++] = -1; - } - else - { // wrap on S - edge[cur_edge++] = (mNumS-1)*2*t+(mNumS-2)*2+1; - } - - if (t > 0) - { // bottom left/bottom right neighbor face - edge[cur_edge++] = (mNumS-1)*2*(t-1)+s*2; - } - else if (mNumT <= 3 || volume->getPath().isOpen()) - { // no neighbor - edge[cur_edge++] = -1; - } - else - { // wrap on T - edge[cur_edge++] = (mNumS-1)*2*(mNumT-2)+s*2; - } - - if (s < mNumS-2) - { // bottom right/top right neighbor face - edge[cur_edge++] = (mNumS-1)*2*t+(s+1)*2; - } - else if (flat_face || volume->getProfile().isOpen()) - { // no neighbor - edge[cur_edge++] = -1; - } - else - { // wrap on S - edge[cur_edge++] = (mNumS-1)*2*t; - } - - // top right/bottom left neighbor face - edge[cur_edge++] = (mNumS-1)*2*t+s*2; - } - } - } - else - { - LL_ERRS() << "Unknown/uninitialized face type!" << LL_ENDL; - } -} - -//----------------------------------------------------------------------------- -// generateSilhouetteVertices() -//----------------------------------------------------------------------------- -void LLVolume::generateSilhouetteVertices(std::vector &vertices, - std::vector &normals, - const LLVector3& obj_cam_vec_in, - const LLMatrix4& mat_in, - const LLMatrix3& norm_mat_in, - S32 face_mask) -{ - LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; - - LLMatrix4a mat; - mat.set(mat_in); - - LLMatrix4a norm_mat; - norm_mat.set(norm_mat_in); - - LLVector4a obj_cam_vec; - obj_cam_vec.load3(obj_cam_vec_in.mV); - - vertices.clear(); - normals.clear(); - - if ((mParams.getSculptType() & LL_SCULPT_TYPE_MASK) == LL_SCULPT_TYPE_MESH) - { - return; - } - - S32 cur_index = 0; - // Scratch array for per-face silhouette edge information. This also has a - // lot of dev-only debug information that we might not care about anymore. - // (see DEBUG_SILHOUETTE_EDGE_MAP) - // *TODO: Consider removing the debug associated with - // DEBUG_SILHOUETTE_EDGE_MAP, and remove its associated computational - // overhead in generateSilhouetteEdge. - std::vector edge; - //for each face - for (face_list_t::iterator iter = mVolumeFaces.begin(); - iter != mVolumeFaces.end(); ++iter) - { - LLVolumeFace& face = *iter; - - if (!(face_mask & (0x1 << cur_index++)) || - face.mNumIndices == 0) - { - continue; - } - // Attempt to generate "edge" info for this silhouette, which is used - // for some prims. If the edge array remains empty, then this - // silhouette generation method is not supported for this face. - edge.clear(); - face.generateSilhouetteEdge(this, edge); - if (edge.empty()) - { - continue; - } - - if (face.mTypeMask & (LLVolumeFace::CAP_MASK)) - { - LLVector4a* v = (LLVector4a*)face.mPositions; - LLVector4a* n = (LLVector4a*)face.mNormals; - - for (S32 j = 0; j < face.mNumIndices / 3; j++) - { - for (S32 k = 0; k < 3; k++) - { - S32 index = edge[j * 3 + k]; - - if (index == -1) - { - // silhouette edge, currently only cubes, so no other conditions - - S32 v1 = face.mIndices[j * 3 + k]; - S32 v2 = face.mIndices[j * 3 + ((k + 1) % 3)]; - - LLVector4a t; - mat.affineTransform(v[v1], t); - vertices.push_back(LLVector3(t[0], t[1], t[2])); - - norm_mat.rotate(n[v1], t); - - t.normalize3fast(); - normals.push_back(LLVector3(t[0], t[1], t[2])); - - mat.affineTransform(v[v2], t); - vertices.push_back(LLVector3(t[0], t[1], t[2])); - - norm_mat.rotate(n[v2], t); - t.normalize3fast(); - normals.push_back(LLVector3(t[0], t[1], t[2])); - } - } - } - - } - else - { - //============================================== - //DEBUG draw edge map instead of silhouette edge - //============================================== - -#if DEBUG_SILHOUETTE_EDGE_MAP - - //for each triangle - U32 tri_count = face.mNumIndices / 3; - for (U32 j = 0; j < tri_count; j++) { - //get vertices - S32 v1 = face.mIndices[j*3+0]; - S32 v2 = face.mIndices[j*3+1]; - S32 v3 = face.mIndices[j*3+2]; - - //get current face center - LLVector3 cCenter = (face.mVertices[v1].getPosition() + - face.mVertices[v2].getPosition() + - face.mVertices[v3].getPosition()) / 3.0f; - - //for each edge - for (S32 k = 0; k < 3; k++) { - S32 nIndex = edge[j*3+k]; - if (nIndex <= -1) { - continue; - } - - if (nIndex >= (S32)tri_count) { - continue; - } - //get neighbor vertices - v1 = face.mIndices[nIndex*3+0]; - v2 = face.mIndices[nIndex*3+1]; - v3 = face.mIndices[nIndex*3+2]; - - //get neighbor face center - LLVector3 nCenter = (face.mVertices[v1].getPosition() + - face.mVertices[v2].getPosition() + - face.mVertices[v3].getPosition()) / 3.0f; - - //draw line - vertices.push_back(cCenter); - vertices.push_back(nCenter); - normals.push_back(LLVector3(1,1,1)); - normals.push_back(LLVector3(1,1,1)); - segments.push_back(vertices.size()); - } - } - - continue; - - //============================================== - //DEBUG - //============================================== - - //============================================== - //DEBUG draw normals instead of silhouette edge - //============================================== -#elif DEBUG_SILHOUETTE_NORMALS - - //for each vertex - for (U32 j = 0; j < face.mNumVertices; j++) { - vertices.push_back(face.mVertices[j].getPosition()); - vertices.push_back(face.mVertices[j].getPosition() + face.mVertices[j].getNormal()*0.1f); - normals.push_back(LLVector3(0,0,1)); - normals.push_back(LLVector3(0,0,1)); - segments.push_back(vertices.size()); -#if DEBUG_SILHOUETTE_BINORMALS - vertices.push_back(face.mVertices[j].getPosition()); - vertices.push_back(face.mVertices[j].getPosition() + face.mVertices[j].mTangent*0.1f); - normals.push_back(LLVector3(0,0,1)); - normals.push_back(LLVector3(0,0,1)); - segments.push_back(vertices.size()); -#endif - } - - continue; -#else - //============================================== - //DEBUG - //============================================== - - constexpr U8 AWAY = 0x01, - TOWARDS = 0x02; - - //for each triangle - std::vector fFacing; - vector_append(fFacing, face.mNumIndices/3); - - LLVector4a* v = (LLVector4a*) face.mPositions; - LLVector4a* n = (LLVector4a*) face.mNormals; - - for (S32 j = 0; j < face.mNumIndices/3; j++) - { - //approximate normal - S32 v1 = face.mIndices[j*3+0]; - S32 v2 = face.mIndices[j*3+1]; - S32 v3 = face.mIndices[j*3+2]; - - LLVector4a c1,c2; - c1.setSub(v[v1], v[v2]); - c2.setSub(v[v2], v[v3]); - - LLVector4a norm; - - norm.setCross3(c1, c2); - - if (norm.dot3(norm) < 0.00000001f) - { - fFacing[j] = AWAY | TOWARDS; - } - else - { - //get view vector - LLVector4a view; - view.setSub(obj_cam_vec, v[v1]); - bool away = view.dot3(norm) > 0.0f; - if (away) - { - fFacing[j] = AWAY; - } - else - { - fFacing[j] = TOWARDS; - } - } - } - - //for each triangle - for (S32 j = 0; j < face.mNumIndices/3; j++) - { - if (fFacing[j] == (AWAY | TOWARDS)) - { //this is a degenerate triangle - //take neighbor facing (degenerate faces get facing of one of their neighbors) - // *FIX IF NEEDED: this does not deal with neighboring degenerate faces - for (S32 k = 0; k < 3; k++) - { - S32 index = edge[j*3+k]; - if (index != -1) - { - fFacing[j] = fFacing[index]; - break; - } - } - continue; //skip degenerate face - } - - //for each edge - for (S32 k = 0; k < 3; k++) { - S32 index = edge[j*3+k]; - if (index != -1 && fFacing[index] == (AWAY | TOWARDS)) { - //our neighbor is degenerate, make him face our direction - fFacing[edge[j*3+k]] = fFacing[j]; - continue; - } - - if (index == -1 || //edge has no neighbor, MUST be a silhouette edge - (fFacing[index] & fFacing[j]) == 0) { //we found a silhouette edge - - S32 v1 = face.mIndices[j*3+k]; - S32 v2 = face.mIndices[j*3+((k+1)%3)]; - - LLVector4a t; - mat.affineTransform(v[v1], t); - vertices.push_back(LLVector3(t[0], t[1], t[2])); - - norm_mat.rotate(n[v1], t); - - t.normalize3fast(); - normals.push_back(LLVector3(t[0], t[1], t[2])); - - mat.affineTransform(v[v2], t); - vertices.push_back(LLVector3(t[0], t[1], t[2])); - - norm_mat.rotate(n[v2], t); - t.normalize3fast(); - normals.push_back(LLVector3(t[0], t[1], t[2])); - } - } - } -#endif - } - } -} - S32 LLVolume::lineSegmentIntersect(const LLVector4a& start, const LLVector4a& end, S32 face, LLVector4a* intersection,LLVector2* tex_coord, LLVector4a* normal, LLVector4a* tangent_out) diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index fa68420dec..ed3cc9860a 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -908,15 +908,6 @@ class LLVolumeFace // Get a reference to the octree, which may be null const LLVolumeOctree* getOctree() const; - // Part of silhouette generation (used by selection outlines) - // Populates the provided edge array with numbers corresponding to - // *partial* logic of whether a particular index should be rendered - // as a silhouette edge. -1 indicates the index should be rendered as a - // silhouette edge. See generateSilhouetteVertices for the full logic. - // Silhouette edges can only be generated for some types of prims. If a - // silhouette edge cannot be generated, the edge array will be left empty. - void generateSilhouetteEdge(const LLVolume* volume, std::vector& edge) const; - enum { SINGLE_MASK = 0x0001, @@ -1049,13 +1040,6 @@ class LLVolume : public LLRefCount S32 getNumTriangles(S32* vcount = nullptr) const; - void generateSilhouetteVertices(std::vector &vertices, - std::vector &normals, - const LLVector3& view_vec, - const LLMatrix4& mat, - const LLMatrix3& norm_mat, - S32 face_index); - //get the face index of the face that intersects with the given line segment at the point //closest to start. Moves end to the point of intersection. Returns -1 if no intersection. //Line segment must be in volume space. diff --git a/indra/llmath/xform.h b/indra/llmath/xform.h index 05691fd318..aebec307e6 100644 --- a/indra/llmath/xform.h +++ b/indra/llmath/xform.h @@ -51,8 +51,7 @@ class LLXform GEOMETRY = 0x10, TEXTURE = 0x20, MOVED = TRANSLATED|ROTATED|SCALED, - SILHOUETTE = 0x40, - ALL_CHANGED = 0x7f + ALL_CHANGED = 0x3f }EChangedFlags; protected: diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index e190311fde..e48ef4564b 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -176,4 +176,6 @@ if(AL_BUILD_TESTS) al_add_test(alinfiniteprojection_gl GL PROJECT llrender LIBRARIES llrender) # Which fragment output reaches a render target's colour attachment. al_add_test(llrendertarget_gl GL PROJECT llrender LIBRARIES llrender) + # LLRender's cached clear colour and scissor box, and LLGLSScissor. + al_add_test(alrenderstate_gl GL PROJECT llrender LIBRARIES llrender) endif() diff --git a/indra/llrender/llglstates.h b/indra/llrender/llglstates.h index de47d47f38..c3b28548c0 100644 --- a/indra/llrender/llglstates.h +++ b/indra/llrender/llglstates.h @@ -171,6 +171,35 @@ class LLGLSColorMask bool mPrev[4]; }; +// Scoped scissor: the test enabled and the box set, both handed back on exit as they were +// found. A box never set in this context is left as the scope set it, since GL takes no +// negative size to restore it with. +class LLGLSScissor +{ +public: + LLGLSScissor(S32 x, S32 y, S32 width, S32 height) + : mTest(GL_SCISSOR_TEST) + { + gGL.getScissor(mPrev); + gGL.setScissor(x, y, width, height); + } + + ~LLGLSScissor() + { + if (mPrev[2] >= 0 && mPrev[3] >= 0) + { + gGL.setScissor(mPrev[0], mPrev[1], mPrev[2], mPrev[3]); + } + } + + LLGLSScissor(const LLGLSScissor&) = delete; + LLGLSScissor& operator=(const LLGLSScissor&) = delete; + +private: + LLGLEnable mTest; + S32 mPrev[4]; +}; + class LLGLSTracker { protected: diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 3559b586d0..8b65316277 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -401,6 +401,16 @@ bool LLRender::init(bool needs_vertex_buffer) mPolygonOffsetUnits = 0.f; mPatchVertices = 3; + // The clear colour is issued rather than assumed: a window may have cleared to a colour of + // its own before this ran. The scissor box GL starts at is the size of the window the + // context was made for, which nothing here knows, so it stays unknown until set. + mClearColor = LLColor4(0.f, 0.f, 0.f, 0.f); + glClearColor(0.f, 0.f, 0.f, 0.f); + mScissor[0] = 0; + mScissor[1] = 0; + mScissor[2] = -1; + mScissor[3] = -1; + // Build this context's sampler objects before anything can ask for one. mSamplerCache.warmup(); @@ -535,6 +545,13 @@ void LLRender::refreshState(void) // requested value and skip the GL call, leaving the fresh context at its 0,0 default. rebasePolygonOffset(); + // The same for the clear colour and a scissor box that was ever set. + glClearColor(mClearColor.mV[VRED], mClearColor.mV[VGREEN], mClearColor.mV[VBLUE], mClearColor.mV[VALPHA]); + if (mScissor[2] >= 0) + { + glScissor(mScissor[0], mScissor[1], mScissor[2], mScissor[3]); + } + flush(); mDirty = false; @@ -1686,6 +1703,35 @@ void LLRender::setPatchVertices(U32 count) } } +void LLRender::setClearColor(const LLColor4& color) +{ + if (mClearColor != color) + { + mClearColor = color; + glClearColor(color.mV[VRED], color.mV[VGREEN], color.mV[VBLUE], color.mV[VALPHA]); + } +} + +void LLRender::clear(U32 mask) +{ + flush(); + glClear(mask); +} + +void LLRender::setScissor(S32 x, S32 y, S32 width, S32 height) +{ + llassert(width >= 0 && height >= 0); + if (mScissor[0] != x || mScissor[1] != y || mScissor[2] != width || mScissor[3] != height) + { + flush(); + mScissor[0] = x; + mScissor[1] = y; + mScissor[2] = width; + mScissor[3] = height; + glScissor(x, y, width, height); + } +} + void LLRender::rebasePolygonOffset() { flush(); diff --git a/indra/llrender/llrender.h b/indra/llrender/llrender.h index 08d562d9a3..436242c327 100644 --- a/indra/llrender/llrender.h +++ b/indra/llrender/llrender.h @@ -381,6 +381,31 @@ class LLRender // Redundant sets are dropped without a flush. void setPatchVertices(U32 count); + // The colour glClear fills colour buffers with. Every writer in the tree goes through + // here, so the cache is the state. Redundant sets are dropped; nothing batched reads it, + // so a change needs no flush. + void setClearColor(const LLColor4& color); + const LLColor4& getClearColor() const { return mClearColor; } + + // Clears the bound framebuffer's buffers named in mask (GL_COLOR_BUFFER_BIT and the rest) + // through the write masks, scissor box and clear values in force, after what is batched + // has been drawn. + void clear(U32 mask); + + // The scissor box, in pixels of the bound framebuffer, that GL_SCISSOR_TEST (an + // LLGLState) cuts to. Every writer goes through here; a change flushes what is batched + // under the old box, and redundant sets are dropped without one. See LLGLSScissor. + void setScissor(S32 x, S32 y, S32 width, S32 height); + // The box in force: x, y, width, height. Width is negative until a box is set in this + // context, which GL starts at the size of the window it was made for. + void getScissor(S32 (&box)[4]) const + { + box[0] = mScissor[0]; + box[1] = mScissor[1]; + box[2] = mScissor[2]; + box[3] = mScissor[3]; + } + ALTextureSlot* getTextureSlot(U32 index); // Drop these texture names, about to be deleted in this context, from every slot's @@ -514,6 +539,8 @@ class LLRender F32 mPolygonOffsetFactor = 0.f; F32 mPolygonOffsetUnits = 0.f; U32 mPatchVertices = 3; // GL's initial GL_PATCH_VERTICES + LLColor4 mClearColor = LLColor4(0.f, 0.f, 0.f, 0.f); // GL's initial GL_COLOR_CLEAR_VALUE + S32 mScissor[4] = { 0, 0, -1, -1 }; // unknown until set (see getScissor) LLPointer mBuffer; LLStrider mVerticesp; diff --git a/indra/llrender/llrendertarget.cpp b/indra/llrender/llrendertarget.cpp index d1eb164b10..0b175e2265 100644 --- a/indra/llrender/llrendertarget.cpp +++ b/indra/llrender/llrendertarget.cpp @@ -33,6 +33,7 @@ #include "alsamplerstate.h" #include "llrender.h" #include "llgl.h" +#include "llglstates.h" #include @@ -589,8 +590,7 @@ void LLRenderTarget::clear(U32 mask_in) } else { - LLGLEnable scissor(GL_SCISSOR_TEST); - glScissor(0, 0, mResX, mResY); + LLGLSScissor scissor(0, 0, mResX, mResY); stop_glerror(); glClear(mask & mask_in); } diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index 9ef8ad059e..26a3d46300 100644 --- a/indra/llrender/llshadermgr.cpp +++ b/indra/llrender/llshadermgr.cpp @@ -2132,6 +2132,25 @@ void LLShaderMgr::initAttribsAndUniforms() mReservedUniforms.push_back("moonQuadU"); mReservedUniforms.push_back("moonQuadV"); + // Selection outlines, hover glow and wireframe (ALSelectionOutline) + mReservedUniforms.push_back("selection_id"); + mReservedUniforms.push_back("selection_priority"); + mReservedUniforms.push_back("selection_show_hidden"); + mReservedUniforms.push_back("selection_outlined"); + mReservedUniforms.push_back("selection_scene_depth"); + mReservedUniforms.push_back("outline_width"); + mReservedUniforms.push_back("outline_inner_width"); + mReservedUniforms.push_back("outline_glow_radius"); + mReservedUniforms.push_back("outline_glow_brightness"); + mReservedUniforms.push_back("wireframe_pass"); + mReservedUniforms.push_back("wireframe_width"); + mReservedUniforms.push_back("selectionIdMap"); + mReservedUniforms.push_back("selectionJumpMap"); + mReservedUniforms.push_back("selectionTileMap"); + mReservedUniforms.push_back("selectionPalette"); + mReservedUniforms.push_back("selectionGlowReachMap"); + mReservedUniforms.push_back("selectionGlowRowMap"); + // The enum and this list are parallel, and an entry added or removed on one side only // shifts every later uniform index for every shader in the viewer -- silently, since a // wrong index still resolves to some other real uniform. Fatal, like the duplicate check diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h index 61fdbdb8df..65d7c7ca2c 100644 --- a/indra/llrender/llshadermgr.h +++ b/indra/llrender/llshadermgr.h @@ -615,6 +615,25 @@ class LLShaderMgr MOON_QUAD_U, // "moonQuadU" its right half-extent over its squared length MOON_QUAD_V, // "moonQuadV" its up half-extent over its squared length + // Selection outlines, hover glow and wireframe (ALSelectionOutline) + SELECTION_ID, // "selection_id" the object's id, 1 to 65535 + SELECTION_PRIORITY, // "selection_priority" ALSelectionOutline::EPriority + SELECTION_SHOW_HIDDEN, // "selection_show_hidden" 1 where hidden parts are drawn dimmed + SELECTION_OUTLINED, // "selection_outlined" 0 where the id pass holds it for its wireframe alone + SELECTION_SCENE_DEPTH, // "selection_scene_depth" 1 where the scene's depth decides what is hidden + OUTLINE_WIDTH, // "outline_width" the contour's width in pixels + OUTLINE_INNER_WIDTH, // "outline_inner_width" the edges between objects' width in pixels + OUTLINE_GLOW_RADIUS, // "outline_glow_radius" the hover glow's reach in pixels, 0 for none + OUTLINE_GLOW_BRIGHTNESS, // "outline_glow_brightness" RenderHighlightBrightness + WIREFRAME_PASS, // "wireframe_pass" ALSelectionOutline::EWirePass + WIREFRAME_WIDTH, // "wireframe_width" the lines' width in pixels + SELECTION_ID_MAP, // "selectionIdMap" the id target, read by the passes after the id pass + SELECTION_JUMP_MAP, // "selectionJumpMap" the near sides of jumps the jump pass marks + SELECTION_TILE_MAP, // "selectionTileMap" the tile pass's record of each tile + SELECTION_PALETTE, // "selectionPalette" each id's visible and hidden colours + SELECTION_GLOW_REACH_MAP, // "selectionGlowReachMap" the glowing ids within the glow's reach of each tile + SELECTION_GLOW_ROW_MAP, // "selectionGlowRowMap" the nearest glowing texel along each texel's row + END_RESERVED_UNIFORMS } eGLSLReservedUniforms; // clang-format on diff --git a/indra/llrender/tests/alinfiniteprojection_gl_test.cpp b/indra/llrender/tests/alinfiniteprojection_gl_test.cpp index 4ceb29cf4b..d6a0b1394e 100644 --- a/indra/llrender/tests/alinfiniteprojection_gl_test.cpp +++ b/indra/llrender/tests/alinfiniteprojection_gl_test.cpp @@ -421,4 +421,55 @@ namespace tut glDeleteQueries(1, &query); ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); } + + // The soften pass draws on the far plane twice (softenLightV.glsl, LLPipeline::renderDeferredLighting): its lit + // program in front of the far plane, as the haze does, and its sky program on it. Between them every pixel is + // shaded once, by the program for what it is, however far out the geometry lies. + template<> template<> + void alinfiniteprojection_object_t::test<7>() + { + setUp(); + const LLMatrix4a proj = infinite(); + GLuint query = 0; + glGenQueries(1, &query); + const auto samples = [&](GLenum func) + { + glDepthFunc(LLGLDepthTest::remap(func)); + glDepthMask(GL_FALSE); + glBeginQuery(GL_SAMPLES_PASSED, query); + draw(proj, 1000.f, true); + glEndQuery(GL_SAMPLES_PASSED); + glDepthMask(GL_TRUE); + glDepthFunc(GL_GREATER); + GLuint passed = 0; + glGetQueryObjectuiv(query, GL_QUERY_RESULT, &passed); + return passed; + }; + + glClear(GL_DEPTH_BUFFER_BIT); + ensure_equals("nothing lit on an empty frame", samples(GL_GREATER), (GLuint)0); + ensure_equals("all sky on an empty frame", samples(GL_EQUAL), (GLuint)(SIZE * SIZE)); + + for (F32 dist : { 1.f, 1000.f, 100000.f, MAX_RECONSTRUCT_DISTANCE }) + { + const std::string at = std::to_string((S32)dist) + " m"; + glClear(GL_DEPTH_BUFFER_BIT); + draw(proj, dist); + ensure_equals("all lit over geometry at " + at, samples(GL_GREATER), (GLuint)(SIZE * SIZE)); + ensure_equals("no sky over geometry at " + at, samples(GL_EQUAL), (GLuint)0); + } + + // Pixel by pixel: geometry at the reconstruction floor over the left half only, sky over the right. + glClear(GL_DEPTH_BUFFER_BIT); + glViewport(0, 0, SIZE / 2, SIZE); + draw(proj, MAX_RECONSTRUCT_DISTANCE); + glViewport(0, 0, SIZE, SIZE); + const GLuint lit = samples(GL_GREATER); + const GLuint sky = samples(GL_EQUAL); + ensure_equals("lit over the geometry's half", lit, (GLuint)(SIZE * SIZE / 2)); + ensure_equals("every pixel shaded once", lit + sky, (GLuint)(SIZE * SIZE)); + + glDeleteQueries(1, &query); + ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + } } diff --git a/indra/llrender/tests/alrenderstate_gl_test.cpp b/indra/llrender/tests/alrenderstate_gl_test.cpp new file mode 100644 index 0000000000..91513340de --- /dev/null +++ b/indra/llrender/tests/alrenderstate_gl_test.cpp @@ -0,0 +1,184 @@ +/** + * @file alrenderstate_gl_test.cpp + * @brief LLRender's cached clear colour and scissor box on the hidden window: what a clear fills + * with and cuts to, what GL holds after refreshState, and what LLGLSScissor hands back. + * + * $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 "../llgl.h" +#include "../llglheaders.h" +#include "../llglstates.h" +#include "../llrender.h" +#include "../llrendertarget.h" + +#include "llheadlessgl_fixture.h" + +#include "../test/lltut.h" + +namespace tut +{ + namespace + { + constexpr U32 SIZE = 4; + + const LLColor4U RED(255, 0, 0, 255); + const LLColor4U GREEN(0, 255, 0, 255); + + LLColor4U pixel(S32 x, S32 y) + { + U8 rgba[4] = { 0 }; + glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba); + return LLColor4U(rgba[0], rgba[1], rgba[2], rgba[3]); + } + + LLColor4 glClearValue() + { + F32 value[4] = { 0.f }; + glGetFloatv(GL_COLOR_CLEAR_VALUE, value); + return LLColor4(value[0], value[1], value[2], value[3]); + } + + void glScissorBox(S32 (&box)[4]) + { + GLint value[4] = { 0 }; + glGetIntegerv(GL_SCISSOR_BOX, value); + for (U32 i = 0; i < 4; ++i) + { + box[i] = value[i]; + } + } + } + + struct alrenderstate_data + { + static ll_test::HeadlessGL& gl() + { + static ll_test::HeadlessGL instance(true, true, true, false); + return instance; + } + + alrenderstate_data() + { + gl(); + } + + ~alrenderstate_data() + { + // A failed check throws out of a test with the target still bound, and a bound target cannot be released. + if (mTarget.isBoundInStack()) + { + mTarget.flush(); + } + mTarget.release(); + gGL.setClearColor(LLColor4::transparent); + } + + void setUp() + { + ensure("target allocates", mTarget.allocate(SIZE, SIZE, GL_RGBA8)); + } + + LLRenderTarget mTarget; + }; + + typedef test_group alrenderstate_t; + typedef alrenderstate_t::object alrenderstate_object_t; + tut::alrenderstate_t tut_alrenderstate("alrenderstate_gl"); + + // The clear colour a target's clear fills with is the one gGL was given, and GL holds what gGL says it does. + template<> template<> + void alrenderstate_object_t::test<1>() + { + setUp(); + mTarget.bindTarget(); + gGL.setClearColor(LLColor4::red); + ensure("GL holds the cached colour", glClearValue() == gGL.getClearColor()); + mTarget.clear(); + ensure_equals("cleared red", pixel(1, 1), RED); + gGL.setClearColor(LLColor4::green); + mTarget.clear(); + ensure_equals("cleared green", pixel(1, 1), GREEN); + mTarget.flush(); + ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + } + + // refreshState is for state changed behind gGL's back: it puts back what gGL holds, where the setters would see + // their cache agreeing and issue nothing. + template<> template<> + void alrenderstate_object_t::test<2>() + { + gGL.setClearColor(LLColor4::green); + gGL.setScissor(1, 1, 2, 2); + + glClearColor(1.f, 0.f, 0.f, 1.f); + glScissor(0, 0, 1, 1); + gGL.setClearColor(LLColor4::green); + gGL.setScissor(1, 1, 2, 2); + ensure("an agreeing cache issues nothing", glClearValue() == LLColor4::red); + + gGL.refreshState(); + ensure("refreshState restores the clear colour", glClearValue() == LLColor4::green); + S32 box[4]; + glScissorBox(box); + ensure("refreshState restores the scissor box", box[0] == 1 && box[1] == 1 && box[2] == 2 && box[3] == 2); + ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + } + + // LLGLSScissor cuts a clear to its box with the test on, and hands back the box and the test it found, nested + // scopes included. + template<> template<> + void alrenderstate_object_t::test<3>() + { + setUp(); + mTarget.bindTarget(); + gGL.setScissor(0, 0, SIZE, SIZE); + gGL.setClearColor(LLColor4::red); + mTarget.clear(); + + { + LLGLSScissor left(0, 0, SIZE / 2, SIZE); + ensure("the test is on in the scope", glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE); + gGL.setClearColor(LLColor4::green); + mTarget.clear(); + { + LLGLSScissor corner(0, 0, 1, 1); + S32 box[4]; + gGL.getScissor(box); + ensure("the inner scope's box", box[0] == 0 && box[1] == 0 && box[2] == 1 && box[3] == 1); + } + S32 box[4]; + glScissorBox(box); + ensure("the outer box is back", box[0] == 0 && box[1] == 0 && box[2] == (S32)SIZE / 2 && box[3] == (S32)SIZE); + ensure("the test is still on", glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE); + } + + ensure_equals("inside the box: cleared green", pixel(0, 1), GREEN); + ensure_equals("outside the box: still red", pixel(SIZE - 1, 1), RED); + ensure("the test is off again", glIsEnabled(GL_SCISSOR_TEST) == GL_FALSE); + S32 box[4]; + glScissorBox(box); + ensure("the first box is back", box[0] == 0 && box[1] == 0 && box[2] == (S32)SIZE && box[3] == (S32)SIZE); + mTarget.flush(); + ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + } +} diff --git a/indra/llrender/tests/llheadlessgl_fixture.h b/indra/llrender/tests/llheadlessgl_fixture.h index ad291db8cb..1576dbb20c 100644 --- a/indra/llrender/tests/llheadlessgl_fixture.h +++ b/indra/llrender/tests/llheadlessgl_fixture.h @@ -53,6 +53,8 @@ #include "llwindowcallbacks.h" #include +#include +#include namespace ll_test { @@ -103,19 +105,29 @@ namespace ll_test FontStateScope& operator=(const FontStateScope&) = delete; }; - // Concrete LLShaderMgr for render-output tests. The tests never load real - // shaders from disk and don't pump the per-frame uniform updates the viewer - // does, so getShaderDirPrefix returns empty and updateShaderUniforms is a + // Concrete LLShaderMgr for render-output tests. They don't pump the + // per-frame uniform updates the viewer does, so updateShaderUniforms is a // no-op. LLRender::syncMatrices still pushes matrix uniforms directly each // draw — that path lives in llrender.cpp and doesn't go through the mgr. + // A test that loads the viewer's own shader files passes the tree's class + // prefix ("/shaders/class"), which loadShaderFile completes + // with the class number and the file's path; the rest load none. class TestShaderMgr : public LLShaderMgr { public: - TestShaderMgr() { sInstance = this; initAttribsAndUniforms(); } + explicit TestShaderMgr(std::string dir_prefix = std::string()) + : mDirPrefix(std::move(dir_prefix)) + { + sInstance = this; + initAttribsAndUniforms(); + } ~TestShaderMgr() override { if (sInstance == this) sInstance = nullptr; } - std::string getShaderDirPrefix() override { return std::string(); } + std::string getShaderDirPrefix() override { return mDirPrefix; } void updateShaderUniforms(LLGLSLShader* /*shader*/) override {} + + private: + std::string mDirPrefix; }; // GL 3.3 core pass-through UI vertex shader. Same uniform / attribute @@ -439,7 +451,7 @@ namespace ll_test // reflects only what the test rendered this turn. void clearFramebuffer() { - glClearColor(0.f, 0.f, 0.f, 1.f); + gGL.setClearColor(LLColor4::black); glClear(GL_COLOR_BUFFER_BIT); } diff --git a/indra/llrender/tests/llrendertarget_gl_test.cpp b/indra/llrender/tests/llrendertarget_gl_test.cpp index 65384b6884..3546e7e8b0 100644 --- a/indra/llrender/tests/llrendertarget_gl_test.cpp +++ b/indra/llrender/tests/llrendertarget_gl_test.cpp @@ -114,7 +114,7 @@ namespace tut LLColor4U drawn(S32 output) { mTarget.bindTarget(); - glClearColor(0.f, 0.f, 0.f, 0.f); + gGL.setClearColor(LLColor4::transparent); mTarget.clear(); if (output >= 0) { diff --git a/indra/llui/lllocalcliprect.cpp b/indra/llui/lllocalcliprect.cpp index da5a6479bd..40e70b175d 100644 --- a/indra/llui/lllocalcliprect.cpp +++ b/indra/llui/lllocalcliprect.cpp @@ -83,11 +83,7 @@ void LLScreenClipRect::updateScissorRegion() { if (sClipRectStack.empty()) return; - // finish any deferred calls in the old clipping region - gGL.flush(); - LLRect rect = sClipRectStack.top(); - stop_glerror(); S32 x,y,w,h; x = llfloor(rect.mLeft * LLUI::getScaleFactor().mV[VX]); y = llfloor(rect.mBottom * LLUI::getScaleFactor().mV[VY]); @@ -96,8 +92,8 @@ void LLScreenClipRect::updateScissorRegion() // names no pixels at all, it let a column of them through at the origin. w = rect.getWidth() > 0 ? llceil(rect.getWidth() * LLUI::getScaleFactor().mV[VX]) + 1 : 0; h = rect.getHeight() > 0 ? llceil(rect.getHeight() * LLUI::getScaleFactor().mV[VY]) + 1 : 0; - glScissor( x,y,w,h ); - stop_glerror(); + // Draws batched under the old box are flushed by the change; an unchanged box costs nothing. + gGL.setScissor(x, y, w, h); } //--------------------------------------------------------------------------- diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 046833269c..c53cf00c09 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -40,6 +40,7 @@ #include "llexception.h" #include "llfasttimer.h" #include "llgl.h" +#include "llrender.h" #include "llstring.h" #include "lldir.h" #include "llsdutil.h" @@ -1823,8 +1824,8 @@ const S32 max_format = (S32)num_formats - 1; if (auto_show) { show(); - glClearColor(0.0f, 0.0f, 0.0f, 0.f); - glClear(GL_COLOR_BUFFER_BIT); + gGL.setClearColor(LLColor4::transparent); + gGL.clear(GL_COLOR_BUFFER_BIT); swapBuffers(); } diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 7c9b8edd21..6b56ef29f8 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -267,6 +267,7 @@ if(AL_BUILD_VIEWER) alfloaterxuistudio.cpp alviewcapture.cpp algradehistory.cpp + alhoverglow.cpp allegacynotificationwellwindow.cpp allightboxdirectory.cpp almotionpredictor.cpp @@ -301,6 +302,7 @@ if(AL_BUILD_VIEWER) alpanelstreaminfo.cpp alpickitem.cpp alsceneexplorermodel.cpp + alselectionoutline.cpp alstreaminfo.cpp altoolalign.cpp altoolscenepicker.cpp @@ -1144,6 +1146,8 @@ if(AL_BUILD_VIEWER) alfloaterxuistudio.h alviewcapture.h algradehistory.h + alhoverglow.h + alhuddepth.h allegacynotificationwellwindow.h allightboxdirectory.h almotionpredictor.h @@ -1176,6 +1180,7 @@ if(AL_BUILD_VIEWER) alpanelstreaminfo.h alpickitem.h alsceneexplorermodel.h + alselectionoutline.h alstreaminfo.h altoolalign.h altoolscenepicker.h @@ -2540,6 +2545,25 @@ if(AL_BUILD_TESTS) LIBRARIES ${test_libs} SOURCES alcolorwheelmodel.cpp ) + # The selection outline's id and edge passes, the viewer's own shader files + # read from the source tree, on LLWindowManager's hidden window. + al_add_test( + alselectionoutline_gl + GL + PROJECT ${test_project} + LIBRARIES llrender + INCLUDES ${INDRA_SOURCE_DIR}/llprimitive + DEFINES "AL_TEST_SHADER_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/app_settings/shaders\"" + ) + # The window's depth under the HUD: cleared in the HUD's rects, then the + # scene's written back with the viewer's own copy shader. + al_add_test( + alhuddepth_gl + GL + PROJECT ${test_project} + LIBRARIES llrender + DEFINES "AL_TEST_SHADER_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/app_settings/shaders\"" + ) # The outbox keeps what the pure feedback code makes. al_add_test( alfeedbackoutbox diff --git a/indra/newview/alfarplaneoverlay.cpp b/indra/newview/alfarplaneoverlay.cpp index 3727ab850b..48a03e6a2d 100644 --- a/indra/newview/alfarplaneoverlay.cpp +++ b/indra/newview/alfarplaneoverlay.cpp @@ -117,7 +117,7 @@ void render() sTarget->bindTarget(); { LLGLSColorMask mask(true, true); - glClearColor(0.f, 0.f, 0.f, 0.f); + gGL.setClearColor(LLColor4::transparent); sTarget->clear(); gDebugProgram.bind(); LLGLDepthTest depth(GL_TRUE, GL_TRUE); diff --git a/indra/newview/alhoverglow.cpp b/indra/newview/alhoverglow.cpp new file mode 100644 index 0000000000..5f3abe1369 --- /dev/null +++ b/indra/newview/alhoverglow.cpp @@ -0,0 +1,48 @@ +/** + * @file alhoverglow.cpp + * @brief The glow of what the pointer is over, fading in and out. + * + * $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 "llviewerprecompiledheaders.h" + +#include "alhoverglow.h" + +#include "alselectionoutline.h" +#include "llviewerobject.h" +#include "llviewerobjectlist.h" +#include "pipeline.h" + +void ALHoverGlow::addTo(ALSelectionOutline& outline) const +{ + for (const Glow& glow : mGlows) + { + LLViewerObject* object = gObjectList.findObject(glow.mID); + if (!object || object->isDead() || object->isHUDAttachment()) + { + continue; + } + LLColor4 colour = LLPipeline::RenderHighlightColor; + colour.mV[VALPHA] *= glow.mFade; + // Every face: the TE mask holds more than a volume has. + outline.add(object, 0xFFFFFFFF, colour, false, ALSelectionOutline::PRIORITY_HOVER, 0); + } +} diff --git a/indra/newview/alhoverglow.h b/indra/newview/alhoverglow.h new file mode 100644 index 0000000000..b4d067d38f --- /dev/null +++ b/indra/newview/alhoverglow.h @@ -0,0 +1,147 @@ +/** + * @file alhoverglow.h + * @brief The glow of what the pointer is over, fading in and out. + * + * $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$ + */ + +#pragma once + +#include "llmath.h" +#include "llsingleton.h" +#include "lluuid.h" + +#include +#include + +class ALSelectionOutline; + +/// The glow of what the pointer is over (RenderHoverGlowEnable): the world object LLToolPie's hover names as one a +/// click does something to, once the name has held for SETTLE_SECONDS, which fades in over RenderHighlightFadeTime +/// while it glows, while the one before it fades out. ALSelectionOutline draws each at PRIORITY_HOVER, its colour's +/// alpha scaled by its fade, in the world's call of LLSelectMgr::renderSilhouettes, which advances the fades first. +/// +/// Objects are held by id and looked up as they are drawn, so a glow keeps nothing alive: one whose object is gone +/// is dropped. +class ALHoverGlow : public LLSingleton +{ + LLSINGLETON_EMPTY_CTOR(ALHoverGlow); + +public: + struct Glow + { + LLUUID mID; + /// 0 to 1: how far it has faded in. + F32 mFade = 0.f; + }; + + /// How long the pointer rests on a name, or on none, before the glow follows it: a sweep across objects lights + /// none of them, and a frame or two off one, at its edge or a seam between its prims, does not put it out. + static constexpr F64 SETTLE_SECONDS = 0.15; + + /// Names `id` as what the pointer is over in frame `frame` (LLFrameTimer::getFrameCount), or nothing with a null + /// id. A name lapses once a frame has passed without it being named again: hover stops being handled when the + /// pointer is over a floater, leaves the window or another tool takes it. + void hover(const LLUUID& id, U32 frame) + { + mHovered = id; + mHoveredFrame = frame; + } + + /// Advances the fades to `now`, in seconds, in frame `frame`, over `fade_time` seconds each way: the glowing + /// object's in, every other's out. What glows follows the name, or its lapse, once it has held for + /// SETTLE_SECONDS. Dropped are the ones faded out and the ones whose object `alive(id)` says is gone. Called more + /// than once in a frame, it moves nothing the second time. + template + void update(F64 now, U32 frame, F32 fade_time, Alive&& alive); + + /// What glows, in the order it was first named. + const std::vector& glows() const { return mGlows; } + + /// The object named in `frame`, or null where its name has lapsed. + LLUUID hovered(U32 frame) const { return (frame - mHoveredFrame <= 1) ? mHovered : LLUUID::null; } + + /// A fade `fade` moved `seconds` toward 1 when `in`, toward 0 otherwise, at a whole fade each `fade_time` + /// seconds, held to 0 to 1; at once where fade_time is not above 0 or not a number. + static F32 stepFade(F32 fade, F32 seconds, F32 fade_time, bool in); + + /// Adds what glows to `outline`, in RenderHighlightColor, for the world's call. + void addTo(ALSelectionOutline& outline) const; + +private: + LLUUID mHovered; + U32 mHoveredFrame = 0; + // The name the pointer has been on since mPendingSince, and the one the glow follows, which becomes it once it + // has settled. + LLUUID mPending; + F64 mPendingSince = 0.0; + LLUUID mTarget; + // When the fades were last moved; none yet before the first update. + F64 mLastTime = 0.0; + bool mTimed = false; + std::vector mGlows; +}; + +inline F32 ALHoverGlow::stepFade(F32 fade, F32 seconds, F32 fade_time, bool in) +{ + // Asked outright: /fp:fast may answer a comparison with a NaN either way. + const F32 step = (llisnan(fade_time) || !(fade_time > 0.f)) ? 1.f : llmax(seconds, 0.f) / fade_time; + return llclamp(fade + (in ? step : -step), 0.f, 1.f); +} + +template +void ALHoverGlow::update(F64 now, U32 frame, F32 fade_time, Alive&& alive) +{ + const F32 seconds = mTimed ? (F32)llmax(now - mLastTime, 0.0) : 0.f; + mLastTime = now; + mTimed = true; + + const LLUUID named = hovered(frame); + if (named != mPending) + { + mPending = named; + mPendingSince = now; + } + if (mPending != mTarget && now - mPendingSince >= SETTLE_SECONDS && (mPending.isNull() || alive(mPending))) + { + mTarget = mPending; + } + if (mTarget.notNull() && !alive(mTarget)) + { + mTarget.setNull(); + } + + const LLUUID& target = mTarget; + if (target.notNull() && + std::none_of(mGlows.begin(), mGlows.end(), [&target](const Glow& glow) { return glow.mID == target; })) + { + // It starts at nothing and takes this frame's step at once, as the ones fading out do. + mGlows.push_back({ target, 0.f }); + } + + for (Glow& glow : mGlows) + { + glow.mFade = stepFade(glow.mFade, seconds, fade_time, glow.mID == target); + } + mGlows.erase(std::remove_if(mGlows.begin(), mGlows.end(), + [&](const Glow& glow) + { return (glow.mID != target && glow.mFade <= 0.f) || !alive(glow.mID); }), + mGlows.end()); +} diff --git a/indra/newview/alhuddepth.h b/indra/newview/alhuddepth.h new file mode 100644 index 0000000000..df32ccd7e2 --- /dev/null +++ b/indra/newview/alhuddepth.h @@ -0,0 +1,164 @@ +/** + * @file alhuddepth.h + * @brief The window's depth under the HUD: cleared where the HUD can draw, so it tests against itself alone, and + * the scene's written back after it for the 3D UI that follows. + * + * $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$ + */ + +#pragma once + +#include "llgl.h" +#include "llglslshader.h" +#include "llglstates.h" +#include "llmath.h" +#include "llmatrix4a.h" +#include "llrect.h" +#include "llrender.h" +#include "llrendertarget.h" +#include "llshadermgr.h" +#include "llvertexbuffer.h" +#include "v3math.h" + +#include +#include + +/// The HUD is drawn over the window's framebuffer, whose depth the final blit filled with the scene's for the 3D UI +/// around it (LLPipeline::renderFinalize). Within the viewport rects the HUD can cover (render_hud_attachments, a +/// rect per attachment point), clear() empties that depth before the HUD draws, and restore() writes the scene's +/// back after it wherever the scene is nearer than what the HUD left. +namespace ALHUDDepth +{ + +/// The pixels of a width x height viewport a box covers through `mvp`, an orthographic projection of the frame the +/// box is in: grown by a pixel each way and clipped to the viewport, empty where it misses. +inline LLRect boxRect(const LLMatrix4a& mvp, const LLVector3& lo, const LLVector3& hi, S32 width, S32 height) +{ + F32 min_x = FLT_MAX; + F32 min_y = FLT_MAX; + F32 max_x = -FLT_MAX; + F32 max_y = -FLT_MAX; + for (U32 i = 0; i < 8; ++i) + { + const LLVector4a corner((i & 1) ? hi.mV[VX] : lo.mV[VX], (i & 2) ? hi.mV[VY] : lo.mV[VY], + (i & 4) ? hi.mV[VZ] : lo.mV[VZ], 1.f); + LLVector4a clip; + mvp.transform4(corner, clip); + // The projection is orthographic, so w is 1; the divide only guards against a degenerate one. + const F32 w = clip[3] != 0.f ? clip[3] : 1.f; + const F32 x = (clip[0] / w * 0.5f + 0.5f) * (F32)width; + const F32 y = (clip[1] / w * 0.5f + 0.5f) * (F32)height; + min_x = llmin(min_x, x); + min_y = llmin(min_y, y); + max_x = llmax(max_x, x); + max_y = llmax(max_y, y); + } + + // Far off-screen corners are clamped before they become integers. + const F32 limit = (F32)(llmax(width, height) + 1); + LLRect rect(llfloor(llclamp(min_x, -limit, limit)) - 1, llceil(llclamp(max_y, -limit, limit)) + 1, + llceil(llclamp(max_x, -limit, limit)) + 1, llfloor(llclamp(min_y, -limit, limit)) - 1); + rect.intersectWith(LLRect(0, height, width, 0)); + return rect; +} + +/// Adds `rect` to `rects`, folding in every rect it overlaps until it overlaps none: HUDs sit in the corners and +/// along the edges, and one rect around them all would take in the screen between. +inline void mergeRect(std::vector& rects, LLRect rect) +{ + bool merged = true; + while (merged) + { + merged = false; + for (size_t i = 0; i < rects.size(); ++i) + { + if (rect.overlaps(rects[i])) + { + rect.unionWith(rects[i]); + rects[i] = rects.back(); + rects.pop_back(); + merged = true; + break; + } + } + } + rects.push_back(rect); +} + +/// Clears the bound framebuffer's depth within each rect of a viewport whose origin is (x, y), where `program`, the +/// copy restore() draws with, is loaded to put the scene's depth back after. Without it the depth is left as the +/// scene's, which the HUD then tests against, and the 3D UI after it keeps the world's occlusion. +inline void clear(const LLGLSLShader& program, const std::vector& rects, S32 x, S32 y) +{ + if (!program.isComplete()) + { + return; + } + + // Depth writes on: they mask the clear. + LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_ALWAYS); + for (const LLRect& rect : rects) + { + LLGLSScissor scissor(x + rect.mLeft, y + rect.mBottom, rect.getWidth(), rect.getHeight()); + gGL.clear(GL_DEPTH_BUFFER_BIT); + } +} + +/// Writes `scene`'s depth over the bound framebuffer's within each rect of a viewport whose origin is (x, y), +/// wherever it is nearer than what is there, through `program` (interface/copyF.glsl with COPY_DEPTH and +/// DEPTH_ONLY) over `triangle`, the full-screen triangle the final blit is drawn with: the same values it wrote, so +/// the 3D UI drawn after the HUD is hidden by the world as before, and by the HUD where the HUD is in front. +inline void restore(LLGLSLShader& program, LLRenderTarget& scene, LLVertexBuffer& triangle, + const std::vector& rects, S32 x, S32 y) +{ + if (rects.empty() || !program.isComplete()) + { + return; + } + + LLGLSLShader* previous = LLGLSLShader::sCurBoundShaderPtr; + { + LLGLSColorMask mask(false, false); + LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_LESS); + LLGLDisable blend(GL_BLEND); + LLGLDisable cull(GL_CULL_FACE); + + program.bind(); + program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, &scene); + triangle.setBuffer(); + for (const LLRect& rect : rects) + { + LLGLSScissor scissor(x + rect.mLeft, y + rect.mBottom, rect.getWidth(), rect.getHeight()); + triangle.drawArrays(LLRender::TRIANGLES, 0, 3); + } + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + } + + if (previous) + { + previous->bind(); + } + else + { + LLGLSLShader::unbind(); + } +} + +} // namespace ALHUDDepth diff --git a/indra/newview/alselectionoutline.cpp b/indra/newview/alselectionoutline.cpp new file mode 100644 index 0000000000..9984be28f6 --- /dev/null +++ b/indra/newview/alselectionoutline.cpp @@ -0,0 +1,706 @@ +/** + * @file alselectionoutline.cpp + * @brief Outlines of selected world objects, drawn on the GPU from an id target in the UI stage. + * + * $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 "llviewerprecompiledheaders.h" + +#include "alselectionoutline.h" + +#include "lldrawable.h" +#include "lldrawpool.h" +#include "llface.h" +#include "llfetchedgltfmaterial.h" +#include "llframetimer.h" +#include "llglstates.h" +#include "lltextureentry.h" +#include "llui.h" +#include "llviewercamera.h" +#include "llviewercontrol.h" +#include "llviewerdisplay.h" +#include "llviewerobject.h" +#include "llviewerregion.h" +#include "llviewershadermgr.h" +#include "llviewertexture.h" +#include "llvovolume.h" +#include "pipeline.h" + +#include + +namespace +{ + // Whether a texture's alpha can leave anything out, as LLPipeline::getPoolTypeFromTE reads it: media is drawn + // opaque whatever its format. + bool texture_has_alpha(const LLViewerTexture* texture) + { + const S8 components = texture->getComponents(); + return (components == 4 && texture->getType() != LLViewerTexture::MEDIA_TEXTURE) || components == 2; + } +} + +ALSelectionOutline::View ALSelectionOutline::worldView() +{ + static LLCachedControl contour_width(gSavedSettings, "AlchemySelectionOutlineWidth", DEFAULT_CONTOUR_WIDTH); + return makeView(LLViewerCamera::getCurrent().getModelview(), gGL.getProjectionMatrix(), contour_width, + LLUI::getScaleFactor().mV[VX], false, LLPipeline::RenderHighlightThickness, + LLPipeline::RenderHighlightBrightness); +} + +bool ALSelectionOutline::hudView(View& view) +{ + static LLCachedControl contour_width(gSavedSettings, "AlchemySelectionOutlineWidth", DEFAULT_CONTOUR_WIDTH); + LLMatrix4a projection; + LLMatrix4a modelview; + if (!get_hud_matrices(projection, modelview)) + { + return false; + } + view = makeView(modelview, projection, contour_width, LLUI::getScaleFactor().mV[VX], true, + LLPipeline::RenderHighlightThickness, LLPipeline::RenderHighlightBrightness); + return true; +} + +void ALSelectionOutline::add(LLViewerObject* object, U32 te_mask, const LLColor4& colour, bool show_hidden, + EPriority priority, U32 parts) +{ + if (!object) + { + return; + } + + // What glows is drawn with nothing else, and only where the scene shows it. + const Entry entry = { object, te_mask, colour, show_hidden && priority != PRIORITY_HOVER, priority, + partsFor(priority, parts) }; + + const auto found = mEntryIndex.find(object); + if (found != mEntryIndex.end()) + { + Entry& added = mEntries[found->second]; + if (replaces(added.mPriority, priority)) + { + added = entry; + } + return; + } + + if (mEntries.size() >= MAX_IDS) + { + return; + } + mEntryIndex.emplace(object, (U32)mEntries.size()); + mEntries.push_back(entry); +} + +void ALSelectionOutline::clearEntries() +{ + mEntries.clear(); + mEntryIndex.clear(); +} + +void ALSelectionOutline::release() +{ + if (mIdMap.isComplete()) + { + mIdMap.release(); + } + if (mJumpMap.isComplete()) + { + mJumpMap.release(); + } + if (mTileMap.isComplete()) + { + mTileMap.release(); + } + if (mGlowReachMap.isComplete()) + { + mGlowReachMap.release(); + } + if (mGlowRowMap.isComplete()) + { + mGlowRowMap.release(); + } + if (mPalette) + { + LLImageGL::deleteTextures(1, &mPalette); + mPalette = 0; + } + mPaletteRows = 0; +} + +void ALSelectionOutline::releaseStale(U32 frame, F64 now) +{ + // Called every frame, so it touches nothing that is not there. + if (mIdMap.isComplete() && stale(mIdUse.mFrame, mIdUse.mTime, frame, now)) + { + mIdMap.release(); + } + if (stale(mEdgeUse.mFrame, mEdgeUse.mTime, frame, now)) + { + if (mJumpMap.isComplete()) + { + mJumpMap.release(); + } + if (mTileMap.isComplete()) + { + mTileMap.release(); + } + } + if (stale(mGlowUse.mFrame, mGlowUse.mTime, frame, now)) + { + if (mGlowReachMap.isComplete()) + { + mGlowReachMap.release(); + } + if (mGlowRowMap.isComplete()) + { + mGlowRowMap.release(); + } + } + if (mPalette && stale(mPaletteUse.mFrame, mPaletteUse.mTime, frame, now)) + { + LLImageGL::deleteTextures(1, &mPalette); + mPalette = 0; + mPaletteRows = 0; + } +} + +void ALSelectionOutline::render(const View& view) +{ + const Use use = { LLFrameTimer::getFrameCount(), LLFrameTimer::getTotalSeconds() }; + if (mEntries.empty()) + { + releaseStale(use.mFrame, use.mTime); + return; + } + + LL_PROFILE_ZONE_SCOPED_CATEGORY_PIPELINE; + + // The window's viewport: the final blit filled it from the scene, the HUD is drawn over it, and the outlines + // are laid over both. + const S32 vp_x = gGLViewport[0]; + const S32 vp_y = gGLViewport[1]; + const S32 width = gGLViewport[2]; + const S32 height = gGLViewport[3]; + + Wants wants; + for (const Entry& entry : mEntries) + { + wants.mContour = wants.mContour || (entry.mParts & PART_CONTOUR); + wants.mWireframe = wants.mWireframe || (entry.mParts & PART_WIREFRAME); + wants.mGlow = wants.mGlow || entry.mPriority == PRIORITY_HOVER; + } + const Passes passes = passesFor(wants, view.mHUD, gSelectionWireframeProgram.isComplete() && + gSelectionWireframeProgram.mRiggedVariant != nullptr); + if (!passes.any() || width <= 0 || height <= 0 || !gPipeline.mRT || !gPipeline.mScreenTriangleVB || + (passes.mIds && (!gSelectionIdProgram.isComplete() || !gSelectionIdProgram.mRiggedVariant)) || + (passes.mEdges && (!gSelectionJumpProgram.isComplete() || !gSelectionTileProgram.isComplete() || + !gSelectionOutlineProgram.isComplete()))) + { + releaseStale(use.mFrame, use.mTime); + clearEntries(); + return; + } + + // The targets the passes that run read are made at the window's size; the others are given up once they are + // stale. + bool allocated = true; + if (passes.mIds) + { + if (!mIdMap.isComplete() || mIdMap.getWidth() != (U32)width || mIdMap.getHeight() != (U32)height) + { + allocated = mIdMap.allocate(width, height, GL_RGBA8, true, false, ALTextureSlot::TT_TEXTURE, + LLRenderTarget::MIPS_NONE, LLPipeline::mainDepthFormat()); + } + mIdUse = use; + } + if (passes.mEdges) + { + if (!mJumpMap.isComplete() || !mTileMap.isComplete() || mJumpMap.getWidth() != (U32)width || + mJumpMap.getHeight() != (U32)height) + { + allocated = allocated && mJumpMap.allocate(width, height, GL_R8) && + mTileMap.allocate(tileCount((U32)width), tileCount((U32)height), GL_RGBA16); + } + mEdgeUse = use; + } + // The glow is drawn where its passes' programs loaded; without them what glows is drawn with nothing. + const bool glow = passes.mEdges && wants.mGlow && gSelectionGlowReachProgram.isComplete() && + gSelectionGlowRowProgram.isComplete(); + if (glow) + { + if (!mGlowReachMap.isComplete() || !mGlowRowMap.isComplete() || mGlowRowMap.getWidth() != (U32)width || + mGlowRowMap.getHeight() != (U32)height) + { + allocated = allocated && mGlowReachMap.allocate(tileCount((U32)width), tileCount((U32)height), GL_RGBA16) && + mGlowRowMap.allocate(width, height, GL_R8); + } + mGlowUse = use; + } + mPaletteUse = use; + releaseStale(use.mFrame, use.mTime); + if (!allocated) + { + LL_WARNS_ONCE("Pipeline") << "Could not allocate the selection outline's targets" << LL_ENDL; + release(); + clearEntries(); + return; + } + + LL_PROFILE_GPU_ZONE("selection outline"); + + LLMatrix4a mvp; + mvp.setMul(view.mModelview, view.mProjection); + + // Ids in priority order: id i + 1 is mEntries[mOrder[i]]. + const U32 count = (U32)mEntries.size(); + mPriorities.resize(count); + for (U32 i = 0; i < count; ++i) + { + mPriorities[i] = mEntries[i].mPriority; + } + priorityOrder(mPriorities, mOrder); + + const U32 rows = paletteRows(count); + mPaletteTexels.assign((size_t)PALETTE_WIDTH * rows * 4, 0); + for (U32 i = 0; i < count; ++i) + { + const Entry& entry = mEntries[mOrder[i]]; + writePalette(mPaletteTexels, i + 1, entry.mColour, entry.mShowHidden); + } + + LLGLSLShader* previous = LLGLSLShader::sCurBoundShaderPtr; + gGL.flush(); + + mScissorValid = false; + mLastAvatar = nullptr; + mLastMeshId = 0; + mSkipLastSkin = false; + + if (passes.mIds) + { + drawIds(view, width, height, mvp); + } + + // What the id pass drew says whether the outline and the glow have anything to touch, and the HUD's wireframe, + // whose faces it drew too; the world's wireframe, drawn without it, draws whatever its faces put on screen. + const bool edges = passes.mEdges && mScissorValid; + const bool wireframe = passes.mWireframe && (!view.mHUD || mScissorValid); + if (edges || wireframe) + { + uploadPalette(mPalette, mPaletteRows, mPaletteTexels, rows); + + // The wireframe first, the outline over it. + if (wireframe) + { + drawWireframes(view, width, height); + } + + if (edges) + { + // The scissor holds every texel drawn and the reach of its line or glow around them; the edge pass reads + // no further, and what it reads past the jump and tile passes' work they clear to nothing, as it is. + gSelectionJumpProgram.bind(); + drawJumps(gSelectionJumpProgram, mIdMap, mJumpMap, mScissor, view, *gPipeline.mScreenTriangleVB); + gSelectionTileProgram.bind(); + drawTiles(gSelectionTileProgram, mIdMap, mJumpMap, mTileMap, mScissor, *gPipeline.mScreenTriangleVB); + if (glow) + { + gSelectionGlowReachProgram.bind(); + drawGlowReach(gSelectionGlowReachProgram, mTileMap, mGlowReachMap, mScissor, view, + *gPipeline.mScreenTriangleVB); + gSelectionGlowRowProgram.bind(); + drawGlowRows(gSelectionGlowRowProgram, mIdMap, mGlowReachMap, mGlowRowMap, mScissor, view, + *gPipeline.mScreenTriangleVB); + } + + LLGLSColorMask mask(true, false); + LLGLDepthTest depth(GL_FALSE); + LLGLEnable blend(GL_BLEND); + gGL.setSceneBlendType(LLRender::BT_ALPHA); + LLGLSScissor scissor(vp_x + mScissor.mLeft, vp_y + mScissor.mBottom, mScissor.getWidth(), + mScissor.getHeight()); + + gSelectionOutlineProgram.bind(); + drawEdges(gSelectionOutlineProgram, mIdMap, mJumpMap, mTileMap, glow ? &mGlowReachMap : nullptr, + glow ? &mGlowRowMap : nullptr, mPalette, view, *gPipeline.mScreenTriangleVB); + } + } + + if (previous) + { + previous->bind(); + } + else + { + LLGLSLShader::unbind(); + } + + clearEntries(); +} + +void ALSelectionOutline::drawIds(const View& view, S32 width, S32 height, const LLMatrix4a& mvp) +{ + const U32 count = (U32)mEntries.size(); + mIdMap.bindTarget(); + { + // Depth writes on before the clear, which they mask. + IdPassState state; + + gGL.setClearColor(LLColor4(0.f, 0.f, 0.f, 0.f)); + mIdMap.clear(); + + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.pushMatrix(); + gGL.loadMatrix(view.mProjection); + // A face's texture animation is loaded for its draw alone (bindFace); the others' is the identity. + gGL.matrixMode(LLRender::MM_TEXTURE0); + gGL.pushMatrix(); + gGL.loadIdentity(); + gGL.matrixMode(LLRender::MM_MODELVIEW); + gGL.pushMatrix(); + + // Static faces through the program, rigged ones through its rigged variant: the depth test, not the + // order, decides which surface each pixel keeps. + for (bool rigged : { false, true }) + { + if (rigged && view.mHUD) + { + break; + } + LLGLSLShader& program = rigged ? *gSelectionIdProgram.mRiggedVariant : gSelectionIdProgram; + gSelectionIdProgram.bind(rigged); + bindIdPass(program, view.mHUD ? nullptr : &gPipeline.mRT->deferredScreen, width, height); + for (U32 i = 0; i < count; ++i) + { + const Entry& entry = mEntries[mOrder[i]]; + if (inIdPass(entry.mParts, entry.mPriority, view.mHUD)) + { + drawObject(program, entry, i + 1, rigged, view, mvp); + } + } + program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + } + + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.popMatrix(); + gGL.matrixMode(LLRender::MM_TEXTURE0); + gGL.popMatrix(); + gGL.matrixMode(LLRender::MM_MODELVIEW); + gGL.popMatrix(); + } + mIdMap.flush(); +} + +template +bool ALSelectionOutline::drawFaces(LLGLSLShader& program, const Entry& entry, bool rigged, const View& view, FaceBox& box, + Begin&& begin) +{ + LLViewerObject* object = entry.mObject; + LLDrawable* drawable = object->mDrawable; + if (!drawable || drawable->isDead() || !drawable->getVOVolume()) + { + return false; + } + + // The frame each face's vertex buffer is in, as LLVolumeGeometryManager::registerFace places it. Rigged faces + // are skinned into agent space and take none. + const LLMatrix4a* model = nullptr; + bool agent_extents = false; + if (!rigged) + { + if (drawable->isState(LLDrawable::ANIMATED_CHILD)) + { + model = &drawable->getWorldMatrix(); + } + else if (drawable->isActive()) + { + model = &drawable->getRenderMatrix(); + } + else if (drawable->getRegion()) + { + model = &drawable->getRegion()->mRenderMatrix; + // A static face's box already has the region's origin in it (LLFace::genVolumeBBoxes). + agent_extents = true; + } + else + { + return false; + } + } + + bool drawn = false; + + // The TE mask holds 32 faces, more than a volume has. + const S32 faces = llmin(llmin((S32)object->getNumTEs(), drawable->getNumFaces()), 32); + for (S32 te = 0; te < faces; ++te) + { + if (!(entry.mTEMask & (1u << te))) + { + continue; + } + + LLFace* face = drawable->getFace(te); + if (!face || face->isState(LLFace::RIGGED) != rigged) + { + continue; + } + + LLVertexBuffer* buffer = face->getVertexBuffer(); + if (!buffer || face->getGeomCount() == 0 || face->getIndicesCount() == 0) + { + continue; + } + + LLVector4a extents[2]; + if (rigged) + { + if (!face->mAvatar || !face->mSkinInfo || + !LLRenderPass::uploadMatrixPalette(face->mAvatar, face->mSkinInfo, mLastAvatar, mLastMeshId, mSkipLastSkin)) + { + continue; + } + // Joint boxes, refreshed while the face is in view (LLFace::calcPixelArea); empty until they are. + extents[0] = face->mRiggedExtents[0]; + extents[1] = face->mRiggedExtents[1]; + LLVector4a size; + size.setSub(extents[1], extents[0]); + if (!(size[0] > 0.f && size[1] > 0.f && size[2] > 0.f)) + { + box.mBounded = false; + } + } + else if (agent_extents) + { + extents[0] = face->mExtents[0]; + extents[1] = face->mExtents[1]; + } + else + { + matMulBoundBox(*model, face->mExtents, extents); + } + box.mLo.setMin(box.mLo, extents[0]); + box.mHi.setMax(box.mHi, extents[1]); + + if (!drawn) + { + gGL.loadMatrix(view.mModelview); + if (model) + { + gGL.multMatrix(model->getF32ptr()); + } + begin(); + drawn = true; + } + + const FaceBinding binding = bindFace(program, face); + drawFace(*buffer, face->getGeomIndex(), face->getGeomIndex() + face->getGeomCount() - 1, face->getIndicesCount(), + face->getIndicesStart(), binding.mDoubleSided); + + if (binding.mAnimated) + { + gGL.matrixMode(LLRender::MM_TEXTURE0); + gGL.loadIdentity(); + gGL.matrixMode(LLRender::MM_MODELVIEW); + } + } + return drawn; +} + +void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U32 id, bool rigged, const View& view, + const LLMatrix4a& mvp) +{ + // An object drawn for the HUD wireframe's depth alone is not outlined: it hides what lies behind it and draws no + // line of its own. + const bool drawn = outlined(entry.mParts, entry.mPriority); + FaceBox box; + if (!drawFaces(program, entry, rigged, view, box, + [&] { setId(program, id, entry.mPriority, entry.mShowHidden, drawn); })) + { + return; + } + + const S32 width = (S32)mIdMap.getWidth(); + const S32 height = (S32)mIdMap.getHeight(); + LLRect rect; + if (!box.mBounded) + { + rect.set(0, height, width, 0); + } + else + { + const LLVector4a extents[2] = { box.mLo, box.mHi }; + const S32 reach = lineReach(entry.mPriority == PRIORITY_HOVER ? view.mGlowRadius : view.mWidth); + if (!scissorRect(mvp, extents, width, height, reach, rect)) + { + return; + } + } + + if (mScissorValid) + { + mScissor.unionWith(rect); + } + else + { + mScissor = rect; + mScissorValid = true; + } +} + +void ALSelectionOutline::drawWireframes(const View& view, S32 width, S32 height) +{ + LL_PROFILE_GPU_ZONE("selection wireframe"); + + // The window's depth is the final blit's point-sampled copy of the scene's, taken a render texel at a time. + F32 resample = 1.f; + if (!view.mHUD && gPipeline.mRT->deferredScreen.getWidth() > 0) + { + resample = (F32)width / (F32)gPipeline.mRT->deferredScreen.getWidth(); + } + const F32 offset_factor = wireOffsetFactor(resample); + + bool any_hidden = false; + for (const Entry& entry : mEntries) + { + any_hidden = any_hidden || inWirePass(entry.mParts, entry.mShowHidden, WIRE_HIDDEN); + } + + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.pushMatrix(); + gGL.loadMatrix(view.mProjection); + gGL.matrixMode(LLRender::MM_TEXTURE0); + gGL.pushMatrix(); + gGL.loadIdentity(); + gGL.matrixMode(LLRender::MM_MODELVIEW); + gGL.pushMatrix(); + + const EWirePass world_passes[] = { WIRE_VISIBLE, WIRE_HIDDEN }; + const EWirePass hud_passes[] = { WIRE_ID_DEPTH }; + const EWirePass* passes = view.mHUD ? hud_passes : world_passes; + const U32 pass_count = view.mHUD ? 1 : 2; + for (U32 p = 0; p < pass_count; ++p) + { + const EWirePass pass = passes[p]; + if (pass == WIRE_HIDDEN && !any_hidden) + { + continue; + } + + WireState state(pass, offset_factor); + for (bool rigged : { false, true }) + { + if (rigged && view.mHUD) + { + break; + } + LLGLSLShader& program = rigged ? *gSelectionWireframeProgram.mRiggedVariant : gSelectionWireframeProgram; + gSelectionWireframeProgram.bind(rigged); + bindWirePass(program, pass, view.mWireWidth, width, height, mPalette, &mIdMap); + // Joint palettes are uploaded to the bound program, which this one is not yet. + mLastAvatar = nullptr; + mLastMeshId = 0; + mSkipLastSkin = false; + for (U32 i = 0; i < (U32)mEntries.size(); ++i) + { + // What glows, or is outlined alone, has no wireframe. + const Entry& entry = mEntries[mOrder[i]]; + if (!inWirePass(entry.mParts, entry.mShowHidden, pass)) + { + continue; + } + FaceBox box; + drawFaces(program, entry, rigged, view, box, [&] { setId(program, i + 1, entry.mPriority, entry.mShowHidden); }); + } + program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_PALETTE); + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + } + } + + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.popMatrix(); + gGL.matrixMode(LLRender::MM_TEXTURE0); + gGL.popMatrix(); + gGL.matrixMode(LLRender::MM_MODELVIEW); + gGL.popMatrix(); +} + +ALSelectionOutline::FaceBinding ALSelectionOutline::bindFace(LLGLSLShader& program, LLFace* face) +{ + // Only the texture's alpha is read, never the face's colour: a prim made invisible by its transparency is + // still outlined where its texture is there. + const LLTextureEntry* te = face->getTextureEntry(); + LLViewerTexture* texture = face->getTexture(); + F32 cutoff = NO_ALPHA_TEST; + LLGLTFMaterial::TextureTransform::Pack transform; + std::copy(std::begin(IDENTITY_TRANSFORM), std::end(IDENTITY_TRANSFORM), std::begin(transform)); + FaceBinding binding; + + auto* gltf = te ? static_cast(te->getGLTFRenderMaterial()) : nullptr; + if (gltf) + { + // As LLRenderPass::pushGLTFBatch culls it, media or not. + binding.mDoubleSided = gltf->mDoubleSided; + + // The base colour, unless the face plays media, which LLVolumeGeometryManager::registerFace hands + // LLFetchedGLTFMaterial::bind in its place. + if (!face->hasMedia() || !texture || texture->getType() != LLViewerTexture::MEDIA_TEXTURE) + { + texture = gltf->mBaseColorTexture.get(); + } + if (texture && texture_has_alpha(texture)) + { + cutoff = faceAlphaCutoff(texture->getID(), gltfAlphaCutoff(gltf->mAlphaMode, gltf->mAlphaCutoff)); + } + gltf->mTextureTransform[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR].getPacked(transform); + } + else if (te && texture) + { + // The diffuse map as the face names it, whatever stands in for it while it loads. + const LLMaterial* material = te->getMaterialParams().get(); + cutoff = faceAlphaCutoff(te->getID(), + legacyAlphaCutoff(material != nullptr, material ? material->getDiffuseAlphaMode() : 0, + material ? material->getAlphaMaskCutoff() : 0, + texture_has_alpha(texture))); + } + + setAlphaTest(program, cutoff, transform); + if (cutoff <= 0.f) + { + return binding; + } + + // Sampled as LLRenderPass::pushBatch samples the diffuse map; alpha is the same with or without sRGB decode. + program.bindTexture(LLShaderMgr::DIFFUSE_MAP, texture, ALSamplers::AnisoWrap); + + // A texture animation the face is drawn with rather than baked into its texture coordinates, as + // LLVolumeGeometryManager::registerFace hands it to the draw. + if (face->mTextureMatrix && face->isState(LLFace::TEXTURE_ANIM) && face->getVirtualSize() > MIN_TEX_ANIM_SIZE) + { + gGL.matrixMode(LLRender::MM_TEXTURE0); + gGL.loadMatrix((const GLfloat*)face->mTextureMatrix->mMatrix); + gGL.matrixMode(LLRender::MM_MODELVIEW); + binding.mAnimated = true; + } + return binding; +} diff --git a/indra/newview/alselectionoutline.h b/indra/newview/alselectionoutline.h new file mode 100644 index 0000000000..f846c000df --- /dev/null +++ b/indra/newview/alselectionoutline.h @@ -0,0 +1,1012 @@ +/** + * @file alselectionoutline.h + * @brief Outlines of selected world objects, drawn on the GPU from an id target in the UI stage. + * + * $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$ + */ + +#pragma once + +#include "indra_constants.h" +#include "llgltfmaterial.h" +#include "llglslshader.h" +#include "llglstates.h" +#include "llimagegl.h" +#include "llmaterial.h" +#include "llmath.h" +#include "llmatrix4a.h" +#include "llrect.h" +#include "llrender.h" +#include "llrendertarget.h" +#include "llshadermgr.h" +#include "llsingleton.h" +#include "lluuid.h" +#include "llvertexbuffer.h" +#include "v4color.h" + +#include + +#include +#include +#include +#include + +class LLFace; +class LLViewerObject; +class LLVOAvatar; + +/// Outlines of the selected, rect-highlighted and media-focused objects, and the glow of what the pointer is over +/// (ALHoverGlow), drawn by LLSelectMgr::renderSilhouettes over the window's framebuffer: world objects after the +/// final blit, with the world camera, and HUD objects after the HUD, with its matrices. Each object asks for its +/// contour, its wireframe or both, as their settings are, or glows (PRIORITY_HOVER); the passes a frame runs are +/// the ones its objects ask for (passesFor). +/// +/// The id pass (interface/selectionIdV.glsl, selectionIdF.glsl) draws each object's selected faces, rigged ones +/// through the program's rigged variant, into a window-sized RGBA8 target with a depth buffer of its own, which +/// keeps the nearest selected surface: the object's id in .rg, 16 bits with 0 for none, in .b whether the outline +/// draws the surface (1 where the scene shows it, 0.5 where the scene hides it and hidden parts are drawn dimmed, 0 +/// where they are left out), and in .a its priority. Faces are culled as the scene culls them, back faces unless a +/// GLTF material is double-sided, so a face the scene does not draw takes no pixel. Where a face's alpha mode cuts +/// it out by its texture's alpha, the fragments are discarded, so the outline follows what is drawn rather than +/// the cards it is drawn on; the face's colour, whose alpha makes a prim invisible, is not read, and a face wearing +/// the library's transparent texture is drawn whole (faceAlphaCutoff). Integer formats are not used because +/// LLGLSLShader gives integer samplers no unit. +/// +/// Ids are given in priority order, the highest first (EPriority). The jump pass (interface/selectionJumpF.glsl) +/// marks, in a one-byte target of its own, the texels of the id target on the near side of a jump: a step in an +/// object's own surface between two neighbours, as where one frond of a mesh lies over another. It is told from a +/// crease or a curve, whose step lies between the slopes either side, by a step outside both, which carried on at +/// its own slope each side misses by more than JUMP_MARGIN (in the shader) of the distance. +/// +/// The edge pass (interface/selectionOutlineF.glsl) is a full-screen triangle, scissored to the objects' projected +/// boxes grown by the reach of their contour or glow. A pixel draws the contour of the drawn surfaces within its reach that stand +/// in front of it: every object, off every object; on an object, another object, or the near side of a jump in its +/// own, whose surface is nearer than the plane of the pixel's own, read from the id target's depth and carried over +/// to the other's texel. The contour is a solid line View::mWidth pixels wide ending in a pixel of anti-aliasing, +/// from the distance to the nearest texel of each object in the disk it reaches: the highest priority's colour +/// within reach over the nearest object's. Where two objects touch, neither in front, the edge between them is +/// drawn thinner (View::mInnerWidth) and fainter (INNER_OPACITY), on the side of the one of higher priority if it is +/// drawn and on the drawn side if not, in that side's colour. An object over its own surface draws nothing but at +/// its jumps: anywhere else that would trace the creases of every mesh. Colours come from a palette holding a +/// visible and a hidden colour per id. +/// +/// An object at PRIORITY_HOVER is not outlined but glows: a soft halo around its visible surface reaching +/// View::mGlowRadius pixels past it (glowOpacity), over what lies behind it as the contour is and never over the +/// object itself, under every other object's contour and edges. Its hidden parts are left out, and it draws no edge +/// with what it touches: the outline of a selected object beside it is drawn as though it were not there, except +/// where it stands in front. +/// +/// The wireframe (AlchemySelectionWireframe) is drawn first, the outline over it: the same faces through the same +/// face walk, alpha cut and culling as the id pass (drawFaces), their triangles' edges drawn as lines View::mWireWidth +/// pixels wide from window-space edge distances a geometry stage gives each fragment (selectionWireframeG.glsl, +/// selectionWireframeF.glsl). In the world the window's depth is the scene's, which the final blit wrote: lines in +/// front of it are drawn, pulled in front of their own surface by a polygon offset, and lines behind it in the +/// hidden colour. On the HUD the window's depth also holds the scene's wherever that is the nearer, so the lines +/// are tested against the id target's depth instead, and a line behind another selected surface is hidden. +/// +/// The disk a pixel searches grows with the square of the contour's width, and most pixels in the scissor are far +/// from anything to draw. The tile pass (interface/selectionTileF.glsl) records for each TILE_SIZE square of the +/// id target the lowest and highest nonzero id it holds, whether it holds the near side of a jump and whether +/// it holds the drawn surface of an object that glows. The edge pass leaves a pixel at once when no tile within its +/// reach holds what it could draw from: any id, off every object; on one, an id not its own or a jump. Whatever a +/// pixel draws, it draws from such a texel within its reach, whose own tile records it, so this changes nothing it +/// draws. +/// +/// The glow reaches farther than the lines, and its disk would be searched by every pixel near a glowing object. +/// Where something glows, the glow reach pass (interface/selectionGlowReachF.glsl) records for each tile the +/// lowest and highest id of the tiles holding a glowing surface within the glow's reach of it, so a pixel learns +/// from one texel whether a glow other than its own object's can be near; and the glow row pass +/// (interface/selectionGlowRowF.glsl) records for each texel how far along its row the nearest glowing texel lies. +/// Off every object, where every glowing surface stands in front, the nearest glowing texel in the disk is the +/// nearest of those a row, which the edge pass reads up and down its column; on an object, which stand in front +/// depends on the object's plane, and the disk is searched. Both give what the full search gives (GLOW_DISK in +/// selectionOutlineF.glsl), and the jump pass leaves out what glows, whose own surface draws nothing. +/// +/// The targets exist only while something is drawn: they are made the first frame there is, given up once a frame +/// and TARGET_KEEP_SECONDS have passed without a call drawing with them (the world calls render every frame and the +/// HUD while the selection is on it, so one call can have nothing to draw while the other draws; and the pointer +/// passes from one glowing object to the next with frames between), and given up with the pipeline's own +/// (LLPipeline::releaseGLBuffers). +class ALSelectionOutline : public LLSingleton +{ + LLSINGLETON_EMPTY_CTOR(ALSelectionOutline); + +public: + /// What an outline is for, the highest priority first. Where two objects' outlines meet, the higher one's + /// contour is laid over the lower one's, and the edge between them is drawn on its side. + enum EPriority : U8 + { + /// What a control-drag takes out of the selection. + PRIORITY_SUBTRACT = 0, + /// The object media is focused on. + PRIORITY_FOCUS, + /// The object picked in the inspect floater or the object panel's contents. + PRIORITY_INSPECT, + /// A selected root, or a root a drag would select. + PRIORITY_ROOT, + /// A selected child, or a child a drag would select. + PRIORITY_CHILD, + /// A transient selection's. + PRIORITY_CONTEXT, + /// What the pointer is over (ALHoverGlow): it glows instead of being outlined, under every other object's + /// lines (PRIORITY_HOVER in selectionUtilF.glsl). + PRIORITY_HOVER, + PRIORITY_COUNT + }; + + /// What an object outlined above PRIORITY_HOVER is drawn with, as a mask. + enum EPart : U32 + { + /// Its contour, and the edges between it and what it touches (RenderHighlightSelections). + PART_CONTOUR = 1 << 0, + /// Its faces' triangles as lines (AlchemySelectionWireframe). + PART_WIREFRAME = 1 << 1, + }; + + /// What render() draws with. + struct View + { + /// From the frame object positions are in to the eye, and the eye to clip space. + LLMatrix4a mModelview; + LLMatrix4a mProjection; + /// The widths in pixels of the contour around the objects and of the edges between them, each ending in a + /// further pixel of anti-aliasing. + S32 mWidth = 1; + S32 mInnerWidth = 1; + /// The HUD: drawn into the window's framebuffer, whose depth cannot be read, so every surface counts as + /// visible; and its rigged faces are left out, since their joints place them about the avatar in the + /// world, which the HUD's matrices have no place for. + bool mHUD = false; + /// The wireframe's lines' width in pixels before their pixel of anti-aliasing. + S32 mWireWidth = 1; + /// How far in pixels the glow reaches past its object's silhouette (glowRadius), and its brightness + /// (RenderHighlightBrightness), which glowOpacity scales it by. + S32 mGlowRadius = 1; + F32 mGlowBrightness = 0.f; + }; + + /// What the objects of a frame ask for between them. + struct Wants + { + /// A contour (PART_CONTOUR). + bool mContour = false; + /// A wireframe (PART_WIREFRAME). + bool mWireframe = false; + /// A glow (PRIORITY_HOVER). + bool mGlow = false; + }; + + /// The passes render() runs for a frame. + struct Passes + { + /// The id pass: the outline's and the glow's ids, and the depth of the selected surfaces the HUD's lines + /// are tested against. + bool mIds = false; + /// The wireframe. + bool mWireframe = false; + /// The jump, tile and edge passes, which draw the outline and the glow from the ids. + bool mEdges = false; + + bool any() const { return mIds || mWireframe || mEdges; } + }; + + /// The passes a frame whose objects ask for `wants` needs, in the world or on the HUD (`hud`), the wireframe + /// only where its program loaded (`wireframe_ready`). The world's lines are tested against the window's depth + /// and need no id pass; the HUD's against the id target's. + static Passes passesFor(const Wants& wants, bool hud, bool wireframe_ready); + + /// What an object added asking for `parts` at `priority` is drawn with (add): at PRIORITY_HOVER nothing but its + /// glow. + static U32 partsFor(EPriority priority, U32 parts) { return priority == PRIORITY_HOVER ? 0u : parts; } + + /// Whether adding an object again at `adding` replaces what it was added with at `added` (add): at a priority as + /// high or higher. What the pointer is over glows only where it is not outlined. + static bool replaces(EPriority added, EPriority adding) { return adding <= added; } + + /// Whether the outline or the glow draws an object drawn with `parts` (partsFor) at `priority`. + static bool outlined(U32 parts, EPriority priority) { return priority == PRIORITY_HOVER || (parts & PART_CONTOUR); } + + /// Whether such an object is drawn into the id pass: where it is outlined or glows, and on the HUD for its + /// wireframe, whose lines are tested against the id target's depth. + static bool inIdPass(U32 parts, EPriority priority, bool hud) + { + return outlined(parts, priority) || (hud && (parts & PART_WIREFRAME)); + } + + /// The world camera's, in the UI stage, where the scene's projection is still loaded. + static View worldView(); + + /// The HUD's, as its attachments were drawn with. False without a HUD to draw. + static bool hudView(View& view); + + /// A view with these matrices whose contour is `contour_width` pixels wide at a UI scale of 1 + /// (AlchemySelectionOutlineWidth), drawn at `ui_scale`, LLUI's scale factor: the same widths in the world and + /// on the HUD. Its glow is `glow_thickness` (RenderHighlightThickness, glowRadius) and `glow_brightness` + /// (RenderHighlightBrightness), held to 0 to MAX_GLOW_BRIGHTNESS, and to its default where it is not a number. + static View makeView(const LLMatrix4a& modelview, const LLMatrix4a& projection, F32 contour_width, F32 ui_scale, + bool hud, F32 glow_thickness = DEFAULT_GLOW_THICKNESS, + F32 glow_brightness = DEFAULT_GLOW_BRIGHTNESS); + + /// Ids the target can tell apart; 0 is no selected object. + static constexpr U32 MAX_IDS = 65535; + /// Ids to a pair of palette rows: their visible colours, then their hidden ones. + static constexpr U32 PALETTE_WIDTH = 256; + /// What a hidden part keeps of its outline's opacity. + static constexpr F32 HIDDEN_ALPHA = 0.4f; + /// The contour's width in pixels at a UI scale of 1: AlchemySelectionOutlineWidth's default, and the range it + /// is held to. + static constexpr F32 DEFAULT_CONTOUR_WIDTH = 2.f; + static constexpr F32 MIN_CONTOUR_WIDTH = 1.f; + static constexpr F32 MAX_CONTOUR_WIDTH = 16.f; + /// The width of the edges between objects in pixels at a UI scale of 1. It does not follow the contour's: the + /// edges mark where prims meet inside a selection, and as wide as the contour they would cover small prims. + static constexpr F32 INNER_WIDTH = 1.f; + /// The side of the id target's squares the tile pass records, in texels (TILE_SIZE in selectionTileF.glsl and + /// selectionOutlineF.glsl). + static constexpr U32 TILE_SIZE = 8; + /// What an edge between two objects keeps of its object's outline opacity (INNER_OPACITY in + /// selectionOutlineF.glsl). + static constexpr F32 INNER_OPACITY = 0.6f; + /// The texture alpha an alpha-blended face is outlined from: wherever it is drawn at all, so a uniformly + /// translucent texture, as glass is, keeps its face, while the clear texels around a leaf on its card are cut. + static constexpr F32 BLEND_ALPHA_CUTOFF = 0.1f; + /// A face whose texture is not read: every fragment is outlined. + static constexpr F32 NO_ALPHA_TEST = -1.f; + /// The base colour transform of a face whose texture coordinates have their transform in them. + static constexpr LLGLTFMaterial::TextureTransform::Pack IDENTITY_TRANSFORM = { 1.f, 1.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f }; + /// The wireframe's lines' width in pixels at a UI scale of 1. + static constexpr F32 WIRE_WIDTH = 1.f; + /// How far the wireframe's lines are drawn in front of their own surface beyond its slope, in the depth + /// buffer's least steps (glPolygonOffset's units). + static constexpr F32 WIRE_OFFSET_UNITS = 4.f; + /// The glow's reach in pixels at a UI scale of 1 for each unit of RenderHighlightThickness: 9 at its default, + /// 0.6. LL's glow blurred a 256-texel-wide copy of the object over 8 x 8 taps 3.5 x thickness texels out at + /// most, a reach that grew with the window, about 16 pixels across a window 1920 wide; this one keeps its width + /// in pixels whatever the window's. + static constexpr F32 GLOW_PIXELS = 15.f; + /// RenderHighlightThickness's and RenderHighlightBrightness's defaults. + static constexpr F32 DEFAULT_GLOW_THICKNESS = 0.6f; + static constexpr F32 DEFAULT_GLOW_BRIGHTNESS = 4.f; + /// The glow's reach in pixels at a UI scale of 1 is held to this, the contour's widest: the edge pass searches + /// a disk that wide around the pixels on an object near a glowing one. + static constexpr F32 MAX_GLOW_RADIUS = 16.f; + /// The glow's reach in pixels is held to this at any UI scale: the glow row pass records a distance one past the + /// reach, and one more than that, in a byte. + static constexpr S32 MAX_GLOW_PIXELS = 253; + static constexpr F32 MAX_GLOW_BRIGHTNESS = 64.f; + /// The glow's profile at its object's silhouette, before the brightness: the default brightness lays it on + /// opaque there, as LL's glow did (GLOW_PEAK in selectionOutlineF.glsl). + static constexpr F32 GLOW_PEAK = 0.25f; + + /// What decides which of the wireframe's lines are hidden (wireframe_pass in selectionWireframeF.glsl). + enum EWirePass : S32 + { + /// In the world, the lines the window's depth, the scene's, passes as in front: drawn in their visible + /// colour. + WIRE_VISIBLE = 0, + /// In the world, the lines it passes as behind: drawn in their hidden colour, dimmed or left out. + WIRE_HIDDEN = 1, + /// On the HUD, where the window's depth also holds the scene's, wherever it is the nearer: the id target's + /// depth, the nearest selected surface, decides, and a line behind another selected surface is hidden. + WIRE_ID_DEPTH = 2, + }; + + /// Whether an object drawn with `parts` (partsFor), its hidden parts dimmed where `show_hidden`, is drawn in + /// wireframe pass `pass`: with a wireframe, and in WIRE_HIDDEN only where its hidden parts are drawn. + static bool inWirePass(U32 parts, bool show_hidden, EWirePass pass) + { + return (parts & PART_WIREFRAME) && (pass != WIRE_HIDDEN || show_hidden); + } + + /// The GL state a wireframe pass draws in while it lives, over the window's framebuffer: colour blended in + /// over its alpha, no depth written, back faces culled as the id pass culls them (drawFace), and for the + /// world's passes the depth test, with the lines pulled in front of their own surface by `offset_factor` + /// (glPolygonOffset's factor, wireOffsetFactor) and WIRE_OFFSET_UNITS. gGL.setPolygonOffset turns the pull + /// toward the eye under either depth convention. + class WireState + { + public: + WireState(EWirePass pass, F32 offset_factor); + ~WireState(); + + WireState(const WireState&) = delete; + WireState& operator=(const WireState&) = delete; + + private: + LLGLSColorMask mMask{ true, false }; + LLGLEnable mCull{ GL_CULL_FACE }; + LLGLEnable mBlend{ GL_BLEND }; + LLGLDepthTest mDepth; + LLGLEnable mOffset; + }; + + /// The polygon offset factor the wireframe's lines need in front of a window depth point-sampled from a scene + /// `resample` window pixels to its texel: a surface's depth there was taken up to that many pixels away along + /// its slope, and a pixel more for the line's own. + static F32 wireOffsetFactor(F32 resample) { return llmax(resample, 1.f) + 1.f; } + + /// Outlines `object` this frame with `parts` (EPart): its faces whose bit is set in te_mask, in `colour`, whose + /// alpha is the outline's opacity, at `priority`. Parts the scene hides are dimmed when show_hidden and left out + /// otherwise. At PRIORITY_HOVER the object glows in `colour` instead, its hidden parts left out, whatever + /// `parts` and show_hidden say. Adding an object again keeps what it was added with at the higher priority, and + /// at the same priority the later. + void add(LLViewerObject* object, U32 te_mask, const LLColor4& colour, bool show_hidden, EPriority priority, + U32 parts); + + /// Draws what was added since the last call over the bound framebuffer, with `view`, and forgets it, giving up + /// the targets that are stale (stale). + void render(const View& view); + + /// Gives up the targets. + void release(); + + /// How long the targets are kept after a call last drew with them: long enough for the pointer to pass from one + /// glowing object to the next without their being made again. + static constexpr F64 TARGET_KEEP_SECONDS = 2.0; + + /// Whether a target last drawn with in frame `used_frame` (LLFrameTimer::getFrameCount), at `used_at` seconds + /// (LLFrameTimer::getTotalSeconds), is given up in frame `frame` at `now`: once a whole frame has passed without + /// it, and TARGET_KEEP_SECONDS. + static bool stale(U32 used_frame, F64 used_at, U32 frame, F64 now) + { + return frame - used_frame > 1 && now - used_at >= TARGET_KEEP_SECONDS; + } + + /// Palette rows for ids 1 to `count`, and the id 0 that is never drawn. + static U32 paletteRows(U32 count) { return 2 * (count / PALETTE_WIDTH + 1); } + + /// Writes id's visible and hidden texels into an RGBA8 palette PALETTE_WIDTH texels wide. + static void writePalette(std::vector& texels, U32 id, const LLColor4& colour, bool show_hidden); + + /// A line `width` pixels wide at a UI scale of 1, at `ui_scale`: at least a pixel. + static S32 lineWidth(F32 width, F32 ui_scale) { return llmax(1, ll_round(width * ui_scale)); } + + /// The contour's width in pixels for the setting `width` at `ui_scale`, the setting held to MIN_CONTOUR_WIDTH + /// to MAX_CONTOUR_WIDTH, and to the least where it is not a number. + static S32 contourWidth(F32 width, F32 ui_scale); + + /// How far in pixels the glow reaches past its object's silhouette for RenderHighlightThickness `thickness` at + /// `ui_scale`: thickness x GLOW_PIXELS at a UI scale of 1, held to MAX_GLOW_RADIUS, and its default where the + /// setting is not a number; a pixel at least, and MAX_GLOW_PIXELS at most. + static S32 glowRadius(F32 thickness, F32 ui_scale); + + /// The glow's opacity before its colour's alpha `dist` pixels from the centre of its object's nearest texel, its + /// silhouette half a texel out: brightness x GLOW_PEAK x (1 - smoothstep(0, 1, x)), x the distance past the + /// silhouette over `radius`, held to 1. It falls smoothly to nothing at `radius` past the silhouette, and the + /// brightness keeps a band of it opaque at the silhouette where it is over 1 / GLOW_PEAK. As the edge pass + /// computes it. + static F32 glowOpacity(F32 dist, S32 radius, F32 brightness); + + /// The texture alpha below which a face whose texture is `texture_id` is not outlined, from `cutoff`, what its + /// alpha mode gives (legacyAlphaCutoff, gltfAlphaCutoff): for the library's transparent texture + /// (IMG_TRANSPARENT) NO_ALPHA_TEST, so the face is drawn whole. Builders put it on roots, collision and touch + /// prims to hide them while they can still be picked and edited, and its alpha would cut every fragment. + static F32 faceAlphaCutoff(const LLUUID& texture_id, F32 cutoff) + { + return texture_id == IMG_TRANSPARENT ? NO_ALPHA_TEST : cutoff; + } + + /// Tiles across `pixels` texels of the id target. + static U32 tileCount(U32 pixels) { return (pixels + TILE_SIZE - 1) / TILE_SIZE; } + + /// How far a line `width` pixels wide reaches from the edge it is drawn along: its width and the pixel its + /// anti-aliasing takes. + static S32 lineReach(S32 width) { return llmax(width, 1) + 1; } + + /// The order ids are given in, as indices into `priorities`: the highest priority first, and in the order + /// they were added within one. + static void priorityOrder(const std::vector& priorities, std::vector& order); + + /// The texture alpha below which a legacy face is not outlined, or NO_ALPHA_TEST. Its material's alpha mode + /// decides: masks at their cutoff (0 to 255), blends at BLEND_ALPHA_CUTOFF, none and emissive not at all. + /// Without a material, a texture with alpha is blended, as the face is drawn. + static F32 legacyAlphaCutoff(bool has_material, U8 diffuse_alpha_mode, U8 mask_cutoff, bool texture_alpha); + + /// The same for a GLTF face from its material's alpha mode and cutoff. + static F32 gltfAlphaCutoff(S32 alpha_mode, F32 alpha_cutoff); + + /// The pixels of a width x height target the outline of a box can touch: its projection through `mvp` grown + /// by `reach`, clipped to the target. A box reaching behind the eye takes the whole target. + /// False when the box cannot touch the target. + static bool scissorRect(const LLMatrix4a& mvp, const LLVector4a extents[2], S32 width, S32 height, S32 reach, + LLRect& rect); + + /// Makes `palette` hold at least `rows` rows, recreating it when it holds fewer, and uploads them. + static void uploadPalette(U32& palette, U32& palette_rows, const std::vector& texels, U32 rows); + + /// Binds what the id pass reads to the bound id program: the target's size, and the scene's depth, which + /// decides what is hidden; with no scene every surface counts as visible. + static void bindIdPass(LLGLSLShader& program, LLRenderTarget* scene, S32 width, S32 height); + + /// The GL state the id pass draws in while it lives: colour and depth written, the depth test keeping the + /// nearest surface, back faces culled as the scene culls them (the main view keeps GL_BACK and + /// counter-clockwise fronts), no blending and no scissor. + struct IdPassState + { + LLGLSColorMask mMask{ true, true }; + LLGLDepthTest mDepth{ GL_TRUE, GL_TRUE, GL_LEQUAL }; + LLGLEnable mCull{ GL_CULL_FACE }; + LLGLDisable mBlend{ GL_BLEND }; + LLGLDisable mScissor{ GL_SCISSOR_TEST }; + }; + + /// The id, priority and hidden parts the bound id program writes for the faces drawn next. Faces not + /// `outlined`, drawn for the depth of the HUD's wireframe alone, are written as drawn nowhere: in the id target + /// they hide what lies behind them, and draw no line and mask no other's. + static void setId(LLGLSLShader& program, U32 id, EPriority priority, bool show_hidden, bool outlined = true); + + /// Draws a face into the id pass from `buffer`'s bound range, culled as the scene culls it: both sides of a + /// double-sided GLTF material, the front alone of everything else (LLRenderPass::pushGLTFBatch). + static void drawFace(LLVertexBuffer& buffer, U32 start, U32 end, U32 count, U32 offset, bool double_sided); + + /// How the bound id program tests the alpha of the face drawn next, whose texture is bound as its diffuse map: + /// fragments under `cutoff` are discarded, and with NO_ALPHA_TEST the texture is not read. `transform` is the + /// base colour's texture transform, packed as LLGLTFMaterial::TextureTransform::getPacked packs it, the + /// identity for a legacy face, whose texture coordinates have their transform in them. + static void setAlphaTest(LLGLSLShader& program, F32 cutoff, const LLGLTFMaterial::TextureTransform::Pack& transform); + + /// The jump pass with the bound jump program: into `jumps`, R8 at the size of `ids`, the id target, 1 on the + /// texels on the near side of a jump and 0 elsewhere, over the texels under `rect`; the others are cleared, as + /// `ids` holds nothing there when the rect covers every selected texel. Its depth is read through `view`'s + /// projection, which the id pass drew with. + static void drawJumps(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& jumps, const LLRect& rect, + const View& view, LLVertexBuffer& triangle); + + /// The tile pass with the bound tile program: into `tiles`, RGBA16, a texel to each TILE_SIZE square of `ids`, + /// the id target, and of `jumps`, the jump pass's marks, over the tiles under `rect`, a rect of `ids`; the others + /// are cleared, and hold nothing when the rect covers every selected texel. + static void drawTiles(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& jumps, LLRenderTarget& tiles, + const LLRect& rect, LLVertexBuffer& triangle); + + /// The tiles of the id target a rect of its texels touches. + static LLRect tileRect(const LLRect& rect); + + /// The glow reach pass with the bound glow reach program: into `reach`, RGBA16 at the size of `tiles`, the tile + /// pass's record, for each tile the lowest and highest id of the tiles within `view`'s glow radius of it that hold + /// a glowing surface, over the tiles under `rect`, a rect of the id target; the others are cleared, and hold no + /// glowing surface near them when the rect covers every selected texel and the glow's reach around them. + static void drawGlowReach(LLGLSLShader& program, LLRenderTarget& tiles, LLRenderTarget& reach, const LLRect& rect, + const View& view, LLVertexBuffer& triangle); + + /// The glow row pass with the bound glow row program: into `rows`, R8 at the size of `ids`, the id target, for + /// each texel how far along its row the nearest glowing texel within `view`'s glow radius lies, with `reach`, the + /// glow reach pass's record, over the texels under `rect`; the others are cleared, as for drawGlowReach. + static void drawGlowRows(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& reach, LLRenderTarget& rows, + const LLRect& rect, const View& view, LLVertexBuffer& triangle); + + /// Binds what a wireframe pass reads to the bound wireframe program: the pass, the lines' width, the viewport's + /// size, the palette, and for WIRE_ID_DEPTH the depth of `ids`, the id target. + static void bindWirePass(LLGLSLShader& program, EWirePass pass, S32 wire_width, S32 width, S32 height, U32 palette, + LLRenderTarget* ids); + + /// The edge pass with the bound outline program and `view`'s widths, over the viewport and scissor in force: + /// ids and their depth from `ids`, the id target, the depth through `view`'s projection, which the id pass drew + /// with, the jump pass's marks from `jumps` and the tile pass's record from `tiles`. With `glow_reach` and + /// `glow_rows`, the glow reach and row passes' records, where an object glows, the pass draws the glow; and only + /// then. + static void drawEdges(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& jumps, LLRenderTarget& tiles, + LLRenderTarget* glow_reach, LLRenderTarget* glow_rows, U32 palette, const View& view, + LLVertexBuffer& triangle); + +private: + // The full-screen triangle with the bound program, `projection` loaded for its draw where given: the triangle + // takes no matrix, and the passes read depth through the projection's inverse. + static void drawTriangle(LLVertexBuffer& triangle, const LLMatrix4a* projection); + + // Clears `target`, then draws the full-screen triangle into its texels under `rect`. + static void drawUnder(LLRenderTarget& target, const LLRect& rect, LLVertexBuffer& triangle, + const LLMatrix4a* projection); + + struct Entry + { + LLViewerObject* mObject; + U32 mTEMask; + LLColor4 mColour; + bool mShowHidden; + EPriority mPriority; + // EPart; none at PRIORITY_HOVER. + U32 mParts; + }; + + // The box the faces a walk drew cover, in the frame the view's modelview takes: unbounded where a rigged face's + // joints have not been measured yet. + struct FaceBox + { + LLVector4a mLo{ FLT_MAX, FLT_MAX, FLT_MAX }; + LLVector4a mHi{ -FLT_MAX, -FLT_MAX, -FLT_MAX }; + bool mBounded = true; + }; + + // The faces of `entry` that are rigged (or not) drawn into the bound program as the scene draws them, for the id + // pass and the wireframe alike: each in the frame LLVolumeGeometryManager::registerFace places it, a rigged one + // through its joint palette, with its alpha cut and culling (bindFace, drawFace). `begin` is called once, + // before the first face, with the object's matrices loaded. False when no face was drawn. + template + bool drawFaces(LLGLSLShader& program, const Entry& entry, bool rigged, const View& view, FaceBox& box, Begin&& begin); + + // The faces of `entry` that are rigged (or not) into the bound id program as `id`, and the box they cover grown + // by what reaches past it, the contour or the glow, into mScissor. + void drawObject(LLGLSLShader& program, const Entry& entry, U32 id, bool rigged, const View& view, + const LLMatrix4a& mvp); + + // The id pass: what was added and is drawn into it (inIdPass) into mIdMap, a width x height target, in priority + // order, and the box it covers, through `mvp`, into mScissor. + void drawIds(const View& view, S32 width, S32 height, const LLMatrix4a& mvp); + + // The wireframe of what was added with one, over the bound framebuffer under the outline: the world's visible + // lines and its hidden ones against the window's depth, the HUD's against the id target's. + void drawWireframes(const View& view, S32 width, S32 height); + + // When a target was last drawn with: the frame (LLFrameTimer::getFrameCount) and the time + // (LLFrameTimer::getTotalSeconds). + struct Use + { + U32 mFrame = 0; + F64 mTime = 0.0; + }; + + // Gives up the targets stale in frame `frame` at `now`. + void releaseStale(U32 frame, F64 now); + + // What a face is drawn with beyond its geometry. + struct FaceBinding + { + // A texture animation was loaded, which the caller unloads after the draw. + bool mAnimated = false; + // Its GLTF material is double-sided: the scene culls neither side. + bool mDoubleSided = false; + }; + + // Binds `face`'s texture and alpha test to the bound program and loads its texture animation, if it is drawn + // with one. + static FaceBinding bindFace(LLGLSLShader& program, LLFace* face); + + void clearEntries(); + + std::vector mEntries; + boost::unordered_flat_map mEntryIndex; + std::vector mPriorities; + std::vector mOrder; + std::vector mPaletteTexels; + + LLRenderTarget mIdMap; + LLRenderTarget mJumpMap; + LLRenderTarget mTileMap; + LLRenderTarget mGlowReachMap; + LLRenderTarget mGlowRowMap; + U32 mPalette = 0; + U32 mPaletteRows = 0; + + // When the id target, the jump and tile targets, the glow's, and the palette were last drawn with. + Use mIdUse; + Use mEdgeUse; + Use mGlowUse; + Use mPaletteUse; + + // The pixels this frame's outlines can touch, and whether any can. + LLRect mScissor; + bool mScissorValid = false; + + // Joint palettes uploaded to the rigged id program this frame, so a mesh's faces upload theirs once. + const LLVOAvatar* mLastAvatar = nullptr; + U64 mLastMeshId = 0; + bool mSkipLastSkin = false; +}; + +inline void ALSelectionOutline::writePalette(std::vector& texels, U32 id, const LLColor4& colour, bool show_hidden) +{ + const U32 x = id % PALETTE_WIDTH; + const U32 row = (id / PALETTE_WIDTH) * 2; + const size_t visible = ((size_t)row * PALETTE_WIDTH + x) * 4; + const size_t hidden = visible + (size_t)PALETTE_WIDTH * 4; + if (hidden + 4 > texels.size()) + { + return; + } + + auto unorm = [](F32 v) { return (U8)llclamp(ll_round(v * 255.f), 0, 255); }; + for (U32 c = 0; c < 3; ++c) + { + texels[visible + c] = unorm(colour.mV[c]); + texels[hidden + c] = texels[visible + c]; + } + texels[visible + 3] = unorm(colour.mV[VALPHA]); + texels[hidden + 3] = show_hidden ? unorm(colour.mV[VALPHA] * HIDDEN_ALPHA) : 0; +} + +inline S32 ALSelectionOutline::contourWidth(F32 width, F32 ui_scale) +{ + // Asked outright: /fp:fast may answer a comparison with a NaN either way. + const F32 held = llisnan(width) ? MIN_CONTOUR_WIDTH : llclamp(width, MIN_CONTOUR_WIDTH, MAX_CONTOUR_WIDTH); + return lineWidth(held, ui_scale); +} + +inline S32 ALSelectionOutline::glowRadius(F32 thickness, F32 ui_scale) +{ + // Asked outright: /fp:fast may answer a comparison with a NaN either way. + const F32 held = llisnan(thickness) ? DEFAULT_GLOW_THICKNESS : thickness; + return llmin(lineWidth(llclamp(held * GLOW_PIXELS, 0.f, MAX_GLOW_RADIUS), ui_scale), MAX_GLOW_PIXELS); +} + +inline F32 ALSelectionOutline::glowOpacity(F32 dist, S32 radius, F32 brightness) +{ + const F32 x = llclamp(llmax(dist - 0.5f, 0.f) / (F32)llmax(radius, 1), 0.f, 1.f); + const F32 falloff = 1.f - x * x * (3.f - 2.f * x); + return llclamp(brightness * GLOW_PEAK * falloff, 0.f, 1.f); +} + +inline ALSelectionOutline::View ALSelectionOutline::makeView(const LLMatrix4a& modelview, const LLMatrix4a& projection, + F32 contour_width, F32 ui_scale, bool hud, + F32 glow_thickness, F32 glow_brightness) +{ + View view; + view.mModelview = modelview; + view.mProjection = projection; + view.mWidth = contourWidth(contour_width, ui_scale); + view.mInnerWidth = lineWidth(INNER_WIDTH, ui_scale); + view.mHUD = hud; + view.mWireWidth = lineWidth(WIRE_WIDTH, ui_scale); + view.mGlowRadius = glowRadius(glow_thickness, ui_scale); + view.mGlowBrightness = + llisnan(glow_brightness) ? DEFAULT_GLOW_BRIGHTNESS : llclamp(glow_brightness, 0.f, MAX_GLOW_BRIGHTNESS); + return view; +} + +inline ALSelectionOutline::Passes ALSelectionOutline::passesFor(const Wants& wants, bool hud, bool wireframe_ready) +{ + Passes passes; + passes.mWireframe = wants.mWireframe && wireframe_ready; + passes.mEdges = wants.mContour || wants.mGlow; + passes.mIds = passes.mEdges || (passes.mWireframe && hud); + return passes; +} + +inline void ALSelectionOutline::priorityOrder(const std::vector& priorities, std::vector& order) +{ + order.resize(priorities.size()); + for (U32 i = 0; i < (U32)order.size(); ++i) + { + order[i] = i; + } + std::stable_sort(order.begin(), order.end(), [&priorities](U32 a, U32 b) { return priorities[a] < priorities[b]; }); +} + +inline F32 ALSelectionOutline::legacyAlphaCutoff(bool has_material, U8 diffuse_alpha_mode, U8 mask_cutoff, bool texture_alpha) +{ + if (!texture_alpha) + { + return NO_ALPHA_TEST; + } + if (!has_material) + { + return BLEND_ALPHA_CUTOFF; + } + switch (diffuse_alpha_mode) + { + case LLMaterial::DIFFUSE_ALPHA_MODE_BLEND: + return BLEND_ALPHA_CUTOFF; + case LLMaterial::DIFFUSE_ALPHA_MODE_MASK: + return (F32)mask_cutoff / 255.f; + default: + return NO_ALPHA_TEST; + } +} + +inline F32 ALSelectionOutline::gltfAlphaCutoff(S32 alpha_mode, F32 alpha_cutoff) +{ + switch (alpha_mode) + { + case LLGLTFMaterial::ALPHA_MODE_BLEND: + return BLEND_ALPHA_CUTOFF; + case LLGLTFMaterial::ALPHA_MODE_MASK: + return alpha_cutoff; + default: + return NO_ALPHA_TEST; + } +} + +inline bool ALSelectionOutline::scissorRect(const LLMatrix4a& mvp, const LLVector4a extents[2], S32 width, S32 height, + S32 reach, LLRect& rect) +{ + const LLRect target(0, height, width, 0); + F32 min_x = FLT_MAX; + F32 min_y = FLT_MAX; + F32 max_x = -FLT_MAX; + F32 max_y = -FLT_MAX; + U32 behind = 0; + for (U32 i = 0; i < 8; ++i) + { + const LLVector4a corner(extents[i & 1][0], extents[(i >> 1) & 1][1], extents[(i >> 2) & 1][2], 1.f); + LLVector4a clip; + mvp.transform4(corner, clip); + if (clip[3] <= 1e-6f) + { + ++behind; + continue; + } + const F32 x = (clip[0] / clip[3] * 0.5f + 0.5f) * (F32)width; + const F32 y = (clip[1] / clip[3] * 0.5f + 0.5f) * (F32)height; + min_x = llmin(min_x, x); + min_y = llmin(min_y, y); + max_x = llmax(max_x, x); + max_y = llmax(max_y, y); + } + + if (behind == 8) + { + return false; + } + if (behind > 0) + { + rect = target; + return true; + } + + // Projections far off screen are clamped before they become integers. + const F32 limit = (F32)(llmax(width, height) + reach + 1); + min_x = llclamp(min_x, -limit, limit); + min_y = llclamp(min_y, -limit, limit); + max_x = llclamp(max_x, -limit, limit); + max_y = llclamp(max_y, -limit, limit); + + rect.set(llfloor(min_x) - reach, llceil(max_y) + reach, llceil(max_x) + reach, llfloor(min_y) - reach); + rect.intersectWith(target); + return rect.notEmpty(); +} + +inline void ALSelectionOutline::uploadPalette(U32& palette, U32& palette_rows, const std::vector& texels, U32 rows) +{ + const U32 target = ALTextureSlot::getInternalType(ALTextureSlot::TT_TEXTURE); + if (!palette || palette_rows < rows) + { + if (palette) + { + LLImageGL::deleteTextures(1, &palette); + } + LLImageGL::generateTextures(1, &palette); + gGL.getTextureSlot(0)->bindManual(ALTextureSlot::TT_TEXTURE, palette); + LLImageGL::allocateTexture2D(target, GL_RGBA8, PALETTE_WIDTH, rows, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + palette_rows = rows; + } + else + { + gGL.getTextureSlot(0)->bindManual(ALTextureSlot::TT_TEXTURE, palette); + } + // setManualSubImage writes whatever is bound on the active unit, slot 0 here. + LLImageGL::setManualSubImage(target, 0, PALETTE_WIDTH, rows, GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); + gGL.getTextureSlot(0)->unbind(); +} + +inline void ALSelectionOutline::bindIdPass(LLGLSLShader& program, LLRenderTarget* scene, S32 width, S32 height) +{ + if (scene) + { + program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, scene); + } + else + { + // Not read without a scene; left empty rather than holding whatever the unit last had. + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + } + program.uniform1i(LLShaderMgr::SELECTION_SCENE_DEPTH, scene ? 1 : 0); + program.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, (F32)width, (F32)height); +} + +inline void ALSelectionOutline::setId(LLGLSLShader& program, U32 id, EPriority priority, bool show_hidden, bool outlined) +{ + program.uniform1i(LLShaderMgr::SELECTION_ID, (GLint)id); + program.uniform1i(LLShaderMgr::SELECTION_PRIORITY, (GLint)priority); + program.uniform1i(LLShaderMgr::SELECTION_SHOW_HIDDEN, show_hidden ? 1 : 0); + program.uniform1i(LLShaderMgr::SELECTION_OUTLINED, outlined ? 1 : 0); +} + +inline void ALSelectionOutline::drawFace(LLVertexBuffer& buffer, U32 start, U32 end, U32 count, U32 offset, + bool double_sided) +{ + LLGLDisable no_cull(double_sided ? GL_CULL_FACE : 0); + buffer.setBuffer(); + buffer.drawRange(LLRender::TRIANGLES, start, end, count, offset); +} + +inline ALSelectionOutline::WireState::WireState(EWirePass pass, F32 offset_factor) +: mDepth(pass != WIRE_ID_DEPTH ? GL_TRUE : GL_FALSE, GL_FALSE, pass == WIRE_HIDDEN ? GL_GREATER : GL_LEQUAL), + mOffset(pass != WIRE_ID_DEPTH ? GL_POLYGON_OFFSET_FILL : 0) +{ + gGL.setSceneBlendType(LLRender::BT_ALPHA); + // Toward the eye in forward depth's terms, which gGL turns for reverse-Z. + gGL.setPolygonOffset(-offset_factor, -WIRE_OFFSET_UNITS); +} + +inline ALSelectionOutline::WireState::~WireState() +{ + gGL.setPolygonOffset(0.f, 0.f); +} + +inline void ALSelectionOutline::bindWirePass(LLGLSLShader& program, EWirePass pass, S32 wire_width, S32 width, S32 height, + U32 palette, LLRenderTarget* ids) +{ + program.uniform1i(LLShaderMgr::WIREFRAME_PASS, (GLint)pass); + program.uniform1i(LLShaderMgr::WIREFRAME_WIDTH, llmax(wire_width, 1)); + program.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, (F32)width, (F32)height); + const S32 channel = program.enableTexture(LLShaderMgr::SELECTION_PALETTE); + if (channel > -1) + { + gGL.getTextureSlot(channel)->bindManual(ALTextureSlot::TT_TEXTURE, palette, gGL.getSampler(ALSamplers::PointClamp)); + } + if (ids && pass == WIRE_ID_DEPTH) + { + program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, ids); + } + else + { + // Not read; left empty rather than holding whatever the unit last had. + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + } +} + +inline void ALSelectionOutline::setAlphaTest(LLGLSLShader& program, F32 cutoff, + const LLGLTFMaterial::TextureTransform::Pack& transform) +{ + program.setMinimumAlpha(cutoff); + program.uniform4fv(LLShaderMgr::TEXTURE_BASE_COLOR_TRANSFORM, 2, transform); +} + +inline void ALSelectionOutline::drawTriangle(LLVertexBuffer& triangle, const LLMatrix4a* projection) +{ + if (projection) + { + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.pushMatrix(); + gGL.loadMatrix(*projection); + gGL.matrixMode(LLRender::MM_MODELVIEW); + } + + triangle.setBuffer(); + triangle.drawArrays(LLRender::TRIANGLES, 0, 3); + + if (projection) + { + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.popMatrix(); + gGL.matrixMode(LLRender::MM_MODELVIEW); + } +} + +inline void ALSelectionOutline::drawUnder(LLRenderTarget& target, const LLRect& rect, LLVertexBuffer& triangle, + const LLMatrix4a* projection) +{ + target.bindTarget(); + { + LLGLSColorMask mask(true, true); + LLGLDepthTest depth(GL_FALSE); + LLGLDisable blend(GL_BLEND); + { + LLGLDisable scissor(GL_SCISSOR_TEST); + gGL.setClearColor(LLColor4(0.f, 0.f, 0.f, 0.f)); + target.clear(); + } + + LLRect under = rect; + under.intersectWith(LLRect(0, (S32)target.getHeight(), (S32)target.getWidth(), 0)); + if (under.notEmpty()) + { + LLGLSScissor scissor(under.mLeft, under.mBottom, under.getWidth(), under.getHeight()); + drawTriangle(triangle, projection); + } + } + target.flush(); +} + +inline void ALSelectionOutline::drawJumps(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& jumps, + const LLRect& rect, const View& view, LLVertexBuffer& triangle) +{ + program.bindTexture(LLShaderMgr::SELECTION_ID_MAP, &ids, ALSamplers::PointClamp); + program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, &ids); + drawUnder(jumps, rect, triangle, &view.mProjection); + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + program.unbindTexture(LLShaderMgr::SELECTION_ID_MAP); +} + +inline LLRect ALSelectionOutline::tileRect(const LLRect& rect) +{ + const S32 left = llmax(rect.mLeft, 0) / (S32)TILE_SIZE; + const S32 bottom = llmax(rect.mBottom, 0) / (S32)TILE_SIZE; + const S32 right = (S32)tileCount((U32)llmax(rect.mRight, 0)); + const S32 top = (S32)tileCount((U32)llmax(rect.mTop, 0)); + return LLRect(left, top, right, bottom); +} + +inline void ALSelectionOutline::drawTiles(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& jumps, + LLRenderTarget& tiles, const LLRect& rect, LLVertexBuffer& triangle) +{ + // The tile program reads no depth, and needs no projection. + program.bindTexture(LLShaderMgr::SELECTION_ID_MAP, &ids, ALSamplers::PointClamp); + program.bindTexture(LLShaderMgr::SELECTION_JUMP_MAP, &jumps, ALSamplers::PointClamp); + drawUnder(tiles, tileRect(rect), triangle, nullptr); + program.unbindTexture(LLShaderMgr::SELECTION_JUMP_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_ID_MAP); +} + +inline void ALSelectionOutline::drawGlowReach(LLGLSLShader& program, LLRenderTarget& tiles, LLRenderTarget& reach, + const LLRect& rect, const View& view, LLVertexBuffer& triangle) +{ + // The glow reach program reads no depth, and needs no projection. + program.bindTexture(LLShaderMgr::SELECTION_TILE_MAP, &tiles, ALSamplers::PointClamp); + program.uniform1i(LLShaderMgr::OUTLINE_GLOW_RADIUS, llclamp(view.mGlowRadius, 1, MAX_GLOW_PIXELS)); + drawUnder(reach, tileRect(rect), triangle, nullptr); + program.unbindTexture(LLShaderMgr::SELECTION_TILE_MAP); +} + +inline void ALSelectionOutline::drawGlowRows(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& reach, + LLRenderTarget& rows, const LLRect& rect, const View& view, + LLVertexBuffer& triangle) +{ + // The glow row program reads no depth, and needs no projection. + program.bindTexture(LLShaderMgr::SELECTION_ID_MAP, &ids, ALSamplers::PointClamp); + program.bindTexture(LLShaderMgr::SELECTION_GLOW_REACH_MAP, &reach, ALSamplers::PointClamp); + program.uniform1i(LLShaderMgr::OUTLINE_GLOW_RADIUS, llclamp(view.mGlowRadius, 1, MAX_GLOW_PIXELS)); + drawUnder(rows, rect, triangle, nullptr); + program.unbindTexture(LLShaderMgr::SELECTION_GLOW_REACH_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_ID_MAP); +} + +inline void ALSelectionOutline::drawEdges(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& jumps, + LLRenderTarget& tiles, LLRenderTarget* glow_reach, LLRenderTarget* glow_rows, + U32 palette, const View& view, LLVertexBuffer& triangle) +{ + // Point sampled all: the ids, their depth, the jumps, the tiles, the glow's records and the palette are data, + // and texelFetch reads them whatever the filter. + const bool glow = glow_reach && glow_rows; + program.bindTexture(LLShaderMgr::SELECTION_ID_MAP, &ids, ALSamplers::PointClamp); + program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, &ids); + program.bindTexture(LLShaderMgr::SELECTION_JUMP_MAP, &jumps, ALSamplers::PointClamp); + program.bindTexture(LLShaderMgr::SELECTION_TILE_MAP, &tiles, ALSamplers::PointClamp); + if (glow) + { + program.bindTexture(LLShaderMgr::SELECTION_GLOW_REACH_MAP, glow_reach, ALSamplers::PointClamp); + program.bindTexture(LLShaderMgr::SELECTION_GLOW_ROW_MAP, glow_rows, ALSamplers::PointClamp); + } + else + { + // Not read without a glow; left empty rather than holding whatever the units last had. + program.unbindTexture(LLShaderMgr::SELECTION_GLOW_REACH_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_GLOW_ROW_MAP); + } + const S32 channel = program.enableTexture(LLShaderMgr::SELECTION_PALETTE); + if (channel > -1) + { + gGL.getTextureSlot(channel)->bindManual(ALTextureSlot::TT_TEXTURE, palette, gGL.getSampler(ALSamplers::PointClamp)); + } + program.uniform1i(LLShaderMgr::OUTLINE_WIDTH, llmax(view.mWidth, 1)); + program.uniform1i(LLShaderMgr::OUTLINE_INNER_WIDTH, llmax(view.mInnerWidth, 1)); + // A radius of 0 is no glow: the pass reaches no further than the lines. + program.uniform1i(LLShaderMgr::OUTLINE_GLOW_RADIUS, glow ? llclamp(view.mGlowRadius, 1, MAX_GLOW_PIXELS) : 0); + program.uniform1f(LLShaderMgr::OUTLINE_GLOW_BRIGHTNESS, view.mGlowBrightness); + + drawTriangle(triangle, &view.mProjection); + + program.unbindTexture(LLShaderMgr::SELECTION_PALETTE); + program.unbindTexture(LLShaderMgr::SELECTION_GLOW_ROW_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_GLOW_REACH_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_TILE_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_JUMP_MAP); + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + program.unbindTexture(LLShaderMgr::SELECTION_ID_MAP); +} diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index b3671534f7..1888bc877c 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -8783,7 +8783,7 @@ RenderHoverGlowEnable Comment - DEPRECATED --- Show glow effect when hovering on interactive objects. + Show glow effect when hovering on interactive objects. Persist 1 Type @@ -8831,7 +8831,7 @@ RenderHighlightColor Comment - Brightness of mouseover highlights. + Color of mouseover highlights. Persist 1 Type diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index c6804db812..c41d3051e2 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2405,6 +2405,28 @@ Value 0.5 + AlchemySelectionOutlineWidth + + Comment + Width in pixels of the outline around selected objects, in the world and on the HUD, at a UI scale of 1; it grows with the UI scale. Range 1 to 16. Applies live. + Persist + 1 + Type + F32 + Value + 2.0 + + AlchemySelectionWireframe + + Comment + Draw the triangles of selected objects as lines under their outline, in the world and on the HUD, with hidden lines dimmed while hidden selections are shown. Applies live. + Persist + 1 + Type + Boolean + Value + 0 + AlchemyRenderUBOUpdateMode Comment diff --git a/indra/newview/app_settings/shaders/class1/deferred/blurLightV.glsl b/indra/newview/app_settings/shaders/class1/deferred/blurLightV.glsl index 969fbc5a7d..01b5960221 100644 --- a/indra/newview/app_settings/shaders/class1/deferred/blurLightV.glsl +++ b/indra/newview/app_settings/shaders/class1/deferred/blurLightV.glsl @@ -30,8 +30,12 @@ uniform vec2 screen_res; void main() { - //transform vertex - vec4 pos = vec4(position.xyz, 1.0); - gl_Position = pos; - vary_fragcoord = (pos.xy*0.5+0.5); + // On the far plane, as the light map's own pass is (sunLightV.glsl): only geometry is blurred, and the sky keeps + // the clear's white, which the blur's plane test never takes in anyway. Reverse-Z puts the far plane at ndc z 0. +#ifdef REVERSE_Z + gl_Position = vec4(position.xy, 0.0, 1.0); +#else + gl_Position = vec4(position.xy, 1.0, 1.0); +#endif + vary_fragcoord = (position.xy*0.5+0.5); } diff --git a/indra/newview/app_settings/shaders/class1/interface/copyF.glsl b/indra/newview/app_settings/shaders/class1/interface/copyF.glsl index 6209d193d1..6dabacf87a 100644 --- a/indra/newview/app_settings/shaders/class1/interface/copyF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/copyF.glsl @@ -29,13 +29,20 @@ in vec2 tc; uniform sampler2D depthMap; #endif +// DEPTH_ONLY copies depth alone, drawn with colour writes off: no colour is read. +#if !defined(DEPTH_ONLY) uniform sampler2D diffuseMap; +#endif out vec4 frag_color; void main() { +#if defined(DEPTH_ONLY) + frag_color = vec4(0.0); +#else frag_color = texture(diffuseMap, tc); +#endif #if defined(COPY_DEPTH) // Reverse-Z neutral: copies the raw stored depth value verbatim, no convention math. gl_FragDepth = texture(depthMap, tc).r; diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionAlphaF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionAlphaF.glsl new file mode 100644 index 0000000000..784023aded --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionAlphaF.glsl @@ -0,0 +1,50 @@ +/** + * @file selectionAlphaF.glsl + * @brief Where a selected face's texture cuts it out (ALSelectionOutline): linked into the passes that draw faces, + * the id pass and the wireframe, so both follow what the scene draws. + * + * $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$ + */ + +// The face's texture: a legacy face's diffuse map, a GLTF face's base colour, through the base colour transform +// and texture animation selectionIdV.glsl carries its texture coordinates through. +uniform sampler2D diffuseMap; + +// The texture alpha under which the face is cut out. At or under 0 the face is opaque and its texture is not read. +uniform float minimum_alpha; + +// The face's texture alpha at `texcoord`, or 1 where it has no alpha test. Only the texture's: the face's colour, +// whose alpha can make a prim invisible, does not cut it out. Read before anything is discarded, while the +// derivatives its level of detail is chosen from are defined. +float faceAlpha(vec2 texcoord) +{ + float alpha = 1.0; + if (minimum_alpha > 0.0) + { + alpha = texture(diffuseMap, texcoord).a; + } + return alpha; +} + +// Whether `alpha`, from faceAlpha, leaves the fragment out. +bool isCutOut(float alpha) +{ + return alpha < minimum_alpha; +} diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionGlowReachF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionGlowReachF.glsl new file mode 100644 index 0000000000..5e64e5f406 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionGlowReachF.glsl @@ -0,0 +1,67 @@ +/** + * @file selectionGlowReachF.glsl + * @brief The selection outline's glow reach pass (ALSelectionOutline): which glowing ids each tile of the id target + * has within the glow's reach, so the passes after it learn from one texel whether a glow can be near. + * + * $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$ + */ + +// Into RGBA16, a texel to each tile: in .r and .g over 65535 the lowest and highest id recorded by the tiles within +// the glow's reach that hold the drawn surface of an object that glows, 0 for none. A pixel the glow's reach from a +// glowing texel lies in a tile no more than this many tiles from the glowing texel's own, along either axis, so +// what the record says none of, no pixel of the tile can draw. +out vec4 frag_color; + +// The tile pass's record (selectionTileF.glsl). +uniform sampler2D selectionTileMap; + +// How far in pixels the glow reaches past its object's silhouette. +uniform int outline_glow_radius; + +// The side of a tile in texels of the id target (ALSelectionOutline::TILE_SIZE). +#define TILE_SIZE 8 + +void main() +{ + // The viewport is the tile target's, a fragment to a tile. + ivec2 tile = ivec2(gl_FragCoord.xy); + ivec2 last = textureSize(selectionTileMap, 0) - ivec2(1); + // A glowing texel draws as far as a pixel the radius and one more from its centre. + int tiles = (outline_glow_radius + 1 + TILE_SIZE - 1) / TILE_SIZE; + ivec2 lo = max(tile - ivec2(tiles), ivec2(0)); + ivec2 hi = min(tile + ivec2(tiles), last); + + int lowest = 65536; + int highest = 0; + for (int y = lo.y; y <= hi.y; ++y) + { + for (int x = lo.x; x <= hi.x; ++x) + { + vec4 record = texelFetch(selectionTileMap, ivec2(x, y), 0); + if (record.a > 0.5) + { + lowest = min(lowest, int(record.r * 65535.0 + 0.5)); + highest = max(highest, int(record.g * 65535.0 + 0.5)); + } + } + } + + frag_color = vec4(float(highest != 0 ? lowest : 0) / 65535.0, float(highest) / 65535.0, 0.0, 0.0); +} diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionGlowRowF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionGlowRowF.glsl new file mode 100644 index 0000000000..d359706a37 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionGlowRowF.glsl @@ -0,0 +1,79 @@ +/** + * @file selectionGlowRowF.glsl + * @brief The selection outline's glow row pass (ALSelectionOutline): how far along its row each texel of the id + * target lies from the nearest drawn surface of an object that glows, the first half of the glow's distance. + * + * $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$ + */ + +// Into R8: over 255, one more than the columns between the texel and the nearest drawn glowing texel in its row, as +// far as the glow reaches, either side; 0 where there is none. Of the texels of one row, the nearest along it is the +// nearest to any pixel, so the edge pass finds the nearest glowing texel in the whole disk from one of these a row. +out vec4 frag_color; + +// The id target (selectionUtilF.glsl), and the glow reach pass's record of its tiles (selectionGlowReachF.glsl). +uniform sampler2D selectionIdMap; +uniform sampler2D selectionGlowReachMap; + +// How far in pixels the glow reaches past its object's silhouette. +uniform int outline_glow_radius; + +// The side of a tile in texels of the id target (ALSelectionOutline::TILE_SIZE). +#define TILE_SIZE 8 + +// selectionUtilF.glsl +int decodeId(vec4 texel); +bool isGlow(vec4 texel); +bool isDrawn(vec4 texel); + +bool glowsAt(ivec2 pos, ivec2 size) +{ + if (any(lessThan(pos, ivec2(0))) || any(greaterThanEqual(pos, size))) + { + return false; + } + vec4 texel = texelFetch(selectionIdMap, pos, 0); + return decodeId(texel) != 0 && isGlow(texel) && isDrawn(texel); +} + +void main() +{ + // The viewport is the target's, a fragment to a texel. + ivec2 size = textureSize(selectionIdMap, 0); + ivec2 pos = ivec2(gl_FragCoord.xy); + + // Where no glowing tile is within the glow's reach, no glowing texel is along the row within it either. + int code = 0; + if (texelFetch(selectionGlowReachMap, pos / TILE_SIZE, 0).g > 0.0) + { + // A glowing texel draws as far as a pixel the radius and one more from its centre. + int reach = outline_glow_radius + 1; + for (int d = 0; d <= reach; ++d) + { + if (glowsAt(pos - ivec2(d, 0), size) || glowsAt(pos + ivec2(d, 0), size)) + { + code = d + 1; + break; + } + } + } + + frag_color = vec4(float(code) / 255.0); +} diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl new file mode 100644 index 0000000000..17d1b8b0af --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl @@ -0,0 +1,95 @@ +/** + * @file selectionIdF.glsl + * @brief The selection outline's id pass (ALSelectionOutline): a selected object's id and priority, and whether + * the scene shows the surface, where the face's texture does not cut it out. + * + * $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$ + */ + +// .rg: the id, low byte then high byte; .b: whether the outline draws this surface, 1 where the scene shows it, +// 0.5 where the scene hides it and the object's hidden parts are drawn dimmed, 0 where they are left out; .a: the +// object's priority. +out vec4 frag_color; + +in vec2 vary_texcoord0; + +// The object's id, 1 to 65535. 0 is what the target is cleared to: no selected object. +uniform int selection_id; + +// The object's priority (ALSelectionOutline::EPriority), 0 the highest. +uniform int selection_priority; + +// 1 where the object's hidden parts are drawn dimmed, 0 where they are left out. +uniform int selection_show_hidden; + +// 1 where the outline or the glow draws the object. 0 where it is drawn for the depth of the HUD's wireframe alone: +// its surface is then drawn nowhere, and hides what lies behind it without drawing a line or masking another's. +uniform int selection_outlined; + +// 1 where the scene's depth decides what is hidden. 0 where it cannot be read, as on the HUD, which is drawn into +// the window's framebuffer: every surface then counts as visible. +uniform int selection_scene_depth; + +// The scene's depth. It is at render resolution, which need not be this target's, so it is read by uv. +uniform sampler2D depthMap; + +// This target's size in pixels. +uniform vec2 screen_res; + +// How far in front of this surface the scene's may lie, as a fraction of the distance, and still be this +// surface: float noise between two draws of the same triangle. +const float VISIBLE_SLACK = 0.001; + +// selectionUtilF.glsl, through the projection the scene and this pass share. +float eyeDistance(float depth); + +// selectionAlphaF.glsl +float faceAlpha(vec2 texcoord); +bool isCutOut(float alpha); + +void main() +{ + // The slope is taken outside the branch, where derivatives are defined whatever it does. + float own = eyeDistance(gl_FragCoord.z); + float slope = fwidth(own); + float alpha = faceAlpha(vary_texcoord0); + + float visible = 1.0; + if (selection_scene_depth != 0) + { + float scene = eyeDistance(texture(depthMap, gl_FragCoord.xy / screen_res).r); + + // The point-sampled scene texel is up to half of itself away from this pixel's centre, so where it is + // coarser than this target the same surface can read nearer by that much of its own slope. + vec2 texels = screen_res / vec2(textureSize(depthMap, 0)); + float reach = 0.5 * max(max(texels.x, texels.y), 1.0) * slope; + + visible = (own <= scene + reach + own * VISIBLE_SLACK) ? 1.0 : 0.0; + } + + if (isCutOut(alpha)) + { + discard; + } + + float drawn = (selection_outlined == 0) ? 0.0 : ((visible > 0.5) ? 1.0 : ((selection_show_hidden != 0) ? 0.5 : 0.0)); + frag_color = vec4(float(selection_id & 255) / 255.0, float((selection_id >> 8) & 255) / 255.0, drawn, + float(selection_priority) / 255.0); +} diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionIdV.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionIdV.glsl new file mode 100644 index 0000000000..6f909f3eba --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionIdV.glsl @@ -0,0 +1,54 @@ +/** + * @file selectionIdV.glsl + * @brief The selection outline's id pass (ALSelectionOutline): a selected face, and where on its texture it is. + * + * $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$ + */ + +// Shared matrix stack + derived matrices, spliced from +// class1/deferred/matricesBlock.glsl and bound at UB_MATRICES. +//[ENGINE_BLOCK Matrices] + +in vec3 position; +in vec2 texcoord0; + +out vec2 vary_texcoord0; + +// A GLTF face's base colour transform (KHR_texture_transform); the identity for a legacy face, whose texture +// coordinates have their transform in them. texture_matrix0 is the face's texture animation where it is drawn +// rather than baked into those coordinates. +uniform vec4[2] texture_base_color_transform; +vec2 texture_transform(vec2 vertex_texcoord, vec4[2] khr_gltf_transform, mat4 sl_animation_transform); + +#ifdef HAS_SKIN +mat3x4 getSkinBlend(); +vec4 skinTransformH(mat3x4 b, vec3 pos, mat4 m); +#endif + +void main() +{ +#ifdef HAS_SKIN + vec4 pos = skinTransformH(getSkinBlend(), position.xyz, modelview_matrix); + gl_Position = projection_matrix * pos; +#else + gl_Position = modelview_projection_matrix * vec4(position.xyz, 1.0); +#endif + vary_texcoord0 = texture_transform(texcoord0, texture_base_color_transform, texture_matrix0); +} diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionJumpF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionJumpF.glsl new file mode 100644 index 0000000000..c46c23dded --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionJumpF.glsl @@ -0,0 +1,122 @@ +/** + * @file selectionJumpF.glsl + * @brief The selection outline's jump pass (ALSelectionOutline): a mark on each texel of the id target where an + * object's surface steps in front of another part of itself, as one frond of a mesh lies over another. + * + * $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$ + */ + +// Into a one-channel target: 1 where the id texel is the near side of a jump, 0 elsewhere. +out vec4 frag_color; + +// The id pass's target, and its depth through the projection loaded for this pass, which the id pass drew with. +uniform sampler2D selectionIdMap; +uniform sampler2D depthMap; + +// How far the surface on each side of a pair of neighbours, carried one texel on at its own slope, must miss the +// other for the pair to be a jump, as a fraction of the other's distance. +const float JUMP_MARGIN = 0.01; + +// selectionUtilF.glsl +int decodeId(vec4 texel); +bool isGlow(vec4 texel); +float eyeDistance(float depth); + +bool hasId(ivec2 pos, ivec2 size, int id) +{ + return all(greaterThanEqual(pos, ivec2(0))) && all(lessThan(pos, size)) && decodeId(texelFetch(selectionIdMap, pos, 0)) == id; +} + +// Whether the surface steps from a texel at stored depth `depth` to its neighbour at `other`, with the texel the +// nearer: `behind` is the texel's own neighbour away from the other, `beyond` the other's, so each side's slope is +// its own. Over a crease or a curve the step between the two lies between their slopes; over an overlap it lies +// outside both, and carried on at either slope each side misses the other by the gap. +bool jumps(float depth, float behind, float other, float beyond) +{ + float own_slope = depth - behind; + float other_slope = beyond - other; + float across = other - depth; + if ((across - own_slope) * (across - other_slope) <= 0.0) + { + return false; + } + + float own = eyeDistance(depth); + float there = eyeDistance(other); + return own < there && + abs(eyeDistance(depth + own_slope) - there) > there * JUMP_MARGIN && + abs(eyeDistance(other - other_slope) - own) > own * JUMP_MARGIN; +} + +// Whether the texel at `pos`, of object `id`, is the near side of a jump to either neighbour along `step`. `at` +// holds the stored depths of its neighbours and theirs beyond, two either side, and all three texels on a side must +// be the object: a side whose slope cannot be read, at the edge of the object or a sliver of it a texel wide, says +// nothing. The depths alone rule out nearly every pair, so the ids are read only for a pair they do not, which +// decides the same. +bool jumpsAlong(ivec2 pos, ivec2 size, ivec2 step, int id, float depth, vec4 at) +{ + bool on[4]; + for (int i = 0; i < 4; ++i) + { + ivec2 texel = pos + step * (i < 2 ? i - 2 : i - 1); + on[i] = all(greaterThanEqual(texel, ivec2(0))) && all(lessThan(texel, size)); + } + if (on[1] && on[2] && on[3] && jumps(depth, at.y, at.z, at.w) && + hasId(pos - step, size, id) && hasId(pos + step, size, id) && hasId(pos + step * 2, size, id)) + { + return true; + } + return on[0] && on[1] && on[2] && jumps(depth, at.z, at.y, at.x) && + hasId(pos + step, size, id) && hasId(pos - step, size, id) && hasId(pos - step * 2, size, id); +} + +// The two-by-two block of stored depths whose lower left texel is `corner`, gathered at the corner they share: +// .w lower left, .z lower right, .x upper left, .y upper right. +vec4 gatherDepth(ivec2 corner, vec2 texel_size) +{ + return textureGather(depthMap, vec2(corner + ivec2(1)) * texel_size); +} + +void main() +{ + // The viewport is the target's, a fragment to a texel. + ivec2 size = textureSize(selectionIdMap, 0); + ivec2 pos = ivec2(gl_FragCoord.xy); + vec4 texel = texelFetch(selectionIdMap, pos, 0); + int id = decodeId(texel); + + // What glows draws nothing over its own surface, jumps and all, so its jumps are not marked. + bool front = false; + if (id != 0 && !isGlow(texel)) + { + // The eight depths two either side along x and y in four gathers; the texels off the target they clamp to + // are never read as the object's. + vec2 texel_size = 1.0 / vec2(size); + vec4 left = gatherDepth(pos + ivec2(-2, 0), texel_size); + vec4 right = gatherDepth(pos + ivec2(1, 0), texel_size); + vec4 below = gatherDepth(pos + ivec2(0, -2), texel_size); + vec4 above = gatherDepth(pos + ivec2(0, 1), texel_size); + float depth = texelFetch(depthMap, pos, 0).r; + front = jumpsAlong(pos, size, ivec2(1, 0), id, depth, vec4(left.w, left.z, right.w, right.z)) || + jumpsAlong(pos, size, ivec2(0, 1), id, depth, vec4(below.w, below.x, above.w, above.x)); + } + + frag_color = vec4(front ? 1.0 : 0.0); +} diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl new file mode 100644 index 0000000000..1c5a747385 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl @@ -0,0 +1,417 @@ +/** + * @file selectionOutlineF.glsl + * @brief The selection outline's edge pass (ALSelectionOutline): the contour of each selected object over what lies + * behind it, the fainter edges between objects that touch, and the glow around what the pointer is over. + * + * $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$ + */ + +in vec2 tc; + +out vec4 frag_color; + +// The id target (selectionUtilF.glsl). Ids are given in priority order, so of two objects the lower id is the +// higher priority. +uniform sampler2D selectionIdMap; + +// The jump pass's marks on the id target (selectionJumpF.glsl). +uniform sampler2D selectionJumpMap; + +// The id target's own depth, through the projection loaded for this pass, which the id pass drew with. +uniform sampler2D depthMap; + +// Two texels per id, PALETTE_WIDTH ids to a pair of rows: the visible colour above the hidden one. +uniform sampler2D selectionPalette; + +// The tile pass's record (selectionTileF.glsl), a texel to each TILE_SIZE square of the id target: the lowest and +// highest nonzero id in .r and .g over 65535, 1 in .b where it holds the near side of a jump, and 1 in .a where it +// holds the drawn surface of an object that glows. +uniform sampler2D selectionTileMap; + +// Where something glows: the glow reach pass's record of the glowing ids within the glow's reach of each tile +// (selectionGlowReachF.glsl), and the glow row pass's distance along each texel's row to the nearest glowing texel +// (selectionGlowRowF.glsl). +uniform sampler2D selectionGlowReachMap; +uniform sampler2D selectionGlowRowMap; + +// The widths in pixels of the contour around the objects and of the edges between them, before the pixel each +// is anti-aliased across. +uniform int outline_width; +uniform int outline_inner_width; + +// How far in pixels the glow reaches past its object's silhouette; 0 where nothing glows, and the pass reaches no +// further than its lines. Its brightness, RenderHighlightBrightness. +uniform int outline_glow_radius; +uniform float outline_glow_brightness; + +#define PALETTE_WIDTH 256 + +// The side of a tile in texels of the id target (ALSelectionOutline::TILE_SIZE). +#define TILE_SIZE 8 + +// What an edge between two objects keeps of its object's opacity (ALSelectionOutline::INNER_OPACITY). +const float INNER_OPACITY = 0.6; + +// The glow's profile at its object's silhouette before the brightness (ALSelectionOutline::GLOW_PEAK). +const float GLOW_PEAK = 0.25; + +// How much nearer than the plane of a pixel's surface another object's must be to stand in front of it, and how +// much farther to lie behind it: a fraction of the distance, for the depth's precision, and a little more for float +// noise. Between the two the objects touch. +const float FRONT_MARGIN = 0.002; +const float FRONT_SLACK = 0.0001; + +// How another object's surface lies against the plane of a pixel's own. +const int IN_FRONT = 0; +const int TOUCHING = 1; +const int BEHIND = 2; + +// selectionUtilF.glsl +int decodeId(vec4 texel); +int decodePriority(vec4 texel); +bool isJumpFront(sampler2D jumps, ivec2 pos); +bool isGlow(vec4 texel); +bool isDrawn(vec4 texel); +bool isVisible(vec4 texel); +float eyeDistance(float depth); + +vec4 paletteColour(int id, bool visible) +{ + int row = (id / PALETTE_WIDTH) * 2 + (visible ? 0 : 1); + return texelFetch(selectionPalette, ivec2(id % PALETTE_WIDTH, row), 0); +} + +// How much of a pixel a line `width` pixels wide along an object's edge covers, `dist` from the centre of the +// object's nearest texel to the pixel's. The edge lies half a texel out from that centre, so this is +// 1 - smoothstep(width - 0.5, width + 0.5, dist - 0.5): solid to the line's width, and anti-aliased across one +// pixel past it. +float coverage(float dist, float width) +{ + return 1.0 - smoothstep(width, width + 1.0, dist); +} + +// The glow's opacity before its colour's alpha, `dist` from the centre of its object's nearest texel, the +// silhouette half a texel out (ALSelectionOutline::glowOpacity): its brightness times a profile that is GLOW_PEAK at +// the silhouette and falls smoothly to nothing outline_glow_radius past it, held to 1. +float glowOpacity(float dist) +{ + float x = max(dist - 0.5, 0.0) / float(outline_glow_radius); + float profile = GLOW_PEAK * (1.0 - smoothstep(0.0, 1.0, x)); + return clamp(outline_glow_brightness * profile, 0.0, 1.0); +} + +bool hasId(ivec2 pos, ivec2 size, int id) +{ + return all(greaterThanEqual(pos, ivec2(0))) && all(lessThan(pos, size)) && decodeId(texelFetch(selectionIdMap, pos, 0)) == id; +} + +// The change in stored depth across a pixel along `step` of the surface at `pos`, from the neighbour on the side +// that is the same object, the gentler where both are, none where neither is. Stored depth is affine across the +// screen on a plane under any projection, so it carries a flat surface on exactly; at a crease it bends least. +float depthSlope(ivec2 pos, ivec2 size, ivec2 step, int id, float depth) +{ + bool back = hasId(pos - step, size, id); + bool ahead = hasId(pos + step, size, id); + float to_back = back ? depth - texelFetch(depthMap, pos - step, 0).r : 0.0; + float to_ahead = ahead ? texelFetch(depthMap, pos + step, 0).r - depth : 0.0; + if (back && ahead) + { + return abs(to_back) <= abs(to_ahead) ? to_back : to_ahead; + } + return back ? to_back : to_ahead; +} + +// Whether a tile under the square of texels within `reach` of `pos` along either axis holds what a pixel of +// `own_id` could draw from. Whatever a pixel draws, it draws from a texel within its reach: off every object, of +// any object; on one, of another object, or of its own where the jump pass marked the near side of a jump. The +// texel's tile records each: without such a tile the disk search finds nothing to draw. `jump` says whether any of +// those tiles holds the near side of a jump, without which no texel within the reach is one and the jump pass's +// marks need not be read. +bool candidateWithin(ivec2 pos, int reach, int own_id, out bool jump) +{ + ivec2 last = textureSize(selectionTileMap, 0) - ivec2(1); + ivec2 lo = max(pos - ivec2(reach), ivec2(0)) / TILE_SIZE; + ivec2 hi = min((pos + ivec2(reach)) / TILE_SIZE, last); + bool found = false; + jump = false; + for (int y = lo.y; y <= hi.y; ++y) + { + for (int x = lo.x; x <= hi.x; ++x) + { + vec4 tile = texelFetch(selectionTileMap, ivec2(x, y), 0); + int lowest = int(tile.r * 65535.0 + 0.5); + int highest = int(tile.g * 65535.0 + 0.5); + bool tile_jump = tile.b > 0.5; + found = found || (own_id == 0 ? (highest != 0) + : ((lowest != 0 && lowest != own_id) || (highest != 0 && highest != own_id) || tile_jump)); + jump = jump || tile_jump; + } + } + return found; +} + +// Whether a tile within the glow's reach of the tile of `pos` holds the drawn surface of an object that glows, other +// than the pixel's own, `own_id`: the glow is never drawn over its own object. A pixel draws a glow only from such a +// texel within the glow's reach, whose tile records it. The glow reach pass gathered the lowest and highest ids +// those tiles record, and a tile records an id other than `own_id` exactly where they are not both `own_id`. +bool glowWithin(ivec2 pos, int own_id) +{ + vec4 record = texelFetch(selectionGlowReachMap, pos / TILE_SIZE, 0); + int lowest = int(record.r * 65535.0 + 0.5); + int highest = int(record.g * 65535.0 + 0.5); + return highest != 0 && (lowest != own_id || highest != own_id); +} + +// The id of the drawn glowing texel at `pos`, or 65536 where there is none, which every id is lower than. +int glowIdAt(ivec2 pos, ivec2 size) +{ + if (any(lessThan(pos, ivec2(0))) || any(greaterThanEqual(pos, size))) + { + return 65536; + } + vec4 texel = texelFetch(selectionIdMap, pos, 0); + int id = decodeId(texel); + return (id != 0 && isGlow(texel) && isDrawn(texel)) ? id : 65536; +} + +// The glow off every object, where every glowing surface stands in front: the nearest drawn glowing texel within +// `reach` of `pos`, the lower id where two are as near, from the glow row pass's nearest along each row. Rows are +// read outward from the pixel's own and stop where none farther can be as near. `glow_d2` is the squared distance, +// which starts one past the reach's. +void glowFromRows(ivec2 pos, ivec2 size, int reach, inout int glow_id, inout int glow_d2) +{ + for (int dy = 0; dy <= reach && dy * dy <= glow_d2; ++dy) + { + for (int side = 0; side < ((dy == 0) ? 1 : 2); ++side) + { + int y = pos.y + ((side == 0) ? dy : -dy); + if (y < 0 || y >= size.y) + { + continue; + } + int code = int(texelFetch(selectionGlowRowMap, ivec2(pos.x, y), 0).r * 255.0 + 0.5); + if (code == 0) + { + continue; + } + int dx = code - 1; + int d2 = dx * dx + dy * dy; + if (d2 > glow_d2) + { + continue; + } + // The nearest along the row lies one side or both, each its own object's. + int id = min(glowIdAt(ivec2(pos.x - dx, y), size), glowIdAt(ivec2(pos.x + dx, y), size)); + if (d2 < glow_d2 || id < glow_id) + { + glow_id = id; + glow_d2 = d2; + } + } + } +} + +void main() +{ + ivec2 size = textureSize(selectionIdMap, 0); + ivec2 pos = clamp(ivec2(tc * vec2(size)), ivec2(0), size - ivec2(1)); + vec4 own = texelFetch(selectionIdMap, pos, 0); + int own_id = decodeId(own); + bool own_drawn = (own_id != 0) && isDrawn(own); + // A glowing object's surface is not the outline's: the lines of what stands in front of it or touches it are + // drawn over it as over the space off every object, and it draws no edge of its own. + bool own_glows = (own_id != 0) && isGlow(own); + + // Every texel in the disk the lines reach, so features thinner than a line are not stepped over. Texels off the + // target are not there: clamping them would read the edge's again, nearer than it is. + int line_reach = max(outline_width, outline_inner_width) + 1; + int inner_reach = outline_inner_width + 1; + int glow_reach = (outline_glow_radius > 0) ? outline_glow_radius + 1 : 0; + bool glow_near = (glow_reach > 0) && glowWithin(pos, own_id); + // A texel of the pixel's own object counts only where it is the near side of a jump, and only within the lines' + // reach, as a glow is never drawn over its own object: the tiles that reach covers say whether one can be. + bool jump_near = false; + bool line_near = candidateWithin(pos, line_reach, own_id, jump_near); + if (!glow_near && !line_near) + { + discard; + } + + // The glow: the nearest drawn texel of a glowing object in front of this pixel, the lower id where two are as + // near, and its squared distance. Off every object every glowing surface is in front, and the glow row pass's + // nearest along each row give it. On an object, which stand in front depends on its plane, so the disk is searched + // as far as the glow reaches. GLOW_DISK searches the disk for every pixel: the full search, which the rows give + // the same as. +#ifdef GLOW_DISK + bool glow_rows = false; +#else + bool glow_rows = (own_id == 0); +#endif + bool glow_taps = glow_near && !glow_rows; + int glow_id = 0; + int glow_d2 = glow_reach * glow_reach + 1; + if (glow_near && glow_rows) + { + glowFromRows(pos, size, glow_reach, glow_id, glow_d2); + } + int reach = glow_taps ? max(line_reach, glow_reach) : line_reach; + + // The plane of this pixel's own surface, found when the first candidate needs it. + bool planed = false; + float own_depth = 0.0; + vec2 slope = vec2(0.0); + + // The contour: of the drawn objects in front of this pixel, the highest priority's from its nearest texel, and + // the nearest's. An object not drawn is no candidate, so its hidden shape neither draws nor masks another's. + int top_id = 0; + int top_priority = 256; + float top_dist = 0.0; + bool top_visible = true; + int near_id = 0; + int near_priority = 256; + float near_dist = float(line_reach) + 1.0; + bool near_visible = true; + + // The nearest texel of an object this pixel touches whose edge with it is drawn on this side. + float inner_dist = float(inner_reach) + 1.0; + + // Without a line within reach the disk holds only what glows, which it is searched for only on an object. + int disk = (line_near || glow_taps) ? reach : -1; + for (int dy = -disk; dy <= disk; ++dy) + { + for (int dx = -disk; dx <= disk; ++dx) + { + ivec2 tap_pos = pos + ivec2(dx, dy); + if (dx * dx + dy * dy > reach * reach || any(lessThan(tap_pos, ivec2(0))) || any(greaterThanEqual(tap_pos, size))) + { + continue; + } + vec4 tap = texelFetch(selectionIdMap, tap_pos, 0); + int tap_id = decodeId(tap); + // An object over its own surface draws only where it steps in front of itself, from the near side of the + // jump: anywhere else that would trace every crease and curve of a mesh. + bool own_object = (tap_id == own_id); + if (tap_id == 0 || (own_object && !(jump_near && isJumpFront(selectionJumpMap, tap_pos)))) + { + continue; + } + // A glow reaches as far as its radius and never over its own object, jumps and all, and is searched for + // here only where the rows do not give it; a line as far as its width. + bool tap_glows = isGlow(tap); + int tap_reach = tap_glows ? glow_reach : line_reach; + int d2 = dx * dx + dy * dy; + if ((tap_glows && (own_object || !glow_taps)) || d2 > tap_reach * tap_reach) + { + continue; + } + float dist = length(vec2(dx, dy)); + + // Off every object, every object is in front. On one, the candidate's surface against this one's plane, + // carried over to the candidate's texel: the near side of a jump stands in front of the far side's + // pixels, and not of its own surface's. + int relation = IN_FRONT; + if (own_id != 0) + { + if (!planed) + { + own_depth = texelFetch(depthMap, pos, 0).r; + slope = vec2(depthSlope(pos, size, ivec2(1, 0), own_id, own_depth), + depthSlope(pos, size, ivec2(0, 1), own_id, own_depth)); + planed = true; + } + float plane = eyeDistance(clamp(own_depth + dot(slope, vec2(dx, dy)), 0.0, 1.0)); + float there = eyeDistance(texelFetch(depthMap, tap_pos, 0).r); + float margin = plane * FRONT_MARGIN + FRONT_SLACK; + relation = (there < plane - margin) ? IN_FRONT : ((there <= plane + margin) ? TOUCHING : BEHIND); + } + + bool tap_drawn = isDrawn(tap); + bool in_front = (relation == IN_FRONT) || (own_glows && relation == TOUCHING); + if (tap_glows) + { + if (relation == IN_FRONT && tap_drawn && (d2 < glow_d2 || (d2 == glow_d2 && tap_id < glow_id))) + { + glow_id = tap_id; + glow_d2 = d2; + } + } + else if (in_front && tap_drawn) + { + int tap_priority = decodePriority(tap); + if (tap_priority < top_priority || (tap_priority == top_priority && dist < top_dist)) + { + top_id = tap_id; + top_priority = tap_priority; + top_dist = dist; + top_visible = isVisible(tap); + } + if (dist < near_dist || (dist == near_dist && tap_priority < near_priority)) + { + near_id = tap_id; + near_priority = tap_priority; + near_dist = dist; + near_visible = isVisible(tap); + } + } + // The edge between two objects that touch is drawn once: on the side of the higher priority where that + // side is drawn, on the drawn side where only one is. + else if (relation == TOUCHING && !own_object && own_drawn && !own_glows && dist <= float(inner_reach) && + (!tap_drawn || own_id < tap_id)) + { + inner_dist = min(inner_dist, dist); + } + } + } + + // Premultiplied: the glow, the edge over it, and the contour over both. + vec3 rgb = vec3(0.0); + float alpha = 0.0; + if (glow_id != 0) + { + vec4 glow_colour = paletteColour(glow_id, true); + alpha = glow_colour.a * glowOpacity(sqrt(float(glow_d2))); + rgb = glow_colour.rgb * alpha; + } + if (own_drawn && !own_glows) + { + vec4 colour = paletteColour(own_id, isVisible(own)); + float inner_alpha = colour.a * INNER_OPACITY * coverage(inner_dist, float(outline_inner_width)); + rgb = colour.rgb * inner_alpha + rgb * (1.0 - inner_alpha); + alpha = inner_alpha + alpha * (1.0 - inner_alpha); + } + if (top_id != 0) + { + vec4 top_colour = paletteColour(top_id, top_visible); + float top_alpha = top_colour.a * coverage(top_dist, float(outline_width)); + vec4 near_colour = paletteColour(near_id, near_visible); + float near_alpha = (near_id != top_id) ? near_colour.a * coverage(near_dist, float(outline_width)) : 0.0; + + vec3 contour_rgb = top_colour.rgb * top_alpha + near_colour.rgb * near_alpha * (1.0 - top_alpha); + float contour_alpha = top_alpha + near_alpha * (1.0 - top_alpha); + rgb = contour_rgb + rgb * (1.0 - contour_alpha); + alpha = contour_alpha + alpha * (1.0 - contour_alpha); + } + + if (alpha <= 0.0) + { + discard; + } + frag_color = vec4(rgb / alpha, alpha); +} diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl new file mode 100644 index 0000000000..fdf045ca66 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl @@ -0,0 +1,73 @@ +/** + * @file selectionTileF.glsl + * @brief The selection outline's tile pass (ALSelectionOutline): which ids a square of the id target holds, and + * whether it holds a jump, so the edge pass can leave the pixels with nothing to draw at once. + * + * $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$ + */ + +// Into RGBA16: .r and .g the lowest and highest nonzero id the tile holds over 65535, 0 for none; .b 1 where it +// holds the near side of a jump; .a 1 where it holds the drawn surface of an object that glows. +out vec4 frag_color; + +// The id target, and the jump pass's marks on it (selectionJumpF.glsl). +uniform sampler2D selectionIdMap; +uniform sampler2D selectionJumpMap; + +// The side of a tile in texels of the id target (ALSelectionOutline::TILE_SIZE). +#define TILE_SIZE 8 + +// selectionUtilF.glsl +int decodeId(vec4 texel); +bool isJumpFront(sampler2D jumps, ivec2 pos); +bool isGlow(vec4 texel); +bool isDrawn(vec4 texel); + +void main() +{ + // The viewport is the tile target's, a fragment to a tile. + ivec2 size = textureSize(selectionIdMap, 0); + ivec2 origin = ivec2(gl_FragCoord.xy) * TILE_SIZE; + ivec2 end = min(origin + ivec2(TILE_SIZE), size); + + int lowest = 65536; + int highest = 0; + bool jump = false; + bool glow = false; + for (int y = origin.y; y < end.y; ++y) + { + for (int x = origin.x; x < end.x; ++x) + { + vec4 texel = texelFetch(selectionIdMap, ivec2(x, y), 0); + int id = decodeId(texel); + if (id != 0) + { + lowest = min(lowest, id); + highest = max(highest, id); + // The jump pass marks no glowing texel. + jump = jump || (!isGlow(texel) && isJumpFront(selectionJumpMap, ivec2(x, y))); + glow = glow || (isGlow(texel) && isDrawn(texel)); + } + } + } + + frag_color = vec4(float(highest != 0 ? lowest : 0) / 65535.0, float(highest) / 65535.0, jump ? 1.0 : 0.0, + glow ? 1.0 : 0.0); +} diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionUtilF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionUtilF.glsl new file mode 100644 index 0000000000..2ea6dbee52 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionUtilF.glsl @@ -0,0 +1,79 @@ +/** + * @file selectionUtilF.glsl + * @brief What the selection outline's passes (ALSelectionOutline) read the id target with, linked into each. + * + * $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$ + */ + +// Shared matrix stack + derived matrices, spliced from +// class1/deferred/matricesBlock.glsl and bound at UB_MATRICES. +//[ENGINE_BLOCK Matrices] + +// An id texel: the id in .rg, low byte then high, 0 for none; in .b whether the outline draws the surface, 1 +// visible, 0.5 hidden and drawn dimmed, 0 hidden and left out; in .a the priority, 0 the highest. + +// The priority of what the pointer is over, which glows instead of being outlined +// (ALSelectionOutline::PRIORITY_HOVER). +#define PRIORITY_HOVER 6 + +int decodeId(vec4 texel) +{ + return int(texel.r * 255.0 + 0.5) + (int(texel.g * 255.0 + 0.5) << 8); +} + +int decodePriority(vec4 texel) +{ + return int(texel.a * 255.0 + 0.5); +} + +bool isGlow(vec4 texel) +{ + return decodePriority(texel) == PRIORITY_HOVER; +} + +// Whether the jump pass (selectionJumpF.glsl) marked the id texel at `pos` as the near side of a jump in its +// object's surface, in `jumps`, its target: 1 there, 0 elsewhere. +bool isJumpFront(sampler2D jumps, ivec2 pos) +{ + return texelFetch(jumps, pos, 0).r > 0.5; +} + +bool isDrawn(vec4 texel) +{ + return texel.b > 0.25; +} + +bool isVisible(vec4 texel) +{ + return texel.b > 0.75; +} + +// Distance in front of the eye of a stored depth, through the projection loaded for the pass. The cleared depth of +// an infinite projection unprojects to w = 0, floored as deferredUtil.glsl floors it. +float eyeDistance(float depth) +{ +#ifdef REVERSE_Z + float ndc_z = depth; +#else + float ndc_z = depth * 2.0 - 1.0; +#endif + vec4 pos = inv_proj * vec4(0.0, 0.0, ndc_z, 1.0); + return -pos.z / max(pos.w, 0.000001); +} diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionWireframeF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionWireframeF.glsl new file mode 100644 index 0000000000..9e127f3b57 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionWireframeF.glsl @@ -0,0 +1,102 @@ +/** + * @file selectionWireframeF.glsl + * @brief The selection wireframe (ALSelectionOutline): an anti-aliased line along each edge of a selected triangle, + * in its object's outline colour, dimmed where something hides it. + * + * $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$ + */ + +out vec4 frag_color; + +in vec2 wire_texcoord; +noperspective in vec3 edge_distance; +noperspective in vec2 window_pos; + +// The object's id, which picks its colours from the palette. +uniform int selection_id; + +// Two texels per id, PALETTE_WIDTH ids to a pair of rows: the visible colour above the hidden one. Alpha 0 in the +// hidden row leaves hidden parts out. +uniform sampler2D selectionPalette; + +// The id target's depth, the nearest selected surface, which the HUD's lines are tested against. +uniform sampler2D depthMap; + +// What decides whether a line is hidden (ALSelectionOutline::EWirePass): 0, the depth test passed it as visible; 1, +// the depth test passed it as hidden; 2, the id target's depth, where a line behind another selected surface is. +uniform int wireframe_pass; + +// The lines' width in pixels, before the pixel they are anti-aliased across. +uniform int wireframe_width; + +#define PALETTE_WIDTH 256 + +// How far behind the id target's surface a line may lie, as a fraction of the distance, and still be on it. +const float ON_SURFACE_SLACK = 0.001; + +// selectionUtilF.glsl +float eyeDistance(float depth); + +// selectionAlphaF.glsl +float faceAlpha(vec2 texcoord); +bool isCutOut(float alpha); + +void main() +{ + // Derivatives and the texture are read before anything is discarded; the distance only where the id target's + // depth is read, which wireframe_pass decides for the whole draw. + float own = 0.0; + float slope = 0.0; + if (wireframe_pass == 2) + { + own = eyeDistance(gl_FragCoord.z); + slope = fwidth(own); + } + float alpha = faceAlpha(wire_texcoord); + if (isCutOut(alpha)) + { + discard; + } + + // A line centred on each edge, each of the two triangles beside it drawing its half: solid to half the width + // from the edge, and anti-aliased across one pixel past it. + float dist = min(min(edge_distance.x, edge_distance.y), edge_distance.z); + float half_width = 0.5 * float(wireframe_width); + float coverage = 1.0 - smoothstep(half_width - 0.5, half_width + 0.5, dist); + if (coverage <= 0.0) + { + discard; + } + + bool visible = (wireframe_pass == 0); + if (wireframe_pass == 2) + { + float surface = eyeDistance(texelFetch(depthMap, ivec2(window_pos), 0).r); + visible = own <= surface + slope + own * ON_SURFACE_SLACK; + } + + int row = (selection_id / PALETTE_WIDTH) * 2 + (visible ? 0 : 1); + vec4 colour = texelFetch(selectionPalette, ivec2(selection_id % PALETTE_WIDTH, row), 0); + if (colour.a <= 0.0) + { + discard; + } + frag_color = vec4(colour.rgb, colour.a * coverage); +} diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionWireframeG.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionWireframeG.glsl new file mode 100644 index 0000000000..212041c3e5 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionWireframeG.glsl @@ -0,0 +1,185 @@ +/** + * @file selectionWireframeG.glsl + * @brief The selection wireframe (ALSelectionOutline): each selected triangle with every corner's distance in + * pixels to the opposite edge, which the fragment stage turns into lines along the edges. + * + * $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$ + */ + +layout(triangles) in; +// A triangle the near plane cuts leaves a quadrilateral at most: a strip of four. +layout(triangle_strip, max_vertices = 4) out; + +// selectionIdV.glsl's texture coordinates, carried on for the alpha cut. +in vec2 vary_texcoord0[]; +out vec2 wire_texcoord; + +// The distance in pixels to the line of each of the triangle's edges, opposite its first, second and third corner. +// Interpolated without perspective: distance to a line in the window is affine across it, and through perspective a +// line would thicken and thin along a triangle that recedes. +noperspective out vec3 edge_distance; + +// The fragment's position in pixels of the viewport, for reading the id target's depth. +noperspective out vec2 window_pos; + +// The viewport's size in pixels. +uniform vec2 screen_res; + +// Far beyond any line's reach: the distance to an edge the near plane cuts away whole, which draws no line. +const float NO_EDGE = 1.0e6; + +// How far a clip-space position lies inside the near plane, where GL clips: z = -w under forward depth, z = w under +// reverse-Z, whose projection puts the near plane at depth 1. A position behind the eye is outside it, and every +// position the rasteriser keeps has w at least the near plane's distance, so its window position is well defined. +float insideNear(vec4 clip) +{ +#ifdef REVERSE_Z + return clip.w - clip.z; +#else + return clip.w + clip.z; +#endif +} + +vec2 toWindow(vec4 clip) +{ + return (clip.xy / clip.w * 0.5 + 0.5) * screen_res; +} + +void emitCorner(vec4 clip, vec2 texcoord, vec3 distance, vec2 window) +{ + gl_Position = clip; + wire_texcoord = texcoord; + edge_distance = distance; + window_pos = window; + EmitVertex(); +} + +void main() +{ + float inside[3]; + bool cut = false; + bool kept = false; + for (int i = 0; i < 3; ++i) + { + inside[i] = insideNear(gl_in[i].gl_Position); + cut = cut || inside[i] < 0.0; + kept = kept || inside[i] >= 0.0; + } + if (!kept) + { + return; + } + + if (!cut) + { + vec2 corner[3]; + for (int i = 0; i < 3; ++i) + { + vec4 clip = gl_in[i].gl_Position; + corner[i] = (clip.xy / max(clip.w, 1.0e-6) * 0.5 + 0.5) * screen_res; + } + + // Twice the triangle's area over each edge's length: the height of the corner opposite it. + vec2 a = corner[1] - corner[0]; + vec2 b = corner[2] - corner[0]; + float area = abs(a.x * b.y - a.y * b.x); + vec3 height = vec3(area / max(length(corner[2] - corner[1]), 1.0e-6), area / max(length(b), 1.0e-6), + area / max(length(a), 1.0e-6)); + + for (int i = 0; i < 3; ++i) + { + // A corner lies on the two edges through it, at its height from the third. + emitCorner(gl_in[i].gl_Position, vary_texcoord0[i], + vec3(i == 0 ? height.x : 0.0, i == 1 ? height.y : 0.0, i == 2 ? height.z : 0.0), corner[i]); + } + EndPrimitive(); + return; + } + + // The part inside the near plane, as GL clips it: the kept corners, and where each edge crosses the plane, its + // texture coordinates carried along in clip space, which is how the rasteriser interpolates them. + vec4 clip[4]; + vec2 texcoord[4]; + int count = 0; + for (int i = 0; i < 3; ++i) + { + int j = (i + 1) % 3; + if (inside[i] >= 0.0) + { + clip[count] = gl_in[i].gl_Position; + texcoord[count] = vary_texcoord0[i]; + ++count; + } + if ((inside[i] >= 0.0) != (inside[j] >= 0.0)) + { + float t = inside[i] / (inside[i] - inside[j]); + clip[count] = mix(gl_in[i].gl_Position, gl_in[j].gl_Position, t); + texcoord[count] = mix(vary_texcoord0[i], vary_texcoord0[j], t); + ++count; + } + } + + // The line of each original edge in the window, through the ends of its part inside the plane: projection + // keeps lines straight, so that part lies on the edge's own line. An edge cut away whole has none, and the cut + // along the plane is no edge of the triangle's. + vec2 line_start[3]; + vec2 line_dir[3]; + bool has_line[3]; + for (int k = 0; k < 3; ++k) + { + int i = (k + 1) % 3; + int j = (k + 2) % 3; + has_line[k] = inside[i] >= 0.0 || inside[j] >= 0.0; + vec4 from = gl_in[i].gl_Position; + vec4 to = gl_in[j].gl_Position; + if (inside[i] < 0.0) + { + from = mix(from, to, inside[i] / (inside[i] - inside[j])); + } + else if (inside[j] < 0.0) + { + to = mix(to, from, inside[j] / (inside[j] - inside[i])); + } + line_start[k] = toWindow(from); + line_dir[k] = toWindow(to) - line_start[k]; + has_line[k] = has_line[k] && length(line_dir[k]) > 1.0e-6; + } + + vec2 window[4]; + vec3 distance[4]; + for (int v = 0; v < count; ++v) + { + window[v] = toWindow(clip[v]); + for (int k = 0; k < 3; ++k) + { + vec2 from_start = window[v] - line_start[k]; + distance[v][k] = has_line[k] ? abs(line_dir[k].x * from_start.y - line_dir[k].y * from_start.x) / length(line_dir[k]) + : NO_EDGE; + } + } + + // Three corners, or four as a strip: the first, second, fourth and third. + for (int n = 0; n < count; ++n) + { + int v = (count == 4 && n >= 2) ? 5 - n : n; + emitCorner(clip[v], texcoord[v], distance[v], window[v]); + } + EndPrimitive(); +} diff --git a/indra/newview/app_settings/shaders/class2/deferred/softenLightV.glsl b/indra/newview/app_settings/shaders/class2/deferred/softenLightV.glsl index e490ad72ee..d87970a03c 100644 --- a/indra/newview/app_settings/shaders/class2/deferred/softenLightV.glsl +++ b/indra/newview/app_settings/shaders/class2/deferred/softenLightV.glsl @@ -35,9 +35,15 @@ void setAdditiveColor(vec3 c); void main() { - //transform vertex + // Level with the depth the sky leaves cleared, as the haze's triangle is (hazeV.glsl): the geometry draw passes + // only in front of the far plane and the sky's draw only on it (LLPipeline::renderDeferredLighting), so each + // pixel is shaded once, by the program for what it is. Reverse-Z puts the far plane at ndc z 0. +#ifdef REVERSE_Z + gl_Position = vec4(position.xy, 0.0, 1.0); +#else + gl_Position = vec4(position.xy, 1.0, 1.0); +#endif vec4 pos = vec4(position.xyz, 1.0); - gl_Position = pos; // appease OSX GLSL compiler/linker by touching all the varyings we said we would setAtmosAttenuation(vec3(1)); diff --git a/indra/newview/app_settings/shaders/class2/deferred/sunLightV.glsl b/indra/newview/app_settings/shaders/class2/deferred/sunLightV.glsl index 0e9f7bbfb2..38036703f4 100644 --- a/indra/newview/app_settings/shaders/class2/deferred/sunLightV.glsl +++ b/indra/newview/app_settings/shaders/class2/deferred/sunLightV.glsl @@ -31,9 +31,14 @@ uniform vec2 screen_res; void main() { - //transform vertex - vec4 pos = vec4(position.xyz, 1.0); - gl_Position = pos; + // Level with the depth the sky leaves cleared, as the haze's triangle is (hazeV.glsl): the draw passes only in + // front of the far plane (LLPipeline::renderDeferredLighting), so no sky pixel is shadowed or occluded, and the + // light map keeps the clear's white there. Reverse-Z puts the far plane at ndc z 0. +#ifdef REVERSE_Z + gl_Position = vec4(position.xy, 0.0, 1.0); +#else + gl_Position = vec4(position.xy, 1.0, 1.0); +#endif - vary_fragcoord = (pos.xy * 0.5 + 0.5); + vary_fragcoord = (position.xy * 0.5 + 0.5); } diff --git a/indra/newview/app_settings/shaders/class3/deferred/multiPointLightV.glsl b/indra/newview/app_settings/shaders/class3/deferred/multiPointLightV.glsl index 09fd389370..7baae7b769 100644 --- a/indra/newview/app_settings/shaders/class3/deferred/multiPointLightV.glsl +++ b/indra/newview/app_settings/shaders/class3/deferred/multiPointLightV.glsl @@ -29,9 +29,15 @@ out vec4 vary_fragcoord; void main() { - //transform vertex vec4 pos = vec4(position.xyz, 1.0); vary_fragcoord = pos; - gl_Position = pos; + // Level with the depth the sky leaves cleared, as the haze's triangle is (hazeV.glsl): the draw passes only in + // front of the far plane (LLPipeline::renderDeferredLighting), so the sky is rejected before a light is shaded + // over it. Reverse-Z puts the far plane at ndc z 0. +#ifdef REVERSE_Z + gl_Position = vec4(position.xy, 0.0, 1.0); +#else + gl_Position = vec4(position.xy, 1.0, 1.0); +#endif } diff --git a/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl b/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl index a4a09e7256..3506eaf7b7 100644 --- a/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl +++ b/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl @@ -152,9 +152,44 @@ float ssaoVisibility(float ambocc) #endif } +// The colour the sky family wrote, which nothing lights: an HDRI sky's as it is, WindLight's scaled for fake HDR. +vec3 skyColor(GBufferInfo gb) +{ + if (GET_GBUFFER_FLAG(gb.gbufferFlag, GBUFFER_FLAG_HAS_HDRI)) + { + // actual HDRI sky, just copy color value + return gb.emissive.rgb; + } + + //should only be true of WL sky, port over base color value and scale for fake HDR +#if defined(HAS_EMISSIVE) + vec3 color = srgb_to_linear(gb.emissive.rgb); +#else + // the sky wrote display-encoded values raw into the sRGB albedo target, and this pass reads it + // with hardware decode, so baseColor is already linear; decoding again darkened the vintage sky + vec3 color = gb.albedo.rgb; +#endif + return color * sky_hdr_scale; +} + +void writeColor(vec3 color) +{ + float final_scale = 1; + if (classic_mode > 0) + final_scale = 1.1; + + frag_color.rgb = clampHDRRange(color.rgb * final_scale); //output linear since local lights will be added to this shader's results + frag_color.a = 0.0; +} + void main() { vec2 tc = vary_fragcoord.xy; +#ifdef SOFTEN_SKY + // This variant's draw passes only at the cleared depth, where nothing but the sky family draws, so nothing here + // is lit or hazed: the sky's colour goes through as it was written (LLPipeline::renderDeferredLighting). + writeColor(skyColor(getGBuffer(tc))); +#else float depth = getDepth(tc.xy); vec4 pos = getPositionWithDepth(tc, depth); @@ -237,22 +272,10 @@ void main() vec3 v = -normalize(pos.xyz); color = pbrBaseLight(diffuseColor, specularColor, metallic, v, gb.normal, perceptualRoughness, light_dir, sunlit_linear, scol, radiance, irradiance, colorEmissive, visibility, additive, atten); } - else if (GET_GBUFFER_FLAG(gb.gbufferFlag, GBUFFER_FLAG_HAS_HDRI)) - { - // actual HDRI sky, just copy color value - color = colorEmissive.rgb; - } - else if (GET_GBUFFER_FLAG(gb.gbufferFlag, GBUFFER_FLAG_SKIP_ATMOS)) + else if (GET_GBUFFER_FLAG(gb.gbufferFlag, GBUFFER_FLAG_HAS_HDRI) || GET_GBUFFER_FLAG(gb.gbufferFlag, GBUFFER_FLAG_SKIP_ATMOS)) { - //should only be true of WL sky, port over base color value and scale for fake HDR -#if defined(HAS_EMISSIVE) - color = srgb_to_linear(colorEmissive.rgb); -#else - // the sky wrote display-encoded values raw into the sRGB albedo target, and this pass reads it - // with hardware decode, so baseColor is already linear; decoding again darkened the vintage sky - color = baseColor.rgb; -#endif - color *= sky_hdr_scale; + // unlit in front of the far plane, where the sky's own draw does not reach: a debug highlight writes SKIP_ATMOS + color = skyColor(gb); } else { @@ -336,10 +359,6 @@ void main() } //color.r = classic_mode > 0 ? 1.0 : 0.0; - float final_scale = 1; - if (classic_mode > 0) - final_scale = 1.1; - - frag_color.rgb = clampHDRRange(color.rgb * final_scale); //output linear since local lights will be added to this shader's results - frag_color.a = 0.0; + writeColor(color); +#endif } diff --git a/indra/newview/llagentcamera.h b/indra/newview/llagentcamera.h index 336632be3e..41d3e0766e 100644 --- a/indra/newview/llagentcamera.h +++ b/indra/newview/llagentcamera.h @@ -153,7 +153,6 @@ class LLAgentCamera F32 getCameraMinOffGround(); // Minimum height off ground for this mode, meters void setCameraCollidePlane(const LLVector4 &plane) { mCameraCollidePlane = plane; } bool calcCameraMinDistance(F32 &obj_min_distance); - F32 getCurrentCameraBuildOffset() const { return (F32)mCameraFocusOffset.length(); } void clearCameraLag() { mCameraLag.clearVec(); } const LLVector3& getCameraUpVector() const { return mCameraUpVector; } private: diff --git a/indra/newview/llface.cpp b/indra/newview/llface.cpp index 9d5dbf0a27..2959e7f8e6 100644 --- a/indra/newview/llface.cpp +++ b/indra/newview/llface.cpp @@ -595,70 +595,6 @@ void LLFace::renderSelected(LLViewerTexture *imagep, const LLColor4& color) } } - -void renderFace(LLDrawable* drawable, LLFace *face) -{ - LL_PROFILE_ZONE_SCOPED_CATEGORY_FACE; - - LLVOVolume* vobj = drawable->getVOVolume(); - if (vobj) - { - LLVolume* volume = NULL; - - if (drawable->isState(LLDrawable::RIGGED)) - { - volume = vobj->getRiggedVolume(); - } - else - { - volume = vobj->getVolume(); - } - - if (volume) - { - const LLVolumeFace& vol_face = volume->getVolumeFace(face->getTEOffset()); - LLVertexBuffer::drawElements(LLRender::TRIANGLES, vol_face.mPositions, NULL, vol_face.mNumIndices, vol_face.mIndices); - } - } -} - -void LLFace::renderOneWireframe(const LLColor4 &color, F32 fogCfx, bool wireframe_selection, bool bRenderHiddenSelections, bool shader) -{ - if (bRenderHiddenSelections) - { - gGL.blendFunc(LLRender::BF_SOURCE_COLOR, LLRender::BF_ONE); - LLGLDepthTest gls_depth(GL_TRUE, GL_FALSE, GL_GEQUAL); - if (shader) - { - gGL.diffuseColor4f(color.mV[VRED], color.mV[VGREEN], color.mV[VBLUE], 0.4f); - renderFace(mDrawablep, this); - } - else - { - gGL.flush(); - { - gGL.diffuseColor4f(color.mV[VRED], color.mV[VGREEN], color.mV[VBLUE], 0.4f); - renderFace(mDrawablep, this); - } - } - } - - gGL.flush(); - gGL.setSceneBlendType(LLRender::BT_ALPHA); - - gGL.diffuseColor4f(color.mV[VRED] * 2, color.mV[VGREEN] * 2, color.mV[VBLUE] * 2, color.mV[VALPHA]); - - { - LLGLDisable depth(wireframe_selection ? 0 : GL_BLEND); - - LLGLEnable offset(GL_POLYGON_OFFSET_LINE); - gGL.setPolygonOffset(3.f, 3.f); - gGL.setLineWidth(5.f); - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); - renderFace(mDrawablep, this); - } -} - void LLFace::setDrawInfo(LLDrawInfo* draw_info) { mDrawInfo = draw_info; diff --git a/indra/newview/llface.h b/indra/newview/llface.h index 0d9ab4f788..7d11d8a923 100644 --- a/indra/newview/llface.h +++ b/indra/newview/llface.h @@ -187,7 +187,6 @@ class alignas(16) LLFace void renderSelectedUV(); void renderSelected(LLViewerTexture *image, const LLColor4 &color); - void renderOneWireframe(const LLColor4 &color, F32 fogCfx, bool wireframe_selection, bool bRenderHiddenSelections, bool shader); F32 getKey() const { return mDistance; } diff --git a/indra/newview/llgltfmaterialpreviewmgr.cpp b/indra/newview/llgltfmaterialpreviewmgr.cpp index d60cb76383..3df66b433f 100644 --- a/indra/newview/llgltfmaterialpreviewmgr.cpp +++ b/indra/newview/llgltfmaterialpreviewmgr.cpp @@ -428,8 +428,8 @@ bool LLGLTFPreviewTexture::render() if (!mShouldRender) { return false; } - glClearColor(0, 0, 0, 0); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + gGL.setClearColor(LLColor4::transparent); + gGL.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); LLGLDepthTest depth(GL_FALSE); LLGLDisable stencil(GL_STENCIL_TEST); @@ -507,7 +507,7 @@ bool LLGLTFPreviewTexture::render() // Alpha blend rendering screen.bindTarget(); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + gGL.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); LLGLSLShader& shader = *gDeferredPBRAlphaProgram.selectVariant(); diff --git a/indra/newview/llpanelprimmediacontrols.cpp b/indra/newview/llpanelprimmediacontrols.cpp index 269e9c1244..65455f7018 100644 --- a/indra/newview/llpanelprimmediacontrols.cpp +++ b/indra/newview/llpanelprimmediacontrols.cpp @@ -63,10 +63,6 @@ #include "llfloatertools.h" // to enable hide if build tools are up #include "llvector4a.h" - -// Functions pulled from llviewerdisplay.cpp -bool get_hud_matrices(LLMatrix4a& proj, LLMatrix4a& model); - // Warning: make sure these two match! const LLPanelPrimMediaControls::EZoomLevel LLPanelPrimMediaControls::kZoomLevels[] = { ZOOM_NONE, ZOOM_MEDIUM }; const int LLPanelPrimMediaControls::kNumZoomLevels = 2; diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index e8d25aa030..a649790825 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -51,7 +51,9 @@ #include "llquaternion.h" // viewer includes +#include "alhoverglow.h" #include "alobjectproperties.h" +#include "alselectionoutline.h" #include "llagent.h" #include "llagentcamera.h" #include "llattachmentsmgr.h" @@ -221,8 +223,6 @@ static void deleteLocalPreviewSelection() // Consts // -constexpr F32 SILHOUETTE_UPDATE_THRESHOLD_SQUARED = 0.02f; -constexpr S32 MAX_SILS_PER_FRAME = 50; constexpr S32 MAX_OBJECTS_PER_PACKET = 254; // For linked sets constexpr S32 MAX_CHILDREN_PER_TASK = 255; @@ -231,13 +231,7 @@ constexpr S32 MAX_CHILDREN_PER_TASK = 255; bool LLSelectMgr::sRectSelectInclusive = true; bool LLSelectMgr::sRenderHiddenSelections = true; bool LLSelectMgr::sRenderLightRadius = false; -F32 LLSelectMgr::sHighlightThickness = 0.f; -F32 LLSelectMgr::sHighlightUScale = 0.f; -F32 LLSelectMgr::sHighlightVScale = 0.f; F32 LLSelectMgr::sHighlightAlpha = 0.f; -F32 LLSelectMgr::sHighlightAlphaTest = 0.f; -F32 LLSelectMgr::sHighlightUAnim = 0.f; -F32 LLSelectMgr::sHighlightVAnim = 0.f; LLUIColor LLSelectMgr::sSilhouetteParentColor; LLUIColor LLSelectMgr::sSilhouetteChildColor; LLUIColor LLSelectMgr::sHighlightInspectColor; @@ -331,15 +325,8 @@ LLSelectMgr::LLSelectMgr() { mTEMode = false; mTextureChannel = LLRender::DIFFUSE_MAP; - mLastCameraPos.clearVec(); - sHighlightThickness = gSavedSettings.getF32("SelectionHighlightThickness"); - sHighlightUScale = gSavedSettings.getF32("SelectionHighlightUScale"); - sHighlightVScale = gSavedSettings.getF32("SelectionHighlightVScale"); sHighlightAlpha = gSavedSettings.getF32("SelectionHighlightAlpha") * 2; - sHighlightAlphaTest = gSavedSettings.getF32("SelectionHighlightAlphaTest"); - sHighlightUAnim = gSavedSettings.getF32("SelectionHighlightUAnim"); - sHighlightVAnim = gSavedSettings.getF32("SelectionHighlightVAnim"); sSilhouetteParentColor =LLUIColorTable::instance().getColor("SilhouetteParentColor"); sSilhouetteChildColor = LLUIColorTable::instance().getColor("SilhouetteChildColor"); @@ -1845,15 +1832,6 @@ void LLSelectMgr::dump() LL_INFOS() << "Center global " << mSelectionCenterGlobal << LL_ENDL; } -//----------------------------------------------------------------------------- -// cleanup() -//----------------------------------------------------------------------------- -void LLSelectMgr::cleanup() -{ - mSilhouetteImagep = NULL; -} - - //--------------------------------------------------------------------------- // Manipulate properties of selected objects //--------------------------------------------------------------------------- @@ -6589,49 +6567,16 @@ void LLSelectMgr::processForceObjectSelect(LLMessageSystem* msg, void**) } -void LLSelectMgr::updateSilhouettes() +void LLSelectMgr::updateHighlightedObjects() { - S32 num_sils_genned = 0; - - LLVector3d cameraPos = gAgentCamera.getCameraPositionGlobal(); - F32 currentCameraZoom = gAgentCamera.getCurrentCameraBuildOffset(); - - if (!mSilhouetteImagep) - { - mSilhouetteImagep = LLViewerTextureManager::getFetchedTextureFromFile("silhouette.j2c", FTT_LOCAL_FILE, true, LLGLTexture::BOOST_UI); - } - mHighlightedObjects->cleanupNodes(); - if((cameraPos - mLastCameraPos).magVecSquared() > SILHOUETTE_UPDATE_THRESHOLD_SQUARED * currentCameraZoom * currentCameraZoom) - { - struct f : public LLSelectedObjectFunctor - { - virtual bool apply(LLViewerObject* object) - { - object->setChanged(LLXform::SILHOUETTE); - return true; - } - } func; - getSelection()->applyToObjects(&func); - - mLastCameraPos = gAgentCamera.getCameraPositionGlobal(); - } - - std::vector changed_objects; - - updateSelectionSilhouette(mSelectedObjects, num_sils_genned, changed_objects); if (mRectSelectedObjects.size() > 0) { - //gGLSPipelineSelection.set(); - - //mSilhouetteImagep->bindTexture(); - //glAlphaFunc(GL_GREATER, sHighlightAlphaTest); - std::set roots; // sync mHighlightedObjects with mRectSelectedObjects since the latter is rebuilt every frame and former - // persists from frame to frame to avoid regenerating object silhouettes + // persists from frame to frame // mHighlightedObjects includes all siblings of rect selected objects bool select_linked_set = !gSavedSettings.getBOOL("EditLinkedParts"); @@ -6740,380 +6685,165 @@ void LLSelectMgr::updateSilhouettes() // Add the root last, to preserve order for link operations. mHighlightedObjects->addNodeAtEnd(rect_select_root_node); } - - num_sils_genned = 0; - - // render silhouettes for highlighted objects - //bool subtracting_from_selection = (gKeyboard->currentMask(true) == MASK_CONTROL); - for (S32 pass = 0; pass < 2; pass++) - { - for (LLObjectSelection::iterator iter = mHighlightedObjects->begin(); - iter != mHighlightedObjects->end(); iter++) - { - LLSelectNode* node = *iter; - LLViewerObject* objectp = node->getObject(); - if (!objectp) - continue; - - // do roots first, then children so that root flags are cleared ASAP - bool roots_only = (pass == 0); - bool is_root = objectp->isRootEdit(); - if (roots_only != is_root) - { - continue; - } - - if (!node->mSilhouetteExists - || objectp->isChanged(LLXform::SILHOUETTE) - || (objectp->getParent() && objectp->getParent()->isChanged(LLXform::SILHOUETTE))) - { - if (num_sils_genned++ < MAX_SILS_PER_FRAME) - { - generateSilhouette(node, LLViewerCamera::getInstance()->getOrigin()); - changed_objects.push_back(objectp); - } - else if (objectp->isAttachment() && objectp->getRootEdit()->mDrawable.notNull()) - { - //RN: hack for orthogonal projection of HUD attachments - LLViewerJointAttachment* attachment_pt = (LLViewerJointAttachment*)objectp->getRootEdit()->mDrawable->getParent(); - if (attachment_pt && attachment_pt->getIsHUDAttachment()) - { - LLVector3 camera_pos = LLVector3(-10000.f, 0.f, 0.f); - generateSilhouette(node, camera_pos); - } - } - } - //LLColor4 highlight_color; - // - //if (subtracting_from_selection) - //{ - // node->renderOneSilhouette(LLColor4::red); - //} - //else if (!objectp->isSelected()) - //{ - // highlight_color = objectp->isRoot() ? sHighlightParentColor : sHighlightChildColor; - // node->renderOneSilhouette(highlight_color); - //} - } - } - //mSilhouetteImagep->unbindTexture(0, GL_TEXTURE_2D); } else { mHighlightedObjects->deleteAllNodes(); } - - for (std::vector::iterator iter = changed_objects.begin(); - iter != changed_objects.end(); ++iter) - { - // clear flags after traversing node list (as child objects need to refer to parent flags, etc) - LLViewerObject* objectp = *iter; - objectp->clearChanged(LLXform::MOVED | LLXform::SILHOUETTE); - } } -void LLSelectMgr::updateSelectionSilhouette(LLObjectSelectionHandle object_handle, S32& num_sils_genned, std::vector& changed_objects) -{ - if (object_handle->getNumNodes()) - { - //gGLSPipelineSelection.set(); - - //mSilhouetteImagep->bindTexture(); - //glAlphaFunc(GL_GREATER, sHighlightAlphaTest); - - for (S32 pass = 0; pass < 2; pass++) - { - for (LLObjectSelection::iterator iter = object_handle->begin(); - iter != object_handle->end(); iter++) - { - LLSelectNode* node = *iter; - LLViewerObject* objectp = node->getObject(); - if (!objectp) - continue; - // do roots first, then children so that root flags are cleared ASAP - bool roots_only = (pass == 0); - bool is_root = (objectp->isRootEdit()); - if (roots_only != is_root || objectp->mDrawable.isNull()) - { - continue; - } - - if (!node->mSilhouetteExists - || objectp->isChanged(LLXform::SILHOUETTE) - || (objectp->getParent() && objectp->getParent()->isChanged(LLXform::SILHOUETTE))) - { - if (num_sils_genned++ < MAX_SILS_PER_FRAME)// && objectp->mDrawable->isVisible()) - { - generateSilhouette(node, LLViewerCamera::getInstance()->getOrigin()); - changed_objects.push_back(objectp); - } - else if (objectp->isAttachment()) - { - //RN: hack for orthogonal projection of HUD attachments - LLViewerJointAttachment* attachment_pt = (LLViewerJointAttachment*)objectp->getRootEdit()->mDrawable->getParent(); - if (attachment_pt && attachment_pt->getIsHUDAttachment()) - { - LLVector3 camera_pos = LLVector3(-10000.f, 0.f, 0.f); - generateSilhouette(node, camera_pos); - } - } - } - } - } - } -} -void LLSelectMgr::renderSilhouettes(bool for_hud) +// The selected and highlighted objects in the world, or on the HUD, each in the colour of its role, drawn with +// `parts` (ALSelectionOutline::EPart). +void LLSelectMgr::addOutlines(ALSelectionOutline& outline, bool for_hud, U32 parts) { - if (!mRenderSilhouettes || !mRenderHighlightSelections) - { - return; - } - - gGL.getTextureSlot(0)->bindSampled(mSilhouetteImagep, ALSamplers::AnisoWrap); - LLGLSPipelineSelection gls_select; - LLGLEnable blend(GL_BLEND); - LLGLDepthTest gls_depth(GL_TRUE, GL_FALSE); - - if (isAgentAvatarValid() && for_hud) - { - LLBBox hud_bbox = gAgentAvatarp->getHUDBBox(); - - F32 cur_zoom = gAgentCamera.mHUDCurZoom; - - // set up transform to encompass bounding box of HUD - gGL.matrixMode(LLRender::MM_PROJECTION); - gGL.pushMatrix(); - gGL.loadIdentity(); - F32 depth = llmax(1.f, hud_bbox.getExtentLocal().mV[VX] * 1.1f); - gGL.ortho(-0.5f * LLViewerCamera::getInstance()->getAspect(), 0.5f * LLViewerCamera::getInstance()->getAspect(), -0.5f, 0.5f, 0.f, depth); - - gGL.matrixMode(LLRender::MM_MODELVIEW); - gGL.pushMatrix(); - gGL.pushUIMatrix(); - gGL.loadUIIdentity(); - gGL.loadIdentity(); - gGL.loadMatrix(OGL_TO_CFR_ROTATION); // Load Cory's favorite reference frame - gGL.translatef(-hud_bbox.getCenterLocal().mV[VX] + (depth *0.5f), 0.f, 0.f); - gGL.scalef(cur_zoom, cur_zoom, cur_zoom); - } - - bool wireframe_selection = (gFloaterTools && gFloaterTools->getVisible()) || LLSelectMgr::sRenderHiddenSelections; - F32 fogCfx = (F32)llclamp((LLSelectMgr::getInstance()->getSelectionCenterGlobal() - gAgentCamera.getCameraPositionGlobal()).magVec() / (LLSelectMgr::getInstance()->getBBoxOfSelection().getExtentLocal().magVec() * 4), 0.0, 1.0); - - LLColor4 sParentColor = sSilhouetteParentColor; - sParentColor.mV[VALPHA] = LLSelectMgr::sHighlightAlpha; - LLColor4 sChildColor = sSilhouetteChildColor; - sChildColor.mV[VALPHA] = LLSelectMgr::sHighlightAlpha; - - auto renderMeshSelection_f = [fogCfx, wireframe_selection](LLSelectNode* node, LLViewerObject* objectp, LLColor4 hlColor) - { - LLGLSLShader* shader = LLGLSLShader::sCurBoundShaderPtr; - - if (shader) - { - gDebugProgram.bind(); - } - - gGL.matrixMode(LLRender::MM_MODELVIEW); - gGL.pushMatrix(); - - bool is_hud_object = objectp->isHUDAttachment(); - - if (!is_hud_object) - { - gGL.loadIdentity(); - gGL.multMatrix(LLViewerCamera::getCurrent().getModelview()); - } - - if (objectp->mDrawable->isActive()) - { - gGL.multMatrix(objectp->getRenderMatrix().getF32ptr()); - } - else if (!is_hud_object) - { - LLVector3 trans = objectp->getRegion()->getOriginAgent(); - gGL.translatef(trans.mV[0], trans.mV[1], trans.mV[2]); - } - - bool bRenderHidenSelection = node->isTransient() ? false : LLSelectMgr::sRenderHiddenSelections; - - - LLVOVolume* vobj = objectp->mDrawable->getVOVolume(); - if (vobj) - { - LLVertexBuffer::unbind(); - gGL.pushMatrix(); - gGL.multMatrix((F32*)vobj->getRelativeXform().mMatrix); - - if (objectp->mDrawable->isState(LLDrawable::RIGGED)) - { - vobj->updateRiggedVolume(true); - } - } - - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); - - S32 num_tes = llmin((S32)objectp->getNumTEs(), (S32)objectp->getNumFaces()); // avatars have TEs but no faces - for (S32 te = 0; te < num_tes; ++te) - { - if (node->isTESelected(te)) - { - objectp->mDrawable->getFace(te)->renderOneWireframe(hlColor, fogCfx, wireframe_selection, bRenderHidenSelection, nullptr != shader); - } - } - - gGL.popMatrix(); - gGL.popMatrix(); - - gGL.setLineWidth(1.f); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - - if (shader) - { - shader->bind(); - } - }; - if (mSelectedObjects->getNumNodes()) { - LLUUID inspect_item_id= LLUUID::null; + LLUUID inspect_item_id = LLUUID::null; LLFloaterInspect* inspect_instance = LLFloaterReg::findTypedInstance("inspect"); - if(inspect_instance && inspect_instance->getVisible()) + if (inspect_instance && inspect_instance->getVisible()) { inspect_item_id = inspect_instance->getSelectedUUID(); } else { - LLSidepanelTaskInfo *panel_task_info = LLSidepanelTaskInfo::getActivePanel(); + LLSidepanelTaskInfo* panel_task_info = LLSidepanelTaskInfo::getActivePanel(); if (panel_task_info) { inspect_item_id = panel_task_info->getSelectedUUID(); } } - LLUUID focus_item_id = LLViewerMediaFocus::getInstance()->getFocusedObjectID(); - for (S32 pass = 0; pass < 2; pass++) + const LLUUID focus_item_id = LLViewerMediaFocus::getInstance()->getFocusedObjectID(); + for (LLObjectSelection::iterator iter = mSelectedObjects->begin(); iter != mSelectedObjects->end(); iter++) { - for (LLObjectSelection::iterator iter = mSelectedObjects->begin(); - iter != mSelectedObjects->end(); iter++) + LLSelectNode* node = *iter; + if (getTEMode() && !node->hasSelectedTE()) { - LLSelectNode* node = *iter; - - if (getTEMode() && !node->hasSelectedTE()) - continue; + continue; + } - LLViewerObject* objectp = node->getObject(); - if (!objectp) - continue; + LLViewerObject* objectp = node->getObject(); + if (!objectp || objectp->isHUDAttachment() != for_hud) + { + continue; + } - if (objectp->mDrawable - && objectp->mDrawable->getVOVolume() - && objectp->mDrawable->getVOVolume()->isMesh()) - { - LLColor4 hlColor = objectp->isRootEdit() ? sParentColor : sChildColor; - if (objectp->getID() == inspect_item_id) - { - hlColor = sHighlightInspectColor; - } - else if (node->isTransient()) - { - hlColor = sContextSilhouetteColor; - } - renderMeshSelection_f(node, objectp, hlColor); - } - else - { - if (objectp->isHUDAttachment() != for_hud) - { - continue; - } - if (objectp->getID() == focus_item_id) - { - node->renderOneSilhouette(gFocusMgr.getFocusColor()); - } - else if (objectp->getID() == inspect_item_id) - { - node->renderOneSilhouette(sHighlightInspectColor); - } - else if (node->isTransient()) - { - bool oldHidden = LLSelectMgr::sRenderHiddenSelections; - LLSelectMgr::sRenderHiddenSelections = false; - node->renderOneSilhouette(sContextSilhouetteColor); - LLSelectMgr::sRenderHiddenSelections = oldHidden; - } - else if (objectp->isRootEdit()) - { - node->renderOneSilhouette(sSilhouetteParentColor); - } - else - { - node->renderOneSilhouette(sSilhouetteChildColor); - } - } - } //for all selected node's - } //for pass + LLColor4 colour; + bool show_hidden = sRenderHiddenSelections; + ALSelectionOutline::EPriority priority; + if (objectp->getID() == focus_item_id) + { + colour = gFocusMgr.getFocusColor(); + priority = ALSelectionOutline::PRIORITY_FOCUS; + } + else if (objectp->getID() == inspect_item_id) + { + colour = sHighlightInspectColor; + priority = ALSelectionOutline::PRIORITY_INSPECT; + } + else if (node->isTransient()) + { + colour = sContextSilhouetteColor; + show_hidden = false; + priority = ALSelectionOutline::PRIORITY_CONTEXT; + } + else if (objectp->isRootEdit()) + { + colour = sSilhouetteParentColor; + priority = ALSelectionOutline::PRIORITY_ROOT; + } + else + { + colour = sSilhouetteChildColor; + priority = ALSelectionOutline::PRIORITY_CHILD; + } + colour.mV[VALPHA] = sHighlightAlpha; + outline.add(objectp, (U32)node->getTESelectMask(), colour, show_hidden, priority, parts); + } } if (mHighlightedObjects->getNumNodes()) { - // render silhouettes for highlighted objects - bool subtracting_from_selection = (gKeyboard->currentMask(true) == MASK_CONTROL); - for (S32 pass = 0; pass < 2; pass++) + // Rect-select highlights: red for what a control-drag takes out of the selection, otherwise only what it + // would add. + const bool subtracting_from_selection = (gKeyboard->currentMask(true) == MASK_CONTROL); + for (LLObjectSelection::iterator iter = mHighlightedObjects->begin(); iter != mHighlightedObjects->end(); iter++) { - for (LLObjectSelection::iterator iter = mHighlightedObjects->begin(); - iter != mHighlightedObjects->end(); iter++) + LLSelectNode* node = *iter; + LLViewerObject* objectp = node->getObject(); + if (!objectp || objectp->isHUDAttachment() != for_hud) { - LLSelectNode* node = *iter; - LLViewerObject* objectp = node->getObject(); - if (!objectp) - continue; - if (objectp->isHUDAttachment() != for_hud) - { - continue; - } + continue; + } - LLColor4 highlight_color = objectp->isRoot() ? sHighlightParentColor : sHighlightChildColor; - if (objectp->mDrawable - && objectp->mDrawable->getVOVolume() - && objectp->mDrawable->getVOVolume()->isMesh()) - { - renderMeshSelection_f(node, objectp, subtracting_from_selection ? LLColor4::red : highlight_color); - } - else if (subtracting_from_selection) - { - node->renderOneSilhouette(LLColor4::red); - } - else if (!objectp->isSelected()) - { - node->renderOneSilhouette(highlight_color); - } + LLColor4 colour; + ALSelectionOutline::EPriority priority; + if (subtracting_from_selection) + { + colour = LLColor4::red; + priority = ALSelectionOutline::PRIORITY_SUBTRACT; + } + else if (!objectp->isSelected()) + { + colour = objectp->isRoot() ? sHighlightParentColor : sHighlightChildColor; + priority = objectp->isRoot() ? ALSelectionOutline::PRIORITY_ROOT : ALSelectionOutline::PRIORITY_CHILD; + } + else + { + continue; } + colour.mV[VALPHA] = sHighlightAlpha; + outline.add(objectp, (U32)node->getTESelectMask(), colour, sRenderHiddenSelections, priority, parts); } } +} - if (isAgentAvatarValid() && for_hud) +// Outlines from an id target (ALSelectionOutline): one id per object, so every prim keeps its own outline. A frame +// calls this for the world, and for the HUD while the selection is on it. The outline (RenderHighlightSelections) and +// the wireframe (AlchemySelectionWireframe) are drawn each with or without the other, and in the world the glow of +// what the pointer is over (ALHoverGlow) with or without either. +void LLSelectMgr::renderSilhouettes(bool for_hud) +{ + ALSelectionOutline& outline = ALSelectionOutline::instance(); + + ALSelectionOutline::View view; + if (!for_hud) { - gGL.matrixMode(LLRender::MM_PROJECTION); - gGL.popMatrix(); + view = ALSelectionOutline::worldView(); - gGL.matrixMode(LLRender::MM_MODELVIEW); - gGL.popMatrix(); - gGL.popUIMatrix(); - stop_glerror(); + // The fades move with time, whatever hides what they draw. + ALHoverGlow::instance().update(LLFrameTimer::getTotalSeconds(), LLFrameTimer::getFrameCount(), + LLPipeline::RenderHighlightFadeTime, + [](const LLUUID& id) + { + const LLViewerObject* object = gObjectList.findObject(id); + return object && !object->isDead(); + }); + } + else if (!ALSelectionOutline::hudView(view)) + { + // The world's call, every frame, gives the targets up once no call draws with them. + return; } - gGL.getTextureSlot(0)->unbind(); -} - -void LLSelectMgr::generateSilhouette(LLSelectNode* nodep, const LLVector3& view_point) -{ - LLViewerObject* objectp = nodep->getObject(); + // A manipulator's drag hides the outlines without giving up their targets for the drag. + if (!mRenderSilhouettes) + { + return; + } - if (objectp && objectp->getPCode() == LL_PCODE_VOLUME) + static LLCachedControl wireframe(gSavedSettings, "AlchemySelectionWireframe", false); + const U32 parts = (mRenderHighlightSelections ? (U32)ALSelectionOutline::PART_CONTOUR : 0u) | + (wireframe ? (U32)ALSelectionOutline::PART_WIREFRAME : 0u); + if (parts) + { + addOutlines(outline, for_hud, parts); + } + if (!for_hud) { - ((LLVOVolume*)objectp)->generateSilhouette(nodep, view_point); + ALHoverGlow::instance().addTo(outline); } + outline.render(view); } // @@ -7126,7 +6856,6 @@ LLSelectNode::LLSelectNode(LLViewerObject* object, bool glow) mValid(false), mPermissions(new LLPermissions()), mInventorySerial(0), - mSilhouetteExists(false), mDuplicated(false), mTESelectMask(0), mLastTESelected(0), @@ -7172,9 +6901,6 @@ LLSelectNode::LLSelectNode(const LLSelectNode& nodep) mSitName = nodep.mSitName; mCreationDate = nodep.mCreationDate; - mSilhouetteVertices = nodep.mSilhouetteVertices; - mSilhouetteNormals = nodep.mSilhouetteNormals; - mSilhouetteExists = nodep.mSilhouetteExists; mObject = nodep.mObject; std::vector::const_iterator color_iter; @@ -7528,176 +7254,6 @@ bool LLSelectNode::allowOperationOnNode(PermissionBit op, U64 group_proxy_power) return (mPermissions->allowOperationBy(op, proxy_agent_id, group_id)); } -//----------------------------------------------------------------------------- -// renderOneSilhouette() -//----------------------------------------------------------------------------- -void LLSelectNode::renderOneSilhouette(const LLColor4 &color) -{ - LLViewerObject* objectp = getObject(); - if (!objectp) - { - return; - } - - LLDrawable* drawable = objectp->mDrawable; - if(!drawable) - { - return; - } - - LLVOVolume* vobj = drawable->getVOVolume(); - if (vobj && vobj->isMesh()) - { - //This check (if(...)) with assert here just for ensure that this situation will not happens, and can be removed later. For example on the next release. - llassert(!"renderOneWireframe() was removed SL-10194"); - return; - } - - if (!mSilhouetteExists) - { - return; - } - - bool is_hud_object = objectp->isHUDAttachment(); - - if (mSilhouetteVertices.size() == 0 || mSilhouetteNormals.size() != mSilhouetteVertices.size()) - { - return; - } - - - LLGLSLShader* shader = LLGLSLShader::sCurBoundShaderPtr; - - if (shader) - { //use UI program for selection highlights (texture color modulated by vertex color) - gUIProgram.bind(); - } - - gGL.matrixMode(LLRender::MM_MODELVIEW); - gGL.pushMatrix(); - gGL.pushUIMatrix(); - gGL.loadUIIdentity(); - - if (!is_hud_object) - { - gGL.loadIdentity(); - gGL.multMatrix(LLViewerCamera::getCurrent().getModelview()); - } - - - if (drawable->isActive()) - { - gGL.multMatrix(objectp->getRenderMatrix().getF32ptr()); - } - - LLVolume *volume = objectp->getVolume(); - if (volume) - { - F32 silhouette_thickness; - if (isAgentAvatarValid() && is_hud_object) - { - silhouette_thickness = LLSelectMgr::sHighlightThickness / gAgentCamera.mHUDCurZoom; - } - else - { - LLVector3 view_vector = LLViewerCamera::getInstance()->getOrigin() - objectp->getRenderPosition(); - silhouette_thickness = view_vector.magVec() * LLSelectMgr::sHighlightThickness * (LLViewerCamera::getInstance()->getView() / LLViewerCamera::getInstance()->getDefaultFOV()); - } - // In F64 until the fraction is taken: the uptime of a long session in F32 moves the - // highlight's texture in steps. - const F64 animationTime = LLFrameTimer::getUptimeSeconds(); - - F32 u_coord = (F32)fmod(animationTime * LLSelectMgr::sHighlightUAnim, 1.0); - F32 v_coord = 1.f - (F32)fmod(animationTime * LLSelectMgr::sHighlightVAnim, 1.0); - F32 u_divisor = 1.f / ((F32)(mSilhouetteVertices.size() - 1)); - - if (LLSelectMgr::sRenderHiddenSelections) // && gFloaterTools && gFloaterTools->getVisible()) - { - gGL.flush(); - gGL.blendFunc(LLRender::BF_SOURCE_COLOR, LLRender::BF_ONE); - - LLGLDepthTest gls_depth(GL_TRUE, GL_FALSE, GL_GEQUAL); - gGL.flush(); - gGL.begin(LLRender::LINES); - { - gGL.color4f(color.mV[VRED], color.mV[VGREEN], color.mV[VBLUE], 0.4f); - - for(S32 i = 0; i < mSilhouetteVertices.size(); i += 2) - { - u_coord += u_divisor * LLSelectMgr::sHighlightUScale; - gGL.texCoord2f( u_coord, v_coord ); - gGL.vertex3fv( mSilhouetteVertices[i].mV); - u_coord += u_divisor * LLSelectMgr::sHighlightUScale; - gGL.texCoord2f( u_coord, v_coord ); - gGL.vertex3fv(mSilhouetteVertices[i+1].mV); - } - } - gGL.end(); - u_coord = (F32)fmod(animationTime * LLSelectMgr::sHighlightUAnim, 1.0); - } - - gGL.flush(); - gGL.setSceneBlendType(LLRender::BT_ALPHA); - gGL.begin(LLRender::TRIANGLES); - { - for(S32 i = 0; i < mSilhouetteVertices.size(); i+=2) - { - if (!mSilhouetteNormals[i].isFinite() || - !mSilhouetteNormals[i+1].isFinite()) - { //skip skewed segments - continue; - } - - LLVector3 v[4]; - LLVector2 tc[4]; - v[0] = mSilhouetteVertices[i] + (mSilhouetteNormals[i] * silhouette_thickness); - tc[0].set(u_coord, v_coord + LLSelectMgr::sHighlightVScale); - - v[1] = mSilhouetteVertices[i]; - tc[1].set(u_coord, v_coord); - - u_coord += u_divisor * LLSelectMgr::sHighlightUScale; - - v[2] = mSilhouetteVertices[i+1] + (mSilhouetteNormals[i+1] * silhouette_thickness); - tc[2].set(u_coord, v_coord + LLSelectMgr::sHighlightVScale); - - v[3] = mSilhouetteVertices[i+1]; - tc[3].set(u_coord,v_coord); - - gGL.color4f(color.mV[VRED], color.mV[VGREEN], color.mV[VBLUE], 0.0f); //LLSelectMgr::sHighlightAlpha); - gGL.texCoord2fv(tc[0].mV); - gGL.vertex3fv( v[0].mV ); - - gGL.color4f(color.mV[VRED]*2, color.mV[VGREEN]*2, color.mV[VBLUE]*2, LLSelectMgr::sHighlightAlpha); - gGL.texCoord2fv( tc[1].mV ); - gGL.vertex3fv( v[1].mV ); - - gGL.color4f(color.mV[VRED], color.mV[VGREEN], color.mV[VBLUE], 0.0f); //LLSelectMgr::sHighlightAlpha); - gGL.texCoord2fv( tc[2].mV ); - gGL.vertex3fv( v[2].mV ); - - gGL.vertex3fv( v[2].mV ); - - gGL.color4f(color.mV[VRED]*2, color.mV[VGREEN]*2, color.mV[VBLUE]*2, LLSelectMgr::sHighlightAlpha); - gGL.texCoord2fv( tc[1].mV ); - gGL.vertex3fv( v[1].mV ); - - gGL.texCoord2fv( tc[3].mV ); - gGL.vertex3fv( v[3].mV ); - } - } - gGL.end(); - gGL.flush(); - } - gGL.popMatrix(); - gGL.popUIMatrix(); - - if (shader) - { - shader->bind(); - } -} - // // Utility Functions // diff --git a/indra/newview/llselectmgr.h b/indra/newview/llselectmgr.h index 4c20e15c66..5ece32f231 100644 --- a/indra/newview/llselectmgr.h +++ b/indra/newview/llselectmgr.h @@ -50,6 +50,7 @@ #include #include +class ALSelectionOutline; class LLMessageSystem; class LLViewerTexture; class LLColor4; @@ -180,7 +181,6 @@ class LLSelectNode S32 getLastSelectedTE() const; S32 getLastOperatedTE() const { return mLastTESelected; } S32 getTESelectMask() { return mTESelectMask; } - void renderOneSilhouette(const LLColor4 &color); void setTransient(bool transient) { mTransient = transient; } bool isTransient() const { return mTransient; } LLViewerObject* getObject() const; @@ -243,9 +243,6 @@ class LLSelectNode std::vector< std::vector > mGLTFScaleRatios; std::vector< std::vector > mGLTFScales; std::vector< std::vector > mGLTFOffsets; - std::vector mSilhouetteVertices; // array of vertices to render silhouette of object - std::vector mSilhouetteNormals; // array of normals to render silhouette of object - bool mSilhouetteExists; // need to generate silhouette? protected: mutable LLPointer mObject; @@ -446,13 +443,7 @@ class LLSelectMgr : public LLEditMenuHandler, public LLSimpleton static bool sRenderHiddenSelections; // do we show selection silhouettes that are occluded? static bool sRenderLightRadius; // do we show the radius of selected lights? - static F32 sHighlightThickness; - static F32 sHighlightUScale; - static F32 sHighlightVScale; static F32 sHighlightAlpha; - static F32 sHighlightAlphaTest; - static F32 sHighlightUAnim; - static F32 sHighlightVAnim; static LLUIColor sSilhouetteParentColor; static LLUIColor sSilhouetteChildColor; static LLUIColor sHighlightParentColor; @@ -624,9 +615,8 @@ class LLSelectMgr : public LLEditMenuHandler, public LLSimpleton LLBBox getSavedBBoxOfSelection() const { return mSavedSelectionBBox; } void dump(); - void cleanup(); - void updateSilhouettes(); + void updateHighlightedObjects(); void renderSilhouettes(bool for_hud); void enableSilhouette(bool enable) { mRenderSilhouettes = enable; } @@ -852,8 +842,7 @@ class LLSelectMgr : public LLEditMenuHandler, public LLSimpleton void convertTransient(); // converts temporarily selected objects to full-fledged selections ESelectType getSelectTypeForObject(LLViewerObject* object); void addAsFamily(std::vector& objects, bool add_to_end = false); - void generateSilhouette(LLSelectNode *nodep, const LLVector3& view_point); - void updateSelectionSilhouette(LLObjectSelectionHandle object_handle, S32& num_sils_genned, std::vector& changed_objects); + void addOutlines(ALSelectionOutline& outline, bool for_hud, U32 parts); // Send one message to each region containing an object on selection list. void sendListToRegions( const std::string& message_name, void (*pack_header)(void *user_data), @@ -916,7 +905,6 @@ class LLSelectMgr : public LLEditMenuHandler, public LLSimpleton update_signal_t mUpdateSignal; private: - LLPointer mSilhouetteImagep; LLObjectSelectionHandle mSelectedObjects; LLObjectSelectionHandle mHoverObjects; LLObjectSelectionHandle mHighlightedObjects; @@ -935,7 +923,6 @@ class LLSelectMgr : public LLEditMenuHandler, public LLSimpleton LLVector3d mLastSentSelectionCenterGlobal; bool mShowSelection; // do we send the selection center name value and do we animate this selection? - LLVector3d mLastCameraPos; // camera position from last generation of selection silhouette bool mRenderSilhouettes; // do we render the silhouette LLBBox mSavedSelectionBBox; diff --git a/indra/newview/llterrainpaintmap.cpp b/indra/newview/llterrainpaintmap.cpp index ad73547923..28bd3e081d 100644 --- a/indra/newview/llterrainpaintmap.cpp +++ b/indra/newview/llterrainpaintmap.cpp @@ -89,7 +89,7 @@ bool LLTerrainPaintMap::bakeHeightNoiseIntoPBRPaintMapRGB(const LLViewerRegion& stop_glerror(); scratch_target.bindTarget(); - glClearColor(0, 0, 0, 0); + gGL.setClearColor(LLColor4::transparent); scratch_target.clear(); // Render terrain heightmap to paint map via shader diff --git a/indra/newview/lltoolpie.cpp b/indra/newview/lltoolpie.cpp index 37c1036509..3cbf66d93b 100644 --- a/indra/newview/lltoolpie.cpp +++ b/indra/newview/lltoolpie.cpp @@ -32,6 +32,7 @@ #include "llclickaction.h" #include "llparcel.h" +#include "alhoverglow.h" #include "llagent.h" #include "llagentcamera.h" #include "llavatarnamecache.h" @@ -85,6 +86,15 @@ static void handle_click_action_play(); static void handle_click_action_open_media(LLPointer objectp); static ECursorType cursor_from_parcel_media(U8 click_action); +// Names `object` to the hover glow (RenderHoverGlowEnable) as what the pointer is over this frame, or nothing: a world +// object, not an avatar or a HUD attachment. +static void hover_glow(const LLViewerObject* object) +{ + static LLCachedControl enabled(gSavedSettings, "RenderHoverGlowEnable", false); + const bool glows = enabled && object && !object->isAvatar() && !object->isHUDAttachment(); + ALHoverGlow::instance().hover(glows ? object->getID() : LLUUID::null, LLFrameTimer::getFrameCount()); +} + LLToolPie::LLToolPie() : LLTool(std::string("Pie")), mMouseButtonDown( false ), @@ -837,6 +847,7 @@ bool LLToolPie::handleHover(S32 x, S32 y, MASK mask) if ( (RlvActions::isRlvEnabled()) && (!RlvActions::canInteract(object, mHoverPick.mObjectOffset)) ) { gViewerWindow->setCursor(UI_CURSOR_ARROW); + hover_glow(nullptr); return true; } // [/RLVa:KB] @@ -846,7 +857,9 @@ bool LLToolPie::handleHover(S32 x, S32 y, MASK mask) parent = object->getRootEdit(); } - if (!handleMediaHover(mHoverPick) + // What the pointer is over glows where it plays media or a click does something to it. + bool show_highlight = handleMediaHover(mHoverPick); + if (!show_highlight && !mMouseOutsideSlop && mMouseButtonDown // disable camera steering if click on land is not used for moving @@ -870,6 +883,11 @@ bool LLToolPie::handleHover(S32 x, S32 y, MASK mask) steerCameraWithMouse(x, y); gViewerWindow->setCursor(UI_CURSOR_TOOLGRAB); } + else if (show_highlight) + { + // The cursor is the media's, which handleMediaHover set. + LL_DEBUGS("UserInput") << "hover handled by LLToolPie (inactive)" << LL_ENDL; + } else { // perform a separate pick that detects transparent objects since they respond to 1-click actions @@ -879,6 +897,7 @@ bool LLToolPie::handleHover(S32 x, S32 y, MASK mask) if (click_action_object && useClickAction(mask, click_action_object, click_action_object->getRootEdit())) { + show_highlight = true; ECursorType cursor = cursorFromObject(click_action_object); gViewerWindow->setCursor(cursor); LL_DEBUGS("UserInput") << "hover handled by LLToolPie (inactive)" << LL_ENDL; @@ -893,6 +912,7 @@ bool LLToolPie::handleHover(S32 x, S32 y, MASK mask) else if ((object && !object->isAvatar() && object->flagUsePhysics()) || (parent && !parent->isAvatar() && parent->flagUsePhysics())) { + show_highlight = true; gViewerWindow->setCursor(UI_CURSOR_TOOLGRAB); LL_DEBUGS("UserInput") << "hover handled by LLToolPie (inactive)" << LL_ENDL; } @@ -900,6 +920,7 @@ bool LLToolPie::handleHover(S32 x, S32 y, MASK mask) && ((object && object->flagHandleTouch()) || (parent && parent->flagHandleTouch())) && (!object || !object->isAvatar())) { + show_highlight = true; gViewerWindow->setCursor(UI_CURSOR_HAND); LL_DEBUGS("UserInput") << "hover handled by LLToolPie (inactive)" << LL_ENDL; } @@ -915,6 +936,8 @@ bool LLToolPie::handleHover(S32 x, S32 y, MASK mask) LLViewerMediaFocus::getInstance()->clearHover(); } + hover_glow(show_highlight ? object : nullptr); + return true; } diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index a086f90a0c..0ddfd97b78 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -30,6 +30,7 @@ #include "alfarplane.h" #include "alfloaterprogressview.h" +#include "alhuddepth.h" #include "aluniformbuffer.h" #include "fsyspath.h" #include "hexdump.h" @@ -823,7 +824,7 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) } gGL.setColorMask(true, true); - glClearColor(0.f, 0.f, 0.f, 0.f); + gGL.setClearColor(LLColor4::transparent); LLGLState::checkStates(); @@ -935,7 +936,7 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) if(gUseWireframe) { - glClearColor(0.5f, 0.5f, 0.5f, 0.f); + gGL.setClearColor(LLColor4(0.5f, 0.5f, 0.5f, 0.f)); glClear(GL_COLOR_BUFFER_BIT); } @@ -990,11 +991,11 @@ void display(bool rebuild, F32 zoom_factor, int subfield, bool for_snapshot) if (gUseWireframe) { constexpr F32 g = 0.5f; - glClearColor(g, g, g, 1.f); + gGL.setClearColor(LLColor4(g, g, g, 1.f)); } else { - glClearColor(1, 0, 1, 1); + gGL.setClearColor(LLColor4::magenta); } gPipeline.mRT->deferredScreen.clear(); @@ -1227,7 +1228,7 @@ void display_cube_face() gGL.setColorMask(true, true); - glClearColor(0.f, 0.f, 0.f, 0.f); + gGL.setClearColor(LLColor4::transparent); gPipeline.generateSunShadow(*LLViewerCamera::getInstance()); glClear(GL_DEPTH_BUFFER_BIT); // | GL_STENCIL_BUFFER_BIT); @@ -1259,11 +1260,11 @@ void display_cube_face() gPipeline.mRT->deferredScreen.bindTarget(); if (gUseWireframe) { - glClearColor(0.5f, 0.5f, 0.5f, 1.f); + gGL.setClearColor(LLColor4(0.5f, 0.5f, 0.5f, 1.f)); } else { - glClearColor(1.f, 0.f, 1.f, 1.f); + gGL.setClearColor(LLColor4::magenta); } gPipeline.mRT->deferredScreen.clear(); @@ -1284,6 +1285,50 @@ void display_cube_face() gPipeline.clearReferences(); } +// The viewport pixels the HUD can cover, a rect per attachment point with something on it, overlapping ones merged +// (ALHUDDepth::mergeRect). A point's box is in the frame the HUD's model matrix takes in (LLVOAvatar::getHUDBBox), +// with no rotation or offset of its own. +static void hud_screen_rects(std::vector& rects) +{ + rects.clear(); + // None without what restore_scene_depth puts the scene's depth back from. + LLMatrix4a proj; + LLMatrix4a model; + if (!gPipeline.mRT || !gPipeline.mScreenTriangleVB || !isAgentAvatarValid() || !get_hud_matrices(proj, model)) + { + return; + } + + LLMatrix4a mvp; + mvp.setMul(model, proj); + + for (const auto& [id, attachment] : gAgentAvatarp->mAttachmentPoints) + { + if (!attachment || !attachment->getIsHUDAttachment() || attachment->mAttachedObjects.empty()) + { + continue; + } + const LLBBox bbox = LLVOAvatar::getHUDBBox(attachment); + const LLRect rect = ALHUDDepth::boxRect(mvp, bbox.getMinLocal(), bbox.getMaxLocal(), gGLViewport[2], gGLViewport[3]); + if (!rect.isEmpty()) + { + ALHUDDepth::mergeRect(rects, rect); + } + } +} + +// Writes the scene's depth back under the HUD, as the final blit left it (ALHUDDepth::restore), within `rects`, which +// hud_screen_rects gives only where it can. +static void restore_scene_depth(const std::vector& rects) +{ + if (rects.empty()) + { + return; + } + ALHUDDepth::restore(gCopyDepthProgram, gPipeline.mRT->deferredScreen, *gPipeline.mScreenTriangleVB, rects, + gGLViewport[0], gGLViewport[1]); +} + void render_hud_attachments() { LLPerfStats::RecordSceneTime T ( LLPerfStats::StatType_t::RENDER_HUDS); // render time capture - Primary contributor to HUDs (though these end up in render batches) @@ -1372,6 +1417,16 @@ void render_hud_attachments() gPipeline.stateSort(hud_cam, result); + // The HUD is drawn over the window's framebuffer, whose depth the final blit filled with the scene's for the + // 3D UI around it. get_hud_matrices squeezes the HUD into a band of that depth in front of the scene, but a + // world surface at the near plane still reached into it, and the band widens with the HUD's own depth: a + // HUD with a prim parked far along its axis put its rear within reach of surfaces half a metre away. So + // under the HUD the depth is cleared, the HUD tests against itself alone, and the scene's is put back after + // it for the 3D UI that follows, which the world and the HUD both hide. + static std::vector hud_rects; + hud_screen_rects(hud_rects); + ALHUDDepth::clear(gCopyDepthProgram, hud_rects, gGLViewport[0], gGLViewport[1]); + gPipeline.renderGeomPostDeferred(hud_cam); LLSpatialGroup::sNoDelete = false; @@ -1379,6 +1434,8 @@ void render_hud_attachments() render_hud_elements(); + restore_scene_depth(hud_rects); + //restore type mask gPipeline.popRenderTypeMask(); diff --git a/indra/newview/llviewerdisplay.h b/indra/newview/llviewerdisplay.h index 2ec02f09fd..f115730a96 100644 --- a/indra/newview/llviewerdisplay.h +++ b/indra/newview/llviewerdisplay.h @@ -27,11 +27,17 @@ #ifndef LL_LLVIEWERDISPLAY_H #define LL_LLVIEWERDISPLAY_H +class LLMatrix4a; + void display_startup(); void display_cleanup(); void display(bool rebuild = true, F32 zoom_factor = 1.f, int subfield = 0, bool for_snapshot = false); +// The projection and modelview the HUD's attachments are drawn with over the whole world view. False where the +// agent's avatar wears no HUD. +bool get_hud_matrices(LLMatrix4a& proj, LLMatrix4a& model); + extern bool gDisplaySwapBuffers; extern bool gDepthDirty; extern bool gTeleportDisplay; diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 29ae8526b0..7e4c491335 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -1612,7 +1612,7 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys, mHudText = temp_string; mHudTextColor = LLColor4(coloru); - setChanged(MOVED | SILHOUETTE); + setChanged(MOVED); } else { @@ -2156,7 +2156,7 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys, // Show particles, icon and HUD hideExtraDisplayItems( false ); - setChanged(MOVED | SILHOUETTE); + setChanged(MOVED); } else { @@ -2295,8 +2295,8 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys, } cur_parentp->removeChild(this); sent_parentp->addChild(this); - setChanged(MOVED | SILHOUETTE); - sent_parentp->setChanged(MOVED | SILHOUETTE); + setChanged(MOVED); + sent_parentp->setChanged(MOVED); if (sent_parentp->mDrawable.notNull()) { gPipeline.markMoved(sent_parentp->mDrawable, false); // undamped @@ -2330,7 +2330,7 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys, cur_parentp->removeChild(this); - setChanged(MOVED | SILHOUETTE); + setChanged(MOVED); if (mDrawable.notNull()) { @@ -2383,7 +2383,7 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys, // Set the change flags for scale if (new_scale != getScale()) { - setChanged(SCALED | SILHOUETTE); + setChanged(SCALED); setScale(new_scale); // Must follow setting permYouOwner() } @@ -2460,7 +2460,7 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys, gAgent.rotate(new_rot); gAgentCamera.resetView(); } - setChanged(ROTATED | SILHOUETTE); + setChanged(ROTATED); } if ( gShowObjectUpdates ) @@ -2686,7 +2686,7 @@ void LLViewerObject::interpolateLinearMotion(const F64SecondsImplicit& frame_tim setVelocity(vel + accel*dt); // for objects that are spinning but not translating, make sure to flag them as having moved - setChanged(MOVED | SILHOUETTE); + setChanged(MOVED); } } else if (!accel.isExactlyZero() || !vel.isExactlyZero()) // object is moving @@ -2860,7 +2860,7 @@ void LLViewerObject::interpolateLinearMotion(const F64SecondsImplicit& frame_tim } // for objects that are spinning but not translating, make sure to flag them as having moved - setChanged(MOVED | SILHOUETTE); + setChanged(MOVED); } // Update the last time we did anything @@ -4951,7 +4951,7 @@ void LLViewerObject::setPosition(const LLVector3 &pos, bool damped) { if (getPosition() != pos) { - setChanged(TRANSLATED | SILHOUETTE); + setChanged(TRANSLATED); } LLXform::setPosition(pos); @@ -4994,7 +4994,7 @@ void LLViewerObject::setPositionGlobal(const LLVector3d &pos_global, bool damped // LLViewerObject version of setPosition? LLVector3 old_pos = mDrawable->mXform.getParent()->getPosition(); mDrawable->mXform.getParent()->setPosition(old_pos + delta_pos); - setChanged(TRANSLATED | SILHOUETTE); + setChanged(TRANSLATED); } if (mParent && ((LLViewerObject*)mParent)->isAvatar()) { @@ -7246,7 +7246,7 @@ void LLViewerObject::setRegion(LLViewerRegion *regionp) mControlAvatar->setRegion(regionp); } - setChanged(MOVED | SILHOUETTE); + setChanged(MOVED); updateDrawable(false); } @@ -7410,7 +7410,7 @@ void LLViewerObject::applyAngularVelocity(F32 dt) new_rot.normalize(); setRotation(new_rot); - setChanged(MOVED | SILHOUETTE); + setChanged(MOVED); } } diff --git a/indra/newview/llviewerobject.h b/indra/newview/llviewerobject.h index 9e7d696f48..b5f56b6ebc 100644 --- a/indra/newview/llviewerobject.h +++ b/indra/newview/llviewerobject.h @@ -1090,14 +1090,14 @@ class LLViewerObject inline void LLViewerObject::setRotation(const LLQuaternion& quat, bool damped) { LLPrimitive::setRotation(quat); - setChanged(ROTATED | SILHOUETTE); + setChanged(ROTATED); updateDrawable(damped); } inline void LLViewerObject::setRotation(const F32 x, const F32 y, const F32 z, bool damped) { LLPrimitive::setRotation(x, y, z); - setChanged(ROTATED | SILHOUETTE); + setChanged(ROTATED); updateDrawable(damped); } diff --git a/indra/newview/llviewerobjectlist.cpp b/indra/newview/llviewerobjectlist.cpp index ae26919c0b..56a8eaca35 100644 --- a/indra/newview/llviewerobjectlist.cpp +++ b/indra/newview/llviewerobjectlist.cpp @@ -2283,7 +2283,7 @@ void LLViewerObjectList::findOrphans(LLViewerObject* objectp, U32 ip, U32 port) addDebugBeacon(objectp->getPositionAgent(),""); #endif gPipeline.markMoved(objectp->mDrawable); - objectp->setChanged(LLXform::MOVED | LLXform::SILHOUETTE); + objectp->setChanged(LLXform::MOVED); // Flag the object as no longer orphaned childp->mOrphaned = false; diff --git a/indra/newview/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp index 55dd0b1ff2..ddfec116d8 100644 --- a/indra/newview/llviewershadermgr.cpp +++ b/indra/newview/llviewershadermgr.cpp @@ -137,6 +137,7 @@ LLGLSLShader gAlphaMaskProgram; LLGLSLShader gBenchmarkProgram; LLGLSLShader gReflectionProbeDisplayProgram; LLGLSLShader gCopyProgram; +LLGLSLShader gCopyDepthProgram; LLGLSLShader gPBRTerrainBakeProgram; LLGLSLShader gDrawColorProgram; @@ -154,6 +155,13 @@ LLGLSLShader gUnderWaterProgram; LLGLSLShader gHighlightProgram; LLGLSLShader gHighlightNormalProgram; LLGLSLShader gHighlightSpecularProgram; +LLGLSLShader gSelectionIdProgram; +LLGLSLShader gSelectionWireframeProgram; +LLGLSLShader gSelectionJumpProgram; +LLGLSLShader gSelectionTileProgram; +LLGLSLShader gSelectionOutlineProgram; +LLGLSLShader gSelectionGlowReachProgram; +LLGLSLShader gSelectionGlowRowProgram; LLGLSLShader gDeferredHighlightProgram; @@ -197,6 +205,7 @@ LLGLSLShader gHazeProgram; LLGLSLShader gHazeWaterProgram; LLGLSLShader gDeferredBlurLightProgram; LLGLSLShader gDeferredSoftenProgram; +LLGLSLShader gDeferredSoftenSkyProgram; LLGLSLShader gDeferredShadowProgram; LLGLSLShader gDeferredTerrainShadowProgram; LLGLSLShader gDeferredShadowCubeProgram; @@ -1309,6 +1318,7 @@ bool LLViewerShaderMgr::loadShadersDeferred() gDeferredSunProgram.unload(); gDeferredBlurLightProgram.unload(); gDeferredSoftenProgram.unload(); + gDeferredSoftenSkyProgram.unload(); gDeferredShadowProgram.unload(); gDeferredTerrainShadowProgram.unload(); gDeferredShadowCubeProgram.unload(); @@ -2443,37 +2453,51 @@ bool LLViewerShaderMgr::loadShadersDeferred() if (success) { - gDeferredSoftenProgram.mName = "Deferred Soften Shader"; - gDeferredSoftenProgram.mShaderFiles.clear(); - gDeferredSoftenProgram.mFeatures.hasSrgb = true; - gDeferredSoftenProgram.mFeatures.calculatesAtmospherics = true; - gDeferredSoftenProgram.mFeatures.hasAtmospherics = true; - gDeferredSoftenProgram.mFeatures.hasGamma = true; - gDeferredSoftenProgram.mFeatures.isDeferred = true; - gDeferredSoftenProgram.mFeatures.hasFullGBuffer = true; - gDeferredSoftenProgram.mFeatures.hasShadows = use_sun_shadow; - gDeferredSoftenProgram.mFeatures.hasReflectionProbes = mShaderLevel[SHADER_DEFERRED] > 2; + // The soften pass, and its variant for the sky alone (softenLightF.glsl, SOFTEN_SKY), from one configuration, + // so the two resolve the same shader level and link the same objects. + const auto load_soften = [&](LLGLSLShader& shader, const char* name, bool sky) + { + shader.mName = name; + shader.mShaderFiles.clear(); + shader.mFeatures.hasSrgb = true; + shader.mFeatures.calculatesAtmospherics = true; + shader.mFeatures.hasAtmospherics = true; + shader.mFeatures.hasGamma = true; + shader.mFeatures.isDeferred = true; + shader.mFeatures.hasFullGBuffer = true; + shader.mFeatures.hasShadows = use_sun_shadow; + shader.mFeatures.hasReflectionProbes = mShaderLevel[SHADER_DEFERRED] > 2; + + shader.clearPermutations(); + add_common_permutations(&shader); + shader.mShaderFiles.push_back(make_pair("deferred/softenLightV.glsl", GL_VERTEX_SHADER)); + shader.mShaderFiles.push_back(make_pair("deferred/softenLightF.glsl", GL_FRAGMENT_SHADER)); + + shader.mShaderLevel = mShaderLevel[SHADER_DEFERRED]; - gDeferredSoftenProgram.clearPermutations(); - add_common_permutations(&gDeferredSoftenProgram); - gDeferredSoftenProgram.mShaderFiles.push_back(make_pair("deferred/softenLightV.glsl", GL_VERTEX_SHADER)); - gDeferredSoftenProgram.mShaderFiles.push_back(make_pair("deferred/softenLightF.glsl", GL_FRAGMENT_SHADER)); + if (use_sun_shadow) + { + shader.addPermutation("HAS_SUN_SHADOW", "1"); + } - gDeferredSoftenProgram.mShaderLevel = mShaderLevel[SHADER_DEFERRED]; + if (gSavedSettings.getBOOL("RenderDeferredSSAO")) + { //if using SSAO, take screen space light map into account as if shadows are enabled + shader.mShaderLevel = llmax(shader.mShaderLevel, 2); + shader.addPermutation("HAS_SSAO", "1"); + } - if (use_sun_shadow) - { - gDeferredSoftenProgram.addPermutation("HAS_SUN_SHADOW", "1"); - } + if (sky) + { + shader.addPermutation("SOFTEN_SKY", "1"); + } - if (gSavedSettings.getBOOL("RenderDeferredSSAO")) - { //if using SSAO, take screen space light map into account as if shadows are enabled - gDeferredSoftenProgram.mShaderLevel = llmax(gDeferredSoftenProgram.mShaderLevel, 2); - gDeferredSoftenProgram.addPermutation("HAS_SSAO", "1"); - } + const bool loaded = shader.createShader(LLGLSLShader::VARIANT_CLASSIC); + llassert(loaded); + return loaded; + }; - success = gDeferredSoftenProgram.createShader(LLGLSLShader::VARIANT_CLASSIC); - llassert(success); + success = load_soften(gDeferredSoftenProgram, "Deferred Soften Shader", false) && + load_soften(gDeferredSoftenSkyProgram, "Deferred Soften Sky Shader", true); } if (success) @@ -3807,6 +3831,66 @@ bool LLViewerShaderMgr::loadShadersInterface() success = gHighlightSpecularProgram.createShader(); } + if (success) + { + // The selection outline's and wireframe's programs. ALSelectionOutline::render draws only with the ones that + // loaded, so a driver that rejects one loses what that one draws and keeps the rest of the interface. + const auto load_selection = [this](LLGLSLShader& program, const char* name, + std::initializer_list> files, U32 variants) + { + program.mName = name; + program.mShaderFiles.clear(); + for (const auto& [file, stage] : files) + { + program.mShaderFiles.push_back(make_pair(file, stage)); + } + program.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; + if (!program.createShader(variants)) + { + LL_WARNS("Shader") << "Could not load " << name << "; selections are drawn without it" << LL_ENDL; + program.unload(); + } + }; + + load_selection(gSelectionIdProgram, "Selection Id Shader", + { { "interface/selectionIdV.glsl", GL_VERTEX_SHADER }, + { "interface/selectionIdF.glsl", GL_FRAGMENT_SHADER }, + { "interface/selectionAlphaF.glsl", GL_FRAGMENT_SHADER }, + { "interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER } }, + LLGLSLShader::VARIANT_RIGGED); + load_selection(gSelectionWireframeProgram, "Selection Wireframe Shader", + { { "interface/selectionIdV.glsl", GL_VERTEX_SHADER }, + { "interface/selectionWireframeG.glsl", GL_GEOMETRY_SHADER }, + { "interface/selectionWireframeF.glsl", GL_FRAGMENT_SHADER }, + { "interface/selectionAlphaF.glsl", GL_FRAGMENT_SHADER }, + { "interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER } }, + LLGLSLShader::VARIANT_RIGGED); + load_selection(gSelectionJumpProgram, "Selection Jump Shader", + { { "interface/copyV.glsl", GL_VERTEX_SHADER }, + { "interface/selectionJumpF.glsl", GL_FRAGMENT_SHADER }, + { "interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER } }, + 0); + load_selection(gSelectionTileProgram, "Selection Tile Shader", + { { "interface/copyV.glsl", GL_VERTEX_SHADER }, + { "interface/selectionTileF.glsl", GL_FRAGMENT_SHADER }, + { "interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER } }, + 0); + load_selection(gSelectionOutlineProgram, "Selection Outline Shader", + { { "interface/copyV.glsl", GL_VERTEX_SHADER }, + { "interface/selectionOutlineF.glsl", GL_FRAGMENT_SHADER }, + { "interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER } }, + 0); + load_selection(gSelectionGlowReachProgram, "Selection Glow Reach Shader", + { { "interface/copyV.glsl", GL_VERTEX_SHADER }, + { "interface/selectionGlowReachF.glsl", GL_FRAGMENT_SHADER } }, + 0); + load_selection(gSelectionGlowRowProgram, "Selection Glow Row Shader", + { { "interface/copyV.glsl", GL_VERTEX_SHADER }, + { "interface/selectionGlowRowF.glsl", GL_FRAGMENT_SHADER }, + { "interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER } }, + 0); + } + if (success) { gUIProgram.mName = "UI Shader"; @@ -4024,6 +4108,19 @@ bool LLViewerShaderMgr::loadShadersInterface() success = gCopyProgram.createShader(); } + if (success) + { + gCopyDepthProgram.mName = "Copy Depth Shader"; + gCopyDepthProgram.mShaderFiles.clear(); + gCopyDepthProgram.mShaderFiles.push_back(make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); + gCopyDepthProgram.mShaderFiles.push_back(make_pair("interface/copyF.glsl", GL_FRAGMENT_SHADER)); + gCopyDepthProgram.clearPermutations(); + gCopyDepthProgram.addPermutation("COPY_DEPTH", "1"); + gCopyDepthProgram.addPermutation("DEPTH_ONLY", "1"); + gCopyDepthProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; + success = gCopyDepthProgram.createShader(); + } + if (success) { gDrawColorProgram.mName = "Draw Color Shader"; diff --git a/indra/newview/llviewershadermgr.h b/indra/newview/llviewershadermgr.h index f701d3f568..8d69271d41 100644 --- a/indra/newview/llviewershadermgr.h +++ b/indra/newview/llviewershadermgr.h @@ -117,6 +117,7 @@ extern LLGLSLShader gClipProgram; extern LLGLSLShader gBenchmarkProgram; extern LLGLSLShader gReflectionProbeDisplayProgram; extern LLGLSLShader gCopyProgram; +extern LLGLSLShader gCopyDepthProgram; extern LLGLSLShader gPBRTerrainBakeProgram; extern LLGLSLShader gDrawColorProgram; @@ -162,6 +163,16 @@ extern LLGLSLShader gLensDirtGenProgram; extern LLGLSLShader gHighlightProgram; extern LLGLSLShader gHighlightNormalProgram; extern LLGLSLShader gHighlightSpecularProgram; +// Selection outlines (ALSelectionOutline): the id pass and the wireframe, with rigged variants and linked with +// interface/selectionAlphaF.glsl, the jump pass, the tile pass and the edge pass, each linked with +// interface/selectionUtilF.glsl. +extern LLGLSLShader gSelectionIdProgram; +extern LLGLSLShader gSelectionWireframeProgram; +extern LLGLSLShader gSelectionJumpProgram; +extern LLGLSLShader gSelectionTileProgram; +extern LLGLSLShader gSelectionOutlineProgram; +extern LLGLSLShader gSelectionGlowReachProgram; +extern LLGLSLShader gSelectionGlowRowProgram; extern LLGLSLShader gDeferredHighlightProgram; @@ -193,6 +204,7 @@ extern LLGLSLShader gHazeWaterProgram; extern LLGLSLShader gDeferredBlurLightProgram; extern LLGLSLShader gDeferredAvatarProgram; extern LLGLSLShader gDeferredSoftenProgram; +extern LLGLSLShader gDeferredSoftenSkyProgram; extern LLGLSLShader gDeferredShadowProgram; extern LLGLSLShader gDeferredTerrainShadowProgram; extern LLGLSLShader gDeferredShadowCubeProgram; diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index e75a66a5ea..8915eaf481 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -169,12 +169,7 @@ void LLViewerTextureList::doPreloadImages() //uv_test->setClamp(false, false); //uv_test->setMipFilterNearest(true, true); - LLViewerFetchedTexture* image = LLViewerTextureManager::getFetchedTextureFromFile("silhouette.j2c", FTT_LOCAL_FILE, MIPMAP_YES, LLViewerFetchedTexture::BOOST_UI); - if (image) - { - mImagePreloads.insert(image); - } - image = LLViewerTextureManager::getFetchedTextureFromFile("world/NoEntryLines.png", FTT_LOCAL_FILE, MIPMAP_YES, LLViewerFetchedTexture::BOOST_UI); + LLViewerFetchedTexture* image = LLViewerTextureManager::getFetchedTextureFromFile("world/NoEntryLines.png", FTT_LOCAL_FILE, MIPMAP_YES, LLViewerFetchedTexture::BOOST_UI); if (image) { mImagePreloads.insert(image); diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 0d2a6c0cc6..30ee15e6cf 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -2761,9 +2761,6 @@ void LLViewerWindow::shutdownGL() LL_INFOS() << "All textures and llimagegl images are destroyed!" << LL_ENDL ; - LL_INFOS() << "Cleaning up select manager" << LL_ENDL; - LLSelectMgr::getInstance()->cleanup(); - LL_INFOS() << "Stopping GL during shutdown" << LL_ENDL; stopGL(); stop_glerror(); @@ -4520,7 +4517,7 @@ void LLViewerWindow::renderSelections( bool for_gl_pick, bool pick_parcel_walls, if (!for_hud && !for_gl_pick) { // Call this once and only once - LLSelectMgr::getInstance()->updateSilhouettes(); + LLSelectMgr::getInstance()->updateHighlightedObjects(); } // render the poser manipulator guides @@ -4541,12 +4538,19 @@ void LLViewerWindow::renderSelections( bool for_gl_pick, bool pick_parcel_walls, { LLViewerParcelMgr::getInstance()->renderParcelCollision(); } + return; } - else if (( for_hud && selection->getSelectType() == SELECT_TYPE_HUD) || - (!for_hud && selection->getSelectType() != SELECT_TYPE_HUD)) + + // The world's outlines every frame, for what the pointer is over glows in the world wherever the selection is; + // the HUD's while the selection is on it. + if (!for_hud || selection->getSelectType() == SELECT_TYPE_HUD) { LLSelectMgr::getInstance()->renderSilhouettes(for_hud); + } + if (( for_hud && selection->getSelectType() == SELECT_TYPE_HUD) || + (!for_hud && selection->getSelectType() != SELECT_TYPE_HUD)) + { stop_glerror(); // setup HUD render diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index 4ec4125c96..7b5ac168f7 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -10054,6 +10054,37 @@ bool LLVOAvatar::hasHUDAttachment() const return false; } +static void add_hud_attachment_bbox(LLBBox& bbox, const LLViewerJointAttachment* attachment) +{ + for (LLViewerJointAttachment::attachedobjs_vec_t::const_iterator attachment_iter = attachment->mAttachedObjects.begin(); + attachment_iter != attachment->mAttachedObjects.end(); + ++attachment_iter) + { + const LLViewerObject* attached_object = attachment_iter->get(); + if (attached_object == NULL) + { + LL_WARNS() << "HUD attached object is NULL!" << LL_ENDL; + continue; + } + // A point added first fixes the box's frame at the HUD's origin with no rotation, so the rotated boxes below + // land in the HUD's own frame. The point is the object's centre on the HUD, inside the box added next, so it + // widens nothing. (The object's position is its offset from the attachment point, another frame: a corner + // HUD's box seeded with it reached towards the middle of the screen.) + const LLBBox object_bbox = attached_object->getBoundingBoxAgent(); + bbox.addPointLocal(object_bbox.getCenterAgent()); + // add rotated bounding box for attached object + bbox.addBBoxAgent(object_bbox); + LLViewerObject::const_child_list_t& child_list = attached_object->getChildren(); + for (LLViewerObject::child_list_t::const_iterator iter = child_list.begin(); + iter != child_list.end(); + ++iter) + { + const LLViewerObject* child_objectp = *iter; + bbox.addBBoxAgent(child_objectp->getBoundingBoxAgent()); + } + } +} + LLBBox LLVOAvatar::getHUDBBox() const { LLBBox bbox; @@ -10061,38 +10092,24 @@ LLBBox LLVOAvatar::getHUDBBox() const iter != mAttachmentPoints.end(); ++iter) { - LLViewerJointAttachment* attachment = iter->second; + const LLViewerJointAttachment* attachment = iter->second; if (attachment->getIsHUDAttachment()) { - for (LLViewerJointAttachment::attachedobjs_vec_t::iterator attachment_iter = attachment->mAttachedObjects.begin(); - attachment_iter != attachment->mAttachedObjects.end(); - ++attachment_iter) - { - const LLViewerObject* attached_object = attachment_iter->get(); - if (attached_object == NULL) - { - LL_WARNS() << "HUD attached object is NULL!" << LL_ENDL; - continue; - } - // initialize bounding box to contain identity orientation and center point for attached object - bbox.addPointLocal(attached_object->getPosition()); - // add rotated bounding box for attached object - bbox.addBBoxAgent(attached_object->getBoundingBoxAgent()); - LLViewerObject::const_child_list_t& child_list = attached_object->getChildren(); - for (LLViewerObject::child_list_t::const_iterator iter = child_list.begin(); - iter != child_list.end(); - ++iter) - { - const LLViewerObject* child_objectp = *iter; - bbox.addBBoxAgent(child_objectp->getBoundingBoxAgent()); - } - } + add_hud_attachment_bbox(bbox, attachment); } } return bbox; } +// static +LLBBox LLVOAvatar::getHUDBBox(const LLViewerJointAttachment* attachment) +{ + LLBBox bbox; + add_hud_attachment_bbox(bbox, attachment); + return bbox; +} + //----------------------------------------------------------------------------- // onFirstTEMessageReceived() //----------------------------------------------------------------------------- diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h index 792f34e2d3..efea7277c7 100644 --- a/indra/newview/llvoavatar.h +++ b/indra/newview/llvoavatar.h @@ -1107,6 +1107,8 @@ class LLVOAvatar : public: bool hasHUDAttachment() const; LLBBox getHUDBBox() const; + // One HUD attachment point's share of getHUDBBox, in the same frame. + static LLBBox getHUDBBox(const LLViewerJointAttachment* attachment); void resetHUDAttachments(); S32 getMaxAttachments() const; bool canAttachMoreObjects(U32 n=1) const; diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index 4aa1e35eb2..aa55920e45 100644 --- a/indra/newview/llvovolume.cpp +++ b/indra/newview/llvovolume.cpp @@ -4005,40 +4005,6 @@ void LLVOVolume::updateRiggingInfo() //---------------------------------------------------------------------------- -void LLVOVolume::generateSilhouette(LLSelectNode* nodep, const LLVector3& view_point) -{ - LLVolume *volume = getVolume(); - - if (volume) - { - LLVector3 view_vector; - view_vector = view_point; - - //transform view vector into volume space - view_vector -= getRenderPosition(); - //mDrawable->mDistanceWRTCamera = view_vector.length(); - LLQuaternion worldRot = getRenderRotation(); - view_vector = view_vector * ~worldRot; - if (!isVolumeGlobal()) - { - LLVector3 objScale = getScale(); - LLVector3 invObjScale(1.f / objScale.mV[VX], 1.f / objScale.mV[VY], 1.f / objScale.mV[VZ]); - view_vector.scaleVec(invObjScale); - } - - updateRelativeXform(); - LLMatrix4 trans_mat = mRelativeXform; - if (mDrawable->isStatic()) - { - trans_mat.translate(getRegion()->getOriginAgent()); - } - - volume->generateSilhouetteVertices(nodep->mSilhouetteVertices, nodep->mSilhouetteNormals, view_vector, trans_mat, mRelativeXformInvTrans, nodep->getTESelectMask()); - - nodep->mSilhouetteExists = true; - } -} - void LLVOVolume::deleteFaces() { S32 face_count = mNumFaces; diff --git a/indra/newview/llvovolume.h b/indra/newview/llvovolume.h index 9e0835cd43..3ebc9b0cb0 100644 --- a/indra/newview/llvovolume.h +++ b/indra/newview/llvovolume.h @@ -138,7 +138,6 @@ class LLVOVolume : public LLViewerObject bool isRootEdit() const override; // overridden for sake of attachments treating themselves as a root object bool isHUDAttachment() const override; - void generateSilhouette(LLSelectNode* nodep, const LLVector3& view_point); /*virtual*/ bool setParent(LLViewerObject* parent) override; S32 getLOD() const override { return mLOD; } void setNoLOD() { mLOD = NO_LOD; mLODChanged = true; } diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index bbfc7d1284..77e2a970fe 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -59,6 +59,7 @@ #include "alcurvemodel.h" #include "alfarplane.h" #include "alfarplaneoverlay.h" +#include "alselectionoutline.h" #include "llrender.h" #include "llstartup.h" #include "llwindow.h" // swapBuffers() @@ -976,6 +977,9 @@ bool LLPipeline::allocateScreenBufferInternal(U32 resX, U32 resY) if (shadow_detail > 0 || ssao) { //only need mRT->deferredLight for shadows OR ssao if (!mRT->deferredLight.allocate(resX, resY, screenFormat)) return false; + // The scene's depth, for the far-plane test that keeps the light map's passes off the sky + // (renderDeferredLighting). + mRT->deferredScreen.shareDepthBuffer(mRT->deferredLight); } else { @@ -1387,6 +1391,10 @@ void LLPipeline::releaseGLBuffers() } ALFarPlaneOverlay::release(); + if (ALSelectionOutline::instanceExists()) + { + ALSelectionOutline::instance().release(); + } mHeroProbeManager.cleanup(); // release hero probes @@ -1688,9 +1696,9 @@ void LLPipeline::createLUTBuffers() // Scale 1 at EV 0: the first frame adapts from no exposure at all. mExposureMap.allocate(1, 1, GL_RGBA16F); mExposureMap.bindTarget(); - glClearColor(1, 0, 0, 1); + gGL.setClearColor(LLColor4::red); mExposureMap.clear(); - glClearColor(0, 0, 0, 0); + gGL.setClearColor(LLColor4::transparent); mExposureMap.flush(); mLuminanceMap.allocate(EXPOSURE_METER_WIDTH, EXPOSURE_METER_HEIGHT, GL_RGBA16F); @@ -3595,7 +3603,7 @@ void LLPipeline::markShift(LLDrawable *drawablep) if (!drawablep->isState(LLDrawable::ON_SHIFT_LIST)) { - drawablep->getVObj()->setChanged(LLXform::SHIFTED | LLXform::SILHOUETTE); + drawablep->getVObj()->setChanged(LLXform::SHIFTED); if (drawablep->getParent()) { markShift(drawablep->getParent()); @@ -3741,10 +3749,6 @@ void LLPipeline::markRebuild(LLDrawable *drawablep, LLDrawable::EDrawableFlags f drawablep->setState(LLDrawable::IN_REBUILD_Q); // mark drawable as being in priority queue } - if (flag & (LLDrawable::REBUILD_VOLUME | LLDrawable::REBUILD_POSITION)) - { - drawablep->getVObj()->setChanged(LLXform::SILHOUETTE); - } drawablep->setState(flag); } } @@ -5106,7 +5110,7 @@ void LLPipeline::renderDebug() { const LLColor4 clearColor = gSavedSettings.getColor4("PathfindingNavMeshClear"); gGL.setColorMask(true, true); - glClearColor(clearColor.mV[0],clearColor.mV[1],clearColor.mV[2],0); + gGL.setClearColor(LLColor4(clearColor.mV[0], clearColor.mV[1], clearColor.mV[2], 0.f)); glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // no stencil -- deprecated | GL_STENCIL_BUFFER_BIT); gGL.setColorMask(true, false); glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); @@ -10102,7 +10106,7 @@ void LLPipeline::renderFinalize() enableLightsFullbright(); gGL.setColorMask(true, true); - glClearColor(0, 0, 0, 0); + gGL.setClearColor(LLColor4::transparent); static LLCachedControl has_hdr(gSavedSettings, "RenderHDREnabled", true); bool hdr = gGLManager.mGLVersion > 4.05f && has_hdr(); @@ -11068,17 +11072,20 @@ void LLPipeline::renderDeferredLighting() LLGLSLShader& sun_shader = gCubeSnapshot ? gDeferredSunProbeProgram : gDeferredSunProgram; bindDeferredShader(sun_shader, deferred_light_target); mScreenTriangleVB->setBuffer(); - glClearColor(1, 1, 1, 1); + gGL.setClearColor(LLColor4::white); deferred_light_target->clear(GL_COLOR_BUFFER_BIT); - glClearColor(0, 0, 0, 0); + gGL.setClearColor(LLColor4::transparent); sun_shader.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, (GLfloat)deferred_light_target->getWidth(), (GLfloat)deferred_light_target->getHeight()); { + // The triangle lies on the far plane (sunLightV.glsl) and the target shares the scene's + // depth: only geometry is shadowed and occluded, and the sky keeps the clear's white, which + // nothing reads there (the soften pass's sky program lights nothing). LLGLDisable blend(GL_BLEND); - LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_ALWAYS); + LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_GREATER); mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3); } @@ -11094,9 +11101,9 @@ void LLPipeline::renderDeferredLighting() LL_PROFILE_GPU_ZONE("soften shadow"); // blur lightmap screen_target->bindTarget(); - glClearColor(1, 1, 1, 1); + gGL.setClearColor(LLColor4::white); screen_target->clear(GL_COLOR_BUFFER_BIT); - glClearColor(0, 0, 0, 0); + gGL.setClearColor(LLColor4::transparent); bindDeferredShader(gDeferredBlurLightProgram); @@ -11123,9 +11130,11 @@ void LLPipeline::renderDeferredLighting() gDeferredBlurLightProgram.uniform3fv(LLShaderMgr::KERN, kern_length, gauss[0].mV); gDeferredBlurLightProgram.uniform1f(LLShaderMgr::KERN_SCALE, blur_size * (kern_length / 2.f - 0.5f)); + // Both blurs on the far plane (blurLightV.glsl), over geometry alone: the sky stays the clear's white + // in either target. { LLGLDisable blend(GL_BLEND); - LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_ALWAYS); + LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_GREATER); mScreenTriangleVB->setBuffer(); mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3); } @@ -11141,7 +11150,7 @@ void LLPipeline::renderDeferredLighting() { LLGLDisable blend(GL_BLEND); - LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_ALWAYS); + LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_GREATER); mScreenTriangleVB->setBuffer(); mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3); } @@ -11151,7 +11160,7 @@ void LLPipeline::renderDeferredLighting() screen_target->bindTarget(); // clear color buffer here - zeroing alpha (glow) is important or it will accumulate against sky - glClearColor(0, 0, 0, 0); + gGL.setClearColor(LLColor4::transparent); screen_target->clear(GL_COLOR_BUFFER_BIT); if (RenderDeferredAtmospheric) @@ -11174,16 +11183,28 @@ void LLPipeline::renderDeferredLighting() soften_shader.uniform4fv(LLShaderMgr::WATER_WATERPLANE, 1, LLDrawPoolAlpha::sWaterPlane.mV); + // The triangle lies on the far plane (softenLightV.glsl), level with the depth the sky leaves cleared: + // this draw passes only where the world drew something, and the sky's draw below only where it did + // not, so no sky pixel pays for the G-buffer decode and atmospherics it would throw away. + LLGLDisable blend(GL_BLEND); { - LLGLDepthTest depth(GL_FALSE); - LLGLDisable blend(GL_BLEND); + LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_GREATER); - // full screen blit mScreenTriangleVB->setBuffer(); mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3); } unbindDeferredShader(soften_shader); + + LLGLSLShader& sky_shader = *gDeferredSoftenSkyProgram.selectVariant(); + bindDeferredShader(sky_shader); + { + LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_EQUAL); + + mScreenTriangleVB->setBuffer(); + mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3); + } + unbindDeferredShader(sky_shader); } static LLCachedControl local_light_count(gSavedSettings, "RenderLocalLightCount", 256); @@ -11372,7 +11393,9 @@ void LLPipeline::renderDeferredLighting() { LL_PROFILE_ZONE_NAMED_CATEGORY_PIPELINE("renderDeferredLighting - fullscreen lights"); - LLGLDepthTest depth(GL_FALSE); + // On the far plane (multiPointLightV.glsl): only where the world drew something, so no light is + // shaded over the sky. + LLGLDepthTest depth(GL_TRUE, GL_FALSE, GL_GREATER); LL_PROFILE_GPU_ZONE("fullscreen lights"); U32 count = 0; @@ -11664,12 +11687,12 @@ void LLPipeline::doWaterExclusionMask() } mWaterExclusionMask.bindTarget(); - glClearColor(1, 1, 1, 1); + gGL.setClearColor(LLColor4::white); mWaterExclusionMask.clear(); mWaterExclusionPool->render(); mWaterExclusionMask.flush(); - glClearColor(0, 0, 0, 0); + gGL.setClearColor(LLColor4::transparent); } // The map from clip space to a texture: x and y from [-1, 1] to [0, 1], z by @@ -13514,7 +13537,7 @@ void LLPipeline::profileAvatar(LLVOAvatar* avatar, bool profile_attachments) // generateImpostor's clear colour is its own business everywhere else, because display() // sets one before each clear it cares about. Nothing does that on the way back into a UI // draw, so put it back to the ambient here. - glClearColor(0.f, 0.f, 0.f, 0.f); + gGL.setClearColor(LLColor4::transparent); if (cur_shader) { @@ -13771,7 +13794,7 @@ void LLPipeline::generateImpostor(LLVOAvatar* avatar, bool preview_avatar, bool avatar->setImpostorViewRotation(bake_rot); } - glClearColor(0.0f,0.0f,0.0f,0.0f); + gGL.setClearColor(LLColor4::transparent); gGL.setColorMask(true, true); // get the number of pixels per angle diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index 78f2cbeda8..25e8efabde 100644 --- a/indra/newview/skins/default/xui/en/menu_viewer.xml +++ b/indra/newview/skins/default/xui/en/menu_viewer.xml @@ -1838,6 +1838,15 @@ function="World.EnvPreset" + + + + @@ -4135,6 +4144,16 @@ function="World.EnvPreset" function="Advanced.HandleAttachedLightParticles" parameter="RenderAttachedParticles" /> + + + + diff --git a/indra/newview/tests/alfarplane_test.cpp b/indra/newview/tests/alfarplane_test.cpp index a782bdccea..ada69cd076 100644 --- a/indra/newview/tests/alfarplane_test.cpp +++ b/indra/newview/tests/alfarplane_test.cpp @@ -160,6 +160,17 @@ namespace tut const std::string haze_v = shader_source("class1/deferred/hazeV.glsl"); ensure("haze drawn on the far plane, reversed", haze_v.find("gl_Position = vec4(position.xy, 0.0, 1.0);") != std::string::npos); ensure("haze drawn on the far plane, forward", haze_v.find("gl_Position = vec4(position.xy, 1.0, 1.0);") != std::string::npos); + // The lighting passes draw there too: the light map's passes, the lights and the soften pass's lit program in + // front of it, the soften pass's sky program on it. + for (const char* path : { "class2/deferred/softenLightV.glsl", "class3/deferred/multiPointLightV.glsl", + "class2/deferred/sunLightV.glsl", "class1/deferred/blurLightV.glsl" }) + { + const std::string v = shader_source(path); + ensure(std::string(path) + " drawn on the far plane, reversed", v.find("gl_Position = vec4(position.xy, 0.0, 1.0);") != std::string::npos); + ensure(std::string(path) + " drawn on the far plane, forward", v.find("gl_Position = vec4(position.xy, 1.0, 1.0);") != std::string::npos); + } + const std::string soften = shader_source("class3/deferred/softenLightF.glsl"); + ensure("the sky's soften program lights nothing", soften.find("writeColor(skyColor(getGBuffer(tc)));") != std::string::npos); ensure("haze takes geometry where it is", haze.find("= getPositionWithDepth(tc, depth);") != std::string::npos); ensure("void water fog keys on the far depth", shader_source("class3/deferred/waterHazeF.glsl").find("if (isFarDepth(depth))") != std::string::npos); const std::string flare = shader_source("class1/alchemy/lensFlareStateF.glsl"); diff --git a/indra/newview/tests/alhuddepth_gl_test.cpp b/indra/newview/tests/alhuddepth_gl_test.cpp new file mode 100644 index 0000000000..de870c5548 --- /dev/null +++ b/indra/newview/tests/alhuddepth_gl_test.cpp @@ -0,0 +1,594 @@ +/** + * @file alhuddepth_gl_test.cpp + * @brief ALHUDDepth on the hidden window, with the viewer's own copy shader: the window's depth under the HUD + * cleared for it and the scene's written back after it, and the rects that bound both. + * + * $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 "../alhuddepth.h" + +#include "llgl.h" +#include "llglheaders.h" +#include "llglslshader.h" +#include "llglstates.h" +#include "llrender.h" +#include "llrendertarget.h" +#include "llshadermgr.h" +#include "llstrider.h" +#include "llvertexbuffer.h" + +#include "../../llrender/tests/llheadlessgl_fixture.h" + +#include "../test/lltut.h" + +#include +#include +#include +#include +#include + +namespace tut +{ + namespace + { + constexpr S32 W = 64; + constexpr S32 H = 64; + constexpr F32 FOV = 1.f; + constexpr F32 NEAR_PLANE = 0.1f; + constexpr F32 FORWARD_FAR = 100.f; + constexpr F32 HUD_DEPTH = 10.f; + + // The scene: a world surface just past the near plane under the lower left quarter, one five metres out + // under the lower right, and sky above. + constexpr F32 NEAR_WORLD = NEAR_PLANE * 1.001f; + constexpr F32 MID_WORLD = 5.f; + + // The HUD point's box covers columns and rows 16 to 47 (the rect, grown a pixel, 15 to 48); what is drawn + // on it, columns 16 to 31, a metre into the HUD. + const LLVector3 HUD_BOX_LO(-0.25f, -0.25f, -2.f); + const LLVector3 HUD_BOX_HI(0.25f, 0.25f, 0.f); + constexpr F32 HUD_Z = -1.f; + + const LLColor4 SKY(0.f, 0.f, 1.f, 1.f); + const LLColor4 NEAR_COLOUR(1.f, 0.f, 0.f, 1.f); + const LLColor4 MID_COLOUR(0.f, 1.f, 0.f, 1.f); + const LLColor4 HUD_COLOUR(1.f, 1.f, 1.f, 1.f); + const LLColor4 OVERLAY_COLOUR(1.f, 1.f, 0.f, 1.f); + + // Places quads by their eye-space corners through a projection, as the HUD and the 3D UI are drawn. + const char* kQuadVertex = + "#version 400\n" + "layout(location = 0) in vec3 position;\n" + "uniform mat4 proj;\n" + "void main() { gl_Position = proj * vec4(position, 1.0); }\n"; + + const char* kQuadFragment = + "#version 400\n" + "uniform vec4 color;\n" + "out vec4 frag_color;\n" + "void main() { frag_color = color; }\n"; + + LLColor4U unorm(const LLColor4& c) + { + return LLColor4U((U8)ll_round(c.mV[0] * 255.f), (U8)ll_round(c.mV[1] * 255.f), (U8)ll_round(c.mV[2] * 255.f), + (U8)ll_round(c.mV[3] * 255.f)); + } + } + + struct alhuddepth_data + { + static ll_test::HeadlessGL& gl() + { + static ll_test::HeadlessGL instance(true, true, true, false); + return instance; + } + + alhuddepth_data() + : mShaders(std::string(AL_TEST_SHADER_DIR) + "/class") + { + gl(); + } + + ~alhuddepth_data() + { + tearDown(); + setConvention(false); + glViewport(0, 0, ll_test::HeadlessGL::WIDTH, ll_test::HeadlessGL::HEIGHT); + } + + void tearDown() + { + LLGLSLShader::unbind(); + glUseProgram(0); + mBlitProgram.unload(); + mCopyDepthProgram.unload(); + mShaders.clearShaderObjects(); + if (mQuadProgram) + { + glDeleteProgram(mQuadProgram); + mQuadProgram = 0; + } + if (mVAO) + { + glDeleteVertexArrays(1, &mVAO); + mVAO = 0; + } + if (mVBO) + { + glDeleteBuffers(1, &mVBO); + mVBO = 0; + } + mWindow.release(); + mScene.release(); + mTriangle = nullptr; + } + + // Forward depth, and reverse-Z where the context can clip zero to one. + static std::vector conventions() + { + std::vector reverse = { false }; + if (gGLManager.mHasClipControl) + { + reverse.push_back(true); + } + return reverse; + } + + static std::string named(bool reverse, const std::string& what) + { + return what + (reverse ? " (reverse-Z)" : " (forward)"); + } + + void setConvention(bool reverse) + { + LLRender::sReverseZ = reverse; + if (gGLManager.mHasClipControl) + { + glClipControl(GL_LOWER_LEFT, reverse ? GL_ZERO_TO_ONE : GL_NEGATIVE_ONE_TO_ONE); + } + glClearDepth(reverse ? 0.0 : 1.0); + LLGLDepthTest::rebase(); + } + + // The final blit's copy (copyF with COPY_DEPTH, as blitWithEffectsF writes depth) and the restore's + // (COPY_DEPTH and DEPTH_ONLY, as LLViewerShaderMgr builds gCopyDepthProgram), with the shared objects + // LLViewerShaderMgr::loadBasicShaders compiles for them; a window whose depth is 24-bit, as the window's + // framebuffer is, and a scene whose depth is the pipeline's. + void setUp(bool reverse) + { + tearDown(); + mReverse = reverse; + setConvention(reverse); + + std::map defines; + for (const char* object : { "deferred/textureUtilV.glsl", "objects/nonindexedTextureV.glsl" }) + { + S32 level = 1; + ensure(std::string("compiled ") + object, mShaders.loadShaderFile(object, level, GL_VERTEX_SHADER, &defines) != 0); + } + S32 level = 1; + ensure("compiled deferred/globalF.glsl", mShaders.loadShaderFile("deferred/globalF.glsl", level, GL_FRAGMENT_SHADER, &defines) != 0); + + mBlitProgram.mName = "Copy Depth Blit"; + mBlitProgram.mShaderFiles.clear(); + mBlitProgram.mShaderFiles.push_back(std::make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); + mBlitProgram.mShaderFiles.push_back(std::make_pair("interface/copyF.glsl", GL_FRAGMENT_SHADER)); + mBlitProgram.clearPermutations(); + mBlitProgram.addPermutation("COPY_DEPTH", "1"); + mBlitProgram.mShaderLevel = 1; + ensure("blit copy builds", mBlitProgram.createShader()); + + mCopyDepthProgram.mName = "Copy Depth Shader"; + mCopyDepthProgram.mShaderFiles.clear(); + mCopyDepthProgram.mShaderFiles.push_back(std::make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); + mCopyDepthProgram.mShaderFiles.push_back(std::make_pair("interface/copyF.glsl", GL_FRAGMENT_SHADER)); + mCopyDepthProgram.clearPermutations(); + mCopyDepthProgram.addPermutation("COPY_DEPTH", "1"); + mCopyDepthProgram.addPermutation("DEPTH_ONLY", "1"); + mCopyDepthProgram.mShaderLevel = 1; + ensure("depth copy builds", mCopyDepthProgram.createShader()); + + const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kQuadVertex); + const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kQuadFragment); + ensure("quad shaders compile", vs && fs); + mQuadProgram = glCreateProgram(); + glAttachShader(mQuadProgram, vs); + glAttachShader(mQuadProgram, fs); + glLinkProgram(mQuadProgram); + glDeleteShader(vs); + glDeleteShader(fs); + GLint linked = GL_FALSE; + glGetProgramiv(mQuadProgram, GL_LINK_STATUS, &linked); + ensure("quad program links", linked == GL_TRUE); + glGenVertexArrays(1, &mVAO); + glGenBuffers(1, &mVBO); + + ensure("window", mWindow.allocate(W, H, GL_RGBA8, true, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_NONE, + LLRenderTarget::DEPTH_FMT_24)); + ensure("scene", mScene.allocate(W, H, GL_RGBA8, true, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_NONE, + reverse ? LLRenderTarget::DEPTH_FMT_32F : LLRenderTarget::DEPTH_FMT_24)); + + mTriangle = new LLVertexBuffer(LLVertexBuffer::MAP_VERTEX); + ensure("triangle", mTriangle->allocateBuffer(3, 0)); + LLStrider vert; + mTriangle->getVertexStrider(vert); + vert[0].set(-1.f, 1.f, 0.f); + vert[1].set(-1.f, -3.f, 0.f); + vert[2].set(3.f, 1.f, 0.f); + mTriangle->unmapBuffer(); + + mSceneProjection = al_perspective(FOV, 1.f, NEAR_PLANE, reverse ? std::numeric_limits::infinity() : FORWARD_FAR); + + // As get_hud_matrices builds it: a unit-high ortho over the HUD's depth, its depth row flattened. + mHUDProjection = al_ortho(-0.5f, 0.5f, -0.5f, 0.5f, 0.f, HUD_DEPTH); + mHUDProjection.mMatrix[2].getF32ptr()[2] = reverse ? 0.005f : -0.01f; + + mRects.clear(); + ALHUDDepth::mergeRect(mRects, ALHUDDepth::boxRect(mHUDProjection, HUD_BOX_LO, HUD_BOX_HI, W, H)); + drawScene(); + } + + void quad(const LLMatrix4a& proj, const LLVector3 corners[4], const LLColor4& colour) + { + const F32 v[18] = { corners[0].mV[0], corners[0].mV[1], corners[0].mV[2], corners[1].mV[0], corners[1].mV[1], corners[1].mV[2], + corners[2].mV[0], corners[2].mV[1], corners[2].mV[2], corners[0].mV[0], corners[0].mV[1], corners[0].mV[2], + corners[2].mV[0], corners[2].mV[1], corners[2].mV[2], corners[3].mV[0], corners[3].mV[1], corners[3].mV[2] }; + // LLRender keeps a vertex array bound for its own buffers, and LLVertexBuffer remembers which buffer is + // bound: both are handed back as they were. + LLGLSLShader::unbind(); + gGL.flush(); + GLint previous_vao = 0; + glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &previous_vao); + glUseProgram(mQuadProgram); + glUniformMatrix4fv(glGetUniformLocation(mQuadProgram, "proj"), 1, GL_FALSE, proj.getF32ptr()); + glUniform4fv(glGetUniformLocation(mQuadProgram, "color"), 1, colour.mV); + glBindVertexArray(mVAO); + glBindBuffer(GL_ARRAY_BUFFER, mVBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(v), v, GL_STREAM_DRAW); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + glEnableVertexAttribArray(0); + glDrawArrays(GL_TRIANGLES, 0, 6); + glDisableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray((GLuint)previous_vao); + glUseProgram(0); + } + + // A quad of the scene over normalized device x0..x1, y0..y1, `dist` metres out. + void worldQuad(F32 x0, F32 x1, F32 y0, F32 y1, F32 dist, const LLColor4& colour) + { + const F32 sx = dist / mSceneProjection.getRow<0>()[0]; + const F32 sy = dist / mSceneProjection.getRow<1>()[1]; + const LLVector3 corners[4] = { LLVector3(x0 * sx, y0 * sy, -dist), LLVector3(x1 * sx, y0 * sy, -dist), + LLVector3(x1 * sx, y1 * sy, -dist), LLVector3(x0 * sx, y1 * sy, -dist) }; + quad(mSceneProjection, corners, colour); + } + + // A 3D UI quad over pixels (x - 2..x + 2, y - 2..y + 2), `dist` metres out, depth tested as the 3D UI is. + void overlay(S32 x, S32 y, F32 dist) + { + auto ndc_x = [](S32 px) { return (F32)px / (F32)W * 2.f - 1.f; }; + auto ndc_y = [](S32 px) { return (F32)px / (F32)H * 2.f - 1.f; }; + mWindow.bindTarget(); + { + LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_LEQUAL); + worldQuad(ndc_x(x - 2), ndc_x(x + 3), ndc_y(y - 2), ndc_y(y + 3), dist, OVERLAY_COLOUR); + } + mWindow.flush(); + } + + void drawScene() + { + mScene.bindTarget(); + { + LLGLSColorMask mask(true, true); + LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_LEQUAL); + gGL.setClearColor(SKY); + mScene.clear(); + worldQuad(-1.f, 0.f, -1.f, 0.f, NEAR_WORLD, NEAR_COLOUR); + worldQuad(0.f, 1.f, -1.f, 0.f, MID_WORLD, MID_COLOUR); + } + mScene.flush(); + } + + // The final blit: the scene's colour and its depth, verbatim, over the whole window. + void blit() + { + mWindow.bindTarget(); + { + LLGLSColorMask mask(true, true); + LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_ALWAYS); + LLGLDisable blend(GL_BLEND); + mBlitProgram.bind(); + mBlitProgram.bindTexture(LLShaderMgr::DIFFUSE_MAP, &mScene, ALSamplers::PointClamp); + mBlitProgram.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, &mScene); + mTriangle->setBuffer(); + mTriangle->drawArrays(LLRender::TRIANGLES, 0, 3); + mBlitProgram.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + mBlitProgram.unbindTexture(LLShaderMgr::DIFFUSE_MAP); + LLGLSLShader::unbind(); + } + mWindow.flush(); + } + + // The clear and the restore as render_hud_attachments runs them, with the copy program, or `copy` in its place. + void clearUnderHUD(const LLGLSLShader* copy = nullptr) + { + mWindow.bindTarget(); + ALHUDDepth::clear(copy ? *copy : mCopyDepthProgram, mRects, 0, 0); + mWindow.flush(); + } + + // The HUD's quad, depth tested as the HUD draws. + void drawHUD() + { + const LLVector3 corners[4] = { LLVector3(-0.25f, -0.25f, HUD_Z), LLVector3(0.f, -0.25f, HUD_Z), + LLVector3(0.f, 0.25f, HUD_Z), LLVector3(-0.25f, 0.25f, HUD_Z) }; + mWindow.bindTarget(); + { + LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_LEQUAL); + quad(mHUDProjection, corners, HUD_COLOUR); + } + mWindow.flush(); + } + + void restoreUnderHUD(LLGLSLShader* copy = nullptr) + { + mWindow.bindTarget(); + ALHUDDepth::restore(copy ? *copy : mCopyDepthProgram, mScene, *mTriangle, mRects, 0, 0); + mWindow.flush(); + } + + LLColor4U colourAt(S32 x, S32 y) + { + mWindow.bindTarget(); + const std::vector px = ll_test::readFramebufferRGBA(W, H); + mWindow.flush(); + const size_t i = ((size_t)y * W + x) * 4; + return LLColor4U(px[i], px[i + 1], px[i + 2], px[i + 3]); + } + + std::vector depths(LLRenderTarget& target) + { + std::vector d((size_t)W * H, 0.f); + target.bindTarget(); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(0, 0, W, H, GL_DEPTH_COMPONENT, GL_FLOAT, d.data()); + target.flush(); + return d; + } + + bool inRects(S32 x, S32 y) const + { + for (const LLRect& rect : mRects) + { + if (x >= rect.mLeft && x < rect.mRight && y >= rect.mBottom && y < rect.mTop) + { + return true; + } + } + return false; + } + + ll_test::TestShaderMgr mShaders; + LLGLSLShader mBlitProgram; + LLGLSLShader mCopyDepthProgram; + GLuint mQuadProgram = 0; + GLuint mVAO = 0; + GLuint mVBO = 0; + LLRenderTarget mWindow; + LLRenderTarget mScene; + LLPointer mTriangle; + LLMatrix4a mSceneProjection; + LLMatrix4a mHUDProjection; + std::vector mRects; + bool mReverse = false; + }; + + typedef test_group alhuddepth_t; + typedef alhuddepth_t::object alhuddepth_object_t; + tut::alhuddepth_t alhuddepth_testcase("ALHUDDepth"); + + // The final blit's copy fills the window's depth with the scene's, as the window's 24 bits hold it. + template<> template<> + void alhuddepth_object_t::test<1>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + blit(); + const std::vector scene = depths(mScene); + const std::vector window = depths(mWindow); + F32 worst = 0.f; + for (size_t i = 0; i < scene.size(); ++i) + { + worst = llmax(worst, std::fabs(scene[i] - window[i])); + } + ensure(named(reverse, "the window holds the scene's depth"), worst < 1e-6f); + ensure(named(reverse, "and its colour"), colourAt(40, 24) == unorm(MID_COLOUR)); + const F32 cleared = reverse ? 0.f : 1.f; + ensure_equals(named(reverse, "sky is the cleared depth"), window[(size_t)40 * W + 40], cleared); + ensure(named(reverse, "the near surface is nearer than the mid one"), + reverse ? window[(size_t)24 * W + 24] > window[(size_t)24 * W + 40] : window[(size_t)24 * W + 24] < window[(size_t)24 * W + 40]); + } + } + + // The reported clipping: against the scene's depth the HUD loses to a world surface at the near plane. With the + // depth under it cleared, it draws over it. + template<> template<> + void alhuddepth_object_t::test<2>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + blit(); + drawHUD(); + ensure(named(reverse, "uncleared, the near world hides the HUD"), colourAt(24, 24) == unorm(NEAR_COLOUR)); + ensure(named(reverse, "where there is sky, the HUD shows anyway"), colourAt(24, 40) == unorm(HUD_COLOUR)); + + blit(); + clearUnderHUD(); + drawHUD(); + ensure(named(reverse, "cleared, the HUD shows over the near world"), colourAt(24, 24) == unorm(HUD_COLOUR)); + ensure(named(reverse, "and over the sky"), colourAt(24, 40) == unorm(HUD_COLOUR)); + } + } + + // After the scene's depth is written back, the 3D UI under the HUD's rect is hidden by the world as before, and + // by the HUD where the HUD is in front. Without it, what the world hides shows through. + template<> template<> + void alhuddepth_object_t::test<3>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + blit(); + clearUnderHUD(); + drawHUD(); + restoreUnderHUD(); + overlay(40, 24, 8.f); + overlay(44, 20, 3.f); + overlay(24, 40, 3.f); + overlay(40, 40, 3.f); + ensure(named(reverse, "behind the world, off the HUD: hidden"), colourAt(40, 24) == unorm(MID_COLOUR)); + ensure(named(reverse, "in front of the world, off the HUD: shown"), colourAt(44, 20) == unorm(OVERLAY_COLOUR)); + ensure(named(reverse, "on the HUD: hidden behind it"), colourAt(24, 40) == unorm(HUD_COLOUR)); + ensure(named(reverse, "over sky, off the HUD: shown"), colourAt(40, 40) == unorm(OVERLAY_COLOUR)); + + blit(); + clearUnderHUD(); + drawHUD(); + overlay(40, 24, 8.f); + ensure(named(reverse, "unrestored, what the world hides shows through"), colourAt(40, 24) == unorm(OVERLAY_COLOUR)); + } + } + + // Outside every rect nothing changes: the clear empties the rects alone, and with nothing drawn between, the + // restore gives back exactly the depth the blit wrote. + template<> template<> + void alhuddepth_object_t::test<4>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + ensure_equals(named(reverse, "one rect"), mRects.size(), size_t(1)); + ensure(named(reverse, "the rect is inside the window and not all of it"), + mRects[0].mLeft > 0 && mRects[0].mRight < W && mRects[0].mBottom > 0 && mRects[0].mTop < H); + + blit(); + const std::vector before = depths(mWindow); + clearUnderHUD(); + const std::vector cleared = depths(mWindow); + const F32 far_value = reverse ? 0.f : 1.f; + size_t changed_outside = 0; + size_t uncleared_inside = 0; + for (S32 y = 0; y < H; ++y) + { + for (S32 x = 0; x < W; ++x) + { + const size_t i = (size_t)y * W + x; + if (inRects(x, y)) + { + uncleared_inside += cleared[i] != far_value ? 1 : 0; + } + else + { + changed_outside += cleared[i] != before[i] ? 1 : 0; + } + } + } + ensure_equals(named(reverse, "the clear leaves the outside alone"), changed_outside, size_t(0)); + ensure_equals(named(reverse, "and empties the inside"), uncleared_inside, size_t(0)); + + restoreUnderHUD(); + const std::vector restored = depths(mWindow); + size_t differing = 0; + for (size_t i = 0; i < before.size(); ++i) + { + differing += restored[i] != before[i] ? 1 : 0; + } + ensure_equals(named(reverse, "the restore gives back the blit's depth"), differing, size_t(0)); + } + } + + // boxRect: the pixels a box covers through an orthographic projection, grown a pixel each way, clipped to the + // viewport, empty off it. + template<> template<> + void alhuddepth_object_t::test<5>() + { + const LLMatrix4a ortho = LLMatrix4a::ortho(-0.5f, 0.5f, -0.5f, 0.5f, 0.f, HUD_DEPTH); + + LLRect rect = ALHUDDepth::boxRect(ortho, LLVector3(-0.25f, -0.25f, -2.f), LLVector3(0.25f, 0.25f, 0.f), W, H); + ensure("pixels 16 to 48 grown to 15 to 49", rect == LLRect(15, 49, 49, 15)); + + // x from -0.1 to 0.13: pixels 25.6 to 40.32; y from 0 to 0.2: pixels 32 to 44.8. + rect = ALHUDDepth::boxRect(ortho, LLVector3(-0.1f, 0.f, -1.f), LLVector3(0.13f, 0.2f, -1.f), W, H); + ensure("fractional edges floored and ceiled before the growth", rect == LLRect(24, 46, 42, 31)); + + // x from 0.4 to 0.8: pixels 57.6 to 83.2. + rect = ALHUDDepth::boxRect(ortho, LLVector3(0.4f, -0.1f, -1.f), LLVector3(0.8f, 0.1f, -1.f), W, H); + ensure("clipped to the viewport", rect == LLRect(56, 40, 64, 24)); + + rect = ALHUDDepth::boxRect(ortho, LLVector3(0.6f, -0.1f, -1.f), LLVector3(0.9f, 0.1f, -1.f), W, H); + ensure("off the viewport, empty", rect.isEmpty()); + } + + // mergeRect: overlapping rects fold into one, disjoint ones are kept, and a rect bridging two folds them all. + template<> template<> + void alhuddepth_object_t::test<6>() + { + std::vector rects; + ALHUDDepth::mergeRect(rects, LLRect(0, 10, 10, 0)); + ALHUDDepth::mergeRect(rects, LLRect(5, 15, 15, 5)); + ensure_equals("overlapping, one", rects.size(), size_t(1)); + ensure("their union", rects[0] == LLRect(0, 15, 15, 0)); + + ALHUDDepth::mergeRect(rects, LLRect(30, 40, 40, 30)); + ensure_equals("disjoint, kept apart", rects.size(), size_t(2)); + + ALHUDDepth::mergeRect(rects, LLRect(14, 31, 31, 14)); + ensure_equals("bridged, one", rects.size(), size_t(1)); + ensure("around all three", rects[0] == LLRect(0, 40, 40, 0)); + } + + // Without the copy program to put the scene's depth back, the depth under the HUD is not cleared: the HUD tests + // against the scene's as before, and what the world hides from the 3D UI after it stays hidden. + template<> template<> + void alhuddepth_object_t::test<7>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + LLGLSLShader unloaded; + blit(); + const std::vector before = depths(mWindow); + clearUnderHUD(&unloaded); + ensure(named(reverse, "nothing cleared"), depths(mWindow) == before); + drawHUD(); + restoreUnderHUD(&unloaded); + overlay(40, 24, 8.f); + ensure(named(reverse, "behind the world under the HUD's rect: hidden"), colourAt(40, 24) == unorm(MID_COLOUR)); + ensure(named(reverse, "the near world still hides the HUD"), colourAt(24, 24) == unorm(NEAR_COLOUR)); + } + } +} diff --git a/indra/newview/tests/alselectionoutline_gl_test.cpp b/indra/newview/tests/alselectionoutline_gl_test.cpp new file mode 100644 index 0000000000..16fc6d5abf --- /dev/null +++ b/indra/newview/tests/alselectionoutline_gl_test.cpp @@ -0,0 +1,2794 @@ +/** + * @file alselectionoutline_gl_test.cpp + * @brief ALSelectionOutline's id and edge passes on the hidden window, with the viewer's own shader files: ids, + * cut-out faces, the contour's width and anti-aliasing, priorities and the edges between objects, hidden + * parts, the scissor, a rigged pose, the HUD, the wireframe, the hover glow and ALHoverGlow's fades, and the + * palette, width and scissor helpers. + * + * $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 "../alhoverglow.h" +#include "../alselectionoutline.h" + +#include "llgl.h" +#include "llglheaders.h" +#include "llglslshader.h" +#include "llglstates.h" +#include "llrender.h" +#include "llrendertarget.h" +#include "llshadermgr.h" +#include "llstrider.h" +#include "llvertexbuffer.h" + +#include "../../llrender/tests/llheadlessgl_fixture.h" + +#include "../test/lltut.h" + +#include +#include +#include +#include +#include +#include + +namespace tut +{ + namespace + { + using Outline = ALSelectionOutline; + + constexpr S32 W = 64; + constexpr S32 H = 64; + constexpr F32 FOV = 1.f; + constexpr F32 NEAR_PLANE = 0.1f; + constexpr F32 FORWARD_FAR = 100.f; + + // Two quads meeting at x = 0, five metres down -z: A covers pixel columns 9 to 31, B 32 to 54, and both + // rows 20 to 43. + constexpr F32 QUAD_Z = -5.f; + constexpr U32 ID_A = 1; + // Past 255, so the high byte and the second pair of palette rows carry it. + constexpr U32 ID_B = 300; + + // How deep quads lie under the pixel projection, whose units are pixels. + constexpr F32 PIXEL_Z = -2.f; + + // The contour width at a UI scale of 1 the cases' pixel positions are laid out for: two pixels and a pixel + // of anti-aliasing, reaching three. + constexpr F32 TEST_WIDTH = 2.f; + + const LLColor4 RED(1.f, 0.f, 0.f, 1.f); + const LLColor4 GREEN(0.f, 1.f, 0.f, 1.f); + + struct Pixel { U8 r, g, b, a; }; + + Pixel at(const std::vector& px, S32 x, S32 y) + { + const size_t i = ((size_t)y * W + x) * 4; + return { px[i], px[i + 1], px[i + 2], px[i + 3] }; + } + + bool black(const Pixel& p) { return p.r == 0 && p.g == 0 && p.b == 0; } + bool red(const Pixel& p) { return p.r > 0 && p.g == 0 && p.b == 0; } + bool green(const Pixel& p) { return p.g > 0 && p.r == 0 && p.b == 0; } + bool blue(const Pixel& p) { return p.b > 0 && p.r == 0 && p.g == 0; } + + // An id texel the scene hides, its object's hidden parts drawn dimmed: 0.5 in .b. + bool drawnHidden(const Pixel& p) { return p.b == 127 || p.b == 128; } + + U32 idAt(const std::vector& px, S32 x, S32 y) + { + const Pixel p = at(px, x, y); + return (U32)p.r | ((U32)p.g << 8); + } + + size_t lit(const std::vector& px) + { + size_t count = 0; + for (S32 y = 0; y < H; ++y) + { + for (S32 x = 0; x < W; ++x) + { + count += black(at(px, x, y)) ? 0 : 1; + } + } + return count; + } + + // Of the frame `px`, the sum of a column's coverage in red from row `y0` to `y1` - 1, in pixels: the width + // of a line across it. + // The rows a column's runs of drawn pixels cover, bottom up, each as its first row and one past its last. + std::vector> columnRuns(const std::vector& px, S32 x) + { + std::vector> found; + for (S32 y = 0; y < H; ++y) + { + if (black(at(px, x, y))) + { + continue; + } + if (!found.empty() && found.back().second == y) + { + found.back().second = y + 1; + } + else + { + found.push_back({ y, y + 1 }); + } + } + return found; + } + + F32 ink(const std::vector& px, S32 x, S32 y0, S32 y1) + { + F32 sum = 0.f; + for (S32 y = y0; y < y1; ++y) + { + sum += (F32)at(px, x, y).r / 255.f; + } + return sum; + } + + LLMatrix4a identity() + { + LLMatrix4a m; + m.setIdentity(); + return m; + } + + // A projection a pixel to the unit over the target, as the HUD's is a projection with no scene behind it. + LLMatrix4a pixelProjection() + { + return al_ortho(0.f, (F32)W, 0.f, (F32)H, 0.f, 10.f); + } + } + + struct alselectionoutline_data + { + static ll_test::HeadlessGL& gl() + { + static ll_test::HeadlessGL instance(true, true, true, false); + return instance; + } + + alselectionoutline_data() + : mShaders(std::string(AL_TEST_SHADER_DIR) + "/class") + { + gl(); + } + + ~alselectionoutline_data() + { + tearDown(); + setConvention(false); + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.loadIdentity(); + gGL.matrixMode(LLRender::MM_TEXTURE0); + gGL.loadIdentity(); + gGL.matrixMode(LLRender::MM_MODELVIEW); + gGL.loadIdentity(); + glViewport(0, 0, ll_test::HeadlessGL::WIDTH, ll_test::HeadlessGL::HEIGHT); + } + + // Unloading a program deletes the shared objects attached to it, so a new convention compiles them again. + void tearDown() + { + LLGLSLShader::unbind(); + mIdProgram.unload(); + mJumpProgram.unload(); + mWireProgram.unload(); + mCopyDepthProgram.unload(); + mTileProgram.unload(); + mOutlineProgram.unload(); + mFullSearchProgram.unload(); + mGlowReachProgram.unload(); + mGlowRowProgram.unload(); + mShaders.clearShaderObjects(); + + mIdMap.release(); + mJumps.release(); + mGlowReach.release(); + mGlowRows.release(); + mWindow.release(); + mTiles.release(); + mAllTiles.release(); + mScene.release(); + mFar.release(); + mFrame.release(); + for (U32* texture : { &mPalette, &mRamp, &mClear, &mGlass }) + { + if (*texture) + { + LLImageGL::deleteTextures(1, texture); + *texture = 0; + } + } + mPaletteRows = 0; + mBuffers.clear(); + mTriangle = nullptr; + } + + // Forward depth, and reverse-Z where the context can clip zero to one. + static std::vector conventions() + { + std::vector reverse = { false }; + if (gGLManager.mHasClipControl) + { + reverse.push_back(true); + } + return reverse; + } + + static std::string named(bool reverse, const std::string& what) + { + return what + (reverse ? " (reverse-Z)" : " (forward)"); + } + + void setConvention(bool reverse) + { + LLRender::sReverseZ = reverse; + if (gGLManager.mHasClipControl) + { + glClipControl(GL_LOWER_LEFT, reverse ? GL_ZERO_TO_ONE : GL_NEGATIVE_ONE_TO_ONE); + } + glClearDepth(reverse ? 0.0 : 1.0); + LLGLDepthTest::rebase(); + } + + // The programs as LLViewerShaderMgr builds them, and the shared objects LLViewerShaderMgr::loadBasicShaders + // compiles for them to attach, under the depth convention asked for: loadShaderFile injects REVERSE_Z as + // the viewer's compiles get it. + void setUp(bool reverse) + { + tearDown(); + mReverse = reverse; + setConvention(mReverse); + + // The palette length the viewer compiles objectSkinV with (LL_MAX_JOINTS_PER_MESH_OBJECT). + std::map defines = { { "MAX_JOINTS_PER_MESH_OBJECT", "110" } }; + for (const char* object : { "avatar/objectSkinV.glsl", "deferred/textureUtilV.glsl", "objects/nonindexedTextureV.glsl" }) + { + S32 level = 1; + ensure(std::string("compiled ") + object, mShaders.loadShaderFile(object, level, GL_VERTEX_SHADER, &defines) != 0); + } + S32 level = 1; + ensure("compiled deferred/globalF.glsl", mShaders.loadShaderFile("deferred/globalF.glsl", level, GL_FRAGMENT_SHADER, &defines) != 0); + + mIdProgram.mName = "Selection Id Shader"; + mIdProgram.mShaderFiles.clear(); + mIdProgram.mShaderFiles.push_back(std::make_pair("interface/selectionIdV.glsl", GL_VERTEX_SHADER)); + mIdProgram.mShaderFiles.push_back(std::make_pair("interface/selectionIdF.glsl", GL_FRAGMENT_SHADER)); + mIdProgram.mShaderFiles.push_back(std::make_pair("interface/selectionAlphaF.glsl", GL_FRAGMENT_SHADER)); + mIdProgram.mShaderFiles.push_back(std::make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); + mIdProgram.mShaderLevel = 1; + ensure("id program builds with its rigged variant", mIdProgram.createShader(LLGLSLShader::VARIANT_RIGGED)); + ensure("id program has a rigged variant", mIdProgram.mRiggedVariant && mIdProgram.mRiggedVariant != &mIdProgram); + + mWireProgram.mName = "Selection Wireframe Shader"; + mWireProgram.mShaderFiles.clear(); + mWireProgram.mShaderFiles.push_back(std::make_pair("interface/selectionIdV.glsl", GL_VERTEX_SHADER)); + mWireProgram.mShaderFiles.push_back(std::make_pair("interface/selectionWireframeG.glsl", GL_GEOMETRY_SHADER)); + mWireProgram.mShaderFiles.push_back(std::make_pair("interface/selectionWireframeF.glsl", GL_FRAGMENT_SHADER)); + mWireProgram.mShaderFiles.push_back(std::make_pair("interface/selectionAlphaF.glsl", GL_FRAGMENT_SHADER)); + mWireProgram.mShaderFiles.push_back(std::make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); + mWireProgram.mShaderLevel = 1; + ensure("wireframe program builds with its rigged variant", mWireProgram.createShader(LLGLSLShader::VARIANT_RIGGED)); + ensure("wireframe program has a rigged variant", mWireProgram.mRiggedVariant && mWireProgram.mRiggedVariant != &mWireProgram); + + // The final blit's depth copy, as LLViewerShaderMgr builds gCopyDepthProgram. + mCopyDepthProgram.mName = "Copy Depth Shader"; + mCopyDepthProgram.mShaderFiles.clear(); + mCopyDepthProgram.mShaderFiles.push_back(std::make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); + mCopyDepthProgram.mShaderFiles.push_back(std::make_pair("interface/copyF.glsl", GL_FRAGMENT_SHADER)); + mCopyDepthProgram.clearPermutations(); + mCopyDepthProgram.addPermutation("COPY_DEPTH", "1"); + mCopyDepthProgram.addPermutation("DEPTH_ONLY", "1"); + mCopyDepthProgram.mShaderLevel = 1; + ensure("copy depth program builds", mCopyDepthProgram.createShader()); + + const std::pair passes[] = { { &mJumpProgram, "selectionJumpF.glsl" }, + { &mTileProgram, "selectionTileF.glsl" }, + { &mOutlineProgram, "selectionOutlineF.glsl" }, + { &mFullSearchProgram, "selectionOutlineF.glsl" }, + { &mGlowReachProgram, "selectionGlowReachF.glsl" }, + { &mGlowRowProgram, "selectionGlowRowF.glsl" } }; + for (const auto& [program, fragment] : passes) + { + program->mName = fragment; + program->mShaderFiles.clear(); + program->mShaderFiles.push_back(std::make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); + program->mShaderFiles.push_back(std::make_pair(std::string("interface/") + fragment, GL_FRAGMENT_SHADER)); + if (program != &mGlowReachProgram) + { + program->mShaderFiles.push_back(std::make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); + } + program->mShaderLevel = 1; + program->clearPermutations(); + if (program == &mFullSearchProgram) + { + // The edge pass searching the disk for the glow too: the full search. + program->addPermutation("GLOW_DISK", "1"); + } + ensure(std::string(fragment) + "'s program builds", program->createShader()); + } + + const LLRenderTarget::eDepthFormat depth = mReverse ? LLRenderTarget::DEPTH_FMT_32F : LLRenderTarget::DEPTH_FMT_24; + ensure("id target", mIdMap.allocate(W, H, GL_RGBA8, true, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_NONE, depth)); + ensure("jump target", mJumps.allocate(W, H, GL_R8)); + ensure("glow reach target", mGlowReach.allocate(Outline::tileCount(W), Outline::tileCount(H), GL_RGBA16)); + ensure("glow row target", mGlowRows.allocate(W, H, GL_R8)); + ensure("frame target", mFrame.allocate(W, H, GL_RGBA8)); + ensure("window target", mWindow.allocate(W, H, GL_RGBA8, true, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_NONE, depth)); + ensure("tile target", mTiles.allocate(Outline::tileCount(W), Outline::tileCount(H), GL_RGBA16)); + // Every tile holding every id and a jump: the edge pass searches every pixel's disk, as it did before + // the tiles. + ensure("all tiles target", mAllTiles.allocate(Outline::tileCount(W), Outline::tileCount(H), GL_RGBA16)); + mAllTiles.bindTarget(); + gGL.setClearColor(LLColor4(1.f, 1.f, 1.f, 1.f)); + mAllTiles.clear(); + gGL.setClearColor(LLColor4(0.f, 0.f, 0.f, 0.f)); + mAllTiles.flush(); + ensure("far depth", mFar.allocate(W, H, 0, true, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_NONE, depth)); + clearDepth(mFar); + sceneAt(W, H); + + mTriangle = new LLVertexBuffer(LLVertexBuffer::MAP_VERTEX); + ensure("triangle", mTriangle->allocateBuffer(3, 0)); + LLStrider vert; + mTriangle->getVertexStrider(vert); + vert[0].set(-1.f, 1.f, 0.f); + vert[1].set(-1.f, -3.f, 0.f); + vert[2].set(3.f, 1.f, 0.f); + mTriangle->unmapBuffer(); + + mProjection = al_perspective(FOV, 1.f, NEAR_PLANE, mReverse ? std::numeric_limits::infinity() : FORWARD_FAR); + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.loadMatrix(mProjection); + gGL.matrixMode(LLRender::MM_TEXTURE0); + gGL.loadIdentity(); + gGL.matrixMode(LLRender::MM_MODELVIEW); + gGL.loadIdentity(); + } + + // The scene's depth at width x height, cleared to the far value. + void sceneAt(S32 width, S32 height) + { + const LLRenderTarget::eDepthFormat depth = mReverse ? LLRenderTarget::DEPTH_FMT_32F : LLRenderTarget::DEPTH_FMT_24; + ensure("scene depth", mScene.allocate(width, height, 0, true, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_NONE, depth)); + clearDepth(mScene); + } + + // Something in the scene `dist` metres out over the pixels x to x + w - 1 and y to y + h - 1, all of them by + // default: the scene's depth there. + void occlude(F32 dist, S32 x = 0, S32 y = 0, S32 w = W, S32 h = H) + { + mScene.bindTarget(); + { + LLGLDepthTest depth(GL_TRUE, GL_TRUE); + LLGLSScissor scissor(x, y, w, h); + glClearDepth(storedDepth(dist)); + mScene.clear(GL_DEPTH_BUFFER_BIT); + glClearDepth(mReverse ? 0.0 : 1.0); + } + mScene.flush(); + } + + void clearDepth(LLRenderTarget& target) + { + target.bindTarget(); + LLGLDepthTest depth(GL_TRUE, GL_TRUE); + target.clear(GL_DEPTH_BUFFER_BIT); + target.flush(); + } + + // The depth the projection stores for a surface `dist` metres in front of the eye. + F32 storedDepth(F32 dist) const + { + LLVector4a clip; + mProjection.transform4(LLVector4a(0.f, 0.f, -dist, 1.f), clip); + const F32 ndc = clip[2] / clip[3]; + return mReverse ? ndc : ndc * 0.5f + 0.5f; + } + + // A quad through four corners, texture coordinates 0 to 1 across it from the first, every vertex weighted + // wholly to joint 0 when `weights`, and coloured `colour`, or opaque white, as a face's vertices are. + LLVertexBuffer* quadAt(const LLVector3 (&corners)[4], bool weights = false, const LLColor4U* colour = nullptr) + { + const U32 mask = LLVertexBuffer::MAP_VERTEX | LLVertexBuffer::MAP_TEXCOORD0 | LLVertexBuffer::MAP_COLOR | + (weights ? LLVertexBuffer::MAP_WEIGHT4 : 0); + LLPointer buffer = new LLVertexBuffer(mask); + ensure("quad", buffer->allocateBuffer(4, 6)); + LLStrider vert; + LLStrider texcoord; + LLStrider index; + buffer->getVertexStrider(vert); + buffer->getTexCoord0Strider(texcoord); + buffer->getIndexStrider(index); + const LLVector2 uvs[4] = { LLVector2(0.f, 0.f), LLVector2(1.f, 0.f), LLVector2(1.f, 1.f), LLVector2(0.f, 1.f) }; + for (U32 i = 0; i < 4; ++i) + { + vert[i] = corners[i]; + texcoord[i] = uvs[i]; + } + const U16 indices[6] = { 0, 1, 2, 0, 2, 3 }; + for (U32 i = 0; i < 6; ++i) + { + index[i] = indices[i]; + } + if (weights) + { + // Joint 0 in the integer part, its weight in the fraction, which the shader renormalises. + LLStrider weight; + buffer->getWeight4Strider(weight); + for (U32 i = 0; i < 4; ++i) + { + weight[i].set(0.999f, 0.f, 0.f, 0.f); + } + } + LLStrider colours; + buffer->getColorStrider(colours); + for (U32 i = 0; i < 4; ++i) + { + colours[i] = colour ? *colour : LLColor4U(255, 255, 255, 255); + } + buffer->unmapBuffer(); + mBuffers.push_back(buffer); + return buffer.get(); + } + + // A quad over x0..x1, y0..y1, at depth z0 along its x0 edge and z1 along its x1 edge. + LLVertexBuffer* quad(F32 x0, F32 x1, F32 y0, F32 y1, F32 z0, F32 z1, bool weights = false, + const LLColor4U* colour = nullptr) + { + const LLVector3 corners[4] = { LLVector3(x0, y0, z0), LLVector3(x1, y0, z1), LLVector3(x1, y1, z1), LLVector3(x0, y1, z0) }; + return quadAt(corners, weights, colour); + } + + LLVertexBuffer* quadA() { return quad(-2.f, 0.f, -1.f, 1.f, QUAD_Z, QUAD_Z); } + LLVertexBuffer* quadB() { return quad(0.f, 2.f, -1.f, 1.f, QUAD_Z, QUAD_Z); } + + // Under the pixel projection: pixel columns x0 to x1 - 1 and rows y0 to y1 - 1. + LLVertexBuffer* pixels(S32 x0, S32 x1, S32 y0, S32 y1, const LLColor4U* colour = nullptr) + { + return quad((F32)x0, (F32)x1, (F32)y0, (F32)y1, PIXEL_Z, PIXEL_Z, false, colour); + } + + // A texture 256 texels wide whose alpha is its column: white, and opaque at its right end. + U32 ramp() + { + if (!mRamp) + { + std::vector texels(256 * 4); + for (U32 x = 0; x < 256; ++x) + { + texels[x * 4] = texels[x * 4 + 1] = texels[x * 4 + 2] = 255; + texels[x * 4 + 3] = (U8)x; + } + LLImageGL::generateTextures(1, &mRamp); + gGL.getTextureSlot(0)->bindManual(ALTextureSlot::TT_TEXTURE, mRamp); + LLImageGL::allocateTexture2D(ALTextureSlot::getInternalType(ALTextureSlot::TT_TEXTURE), GL_RGBA8, 256, 1, + GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); + gGL.getTextureSlot(0)->unbind(); + } + return mRamp; + } + + // A 4 x 4 texture whose every texel is white with alpha `alpha`. + static U32 uniformTexture(U8 alpha) + { + std::vector texels(4 * 4 * 4, 255); + for (size_t i = 3; i < texels.size(); i += 4) + { + texels[i] = alpha; + } + U32 texture = 0; + LLImageGL::generateTextures(1, &texture); + gGL.getTextureSlot(0)->bindManual(ALTextureSlot::TT_TEXTURE, texture); + LLImageGL::allocateTexture2D(ALTextureSlot::getInternalType(ALTextureSlot::TT_TEXTURE), GL_RGBA8, 4, 4, + GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); + gGL.getTextureSlot(0)->unbind(); + return texture; + } + + // A texture that is clear everywhere, as the library's transparent texture (IMG_TRANSPARENT) is. + U32 clearTexture() + { + if (!mClear) + { + mClear = uniformTexture(0); + } + return mClear; + } + + // A texture drawn at three tenths everywhere, as a pane of glass is. + U32 glassTexture() + { + if (!mGlass) + { + mGlass = uniformTexture(77); + } + return mGlass; + } + + struct Draw + { + LLVertexBuffer* mBuffer; + U32 mId; + Outline::EPriority mPriority = Outline::PRIORITY_ROOT; + // The face's alpha test, its texture, its base colour transform and its texture animation. + F32 mCutoff = Outline::NO_ALPHA_TEST; + U32 mTexture = 0; + const LLGLTFMaterial::TextureTransform::Pack* mTransform = nullptr; + const LLMatrix4a* mTextureMatrix = nullptr; + // Hidden parts drawn dimmed, as LLSelectMgr asks of most roles; and a double-sided material. + bool mShowHidden = true; + bool mDoubleSided = false; + // What it asks for (ALSelectionOutline::EPart), which ALSelectionOutline::add keeps as partsFor gives. + U32 mParts = Outline::PART_CONTOUR | Outline::PART_WIREFRAME; + + U32 parts() const { return Outline::partsFor(mPriority, mParts); } + }; + + // A draw of `buffer` as `id` at PRIORITY_HOVER, as ALHoverGlow adds what the pointer is over: hidden parts + // left out, as ALSelectionOutline::add leaves them. + Draw hoverDraw(LLVertexBuffer* buffer, U32 id) const + { + Draw draw{ buffer, id, Outline::PRIORITY_HOVER }; + draw.mShowHidden = false; + return draw; + } + + // The draws into the bound id or wireframe program, as ALSelectionOutline's face walk draws faces: each with its + // id, alpha test, texture, texture animation and culling. A rigged program takes `joints`: joint 0's three + // columns, rotation in .xyz and translation in .w. Only the draws `drawn` passes are drawn. False where a + // texture had no unit to go to. + template + bool drawAll(LLGLSLShader& program, const std::vector& draws, const F32* joints, Drawn&& drawn) + { + bool texture_read = true; + if (joints) + { + const F32 origin[3] = { 0.f, 0.f, 0.f }; + program.uniformMatrix3x4fv(LLShaderMgr::AVATAR_MATRIX, 1, GL_FALSE, joints); + program.uniform3fv(LLShaderMgr::SKIN_ORIGIN, 1, origin); + } + for (const Draw& draw : draws) + { + if (!drawn(draw)) + { + continue; + } + Outline::setId(program, draw.mId, draw.mPriority, draw.mShowHidden, + Outline::outlined(draw.parts(), draw.mPriority)); + Outline::setAlphaTest(program, draw.mCutoff, draw.mTransform ? *draw.mTransform : Outline::IDENTITY_TRANSFORM); + const S32 channel = draw.mTexture ? program.enableTexture(LLShaderMgr::DIFFUSE_MAP) : -1; + if (channel > -1) + { + gGL.getTextureSlot(channel)->bindManual(ALTextureSlot::TT_TEXTURE, draw.mTexture, + gGL.getSampler(ALSamplers::PointClamp)); + } + texture_read = texture_read && (!draw.mTexture || channel > -1); + if (draw.mTextureMatrix) + { + gGL.matrixMode(LLRender::MM_TEXTURE0); + gGL.loadMatrix(*draw.mTextureMatrix); + gGL.matrixMode(LLRender::MM_MODELVIEW); + } + Outline::drawFace(*draw.mBuffer, 0, 3, 6, 0, draw.mDoubleSided); + if (draw.mTextureMatrix) + { + gGL.matrixMode(LLRender::MM_TEXTURE0); + gGL.loadIdentity(); + gGL.matrixMode(LLRender::MM_MODELVIEW); + } + } + return texture_read; + } + + // The id pass as ALSelectionOutline::render runs it, into `target`, reading `scene`'s depth, or with no + // scene as it runs for the HUD; `unread` is then bound where the scene's depth would be, which the pass must + // ignore. A rigged pass takes `palette`: joint 0's three columns, rotation in .xyz and translation in .w. + void idPass(LLRenderTarget& target, LLRenderTarget* scene, const std::vector& draws, + const F32* palette = nullptr, LLRenderTarget* unread = nullptr) + { + // Checked once the target is let go, so a failure leaves nothing bound for the tests after it. + bool texture_read = true; + mIdProjection = gGL.getProjectionMatrix(); + target.bindTarget(); + { + Outline::IdPassState state; + gGL.setClearColor(LLColor4(0.f, 0.f, 0.f, 0.f)); + target.clear(); + + const bool rigged = palette != nullptr; + LLGLSLShader& program = rigged ? *mIdProgram.mRiggedVariant : mIdProgram; + mIdProgram.bind(rigged); + Outline::bindIdPass(program, scene, (S32)target.getWidth(), (S32)target.getHeight()); + if (unread) + { + program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, unread); + } + // The HUD's is the pass with no scene. + const bool hud = scene == nullptr; + texture_read = drawAll(program, draws, palette, [hud](const Draw& draw) + { return Outline::inIdPass(draw.parts(), draw.mPriority, hud); }); + program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + LLGLSLShader::unbind(); + } + target.flush(); + ensure("the id program reads the face's texture", texture_read); + } + + void idPass(const std::vector& draws, const F32* palette = nullptr) + { + idPass(mIdMap, &mScene, draws, palette); + } + + // The id pass under `projection` with the identity modelview: the HUD's shape, a projection and no scene. + std::vector projectedIds(const LLMatrix4a& projection, const std::vector& draws) + { + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.loadMatrix(projection); + gGL.matrixMode(LLRender::MM_MODELVIEW); + idPass(mIdMap, nullptr, draws); + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.loadMatrix(mProjection); + gGL.matrixMode(LLRender::MM_MODELVIEW); + return read(mIdMap); + } + + std::vector pixelIds(const std::vector& draws) + { + return projectedIds(pixelProjection(), draws); + } + + std::vector read(LLRenderTarget& target) + { + target.bindTarget(); + std::vector px = ll_test::readFramebufferRGBA((S32)target.getWidth(), (S32)target.getHeight()); + target.flush(); + return px; + } + + void palette(const std::vector>& colours, bool show_hidden) + { + U32 count = 0; + for (const auto& [id, colour] : colours) + { + count = llmax(count, id); + } + mRows = Outline::paletteRows(count); + mPaletteTexels.assign((size_t)Outline::PALETTE_WIDTH * mRows * 4, 0); + for (const auto& [id, colour] : colours) + { + Outline::writePalette(mPaletteTexels, id, colour, show_hidden); + } + } + + // The lines' widths at `ui_scale`, as the world's view has them with a contour TEST_WIDTH wide, which the + // cases' pixel positions are laid out for, through the projection the last id pass drew with. + Outline::View view(F32 ui_scale = 1.f) const + { + return Outline::makeView(identity(), mIdProjection, TEST_WIDTH, ui_scale, false); + } + + // Lines `width` and `inner_width` pixels wide whatever the UI scale, through the last id pass's projection. + Outline::View widths(S32 width, S32 inner_width) const + { + Outline::View lines = Outline::makeView(identity(), mIdProjection, 1.f, 1.f, false); + lines.mWidth = width; + lines.mInnerWidth = inner_width; + return lines; + } + + // view()'s, with a glow reaching `radius` pixels at `brightness`. + Outline::View glowView(S32 radius, F32 brightness = Outline::DEFAULT_GLOW_BRIGHTNESS) const + { + Outline::View lines = view(); + lines.mGlowRadius = radius; + lines.mGlowBrightness = brightness; + return lines; + } + + // The jump and tile passes as ALSelectionOutline::render runs them, over the texels under `rect`, or the + // whole id target, through `lines`' projection; the tile pass left out when `tiles` is false. + void markPasses(const Outline::View& lines, const LLRect* rect = nullptr, bool tiles = true) + { + const LLRect under = rect ? *rect : LLRect(0, H, W, 0); + mJumpProgram.bind(); + Outline::drawJumps(mJumpProgram, mIdMap, mJumps, under, lines, *mTriangle); + if (tiles) + { + mTileProgram.bind(); + Outline::drawTiles(mTileProgram, mIdMap, mJumps, mTiles, under, *mTriangle); + } + LLGLSLShader::unbind(); + } + + // What the tile pass recorded, four 16-bit values a tile: the lowest and highest id, the jump and the glow. + std::vector tileRecords() + { + const S32 columns = (S32)Outline::tileCount(W); + const S32 rows = (S32)Outline::tileCount(H); + std::vector records((size_t)columns * rows * 4); + mTiles.bindTarget(); + glReadPixels(0, 0, columns, rows, GL_RGBA, GL_UNSIGNED_SHORT, records.data()); + mTiles.flush(); + return records; + } + + // Whether the jump pass marked the texel at (x, y) as the near side of a jump: its one channel, read back as red. + bool markedAt(S32 x, S32 y) + { + return at(read(mJumps), x, y).r >= 128; + } + + // The edge pass as ALSelectionOutline::render runs it, the jump and tile passes first, over the texels under + // `mark_rect` or all of them, and with `glow` the glow reach and row passes, then over a cleared frame, + // scissored when asked, drawing the glow with `glow`. `untiled` is the full search: every tile holds every id, + // a jump and a glowing surface, and the glow is searched for in the disk (GLOW_DISK), so every pixel searches + // its whole disk as far as the lines and the glow reach. + std::vector edgePass(const Outline::View& lines, const LLRect* scissor = nullptr, bool untiled = false, + const LLRect* mark_rect = nullptr, bool glow = false) + { + Outline::uploadPalette(mPalette, mPaletteRows, mPaletteTexels, mRows); + markPasses(lines, mark_rect, !untiled); + LLRenderTarget& tiles = untiled ? mAllTiles : mTiles; + if (glow) + { + const LLRect under = mark_rect ? *mark_rect : LLRect(0, H, W, 0); + mGlowReachProgram.bind(); + Outline::drawGlowReach(mGlowReachProgram, tiles, mGlowReach, under, lines, *mTriangle); + mGlowRowProgram.bind(); + Outline::drawGlowRows(mGlowRowProgram, mIdMap, mGlowReach, mGlowRows, under, lines, *mTriangle); + LLGLSLShader::unbind(); + } + + mFrame.bindTarget(); + std::vector px; + { + LLGLSColorMask mask(true, true); + gGL.setClearColor(LLColor4(0.f, 0.f, 0.f, 0.f)); + mFrame.clear(); + + LLGLDepthTest depth(GL_FALSE); + LLGLEnable blend(GL_BLEND); + gGL.setSceneBlendType(LLRender::BT_ALPHA); + + LLGLSLShader& program = untiled ? mFullSearchProgram : mOutlineProgram; + program.bind(); + LLRenderTarget* glow_reach = glow ? &mGlowReach : nullptr; + LLRenderTarget* glow_rows = glow ? &mGlowRows : nullptr; + if (scissor) + { + LLGLSScissor scissored(scissor->mLeft, scissor->mBottom, scissor->getWidth(), scissor->getHeight()); + Outline::drawEdges(program, mIdMap, mJumps, tiles, glow_reach, glow_rows, mPalette, lines, *mTriangle); + } + else + { + Outline::drawEdges(program, mIdMap, mJumps, tiles, glow_reach, glow_rows, mPalette, lines, *mTriangle); + } + LLGLSLShader::unbind(); + px = ll_test::readFramebufferRGBA(W, H); + } + mFrame.flush(); + return px; + } + + std::vector edgePass(const LLRect* scissor = nullptr) + { + return edgePass(view(), scissor); + } + + // The window's depth: the far value, and the scene's from `scene` over it, point-sampled as the final blit + // copies it. + void windowDepth(LLRenderTarget* scene) + { + mWindow.bindTarget(); + { + LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_ALWAYS); + mWindow.clear(GL_DEPTH_BUFFER_BIT); + if (scene) + { + LLGLSColorMask mask(false, false); + mCopyDepthProgram.bind(); + mCopyDepthProgram.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, scene); + mTriangle->setBuffer(); + mTriangle->drawArrays(LLRender::TRIANGLES, 0, 3); + mCopyDepthProgram.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + LLGLSLShader::unbind(); + } + } + mWindow.flush(); + } + + // The wireframe as ALSelectionOutline::drawWireframes draws it, over the window stand-in, its colour cleared + // and its depth kept: `passes` in turn, through `projection`, the window's depth `resample` window pixels to + // the scene's texel, lines `wire_width` pixels wide. A rigged draw takes `joints` as idPass does. + std::vector wireframe(const std::vector& passes, const LLMatrix4a& projection, + const std::vector& draws, S32 wire_width = 1, F32 resample = 1.f, + const F32* joints = nullptr) + { + Outline::uploadPalette(mPalette, mPaletteRows, mPaletteTexels, mRows); + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.loadMatrix(projection); + gGL.matrixMode(LLRender::MM_MODELVIEW); + + bool texture_read = true; + std::vector px; + mWindow.bindTarget(); + { + { + LLGLSColorMask mask(true, true); + LLGLDisable scissor(GL_SCISSOR_TEST); + gGL.setClearColor(LLColor4(0.f, 0.f, 0.f, 0.f)); + mWindow.clear(GL_COLOR_BUFFER_BIT); + } + for (Outline::EWirePass pass : passes) + { + Outline::WireState state(pass, Outline::wireOffsetFactor(resample)); + LLGLSLShader& program = joints ? *mWireProgram.mRiggedVariant : mWireProgram; + mWireProgram.bind(joints != nullptr); + Outline::bindWirePass(program, pass, wire_width, W, H, mPalette, &mIdMap); + texture_read = drawAll(program, draws, joints, [pass](const Draw& draw) + { return Outline::inWirePass(draw.parts(), draw.mShowHidden, pass); }) && + texture_read; + program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_PALETTE); + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + LLGLSLShader::unbind(); + } + px = ll_test::readFramebufferRGBA(W, H); + } + mWindow.flush(); + + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.loadMatrix(mProjection); + gGL.matrixMode(LLRender::MM_MODELVIEW); + ensure("the wireframe program reads the face's texture", texture_read); + return px; + } + + // The world's two passes, visible lines then hidden ones. + std::vector worldWireframe(const std::vector& draws, S32 wire_width = 1, F32 resample = 1.f, + const F32* joints = nullptr) + { + return wireframe({ Outline::WIRE_VISIBLE, Outline::WIRE_HIDDEN }, mProjection, draws, wire_width, resample, joints); + } + + LLRect boxRect(S32 reach) const + { + const LLVector4a extents[2] = { LLVector4a(-2.f, -1.f, QUAD_Z), LLVector4a(2.f, 1.f, QUAD_Z) }; + LLRect rect; + ensure("the quads' box is on screen", Outline::scissorRect(mProjection, extents, W, H, reach, rect)); + return rect; + } + + ll_test::TestShaderMgr mShaders; + LLGLSLShader mIdProgram; + LLGLSLShader mJumpProgram; + LLGLSLShader mWireProgram; + LLGLSLShader mCopyDepthProgram; + LLGLSLShader mTileProgram; + LLGLSLShader mOutlineProgram; + LLGLSLShader mFullSearchProgram; + LLGLSLShader mGlowReachProgram; + LLGLSLShader mGlowRowProgram; + LLRenderTarget mIdMap; + // The jump pass's marks, one channel. + LLRenderTarget mJumps; + // The glow reach and row passes' records. + LLRenderTarget mGlowReach; + LLRenderTarget mGlowRows; + // The window's framebuffer: what the wireframe draws over, its depth the scene's. + LLRenderTarget mWindow; + LLRenderTarget mTiles; + LLRenderTarget mAllTiles; + LLRenderTarget mScene; + LLRenderTarget mFar; + LLRenderTarget mFrame; + U32 mPalette = 0; + U32 mPaletteRows = 0; + U32 mRows = 0; + U32 mRamp = 0; + U32 mClear = 0; + U32 mGlass = 0; + std::vector mPaletteTexels; + LLPointer mTriangle; + std::vector> mBuffers; + LLMatrix4a mProjection; + // The projection the last id pass drew with, which the edge pass reads its depth through. + LLMatrix4a mIdProjection; + bool mReverse = false; + }; + + typedef test_group alselectionoutline_t; + typedef alselectionoutline_t::object alselectionoutline_object_t; + tut::alselectionoutline_t alselectionoutline_testcase("ALSelectionOutline"); + + // Two objects side by side: each is outlined around its outer contour in its own colour, a solid line two + // pixels wide at a UI scale of 1. The edge between them is drawn once, thin and faint, on the side of the one + // given the lower id. Their insides and the space past the line are left alone. + template<> template<> + void alselectionoutline_object_t::test<1>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + idPass({ { quadA(), ID_A }, { quadB(), ID_B } }); + + const std::vector ids = read(mIdMap); + ensure_equals(named(reverse, "A's id"), idAt(ids, 20, 32), ID_A); + ensure_equals(named(reverse, "B's id, past one byte"), idAt(ids, 40, 32), ID_B); + ensure_equals(named(reverse, "nothing selected outside"), idAt(ids, 2, 2), 0U); + ensure_equals(named(reverse, "the priority in alpha"), (U32)at(ids, 20, 32).a, (U32)Outline::PRIORITY_ROOT); + + palette({ { ID_A, RED }, { ID_B, GREEN } }, true); + const std::vector px = edgePass(); + + ensure_equals(named(reverse, "A outlined at full strength beside its contour"), (U32)at(px, 8, 32).r, 255U); + ensure_equals(named(reverse, "and a pixel further"), (U32)at(px, 7, 32).r, 255U); + ensure(named(reverse, "and in its colour"), red(at(px, 7, 32))); + ensure(named(reverse, "nothing past the line"), black(at(px, 6, 32))); + ensure(named(reverse, "A outlined above"), red(at(px, 20, 45)) && black(at(px, 20, 46))); + ensure(named(reverse, "A outlined below"), red(at(px, 20, 18)) && black(at(px, 20, 17))); + ensure(named(reverse, "B outlined right of its contour"), green(at(px, 56, 32)) && black(at(px, 57, 32))); + ensure(named(reverse, "B outlined above"), green(at(px, 40, 45))); + ensure(named(reverse, "nothing far away"), black(at(px, 2, 2))); + + const Pixel edge = at(px, 31, 32); + ensure(named(reverse, "the shared edge on A's side, in A's colour"), red(edge)); + ensure(named(reverse, "fainter than the contour"), edge.r >= 150 && edge.r <= 156); + ensure(named(reverse, "and not on B's"), black(at(px, 32, 32))); + ensure(named(reverse, "A's inside left alone"), black(at(px, 20, 32))); + ensure(named(reverse, "B's inside left alone"), black(at(px, 44, 32))); + } + } + + // The same two quads under one id, as a mask without ids would see them: the contour stays and the edge + // between them is gone. This is what the ids buy. + template<> template<> + void alselectionoutline_object_t::test<2>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + idPass({ { quadA(), ID_A }, { quadB(), ID_A } }); + palette({ { ID_A, RED } }, true); + const std::vector px = edgePass(); + + ensure(named(reverse, "contour still outlined"), red(at(px, 8, 32))); + ensure(named(reverse, "contour still outlined on the far side"), red(at(px, 55, 32))); + ensure(named(reverse, "no edge between them on one side"), black(at(px, 31, 32))); + ensure(named(reverse, "no edge between them on the other"), black(at(px, 32, 32))); + } + } + + // Behind something nearer in the scene's depth, the surface is marked hidden and its outline drawn dimmed; + // with hidden parts off, not at all. Where nothing covers it, the outline is unchanged. + template<> template<> + void alselectionoutline_object_t::test<3>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + + // An occluder three metres out over columns 0 to 19, which covers A's left part. + mScene.bindTarget(); + { + LLGLDepthTest depth(GL_TRUE, GL_TRUE); + LLGLSScissor scissor(0, 0, 20, H); + glClearDepth(storedDepth(3.f)); + mScene.clear(GL_DEPTH_BUFFER_BIT); + glClearDepth(mReverse ? 0.0 : 1.0); + } + mScene.flush(); + + idPass({ { quadA(), ID_A } }); + std::vector ids = read(mIdMap); + ensure(named(reverse, "covered part marked hidden and drawn"), drawnHidden(at(ids, 12, 32))); + ensure_equals(named(reverse, "uncovered part marked visible"), (U32)at(ids, 25, 32).b, 255U); + ensure_equals(named(reverse, "hidden parts keep their id"), idAt(ids, 12, 32), ID_A); + + palette({ { ID_A, RED } }, true); + std::vector px = edgePass(); + const U32 hidden = at(px, 8, 32).r; + const U32 visible = at(px, 25, 44).r; + ensure_equals(named(reverse, "visible outline at full strength"), visible, 255U); + ensure(named(reverse, "hidden outline dimmed to the hidden alpha"), hidden >= 90 && hidden <= 115); + + Draw left_out = { quadA(), ID_A }; + left_out.mShowHidden = false; + idPass({ left_out }); + ids = read(mIdMap); + ensure_equals(named(reverse, "covered part marked not drawn"), (U32)at(ids, 12, 32).b, 0U); + ensure_equals(named(reverse, "uncovered part still visible"), (U32)at(ids, 25, 32).b, 255U); + palette({ { ID_A, RED } }, false); + px = edgePass(); + ensure(named(reverse, "hidden outline left out"), black(at(px, 8, 32))); + ensure_equals(named(reverse, "visible outline kept"), (U32)at(px, 25, 44).r, 255U); + } + } + + // A surface that is itself in the scene is visible, at the window's resolution and at half of it, where the + // scene's depth is read by uv from texels twice the size and, on a surface turned away, nearer than the + // fragment by up to half a texel of its slope. + template<> template<> + void alselectionoutline_object_t::test<4>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + + for (S32 divisor : { 1, 2 }) + { + sceneAt(W / divisor, H / divisor); + // Turned about 60 degrees from the eye: five metres deep across its width. + LLVertexBuffer* tilted = quad(-1.5f, 1.5f, -1.f, 1.f, -3.5f, -8.5f); + + // The scene draws it into its own depth, at its own resolution. + idPass(mScene, &mFar, { { tilted, ID_A } }); + idPass({ { tilted, ID_A } }); + + const std::vector ids = read(mIdMap); + size_t covered = 0; + size_t hidden = 0; + for (S32 y = 0; y < H; ++y) + { + for (S32 x = 0; x < W; ++x) + { + if (idAt(ids, x, y) == ID_A) + { + ++covered; + hidden += at(ids, x, y).b != 255 ? 1 : 0; + } + } + } + const std::string at_divisor = " at render resolution / " + std::to_string(divisor); + ensure(named(reverse, "the surface covers pixels" + at_divisor), covered > 200); + ensure_equals(named(reverse, "none of its own pixels hidden by itself" + at_divisor), hidden, size_t(0)); + } + } + } + + // The scissor is the objects' projected box grown by the contour's reach: the scissored edge pass draws exactly + // what the unscissored one does. Without the growth the line outside the box is clipped. + template<> template<> + void alselectionoutline_object_t::test<5>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + idPass({ { quadA(), ID_A }, { quadB(), ID_B } }); + palette({ { ID_A, RED }, { ID_B, GREEN } }, true); + + const std::vector full = edgePass(); + const S32 reach = Outline::lineReach(view().mWidth); + ensure_equals(named(reverse, "the contour reaches its width and a pixel"), reach, 3); + const LLRect rect = boxRect(reach); + ensure_equals(named(reverse, "left: column 8.57 floored, less the reach"), rect.mLeft, 8 - reach); + ensure_equals(named(reverse, "right: column 55.43 ceiled, plus the reach"), rect.mRight, 56 + reach); + ensure_equals(named(reverse, "bottom: row 20.28 floored, less the reach"), rect.mBottom, 20 - reach); + ensure_equals(named(reverse, "top: row 43.72 ceiled, plus the reach"), rect.mTop, 44 + reach); + + const std::vector scissored = edgePass(&rect); + ensure(named(reverse, "something is outlined"), lit(full) > 0); + ensure(named(reverse, "the scissored pass draws every outline pixel"), scissored == full); + + const LLRect tight = boxRect(0); + const std::vector clipped = edgePass(&tight); + ensure(named(reverse, "a box without the reach clips the line"), lit(clipped) < lit(full)); + ensure(named(reverse, "the line's outer pixels are what it loses"), !black(at(full, 7, 32)) && black(at(clipped, 7, 32))); + } + } + + // Where the scissor gives up: a box reaching behind the eye takes the whole target, and one wholly behind it + // touches nothing. + template<> template<> + void alselectionoutline_object_t::test<6>() + { + const LLMatrix4a proj = al_perspective(FOV, 1.f, NEAR_PLANE, FORWARD_FAR); + LLRect rect; + + const LLVector4a straddling[2] = { LLVector4a(-1.f, -1.f, -5.f), LLVector4a(1.f, 1.f, 2.f) }; + ensure("a box reaching behind the eye is drawn", Outline::scissorRect(proj, straddling, W, H, 3, rect)); + ensure("over the whole target", rect == LLRect(0, H, W, 0)); + + const LLVector4a behind[2] = { LLVector4a(-1.f, -1.f, 2.f), LLVector4a(1.f, 1.f, 5.f) }; + ensure("a box behind the eye is not", !Outline::scissorRect(proj, behind, W, H, 3, rect)); + + const LLVector4a aside[2] = { LLVector4a(30.f, -1.f, -5.f), LLVector4a(32.f, 1.f, -5.f) }; + ensure("a box off to the side is not", !Outline::scissorRect(proj, aside, W, H, 3, rect)); + } + + // A rigged object's id lands where its joint palette puts it, not at its bind pose, and moves with the pose. + template<> template<> + void alselectionoutline_object_t::test<7>() + { + // Joint 0 moved 1.5 m along +x, then along -x. + const F32 right[12] = { 1.f, 0.f, 0.f, 1.5f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f }; + const F32 left[12] = { 1.f, 0.f, 0.f, -1.5f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f }; + + for (bool reverse : conventions()) + { + setUp(reverse); + LLVertexBuffer* bound = quad(-0.5f, 0.5f, -0.5f, 0.5f, QUAD_Z, QUAD_Z, true); + + idPass({ { bound, ID_A } }, right); + std::vector ids = read(mIdMap); + ensure_equals(named(reverse, "posed right"), idAt(ids, 49, 32), ID_A); + ensure_equals(named(reverse, "not at the bind pose"), idAt(ids, 32, 32), 0U); + ensure_equals(named(reverse, "not posed left"), idAt(ids, 14, 32), 0U); + + idPass({ { bound, ID_A } }, left); + ids = read(mIdMap); + ensure_equals(named(reverse, "posed left"), idAt(ids, 14, 32), ID_A); + ensure_equals(named(reverse, "gone from the right"), idAt(ids, 49, 32), 0U); + + // Posed, it covers columns 9 to 20 and rows 26 to 37. + palette({ { ID_A, RED } }, true); + const std::vector px = edgePass(); + ensure(named(reverse, "outlined above where it is posed"), red(at(px, 14, 39))); + ensure(named(reverse, "outlined below where it is posed"), red(at(px, 14, 24))); + ensure(named(reverse, "not where it is not"), black(at(px, 49, 39)) && black(at(px, 49, 24))); + } + } + + // The palette: an id's visible colour above its hidden one, PALETTE_WIDTH ids to a pair of rows; the hidden + // one dimmed, or transparent with hidden parts off. + template<> template<> + void alselectionoutline_object_t::test<8>() + { + ensure_equals("one pair of rows up to 255", Outline::paletteRows(255), 2U); + ensure_equals("two from 256", Outline::paletteRows(256), 4U); + + std::vector texels((size_t)Outline::PALETTE_WIDTH * 4 * 4, 0); + Outline::writePalette(texels, 300, LLColor4(1.f, 0.5f, 0.f, 0.8f), true); + const size_t visible = ((size_t)2 * Outline::PALETTE_WIDTH + 44) * 4; + const size_t hidden = ((size_t)3 * Outline::PALETTE_WIDTH + 44) * 4; + ensure_equals("visible red", (U32)texels[visible], 255U); + ensure_equals("visible green", (U32)texels[visible + 1], 128U); + ensure_equals("visible alpha", (U32)texels[visible + 3], 204U); + ensure_equals("hidden keeps the colour", (U32)texels[hidden + 1], 128U); + ensure_equals("hidden alpha dimmed", (U32)texels[hidden + 3], (U32)ll_round(0.8f * Outline::HIDDEN_ALPHA * 255.f)); + + Outline::writePalette(texels, 300, LLColor4(1.f, 0.5f, 0.f, 0.8f), false); + ensure_equals("hidden transparent with hidden parts off", (U32)texels[hidden + 3], 0U); + } + + // The widths: the contour AlchemySelectionOutlineWidth wide, held to 1 to 16, and the edges between objects a + // pixel, at a UI scale of 1; both scaled with it and rounded, never under a pixel; the world and the HUD alike. + template<> template<> + void alselectionoutline_object_t::test<9>() + { + ensure_equals("the setting's default", Outline::contourWidth(Outline::DEFAULT_CONTOUR_WIDTH, 1.f), 2); + ensure_equals("the default at 1.5", Outline::contourWidth(Outline::DEFAULT_CONTOUR_WIDTH, 1.5f), 3); + ensure_equals("the default at 2", Outline::contourWidth(Outline::DEFAULT_CONTOUR_WIDTH, 2.f), 4); + ensure_equals("a setting between pixels rounds", Outline::contourWidth(2.5f, 1.f), 3); + ensure_equals("a setting under 1 held to 1", Outline::contourWidth(0.25f, 1.f), 1); + ensure_equals("a setting over 16 held to 16", Outline::contourWidth(40.f, 1.f), 16); + ensure_equals("and scaled after", Outline::contourWidth(40.f, 2.f), 32); + ensure_equals("a setting that is not a number held to 1", Outline::contourWidth(std::numeric_limits::quiet_NaN(), 1.f), 1); + ensure_equals("never under a pixel", Outline::contourWidth(1.f, 0.2f), 1); + ensure_equals("edges at 1", Outline::lineWidth(Outline::INNER_WIDTH, 1.f), 1); + ensure_equals("edges at 2", Outline::lineWidth(Outline::INNER_WIDTH, 2.f), 2); + ensure_equals("a line reaches its width and a pixel", Outline::lineReach(4), 5); + + const LLMatrix4a hud = al_ortho(-0.5f, 0.5f, -0.5f, 0.5f, 0.f, 10.f); + const LLMatrix4a world = al_perspective(FOV, 1.f, NEAR_PLANE, FORWARD_FAR); + for (F32 setting : { 1.f, Outline::DEFAULT_CONTOUR_WIDTH, 16.f, 40.f }) + { + for (F32 ui_scale : { 1.f, 1.5f, 2.f }) + { + const Outline::View in_world = Outline::makeView(identity(), world, setting, ui_scale, false); + const Outline::View on_hud = Outline::makeView(identity(), hud, setting, ui_scale, true); + const std::string with = " for " + std::to_string(setting) + " at UI scale " + std::to_string(ui_scale); + ensure_equals("the contour" + with, in_world.mWidth, Outline::contourWidth(setting, ui_scale)); + ensure_equals("the edges" + with, in_world.mInnerWidth, Outline::lineWidth(Outline::INNER_WIDTH, ui_scale)); + ensure_equals("the HUD's contour as the world's" + with, on_hud.mWidth, in_world.mWidth); + ensure_equals("the HUD's edges as the world's" + with, on_hud.mInnerWidth, in_world.mInnerWidth); + ensure("the HUD's view is the HUD's" + with, on_hud.mHUD && !in_world.mHUD); + } + } + } + + // On the HUD the scene's depth is not read: a surface behind something nearer, in a depth bound where the + // scene's would be, still counts as visible, and its outline is drawn at full strength with hidden parts off. + template<> template<> + void alselectionoutline_object_t::test<10>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + + // An occluder three metres out over the whole view, in front of A. + mScene.bindTarget(); + { + LLGLDepthTest depth(GL_TRUE, GL_TRUE); + glClearDepth(storedDepth(3.f)); + mScene.clear(GL_DEPTH_BUFFER_BIT); + glClearDepth(mReverse ? 0.0 : 1.0); + } + mScene.flush(); + + idPass({ { quadA(), ID_A } }); + ensure(named(reverse, "the world's pass hides A behind it"), drawnHidden(at(read(mIdMap), 20, 32))); + + idPass(mIdMap, nullptr, { { quadA(), ID_A } }, nullptr, &mScene); + const std::vector ids = read(mIdMap); + size_t covered = 0; + size_t hidden = 0; + for (S32 y = 0; y < H; ++y) + { + for (S32 x = 0; x < W; ++x) + { + if (idAt(ids, x, y) == ID_A) + { + ++covered; + hidden += at(ids, x, y).b == 0 ? 1 : 0; + } + } + } + ensure(named(reverse, "the HUD's pass draws A"), covered > 200); + ensure_equals(named(reverse, "and hides none of it"), hidden, size_t(0)); + + palette({ { ID_A, RED } }, false); + const std::vector px = edgePass(); + ensure_equals(named(reverse, "outlined at full strength"), (U32)at(px, 8, 32).r, 255U); + } + } + + // The HUD's projections keep the nearest surface in the id target's own depth, whichever is drawn first, under + // both conventions: al_ortho (gGL.ortho) reverses its depth with reverse-Z, and so does the flattened depth row + // get_hud_matrices gives the HUD. An orthographic projection left unreversed under reverse-Z keeps the farthest + // instead, which is what the reversal is for. + template<> template<> + void alselectionoutline_object_t::test<11>() + { + constexpr F32 HUD_DEPTH = 10.f; + constexpr U32 ID_NEAR = 1; + constexpr U32 ID_FAR = 2; + + for (bool reverse : conventions()) + { + setUp(reverse); + // Columns and rows 19 to 44, and 6 to 57: the farther shows only around the nearer. + LLVertexBuffer* nearer = quad(-0.2f, 0.2f, -0.2f, 0.2f, -2.f, -2.f); + LLVertexBuffer* farther = quad(-0.4f, 0.4f, -0.4f, 0.4f, -6.f, -6.f); + + const LLMatrix4a ortho = al_ortho(-0.5f, 0.5f, -0.5f, 0.5f, 0.f, HUD_DEPTH); + LLMatrix4a flattened = ortho; + flattened.mMatrix[2].getF32ptr()[2] = reverse ? 0.005f : -0.01f; + + const LLMatrix4a* projections[] = { &ortho, &flattened }; + for (const LLMatrix4a* projection : projections) + { + const std::string which = projection == &ortho ? "al_ortho" : "the HUD's flattened row"; + for (bool near_first : { true, false }) + { + const std::vector ids = near_first + ? projectedIds(*projection, { { nearer, ID_NEAR }, { farther, ID_FAR } }) + : projectedIds(*projection, { { farther, ID_FAR }, { nearer, ID_NEAR } }); + const std::string order = near_first ? ", nearer drawn first" : ", farther drawn first"; + ensure_equals(named(reverse, which + ": the nearer kept" + order), idAt(ids, 32, 32), ID_NEAR); + ensure_equals(named(reverse, which + ": the farther around it" + order), idAt(ids, 10, 32), ID_FAR); + } + } + + if (reverse) + { + const LLMatrix4a unreversed = LLMatrix4a::orthoZO(-0.5f, 0.5f, -0.5f, 0.5f, 0.f, HUD_DEPTH); + const std::vector ids = projectedIds(unreversed, { { farther, ID_FAR }, { nearer, ID_NEAR } }); + ensure_equals(named(reverse, "unreversed, the farther wins"), idAt(ids, 32, 32), ID_FAR); + } + } + } + + // The contour is drawn as wide as the setting and the UI scale make it, held to the setting's range, and as many + // pixels wide in the world, through a perspective with the scene's depth read, as on the HUD, through an + // orthographic projection with none: its width is in pixels. + template<> template<> + void alselectionoutline_object_t::test<12>() + { + const std::pair settings[] = { { Outline::DEFAULT_CONTOUR_WIDTH, 1.f }, { Outline::DEFAULT_CONTOUR_WIDTH, 2.f }, + { 4.f, 1.f }, { 1.f, 1.f }, { 2.5f, 1.f }, { 0.25f, 1.f }, { 40.f, 1.f } }; + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED } }, true); + for (const auto& [setting, ui_scale] : settings) + { + const std::string with = " for " + std::to_string(setting) + " at UI scale " + std::to_string((S32)ui_scale); + const S32 width = Outline::contourWidth(setting, ui_scale); + + // The HUD's: its left edge is column 20. + pixelIds({ { pixels(20, 52, 16, 48), ID_A } }); + std::vector px = edgePass(Outline::makeView(identity(), pixelProjection(), setting, ui_scale, true)); + S32 hud_band = 0; + for (S32 x = 19; x >= 0 && at(px, x, 32).r == 255; --x) + { + ++hud_band; + } + ensure_equals(named(reverse, "the HUD's contour is the setting's width" + with), hud_band, width); + ensure(named(reverse, "nothing past it" + with), black(at(px, 19 - width - 1, 32))); + + // The world's: A's left edge is column 9, room for a line eight pixels wide. + if (width <= 8) + { + idPass({ { quadA(), ID_A } }); + px = edgePass(Outline::makeView(identity(), mProjection, setting, ui_scale, false)); + S32 world_band = 0; + for (S32 x = 8; x >= 0 && at(px, x, 32).r == 255; --x) + { + ++world_band; + } + ensure_equals(named(reverse, "the world's is the same" + with), world_band, hud_band); + } + } + } + } + + // A face's texture alpha cuts it out of the id target: a mask at its cutoff, a blend at BLEND_ALPHA_CUTOFF, + // through the base colour's transform and the face's texture animation; an opaque face is not cut at all, and + // the face's colour, transparent or not, never is. The ramp's alpha is its column, and pixel column c samples + // column 4c + 2 of it. + template<> template<> + void alselectionoutline_object_t::test<13>() + { + ensure_equals("a mask at its cutoff", Outline::legacyAlphaCutoff(true, LLMaterial::DIFFUSE_ALPHA_MODE_MASK, 64, true), 64.f / 255.f); + ensure_equals("a blend at the blend cutoff", Outline::legacyAlphaCutoff(true, LLMaterial::DIFFUSE_ALPHA_MODE_BLEND, 64, true), Outline::BLEND_ALPHA_CUTOFF); + ensure_equals("no material blends", Outline::legacyAlphaCutoff(false, 0, 0, true), Outline::BLEND_ALPHA_CUTOFF); + ensure_equals("opaque", Outline::legacyAlphaCutoff(true, LLMaterial::DIFFUSE_ALPHA_MODE_NONE, 64, true), Outline::NO_ALPHA_TEST); + ensure_equals("emissive", Outline::legacyAlphaCutoff(true, LLMaterial::DIFFUSE_ALPHA_MODE_EMISSIVE, 64, true), Outline::NO_ALPHA_TEST); + ensure_equals("a texture without alpha", Outline::legacyAlphaCutoff(true, LLMaterial::DIFFUSE_ALPHA_MODE_MASK, 64, false), Outline::NO_ALPHA_TEST); + ensure_equals("GLTF mask", Outline::gltfAlphaCutoff(LLGLTFMaterial::ALPHA_MODE_MASK, 0.25f), 0.25f); + ensure_equals("GLTF blend", Outline::gltfAlphaCutoff(LLGLTFMaterial::ALPHA_MODE_BLEND, 0.25f), Outline::BLEND_ALPHA_CUTOFF); + ensure_equals("GLTF opaque", Outline::gltfAlphaCutoff(LLGLTFMaterial::ALPHA_MODE_OPAQUE, 0.25f), Outline::NO_ALPHA_TEST); + + // Halved along u, as a GLTF transform and as a texture animation: column c samples 2c + 1. The transform is + // packed as LLGLTFMaterial::TextureTransform::getPacked packs a scale of (0.5, 1). + const LLGLTFMaterial::TextureTransform::Pack halved_pack = { 0.5f, 1.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f }; + LLMatrix4a halved_matrix; + halved_matrix.setIdentity(); + halved_matrix.mMatrix[0].getF32ptr()[0] = 0.5f; + + // A transparent face colour, as a prim made invisible has. + const LLColor4U invisible(255, 255, 255, 0); + + for (bool reverse : conventions()) + { + setUp(reverse); + const U32 texture = ramp(); + const F32 mask = Outline::legacyAlphaCutoff(true, LLMaterial::DIFFUSE_ALPHA_MODE_MASK, 64, true); + const F32 blend = Outline::legacyAlphaCutoff(true, LLMaterial::DIFFUSE_ALPHA_MODE_BLEND, 0, true); + const F32 opaque = Outline::legacyAlphaCutoff(true, LLMaterial::DIFFUSE_ALPHA_MODE_NONE, 64, true); + + // Column 15 samples alpha 62, column 16 alpha 66. + std::vector ids = pixelIds({ { pixels(0, W, 16, 48), ID_A, Outline::PRIORITY_ROOT, mask, texture } }); + ensure_equals(named(reverse, "a mask cuts under its cutoff"), idAt(ids, 15, 32), 0U); + ensure_equals(named(reverse, "and keeps over it"), idAt(ids, 16, 32), ID_A); + ensure_equals(named(reverse, "and keeps the opaque end"), idAt(ids, 63, 32), ID_A); + + // Column 5 samples alpha 22, column 6 alpha 26: either side of a tenth. + ids = pixelIds({ { pixels(0, W, 16, 48), ID_A, Outline::PRIORITY_ROOT, blend, texture } }); + ensure_equals(named(reverse, "a blend cuts what is all but clear"), idAt(ids, 5, 32), 0U); + ensure_equals(named(reverse, "and keeps the faintest it draws"), idAt(ids, 6, 32), ID_A); + + ids = pixelIds({ { pixels(0, W, 16, 48), ID_A, Outline::PRIORITY_ROOT, opaque, texture } }); + ensure_equals(named(reverse, "an opaque face keeps its transparent texels"), idAt(ids, 0, 32), ID_A); + ensure_equals(named(reverse, "and the rest"), idAt(ids, 40, 32), ID_A); + + ids = pixelIds({ { pixels(0, W, 16, 48, &invisible), ID_A, Outline::PRIORITY_ROOT, mask, texture } }); + ensure_equals(named(reverse, "a transparent face colour cuts nothing more"), idAt(ids, 16, 32), ID_A); + ensure_equals(named(reverse, "and its texture still cuts"), idAt(ids, 15, 32), 0U); + + // Column 20 samples alpha 41 halved, 82 not; column 40 81 halved. + Draw transformed = { pixels(0, W, 16, 48), ID_A, Outline::PRIORITY_ROOT, mask, texture }; + transformed.mTransform = &halved_pack; + ids = pixelIds({ transformed }); + ensure_equals(named(reverse, "the base colour transform moves the cut"), idAt(ids, 20, 32), 0U); + ensure_equals(named(reverse, "and keeps past it"), idAt(ids, 40, 32), ID_A); + + Draw animated = { pixels(0, W, 16, 48), ID_A, Outline::PRIORITY_ROOT, mask, texture }; + animated.mTextureMatrix = &halved_matrix; + ids = pixelIds({ animated }); + ensure_equals(named(reverse, "the texture animation moves the cut"), idAt(ids, 20, 32), 0U); + ensure_equals(named(reverse, "and keeps past it"), idAt(ids, 40, 32), ID_A); + + // A cut-out face shows what is behind it: here another object, which is outlined through the hole. + ids = pixelIds({ { pixels(0, W, 16, 48), ID_A, Outline::PRIORITY_ROOT, mask, texture }, + { quad(0.f, (F32)W, 0.f, (F32)H, PIXEL_Z - 1.f, PIXEL_Z - 1.f), ID_B } }); + ensure_equals(named(reverse, "the one behind shows through the cut"), idAt(ids, 15, 32), ID_B); + ensure_equals(named(reverse, "and not through the rest"), idAt(ids, 16, 32), ID_A); + } + } + + // The contour's anti-aliasing: across a diagonal edge, walking away from it along a row, the line's coverage + // never rises, starts solid, takes values between and ends at nothing. Along a row the distance from a + // 45-degree edge grows by sqrt(1/2) a pixel a pixel, so the walk takes twice the line's reach. + template<> template<> + void alselectionoutline_object_t::test<14>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + // A diamond about (32.25, 32.25), 24 pixels to each point: its edge from (32.25, 8.25) to + // (8.25, 32.25) runs through x = 40 - y, between pixel centres, so the pixels it covers are not left to + // the rasteriser's tie rule. + const LLVector3 diamond[4] = { LLVector3(32.25f, 8.25f, PIXEL_Z), LLVector3(56.25f, 32.25f, PIXEL_Z), + LLVector3(32.25f, 56.25f, PIXEL_Z), LLVector3(8.25f, 32.25f, PIXEL_Z) }; + const std::vector ids = pixelIds({ { quadAt(diamond), ID_A } }); + palette({ { ID_A, RED } }, true); + const Outline::View lines = view(); + const std::vector px = edgePass(lines); + const S32 walk = 2 * Outline::lineReach(lines.mWidth); + + S32 between = 0; + for (S32 y = 14; y <= 28; ++y) + { + const std::string row = ", row " + std::to_string(y); + S32 first = -1; + for (S32 x = 0; x < W && first < 0; ++x) + { + first = idAt(ids, x, y) == ID_A ? x : -1; + } + ensure_equals(named(reverse, "the diamond's edge" + row), first, 40 - y); + + U32 last = 255; + for (S32 x = first - 1; x >= first - walk; --x) + { + const U32 r = at(px, x, y).r; + ensure(named(reverse, "coverage never rises away from the edge" + row), r <= last); + between += (r > 0 && r < 255) ? 1 : 0; + last = r; + } + ensure_equals(named(reverse, "solid beside the edge" + row), (U32)at(px, first - 1, y).r, 255U); + ensure_equals(named(reverse, "nothing past the reach" + row), last, 0U); + } + ensure(named(reverse, "the edge is anti-aliased"), between >= 15); + } + } + + // A feature a pixel across is outlined all the way round: every pixel in the disk the line covers is drawn, + // including those no straight or diagonal line from the feature passes through. + template<> template<> + void alselectionoutline_object_t::test<15>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + pixelIds({ { pixels(32, 33, 32, 33), ID_A } }); + palette({ { ID_A, RED } }, true); + const Outline::View lines = view(); + const std::vector px = edgePass(lines); + + const S32 reach = Outline::lineReach(lines.mWidth); + for (S32 dy = -reach - 1; dy <= reach + 1; ++dy) + { + for (S32 dx = -reach - 1; dx <= reach + 1; ++dx) + { + if (dx == 0 && dy == 0) + { + continue; + } + const F32 dist = sqrtf((F32)(dx * dx + dy * dy)); + const U32 r = at(px, 32 + dx, 32 + dy).r; + const std::string where = " at (" + std::to_string(dx) + ", " + std::to_string(dy) + ")"; + if (dist <= (F32)lines.mWidth) + { + ensure_equals(named(reverse, "solid within the width" + where), r, 255U); + } + else if (dist < (F32)lines.mWidth + 0.9f) + { + ensure(named(reverse, "anti-aliased past it" + where), r > 0 && r < 255); + } + else if (dist >= (F32)reach) + { + ensure_equals(named(reverse, "nothing at the reach" + where), r, 0U); + } + } + } + // Two across and one up: off every line of taps in eight directions. + ensure(named(reverse, "the knight's move is outlined"), at(px, 34, 33).r > 0); + } + } + + // Priorities: ids are given the highest priority first, and in the order they were added within one. + // Where a root and a child meet, the root's contour is laid over the child's, which shows under it only where + // the root's fades; the edge between them is drawn on the root's side, a pixel wide, at INNER_OPACITY, and not + // on the child's. + template<> template<> + void alselectionoutline_object_t::test<16>() + { + const std::vector priorities = { Outline::PRIORITY_CHILD, Outline::PRIORITY_ROOT, + Outline::PRIORITY_CONTEXT, Outline::PRIORITY_SUBTRACT, + Outline::PRIORITY_CHILD, Outline::PRIORITY_FOCUS, + Outline::PRIORITY_INSPECT }; + std::vector order; + Outline::priorityOrder(priorities, order); + ensure("the highest priority first, in the order added within one", order == std::vector({ 3, 5, 6, 1, 0, 4, 2 })); + + constexpr U32 ROOT = 1; + constexpr U32 CHILD = 2; + for (bool reverse : conventions()) + { + setUp(reverse); + // The root over columns 16 to 31, the child 32 to 47, both rows 16 to 47. + const std::vector ids = pixelIds({ { pixels(16, 32, 16, 48), ROOT, Outline::PRIORITY_ROOT }, + { pixels(32, 48, 16, 48), CHILD, Outline::PRIORITY_CHILD } }); + ensure_equals(named(reverse, "the child's priority in alpha"), (U32)at(ids, 40, 32).a, (U32)Outline::PRIORITY_CHILD); + palette({ { ROOT, RED }, { CHILD, GREEN } }, true); + const std::vector px = edgePass(); + + // Above the child, a pixel from it and the root's corner a knight's move away. + const Pixel over = at(px, 33, 48); + ensure(named(reverse, "the root's contour over the child's"), over.r > over.g && over.r > 200); + ensure(named(reverse, "the child's under it where it fades"), over.g > 0); + ensure(named(reverse, "the root's alone beside it"), red(at(px, 31, 48)) && at(px, 31, 48).r == 255); + ensure(named(reverse, "the child's alone past the root's reach"), green(at(px, 36, 48)) && at(px, 36, 48).g == 255); + + const Pixel edge = at(px, 31, 32); + const U32 faint = (U32)ll_round(255.f * Outline::INNER_OPACITY); + ensure(named(reverse, "the edge on the root's side, in its colour"), red(edge)); + ensure(named(reverse, "at INNER_OPACITY"), (U32)edge.r + 2 >= faint && (U32)edge.r <= faint + 2); + ensure(named(reverse, "a pixel wide, where the contour is two"), black(at(px, 30, 32))); + ensure(named(reverse, "and not on the child's side"), black(at(px, 32, 32))); + ensure(named(reverse, "the insides left alone"), black(at(px, 20, 32)) && black(at(px, 40, 32))); + } + } + + // The tile pass's early out draws exactly what searching every pixel's disk draws: with edges along tile + // borders and across them, features a pixel thin at tile corners, objects at the target's edges, an island + // inside another, both priorities, and lines from one pixel wide to sixteen, whose reach spans five tiles. A + // change along a tile's left or lower border belongs to the tile beyond it, which flags it; a tile inside an + // object flags nothing. + template<> template<> + void alselectionoutline_object_t::test<17>() + { + const std::pair line_widths[] = { { 1, 1 }, { 2, 1 }, { 4, 1 }, { 8, 2 }, { 16, 2 } }; + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { 1, RED }, { 2, GREEN }, { 3, LLColor4(0.f, 0.f, 1.f, 1.f) }, { 4, LLColor4(1.f, 1.f, 0.f, 0.5f) } }, true); + + // A strip a pixel and a half across, running up and to the right at 45 degrees. + const LLVector3 strip[4] = { LLVector3(4.f, 30.f, PIXEL_Z), LLVector3(5.5f, 30.f, PIXEL_Z), + LLVector3(21.5f, 46.f, PIXEL_Z), LLVector3(20.f, 46.f, PIXEL_Z) }; + const std::vector>> scenes = { + { "edges on tile borders", { { pixels(16, 32, 16, 40), 1, Outline::PRIORITY_ROOT }, + { pixels(32, 48, 16, 40), 2, Outline::PRIORITY_CHILD }, + { pixels(16, 48, 40, 48), 3, Outline::PRIORITY_CHILD } } }, + { "edges across tile borders", { { pixels(13, 29, 21, 37), 1, Outline::PRIORITY_ROOT }, + { pixels(29, 45, 21, 37), 2, Outline::PRIORITY_CHILD }, + { pixels(50, 53, 50, 61), 3, Outline::PRIORITY_CHILD } } }, + { "thin features", { { pixels(23, 24, 23, 24), 1, Outline::PRIORITY_ROOT }, + { pixels(40, 41, 40, 41), 2, Outline::PRIORITY_CHILD }, + { pixels(39, 40, 8, 56), 3, Outline::PRIORITY_CHILD }, + { pixels(0, W, 7, 8), 4, Outline::PRIORITY_CHILD }, + { quadAt(strip), 1, Outline::PRIORITY_ROOT } } }, + { "objects at the target's corners", { { pixels(0, 10, 54, H), 1, Outline::PRIORITY_ROOT }, + { pixels(58, W, 0, 5), 2, Outline::PRIORITY_CHILD } } }, + { "an island", { { pixels(20, 44, 20, 44), 2, Outline::PRIORITY_CHILD }, + { pixels(28, 36, 28, 36), 1, Outline::PRIORITY_ROOT } } }, + { "slanted surfaces touching, and one in front", { { quad(8.f, 40.f, 8.f, 30.f, -1.f, -5.f), 1, Outline::PRIORITY_ROOT }, + { quad(40.f, 56.f, 8.f, 30.f, -5.f, -7.f), 2, Outline::PRIORITY_CHILD }, + { quad(20.f, 50.f, 24.f, 44.f, -0.5f, -0.5f), 3, Outline::PRIORITY_CHILD } } }, + { "one object over itself, its jumps on tile borders", { { pixels(8, 56, 8, 56), 1, Outline::PRIORITY_ROOT }, + { quad(24.f, 40.f, 24.f, 40.f, PIXEL_Z + 1.f, PIXEL_Z + 1.f), 1, Outline::PRIORITY_ROOT }, + { quad(31.f, 50.f, 13.f, 21.f, PIXEL_Z + 0.5f, PIXEL_Z + 0.5f), 1, Outline::PRIORITY_ROOT } } }, + { "fronds of one mesh over each other and another object", { { quad(4.f, 40.f, 30.f, 34.f, -3.f, -6.f), 1, Outline::PRIORITY_CHILD }, + { quad(10.f, 14.f, 4.f, 60.f, -2.f, -2.f), 1, Outline::PRIORITY_CHILD }, + { quad(20.f, 60.f, 40.f, 46.f, -1.f, -4.f), 1, Outline::PRIORITY_CHILD }, + { quad(30.f, 31.f, 0.f, 64.f, -1.5f, -1.5f), 1, Outline::PRIORITY_CHILD }, + { pixels(36, 60, 20, 60), 2, Outline::PRIORITY_ROOT } } }, + }; + + for (const auto& [name, draws] : scenes) + { + pixelIds(draws); + for (const auto& [width, inner_width] : line_widths) + { + const Outline::View lines = widths(width, inner_width); + const std::string what = name + ", lines " + std::to_string(width) + " and " + std::to_string(inner_width); + const std::vector untiled = edgePass(lines, nullptr, true); + const std::vector tiled = edgePass(lines); + ensure(named(reverse, what + ": something is drawn"), lit(untiled) > 0); + ensure(named(reverse, what + ": tiled as untiled"), tiled == untiled); + } + + const std::vector records = tileRecords(); + size_t empty = 0; + for (size_t i = 0; i < records.size(); i += 4) + { + empty += records[i + 1] == 0 ? 1 : 0; + } + ensure(named(reverse, name + ": some tiles hold nothing"), empty > 0); + } + + // The first scene again, its edges on tile borders: a tile holds the ids of its own texels, and a tile + // across the border from another object's holds none of that object's. + pixelIds(scenes[0].second); + markPasses(view()); + std::vector records = tileRecords(); + const S32 columns = (S32)Outline::tileCount(W); + auto record = [&](S32 column, S32 row, S32 i) { return (U32)records[((size_t)row * columns + column) * 4 + i]; }; + ensure(named(reverse, "the tile left of the root holds nothing"), record(1, 2, 0) == 0 && record(1, 2, 1) == 0); + ensure(named(reverse, "the tile inside the root holds the root alone"), record(2, 2, 0) == 1 && record(2, 2, 1) == 1); + ensure(named(reverse, "the tile inside the child beside it, the child alone"), record(4, 2, 0) == 2 && record(4, 2, 1) == 2); + ensure(named(reverse, "the tile under the child above, the root alone"), record(2, 4, 0) == 1 && record(2, 4, 1) == 1); + ensure(named(reverse, "no jump in it"), record(2, 2, 2) == 0); + ensure(named(reverse, "a tile far away holds nothing"), record(0, 0, 1) == 0); + + // The second, across tile borders: the tile at column and row 3 holds both the root and the child. + pixelIds(scenes[1].second); + markPasses(view()); + records = tileRecords(); + ensure(named(reverse, "a tile across the edge holds both"), record(3, 3, 0) == 1 && record(3, 3, 1) == 2); + + // The tile pass over the tiles under a rect holding every selected texel and one more around them, as the + // viewer runs it under its scissor, draws the same. + pixelIds(scenes[1].second); + const LLRect around(12, 62, 54, 20); + for (const auto& [width, inner_width] : line_widths) + { + const Outline::View lines = widths(width, inner_width); + const std::string what = "under a rect, lines " + std::to_string(width); + ensure(named(reverse, what), edgePass(lines, nullptr, false, &around) == edgePass(lines, nullptr, true)); + } + } + } + + // Faces are culled as the scene culls them: a single-sided face seen from behind, which the scene does not draw, + // takes no pixel from the object behind it, whose contour stays whole; a double-sided one is drawn, and outlined. + template<> template<> + void alselectionoutline_object_t::test<18>() + { + constexpr U32 FRONT = 1; + constexpr U32 REAR = 2; + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { FRONT, RED }, { REAR, GREEN } }, true); + // The rear over columns and rows 16 to 47; the front over 24 to 39, nearer, wound clockwise: its back. + const LLVector3 back_on[4] = { LLVector3(24.f, 24.f, PIXEL_Z + 1.f), LLVector3(24.f, 40.f, PIXEL_Z + 1.f), + LLVector3(40.f, 40.f, PIXEL_Z + 1.f), LLVector3(40.f, 24.f, PIXEL_Z + 1.f) }; + LLVertexBuffer* rear = pixels(16, 48, 16, 48); + LLVertexBuffer* front = quadAt(back_on); + + std::vector ids = pixelIds({ { rear, REAR, Outline::PRIORITY_CHILD }, { front, FRONT, Outline::PRIORITY_ROOT } }); + ensure_equals(named(reverse, "a single-sided face from behind takes no pixel"), idAt(ids, 32, 32), REAR); + std::vector px = edgePass(); + ensure(named(reverse, "nothing traces it"), black(at(px, 23, 32)) && black(at(px, 24, 32)) && black(at(px, 40, 32))); + ensure_equals(named(reverse, "the rear's contour is whole"), (U32)at(px, 15, 32).g, 255U); + + Draw double_sided = { front, FRONT, Outline::PRIORITY_ROOT }; + double_sided.mDoubleSided = true; + ids = pixelIds({ { rear, REAR, Outline::PRIORITY_CHILD }, double_sided }); + ensure_equals(named(reverse, "a double-sided face from behind is drawn"), idAt(ids, 32, 32), FRONT); + px = edgePass(); + ensure(named(reverse, "and outlined over the rear"), red(at(px, 23, 32)) && at(px, 23, 32).r == 255); + } + } + + // A visible object in front of a selected one the scene hides is outlined in full over it: its contour neither + // turns into an edge between the two nor goes on the hidden one's side. With hidden parts drawn, the visible + // object's contour is still drawn at full strength, and the hidden one's own dimmed. + template<> template<> + void alselectionoutline_object_t::test<19>() + { + constexpr U32 ROOT = 1; + constexpr U32 FROND = 2; + for (bool reverse : conventions()) + { + setUp(reverse); + // The root, six metres out over columns 12 to 51 and rows 18 to 46, behind something five metres out; + // the frond four metres out over columns and rows 25 to 38, in front of it. + occlude(5.f); + LLVertexBuffer* root = quad(-2.f, 2.f, -1.5f, 1.5f, -6.f, -6.f); + LLVertexBuffer* frond = quad(-0.5f, 0.5f, -0.5f, 0.5f, -4.f, -4.f); + + for (bool show_hidden : { false, true }) + { + const std::string hidden_parts = show_hidden ? ", hidden parts drawn" : ", hidden parts left out"; + Draw hidden_root = { root, ROOT, Outline::PRIORITY_ROOT }; + hidden_root.mShowHidden = show_hidden; + idPass({ hidden_root, { frond, FROND, Outline::PRIORITY_CHILD } }); + palette({ { ROOT, RED }, { FROND, GREEN } }, show_hidden); + const std::vector px = edgePass(); + + ensure(named(reverse, "the frond's contour over the root" + hidden_parts), + green(at(px, 24, 32)) && at(px, 24, 32).g == 255 && green(at(px, 23, 32)) && at(px, 23, 32).g == 255); + ensure(named(reverse, "above it too" + hidden_parts), green(at(px, 32, 39)) && at(px, 32, 39).g == 255); + ensure(named(reverse, "as wide as a contour" + hidden_parts), black(at(px, 21, 32))); + ensure(named(reverse, "nothing inside the frond" + hidden_parts), black(at(px, 32, 32)) && black(at(px, 25, 32))); + if (show_hidden) + { + const U32 dimmed = at(px, 11, 32).r; + ensure(named(reverse, "the hidden root's contour dimmed"), red(at(px, 11, 32)) && dimmed >= 90 && dimmed <= 115); + } + else + { + ensure(named(reverse, "the hidden root's contour left out"), black(at(px, 11, 32))); + } + } + } + } + + // A child in front of another draws its whole contour over it; two surfaces that touch on one plane, here a + // wall seen at a slant, whose depth changes by some percent a pixel, draw only the thin edge between them, on + // the side of the higher priority. + template<> template<> + void alselectionoutline_object_t::test<20>() + { + const LLColor4 BLUE(0.f, 0.f, 1.f, 1.f); + for (bool reverse : conventions()) + { + setUp(reverse); + + // Six metres out over columns 12 to 51, and four metres out over columns and rows 25 to 38. + idPass({ { quad(-2.f, 2.f, -1.5f, 1.5f, -6.f, -6.f), 2, Outline::PRIORITY_CHILD }, + { quad(-0.5f, 0.5f, -0.5f, 0.5f, -4.f, -4.f), 3, Outline::PRIORITY_CHILD } }); + palette({ { 2, GREEN }, { 3, BLUE } }, true); + std::vector px = edgePass(); + const Pixel over = at(px, 24, 32); + ensure(named(reverse, "the front child's contour over the rear"), over.b == 255 && over.r == 0 && over.g == 0); + ensure(named(reverse, "its full width"), at(px, 23, 32).b == 255 && at(px, 32, 39).b == 255); + ensure(named(reverse, "nothing inside it"), black(at(px, 32, 32))); + + // A wall from four metres out at the left to twelve at the right, split at eight, columns 31 and 32. + idPass({ { quad(-1.6f, 0.f, -1.f, 1.f, -4.f, -8.f), ID_A, Outline::PRIORITY_ROOT }, + { quad(0.f, 1.6f, -1.f, 1.f, -8.f, -12.f), ID_B, Outline::PRIORITY_CHILD } }); + palette({ { ID_A, RED }, { ID_B, GREEN } }, true); + px = edgePass(); + const Pixel edge = at(px, 31, 32); + const U32 faint = (U32)ll_round(255.f * Outline::INNER_OPACITY); + ensure(named(reverse, "the edge on the root's side"), red(edge) && (U32)edge.r + 2 >= faint && (U32)edge.r <= faint + 2); + ensure(named(reverse, "no contour on the child's side"), black(at(px, 32, 32)) && black(at(px, 33, 32))); + ensure(named(reverse, "none on the root's"), black(at(px, 30, 32))); + } + } + + // A selected object behind one the scene hides, its hidden parts left out, does not trace the hidden one's shape: + // a frond behind a rock, in front of the visible root. And an object not drawn neither draws nor masks the + // contour of one that is, though it is the nearer and of the higher priority. + template<> template<> + void alselectionoutline_object_t::test<21>() + { + constexpr U32 ROOT = 1; + constexpr U32 FROND = 2; + for (bool reverse : conventions()) + { + setUp(reverse); + // The root six metres out over columns 12 to 51; the frond five metres out over columns and rows 26 to + // 37, behind a rock four and a half metres out over exactly those. + occlude(4.5f, 26, 26, 12, 12); + Draw frond = { quad(-0.5f, 0.5f, -0.5f, 0.5f, -5.f, -5.f), FROND, Outline::PRIORITY_CHILD }; + frond.mShowHidden = false; + idPass({ { quad(-2.f, 2.f, -1.5f, 1.5f, -6.f, -6.f), ROOT, Outline::PRIORITY_ROOT }, frond }); + const std::vector ids = read(mIdMap); + ensure_equals(named(reverse, "the frond is over the root"), idAt(ids, 32, 32), FROND); + ensure_equals(named(reverse, "and not drawn"), (U32)at(ids, 32, 32).b, 0U); + palette({ { ROOT, RED }, { FROND, GREEN } }, false); + std::vector px = edgePass(); + for (const auto& [x, y] : { std::pair{ 25, 32 }, { 24, 32 }, { 38, 32 }, { 39, 32 }, { 32, 25 }, { 32, 38 }, { 31, 31 } }) + { + ensure(named(reverse, "nothing traces the frond at (" + std::to_string(x) + ", " + std::to_string(y) + ")"), black(at(px, x, y))); + } + ensure_equals(named(reverse, "the root's own contour drawn"), (U32)at(px, 11, 32).r, 255U); + + // A root five metres out over columns 9 to 31, hidden, and a child beside it over 32 to 54, visible, both + // rows 21 to 43. Above the root, a pixel from it and a knight's move from the child. + setUp(reverse); + occlude(4.f, 0, 0, 32, H); + Draw root = { quad(-2.f, 0.f, -1.f, 1.f, -5.f, -5.f), ROOT, Outline::PRIORITY_ROOT }; + root.mShowHidden = false; + idPass({ root, { quad(0.f, 2.f, -1.f, 1.f, -5.f, -5.f), FROND, Outline::PRIORITY_CHILD } }); + palette({ { ROOT, RED }, { FROND, GREEN } }, false); + px = edgePass(); + const Pixel above = at(px, 30, 44); + ensure(named(reverse, "the child's contour where the hidden root is nearer"), green(above) && above.g > 200); + + // The two touch: the edge between them goes on the drawn side, the child's, though the root's is the + // higher priority. + const Pixel edge = at(px, 32, 32); + const U32 faint = (U32)ll_round(255.f * Outline::INNER_OPACITY); + ensure(named(reverse, "the edge on the drawn side"), green(edge) && (U32)edge.g + 2 >= faint && (U32)edge.g <= faint + 2); + ensure(named(reverse, "not on the hidden root's"), black(at(px, 31, 32))); + } + } + + // One object over itself, as one frond of a mesh lies over another: the jump pass marks the near side of the + // step, and the edge pass draws the near part's whole contour over the far part from it, as another object's, + // and nothing inside the near part. A gap of three percent of the distance is a jump too. The tile holding the + // near side records it. + template<> template<> + void alselectionoutline_object_t::test<22>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED } }, true); + // The far part six metres out over columns 12 to 51; the near part over columns and rows 25 to 38 at + // four metres, and at 5.8. + for (F32 near_z : { -4.f, -5.8f }) + { + const F32 half = 0.125f * -near_z; + const std::string at_z = " at " + std::to_string(-near_z) + " m"; + idPass({ { quad(-2.f, 2.f, -1.5f, 1.5f, -6.f, -6.f), ID_A }, { quad(-half, half, -half, half, near_z, near_z), ID_A } }); + const std::vector px = edgePass(); + ensure(named(reverse, "the near side of the step marked" + at_z), markedAt(25, 32) && markedAt(32, 38)); + ensure(named(reverse, "not the far side, nor inside" + at_z), !markedAt(24, 32) && !markedAt(32, 32)); + + ensure(named(reverse, "the near part's contour over the far part" + at_z), + red(at(px, 24, 32)) && at(px, 24, 32).r == 255 && at(px, 23, 32).r == 255 && at(px, 32, 39).r == 255); + ensure(named(reverse, "as wide as a contour" + at_z), black(at(px, 21, 32))); + ensure(named(reverse, "nothing inside the near part" + at_z), black(at(px, 25, 32)) && black(at(px, 26, 32)) && black(at(px, 32, 32))); + ensure(named(reverse, "nothing over the far part away from it" + at_z), black(at(px, 15, 32))); + } + + // The near part's edges on tile borders: the tiles holding them record a jump, a tile of the far part alone + // none. + pixelIds({ { pixels(8, 56, 8, 56), ID_A }, { quad(24.f, 40.f, 24.f, 40.f, PIXEL_Z + 1.f, PIXEL_Z + 1.f), ID_A } }); + markPasses(view()); + const std::vector records = tileRecords(); + const S32 columns = (S32)Outline::tileCount(W); + ensure(named(reverse, "a tile on the near side's edge records the jump"), records[((size_t)3 * columns + 3) * 4 + 2] != 0); + ensure(named(reverse, "a tile of the far part alone none"), records[((size_t)1 * columns + 1) * 4 + 2] == 0); + } + } + + // A surface bending, not stepping, marks no jump and draws nothing across itself: one mesh's inside corner at + // 30, 45 and 60 degrees to the view, the outside and the inside of a cylinder out to their rims, and a plane + // whose depth changes by three to five percent a pixel, in one piece and in two. + template<> template<> + void alselectionoutline_object_t::test<23>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED } }, true); + + std::vector>> surfaces; + for (F32 degrees : { 30.f, 45.f, 60.f }) + { + // Two walls 2.5 m long meeting at a right angle six metres out, the corner away from the eye. + const F32 angle = degrees * DEG_TO_RAD; + surfaces.push_back({ "an inside corner at " + std::to_string((S32)degrees) + " degrees", + { { quad(-2.5f * cosf(angle), 0.f, -1.f, 1.f, -6.f + 2.5f * sinf(angle), -6.f), ID_A }, + { quad(0.f, 2.5f * sinf(angle), -1.f, 1.f, -6.f, -6.f + 2.5f * cosf(angle)), ID_A } } }); + } + // Half a cylinder of radius 1.5 m in 48 facets: its outside about an axis six metres out, and its inside + // about one four metres out. + for (bool inside : { false, true }) + { + std::vector facets; + constexpr S32 FACETS = 48; + for (S32 i = 0; i < FACETS; ++i) + { + const F32 a0 = F_PI * ((F32)i / FACETS - 0.5f); + const F32 a1 = F_PI * ((F32)(i + 1) / FACETS - 0.5f); + const F32 axis = inside ? -4.f : -6.f; + const F32 bulge = inside ? -1.5f : 1.5f; + facets.push_back({ quad(1.5f * sinf(a0), 1.5f * sinf(a1), -1.f, 1.f, axis + bulge * cosf(a0), axis + bulge * cosf(a1)), ID_A }); + } + surfaces.push_back({ inside ? "a cylinder's inside" : "a cylinder's outside", facets }); + } + surfaces.push_back({ "a slanted plane", { { quad(-1.6f, 1.6f, -1.f, 1.f, -4.f, -12.f), ID_A } } }); + surfaces.push_back({ "a slanted plane in two", { { quad(-1.6f, 0.f, -1.f, 1.f, -4.f, -8.f), ID_A }, + { quad(0.f, 1.6f, -1.f, 1.f, -8.f, -12.f), ID_A } } }); + + for (const auto& [name, draws] : surfaces) + { + idPass(draws); + const std::vector ids = read(mIdMap); + const std::vector px = edgePass(); + const std::vector marked = read(mJumps); + // Every pixel searching its disk, so what the edge pass would draw is not hidden by the tiles. + const std::vector searched = edgePass(view(), nullptr, true); + size_t covered = 0; + size_t marks = 0; + size_t drawn = 0; + for (S32 y = 0; y < H; ++y) + { + for (S32 x = 0; x < W; ++x) + { + if (idAt(ids, x, y) == ID_A) + { + ++covered; + marks += at(marked, x, y).r >= 128 ? 1 : 0; + drawn += (black(at(px, x, y)) && black(at(searched, x, y))) ? 0 : 1; + } + } + } + ensure(named(reverse, name + ": it covers pixels"), covered > 200); + ensure_equals(named(reverse, name + ": no jump marked"), marks, size_t(0)); + ensure_equals(named(reverse, name + ": nothing drawn across it"), drawn, size_t(0)); + } + } + } + + // The wireframe's lines are as wide in the window wherever they are: along an edge receding from four to twelve + // metres, on the same quad four times as far, and twice as wide at twice the width. The width across a line is + // read as the sum of its coverage down a column, the line's width over the cosine of its slope. + template<> template<> + void alselectionoutline_object_t::test<24>() + { + // The rows a column's runs of drawn pixels cover, bottom up. + // The width down column x of the quad's diagonal, the middle of the three lines a column crosses. + auto diagonal = [&](const std::vector& px, S32 x) + { + const std::vector> found = columnRuns(px, x); + ensure_equals("a column crosses the bottom edge, the diagonal and the top edge", found.size(), size_t(3)); + return ink(px, x, found[1].first, found[1].second); + }; + + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED } }, true); + windowDepth(nullptr); + + // From three metres out at its left edge to twelve at its right, columns 13 to 39; its diagonal runs + // from the near lower corner to the far upper one, about five metres out at column 26 and eight at 34. + const std::vector near_px = worldWireframe({ { quad(-1.f, 1.5f, -1.f, 1.f, -3.f, -12.f), ID_A } }); + const F32 near_end = diagonal(near_px, 26); + const F32 far_end = diagonal(near_px, 34); + // The line's slope down the screen is about 0.91: a width of 1 reads as about 1.35 down a column. + ensure(named(reverse, "the width down the diagonal, near: " + std::to_string(near_end)), near_end > 1.15f && near_end < 1.55f); + ensure(named(reverse, "the same along the receding edge, far: " + std::to_string(far_end)), fabsf(far_end - near_end) < 0.1f); + + const std::vector far_px = worldWireframe({ { quad(-4.f, 6.f, -4.f, 4.f, -12.f, -48.f), ID_A } }); + ensure(named(reverse, "the same four times as far"), fabsf(diagonal(far_px, 26) - near_end) < 0.1f && fabsf(diagonal(far_px, 34) - far_end) < 0.1f); + + const std::vector wide_px = worldWireframe({ { quad(-1.f, 1.5f, -1.f, 1.f, -3.f, -12.f), ID_A } }, 2); + const F32 narrow = diagonal(near_px, 30); + const F32 wide = diagonal(wide_px, 30); + ensure(named(reverse, "twice as wide at twice the width: " + std::to_string(wide)), wide > 1.8f * narrow && wide < 2.2f * narrow); + } + } + + // Across a line, coverage falls from its middle on either side, and is gone within a pixel and a half. + template<> template<> + void alselectionoutline_object_t::test<25>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED } }, true); + windowDepth(nullptr); + // A square from 8 to 56 each way: its diagonal, through x = y, crosses each row between them. + const std::vector px = wireframe({ Outline::WIRE_VISIBLE }, pixelProjection(), { { pixels(8, 56, 8, 56), ID_A } }); + for (S32 y = 16; y < 48; ++y) + { + const std::string row = ", row " + std::to_string(y); + ensure(named(reverse, "the line is drawn" + row), at(px, y, y).r > 127); + for (S32 step : { -1, 1 }) + { + U32 last = at(px, y, y).r; + for (S32 x = y + step; x != y + 4 * step; x += step) + { + const U32 r = at(px, x, y).r; + ensure(named(reverse, "coverage falls away from the line" + row), r <= last); + last = r; + } + ensure_equals(named(reverse, "and is gone" + row), last, 0U); + } + } + } + } + + // Lines behind something in the scene are drawn in the hidden colour with hidden selections shown, and not at + // all without; the lines in front keep their colour. + template<> template<> + void alselectionoutline_object_t::test<26>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + + // The brightest of a row's pixels between two columns. + auto brightest = [](const std::vector& px, S32 y, S32 x0, S32 x1) + { + U32 most = 0; + for (S32 x = x0; x < x1; ++x) + { + most = llmax(most, (U32)at(px, x, y).r); + } + return most; + }; + + // A's diagonal crosses row 25 at about column 14 and row 38 at about 26: first with nothing in front. + palette({ { ID_A, RED } }, true); + windowDepth(nullptr); + std::vector px = worldWireframe({ { quadA(), ID_A } }); + const U32 open = brightest(px, 25, 11, 18); + ensure(named(reverse, "the line drawn with nothing in front"), open > 127); + + // Then with something three metres out over columns 0 to 19, in front of A's left part. + occlude(3.f, 0, 0, 20, H); + windowDepth(&mScene); + px = worldWireframe({ { quadA(), ID_A } }); + const U32 hidden = brightest(px, 25, 11, 18); + ensure(named(reverse, "the line in front drawn"), brightest(px, 38, 22, 30) > 127); + ensure(named(reverse, "the line behind dimmed: " + std::to_string(hidden) + " of " + std::to_string(open)), + hidden * 100 >= open * 35 && hidden * 100 <= open * 45); + + Draw left_out = { quadA(), ID_A }; + left_out.mShowHidden = false; + palette({ { ID_A, RED } }, false); + px = worldWireframe({ left_out }); + ensure_equals(named(reverse, "the line behind left out"), brightest(px, 25, 11, 18), 0U); + ensure(named(reverse, "the line in front kept"), brightest(px, 38, 22, 30) > 127); + } + } + + // Lines on their own surface, which the scene's depth holds, are all drawn: the window's depth is the scene's + // point-sampled from half the resolution, as with RenderResolutionDivisor 2, nearer than the lines over half + // their pixels on a surface turned away, and the polygon offset pulls them in front of it under either depth + // convention. None is taken for hidden. + template<> template<> + void alselectionoutline_object_t::test<27>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED } }, true); + // Turned about 60 degrees from the eye, five metres deep across its width. + LLVertexBuffer* tilted = quad(-1.5f, 1.5f, -1.f, 1.f, -3.5f, -8.5f); + + windowDepth(nullptr); + const std::vector alone = worldWireframe({ { tilted, ID_A } }, 1, 2.f); + + sceneAt(W / 2, H / 2); + idPass(mScene, &mFar, { { tilted, ID_A } }); + windowDepth(&mScene); + const std::vector on_surface = worldWireframe({ { tilted, ID_A } }, 1, 2.f); + + size_t drawn = 0; + size_t lost = 0; + for (S32 y = 0; y < H; ++y) + { + for (S32 x = 0; x < W; ++x) + { + drawn += black(at(alone, x, y)) ? 0 : 1; + lost += at(alone, x, y).r != at(on_surface, x, y).r ? 1 : 0; + } + } + ensure(named(reverse, "lines are drawn"), drawn > 100); + ensure_equals(named(reverse, "every line pixel drawn on its own surface as on nothing"), lost, size_t(0)); + } + } + + // A face's cut-out texels draw no lines, its kept ones do; a rigged face's lines are where its joints put it; a + // face the scene culls draws none. + template<> template<> + void alselectionoutline_object_t::test<28>() + { + // Joint 0 moved 1.5 m along +x. + const F32 right[12] = { 1.f, 0.f, 0.f, 1.5f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f }; + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED } }, true); + windowDepth(nullptr); + + // The ramp's alpha masked at 64: columns up to 15 cut out, 16 on kept. The diagonal, from (0, 16) to + // (64, 48), crosses column 8 at row 20 and column 40 at row 36. + const F32 mask = Outline::legacyAlphaCutoff(true, LLMaterial::DIFFUSE_ALPHA_MODE_MASK, 64, true); + std::vector px = wireframe({ Outline::WIRE_VISIBLE }, pixelProjection(), + { { pixels(0, W, 16, 48), ID_A, Outline::PRIORITY_ROOT, mask, ramp() } }); + ensure_equals(named(reverse, "no line across the cut-out texels"), ink(px, 8, 17, 24), 0.f); + ensure(named(reverse, "nor along their edge"), black(at(px, 8, 16))); + ensure(named(reverse, "lines across the kept ones"), ink(px, 40, 33, 40) > 0.5f && !black(at(px, 40, 16))); + + // Posed, the quad covers columns 43 to 54 and rows 26 to 37. + LLVertexBuffer* bound = quad(-0.5f, 0.5f, -0.5f, 0.5f, QUAD_Z, QUAD_Z, true); + px = worldWireframe({ { bound, ID_A } }, 1, 1.f, right); + ensure(named(reverse, "the rigged face's lines where it is posed"), ink(px, 49, 26, 38) > 0.5f); + ensure(named(reverse, "not at its bind pose"), ink(px, 32, 26, 38) == 0.f); + + // A face seen from behind, wound clockwise, as the id pass culls it: no lines unless double-sided. + const LLVector3 back_on[4] = { LLVector3(16.f, 16.f, PIXEL_Z), LLVector3(16.f, 48.f, PIXEL_Z), + LLVector3(48.f, 48.f, PIXEL_Z), LLVector3(48.f, 16.f, PIXEL_Z) }; + Draw back = { quadAt(back_on), ID_A }; + px = wireframe({ Outline::WIRE_VISIBLE }, pixelProjection(), { back }); + ensure_equals(named(reverse, "no lines on a face from behind"), lit(px), size_t(0)); + back.mDoubleSided = true; + px = wireframe({ Outline::WIRE_VISIBLE }, pixelProjection(), { back }); + ensure(named(reverse, "lines on a double-sided one"), lit(px) > 50); + } + } + + // On the HUD the window's depth also holds the scene's, wherever that is the nearer, so the HUD's lines are + // tested against the id target's depth instead: drawn whatever the window's depth holds, here the nearest value, + // and hidden where they lie behind another selected surface. + template<> template<> + void alselectionoutline_object_t::test<29>() + { + constexpr U32 REAR = 1; + constexpr U32 FRONT = 2; + for (bool reverse : conventions()) + { + setUp(reverse); + // The rear from 16 to 47 each way, its diagonal through x = y; the front nearer, over columns 28 to 43 + // and rows 20 to 35, its diagonal through x = y + 8. The rear's diagonal passes behind the front at + // (32, 32), four pixels from its nearest edge. + const std::vector draws = { { pixels(16, 48, 16, 48), REAR, Outline::PRIORITY_ROOT }, + { quad(28.f, 44.f, 20.f, 36.f, PIXEL_Z + 1.f, PIXEL_Z + 1.f), FRONT, Outline::PRIORITY_CHILD } }; + pixelIds(draws); + mWindow.bindTarget(); + { + LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_ALWAYS); + glClearDepth(reverse ? 1.0 : 0.0); + mWindow.clear(GL_DEPTH_BUFFER_BIT); + glClearDepth(reverse ? 0.0 : 1.0); + } + mWindow.flush(); + + for (bool show_hidden : { false, true }) + { + const std::string hidden_parts = show_hidden ? ", hidden parts drawn" : ", hidden parts left out"; + std::vector shown = draws; + for (Draw& draw : shown) + { + draw.mShowHidden = show_hidden; + } + palette({ { REAR, RED }, { FRONT, GREEN } }, show_hidden); + const std::vector px = wireframe({ Outline::WIRE_ID_DEPTH }, pixelProjection(), shown); + ensure(named(reverse, "the rear's line in the open" + hidden_parts), red(at(px, 20, 20)) && at(px, 20, 20).r > 200); + ensure(named(reverse, "the front's line" + hidden_parts), green(at(px, 40, 32)) && at(px, 40, 32).g > 200); + if (show_hidden) + { + const U32 dimmed = at(px, 32, 32).r; + const U32 open = at(px, 20, 20).r; + ensure(named(reverse, "the rear's line behind the front dimmed: " + std::to_string(dimmed)), + red(at(px, 32, 32)) && dimmed * 100 >= open * 35 && dimmed * 100 <= open * 45); + } + else + { + ensure(named(reverse, "the rear's line behind the front left out"), black(at(px, 32, 32))); + } + } + } + } + + // The wireframe's width, a pixel at a UI scale of 1 and scaled with it, the same for the world and the HUD; and + // the polygon offset its lines need over a resampled depth. + template<> template<> + void alselectionoutline_object_t::test<30>() + { + ensure_equals("a pixel at 1", Outline::lineWidth(Outline::WIRE_WIDTH, 1.f), 1); + ensure_equals("two at 2", Outline::lineWidth(Outline::WIRE_WIDTH, 2.f), 2); + for (bool hud : { false, true }) + { + const Outline::View scaled = Outline::makeView(identity(), identity(), 2.f, 2.f, hud); + ensure_equals("its width at the UI scale", scaled.mWireWidth, 2); + } + ensure_equals("a window depth at the scene's resolution", Outline::wireOffsetFactor(1.f), 2.f); + ensure_equals("at half of it", Outline::wireOffsetFactor(2.f), 3.f); + ensure_equals("never under the window's", Outline::wireOffsetFactor(0.5f), 2.f); + } + + // A triangle reaching behind the eye, as a floor or a wall a builder stands at has, is clipped at the near plane + // where GL clips it: its edges' parts in front are drawn at their width, the cut along the near plane draws no + // line, and its texture coordinates are carried to the cut, so the alpha cut still follows the texture. + template<> template<> + void alselectionoutline_object_t::test<31>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED } }, true); + windowDepth(nullptr); + + // One corner a metre behind the eye, the other three six metres out: on screen, the bottom edge from the + // bottom of the window to (61, 22), the diagonal from its left side to (61, 42), and the top edge along + // row 42. Both triangles hold the corner behind, and the diagonal is the edge between them. + const LLVector3 one_behind[4] = { LLVector3(-3.f, -1.f, 1.f), LLVector3(3.f, -1.f, -6.f), + LLVector3(3.f, 1.f, -6.f), LLVector3(-3.f, 1.f, -6.f) }; + LLVertexBuffer* reaching = quadAt(one_behind); + std::vector px = worldWireframe({ { reaching, ID_A } }); + std::vector> found = columnRuns(px, 40); + ensure_equals(named(reverse, "column 40 crosses the bottom edge, the diagonal and the top edge"), found.size(), size_t(3)); + // The diagonal's slope down the screen is about a third: a width of 1 reads as about 1.05 down a column. + const F32 across = ink(px, 40, found[1].first, found[1].second); + ensure(named(reverse, "the diagonal at its width: " + std::to_string(across)), across > 0.9f && across < 1.25f); + // The top edge, along row 41.8, has one triangle, which draws the half of its line inside the quad. + ensure(named(reverse, "the top edge drawn inside the quad"), at(px, 40, 41).r > 127 && black(at(px, 40, 42))); + + // Two corners behind the eye: a wall crossing the near plane at the middle of the view, column 32, and + // reaching ten metres out at column 43. Its edges all run near column 43; the cut at column 32 is no edge. + px = worldWireframe({ { quad(-0.5f, 2.f, -1.f, 1.f, 2.4f, -10.1f), ID_A } }); + size_t at_cut = 0; + size_t at_edges = 0; + for (S32 y = 0; y < H; ++y) + { + for (S32 x = 32; x <= 38; ++x) + { + at_cut += black(at(px, x, y)) ? 0 : 1; + } + for (S32 x = 41; x <= 45; ++x) + { + at_edges += black(at(px, x, y)) ? 0 : 1; + } + } + ensure_equals(named(reverse, "no line along the near plane's cut"), at_cut, size_t(0)); + ensure(named(reverse, "the edges in front drawn: " + std::to_string(at_edges)), at_edges > 40); + + // The first quad textured with the ramp, masked at half: its diagonal's texture coordinate runs from 0 + // behind the eye to 1, about 0.43 at column 20 and 0.57 at column 40. + const F32 mask = Outline::legacyAlphaCutoff(true, LLMaterial::DIFFUSE_ALPHA_MODE_MASK, 128, true); + px = worldWireframe({ { reaching, ID_A, Outline::PRIORITY_ROOT, mask, ramp() } }); + ensure_equals(named(reverse, "no line where the clipped triangle's texture is cut"), ink(px, 20, 24, 32), 0.f); + ensure(named(reverse, "a line where it is kept"), ink(px, 40, 32, 38) > 0.5f); + } + } + + // The outline, the wireframe and the glow are drawn each without the others, and a frame runs only the passes its + // objects ask for: the outline and the glow the id pass and the passes after it, the world's wireframe none of + // them, since it is tested against the window's depth, and the HUD's the id pass alone, whose depth it is tested + // against. What glows asks for nothing else, and a selection's role keeps its object whatever glows. + template<> template<> + void alselectionoutline_object_t::test<32>() + { + for (bool hud : { false, true }) + { + const std::string where = hud ? " on the HUD" : " in the world"; + Outline::Wants wants; + wants.mContour = true; + Outline::Passes passes = Outline::passesFor(wants, hud, true); + ensure("the outline alone runs the id pass" + where, passes.mIds); + ensure("and the edge passes" + where, passes.mEdges); + ensure("and no wireframe" + where, !passes.mWireframe); + + wants.mWireframe = true; + passes = Outline::passesFor(wants, hud, true); + ensure("both run everything" + where, passes.mIds && passes.mEdges && passes.mWireframe); + passes = Outline::passesFor(wants, hud, false); + ensure("a wireframe program that did not load leaves the outline" + where, + passes.mIds && passes.mEdges && !passes.mWireframe); + + wants.mContour = false; + passes = Outline::passesFor(wants, hud, true); + ensure("the wireframe alone is drawn" + where, passes.mWireframe); + ensure("without the edge passes" + where, !passes.mEdges); + ensure_equals("with the id pass only on the HUD" + where, passes.mIds, hud); + passes = Outline::passesFor(wants, hud, false); + ensure("nothing when its program did not load" + where, !passes.any()); + + wants.mGlow = true; + passes = Outline::passesFor(wants, hud, true); + ensure("a glow beside a wireframe runs everything" + where, passes.mIds && passes.mEdges && passes.mWireframe); + + wants.mWireframe = false; + passes = Outline::passesFor(wants, hud, true); + ensure("a glow with the outline and the wireframe off runs the id and edge passes" + where, + passes.mIds && passes.mEdges && !passes.mWireframe); + ensure("and whether or not the wireframe loaded" + where, Outline::passesFor(wants, hud, false).mEdges); + + wants.mGlow = false; + ensure("nothing with all three off" + where, !Outline::passesFor(wants, hud, true).any()); + } + + const U32 both = Outline::PART_CONTOUR | Outline::PART_WIREFRAME; + ensure_equals("what glows has no contour and no wireframe", Outline::partsFor(Outline::PRIORITY_HOVER, both), 0U); + ensure_equals("a selection keeps what it asks for", Outline::partsFor(Outline::PRIORITY_ROOT, both), both); + + ensure("what glows is in the id pass", Outline::inIdPass(0, Outline::PRIORITY_HOVER, false)); + ensure("and drawn there", Outline::outlined(0, Outline::PRIORITY_HOVER)); + ensure("a contour is", Outline::inIdPass(Outline::PART_CONTOUR, Outline::PRIORITY_ROOT, false)); + ensure("a world wireframe alone is not", !Outline::inIdPass(Outline::PART_WIREFRAME, Outline::PRIORITY_ROOT, false)); + ensure("a HUD wireframe alone is, for its depth", Outline::inIdPass(Outline::PART_WIREFRAME, Outline::PRIORITY_ROOT, true)); + ensure("and drawn nowhere", !Outline::outlined(Outline::PART_WIREFRAME, Outline::PRIORITY_ROOT)); + + for (Outline::EWirePass pass : { Outline::WIRE_VISIBLE, Outline::WIRE_HIDDEN, Outline::WIRE_ID_DEPTH }) + { + ensure("what glows has no wireframe in any pass", + !Outline::inWirePass(Outline::partsFor(Outline::PRIORITY_HOVER, both), true, pass)); + ensure("nor an outline alone", !Outline::inWirePass(Outline::PART_CONTOUR, true, pass)); + } + ensure("a wireframe's visible lines", Outline::inWirePass(Outline::PART_WIREFRAME, false, Outline::WIRE_VISIBLE)); + ensure("its hidden ones only where hidden parts are drawn", + !Outline::inWirePass(Outline::PART_WIREFRAME, false, Outline::WIRE_HIDDEN) && + Outline::inWirePass(Outline::PART_WIREFRAME, true, Outline::WIRE_HIDDEN)); + + ensure("a glow never replaces a selection", !Outline::replaces(Outline::PRIORITY_CONTEXT, Outline::PRIORITY_HOVER)); + ensure("a selection replaces a glow", Outline::replaces(Outline::PRIORITY_HOVER, Outline::PRIORITY_CHILD)); + ensure("what a drag takes out replaces a root", Outline::replaces(Outline::PRIORITY_ROOT, Outline::PRIORITY_SUBTRACT)); + ensure("at one priority the later", Outline::replaces(Outline::PRIORITY_ROOT, Outline::PRIORITY_ROOT)); + } + + // The glow's reach and profile, the fades of what the pointer is over, and how long the targets are kept: each + // object fades in over the fade time while it is named, the one before it fading out as it does, and is dropped + // at nothing or when its object is gone. + template<> template<> + void alselectionoutline_object_t::test<33>() + { + const F32 nan = std::numeric_limits::quiet_NaN(); + ensure_equals("9 pixels at the default thickness", Outline::glowRadius(Outline::DEFAULT_GLOW_THICKNESS, 1.f), 9); + ensure_equals("scaled with the UI", Outline::glowRadius(Outline::DEFAULT_GLOW_THICKNESS, 2.f), 18); + ensure_equals("held to the widest", Outline::glowRadius(100.f, 1.f), (S32)Outline::MAX_GLOW_RADIUS); + ensure_equals("a pixel at least", Outline::glowRadius(-1.f, 1.f), 1); + ensure_equals("the default where the setting is not a number", Outline::glowRadius(nan, 1.f), 9); + const Outline::View defaults = Outline::makeView(identity(), identity(), 2.f, 1.f, false); + ensure_equals("a view's glow by default", defaults.mGlowRadius, 9); + ensure_equals("and its brightness", defaults.mGlowBrightness, Outline::DEFAULT_GLOW_BRIGHTNESS); + ensure_equals("a brightness that is not a number is the default", + Outline::makeView(identity(), identity(), 2.f, 1.f, false, 0.6f, nan).mGlowBrightness, + Outline::DEFAULT_GLOW_BRIGHTNESS); + ensure_equals("and one past the brightest held to it", + Outline::makeView(identity(), identity(), 2.f, 1.f, false, 0.6f, 1000.f).mGlowBrightness, + Outline::MAX_GLOW_BRIGHTNESS); + + // The silhouette is half a texel out from its texel's centre. + ensure_approximately_equals_range("opaque at the silhouette at the default brightness", + Outline::glowOpacity(0.5f, 9, 4.f), 1.f, 1e-6f); + ensure_approximately_equals_range("half as opaque at half of it", Outline::glowOpacity(0.5f, 9, 2.f), 0.5f, 1e-6f); + ensure_approximately_equals_range("half way out, half of the silhouette's", Outline::glowOpacity(5.f, 9, 4.f), 0.5f, + 1e-6f); + ensure_equals("nothing at its reach", Outline::glowOpacity(9.5f, 9, 4.f), 0.f); + ensure_equals("nor past it", Outline::glowOpacity(30.f, 9, 4.f), 0.f); + ensure_equals("held to opaque", Outline::glowOpacity(2.f, 9, Outline::MAX_GLOW_BRIGHTNESS), 1.f); + const F32 last_step = Outline::glowOpacity(9.25f, 9, 4.f); + ensure("fading smoothly into nothing: " + std::to_string(last_step), last_step > 0.f && last_step < 0.01f); + F32 previous = 2.f; + bool falling = true; + for (S32 step = 0; step <= 40; ++step) + { + const F32 opacity = Outline::glowOpacity(0.5f + 0.25f * (F32)step, 9, 1.f); + falling = falling && opacity <= previous; + previous = opacity; + } + ensure("falling all the way out", falling); + + ensure_approximately_equals_range("half way in half the fade time", ALHoverGlow::stepFade(0.f, 0.05f, 0.1f, true), + 0.5f, 1e-6f); + ensure_approximately_equals_range("out as fast", ALHoverGlow::stepFade(0.5f, 0.05f, 0.1f, false), 0.f, 1e-6f); + ensure_equals("held to one", ALHoverGlow::stepFade(0.9f, 1.f, 0.1f, true), 1.f); + ensure_equals("and to nothing", ALHoverGlow::stepFade(0.1f, 1.f, 0.1f, false), 0.f); + ensure_equals("at once with no fade time", ALHoverGlow::stepFade(0.f, 0.f, 0.f, true), 1.f); + ensure_equals("or one that is not a number", ALHoverGlow::stepFade(1.f, 0.f, nan, false), 0.f); + ensure_equals("time running back moves nothing", ALHoverGlow::stepFade(0.5f, -1.f, 0.1f, true), 0.5f); + + // Steps of an eighth of a second over a quarter: halves, exactly. + const LLUUID a("c0ffee00-0000-4000-8000-00000000000a"); + const LLUUID b("c0ffee00-0000-4000-8000-00000000000b"); + std::vector alive = { a, b }; + auto is_alive = [&alive](const LLUUID& id) { return std::find(alive.begin(), alive.end(), id) != alive.end(); }; + // A fresh singleton for this case, given up at its end. + ALHoverGlow::deleteSingleton(); + ALHoverGlow& glow = ALHoverGlow::instance(); + auto fade = [&glow](const LLUUID& id) + { + for (const ALHoverGlow::Glow& each : glow.glows()) + { + if (each.mID == id) + { + return each.mFade; + } + } + return -1.f; + }; + // A frame an eighth of a second long, the settle time between one and two of them. + const F32 fade_time = 0.25f; + ensure("the settle time lies between one frame and two", + ALHoverGlow::SETTLE_SECONDS > 0.125 && ALHoverGlow::SETTLE_SECONDS <= 0.25); + U32 frame = 0; + auto at = [&](const LLUUID& id, F64 now) + { + ++frame; + glow.hover(id, frame); + glow.update(now, frame, fade_time, is_alive); + }; + + at(a, 10.0); + ensure("named, nothing glows yet", glow.glows().empty()); + at(a, 10.125); + ensure("nor before the name has settled", glow.glows().empty()); + at(a, 10.25); + ensure_equals("settled, it fades in", fade(a), 0.5f); + glow.update(10.25, frame, fade_time, is_alive); + ensure_equals("a second update in a frame moves nothing", fade(a), 0.5f); + at(a, 10.375); + ensure_equals("in over the fade time", fade(a), 1.f); + + at(b, 10.5); + at(b, 10.625); + ensure_equals("the glow stays while the next name settles", fade(a), 1.f); + ensure_equals("which does not glow yet", fade(b), -1.f); + at(b, 10.75); + ensure_equals("then the one before fades out", fade(a), 0.5f); + ensure_equals("while the next fades in", fade(b), 0.5f); + at(b, 10.875); + ensure_equals("dropped at nothing", fade(a), -1.f); + ensure_equals("one glows", glow.glows().size(), size_t(1)); + + at(LLUUID::null, 11.0); + at(b, 11.125); + ensure_equals("a frame off it, at its edge or a seam, does not put it out", fade(b), 1.f); + at(LLUUID::null, 11.25); + at(LLUUID::null, 11.375); + ensure_equals("nor does leaving it, until that settles", fade(b), 1.f); + at(LLUUID::null, 11.5); + ensure_equals("then it fades out", fade(b), 0.5f); + + at(a, 11.625); + at(b, 11.75); + at(a, 11.875); + at(LLUUID::null, 12.0); + ensure("a sweep across objects lights none of them", glow.glows().empty()); + + at(a, 12.125); + at(a, 12.25); + at(a, 12.375); + ensure_equals("one rested on lights", fade(a), 0.5f); + alive.clear(); + at(a, 12.5); + ensure("dropped when its object is gone, named or not", glow.glows().empty()); + at(LLUUID::null, 12.625); + at(LLUUID::null, 12.75); + ensure("nothing named, nothing glows", glow.glows().empty()); + + glow.hover(b, 100); + ensure("named a frame ago, it is still named", glow.hovered(101) == b); + ensure("unnamed for longer, the name lapses", glow.hovered(102).isNull()); + ALHoverGlow::deleteSingleton(); + + const F64 kept = Outline::TARGET_KEEP_SECONDS; + ensure("a target drawn with this frame is kept", !Outline::stale(5, 100.0, 5, 100.0 + kept * 2.0)); + ensure("and the next, however long it took", !Outline::stale(5, 100.0, 6, 100.0 + kept * 2.0)); + ensure("and for its time, however many frames pass", !Outline::stale(5, 100.0, 500, 100.0 + kept * 0.5)); + ensure("given up after both", Outline::stale(5, 100.0, 7, 100.0 + kept)); + ensure("across the frame counter's wrap", + !Outline::stale(0xFFFFFFFFu, 100.0, 0u, 200.0) && Outline::stale(0xFFFFFFFEu, 100.0, 1u, 200.0)); + } + + // A glowing object draws a halo around its silhouette in its colour, falling off from the silhouette as + // glowOpacity says, as bright as asked, to nothing at its reach; and nothing over itself, its own folds included, + // whose jumps the jump pass leaves unmarked. + template<> template<> + void alselectionoutline_object_t::test<34>() + { + constexpr S32 RADIUS = 6; + for (bool reverse : conventions()) + { + setUp(reverse); + // Columns 20 to 39 and rows 20 to 43, and a fold of the same object nearer over its middle, columns 25 to + // 34: selected, the near side of a jump in its own surface. + pixelIds({ { pixels(20, 40, 20, 44), ID_A }, { quad(25.f, 35.f, 25.f, 39.f, -1.f, -1.f), ID_A } }); + markPasses(glowView(RADIUS)); + ensure(named(reverse, "selected, the fold's edge is the near side of a jump"), markedAt(25, 32) && markedAt(34, 32)); + + pixelIds({ hoverDraw(pixels(20, 40, 20, 44), ID_A), hoverDraw(quad(25.f, 35.f, 25.f, 39.f, -1.f, -1.f), ID_A) }); + palette({ { ID_A, GREEN } }, false); + + for (F32 brightness : { Outline::DEFAULT_GLOW_BRIGHTNESS, 2.f }) + { + const std::string at_brightness = " at brightness " + std::to_string(brightness); + const std::vector px = edgePass(glowView(RADIUS, brightness), nullptr, false, nullptr, true); + ensure(named(reverse, "glowing, its fold's edge is not marked"), !markedAt(25, 32) && !markedAt(34, 32)); + for (S32 k = 0; k <= RADIUS + 2; ++k) + { + // Column 40 + k is k + 1 from the centre of the nearest texel, in column 39. + const F32 expected = 255.f * Outline::glowOpacity((F32)(k + 1), RADIUS, brightness); + const Pixel p = at(px, 40 + k, 32); + ensure(named(reverse, "the glow " + std::to_string(k + 1) + " pixels out" + at_brightness + ": " + + std::to_string(p.g) + " for " + std::to_string(expected)), + std::fabs((F32)p.g - expected) <= 2.f && p.r == 0 && p.b == 0); + } + const U8 between = at(px, 43, 32).g; + ensure(named(reverse, "soft, not a solid band" + at_brightness), between > 10 && between < 245); + ensure(named(reverse, "nothing over the object" + at_brightness), + black(at(px, 30, 32)) && black(at(px, 39, 32)) && black(at(px, 20, 20))); + ensure(named(reverse, "nor over its far side beside its fold" + at_brightness), + black(at(px, 24, 32)) && black(at(px, 35, 32))); + } + } + } + + // A selection's contour and the edges between selected objects are laid over the glow; and beside a glowing + // object a selection is outlined as though it were not there, except where it stands in front. + template<> template<> + void alselectionoutline_object_t::test<35>() + { + constexpr U32 ID_GLOW = 400; + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED }, { ID_B, RED }, { ID_GLOW, GREEN } }, true); + + // A selected object, columns 40 to 54, and a glowing one six columns to its left, 20 to 33. + pixelIds({ { pixels(40, 55, 20, 44), ID_A }, hoverDraw(pixels(20, 34, 20, 44), ID_GLOW) }); + std::vector px = edgePass(glowView(9), nullptr, false, nullptr, true); + for (S32 x : { 38, 39 }) + { + const Pixel p = at(px, x, 32); + ensure(named(reverse, "the contour opaque over the glow at column " + std::to_string(x)), + p.r == 255 && p.g == 0); + } + ensure(named(reverse, "the glow past the contour"), green(at(px, 37, 32)) && green(at(px, 36, 32))); + + // Touching it, at column 40: the selection's contour drawn over the glowing object as over nothing, and + // no edge between them. + pixelIds({ { pixels(40, 55, 20, 44), ID_A } }); + const std::vector alone = edgePass(glowView(9), nullptr, false, nullptr, true); + pixelIds({ { pixels(40, 55, 20, 44), ID_A }, hoverDraw(pixels(20, 40, 20, 44), ID_GLOW) }); + px = edgePass(glowView(9), nullptr, false, nullptr, true); + for (S32 x : { 37, 38, 39, 40, 41 }) + { + const Pixel p = at(px, x, 32); + const Pixel q = at(alone, x, 32); + ensure(named(reverse, "the contour beside it as without it at column " + std::to_string(x)), + p.r == q.r && p.g == q.g && p.b == q.b && p.a == q.a); + } + + // Two selected objects touching at column 30, their edge on A's side, and a glowing one nearer below + // them, rows 8 to 17: its glow over A's surface, and the edge over the glow. + pixelIds({ { pixels(10, 30, 20, 44), ID_A }, { pixels(30, 50, 20, 44), ID_B }, + hoverDraw(quad(24.f, 36.f, 8.f, 18.f, -1.f, -1.f), ID_GLOW) }); + px = edgePass(glowView(9), nullptr, false, nullptr, true); + // Pixel (29, 20) is a pixel from B and three from the glowing object's top row. + const F32 glow = Outline::glowOpacity(3.f, 9, Outline::DEFAULT_GLOW_BRIGHTNESS); + const Pixel edge = at(px, 29, 20); + ensure(named(reverse, "the edge over the glow: red " + std::to_string(edge.r)), + std::fabs((F32)edge.r - 255.f * Outline::INNER_OPACITY) <= 2.f); + ensure(named(reverse, "the glow under it: green " + std::to_string(edge.g)), + std::fabs((F32)edge.g - 255.f * glow * (1.f - Outline::INNER_OPACITY)) <= 2.f); + ensure(named(reverse, "the glow over A's surface, which lies behind it"), green(at(px, 20, 21))); + } + } + + // A glowing object's hidden parts do not glow: its halo follows the part the scene shows. + template<> template<> + void alselectionoutline_object_t::test<36>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + // A, columns 9 to 31, behind something in the scene over columns 0 to 20. + occlude(3.f, 0, 0, 21, H); + idPass({ hoverDraw(quadA(), ID_A) }); + const std::vector ids = read(mIdMap); + ensure_equals(named(reverse, "its hidden part left out"), (U32)at(ids, 12, 32).b, 0U); + palette({ { ID_A, GREEN } }, false); + const std::vector px = edgePass(glowView(6), nullptr, false, nullptr, true); + ensure(named(reverse, "a glow past its visible side"), green(at(px, 32, 32)) && at(px, 32, 32).g > 200); + ensure(named(reverse, "none past its hidden one"), black(at(px, 8, 32)) && black(at(px, 5, 32))); + ensure(named(reverse, "nor above it"), black(at(px, 12, 46))); + ensure(named(reverse, "but above the part shown"), green(at(px, 26, 45))); + } + } + + // The wireframe draws no lines for what glows, beside a selection whose lines it draws. + template<> template<> + void alselectionoutline_object_t::test<37>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED }, { ID_B, GREEN } }, true); + windowDepth(nullptr); + const std::vector px = worldWireframe({ { quadA(), ID_A }, hoverDraw(quadB(), ID_B) }); + size_t selected = 0; + size_t glowing = 0; + for (S32 y = 0; y < H; ++y) + { + for (S32 x = 0; x < W; ++x) + { + const Pixel p = at(px, x, y); + selected += (x >= 9 && x <= 30 && red(p)) ? 1 : 0; + glowing += p.g > 0 ? 1 : 0; + } + } + ensure(named(reverse, "the selection's lines"), selected > 20); + ensure_equals(named(reverse, "none for what glows"), glowing, size_t(0)); + } + } + + // The tiles change nothing the glow draws, where it reaches past the lines; and a glowing object's glow lies in + // its box grown by the glow's reach, which drawObject grows the scissor by. + template<> template<> + void alselectionoutline_object_t::test<38>() + { + constexpr U32 ID_GLOW = 400; + constexpr U32 ID_FRONT = 401; + constexpr S32 RADIUS = 9; + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED }, { ID_B, RED }, { ID_GLOW, GREEN }, { ID_FRONT, GREEN } }, true); + // Two selections, a glowing object apart from them, and another nearer, over a corner of one. + pixelIds({ { pixels(30, 50, 30, 50), ID_A }, { pixels(8, 22, 40, 56), ID_B }, hoverDraw(pixels(6, 22, 6, 24), ID_GLOW), + hoverDraw(quad(44.f, 58.f, 22.f, 36.f, -1.f, -1.f), ID_FRONT) }); + const std::vector tiled = edgePass(glowView(RADIUS), nullptr, false, nullptr, true); + const std::vector untiled = edgePass(glowView(RADIUS), nullptr, true, nullptr, true); + ensure(named(reverse, "the glow reaches past the lines"), green(at(tiled, 28, 15))); + ensure(named(reverse, "the tiles change nothing it draws"), tiled == untiled); + + const LLVector4a extents[2] = { LLVector4a(6.f, 6.f, PIXEL_Z), LLVector4a(22.f, 24.f, PIXEL_Z) }; + LLRect rect; + ensure(named(reverse, "its box is on screen"), + Outline::scissorRect(pixelProjection(), extents, W, H, Outline::lineReach(RADIUS), rect)); + pixelIds({ hoverDraw(pixels(6, 22, 6, 24), ID_GLOW) }); + const std::vector whole = edgePass(glowView(RADIUS), nullptr, false, nullptr, true); + const std::vector scissored = edgePass(glowView(RADIUS), &rect, false, &rect, true); + ensure(named(reverse, "its glow is drawn"), lit(whole) > 100); + ensure(named(reverse, "and lies in its box grown by its reach"), whole == scissored); + } + } + + // A face wearing the library's transparent texture is drawn whole, legacy or GLTF, whatever its alpha mode, so + // the invisible prims builders hide roots and touch areas with are outlined; any other texture keeps its alpha cut. + template<> template<> + void alselectionoutline_object_t::test<39>() + { + const LLUUID other("c0ffee00-0000-4000-8000-0000000000aa"); + const F32 blend = Outline::legacyAlphaCutoff(false, 0, 0, true); + const F32 mask = Outline::gltfAlphaCutoff(LLGLTFMaterial::ALPHA_MODE_MASK, 0.5f); + ensure_equals("the transparent texture is drawn whole", Outline::faceAlphaCutoff(IMG_TRANSPARENT, blend), + Outline::NO_ALPHA_TEST); + ensure_equals("as a GLTF base colour too", Outline::faceAlphaCutoff(IMG_TRANSPARENT, mask), Outline::NO_ALPHA_TEST); + ensure_equals("any other keeps its cut", Outline::faceAlphaCutoff(other, blend), blend); + ensure_equals("a GLTF one too", Outline::faceAlphaCutoff(other, mask), mask); + + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED } }, true); + for (const LLUUID& texture : { IMG_TRANSPARENT, other }) + { + const bool whole = texture == IMG_TRANSPARENT; + const std::string which = whole ? "the transparent texture" : "another clear texture"; + idPass({ { quadA(), ID_A, Outline::PRIORITY_ROOT, Outline::faceAlphaCutoff(texture, blend), clearTexture() } }); + const std::vector ids = read(mIdMap); + ensure_equals(named(reverse, which + (whole ? " drawn whole" : " cut away")), idAt(ids, 20, 32), + whole ? ID_A : 0U); + const std::vector px = edgePass(); + ensure(named(reverse, which + (whole ? " outlined" : " not outlined")), + whole ? red(at(px, 8, 32)) : black(at(px, 8, 32))); + } + } + } + + // On the HUD, an object in the id pass for its wireframe's depth alone is drawn nowhere there: its id is in the + // target, hiding what lies behind it from the lines, and it has no contour while another object's is drawn. + template<> template<> + void alselectionoutline_object_t::test<40>() + { + for (bool reverse : conventions()) + { + setUp(reverse); + Draw wire_only{ pixels(36, 50, 20, 44), ID_B }; + wire_only.mParts = Outline::PART_WIREFRAME; + const std::vector ids = pixelIds({ { pixels(10, 26, 20, 44), ID_A }, wire_only }); + ensure_equals(named(reverse, "its id is in the target"), idAt(ids, 40, 32), ID_B); + ensure_equals(named(reverse, "drawn nowhere"), (U32)at(ids, 40, 32).b, 0U); + ensure_equals(named(reverse, "the other drawn"), (U32)at(ids, 20, 32).b, 255U); + + palette({ { ID_A, RED }, { ID_B, GREEN } }, true); + const std::vector px = edgePass(Outline::makeView(identity(), pixelProjection(), TEST_WIDTH, 1.f, true)); + ensure(named(reverse, "the other's contour"), red(at(px, 9, 32)) && red(at(px, 26, 32))); + ensure(named(reverse, "none around it"), black(at(px, 35, 32)) && black(at(px, 50, 32)) && black(at(px, 43, 45))); + } + } + + // A blended face is cut only where its texture is all but clear: one translucent all over, as a pane of glass is, + // is drawn whole, outlined and wireframed, where the same texture under a cutoff of a half is cut away. + template<> template<> + void alselectionoutline_object_t::test<41>() + { + const F32 blend = Outline::legacyAlphaCutoff(false, 0, 0, true); + const F32 half = Outline::gltfAlphaCutoff(LLGLTFMaterial::ALPHA_MODE_MASK, 0.5f); + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED } }, true); + const U32 glass = glassTexture(); + + idPass({ { quadA(), ID_A, Outline::PRIORITY_ROOT, blend, glass } }); + std::vector ids = read(mIdMap); + ensure_equals(named(reverse, "a blended pane of glass drawn whole"), idAt(ids, 20, 32), ID_A); + std::vector px = edgePass(); + ensure(named(reverse, "and outlined"), red(at(px, 8, 32))); + + windowDepth(nullptr); + px = worldWireframe({ { quadA(), ID_A, Outline::PRIORITY_ROOT, blend, glass } }); + ensure(named(reverse, "and wireframed"), lit(px) > 0); + + idPass({ { quadA(), ID_A, Outline::PRIORITY_ROOT, half, glass } }); + ids = read(mIdMap); + ensure_equals(named(reverse, "under a cutoff of a half it is cut away"), idAt(ids, 20, 32), 0U); + px = edgePass(); + ensure(named(reverse, "and not outlined"), black(at(px, 8, 32))); + px = worldWireframe({ { quadA(), ID_A, Outline::PRIORITY_ROOT, half, glass } }); + ensure_equals(named(reverse, "nor wireframed"), lit(px), size_t(0)); + } + } + + // The glow from the glow reach and row passes is the full search's, pixel for pixel: at reaches short and long, + // around shapes thin, slanted and cut off by the target's edges, between two glowing objects, where the lower id's + // colour is drawn where both are as near, beside selections and with one in front of it, and around an object + // partly hidden. + template<> template<> + void alselectionoutline_object_t::test<42>() + { + constexpr U32 ID_GLOW = 400; + constexpr U32 ID_OTHER = 401; + const LLColor4 BLUE(0.f, 0.f, 1.f, 1.f); + for (bool reverse : conventions()) + { + setUp(reverse); + palette({ { ID_A, RED }, { ID_B, RED }, { ID_GLOW, GREEN }, { ID_OTHER, BLUE } }, true); + + const LLVector3 diamond[4] = { LLVector3(44.f, 30.f, PIXEL_Z), LLVector3(54.f, 40.f, PIXEL_Z), + LLVector3(44.f, 50.f, PIXEL_Z), LLVector3(34.f, 40.f, PIXEL_Z) }; + const std::vector>> scenes = { + { "two glowing objects, columns 10 to 19 and 29 to 38", + { hoverDraw(pixels(10, 20, 20, 44), ID_OTHER), hoverDraw(pixels(29, 39, 20, 44), ID_GLOW) } }, + { "a line a pixel thin and a slanted square", + { hoverDraw(pixels(8, 9, 10, 54), ID_GLOW), hoverDraw(quadAt(diamond), ID_OTHER) } }, + { "two cut off by the target's edges", + { hoverDraw(pixels(0, 12, 0, 12), ID_GLOW), hoverDraw(pixels(56, 64, 40, 64), ID_OTHER) } }, + { "beside selections, one in front of it", + { { pixels(30, 50, 30, 50), ID_A }, { quad(14.f, 26.f, 14.f, 26.f, PIXEL_Z + 1.f, PIXEL_Z + 1.f), ID_B }, + hoverDraw(pixels(6, 30, 6, 30), ID_GLOW) } }, + { "two glowing objects five rows above and five columns right of pixel (30, 30)", + { hoverDraw(pixels(26, 35, 35, 41), ID_GLOW), hoverDraw(pixels(35, 41, 26, 35), ID_OTHER) } }, + }; + for (const auto& [name, draws] : scenes) + { + pixelIds(draws); + for (S32 radius : { 1, 6, 9, 17, 33 }) + { + const std::string at_radius = name + ", a reach of " + std::to_string(radius); + const std::vector rows = edgePass(glowView(radius), nullptr, false, nullptr, true); + const std::vector full = edgePass(glowView(radius), nullptr, true, nullptr, true); + ensure(named(reverse, at_radius + ": it glows"), lit(rows) > 0); + ensure(named(reverse, at_radius + ": as the full search draws it"), rows == full); + } + } + + // Column 24 is five columns from each of the first scene's objects, column 23 four from the one on the left. + pixelIds(scenes[0].second); + const std::vector between = edgePass(glowView(9), nullptr, false, nullptr, true); + ensure(named(reverse, "as near both along a row, the lower id's colour"), green(at(between, 24, 32))); + ensure(named(reverse, "nearer the left, its colour"), blue(at(between, 23, 32))); + pixelIds(scenes[4].second); + const std::vector across = edgePass(glowView(9), nullptr, false, nullptr, true); + ensure(named(reverse, "as near both along a column and a row, the lower id's colour"), green(at(across, 30, 30))); + + // Behind something in the scene over columns 0 to 20, its hidden part drawing no glow. + occlude(3.f, 0, 0, 21, H); + idPass({ hoverDraw(quadA(), ID_GLOW) }); + for (S32 radius : { 6, 17 }) + { + const std::string at_radius = "partly hidden, a reach of " + std::to_string(radius); + const std::vector rows = edgePass(glowView(radius), nullptr, false, nullptr, true); + const std::vector full = edgePass(glowView(radius), nullptr, true, nullptr, true); + ensure(named(reverse, at_radius + ": it glows"), lit(rows) > 0); + ensure(named(reverse, at_radius + ": as the full search draws it"), rows == full); + } + } + } +}