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:
209
packages/wolf_3d_renderer/lib/wolf_3d_asset_painter.dart
Normal file
209
packages/wolf_3d_renderer/lib/wolf_3d_asset_painter.dart
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user