diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPass.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPass.cpp index d3f2f94..d65cddb 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPass.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPass.cpp @@ -188,6 +188,7 @@ std::vector OpenGLRenderPass::BuildLayerPassDescriptors( pass.passId = passProgram.passId; pass.layerId = state.layerId; pass.shaderId = state.shaderId; + pass.layerInputTexture = layerInputTexture; pass.sourceTexture = passSourceTexture; pass.sourceFramebuffer = passIndex == 0 ? layerInputFramebuffer : passSourceFramebuffer; pass.destinationTexture = passDestinationTexture; @@ -226,6 +227,7 @@ void OpenGLRenderPass::RenderLayerPass( return; RenderShaderProgram( + pass.layerInputTexture, pass.sourceTexture, pass.destinationFramebuffer, *pass.passProgram, @@ -243,6 +245,7 @@ void OpenGLRenderPass::RenderLayerPass( } void OpenGLRenderPass::RenderShaderProgram( + GLuint layerInputTexture, GLuint sourceTexture, GLuint destinationFrameBuffer, PassProgram& passProgram, @@ -267,7 +270,7 @@ void OpenGLRenderPass::RenderShaderProgram( const std::vector temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0); const GLuint feedbackTexture = mRenderer.FeedbackBuffers().ResolveReadTexture(state); const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan = - mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, state, feedbackTexture, sourceHistoryTextures, temporalHistoryTextures); + mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, layerInputTexture, state, feedbackTexture, sourceHistoryTextures, temporalHistoryTextures); mTextureBindings.BindRuntimeTexturePlan(texturePlan); glBindVertexArray(mRenderer.FullscreenVertexArray()); glUseProgram(passProgram.program); diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPass.h b/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPass.h index c2aaae2..441cfe2 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPass.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPass.h @@ -44,6 +44,7 @@ private: const TextBindingUpdater& updateTextBinding, const GlobalParamsUpdater& updateGlobalParams); void RenderShaderProgram( + GLuint layerInputTexture, GLuint sourceTexture, GLuint destinationFrameBuffer, PassProgram& passProgram, diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/RenderPassDescriptor.h b/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/RenderPassDescriptor.h index ba328e1..937ce3e 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/RenderPassDescriptor.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/RenderPassDescriptor.h @@ -28,6 +28,7 @@ struct RenderPassDescriptor std::string passId; std::string layerId; std::string shaderId; + GLuint layerInputTexture = 0; GLuint sourceTexture = 0; GLuint sourceFramebuffer = 0; GLuint destinationTexture = 0; diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/renderer/GlRenderConstants.h b/apps/LoopThroughWithOpenGLCompositing/gl/renderer/GlRenderConstants.h index 17bf1ff..8cba3b0 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/renderer/GlRenderConstants.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/renderer/GlRenderConstants.h @@ -2,6 +2,7 @@ #include +constexpr GLuint kLayerInputTextureUnit = 0; constexpr GLuint kDecodedVideoTextureUnit = 1; constexpr GLuint kSourceHistoryTextureUnitBase = 2; constexpr GLuint kPackedVideoTextureUnit = 2; diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderTextureBindings.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderTextureBindings.cpp index 8b0ff2b..5d9a542 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderTextureBindings.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderTextureBindings.cpp @@ -117,7 +117,11 @@ void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const Run { const GLuint shaderTextureBase = ResolveShaderTextureBase(state, historyCap); - const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput"); + const GLint layerInputLocation = FindSamplerUniformLocation(program, "gLayerInput"); + if (layerInputLocation >= 0) + glUniform1i(layerInputLocation, static_cast(kLayerInputTextureUnit)); + + const GLint videoInputLocation = FindSamplerUniformLocation(program, "gVideoInput"); if (videoInputLocation >= 0) glUniform1i(videoInputLocation, static_cast(kDecodedVideoTextureUnit)); @@ -136,7 +140,7 @@ void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const Run if (state.feedback.enabled) { - const GLint feedbackSamplerLocation = glGetUniformLocation(program, "gFeedbackState"); + const GLint feedbackSamplerLocation = FindSamplerUniformLocation(program, "gFeedbackState"); if (feedbackSamplerLocation >= 0) glUniform1i(feedbackSamplerLocation, static_cast(ResolveFeedbackTextureUnit(state, historyCap))); } @@ -160,12 +164,14 @@ void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const Run ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan( const PassProgram& passProgram, GLuint layerInputTexture, + GLuint originalLayerInputTexture, const RuntimeRenderState& state, GLuint feedbackTexture, const std::vector& sourceHistoryTextures, const std::vector& temporalHistoryTextures) const { RuntimeTextureBindingPlan plan; + plan.bindings.push_back({ "originalLayerInput", "gLayerInput", originalLayerInputTexture, kLayerInputTextureUnit }); plan.bindings.push_back({ "layerInput", "gVideoInput", layerInputTexture, kDecodedVideoTextureUnit }); for (std::size_t index = 0; index < sourceHistoryTextures.size(); ++index) diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderTextureBindings.h b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderTextureBindings.h index 7eabdf7..1f6210a 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderTextureBindings.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderTextureBindings.h @@ -35,6 +35,7 @@ public: RuntimeTextureBindingPlan BuildLayerRuntimeBindingPlan( const PassProgram& passProgram, GLuint layerInputTexture, + GLuint originalLayerInputTexture, const RuntimeRenderState& state, GLuint feedbackTexture, const std::vector& sourceHistoryTextures, diff --git a/runtime/templates/shader_wrapper.slang.in b/runtime/templates/shader_wrapper.slang.in index 66ca7c6..f4def25 100644 --- a/runtime/templates/shader_wrapper.slang.in +++ b/runtime/templates/shader_wrapper.slang.in @@ -39,6 +39,7 @@ cbuffer GlobalParams {{PARAMETER_UNIFORMS}}}; Sampler2D gVideoInput; +Sampler2D gLayerInput; {{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{FEEDBACK_SAMPLER}}{{TEXTURE_SAMPLERS}} {{TEXT_SAMPLERS}} float4 sampleVideo(float2 tc) @@ -46,6 +47,11 @@ float4 sampleVideo(float2 tc) return gVideoInput.Sample(tc); } +float4 sampleLayerInput(float2 tc) +{ + return gLayerInput.Sample(tc); +} + float4 sampleSourceHistory(int framesAgo, float2 tc) { if (gSourceHistoryLength <= 0) diff --git a/shaders/feedback-highlight-accumulator/shader.json b/shaders/feedback-highlight-accumulator/shader.json index 52925ce..24c0e4c 100644 --- a/shaders/feedback-highlight-accumulator/shader.json +++ b/shaders/feedback-highlight-accumulator/shader.json @@ -1,18 +1,36 @@ { "id": "feedback-highlight-accumulator", "name": "Feedback Highlight Accumulator", - "description": "Demonstrates shader-local feedback by accumulating only the brightest-looking parts of the image over a timed window, then resetting and starting again. The highlight selection is an approximation based on a luminance threshold rather than an exact percentile reduction.", + "description": "Demonstrates shader-local feedback by storing a rolling 4-frame per-pixel highlight history in RGBA and displaying only the newest 3 frames. The highlight selection is an approximation based on a luminance threshold rather than an exact percentile reduction.", "category": "Feedback", - "entryPoint": "shadeVideo", + "entryPoint": "buildHistory", + "passes": [ + { + "id": "history", + "source": "shader.slang", + "entryPoint": "buildHistory", + "output": "historyBuffer" + }, + { + "id": "display", + "source": "shader.slang", + "entryPoint": "displayHistory", + "inputs": [ + "historyBuffer" + ], + "output": "layerOutput" + } + ], "feedback": { - "enabled": true + "enabled": true, + "writePass": "history" }, "parameters": [ { "id": "highlightThreshold", "label": "Highlight Threshold", "type": "float", - "default": 0.99, + "default": 0.92, "min": 0.5, "max": 1.5, "step": 0.001, @@ -30,23 +48,23 @@ }, { "id": "accumulateAmount", - "label": "Accumulate Amount", + "label": "Contribution Amount", "type": "float", - "default": 0.2, + "default": 0.6, "min": 0.0, "max": 2.0, "step": 0.001, - "description": "How much of each bright sample gets added into the running feedback image." + "description": "Strength of each frame's highlight contribution before it is shifted into the rolling history." }, { "id": "displayGain", "label": "Display Gain", "type": "float", - "default": 1.0, + "default": 2.5, "min": 0.0, - "max": 4.0, + "max": 8.0, "step": 0.01, - "description": "Brightness applied to the accumulated feedback when displayed." + "description": "Brightness applied to the sum of the most recent three stored highlight samples." }, { "id": "baseMix", @@ -56,17 +74,7 @@ "min": 0.0, "max": 1.0, "step": 0.01, - "description": "Amount of the live source image kept under the accumulation." - }, - { - "id": "resetSeconds", - "label": "Reset Seconds", - "type": "float", - "default": 10.0, - "min": 1.0, - "max": 60.0, - "step": 0.1, - "description": "Length of each accumulation window before the feedback resets." + "description": "Amount of the live source image kept under the recent highlight history display." } ] } diff --git a/shaders/feedback-highlight-accumulator/shader.slang b/shaders/feedback-highlight-accumulator/shader.slang index 9da5001..2110cbf 100644 --- a/shaders/feedback-highlight-accumulator/shader.slang +++ b/shaders/feedback-highlight-accumulator/shader.slang @@ -3,29 +3,45 @@ float luminance(float3 color) return dot(color, float3(0.2126, 0.7152, 0.0722)); } -float currentResetPhase(float timeSeconds, float resetInterval) +float4 buildHistory(ShaderContext context) { - float safeInterval = max(resetInterval, 0.001); - float cycleIndex = floor(timeSeconds / safeInterval); - return frac(cycleIndex * 0.5) >= 0.5 ? 1.0 : 0.0; -} - -float4 shadeVideo(ShaderContext context) -{ - float safeResetSeconds = max(resetSeconds, 0.001); - float phase = currentResetPhase(context.time, safeResetSeconds); - float4 previousFeedback = sampleFeedback(context.uv); - bool phaseMatches = context.feedbackAvailable > 0 && abs(previousFeedback.a - phase) < 0.25; - float3 previousAccumulation = phaseMatches ? previousFeedback.rgb : float3(0.0, 0.0, 0.0); + float4 previousHistory = context.feedbackAvailable > 0 + ? previousFeedback + : float4(0.0, 0.0, 0.0, 0.0); + // Highlight selection is always based on the live source image only. + // The stored feedback never feeds back into the threshold test. float currentLuma = luminance(context.sourceColor.rgb); float thresholdWidth = max(softness, 0.0001); float brightMask = smoothstep(highlightThreshold - thresholdWidth, highlightThreshold + thresholdWidth, currentLuma); - float3 currentContribution = context.sourceColor.rgb * brightMask * max(accumulateAmount, 0.0); - float3 nextAccumulation = saturate(previousAccumulation + currentContribution); + // Treat RGBA as a 4-slot rolling scalar history: + // R = current frame, G = 1 frame ago, B = 2 frames ago, A = 3 frames ago. + // Each frame shifts the history forward and drops the oldest sample. + float currentContribution = currentLuma * brightMask * max(accumulateAmount, 0.0); + float4 nextHistory = float4( + currentContribution, + previousHistory.r, + previousHistory.g, + previousHistory.b + ); - float3 displayColor = context.sourceColor.rgb * saturate(baseMix) + nextAccumulation * max(displayGain, 0.0); - return float4(saturate(displayColor), phase); + return saturate(nextHistory); +} + +float4 displayHistory(ShaderContext context) +{ + // In the display pass, context.sourceColor is the same-frame historyBuffer + // produced by buildHistory(). + float4 history = context.sourceColor; + float recentEnergy = history.r + history.g + history.b; + float3 originalColor = sampleLayerInput(context.uv).rgb; + float highlightBoost = recentEnergy * max(displayGain, 0.0); + float3 sourceHue = originalColor / max(max(originalColor.r, originalColor.g), max(originalColor.b, 0.0001)); + float3 displayColor = + originalColor + + sourceHue * highlightBoost * 1.5; + + return float4(saturate(displayColor), 1.0); }