Refactor rasterizer classes to centralize HUD drawing and pixel decoding

- Introduced `decodePixel` method in `VgaImage` to handle pixel mapping for planar VGA images.
- Updated `AsciiRasterizer`, `SixelRasterizer`, and `SoftwareRasterizer` to utilize the new `decodePixel` method for improved clarity and reduced code duplication.
- Centralized HUD drawing logic in the `Rasterizer` base class with `drawStandardVgaHud` method, allowing subclasses to easily implement HUD rendering.
- Removed redundant HUD drawing methods from individual rasterizers, streamlining the codebase.
- Enhanced documentation for clarity on methods and their purposes.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-18 17:25:20 +01:00
parent be03bd45c8
commit d93f467163
5 changed files with 217 additions and 373 deletions

View File

@@ -24,4 +24,18 @@ class VgaImage {
required this.height,
required this.pixels,
});
/// Returns the palette index at `[srcX, srcY]` for planar VGA image data.
///
/// Callers are expected to provide coordinates already clamped to the image
/// bounds. The Wolf3D VGA format stores image bytes in 4 interleaved planes;
/// this helper centralizes that mapping so rasterizers do not duplicate it.
int decodePixel(int srcX, int srcY) {
final int planeWidth = width ~/ 4;
final int planeSize = planeWidth * height;
final int plane = srcX % 4;
final int sx = srcX ~/ 4;
final int index = (plane * planeSize) + (srcY * planeWidth) + sx;
return pixels[index];
}
}

View File

