Added some dartdoc comments and an (untested) sixel rasterizer
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
|
||||
class SixelRasterizer extends Rasterizer {
|
||||
late Uint8List _screen;
|
||||
late WolfEngine _engine;
|
||||
|
||||
@override
|
||||
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||
_engine = engine;
|
||||
// We only need 8-bit indices for the 256 VGA colors
|
||||
_screen = Uint8List(buffer.width * buffer.height);
|
||||
return super.render(engine, buffer);
|
||||
}
|
||||
|
||||
@override
|
||||
void prepareFrame(WolfEngine engine) {
|
||||
// Top half is ceiling color index (25), bottom half is floor color index (29)
|
||||
for (int y = 0; y < viewHeight; y++) {
|
||||
int colorIndex = (y < viewHeight / 2) ? 25 : 29;
|
||||
for (int x = 0; x < width; x++) {
|
||||
_screen[y * width + x] = colorIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void drawWallColumn(
|
||||
int x,
|
||||
int drawStart,
|
||||
int drawEnd,
|
||||
int columnHeight,
|
||||
Sprite texture,
|
||||
int texX,
|
||||
double perpWallDist,
|
||||
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 colorByte = texture.pixels[texX * 64 + texY];
|
||||
|
||||
// Note: Directional shading is omitted here to preserve strict VGA palette indices.
|
||||
// Sixel uses a fixed 256-color palette, so real-time shading requires a lookup table.
|
||||
_screen[y * width + x] = colorByte;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void drawSpriteStripe(
|
||||
int stripeX,
|
||||
int drawStartY,
|
||||
int drawEndY,
|
||||
int spriteHeight,
|
||||
Sprite texture,
|
||||
int texX,
|
||||
double transformY,
|
||||
) {
|
||||
for (
|
||||
int y = math.max(0, drawStartY);
|
||||
y < math.min(viewHeight, drawEndY);
|
||||
y++
|
||||
) {
|
||||
double relativeY = (y - drawStartY) / spriteHeight;
|
||||
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||
|
||||
int colorByte = texture.pixels[texX * 64 + texY];
|
||||
|
||||
// 255 is the "transparent" color index
|
||||
if (colorByte != 255) {
|
||||
_screen[y * width + stripeX] = colorByte;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void drawWeapon(WolfEngine engine) {
|
||||
int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
|
||||
engine.data.sprites.length,
|
||||
);
|
||||
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);
|
||||
|
||||
for (int dy = 0; dy < weaponHeight; dy++) {
|
||||
for (int dx = 0; dx < weaponWidth; dx++) {
|
||||
int texX = (dx * 64 ~/ weaponWidth).clamp(0, 63);
|
||||
int texY = (dy * 64 ~/ weaponHeight).clamp(0, 63);
|
||||
|
||||
int colorByte = weaponSprite.pixels[texX * 64 + texY];
|
||||
if (colorByte != 255) {
|
||||
int drawX = startX + dx;
|
||||
int drawY = startY + dy;
|
||||
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
|
||||
_screen[drawY * width + drawX] = colorByte;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void drawHud(WolfEngine engine) {
|
||||
int statusBarIndex = engine.data.vgaImages.indexWhere(
|
||||
(img) => img.width == 320 && img.height == 40,
|
||||
);
|
||||
if (statusBarIndex == -1) return;
|
||||
|
||||
_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
|
||||
dynamic finalizeFrame() {
|
||||
return toSixelString();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// SIXEL ENCODER
|
||||
// ===========================================================================
|
||||
|
||||
/// Converts the 8-bit index buffer into a standard Sixel sequence
|
||||
String toSixelString() {
|
||||
StringBuffer sb = StringBuffer();
|
||||
|
||||
// Start Sixel sequence (q = Sixel format)
|
||||
sb.write('\x1bPq');
|
||||
|
||||
// 1. Define the Palette (and apply damage flash directly to the palette!)
|
||||
double damageIntensity = _engine.player.damageFlash;
|
||||
int redBoost = (150 * damageIntensity).toInt();
|
||||
double colorDrop = 1.0 - (0.5 * damageIntensity);
|
||||
|
||||
for (int i = 0; i < 256; i++) {
|
||||
int color = ColorPalette.vga32Bit[i];
|
||||
int r = color & 0xFF;
|
||||
int g = (color >> 8) & 0xFF;
|
||||
int b = (color >> 16) & 0xFF;
|
||||
|
||||
if (damageIntensity > 0) {
|
||||
r = (r + redBoost).clamp(0, 255);
|
||||
g = (g * colorDrop).toInt().clamp(0, 255);
|
||||
b = (b * colorDrop).toInt().clamp(0, 255);
|
||||
}
|
||||
|
||||
// Sixel RGB ranges from 0 to 100
|
||||
int sixelR = (r * 100) ~/ 255;
|
||||
int sixelG = (g * 100) ~/ 255;
|
||||
int sixelB = (b * 100) ~/ 255;
|
||||
|
||||
sb.write('#$i;2;$sixelR;$sixelG;$sixelB');
|
||||
}
|
||||
|
||||
// 2. Encode Image in 6-pixel vertical bands
|
||||
for (int band = 0; band < height; band += 6) {
|
||||
Map<int, Uint8List> colorMap = {};
|
||||
|
||||
// Map out which pixels use which color in this 6px high band
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int yOffset = 0; yOffset < 6; yOffset++) {
|
||||
int y = band + yOffset;
|
||||
if (y >= height) break;
|
||||
|
||||
int colorIdx = _screen[y * width + x];
|
||||
if (!colorMap.containsKey(colorIdx)) {
|
||||
colorMap[colorIdx] = Uint8List(width);
|
||||
}
|
||||
// Set the bit corresponding to the vertical position (0-5)
|
||||
colorMap[colorIdx]![x] |= (1 << yOffset);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the encoded Sixel characters for each color present in the band
|
||||
bool firstColor = true;
|
||||
for (var entry in colorMap.entries) {
|
||||
if (!firstColor)
|
||||
sb.write('\$'); // Carriage return to overlay colors on the same band
|
||||
firstColor = false;
|
||||
|
||||
// Select color index
|
||||
sb.write('#${entry.key}');
|
||||
|
||||
Uint8List cols = entry.value;
|
||||
int currentVal = -1;
|
||||
int runLength = 0;
|
||||
|
||||
// Run-Length Encoding (RLE) loop
|
||||
for (int x = 0; x < width; x++) {
|
||||
int val = cols[x];
|
||||
if (val == currentVal) {
|
||||
runLength++;
|
||||
} else {
|
||||
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
||||
currentVal = val;
|
||||
runLength = 1;
|
||||
}
|
||||
}
|
||||
if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
|
||||
}
|
||||
|
||||
sb.write('-'); // Move down to the next 6-pixel band
|
||||
}
|
||||
|
||||
// End Sixel sequence
|
||||
sb.write('\x1b\\');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
void _writeSixelRle(StringBuffer sb, int value, int runLength) {
|
||||
String char = String.fromCharCode(value + 63);
|
||||
// Sixel RLE format: !<count><char> (only worth it if count > 3)
|
||||
if (runLength > 3) {
|
||||
sb.write('!$runLength$char');
|
||||
} else {
|
||||
sb.write(char * runLength);
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// PRIVATE HUD HELPERS (Adapted for 8-bit index buffer)
|
||||
// ===========================================================================
|
||||
|
||||
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;
|
||||
int drawY = startY + dy;
|
||||
|
||||
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];
|
||||
if (colorByte != 255) {
|
||||
_screen[drawY * width + drawX] = colorByte;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user