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:
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
import 'package:wolf_3d_flutter/wolf_3d.dart';
|
import 'package:wolf_3d_flutter/wolf_3d.dart';
|
||||||
|
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
|
||||||
|
|
||||||
class SpriteGallery extends StatelessWidget {
|
class SpriteGallery extends StatelessWidget {
|
||||||
final List<Sprite> sprites;
|
final List<Sprite> sprites;
|
||||||
@@ -53,10 +54,7 @@ class SpriteGallery extends StatelessWidget {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CustomPaint(
|
child: WolfAssetPainter.sprite(sprites[index]),
|
||||||
painter: SingleSpritePainter(sprite: sprites[index]),
|
|
||||||
size: const Size(64, 64),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -65,28 +63,3 @@ class SpriteGallery extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SingleSpritePainter extends CustomPainter {
|
|
||||||
final Sprite sprite;
|
|
||||||
SingleSpritePainter({required this.sprite});
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
double pixelSize = size.width / 64;
|
|
||||||
for (int x = 0; x < 64; x++) {
|
|
||||||
for (int y = 0; y < 64; y++) {
|
|
||||||
int colorByte = sprite.pixels[x * 64 + y];
|
|
||||||
if (colorByte != 255) {
|
|
||||||
// Skip transparency
|
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(x * pixelSize, y * pixelSize, pixelSize, pixelSize),
|
|
||||||
Paint()..color = Color(ColorPalette.vga32Bit[colorByte]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
|
||||||
|
|
||||||
class VgaGallery extends StatelessWidget {
|
class VgaGallery extends StatelessWidget {
|
||||||
final List<VgaImage> images;
|
final List<VgaImage> images;
|
||||||
@@ -30,14 +31,7 @@ class VgaGallery extends StatelessWidget {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: CustomPaint(
|
child: WolfAssetPainter.vga(images[index]),
|
||||||
painter: VgaPainter(image: images[index]),
|
|
||||||
// Scale it up so tiny fonts are readable
|
|
||||||
size: Size(
|
|
||||||
images[index].width * 2.0,
|
|
||||||
images[index].height * 2.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -47,49 +41,3 @@ class VgaGallery extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VgaPainter extends CustomPainter {
|
|
||||||
final VgaImage image;
|
|
||||||
VgaPainter({required this.image});
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
int planeWidth = image.width ~/ 4;
|
|
||||||
int planeSize = planeWidth * image.height;
|
|
||||||
|
|
||||||
double pixelW = size.width / image.width;
|
|
||||||
double pixelH = size.height / image.height;
|
|
||||||
final Paint paint = Paint()..isAntiAlias = false;
|
|
||||||
|
|
||||||
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];
|
|
||||||
|
|
||||||
if (colorByte != 255) {
|
|
||||||
int abgr = ColorPalette.vga32Bit[colorByte];
|
|
||||||
|
|
||||||
// Extract the bytes
|
|
||||||
int r = abgr & 0xFF;
|
|
||||||
int g = (abgr >> 8) & 0xFF;
|
|
||||||
int b = (abgr >> 16) & 0xFF;
|
|
||||||
int a = (abgr >> 24) & 0xFF;
|
|
||||||
|
|
||||||
// Repack them as ARGB for Flutter's Color object
|
|
||||||
int argb = (a << 24) | (r << 16) | (g << 8) | b;
|
|
||||||
|
|
||||||
paint.color = Color(argb);
|
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(x * pixelW, y * pixelH, pixelW + 0.5, pixelH + 0.5),
|
|
||||||
paint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
||||||
}
|
|
||||||
|
|||||||
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_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
import 'package:wolf_3d_renderer/base_renderer.dart';
|
import 'package:wolf_3d_renderer/base_renderer.dart';
|
||||||
|
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
|
||||||
|
|
||||||
class WolfFlutterRenderer extends BaseWolfRenderer {
|
class WolfFlutterRenderer extends BaseWolfRenderer {
|
||||||
const WolfFlutterRenderer({
|
const WolfFlutterRenderer({
|
||||||
@@ -61,33 +62,8 @@ class _WolfFlutterRendererState
|
|||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 4 / 3,
|
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