Massively improved the ASCII renderer

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-16 13:47:38 +01:00
parent 6d74208ff4
commit 6f7885a924
2 changed files with 85 additions and 48 deletions

View File

@@ -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<List<ColoredChar>> _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]),
);
}
}

View File

@@ -104,31 +104,53 @@ class AsciiFrameWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FittedBox(
fit: BoxFit.contain,
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<TextSpan> 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,
letterSpacing: 2.0,
),
children: row.map((cell) {
return TextSpan(
text: cell.char,
style: TextStyle(color: cell.color),
);
}).toList(),
style: const TextStyle(fontFamily: 'monospace', height: 1.0),
children: optimizedSpans,
),
);
}).toList(),
),
),
),
);
}
}