diff --git a/apps/openmw/mwrender/pingpongcanvas.cpp b/apps/openmw/mwrender/pingpongcanvas.cpp index 5ac68acf5f..6782bdddfd 100644 --- a/apps/openmw/mwrender/pingpongcanvas.cpp +++ b/apps/openmw/mwrender/pingpongcanvas.cpp @@ -238,10 +238,35 @@ namespace MWRender if (pass.mRenderTarget) { + if (mDirtyAttachments) + { + 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. + osg::Texture2D* texture = const_cast(dynamic_cast( + pass.mRenderTarget->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0) + .getTexture())); + + texture->setTextureSize(w, h); + texture->setNumMipmapLevels(pass.mRenderTexture->getNumMipmapLevels()); + texture->dirtyTextureObject(); + + mDirtyAttachments = false; + } + pass.mRenderTarget->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); if (pass.mRenderTexture->getNumMipmapLevels() > 0) { + state.setActiveTextureUnit(0); state.applyTextureAttribute(0, pass.mRenderTarget->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0) diff --git a/apps/openmw/mwrender/pingpongcanvas.hpp b/apps/openmw/mwrender/pingpongcanvas.hpp index d8758303d7..557813b816 100644 --- a/apps/openmw/mwrender/pingpongcanvas.hpp +++ b/apps/openmw/mwrender/pingpongcanvas.hpp @@ -30,6 +30,8 @@ namespace MWRender void dirty() { mDirty = true; } + void resizeRenderTargets() { mDirtyAttachments = true; } + const fx::DispatchArray& getPasses() { return mPasses; } void setPasses(fx::DispatchArray&& passes); @@ -65,6 +67,7 @@ namespace MWRender osg::ref_ptr mTextureNormals; mutable bool mDirty = false; + mutable bool mDirtyAttachments = false; 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 c3106802db..66ade9495c 100644 --- a/apps/openmw/mwrender/postprocessor.cpp +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -211,25 +211,14 @@ namespace MWRender if (Stereo::getStereo()) Stereo::Manager::instance().screenResolutionChanged(); - auto width = renderWidth(); - auto height = renderHeight(); - for (auto& technique : mTechniques) - { - for (auto& [name, rt] : technique->getRenderTargetsMap()) - { - const auto [w, h] = rt.mSize.get(width, height); - rt.mTarget->setTextureSize(w, h); - } - } - size_t frameId = frame() % 2; createObjectsForFrame(frameId); mRendering.updateProjectionMatrix(); - mRendering.setScreenRes(width, height); + mRendering.setScreenRes(renderWidth(), renderHeight()); - dirtyTechniques(); + dirtyTechniques(true); mDirty = true; mDirtyFrameId = !frameId; @@ -534,7 +523,7 @@ namespace MWRender mCanvases[frameId]->dirty(); } - void PostProcessor::dirtyTechniques() + void PostProcessor::dirtyTechniques(bool dirtyAttachments) { size_t frameId = frame() % 2; @@ -613,8 +602,6 @@ namespace MWRender uniform->mName.c_str(), *type, uniform->getNumElements())); } - std::unordered_map renderTargetCache; - for (const auto& pass : technique->getPasses()) { int subTexUnit = texUnit; @@ -626,32 +613,27 @@ namespace MWRender if (!pass->getTarget().empty()) { - const auto& rt = technique->getRenderTargetsMap()[pass->getTarget()]; - - const auto [w, h] = rt.mSize.get(renderWidth(), renderHeight()); - - subPass.mRenderTexture = new osg::Texture2D(*rt.mTarget); - renderTargetCache[rt.mTarget] = subPass.mRenderTexture; - subPass.mRenderTexture->setTextureSize(w, h); - subPass.mRenderTexture->setName(std::string(pass->getTarget())); - - if (rt.mMipMap) - subPass.mRenderTexture->setNumMipmapLevels(osg::Image::computeNumberOfMipmapLevels(w, h)); + const auto& renderTarget = technique->getRenderTargetsMap()[pass->getTarget()]; + subPass.mSize = renderTarget.mSize; + subPass.mRenderTexture = renderTarget.mTarget; + subPass.mMipMap = renderTarget.mMipMap; + subPass.mStateSet->setAttributeAndModes(new osg::Viewport( + 0, 0, subPass.mRenderTexture->getTextureWidth(), subPass.mRenderTexture->getTextureHeight())); subPass.mRenderTarget = new osg::FrameBufferObject; subPass.mRenderTarget->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(subPass.mRenderTexture)); + + const auto [w, h] = renderTarget.mSize.get(renderWidth(), renderHeight()); subPass.mStateSet->setAttributeAndModes(new osg::Viewport(0, 0, w, h)); } - for (const auto& whitelist : pass->getRenderTargets()) + for (const auto& name : pass->getRenderTargets()) { - auto it = technique->getRenderTargetsMap().find(whitelist); - if (it != technique->getRenderTargetsMap().end() && renderTargetCache[it->second.mTarget]) - { - subPass.mStateSet->setTextureAttribute(subTexUnit, renderTargetCache[it->second.mTarget]); - subPass.mStateSet->addUniform(new osg::Uniform(std::string(it->first).c_str(), subTexUnit++)); - } + subPass.mStateSet->setTextureAttribute(subTexUnit, technique->getRenderTargetsMap()[name].mTarget); + subPass.mStateSet->addUniform(new osg::Uniform(name.c_str(), subTexUnit)); + + subTexUnit++; } node.mPasses.emplace_back(std::move(subPass)); @@ -668,6 +650,9 @@ namespace MWRender hud->updateTechniques(); mRendering.getSkyManager()->setSunglare(sunglare); + + if (dirtyAttachments) + mCanvases[frameId]->resizeRenderTargets(); } PostProcessor::Status PostProcessor::enableTechnique( @@ -774,7 +759,7 @@ namespace MWRender for (auto& technique : mTemplates) technique->compile(); - dirtyTechniques(); + dirtyTechniques(true); } void PostProcessor::disableDynamicShaders() diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp index 153ec8166b..e9f19bf6b5 100644 --- a/apps/openmw/mwrender/postprocessor.hpp +++ b/apps/openmw/mwrender/postprocessor.hpp @@ -204,7 +204,7 @@ namespace MWRender void createObjectsForFrame(size_t frameId); - void dirtyTechniques(); + void dirtyTechniques(bool dirtyAttachments = false); void update(size_t frameId); diff --git a/components/fx/pass.cpp b/components/fx/pass.cpp index 7a7329d755..4b91939e01 100644 --- a/components/fx/pass.cpp +++ b/components/fx/pass.cpp @@ -339,7 +339,7 @@ float omw_EstimateFogCoverageFromUV(vec2 uv) if (mCompiled) return; - mLegacyGLSL = technique.getGLSLVersion() != 330; + mLegacyGLSL = technique.getGLSLVersion() < 330; if (mType == Type::Pixel) { diff --git a/components/fx/technique.cpp b/components/fx/technique.cpp index 0b5d784ad9..3abcd8c0ba 100644 --- a/components/fx/technique.cpp +++ b/components/fx/technique.cpp @@ -279,6 +279,7 @@ namespace fx rt.mTarget->setSourceType(GL_UNSIGNED_BYTE); rt.mTarget->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); rt.mTarget->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + rt.mTarget->setName(std::string(mBlockName)); while (!isNext() && !isNext()) { diff --git a/components/fx/technique.hpp b/components/fx/technique.hpp index 844e4b552a..ed356e0a37 100644 --- a/components/fx/technique.hpp +++ b/components/fx/technique.hpp @@ -54,10 +54,14 @@ namespace fx osg::ref_ptr mRenderTarget; osg::ref_ptr mRenderTexture; bool mResolve = false; + Types::SizeProxy mSize; + bool mMipMap; SubPass(const SubPass& other, const osg::CopyOp& copyOp = osg::CopyOp::SHALLOW_COPY) : mStateSet(new osg::StateSet(*other.mStateSet, copyOp)) , mResolve(other.mResolve) + , mSize(other.mSize) + , mMipMap(other.mMipMap) { if (other.mRenderTarget) mRenderTarget = new osg::FrameBufferObject(*other.mRenderTarget, copyOp); diff --git a/components/fx/types.hpp b/components/fx/types.hpp index 09f191c61c..0683126dec 100644 --- a/components/fx/types.hpp +++ b/components/fx/types.hpp @@ -29,6 +29,16 @@ namespace fx std::optional mWidth; std::optional mHeight; + SizeProxy() = default; + + SizeProxy(const SizeProxy& other) + : mWidthRatio(other.mWidthRatio) + , mHeightRatio(other.mHeightRatio) + , mWidth(other.mWidth) + , mHeight(other.mHeight) + { + } + std::tuple get(int width, int height) const { int scaledWidth = width; diff --git a/docs/source/reference/postprocessing/omwfx.rst b/docs/source/reference/postprocessing/omwfx.rst index 36a6f0883a..2fb21a5162 100644 --- a/docs/source/reference/postprocessing/omwfx.rst +++ b/docs/source/reference/postprocessing/omwfx.rst @@ -529,48 +529,134 @@ is not wanted and you want a custom render target. | mipmaps | boolean | Whether mipmaps should be generated every frame | +------------------+---------------------+-----------------------------------------------------------------------------+ -To use the render target a pass must be assigned to it, along with any optional clear or blend modes. +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. -In the code snippet below a rendertarget is used to draw the red channel of a scene at half resolution, then a quarter. As a restriction, -only three render targets can be bound per pass with ``rt1``, ``rt2``, ``rt3``, respectively. +Blending modes can be useful at times. Below is a simple shader which, when activated, will slowly turn the screen pure red. +Notice how we only ever write the value `.01` to the `RT_Red` buffer. Since we're using appropriate blending modes the +color buffer will accumulate. .. code-block:: none - render_target RT_Downsample { - width_ratio = 0.5; - height_ratio = 0.5; - internal_format = r16f; + render_target RT_Red { + width = 4; + height = 4; + source_format = rgb; + internal_format = rgb16f; source_type = float; - source_format = red; } - render_target RT_Downsample4 { - width_ratio = 0.25; - height_ratio = 0.25; - } - - fragment downsample2x(target=RT_Downsample) { - + fragment red(target=RT_Red,blend=(add, src_color, one), rt1=RT_Red) { omw_In vec2 omw_TexCoord; void main() { - omw_FragColor.r = omw_GetLastShader(omw_TexCoord).r; + omw_FragColor.rgb = vec3(0.01,0,0); } } - fragment downsample4x(target=RT_Downsample4, rt1=RT_Downsample) { - + fragment view(rt1=RT_Red) { omw_In vec2 omw_TexCoord; void main() { - omw_FragColor = omw_Texture2D(RT_Downsample, omw_TexCoord); + omw_FragColor = omw_Texture2D(RT_Red, omw_TexCoord); } } -Now, when the `downsample2x` pass runs it will write to the target buffer instead of the default -one assigned by the engine. + technique { + author = "OpenMW"; + passes = red, view; + } + + +These custom render targets are persistent and ownership is given to the shader which defines them. +This gives potential to implement temporal effects by storing previous frame data in these buffers. +Below is an example which calculates a naive average scene luminance and transitions between values smoothly. + +.. code-block:: none + + render_target RT_Lum { + width = 256; + height = 256; + mipmaps = true; + source_format = rgb; + internal_format = rgb16f; + source_type = float; + min_filter = linear_mipmap_linear; + mag_filter = linear; + } + + render_target RT_LumAvg { + source_type = float; + source_format = rgb; + internal_format = rgb16f; + min_filter = nearest; + mag_filter = nearest; + } + + render_target RT_LumAvgLastFrame { + source_type = float; + source_format = rgb; + internal_format = rgb16f; + min_filter = nearest; + mag_filter = nearest; + } + + fragment calculateLum(target=RT_Lum) { + omw_In vec2 omw_TexCoord; + + void main() + { + vec3 orgi = pow(omw_GetLastShader(omw_TexCoord), vec4(2.2)).rgb; + omw_FragColor.rgb = orgi; + } + } + + fragment fetchLumAvg(target=RT_LumAvg, rt1=RT_Lum, rt2=RT_LumAvgLastFrame) { + omw_In vec2 omw_TexCoord; + + void main() + { + vec3 avgLumaCurrFrame = textureLod(RT_Lum, vec2(0.5, 0.5), 6).rgb; + vec3 avgLumaLastFrame = omw_Texture2D(RT_LumAvgLastFrame, vec2(0.5, 0.5)).rgb; + + const float speed = 0.9; + + vec3 avgLuma = avgLumaLastFrame + (avgLumaCurrFrame - avgLumaLastFrame) * (1.0 - exp(-omw.deltaSimulationTime * speed)); + + omw_FragColor.rgb = avgLuma; + } + } + + fragment adaptation(rt1=RT_LumAvg) { + omw_In vec2 omw_TexCoord; + + void main() + { + vec3 avgLuma = omw_Texture2D(RT_LumAvg, vec2(0.5, 0.5)).rgb; + + if (omw_TexCoord.y < 0.2) + omw_FragColor = vec4(avgLuma, 1.0); + else + omw_FragColor = omw_GetLastShader(omw_TexCoord); + } + } + + fragment store(target=RT_LumAvgLastFrame, rt1=RT_LumAvg) { + void main() + { + vec3 avgLuma = omw_Texture2D(RT_LumAvg, vec2(0.5, 0.5)).rgb; + omw_FragColor.rgb = avgLuma; + } + } + + technique { + author = "OpenMW"; + passes = calculateLum, fetchLumAvg, store, adaptation; + glsl_version = 330; + } + Simple Example ##############