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:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"cmake.ignoreCMakeListsMissing": true
|
"cmake.ignoreCMakeListsMissing": true,
|
||||||
|
"chat.tools.terminal.autoApprove": {
|
||||||
|
"flutter": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,7 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
late final WolfEngine _engine;
|
late final WolfEngine _engine;
|
||||||
RendererMode _rendererMode = RendererMode.hardware;
|
RendererMode _rendererMode = RendererMode.hardware;
|
||||||
AsciiTheme _asciiTheme = AsciiThemes.blocks;
|
AsciiTheme _asciiTheme = AsciiThemes.blocks;
|
||||||
|
bool _glslEffectsEnabled = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -109,7 +110,7 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
top: 16,
|
top: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
child: Text(
|
child: Text(
|
||||||
'<${widget.wolf3d.input.rendererToggleKeyLabel}> ${_rendererMode.name}${_rendererMode == RendererMode.ascii ? ' <${widget.wolf3d.input.asciiThemeCycleKeyLabel}> ${_asciiTheme.name}' : ''} <${widget.wolf3d.input.fpsToggleKeyLabel}> FPS ${_engine.showFpsCounter ? 'On' : 'Off'}',
|
'<${widget.wolf3d.input.rendererToggleKeyLabel}> ${_rendererMode.name}${_activeModeOverlayHint()} <${widget.wolf3d.input.fpsToggleKeyLabel}> FPS ${_engine.showFpsCounter ? 'On' : 'Off'}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.5),
|
color: Colors.white.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
@@ -142,6 +143,7 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
case RendererMode.hardware:
|
case RendererMode.hardware:
|
||||||
return WolfGlslRenderer(
|
return WolfGlslRenderer(
|
||||||
engine: _engine,
|
engine: _engine,
|
||||||
|
effectsEnabled: _glslEffectsEnabled,
|
||||||
onKeyEvent: _handleRendererKeyEvent,
|
onKeyEvent: _handleRendererKeyEvent,
|
||||||
onUnavailable: _onGlslUnavailable,
|
onUnavailable: _onGlslUnavailable,
|
||||||
);
|
);
|
||||||
@@ -164,9 +166,23 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.logicalKey == widget.wolf3d.input.asciiThemeCycleKey) {
|
if (event.logicalKey == widget.wolf3d.input.asciiThemeCycleKey) {
|
||||||
|
if (_rendererMode == RendererMode.ascii) {
|
||||||
setState(_cycleAsciiTheme);
|
setState(_cycleAsciiTheme);
|
||||||
|
} else if (_rendererMode == RendererMode.hardware) {
|
||||||
|
setState(_toggleGlslEffects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _activeModeOverlayHint() {
|
||||||
|
if (_rendererMode == RendererMode.ascii) {
|
||||||
|
return ' <${widget.wolf3d.input.asciiThemeCycleKeyLabel}> ${_asciiTheme.name}';
|
||||||
|
}
|
||||||
|
if (_rendererMode == RendererMode.hardware) {
|
||||||
|
return ' <${widget.wolf3d.input.asciiThemeCycleKeyLabel}> Effects ${_glslEffectsEnabled ? 'on' : 'off'}';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
void _cycleRendererMode() {
|
void _cycleRendererMode() {
|
||||||
switch (_rendererMode) {
|
switch (_rendererMode) {
|
||||||
@@ -198,4 +214,8 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
void _cycleAsciiTheme() {
|
void _cycleAsciiTheme() {
|
||||||
_asciiTheme = AsciiThemes.nextOf(_asciiTheme);
|
_asciiTheme = AsciiThemes.nextOf(_asciiTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _toggleGlslEffects() {
|
||||||
|
_glslEffectsEnabled = !_glslEffectsEnabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,16 @@ import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
|
|||||||
|
|
||||||
/// Displays software-rendered frames through a GLSL post-processing pass.
|
/// Displays software-rendered frames through a GLSL post-processing pass.
|
||||||
class WolfGlslRenderer extends BaseWolfRenderer {
|
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.
|
/// Callback when shader loading fails and software fallback should be used.
|
||||||
final VoidCallback? onUnavailable;
|
final VoidCallback? onUnavailable;
|
||||||
|
|
||||||
/// Creates a GLSL renderer bound to [engine].
|
/// Creates a GLSL renderer bound to [engine].
|
||||||
const WolfGlslRenderer({
|
const WolfGlslRenderer({
|
||||||
required super.engine,
|
required super.engine,
|
||||||
|
this.effectsEnabled = false,
|
||||||
super.onKeyEvent,
|
super.onKeyEvent,
|
||||||
this.onUnavailable,
|
this.onUnavailable,
|
||||||
super.key,
|
super.key,
|
||||||
@@ -113,6 +117,8 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
|||||||
painter: _GlslFramePainter(
|
painter: _GlslFramePainter(
|
||||||
frame: _renderedFrame!,
|
frame: _renderedFrame!,
|
||||||
shader: _shader!,
|
shader: _shader!,
|
||||||
|
effectsEnabled: widget.effectsEnabled,
|
||||||
|
elapsedSeconds: widget.engine.timeAliveMs / 1000.0,
|
||||||
),
|
),
|
||||||
child: const SizedBox.expand(),
|
child: const SizedBox.expand(),
|
||||||
),
|
),
|
||||||
@@ -152,10 +158,14 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
|||||||
class _GlslFramePainter extends CustomPainter {
|
class _GlslFramePainter extends CustomPainter {
|
||||||
final ui.Image frame;
|
final ui.Image frame;
|
||||||
final ui.FragmentShader shader;
|
final ui.FragmentShader shader;
|
||||||
|
final bool effectsEnabled;
|
||||||
|
final double elapsedSeconds;
|
||||||
|
|
||||||
_GlslFramePainter({
|
_GlslFramePainter({
|
||||||
required this.frame,
|
required this.frame,
|
||||||
required this.shader,
|
required this.shader,
|
||||||
|
required this.effectsEnabled,
|
||||||
|
required this.elapsedSeconds,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -167,6 +177,8 @@ class _GlslFramePainter extends CustomPainter {
|
|||||||
..setFloat(1, size.height)
|
..setFloat(1, size.height)
|
||||||
..setFloat(2, texelX)
|
..setFloat(2, texelX)
|
||||||
..setFloat(3, texelY)
|
..setFloat(3, texelY)
|
||||||
|
..setFloat(4, effectsEnabled ? 1.0 : 0.0)
|
||||||
|
..setFloat(5, elapsedSeconds)
|
||||||
..setImageSampler(0, frame);
|
..setImageSampler(0, frame);
|
||||||
|
|
||||||
final Paint paint = Paint()
|
final Paint paint = Paint()
|
||||||
@@ -178,6 +190,9 @@ class _GlslFramePainter extends CustomPainter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(covariant _GlslFramePainter oldDelegate) {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
uniform vec2 uResolution;
|
uniform vec2 uResolution;
|
||||||
// One source-texel step in UV space: (1/width, 1/height).
|
// One source-texel step in UV space: (1/width, 1/height).
|
||||||
uniform vec2 uTexel;
|
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.
|
// Source frame produced by the software renderer.
|
||||||
uniform sampler2D uTexture;
|
uniform sampler2D uTexture;
|
||||||
|
|
||||||
@@ -17,6 +21,30 @@ float luma(vec3 color) {
|
|||||||
void main() {
|
void main() {
|
||||||
// Convert fragment coordinates to normalized UV coordinates.
|
// Convert fragment coordinates to normalized UV coordinates.
|
||||||
vec2 uv = FlutterFragCoord().xy / uResolution;
|
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.
|
// Read the base color from the source frame.
|
||||||
vec4 centerSample = texture(uTexture, uv);
|
vec4 centerSample = texture(uTexture, uv);
|
||||||
|
|
||||||
@@ -47,5 +75,24 @@ void main() {
|
|||||||
vec3 aaColor = mix(centerSample.rgb, neighborhoodAvg, edgeAmount * 0.45);
|
vec3 aaColor = mix(centerSample.rgb, neighborhoodAvg, edgeAmount * 0.45);
|
||||||
|
|
||||||
// Preserve source alpha and output the anti-aliased color.
|
// 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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user