Unified asset painter and added to package. Fixes and simplifes sprite rendering.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-17 13:00:04 +01:00
parent 2ff7e04ba4
commit 1575042870
4 changed files with 215 additions and 109 deletions

View File

@@ -0,0 +1,209 @@
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
/// A unified widget to display and cache Wolf3D assets.
class WolfAssetPainter extends StatefulWidget {
final Sprite? sprite;
final VgaImage? vgaImage;
final ui.Image? frame;
const WolfAssetPainter.sprite(this.sprite, {super.key})
: vgaImage = null,
frame = null;
const WolfAssetPainter.vga(this.vgaImage, {super.key})
: sprite = null,
frame = null;
const WolfAssetPainter.frame(this.frame, {super.key})
: sprite = null,
vgaImage = null;
@override
State<WolfAssetPainter> createState() => _WolfAssetPainterState();
}
class _WolfAssetPainterState extends State<WolfAssetPainter> {
ui.Image? _cachedImage;
// Tracks if we should dispose the image to free native memory
bool _ownsImage = false;
@override
void initState() {
super.initState();
_prepareImage();
}
@override
void didUpdateWidget(covariant WolfAssetPainter oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.sprite != oldWidget.sprite ||
widget.vgaImage != oldWidget.vgaImage ||
widget.frame != oldWidget.frame) {
_prepareImage();
}
}
@override
void dispose() {
if (_ownsImage) {
_cachedImage?.dispose();
}
super.dispose();
}
Future<void> _prepareImage() async {
// Clean up previous internally generated image
if (_ownsImage && _cachedImage != null) {
_cachedImage!.dispose();
_cachedImage = null;
}
// If a pre-rendered frame is passed in, just use it directly
if (widget.frame != null) {
_ownsImage = false;
if (mounted) {
setState(() => _cachedImage = widget.frame);
}
return;
}
ui.Image? newImage;
if (widget.sprite != null) {
newImage = await _buildSpriteImage(widget.sprite!);
} else if (widget.vgaImage != null) {
newImage = await _buildVgaImage(widget.vgaImage!);
}
if (mounted) {
setState(() {
_ownsImage = true;
_cachedImage = newImage;
});
} else {
// If the widget was unmounted while building, dispose the unused image
newImage?.dispose();
}
}
/// Converts a Sprite's 8-bit palette data to a 32-bit RGBA ui.Image
Future<ui.Image> _buildSpriteImage(Sprite sprite) {
final completer = Completer<ui.Image>();
final pixels = Uint8List(64 * 64 * 4); // 4 bytes per pixel (RGBA)
for (int x = 0; x < 64; x++) {
for (int y = 0; y < 64; y++) {
int colorByte = sprite.pixels[x * 64 + y];
int offset = (y * 64 + x) * 4; // Row-major layout for ui.decodeImage
if (colorByte != 255) {
// 255 is transparency
int abgr = ColorPalette.vga32Bit[colorByte];
pixels[offset] = abgr & 0xFF; // R
pixels[offset + 1] = (abgr >> 8) & 0xFF; // G
pixels[offset + 2] = (abgr >> 16) & 0xFF; // B
pixels[offset + 3] = (abgr >> 24) & 0xFF; // A
} else {
pixels[offset + 3] = 0; // Alpha 0
}
}
}
ui.decodeImageFromPixels(
pixels,
64,
64,
ui.PixelFormat.rgba8888,
(img) => completer.complete(img),
);
return completer.future;
}
/// Converts a VgaImage's planar 8-bit palette data to a 32-bit RGBA ui.Image
Future<ui.Image> _buildVgaImage(VgaImage image) {
final completer = Completer<ui.Image>();
final pixels = Uint8List(image.width * image.height * 4);
int planeWidth = image.width ~/ 4;
int planeSize = planeWidth * image.height;
for (int y = 0; y < image.height; y++) {
for (int x = 0; x < image.width; x++) {
int plane = x % 4;
int sx = x ~/ 4;
int colorByte =
image.pixels[(plane * planeSize) + (y * planeWidth) + sx];
int offset = (y * image.width + x) * 4;
if (colorByte != 255) {
int abgr = ColorPalette.vga32Bit[colorByte];
pixels[offset] = abgr & 0xFF; // R
pixels[offset + 1] = (abgr >> 8) & 0xFF; // G
pixels[offset + 2] = (abgr >> 16) & 0xFF; // B
pixels[offset + 3] = (abgr >> 24) & 0xFF; // A
} else {
pixels[offset + 3] = 0; // Alpha 0
}
}
}
ui.decodeImageFromPixels(
pixels,
image.width,
image.height,
ui.PixelFormat.rgba8888,
(img) => completer.complete(img),
);
return completer.future;
}
@override
Widget build(BuildContext context) {
if (_cachedImage == null) {
return const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white24,
strokeWidth: 2,
),
),
);
}
return CustomPaint(
painter: _ImagePainter(_cachedImage!),
child: const SizedBox.expand(),
);
}
}
class _ImagePainter extends CustomPainter {
final ui.Image image;
_ImagePainter(this.image);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..filterQuality = FilterQuality.none;
final srcRect = Rect.fromLTWH(
0,
0,
image.width.toDouble(),
image.height.toDouble(),
);
final dstRect = Rect.fromLTWH(0, 0, size.width, size.height);
// Draw the entire cached image in one pass
canvas.drawImageRect(image, srcRect, dstRect, paint);
}
@override
bool shouldRepaint(covariant _ImagePainter oldDelegate) =>
oldDelegate.image != image;
}

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_renderer/base_renderer.dart';
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
class WolfFlutterRenderer extends BaseWolfRenderer {
const WolfFlutterRenderer({
@@ -61,33 +62,8 @@ class _WolfFlutterRendererState
padding: const EdgeInsets.all(16.0),
child: AspectRatio(
aspectRatio: 4 / 3,
child: CustomPaint(painter: BufferPainter(_renderedFrame)),
child: WolfAssetPainter.frame(_renderedFrame),
),
);
}
}
class BufferPainter extends CustomPainter {
final ui.Image? frame;
BufferPainter(this.frame);
@override
void paint(Canvas canvas, Size size) {
if (frame == null) return;
final Paint paint = Paint()..filterQuality = FilterQuality.none;
final Rect srcRect = Rect.fromLTWH(
0,
0,
frame!.width.toDouble(),
frame!.height.toDouble(),
);
final Rect dstRect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawImageRect(frame!, srcRect, dstRect, paint);
}
@override
bool shouldRepaint(covariant BufferPainter oldDelegate) =>
oldDelegate.frame != frame;
}