From be83890063f046360ba9bacc5234aa881df9ec6e Mon Sep 17 00:00:00 2001 From: Filipp Pavlov Date: Fri, 11 Sep 2026 11:41:26 +0000 Subject: [PATCH 1/7] Allow multiple generic post-processing effects: - add generic effects to post-processing volumes - mix multiple effects together - expose two places in post-processing pipeline to execute these effects --- .../Effects/Tr2PPGenericEffect.cpp | 340 +++++++++++++++++- .../PostProcess/Effects/Tr2PPGenericEffect.h | 70 +++- .../Effects/Tr2PPGenericEffect_Blue.cpp | 18 + trinity/PostProcess/Tr2PostProcess2.h | 2 + .../PostProcess/Tr2PostProcessAttributes.cpp | 5 +- .../PostProcess/Tr2PostProcessAttributes.h | 5 + .../Tr2PostProcessAttributes_Blue.cpp | 2 + .../PostProcess/Tr2PostProcessRenderer.cpp | 76 ++-- trinity/PostProcess/Tr2PostProcessRenderer.h | 4 +- trinity/Shader/Tr2Effect.cpp | 33 ++ trinity/Shader/Tr2Effect.h | 1 + 11 files changed, 524 insertions(+), 32 deletions(-) diff --git a/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp b/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp index f76d4c00a..b88a74fcf 100644 --- a/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp +++ b/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp @@ -1,13 +1,345 @@ // Copyright © 2026 CCP ehf. #include "Tr2PPGenericEffect.h" +#include "../Tr2PostProcessAttributes.h" +#include "../Shader/Tr2Effect.h" +#include "../Shader/Tr2Shader.h" +#include "../Shader/Parameter/Tr2FloatParameter.h" +#include "../Shader/Parameter/Tr2Vector2Parameter.h" +#include "../Shader/Parameter/Tr2Vector3Parameter.h" +#include "../Shader/Parameter/Tr2Vector4Parameter.h" -Tr2PPGenericEffect::Tr2PPGenericEffect( IRoot* lockobj ) : - m_quality( PostProcess::Quality::MEDIUM ) + +namespace +{ + +struct EffectInstance +{ + Tr2PPGenericEffect* effect; + PostProcessEnums::Priority priority; + float intensity; +}; + +template +struct BlendableParameter +{ + BluePtr parameter; + Value value; +}; + +struct EffectBucket +{ + EffectBucket() = default; + explicit EffectBucket( const Tr2PPGenericEffect& effect ) + { + for( auto& param : effect.blendableFloatParameters ) + { + floatValues.push_back( param.defaultValue ); + } + for( auto& param : effect.blendableVector2Parameters ) + { + vector2Values.push_back( param.defaultValue ); + } + for( auto& param : effect.blendableVector3Parameters ) + { + vector3Values.push_back( param.defaultValue ); + } + for( auto& param : effect.blendableVector4Parameters ) + { + vector4Values.push_back( param.defaultValue ); + } + } + + void Accumulate( const Tr2PPGenericEffect& effect, float weight ) + { + auto AccumulateValues = [&]( auto& params, auto& values ) { + for( auto& param : params ) + { + auto value = ( param.parameter->GetValue() - param.defaultValue ) * weight; + values[¶m - params.data()] += value; + } + }; + AccumulateValues( effect.blendableFloatParameters, floatValues ); + AccumulateValues( effect.blendableVector2Parameters, vector2Values ); + AccumulateValues( effect.blendableVector3Parameters, vector3Values ); + AccumulateValues( effect.blendableVector4Parameters, vector4Values ); + } + + std::vector effects; + std::vector floatValues; + std::vector vector2Values; + std::vector vector3Values; + std::vector vector4Values; +}; + +} + +Tr2PPGenericEffect::Tr2PPGenericEffect( IRoot* ) +{ +} + +void Tr2PPGenericEffect::UpdateEffectParameters() +{ + if( !m_effect ) + { + return; + } + auto shader = m_effect->GetShaderStateInterface(); + if( !shader ) + { + return; + } + auto hash = m_effect->GetHashValue(); + if( hash == m_lastHashValue && m_lastShader == shader ) + { + return; + } + m_lastHashValue = hash; + m_lastShader = shader; + m_nonBlendableHash = m_effect->GetNonBlendableHashValue(); + blendableFloatParameters.clear(); + blendableVector2Parameters.clear(); + blendableVector3Parameters.clear(); + blendableVector4Parameters.clear(); + m_requiresSourceTexture = false; + + auto& desc = shader->GetEffectDescription(); + for( auto& technique : desc.techniques ) + { + for( auto& pass : technique.passes ) + { + for( auto& stage : pass.stageInputs ) + { + for( auto& constant : stage.constants ) + { + if( constant.type != Tr2EffectConstant::FLOAT || constant.elements > 1 || constant.dimension < 1 || constant.dimension > 4 ) + { + continue; + } + if( auto annotations = shader->GetParameterAnnotations( constant.name.c_str() ) ) + { + auto found = std::find_if( annotations->begin(), annotations->end(), []( const Tr2EffectParameterAnnotation& annotation ) { return strcmp( annotation.name, "IsBlendable" ) == 0; } ); + if( found == annotations->end() || found->type != Tr2EffectParameterAnnotation::BOOL || !found->boolValue ) + { + continue; + } + } + + auto AddBlendableParameter = [&]( auto& blendableParameters ) { + using Element = typename std::decay_t::value_type; + using ParamType = typename Element::ParameterType; + using ValueType = typename Element::ValueType; + ValueType defaultValue; + memcpy( &defaultValue, stage.constantValues + constant.offset, sizeof( ValueType ) ); + BluePtr param = BlueCastPtr( m_effect->GetParameterByName( constant.name.c_str() ) ); + if( !param ) + { + m_effect->SetParameter( constant.name, defaultValue ); + param = BlueCastPtr( m_effect->GetParameterByName( constant.name.c_str() ) ); + if( !param ) + { + return; + } + } + blendableParameters.push_back( { param, defaultValue } ); + }; + + switch( constant.dimension ) + { + case 1: + AddBlendableParameter( blendableFloatParameters ); + break; + case 2: + AddBlendableParameter( blendableVector2Parameters ); + break; + case 3: + AddBlendableParameter( blendableVector3Parameters ); + break; + case 4: + AddBlendableParameter( blendableVector4Parameters ); + break; + default: + break; + } + } + if( !m_requiresSourceTexture ) + { + m_requiresSourceTexture = find_if( stage.resources.begin(), stage.resources.end(), []( const auto& texture ) { return strcmp( texture.second.name, "Blit" ) == 0; } ) != stage.resources.end(); + } + } + } + } +} + +bool Tr2PPGenericEffect::IsValid() const { + return m_effect && m_effect->GetShaderStateInterface(); } -Tr2EffectPtr Tr2PPGenericEffect::GetEffect() const +bool Tr2PPGenericEffect::CanBeMerged( const Tr2PPGenericEffect& other ) const { - return m_effect; + if( m_executionSlot != other.m_executionSlot || m_quality != other.m_quality ) + { + return false; + } + if( m_effect->GetShaderStateInterface() != other.m_effect->GetShaderStateInterface() ) + { + return false; + } + return m_nonBlendableHash == other.m_nonBlendableHash; } + +bool Tr2PPGenericEffect::RequiresSourceTexture() const +{ + return m_requiresSourceTexture; +} + +void Tr2AccumulatedGenericEffects::GenericEffectInstance::SetParameters() +{ + for( auto& param : effect->blendableFloatParameters ) + { + auto value = param.parameter->GetValue(); + std::swap( floatValues[¶m - &effect->blendableFloatParameters[0]], value ); + param.parameter->SetValue( value ); + } + for( auto& param : effect->blendableVector2Parameters ) + { + auto value = param.parameter->GetValue(); + std::swap( vector2Values[¶m - &effect->blendableVector2Parameters[0]], value ); + param.parameter->SetValue( value ); + } + for( auto& param : effect->blendableVector3Parameters ) + { + auto value = param.parameter->GetValue(); + std::swap( vector3Values[¶m - &effect->blendableVector3Parameters[0]], value ); + param.parameter->SetValue( value ); + } + for( auto& param : effect->blendableVector4Parameters ) + { + auto value = param.parameter->GetValue(); + std::swap( vector4Values[¶m - &effect->blendableVector4Parameters[0]], value ); + param.parameter->SetValue( value ); + } +} + +void Tr2AccumulatedGenericEffects::GenericEffectInstance::RestoreParameters() +{ + SetParameters(); +} + + +void AccumulateGenericEffects( Tr2PostProcess2& postprocess, std::vector& sources ) +{ + std::vector buckets; + + if ( auto deprecated = postprocess.GetGenericEffectIfAvailable() ) + { + if( deprecated->IsActive() ) + { + deprecated->UpdateEffectParameters(); + if( deprecated->IsValid() ) + { + auto& bucket = buckets.emplace_back( *deprecated ); + bucket.effects.push_back( EffectInstance{ deprecated, PostProcessEnums::SCENE_DEFAULT_PRIORITY, 1.0f } ); + } + } + } + + for( auto& src : sources ) + { + if( src->intensity <= 0 ) + { + continue; + } + + for( auto& effect : src->genericEffects ) + { + if( !effect || !effect->IsActive() ) + { + continue; + } + effect->UpdateEffectParameters(); + if( !effect->IsValid() ) + { + continue; + } + auto found = std::find_if( buckets.begin(), buckets.end(), [&]( const EffectBucket& bucket ) { + return effect->CanBeMerged( *bucket.effects.front().effect ); + } ); + if( found == buckets.end() ) + { + buckets.emplace_back( *effect ); + found = std::prev( buckets.end() ); + } + found->effects.push_back( EffectInstance{ effect, src->priority, src->intensity } ); + } + } + + + for( auto& bucket : buckets ) + { + float remainingWeight = 1.0f; + + std::sort( bucket.effects.begin(), bucket.effects.end(), []( const EffectInstance& a, const EffectInstance& b ) { + return a.priority > b.priority; + } ); + + for( auto it = begin( bucket.effects ); it != end( bucket.effects ); ) + { + // figure out the range of sources with the same priority + auto jt = it; + while( jt != end( bucket.effects ) && ( *jt ).priority == ( *it ).priority ) + { + ++jt; + } + + float totalPriorityIntensity = 0.0f; + for( auto kt = it; kt != jt; ++kt ) + { + totalPriorityIntensity += kt->intensity; + } + if( totalPriorityIntensity == 0.0f ) + { + it = jt; + continue; + } + + float normalizationFactor = 1.f / std::max( totalPriorityIntensity, 1.0f ) * remainingWeight; + + for( auto kt = it; kt != jt; ++kt ) + { + float weight = kt->intensity * normalizationFactor; + bucket.Accumulate( *kt->effect, weight ); + } + + remainingWeight -= totalPriorityIntensity; + it = jt; + if( remainingWeight <= 0 ) + { + break; + } + } + } + + for( auto& effects : postprocess.m_genericEffects.effects ) + { + effects.clear(); + } + + for( auto& bucket : buckets ) + { + Tr2AccumulatedGenericEffects::GenericEffectInstance instance; + instance.effect = bucket.effects.front().effect; + instance.floatValues = std::move( bucket.floatValues ); + instance.vector2Values = std::move( bucket.vector2Values ); + instance.vector3Values = std::move( bucket.vector3Values ); + instance.vector4Values = std::move( bucket.vector4Values ); + postprocess.m_genericEffects.effects[bucket.effects.front().effect->m_executionSlot].push_back( instance ); + } + for( auto& each : postprocess.m_genericEffects.effects ) + { + std::sort( each.begin(), each.end(), []( const auto& a, const auto& b ) { + return a.effect->m_order < b.effect->m_order; + } ); + } +} \ No newline at end of file diff --git a/trinity/PostProcess/Effects/Tr2PPGenericEffect.h b/trinity/PostProcess/Effects/Tr2PPGenericEffect.h index 429beeb83..99273a65b 100644 --- a/trinity/PostProcess/Effects/Tr2PPGenericEffect.h +++ b/trinity/PostProcess/Effects/Tr2PPGenericEffect.h @@ -5,6 +5,13 @@ #include "PostProcess/Effects/Tr2PPEffect.h" BLUE_DECLARE( Tr2Effect ); +BLUE_DECLARE( Tr2Shader ); +BLUE_DECLARE( Tr2FloatParameter ); +BLUE_DECLARE( Tr2Vector2Parameter ); +BLUE_DECLARE( Tr2Vector3Parameter ); +BLUE_DECLARE( Tr2Vector4Parameter ); +BLUE_DECLARE( Tr2PostProcessAttributes ); +BLUE_DECLARE( Tr2PostProcess2 ); BLUE_CLASS( Tr2PPGenericEffect ) : public Tr2PPEffect @@ -14,11 +21,68 @@ BLUE_CLASS( Tr2PPGenericEffect ) : Tr2PPGenericEffect( IRoot* lockobj = NULL ); - Tr2EffectPtr GetEffect() const; + void UpdateEffectParameters(); + bool IsValid() const; + bool CanBeMerged( const Tr2PPGenericEffect& other ) const; + bool RequiresSourceTexture() const; + + /// Effect used for post-processing. + Tr2EffectPtr m_effect; + /// Post-processing quality threshold for this effect. If the current post-processing quality is lower than this value, the effect will not be executed. + PostProcess::Quality m_quality = PostProcess::LOW; + + enum ExecutionSlot + { + BEFORE_UPSCALING, + AFTER_TONEMAP, + }; + static constexpr size_t ExecutionSlotCount = 2; + + /// Place in the post-processing pipeline where this effect should be executed. + ExecutionSlot m_executionSlot = BEFORE_UPSCALING; + /// Order of execution for this effect in relation to other generic effects. Lower values are executed first. + int32_t m_order = 0; + + template + struct BlendableParameter + { + using ParameterType = Param; + using ValueType = Value; + + BluePtr parameter; + Value defaultValue; + }; + + std::vector> blendableFloatParameters; + std::vector> blendableVector2Parameters; + std::vector> blendableVector3Parameters; + std::vector> blendableVector4Parameters; - PostProcess::Quality m_quality; private: - Tr2EffectPtr m_effect; + unsigned m_lastHashValue = 0; + unsigned m_nonBlendableHash = 0; + Tr2ShaderPtr m_lastShader; + bool m_requiresSourceTexture = false; }; TYPEDEF_BLUECLASS( Tr2PPGenericEffect ); + + +struct Tr2AccumulatedGenericEffects +{ + struct GenericEffectInstance + { + void SetParameters(); + void RestoreParameters(); + + Tr2PPGenericEffectPtr effect; + std::vector floatValues; + std::vector vector2Values; + std::vector vector3Values; + std::vector vector4Values; + }; + + std::array, Tr2PPGenericEffect::ExecutionSlotCount> effects; +}; + +void AccumulateGenericEffects( Tr2PostProcess2& postprocess, std::vector& sources ); \ No newline at end of file diff --git a/trinity/PostProcess/Effects/Tr2PPGenericEffect_Blue.cpp b/trinity/PostProcess/Effects/Tr2PPGenericEffect_Blue.cpp index 5b4fa2822..9ca3e13df 100644 --- a/trinity/PostProcess/Effects/Tr2PPGenericEffect_Blue.cpp +++ b/trinity/PostProcess/Effects/Tr2PPGenericEffect_Blue.cpp @@ -3,6 +3,22 @@ #include "StdAfx.h" #include "Tr2PPGenericEffect.h" +namespace PostProcessEnums +{ +Be::VarChooser Tr2PostProcessExecutionSlotChooser[] = { + + { "BEFORE_UPSCALING", + BeCast( Tr2PPGenericEffect::BEFORE_UPSCALING ), + "Render effect before upscaling" }, + { "AFTER_TONEMAP", + BeCast( Tr2PPGenericEffect::AFTER_TONEMAP ), + "Render effect after tonemapping" }, + { 0 } +}; +BLUE_REGISTER_ENUM_EX( "Tr2PostProcessExecutionSlot", Tr2PPGenericEffect::ExecutionSlot, Tr2PostProcessExecutionSlotChooser, ENUM_REG_ENUM_OBJECT_ON_MODULE ); +} + + BLUE_DEFINE( Tr2PPGenericEffect ); @@ -17,6 +33,8 @@ const Be::ClassInfo* Tr2PPGenericEffect::ExposeToBlue() m_effect, "The effect to use. The Tr2PostProcessRenderer passes the pre upscaled source into the Blit texture parameter of this effect", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE_WITH_CHOOSER( "executionSlot", m_executionSlot, "The execution slot for this effect", Be::READWRITE | Be::PERSIST | Be::ENUM, PostProcessEnums::Tr2PostProcessExecutionSlotChooser ); + MAP_ATTRIBUTE( "order", m_order, "Affects the order of rendering for this effect in relation to other generic effects", Be::READWRITE | Be::PERSIST ); EXPOSURE_CHAINTO( Tr2PPEffect ) } diff --git a/trinity/PostProcess/Tr2PostProcess2.h b/trinity/PostProcess/Tr2PostProcess2.h index 06a76932c..c76c7c136 100644 --- a/trinity/PostProcess/Tr2PostProcess2.h +++ b/trinity/PostProcess/Tr2PostProcess2.h @@ -87,6 +87,8 @@ BLUE_CLASS( Tr2PostProcess2 ) : float m_exposureAdjustment = 0; + Tr2AccumulatedGenericEffects m_genericEffects; + private: Tr2PPSignalLossEffectPtr m_signalLoss; Tr2PPGodRaysEffectPtr m_godRays; diff --git a/trinity/PostProcess/Tr2PostProcessAttributes.cpp b/trinity/PostProcess/Tr2PostProcessAttributes.cpp index 7d28aa3e2..ade1068b3 100644 --- a/trinity/PostProcess/Tr2PostProcessAttributes.cpp +++ b/trinity/PostProcess/Tr2PostProcessAttributes.cpp @@ -48,7 +48,8 @@ Tr2PostProcessAttributes::Tr2PostProcessAttributes( IRoot* lockobj ) : depthOfFieldFocalDistance( Attribute( 0.0f ) ), depthOfFieldFocalLength( Attribute( 0.0f ) ), depthOfFieldShape( Attribute( Tr2Bokeh::Disk ) ), - depthOfFieldForegroundBlurNeeded( Attribute( false ) ) + depthOfFieldForegroundBlurNeeded( Attribute( false ) ), + PARENTLOCK( genericEffects ) { } @@ -146,6 +147,8 @@ void Tr2PostProcessAttributes::MergeInto( Tr2PostProcess2& postprocess, std::vec auto colorGain = Accumulate( &Tr2PostProcessAttributes::colorGain, sources, debugObserver ); auto colorOffset = Accumulate( &Tr2PostProcessAttributes::colorOffset, sources, debugObserver ); + AccumulateGenericEffects( postprocess, sources ); + postprocess.SetBloom( nullptr ); postprocess.SetDesaturate( nullptr ); postprocess.SetFade( nullptr ); diff --git a/trinity/PostProcess/Tr2PostProcessAttributes.h b/trinity/PostProcess/Tr2PostProcessAttributes.h index 09a5a5096..7cf957c29 100644 --- a/trinity/PostProcess/Tr2PostProcessAttributes.h +++ b/trinity/PostProcess/Tr2PostProcessAttributes.h @@ -10,6 +10,9 @@ BLUE_DECLARE( Tr2PostProcessAttributes ); BLUE_DECLARE_VECTOR( Tr2PostProcessAttributes ); +BLUE_DECLARE( Tr2PPGenericEffect ); +BLUE_DECLARE_VECTOR( Tr2PPGenericEffect ); + BLUE_CLASS( Tr2PostProcessAttributes ) : public IRoot { @@ -102,6 +105,8 @@ BLUE_CLASS( Tr2PostProcessAttributes ) : PriorityBlend::Attribute colorGamma = 1.f; PriorityBlend::Attribute colorGain = Vector3( 1, 1, 1 ); PriorityBlend::Attribute colorOffset = Vector3( 0, 0, 0 ); + + PTr2PPGenericEffectVector genericEffects; }; TYPEDEF_BLUECLASS( Tr2PostProcessAttributes ); diff --git a/trinity/PostProcess/Tr2PostProcessAttributes_Blue.cpp b/trinity/PostProcess/Tr2PostProcessAttributes_Blue.cpp index 51ede9e65..eba483f19 100644 --- a/trinity/PostProcess/Tr2PostProcessAttributes_Blue.cpp +++ b/trinity/PostProcess/Tr2PostProcessAttributes_Blue.cpp @@ -114,5 +114,7 @@ const Be::ClassInfo* Tr2PostProcessAttributes::ExposeToBlue() POSTPROCESSATTRIBUTE_DEFINE( colorGain, Color Correction, "\n:jessica-numeric-range: (0.0, 2.0)" ) POSTPROCESSATTRIBUTE_DEFINE( colorOffset, Color Correction, "\n:jessica-numeric-range: (0.0, 2.0)" ) + MAP_ATTRIBUTE( "genericEffects", genericEffects, "List of generic post-processing effects", Be::READ | Be::PERSIST ) + EXPOSURE_END() } \ No newline at end of file diff --git a/trinity/PostProcess/Tr2PostProcessRenderer.cpp b/trinity/PostProcess/Tr2PostProcessRenderer.cpp index cd759a319..6b1b582a7 100644 --- a/trinity/PostProcess/Tr2PostProcessRenderer.cpp +++ b/trinity/PostProcess/Tr2PostProcessRenderer.cpp @@ -701,11 +701,6 @@ void Tr2PostProcessRenderer::Execute( if( postProcess != nullptr ) { - if( auto genericEffect = postProcess->GetGenericEffectIfAvailable( m_quality ) ) - { - RenderGenericEffect( nonMsaaSource, sourceBuffer, renderContext, genericEffect ); - } - if( auto fog = postProcess->GetFogIfAvailable( m_quality ) ) { RenderFog( nonMsaaSource, sourceBuffer, gpuResourcePool, renderContext, fog ); @@ -717,6 +712,8 @@ void Tr2PostProcessRenderer::Execute( RenderGodRays( nonMsaaSource, depthMap, gpuResourcePool, renderContext, godrays ); } + nonMsaaSource = RenderGenericEffects( postProcess->m_genericEffects.effects[Tr2PPGenericEffect::BEFORE_UPSCALING], nonMsaaSource, gpuResourcePool, renderContext ); + if( auto dof = postProcess->GetDepthOfFieldIfAvailable( m_quality ) ) { bool temporal = upscalingInfo.temporal || postProcess->GetTaaIfAvailable( m_quality ) != nullptr; @@ -806,10 +803,12 @@ void Tr2PostProcessRenderer::Execute( RenderTonemapping( tonemappedOutput, postProcess, renderContext ); output = RenderUpscaling( tonemappedOutput, depthMap, velocity, opaqueColor, scene->GetReprojectionMatrix(), gpuResourcePool, renderContext, upscalingContext, dynamicExposure ); - depthMap = {}; - velocity = {}; - opaqueColor = {}; - + if( !postProcess || postProcess->m_genericEffects.effects[Tr2PPGenericEffect::AFTER_TONEMAP].empty() ) + { + depthMap = {}; + velocity = {}; + opaqueColor = {}; + } // need to reset the perframedata so we have the correct viewport size etc scene->ApplyUpscalingToPerFrameData( displaySize.width, displaySize.height, renderContext ); } @@ -818,6 +817,14 @@ void Tr2PostProcessRenderer::Execute( RenderTonemapping( output, postProcess, renderContext ); } + { + auto newOutput = RenderGenericEffects( postProcess->m_genericEffects.effects[Tr2PPGenericEffect::AFTER_TONEMAP], output, gpuResourcePool, renderContext ); + if( !( newOutput.Get() == output.Get() ) ) + { + DrawInto( output, Tr2LoadAction::DONT_CARE, newOutput, renderContext ); + } + } + renderContext.m_esm.SetRenderTarget( 0, destination ); if( filmGrain != nullptr ) { @@ -831,6 +838,14 @@ void Tr2PostProcessRenderer::Execute( else { RenderTonemapping( output, postProcess, renderContext ); + { + auto newOutput = RenderGenericEffects( postProcess->m_genericEffects.effects[Tr2PPGenericEffect::AFTER_TONEMAP], output, gpuResourcePool, renderContext ); + if( !( newOutput.Get() == output.Get() ) ) + { + DrawInto( output, Tr2LoadAction::DONT_CARE, newOutput, renderContext ); + } + } + Tr2Renderer::DrawTexture( renderContext, output ); } @@ -1582,19 +1597,6 @@ void Tr2PostProcessRenderer::RenderTonemapping( DrawInto( dest, Tr2LoadAction::DONT_CARE, m_tonemappingEffect, renderContext ); } -void Tr2PostProcessRenderer::RenderGenericEffect( const Tr2TextureAL& dest, const Tr2TextureAL& src, Tr2RenderContext& renderContext, Tr2PPGenericEffectPtr genericEffect ) -{ - Tr2EffectPtr effect = genericEffect->GetEffect(); - if( effect != nullptr ) - { - GPU_REGION( renderContext, "GenericEffect" ); - renderContext.m_esm.ApplyStandardStates( Tr2EffectStateManager::RM_FULLSCREEN ); - - TEMP_PARAM( effect, "Blit", src ); - DrawInto( dest, Tr2LoadAction::DONT_CARE, effect, renderContext ); - } -} - void Tr2PostProcessRenderer::RenderDepthOfField( const Tr2TextureAL& dest, Tr2GpuResourcePool& gpuResourcePool, Tr2RenderContext& renderContext, Tr2PPDepthOfFieldEffect* depthOfField, bool temporal, float upscalingAmount ) { GPU_REGION( renderContext, "DepthOfField" ); @@ -1712,3 +1714,33 @@ Tr2GpuResourcePool::Texture Tr2PostProcessRenderer::GetBlackTexture( Tr2GpuResou Tr2SubresourceData initData = { blackColor, 4 * sizeof( uint32_t ), 4 * 4 * sizeof( uint32_t ) }; return gpuResourcePool.GetPersistentTexture( "Black", 4, 4, Tr2RenderContextEnum::PIXEL_FORMAT_B8G8R8A8_UNORM, Tr2GpuUsage::SHADER_RESOURCE, &initData ); } + +Tr2GpuResourcePool::Texture Tr2PostProcessRenderer::RenderGenericEffects( std::vector& effects, const Tr2GpuResourcePool::Texture& src, Tr2GpuResourcePool& gpuResourcePool, Tr2RenderContext& renderContext ) const +{ + auto effectSrc = src; + for( auto& genericEffect : effects ) + { + if ( genericEffect.effect->m_quality > m_quality ) + { + continue; + } + + GPU_REGION( renderContext, "GenericEffect" ); + renderContext.m_esm.ApplyStandardStates( Tr2EffectStateManager::RM_FULLSCREEN ); + + genericEffect.SetParameters(); + if( genericEffect.effect->RequiresSourceTexture() ) + { + auto dest = gpuResourcePool.GetTempTexture( "", src->GetWidth(), src->GetHeight(), src->GetFormat(), RENDER_TARGET ); + TEMP_PARAM( genericEffect.effect->m_effect, "Blit", effectSrc ); + DrawInto( dest, Tr2LoadAction::DONT_CARE, genericEffect.effect->m_effect, renderContext ); + effectSrc = dest; + } + else + { + DrawInto( effectSrc, Tr2LoadAction::LOAD, genericEffect.effect->m_effect, renderContext ); + } + genericEffect.RestoreParameters(); + } + return effectSrc; +} \ No newline at end of file diff --git a/trinity/PostProcess/Tr2PostProcessRenderer.h b/trinity/PostProcess/Tr2PostProcessRenderer.h index 6c296ade0..d5a6b4f04 100644 --- a/trinity/PostProcess/Tr2PostProcessRenderer.h +++ b/trinity/PostProcess/Tr2PostProcessRenderer.h @@ -230,8 +230,6 @@ BLUE_CLASS( Tr2PostProcessRenderer ) : Tr2PostProcess2* activePostProcess, Tr2RenderContext& renderContext ); - void RenderGenericEffect( const Tr2TextureAL& dest, const Tr2TextureAL& src, Tr2RenderContext& renderContext, Tr2PPGenericEffectPtr genericEffect ); - // General PostProcess::Quality m_quality; @@ -248,6 +246,8 @@ BLUE_CLASS( Tr2PostProcessRenderer ) : Tr2GpuResourcePool::Texture Blur( Tr2GpuResourcePool::Texture src, Tr2GpuResourcePool & gpuResourcePool, Tr2RenderContext & renderContext, const PostProcessBlur::BlurContext& blurContext ); Tr2GpuResourcePool::Texture DownSampleDepth( const Tr2TextureAL& depth, Tr2GpuResourcePool& gpuResourcePool, Tr2RenderContext& renderContext ); + [[nodiscard]] Tr2GpuResourcePool::Texture RenderGenericEffects( std::vector& effects, const Tr2GpuResourcePool::Texture& src, Tr2GpuResourcePool& gpuResourcePool, Tr2RenderContext& renderContext ) const; + Tr2EffectPtr m_downsampleDepthEffect; std::map> m_blurEffects; diff --git a/trinity/Shader/Tr2Effect.cpp b/trinity/Shader/Tr2Effect.cpp index ec59b317a..34bcd2216 100644 --- a/trinity/Shader/Tr2Effect.cpp +++ b/trinity/Shader/Tr2Effect.cpp @@ -1549,6 +1549,39 @@ unsigned Tr2Effect::GetHashValue() const return hash; } +uint32_t Tr2Effect::GetNonBlendableHashValue() const +{ + unsigned hash = 0; + if( m_effectResource ) + { + hash = CcpHashFNV1( m_effectResource->GetPath(), wcslen( m_effectResource->GetPath() ) * sizeof( wchar_t ) ); + } + for( auto& option : m_options ) + { + hash = CcpHashFNV1( &option, sizeof( option ), hash ); + } + for( auto it = m_constParameters.begin(); it != m_constParameters.end(); ++it ) + { + auto name = it->name.c_str(); + // looks scary, but it's a constant string, so the pointer is unique + hash = CcpHashFNV1( &name, sizeof( name ), hash ); + hash = CcpHashFNV1( &it->value, sizeof( it->value ), hash ); + } + for( auto it = m_parameters.begin(); it != m_parameters.end(); ++it ) + { + if ( GetBool( m_shader, ( *it )->GetParameterName(), "IsBlendable", false ) ) + { + continue; + } + hash = ( *it )->GetHashValue( hash ); + } + for( auto it = m_resources.begin(); it != m_resources.end(); ++it ) + { + hash = ( *it )->GetHashValue( hash ); + } + return hash; +} + ITriEffectParameter* Tr2Effect::FindParameterByName( const char* name ) const { CCP_STATS_ZONE( __FUNCTION__ ); diff --git a/trinity/Shader/Tr2Effect.h b/trinity/Shader/Tr2Effect.h index a2aeb33f3..30f824b30 100644 --- a/trinity/Shader/Tr2Effect.h +++ b/trinity/Shader/Tr2Effect.h @@ -116,6 +116,7 @@ BLUE_CLASS( Tr2Effect ) : void Render( IRenderCallback * cb, Tr2RenderContext & renderContext ); unsigned GetHashValue() const; + uint32_t GetNonBlendableHashValue() const; const Tr2ConstantEffectParameter* GetConstParameters( size_t& count ) const; ITriEffectParameter* GetParameterByName( const char* name ) const; From 2caf63b02ef3ac0fc4c9b7125d78259af1c4b651 Mon Sep 17 00:00:00 2001 From: Filipp Pavlov Date: Fri, 11 Sep 2026 13:07:27 +0000 Subject: [PATCH 2/7] Minor code cleanup --- .../Effects/Tr2PPGenericEffect.cpp | 213 ++++++++++-------- .../PostProcess/Effects/Tr2PPGenericEffect.h | 4 + 2 files changed, 120 insertions(+), 97 deletions(-) diff --git a/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp b/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp index b88a74fcf..8acd17c14 100644 --- a/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp +++ b/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp @@ -72,6 +72,71 @@ struct EffectBucket std::vector vector4Values; }; +std::vector PopulateBuckets( Tr2PostProcess2& postprocess, std::vector& sources ) +{ + std::vector buckets; + + if( auto deprecated = postprocess.GetGenericEffectIfAvailable() ) + { + if( deprecated->IsActive() ) + { + deprecated->UpdateEffectParameters(); + if( deprecated->IsValid() ) + { + auto& bucket = buckets.emplace_back( *deprecated ); + bucket.effects.push_back( EffectInstance{ deprecated, PostProcessEnums::SCENE_DEFAULT_PRIORITY, 1.0f } ); + } + } + } + + for( auto& src : sources ) + { + if( src->intensity <= 0 ) + { + continue; + } + + for( auto& effect : src->genericEffects ) + { + if( !effect || !effect->IsActive() ) + { + continue; + } + effect->UpdateEffectParameters(); + if( !effect->IsValid() ) + { + continue; + } + auto found = std::find_if( buckets.begin(), buckets.end(), [&]( const EffectBucket& bucket ) { + return effect->CanBeMerged( *bucket.effects.front().effect ); + } ); + if( found == buckets.end() ) + { + buckets.emplace_back( *effect ); + found = std::prev( buckets.end() ); + } + found->effects.push_back( EffectInstance{ effect, src->priority, src->intensity } ); + } + } + return buckets; +} + +bool IsBlendableParameter( const Tr2EffectConstant& constant, const Tr2Shader& shader ) +{ + if( constant.type != Tr2EffectConstant::FLOAT || constant.elements > 1 || constant.dimension < 1 || constant.dimension > 4 ) + { + return false; + } + if( auto annotations = shader.GetParameterAnnotations( constant.name.c_str() ) ) + { + auto found = std::find_if( annotations->begin(), annotations->end(), []( const Tr2EffectParameterAnnotation& annotation ) { + return strcmp( annotation.name, Tr2PPGenericEffect::IsBlendableAnnotationName ) == 0; + } ); + return found != annotations->end() && found->type == Tr2EffectParameterAnnotation::BOOL && found->boolValue; + } + return false; +} + } Tr2PPGenericEffect::Tr2PPGenericEffect( IRoot* ) @@ -112,59 +177,15 @@ void Tr2PPGenericEffect::UpdateEffectParameters() { for( auto& constant : stage.constants ) { - if( constant.type != Tr2EffectConstant::FLOAT || constant.elements > 1 || constant.dimension < 1 || constant.dimension > 4 ) + if( !IsBlendableParameter( constant, *shader ) ) { continue; } - if( auto annotations = shader->GetParameterAnnotations( constant.name.c_str() ) ) - { - auto found = std::find_if( annotations->begin(), annotations->end(), []( const Tr2EffectParameterAnnotation& annotation ) { return strcmp( annotation.name, "IsBlendable" ) == 0; } ); - if( found == annotations->end() || found->type != Tr2EffectParameterAnnotation::BOOL || !found->boolValue ) - { - continue; - } - } - - auto AddBlendableParameter = [&]( auto& blendableParameters ) { - using Element = typename std::decay_t::value_type; - using ParamType = typename Element::ParameterType; - using ValueType = typename Element::ValueType; - ValueType defaultValue; - memcpy( &defaultValue, stage.constantValues + constant.offset, sizeof( ValueType ) ); - BluePtr param = BlueCastPtr( m_effect->GetParameterByName( constant.name.c_str() ) ); - if( !param ) - { - m_effect->SetParameter( constant.name, defaultValue ); - param = BlueCastPtr( m_effect->GetParameterByName( constant.name.c_str() ) ); - if( !param ) - { - return; - } - } - blendableParameters.push_back( { param, defaultValue } ); - }; - - switch( constant.dimension ) - { - case 1: - AddBlendableParameter( blendableFloatParameters ); - break; - case 2: - AddBlendableParameter( blendableVector2Parameters ); - break; - case 3: - AddBlendableParameter( blendableVector3Parameters ); - break; - case 4: - AddBlendableParameter( blendableVector4Parameters ); - break; - default: - break; - } + AddBlendableParameter( constant, reinterpret_cast( stage.constantValues ) ); } if( !m_requiresSourceTexture ) { - m_requiresSourceTexture = find_if( stage.resources.begin(), stage.resources.end(), []( const auto& texture ) { return strcmp( texture.second.name, "Blit" ) == 0; } ) != stage.resources.end(); + m_requiresSourceTexture = find_if( stage.resources.begin(), stage.resources.end(), []( const auto& texture ) { return strcmp( texture.second.name, SourceTextureName ) == 0; } ) != stage.resources.end(); } } } @@ -194,30 +215,70 @@ bool Tr2PPGenericEffect::RequiresSourceTexture() const return m_requiresSourceTexture; } +void Tr2PPGenericEffect::AddBlendableParameter( const Tr2EffectConstant& constant, const uint8_t* defaultValues ) +{ + auto AddParameter = [&]( auto& blendableParameters ) { + using Element = typename std::decay_t::value_type; + using ParamType = typename Element::ParameterType; + using ValueType = typename Element::ValueType; + ValueType defaultValue; + memcpy( &defaultValue, defaultValues + constant.offset, sizeof( ValueType ) ); + BluePtr param = BlueCastPtr( m_effect->GetParameterByName( constant.name.c_str() ) ); + if( !param ) + { + m_effect->SetParameter( constant.name, defaultValue ); + param = BlueCastPtr( m_effect->GetParameterByName( constant.name.c_str() ) ); + if( !param ) + { + return; + } + } + blendableParameters.push_back( { param, defaultValue } ); + }; + + switch( constant.dimension ) + { + case 1: + AddParameter( blendableFloatParameters ); + break; + case 2: + AddParameter( blendableVector2Parameters ); + break; + case 3: + AddParameter( blendableVector3Parameters ); + break; + case 4: + AddParameter( blendableVector4Parameters ); + break; + default: + break; + } +} + void Tr2AccumulatedGenericEffects::GenericEffectInstance::SetParameters() { for( auto& param : effect->blendableFloatParameters ) { auto value = param.parameter->GetValue(); - std::swap( floatValues[¶m - &effect->blendableFloatParameters[0]], value ); + std::swap( floatValues[¶m - effect->blendableFloatParameters.data()], value ); param.parameter->SetValue( value ); } for( auto& param : effect->blendableVector2Parameters ) { auto value = param.parameter->GetValue(); - std::swap( vector2Values[¶m - &effect->blendableVector2Parameters[0]], value ); + std::swap( vector2Values[¶m - effect->blendableVector2Parameters.data()], value ); param.parameter->SetValue( value ); } for( auto& param : effect->blendableVector3Parameters ) { auto value = param.parameter->GetValue(); - std::swap( vector3Values[¶m - &effect->blendableVector3Parameters[0]], value ); + std::swap( vector3Values[¶m - effect->blendableVector3Parameters.data()], value ); param.parameter->SetValue( value ); } for( auto& param : effect->blendableVector4Parameters ) { auto value = param.parameter->GetValue(); - std::swap( vector4Values[¶m - &effect->blendableVector4Parameters[0]], value ); + std::swap( vector4Values[¶m - effect->blendableVector4Parameters.data()], value ); param.parameter->SetValue( value ); } } @@ -228,53 +289,11 @@ void Tr2AccumulatedGenericEffects::GenericEffectInstance::RestoreParameters() } -void AccumulateGenericEffects( Tr2PostProcess2& postprocess, std::vector& sources ) -{ - std::vector buckets; - - if ( auto deprecated = postprocess.GetGenericEffectIfAvailable() ) - { - if( deprecated->IsActive() ) - { - deprecated->UpdateEffectParameters(); - if( deprecated->IsValid() ) - { - auto& bucket = buckets.emplace_back( *deprecated ); - bucket.effects.push_back( EffectInstance{ deprecated, PostProcessEnums::SCENE_DEFAULT_PRIORITY, 1.0f } ); - } - } - } - - for( auto& src : sources ) - { - if( src->intensity <= 0 ) - { - continue; - } - for( auto& effect : src->genericEffects ) - { - if( !effect || !effect->IsActive() ) - { - continue; - } - effect->UpdateEffectParameters(); - if( !effect->IsValid() ) - { - continue; - } - auto found = std::find_if( buckets.begin(), buckets.end(), [&]( const EffectBucket& bucket ) { - return effect->CanBeMerged( *bucket.effects.front().effect ); - } ); - if( found == buckets.end() ) - { - buckets.emplace_back( *effect ); - found = std::prev( buckets.end() ); - } - found->effects.push_back( EffectInstance{ effect, src->priority, src->intensity } ); - } - } +void AccumulateGenericEffects( Tr2PostProcess2& postprocess, std::vector& sources ) +{ + std::vector buckets = PopulateBuckets( postprocess, sources ); for( auto& bucket : buckets ) { @@ -334,7 +353,7 @@ void AccumulateGenericEffects( Tr2PostProcess2& postprocess, std::vectorm_executionSlot].push_back( instance ); + postprocess.m_genericEffects.effects[instance.effect->m_executionSlot].push_back( instance ); } for( auto& each : postprocess.m_genericEffects.effects ) { diff --git a/trinity/PostProcess/Effects/Tr2PPGenericEffect.h b/trinity/PostProcess/Effects/Tr2PPGenericEffect.h index 99273a65b..ac9f7b66d 100644 --- a/trinity/PostProcess/Effects/Tr2PPGenericEffect.h +++ b/trinity/PostProcess/Effects/Tr2PPGenericEffect.h @@ -58,8 +58,12 @@ BLUE_CLASS( Tr2PPGenericEffect ) : std::vector> blendableVector3Parameters; std::vector> blendableVector4Parameters; + static constexpr const char* IsBlendableAnnotationName = "IsBlendable"; + static constexpr const char* SourceTextureName = "Blit"; private: + void AddBlendableParameter( const Tr2EffectConstant& constant, const uint8_t* defaultValues ); + unsigned m_lastHashValue = 0; unsigned m_nonBlendableHash = 0; Tr2ShaderPtr m_lastShader; From 59647c13160c4fdfee5403eaaa4cd3984f0e57b7 Mon Sep 17 00:00:00 2001 From: Filipp Pavlov Date: Fri, 11 Sep 2026 13:18:34 +0000 Subject: [PATCH 3/7] Code cleanup; sort blendable parameters as we assume the same order between instances --- .../Effects/Tr2PPGenericEffect.cpp | 67 +++++++++---------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp b/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp index 8acd17c14..286a1e470 100644 --- a/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp +++ b/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp @@ -20,33 +20,28 @@ struct EffectInstance float intensity; }; -template -struct BlendableParameter -{ - BluePtr parameter; - Value value; -}; - struct EffectBucket { EffectBucket() = default; - explicit EffectBucket( const Tr2PPGenericEffect& effect ) + EffectBucket( Tr2PPGenericEffect* effect, PostProcessEnums::Priority priority, float intensity ) { - for( auto& param : effect.blendableFloatParameters ) + instance.effect = effect; + effects.push_back( EffectInstance{ effect, priority, intensity } ); + for( auto& param : effect->blendableFloatParameters ) { - floatValues.push_back( param.defaultValue ); + instance.floatValues.push_back( param.defaultValue ); } - for( auto& param : effect.blendableVector2Parameters ) + for( auto& param : effect->blendableVector2Parameters ) { - vector2Values.push_back( param.defaultValue ); + instance.vector2Values.push_back( param.defaultValue ); } - for( auto& param : effect.blendableVector3Parameters ) + for( auto& param : effect->blendableVector3Parameters ) { - vector3Values.push_back( param.defaultValue ); + instance.vector3Values.push_back( param.defaultValue ); } - for( auto& param : effect.blendableVector4Parameters ) + for( auto& param : effect->blendableVector4Parameters ) { - vector4Values.push_back( param.defaultValue ); + instance.vector4Values.push_back( param.defaultValue ); } } @@ -59,19 +54,19 @@ struct EffectBucket values[¶m - params.data()] += value; } }; - AccumulateValues( effect.blendableFloatParameters, floatValues ); - AccumulateValues( effect.blendableVector2Parameters, vector2Values ); - AccumulateValues( effect.blendableVector3Parameters, vector3Values ); - AccumulateValues( effect.blendableVector4Parameters, vector4Values ); + AccumulateValues( effect.blendableFloatParameters, instance.floatValues ); + AccumulateValues( effect.blendableVector2Parameters, instance.vector2Values ); + AccumulateValues( effect.blendableVector3Parameters, instance.vector3Values ); + AccumulateValues( effect.blendableVector4Parameters, instance.vector4Values ); } + Tr2AccumulatedGenericEffects::GenericEffectInstance instance; std::vector effects; - std::vector floatValues; - std::vector vector2Values; - std::vector vector3Values; - std::vector vector4Values; }; +/** + Partition effects into buckets of compatible effects that can be blended together. Each bucket contains a list of effects that can be merged, + along with the accumulated parameters for the bucket. */ std::vector PopulateBuckets( Tr2PostProcess2& postprocess, std::vector& sources ) { std::vector buckets; @@ -83,8 +78,7 @@ std::vector PopulateBuckets( Tr2PostProcess2& postprocess, std::ve deprecated->UpdateEffectParameters(); if( deprecated->IsValid() ) { - auto& bucket = buckets.emplace_back( *deprecated ); - bucket.effects.push_back( EffectInstance{ deprecated, PostProcessEnums::SCENE_DEFAULT_PRIORITY, 1.0f } ); + buckets.emplace_back( deprecated, PostProcessEnums::SCENE_DEFAULT_PRIORITY, 1.0f ); } } } @@ -112,15 +106,18 @@ std::vector PopulateBuckets( Tr2PostProcess2& postprocess, std::ve } ); if( found == buckets.end() ) { - buckets.emplace_back( *effect ); - found = std::prev( buckets.end() ); + buckets.emplace_back( effect, src->priority, src->intensity ); + } + else + { + found->effects.push_back( EffectInstance{ effect, src->priority, src->intensity } ); } - found->effects.push_back( EffectInstance{ effect, src->priority, src->intensity } ); } } return buckets; } +/// Checks if the constant is a float# type parameter with IsBlendable annotation. Only such parameters can be blended together. bool IsBlendableParameter( const Tr2EffectConstant& constant, const Tr2Shader& shader ) { if( constant.type != Tr2EffectConstant::FLOAT || constant.elements > 1 || constant.dimension < 1 || constant.dimension > 4 ) @@ -190,6 +187,10 @@ void Tr2PPGenericEffect::UpdateEffectParameters() } } } + std::sort( blendableFloatParameters.begin(), blendableFloatParameters.end(), []( const auto& a, const auto& b ) { return a.parameter->m_name < b.parameter->m_name; } ); + std::sort( blendableVector2Parameters.begin(), blendableVector2Parameters.end(), []( const auto& a, const auto& b ) { return a.parameter->m_name < b.parameter->m_name; } ); + std::sort( blendableVector3Parameters.begin(), blendableVector3Parameters.end(), []( const auto& a, const auto& b ) { return a.parameter->m_name < b.parameter->m_name; } ); + std::sort( blendableVector4Parameters.begin(), blendableVector4Parameters.end(), []( const auto& a, const auto& b ) { return a.parameter->m_name < b.parameter->m_name; } ); } bool Tr2PPGenericEffect::IsValid() const @@ -347,13 +348,7 @@ void AccumulateGenericEffects( Tr2PostProcess2& postprocess, std::vectorm_executionSlot].push_back( instance ); + postprocess.m_genericEffects.effects[bucket.instance.effect->m_executionSlot].push_back( bucket.instance ); } for( auto& each : postprocess.m_genericEffects.effects ) { From 361c3dd6ac387a611c2bc53d6c450c76865eb76d Mon Sep 17 00:00:00 2001 From: Filipp Pavlov Date: Wed, 16 Sep 2026 11:20:45 +0000 Subject: [PATCH 4/7] Add documentation --- doc/source/eve/generic-pp-effects.rst | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 doc/source/eve/generic-pp-effects.rst diff --git a/doc/source/eve/generic-pp-effects.rst b/doc/source/eve/generic-pp-effects.rst new file mode 100644 index 000000000..1f53b2f80 --- /dev/null +++ b/doc/source/eve/generic-pp-effects.rst @@ -0,0 +1,67 @@ +Custom Post-Processing Effects +=============================== + +Trinity supports having custom post-processing effects that can be applied to the rendered scene. These effects are specified in post-process volume +objects (see Tr2PostProcessAttributes and EveChildPostProcessVolume). The Tr2PostProcessAttributes maintains a list `genericEffects` that contains +custom post-processing effects (of type `Tr2PPGenericEffect`). Each of these represents a custom post-processing pass. + +`Tr2PPGenericEffect` objects have members: + +- `effect`: The shader effect used for this post-processing pass. +- `quality`: Minimal post-processing quality setting when this effect is visible. +- `execututionSlot`: Place in the post-processing pipeline where this effect should be executed. +- `order`: The order in which this effect should be applied relative to other effects in the same execution slot. + +Effect Files (Shaders) +---------------------- + +Effect files (.fx) for custom post-processing effects are used to define the shader programs that implement the visual effects. These effects need +to have a `"Main"` technique defined, which serves as the entry point for the post-processing effect. Trinity will render a quad using this technique +to apply the effect to the scene. + +Trinity will pass pre-transform position (in clip space) and texture coordinates (0 to 1) to the vertex shader of the `"Main"` technique: + +``` +struct PostProcessVertex +{ + float4 pos : POSITION; + float2 texCoord : TEXCOORD0; +}; +``` + +The effect may use the the output of the previous post-processing step as the input texture. For that, the shader needs to define a texture 2D object +with the name `"Blit"`. It is possible to have a shader that does not use the previous post-processing output, in which case the Trinity will render +the effect into the output of the previous pass directly. This can be used for overlay/additive effects. + +The effect may use other scene-wide textures, like `DepthMap` or `VelocityMap`. Note that currently, there is no guarantee that these textures will always be +available, especially at the execution slots later in the pipeline. Normally, `DepthMap` is safe to use at most stages, but `VelocityMap` may not be available +after tonemapping, or with certain rendering settings. + +The effect may expose parameters that can be adjusted by post-processing based on the volume intensity/relative weight. Such parameters need to have +floating point type (e.g., `float`, `float2`, `float3`, `float4`) and have `bool IsBlendable = true;` annotation. When rendering the post-processing effect, +Trinity will blend these parameters based on the volume intensity/relative weight. For each effect instanced it will interpolate such parameters between their +default values, specified in HLSL code and the values specified in the effect's parameter object. + + +Blending and Grouping +--------------------- + +Generally, Trinity treats each custom post-processing effect as an individual pass. This means that different effects are applied separately, +without any automatic blending or overriding each other. + +For identical effects, Trinity tries to group them together to optimize rendering. Effects that share the same shader (along with all the non-blendable +material parameters), execution slot and quality threshold are merged together, and blended as a single post-processing effect. When custom effects are +blended together, all of their blendable parameters are blended using the normal rules similar to any other post-processing attribute. + + +Execution Slots +--------------- + +Execution slots determine the place in the post-processing pipeline where the effect should be executed: + +- `BEFORE_UPSCALING`: The effect is applied before the upscaling step or TAA in the rendering pipeline, after legacy fog and godray passes. Note, that the + input Src texture and the required output are in linear HDR color space. +- `AFTER_TONEMAP`: The effect is applied after the tonemapping step in the rendering pipeline. The input Src texture and the required output are in the + final color space, typically sRGB. Note that the scene-wide textures like `DepthMap` and `VelocityMap` may not be available at this stage, and if they + are, they may be of a different resolution that the target one, and may have camera jitter applied. + From db5d55aad0e34ba765d7b3b2b780af84b78c5864 Mon Sep 17 00:00:00 2001 From: Filipp Pavlov Date: Wed, 16 Sep 2026 11:39:01 +0000 Subject: [PATCH 5/7] Formatting --- trinity/PostProcess/Tr2PostProcessRenderer.cpp | 2 +- trinity/PostProcess/Tr2PostProcessRenderer.h | 2 +- trinity/Shader/Tr2Effect.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/trinity/PostProcess/Tr2PostProcessRenderer.cpp b/trinity/PostProcess/Tr2PostProcessRenderer.cpp index 6b1b582a7..c084c0ea8 100644 --- a/trinity/PostProcess/Tr2PostProcessRenderer.cpp +++ b/trinity/PostProcess/Tr2PostProcessRenderer.cpp @@ -1720,7 +1720,7 @@ Tr2GpuResourcePool::Texture Tr2PostProcessRenderer::RenderGenericEffects( std::v auto effectSrc = src; for( auto& genericEffect : effects ) { - if ( genericEffect.effect->m_quality > m_quality ) + if( genericEffect.effect->m_quality > m_quality ) { continue; } diff --git a/trinity/PostProcess/Tr2PostProcessRenderer.h b/trinity/PostProcess/Tr2PostProcessRenderer.h index d5a6b4f04..cfa7214ce 100644 --- a/trinity/PostProcess/Tr2PostProcessRenderer.h +++ b/trinity/PostProcess/Tr2PostProcessRenderer.h @@ -246,7 +246,7 @@ BLUE_CLASS( Tr2PostProcessRenderer ) : Tr2GpuResourcePool::Texture Blur( Tr2GpuResourcePool::Texture src, Tr2GpuResourcePool & gpuResourcePool, Tr2RenderContext & renderContext, const PostProcessBlur::BlurContext& blurContext ); Tr2GpuResourcePool::Texture DownSampleDepth( const Tr2TextureAL& depth, Tr2GpuResourcePool& gpuResourcePool, Tr2RenderContext& renderContext ); - [[nodiscard]] Tr2GpuResourcePool::Texture RenderGenericEffects( std::vector& effects, const Tr2GpuResourcePool::Texture& src, Tr2GpuResourcePool& gpuResourcePool, Tr2RenderContext& renderContext ) const; + [[nodiscard]] Tr2GpuResourcePool::Texture RenderGenericEffects( std::vector & effects, const Tr2GpuResourcePool::Texture& src, Tr2GpuResourcePool& gpuResourcePool, Tr2RenderContext& renderContext ) const; Tr2EffectPtr m_downsampleDepthEffect; std::map> m_blurEffects; diff --git a/trinity/Shader/Tr2Effect.cpp b/trinity/Shader/Tr2Effect.cpp index 34bcd2216..014d6bad1 100644 --- a/trinity/Shader/Tr2Effect.cpp +++ b/trinity/Shader/Tr2Effect.cpp @@ -1569,7 +1569,7 @@ uint32_t Tr2Effect::GetNonBlendableHashValue() const } for( auto it = m_parameters.begin(); it != m_parameters.end(); ++it ) { - if ( GetBool( m_shader, ( *it )->GetParameterName(), "IsBlendable", false ) ) + if( GetBool( m_shader, ( *it )->GetParameterName(), "IsBlendable", false ) ) { continue; } From 7465ee1ced751ed314981cbccc505380d3eb84d8 Mon Sep 17 00:00:00 2001 From: Filipp Pavlov Date: Thu, 17 Sep 2026 08:40:30 +0000 Subject: [PATCH 6/7] Address PR comments --- doc/source/eve/generic-pp-effects.rst | 2 +- trinity/Eve/EveSpaceScene.cpp | 3 +-- trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp | 6 +++++- trinity/PostProcess/Tr2PostProcessRenderer.cpp | 2 ++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/doc/source/eve/generic-pp-effects.rst b/doc/source/eve/generic-pp-effects.rst index 1f53b2f80..b3c842f7a 100644 --- a/doc/source/eve/generic-pp-effects.rst +++ b/doc/source/eve/generic-pp-effects.rst @@ -9,7 +9,7 @@ custom post-processing effects (of type `Tr2PPGenericEffect`). Each of these rep - `effect`: The shader effect used for this post-processing pass. - `quality`: Minimal post-processing quality setting when this effect is visible. -- `execututionSlot`: Place in the post-processing pipeline where this effect should be executed. +- `executionSlot`: Place in the post-processing pipeline where this effect should be executed. - `order`: The order in which this effect should be applied relative to other effects in the same execution slot. Effect Files (Shaders) diff --git a/trinity/Eve/EveSpaceScene.cpp b/trinity/Eve/EveSpaceScene.cpp index 088a09e8e..f47b8f0c9 100644 --- a/trinity/Eve/EveSpaceScene.cpp +++ b/trinity/Eve/EveSpaceScene.cpp @@ -377,6 +377,7 @@ void EveSpaceScene::UpdatePostProcessAttributes() []( Tr2PostProcessAttributes* a, Tr2PostProcessAttributes* b ) { return a->priority > b->priority; } ); + m_combinedPostProcess->SetGenericEffect( m_sceneDefaultPostProcess ? m_sceneDefaultPostProcess->GetGenericEffectIfAvailable() : nullptr ); if( g_enablePostProcessDebugging ) { PriorityBlend::AttributesDebugObserver observer; @@ -395,7 +396,6 @@ void EveSpaceScene::UpdatePostProcessAttributes() m_combinedPostProcess->SetTonemapping( m_sceneDefaultPostProcess->GetTonemappingIfAvailable() ); m_combinedPostProcess->SetFog( m_sceneDefaultPostProcess->GetFogIfAvailable() ); m_combinedPostProcess->SetGodRays( m_sceneDefaultPostProcess->GetGodRaysIfAvailable() ); - m_combinedPostProcess->SetGenericEffect( m_sceneDefaultPostProcess->GetGenericEffectIfAvailable() ); } else { @@ -404,7 +404,6 @@ void EveSpaceScene::UpdatePostProcessAttributes() m_combinedPostProcess->SetTonemapping( nullptr ); m_combinedPostProcess->SetFog( nullptr ); m_combinedPostProcess->SetGodRays( nullptr ); - m_combinedPostProcess->SetGenericEffect( nullptr ); } m_combinedPostProcessAttributes->FromPostProcess( m_combinedPostProcess, PostProcessEnums::MEDIUM_PRIORITY, 1.0f ); } diff --git a/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp b/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp index 286a1e470..93132f0be 100644 --- a/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp +++ b/trinity/PostProcess/Effects/Tr2PPGenericEffect.cpp @@ -234,7 +234,11 @@ void Tr2PPGenericEffect::AddBlendableParameter( const Tr2EffectConstant& constan return; } } - blendableParameters.push_back( { param, defaultValue } ); + auto seen = std::find_if( blendableParameters.begin(), blendableParameters.end(), [&]( const auto& p ) { return p.parameter == param; } ); + if( seen == blendableParameters.end() ) + { + blendableParameters.push_back( { param, defaultValue } ); + } }; switch( constant.dimension ) diff --git a/trinity/PostProcess/Tr2PostProcessRenderer.cpp b/trinity/PostProcess/Tr2PostProcessRenderer.cpp index c084c0ea8..b3f36e3d1 100644 --- a/trinity/PostProcess/Tr2PostProcessRenderer.cpp +++ b/trinity/PostProcess/Tr2PostProcessRenderer.cpp @@ -817,6 +817,7 @@ void Tr2PostProcessRenderer::Execute( RenderTonemapping( output, postProcess, renderContext ); } + if( postProcess ) { auto newOutput = RenderGenericEffects( postProcess->m_genericEffects.effects[Tr2PPGenericEffect::AFTER_TONEMAP], output, gpuResourcePool, renderContext ); if( !( newOutput.Get() == output.Get() ) ) @@ -838,6 +839,7 @@ void Tr2PostProcessRenderer::Execute( else { RenderTonemapping( output, postProcess, renderContext ); + if( postProcess ) { auto newOutput = RenderGenericEffects( postProcess->m_genericEffects.effects[Tr2PPGenericEffect::AFTER_TONEMAP], output, gpuResourcePool, renderContext ); if( !( newOutput.Get() == output.Get() ) ) From 5d6c54fe22199c3d618f68dfa0483228262bdb83 Mon Sep 17 00:00:00 2001 From: filipppavlov Date: Fri, 18 Sep 2026 15:09:37 +0000 Subject: [PATCH 7/7] Formatting in generic-pp-effects.rst --- doc/source/eve/generic-pp-effects.rst | 45 +++++++++++++-------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/doc/source/eve/generic-pp-effects.rst b/doc/source/eve/generic-pp-effects.rst index b3c842f7a..4a84a4aab 100644 --- a/doc/source/eve/generic-pp-effects.rst +++ b/doc/source/eve/generic-pp-effects.rst @@ -2,15 +2,15 @@ Custom Post-Processing Effects =============================== Trinity supports having custom post-processing effects that can be applied to the rendered scene. These effects are specified in post-process volume -objects (see Tr2PostProcessAttributes and EveChildPostProcessVolume). The Tr2PostProcessAttributes maintains a list `genericEffects` that contains -custom post-processing effects (of type `Tr2PPGenericEffect`). Each of these represents a custom post-processing pass. +objects (see ``Tr2PostProcessAttributes`` and ``EveChildPostProcessVolume``). The Tr2PostProcessAttributes maintains a list ``genericEffects`` that contains +custom post-processing effects (of type ``Tr2PPGenericEffect``). Each of these represents a custom post-processing pass. -`Tr2PPGenericEffect` objects have members: +``Tr2PPGenericEffect`` objects have members: -- `effect`: The shader effect used for this post-processing pass. -- `quality`: Minimal post-processing quality setting when this effect is visible. -- `executionSlot`: Place in the post-processing pipeline where this effect should be executed. -- `order`: The order in which this effect should be applied relative to other effects in the same execution slot. +- ``effect``: The shader effect used for this post-processing pass. +- ``quality``: Minimal post-processing quality setting when this effect is visible. +- ``executionSlot``: Place in the post-processing pipeline where this effect should be executed. +- ``order``: The order in which this effect should be applied relative to other effects in the same execution slot. Effect Files (Shaders) ---------------------- @@ -19,18 +19,17 @@ Effect files (.fx) for custom post-processing effects are used to define the sha to have a `"Main"` technique defined, which serves as the entry point for the post-processing effect. Trinity will render a quad using this technique to apply the effect to the scene. -Trinity will pass pre-transform position (in clip space) and texture coordinates (0 to 1) to the vertex shader of the `"Main"` technique: +Trinity will pass pre-transform position (in clip space) and texture coordinates (0 to 1) to the vertex shader of the `"Main"` technique: :: -``` -struct PostProcessVertex -{ - float4 pos : POSITION; - float2 texCoord : TEXCOORD0; -}; -``` + struct PostProcessVertex + { + float4 pos : POSITION; + float2 texCoord : TEXCOORD0; + }; -The effect may use the the output of the previous post-processing step as the input texture. For that, the shader needs to define a texture 2D object -with the name `"Blit"`. It is possible to have a shader that does not use the previous post-processing output, in which case the Trinity will render + +The effect may use the output of the previous post-processing step as the input texture. For that, the shader needs to define a ``Texture2D`` object +with the name "Blit". It is possible to have a shader that does not use the previous post-processing output, in which case the Trinity will render the effect into the output of the previous pass directly. This can be used for overlay/additive effects. The effect may use other scene-wide textures, like `DepthMap` or `VelocityMap`. Note that currently, there is no guarantee that these textures will always be @@ -38,8 +37,8 @@ available, especially at the execution slots later in the pipeline. Normally, `D after tonemapping, or with certain rendering settings. The effect may expose parameters that can be adjusted by post-processing based on the volume intensity/relative weight. Such parameters need to have -floating point type (e.g., `float`, `float2`, `float3`, `float4`) and have `bool IsBlendable = true;` annotation. When rendering the post-processing effect, -Trinity will blend these parameters based on the volume intensity/relative weight. For each effect instanced it will interpolate such parameters between their +floating-point type (e.g., ``float``, ``float2``, ``float3``, ``float4``) and have ``bool IsBlendable = true;`` annotation. When rendering the post-processing effect, +Trinity will blend these parameters based on the volume intensity/relative weight. For each effect instanced, it will interpolate such parameters between their default values, specified in HLSL code and the values specified in the effect's parameter object. @@ -50,7 +49,7 @@ Generally, Trinity treats each custom post-processing effect as an individual pa without any automatic blending or overriding each other. For identical effects, Trinity tries to group them together to optimize rendering. Effects that share the same shader (along with all the non-blendable -material parameters), execution slot and quality threshold are merged together, and blended as a single post-processing effect. When custom effects are +material parameters), execution slot, and quality threshold are merged together and blended as a single post-processing effect. When custom effects are blended together, all of their blendable parameters are blended using the normal rules similar to any other post-processing attribute. @@ -59,9 +58,9 @@ Execution Slots Execution slots determine the place in the post-processing pipeline where the effect should be executed: -- `BEFORE_UPSCALING`: The effect is applied before the upscaling step or TAA in the rendering pipeline, after legacy fog and godray passes. Note, that the +- ``BEFORE_UPSCALING``: The effect is applied before the upscaling step or TAA in the rendering pipeline, after legacy fog and godray passes. Note that the input Src texture and the required output are in linear HDR color space. -- `AFTER_TONEMAP`: The effect is applied after the tonemapping step in the rendering pipeline. The input Src texture and the required output are in the +- ``AFTER_TONEMAP``: The effect is applied after the tonemapping step in the rendering pipeline. The input Src texture and the required output are in the final color space, typically sRGB. Note that the scene-wide textures like `DepthMap` and `VelocityMap` may not be available at this stage, and if they - are, they may be of a different resolution that the target one, and may have camera jitter applied. + are, they may be of a different resolution than the target one, and may have camera jitter applied.