From 6f7885a92430fbb899683ef5fb4964076547c90f Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Mon, 16 Mar 2026 13:47:38 +0100 Subject: [PATCH] Massively improved the ASCII renderer Signed-off-by: Hans Kokx --- .../lib/ascii_rasterizer.dart | 67 ++++++++++++------- .../lib/wolf_3d_ascii_renderer.dart | 66 ++++++++++++------ 2 files changed, 85 insertions(+), 48 deletions(-) diff --git a/packages/wolf_3d_renderer/lib/ascii_rasterizer.dart b/packages/wolf_3d_renderer/lib/ascii_rasterizer.dart index de5fcc6..bb51770 100644 --- a/packages/wolf_3d_renderer/lib/ascii_rasterizer.dart +++ b/packages/wolf_3d_renderer/lib/ascii_rasterizer.dart @@ -4,6 +4,34 @@ import 'package:flutter/material.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_engine/wolf_3d_engine.dart'; +class AsciiTheme { + /// The character ramp, ordered from most dense (index 0) to least dense (last index). + final String ramp; + + const AsciiTheme(this.ramp); + + /// Always returns the densest character (e.g., for walls, UI, floors) + String get solid => ramp[0]; + + /// Always returns the completely empty character (e.g., for pitch black darkness) + String get empty => ramp[ramp.length - 1]; + + /// Returns a character based on a 0.0 to 1.0 brightness scale. + /// 1.0 returns the [solid] character, 0.0 returns the [empty] character. + String getByBrightness(double brightness) { + double b = brightness.clamp(0.0, 1.0); + int index = ((1.0 - b) * (ramp.length - 1)).round(); + return ramp[index]; + } +} + +/// A collection of pre-defined character sets +abstract class AsciiThemes { + static const AsciiTheme blocks = AsciiTheme("█▓▒░ "); + static const AsciiTheme classic = AsciiTheme("@%#*+=-:. "); + static const AsciiTheme dense = AsciiTheme("█▇▆▅▄▃▂ "); +} + class ColoredChar { final String char; final Color color; @@ -11,7 +39,7 @@ class ColoredChar { } class AsciiRasterizer extends Rasterizer { - static const String _charset = "@%#*+=-:. "; + final AsciiTheme activeTheme = AsciiThemes.blocks; late List> _screen; late WolfEngine _engine; @@ -22,15 +50,11 @@ class AsciiRasterizer extends Rasterizer { double get aspectMultiplier => 0.6; // --- HELPER: Color Conversion --- - Color _vgaToColor(int vgaColor, {double brightnessBoost = 2.0}) { + Color _vgaToColor(int vgaColor) { int r = vgaColor & 0xFF; int g = (vgaColor >> 8) & 0xFF; int b = (vgaColor >> 16) & 0xFF; - r = (r * brightnessBoost).toInt().clamp(0, 255); - g = (g * brightnessBoost).toInt().clamp(0, 255); - b = (b * brightnessBoost).toInt().clamp(0, 255); - return Color.fromARGB(255, r, g, b); } @@ -53,9 +77,11 @@ class AsciiRasterizer extends Rasterizer { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if (y < viewHeight / 2) { - _screen[y][x] = ColoredChar(' ', ceilingColor); + // Fetch the solid character from the theme + _screen[y][x] = ColoredChar(activeTheme.solid, ceilingColor); } else if (y < viewHeight) { - _screen[y][x] = ColoredChar('.', floorColor); + // Fetch the solid character from the theme + _screen[y][x] = ColoredChar(activeTheme.solid, floorColor); } } } @@ -72,12 +98,8 @@ class AsciiRasterizer extends Rasterizer { double perpWallDist, int side, ) { - double brightness = (1.5 / (perpWallDist + 1.0)).clamp(0.0, 1.0); - String wallChar = - _charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp( - 0, - _charset.length - 1, - )]; + double brightness = (4.0 / (perpWallDist + 1.0)); + String wallChar = activeTheme.getByBrightness(brightness); for (int y = drawStart; y < drawEnd; y++) { double relativeY = @@ -111,12 +133,8 @@ class AsciiRasterizer extends Rasterizer { int texX, double transformY, ) { - double brightness = (1.5 / (transformY + 1.0)).clamp(0.0, 1.0); - String spriteChar = - _charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp( - 0, - _charset.length - 1, - )]; + double brightness = (4.0 / (transformY + 1.0)).clamp(0.0, 1.0); + String spriteChar = activeTheme.getByBrightness(brightness); for ( int y = math.max(0, drawStartY); @@ -161,7 +179,7 @@ class AsciiRasterizer extends Rasterizer { int drawY = startY + dy; if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) { _screen[drawY][drawX] = ColoredChar( - '@', + activeTheme.solid, _vgaToColor(ColorPalette.vga32Bit[colorByte]), ); } @@ -294,11 +312,8 @@ class AsciiRasterizer extends Rasterizer { if (colorByte != 255) { // Using '█' for UI to make it look solid _screen[drawY][drawX] = ColoredChar( - '█', - _vgaToColor( - ColorPalette.vga32Bit[colorByte], - brightnessBoost: 1.5, - ), + activeTheme.solid, + _vgaToColor(ColorPalette.vga32Bit[colorByte]), ); } } diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart index 97ae1a1..3a3b21b 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart @@ -104,29 +104,51 @@ class AsciiFrameWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return FittedBox( - fit: BoxFit.contain, - child: Container( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: frameData.map((row) { - return RichText( - text: TextSpan( - style: const TextStyle( - fontFamily: 'monospace', - height: 1.0, - letterSpacing: 2.0, + return AspectRatio( + aspectRatio: 4 / 3, + child: FittedBox( + fit: BoxFit.fill, + child: Container( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: frameData.map((row) { + List optimizedSpans = []; + if (row.isNotEmpty) { + Color currentColor = row[0].color; + StringBuffer currentSegment = StringBuffer(row[0].char); + + for (int i = 1; i < row.length; i++) { + if (row[i].color == currentColor) { + currentSegment.write(row[i].char); + } else { + optimizedSpans.add( + TextSpan( + text: currentSegment.toString(), + style: TextStyle(color: currentColor), + ), + ); + currentColor = row[i].color; + currentSegment = StringBuffer(row[i].char); + } + } + optimizedSpans.add( + TextSpan( + text: currentSegment.toString(), + style: TextStyle(color: currentColor), + ), + ); + } + + return RichText( + text: TextSpan( + style: const TextStyle(fontFamily: 'monospace', height: 1.0), + children: optimizedSpans, ), - children: row.map((cell) { - return TextSpan( - text: cell.char, - style: TextStyle(color: cell.color), - ); - }).toList(), - ), - ); - }).toList(), + ); + }).toList(), + ), ), ), );