From 8f1cf12eeb82c97c9f34486cd0bf2e9d41c0f4b0 Mon Sep 17 00:00:00 2001 From: Rye Date: Fri, 9 Oct 2026 21:42:27 -0400 Subject: [PATCH 01/11] The clear colour and the scissor box are LLRender state, cached like the colour mask gGL.setClearColor and gGL.setScissor drop redundant sets, and their getters say what is in force. init issues the clear colour rather than assuming GL's default, since a window may have cleared to one of its own first; the scissor box stays unknown until something sets it, as GL starts it at the size of a window nothing here knows. refreshState re-issues both. LLGLSScissor scopes the test and the box and hands both back, nested scopes included. gGL.clear flushes what is batched before it clears. A cache is only the state if every writer goes through it, so the 27 raw glClearColor calls (newview, llwindowwin32, the appearance utility, the GL test fixture) and the scissor's writers (LLRenderTarget::clear, LLScreenClipRect) move onto it. LLScreenClipRect flushed the batch on every clip push and pop; it now flushes only when the box changes. alrenderstate_gl: a target clears with the colour gGL was given, GL holds what gGL says, refreshState restores both after GL is changed behind gGL's back, and LLGLSScissor cuts a clear to its box and restores the box and the test it found. Co-Authored-By: Claude Opus 5.5 --- .../llappearanceutility/llprocesstexture.cpp | 2 +- indra/llrender/CMakeLists.txt | 2 + indra/llrender/llglstates.h | 29 +++ indra/llrender/llrender.cpp | 46 +++++ indra/llrender/llrender.h | 27 +++ indra/llrender/llrendertarget.cpp | 4 +- .../llrender/tests/alrenderstate_gl_test.cpp | 184 ++++++++++++++++++ indra/llrender/tests/llheadlessgl_fixture.h | 2 +- .../llrender/tests/llrendertarget_gl_test.cpp | 2 +- indra/llui/lllocalcliprect.cpp | 8 +- indra/llwindow/llwindowwin32.cpp | 3 +- indra/newview/alfarplaneoverlay.cpp | 2 +- indra/newview/llgltfmaterialpreviewmgr.cpp | 2 +- indra/newview/llterrainpaintmap.cpp | 2 +- indra/newview/llviewerdisplay.cpp | 14 +- indra/newview/pipeline.cpp | 26 +-- 16 files changed, 320 insertions(+), 35 deletions(-) create mode 100644 indra/llrender/tests/alrenderstate_gl_test.cpp 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/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/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..20a475c52c 100644 --- a/indra/llrender/tests/llheadlessgl_fixture.h +++ b/indra/llrender/tests/llheadlessgl_fixture.h @@ -439,7 +439,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..8a86642634 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,7 +1824,7 @@ const S32 max_format = (S32)num_formats - 1; if (auto_show) { show(); - glClearColor(0.0f, 0.0f, 0.0f, 0.f); + gGL.setClearColor(LLColor4::transparent); glClear(GL_COLOR_BUFFER_BIT); swapBuffers(); } 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/llgltfmaterialpreviewmgr.cpp b/indra/newview/llgltfmaterialpreviewmgr.cpp index d60cb76383..8b52727047 100644 --- a/indra/newview/llgltfmaterialpreviewmgr.cpp +++ b/indra/newview/llgltfmaterialpreviewmgr.cpp @@ -428,7 +428,7 @@ bool LLGLTFPreviewTexture::render() if (!mShouldRender) { return false; } - glClearColor(0, 0, 0, 0); + gGL.setClearColor(LLColor4::transparent); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); LLGLDepthTest depth(GL_FALSE); 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/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index a086f90a0c..7b4070ed32 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -823,7 +823,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 +935,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 +990,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 +1227,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 +1259,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(); diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index bbfc7d1284..a7efd0c8c2 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -1688,9 +1688,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); @@ -5106,7 +5106,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 +10102,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,9 +11068,9 @@ 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(), @@ -11094,9 +11094,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); @@ -11151,7 +11151,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) @@ -11664,12 +11664,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 +13514,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 +13771,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 From 4d78386e5d16e0a3f7977682dce95081a0f80340 Mon Sep 17 00:00:00 2001 From: Rye Date: Fri, 9 Oct 2026 21:43:01 -0400 Subject: [PATCH 02/11] The lighting passes leave the sky alone softenLightF read the whole G-buffer and the light map and ran WindLight's atmospherics for every pixel, the sky's included, and only then branched to the sky's colour copy. Its triangle now lies on the far plane, as the haze's does: the lit program draws in front of it (GREATER), so the depth test rejects the sky before it is shaded, and a SOFTEN_SKY program (gDeferredSoftenSkyProgram) draws on it (EQUAL), copying the sky's colour and lighting nothing. The two share skyColor and writeColor, and a SKIP_ATMOS or HDRI pixel in front of the far plane (a debug highlight) still takes the copy in the lit program's flag branch. The full-screen lights, the sun shadow and SSAO pass and both of its blurs draw on the far plane with the same test; deferredLight shares the scene's depth for it. The light map keeps the clear's white over the sky, which nothing reads there: the lit soften program reads it only at geometry, and the projectors discard sky pixels before their read. Every one of these passes costs per pixel, and on 5k x 2k and HiDPI screens the sky is millions of them. alinfiniteprojection_gl test 7: between the lit draw and the sky's, every pixel is shaded once, with geometry as far out as reconstruction reaches. alfarplane test 5 pins the far-plane lines of all four vertex shaders and the sky program's copy. Co-Authored-By: Claude Opus 5.5 --- .../tests/alinfiniteprojection_gl_test.cpp | 51 ++++++++++++++ .../shaders/class1/deferred/blurLightV.glsl | 12 ++-- .../shaders/class2/deferred/softenLightV.glsl | 10 ++- .../shaders/class2/deferred/sunLightV.glsl | 13 ++-- .../class3/deferred/multiPointLightV.glsl | 10 ++- .../shaders/class3/deferred/softenLightF.glsl | 61 +++++++++++------ indra/newview/llviewershadermgr.cpp | 68 ++++++++++++------- indra/newview/llviewershadermgr.h | 1 + indra/newview/pipeline.cpp | 36 ++++++++-- indra/newview/tests/alfarplane_test.cpp | 11 +++ 10 files changed, 207 insertions(+), 66 deletions(-) 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/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/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/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp index 55dd0b1ff2..2514b26843 100644 --- a/indra/newview/llviewershadermgr.cpp +++ b/indra/newview/llviewershadermgr.cpp @@ -197,6 +197,7 @@ LLGLSLShader gHazeProgram; LLGLSLShader gHazeWaterProgram; LLGLSLShader gDeferredBlurLightProgram; LLGLSLShader gDeferredSoftenProgram; +LLGLSLShader gDeferredSoftenSkyProgram; LLGLSLShader gDeferredShadowProgram; LLGLSLShader gDeferredTerrainShadowProgram; LLGLSLShader gDeferredShadowCubeProgram; @@ -1309,6 +1310,7 @@ bool LLViewerShaderMgr::loadShadersDeferred() gDeferredSunProgram.unload(); gDeferredBlurLightProgram.unload(); gDeferredSoftenProgram.unload(); + gDeferredSoftenSkyProgram.unload(); gDeferredShadowProgram.unload(); gDeferredTerrainShadowProgram.unload(); gDeferredShadowCubeProgram.unload(); @@ -2443,37 +2445,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) diff --git a/indra/newview/llviewershadermgr.h b/indra/newview/llviewershadermgr.h index f701d3f568..ccc9f82e18 100644 --- a/indra/newview/llviewershadermgr.h +++ b/indra/newview/llviewershadermgr.h @@ -193,6 +193,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/pipeline.cpp b/indra/newview/pipeline.cpp index a7efd0c8c2..5acc08911b 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -976,6 +976,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 { @@ -11077,8 +11080,11 @@ void LLPipeline::renderDeferredLighting() (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); } @@ -11123,9 +11129,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 +11149,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); } @@ -11174,16 +11182,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 +11392,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; 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"); From 182f1b010b7c645842ec278da9e9b2720fe6ab6a Mon Sep 17 00:00:00 2001 From: Rye Date: Fri, 9 Oct 2026 21:43:44 -0400 Subject: [PATCH 03/11] Selected objects are outlined on the GPU, and the CPU silhouettes and mesh wireframes are gone Prim silhouettes were edges the CPU found face by face (LLVolume::generateSilhouetteVertices), found again for every selected object whenever the camera moved, at most 50 objects a frame, so a large linkset's outline trailed the camera for several frames. Meshes got a five-pixel GL_LINE wireframe of every triangle instead, which a forward-compatible core profile draws one pixel wide, and HUD selections a third path of their own. ALSelectionOutline draws them all from an id target, in the UI stage where the silhouettes were drawn: - the id pass draws each selected, rect-highlighted or focused object's faces, rigged ones through the program's rigged variant, into a window-sized RGBA8 target with a depth buffer of its own: the object's id in .rg, and in .b whether the scene's depth shows the surface; - the edge pass, scissored to the objects' projected boxes, colours what lies within the outline's radius of a different id from a palette of each object's role, the hidden parts dimmed or left out; - HUD selections go through the same passes with the HUD's own matrices; the HUD's depth cannot be read, so none of it counts as hidden. The targets exist only while something is outlined. Deleted with the paths they served: LLFace::renderOneWireframe and its renderFace helper, LLSelectNode::renderOneSilhouette and its silhouette state, LLSelectMgr::generateSilhouette and the regeneration around it, LLVOVolume::generateSilhouette, LLVolume::generateSilhouetteVertices and LLVolumeFace::generateSilhouetteEdge, the silhouette texture and its preload, the silhouette U/V scroll statics, and LLXform::SILHOUETTE, whose seventeen setters only ORed in a bit nothing read any more. The settings those statics read stay in settings.xml. alselectionoutline_gl reads the real shader files: edges between and around ids, hidden parts, a surface not hidden by itself at lower render resolutions, the scissor, a rigged face following its pose, and the HUD's depth order and radius, under forward and reverse-Z. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/llvolume.cpp | 488 ----------- indra/llmath/llvolume.h | 16 - indra/llmath/xform.h | 3 +- indra/llrender/tests/llheadlessgl_fixture.h | 22 +- indra/newview/CMakeLists.txt | 11 + indra/newview/alselectionoutline.cpp | 378 ++++++++ indra/newview/alselectionoutline.h | 341 ++++++++ .../class1/interface/selectionIdF.glsl | 83 ++ .../class1/interface/selectionOutlineF.glsl | 99 +++ indra/newview/llagentcamera.h | 1 - indra/newview/llface.cpp | 64 -- indra/newview/llface.h | 1 - indra/newview/llselectmgr.cpp | 656 ++------------ indra/newview/llselectmgr.h | 18 +- indra/newview/llviewerobject.cpp | 26 +- indra/newview/llviewerobject.h | 4 +- indra/newview/llviewerobjectlist.cpp | 2 +- indra/newview/llviewershadermgr.cpp | 22 + indra/newview/llviewershadermgr.h | 3 + indra/newview/llviewertexturelist.cpp | 7 +- indra/newview/llviewerwindow.cpp | 5 +- indra/newview/llvovolume.cpp | 34 - indra/newview/llvovolume.h | 1 - indra/newview/pipeline.cpp | 8 +- .../tests/alselectionoutline_gl_test.cpp | 812 ++++++++++++++++++ 25 files changed, 1885 insertions(+), 1220 deletions(-) create mode 100644 indra/newview/alselectionoutline.cpp create mode 100644 indra/newview/alselectionoutline.h create mode 100644 indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl create mode 100644 indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl create mode 100644 indra/newview/tests/alselectionoutline_gl_test.cpp 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/tests/llheadlessgl_fixture.h b/indra/llrender/tests/llheadlessgl_fixture.h index 20a475c52c..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 diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 7c9b8edd21..8627dd591c 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -301,6 +301,7 @@ if(AL_BUILD_VIEWER) alpanelstreaminfo.cpp alpickitem.cpp alsceneexplorermodel.cpp + alselectionoutline.cpp alstreaminfo.cpp altoolalign.cpp altoolscenepicker.cpp @@ -1176,6 +1177,7 @@ if(AL_BUILD_VIEWER) alpanelstreaminfo.h alpickitem.h alsceneexplorermodel.h + alselectionoutline.h alstreaminfo.h altoolalign.h altoolscenepicker.h @@ -2540,6 +2542,15 @@ 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 + 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/alselectionoutline.cpp b/indra/newview/alselectionoutline.cpp new file mode 100644 index 0000000000..0e1faf2a67 --- /dev/null +++ b/indra/newview/alselectionoutline.cpp @@ -0,0 +1,378 @@ +/** + * @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 "llglstates.h" +#include "llselectmgr.h" +#include "llviewercamera.h" +#include "llviewerobject.h" +#include "llviewerregion.h" +#include "llviewershadermgr.h" +#include "llvovolume.h" +#include "pipeline.h" + +// llviewerdisplay.cpp: the matrices the HUD's attachments are drawn with. +bool get_hud_matrices(LLMatrix4a& proj, LLMatrix4a& model); + +ALSelectionOutline& ALSelectionOutline::instance() +{ + static ALSelectionOutline* outline = new ALSelectionOutline(); + return *outline; +} + +ALSelectionOutline::View ALSelectionOutline::worldView() +{ + const LLViewerCamera* camera = LLViewerCamera::getInstance(); + View view; + view.mModelview = LLViewerCamera::getCurrent().getModelview(); + view.mProjection = gGL.getProjectionMatrix(); + view.mRadius = radiusPixels(LLSelectMgr::sHighlightThickness, (F32)gGLViewport[3], camera->getView(), camera->getDefaultFOV()); + view.mHUD = false; + return view; +} + +bool ALSelectionOutline::hudView(View& view) +{ + if (!get_hud_matrices(view.mProjection, view.mModelview)) + { + return false; + } + view.mRadius = hudRadiusPixels(LLSelectMgr::sHighlightThickness, (F32)gGLViewport[3]); + view.mHUD = true; + return true; +} + +void ALSelectionOutline::add(LLViewerObject* object, U32 te_mask, const LLColor4& colour, bool show_hidden) +{ + if (!object) + { + return; + } + + const auto found = mEntryIndex.find(object); + if (found != mEntryIndex.end()) + { + mEntries[found->second] = { object, te_mask, colour, show_hidden }; + return; + } + + if (mEntries.size() >= MAX_IDS) + { + return; + } + mEntryIndex.emplace(object, (U32)mEntries.size()); + mEntries.push_back({ object, te_mask, colour, show_hidden }); +} + +void ALSelectionOutline::clearEntries() +{ + mEntries.clear(); + mEntryIndex.clear(); +} + +void ALSelectionOutline::release() +{ + // Called every frame nothing is outlined, so it touches nothing that is not there. + if (mIdMap.isComplete()) + { + mIdMap.release(); + } + if (mPalette) + { + LLImageGL::deleteTextures(1, &mPalette); + mPalette = 0; + } + mPaletteRows = 0; +} + +void ALSelectionOutline::render(const View& view) +{ + if (mEntries.empty()) + { + release(); + 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]; + if (width <= 0 || height <= 0 || !gSelectionIdProgram.isComplete() || !gSelectionIdProgram.mRiggedVariant || + !gSelectionOutlineProgram.isComplete() || !gPipeline.mRT || !gPipeline.mScreenTriangleVB) + { + clearEntries(); + return; + } + + if (!mIdMap.isComplete() || mIdMap.getWidth() != (U32)width || mIdMap.getHeight() != (U32)height) + { + if (!mIdMap.allocate(width, height, GL_RGBA8, true, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_NONE, + LLPipeline::mainDepthFormat())) + { + LL_WARNS_ONCE("Pipeline") << "Could not allocate the selection outline's id target" << LL_ENDL; + clearEntries(); + return; + } + } + + LL_PROFILE_GPU_ZONE("selection outline"); + + LLMatrix4a mvp; + mvp.setMul(view.mModelview, view.mProjection); + + const U32 count = (U32)mEntries.size(); + const U32 rows = paletteRows(count); + mPaletteTexels.assign((size_t)PALETTE_WIDTH * rows * 4, 0); + for (U32 i = 0; i < count; ++i) + { + writePalette(mPaletteTexels, i + 1, mEntries[i].mColour, mEntries[i].mShowHidden); + } + + LLGLSLShader* previous = LLGLSLShader::sCurBoundShaderPtr; + gGL.flush(); + + mScissorValid = false; + mLastAvatar = nullptr; + mLastMeshId = 0; + mSkipLastSkin = false; + + mIdMap.bindTarget(); + { + LLGLSColorMask mask(true, true); + // Depth writes on before the clear, which they mask. + LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_LEQUAL); + // Faces seen from behind are outlined too. + LLGLDisable cull(GL_CULL_FACE); + LLGLDisable blend(GL_BLEND); + LLGLDisable scissor(GL_SCISSOR_TEST); + + gGL.setClearColor(LLColor4(0.f, 0.f, 0.f, 0.f)); + mIdMap.clear(); + + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.pushMatrix(); + gGL.loadMatrix(view.mProjection); + 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) + { + drawObject(program, mEntries[i], i + 1, rigged, view, mvp); + } + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + } + + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.popMatrix(); + gGL.matrixMode(LLRender::MM_MODELVIEW); + gGL.popMatrix(); + } + mIdMap.flush(); + + if (mScissorValid) + { + uploadPalette(mPalette, mPaletteRows, mPaletteTexels, rows); + + 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, mPalette, view.mRadius, *gPipeline.mScreenTriangleVB); + } + + if (previous) + { + previous->bind(); + } + else + { + LLGLSLShader::unbind(); + } + + clearEntries(); +} + +void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U32 id, bool rigged, const View& view, + const LLMatrix4a& mvp) +{ + LLViewerObject* object = entry.mObject; + LLDrawable* drawable = object->mDrawable; + if (!drawable || drawable->isDead() || !drawable->getVOVolume()) + { + return; + } + + // 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; + } + } + + LLVector4a lo(FLT_MAX, FLT_MAX, FLT_MAX); + LLVector4a hi(-FLT_MAX, -FLT_MAX, -FLT_MAX); + bool bounded = true; + 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 box[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. + box[0] = face->mRiggedExtents[0]; + box[1] = face->mRiggedExtents[1]; + LLVector4a size; + size.setSub(box[1], box[0]); + if (!(size[0] > 0.f && size[1] > 0.f && size[2] > 0.f)) + { + bounded = false; + } + } + else if (agent_extents) + { + box[0] = face->mExtents[0]; + box[1] = face->mExtents[1]; + } + else + { + matMulBoundBox(*model, face->mExtents, box); + } + lo.setMin(lo, box[0]); + hi.setMax(hi, box[1]); + + if (!drawn) + { + gGL.loadMatrix(view.mModelview); + if (model) + { + gGL.multMatrix(model->getF32ptr()); + } + setId(program, id); + drawn = true; + } + + buffer->setBuffer(); + buffer->drawRange(LLRender::TRIANGLES, face->getGeomIndex(), face->getGeomIndex() + face->getGeomCount() - 1, + face->getIndicesCount(), face->getIndicesStart()); + } + + if (!drawn) + { + return; + } + + const S32 width = (S32)mIdMap.getWidth(); + const S32 height = (S32)mIdMap.getHeight(); + LLRect rect; + if (!bounded) + { + rect.set(0, height, width, 0); + } + else + { + const LLVector4a extents[2] = { lo, hi }; + if (!scissorRect(mvp, extents, width, height, view.mRadius, rect)) + { + return; + } + } + + if (mScissorValid) + { + mScissor.unionWith(rect); + } + else + { + mScissor = rect; + mScissorValid = true; + } +} diff --git a/indra/newview/alselectionoutline.h b/indra/newview/alselectionoutline.h new file mode 100644 index 0000000000..d062d87740 --- /dev/null +++ b/indra/newview/alselectionoutline.h @@ -0,0 +1,341 @@ +/** + * @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 "llglslshader.h" +#include "llimagegl.h" +#include "llmath.h" +#include "llmatrix4a.h" +#include "llrect.h" +#include "llrender.h" +#include "llrendertarget.h" +#include "llshadermgr.h" +#include "llstaticstringtable.h" +#include "llvertexbuffer.h" +#include "v4color.h" + +#include + +#include +#include +#include + +class LLViewerObject; +class LLVOAvatar; + +/// Outlines of the selected, rect-highlighted and media-focused objects, 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. A frame outlines one or the other, as the selection is in the world or on +/// the HUD. +/// +/// The id pass (interface/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, and in .b whether the scene's +/// depth shows the surface. Integer formats are not used because LLGLSLShader gives integer samplers no unit. +/// +/// The edge pass (interface/selectionOutlineF.glsl) is a full-screen triangle, scissored to the objects' +/// projected boxes grown by the outline's radius. It colours every pixel within the radius of a different id +/// from a palette holding a visible and a hidden colour per id: outside an object along its contour, and on both +/// sides of the edge between two objects, so every prim keeps its own outline. Hidden parts are dimmed or left +/// out per object. +/// +/// The targets exist only while something is outlined: they are made the first frame there is, given up the +/// first frame there is not, and given up with the pipeline's own (LLPipeline::releaseGLBuffers). +class ALSelectionOutline +{ +public: + /// 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 outline's width in pixels. + S32 mRadius = 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 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); + + /// 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; + /// Most rings of eight taps the edge pass reaches its radius with, however wide the outline. + static constexpr S32 MAX_RINGS = 8; + /// What a hidden part keeps of its outline's opacity. + static constexpr F32 HIDDEN_ALPHA = 0.4f; + + /// The uniforms the passes set beyond LLShaderMgr's reserved ones. + static constexpr const char* ID_UNIFORM = "selection_id"; + static constexpr const char* SCENE_DEPTH_UNIFORM = "selection_scene_depth"; + static constexpr const char* RADIUS_UNIFORM = "outline_radius"; + static constexpr const char* RINGS_UNIFORM = "outline_rings"; + + /// Lives for the session and is never destroyed: its GL objects go with the pipeline's, and at exit GL is gone. + static ALSelectionOutline& instance(); + + /// Outlines `object` this frame: its faces whose bit is set in te_mask, in `colour`, whose alpha is the + /// outline's opacity. Parts the scene hides are dimmed when show_hidden and left out otherwise. Adding an + /// object again replaces what it was added with. + void add(LLViewerObject* object, U32 te_mask, const LLColor4& colour, bool show_hidden); + + /// Draws what was added since the last call over the bound framebuffer, with `view`, and forgets it. With + /// nothing added it gives up its targets. + void render(const View& view); + + /// Gives up the targets. + void release(); + + /// 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); + + /// The outline's width in pixels for a view `view_height` pixels tall: `thickness` of the distance across the + /// line of sight, widened by the zoom (fov / default_fov), seen from that distance, so no distance changes it. + static S32 radiusPixels(F32 thickness, F32 view_height, F32 fov, F32 default_fov); + + /// The outline's width in pixels on the HUD, whose projection is a unit high and whose zoom scales its + /// objects: `thickness` of the view's height. + static S32 hudRadiusPixels(F32 thickness, F32 view_height) { return llmax(1, ll_round(thickness * view_height)); } + + /// Rings the edge pass taps to reach `radius`: one a pixel up to MAX_RINGS. + static S32 ringCount(S32 radius) { return llclamp(radius, 1, MAX_RINGS); } + + /// The pixels of a width x height target the outline of a box can touch: its projection through `mvp` grown + /// by `radius`, 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 radius, + 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 id the bound id program writes for the faces drawn next. + static void setId(LLGLSLShader& program, U32 id); + + /// The edge pass with the bound outline program, over the viewport and scissor in force. + static void drawEdges(LLGLSLShader& program, LLRenderTarget& ids, U32 palette, S32 radius, + LLVertexBuffer& triangle); + +private: + ALSelectionOutline() = default; + + struct Entry + { + LLViewerObject* mObject; + U32 mTEMask; + LLColor4 mColour; + bool mShowHidden; + }; + + // The faces of entry id - 1 that are rigged (or not) into the bound id program, and the box they cover into + // mScissor. + void drawObject(LLGLSLShader& program, const Entry& entry, U32 id, bool rigged, const View& view, + const LLMatrix4a& mvp); + + void clearEntries(); + + std::vector mEntries; + boost::unordered_flat_map mEntryIndex; + std::vector mPaletteTexels; + + LLRenderTarget mIdMap; + U32 mPalette = 0; + U32 mPaletteRows = 0; + + // 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::radiusPixels(F32 thickness, F32 view_height, F32 fov, F32 default_fov) +{ + // thickness * distance * (fov / default_fov) across the line of sight, and the projection puts + // view_height / (2 tan(fov / 2)) pixels to a unit of that at unit distance. + if (!(fov > 0.f) || !(default_fov > 0.f)) + { + return 1; + } + const F32 width = thickness * view_height * fov / (default_fov * 2.f * tanf(fov * 0.5f)); + return llmax(1, ll_round(width)); +} + +inline bool ALSelectionOutline::scissorRect(const LLMatrix4a& mvp, const LLVector4a extents[2], S32 width, S32 height, + S32 radius, 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) + radius + 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) - radius, llceil(max_y) + radius, llceil(max_x) + radius, llfloor(min_y) - radius); + 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) +{ + static const LLStaticHashedString scene_depth_uniform(SCENE_DEPTH_UNIFORM); + + 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(scene_depth_uniform, scene ? 1 : 0); + program.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, (F32)width, (F32)height); +} + +inline void ALSelectionOutline::setId(LLGLSLShader& program, U32 id) +{ + static const LLStaticHashedString id_uniform(ID_UNIFORM); + program.uniform1i(id_uniform, (GLint)id); +} + +inline void ALSelectionOutline::drawEdges(LLGLSLShader& program, LLRenderTarget& ids, U32 palette, S32 radius, + LLVertexBuffer& triangle) +{ + static const LLStaticHashedString radius_uniform(RADIUS_UNIFORM); + static const LLStaticHashedString rings_uniform(RINGS_UNIFORM); + + // Point sampled both: the ids and the palette are data, and texelFetch reads them whatever the filter. + program.bindTexture(LLShaderMgr::DIFFUSE_MAP, &ids, ALSamplers::PointClamp); + const S32 channel = program.enableTexture(LLShaderMgr::ALTERNATE_DIFFUSE_MAP); + if (channel > -1) + { + gGL.getTextureSlot(channel)->bindManual(ALTextureSlot::TT_TEXTURE, palette, gGL.getSampler(ALSamplers::PointClamp)); + } + program.uniform1i(radius_uniform, llmax(radius, 1)); + program.uniform1i(rings_uniform, ringCount(radius)); + + triangle.setBuffer(); + triangle.drawArrays(LLRender::TRIANGLES, 0, 3); + + program.unbindTexture(LLShaderMgr::ALTERNATE_DIFFUSE_MAP); + program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); +} 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..c34fd59601 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl @@ -0,0 +1,83 @@ +/** + * @file selectionIdF.glsl + * @brief The selection outline's id pass (ALSelectionOutline): a selected object's id, and whether the scene + * shows the surface. + * + * $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] + +// .rg: the id, low byte then high byte; .b: 1 where the scene shows this surface, 0 where it hides it. +out vec4 frag_color; + +// The object's id, 1 to 65535. 0 is what the target is cleared to: no selected object. +uniform int selection_id; + +// 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; + +// Distance in front of the eye of a stored depth, through the projection the scene and this pass share. +// 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); +} + +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 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; + } + + frag_color = vec4(float(selection_id & 255) / 255.0, float((selection_id >> 8) & 255) / 255.0, visible, 1.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..d1b2f512aa --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl @@ -0,0 +1,99 @@ +/** + * @file selectionOutlineF.glsl + * @brief The selection outline's edge pass (ALSelectionOutline): colours every pixel within the outline radius of + * a different selected object's id. + * + * $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 pass's target (selectionIdF.glsl): the id in .rg, low byte then high, 0 for none; 1 in .b where the +// scene shows the surface. +uniform sampler2D diffuseMap; + +// 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 altDiffuseMap; + +// The outline's width in pixels, and how many rings of eight taps reach out to it. +uniform int outline_radius; +uniform int outline_rings; + +#define PALETTE_WIDTH 256 + +const vec2 DIRECTIONS[8] = vec2[8](vec2(1.0, 0.0), vec2(-1.0, 0.0), vec2(0.0, 1.0), vec2(0.0, -1.0), + vec2(0.70710678, 0.70710678), vec2(-0.70710678, 0.70710678), + vec2(0.70710678, -0.70710678), vec2(-0.70710678, -0.70710678)); + +int decodeId(vec4 texel) +{ + return int(texel.r * 255.0 + 0.5) + (int(texel.g * 255.0 + 0.5) << 8); +} + +void main() +{ + ivec2 size = textureSize(diffuseMap, 0); + ivec2 pos = clamp(ivec2(tc * vec2(size)), ivec2(0), size - ivec2(1)); + vec4 own = texelFetch(diffuseMap, pos, 0); + int own_id = decodeId(own); + + // Nearest ring first. Off every object, the nearest object outlines itself here. On an object, another + // object's id is the edge between them, drawn on this side in this object's colour, so each keeps its own; + // where the object meets nothing selected it is left alone. + int id = 0; + vec4 source = own; + float dist = 0.0; + for (int ring = 1; ring <= outline_rings && id == 0; ++ring) + { + float reach = float(ring * outline_radius) / float(outline_rings); + for (int i = 0; i < 8; ++i) + { + ivec2 tap_pos = clamp(pos + ivec2(round(DIRECTIONS[i] * reach)), ivec2(0), size - ivec2(1)); + vec4 tap = texelFetch(diffuseMap, tap_pos, 0); + int tap_id = decodeId(tap); + if (tap_id != 0 && tap_id != own_id) + { + id = (own_id != 0) ? own_id : tap_id; + source = (own_id != 0) ? own : tap; + dist = reach; + break; + } + } + } + + if (id == 0) + { + discard; + } + + int row = (id / PALETTE_WIDTH) * 2 + ((source.b > 0.5) ? 0 : 1); + vec4 color = texelFetch(altDiffuseMap, ivec2(id % PALETTE_WIDTH, row), 0); + if (color.a <= 0.0) + { + discard; + } + + // Twice as bright at the edge and fading out to the radius. + float fade = (dist - 1.0) / float(outline_radius); + frag_color = vec4(mix(min(color.rgb * 2.0, vec3(1.0)), color.rgb, fade), color.a * (1.0 - fade)); +} 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/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index e8d25aa030..d85fd8e094 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -52,6 +52,7 @@ // viewer includes #include "alobjectproperties.h" +#include "alselectionoutline.h" #include "llagent.h" #include "llagentcamera.h" #include "llattachmentsmgr.h" @@ -221,8 +222,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; @@ -232,12 +231,7 @@ 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,9 @@ 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 +1833,6 @@ void LLSelectMgr::dump() LL_INFOS() << "Center global " << mSelectionCenterGlobal << LL_ENDL; } -//----------------------------------------------------------------------------- -// cleanup() -//----------------------------------------------------------------------------- -void LLSelectMgr::cleanup() -{ - mSilhouetteImagep = NULL; -} - - //--------------------------------------------------------------------------- // Manipulate properties of selected objects //--------------------------------------------------------------------------- @@ -6589,49 +6568,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 +6686,140 @@ 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. +void LLSelectMgr::addOutlines(ALSelectionOutline& outline, bool for_hud) { - 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; + if (objectp->getID() == focus_item_id) + { + colour = gFocusMgr.getFocusColor(); + } + else if (objectp->getID() == inspect_item_id) + { + colour = sHighlightInspectColor; + } + else if (node->isTransient()) + { + colour = sContextSilhouetteColor; + show_hidden = false; + } + else if (objectp->isRootEdit()) + { + colour = sSilhouetteParentColor; + } + else + { + colour = sSilhouetteChildColor; + } + colour.mV[VALPHA] = sHighlightAlpha; + outline.add(objectp, (U32)node->getTESelectMask(), colour, show_hidden); + } } 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; + if (subtracting_from_selection) + { + colour = LLColor4::red; + } + else if (!objectp->isSelected()) + { + colour = objectp->isRoot() ? sHighlightParentColor : sHighlightChildColor; + } + else + { + continue; } + colour.mV[VALPHA] = sHighlightAlpha; + outline.add(objectp, (U32)node->getTESelectMask(), colour, sRenderHiddenSelections); } } +} - if (isAgentAvatarValid() && for_hud) - { - gGL.matrixMode(LLRender::MM_PROJECTION); - gGL.popMatrix(); +// Outlines from an id target (ALSelectionOutline): one id per object, so every prim keeps its own outline. A +// frame calls this once, for the world or for the HUD as the selection is in one or on the other. +void LLSelectMgr::renderSilhouettes(bool for_hud) +{ + ALSelectionOutline& outline = ALSelectionOutline::instance(); - gGL.matrixMode(LLRender::MM_MODELVIEW); - gGL.popMatrix(); - gGL.popUIMatrix(); - stop_glerror(); + if (!mRenderHighlightSelections) + { + outline.release(); + 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) + ALSelectionOutline::View view; + if (!for_hud) + { + view = ALSelectionOutline::worldView(); + } + else if (!ALSelectionOutline::hudView(view)) { - ((LLVOVolume*)objectp)->generateSilhouette(nodep, view_point); + outline.release(); + return; } + + addOutlines(outline, for_hud); + outline.render(view); } // @@ -7126,7 +6832,6 @@ LLSelectNode::LLSelectNode(LLViewerObject* object, bool glow) mValid(false), mPermissions(new LLPermissions()), mInventorySerial(0), - mSilhouetteExists(false), mDuplicated(false), mTESelectMask(0), mLastTESelected(0), @@ -7172,9 +6877,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 +7230,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..ab0cf30b3d 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; @@ -447,12 +444,7 @@ class LLSelectMgr : public LLEditMenuHandler, public LLSimpleton 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 +616,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 +843,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); // 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 +906,6 @@ class LLSelectMgr : public LLEditMenuHandler, public LLSimpleton update_signal_t mUpdateSignal; private: - LLPointer mSilhouetteImagep; LLObjectSelectionHandle mSelectedObjects; LLObjectSelectionHandle mHoverObjects; LLObjectSelectionHandle mHighlightedObjects; @@ -935,7 +924,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/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 2514b26843..6f616737e1 100644 --- a/indra/newview/llviewershadermgr.cpp +++ b/indra/newview/llviewershadermgr.cpp @@ -154,6 +154,8 @@ LLGLSLShader gUnderWaterProgram; LLGLSLShader gHighlightProgram; LLGLSLShader gHighlightNormalProgram; LLGLSLShader gHighlightSpecularProgram; +LLGLSLShader gSelectionIdProgram; +LLGLSLShader gSelectionOutlineProgram; LLGLSLShader gDeferredHighlightProgram; @@ -3823,6 +3825,26 @@ bool LLViewerShaderMgr::loadShadersInterface() success = gHighlightSpecularProgram.createShader(); } + if (success) + { + gSelectionIdProgram.mName = "Selection Id Shader"; + gSelectionIdProgram.mShaderFiles.clear(); + gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/debugV.glsl", GL_VERTEX_SHADER)); + gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionIdF.glsl", GL_FRAGMENT_SHADER)); + gSelectionIdProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; + success = gSelectionIdProgram.createShader(LLGLSLShader::VARIANT_RIGGED); + } + + if (success) + { + gSelectionOutlineProgram.mName = "Selection Outline Shader"; + gSelectionOutlineProgram.mShaderFiles.clear(); + gSelectionOutlineProgram.mShaderFiles.push_back(make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); + gSelectionOutlineProgram.mShaderFiles.push_back(make_pair("interface/selectionOutlineF.glsl", GL_FRAGMENT_SHADER)); + gSelectionOutlineProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; + success = gSelectionOutlineProgram.createShader(); + } + if (success) { gUIProgram.mName = "UI Shader"; diff --git a/indra/newview/llviewershadermgr.h b/indra/newview/llviewershadermgr.h index ccc9f82e18..5ad5a6fddc 100644 --- a/indra/newview/llviewershadermgr.h +++ b/indra/newview/llviewershadermgr.h @@ -162,6 +162,9 @@ extern LLGLSLShader gLensDirtGenProgram; extern LLGLSLShader gHighlightProgram; extern LLGLSLShader gHighlightNormalProgram; extern LLGLSLShader gHighlightSpecularProgram; +// Selection outlines (ALSelectionOutline): the id pass, with a rigged variant, and the edge pass. +extern LLGLSLShader gSelectionIdProgram; +extern LLGLSLShader gSelectionOutlineProgram; extern LLGLSLShader gDeferredHighlightProgram; 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..606bbfe9e8 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 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 5acc08911b..4933ea1400 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() @@ -1390,6 +1391,7 @@ void LLPipeline::releaseGLBuffers() } ALFarPlaneOverlay::release(); + ALSelectionOutline::instance().release(); mHeroProbeManager.cleanup(); // release hero probes @@ -3598,7 +3600,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()); @@ -3744,10 +3746,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); } } diff --git a/indra/newview/tests/alselectionoutline_gl_test.cpp b/indra/newview/tests/alselectionoutline_gl_test.cpp new file mode 100644 index 0000000000..d3614642d4 --- /dev/null +++ b/indra/newview/tests/alselectionoutline_gl_test.cpp @@ -0,0 +1,812 @@ +/** + * @file alselectionoutline_gl_test.cpp + * @brief ALSelectionOutline's id and edge passes on the hidden window, with the viewer's own shader files: + * ids and edges, hidden parts, the scissor, a rigged pose, and the palette, radius 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 "../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 + +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 S32 RADIUS = 3; + + // 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; + + 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; } + + 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; + } + } + + 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_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(); + mOutlineProgram.unload(); + mShaders.clearShaderObjects(); + + mIdMap.release(); + mScene.release(); + mFar.release(); + mFrame.release(); + if (mPalette) + { + LLImageGL::deleteTextures(1, &mPalette); + mPalette = 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/debugV.glsl", GL_VERTEX_SHADER)); + mIdProgram.mShaderFiles.push_back(std::make_pair("interface/selectionIdF.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); + + mOutlineProgram.mName = "Selection Outline Shader"; + mOutlineProgram.mShaderFiles.clear(); + mOutlineProgram.mShaderFiles.push_back(std::make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); + mOutlineProgram.mShaderFiles.push_back(std::make_pair("interface/selectionOutlineF.glsl", GL_FRAGMENT_SHADER)); + mOutlineProgram.mShaderLevel = 1; + ensure("outline program builds", mOutlineProgram.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("frame target", mFrame.allocate(W, H, GL_RGBA8)); + 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_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); + } + + 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 over x0..x1, y0..y1, at depth z0 along its x0 edge and z1 along its x1 edge, every vertex weighted + // wholly to joint 0 when `weights`. + LLVertexBuffer* quad(F32 x0, F32 x1, F32 y0, F32 y1, F32 z0, F32 z1, bool weights = false) + { + LLPointer buffer = new LLVertexBuffer(LLVertexBuffer::MAP_VERTEX | (weights ? LLVertexBuffer::MAP_WEIGHT4 : 0)); + ensure("quad", buffer->allocateBuffer(4, 6)); + LLStrider vert; + LLStrider index; + buffer->getVertexStrider(vert); + buffer->getIndexStrider(index); + vert[0].set(x0, y0, z0); + vert[1].set(x1, y0, z1); + vert[2].set(x1, y1, z1); + vert[3].set(x0, y1, z0); + 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); + } + } + buffer->unmapBuffer(); + mBuffers.push_back(buffer); + return buffer.get(); + } + + 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); } + + struct Draw + { + LLVertexBuffer* mBuffer; + U32 mId; + }; + + // 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) + { + target.bindTarget(); + { + LLGLSColorMask mask(true, true); + LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_LEQUAL); + LLGLDisable cull(GL_CULL_FACE); + LLGLDisable blend(GL_BLEND); + glClearColor(0.f, 0.f, 0.f, 0.f); + target.clear(); + + const bool rigged = palette != nullptr; + LLGLSLShader& program = rigged ? *mIdProgram.mRiggedVariant : mIdProgram; + mIdProgram.bind(rigged); + ALSelectionOutline::bindIdPass(program, scene, (S32)target.getWidth(), (S32)target.getHeight()); + if (unread) + { + program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, unread); + } + if (rigged) + { + const F32 origin[3] = { 0.f, 0.f, 0.f }; + program.uniformMatrix3x4fv(LLShaderMgr::AVATAR_MATRIX, 1, GL_FALSE, palette); + program.uniform3fv(LLShaderMgr::SKIN_ORIGIN, 1, origin); + } + for (const Draw& draw : draws) + { + ALSelectionOutline::setId(program, draw.mId); + draw.mBuffer->setBuffer(); + draw.mBuffer->drawRange(LLRender::TRIANGLES, 0, 3, 6, 0); + } + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + LLGLSLShader::unbind(); + } + target.flush(); + } + + 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 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 = ALSelectionOutline::paletteRows(count); + mPaletteTexels.assign((size_t)ALSelectionOutline::PALETTE_WIDTH * mRows * 4, 0); + for (const auto& [id, colour] : colours) + { + ALSelectionOutline::writePalette(mPaletteTexels, id, colour, show_hidden); + } + } + + // The edge pass as ALSelectionOutline::render runs it, over a cleared frame, scissored when asked. + std::vector edgePass(S32 radius, const LLRect* scissor = nullptr) + { + ALSelectionOutline::uploadPalette(mPalette, mPaletteRows, mPaletteTexels, mRows); + + mFrame.bindTarget(); + std::vector px; + { + LLGLSColorMask mask(true, true); + glClearColor(0.f, 0.f, 0.f, 0.f); + mFrame.clear(); + + LLGLDepthTest depth(GL_FALSE); + LLGLEnable blend(GL_BLEND); + gGL.setSceneBlendType(LLRender::BT_ALPHA); + LLGLState scissor_test(GL_SCISSOR_TEST, scissor ? LLGLState::ENABLED_STATE : LLGLState::DISABLED_STATE); + if (scissor) + { + glScissor(scissor->mLeft, scissor->mBottom, scissor->getWidth(), scissor->getHeight()); + } + + mOutlineProgram.bind(); + ALSelectionOutline::drawEdges(mOutlineProgram, mIdMap, mPalette, radius, *mTriangle); + LLGLSLShader::unbind(); + px = ll_test::readFramebufferRGBA(W, H); + } + mFrame.flush(); + return px; + } + + LLRect boxRect(S32 radius) 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", ALSelectionOutline::scissorRect(mProjection, extents, W, H, radius, rect)); + return rect; + } + + ll_test::TestShaderMgr mShaders; + LLGLSLShader mIdProgram; + LLGLSLShader mOutlineProgram; + LLRenderTarget mIdMap; + LLRenderTarget mScene; + LLRenderTarget mFar; + LLRenderTarget mFrame; + U32 mPalette = 0; + U32 mPaletteRows = 0; + U32 mRows = 0; + std::vector mPaletteTexels; + LLPointer mTriangle; + std::vector> mBuffers; + LLMatrix4a mProjection; + 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 has an outline around its outer contour, in its own colour, and the edge + // between them is drawn on both sides, each in its own colour. Their insides and the space beyond the radius + // 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); + + palette({ { ID_A, RED }, { ID_B, GREEN } }, true); + const std::vector px = edgePass(RADIUS); + + const Pixel left = at(px, 8, 32); + ensure(named(reverse, "A outlined left of its contour"), red(left)); + ensure_equals(named(reverse, "at full strength beside it"), (U32)left.r, 255U); + ensure(named(reverse, "A outlined above"), red(at(px, 20, 44))); + ensure(named(reverse, "A outlined below"), red(at(px, 20, 19))); + ensure(named(reverse, "B outlined right of its contour"), green(at(px, 55, 32))); + ensure(named(reverse, "B outlined above"), green(at(px, 40, 44))); + + ensure(named(reverse, "fading out to the radius"), at(px, 6, 32).r > 0 && at(px, 6, 32).r < left.r); + ensure(named(reverse, "nothing past the radius"), black(at(px, 5, 32))); + ensure(named(reverse, "nothing far away"), black(at(px, 2, 2))); + + ensure(named(reverse, "A's side of the shared edge in A's colour"), red(at(px, 31, 32))); + ensure(named(reverse, "B's side of the shared edge in B's colour"), green(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(RADIUS); + + 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); + LLGLEnable scissor(GL_SCISSOR_TEST); + glScissor(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 } }); + const std::vector ids = read(mIdMap); + ensure_equals(named(reverse, "covered part marked hidden"), (U32)at(ids, 12, 32).b, 0U); + 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(RADIUS); + 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); + + palette({ { ID_A, RED } }, false); + px = edgePass(RADIUS); + 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 == 0 ? 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 radius: the scissored edge pass draws exactly what + // the unscissored one does. Without the growth the ring 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(RADIUS); + const LLRect rect = boxRect(RADIUS); + ensure_equals(named(reverse, "left: column 8.57 floored, less the radius"), rect.mLeft, 8 - RADIUS); + ensure_equals(named(reverse, "right: column 55.43 ceiled, plus the radius"), rect.mRight, 56 + RADIUS); + ensure_equals(named(reverse, "bottom: row 20.28 floored, less the radius"), rect.mBottom, 20 - RADIUS); + ensure_equals(named(reverse, "top: row 43.72 ceiled, plus the radius"), rect.mTop, 44 + RADIUS); + + const std::vector scissored = edgePass(RADIUS, &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(RADIUS, &tight); + ensure(named(reverse, "a box without the radius clips the ring"), lit(clipped) < lit(full)); + ensure(named(reverse, "the ring's outer pixels are what it loses"), !black(at(full, 6, 32)) && black(at(clipped, 6, 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", ALSelectionOutline::scissorRect(proj, straddling, W, H, RADIUS, 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", !ALSelectionOutline::scissorRect(proj, behind, W, H, RADIUS, 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", !ALSelectionOutline::scissorRect(proj, aside, W, H, RADIUS, 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(RADIUS); + 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", ALSelectionOutline::paletteRows(255), 2U); + ensure_equals("two from 256", ALSelectionOutline::paletteRows(256), 4U); + + std::vector texels((size_t)ALSelectionOutline::PALETTE_WIDTH * 4 * 4, 0); + ALSelectionOutline::writePalette(texels, 300, LLColor4(1.f, 0.5f, 0.f, 0.8f), true); + const size_t visible = ((size_t)2 * ALSelectionOutline::PALETTE_WIDTH + 44) * 4; + const size_t hidden = ((size_t)3 * ALSelectionOutline::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 * ALSelectionOutline::HIDDEN_ALPHA * 255.f)); + + ALSelectionOutline::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 radius: thickness times distance times the zoom, seen from that distance, so no distance changes it; it + // grows with the window and is at least a pixel. + template<> template<> + void alselectionoutline_object_t::test<9>() + { + const F32 fov = 60.f * DEG_TO_RAD; + ensure_equals("1080 rows at the default view", ALSelectionOutline::radiusPixels(0.01f, 1080.f, fov, fov), 9); + ensure_equals("2160 rows", ALSelectionOutline::radiusPixels(0.01f, 2160.f, fov, fov), 19); + ensure_equals("zoomed in to half the view", ALSelectionOutline::radiusPixels(0.01f, 1080.f, fov * 0.5f, fov), 10); + ensure_equals("never under a pixel", ALSelectionOutline::radiusPixels(0.01f, 10.f, fov, fov), 1); + ensure_equals("no view, a pixel", ALSelectionOutline::radiusPixels(0.01f, 1080.f, 0.f, fov), 1); + ensure_equals("rings: one a pixel", ALSelectionOutline::ringCount(3), 3); + ensure_equals("rings: at most MAX_RINGS", ALSelectionOutline::ringCount(19), ALSelectionOutline::MAX_RINGS); + } + + // 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_equals(named(reverse, "the world's pass hides A behind it"), (U32)at(read(mIdMap), 20, 32).b, 0U); + + 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(RADIUS); + 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 HUD's radius: its projection is a unit high and its zoom scales objects and thickness alike, so the + // outline is `thickness` of the view's height whatever the zoom. A band a tenth of that projection high covers + // the rows a thickness of a tenth gives. + template<> template<> + void alselectionoutline_object_t::test<12>() + { + ensure_equals("1080 rows", ALSelectionOutline::hudRadiusPixels(0.01f, 1080.f), 11); + ensure_equals("2160 rows", ALSelectionOutline::hudRadiusPixels(0.01f, 2160.f), 22); + ensure_equals("never under a pixel", ALSelectionOutline::hudRadiusPixels(0.01f, 10.f), 1); + + setUp(false); + const LLMatrix4a hud = al_ortho(-0.5f, 0.5f, -0.5f, 0.5f, 0.f, 10.f); + LLVertexBuffer* band = quad(-0.4f, 0.4f, 0.f, 0.1f, -2.f, -2.f); + const std::vector ids = projectedIds(hud, { { band, ID_A } }); + S32 rows = 0; + for (S32 y = 0; y < H; ++y) + { + rows += idAt(ids, 32, y) == ID_A ? 1 : 0; + } + ensure_equals("a band a tenth of the projection high", rows, ALSelectionOutline::hudRadiusPixels(0.1f, (F32)H)); + } +} From ff69000bea2f80cff280c786f2dfc4edfc74655a Mon Sep 17 00:00:00 2001 From: Rye Date: Fri, 9 Oct 2026 21:44:05 -0400 Subject: [PATCH 04/11] The HUD is depth-tested against itself, and the scene's depth is put back after it 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, and nothing cleared it. get_hud_matrices squeezes the HUD into a band of that depth in front of the scene, 0.005 of the range per metre of the HUD's own depth, so a world surface at the near plane still reached into it, and a HUD with a prim parked far along its axis put its rear within reach of anything about two near-plane distances away: world geometry drew through HUD prims. For each HUD attachment point with something on it, the HUD's box through the matrices it is drawn with becomes a viewport rect, overlapping rects merged, so a corner HUD and a centre one do not take in the screen between them. The depth is cleared in those rects before the HUD draws, and the scene's is written back over them after it, wherever it is nearer, with the same full-screen triangle and samples the final blit used (gCopyDepthProgram: copyF.glsl with COPY_DEPTH and DEPTH_ONLY). Name tags, hover text, look-at, script icons and the tracker, all drawn after the HUD and depth-tested, are hidden by the world as before, and by the HUD where it is in front. The GL side is ALHUDDepth (alhuddepth.h). LLVOAvatar::getHUDBBox gains a per-attachment-point form sharing its loop. The point that fixes the box's frame is now the object's centre on the HUD rather than its offset from its attachment point, which belongs to another frame and stretched a corner HUD's box towards the middle of the screen; the x extent that get_hud_matrices reads is unchanged, since every HUD point sits at x = 0. alhuddepth_gl, under forward and reverse-Z, with the real copy shader: without the clear, a world surface at the near plane hides the HUD; with it, the HUD shows; after the restore an overlay behind the world is hidden off the HUD and behind the HUD on it; outside the rects nothing changes; and the rect and merge arithmetic. Co-Authored-By: Claude Opus 5.5 --- indra/newview/CMakeLists.txt | 10 + indra/newview/alhuddepth.h | 157 +++++ .../shaders/class1/interface/copyF.glsl | 7 + indra/newview/llviewerdisplay.cpp | 56 ++ indra/newview/llviewershadermgr.cpp | 14 + indra/newview/llviewershadermgr.h | 1 + indra/newview/llvoavatar.cpp | 65 +- indra/newview/llvoavatar.h | 2 + indra/newview/tests/alhuddepth_gl_test.cpp | 572 ++++++++++++++++++ 9 files changed, 860 insertions(+), 24 deletions(-) create mode 100644 indra/newview/alhuddepth.h create mode 100644 indra/newview/tests/alhuddepth_gl_test.cpp diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 8627dd591c..36d562df83 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -1145,6 +1145,7 @@ if(AL_BUILD_VIEWER) alfloaterxuistudio.h alviewcapture.h algradehistory.h + alhuddepth.h allegacynotificationwellwindow.h allightboxdirectory.h almotionpredictor.h @@ -2551,6 +2552,15 @@ if(AL_BUILD_TESTS) LIBRARIES llrender 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/alhuddepth.h b/indra/newview/alhuddepth.h new file mode 100644 index 0000000000..56c95b2c7a --- /dev/null +++ b/indra/newview/alhuddepth.h @@ -0,0 +1,157 @@ +/** + * @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). +inline void clear(const std::vector& rects, S32 x, S32 y) +{ + // 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/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/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 7b4070ed32..8339218485 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" @@ -140,6 +141,7 @@ void render_hud_attachments(); void render_ui_3d(); void render_ui_2d(); void render_disconnected_background(); +bool get_hud_matrices(LLMatrix4a& proj, LLMatrix4a& model); void getProfileStatsContext(LLSD& stats); std::string getProfileStatsFilename(); @@ -1284,6 +1286,48 @@ 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(); + LLMatrix4a proj; + LLMatrix4a model; + if (!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). +static void restore_scene_depth(const std::vector& rects) +{ + if (!gPipeline.mRT || !gPipeline.mScreenTriangleVB) + { + 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 +1416,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(hud_rects, gGLViewport[0], gGLViewport[1]); + gPipeline.renderGeomPostDeferred(hud_cam); LLSpatialGroup::sNoDelete = false; @@ -1379,6 +1433,8 @@ void render_hud_attachments() render_hud_elements(); + restore_scene_depth(hud_rects); + //restore type mask gPipeline.popRenderTypeMask(); diff --git a/indra/newview/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp index 6f616737e1..9ff559e4da 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; @@ -4062,6 +4063,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 5ad5a6fddc..f2998cf52c 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; 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/tests/alhuddepth_gl_test.cpp b/indra/newview/tests/alhuddepth_gl_test.cpp new file mode 100644 index 0000000000..cf21d09ebc --- /dev/null +++ b/indra/newview/tests/alhuddepth_gl_test.cpp @@ -0,0 +1,572 @@ +/** + * @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(); + } + + void clearUnderHUD() + { + mWindow.bindTarget(); + ALHUDDepth::clear(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() + { + mWindow.bindTarget(); + ALHUDDepth::restore(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)); + } +} From 55c962680b46df5a6c3af35e8005b0d245f5f862 Mon Sep 17 00:00:00 2001 From: Rye Date: Fri, 9 Oct 2026 22:54:27 -0400 Subject: [PATCH 05/11] Selection outlines follow what is drawn, in front of what, at a width that is a setting The id pass draws a face as the scene draws it. Where its alpha mode cuts it out, it is cut by its texture's alpha alone: a legacy mask at its cutoff, a blend at half, a GLTF face by its mode and cutoff, through the base colour's transform (selectionIdV.glsl) and the face's texture animation. The face's colour is not read, so a prim made invisible by it is still outlined. Back faces are culled unless a GLTF material is double sided. Before, every face was its whole card, from both sides: a single-sided frond seen from behind took the pixel from the one behind it. Ids are given in priority order (subtract, focus, inspect, root, child, context) and the target carries the priority, so a root's contour is laid over its children's rather than lost under them. The contour is ordered by depth, from the id target's own. A pixel draws another object's contour where that object is nearer than the pixel's own surface, its plane carried over to the other's texel, by 0.2% of the distance: a frond over another is outlined over it, while coplanar prims meeting at a grazing angle are not. Objects that touch get a thinner, fainter edge, on the side of the higher priority. The target records whether each texel is drawn at all (visible, hidden and dimmed, hidden and left out), so a hidden selected surface, such as a root under the terrain, neither draws nor takes the contour of what stands in front of it. The contour is AlchemySelectionOutlineWidth pixels wide at a UI scale of 1, 4 by default, held to 1 to 16 and read live, solid to its width from a full disk of taps and anti-aliased across one pixel more. The edges between objects are a pixel at a UI scale of 1, at 60%. A tile pass (selectionTileF.glsl) flags each 8x8 square of the id target holding an id change, and the edge pass leaves a pixel at once when no flagged square lies within its reach. Whatever a pixel draws comes from a texel of another id within that reach, so the output is the same. LLSelectMgr::sHighlightThickness, read by nothing since the CPU silhouettes went, is gone. alselectionoutline_gl, 21 cases under forward and reverse-Z with the real shaders: the alpha cut and its transform, priorities, widths and the setting's clamp, culling and double-sided faces, contours over hidden and in-front objects, coplanar seams, and the tiled output against the untiled one. Co-Authored-By: Claude Opus 5.5 --- indra/newview/CMakeLists.txt | 1 + indra/newview/alselectionoutline.cpp | 168 +++- indra/newview/alselectionoutline.h | 351 ++++++- .../newview/app_settings/settings_alchemy.xml | 11 + .../class1/interface/selectionIdF.glsl | 40 +- .../class1/interface/selectionIdV.glsl | 54 + .../class1/interface/selectionOutlineF.glsl | 263 ++++- .../class1/interface/selectionTileF.glsl | 82 ++ indra/newview/llselectmgr.cpp | 15 +- indra/newview/llselectmgr.h | 1 - indra/newview/llviewershadermgr.cpp | 13 +- indra/newview/llviewershadermgr.h | 3 +- .../tests/alselectionoutline_gl_test.cpp | 934 +++++++++++++++--- 13 files changed, 1693 insertions(+), 243 deletions(-) create mode 100644 indra/newview/app_settings/shaders/class1/interface/selectionIdV.glsl create mode 100644 indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 36d562df83..c14702fec4 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -2550,6 +2550,7 @@ if(AL_BUILD_TESTS) 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 diff --git a/indra/newview/alselectionoutline.cpp b/indra/newview/alselectionoutline.cpp index 0e1faf2a67..3a8aa46c05 100644 --- a/indra/newview/alselectionoutline.cpp +++ b/indra/newview/alselectionoutline.cpp @@ -29,18 +29,35 @@ #include "lldrawable.h" #include "lldrawpool.h" #include "llface.h" +#include "llfetchedgltfmaterial.h" #include "llglstates.h" -#include "llselectmgr.h" +#include "lltextureentry.h" +#include "llui.h" #include "llviewercamera.h" +#include "llviewercontrol.h" #include "llviewerobject.h" #include "llviewerregion.h" #include "llviewershadermgr.h" +#include "llviewertexture.h" #include "llvovolume.h" #include "pipeline.h" +#include + // llviewerdisplay.cpp: the matrices the HUD's attachments are drawn with. bool get_hud_matrices(LLMatrix4a& proj, LLMatrix4a& model); +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& ALSelectionOutline::instance() { static ALSelectionOutline* outline = new ALSelectionOutline(); @@ -49,27 +66,26 @@ ALSelectionOutline& ALSelectionOutline::instance() ALSelectionOutline::View ALSelectionOutline::worldView() { - const LLViewerCamera* camera = LLViewerCamera::getInstance(); - View view; - view.mModelview = LLViewerCamera::getCurrent().getModelview(); - view.mProjection = gGL.getProjectionMatrix(); - view.mRadius = radiusPixels(LLSelectMgr::sHighlightThickness, (F32)gGLViewport[3], camera->getView(), camera->getDefaultFOV()); - view.mHUD = false; - return view; + static LLCachedControl contour_width(gSavedSettings, "AlchemySelectionOutlineWidth", DEFAULT_CONTOUR_WIDTH); + return makeView(LLViewerCamera::getCurrent().getModelview(), gGL.getProjectionMatrix(), contour_width, + LLUI::getScaleFactor().mV[VX], false); } bool ALSelectionOutline::hudView(View& view) { - if (!get_hud_matrices(view.mProjection, view.mModelview)) + static LLCachedControl contour_width(gSavedSettings, "AlchemySelectionOutlineWidth", DEFAULT_CONTOUR_WIDTH); + LLMatrix4a projection; + LLMatrix4a modelview; + if (!get_hud_matrices(projection, modelview)) { return false; } - view.mRadius = hudRadiusPixels(LLSelectMgr::sHighlightThickness, (F32)gGLViewport[3]); - view.mHUD = true; + view = makeView(modelview, projection, contour_width, LLUI::getScaleFactor().mV[VX], true); return true; } -void ALSelectionOutline::add(LLViewerObject* object, U32 te_mask, const LLColor4& colour, bool show_hidden) +void ALSelectionOutline::add(LLViewerObject* object, U32 te_mask, const LLColor4& colour, bool show_hidden, + EPriority priority) { if (!object) { @@ -79,7 +95,7 @@ void ALSelectionOutline::add(LLViewerObject* object, U32 te_mask, const LLColor4 const auto found = mEntryIndex.find(object); if (found != mEntryIndex.end()) { - mEntries[found->second] = { object, te_mask, colour, show_hidden }; + mEntries[found->second] = { object, te_mask, colour, show_hidden, priority }; return; } @@ -88,7 +104,7 @@ void ALSelectionOutline::add(LLViewerObject* object, U32 te_mask, const LLColor4 return; } mEntryIndex.emplace(object, (U32)mEntries.size()); - mEntries.push_back({ object, te_mask, colour, show_hidden }); + mEntries.push_back({ object, te_mask, colour, show_hidden, priority }); } void ALSelectionOutline::clearEntries() @@ -104,6 +120,10 @@ void ALSelectionOutline::release() { mIdMap.release(); } + if (mTileMap.isComplete()) + { + mTileMap.release(); + } if (mPalette) { LLImageGL::deleteTextures(1, &mPalette); @@ -129,18 +149,22 @@ void ALSelectionOutline::render(const View& view) const S32 width = gGLViewport[2]; const S32 height = gGLViewport[3]; if (width <= 0 || height <= 0 || !gSelectionIdProgram.isComplete() || !gSelectionIdProgram.mRiggedVariant || - !gSelectionOutlineProgram.isComplete() || !gPipeline.mRT || !gPipeline.mScreenTriangleVB) + !gSelectionTileProgram.isComplete() || !gSelectionOutlineProgram.isComplete() || !gPipeline.mRT || + !gPipeline.mScreenTriangleVB) { clearEntries(); return; } - if (!mIdMap.isComplete() || mIdMap.getWidth() != (U32)width || mIdMap.getHeight() != (U32)height) + if (!mIdMap.isComplete() || !mTileMap.isComplete() || mIdMap.getWidth() != (U32)width || + mIdMap.getHeight() != (U32)height) { if (!mIdMap.allocate(width, height, GL_RGBA8, true, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_NONE, - LLPipeline::mainDepthFormat())) + LLPipeline::mainDepthFormat()) || + !mTileMap.allocate(tileCount((U32)width), tileCount((U32)height), GL_R8)) { - LL_WARNS_ONCE("Pipeline") << "Could not allocate the selection outline's id target" << LL_ENDL; + LL_WARNS_ONCE("Pipeline") << "Could not allocate the selection outline's id and tile targets" << LL_ENDL; + release(); clearEntries(); return; } @@ -151,12 +175,21 @@ void ALSelectionOutline::render(const View& view) 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) { - writePalette(mPaletteTexels, i + 1, mEntries[i].mColour, mEntries[i].mShowHidden); + const Entry& entry = mEntries[mOrder[i]]; + writePalette(mPaletteTexels, i + 1, entry.mColour, entry.mShowHidden); } LLGLSLShader* previous = LLGLSLShader::sCurBoundShaderPtr; @@ -169,13 +202,8 @@ void ALSelectionOutline::render(const View& view) mIdMap.bindTarget(); { - LLGLSColorMask mask(true, true); // Depth writes on before the clear, which they mask. - LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_LEQUAL); - // Faces seen from behind are outlined too. - LLGLDisable cull(GL_CULL_FACE); - LLGLDisable blend(GL_BLEND); - LLGLDisable scissor(GL_SCISSOR_TEST); + IdPassState state; gGL.setClearColor(LLColor4(0.f, 0.f, 0.f, 0.f)); mIdMap.clear(); @@ -183,6 +211,10 @@ void ALSelectionOutline::render(const View& view) 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(); @@ -199,13 +231,16 @@ void ALSelectionOutline::render(const View& view) bindIdPass(program, view.mHUD ? nullptr : &gPipeline.mRT->deferredScreen, width, height); for (U32 i = 0; i < count; ++i) { - drawObject(program, mEntries[i], i + 1, rigged, view, mvp); + drawObject(program, mEntries[mOrder[i]], 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(); } @@ -215,6 +250,11 @@ void ALSelectionOutline::render(const View& view) { uploadPalette(mPalette, mPaletteRows, mPaletteTexels, rows); + // The scissor holds every selected texel and the contour's reach around them, more than the texel the tile + // pass needs. + gSelectionTileProgram.bind(); + drawTiles(gSelectionTileProgram, mIdMap, mTileMap, mScissor, *gPipeline.mScreenTriangleVB); + LLGLSColorMask mask(true, false); LLGLDepthTest depth(GL_FALSE); LLGLEnable blend(GL_BLEND); @@ -222,7 +262,7 @@ void ALSelectionOutline::render(const View& view) LLGLSScissor scissor(vp_x + mScissor.mLeft, vp_y + mScissor.mBottom, mScissor.getWidth(), mScissor.getHeight()); gSelectionOutlineProgram.bind(); - drawEdges(gSelectionOutlineProgram, mIdMap, mPalette, view.mRadius, *gPipeline.mScreenTriangleVB); + drawEdges(gSelectionOutlineProgram, mIdMap, mTileMap, mPalette, view, *gPipeline.mScreenTriangleVB); } if (previous) @@ -336,13 +376,20 @@ void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U { gGL.multMatrix(model->getF32ptr()); } - setId(program, id); + setId(program, id, entry.mPriority, entry.mShowHidden); drawn = true; } - buffer->setBuffer(); - buffer->drawRange(LLRender::TRIANGLES, face->getGeomIndex(), face->getGeomIndex() + face->getGeomCount() - 1, - face->getIndicesCount(), face->getIndicesStart()); + 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); + } } if (!drawn) @@ -360,7 +407,7 @@ void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U else { const LLVector4a extents[2] = { lo, hi }; - if (!scissorRect(mvp, extents, width, height, view.mRadius, rect)) + if (!scissorRect(mvp, extents, width, height, lineReach(view.mWidth), rect)) { return; } @@ -376,3 +423,60 @@ void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U mScissorValid = true; } } + +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 = gltfAlphaCutoff(gltf->mAlphaMode, gltf->mAlphaCutoff); + } + gltf->mTextureTransform[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR].getPacked(transform); + } + else if (te && texture) + { + const LLMaterial* material = te->getMaterialParams().get(); + cutoff = 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 index d062d87740..5f94e22be8 100644 --- a/indra/newview/alselectionoutline.h +++ b/indra/newview/alselectionoutline.h @@ -24,8 +24,11 @@ #pragma once +#include "llgltfmaterial.h" #include "llglslshader.h" +#include "llglstates.h" #include "llimagegl.h" +#include "llmaterial.h" #include "llmath.h" #include "llmatrix4a.h" #include "llrect.h" @@ -38,10 +41,12 @@ #include +#include #include #include #include +class LLFace; class LLViewerObject; class LLVOAvatar; @@ -50,30 +55,69 @@ class LLVOAvatar; /// after the HUD, with its matrices. A frame outlines one or the other, as the selection is in the world or on /// the HUD. /// -/// The id pass (interface/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, and in .b whether the scene's -/// depth shows the surface. Integer formats are not used because LLGLSLShader gives integer samplers no unit. +/// 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. Integer formats +/// are not used because LLGLSLShader gives integer samplers no unit. /// -/// The edge pass (interface/selectionOutlineF.glsl) is a full-screen triangle, scissored to the objects' -/// projected boxes grown by the outline's radius. It colours every pixel within the radius of a different id -/// from a palette holding a visible and a hidden colour per id: outside an object along its contour, and on both -/// sides of the edge between two objects, so every prim keeps its own outline. Hidden parts are dimmed or left -/// out per object. +/// Ids are given in priority order, the highest first (EPriority). The edge pass +/// (interface/selectionOutlineF.glsl) is a full-screen triangle, scissored to the objects' projected boxes grown +/// by the contour's reach. A pixel draws the contour of the drawn objects within its reach that stand in front of +/// it: every object, off every object; on an object, another object 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: that would trace the creases of every mesh. Colours +/// come from a palette holding a visible and a hidden colour per id. +/// +/// The disk a pixel searches grows with the square of the contour's width, and most pixels in the scissor are far +/// from any edge. The tile pass (interface/selectionTileF.glsl) flags each TILE_SIZE square of the id target that +/// holds an id change, a texel whose right or upper neighbour has a different id, and the edge pass leaves a pixel +/// at once when no tile within its reach is flagged. Whatever a pixel draws, it draws from a texel within its reach +/// whose id is not its own, and between the two, inside the square its reach spans, two neighbouring texels differ, +/// so the early out changes nothing it draws. /// /// The targets exist only while something is outlined: they are made the first frame there is, given up the /// first frame there is not, and given up with the pipeline's own (LLPipeline::releaseGLBuffers). class 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, + PRIORITY_COUNT + }; + /// 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 outline's width in pixels. - S32 mRadius = 1; + /// 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. @@ -86,28 +130,54 @@ class ALSelectionOutline /// 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. + static View makeView(const LLMatrix4a& modelview, const LLMatrix4a& projection, F32 contour_width, F32 ui_scale, + bool hud); + /// 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; - /// Most rings of eight taps the edge pass reaches its radius with, however wide the outline. - static constexpr S32 MAX_RINGS = 8; /// 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 = 4.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 flags, 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: where it is drawn at least half opaque. + static constexpr F32 BLEND_ALPHA_CUTOFF = 0.5f; + /// 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 uniforms the passes set beyond LLShaderMgr's reserved ones. static constexpr const char* ID_UNIFORM = "selection_id"; + static constexpr const char* PRIORITY_UNIFORM = "selection_priority"; + static constexpr const char* SHOW_HIDDEN_UNIFORM = "selection_show_hidden"; static constexpr const char* SCENE_DEPTH_UNIFORM = "selection_scene_depth"; - static constexpr const char* RADIUS_UNIFORM = "outline_radius"; - static constexpr const char* RINGS_UNIFORM = "outline_rings"; + static constexpr const char* WIDTH_UNIFORM = "outline_width"; + static constexpr const char* INNER_WIDTH_UNIFORM = "outline_inner_width"; /// Lives for the session and is never destroyed: its GL objects go with the pipeline's, and at exit GL is gone. static ALSelectionOutline& instance(); /// Outlines `object` this frame: its faces whose bit is set in te_mask, in `colour`, whose alpha is the - /// outline's opacity. Parts the scene hides are dimmed when show_hidden and left out otherwise. Adding an - /// object again replaces what it was added with. - void add(LLViewerObject* object, U32 te_mask, const LLColor4& colour, bool show_hidden); + /// outline's opacity, at `priority`. Parts the scene hides are dimmed when show_hidden and left out otherwise. + /// Adding an object again replaces what it was added with. + void add(LLViewerObject* object, U32 te_mask, const LLColor4& colour, bool show_hidden, EPriority priority); /// Draws what was added since the last call over the bound framebuffer, with `view`, and forgets it. With /// nothing added it gives up its targets. @@ -122,21 +192,36 @@ class ALSelectionOutline /// 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); - /// The outline's width in pixels for a view `view_height` pixels tall: `thickness` of the distance across the - /// line of sight, widened by the zoom (fov / default_fov), seen from that distance, so no distance changes it. - static S32 radiusPixels(F32 thickness, F32 view_height, F32 fov, F32 default_fov); + /// 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); + + /// 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 outline's width in pixels on the HUD, whose projection is a unit high and whose zoom scales its - /// objects: `thickness` of the view's height. - static S32 hudRadiusPixels(F32 thickness, F32 view_height) { return llmax(1, ll_round(thickness * view_height)); } + /// 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); - /// Rings the edge pass taps to reach `radius`: one a pixel up to MAX_RINGS. - static S32 ringCount(S32 radius) { return llclamp(radius, 1, MAX_RINGS); } + /// 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 `radius`, clipped to the target. A box reaching behind the eye takes the whole target. + /// 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 radius, + 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. @@ -146,13 +231,43 @@ class ALSelectionOutline /// decides what is hidden; with no scene every surface counts as visible. static void bindIdPass(LLGLSLShader& program, LLRenderTarget* scene, S32 width, S32 height); - /// The id the bound id program writes for the faces drawn next. - static void setId(LLGLSLShader& program, U32 id); + /// 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. + static void setId(LLGLSLShader& program, U32 id, EPriority priority, bool show_hidden); - /// The edge pass with the bound outline program, over the viewport and scissor in force. - static void drawEdges(LLGLSLShader& program, LLRenderTarget& ids, U32 palette, S32 radius, + /// 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 tile pass with the bound tile program: flags into `tiles`, a texel to each TILE_SIZE square of `ids`, + /// over the tiles under `rect`, a rect of `ids`; the others are cleared. Every square holding an id change + /// lies under the rect when it covers every selected texel and a texel more around them. + static void drawTiles(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& tiles, const LLRect& rect, LLVertexBuffer& triangle); + /// The edge pass with the bound outline program and `view`'s widths, over the viewport and scissor in force, + /// reading the tile pass's flags from `tiles` and the depth of `ids` through `view`'s projection, which the + /// id pass drew it with. + static void drawEdges(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& tiles, U32 palette, + const View& view, LLVertexBuffer& triangle); + private: ALSelectionOutline() = default; @@ -162,20 +277,37 @@ class ALSelectionOutline U32 mTEMask; LLColor4 mColour; bool mShowHidden; + EPriority mPriority; }; - // The faces of entry id - 1 that are rigged (or not) into the bound id program, and the box they cover into + // The faces of `entry` that are rigged (or not) into the bound id program as `id`, and the box they cover into // mScissor. void drawObject(LLGLSLShader& program, const Entry& entry, U32 id, bool rigged, const View& view, const LLMatrix4a& mvp); + // What the id pass draws a face 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 id 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 mTileMap; U32 mPalette = 0; U32 mPaletteRows = 0; @@ -210,20 +342,71 @@ inline void ALSelectionOutline::writePalette(std::vector& texels, U32 id, co texels[hidden + 3] = show_hidden ? unorm(colour.mV[VALPHA] * HIDDEN_ALPHA) : 0; } -inline S32 ALSelectionOutline::radiusPixels(F32 thickness, F32 view_height, F32 fov, F32 default_fov) +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 ALSelectionOutline::View ALSelectionOutline::makeView(const LLMatrix4a& modelview, const LLMatrix4a& projection, + F32 contour_width, F32 ui_scale, bool hud) { - // thickness * distance * (fov / default_fov) across the line of sight, and the projection puts - // view_height / (2 tan(fov / 2)) pixels to a unit of that at unit distance. - if (!(fov > 0.f) || !(default_fov > 0.f)) + 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; + return view; +} + +inline void ALSelectionOutline::priorityOrder(const std::vector& priorities, std::vector& order) +{ + order.resize(priorities.size()); + for (U32 i = 0; i < (U32)order.size(); ++i) { - return 1; + 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; } - const F32 width = thickness * view_height * fov / (default_fov * 2.f * tanf(fov * 0.5f)); - return llmax(1, ll_round(width)); } inline bool ALSelectionOutline::scissorRect(const LLMatrix4a& mvp, const LLVector4a extents[2], S32 width, S32 height, - S32 radius, LLRect& rect) + S32 reach, LLRect& rect) { const LLRect target(0, height, width, 0); F32 min_x = FLT_MAX; @@ -260,13 +443,13 @@ inline bool ALSelectionOutline::scissorRect(const LLMatrix4a& mvp, const LLVecto } // Projections far off screen are clamped before they become integers. - const F32 limit = (F32)(llmax(width, height) + radius + 1); + 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) - radius, llceil(max_y) + radius, llceil(max_x) + radius, llfloor(min_y) - radius); + rect.set(llfloor(min_x) - reach, llceil(max_y) + reach, llceil(max_x) + reach, llfloor(min_y) - reach); rect.intersectWith(target); return rect.notEmpty(); } @@ -311,31 +494,97 @@ inline void ALSelectionOutline::bindIdPass(LLGLSLShader& program, LLRenderTarget program.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, (F32)width, (F32)height); } -inline void ALSelectionOutline::setId(LLGLSLShader& program, U32 id) +inline void ALSelectionOutline::setId(LLGLSLShader& program, U32 id, EPriority priority, bool show_hidden) { static const LLStaticHashedString id_uniform(ID_UNIFORM); + static const LLStaticHashedString priority_uniform(PRIORITY_UNIFORM); + static const LLStaticHashedString show_hidden_uniform(SHOW_HIDDEN_UNIFORM); program.uniform1i(id_uniform, (GLint)id); + program.uniform1i(priority_uniform, (GLint)priority); + program.uniform1i(show_hidden_uniform, show_hidden ? 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 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::drawTiles(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& tiles, + const LLRect& rect, LLVertexBuffer& triangle) +{ + tiles.bindTarget(); + { + LLGLSColorMask mask(true, true); + LLGLDepthTest depth(GL_FALSE); + LLGLDisable blend(GL_BLEND); + { + // The tiles the pass leaves out hold no change, and read as none. + LLGLDisable scissor(GL_SCISSOR_TEST); + gGL.setClearColor(LLColor4(0.f, 0.f, 0.f, 0.f)); + tiles.clear(); + } + + 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)); + if (right > left && top > bottom) + { + LLGLSScissor scissor(left, bottom, right - left, top - bottom); + program.bindTexture(LLShaderMgr::DIFFUSE_MAP, &ids, ALSamplers::PointClamp); + triangle.setBuffer(); + triangle.drawArrays(LLRender::TRIANGLES, 0, 3); + program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); + } + } + tiles.flush(); } -inline void ALSelectionOutline::drawEdges(LLGLSLShader& program, LLRenderTarget& ids, U32 palette, S32 radius, - LLVertexBuffer& triangle) +inline void ALSelectionOutline::drawEdges(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& tiles, U32 palette, + const View& view, LLVertexBuffer& triangle) { - static const LLStaticHashedString radius_uniform(RADIUS_UNIFORM); - static const LLStaticHashedString rings_uniform(RINGS_UNIFORM); + static const LLStaticHashedString width_uniform(WIDTH_UNIFORM); + static const LLStaticHashedString inner_width_uniform(INNER_WIDTH_UNIFORM); - // Point sampled both: the ids and the palette are data, and texelFetch reads them whatever the filter. + // Point sampled all four: the ids, their depth, the flags and the palette are data, and texelFetch reads them + // whatever the filter. program.bindTexture(LLShaderMgr::DIFFUSE_MAP, &ids, ALSamplers::PointClamp); + program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, &ids); + program.bindTexture(LLShaderMgr::SPECULAR_MAP, &tiles, ALSamplers::PointClamp); const S32 channel = program.enableTexture(LLShaderMgr::ALTERNATE_DIFFUSE_MAP); if (channel > -1) { gGL.getTextureSlot(channel)->bindManual(ALTextureSlot::TT_TEXTURE, palette, gGL.getSampler(ALSamplers::PointClamp)); } - program.uniform1i(radius_uniform, llmax(radius, 1)); - program.uniform1i(rings_uniform, ringCount(radius)); + program.uniform1i(width_uniform, llmax(view.mWidth, 1)); + program.uniform1i(inner_width_uniform, llmax(view.mInnerWidth, 1)); + + // The full-screen triangle takes no matrix; the projection is loaded for its inverse, which turns the ids' + // depth back into distance. + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.pushMatrix(); + gGL.loadMatrix(view.mProjection); + gGL.matrixMode(LLRender::MM_MODELVIEW); triangle.setBuffer(); triangle.drawArrays(LLRender::TRIANGLES, 0, 3); + gGL.matrixMode(LLRender::MM_PROJECTION); + gGL.popMatrix(); + gGL.matrixMode(LLRender::MM_MODELVIEW); + program.unbindTexture(LLShaderMgr::ALTERNATE_DIFFUSE_MAP); + program.unbindTexture(LLShaderMgr::SPECULAR_MAP); + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); } diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index c6804db812..b7cd501def 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2405,6 +2405,17 @@ 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 + 4.0 + AlchemyRenderUBOUpdateMode Comment diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl index c34fd59601..b161b38d99 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl @@ -1,7 +1,7 @@ /** * @file selectionIdF.glsl - * @brief The selection outline's id pass (ALSelectionOutline): a selected object's id, and whether the scene - * shows the surface. + * @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 @@ -27,12 +27,29 @@ // class1/deferred/matricesBlock.glsl and bound at UB_MATRICES. //[ENGINE_BLOCK Matrices] -// .rg: the id, low byte then high byte; .b: 1 where the scene shows this surface, 0 where it hides it. +// .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; + +// The face's texture: a legacy face's diffuse map, a GLTF face's base colour. +uniform sampler2D diffuseMap; + +// The texture alpha under which the face is cut out and the fragment left out of the target. At or under 0 the +// face is opaque and its texture is not read. +uniform float minimum_alpha; + // 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; @@ -66,6 +83,14 @@ void main() float own = eyeDistance(gl_FragCoord.z); float slope = fwidth(own); + // Only the texture's alpha: 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 alpha = 1.0; + if (minimum_alpha > 0.0) + { + alpha = texture(diffuseMap, vary_texcoord0).a; + } + float visible = 1.0; if (selection_scene_depth != 0) { @@ -79,5 +104,12 @@ void main() visible = (own <= scene + reach + own * VISIBLE_SLACK) ? 1.0 : 0.0; } - frag_color = vec4(float(selection_id & 255) / 255.0, float((selection_id >> 8) & 255) / 255.0, visible, 1.0); + if (alpha < minimum_alpha) + { + discard; + } + + float drawn = (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/selectionOutlineF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl index d1b2f512aa..2f02935f25 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl @@ -1,7 +1,7 @@ /** * @file selectionOutlineF.glsl - * @brief The selection outline's edge pass (ALSelectionOutline): colours every pixel within the outline radius of - * a different selected object's id. + * @brief The selection outline's edge pass (ALSelectionOutline): the contour of each selected object over what lies + * behind it, and the fainter edges between objects that touch. * * $LicenseInfo:firstyear=2026&license=viewerlgpl$ * Alchemy Viewer Source Code @@ -23,77 +23,268 @@ * $/LicenseInfo$ */ +// Shared matrix stack + derived matrices, spliced from +// class1/deferred/matricesBlock.glsl and bound at UB_MATRICES. +//[ENGINE_BLOCK Matrices] + in vec2 tc; out vec4 frag_color; -// The id pass's target (selectionIdF.glsl): the id in .rg, low byte then high, 0 for none; 1 in .b where the -// scene shows the surface. +// The id pass's target (selectionIdF.glsl): 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; the priority in .a, 0 +// the highest. Ids are given in priority order, so of two objects the lower id is the higher priority. uniform sampler2D diffuseMap; -// 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. +// 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 altDiffuseMap; -// The outline's width in pixels, and how many rings of eight taps reach out to it. -uniform int outline_radius; -uniform int outline_rings; +// The tile pass's flags (selectionTileF.glsl), a texel to each TILE_SIZE square of the id target: 1 in .r where the +// square holds an id change. Bound under a reserved name, the only kind LLGLSLShader gives a texture unit. +uniform sampler2D specularMap; + +// 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; #define PALETTE_WIDTH 256 -const vec2 DIRECTIONS[8] = vec2[8](vec2(1.0, 0.0), vec2(-1.0, 0.0), vec2(0.0, 1.0), vec2(0.0, -1.0), - vec2(0.70710678, 0.70710678), vec2(-0.70710678, 0.70710678), - vec2(0.70710678, -0.70710678), vec2(-0.70710678, -0.70710678)); +// 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; + +// 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; 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 isDrawn(vec4 texel) +{ + return texel.b > 0.25; +} + +bool isVisible(vec4 texel) +{ + return texel.b > 0.75; +} + +vec4 paletteColour(int id, bool visible) +{ + int row = (id / PALETTE_WIDTH) * 2 + (visible ? 0 : 1); + return texelFetch(altDiffuseMap, 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); +} + +// Distance in front of the eye of a stored depth, as selectionIdF.glsl finds 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); +} + +bool hasId(ivec2 pos, ivec2 size, int id) +{ + return all(greaterThanEqual(pos, ivec2(0))) && all(lessThan(pos, size)) && decodeId(texelFetch(diffuseMap, 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 an id change. Whatever +// a pixel draws, it draws from a texel within its reach whose id is not its own, and between the two, inside that +// square, two neighbouring texels differ, which the tile of the left or lower one flags: without a flagged tile +// there the disk search finds nothing to draw. +bool changeWithin(ivec2 pos, int reach) +{ + ivec2 last = textureSize(specularMap, 0) - ivec2(1); + ivec2 lo = max(pos - ivec2(reach), ivec2(0)) / TILE_SIZE; + ivec2 hi = min((pos + ivec2(reach)) / TILE_SIZE, last); + for (int y = lo.y; y <= hi.y; ++y) + { + for (int x = lo.x; x <= hi.x; ++x) + { + if (texelFetch(specularMap, ivec2(x, y), 0).r > 0.5) + { + return true; + } + } + } + return false; +} + void main() { ivec2 size = textureSize(diffuseMap, 0); ivec2 pos = clamp(ivec2(tc * vec2(size)), ivec2(0), size - ivec2(1)); vec4 own = texelFetch(diffuseMap, pos, 0); int own_id = decodeId(own); + bool own_drawn = (own_id != 0) && isDrawn(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 reach = max(outline_width, outline_inner_width) + 1; + int inner_reach = outline_inner_width + 1; + if (!changeWithin(pos, reach)) + { + discard; + } + + // The plane of this pixel's own surface, found when the first other object 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(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; - // Nearest ring first. Off every object, the nearest object outlines itself here. On an object, another - // object's id is the edge between them, drawn on this side in this object's colour, so each keeps its own; - // where the object meets nothing selected it is left alone. - int id = 0; - vec4 source = own; - float dist = 0.0; - for (int ring = 1; ring <= outline_rings && id == 0; ++ring) + for (int dy = -reach; dy <= reach; ++dy) { - float reach = float(ring * outline_radius) / float(outline_rings); - for (int i = 0; i < 8; ++i) + for (int dx = -reach; dx <= reach; ++dx) { - ivec2 tap_pos = clamp(pos + ivec2(round(DIRECTIONS[i] * reach)), ivec2(0), size - ivec2(1)); + 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(diffuseMap, tap_pos, 0); int tap_id = decodeId(tap); - if (tap_id != 0 && tap_id != own_id) + // An object over its own surface draws nothing: that would trace every crease of a mesh. + if (tap_id == 0 || tap_id == own_id) + { + continue; + } + float dist = length(vec2(dx, dy)); + + // Off every object, every object is in front. On one, the other's surface against this one's plane, + // carried over to the other's texel. + 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); + if (relation == IN_FRONT && tap_drawn) { - id = (own_id != 0) ? own_id : tap_id; - source = (own_id != 0) ? own : tap; - dist = reach; - break; + 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_drawn && dist <= float(inner_reach) && (!tap_drawn || own_id < tap_id)) + { + inner_dist = min(inner_dist, dist); } } } - if (id == 0) + // Premultiplied, the contour over the edge. + vec3 rgb = vec3(0.0); + float alpha = 0.0; + if (own_drawn) { - discard; + vec4 colour = paletteColour(own_id, isVisible(own)); + alpha = colour.a * INNER_OPACITY * coverage(inner_dist, float(outline_inner_width)); + rgb = colour.rgb * 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; - int row = (id / PALETTE_WIDTH) * 2 + ((source.b > 0.5) ? 0 : 1); - vec4 color = texelFetch(altDiffuseMap, ivec2(id % PALETTE_WIDTH, row), 0); - if (color.a <= 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; } - - // Twice as bright at the edge and fading out to the radius. - float fade = (dist - 1.0) / float(outline_radius); - frag_color = vec4(mix(min(color.rgb * 2.0, vec3(1.0)), color.rgb, fade), color.a * (1.0 - fade)); + 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..bdd4e861ef --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl @@ -0,0 +1,82 @@ +/** + * @file selectionTileF.glsl + * @brief The selection outline's tile pass (ALSelectionOutline): whether a square of the id target holds an id + * change, so the edge pass can leave the pixels far from every one 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$ + */ + +// .r: 1 where the tile holds an id change, 0 where it holds none. +out vec4 frag_color; + +// The id pass's target (selectionIdF.glsl): the id in .rg, low byte then high, 0 for none. +uniform sampler2D diffuseMap; + +// The side of a tile in texels of the id target (ALSelectionOutline::TILE_SIZE). +#define TILE_SIZE 8 + +int decodeId(vec4 texel) +{ + return int(texel.r * 255.0 + 0.5) + (int(texel.g * 255.0 + 0.5) << 8); +} + +void main() +{ + // The viewport is the tile target's, a fragment to a tile. + ivec2 size = textureSize(diffuseMap, 0); + ivec2 origin = ivec2(gl_FragCoord.xy) * TILE_SIZE; + + // A change is a texel whose right or upper neighbour has another id, and belongs to the tile of that texel: so + // the tile's own texels are read with the column to its right and the row above it, and every pair of + // neighbours in the target is looked at by exactly one tile. A pair half off the target is no change. + int below[TILE_SIZE]; + bool changed = false; + for (int y = 0; y <= TILE_SIZE && !changed; ++y) + { + int left = -1; + for (int x = 0; x <= TILE_SIZE && !changed; ++x) + { + if (x == TILE_SIZE && y == TILE_SIZE) + { + break; + } + ivec2 pos = origin + ivec2(x, y); + int id = all(lessThan(pos, size)) ? decodeId(texelFetch(diffuseMap, pos, 0)) : -1; + + // With the texel to its left, along the tile's own rows. + if (y < TILE_SIZE && x > 0 && left >= 0 && id >= 0 && id != left) + { + changed = true; + } + // With the texel below it, up the tile's own columns. + if (x < TILE_SIZE) + { + if (y > 0 && below[x] >= 0 && id >= 0 && id != below[x]) + { + changed = true; + } + below[x] = id; + } + left = id; + } + } + + frag_color = vec4(changed ? 1.0 : 0.0, 0.0, 0.0, 1.0); +} diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index d85fd8e094..c46046b0b5 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -230,7 +230,6 @@ 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::sHighlightAlpha = 0.f; LLUIColor LLSelectMgr::sSilhouetteParentColor; LLUIColor LLSelectMgr::sSilhouetteChildColor; @@ -326,7 +325,6 @@ LLSelectMgr::LLSelectMgr() mTEMode = false; mTextureChannel = LLRender::DIFFUSE_MAP; - sHighlightThickness = gSavedSettings.getF32("SelectionHighlightThickness"); sHighlightAlpha = gSavedSettings.getF32("SelectionHighlightAlpha") * 2; sSilhouetteParentColor =LLUIColorTable::instance().getColor("SilhouetteParentColor"); @@ -6730,29 +6728,35 @@ void LLSelectMgr::addOutlines(ALSelectionOutline& outline, bool for_hud) 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); + outline.add(objectp, (U32)node->getTESelectMask(), colour, show_hidden, priority); } } @@ -6771,20 +6775,23 @@ void LLSelectMgr::addOutlines(ALSelectionOutline& outline, bool for_hud) } 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); + outline.add(objectp, (U32)node->getTESelectMask(), colour, sRenderHiddenSelections, priority); } } } diff --git a/indra/newview/llselectmgr.h b/indra/newview/llselectmgr.h index ab0cf30b3d..cc6f30145d 100644 --- a/indra/newview/llselectmgr.h +++ b/indra/newview/llselectmgr.h @@ -443,7 +443,6 @@ 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 sHighlightAlpha; static LLUIColor sSilhouetteParentColor; static LLUIColor sSilhouetteChildColor; diff --git a/indra/newview/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp index 9ff559e4da..190c4f6c04 100644 --- a/indra/newview/llviewershadermgr.cpp +++ b/indra/newview/llviewershadermgr.cpp @@ -156,6 +156,7 @@ LLGLSLShader gHighlightProgram; LLGLSLShader gHighlightNormalProgram; LLGLSLShader gHighlightSpecularProgram; LLGLSLShader gSelectionIdProgram; +LLGLSLShader gSelectionTileProgram; LLGLSLShader gSelectionOutlineProgram; LLGLSLShader gDeferredHighlightProgram; @@ -3830,12 +3831,22 @@ bool LLViewerShaderMgr::loadShadersInterface() { gSelectionIdProgram.mName = "Selection Id Shader"; gSelectionIdProgram.mShaderFiles.clear(); - gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/debugV.glsl", GL_VERTEX_SHADER)); + gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionIdV.glsl", GL_VERTEX_SHADER)); gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionIdF.glsl", GL_FRAGMENT_SHADER)); gSelectionIdProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; success = gSelectionIdProgram.createShader(LLGLSLShader::VARIANT_RIGGED); } + if (success) + { + gSelectionTileProgram.mName = "Selection Tile Shader"; + gSelectionTileProgram.mShaderFiles.clear(); + gSelectionTileProgram.mShaderFiles.push_back(make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); + gSelectionTileProgram.mShaderFiles.push_back(make_pair("interface/selectionTileF.glsl", GL_FRAGMENT_SHADER)); + gSelectionTileProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; + success = gSelectionTileProgram.createShader(); + } + if (success) { gSelectionOutlineProgram.mName = "Selection Outline Shader"; diff --git a/indra/newview/llviewershadermgr.h b/indra/newview/llviewershadermgr.h index f2998cf52c..e3f677b9f5 100644 --- a/indra/newview/llviewershadermgr.h +++ b/indra/newview/llviewershadermgr.h @@ -163,8 +163,9 @@ extern LLGLSLShader gLensDirtGenProgram; extern LLGLSLShader gHighlightProgram; extern LLGLSLShader gHighlightNormalProgram; extern LLGLSLShader gHighlightSpecularProgram; -// Selection outlines (ALSelectionOutline): the id pass, with a rigged variant, and the edge pass. +// Selection outlines (ALSelectionOutline): the id pass, with a rigged variant, the tile pass and the edge pass. extern LLGLSLShader gSelectionIdProgram; +extern LLGLSLShader gSelectionTileProgram; extern LLGLSLShader gSelectionOutlineProgram; extern LLGLSLShader gDeferredHighlightProgram; diff --git a/indra/newview/tests/alselectionoutline_gl_test.cpp b/indra/newview/tests/alselectionoutline_gl_test.cpp index d3614642d4..97898ff872 100644 --- a/indra/newview/tests/alselectionoutline_gl_test.cpp +++ b/indra/newview/tests/alselectionoutline_gl_test.cpp @@ -1,7 +1,8 @@ /** * @file alselectionoutline_gl_test.cpp - * @brief ALSelectionOutline's id and edge passes on the hidden window, with the viewer's own shader files: - * ids and edges, hidden parts, the scissor, a rigged pose, and the palette, radius and scissor helpers. + * @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, and the palette, width and scissor helpers. * * $LicenseInfo:firstyear=2026&license=viewerlgpl$ * Alchemy Viewer Source Code @@ -41,6 +42,7 @@ #include "../test/lltut.h" +#include #include #include #include @@ -50,12 +52,13 @@ 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; - constexpr S32 RADIUS = 3; // 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. @@ -64,6 +67,13 @@ namespace tut // 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); @@ -79,6 +89,9 @@ namespace tut 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; } + // 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); @@ -97,6 +110,19 @@ namespace tut } return count; } + + 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 @@ -119,6 +145,8 @@ namespace tut 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); @@ -129,17 +157,23 @@ namespace tut { LLGLSLShader::unbind(); mIdProgram.unload(); + mTileProgram.unload(); mOutlineProgram.unload(); mShaders.clearShaderObjects(); mIdMap.release(); + mTiles.release(); + mAllTiles.release(); mScene.release(); mFar.release(); mFrame.release(); - if (mPalette) + for (U32* texture : { &mPalette, &mRamp }) { - LLImageGL::deleteTextures(1, &mPalette); - mPalette = 0; + if (*texture) + { + LLImageGL::deleteTextures(1, texture); + *texture = 0; + } } mPaletteRows = 0; mBuffers.clear(); @@ -194,12 +228,19 @@ namespace tut mIdProgram.mName = "Selection Id Shader"; mIdProgram.mShaderFiles.clear(); - mIdProgram.mShaderFiles.push_back(std::make_pair("interface/debugV.glsl", GL_VERTEX_SHADER)); + 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.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); + mTileProgram.mName = "Selection Tile Shader"; + mTileProgram.mShaderFiles.clear(); + mTileProgram.mShaderFiles.push_back(std::make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); + mTileProgram.mShaderFiles.push_back(std::make_pair("interface/selectionTileF.glsl", GL_FRAGMENT_SHADER)); + mTileProgram.mShaderLevel = 1; + ensure("tile program builds", mTileProgram.createShader()); + mOutlineProgram.mName = "Selection Outline Shader"; mOutlineProgram.mShaderFiles.clear(); mOutlineProgram.mShaderFiles.push_back(std::make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); @@ -210,6 +251,14 @@ namespace tut 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("frame target", mFrame.allocate(W, H, GL_RGBA8)); + ensure("tile target", mTiles.allocate(Outline::tileCount(W), Outline::tileCount(H), GL_R8)); + // Every tile flagged: 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_R8)); + 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); @@ -226,6 +275,8 @@ namespace tut 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(); } @@ -238,6 +289,21 @@ namespace tut 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(); @@ -255,20 +321,26 @@ namespace tut return mReverse ? ndc : ndc * 0.5f + 0.5f; } - // A quad over x0..x1, y0..y1, at depth z0 along its x0 edge and z1 along its x1 edge, every vertex weighted - // wholly to joint 0 when `weights`. - LLVertexBuffer* quad(F32 x0, F32 x1, F32 y0, F32 y1, F32 z0, F32 z1, bool weights = false) + // 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) { - LLPointer buffer = new LLVertexBuffer(LLVertexBuffer::MAP_VERTEX | (weights ? LLVertexBuffer::MAP_WEIGHT4 : 0)); + 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); - vert[0].set(x0, y0, z0); - vert[1].set(x1, y0, z1); - vert[2].set(x1, y1, z1); - vert[3].set(x0, y1, z0); + 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) { @@ -284,18 +356,67 @@ namespace tut 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; + } + 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; }; // The id pass as ALSelectionOutline::render runs it, into `target`, reading `scene`'s depth, or with no @@ -304,19 +425,19 @@ namespace tut 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(); { - LLGLSColorMask mask(true, true); - LLGLDepthTest depth(GL_TRUE, GL_TRUE, GL_LEQUAL); - LLGLDisable cull(GL_CULL_FACE); - LLGLDisable blend(GL_BLEND); - glClearColor(0.f, 0.f, 0.f, 0.f); + 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); - ALSelectionOutline::bindIdPass(program, scene, (S32)target.getWidth(), (S32)target.getHeight()); + Outline::bindIdPass(program, scene, (S32)target.getWidth(), (S32)target.getHeight()); if (unread) { program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, unread); @@ -329,14 +450,35 @@ namespace tut } for (const Draw& draw : draws) { - ALSelectionOutline::setId(program, draw.mId); - draw.mBuffer->setBuffer(); - draw.mBuffer->drawRange(LLRender::TRIANGLES, 0, 3, 6, 0); + Outline::setId(program, draw.mId, draw.mPriority, draw.mShowHidden); + 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); + } } + 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) @@ -357,6 +499,11 @@ namespace tut return read(mIdMap); } + std::vector pixelIds(const std::vector& draws) + { + return projectedIds(pixelProjection(), draws); + } + std::vector read(LLRenderTarget& target) { target.bindTarget(); @@ -372,37 +519,84 @@ namespace tut { count = llmax(count, id); } - mRows = ALSelectionOutline::paletteRows(count); - mPaletteTexels.assign((size_t)ALSelectionOutline::PALETTE_WIDTH * mRows * 4, 0); + mRows = Outline::paletteRows(count); + mPaletteTexels.assign((size_t)Outline::PALETTE_WIDTH * mRows * 4, 0); for (const auto& [id, colour] : colours) { - ALSelectionOutline::writePalette(mPaletteTexels, id, colour, show_hidden); + 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; + } + + // The tile pass as ALSelectionOutline::render runs it, over the tiles under `rect`, or the whole id target. + void tilePass(const LLRect* rect = nullptr) + { + mTileProgram.bind(); + Outline::drawTiles(mTileProgram, mIdMap, mTiles, rect ? *rect : LLRect(0, H, W, 0), *mTriangle); + LLGLSLShader::unbind(); + } + + // The flags the tile pass left, a byte a tile. + std::vector tileFlags() + { + const std::vector px = read(mTiles); + std::vector flags; + for (size_t i = 0; i < px.size(); i += 4) + { + flags.push_back(px[i] > 127 ? 1 : 0); } + return flags; } - // The edge pass as ALSelectionOutline::render runs it, over a cleared frame, scissored when asked. - std::vector edgePass(S32 radius, const LLRect* scissor = nullptr) + // The edge pass as ALSelectionOutline::render runs it, the tile pass first, over the tiles under `tile_rect` + // or all of them, then over a cleared frame, scissored when asked. `untiled` flags every tile instead, so + // every pixel searches its whole disk. + std::vector edgePass(const Outline::View& lines, const LLRect* scissor = nullptr, bool untiled = false, + const LLRect* tile_rect = nullptr) { - ALSelectionOutline::uploadPalette(mPalette, mPaletteRows, mPaletteTexels, mRows); + Outline::uploadPalette(mPalette, mPaletteRows, mPaletteTexels, mRows); + if (!untiled) + { + tilePass(tile_rect); + } mFrame.bindTarget(); std::vector px; { LLGLSColorMask mask(true, true); - glClearColor(0.f, 0.f, 0.f, 0.f); + 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); - LLGLState scissor_test(GL_SCISSOR_TEST, scissor ? LLGLState::ENABLED_STATE : LLGLState::DISABLED_STATE); + + mOutlineProgram.bind(); + LLRenderTarget& tiles = untiled ? mAllTiles : mTiles; if (scissor) { - glScissor(scissor->mLeft, scissor->mBottom, scissor->getWidth(), scissor->getHeight()); + LLGLSScissor scissored(scissor->mLeft, scissor->mBottom, scissor->getWidth(), scissor->getHeight()); + Outline::drawEdges(mOutlineProgram, mIdMap, tiles, mPalette, lines, *mTriangle); + } + else + { + Outline::drawEdges(mOutlineProgram, mIdMap, tiles, mPalette, lines, *mTriangle); } - - mOutlineProgram.bind(); - ALSelectionOutline::drawEdges(mOutlineProgram, mIdMap, mPalette, radius, *mTriangle); LLGLSLShader::unbind(); px = ll_test::readFramebufferRGBA(W, H); } @@ -410,28 +604,39 @@ namespace tut return px; } - LLRect boxRect(S32 radius) const + std::vector edgePass(const LLRect* scissor = nullptr) + { + return edgePass(view(), scissor); + } + + 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", ALSelectionOutline::scissorRect(mProjection, extents, W, H, radius, 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 mTileProgram; LLGLSLShader mOutlineProgram; LLRenderTarget mIdMap; + LLRenderTarget mTiles; + LLRenderTarget mAllTiles; LLRenderTarget mScene; LLRenderTarget mFar; LLRenderTarget mFrame; U32 mPalette = 0; U32 mPaletteRows = 0; U32 mRows = 0; + U32 mRamp = 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; }; @@ -439,9 +644,9 @@ namespace tut typedef alselectionoutline_t::object alselectionoutline_object_t; tut::alselectionoutline_t alselectionoutline_testcase("ALSelectionOutline"); - // Two objects side by side: each has an outline around its outer contour, in its own colour, and the edge - // between them is drawn on both sides, each in its own colour. Their insides and the space beyond the radius - // are left alone. + // 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>() { @@ -454,24 +659,25 @@ namespace tut 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(RADIUS); - - const Pixel left = at(px, 8, 32); - ensure(named(reverse, "A outlined left of its contour"), red(left)); - ensure_equals(named(reverse, "at full strength beside it"), (U32)left.r, 255U); - ensure(named(reverse, "A outlined above"), red(at(px, 20, 44))); - ensure(named(reverse, "A outlined below"), red(at(px, 20, 19))); - ensure(named(reverse, "B outlined right of its contour"), green(at(px, 55, 32))); - ensure(named(reverse, "B outlined above"), green(at(px, 40, 44))); - - ensure(named(reverse, "fading out to the radius"), at(px, 6, 32).r > 0 && at(px, 6, 32).r < left.r); - ensure(named(reverse, "nothing past the radius"), black(at(px, 5, 32))); + 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))); - ensure(named(reverse, "A's side of the shared edge in A's colour"), red(at(px, 31, 32))); - ensure(named(reverse, "B's side of the shared edge in B's colour"), green(at(px, 32, 32))); + 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))); } @@ -487,7 +693,7 @@ namespace tut setUp(reverse); idPass({ { quadA(), ID_A }, { quadB(), ID_A } }); palette({ { ID_A, RED } }, true); - const std::vector px = edgePass(RADIUS); + 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))); @@ -509,8 +715,7 @@ namespace tut mScene.bindTarget(); { LLGLDepthTest depth(GL_TRUE, GL_TRUE); - LLGLEnable scissor(GL_SCISSOR_TEST); - glScissor(0, 0, 20, H); + LLGLSScissor scissor(0, 0, 20, H); glClearDepth(storedDepth(3.f)); mScene.clear(GL_DEPTH_BUFFER_BIT); glClearDepth(mReverse ? 0.0 : 1.0); @@ -518,20 +723,26 @@ namespace tut mScene.flush(); idPass({ { quadA(), ID_A } }); - const std::vector ids = read(mIdMap); - ensure_equals(named(reverse, "covered part marked hidden"), (U32)at(ids, 12, 32).b, 0U); + 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(RADIUS); + 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(RADIUS); + 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); } @@ -567,7 +778,7 @@ namespace tut if (idAt(ids, x, y) == ID_A) { ++covered; - hidden += at(ids, x, y).b == 0 ? 1 : 0; + hidden += at(ids, x, y).b != 255 ? 1 : 0; } } } @@ -578,8 +789,8 @@ namespace tut } } - // The scissor is the objects' projected box grown by the radius: the scissored edge pass draws exactly what - // the unscissored one does. Without the growth the ring outside the box is clipped. + // 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>() { @@ -589,21 +800,23 @@ namespace tut idPass({ { quadA(), ID_A }, { quadB(), ID_B } }); palette({ { ID_A, RED }, { ID_B, GREEN } }, true); - const std::vector full = edgePass(RADIUS); - const LLRect rect = boxRect(RADIUS); - ensure_equals(named(reverse, "left: column 8.57 floored, less the radius"), rect.mLeft, 8 - RADIUS); - ensure_equals(named(reverse, "right: column 55.43 ceiled, plus the radius"), rect.mRight, 56 + RADIUS); - ensure_equals(named(reverse, "bottom: row 20.28 floored, less the radius"), rect.mBottom, 20 - RADIUS); - ensure_equals(named(reverse, "top: row 43.72 ceiled, plus the radius"), rect.mTop, 44 + RADIUS); + 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(RADIUS, &rect); + 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(RADIUS, &tight); - ensure(named(reverse, "a box without the radius clips the ring"), lit(clipped) < lit(full)); - ensure(named(reverse, "the ring's outer pixels are what it loses"), !black(at(full, 6, 32)) && black(at(clipped, 6, 32))); + 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))); } } @@ -616,14 +829,14 @@ namespace tut 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", ALSelectionOutline::scissorRect(proj, straddling, W, H, RADIUS, rect)); + 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", !ALSelectionOutline::scissorRect(proj, behind, W, H, RADIUS, rect)); + 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", !ALSelectionOutline::scissorRect(proj, aside, W, H, RADIUS, rect)); + 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. @@ -652,7 +865,7 @@ namespace tut // Posed, it covers columns 9 to 20 and rows 26 to 37. palette({ { ID_A, RED } }, true); - const std::vector px = edgePass(RADIUS); + 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))); @@ -664,36 +877,57 @@ namespace tut template<> template<> void alselectionoutline_object_t::test<8>() { - ensure_equals("one pair of rows up to 255", ALSelectionOutline::paletteRows(255), 2U); - ensure_equals("two from 256", ALSelectionOutline::paletteRows(256), 4U); + 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)ALSelectionOutline::PALETTE_WIDTH * 4 * 4, 0); - ALSelectionOutline::writePalette(texels, 300, LLColor4(1.f, 0.5f, 0.f, 0.8f), true); - const size_t visible = ((size_t)2 * ALSelectionOutline::PALETTE_WIDTH + 44) * 4; - const size_t hidden = ((size_t)3 * ALSelectionOutline::PALETTE_WIDTH + 44) * 4; + 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 * ALSelectionOutline::HIDDEN_ALPHA * 255.f)); + ensure_equals("hidden alpha dimmed", (U32)texels[hidden + 3], (U32)ll_round(0.8f * Outline::HIDDEN_ALPHA * 255.f)); - ALSelectionOutline::writePalette(texels, 300, LLColor4(1.f, 0.5f, 0.f, 0.8f), false); + 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 radius: thickness times distance times the zoom, seen from that distance, so no distance changes it; it - // grows with the window and is at least a pixel. + // 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>() { - const F32 fov = 60.f * DEG_TO_RAD; - ensure_equals("1080 rows at the default view", ALSelectionOutline::radiusPixels(0.01f, 1080.f, fov, fov), 9); - ensure_equals("2160 rows", ALSelectionOutline::radiusPixels(0.01f, 2160.f, fov, fov), 19); - ensure_equals("zoomed in to half the view", ALSelectionOutline::radiusPixels(0.01f, 1080.f, fov * 0.5f, fov), 10); - ensure_equals("never under a pixel", ALSelectionOutline::radiusPixels(0.01f, 10.f, fov, fov), 1); - ensure_equals("no view, a pixel", ALSelectionOutline::radiusPixels(0.01f, 1080.f, 0.f, fov), 1); - ensure_equals("rings: one a pixel", ALSelectionOutline::ringCount(3), 3); - ensure_equals("rings: at most MAX_RINGS", ALSelectionOutline::ringCount(19), ALSelectionOutline::MAX_RINGS); + ensure_equals("the setting's default", Outline::contourWidth(Outline::DEFAULT_CONTOUR_WIDTH, 1.f), 4); + ensure_equals("the default at 1.5", Outline::contourWidth(Outline::DEFAULT_CONTOUR_WIDTH, 1.5f), 6); + ensure_equals("the default at 2", Outline::contourWidth(Outline::DEFAULT_CONTOUR_WIDTH, 2.f), 8); + 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 @@ -716,7 +950,7 @@ namespace tut mScene.flush(); idPass({ { quadA(), ID_A } }); - ensure_equals(named(reverse, "the world's pass hides A behind it"), (U32)at(read(mIdMap), 20, 32).b, 0U); + 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); @@ -737,7 +971,7 @@ namespace tut ensure_equals(named(reverse, "and hides none of it"), hidden, size_t(0)); palette({ { ID_A, RED } }, false); - const std::vector px = edgePass(RADIUS); + const std::vector px = edgePass(); ensure_equals(named(reverse, "outlined at full strength"), (U32)at(px, 8, 32).r, 255U); } } @@ -788,25 +1022,499 @@ namespace tut } } - // The HUD's radius: its projection is a unit high and its zoom scales objects and thickness alike, so the - // outline is `thickness` of the view's height whatever the zoom. A band a tenth of that projection high covers - // the rows a thickness of a tenth gives. + // 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>() { - ensure_equals("1080 rows", ALSelectionOutline::hudRadiusPixels(0.01f, 1080.f), 11); - ensure_equals("2160 rows", ALSelectionOutline::hudRadiusPixels(0.01f, 2160.f), 22); - ensure_equals("never under a pixel", ALSelectionOutline::hudRadiusPixels(0.01f, 10.f), 1); + const std::pair settings[] = { { Outline::DEFAULT_CONTOUR_WIDTH, 1.f }, { Outline::DEFAULT_CONTOUR_WIDTH, 2.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))); - setUp(false); - const LLMatrix4a hud = al_ortho(-0.5f, 0.5f, -0.5f, 0.5f, 0.f, 10.f); - LLVertexBuffer* band = quad(-0.4f, 0.4f, 0.f, 0.1f, -2.f, -2.f); - const std::vector ids = projectedIds(hud, { { band, ID_A } }); - S32 rows = 0; - for (S32 y = 0; y < H; ++y) + // 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()) { - rows += idAt(ids, 32, y) == ID_A ? 1 : 0; + 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 31 samples alpha 126, column 32 alpha 130. + ids = pixelIds({ { pixels(0, W, 16, 48), ID_A, Outline::PRIORITY_ROOT, blend, texture } }); + ensure_equals(named(reverse, "a blend cuts under half"), idAt(ids, 31, 32), 0U); + ensure_equals(named(reverse, "and keeps over it"), idAt(ids, 32, 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 } } }, + }; + + 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); + } + + std::vector flags = tileFlags(); + size_t flagged = 0; + for (U8 flag : flags) + { + flagged += flag; + } + ensure(named(reverse, name + ": some tiles hold no change"), flagged < flags.size()); + } + + // The first scene again: the root's left edge, at x = 16, belongs to tile column 1; the tile inside it, + // column 2, row 2, flags nothing; its right edge, inside tile column 3, flags that; nothing far away does. + pixelIds(scenes[0].second); + tilePass(); + const std::vector flags = tileFlags(); + const S32 columns = (S32)Outline::tileCount(W); + ensure_equals(named(reverse, "the left border's tile"), (U32)flags[2 * columns + 1], 1U); + ensure_equals(named(reverse, "the tile inside"), (U32)flags[2 * columns + 2], 0U); + ensure_equals(named(reverse, "the right edge's tile"), (U32)flags[2 * columns + 3], 1U); + ensure_equals(named(reverse, "a tile far away"), (U32)flags[0], 0U); + + // 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))); } - ensure_equals("a band a tenth of the projection high", rows, ALSelectionOutline::hudRadiusPixels(0.1f, (F32)H)); } } From ab890cacaefcc5902d3669dfbde4d267fec45e20 Mon Sep 17 00:00:00 2001 From: Rye Date: Fri, 9 Oct 2026 23:30:35 -0400 Subject: [PATCH 06/11] One mesh's parts lying over each other are outlined, and the tiles say which ids they hold A selection's contour was drawn where another object stood in front of a pixel, but never one object over itself, so a fern built as one mesh was outlined only round its whole while the same fern linked from one prim a frond was outlined frond over frond. A jump pass (selectionJumpF.glsl) copies the id target and marks, in its .a, each texel that is the near side of a step in depth to a neighbour of the same id. A pair of neighbours is a step when the depth between them lies outside the slope on both sides, each read from the texel beyond, and carrying either side on at its own slope misses the other by more than 1% of the distance. Over an inside crease or a curve the step lies between the two slopes, so neither is marked at any resolution; three texels of the object are needed on a side, so its edge and a sliver of it say nothing. The eight depths come from four gathers, and ids are read only for the pairs whose depths pass. In the edge pass a texel of the pixel's own id is a candidate when it is marked and stands in front of the pixel's own plane, as another object's does. The tiles (RGBA16) record the lowest and highest id they hold and whether they hold a mark. A pixel off every object is left when no tile in its reach holds an id; one on an object when none holds another id or a mark. Every candidate is a texel of another id or a marked texel of its own, and its own tile records it, so the output is the same as the full search. Pixels on an object with nothing to draw near them, which searched the whole disk since contours were ordered by depth, are now left at once. The helpers the five programs share (the id's decoding, the drawn and visible states, the mark, eyeDistance) are one file, selectionUtilF.glsl, linked into each. alselectionoutline_gl, now 23 cases: overlapping parts of one object at a large and a small gap draw the near part's contour over the far; inside corners at 30, 45 and 60 degrees, a cylinder inside and out to its rims and slanted planes mark and draw nothing; the tiles' records, and the tiled output against the untiled one with marks on tile borders. Co-Authored-By: Claude Opus 5.5 --- indra/newview/alselectionoutline.cpp | 27 +- indra/newview/alselectionoutline.h | 166 +++++++----- .../class1/interface/selectionIdF.glsl | 18 +- .../class1/interface/selectionJumpF.glsl | 120 +++++++++ .../class1/interface/selectionOutlineF.glsl | 86 +++--- .../class1/interface/selectionTileF.glsl | 58 ++-- .../class1/interface/selectionUtilF.glsl | 69 +++++ indra/newview/llviewershadermgr.cpp | 15 ++ indra/newview/llviewershadermgr.h | 4 +- .../tests/alselectionoutline_gl_test.cpp | 249 ++++++++++++++---- 10 files changed, 584 insertions(+), 228 deletions(-) create mode 100644 indra/newview/app_settings/shaders/class1/interface/selectionJumpF.glsl create mode 100644 indra/newview/app_settings/shaders/class1/interface/selectionUtilF.glsl diff --git a/indra/newview/alselectionoutline.cpp b/indra/newview/alselectionoutline.cpp index 3a8aa46c05..05bd775d62 100644 --- a/indra/newview/alselectionoutline.cpp +++ b/indra/newview/alselectionoutline.cpp @@ -120,6 +120,10 @@ void ALSelectionOutline::release() { mIdMap.release(); } + if (mMarkedMap.isComplete()) + { + mMarkedMap.release(); + } if (mTileMap.isComplete()) { mTileMap.release(); @@ -149,21 +153,22 @@ void ALSelectionOutline::render(const View& view) const S32 width = gGLViewport[2]; const S32 height = gGLViewport[3]; if (width <= 0 || height <= 0 || !gSelectionIdProgram.isComplete() || !gSelectionIdProgram.mRiggedVariant || - !gSelectionTileProgram.isComplete() || !gSelectionOutlineProgram.isComplete() || !gPipeline.mRT || - !gPipeline.mScreenTriangleVB) + !gSelectionJumpProgram.isComplete() || !gSelectionTileProgram.isComplete() || + !gSelectionOutlineProgram.isComplete() || !gPipeline.mRT || !gPipeline.mScreenTriangleVB) { clearEntries(); return; } - if (!mIdMap.isComplete() || !mTileMap.isComplete() || mIdMap.getWidth() != (U32)width || - mIdMap.getHeight() != (U32)height) + if (!mIdMap.isComplete() || !mMarkedMap.isComplete() || !mTileMap.isComplete() || + mIdMap.getWidth() != (U32)width || mIdMap.getHeight() != (U32)height) { if (!mIdMap.allocate(width, height, GL_RGBA8, true, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_NONE, LLPipeline::mainDepthFormat()) || - !mTileMap.allocate(tileCount((U32)width), tileCount((U32)height), GL_R8)) + !mMarkedMap.allocate(width, height, GL_RGBA8) || + !mTileMap.allocate(tileCount((U32)width), tileCount((U32)height), GL_RGBA16)) { - LL_WARNS_ONCE("Pipeline") << "Could not allocate the selection outline's id and tile targets" << LL_ENDL; + LL_WARNS_ONCE("Pipeline") << "Could not allocate the selection outline's targets" << LL_ENDL; release(); clearEntries(); return; @@ -250,10 +255,12 @@ void ALSelectionOutline::render(const View& view) { uploadPalette(mPalette, mPaletteRows, mPaletteTexels, rows); - // The scissor holds every selected texel and the contour's reach around them, more than the texel the tile - // pass needs. + // The scissor holds every selected texel and the contour's reach 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, mMarkedMap, mScissor, view, *gPipeline.mScreenTriangleVB); gSelectionTileProgram.bind(); - drawTiles(gSelectionTileProgram, mIdMap, mTileMap, mScissor, *gPipeline.mScreenTriangleVB); + drawTiles(gSelectionTileProgram, mMarkedMap, mTileMap, mScissor, *gPipeline.mScreenTriangleVB); LLGLSColorMask mask(true, false); LLGLDepthTest depth(GL_FALSE); @@ -262,7 +269,7 @@ void ALSelectionOutline::render(const View& view) LLGLSScissor scissor(vp_x + mScissor.mLeft, vp_y + mScissor.mBottom, mScissor.getWidth(), mScissor.getHeight()); gSelectionOutlineProgram.bind(); - drawEdges(gSelectionOutlineProgram, mIdMap, mTileMap, mPalette, view, *gPipeline.mScreenTriangleVB); + drawEdges(gSelectionOutlineProgram, mMarkedMap, mIdMap, mTileMap, mPalette, view, *gPipeline.mScreenTriangleVB); } if (previous) diff --git a/indra/newview/alselectionoutline.h b/indra/newview/alselectionoutline.h index 5f94e22be8..19c324a12b 100644 --- a/indra/newview/alselectionoutline.h +++ b/indra/newview/alselectionoutline.h @@ -65,24 +65,30 @@ class LLVOAvatar; /// the cards it is drawn on; the face's colour, whose alpha makes a prim invisible, is not read. Integer formats /// are not used because LLGLSLShader gives integer samplers no unit. /// -/// Ids are given in priority order, the highest first (EPriority). The edge pass -/// (interface/selectionOutlineF.glsl) is a full-screen triangle, scissored to the objects' projected boxes grown -/// by the contour's reach. A pixel draws the contour of the drawn objects within its reach that stand in front of -/// it: every object, off every object; on an object, another object 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: that would trace the creases of every mesh. Colours -/// come from a palette holding a visible and a hidden colour per id. +/// Ids are given in priority order, the highest first (EPriority). The jump pass (interface/selectionJumpF.glsl) +/// copies the id target into another, marking the texels 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 contour's reach. 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. /// /// The disk a pixel searches grows with the square of the contour's width, and most pixels in the scissor are far -/// from any edge. The tile pass (interface/selectionTileF.glsl) flags each TILE_SIZE square of the id target that -/// holds an id change, a texel whose right or upper neighbour has a different id, and the edge pass leaves a pixel -/// at once when no tile within its reach is flagged. Whatever a pixel draws, it draws from a texel within its reach -/// whose id is not its own, and between the two, inside the square its reach spans, two neighbouring texels differ, -/// so the early out changes nothing it draws. +/// from anything to draw. The tile pass (interface/selectionTileF.glsl) records for each TILE_SIZE square of the +/// marked target the lowest and highest nonzero id it holds and whether it holds the near side of a jump, and 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 the early out changes nothing it draws. /// /// The targets exist only while something is outlined: they are made the first frame there is, given up the /// first frame there is not, and given up with the pipeline's own (LLPipeline::releaseGLBuffers). @@ -150,7 +156,7 @@ class ALSelectionOutline /// 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 flags, in texels (TILE_SIZE in selectionTileF.glsl and + /// 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 @@ -256,21 +262,35 @@ class ALSelectionOutline /// 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 tile pass with the bound tile program: flags into `tiles`, a texel to each TILE_SIZE square of `ids`, - /// over the tiles under `rect`, a rect of `ids`; the others are cleared. Every square holding an id change - /// lies under the rect when it covers every selected texel and a texel more around them. - static void drawTiles(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& tiles, const LLRect& rect, + /// The jump pass with the bound jump program: `ids`, the id target, copied into `marked` with the near sides of + /// jumps marked, 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& marked, 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 + /// `marked`, over the tiles under `rect`, a rect of `marked`; the others are cleared, and hold nothing when the + /// rect covers every selected texel. + static void drawTiles(LLGLSLShader& program, LLRenderTarget& marked, LLRenderTarget& tiles, const LLRect& rect, LLVertexBuffer& triangle); - /// The edge pass with the bound outline program and `view`'s widths, over the viewport and scissor in force, - /// reading the tile pass's flags from `tiles` and the depth of `ids` through `view`'s projection, which the - /// id pass drew it with. - static void drawEdges(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& tiles, U32 palette, - const View& view, LLVertexBuffer& triangle); + /// The edge pass with the bound outline program and `view`'s widths, over the viewport and scissor in force: + /// ids from `marked`, their depth from `ids` through `view`'s projection, which the id pass drew with, and the + /// tile pass's record from `tiles`. + static void drawEdges(LLGLSLShader& program, LLRenderTarget& marked, LLRenderTarget& ids, LLRenderTarget& tiles, + U32 palette, const View& view, LLVertexBuffer& triangle); private: ALSelectionOutline() = default; + // 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; @@ -307,6 +327,7 @@ class ALSelectionOutline std::vector mPaletteTexels; LLRenderTarget mIdMap; + LLRenderTarget mMarkedMap; LLRenderTarget mTileMap; U32 mPalette = 0; U32 mPaletteRows = 0; @@ -519,46 +540,85 @@ inline void ALSelectionOutline::setAlphaTest(LLGLSLShader& program, F32 cutoff, program.uniform4fv(LLShaderMgr::TEXTURE_BASE_COLOR_TRANSFORM, 2, transform); } -inline void ALSelectionOutline::drawTiles(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& tiles, - const LLRect& rect, LLVertexBuffer& triangle) +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) { - tiles.bindTarget(); + target.bindTarget(); { LLGLSColorMask mask(true, true); LLGLDepthTest depth(GL_FALSE); LLGLDisable blend(GL_BLEND); { - // The tiles the pass leaves out hold no change, and read as none. LLGLDisable scissor(GL_SCISSOR_TEST); gGL.setClearColor(LLColor4(0.f, 0.f, 0.f, 0.f)); - tiles.clear(); + target.clear(); } - 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)); - if (right > left && top > bottom) + LLRect under = rect; + under.intersectWith(LLRect(0, (S32)target.getHeight(), (S32)target.getWidth(), 0)); + if (under.notEmpty()) { - LLGLSScissor scissor(left, bottom, right - left, top - bottom); - program.bindTexture(LLShaderMgr::DIFFUSE_MAP, &ids, ALSamplers::PointClamp); - triangle.setBuffer(); - triangle.drawArrays(LLRender::TRIANGLES, 0, 3); - program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); + LLGLSScissor scissor(under.mLeft, under.mBottom, under.getWidth(), under.getHeight()); + drawTriangle(triangle, projection); } } - tiles.flush(); + target.flush(); } -inline void ALSelectionOutline::drawEdges(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& tiles, U32 palette, - const View& view, LLVertexBuffer& triangle) +inline void ALSelectionOutline::drawJumps(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& marked, + const LLRect& rect, const View& view, LLVertexBuffer& triangle) +{ + program.bindTexture(LLShaderMgr::DIFFUSE_MAP, &ids, ALSamplers::PointClamp); + program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, &ids); + drawUnder(marked, rect, triangle, &view.mProjection); + program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); + program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); +} + +inline void ALSelectionOutline::drawTiles(LLGLSLShader& program, LLRenderTarget& marked, LLRenderTarget& tiles, + const LLRect& rect, LLVertexBuffer& triangle) +{ + 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)); + + // The tile program reads no depth, and needs no projection. + program.bindTexture(LLShaderMgr::DIFFUSE_MAP, &marked, ALSamplers::PointClamp); + drawUnder(tiles, LLRect(left, top, right, bottom), triangle, nullptr); + program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); +} + +inline void ALSelectionOutline::drawEdges(LLGLSLShader& program, LLRenderTarget& marked, LLRenderTarget& ids, + LLRenderTarget& tiles, U32 palette, const View& view, LLVertexBuffer& triangle) { static const LLStaticHashedString width_uniform(WIDTH_UNIFORM); static const LLStaticHashedString inner_width_uniform(INNER_WIDTH_UNIFORM); - // Point sampled all four: the ids, their depth, the flags and the palette are data, and texelFetch reads them + // Point sampled all four: the ids, their depth, the tiles and the palette are data, and texelFetch reads them // whatever the filter. - program.bindTexture(LLShaderMgr::DIFFUSE_MAP, &ids, ALSamplers::PointClamp); + program.bindTexture(LLShaderMgr::DIFFUSE_MAP, &marked, ALSamplers::PointClamp); program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, &ids); program.bindTexture(LLShaderMgr::SPECULAR_MAP, &tiles, ALSamplers::PointClamp); const S32 channel = program.enableTexture(LLShaderMgr::ALTERNATE_DIFFUSE_MAP); @@ -569,19 +629,7 @@ inline void ALSelectionOutline::drawEdges(LLGLSLShader& program, LLRenderTarget& program.uniform1i(width_uniform, llmax(view.mWidth, 1)); program.uniform1i(inner_width_uniform, llmax(view.mInnerWidth, 1)); - // The full-screen triangle takes no matrix; the projection is loaded for its inverse, which turns the ids' - // depth back into distance. - gGL.matrixMode(LLRender::MM_PROJECTION); - gGL.pushMatrix(); - gGL.loadMatrix(view.mProjection); - gGL.matrixMode(LLRender::MM_MODELVIEW); - - triangle.setBuffer(); - triangle.drawArrays(LLRender::TRIANGLES, 0, 3); - - gGL.matrixMode(LLRender::MM_PROJECTION); - gGL.popMatrix(); - gGL.matrixMode(LLRender::MM_MODELVIEW); + drawTriangle(triangle, &view.mProjection); program.unbindTexture(LLShaderMgr::ALTERNATE_DIFFUSE_MAP); program.unbindTexture(LLShaderMgr::SPECULAR_MAP); diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl index b161b38d99..6753aaa546 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl @@ -23,10 +23,6 @@ * $/LicenseInfo$ */ -// Shared matrix stack + derived matrices, spliced from -// class1/deferred/matricesBlock.glsl and bound at UB_MATRICES. -//[ENGINE_BLOCK Matrices] - // .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. @@ -64,18 +60,8 @@ uniform vec2 screen_res; // surface: float noise between two draws of the same triangle. const float VISIBLE_SLACK = 0.001; -// Distance in front of the eye of a stored depth, through the projection the scene and this pass share. -// 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); -} +// selectionUtilF.glsl, through the projection the scene and this pass share. +float eyeDistance(float depth); void main() { 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..1ef4b60569 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionJumpF.glsl @@ -0,0 +1,120 @@ +/** + * @file selectionJumpF.glsl + * @brief The selection outline's jump pass (ALSelectionOutline): the id target again, its texels marked 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$ + */ + +// The id texel (selectionUtilF.glsl), 128 added to its .a where it is the near side of a jump. +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 diffuseMap; +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); +float eyeDistance(float depth); + +bool hasId(ivec2 pos, ivec2 size, int id) +{ + return all(greaterThanEqual(pos, ivec2(0))) && all(lessThan(pos, size)) && decodeId(texelFetch(diffuseMap, 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(diffuseMap, 0); + ivec2 pos = ivec2(gl_FragCoord.xy); + vec4 texel = texelFetch(diffuseMap, pos, 0); + int id = decodeId(texel); + + bool front = false; + if (id != 0) + { + // 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(texel.rgb, (floor(texel.a * 255.0 + 0.5) + (front ? 128.0 : 0.0)) / 255.0); +} diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl index 2f02935f25..109014fd34 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl @@ -23,17 +23,12 @@ * $/LicenseInfo$ */ -// Shared matrix stack + derived matrices, spliced from -// class1/deferred/matricesBlock.glsl and bound at UB_MATRICES. -//[ENGINE_BLOCK Matrices] - in vec2 tc; out vec4 frag_color; -// The id pass's target (selectionIdF.glsl): 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; the priority in .a, 0 -// the highest. Ids are given in priority order, so of two objects the lower id is the higher priority. +// The id target with its jumps marked (selectionJumpF.glsl, selectionUtilF.glsl). Ids are given in priority order, +// so of two objects the lower id is the higher priority. uniform sampler2D diffuseMap; // The id target's own depth, through the projection loaded for this pass, which the id pass drew with. @@ -42,8 +37,9 @@ uniform sampler2D depthMap; // Two texels per id, PALETTE_WIDTH ids to a pair of rows: the visible colour above the hidden one. uniform sampler2D altDiffuseMap; -// The tile pass's flags (selectionTileF.glsl), a texel to each TILE_SIZE square of the id target: 1 in .r where the -// square holds an id change. Bound under a reserved name, the only kind LLGLSLShader gives a texture unit. +// 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, and 1 in .b where it holds the near side of a jump. Bound under a +// reserved name, the only kind LLGLSLShader gives a texture unit. uniform sampler2D specularMap; // The widths in pixels of the contour around the objects and of the edges between them, before the pixel each @@ -70,25 +66,13 @@ const int IN_FRONT = 0; const int TOUCHING = 1; const int BEHIND = 2; -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 isDrawn(vec4 texel) -{ - return texel.b > 0.25; -} - -bool isVisible(vec4 texel) -{ - return texel.b > 0.75; -} +// selectionUtilF.glsl +int decodeId(vec4 texel); +int decodePriority(vec4 texel); +bool isJumpFront(vec4 texel); +bool isDrawn(vec4 texel); +bool isVisible(vec4 texel); +float eyeDistance(float depth); vec4 paletteColour(int id, bool visible) { @@ -105,18 +89,6 @@ float coverage(float dist, float width) return 1.0 - smoothstep(width, width + 1.0, dist); } -// Distance in front of the eye of a stored depth, as selectionIdF.glsl finds 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); -} - bool hasId(ivec2 pos, ivec2 size, int id) { return all(greaterThanEqual(pos, ivec2(0))) && all(lessThan(pos, size)) && decodeId(texelFetch(diffuseMap, pos, 0)) == id; @@ -138,11 +110,11 @@ float depthSlope(ivec2 pos, ivec2 size, ivec2 step, int id, float depth) return back ? to_back : to_ahead; } -// Whether a tile under the square of texels within `reach` of `pos` along either axis holds an id change. Whatever -// a pixel draws, it draws from a texel within its reach whose id is not its own, and between the two, inside that -// square, two neighbouring texels differ, which the tile of the left or lower one flags: without a flagged tile -// there the disk search finds nothing to draw. -bool changeWithin(ivec2 pos, int reach) +// 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. +bool candidateWithin(ivec2 pos, int reach, int own_id) { ivec2 last = textureSize(specularMap, 0) - ivec2(1); ivec2 lo = max(pos - ivec2(reach), ivec2(0)) / TILE_SIZE; @@ -151,7 +123,11 @@ bool changeWithin(ivec2 pos, int reach) { for (int x = lo.x; x <= hi.x; ++x) { - if (texelFetch(specularMap, ivec2(x, y), 0).r > 0.5) + vec4 tile = texelFetch(specularMap, ivec2(x, y), 0); + int lowest = int(tile.r * 65535.0 + 0.5); + int highest = int(tile.g * 65535.0 + 0.5); + if (own_id == 0 ? (highest != 0) + : ((lowest != 0 && lowest != own_id) || (highest != 0 && highest != own_id) || tile.b > 0.5)) { return true; } @@ -172,12 +148,12 @@ void main() // target are not there: clamping them would read the edge's again, nearer than it is. int reach = max(outline_width, outline_inner_width) + 1; int inner_reach = outline_inner_width + 1; - if (!changeWithin(pos, reach)) + if (!candidateWithin(pos, reach, own_id)) { discard; } - // The plane of this pixel's own surface, found when the first other object needs it. + // 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); @@ -207,15 +183,18 @@ void main() } vec4 tap = texelFetch(diffuseMap, tap_pos, 0); int tap_id = decodeId(tap); - // An object over its own surface draws nothing: that would trace every crease of a mesh. - if (tap_id == 0 || tap_id == own_id) + // 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 && !isJumpFront(tap))) { continue; } float dist = length(vec2(dx, dy)); - // Off every object, every object is in front. On one, the other's surface against this one's plane, - // carried over to the other's texel. + // 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) { @@ -253,7 +232,8 @@ void main() } // 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_drawn && dist <= float(inner_reach) && (!tap_drawn || own_id < tap_id)) + else if (relation == TOUCHING && !own_object && own_drawn && dist <= float(inner_reach) && + (!tap_drawn || own_id < tap_id)) { inner_dist = min(inner_dist, dist); } diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl index bdd4e861ef..b490c26537 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl @@ -1,7 +1,7 @@ /** * @file selectionTileF.glsl - * @brief The selection outline's tile pass (ALSelectionOutline): whether a square of the id target holds an id - * change, so the edge pass can leave the pixels far from every one at once. + * @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 @@ -23,60 +23,44 @@ * $/LicenseInfo$ */ -// .r: 1 where the tile holds an id change, 0 where it holds none. +// 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. out vec4 frag_color; -// The id pass's target (selectionIdF.glsl): the id in .rg, low byte then high, 0 for none. +// The id target with its jumps marked (selectionJumpF.glsl). uniform sampler2D diffuseMap; // The side of a tile in texels of the id target (ALSelectionOutline::TILE_SIZE). #define TILE_SIZE 8 -int decodeId(vec4 texel) -{ - return int(texel.r * 255.0 + 0.5) + (int(texel.g * 255.0 + 0.5) << 8); -} +// selectionUtilF.glsl +int decodeId(vec4 texel); +bool isJumpFront(vec4 texel); void main() { // The viewport is the tile target's, a fragment to a tile. ivec2 size = textureSize(diffuseMap, 0); ivec2 origin = ivec2(gl_FragCoord.xy) * TILE_SIZE; + ivec2 end = min(origin + ivec2(TILE_SIZE), size); - // A change is a texel whose right or upper neighbour has another id, and belongs to the tile of that texel: so - // the tile's own texels are read with the column to its right and the row above it, and every pair of - // neighbours in the target is looked at by exactly one tile. A pair half off the target is no change. - int below[TILE_SIZE]; - bool changed = false; - for (int y = 0; y <= TILE_SIZE && !changed; ++y) + int lowest = 65536; + int highest = 0; + bool jump = false; + for (int y = origin.y; y < end.y; ++y) { - int left = -1; - for (int x = 0; x <= TILE_SIZE && !changed; ++x) + for (int x = origin.x; x < end.x; ++x) { - if (x == TILE_SIZE && y == TILE_SIZE) - { - break; - } - ivec2 pos = origin + ivec2(x, y); - int id = all(lessThan(pos, size)) ? decodeId(texelFetch(diffuseMap, pos, 0)) : -1; - - // With the texel to its left, along the tile's own rows. - if (y < TILE_SIZE && x > 0 && left >= 0 && id >= 0 && id != left) - { - changed = true; - } - // With the texel below it, up the tile's own columns. - if (x < TILE_SIZE) + vec4 texel = texelFetch(diffuseMap, ivec2(x, y), 0); + int id = decodeId(texel); + if (id != 0) { - if (y > 0 && below[x] >= 0 && id >= 0 && id != below[x]) - { - changed = true; - } - below[x] = id; + lowest = min(lowest, id); + highest = max(highest, id); + jump = jump || isJumpFront(texel); } - left = id; } } - frag_color = vec4(changed ? 1.0 : 0.0, 0.0, 0.0, 1.0); + frag_color = vec4(float(highest != 0 ? lowest : 0) / 65535.0, float(highest) / 65535.0, jump ? 1.0 : 0.0, 1.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..4d43f349d2 --- /dev/null +++ b/indra/newview/app_settings/shaders/class1/interface/selectionUtilF.glsl @@ -0,0 +1,69 @@ +/** + * @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, below 128, and +// 128 more on a texel the jump pass marks as the near side of a jump in its object's surface. + +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) & 127; +} + +bool isJumpFront(vec4 texel) +{ + return int(texel.a * 255.0 + 0.5) >= 128; +} + +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/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp index 190c4f6c04..3cf65d2dc5 100644 --- a/indra/newview/llviewershadermgr.cpp +++ b/indra/newview/llviewershadermgr.cpp @@ -156,6 +156,7 @@ LLGLSLShader gHighlightProgram; LLGLSLShader gHighlightNormalProgram; LLGLSLShader gHighlightSpecularProgram; LLGLSLShader gSelectionIdProgram; +LLGLSLShader gSelectionJumpProgram; LLGLSLShader gSelectionTileProgram; LLGLSLShader gSelectionOutlineProgram; @@ -3833,16 +3834,29 @@ bool LLViewerShaderMgr::loadShadersInterface() gSelectionIdProgram.mShaderFiles.clear(); gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionIdV.glsl", GL_VERTEX_SHADER)); gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionIdF.glsl", GL_FRAGMENT_SHADER)); + gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); gSelectionIdProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; success = gSelectionIdProgram.createShader(LLGLSLShader::VARIANT_RIGGED); } + if (success) + { + gSelectionJumpProgram.mName = "Selection Jump Shader"; + gSelectionJumpProgram.mShaderFiles.clear(); + gSelectionJumpProgram.mShaderFiles.push_back(make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); + gSelectionJumpProgram.mShaderFiles.push_back(make_pair("interface/selectionJumpF.glsl", GL_FRAGMENT_SHADER)); + gSelectionJumpProgram.mShaderFiles.push_back(make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); + gSelectionJumpProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; + success = gSelectionJumpProgram.createShader(); + } + if (success) { gSelectionTileProgram.mName = "Selection Tile Shader"; gSelectionTileProgram.mShaderFiles.clear(); gSelectionTileProgram.mShaderFiles.push_back(make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); gSelectionTileProgram.mShaderFiles.push_back(make_pair("interface/selectionTileF.glsl", GL_FRAGMENT_SHADER)); + gSelectionTileProgram.mShaderFiles.push_back(make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); gSelectionTileProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; success = gSelectionTileProgram.createShader(); } @@ -3853,6 +3867,7 @@ bool LLViewerShaderMgr::loadShadersInterface() gSelectionOutlineProgram.mShaderFiles.clear(); gSelectionOutlineProgram.mShaderFiles.push_back(make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); gSelectionOutlineProgram.mShaderFiles.push_back(make_pair("interface/selectionOutlineF.glsl", GL_FRAGMENT_SHADER)); + gSelectionOutlineProgram.mShaderFiles.push_back(make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); gSelectionOutlineProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; success = gSelectionOutlineProgram.createShader(); } diff --git a/indra/newview/llviewershadermgr.h b/indra/newview/llviewershadermgr.h index e3f677b9f5..4065b9e5bb 100644 --- a/indra/newview/llviewershadermgr.h +++ b/indra/newview/llviewershadermgr.h @@ -163,8 +163,10 @@ extern LLGLSLShader gLensDirtGenProgram; extern LLGLSLShader gHighlightProgram; extern LLGLSLShader gHighlightNormalProgram; extern LLGLSLShader gHighlightSpecularProgram; -// Selection outlines (ALSelectionOutline): the id pass, with a rigged variant, the tile pass and the edge pass. +// Selection outlines (ALSelectionOutline): the id pass, with a rigged variant, the jump pass, the tile pass and the +// edge pass, each linked with interface/selectionUtilF.glsl. extern LLGLSLShader gSelectionIdProgram; +extern LLGLSLShader gSelectionJumpProgram; extern LLGLSLShader gSelectionTileProgram; extern LLGLSLShader gSelectionOutlineProgram; diff --git a/indra/newview/tests/alselectionoutline_gl_test.cpp b/indra/newview/tests/alselectionoutline_gl_test.cpp index 97898ff872..26963806bf 100644 --- a/indra/newview/tests/alselectionoutline_gl_test.cpp +++ b/indra/newview/tests/alselectionoutline_gl_test.cpp @@ -157,11 +157,13 @@ namespace tut { LLGLSLShader::unbind(); mIdProgram.unload(); + mJumpProgram.unload(); mTileProgram.unload(); mOutlineProgram.unload(); mShaders.clearShaderObjects(); mIdMap.release(); + mMarked.release(); mTiles.release(); mAllTiles.release(); mScene.release(); @@ -230,30 +232,33 @@ namespace tut 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/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); - mTileProgram.mName = "Selection Tile Shader"; - mTileProgram.mShaderFiles.clear(); - mTileProgram.mShaderFiles.push_back(std::make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); - mTileProgram.mShaderFiles.push_back(std::make_pair("interface/selectionTileF.glsl", GL_FRAGMENT_SHADER)); - mTileProgram.mShaderLevel = 1; - ensure("tile program builds", mTileProgram.createShader()); - - mOutlineProgram.mName = "Selection Outline Shader"; - mOutlineProgram.mShaderFiles.clear(); - mOutlineProgram.mShaderFiles.push_back(std::make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); - mOutlineProgram.mShaderFiles.push_back(std::make_pair("interface/selectionOutlineF.glsl", GL_FRAGMENT_SHADER)); - mOutlineProgram.mShaderLevel = 1; - ensure("outline program builds", mOutlineProgram.createShader()); + const std::pair passes[] = { { &mJumpProgram, "selectionJumpF.glsl" }, + { &mTileProgram, "selectionTileF.glsl" }, + { &mOutlineProgram, "selectionOutlineF.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)); + program->mShaderFiles.push_back(std::make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); + program->mShaderLevel = 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("marked target", mMarked.allocate(W, H, GL_RGBA8)); ensure("frame target", mFrame.allocate(W, H, GL_RGBA8)); - ensure("tile target", mTiles.allocate(Outline::tileCount(W), Outline::tileCount(H), GL_R8)); - // Every tile flagged: 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_R8)); + 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(); @@ -543,37 +548,47 @@ namespace tut return lines; } - // The tile pass as ALSelectionOutline::render runs it, over the tiles under `rect`, or the whole id target. - void tilePass(const LLRect* rect = nullptr) + // 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) { - mTileProgram.bind(); - Outline::drawTiles(mTileProgram, mIdMap, mTiles, rect ? *rect : LLRect(0, H, W, 0), *mTriangle); + const LLRect under = rect ? *rect : LLRect(0, H, W, 0); + mJumpProgram.bind(); + Outline::drawJumps(mJumpProgram, mIdMap, mMarked, under, lines, *mTriangle); + if (tiles) + { + mTileProgram.bind(); + Outline::drawTiles(mTileProgram, mMarked, mTiles, under, *mTriangle); + } LLGLSLShader::unbind(); } - // The flags the tile pass left, a byte a tile. - std::vector tileFlags() + // What the tile pass recorded, four 16-bit values a tile: the lowest and highest id, the jump, and 1. + std::vector tileRecords() { - const std::vector px = read(mTiles); - std::vector flags; - for (size_t i = 0; i < px.size(); i += 4) - { - flags.push_back(px[i] > 127 ? 1 : 0); - } - return flags; + 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; } - // The edge pass as ALSelectionOutline::render runs it, the tile pass first, over the tiles under `tile_rect` - // or all of them, then over a cleared frame, scissored when asked. `untiled` flags every tile instead, so - // every pixel searches its whole disk. + // Whether the jump pass marked the texel at (x, y) as the near side of a jump. + bool markedAt(S32 x, S32 y) + { + return at(read(mMarked), x, y).a >= 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, then over a cleared frame, scissored when asked. `untiled` holds every id and a + // jump in every tile instead, so every pixel searches its whole disk. std::vector edgePass(const Outline::View& lines, const LLRect* scissor = nullptr, bool untiled = false, - const LLRect* tile_rect = nullptr) + const LLRect* mark_rect = nullptr) { Outline::uploadPalette(mPalette, mPaletteRows, mPaletteTexels, mRows); - if (!untiled) - { - tilePass(tile_rect); - } + markPasses(lines, mark_rect, !untiled); mFrame.bindTarget(); std::vector px; @@ -591,11 +606,11 @@ namespace tut if (scissor) { LLGLSScissor scissored(scissor->mLeft, scissor->mBottom, scissor->getWidth(), scissor->getHeight()); - Outline::drawEdges(mOutlineProgram, mIdMap, tiles, mPalette, lines, *mTriangle); + Outline::drawEdges(mOutlineProgram, mMarked, mIdMap, tiles, mPalette, lines, *mTriangle); } else { - Outline::drawEdges(mOutlineProgram, mIdMap, tiles, mPalette, lines, *mTriangle); + Outline::drawEdges(mOutlineProgram, mMarked, mIdMap, tiles, mPalette, lines, *mTriangle); } LLGLSLShader::unbind(); px = ll_test::readFramebufferRGBA(W, H); @@ -619,9 +634,11 @@ namespace tut ll_test::TestShaderMgr mShaders; LLGLSLShader mIdProgram; + LLGLSLShader mJumpProgram; LLGLSLShader mTileProgram; LLGLSLShader mOutlineProgram; LLRenderTarget mIdMap; + LLRenderTarget mMarked; LLRenderTarget mTiles; LLRenderTarget mAllTiles; LLRenderTarget mScene; @@ -1311,6 +1328,14 @@ namespace tut { "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) @@ -1326,25 +1351,34 @@ namespace tut ensure(named(reverse, what + ": tiled as untiled"), tiled == untiled); } - std::vector flags = tileFlags(); - size_t flagged = 0; - for (U8 flag : flags) + const std::vector records = tileRecords(); + size_t empty = 0; + for (size_t i = 0; i < records.size(); i += 4) { - flagged += flag; + empty += records[i + 1] == 0 ? 1 : 0; } - ensure(named(reverse, name + ": some tiles hold no change"), flagged < flags.size()); + ensure(named(reverse, name + ": some tiles hold nothing"), empty > 0); } - // The first scene again: the root's left edge, at x = 16, belongs to tile column 1; the tile inside it, - // column 2, row 2, flags nothing; its right edge, inside tile column 3, flags that; nothing far away does. + // 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); - tilePass(); - const std::vector flags = tileFlags(); + markPasses(view()); + std::vector records = tileRecords(); const S32 columns = (S32)Outline::tileCount(W); - ensure_equals(named(reverse, "the left border's tile"), (U32)flags[2 * columns + 1], 1U); - ensure_equals(named(reverse, "the tile inside"), (U32)flags[2 * columns + 2], 0U); - ensure_equals(named(reverse, "the right edge's tile"), (U32)flags[2 * columns + 3], 1U); - ensure_equals(named(reverse, "a tile far away"), (U32)flags[0], 0U); + 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. @@ -1517,4 +1551,115 @@ namespace tut 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(); + const std::vector marked = read(mMarked); + ensure(named(reverse, "the near side of the step marked" + at_z), at(marked, 25, 32).a >= 128 && at(marked, 32, 38).a >= 128); + ensure(named(reverse, "not the far side, nor inside" + at_z), at(marked, 24, 32).a < 128 && at(marked, 32, 32).a < 128); + + 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(mMarked); + // 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).a >= 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)); + } + } + } } From 86725ba66c1d3f64d828a73cb756cd9347abf3a3 Mon Sep 17 00:00:00 2001 From: Rye Date: Fri, 9 Oct 2026 23:30:37 -0400 Subject: [PATCH 07/11] The selection outline is 2 pixels wide by default AlchemySelectionOutlineWidth defaults to 2 rather than 4: with contours drawn wherever selected parts overlap, 2 reads well. Co-Authored-By: Claude Opus 5.5 --- indra/newview/alselectionoutline.h | 2 +- indra/newview/app_settings/settings_alchemy.xml | 2 +- indra/newview/tests/alselectionoutline_gl_test.cpp | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/indra/newview/alselectionoutline.h b/indra/newview/alselectionoutline.h index 19c324a12b..58f982b79b 100644 --- a/indra/newview/alselectionoutline.h +++ b/indra/newview/alselectionoutline.h @@ -150,7 +150,7 @@ class ALSelectionOutline 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 = 4.f; + 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 diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index b7cd501def..f9e00879a4 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2414,7 +2414,7 @@ Type F32 Value - 4.0 + 2.0 AlchemyRenderUBOUpdateMode diff --git a/indra/newview/tests/alselectionoutline_gl_test.cpp b/indra/newview/tests/alselectionoutline_gl_test.cpp index 26963806bf..76d5d46fac 100644 --- a/indra/newview/tests/alselectionoutline_gl_test.cpp +++ b/indra/newview/tests/alselectionoutline_gl_test.cpp @@ -916,9 +916,9 @@ namespace tut template<> template<> void alselectionoutline_object_t::test<9>() { - ensure_equals("the setting's default", Outline::contourWidth(Outline::DEFAULT_CONTOUR_WIDTH, 1.f), 4); - ensure_equals("the default at 1.5", Outline::contourWidth(Outline::DEFAULT_CONTOUR_WIDTH, 1.5f), 6); - ensure_equals("the default at 2", Outline::contourWidth(Outline::DEFAULT_CONTOUR_WIDTH, 2.f), 8); + 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); @@ -1046,7 +1046,7 @@ namespace tut void alselectionoutline_object_t::test<12>() { const std::pair settings[] = { { Outline::DEFAULT_CONTOUR_WIDTH, 1.f }, { Outline::DEFAULT_CONTOUR_WIDTH, 2.f }, - { 1.f, 1.f }, { 2.5f, 1.f }, { 0.25f, 1.f }, { 40.f, 1.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); From 1a31996d70f9ebe2d6cbe9fd7dedb228b545857d Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 00:24:49 -0400 Subject: [PATCH 08/11] Selected objects can be drawn as a wireframe, with or without their outline Builders read a mesh's triangles and its level of detail from a wireframe. The old one went with the CPU silhouettes; it drew GL_LINE polygons at a line width core profiles do not have, so it was a pixel wide on some drivers and five on others, and its hidden pass filled whole surfaces. AlchemySelectionWireframe (off by default; Build > Options > Show Selection Wireframe) draws every selected face's triangles as lines a pixel wide at a UI scale of 1, anti-aliased, the same width near and far: a geometry stage (selectionWireframeG.glsl) gives each corner its distance in the window to the opposite edge, interpolated without perspective, and the fragment stage (selectionWireframeF.glsl) draws a line centred on each edge, half from each triangle beside it. A triangle reaching behind the eye is clipped at the near plane where GL clips it, its distances taken to its original edges' lines, so a floor or a wall a builder stands at keeps its lines and the cut draws none. The lines are drawn from the same faces as the outline, through one walk (drawFaces) shared with the id pass: the same model matrices, rigged faces through the rigged variant, culling as the scene culls, and the same alpha cut, now one file (selectionAlphaF.glsl) linked into both programs. They take each object's outline colour from its palette. In the world they are tested against the window's depth, which the final blit filled from the scene's, pulled in front of their own surface by a polygon offset that covers the blit's point sampling; hidden lines are drawn dimmed, under GREATER, only for objects whose hidden parts are shown. On the HUD the window's depth holds whichever of the HUD's and the scene's is nearer, so the lines are tested against the id target's: a line behind another selected HUD surface is hidden. The outline (RenderHighlightSelections) and the wireframe are drawn each with or without the other, and passesFor picks only the passes a view needs: the outline the id, jump, tile and edge passes, the world's wireframe none of them, the HUD's the id pass alone. The targets a frame's passes do not read are given up. The wireframe draws first and the outline over it. alselectionoutline_gl, now 32 cases: line width held near and far and with the UI scale, the feather, hidden lines with hidden selections on and off, no gaps over a depth resampled from half resolution under either depth convention, alpha cuts, rigged poses and culling, the HUD's lines against the id target, near-plane clipping, and the passes each combination runs. Co-Authored-By: Claude Opus 5.5 --- indra/newview/alselectionoutline.cpp | 274 ++++++--- indra/newview/alselectionoutline.h | 165 ++++- .../newview/app_settings/settings_alchemy.xml | 11 + .../class1/interface/selectionAlphaF.glsl | 50 ++ .../class1/interface/selectionIdF.glsl | 22 +- .../class1/interface/selectionWireframeF.glsl | 102 ++++ .../class1/interface/selectionWireframeG.glsl | 185 ++++++ indra/newview/llselectmgr.cpp | 24 +- indra/newview/llviewershadermgr.cpp | 15 + indra/newview/llviewershadermgr.h | 6 +- .../skins/default/xui/en/menu_viewer.xml | 9 + .../tests/alselectionoutline_gl_test.cpp | 572 +++++++++++++++++- 12 files changed, 1303 insertions(+), 132 deletions(-) create mode 100644 indra/newview/app_settings/shaders/class1/interface/selectionAlphaF.glsl create mode 100644 indra/newview/app_settings/shaders/class1/interface/selectionWireframeF.glsl create mode 100644 indra/newview/app_settings/shaders/class1/interface/selectionWireframeG.glsl diff --git a/indra/newview/alselectionoutline.cpp b/indra/newview/alselectionoutline.cpp index 05bd775d62..6d3822ef75 100644 --- a/indra/newview/alselectionoutline.cpp +++ b/indra/newview/alselectionoutline.cpp @@ -67,20 +67,22 @@ ALSelectionOutline& ALSelectionOutline::instance() ALSelectionOutline::View ALSelectionOutline::worldView() { static LLCachedControl contour_width(gSavedSettings, "AlchemySelectionOutlineWidth", DEFAULT_CONTOUR_WIDTH); + static LLCachedControl wireframe(gSavedSettings, "AlchemySelectionWireframe", false); return makeView(LLViewerCamera::getCurrent().getModelview(), gGL.getProjectionMatrix(), contour_width, - LLUI::getScaleFactor().mV[VX], false); + LLUI::getScaleFactor().mV[VX], false, wireframe); } bool ALSelectionOutline::hudView(View& view) { static LLCachedControl contour_width(gSavedSettings, "AlchemySelectionOutlineWidth", DEFAULT_CONTOUR_WIDTH); + static LLCachedControl wireframe(gSavedSettings, "AlchemySelectionWireframe", false); LLMatrix4a projection; LLMatrix4a modelview; if (!get_hud_matrices(projection, modelview)) { return false; } - view = makeView(modelview, projection, contour_width, LLUI::getScaleFactor().mV[VX], true); + view = makeView(modelview, projection, contour_width, LLUI::getScaleFactor().mV[VX], true, wireframe); return true; } @@ -152,28 +154,61 @@ void ALSelectionOutline::render(const View& view) const S32 vp_y = gGLViewport[1]; const S32 width = gGLViewport[2]; const S32 height = gGLViewport[3]; - if (width <= 0 || height <= 0 || !gSelectionIdProgram.isComplete() || !gSelectionIdProgram.mRiggedVariant || - !gSelectionJumpProgram.isComplete() || !gSelectionTileProgram.isComplete() || - !gSelectionOutlineProgram.isComplete() || !gPipeline.mRT || !gPipeline.mScreenTriangleVB) + const Passes passes = passesFor(view, gSelectionWireframeProgram.isComplete() && + gSelectionWireframeProgram.mRiggedVariant != nullptr); + if (!passes.any()) + { + release(); + clearEntries(); + return; + } + if (width <= 0 || height <= 0 || !gPipeline.mRT || !gPipeline.mScreenTriangleVB || + (passes.mIds && (!gSelectionIdProgram.isComplete() || !gSelectionIdProgram.mRiggedVariant)) || + (passes.mEdges && (!gSelectionJumpProgram.isComplete() || !gSelectionTileProgram.isComplete() || + !gSelectionOutlineProgram.isComplete()))) { clearEntries(); return; } - if (!mIdMap.isComplete() || !mMarkedMap.isComplete() || !mTileMap.isComplete() || - mIdMap.getWidth() != (U32)width || mIdMap.getHeight() != (U32)height) + // The targets a pass that does not run reads are given up; the others are made at the window's size. + bool allocated = true; + if (!passes.mIds) { - if (!mIdMap.allocate(width, height, GL_RGBA8, true, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_NONE, - LLPipeline::mainDepthFormat()) || - !mMarkedMap.allocate(width, height, GL_RGBA8) || - !mTileMap.allocate(tileCount((U32)width), tileCount((U32)height), GL_RGBA16)) + if (mIdMap.isComplete()) { - LL_WARNS_ONCE("Pipeline") << "Could not allocate the selection outline's targets" << LL_ENDL; - release(); - clearEntries(); - return; + mIdMap.release(); + } + } + else 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()); + } + if (!passes.mEdges) + { + if (mMarkedMap.isComplete()) + { + mMarkedMap.release(); + } + if (mTileMap.isComplete()) + { + mTileMap.release(); } } + else if (!mMarkedMap.isComplete() || !mTileMap.isComplete() || mMarkedMap.getWidth() != (U32)width || + mMarkedMap.getHeight() != (U32)height) + { + allocated = allocated && mMarkedMap.allocate(width, height, GL_RGBA8) && + mTileMap.allocate(tileCount((U32)width), tileCount((U32)height), GL_RGBA16); + } + 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"); @@ -205,6 +240,60 @@ void ALSelectionOutline::render(const View& view) mLastMeshId = 0; mSkipLastSkin = false; + if (passes.mIds) + { + drawIds(view, width, height, mvp); + } + + // What the id pass drew says whether the outline has anything to touch; the world's wireframe, drawn without + // it, draws whatever its faces put on screen. + if (passes.mIds ? mScissorValid : passes.mWireframe) + { + uploadPalette(mPalette, mPaletteRows, mPaletteTexels, rows); + + // The wireframe first, the outline over it. + if (passes.mWireframe) + { + drawWireframes(view, width, height); + } + + if (passes.mEdges) + { + // The scissor holds every selected texel and the contour's reach 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, mMarkedMap, mScissor, view, *gPipeline.mScreenTriangleVB); + gSelectionTileProgram.bind(); + drawTiles(gSelectionTileProgram, mMarkedMap, mTileMap, mScissor, *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, mMarkedMap, mIdMap, mTileMap, 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. @@ -250,48 +339,17 @@ void ALSelectionOutline::render(const View& view) gGL.popMatrix(); } mIdMap.flush(); - - if (mScissorValid) - { - uploadPalette(mPalette, mPaletteRows, mPaletteTexels, rows); - - // The scissor holds every selected texel and the contour's reach 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, mMarkedMap, mScissor, view, *gPipeline.mScreenTriangleVB); - gSelectionTileProgram.bind(); - drawTiles(gSelectionTileProgram, mMarkedMap, mTileMap, mScissor, *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, mMarkedMap, mIdMap, mTileMap, mPalette, view, *gPipeline.mScreenTriangleVB); - } - - if (previous) - { - previous->bind(); - } - else - { - LLGLSLShader::unbind(); - } - - clearEntries(); } -void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U32 id, bool rigged, const View& view, - const LLMatrix4a& mvp) +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; + return false; } // The frame each face's vertex buffer is in, as LLVolumeGeometryManager::registerFace places it. Rigged faces @@ -316,13 +374,10 @@ void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U } else { - return; + return false; } } - LLVector4a lo(FLT_MAX, FLT_MAX, FLT_MAX); - LLVector4a hi(-FLT_MAX, -FLT_MAX, -FLT_MAX); - bool bounded = true; bool drawn = false; // The TE mask holds 32 faces, more than a volume has. @@ -346,7 +401,7 @@ void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U continue; } - LLVector4a box[2]; + LLVector4a extents[2]; if (rigged) { if (!face->mAvatar || !face->mSkinInfo || @@ -355,26 +410,26 @@ void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U continue; } // Joint boxes, refreshed while the face is in view (LLFace::calcPixelArea); empty until they are. - box[0] = face->mRiggedExtents[0]; - box[1] = face->mRiggedExtents[1]; + extents[0] = face->mRiggedExtents[0]; + extents[1] = face->mRiggedExtents[1]; LLVector4a size; - size.setSub(box[1], box[0]); + size.setSub(extents[1], extents[0]); if (!(size[0] > 0.f && size[1] > 0.f && size[2] > 0.f)) { - bounded = false; + box.mBounded = false; } } else if (agent_extents) { - box[0] = face->mExtents[0]; - box[1] = face->mExtents[1]; + extents[0] = face->mExtents[0]; + extents[1] = face->mExtents[1]; } else { - matMulBoundBox(*model, face->mExtents, box); + matMulBoundBox(*model, face->mExtents, extents); } - lo.setMin(lo, box[0]); - hi.setMax(hi, box[1]); + box.mLo.setMin(box.mLo, extents[0]); + box.mHi.setMax(box.mHi, extents[1]); if (!drawn) { @@ -383,7 +438,7 @@ void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U { gGL.multMatrix(model->getF32ptr()); } - setId(program, id, entry.mPriority, entry.mShowHidden); + begin(); drawn = true; } @@ -398,8 +453,14 @@ void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U gGL.matrixMode(LLRender::MM_MODELVIEW); } } + return drawn; +} - if (!drawn) +void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U32 id, bool rigged, const View& view, + const LLMatrix4a& mvp) +{ + FaceBox box; + if (!drawFaces(program, entry, rigged, view, box, [&] { setId(program, id, entry.mPriority, entry.mShowHidden); })) { return; } @@ -407,13 +468,13 @@ void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U const S32 width = (S32)mIdMap.getWidth(); const S32 height = (S32)mIdMap.getHeight(); LLRect rect; - if (!bounded) + if (!box.mBounded) { rect.set(0, height, width, 0); } else { - const LLVector4a extents[2] = { lo, hi }; + const LLVector4a extents[2] = { box.mLo, box.mHi }; if (!scissorRect(mvp, extents, width, height, lineReach(view.mWidth), rect)) { return; @@ -431,6 +492,83 @@ void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U } } +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 || entry.mShowHidden; + } + + 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) + { + const Entry& entry = mEntries[mOrder[i]]; + if (pass == WIRE_HIDDEN && !entry.mShowHidden) + { + 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::ALTERNATE_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(); +} + 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 diff --git a/indra/newview/alselectionoutline.h b/indra/newview/alselectionoutline.h index 58f982b79b..1577f1b3ef 100644 --- a/indra/newview/alselectionoutline.h +++ b/indra/newview/alselectionoutline.h @@ -83,6 +83,14 @@ class LLVOAvatar; /// 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. /// +/// 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 /// marked target the lowest and highest nonzero id it holds and whether it holds the near side of a jump, and the @@ -128,8 +136,31 @@ class ALSelectionOutline /// 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 outline (RenderHighlightSelections), drawn with or without the wireframe. + bool mOutline = true; + /// The selected faces' triangles drawn as lines under the outline (AlchemySelectionWireframe), mWireWidth + /// pixels wide before their pixel of anti-aliasing, with or without the outline. + bool mWireframe = false; + S32 mWireWidth = 1; + }; + + /// The passes render() runs for a view. + struct Passes + { + /// The id pass: the outline'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 from the ids. + bool mEdges = false; + + bool any() const { return mIds || mWireframe || mEdges; } }; + /// The passes `view` needs, 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 View& view, bool wireframe_ready); + /// The world camera's, in the UI stage, where the scene's projection is still loaded. static View worldView(); @@ -140,7 +171,7 @@ class ALSelectionOutline /// (AlchemySelectionOutlineWidth), drawn at `ui_scale`, LLUI's scale factor: the same widths in the world and /// on the HUD. static View makeView(const LLMatrix4a& modelview, const LLMatrix4a& projection, F32 contour_width, F32 ui_scale, - bool hud); + bool hud, bool wireframe = false); /// Ids the target can tell apart; 0 is no selected object. static constexpr U32 MAX_IDS = 65535; @@ -168,6 +199,11 @@ class ALSelectionOutline 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 uniforms the passes set beyond LLShaderMgr's reserved ones. static constexpr const char* ID_UNIFORM = "selection_id"; @@ -176,6 +212,48 @@ class ALSelectionOutline static constexpr const char* SCENE_DEPTH_UNIFORM = "selection_scene_depth"; static constexpr const char* WIDTH_UNIFORM = "outline_width"; static constexpr const char* INNER_WIDTH_UNIFORM = "outline_inner_width"; + static constexpr const char* WIRE_PASS_UNIFORM = "wireframe_pass"; + static constexpr const char* WIRE_WIDTH_UNIFORM = "wireframe_width"; + + /// 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, + }; + + /// 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; } /// Lives for the session and is never destroyed: its GL objects go with the pipeline's, and at exit GL is gone. static ALSelectionOutline& instance(); @@ -274,6 +352,11 @@ class ALSelectionOutline static void drawTiles(LLGLSLShader& program, LLRenderTarget& marked, LLRenderTarget& tiles, const LLRect& rect, 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 from `marked`, their depth from `ids` through `view`'s projection, which the id pass drew with, and the /// tile pass's record from `tiles`. @@ -300,12 +383,36 @@ class ALSelectionOutline EPriority mPriority; }; + // 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 into // mScissor. void drawObject(LLGLSLShader& program, const Entry& entry, U32 id, bool rigged, const View& view, const LLMatrix4a& mvp); - // What the id pass draws a face with beyond its geometry. + // The id pass: what was added 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, 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); + + // What a face is drawn with beyond its geometry. struct FaceBinding { // A texture animation was loaded, which the caller unloads after the draw. @@ -314,7 +421,7 @@ class ALSelectionOutline bool mDoubleSided = false; }; - // Binds `face`'s texture and alpha test to the bound id program and loads its texture animation, if it is drawn + // 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); @@ -371,7 +478,7 @@ inline S32 ALSelectionOutline::contourWidth(F32 width, F32 ui_scale) } inline ALSelectionOutline::View ALSelectionOutline::makeView(const LLMatrix4a& modelview, const LLMatrix4a& projection, - F32 contour_width, F32 ui_scale, bool hud) + F32 contour_width, F32 ui_scale, bool hud, bool wireframe) { View view; view.mModelview = modelview; @@ -379,9 +486,20 @@ inline ALSelectionOutline::View ALSelectionOutline::makeView(const LLMatrix4a& m view.mWidth = contourWidth(contour_width, ui_scale); view.mInnerWidth = lineWidth(INNER_WIDTH, ui_scale); view.mHUD = hud; + view.mWireframe = wireframe; + view.mWireWidth = lineWidth(WIRE_WIDTH, ui_scale); return view; } +inline ALSelectionOutline::Passes ALSelectionOutline::passesFor(const View& view, bool wireframe_ready) +{ + Passes passes; + passes.mWireframe = view.mWireframe && wireframe_ready; + passes.mEdges = view.mOutline; + passes.mIds = view.mOutline || (passes.mWireframe && view.mHUD); + return passes; +} + inline void ALSelectionOutline::priorityOrder(const std::vector& priorities, std::vector& order) { order.resize(priorities.size()); @@ -533,6 +651,45 @@ inline void ALSelectionOutline::drawFace(LLVertexBuffer& buffer, U32 start, U32 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) +{ + static const LLStaticHashedString pass_uniform(WIRE_PASS_UNIFORM); + static const LLStaticHashedString width_uniform(WIRE_WIDTH_UNIFORM); + + program.uniform1i(pass_uniform, (GLint)pass); + program.uniform1i(width_uniform, llmax(wire_width, 1)); + program.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, (F32)width, (F32)height); + const S32 channel = program.enableTexture(LLShaderMgr::ALTERNATE_DIFFUSE_MAP); + 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) { diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index f9e00879a4..c41d3051e2 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2416,6 +2416,17 @@ 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/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/selectionIdF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl index 6753aaa546..b252f8d484 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl @@ -39,13 +39,6 @@ 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; -// The face's texture: a legacy face's diffuse map, a GLTF face's base colour. -uniform sampler2D diffuseMap; - -// The texture alpha under which the face is cut out and the fragment left out of the target. At or under 0 the -// face is opaque and its texture is not read. -uniform float minimum_alpha; - // 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; @@ -63,19 +56,16 @@ 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); - - // Only the texture's alpha: 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 alpha = 1.0; - if (minimum_alpha > 0.0) - { - alpha = texture(diffuseMap, vary_texcoord0).a; - } + float alpha = faceAlpha(vary_texcoord0); float visible = 1.0; if (selection_scene_depth != 0) @@ -90,7 +80,7 @@ void main() visible = (own <= scene + reach + own * VISIBLE_SLACK) ? 1.0 : 0.0; } - if (alpha < minimum_alpha) + if (isCutOut(alpha)) { discard; } 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..59b8dbac61 --- /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 altDiffuseMap; + +// 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(altDiffuseMap, 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/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index c46046b0b5..171fbc782c 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -6797,31 +6797,33 @@ void LLSelectMgr::addOutlines(ALSelectionOutline& outline, bool for_hud) } // Outlines from an id target (ALSelectionOutline): one id per object, so every prim keeps its own outline. A -// frame calls this once, for the world or for the HUD as the selection is in one or on the other. +// frame calls this once, for the world or for the HUD as the selection is in one or on the other. The outline +// (RenderHighlightSelections) and the wireframe (AlchemySelectionWireframe) are drawn each with or without the other. void LLSelectMgr::renderSilhouettes(bool for_hud) { ALSelectionOutline& outline = ALSelectionOutline::instance(); - if (!mRenderHighlightSelections) + ALSelectionOutline::View view; + if (!for_hud) + { + view = ALSelectionOutline::worldView(); + } + else if (!ALSelectionOutline::hudView(view)) { outline.release(); return; } + view.mOutline = mRenderHighlightSelections; - // A manipulator's drag hides the outlines without giving up their targets for the drag. - if (!mRenderSilhouettes) + if (!view.mOutline && !view.mWireframe) { + outline.release(); return; } - ALSelectionOutline::View view; - if (!for_hud) - { - view = ALSelectionOutline::worldView(); - } - else if (!ALSelectionOutline::hudView(view)) + // A manipulator's drag hides the outlines without giving up their targets for the drag. + if (!mRenderSilhouettes) { - outline.release(); return; } diff --git a/indra/newview/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp index 3cf65d2dc5..c8ed8d9e35 100644 --- a/indra/newview/llviewershadermgr.cpp +++ b/indra/newview/llviewershadermgr.cpp @@ -156,6 +156,7 @@ LLGLSLShader gHighlightProgram; LLGLSLShader gHighlightNormalProgram; LLGLSLShader gHighlightSpecularProgram; LLGLSLShader gSelectionIdProgram; +LLGLSLShader gSelectionWireframeProgram; LLGLSLShader gSelectionJumpProgram; LLGLSLShader gSelectionTileProgram; LLGLSLShader gSelectionOutlineProgram; @@ -3834,11 +3835,25 @@ bool LLViewerShaderMgr::loadShadersInterface() gSelectionIdProgram.mShaderFiles.clear(); gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionIdV.glsl", GL_VERTEX_SHADER)); gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionIdF.glsl", GL_FRAGMENT_SHADER)); + gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionAlphaF.glsl", GL_FRAGMENT_SHADER)); gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); gSelectionIdProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; success = gSelectionIdProgram.createShader(LLGLSLShader::VARIANT_RIGGED); } + if (success) + { + gSelectionWireframeProgram.mName = "Selection Wireframe Shader"; + gSelectionWireframeProgram.mShaderFiles.clear(); + gSelectionWireframeProgram.mShaderFiles.push_back(make_pair("interface/selectionIdV.glsl", GL_VERTEX_SHADER)); + gSelectionWireframeProgram.mShaderFiles.push_back(make_pair("interface/selectionWireframeG.glsl", GL_GEOMETRY_SHADER)); + gSelectionWireframeProgram.mShaderFiles.push_back(make_pair("interface/selectionWireframeF.glsl", GL_FRAGMENT_SHADER)); + gSelectionWireframeProgram.mShaderFiles.push_back(make_pair("interface/selectionAlphaF.glsl", GL_FRAGMENT_SHADER)); + gSelectionWireframeProgram.mShaderFiles.push_back(make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); + gSelectionWireframeProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; + success = gSelectionWireframeProgram.createShader(LLGLSLShader::VARIANT_RIGGED); + } + if (success) { gSelectionJumpProgram.mName = "Selection Jump Shader"; diff --git a/indra/newview/llviewershadermgr.h b/indra/newview/llviewershadermgr.h index 4065b9e5bb..1356c250ff 100644 --- a/indra/newview/llviewershadermgr.h +++ b/indra/newview/llviewershadermgr.h @@ -163,9 +163,11 @@ extern LLGLSLShader gLensDirtGenProgram; extern LLGLSLShader gHighlightProgram; extern LLGLSLShader gHighlightNormalProgram; extern LLGLSLShader gHighlightSpecularProgram; -// Selection outlines (ALSelectionOutline): the id pass, with a rigged variant, the jump pass, the tile pass and the -// edge pass, each linked with interface/selectionUtilF.glsl. +// 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; diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index 78f2cbeda8..aad6bb9672 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" + + + + diff --git a/indra/newview/tests/alselectionoutline_gl_test.cpp b/indra/newview/tests/alselectionoutline_gl_test.cpp index 76d5d46fac..af59a636cd 100644 --- a/indra/newview/tests/alselectionoutline_gl_test.cpp +++ b/indra/newview/tests/alselectionoutline_gl_test.cpp @@ -111,6 +111,40 @@ namespace tut 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; @@ -158,12 +192,15 @@ namespace tut LLGLSLShader::unbind(); mIdProgram.unload(); mJumpProgram.unload(); + mWireProgram.unload(); + mCopyDepthProgram.unload(); mTileProgram.unload(); mOutlineProgram.unload(); mShaders.clearShaderObjects(); mIdMap.release(); mMarked.release(); + mWindow.release(); mTiles.release(); mAllTiles.release(); mScene.release(); @@ -232,11 +269,34 @@ namespace tut 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" } }; @@ -255,6 +315,7 @@ namespace tut ensure("id target", mIdMap.allocate(W, H, GL_RGBA8, true, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_NONE, depth)); ensure("marked target", mMarked.allocate(W, H, GL_RGBA8)); 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. @@ -424,6 +485,50 @@ namespace tut bool mDoubleSided = false; }; + // 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. False where a texture had no unit to go to. + bool drawAll(LLGLSLShader& program, const std::vector& draws, const F32* joints, bool hidden_only = false) + { + 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 (hidden_only && !draw.mShowHidden) + { + continue; + } + Outline::setId(program, draw.mId, draw.mPriority, draw.mShowHidden); + 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. @@ -447,37 +552,7 @@ namespace tut { program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, unread); } - if (rigged) - { - const F32 origin[3] = { 0.f, 0.f, 0.f }; - program.uniformMatrix3x4fv(LLShaderMgr::AVATAR_MATRIX, 1, GL_FALSE, palette); - program.uniform3fv(LLShaderMgr::SKIN_ORIGIN, 1, origin); - } - for (const Draw& draw : draws) - { - Outline::setId(program, draw.mId, draw.mPriority, draw.mShowHidden); - 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); - } - } + texture_read = drawAll(program, draws, palette); program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); LLGLSLShader::unbind(); @@ -624,6 +699,80 @@ namespace tut 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 == Outline::WIRE_HIDDEN) && texture_read; + program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); + program.unbindTexture(LLShaderMgr::ALTERNATE_DIFFUSE_MAP); + 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) }; @@ -635,10 +784,14 @@ namespace tut ll_test::TestShaderMgr mShaders; LLGLSLShader mIdProgram; LLGLSLShader mJumpProgram; + LLGLSLShader mWireProgram; + LLGLSLShader mCopyDepthProgram; LLGLSLShader mTileProgram; LLGLSLShader mOutlineProgram; LLRenderTarget mIdMap; LLRenderTarget mMarked; + // The window's framebuffer: what the wireframe draws over, its depth the scene's. + LLRenderTarget mWindow; LLRenderTarget mTiles; LLRenderTarget mAllTiles; LLRenderTarget mScene; @@ -1662,4 +1815,361 @@ namespace tut } } } + + // 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 + // on only with its setting; 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 off = Outline::makeView(identity(), identity(), 2.f, 2.f, hud); + const Outline::View on = Outline::makeView(identity(), identity(), 2.f, 2.f, hud, true); + ensure("off by default", !off.mWireframe); + ensure("on with its setting", on.mWireframe); + ensure_equals("its width at the UI scale", on.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 and the wireframe are drawn each without the other, and each runs only the passes it reads: the + // outline 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. + 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::View view = Outline::makeView(identity(), identity(), 2.f, 1.f, hud); + ensure("the outline on by default" + where, view.mOutline); + + Outline::Passes passes = Outline::passesFor(view, 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); + + view.mWireframe = true; + passes = Outline::passesFor(view, true); + ensure("both run everything" + where, passes.mIds && passes.mEdges && passes.mWireframe); + passes = Outline::passesFor(view, false); + ensure("a wireframe program that did not load leaves the outline" + where, + passes.mIds && passes.mEdges && !passes.mWireframe); + + view.mOutline = false; + passes = Outline::passesFor(view, 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(view, false); + ensure("nothing when its program did not load" + where, !passes.any()); + + view.mWireframe = false; + ensure("nothing with both off" + where, !Outline::passesFor(view, true).any()); + } + } } From dd80e9d68afbd53c59b94e4f6c5082110b4654ea Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 01:47:46 -0400 Subject: [PATCH 09/11] What the pointer is over glows again, and invisible prims are outlined Hover Glow Objects (RenderHoverGlowEnable, off by default; Develop > Rendering), which LL removed in 2022 with the 256-pixel target and stencil it was drawn with, is back on the outline's id target. LLToolPie's hover names what the pointer is over where it would before: media, a click action, a physical object, a touch handler, and not where RLVa blocks the touch, nor an avatar or a HUD attachment. ALHoverGlow fades the named prim in over RenderHighlightFadeTime and the one before it out, once the name, or its lapse, has held for 0.15 s, so a sweep across objects lights none of them and a frame off one at its edge or a seam does not put it out. It holds ids, so a glow keeps nothing alive. The glow is drawn at a priority of its own below every selection's: a halo outside the prim's visible silhouette, never over the prim, in RenderHighlightColor, as opaque at the silhouette as RenderHighlightBrightness makes it and falling smoothly to nothing RenderHighlightThickness x 15 pixels out at a UI scale of 1. Selection contours and edges lie over it. The tiles record where something glows, so only pixels within its reach search that far, and the output is the same as the full search. Entries now say which parts they want, contour or wireframe, so the glow is drawn with the outline and the wireframe on or off and never takes a wireframe. The world's outline call runs every frame for it, and the targets are kept 2 s after a frame last drew with them rather than given up at once, now that the world's and the HUD's calls can both run in a frame. A face textured with the library's transparent texture (IMG_TRANSPARENT) is invisible on purpose and still selectable: roots, collision and touch prims. The texture-alpha cut discarded all of it, so it had no outline. It is now drawn whole in the id pass, and so outlined, wireframed and glowing. ALSelectionOutline and ALHoverGlow are LLSingletons. The outline's targets are given up with the pipeline's GL, before singletons are deleted. The uniforms the outline, glow and wireframe programs set are LLShaderMgr reserved uniforms (SELECTION_ID to WIREFRAME_WIDTH), set by index. alselectionoutline_gl, now 40 cases: the glow's profile and its reach in the tiles, the fades and the settle, the glow under a selection's contour and off its own prim, the parts each entry draws and the passes they need, the targets' lifetime, and IMG_TRANSPARENT on both sides. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llshadermgr.cpp | 13 + indra/llrender/llshadermgr.h | 13 + indra/newview/CMakeLists.txt | 2 + indra/newview/alhoverglow.cpp | 48 ++ indra/newview/alhoverglow.h | 147 +++++ indra/newview/alselectionoutline.cpp | 160 ++++-- indra/newview/alselectionoutline.h | 309 ++++++++--- indra/newview/app_settings/settings.xml | 2 +- .../class1/interface/selectionIdF.glsl | 6 +- .../class1/interface/selectionOutlineF.glsl | 108 +++- .../class1/interface/selectionTileF.glsl | 9 +- .../class1/interface/selectionUtilF.glsl | 9 + indra/newview/llselectmgr.cpp | 47 +- indra/newview/llselectmgr.h | 2 +- indra/newview/lltoolpie.cpp | 21 +- indra/newview/llviewerwindow.cpp | 11 +- indra/newview/pipeline.cpp | 5 +- .../skins/default/xui/en/menu_viewer.xml | 10 + .../tests/alselectionoutline_gl_test.cpp | 525 ++++++++++++++++-- 19 files changed, 1226 insertions(+), 221 deletions(-) create mode 100644 indra/newview/alhoverglow.cpp create mode 100644 indra/newview/alhoverglow.h diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index 9ef8ad059e..1ab20625c0 100644 --- a/indra/llrender/llshadermgr.cpp +++ b/indra/llrender/llshadermgr.cpp @@ -2132,6 +2132,19 @@ 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"); + // 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..27aa139786 100644 --- a/indra/llrender/llshadermgr.h +++ b/indra/llrender/llshadermgr.h @@ -615,6 +615,19 @@ 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 + END_RESERVED_UNIFORMS } eGLSLReservedUniforms; // clang-format on diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index c14702fec4..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 @@ -1145,6 +1146,7 @@ if(AL_BUILD_VIEWER) alfloaterxuistudio.h alviewcapture.h algradehistory.h + alhoverglow.h alhuddepth.h allegacynotificationwellwindow.h allightboxdirectory.h 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..d18f649236 --- /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/alselectionoutline.cpp b/indra/newview/alselectionoutline.cpp index 6d3822ef75..441f3ff1bc 100644 --- a/indra/newview/alselectionoutline.cpp +++ b/indra/newview/alselectionoutline.cpp @@ -30,6 +30,7 @@ #include "lldrawpool.h" #include "llface.h" #include "llfetchedgltfmaterial.h" +#include "llframetimer.h" #include "llglstates.h" #include "lltextureentry.h" #include "llui.h" @@ -58,46 +59,48 @@ namespace } } -ALSelectionOutline& ALSelectionOutline::instance() -{ - static ALSelectionOutline* outline = new ALSelectionOutline(); - return *outline; -} - ALSelectionOutline::View ALSelectionOutline::worldView() { static LLCachedControl contour_width(gSavedSettings, "AlchemySelectionOutlineWidth", DEFAULT_CONTOUR_WIDTH); - static LLCachedControl wireframe(gSavedSettings, "AlchemySelectionWireframe", false); return makeView(LLViewerCamera::getCurrent().getModelview(), gGL.getProjectionMatrix(), contour_width, - LLUI::getScaleFactor().mV[VX], false, wireframe); + LLUI::getScaleFactor().mV[VX], false, LLPipeline::RenderHighlightThickness, + LLPipeline::RenderHighlightBrightness); } bool ALSelectionOutline::hudView(View& view) { static LLCachedControl contour_width(gSavedSettings, "AlchemySelectionOutlineWidth", DEFAULT_CONTOUR_WIDTH); - static LLCachedControl wireframe(gSavedSettings, "AlchemySelectionWireframe", false); LLMatrix4a projection; LLMatrix4a modelview; if (!get_hud_matrices(projection, modelview)) { return false; } - view = makeView(modelview, projection, contour_width, LLUI::getScaleFactor().mV[VX], true, wireframe); + 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) + 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()) { - mEntries[found->second] = { object, te_mask, colour, show_hidden, priority }; + Entry& added = mEntries[found->second]; + if (replaces(added.mPriority, priority)) + { + added = entry; + } return; } @@ -106,7 +109,7 @@ void ALSelectionOutline::add(LLViewerObject* object, U32 te_mask, const LLColor4 return; } mEntryIndex.emplace(object, (U32)mEntries.size()); - mEntries.push_back({ object, te_mask, colour, show_hidden, priority }); + mEntries.push_back(entry); } void ALSelectionOutline::clearEntries() @@ -117,7 +120,6 @@ void ALSelectionOutline::clearEntries() void ALSelectionOutline::release() { - // Called every frame nothing is outlined, so it touches nothing that is not there. if (mIdMap.isComplete()) { mIdMap.release(); @@ -138,11 +140,38 @@ void ALSelectionOutline::release() 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 (mMarkedMap.isComplete()) + { + mMarkedMap.release(); + } + if (mTileMap.isComplete()) + { + mTileMap.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()) { - release(); + releaseStale(use.mFrame, use.mTime); return; } @@ -154,54 +183,50 @@ void ALSelectionOutline::render(const View& view) const S32 vp_y = gGLViewport[1]; const S32 width = gGLViewport[2]; const S32 height = gGLViewport[3]; - const Passes passes = passesFor(view, gSelectionWireframeProgram.isComplete() && - gSelectionWireframeProgram.mRiggedVariant != nullptr); - if (!passes.any()) + + Wants wants; + for (const Entry& entry : mEntries) { - release(); - clearEntries(); - return; + wants.mContour = wants.mContour || (entry.mParts & PART_CONTOUR); + wants.mWireframe = wants.mWireframe || (entry.mParts & PART_WIREFRAME); + wants.mGlow = wants.mGlow || entry.mPriority == PRIORITY_HOVER; } - if (width <= 0 || height <= 0 || !gPipeline.mRT || !gPipeline.mScreenTriangleVB || + 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 a pass that does not run reads are given up; the others are made at the window's size. + // 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 (passes.mIds) { - if (mIdMap.isComplete()) + if (!mIdMap.isComplete() || mIdMap.getWidth() != (U32)width || mIdMap.getHeight() != (U32)height) { - mIdMap.release(); + allocated = mIdMap.allocate(width, height, GL_RGBA8, true, false, ALTextureSlot::TT_TEXTURE, + LLRenderTarget::MIPS_NONE, LLPipeline::mainDepthFormat()); } + mIdUse = use; } - else 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()); - } - if (!passes.mEdges) + if (passes.mEdges) { - if (mMarkedMap.isComplete()) - { - mMarkedMap.release(); - } - if (mTileMap.isComplete()) + if (!mMarkedMap.isComplete() || !mTileMap.isComplete() || mMarkedMap.getWidth() != (U32)width || + mMarkedMap.getHeight() != (U32)height) { - mTileMap.release(); + allocated = allocated && mMarkedMap.allocate(width, height, GL_RGBA8) && + mTileMap.allocate(tileCount((U32)width), tileCount((U32)height), GL_RGBA16); } + mEdgeUse = use; } - else if (!mMarkedMap.isComplete() || !mTileMap.isComplete() || mMarkedMap.getWidth() != (U32)width || - mMarkedMap.getHeight() != (U32)height) - { - allocated = allocated && mMarkedMap.allocate(width, height, GL_RGBA8) && - mTileMap.allocate(tileCount((U32)width), tileCount((U32)height), GL_RGBA16); - } + mPaletteUse = use; + releaseStale(use.mFrame, use.mTime); if (!allocated) { LL_WARNS_ONCE("Pipeline") << "Could not allocate the selection outline's targets" << LL_ENDL; @@ -245,22 +270,24 @@ void ALSelectionOutline::render(const View& view) drawIds(view, width, height, mvp); } - // What the id pass drew says whether the outline has anything to touch; the world's wireframe, drawn without - // it, draws whatever its faces put on screen. - if (passes.mIds ? mScissorValid : passes.mWireframe) + // 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 (passes.mWireframe) + if (wireframe) { drawWireframes(view, width, height); } - if (passes.mEdges) + if (edges) { - // The scissor holds every selected texel and the contour's reach 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. + // 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, mMarkedMap, mScissor, view, *gPipeline.mScreenTriangleVB); gSelectionTileProgram.bind(); @@ -274,7 +301,7 @@ void ALSelectionOutline::render(const View& view) mScissor.getHeight()); gSelectionOutlineProgram.bind(); - drawEdges(gSelectionOutlineProgram, mMarkedMap, mIdMap, mTileMap, mPalette, view, + drawEdges(gSelectionOutlineProgram, mMarkedMap, mIdMap, mTileMap, mPalette, view, wants.mGlow, *gPipeline.mScreenTriangleVB); } } @@ -325,7 +352,11 @@ void ALSelectionOutline::drawIds(const View& view, S32 width, S32 height, const bindIdPass(program, view.mHUD ? nullptr : &gPipeline.mRT->deferredScreen, width, height); for (U32 i = 0; i < count; ++i) { - drawObject(program, mEntries[mOrder[i]], i + 1, rigged, view, mvp); + 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); @@ -459,8 +490,12 @@ bool ALSelectionOutline::drawFaces(LLGLSLShader& program, const Entry& entry, bo 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); })) + if (!drawFaces(program, entry, rigged, view, box, + [&] { setId(program, id, entry.mPriority, entry.mShowHidden, drawn); })) { return; } @@ -475,7 +510,8 @@ void ALSelectionOutline::drawObject(LLGLSLShader& program, const Entry& entry, U else { const LLVector4a extents[2] = { box.mLo, box.mHi }; - if (!scissorRect(mvp, extents, width, height, lineReach(view.mWidth), rect)) + const S32 reach = lineReach(entry.mPriority == PRIORITY_HOVER ? view.mGlowRadius : view.mWidth); + if (!scissorRect(mvp, extents, width, height, reach, rect)) { return; } @@ -507,7 +543,7 @@ void ALSelectionOutline::drawWireframes(const View& view, S32 width, S32 height) bool any_hidden = false; for (const Entry& entry : mEntries) { - any_hidden = any_hidden || entry.mShowHidden; + any_hidden = any_hidden || inWirePass(entry.mParts, entry.mShowHidden, WIRE_HIDDEN); } gGL.matrixMode(LLRender::MM_PROJECTION); @@ -547,8 +583,9 @@ void ALSelectionOutline::drawWireframes(const View& view, S32 width, S32 height) 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 (pass == WIRE_HIDDEN && !entry.mShowHidden) + if (!inWirePass(entry.mParts, entry.mShowHidden, pass)) { continue; } @@ -594,15 +631,18 @@ ALSelectionOutline::FaceBinding ALSelectionOutline::bindFace(LLGLSLShader& progr } if (texture && texture_has_alpha(texture)) { - cutoff = gltfAlphaCutoff(gltf->mAlphaMode, gltf->mAlphaCutoff); + 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 = legacyAlphaCutoff(material != nullptr, material ? material->getDiffuseAlphaMode() : 0, - material ? material->getAlphaMaskCutoff() : 0, texture_has_alpha(texture)); + cutoff = faceAlphaCutoff(te->getID(), + legacyAlphaCutoff(material != nullptr, material ? material->getDiffuseAlphaMode() : 0, + material ? material->getAlphaMaskCutoff() : 0, + texture_has_alpha(texture))); } setAlphaTest(program, cutoff, transform); diff --git a/indra/newview/alselectionoutline.h b/indra/newview/alselectionoutline.h index 1577f1b3ef..88e68e84d8 100644 --- a/indra/newview/alselectionoutline.h +++ b/indra/newview/alselectionoutline.h @@ -24,6 +24,7 @@ #pragma once +#include "indra_constants.h" #include "llgltfmaterial.h" #include "llglslshader.h" #include "llglstates.h" @@ -35,7 +36,8 @@ #include "llrender.h" #include "llrendertarget.h" #include "llshadermgr.h" -#include "llstaticstringtable.h" +#include "llsingleton.h" +#include "lluuid.h" #include "llvertexbuffer.h" #include "v4color.h" @@ -50,10 +52,11 @@ class LLFace; class LLViewerObject; class LLVOAvatar; -/// Outlines of the selected, rect-highlighted and media-focused objects, 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. A frame outlines one or the other, as the selection is in the world or on -/// the HUD. +/// 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 @@ -62,8 +65,9 @@ class LLVOAvatar; /// 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. Integer formats -/// are not used because LLGLSLShader gives integer samplers no unit. +/// 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) /// copies the id target into another, marking the texels on the near side of a jump: a step in an object's own @@ -72,7 +76,7 @@ class LLVOAvatar; /// 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 contour's reach. A pixel draws the contour of the drawn surfaces within its reach that stand +/// 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, @@ -83,6 +87,12 @@ class LLVOAvatar; /// 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, @@ -93,15 +103,22 @@ class LLVOAvatar; /// /// 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 -/// marked target the lowest and highest nonzero id it holds and whether it holds the near side of a jump, and 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 the early out changes nothing it draws. +/// marked 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. It searches +/// as far as the glow reaches only where a tile within that reach holds a glowing surface other than the pixel's +/// own. Whatever a pixel draws, it draws from such a texel within its reach, whose own tile records it, so neither +/// changes anything it draws. /// -/// The targets exist only while something is outlined: they are made the first frame there is, given up the -/// first frame there is not, and given up with the pipeline's own (LLPipeline::releaseGLBuffers). -class ALSelectionOutline +/// 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. @@ -119,9 +136,21 @@ class ALSelectionOutline 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 { @@ -136,30 +165,61 @@ class ALSelectionOutline /// 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 outline (RenderHighlightSelections), drawn with or without the wireframe. - bool mOutline = true; - /// The selected faces' triangles drawn as lines under the outline (AlchemySelectionWireframe), mWireWidth - /// pixels wide before their pixel of anti-aliasing, with or without the outline. - bool mWireframe = 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; }; - /// The passes render() runs for a view. + /// 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 ids, and the depth of the selected surfaces the HUD's lines are tested against. + /// 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 from the ids. + /// 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 `view` needs, 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 View& view, bool wireframe_ready); + /// 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(); @@ -169,9 +229,11 @@ class ALSelectionOutline /// 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. + /// 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, bool wireframe = false); + 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; @@ -204,16 +266,21 @@ class ALSelectionOutline /// 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 uniforms the passes set beyond LLShaderMgr's reserved ones. - static constexpr const char* ID_UNIFORM = "selection_id"; - static constexpr const char* PRIORITY_UNIFORM = "selection_priority"; - static constexpr const char* SHOW_HIDDEN_UNIFORM = "selection_show_hidden"; - static constexpr const char* SCENE_DEPTH_UNIFORM = "selection_scene_depth"; - static constexpr const char* WIDTH_UNIFORM = "outline_width"; - static constexpr const char* INNER_WIDTH_UNIFORM = "outline_inner_width"; - static constexpr const char* WIRE_PASS_UNIFORM = "wireframe_pass"; - static constexpr const char* WIRE_WIDTH_UNIFORM = "wireframe_width"; + /// 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 every pixel near a glowing object. + static constexpr F32 MAX_GLOW_RADIUS = 16.f; + 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 @@ -228,6 +295,13 @@ class ALSelectionOutline 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` @@ -255,21 +329,33 @@ class ALSelectionOutline /// its slope, and a pixel more for the line's own. static F32 wireOffsetFactor(F32 resample) { return llmax(resample, 1.f) + 1.f; } - /// Lives for the session and is never destroyed: its GL objects go with the pipeline's, and at exit GL is gone. - static ALSelectionOutline& instance(); + /// 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); - /// Outlines `object` this frame: 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. - /// Adding an object again replaces what it was added with. - void add(LLViewerObject* object, U32 te_mask, const LLColor4& colour, bool show_hidden, EPriority priority); - - /// Draws what was added since the last call over the bound framebuffer, with `view`, and forgets it. With - /// nothing added it gives up its targets. + /// Draws what was added since the last call over the bound framebuffer, with `view`, and forgets it, giving up + /// the targets no call has drawn with since the frame before this one. 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); } @@ -283,6 +369,27 @@ class ALSelectionOutline /// 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. + 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, with the brightness in the integer steps the pass is given it in. + 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; } @@ -327,8 +434,10 @@ class ALSelectionOutline LLGLDisable mScissor{ GL_SCISSOR_TEST }; }; - /// The id, priority and hidden parts the bound id program writes for the faces drawn next. - static void setId(LLGLSLShader& program, U32 id, EPriority priority, bool show_hidden); + /// 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). @@ -359,13 +468,12 @@ class ALSelectionOutline /// The edge pass with the bound outline program and `view`'s widths, over the viewport and scissor in force: /// ids from `marked`, their depth from `ids` through `view`'s projection, which the id pass drew with, and the - /// tile pass's record from `tiles`. + /// tile pass's record from `tiles`. With `glow`, where an object glows, the pass reaches as far as the glow; and + /// only then. static void drawEdges(LLGLSLShader& program, LLRenderTarget& marked, LLRenderTarget& ids, LLRenderTarget& tiles, - U32 palette, const View& view, LLVertexBuffer& triangle); + U32 palette, const View& view, bool glow, LLVertexBuffer& triangle); private: - ALSelectionOutline() = default; - // 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); @@ -381,6 +489,8 @@ class ALSelectionOutline 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 @@ -399,19 +509,30 @@ class ALSelectionOutline 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 into - // mScissor. + // 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 into mIdMap, a width x height target, in priority order, and the box it covers, - // through `mvp`, into mScissor. + // 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, 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. + // 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 { @@ -439,6 +560,11 @@ class ALSelectionOutline U32 mPalette = 0; U32 mPaletteRows = 0; + // When the id target, the jump and tile targets, and the palette were last drawn with. + Use mIdUse; + Use mEdgeUse; + Use mPaletteUse; + // The pixels this frame's outlines can touch, and whether any can. LLRect mScissor; bool mScissorValid = false; @@ -477,8 +603,23 @@ inline S32 ALSelectionOutline::contourWidth(F32 width, F32 ui_scale) 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 lineWidth(llclamp(held * GLOW_PIXELS, 0.f, MAX_GLOW_RADIUS), ui_scale); +} + +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, bool wireframe) + F32 contour_width, F32 ui_scale, bool hud, + F32 glow_thickness, F32 glow_brightness) { View view; view.mModelview = modelview; @@ -486,17 +627,19 @@ inline ALSelectionOutline::View ALSelectionOutline::makeView(const LLMatrix4a& m view.mWidth = contourWidth(contour_width, ui_scale); view.mInnerWidth = lineWidth(INNER_WIDTH, ui_scale); view.mHUD = hud; - view.mWireframe = wireframe; 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 View& view, bool wireframe_ready) +inline ALSelectionOutline::Passes ALSelectionOutline::passesFor(const Wants& wants, bool hud, bool wireframe_ready) { Passes passes; - passes.mWireframe = view.mWireframe && wireframe_ready; - passes.mEdges = view.mOutline; - passes.mIds = view.mOutline || (passes.mWireframe && view.mHUD); + passes.mWireframe = wants.mWireframe && wireframe_ready; + passes.mEdges = wants.mContour || wants.mGlow; + passes.mIds = passes.mEdges || (passes.mWireframe && hud); return passes; } @@ -618,8 +761,6 @@ inline void ALSelectionOutline::uploadPalette(U32& palette, U32& palette_rows, c inline void ALSelectionOutline::bindIdPass(LLGLSLShader& program, LLRenderTarget* scene, S32 width, S32 height) { - static const LLStaticHashedString scene_depth_uniform(SCENE_DEPTH_UNIFORM); - if (scene) { program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, scene); @@ -629,18 +770,16 @@ inline void ALSelectionOutline::bindIdPass(LLGLSLShader& program, LLRenderTarget // Not read without a scene; left empty rather than holding whatever the unit last had. program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); } - program.uniform1i(scene_depth_uniform, scene ? 1 : 0); + 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) +inline void ALSelectionOutline::setId(LLGLSLShader& program, U32 id, EPriority priority, bool show_hidden, bool outlined) { - static const LLStaticHashedString id_uniform(ID_UNIFORM); - static const LLStaticHashedString priority_uniform(PRIORITY_UNIFORM); - static const LLStaticHashedString show_hidden_uniform(SHOW_HIDDEN_UNIFORM); - program.uniform1i(id_uniform, (GLint)id); - program.uniform1i(priority_uniform, (GLint)priority); - program.uniform1i(show_hidden_uniform, show_hidden ? 1 : 0); + 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, @@ -668,11 +807,8 @@ inline ALSelectionOutline::WireState::~WireState() inline void ALSelectionOutline::bindWirePass(LLGLSLShader& program, EWirePass pass, S32 wire_width, S32 width, S32 height, U32 palette, LLRenderTarget* ids) { - static const LLStaticHashedString pass_uniform(WIRE_PASS_UNIFORM); - static const LLStaticHashedString width_uniform(WIRE_WIDTH_UNIFORM); - - program.uniform1i(pass_uniform, (GLint)pass); - program.uniform1i(width_uniform, llmax(wire_width, 1)); + 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::ALTERNATE_DIFFUSE_MAP); if (channel > -1) @@ -768,11 +904,9 @@ inline void ALSelectionOutline::drawTiles(LLGLSLShader& program, LLRenderTarget& } inline void ALSelectionOutline::drawEdges(LLGLSLShader& program, LLRenderTarget& marked, LLRenderTarget& ids, - LLRenderTarget& tiles, U32 palette, const View& view, LLVertexBuffer& triangle) + LLRenderTarget& tiles, U32 palette, const View& view, bool glow, + LLVertexBuffer& triangle) { - static const LLStaticHashedString width_uniform(WIDTH_UNIFORM); - static const LLStaticHashedString inner_width_uniform(INNER_WIDTH_UNIFORM); - // Point sampled all four: the ids, their depth, the tiles and the palette are data, and texelFetch reads them // whatever the filter. program.bindTexture(LLShaderMgr::DIFFUSE_MAP, &marked, ALSamplers::PointClamp); @@ -783,8 +917,11 @@ inline void ALSelectionOutline::drawEdges(LLGLSLShader& program, LLRenderTarget& { gGL.getTextureSlot(channel)->bindManual(ALTextureSlot::TT_TEXTURE, palette, gGL.getSampler(ALSamplers::PointClamp)); } - program.uniform1i(width_uniform, llmax(view.mWidth, 1)); - program.uniform1i(inner_width_uniform, llmax(view.mInnerWidth, 1)); + 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 ? llmax(view.mGlowRadius, 1) : 0); + program.uniform1f(LLShaderMgr::OUTLINE_GLOW_BRIGHTNESS, view.mGlowBrightness); drawTriangle(triangle, &view.mProjection); diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index b3671534f7..eab6b81e6d 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 diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl index b252f8d484..17d1b8b0af 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionIdF.glsl @@ -39,6 +39,10 @@ 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; @@ -85,7 +89,7 @@ void main() discard; } - float drawn = (visible > 0.5) ? 1.0 : ((selection_show_hidden != 0) ? 0.5 : 0.0); + 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/selectionOutlineF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl index 109014fd34..6d8bc54ac1 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl @@ -1,7 +1,7 @@ /** * @file selectionOutlineF.glsl * @brief The selection outline's edge pass (ALSelectionOutline): the contour of each selected object over what lies - * behind it, and the fainter edges between objects that touch. + * 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 @@ -38,8 +38,9 @@ uniform sampler2D depthMap; uniform sampler2D altDiffuseMap; // 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, and 1 in .b where it holds the near side of a jump. Bound under a -// reserved name, the only kind LLGLSLShader gives a texture unit. +// 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. Bound under a reserved name, the only kind LLGLSLShader gives a +// texture unit. uniform sampler2D specularMap; // The widths in pixels of the contour around the objects and of the edges between them, before the pixel each @@ -47,6 +48,11 @@ uniform sampler2D specularMap; 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). @@ -55,6 +61,9 @@ uniform int outline_inner_width; // 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. @@ -70,6 +79,7 @@ const int BEHIND = 2; int decodeId(vec4 texel); int decodePriority(vec4 texel); bool isJumpFront(vec4 texel); +bool isGlow(vec4 texel); bool isDrawn(vec4 texel); bool isVisible(vec4 texel); float eyeDistance(float depth); @@ -89,6 +99,16 @@ 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(diffuseMap, pos, 0)) == id; @@ -136,6 +156,30 @@ bool candidateWithin(ivec2 pos, int reach, int own_id) return false; } +// Whether a tile under the square of texels within `reach` of `pos` along either axis 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. +bool glowWithin(ivec2 pos, int reach, int own_id) +{ + ivec2 last = textureSize(specularMap, 0) - ivec2(1); + ivec2 lo = max(pos - ivec2(reach), ivec2(0)) / TILE_SIZE; + ivec2 hi = min((pos + ivec2(reach)) / TILE_SIZE, last); + for (int y = lo.y; y <= hi.y; ++y) + { + for (int x = lo.x; x <= hi.x; ++x) + { + vec4 tile = texelFetch(specularMap, ivec2(x, y), 0); + int lowest = int(tile.r * 65535.0 + 0.5); + int highest = int(tile.g * 65535.0 + 0.5); + if (tile.a > 0.5 && (lowest != own_id || highest != own_id)) + { + return true; + } + } + } + return false; +} + void main() { ivec2 size = textureSize(diffuseMap, 0); @@ -143,15 +187,22 @@ void main() vec4 own = texelFetch(diffuseMap, 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 reach = max(outline_width, outline_inner_width) + 1; + // Every texel in the disk the lines reach, so features thinner than a line are not stepped over, and as far as + // the glow reaches where a glowing surface is near. 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; - if (!candidateWithin(pos, reach, own_id)) + int glow_reach = (outline_glow_radius > 0) ? outline_glow_radius + 1 : 0; + bool glow_near = (glow_reach > 0) && glowWithin(pos, glow_reach, own_id); + if (!glow_near && !candidateWithin(pos, line_reach, own_id)) { discard; } + int reach = glow_near ? 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; @@ -166,12 +217,17 @@ void main() bool top_visible = true; int near_id = 0; int near_priority = 256; - float near_dist = float(reach) + 1.0; + 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; + // The glow: the nearest drawn texel of a glowing object in front of this pixel, the lower id where two are as + // near. + int glow_id = 0; + float glow_dist = float(glow_reach) + 1.0; + for (int dy = -reach; dy <= reach; ++dy) { for (int dx = -reach; dx <= reach; ++dx) @@ -190,6 +246,14 @@ void main() { continue; } + // A glow reaches as far as its radius and never over its own object, jumps and all; a line as far as its + // width. + bool tap_glows = isGlow(tap); + int tap_reach = tap_glows ? glow_reach : line_reach; + if ((tap_glows && own_object) || dx * dx + dy * dy > 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, @@ -212,7 +276,16 @@ void main() } bool tap_drawn = isDrawn(tap); - if (relation == IN_FRONT && tap_drawn) + bool in_front = (relation == IN_FRONT) || (own_glows && relation == TOUCHING); + if (tap_glows) + { + if (relation == IN_FRONT && tap_drawn && (dist < glow_dist || (dist == glow_dist && tap_id < glow_id))) + { + glow_id = tap_id; + glow_dist = dist; + } + } + else if (in_front && tap_drawn) { int tap_priority = decodePriority(tap); if (tap_priority < top_priority || (tap_priority == top_priority && dist < top_dist)) @@ -232,7 +305,7 @@ void main() } // 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 && dist <= float(inner_reach) && + 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); @@ -240,14 +313,21 @@ void main() } } - // Premultiplied, the contour over the edge. + // Premultiplied: the glow, the edge over it, and the contour over both. vec3 rgb = vec3(0.0); float alpha = 0.0; - if (own_drawn) + if (glow_id != 0) + { + vec4 glow_colour = paletteColour(glow_id, true); + alpha = glow_colour.a * glowOpacity(glow_dist); + rgb = glow_colour.rgb * alpha; + } + if (own_drawn && !own_glows) { vec4 colour = paletteColour(own_id, isVisible(own)); - alpha = colour.a * INNER_OPACITY * coverage(inner_dist, float(outline_inner_width)); - rgb = colour.rgb * alpha; + 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) { diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl index b490c26537..192c277032 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl @@ -24,7 +24,7 @@ */ // 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. +// 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 with its jumps marked (selectionJumpF.glsl). @@ -36,6 +36,8 @@ uniform sampler2D diffuseMap; // selectionUtilF.glsl int decodeId(vec4 texel); bool isJumpFront(vec4 texel); +bool isGlow(vec4 texel); +bool isDrawn(vec4 texel); void main() { @@ -47,6 +49,7 @@ void main() 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) @@ -58,9 +61,11 @@ void main() lowest = min(lowest, id); highest = max(highest, id); jump = jump || isJumpFront(texel); + 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, 1.0); + 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 index 4d43f349d2..9868410fc3 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionUtilF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionUtilF.glsl @@ -30,6 +30,10 @@ // visible, 0.5 hidden and drawn dimmed, 0 hidden and left out; in .a the priority, 0 the highest, below 128, and // 128 more on a texel the jump pass marks as the near side of a jump in its object's surface. +// 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); @@ -40,6 +44,11 @@ int decodePriority(vec4 texel) return int(texel.a * 255.0 + 0.5) & 127; } +bool isGlow(vec4 texel) +{ + return decodePriority(texel) == PRIORITY_HOVER; +} + bool isJumpFront(vec4 texel) { return int(texel.a * 255.0 + 0.5) >= 128; diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index 171fbc782c..a649790825 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -51,6 +51,7 @@ #include "llquaternion.h" // viewer includes +#include "alhoverglow.h" #include "alobjectproperties.h" #include "alselectionoutline.h" #include "llagent.h" @@ -6691,8 +6692,9 @@ void LLSelectMgr::updateHighlightedObjects() } } -// The selected and highlighted objects in the world, or on the HUD, each in the colour of its role. -void LLSelectMgr::addOutlines(ALSelectionOutline& outline, 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 (mSelectedObjects->getNumNodes()) { @@ -6756,7 +6758,7 @@ void LLSelectMgr::addOutlines(ALSelectionOutline& outline, bool for_hud) priority = ALSelectionOutline::PRIORITY_CHILD; } colour.mV[VALPHA] = sHighlightAlpha; - outline.add(objectp, (U32)node->getTESelectMask(), colour, show_hidden, priority); + outline.add(objectp, (U32)node->getTESelectMask(), colour, show_hidden, priority, parts); } } @@ -6791,14 +6793,15 @@ void LLSelectMgr::addOutlines(ALSelectionOutline& outline, bool for_hud) continue; } colour.mV[VALPHA] = sHighlightAlpha; - outline.add(objectp, (U32)node->getTESelectMask(), colour, sRenderHiddenSelections, priority); + outline.add(objectp, (U32)node->getTESelectMask(), colour, sRenderHiddenSelections, priority, parts); } } } -// Outlines from an id target (ALSelectionOutline): one id per object, so every prim keeps its own outline. A -// frame calls this once, for the world or for the HUD as the selection is in one or on the other. The outline -// (RenderHighlightSelections) and the wireframe (AlchemySelectionWireframe) are drawn each with or without the other. +// 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(); @@ -6807,17 +6810,19 @@ void LLSelectMgr::renderSilhouettes(bool for_hud) if (!for_hud) { view = ALSelectionOutline::worldView(); + + // 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)) { - outline.release(); - return; - } - view.mOutline = mRenderHighlightSelections; - - if (!view.mOutline && !view.mWireframe) - { - outline.release(); + // The world's call, every frame, gives the targets up once no call draws with them. return; } @@ -6827,7 +6832,17 @@ void LLSelectMgr::renderSilhouettes(bool for_hud) return; } - addOutlines(outline, for_hud); + 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) + { + ALHoverGlow::instance().addTo(outline); + } outline.render(view); } diff --git a/indra/newview/llselectmgr.h b/indra/newview/llselectmgr.h index cc6f30145d..5ece32f231 100644 --- a/indra/newview/llselectmgr.h +++ b/indra/newview/llselectmgr.h @@ -842,7 +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 addOutlines(ALSelectionOutline& outline, bool for_hud); + 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), diff --git a/indra/newview/lltoolpie.cpp b/indra/newview/lltoolpie.cpp index 37c1036509..aaa903c2ba 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 @@ -879,6 +892,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; @@ -887,12 +901,14 @@ bool LLToolPie::handleHover(S32 x, S32 y, MASK mask) else if ( (object) && (RlvActions::isRlvEnabled()) && (!RlvActions::canTouch(object, mHoverPick.mObjectOffset)) ) { // Block showing the "grab" or "touch" cursor if we can't touch/grab the object + show_highlight = false; gViewerWindow->setCursor(UI_CURSOR_ARROW); } // [/RLVa:KB] 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 +916,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 +932,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/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 606bbfe9e8..30ee15e6cf 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -4538,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/pipeline.cpp b/indra/newview/pipeline.cpp index 4933ea1400..77e2a970fe 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -1391,7 +1391,10 @@ void LLPipeline::releaseGLBuffers() } ALFarPlaneOverlay::release(); - ALSelectionOutline::instance().release(); + if (ALSelectionOutline::instanceExists()) + { + ALSelectionOutline::instance().release(); + } mHeroProbeManager.cleanup(); // release hero probes diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index aad6bb9672..25e8efabde 100644 --- a/indra/newview/skins/default/xui/en/menu_viewer.xml +++ b/indra/newview/skins/default/xui/en/menu_viewer.xml @@ -4144,6 +4144,16 @@ function="World.EnvPreset" function="Advanced.HandleAttachedLightParticles" parameter="RenderAttachedParticles" /> + + + + diff --git a/indra/newview/tests/alselectionoutline_gl_test.cpp b/indra/newview/tests/alselectionoutline_gl_test.cpp index af59a636cd..80ebe31d99 100644 --- a/indra/newview/tests/alselectionoutline_gl_test.cpp +++ b/indra/newview/tests/alselectionoutline_gl_test.cpp @@ -2,7 +2,8 @@ * @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, and the palette, width and scissor helpers. + * 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 @@ -26,6 +27,7 @@ #include "linden_common.h" +#include "../alhoverglow.h" #include "../alselectionoutline.h" #include "llgl.h" @@ -42,6 +44,7 @@ #include "../test/lltut.h" +#include #include #include #include @@ -206,7 +209,7 @@ namespace tut mScene.release(); mFar.release(); mFrame.release(); - for (U32* texture : { &mPalette, &mRamp }) + for (U32* texture : { &mPalette, &mRamp, &mClear }) { if (*texture) { @@ -470,6 +473,21 @@ namespace tut return mRamp; } + // A texture that is clear everywhere, as the library's transparent texture (IMG_TRANSPARENT) is. + U32 clearTexture() + { + if (!mClear) + { + const std::vector texels(4 * 4 * 4, 0); + LLImageGL::generateTextures(1, &mClear); + gGL.getTextureSlot(0)->bindManual(ALTextureSlot::TT_TEXTURE, mClear); + LLImageGL::allocateTexture2D(ALTextureSlot::getInternalType(ALTextureSlot::TT_TEXTURE), GL_RGBA8, 4, 4, + GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); + gGL.getTextureSlot(0)->unbind(); + } + return mClear; + } + struct Draw { LLVertexBuffer* mBuffer; @@ -483,12 +501,27 @@ namespace tut // 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. False where a texture had no unit to go to. - bool drawAll(LLGLSLShader& program, const std::vector& draws, const F32* joints, bool hidden_only = false) + // 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) @@ -499,11 +532,12 @@ namespace tut } for (const Draw& draw : draws) { - if (hidden_only && !draw.mShowHidden) + if (!drawn(draw)) { continue; } - Outline::setId(program, draw.mId, draw.mPriority, draw.mShowHidden); + 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) @@ -552,7 +586,10 @@ namespace tut { program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, unread); } - texture_read = drawAll(program, draws, palette); + // 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(); @@ -623,6 +660,15 @@ namespace tut 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) @@ -638,7 +684,7 @@ namespace tut LLGLSLShader::unbind(); } - // What the tile pass recorded, four 16-bit values a tile: the lowest and highest id, the jump, and 1. + // 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); @@ -657,10 +703,11 @@ namespace tut } // The edge pass as ALSelectionOutline::render runs it, the jump and tile passes first, over the texels under - // `mark_rect` or all of them, then over a cleared frame, scissored when asked. `untiled` holds every id and a - // jump in every tile instead, so every pixel searches its whole disk. + // `mark_rect` or all of them, then over a cleared frame, scissored when asked, reaching as far as the glow + // with `glow`. `untiled` holds every id, a jump and a glowing surface in every tile instead, so every pixel + // searches its whole disk. std::vector edgePass(const Outline::View& lines, const LLRect* scissor = nullptr, bool untiled = false, - const LLRect* mark_rect = nullptr) + const LLRect* mark_rect = nullptr, bool glow = false) { Outline::uploadPalette(mPalette, mPaletteRows, mPaletteTexels, mRows); markPasses(lines, mark_rect, !untiled); @@ -681,11 +728,11 @@ namespace tut if (scissor) { LLGLSScissor scissored(scissor->mLeft, scissor->mBottom, scissor->getWidth(), scissor->getHeight()); - Outline::drawEdges(mOutlineProgram, mMarked, mIdMap, tiles, mPalette, lines, *mTriangle); + Outline::drawEdges(mOutlineProgram, mMarked, mIdMap, tiles, mPalette, lines, glow, *mTriangle); } else { - Outline::drawEdges(mOutlineProgram, mMarked, mIdMap, tiles, mPalette, lines, *mTriangle); + Outline::drawEdges(mOutlineProgram, mMarked, mIdMap, tiles, mPalette, lines, glow, *mTriangle); } LLGLSLShader::unbind(); px = ll_test::readFramebufferRGBA(W, H); @@ -749,7 +796,9 @@ namespace tut 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 == Outline::WIRE_HIDDEN) && texture_read; + 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::ALTERNATE_DIFFUSE_MAP); program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); @@ -801,6 +850,7 @@ namespace tut U32 mPaletteRows = 0; U32 mRows = 0; U32 mRamp = 0; + U32 mClear = 0; std::vector mPaletteTexels; LLPointer mTriangle; std::vector> mBuffers; @@ -2061,8 +2111,8 @@ namespace tut } } - // 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 - // on only with its setting; and the polygon offset its lines need over a resampled depth. + // 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>() { @@ -2070,11 +2120,8 @@ namespace tut ensure_equals("two at 2", Outline::lineWidth(Outline::WIRE_WIDTH, 2.f), 2); for (bool hud : { false, true }) { - const Outline::View off = Outline::makeView(identity(), identity(), 2.f, 2.f, hud); - const Outline::View on = Outline::makeView(identity(), identity(), 2.f, 2.f, hud, true); - ensure("off by default", !off.mWireframe); - ensure("on with its setting", on.mWireframe); - ensure_equals("its width at the UI scale", on.mWireWidth, 2); + 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); @@ -2136,40 +2183,446 @@ namespace tut } } - // The outline and the wireframe are drawn each without the other, and each runs only the passes it reads: the - // outline 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. + // 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::View view = Outline::makeView(identity(), identity(), 2.f, 1.f, hud); - ensure("the outline on by default" + where, view.mOutline); - - Outline::Passes passes = Outline::passesFor(view, true); + 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); - view.mWireframe = true; - passes = Outline::passesFor(view, true); + wants.mWireframe = true; + passes = Outline::passesFor(wants, hud, true); ensure("both run everything" + where, passes.mIds && passes.mEdges && passes.mWireframe); - passes = Outline::passesFor(view, false); + passes = Outline::passesFor(wants, hud, false); ensure("a wireframe program that did not load leaves the outline" + where, passes.mIds && passes.mEdges && !passes.mWireframe); - view.mOutline = false; - passes = Outline::passesFor(view, true); + 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(view, false); + passes = Outline::passesFor(wants, hud, false); ensure("nothing when its program did not load" + where, !passes.any()); - view.mWireframe = false; - ensure("nothing with both off" + where, !Outline::passesFor(view, true).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. + 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: the near side of a jump in its own surface. + 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, "the fold's edge is the near side of a jump"), 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))); } } } From 4691b9ca7c884ce4154c62ca202807d721a85800 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 02:39:29 -0400 Subject: [PATCH 10/11] Glass keeps its selection outline, and the hover glow costs a fraction of what it did A blended face is cut out of the id pass where its texture alpha is under 0.1, not 0.5. A pane of glass textured at 0.2 to 0.45 lost its outline, its wireframe and its glow outright; the clear texels around a leaf on its card are still cut. The hover glow searched a disk out to its reach, about 1100 texels a pixel at 5K and a UI scale of 2, around every glowing object, and every pixel in the scissor scanned up to 36 tiles first to learn whether a glow was near. Two passes now do that work once: - The glow reach pass (selectionGlowReachF) records for each tile the lowest and highest id of the glowing tiles within the glow's reach, so a pixel learns from one texel whether a glow other than its own object's can be near. - The glow row pass (selectionGlowRowF) records how far along its row each texel lies from the nearest glowing texel. Off every object, where every glowing surface stands in front, the edge pass walks its column over those, about 2R reads; on an object, which glows stand in front depends on its plane, and the disk is still searched. The jump pass leaves out what glows, whose own surface draws nothing, so a hovered floor costs no jump work. Both give exactly what the full search gives, which stays in selectionOutlineF behind GLOW_DISK as the reference the tests compare against. Estimated fetches: 3.6 to 6.9 times fewer; a hovered floor under the camera at 5K, whose box takes the whole window, goes from about 750 M to 165 M. The jump pass writes its mark into a one-byte target of its own instead of a full RGBA8 copy of the id target, and the tile and edge passes read the ids from the id target: 9 bytes a pixel instead of 12 while anything is selected or hovered, about 44 MB less at 5K. The edge pass reads the marks only where a tile within the lines' reach holds a jump. The passes' textures are reserved samplers of their own (selectionIdMap, selectionJumpMap, selectionTileMap, selectionPalette, selectionGlowReachMap, selectionGlowRowMap) rather than diffuseMap, specularMap and altDiffuseMap. The selection programs load without failing the interface shaders: one a driver rejects is warned of and unloaded, and ALSelectionOutline draws without it. get_hud_matrices is declared in llviewerdisplay.h. RenderHighlightColor's comment calls it a colour. alselectionoutline_gl, now 42 cases: a blend cut at a tenth (13), a pane of glass outlined and wireframed whole and cut under a cutoff of a half (41), and the glow from the rows the full search's pixel for pixel at five reaches over thin, slanted, edge-clipped, hidden and tied shapes beside selections (42). 13 and 41 fail at the old cutoff; five shader mutants of the glow passes fail 34, 35, 36, 38 or 42. Co-Authored-By: Claude Opus 5.5 --- indra/llrender/llshadermgr.cpp | 6 + indra/llrender/llshadermgr.h | 6 + indra/newview/alhoverglow.h | 4 +- indra/newview/alselectionoutline.cpp | 69 +++-- indra/newview/alselectionoutline.h | 188 ++++++++++---- indra/newview/app_settings/settings.xml | 2 +- .../class1/interface/selectionGlowReachF.glsl | 67 +++++ .../class1/interface/selectionGlowRowF.glsl | 79 ++++++ .../class1/interface/selectionJumpF.glsl | 18 +- .../class1/interface/selectionOutlineF.glsl | 183 +++++++++----- .../class1/interface/selectionTileF.glsl | 14 +- .../class1/interface/selectionUtilF.glsl | 11 +- .../class1/interface/selectionWireframeF.glsl | 4 +- indra/newview/llpanelprimmediacontrols.cpp | 4 - indra/newview/llviewerdisplay.cpp | 1 - indra/newview/llviewerdisplay.h | 6 + indra/newview/llviewershadermgr.cpp | 110 ++++---- indra/newview/llviewershadermgr.h | 2 + .../tests/alselectionoutline_gl_test.cpp | 236 +++++++++++++++--- 19 files changed, 766 insertions(+), 244 deletions(-) create mode 100644 indra/newview/app_settings/shaders/class1/interface/selectionGlowReachF.glsl create mode 100644 indra/newview/app_settings/shaders/class1/interface/selectionGlowRowF.glsl diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index 1ab20625c0..26a3d46300 100644 --- a/indra/llrender/llshadermgr.cpp +++ b/indra/llrender/llshadermgr.cpp @@ -2144,6 +2144,12 @@ void LLShaderMgr::initAttribsAndUniforms() 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 diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h index 27aa139786..65d7c7ca2c 100644 --- a/indra/llrender/llshadermgr.h +++ b/indra/llrender/llshadermgr.h @@ -627,6 +627,12 @@ class LLShaderMgr 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; diff --git a/indra/newview/alhoverglow.h b/indra/newview/alhoverglow.h index d18f649236..b4d067d38f 100644 --- a/indra/newview/alhoverglow.h +++ b/indra/newview/alhoverglow.h @@ -35,8 +35,8 @@ 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. +/// 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. diff --git a/indra/newview/alselectionoutline.cpp b/indra/newview/alselectionoutline.cpp index 441f3ff1bc..9984be28f6 100644 --- a/indra/newview/alselectionoutline.cpp +++ b/indra/newview/alselectionoutline.cpp @@ -36,6 +36,7 @@ #include "llui.h" #include "llviewercamera.h" #include "llviewercontrol.h" +#include "llviewerdisplay.h" #include "llviewerobject.h" #include "llviewerregion.h" #include "llviewershadermgr.h" @@ -45,9 +46,6 @@ #include -// llviewerdisplay.cpp: the matrices the HUD's attachments are drawn with. -bool get_hud_matrices(LLMatrix4a& proj, LLMatrix4a& model); - namespace { // Whether a texture's alpha can leave anything out, as LLPipeline::getPoolTypeFromTE reads it: media is drawn @@ -124,14 +122,22 @@ void ALSelectionOutline::release() { mIdMap.release(); } - if (mMarkedMap.isComplete()) + if (mJumpMap.isComplete()) { - mMarkedMap.release(); + mJumpMap.release(); } if (mTileMap.isComplete()) { mTileMap.release(); } + if (mGlowReachMap.isComplete()) + { + mGlowReachMap.release(); + } + if (mGlowRowMap.isComplete()) + { + mGlowRowMap.release(); + } if (mPalette) { LLImageGL::deleteTextures(1, &mPalette); @@ -149,15 +155,26 @@ void ALSelectionOutline::releaseStale(U32 frame, F64 now) } if (stale(mEdgeUse.mFrame, mEdgeUse.mTime, frame, now)) { - if (mMarkedMap.isComplete()) + if (mJumpMap.isComplete()) { - mMarkedMap.release(); + 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); @@ -217,14 +234,27 @@ void ALSelectionOutline::render(const View& view) } if (passes.mEdges) { - if (!mMarkedMap.isComplete() || !mTileMap.isComplete() || mMarkedMap.getWidth() != (U32)width || - mMarkedMap.getHeight() != (U32)height) + if (!mJumpMap.isComplete() || !mTileMap.isComplete() || mJumpMap.getWidth() != (U32)width || + mJumpMap.getHeight() != (U32)height) { - allocated = allocated && mMarkedMap.allocate(width, height, GL_RGBA8) && + 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) @@ -289,9 +319,18 @@ void ALSelectionOutline::render(const View& view) // 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, mMarkedMap, mScissor, view, *gPipeline.mScreenTriangleVB); + drawJumps(gSelectionJumpProgram, mIdMap, mJumpMap, mScissor, view, *gPipeline.mScreenTriangleVB); gSelectionTileProgram.bind(); - drawTiles(gSelectionTileProgram, mMarkedMap, mTileMap, mScissor, *gPipeline.mScreenTriangleVB); + 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); @@ -301,8 +340,8 @@ void ALSelectionOutline::render(const View& view) mScissor.getHeight()); gSelectionOutlineProgram.bind(); - drawEdges(gSelectionOutlineProgram, mMarkedMap, mIdMap, mTileMap, mPalette, view, wants.mGlow, - *gPipeline.mScreenTriangleVB); + drawEdges(gSelectionOutlineProgram, mIdMap, mJumpMap, mTileMap, glow ? &mGlowReachMap : nullptr, + glow ? &mGlowRowMap : nullptr, mPalette, view, *gPipeline.mScreenTriangleVB); } } @@ -593,7 +632,7 @@ void ALSelectionOutline::drawWireframes(const View& view, S32 width, S32 height) drawFaces(program, entry, rigged, view, box, [&] { setId(program, i + 1, entry.mPriority, entry.mShowHidden); }); } program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); - program.unbindTexture(LLShaderMgr::ALTERNATE_DIFFUSE_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_PALETTE); program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); } } diff --git a/indra/newview/alselectionoutline.h b/indra/newview/alselectionoutline.h index 88e68e84d8..f846c000df 100644 --- a/indra/newview/alselectionoutline.h +++ b/indra/newview/alselectionoutline.h @@ -70,10 +70,10 @@ class LLVOAvatar; /// LLGLSLShader gives integer samplers no unit. /// /// Ids are given in priority order, the highest first (EPriority). The jump pass (interface/selectionJumpF.glsl) -/// copies the id target into another, marking the texels 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. +/// 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 @@ -103,12 +103,21 @@ class LLVOAvatar; /// /// 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 -/// marked target the lowest and highest nonzero id it holds, whether it holds the near side of a jump and whether +/// 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. It searches -/// as far as the glow reaches only where a tile within that reach holds a glowing surface other than the pixel's -/// own. Whatever a pixel draws, it draws from such a texel within its reach, whose own tile records it, so neither -/// changes anything it draws. +/// 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 @@ -255,8 +264,9 @@ class ALSelectionOutline : public LLSingleton /// 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: where it is drawn at least half opaque. - static constexpr F32 BLEND_ALPHA_CUTOFF = 0.5f; + /// 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. @@ -275,8 +285,11 @@ class ALSelectionOutline : public LLSingleton 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 every pixel near a glowing object. + /// 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). @@ -338,7 +351,7 @@ class ALSelectionOutline : public LLSingleton U32 parts); /// Draws what was added since the last call over the bound framebuffer, with `view`, and forgets it, giving up - /// the targets no call has drawn with since the frame before this one. + /// the targets that are stale (stale). void render(const View& view); /// Gives up the targets. @@ -371,14 +384,14 @@ class ALSelectionOutline : public LLSingleton /// 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. + /// 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, with the brightness in the integer steps the pass is given it in. + /// 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 @@ -449,17 +462,34 @@ class ALSelectionOutline : public LLSingleton /// 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: `ids`, the id target, copied into `marked` with the near sides of - /// jumps marked, 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& marked, const LLRect& rect, + /// 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 - /// `marked`, over the tiles under `rect`, a rect of `marked`; the others are cleared, and hold nothing when the - /// rect covers every selected texel. - static void drawTiles(LLGLSLShader& program, LLRenderTarget& marked, LLRenderTarget& tiles, const LLRect& rect, - 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. @@ -467,11 +497,13 @@ class ALSelectionOutline : public LLSingleton LLRenderTarget* ids); /// The edge pass with the bound outline program and `view`'s widths, over the viewport and scissor in force: - /// ids from `marked`, their depth from `ids` through `view`'s projection, which the id pass drew with, and the - /// tile pass's record from `tiles`. With `glow`, where an object glows, the pass reaches as far as the glow; and - /// only then. - static void drawEdges(LLGLSLShader& program, LLRenderTarget& marked, LLRenderTarget& ids, LLRenderTarget& tiles, - U32 palette, const View& view, bool glow, LLVertexBuffer& triangle); + /// 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 @@ -555,14 +587,17 @@ class ALSelectionOutline : public LLSingleton std::vector mPaletteTexels; LLRenderTarget mIdMap; - LLRenderTarget mMarkedMap; + LLRenderTarget mJumpMap; LLRenderTarget mTileMap; + LLRenderTarget mGlowReachMap; + LLRenderTarget mGlowRowMap; U32 mPalette = 0; U32 mPaletteRows = 0; - // When the id target, the jump and tile targets, and the palette were last drawn with. + // 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. @@ -607,7 +642,7 @@ 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 lineWidth(llclamp(held * GLOW_PIXELS, 0.f, MAX_GLOW_RADIUS), ui_scale); + 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) @@ -810,7 +845,7 @@ inline void ALSelectionOutline::bindWirePass(LLGLSLShader& program, EWirePass pa 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::ALTERNATE_DIFFUSE_MAP); + const S32 channel = program.enableTexture(LLShaderMgr::SELECTION_PALETTE); if (channel > -1) { gGL.getTextureSlot(channel)->bindManual(ALTextureSlot::TT_TEXTURE, palette, gGL.getSampler(ALSamplers::PointClamp)); @@ -879,40 +914,82 @@ inline void ALSelectionOutline::drawUnder(LLRenderTarget& target, const LLRect& target.flush(); } -inline void ALSelectionOutline::drawJumps(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& marked, +inline void ALSelectionOutline::drawJumps(LLGLSLShader& program, LLRenderTarget& ids, LLRenderTarget& jumps, const LLRect& rect, const View& view, LLVertexBuffer& triangle) { - program.bindTexture(LLShaderMgr::DIFFUSE_MAP, &ids, ALSamplers::PointClamp); + program.bindTexture(LLShaderMgr::SELECTION_ID_MAP, &ids, ALSamplers::PointClamp); program.bindDepthTexture(LLShaderMgr::DEFERRED_DEPTH, &ids); - drawUnder(marked, rect, triangle, &view.mProjection); + drawUnder(jumps, rect, triangle, &view.mProjection); program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); - program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_ID_MAP); } -inline void ALSelectionOutline::drawTiles(LLGLSLShader& program, LLRenderTarget& marked, LLRenderTarget& tiles, - const LLRect& rect, LLVertexBuffer& triangle) +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::DIFFUSE_MAP, &marked, ALSamplers::PointClamp); - drawUnder(tiles, LLRect(left, top, right, bottom), triangle, nullptr); - program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); + 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::drawEdges(LLGLSLShader& program, LLRenderTarget& marked, LLRenderTarget& ids, - LLRenderTarget& tiles, U32 palette, const View& view, bool glow, - LLVertexBuffer& triangle) +inline void ALSelectionOutline::drawGlowReach(LLGLSLShader& program, LLRenderTarget& tiles, LLRenderTarget& reach, + const LLRect& rect, const View& view, LLVertexBuffer& triangle) { - // Point sampled all four: the ids, their depth, the tiles and the palette are data, and texelFetch reads them - // whatever the filter. - program.bindTexture(LLShaderMgr::DIFFUSE_MAP, &marked, ALSamplers::PointClamp); + // 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::SPECULAR_MAP, &tiles, ALSamplers::PointClamp); - const S32 channel = program.enableTexture(LLShaderMgr::ALTERNATE_DIFFUSE_MAP); + 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)); @@ -920,13 +997,16 @@ inline void ALSelectionOutline::drawEdges(LLGLSLShader& program, LLRenderTarget& 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 ? llmax(view.mGlowRadius, 1) : 0); + 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::ALTERNATE_DIFFUSE_MAP); - program.unbindTexture(LLShaderMgr::SPECULAR_MAP); + 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::DIFFUSE_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_ID_MAP); } diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index eab6b81e6d..1888bc877c 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -8831,7 +8831,7 @@ RenderHighlightColor Comment - Brightness of mouseover highlights. + Color of mouseover highlights. Persist 1 Type 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/selectionJumpF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionJumpF.glsl index 1ef4b60569..c46c23dded 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionJumpF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionJumpF.glsl @@ -1,6 +1,6 @@ /** * @file selectionJumpF.glsl - * @brief The selection outline's jump pass (ALSelectionOutline): the id target again, its texels marked where an + * @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$ @@ -23,11 +23,11 @@ * $/LicenseInfo$ */ -// The id texel (selectionUtilF.glsl), 128 added to its .a where it is the near side of a jump. +// 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 diffuseMap; +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 @@ -36,11 +36,12 @@ 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(diffuseMap, pos, 0)) == 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 @@ -96,13 +97,14 @@ vec4 gatherDepth(ivec2 corner, vec2 texel_size) void main() { // The viewport is the target's, a fragment to a texel. - ivec2 size = textureSize(diffuseMap, 0); + ivec2 size = textureSize(selectionIdMap, 0); ivec2 pos = ivec2(gl_FragCoord.xy); - vec4 texel = texelFetch(diffuseMap, pos, 0); + 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) + 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. @@ -116,5 +118,5 @@ void main() jumpsAlong(pos, size, ivec2(0, 1), id, depth, vec4(below.w, below.x, above.w, above.x)); } - frag_color = vec4(texel.rgb, (floor(texel.a * 255.0 + 0.5) + (front ? 128.0 : 0.0)) / 255.0); + 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 index 6d8bc54ac1..1c5a747385 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionOutlineF.glsl @@ -27,21 +27,29 @@ in vec2 tc; out vec4 frag_color; -// The id target with its jumps marked (selectionJumpF.glsl, selectionUtilF.glsl). Ids are given in priority order, -// so of two objects the lower id is the higher priority. -uniform sampler2D diffuseMap; +// 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 altDiffuseMap; +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. Bound under a reserved name, the only kind LLGLSLShader gives a -// texture unit. -uniform sampler2D specularMap; +// 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. @@ -78,7 +86,7 @@ const int BEHIND = 2; // selectionUtilF.glsl int decodeId(vec4 texel); int decodePriority(vec4 texel); -bool isJumpFront(vec4 texel); +bool isJumpFront(sampler2D jumps, ivec2 pos); bool isGlow(vec4 texel); bool isDrawn(vec4 texel); bool isVisible(vec4 texel); @@ -87,7 +95,7 @@ float eyeDistance(float depth); vec4 paletteColour(int id, bool visible) { int row = (id / PALETTE_WIDTH) * 2 + (visible ? 0 : 1); - return texelFetch(altDiffuseMap, ivec2(id % PALETTE_WIDTH, row), 0); + 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 @@ -111,7 +119,7 @@ float glowOpacity(float dist) bool hasId(ivec2 pos, ivec2 size, int id) { - return all(greaterThanEqual(pos, ivec2(0))) && all(lessThan(pos, size)) && decodeId(texelFetch(diffuseMap, pos, 0)) == 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 @@ -133,76 +141,137 @@ float depthSlope(ivec2 pos, ivec2 size, ivec2 step, int id, float depth) // 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. -bool candidateWithin(ivec2 pos, int reach, int own_id) +// 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(specularMap, 0) - ivec2(1); + 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(specularMap, ivec2(x, y), 0); + 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); - if (own_id == 0 ? (highest != 0) - : ((lowest != 0 && lowest != own_id) || (highest != 0 && highest != own_id) || tile.b > 0.5)) - { - return true; - } + 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 false; + return found; } -// Whether a tile under the square of texels within `reach` of `pos` along either axis 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. -bool glowWithin(ivec2 pos, int reach, int own_id) +// 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) { - ivec2 last = textureSize(specularMap, 0) - ivec2(1); - ivec2 lo = max(pos - ivec2(reach), ivec2(0)) / TILE_SIZE; - ivec2 hi = min((pos + ivec2(reach)) / TILE_SIZE, last); - for (int y = lo.y; y <= hi.y; ++y) + 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))) { - for (int x = lo.x; x <= hi.x; ++x) + 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) { - vec4 tile = texelFetch(specularMap, ivec2(x, y), 0); - int lowest = int(tile.r * 65535.0 + 0.5); - int highest = int(tile.g * 65535.0 + 0.5); - if (tile.a > 0.5 && (lowest != own_id || highest != own_id)) + int y = pos.y + ((side == 0) ? dy : -dy); + if (y < 0 || y >= size.y) { - return true; + 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; } } } - return false; } void main() { - ivec2 size = textureSize(diffuseMap, 0); + ivec2 size = textureSize(selectionIdMap, 0); ivec2 pos = clamp(ivec2(tc * vec2(size)), ivec2(0), size - ivec2(1)); - vec4 own = texelFetch(diffuseMap, pos, 0); + 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, and as far as - // the glow reaches where a glowing surface is near. Texels off the target are not there: clamping them would read - // the edge's again, nearer than it is. + // 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, glow_reach, own_id); - if (!glow_near && !candidateWithin(pos, line_reach, own_id)) + 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; } - int reach = glow_near ? max(line_reach, glow_reach) : line_reach; + + // 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; @@ -223,34 +292,32 @@ void main() // 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; - // The glow: the nearest drawn texel of a glowing object in front of this pixel, the lower id where two are as - // near. - int glow_id = 0; - float glow_dist = float(glow_reach) + 1.0; - - for (int dy = -reach; dy <= reach; ++dy) + // 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 = -reach; dx <= reach; ++dx) + 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(diffuseMap, tap_pos, 0); + 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 && !isJumpFront(tap))) + 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; a line as far as its - // width. + // 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; - if ((tap_glows && own_object) || dx * dx + dy * dy > tap_reach * tap_reach) + int d2 = dx * dx + dy * dy; + if ((tap_glows && (own_object || !glow_taps)) || d2 > tap_reach * tap_reach) { continue; } @@ -279,10 +346,10 @@ void main() bool in_front = (relation == IN_FRONT) || (own_glows && relation == TOUCHING); if (tap_glows) { - if (relation == IN_FRONT && tap_drawn && (dist < glow_dist || (dist == glow_dist && tap_id < glow_id))) + if (relation == IN_FRONT && tap_drawn && (d2 < glow_d2 || (d2 == glow_d2 && tap_id < glow_id))) { glow_id = tap_id; - glow_dist = dist; + glow_d2 = d2; } } else if (in_front && tap_drawn) @@ -319,7 +386,7 @@ void main() if (glow_id != 0) { vec4 glow_colour = paletteColour(glow_id, true); - alpha = glow_colour.a * glowOpacity(glow_dist); + alpha = glow_colour.a * glowOpacity(sqrt(float(glow_d2))); rgb = glow_colour.rgb * alpha; } if (own_drawn && !own_glows) diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl index 192c277032..fdf045ca66 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionTileF.glsl @@ -27,22 +27,23 @@ // 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 with its jumps marked (selectionJumpF.glsl). -uniform sampler2D diffuseMap; +// 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(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(diffuseMap, 0); + ivec2 size = textureSize(selectionIdMap, 0); ivec2 origin = ivec2(gl_FragCoord.xy) * TILE_SIZE; ivec2 end = min(origin + ivec2(TILE_SIZE), size); @@ -54,13 +55,14 @@ void main() { for (int x = origin.x; x < end.x; ++x) { - vec4 texel = texelFetch(diffuseMap, ivec2(x, y), 0); + vec4 texel = texelFetch(selectionIdMap, ivec2(x, y), 0); int id = decodeId(texel); if (id != 0) { lowest = min(lowest, id); highest = max(highest, id); - jump = jump || isJumpFront(texel); + // The jump pass marks no glowing texel. + jump = jump || (!isGlow(texel) && isJumpFront(selectionJumpMap, ivec2(x, y))); glow = glow || (isGlow(texel) && isDrawn(texel)); } } diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionUtilF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionUtilF.glsl index 9868410fc3..2ea6dbee52 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionUtilF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionUtilF.glsl @@ -27,8 +27,7 @@ //[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, below 128, and -// 128 more on a texel the jump pass marks as the near side of a jump in its object's surface. +// 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). @@ -41,7 +40,7 @@ int decodeId(vec4 texel) int decodePriority(vec4 texel) { - return int(texel.a * 255.0 + 0.5) & 127; + return int(texel.a * 255.0 + 0.5); } bool isGlow(vec4 texel) @@ -49,9 +48,11 @@ bool isGlow(vec4 texel) return decodePriority(texel) == PRIORITY_HOVER; } -bool isJumpFront(vec4 texel) +// 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 int(texel.a * 255.0 + 0.5) >= 128; + return texelFetch(jumps, pos, 0).r > 0.5; } bool isDrawn(vec4 texel) diff --git a/indra/newview/app_settings/shaders/class1/interface/selectionWireframeF.glsl b/indra/newview/app_settings/shaders/class1/interface/selectionWireframeF.glsl index 59b8dbac61..9e127f3b57 100644 --- a/indra/newview/app_settings/shaders/class1/interface/selectionWireframeF.glsl +++ b/indra/newview/app_settings/shaders/class1/interface/selectionWireframeF.glsl @@ -34,7 +34,7 @@ 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 altDiffuseMap; +uniform sampler2D selectionPalette; // The id target's depth, the nearest selected surface, which the HUD's lines are tested against. uniform sampler2D depthMap; @@ -93,7 +93,7 @@ void main() } int row = (selection_id / PALETTE_WIDTH) * 2 + (visible ? 0 : 1); - vec4 colour = texelFetch(altDiffuseMap, ivec2(selection_id % PALETTE_WIDTH, row), 0); + vec4 colour = texelFetch(selectionPalette, ivec2(selection_id % PALETTE_WIDTH, row), 0); if (colour.a <= 0.0) { discard; 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/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 8339218485..49d577994d 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -141,7 +141,6 @@ void render_hud_attachments(); void render_ui_3d(); void render_ui_2d(); void render_disconnected_background(); -bool get_hud_matrices(LLMatrix4a& proj, LLMatrix4a& model); void getProfileStatsContext(LLSD& stats); std::string getProfileStatsFilename(); 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/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp index c8ed8d9e35..ddfec116d8 100644 --- a/indra/newview/llviewershadermgr.cpp +++ b/indra/newview/llviewershadermgr.cpp @@ -160,6 +160,8 @@ LLGLSLShader gSelectionWireframeProgram; LLGLSLShader gSelectionJumpProgram; LLGLSLShader gSelectionTileProgram; LLGLSLShader gSelectionOutlineProgram; +LLGLSLShader gSelectionGlowReachProgram; +LLGLSLShader gSelectionGlowRowProgram; LLGLSLShader gDeferredHighlightProgram; @@ -3831,60 +3833,62 @@ bool LLViewerShaderMgr::loadShadersInterface() if (success) { - gSelectionIdProgram.mName = "Selection Id Shader"; - gSelectionIdProgram.mShaderFiles.clear(); - gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionIdV.glsl", GL_VERTEX_SHADER)); - gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionIdF.glsl", GL_FRAGMENT_SHADER)); - gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionAlphaF.glsl", GL_FRAGMENT_SHADER)); - gSelectionIdProgram.mShaderFiles.push_back(make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); - gSelectionIdProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; - success = gSelectionIdProgram.createShader(LLGLSLShader::VARIANT_RIGGED); - } - - if (success) - { - gSelectionWireframeProgram.mName = "Selection Wireframe Shader"; - gSelectionWireframeProgram.mShaderFiles.clear(); - gSelectionWireframeProgram.mShaderFiles.push_back(make_pair("interface/selectionIdV.glsl", GL_VERTEX_SHADER)); - gSelectionWireframeProgram.mShaderFiles.push_back(make_pair("interface/selectionWireframeG.glsl", GL_GEOMETRY_SHADER)); - gSelectionWireframeProgram.mShaderFiles.push_back(make_pair("interface/selectionWireframeF.glsl", GL_FRAGMENT_SHADER)); - gSelectionWireframeProgram.mShaderFiles.push_back(make_pair("interface/selectionAlphaF.glsl", GL_FRAGMENT_SHADER)); - gSelectionWireframeProgram.mShaderFiles.push_back(make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); - gSelectionWireframeProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; - success = gSelectionWireframeProgram.createShader(LLGLSLShader::VARIANT_RIGGED); - } - - if (success) - { - gSelectionJumpProgram.mName = "Selection Jump Shader"; - gSelectionJumpProgram.mShaderFiles.clear(); - gSelectionJumpProgram.mShaderFiles.push_back(make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); - gSelectionJumpProgram.mShaderFiles.push_back(make_pair("interface/selectionJumpF.glsl", GL_FRAGMENT_SHADER)); - gSelectionJumpProgram.mShaderFiles.push_back(make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); - gSelectionJumpProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; - success = gSelectionJumpProgram.createShader(); - } - - if (success) - { - gSelectionTileProgram.mName = "Selection Tile Shader"; - gSelectionTileProgram.mShaderFiles.clear(); - gSelectionTileProgram.mShaderFiles.push_back(make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); - gSelectionTileProgram.mShaderFiles.push_back(make_pair("interface/selectionTileF.glsl", GL_FRAGMENT_SHADER)); - gSelectionTileProgram.mShaderFiles.push_back(make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); - gSelectionTileProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; - success = gSelectionTileProgram.createShader(); - } + // 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(); + } + }; - if (success) - { - gSelectionOutlineProgram.mName = "Selection Outline Shader"; - gSelectionOutlineProgram.mShaderFiles.clear(); - gSelectionOutlineProgram.mShaderFiles.push_back(make_pair("interface/copyV.glsl", GL_VERTEX_SHADER)); - gSelectionOutlineProgram.mShaderFiles.push_back(make_pair("interface/selectionOutlineF.glsl", GL_FRAGMENT_SHADER)); - gSelectionOutlineProgram.mShaderFiles.push_back(make_pair("interface/selectionUtilF.glsl", GL_FRAGMENT_SHADER)); - gSelectionOutlineProgram.mShaderLevel = mShaderLevel[SHADER_INTERFACE]; - success = gSelectionOutlineProgram.createShader(); + 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) diff --git a/indra/newview/llviewershadermgr.h b/indra/newview/llviewershadermgr.h index 1356c250ff..8d69271d41 100644 --- a/indra/newview/llviewershadermgr.h +++ b/indra/newview/llviewershadermgr.h @@ -171,6 +171,8 @@ extern LLGLSLShader gSelectionWireframeProgram; extern LLGLSLShader gSelectionJumpProgram; extern LLGLSLShader gSelectionTileProgram; extern LLGLSLShader gSelectionOutlineProgram; +extern LLGLSLShader gSelectionGlowReachProgram; +extern LLGLSLShader gSelectionGlowRowProgram; extern LLGLSLShader gDeferredHighlightProgram; diff --git a/indra/newview/tests/alselectionoutline_gl_test.cpp b/indra/newview/tests/alselectionoutline_gl_test.cpp index 80ebe31d99..16fc6d5abf 100644 --- a/indra/newview/tests/alselectionoutline_gl_test.cpp +++ b/indra/newview/tests/alselectionoutline_gl_test.cpp @@ -91,6 +91,7 @@ namespace tut 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; } @@ -199,17 +200,22 @@ namespace tut mCopyDepthProgram.unload(); mTileProgram.unload(); mOutlineProgram.unload(); + mFullSearchProgram.unload(); + mGlowReachProgram.unload(); + mGlowRowProgram.unload(); mShaders.clearShaderObjects(); mIdMap.release(); - mMarked.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 }) + for (U32* texture : { &mPalette, &mRamp, &mClear, &mGlass }) { if (*texture) { @@ -302,21 +308,35 @@ namespace tut const std::pair passes[] = { { &mJumpProgram, "selectionJumpF.glsl" }, { &mTileProgram, "selectionTileF.glsl" }, - { &mOutlineProgram, "selectionOutlineF.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)); - program->mShaderFiles.push_back(std::make_pair("interface/selectionUtilF.glsl", 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("marked target", mMarked.allocate(W, H, GL_RGBA8)); + 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)); @@ -473,21 +493,43 @@ namespace tut 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) { - const std::vector texels(4 * 4 * 4, 0); - LLImageGL::generateTextures(1, &mClear); - gGL.getTextureSlot(0)->bindManual(ALTextureSlot::TT_TEXTURE, mClear); - LLImageGL::allocateTexture2D(ALTextureSlot::getInternalType(ALTextureSlot::TT_TEXTURE), GL_RGBA8, 4, 4, - GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); - gGL.getTextureSlot(0)->unbind(); + 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; @@ -675,11 +717,11 @@ namespace tut { const LLRect under = rect ? *rect : LLRect(0, H, W, 0); mJumpProgram.bind(); - Outline::drawJumps(mJumpProgram, mIdMap, mMarked, under, lines, *mTriangle); + Outline::drawJumps(mJumpProgram, mIdMap, mJumps, under, lines, *mTriangle); if (tiles) { mTileProgram.bind(); - Outline::drawTiles(mTileProgram, mMarked, mTiles, under, *mTriangle); + Outline::drawTiles(mTileProgram, mIdMap, mJumps, mTiles, under, *mTriangle); } LLGLSLShader::unbind(); } @@ -696,21 +738,32 @@ namespace tut return records; } - // Whether the jump pass marked the texel at (x, y) as the near side of a jump. + // 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(mMarked), x, y).a >= 128; + 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, then over a cleared frame, scissored when asked, reaching as far as the glow - // with `glow`. `untiled` holds every id, a jump and a glowing surface in every tile instead, so every pixel - // searches its whole disk. + // `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; @@ -723,16 +776,18 @@ namespace tut LLGLEnable blend(GL_BLEND); gGL.setSceneBlendType(LLRender::BT_ALPHA); - mOutlineProgram.bind(); - LLRenderTarget& tiles = untiled ? mAllTiles : mTiles; + 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(mOutlineProgram, mMarked, mIdMap, tiles, mPalette, lines, glow, *mTriangle); + Outline::drawEdges(program, mIdMap, mJumps, tiles, glow_reach, glow_rows, mPalette, lines, *mTriangle); } else { - Outline::drawEdges(mOutlineProgram, mMarked, mIdMap, tiles, mPalette, lines, glow, *mTriangle); + Outline::drawEdges(program, mIdMap, mJumps, tiles, glow_reach, glow_rows, mPalette, lines, *mTriangle); } LLGLSLShader::unbind(); px = ll_test::readFramebufferRGBA(W, H); @@ -800,7 +855,7 @@ namespace tut { return Outline::inWirePass(draw.parts(), draw.mShowHidden, pass); }) && texture_read; program.unbindTexture(LLShaderMgr::DIFFUSE_MAP); - program.unbindTexture(LLShaderMgr::ALTERNATE_DIFFUSE_MAP); + program.unbindTexture(LLShaderMgr::SELECTION_PALETTE); program.unbindTexture(LLShaderMgr::DEFERRED_DEPTH); LLGLSLShader::unbind(); } @@ -837,8 +892,15 @@ namespace tut LLGLSLShader mCopyDepthProgram; LLGLSLShader mTileProgram; LLGLSLShader mOutlineProgram; + LLGLSLShader mFullSearchProgram; + LLGLSLShader mGlowReachProgram; + LLGLSLShader mGlowRowProgram; LLRenderTarget mIdMap; - LLRenderTarget mMarked; + // 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; @@ -851,6 +913,7 @@ namespace tut U32 mRows = 0; U32 mRamp = 0; U32 mClear = 0; + U32 mGlass = 0; std::vector mPaletteTexels; LLPointer mTriangle; std::vector> mBuffers; @@ -1327,10 +1390,10 @@ namespace tut 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 31 samples alpha 126, column 32 alpha 130. + // 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 under half"), idAt(ids, 31, 32), 0U); - ensure_equals(named(reverse, "and keeps over it"), idAt(ids, 32, 32), ID_A); + 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); @@ -1774,9 +1837,8 @@ namespace tut 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(); - const std::vector marked = read(mMarked); - ensure(named(reverse, "the near side of the step marked" + at_z), at(marked, 25, 32).a >= 128 && at(marked, 32, 38).a >= 128); - ensure(named(reverse, "not the far side, nor inside" + at_z), at(marked, 24, 32).a < 128 && at(marked, 32, 32).a < 128); + 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); @@ -1841,7 +1903,7 @@ namespace tut idPass(draws); const std::vector ids = read(mIdMap); const std::vector px = edgePass(); - const std::vector marked = read(mMarked); + 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; @@ -1854,7 +1916,7 @@ namespace tut if (idAt(ids, x, y) == ID_A) { ++covered; - marks += at(marked, x, y).a >= 128 ? 1 : 0; + marks += at(marked, x, y).r >= 128 ? 1 : 0; drawn += (black(at(px, x, y)) && black(at(searched, x, y))) ? 0 : 1; } } @@ -2403,7 +2465,8 @@ namespace tut } // 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. + // 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>() { @@ -2412,7 +2475,11 @@ namespace tut { 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: the near side of a jump in its own surface. + // 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); @@ -2420,7 +2487,7 @@ namespace tut { 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, "the fold's edge is the near side of a jump"), markedAt(25, 32) && markedAt(34, 32)); + 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. @@ -2625,4 +2692,103 @@ namespace tut 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); + } + } + } } From 8297b54e954c34da44aad5bfcb394c4b90a17203 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 10 Oct 2026 03:35:37 -0400 Subject: [PATCH 11/11] The HUD's depth is cleared only where it can be put back, and media keeps its cursor ALHUDDepth::clear takes the copy program restore draws with, and clears nothing without it: the scene's depth under the HUD stays, the HUD tests against it as it did, and the 3D UI drawn after the HUD keeps the world's occlusion. Before, a missing program left the rects empty for the rest of the frame. render_hud_attachments gives no rects without the pipeline's targets either, so restore_scene_depth needs no guard of its own. Hovering media kept falling through to the click-action cursors, which put an arrow over the cursor handleMediaHover set; the media branch, folded into the camera steering condition in 2023, is its own branch again, after steering so a drag across media still steers. The material preview's and the window's first clears go through gGL.clear, which flushes what is batched before it clears. alhuddepth_gl 7: with the copy program unloaded, nothing under the HUD is cleared, the near world still hides the HUD, and what the world hides from the 3D UI stays hidden. It fails with the clear unguarded. Co-Authored-By: Claude Opus 5.5 --- indra/llwindow/llwindowwin32.cpp | 2 +- indra/newview/alhuddepth.h | 11 ++++++-- indra/newview/llgltfmaterialpreviewmgr.cpp | 4 +-- indra/newview/lltoolpie.cpp | 6 ++++- indra/newview/llviewerdisplay.cpp | 10 +++++--- indra/newview/tests/alhuddepth_gl_test.cpp | 30 +++++++++++++++++++--- 6 files changed, 49 insertions(+), 14 deletions(-) diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 8a86642634..c53cf00c09 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -1825,7 +1825,7 @@ const S32 max_format = (S32)num_formats - 1; { show(); gGL.setClearColor(LLColor4::transparent); - glClear(GL_COLOR_BUFFER_BIT); + gGL.clear(GL_COLOR_BUFFER_BIT); swapBuffers(); } diff --git a/indra/newview/alhuddepth.h b/indra/newview/alhuddepth.h index 56c95b2c7a..df32ccd7e2 100644 --- a/indra/newview/alhuddepth.h +++ b/indra/newview/alhuddepth.h @@ -102,9 +102,16 @@ inline void mergeRect(std::vector& rects, LLRect rect) rects.push_back(rect); } -/// Clears the bound framebuffer's depth within each rect of a viewport whose origin is (x, y). -inline void clear(const std::vector& rects, S32 x, S32 y) +/// 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) diff --git a/indra/newview/llgltfmaterialpreviewmgr.cpp b/indra/newview/llgltfmaterialpreviewmgr.cpp index 8b52727047..3df66b433f 100644 --- a/indra/newview/llgltfmaterialpreviewmgr.cpp +++ b/indra/newview/llgltfmaterialpreviewmgr.cpp @@ -429,7 +429,7 @@ bool LLGLTFPreviewTexture::render() if (!mShouldRender) { return false; } gGL.setClearColor(LLColor4::transparent); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + 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/lltoolpie.cpp b/indra/newview/lltoolpie.cpp index aaa903c2ba..3cbf66d93b 100644 --- a/indra/newview/lltoolpie.cpp +++ b/indra/newview/lltoolpie.cpp @@ -883,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 @@ -901,7 +906,6 @@ bool LLToolPie::handleHover(S32 x, S32 y, MASK mask) else if ( (object) && (RlvActions::isRlvEnabled()) && (!RlvActions::canTouch(object, mHoverPick.mObjectOffset)) ) { // Block showing the "grab" or "touch" cursor if we can't touch/grab the object - show_highlight = false; gViewerWindow->setCursor(UI_CURSOR_ARROW); } // [/RLVa:KB] diff --git a/indra/newview/llviewerdisplay.cpp b/indra/newview/llviewerdisplay.cpp index 49d577994d..0ddfd97b78 100644 --- a/indra/newview/llviewerdisplay.cpp +++ b/indra/newview/llviewerdisplay.cpp @@ -1291,9 +1291,10 @@ void display_cube_face() 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 (!isAgentAvatarValid() || !get_hud_matrices(proj, model)) + if (!gPipeline.mRT || !gPipeline.mScreenTriangleVB || !isAgentAvatarValid() || !get_hud_matrices(proj, model)) { return; } @@ -1316,10 +1317,11 @@ static void hud_screen_rects(std::vector& rects) } } -// Writes the scene's depth back under the HUD, as the final blit left it (ALHUDDepth::restore). +// 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 (!gPipeline.mRT || !gPipeline.mScreenTriangleVB) + if (rects.empty()) { return; } @@ -1423,7 +1425,7 @@ void render_hud_attachments() // 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(hud_rects, gGLViewport[0], gGLViewport[1]); + ALHUDDepth::clear(gCopyDepthProgram, hud_rects, gGLViewport[0], gGLViewport[1]); gPipeline.renderGeomPostDeferred(hud_cam); diff --git a/indra/newview/tests/alhuddepth_gl_test.cpp b/indra/newview/tests/alhuddepth_gl_test.cpp index cf21d09ebc..de870c5548 100644 --- a/indra/newview/tests/alhuddepth_gl_test.cpp +++ b/indra/newview/tests/alhuddepth_gl_test.cpp @@ -331,10 +331,11 @@ namespace tut mWindow.flush(); } - void clearUnderHUD() + // 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(mRects, 0, 0); + ALHUDDepth::clear(copy ? *copy : mCopyDepthProgram, mRects, 0, 0); mWindow.flush(); } @@ -351,10 +352,10 @@ namespace tut mWindow.flush(); } - void restoreUnderHUD() + void restoreUnderHUD(LLGLSLShader* copy = nullptr) { mWindow.bindTarget(); - ALHUDDepth::restore(mCopyDepthProgram, mScene, *mTriangle, mRects, 0, 0); + ALHUDDepth::restore(copy ? *copy : mCopyDepthProgram, mScene, *mTriangle, mRects, 0, 0); mWindow.flush(); } @@ -569,4 +570,25 @@ namespace tut 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)); + } + } }