feat: Add GLSL effects toggle and enhance shader for CRT-like post-processing

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-20 11:03:33 +01:00
parent cbbcd3223a
commit abca679a99
4 changed files with 90 additions and 5 deletions

View File

@@ -11,12 +11,16 @@ import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
/// Displays software-rendered frames through a GLSL post-processing pass.
class WolfGlslRenderer extends BaseWolfRenderer {
/// Whether CRT-like post effects are enabled in the shader pass.
final bool effectsEnabled;
/// Callback when shader loading fails and software fallback should be used.
final VoidCallback? onUnavailable;
/// Creates a GLSL renderer bound to [engine].
const WolfGlslRenderer({
required super.engine,
this.effectsEnabled = false,
super.onKeyEvent,
this.onUnavailable,
super.key,
@@ -113,6 +117,8 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
painter: _GlslFramePainter(
frame: _renderedFrame!,
shader: _shader!,
effectsEnabled: widget.effectsEnabled,
elapsedSeconds: widget.engine.timeAliveMs / 1000.0,
),
child: const SizedBox.expand(),
),
@@ -152,10 +158,14 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
class _GlslFramePainter extends CustomPainter {
final ui.Image frame;
final ui.FragmentShader shader;
final bool effectsEnabled;
final double elapsedSeconds;
_GlslFramePainter({
required this.frame,
required this.shader,
required this.effectsEnabled,
required this.elapsedSeconds,
});
@override
@@ -167,6 +177,8 @@ class _GlslFramePainter extends CustomPainter {
..setFloat(1, size.height)
..setFloat(2, texelX)
..setFloat(3, texelY)
..setFloat(4, effectsEnabled ? 1.0 : 0.0)
..setFloat(5, elapsedSeconds)
..setImageSampler(0, frame);
final Paint paint = Paint()
@@ -178,6 +190,9 @@ class _GlslFramePainter extends CustomPainter {
@override
bool shouldRepaint(covariant _GlslFramePainter oldDelegate) {
return oldDelegate.frame != frame || oldDelegate.shader != shader;
return oldDelegate.frame != frame ||
oldDelegate.shader != shader ||
oldDelegate.effectsEnabled != effectsEnabled ||
oldDelegate.elapsedSeconds != elapsedSeconds;
}
}

View File

@@ -4,6 +4,10 @@
uniform vec2 uResolution;
// One source-texel step in UV space: (1/width, 1/height).
uniform vec2 uTexel;
// 1.0 enables CRT post-process effects, 0.0 keeps only base AA.
uniform float uEffectsEnabled;
// Engine time in seconds used to animate scanline travel.
uniform float uTime;
// Source frame produced by the software renderer.
uniform sampler2D uTexture;
@@ -17,6 +21,30 @@ float luma(vec3 color) {
void main() {
// Convert fragment coordinates to normalized UV coordinates.
vec2 uv = FlutterFragCoord().xy / uResolution;
if (uEffectsEnabled > 0.5) {
// Barrel-like warp to emulate curved CRT glass.
vec2 centered = uv * 2.0 - 1.0;
float radius2 = dot(centered, centered);
centered *= 1.0 + radius2 * 0.045;
uv = centered * 0.5 + 0.5;
// Fill outside warped bounds with a subtle gray plastic TV bezel.
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
vec2 clampedUv = clamp(uv, 0.0, 1.0);
vec2 edgeDelta = abs(uv - clampedUv);
float overflow = max(edgeDelta.x, edgeDelta.y);
float verticalShade = 0.96 + 0.06 * (1.0 - (FlutterFragCoord().y / uResolution.y));
float depthShade = 1.0 - smoothstep(0.0, 0.06, overflow) * 0.18;
float grain = sin(FlutterFragCoord().x * 0.21 + FlutterFragCoord().y * 0.11) * 0.01;
vec3 bezelColor = vec3(0.46, 0.46, 0.44) * verticalShade * depthShade + vec3(grain);
fragColor = vec4(bezelColor, 1.0);
return;
}
}
// Read the base color from the source frame.
vec4 centerSample = texture(uTexture, uv);
@@ -47,5 +75,24 @@ void main() {
vec3 aaColor = mix(centerSample.rgb, neighborhoodAvg, edgeAmount * 0.45);
// Preserve source alpha and output the anti-aliased color.
fragColor = vec4(aaColor, centerSample.a);
vec3 outColor = aaColor;
if (uEffectsEnabled > 0.5) {
// Horizontal scanline modulation.
float scanlineBand = 0.88 + 0.12 * sin(uv.y * uResolution.y * 3.14159265);
// Slow bright line crawling down the screen.
float sweepPos = fract(uTime * 0.08);
float sweepBand = 1.0 + 0.16 * exp(-pow((uv.y - sweepPos) * 120.0, 2.0));
// Slight center brightening and edge falloff (CRT phosphor + lens feel).
vec2 centeredUv = uv * 2.0 - 1.0;
float vignette = smoothstep(1.15, 0.25, length(centeredUv));
float centerLift = 1.0 + 0.08 * (1.0 - length(centeredUv));
outColor *= scanlineBand * sweepBand * centerLift;
outColor *= mix(0.62, 1.0, vignette);
}
fragColor = vec4(outColor, centerSample.a);
}