diff --git a/CHANGELOG.md b/CHANGELOG.md index 5228662d08..1afca19c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ Bug #7654: Tooltips for enchantments with invalid effects cause crashes Bug #7660: Some inconsistencies regarding Invisibility breaking Bug #7675: Successful lock spell doesn't produce a sound + Bug #7679: Scene luminance value flashes when toggling shaders Feature #3537: Shader-based water ripples Feature #5492: Let rain and snow collide with statics Feature #6149: Dehardcode Lua API_REVISION diff --git a/apps/openmw/mwrender/luminancecalculator.cpp b/apps/openmw/mwrender/luminancecalculator.cpp index 5b7fe272aa..ae29b7fdcc 100644 --- a/apps/openmw/mwrender/luminancecalculator.cpp +++ b/apps/openmw/mwrender/luminancecalculator.cpp @@ -20,11 +20,6 @@ namespace MWRender mResolveProgram = shaderManager.getProgram(vertex, std::move(resolveFragment)); mLuminanceProgram = shaderManager.getProgram(vertex, std::move(luminanceFragment)); - } - - void LuminanceCalculator::compile() - { - int mipmapLevels = osg::Image::computeNumberOfMipmapLevels(mWidth, mHeight); for (auto& buffer : mBuffers) { @@ -38,7 +33,6 @@ namespace MWRender osg::Texture2D::MIN_FILTER, osg::Texture2D::LINEAR_MIPMAP_NEAREST); buffer.mipmappedSceneLuminanceTex->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::LINEAR); buffer.mipmappedSceneLuminanceTex->setTextureSize(mWidth, mHeight); - buffer.mipmappedSceneLuminanceTex->setNumMipmapLevels(mipmapLevels); buffer.luminanceTex = new osg::Texture2D; buffer.luminanceTex->setInternalFormat(GL_R16F); @@ -62,14 +56,6 @@ namespace MWRender buffer.luminanceProxyFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(buffer.luminanceProxyTex)); - buffer.resolveSceneLumFbo = new osg::FrameBufferObject; - buffer.resolveSceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, - osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex, mipmapLevels - 1)); - - buffer.sceneLumFbo = new osg::FrameBufferObject; - buffer.sceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, - osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex)); - buffer.sceneLumSS = new osg::StateSet; buffer.sceneLumSS->setAttributeAndModes(mLuminanceProgram); buffer.sceneLumSS->addUniform(new osg::Uniform("sceneTex", 0)); @@ -84,6 +70,26 @@ namespace MWRender mBuffers[0].resolveSS->setTextureAttributeAndModes(1, mBuffers[1].luminanceTex); mBuffers[1].resolveSS->setTextureAttributeAndModes(1, mBuffers[0].luminanceTex); + } + + void LuminanceCalculator::compile() + { + int mipmapLevels = osg::Image::computeNumberOfMipmapLevels(mWidth, mHeight); + + for (auto& buffer : mBuffers) + { + buffer.mipmappedSceneLuminanceTex->setTextureSize(mWidth, mHeight); + buffer.mipmappedSceneLuminanceTex->setNumMipmapLevels(mipmapLevels); + buffer.mipmappedSceneLuminanceTex->dirtyTextureObject(); + + buffer.resolveSceneLumFbo = new osg::FrameBufferObject; + buffer.resolveSceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex, mipmapLevels - 1)); + + buffer.sceneLumFbo = new osg::FrameBufferObject; + buffer.sceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex)); + } mCompiled = true; } @@ -114,13 +120,14 @@ namespace MWRender buffer.luminanceProxyFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); ext->glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_NEAREST); - if (dirty) + if (mIsBlank) { // Use current frame data for previous frame to warm up calculations and prevent popin mBuffers[(frameId + 1) % 2].resolveFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); ext->glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_NEAREST); buffer.luminanceProxyFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + mIsBlank = false; } buffer.resolveFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); diff --git a/apps/openmw/mwrender/luminancecalculator.hpp b/apps/openmw/mwrender/luminancecalculator.hpp index 71ea2f7971..8b51081e2f 100644 --- a/apps/openmw/mwrender/luminancecalculator.hpp +++ b/apps/openmw/mwrender/luminancecalculator.hpp @@ -58,6 +58,7 @@ namespace MWRender bool mCompiled = false; bool mEnabled = false; + bool mIsBlank = true; int mWidth = 1; int mHeight = 1; diff --git a/apps/openmw/mwrender/pingpongcanvas.cpp b/apps/openmw/mwrender/pingpongcanvas.cpp index b96b40ff3f..4fecdf87f9 100644 --- a/apps/openmw/mwrender/pingpongcanvas.cpp +++ b/apps/openmw/mwrender/pingpongcanvas.cpp @@ -196,6 +196,39 @@ namespace MWRender } }; + // When textures are created (or resized) we need to either dirty them and/or clear them. + // Otherwise, there will be undefined behavior when reading from a texture that has yet to be written to in a + // later pass. + for (const auto& attachment : mDirtyAttachments) + { + const auto [w, h] + = attachment.mSize.get(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); + + attachment.mTarget->setTextureSize(w, h); + if (attachment.mMipMap) + attachment.mTarget->setNumMipmapLevels(osg::Image::computeNumberOfMipmapLevels(w, h)); + attachment.mTarget->dirtyTextureObject(); + + osg::ref_ptr fbo = new osg::FrameBufferObject; + + fbo->setAttachment( + osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(attachment.mTarget)); + fbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + + glViewport(0, 0, attachment.mTarget->getTextureWidth(), attachment.mTarget->getTextureHeight()); + state.haveAppliedAttribute(osg::StateAttribute::VIEWPORT); + glClearColor(attachment.mClearColor.r(), attachment.mClearColor.g(), attachment.mClearColor.b(), + attachment.mClearColor.a()); + glClear(GL_COLOR_BUFFER_BIT); + + if (attachment.mTarget->getNumMipmapLevels() > 0) + { + state.setActiveTextureUnit(0); + state.applyTextureAttribute(0, attachment.mTarget); + ext->glGenerateMipmap(GL_TEXTURE_2D); + } + } + for (const size_t& index : filtered) { const auto& node = mPasses[index]; @@ -239,16 +272,11 @@ namespace MWRender if (pass.mRenderTarget) { - if (mDirtyAttachments) + if (mDirtyAttachments.size() > 0) { const auto [w, h] = pass.mSize.get(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); - pass.mRenderTexture->setTextureSize(w, h); - if (pass.mMipMap) - pass.mRenderTexture->setNumMipmapLevels(osg::Image::computeNumberOfMipmapLevels(w, h)); - pass.mRenderTexture->dirtyTextureObject(); - // Custom render targets must be shared between frame ids, so it's impossible to double buffer // without expensive copies. That means the only thread-safe place to resize is in the draw // thread. @@ -265,7 +293,6 @@ namespace MWRender if (pass.mRenderTexture->getNumMipmapLevels() > 0) { - state.setActiveTextureUnit(0); state.applyTextureAttribute(0, pass.mRenderTarget->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0) @@ -336,7 +363,6 @@ namespace MWRender bindDestinationFbo(); } - if (mDirtyAttachments) - mDirtyAttachments = false; + mDirtyAttachments.clear(); } } diff --git a/apps/openmw/mwrender/pingpongcanvas.hpp b/apps/openmw/mwrender/pingpongcanvas.hpp index 0d53fd049a..a03e3591ae 100644 --- a/apps/openmw/mwrender/pingpongcanvas.hpp +++ b/apps/openmw/mwrender/pingpongcanvas.hpp @@ -31,7 +31,10 @@ namespace MWRender void dirty() { mDirty = true; } - void resizeRenderTargets() { mDirtyAttachments = true; } + void setDirtyAttachments(const std::vector& attachments) + { + mDirtyAttachments = attachments; + } const fx::DispatchArray& getPasses() { return mPasses; } @@ -68,7 +71,7 @@ namespace MWRender osg::ref_ptr mTextureNormals; mutable bool mDirty = false; - mutable bool mDirtyAttachments = false; + mutable std::vector mDirtyAttachments; mutable osg::ref_ptr mRenderViewport; mutable osg::ref_ptr mMultiviewResolveFramebuffer; mutable osg::ref_ptr mDestinationFBO; diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp index 2527f52df1..2c77981244 100644 --- a/apps/openmw/mwrender/postprocessor.cpp +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -541,6 +541,8 @@ namespace MWRender mNormals = false; mPassLights = false; + std::vector attachmentsToDirty; + for (const auto& technique : mTechniques) { if (!technique->isValid()) @@ -617,7 +619,7 @@ namespace MWRender if (!pass->getTarget().empty()) { - const auto& renderTarget = technique->getRenderTargetsMap()[pass->getTarget()]; + auto& renderTarget = technique->getRenderTargetsMap()[pass->getTarget()]; subPass.mSize = renderTarget.mSize; subPass.mRenderTexture = renderTarget.mTarget; subPass.mMipMap = renderTarget.mMipMap; @@ -628,13 +630,27 @@ namespace MWRender const auto [w, h] = renderTarget.mSize.get(renderWidth(), renderHeight()); subPass.mStateSet->setAttributeAndModes(new osg::Viewport(0, 0, w, h)); + + if (std::find_if(attachmentsToDirty.cbegin(), attachmentsToDirty.cend(), + [renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; }) + == attachmentsToDirty.cend()) + { + attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget)); + } } for (const auto& name : pass->getRenderTargets()) { - subPass.mStateSet->setTextureAttribute(subTexUnit, technique->getRenderTargetsMap()[name].mTarget); + auto& renderTarget = technique->getRenderTargetsMap()[name]; + subPass.mStateSet->setTextureAttribute(subTexUnit, renderTarget.mTarget); subPass.mStateSet->addUniform(new osg::Uniform(name.c_str(), subTexUnit)); + if (std::find_if(attachmentsToDirty.cbegin(), attachmentsToDirty.cend(), + [renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; }) + == attachmentsToDirty.cend()) + { + attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget)); + } subTexUnit++; } @@ -654,7 +670,7 @@ namespace MWRender mRendering.getSkyManager()->setSunglare(sunglare); if (dirtyAttachments) - mCanvases[frameId]->resizeRenderTargets(); + mCanvases[frameId]->setDirtyAttachments(attachmentsToDirty); } PostProcessor::Status PostProcessor::enableTechnique( @@ -668,7 +684,7 @@ namespace MWRender int pos = std::min(location.value_or(mTechniques.size()), mTechniques.size()); mTechniques.insert(mTechniques.begin() + pos, technique); - dirtyTechniques(); + dirtyTechniques(Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug); return Status_Toggled; } diff --git a/components/fx/pass.cpp b/components/fx/pass.cpp index daaaf22968..d20813f1ce 100644 --- a/components/fx/pass.cpp +++ b/components/fx/pass.cpp @@ -12,7 +12,6 @@ #include #include -#include #include #include #include @@ -326,9 +325,6 @@ float omw_EstimateFogCoverageFromUV(vec2 uv) if (mBlendEq) stateSet->setAttributeAndModes(new osg::BlendEquation(mBlendEq.value())); - - if (mClearColor) - stateSet->setAttributeAndModes(new SceneUtil::ClearColor(mClearColor.value(), GL_COLOR_BUFFER_BIT)); } void Pass::dirty() diff --git a/components/fx/pass.hpp b/components/fx/pass.hpp index 509127a163..e176afc699 100644 --- a/components/fx/pass.hpp +++ b/components/fx/pass.hpp @@ -72,7 +72,6 @@ namespace fx std::array mRenderTargets; std::string mTarget; - std::optional mClearColor; std::optional mBlendSource; std::optional mBlendDest; diff --git a/components/fx/technique.cpp b/components/fx/technique.cpp index 3abcd8c0ba..defb581cfc 100644 --- a/components/fx/technique.cpp +++ b/components/fx/technique.cpp @@ -313,6 +313,8 @@ namespace fx rt.mTarget->setSourceFormat(parseSourceFormat()); else if (key == "mipmaps") rt.mMipMap = parseBool(); + else if (key == "clear_color") + rt.mClearColor = parseVec(); else error(Misc::StringUtils::format("unexpected key '%s'", std::string(key))); @@ -798,9 +800,6 @@ namespace fx if (!pass) pass = std::make_shared(); - bool clear = true; - osg::Vec4f clearColor = { 1, 1, 1, 1 }; - while (!isNext()) { expect("invalid key in block header"); @@ -844,10 +843,6 @@ namespace fx if (blendEq != osg::BlendEquation::FUNC_ADD) pass->mBlendEq = blendEq; } - else if (key == "clear") - clear = parseBool(); - else if (key == "clear_color") - clearColor = parseVec(); else error(Misc::StringUtils::format("unrecognized key '%s' in block header", std::string(key))); @@ -865,9 +860,6 @@ namespace fx return; } - if (clear) - pass->mClearColor = clearColor; - error("malformed block header"); } diff --git a/components/fx/types.hpp b/components/fx/types.hpp index 0683126dec..0f33d29e1a 100644 --- a/components/fx/types.hpp +++ b/components/fx/types.hpp @@ -63,6 +63,17 @@ namespace fx osg::ref_ptr mTarget = new osg::Texture2D; SizeProxy mSize; bool mMipMap = false; + osg::Vec4f mClearColor = osg::Vec4f(0.0, 0.0, 0.0, 1.0); + + RenderTarget() = default; + + RenderTarget(const RenderTarget& other) + : mTarget(other.mTarget) + , mSize(other.mSize) + , mMipMap(other.mMipMap) + , mClearColor(other.mClearColor) + { + } }; template diff --git a/docs/source/reference/postprocessing/omwfx.rst b/docs/source/reference/postprocessing/omwfx.rst index 8ff075bc6d..6d191e0a7b 100644 --- a/docs/source/reference/postprocessing/omwfx.rst +++ b/docs/source/reference/postprocessing/omwfx.rst @@ -539,6 +539,8 @@ is not wanted and you want a custom render target. +------------------+---------------------+-----------------------------------------------------------------------------+ | mipmaps | boolean | Whether mipmaps should be generated every frame | +------------------+---------------------+-----------------------------------------------------------------------------+ +| clear_color | vec4 | The color the texture will be cleared to when it's first created | ++------------------+---------------------+-----------------------------------------------------------------------------+ To use the render target a pass must be assigned to it, along with any optional blend modes. As a restriction, only three render targets can be bound per pass with ``rt1``, ``rt2``, ``rt3``, respectively. @@ -555,6 +557,7 @@ color buffer will accumulate. source_format = rgb; internal_format = rgb16f; source_type = float; + clear_color = vec4(1,0,0,1); } fragment red(target=RT_Red,blend=(add, src_color, one), rt1=RT_Red) {