feat: Add GLSL renderer and implement FPS overlay across rendering backends
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
182
packages/wolf_3d_renderer/lib/wolf_3d_glsl_renderer.dart
Normal file
182
packages/wolf_3d_renderer/lib/wolf_3d_glsl_renderer.dart
Normal file
@@ -0,0 +1,182 @@
|
||||
/// Flutter widget that applies a GLSL post-process over Wolf3D frames.
|
||||
library;
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
|
||||
import 'package:wolf_3d_renderer/base_renderer.dart';
|
||||
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
|
||||
|
||||
/// Displays software-rendered frames through a GLSL post-processing pass.
|
||||
class WolfGlslRenderer extends BaseWolfRenderer {
|
||||
/// 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.onUnavailable,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<WolfGlslRenderer> createState() => _WolfGlslRendererState();
|
||||
}
|
||||
|
||||
class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
||||
static const int _renderWidth = 320;
|
||||
static const int _renderHeight = 200;
|
||||
|
||||
final SoftwareRenderer _renderer = SoftwareRenderer();
|
||||
|
||||
ui.Image? _renderedFrame;
|
||||
ui.FragmentProgram? _shaderProgram;
|
||||
ui.FragmentShader? _shader;
|
||||
bool _isRendering = false;
|
||||
bool _isShaderUnavailable = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.engine.frameBuffer.width != _renderWidth ||
|
||||
widget.engine.frameBuffer.height != _renderHeight) {
|
||||
widget.engine.setFrameBuffer(_renderWidth, _renderHeight);
|
||||
}
|
||||
_loadShader();
|
||||
}
|
||||
|
||||
@override
|
||||
Color get scaffoldColor => widget.engine.difficulty == null
|
||||
? _colorFromRgb(widget.engine.menuBackgroundRgb)
|
||||
: const Color.fromARGB(255, 4, 64, 64);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_renderedFrame?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void performRender() {
|
||||
if (_isRendering) {
|
||||
return;
|
||||
}
|
||||
_isRendering = true;
|
||||
|
||||
final FrameBuffer frameBuffer = widget.engine.frameBuffer;
|
||||
_renderer.render(widget.engine);
|
||||
|
||||
ui.decodeImageFromPixels(
|
||||
frameBuffer.pixels.buffer.asUint8List(),
|
||||
frameBuffer.width,
|
||||
frameBuffer.height,
|
||||
ui.PixelFormat.rgba8888,
|
||||
(ui.Image image) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_renderedFrame?.dispose();
|
||||
_renderedFrame = image;
|
||||
});
|
||||
} else {
|
||||
image.dispose();
|
||||
}
|
||||
_isRendering = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildViewport(BuildContext context) {
|
||||
if (_renderedFrame == null) {
|
||||
return const CircularProgressIndicator(color: Colors.white24);
|
||||
}
|
||||
|
||||
if (_isShaderUnavailable || _shader == null) {
|
||||
// Keep frames visible even if GLSL initialization failed.
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 4 / 3,
|
||||
child: WolfAssetPainter.frame(_renderedFrame),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 4 / 3,
|
||||
child: CustomPaint(
|
||||
painter: _GlslFramePainter(
|
||||
frame: _renderedFrame!,
|
||||
shader: _shader!,
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadShader() async {
|
||||
try {
|
||||
final ui.FragmentProgram program = await ui.FragmentProgram.fromAsset(
|
||||
'packages/wolf_3d_renderer/shaders/wolf_world.frag',
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_shaderProgram = program;
|
||||
_shader = _shaderProgram!.fragmentShader();
|
||||
_isShaderUnavailable = false;
|
||||
});
|
||||
} catch (_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isShaderUnavailable = true;
|
||||
});
|
||||
widget.onUnavailable?.call();
|
||||
}
|
||||
}
|
||||
|
||||
Color _colorFromRgb(int rgb) {
|
||||
return Color(0xFF000000 | (rgb & 0x00FFFFFF));
|
||||
}
|
||||
}
|
||||
|
||||
class _GlslFramePainter extends CustomPainter {
|
||||
final ui.Image frame;
|
||||
final ui.FragmentShader shader;
|
||||
|
||||
_GlslFramePainter({
|
||||
required this.frame,
|
||||
required this.shader,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final double texelX = frame.width > 0 ? 1.0 / frame.width : 1.0;
|
||||
final double texelY = frame.height > 0 ? 1.0 / frame.height : 1.0;
|
||||
shader
|
||||
..setFloat(0, size.width)
|
||||
..setFloat(1, size.height)
|
||||
..setFloat(2, texelX)
|
||||
..setFloat(3, texelY)
|
||||
..setImageSampler(0, frame);
|
||||
|
||||
final Paint paint = Paint()
|
||||
..shader = shader
|
||||
..filterQuality = FilterQuality.none;
|
||||
|
||||
canvas.drawRect(Offset.zero & size, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _GlslFramePainter oldDelegate) {
|
||||
return oldDelegate.frame != frame || oldDelegate.shader != shader;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ dev_dependencies:
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
shaders:
|
||||
- shaders/wolf_world.frag
|
||||
|
||||
# To add assets to your package, add an assets section, like this:
|
||||
# assets:
|
||||
|
||||
43
packages/wolf_3d_renderer/shaders/wolf_world.frag
Normal file
43
packages/wolf_3d_renderer/shaders/wolf_world.frag
Normal file
@@ -0,0 +1,43 @@
|
||||
#include <flutter/runtime_effect.glsl>
|
||||
|
||||
uniform vec2 uResolution;
|
||||
uniform vec2 uTexel;
|
||||
uniform sampler2D uTexture;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
float luma(vec3 color) {
|
||||
return dot(color, vec3(0.299, 0.587, 0.114));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = FlutterFragCoord().xy / uResolution;
|
||||
vec4 centerSample = texture(uTexture, uv);
|
||||
|
||||
vec3 sampleN = texture(uTexture, uv + vec2(0.0, -uTexel.y)).rgb;
|
||||
vec3 sampleS = texture(uTexture, uv + vec2(0.0, uTexel.y)).rgb;
|
||||
vec3 sampleE = texture(uTexture, uv + vec2(uTexel.x, 0.0)).rgb;
|
||||
vec3 sampleW = texture(uTexture, uv + vec2(-uTexel.x, 0.0)).rgb;
|
||||
|
||||
float lumaCenter = luma(centerSample.rgb);
|
||||
float lumaMin = min(
|
||||
lumaCenter,
|
||||
min(min(luma(sampleN), luma(sampleS)), min(luma(sampleE), luma(sampleW)))
|
||||
);
|
||||
float lumaMax = max(
|
||||
lumaCenter,
|
||||
max(max(luma(sampleN), luma(sampleS)), max(luma(sampleE), luma(sampleW)))
|
||||
);
|
||||
|
||||
float edgeSpan = max(lumaMax - lumaMin, 0.0001);
|
||||
float edgeAmount = smoothstep(0.03, 0.18, edgeSpan);
|
||||
vec3 neighborhoodAvg = (sampleN + sampleS + sampleE + sampleW) * 0.25;
|
||||
vec3 aaColor = mix(centerSample.rgb, neighborhoodAvg, edgeAmount * 0.45);
|
||||
|
||||
vec2 centered = uv - 0.5;
|
||||
float vignette = 1.0 - dot(centered, centered) * 0.35;
|
||||
vignette = clamp(vignette, 0.75, 1.0);
|
||||
|
||||
vec3 color = aaColor * vignette;
|
||||
fragColor = vec4(color, centerSample.a);
|
||||
}
|
||||
Reference in New Issue
Block a user