@@ -78,6 +78,8 @@ enum AsciiRasterizerMode {
terminalGrid,
}
/// Text-mode rasterizer that can render to ANSI escape output or a Flutter
/// grid model of colored characters.
class AsciiRasterizer extends CliRasterizer<dynamic> {
static const double _targetAspectRatio = 4 / 3;
static const int _terminalBackdropPaletteIndex = 153;
@@ -106,7 +108,6 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
late List<List<ColoredChar>> _screen;
late List<List<int>> _scenePixels;
late WolfEngine _engine;
@override
final double aspectMultiplier;
@@ -151,8 +152,8 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
// Intercept the base render call to initialize our text grid
@override
/// Initializes the character grid before running the shared render pipeline.
dynamic render(WolfEngine engine) {
_engine = engine;
_screen = List.generate(
engine.frameBuffer.height,
(_) => List.filled(
@@ -213,9 +214,7 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
double brightness = calculateDepthBrightness(perpWallDist);
for (int y = drawStart; y < drawEnd; y++) {
double relativeY =
(y - (-columnHeight ~/ 2 + projectionViewHeight ~/ 2)) / columnHeight;
int texY = (relativeY * 64).toInt().clamp(0, 63);
int texY = wallTexY(y, columnHeight);
int colorByte = texture.pixels[texX * 64 + texY];
int pixelColor = ColorPalette.vga32Bit[colorByte]; // Raw int
@@ -251,8 +250,7 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
y < math.min(projectionViewHeight, drawEndY);
y++
) {
double relativeY = (y - drawStartY) / spriteHeight;
int texY = (relativeY * 64).toInt().clamp(0, 63);
int texY = spriteTexY(y, drawStartY, spriteHeight);
int colorByte = texture.pixels[texX * 64 + texY];
if (colorByte != 255) {
@@ -285,12 +283,11 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
);
Sprite weaponSprite = engine.data.sprites[spriteIndex];
int weaponWidth = (projectionWidth * 0.5).toInt();
int weaponHeight = ((projectionViewHeight * 0.8)).toInt();
int startX =
projectionOffsetX + (projectionWidth ~/ 2) - (weaponWidth ~/ 2);
int startY =
final bounds = weaponScreenBounds(engine);
final int weaponWidth = bounds.weaponWidth;
final int weaponHeight = (projectionViewHeight * 0.8).toInt();
final int startX = bounds.startX;
final int startY =
projectionViewHeight -
weaponHeight +
(engine.player.weaponAnimOffset * (_usesTerminalLayout ? 2 : 1) ~/ 4);
@@ -925,87 +922,13 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
}
void _drawFullVgaHud(WolfEngine engine) {
int statusBarIndex = engine.data.vgaImages.indexWhere(
(img) => img.width == 320 && img.height == 40,
);
if (statusBarIndex == -1) return;
// 1. Draw Background
_blitVgaImageAscii(engine.data.vgaImages[statusBarIndex], 0, 160);
// 2. Draw Stats
_drawNumberAscii(1, 32, 176, engine.data.vgaImages); // Floor
_drawNumberAscii(
engine.player.score,
96,
176,
engine.data.vgaImages,
); // Score
_drawNumberAscii(3, 120, 176, engine.data.vgaImages); // Lives
_drawNumberAscii(
engine.player.health,
192,
176,
engine.data.vgaImages,
); // Health
_drawNumberAscii(
engine.player.ammo,
232,
176,
engine.data.vgaImages,
); // Ammo
// 3. Draw BJ's Face & Current Weapon
_drawFaceAscii(engine);
_drawWeaponIconAscii(engine);
drawStandardVgaHud(engine);
}
void _drawNumberAscii(
int value,
int rightAlignX,
int startY,
List<VgaImage> vgaImages,
) {
const int zeroIndex = 96;
String numStr = value.toString();
int currentX = rightAlignX - (numStr.length * 8);
for (int i = 0; i < numStr.length; i++) {
int digit = int.parse(numStr[i]);
if (zeroIndex + digit < vgaImages.length) {
_blitVgaImageAscii(vgaImages[zeroIndex + digit], currentX, startY);
}
currentX += 8;
}
}
void _drawFaceAscii(WolfEngine engine) {
int health = engine.player.health;
int faceIndex;
if (health <= 0) {
faceIndex = 127;
} else {
int healthTier = ((100 - health) ~/ 16).clamp(0, 6);
faceIndex = 106 + (healthTier * 3);
}
if (faceIndex < engine.data.vgaImages.length) {
_blitVgaImageAscii(engine.data.vgaImages[faceIndex], 136, 164);
}
}
void _drawWeaponIconAscii(WolfEngine engine) {
int weaponIndex = 89;
if (engine.player.hasChainGun) {
weaponIndex = 91;
} else if (engine.player.hasMachineGun) {
weaponIndex = 90;
}
if (weaponIndex < engine.data.vgaImages.length) {
_blitVgaImageAscii(engine.data.vgaImages[weaponIndex], 256, 164);
}
@override
/// Blits a VGA image into ASCII HUD space (mapped/scaled from 320x200).
void blitHudVgaImage(VgaImage image, int startX320, int startY200) {
_blitVgaImageAscii(image, startX320, startY200);
}
/// Helper to fill a rectangular area with a specific char and background color
@@ -1023,7 +946,7 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
@override
dynamic finalizeFrame() {
if (_engine.difficulty != null && _engine.player.damageFlash > 0.0) {
if (engine.difficulty != null && engine.player.damageFlash > 0.0) {
if (_usesTerminalLayout) {
_applyDamageFlashToScene();
} else {
@@ -1040,8 +963,6 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
// --- PRIVATE HUD DRAWING HELPERS ---
void _blitVgaImageAscii(VgaImage image, int startX_320, int startY_200) {
int planeWidth = image.width ~/ 4;
int planeSize = planeWidth * image.height;
int maxDrawHeight = _usesTerminalLayout ? _terminalPixelHeight : height;
int maxDrawWidth = _usesTerminalLayout ? _viewportRightX : width;
@@ -1068,11 +989,7 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
int srcX = (dx / scaleX).toInt().clamp(0, image.width - 1);
int srcY = (dy / scaleY).toInt().clamp(0, image.height - 1);
int plane = srcX % 4;
int sx = srcX ~/ 4;
int index = (plane * planeSize) + (srcY * planeWidth) + sx;
int colorByte = image.pixels[index];
int colorByte = image.decodePixel(srcX, srcY);
if (colorByte != 255) {
if (_usesTerminalLayout) {
_scenePixels[drawY][drawX] = ColorPalette.vga32Bit[colorByte];
@@ -1151,7 +1068,7 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
}
int _applyDamageFlashToColor(int color) {
double intensity = _engine.player.damageFlash;
double intensity = engine.player.damageFlash;
int redBoost = (150 * intensity).toInt();
double colorDrop = 1.0 - (0.5 * intensity);
@@ -1253,34 +1170,6 @@ class AsciiRasterizer extends CliRasterizer<dynamic> {
}
int _rgbToPaletteColor(int rgb) {
return ColorPalette.vga32Bit[_rgbToPaletteIndex(rgb)];
}
int _rgbToPaletteIndex(int rgb) {
final int targetR = (rgb >> 16) & 0xFF;
final int targetG = (rgb >> 8) & 0xFF;
final int targetB = rgb & 0xFF;
int bestIndex = 0;
int bestDistance = 1 << 30;
for (int i = 0; i < 256; i++) {
final int color = ColorPalette.vga32Bit[i];
final int r = color & 0xFF;
final int g = (color >> 8) & 0xFF;
final int b = (color >> 16) & 0xFF;
final int dr = targetR - r;
final int dg = targetG - g;
final int db = targetB - b;
final int distance = (dr * dr) + (dg * dg) + (db * db);
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = i;
}
}
return bestIndex;
return ColorPalette.vga32Bit[ColorPalette.findClosestPaletteIndex(rgb)];
}
}

View File

@@ -4,12 +4,20 @@ 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_entities.dart';
/// Shared rendering pipeline and math utilities for all Wolf3D rasterizers.
///
/// Subclasses implement draw primitives for their output target (software
/// framebuffer, ANSI text, Sixel, etc), while this base class coordinates
/// raycasting, sprite projection, and common HUD calculations.
abstract class Rasterizer<T> {
late List<double> zBuffer;
late int width;
late int height;
late int viewHeight;
/// The current engine instance; set at the start of every [render] call.
late WolfEngine engine;
/// A multiplier to adjust the width of sprites.
/// Pixel renderers usually keep this at 1.0.
/// ASCII renderers can override this (e.g., 0.6) to account for tall characters.
@@ -41,6 +49,7 @@ abstract class Rasterizer<T> {
/// The main entry point called by the game loop.
/// Orchestrates the mathematical rendering pipeline.
T render(WolfEngine engine) {
this.engine = engine;
width = engine.frameBuffer.width;
height = engine.frameBuffer.height;
// The 3D view typically takes up the top 80% of the screen
@@ -113,6 +122,68 @@ abstract class Rasterizer<T> {
/// Default implementation is a no-op for renderers that don't support menus.
void drawMenu(WolfEngine engine) {}
/// Plots a VGA image into this renderer's HUD coordinate space.
///
/// Coordinates are in the original 320x200 HUD space. Renderers that support
/// shared HUD composition should override this.
void blitHudVgaImage(VgaImage image, int startX320, int startY200) {}
/// Shared Wolf3D VGA HUD sequence used by software/sixel/ASCII-full-HUD.
///
/// Coordinates are intentionally in original 320x200 HUD space so each
/// renderer can scale/map them consistently via [blitHudVgaImage].
void drawStandardVgaHud(WolfEngine engine) {
final List<VgaImage> vgaImages = engine.data.vgaImages;
final int statusBarIndex = vgaImages.indexWhere(
(img) => img.width == 320 && img.height == 40,
);
if (statusBarIndex == -1) return;
blitHudVgaImage(vgaImages[statusBarIndex], 0, 160);
_drawHudNumber(vgaImages, 1, 32, 176);
_drawHudNumber(vgaImages, engine.player.score, 96, 176);
_drawHudNumber(vgaImages, 3, 120, 176);
_drawHudNumber(vgaImages, engine.player.health, 192, 176);
_drawHudNumber(vgaImages, engine.player.ammo, 232, 176);
_drawHudFace(engine, vgaImages);
_drawHudWeaponIcon(engine, vgaImages);
}
void _drawHudNumber(
List<VgaImage> vgaImages,
int value,
int rightAlignX,
int startY,
) {
// HUD numbers are rendered with fixed-width VGA glyphs (8 px advance).
const int zeroIndex = 96;
final String numStr = value.toString();
int currentX = rightAlignX - (numStr.length * 8);
for (int i = 0; i < numStr.length; i++) {
final int digit = int.parse(numStr[i]);
final int imageIndex = zeroIndex + digit;
if (imageIndex < vgaImages.length) {
blitHudVgaImage(vgaImages[imageIndex], currentX, startY);
}
currentX += 8;
}
}
void _drawHudFace(WolfEngine engine, List<VgaImage> vgaImages) {
final int faceIndex = hudFaceVgaIndex(engine.player.health);
if (faceIndex < vgaImages.length) {
blitHudVgaImage(vgaImages[faceIndex], 136, 164);
}
}
void _drawHudWeaponIcon(WolfEngine engine, List<VgaImage> vgaImages) {
final int weaponIndex = hudWeaponVgaIndex(engine);
if (weaponIndex < vgaImages.length) {
blitHudVgaImage(vgaImages[weaponIndex], 256, 164);
}
}
// ===========================================================================
// SHARED LIGHTING MATH
// ===========================================================================
@@ -124,6 +195,53 @@ abstract class Rasterizer<T> {
return (10.0 / (distance + 2.0)).clamp(0.0, 1.0);
}
// ===========================================================================
// SHARED PROJECTION MATH
// ===========================================================================
/// Returns the texture Y coordinate for the given screen row inside a wall
/// column. Works for both pixel and terminal renderers.
int wallTexY(int y, int columnHeight) {
final double relativeY =
(y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
return (relativeY * 64).toInt().clamp(0, 63);
}
/// Returns the texture Y coordinate for the given screen row inside a sprite
/// stripe.
int spriteTexY(int y, int drawStartY, int spriteHeight) {
final double relativeY = (y - drawStartY) / spriteHeight;
return (relativeY * 64).toInt().clamp(0, 63);
}
/// Returns the screen-space bounds for the player's weapon overlay.
///
/// [weaponWidth] and [weaponHeight] are in pixels; [startX]/[startY] are
/// the top-left draw origin. Uses [projectionWidth] so that renderers
/// with a narrower projection area (e.g. ASCII terminal) are handled
/// correctly.
({int weaponWidth, int weaponHeight, int startX, int startY})
weaponScreenBounds(WolfEngine engine) {
final int ww = (projectionWidth * 0.5).toInt();
final int wh = (viewHeight * 0.8).toInt();
final int sx = projectionOffsetX + (projectionWidth ~/ 2) - (ww ~/ 2);
final int sy = viewHeight - wh + (engine.player.weaponAnimOffset ~/ 4);
return (weaponWidth: ww, weaponHeight: wh, startX: sx, startY: sy);
}
/// Returns the VGA image index for BJ's face sprite based on player health.
int hudFaceVgaIndex(int health) {
if (health <= 0) return 127;
return 106 + (((100 - health) ~/ 16).clamp(0, 6) * 3);
}
/// Returns the VGA image index for the current weapon icon in the HUD.
int hudWeaponVgaIndex(WolfEngine engine) {
if (engine.player.hasChainGun) return 91;
if (engine.player.hasMachineGun) return 90;
return 89;
}
({double distance, int side, int hitWallId, double wallX})?
_intersectActivePushwall(
Player player,

View File

@@ -31,7 +31,6 @@ class SixelRasterizer extends CliRasterizer<String> {
static const String _terminalTealBackground = '\x1b[48;2;0;150;136m';
late Uint8List _screen;
late WolfEngine _engine;
int _offsetColumns = 0;
int _offsetRows = 0;
int _outputWidth = 1;
@@ -205,8 +204,9 @@ class SixelRasterizer extends CliRasterizer<String> {
}
@override
/// Renders using a temporary scaled framebuffer sized for Sixel output,
/// then restores the original terminal-sized framebuffer.
String render(WolfEngine engine) {
_engine = engine;
final FrameBuffer originalBuffer = engine.frameBuffer;
final FrameBuffer scaledBuffer = _createScaledBuffer(originalBuffer);
@@ -244,9 +244,7 @@ class SixelRasterizer extends CliRasterizer<String> {
int side,
) {
for (int y = drawStart; y < drawEnd; y++) {
double relativeY =
(y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
int texY = (relativeY * 64).toInt().clamp(0, 63);
int texY = wallTexY(y, columnHeight);
int colorByte = texture.pixels[texX * 64 + texY];
_screen[y * width + x] = colorByte;
@@ -268,8 +266,7 @@ class SixelRasterizer extends CliRasterizer<String> {
y < math.min(viewHeight, drawEndY);
y++
) {
double relativeY = (y - drawStartY) / spriteHeight;
int texY = (relativeY * 64).toInt().clamp(0, 63);
int texY = spriteTexY(y, drawStartY, spriteHeight);
int colorByte = texture.pixels[texX * 64 + texY];
@@ -287,12 +284,11 @@ class SixelRasterizer extends CliRasterizer<String> {
);
Sprite weaponSprite = engine.data.sprites[spriteIndex];
int weaponWidth = (width * 0.5).toInt();
int weaponHeight = (viewHeight * 0.8).toInt();
int startX = (width ~/ 2) - (weaponWidth ~/ 2);
int startY =
viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 4);
final bounds = weaponScreenBounds(engine);
final int weaponWidth = bounds.weaponWidth;
final int weaponHeight = bounds.weaponHeight;
final int startX = bounds.startX;
final int startY = bounds.startY;
for (int dy = 0; dy < weaponHeight; dy++) {
for (int dx = 0; dx < weaponWidth; dx++) {
@@ -312,22 +308,15 @@ class SixelRasterizer extends CliRasterizer<String> {
}
@override
/// Delegates the canonical Wolf3D VGA HUD draw sequence to the base class.
void drawHud(WolfEngine engine) {
int statusBarIndex = engine.data.vgaImages.indexWhere(
(img) => img.width == 320 && img.height == 40,
);
if (statusBarIndex == -1) return;
drawStandardVgaHud(engine);
}
_blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160);
_drawNumber(1, 32, 176, engine.data.vgaImages);
_drawNumber(engine.player.score, 96, 176, engine.data.vgaImages);
_drawNumber(3, 120, 176, engine.data.vgaImages);
_drawNumber(engine.player.health, 192, 176, engine.data.vgaImages);
_drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages);
_drawFace(engine);
_drawWeaponIcon(engine);
@override
/// Blits a VGA image into the Sixel index buffer HUD space (320x200).
void blitHudVgaImage(VgaImage image, int startX320, int startY200) {
_blitVgaImage(image, startX320, startY200);
}
@override
@@ -402,6 +391,7 @@ class SixelRasterizer extends CliRasterizer<String> {
int get _menuOptionScale => width < 220 ? 1 : 1;
/// Draws a compact fallback menu layout for narrow render targets.
void _drawCompactMenu(
int selectedDifficultyIndex,
int headingIndex,
@@ -427,6 +417,7 @@ class SixelRasterizer extends CliRasterizer<String> {
}
}
/// Draws bitmap menu text into the indexed Sixel back buffer.
void _drawMenuText(
String text,
int startX,
@@ -466,6 +457,7 @@ class SixelRasterizer extends CliRasterizer<String> {
}
}
/// Draws bitmap menu text centered in 320x200 menu space.
void _drawMenuTextCentered(
String text,
int y,
@@ -494,6 +486,7 @@ class SixelRasterizer extends CliRasterizer<String> {
// UI FALLBACK
// ===========================================================================
/// Returns an ANSI fallback UI when Sixel capability is unavailable.
String _renderNotSupportedMessage() {
int cols = 80;
int rows = 24;
@@ -542,13 +535,14 @@ class SixelRasterizer extends CliRasterizer<String> {
// SIXEL ENCODER
// ===========================================================================
/// Encodes the current indexed frame buffer as a Sixel image stream.
String toSixelString() {
StringBuffer sb = StringBuffer();
sb.write('\x1bPq');
double damageIntensity = _engine.difficulty == null
double damageIntensity = engine.difficulty == null
? 0.0
: _engine.player.damageFlash;
: engine.player.damageFlash;
int redBoost = (150 * damageIntensity).toInt();
double colorDrop = 1.0 - (0.5 * damageIntensity);
@@ -621,6 +615,8 @@ class SixelRasterizer extends CliRasterizer<String> {
return sb.toString();
}
/// Samples the render buffer at output-space coordinates using nearest-neighbor
/// mapping from Sixel output pixels to source pixels.
int _sampleScaledPixel(int outX, int outY) {
final int srcX = ((((outX + 0.5) * width) / _outputWidth) - 0.5)
.round()
@@ -637,6 +633,7 @@ class SixelRasterizer extends CliRasterizer<String> {
return _screen[srcY * width + srcX];
}
/// Emits run-length encoded Sixel data for one repeated column mask value.
void _writeSixelRle(StringBuffer sb, int value, int runLength) {
String char = String.fromCharCode(value + 63);
if (runLength > 3) {
@@ -650,9 +647,8 @@ class SixelRasterizer extends CliRasterizer<String> {
// PRIVATE HUD HELPERS
// ===========================================================================
/// Blits a VGA image into the Sixel back buffer with 320x200 scaling.
void _blitVgaImage(VgaImage image, int startX, int startY) {
int planeWidth = image.width ~/ 4;
int planeSize = planeWidth * image.height;
final double scaleX = width / 320.0;
final double scaleY = height / 200.0;
@@ -670,11 +666,7 @@ class SixelRasterizer extends CliRasterizer<String> {
int srcX = (dx / scaleX).toInt().clamp(0, image.width - 1);
int srcY = (dy / scaleY).toInt().clamp(0, image.height - 1);
int plane = srcX % 4;
int sx = srcX ~/ 4;
int index = (plane * planeSize) + (srcY * planeWidth) + sx;
int colorByte = image.pixels[index];
int colorByte = image.decodePixel(srcX, srcY);
if (colorByte != 255) {
_screen[drawY * width + drawX] = colorByte;
}
@@ -683,6 +675,7 @@ class SixelRasterizer extends CliRasterizer<String> {
}
}
/// Fills a rectangle in 320x200 virtual space into the scaled Sixel buffer.
void _fillRect320(int startX, int startY, int w, int h, int colorIndex) {
final double scaleX = width / 320.0;
final double scaleY = height / 200.0;
@@ -703,73 +696,8 @@ class SixelRasterizer extends CliRasterizer<String> {
}
}
void _drawNumber(
int value,
int rightAlignX,
int startY,
List<VgaImage> vgaImages,
) {
const int zeroIndex = 96;
String numStr = value.toString();
int currentX = rightAlignX - (numStr.length * 8);
for (int i = 0; i < numStr.length; i++) {
int digit = int.parse(numStr[i]);
if (zeroIndex + digit < vgaImages.length) {
_blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY);
}
currentX += 8;
}
}
void _drawFace(WolfEngine engine) {
int health = engine.player.health;
int faceIndex = (health <= 0)
? 127
: 106 + (((100 - health) ~/ 16).clamp(0, 6) * 3);
if (faceIndex < engine.data.vgaImages.length) {
_blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164);
}
}
void _drawWeaponIcon(WolfEngine engine) {
int weaponIndex = 89;
if (engine.player.hasChainGun) {
weaponIndex = 91;
} else if (engine.player.hasMachineGun) {
weaponIndex = 90;
}
if (weaponIndex < engine.data.vgaImages.length) {
_blitVgaImage(engine.data.vgaImages[weaponIndex], 256, 164);
}
}
/// Maps an RGB color to the nearest VGA palette index.
int _rgbToPaletteIndex(int rgb) {
final int targetR = (rgb >> 16) & 0xFF;
final int targetG = (rgb >> 8) & 0xFF;
final int targetB = rgb & 0xFF;
int bestIndex = 0;
int bestDistance = 1 << 30;
for (int i = 0; i < 256; i++) {
final int color = ColorPalette.vga32Bit[i];
final int r = color & 0xFF;
final int g = (color >> 8) & 0xFF;
final int b = (color >> 16) & 0xFF;
final int dr = targetR - r;
final int dg = targetG - g;
final int db = targetB - b;
final int dist = (dr * dr) + (dg * dg) + (db * db);
if (dist < bestDistance) {
bestDistance = dist;
bestIndex = i;
}
}
return bestIndex;
return ColorPalette.findClosestPaletteIndex(rgb);
}
}

View File

@@ -1,54 +1,21 @@
import 'dart:math' as math;
import 'package:wolf_3d_dart/src/rasterizer/menu_font.dart';
import 'package:wolf_3d_dart/src/rasterizer/rasterizer.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_menu.dart';
/// Pixel-accurate software rasterizer that writes directly into [FrameBuffer].
///
/// This is the canonical "modern framebuffer" implementation and serves as a
/// visual reference for terminal renderers.
class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
static const Map<String, List<String>> _menuFont = {
'A': ['01110', '10001', '10001', '11111', '10001', '10001', '10001'],
'B': ['11110', '10001', '10001', '11110', '10001', '10001', '11110'],
'C': ['01110', '10001', '10000', '10000', '10000', '10001', '01110'],
'D': ['11110', '10001', '10001', '10001', '10001', '10001', '11110'],
'E': ['11111', '10000', '10000', '11110', '10000', '10000', '11111'],
'F': ['11111', '10000', '10000', '11110', '10000', '10000', '10000'],
'G': ['01110', '10001', '10000', '10111', '10001', '10001', '01111'],
'H': ['10001', '10001', '10001', '11111', '10001', '10001', '10001'],
'I': ['11111', '00100', '00100', '00100', '00100', '00100', '11111'],
'K': ['10001', '10010', '10100', '11000', '10100', '10010', '10001'],
'L': ['10000', '10000', '10000', '10000', '10000', '10000', '11111'],
'M': ['10001', '11011', '10101', '10101', '10001', '10001', '10001'],
'N': ['10001', '10001', '11001', '10101', '10011', '10001', '10001'],
'O': ['01110', '10001', '10001', '10001', '10001', '10001', '01110'],
'P': ['11110', '10001', '10001', '11110', '10000', '10000', '10000'],
'R': ['11110', '10001', '10001', '11110', '10100', '10010', '10001'],
'S': ['01111', '10000', '10000', '01110', '00001', '00001', '11110'],
'T': ['11111', '00100', '00100', '00100', '00100', '00100', '00100'],
'U': ['10001', '10001', '10001', '10001', '10001', '10001', '01110'],
'W': ['10001', '10001', '10001', '10101', '10101', '11011', '10001'],
'Y': ['10001', '10001', '01010', '00100', '00100', '00100', '00100'],
'?': ['01110', '10001', '00001', '00010', '00100', '00000', '00100'],
'!': ['00100', '00100', '00100', '00100', '00100', '00000', '00100'],
',': ['00000', '00000', '00000', '00000', '00110', '00100', '01000'],
'.': ['00000', '00000', '00000', '00000', '00000', '00110', '00110'],
"'": ['00100', '00100', '00100', '00000', '00000', '00000', '00000'],
' ': ['00000', '00000', '00000', '00000', '00000', '00000', '00000'],
};
late FrameBuffer _buffer;
late WolfEngine _engine;
// Intercept the base render call to store our references
@override
FrameBuffer render(WolfEngine engine) {
_engine = engine;
_buffer = engine.frameBuffer;
return super.render(engine);
}
@override
void prepareFrame(WolfEngine engine) {
_buffer = engine.frameBuffer;
// Top half is ceiling color (25), bottom half is floor color (29)
int ceilingColor = ColorPalette.vga32Bit[25];
int floorColor = ColorPalette.vga32Bit[29];
@@ -73,10 +40,7 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
int side,
) {
for (int y = drawStart; y < drawEnd; y++) {
// Calculate which Y pixel of the texture to sample
double relativeY =
(y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
int texY = (relativeY * 64).toInt().clamp(0, 63);
int texY = wallTexY(y, columnHeight);
int colorByte = texture.pixels[texX * 64 + texY];
int pixelColor = ColorPalette.vga32Bit[colorByte];
@@ -105,8 +69,7 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
y < math.min(viewHeight, drawEndY);
y++
) {
double relativeY = (y - drawStartY) / spriteHeight;
int texY = (relativeY * 64).toInt().clamp(0, 63);
int texY = spriteTexY(y, drawStartY, spriteHeight);
int colorByte = texture.pixels[texX * 64 + texY];
@@ -124,12 +87,11 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
);
Sprite weaponSprite = engine.data.sprites[spriteIndex];
int weaponWidth = (width * 0.5).toInt();
int weaponHeight = (viewHeight * 0.8).toInt();
int startX = (width ~/ 2) - (weaponWidth ~/ 2);
int startY =
viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 4);
final bounds = weaponScreenBounds(engine);
final int weaponWidth = bounds.weaponWidth;
final int weaponHeight = bounds.weaponHeight;
final int startX = bounds.startX;
final int startY = bounds.startY;
for (int dy = 0; dy < weaponHeight; dy++) {
for (int dx = 0; dx < weaponWidth; dx++) {
@@ -150,30 +112,15 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
}
@override
/// Delegates the canonical Wolf3D VGA HUD draw sequence to the base class.
void drawHud(WolfEngine engine) {
int statusBarIndex = engine.data.vgaImages.indexWhere(
(img) => img.width == 320 && img.height == 40,
);
if (statusBarIndex == -1) return;
drawStandardVgaHud(engine);
}
// 1. Draw Background
_blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160);
// 2. Draw Stats (100% mathematically accurate right-aligned coordinates)
_drawNumber(1, 32, 176, engine.data.vgaImages); // Floor
_drawNumber(engine.player.score, 96, 176, engine.data.vgaImages); // Score
_drawNumber(3, 120, 176, engine.data.vgaImages); // Lives
_drawNumber(
engine.player.health,
192,
176,
engine.data.vgaImages,
); // Health
_drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages); // Ammo
// 3. Draw BJ's Face & Current Weapon
_drawFace(engine);
_drawWeaponIcon(engine);
@override
/// Blits a VGA image into the software framebuffer HUD space (320x200).
void blitHudVgaImage(VgaImage image, int startX320, int startY200) {
_blitVgaImage(image, startX320, startY200);
}
@override
@@ -245,6 +192,8 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
}
}
/// Converts an `RRGGBB` menu color into the framebuffer's packed channel
/// order (`0xAABBGGRR`) used throughout this renderer.
int _rgbToFrameColor(int rgb) {
final int r = (rgb >> 16) & 0xFF;
final int g = (rgb >> 8) & 0xFF;
@@ -254,6 +203,7 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
return (0xFF000000) | (b << 16) | (g << 8) | r;
}
/// Draws bitmap menu text directly into the framebuffer.
void _drawMenuText(
String text,
int startX,
@@ -264,7 +214,7 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
int x = startX;
for (final rune in text.runes) {
final char = String.fromCharCode(rune).toUpperCase();
final pattern = _menuFont[char] ?? _menuFont[' ']!;
final pattern = WolfMenuFont.glyphFor(char);
for (int row = 0; row < pattern.length; row++) {
final bits = pattern[row];
@@ -282,25 +232,26 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
}
}
x += (6 * scale);
x += WolfMenuFont.glyphAdvance(char, scale);
}
}
/// Draws bitmap menu text centered in the current framebuffer width.
void _drawMenuTextCentered(
String text,
int y,
int color, {
int scale = 1,
}) {
final textWidth = text.length * 6 * scale;
final x = ((width - textWidth) ~/ 2).clamp(0, width - 1);
final int textWidth = WolfMenuFont.measureTextWidth(text, scale);
final int x = ((width - textWidth) ~/ 2).clamp(0, width - 1);
_drawMenuText(text, x, y, color, scale: scale);
}
@override
FrameBuffer finalizeFrame() {
// If the player took damage, overlay a red tint across the 3D view
if (_engine.difficulty != null && _engine.player.damageFlash > 0) {
if (engine.difficulty != null && engine.player.damageFlash > 0) {
_applyDamageFlash();
}
return _buffer; // Return the fully painted pixel array
@@ -310,12 +261,10 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
// PRIVATE HELPER METHODS
// ===========================================================================
/// Maps the planar VGA image data directly to 32-bit pixels.
/// (Assuming a 1:1 scale, which is standard for the 320x200 software renderer).
/// Maps planar VGA image data directly to 32-bit framebuffer pixels.
///
/// This renderer assumes a 1:1 mapping with the canonical 320x200 layout.
void _blitVgaImage(VgaImage image, int startX, int startY) {
int planeWidth = image.width ~/ 4;
int planeSize = planeWidth * image.height;
for (int dy = 0; dy < image.height; dy++) {
for (int dx = 0; dx < image.width; dx++) {
int drawX = startX + dx;
@@ -324,12 +273,7 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) {
int srcX = dx.clamp(0, image.width - 1);
int srcY = dy.clamp(0, image.height - 1);
int plane = srcX % 4;
int sx = srcX ~/ 4;
int index = (plane * planeSize) + (srcY * planeWidth) + sx;
int colorByte = image.pixels[index];
int colorByte = image.decodePixel(srcX, srcY);
if (colorByte != 255) {
_buffer.pixels[drawY * width + drawX] =
ColorPalette.vga32Bit[colorByte];
@@ -339,59 +283,10 @@ class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
}
}
void _drawNumber(
int value,
int rightAlignX,
int startY,
List<VgaImage> vgaImages,
) {
const int zeroIndex = 96;
String numStr = value.toString();
int currentX = rightAlignX - (numStr.length * 8);
for (int i = 0; i < numStr.length; i++) {
int digit = int.parse(numStr[i]);
if (zeroIndex + digit < vgaImages.length) {
_blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY);
}
currentX += 8;
}
}
void _drawFace(WolfEngine engine) {
int health = engine.player.health;
int faceIndex;
if (health <= 0) {
faceIndex = 127; // Dead face
} else {
int healthTier = ((100 - health) ~/ 16).clamp(0, 6);
faceIndex = 106 + (healthTier * 3);
}
if (faceIndex < engine.data.vgaImages.length) {
_blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164);
}
}
void _drawWeaponIcon(WolfEngine engine) {
int weaponIndex = 89; // Default to Pistol
if (engine.player.hasChainGun) {
weaponIndex = 91;
} else if (engine.player.hasMachineGun) {
weaponIndex = 90;
}
if (weaponIndex < engine.data.vgaImages.length) {
_blitVgaImage(engine.data.vgaImages[weaponIndex], 256, 164);
}
}
/// Tints the top 80% of the screen red based on player.damageFlash intensity
/// Tints the 3D view red based on `player.damageFlash` intensity.
void _applyDamageFlash() {
// Grab the intensity (0.0 to 1.0)
double intensity = _engine.player.damageFlash;
double intensity = engine.player.damageFlash;
// Calculate how much to boost red and drop green/blue
int redBoost = (150 * intensity).toInt();