Move rasterizer to engine
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
388
packages/wolf_3d_engine/lib/src/rasterizer/rasterizer.dart
Normal file
388
packages/wolf_3d_engine/lib/src/rasterizer/rasterizer.dart
Normal file
@@ -0,0 +1,388 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
|
||||
|
||||
abstract class Rasterizer {
|
||||
late List<double> zBuffer;
|
||||
late int width;
|
||||
late int height;
|
||||
late int viewHeight;
|
||||
|
||||
/// 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.
|
||||
double get aspectMultiplier => 1.0;
|
||||
|
||||
/// A multiplier to counteract tall pixel formats (like 1:2 terminal fonts).
|
||||
/// Defaults to 1.0 (no squish) for standard pixel rendering.
|
||||
double get verticalStretch => 1.0;
|
||||
|
||||
/// The main entry point called by the game loop.
|
||||
/// Orchestrates the mathematical rendering pipeline.
|
||||
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||
width = buffer.width;
|
||||
height = buffer.height;
|
||||
// The 3D view typically takes up the top 80% of the screen
|
||||
viewHeight = (height * 0.8).toInt();
|
||||
zBuffer = List.filled(width, 0.0);
|
||||
|
||||
// 1. Setup the frame (clear screen, draw floor/ceiling)
|
||||
prepareFrame(engine);
|
||||
|
||||
// 2. Do the heavy math for Raycasting Walls
|
||||
_castWalls(engine);
|
||||
|
||||
// 3. Do the heavy math for Projecting Sprites
|
||||
_castSprites(engine);
|
||||
|
||||
// 4. Draw 2D Overlays
|
||||
drawWeapon(engine);
|
||||
drawHud(engine);
|
||||
|
||||
// 5. Finalize and return the frame data (Buffer or String/List)
|
||||
return finalizeFrame();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// ABSTRACT METHODS (Implemented by the child renderers)
|
||||
// ===========================================================================
|
||||
|
||||
/// Initialize buffers, clear the screen, and draw the floor/ceiling.
|
||||
void prepareFrame(WolfEngine engine);
|
||||
|
||||
/// Draw a single vertical column of a wall.
|
||||
void drawWallColumn(
|
||||
int x,
|
||||
int drawStart,
|
||||
int drawEnd,
|
||||
int columnHeight,
|
||||
Sprite texture,
|
||||
int texX,
|
||||
double perpWallDist,
|
||||
int side,
|
||||
);
|
||||
|
||||
/// Draw a single vertical stripe of a sprite (enemy/item).
|
||||
void drawSpriteStripe(
|
||||
int stripeX,
|
||||
int drawStartY,
|
||||
int drawEndY,
|
||||
int spriteHeight,
|
||||
Sprite texture,
|
||||
int texX,
|
||||
double transformY,
|
||||
);
|
||||
|
||||
/// Draw the player's weapon overlay at the bottom of the 3D view.
|
||||
void drawWeapon(WolfEngine engine);
|
||||
|
||||
/// Draw the 2D status bar at the bottom 20% of the screen.
|
||||
void drawHud(WolfEngine engine);
|
||||
|
||||
/// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list).
|
||||
dynamic finalizeFrame();
|
||||
|
||||
// ===========================================================================
|
||||
// SHARED LIGHTING MATH
|
||||
// ===========================================================================
|
||||
|
||||
/// Calculates depth-based lighting falloff (0.0 to 1.0).
|
||||
/// While the original Wolf3D didn't use depth fog, this provides a great
|
||||
/// atmospheric effect for custom renderers (like ASCII dithering).
|
||||
double calculateDepthBrightness(double distance) {
|
||||
return (10.0 / (distance + 2.0)).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// CORE ENGINE MATH (Shared across all renderers)
|
||||
// ===========================================================================
|
||||
|
||||
void _castWalls(WolfEngine engine) {
|
||||
final Player player = engine.player;
|
||||
final SpriteMap map = engine.currentLevel;
|
||||
final List<Sprite> wallTextures = engine.data.walls;
|
||||
|
||||
final Map<String, double> doorOffsets = engine.doorManager
|
||||
.getOffsetsForRenderer();
|
||||
final Pushwall? activePushwall = engine.pushwallManager.activePushwall;
|
||||
|
||||
final double fov = math.pi / 3;
|
||||
Coordinate2D dir = Coordinate2D(
|
||||
math.cos(player.angle),
|
||||
math.sin(player.angle),
|
||||
);
|
||||
Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2);
|
||||
|
||||
for (int x = 0; x < width; x++) {
|
||||
double cameraX = 2 * x / width - 1.0;
|
||||
Coordinate2D rayDir = dir + (plane * cameraX);
|
||||
|
||||
int mapX = player.x.toInt();
|
||||
int mapY = player.y.toInt();
|
||||
|
||||
double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs();
|
||||
double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs();
|
||||
|
||||
double sideDistX, sideDistY, perpWallDist = 0.0;
|
||||
int stepX, stepY, side = 0, hitWallId = 0;
|
||||
bool hit = false, hitOutOfBounds = false, customDistCalculated = false;
|
||||
double textureOffset = 0.0;
|
||||
Set<String> ignoredDoors = {};
|
||||
|
||||
if (rayDir.x < 0) {
|
||||
stepX = -1;
|
||||
sideDistX = (player.x - mapX) * deltaDistX;
|
||||
} else {
|
||||
stepX = 1;
|
||||
sideDistX = (mapX + 1.0 - player.x) * deltaDistX;
|
||||
}
|
||||
if (rayDir.y < 0) {
|
||||
stepY = -1;
|
||||
sideDistY = (player.y - mapY) * deltaDistY;
|
||||
} else {
|
||||
stepY = 1;
|
||||
sideDistY = (mapY + 1.0 - player.y) * deltaDistY;
|
||||
}
|
||||
|
||||
// DDA Loop
|
||||
while (!hit) {
|
||||
if (sideDistX < sideDistY) {
|
||||
sideDistX += deltaDistX;
|
||||
mapX += stepX;
|
||||
side = 0;
|
||||
} else {
|
||||
sideDistY += deltaDistY;
|
||||
mapY += stepY;
|
||||
side = 1;
|
||||
}
|
||||
|
||||
if (mapY < 0 ||
|
||||
mapY >= map.length ||
|
||||
mapX < 0 ||
|
||||
mapX >= map[0].length) {
|
||||
hit = true;
|
||||
hitOutOfBounds = true;
|
||||
} else if (map[mapY][mapX] > 0) {
|
||||
String mapKey = '$mapX,$mapY';
|
||||
|
||||
// DOOR LOGIC
|
||||
if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) {
|
||||
double currentOffset = doorOffsets[mapKey] ?? 0.0;
|
||||
if (currentOffset > 0.0) {
|
||||
double perpWallDistTemp = (side == 0)
|
||||
? (sideDistX - deltaDistX)
|
||||
: (sideDistY - deltaDistY);
|
||||
double wallXTemp = (side == 0)
|
||||
? player.y + perpWallDistTemp * rayDir.y
|
||||
: player.x + perpWallDistTemp * rayDir.x;
|
||||
wallXTemp -= wallXTemp.floor();
|
||||
if (wallXTemp < currentOffset) {
|
||||
ignoredDoors.add(mapKey);
|
||||
continue; // Ray passes through the open part of the door
|
||||
}
|
||||
}
|
||||
hit = true;
|
||||
hitWallId = map[mapY][mapX];
|
||||
textureOffset = currentOffset;
|
||||
}
|
||||
// PUSHWALL LOGIC
|
||||
else if (activePushwall != null &&
|
||||
mapX == activePushwall.x &&
|
||||
mapY == activePushwall.y) {
|
||||
hit = true;
|
||||
hitWallId = map[mapY][mapX];
|
||||
|
||||
double pOffset = activePushwall.offset;
|
||||
int pDirX = activePushwall.dirX;
|
||||
int pDirY = activePushwall.dirY;
|
||||
|
||||
perpWallDist = (side == 0)
|
||||
? (sideDistX - deltaDistX)
|
||||
: (sideDistY - deltaDistY);
|
||||
|
||||
if (side == 0 && pDirX != 0) {
|
||||
if (pDirX == stepX) {
|
||||
double intersect = perpWallDist + pOffset * deltaDistX;
|
||||
if (intersect < sideDistY) {
|
||||
perpWallDist = intersect;
|
||||
} else {
|
||||
side = 1;
|
||||
perpWallDist = sideDistY - deltaDistY;
|
||||
}
|
||||
} else {
|
||||
perpWallDist -= (1.0 - pOffset) * deltaDistX;
|
||||
}
|
||||
} else if (side == 1 && pDirY != 0) {
|
||||
if (pDirY == stepY) {
|
||||
double intersect = perpWallDist + pOffset * deltaDistY;
|
||||
if (intersect < sideDistX) {
|
||||
perpWallDist = intersect;
|
||||
} else {
|
||||
side = 0;
|
||||
perpWallDist = sideDistX - deltaDistX;
|
||||
}
|
||||
} else {
|
||||
perpWallDist -= (1.0 - pOffset) * deltaDistY;
|
||||
}
|
||||
} else {
|
||||
double wallFraction = (side == 0)
|
||||
? player.y + perpWallDist * rayDir.y
|
||||
: player.x + perpWallDist * rayDir.x;
|
||||
wallFraction -= wallFraction.floor();
|
||||
if (side == 0) {
|
||||
if (pDirY == 1 && wallFraction < pOffset) hit = false;
|
||||
if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
||||
if (hit) textureOffset = pOffset * pDirY;
|
||||
} else {
|
||||
if (pDirX == 1 && wallFraction < pOffset) hit = false;
|
||||
if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
||||
if (hit) textureOffset = pOffset * pDirX;
|
||||
}
|
||||
}
|
||||
if (!hit) continue;
|
||||
customDistCalculated = true;
|
||||
} else {
|
||||
hit = true;
|
||||
hitWallId = map[mapY][mapX];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hitOutOfBounds) continue;
|
||||
|
||||
if (!customDistCalculated) {
|
||||
perpWallDist = (side == 0)
|
||||
? (sideDistX - deltaDistX)
|
||||
: (sideDistY - deltaDistY);
|
||||
}
|
||||
if (perpWallDist < 0.1) perpWallDist = 0.1;
|
||||
|
||||
// Save for sprite depth checks
|
||||
zBuffer[x] = perpWallDist;
|
||||
|
||||
// Calculate Texture X Coordinate
|
||||
double wallX = (side == 0)
|
||||
? player.y + perpWallDist * rayDir.y
|
||||
: player.x + perpWallDist * rayDir.x;
|
||||
wallX -= wallX.floor();
|
||||
|
||||
int texNum;
|
||||
if (hitWallId >= 90) {
|
||||
texNum = 98.clamp(0, wallTextures.length - 1);
|
||||
} else {
|
||||
texNum = ((hitWallId - 1) * 2).clamp(0, wallTextures.length - 2);
|
||||
if (side == 1) texNum += 1;
|
||||
}
|
||||
Sprite texture = wallTextures[texNum];
|
||||
|
||||
// Texture flipping for specific orientations
|
||||
int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63);
|
||||
if (side == 0 && math.cos(player.angle) > 0) texX = 63 - texX;
|
||||
if (side == 1 && math.sin(player.angle) < 0) texX = 63 - texX;
|
||||
|
||||
// Calculate drawing dimensions
|
||||
int columnHeight = ((viewHeight / perpWallDist) * verticalStretch)
|
||||
.toInt();
|
||||
int drawStart = (-columnHeight ~/ 2 + viewHeight ~/ 2).clamp(
|
||||
0,
|
||||
viewHeight,
|
||||
);
|
||||
int drawEnd = (columnHeight ~/ 2 + viewHeight ~/ 2).clamp(0, viewHeight);
|
||||
|
||||
// Tell the implementation to draw this column
|
||||
drawWallColumn(
|
||||
x,
|
||||
drawStart,
|
||||
drawEnd,
|
||||
columnHeight,
|
||||
texture,
|
||||
texX,
|
||||
perpWallDist,
|
||||
side,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _castSprites(WolfEngine engine) {
|
||||
final Player player = engine.player;
|
||||
final List<Entity> activeSprites = List.from(engine.entities);
|
||||
|
||||
// Sort from furthest to closest (Painter's Algorithm)
|
||||
activeSprites.sort((a, b) {
|
||||
double distA = player.position.distanceTo(a.position);
|
||||
double distB = player.position.distanceTo(b.position);
|
||||
return distB.compareTo(distA);
|
||||
});
|
||||
|
||||
Coordinate2D dir = Coordinate2D(
|
||||
math.cos(player.angle),
|
||||
math.sin(player.angle),
|
||||
);
|
||||
Coordinate2D plane =
|
||||
Coordinate2D(-dir.y, dir.x) * math.tan((math.pi / 3) / 2);
|
||||
|
||||
for (Entity entity in activeSprites) {
|
||||
Coordinate2D spritePos = entity.position - player.position;
|
||||
|
||||
double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y);
|
||||
double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y);
|
||||
double transformY =
|
||||
invDet * (-plane.y * spritePos.x + plane.x * spritePos.y);
|
||||
|
||||
// Only process if the sprite is in front of the camera
|
||||
if (transformY > 0) {
|
||||
int spriteScreenX = ((width / 2) * (1 + transformX / transformY))
|
||||
.toInt();
|
||||
int spriteHeight = ((viewHeight / transformY).abs() * verticalStretch)
|
||||
.toInt();
|
||||
|
||||
// Scale width based on the aspectMultiplier (useful for ASCII)
|
||||
int spriteWidth = (spriteHeight * aspectMultiplier / verticalStretch)
|
||||
.toInt();
|
||||
|
||||
int drawStartY = -spriteHeight ~/ 2 + viewHeight ~/ 2;
|
||||
int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2;
|
||||
int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
|
||||
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
|
||||
|
||||
int clipStartX = math.max(0, drawStartX);
|
||||
int clipEndX = math.min(width - 1, drawEndX);
|
||||
|
||||
int safeIndex = entity.spriteIndex.clamp(
|
||||
0,
|
||||
engine.data.sprites.length - 1,
|
||||
);
|
||||
Sprite texture = engine.data.sprites[safeIndex];
|
||||
|
||||
// Loop through the visible vertical stripes
|
||||
for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
|
||||
// Check the Z-Buffer to see if a wall is in front of this stripe
|
||||
if (transformY < zBuffer[stripe]) {
|
||||
int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63);
|
||||
|
||||
// Tell the implementation to draw this stripe
|
||||
drawSpriteStripe(
|
||||
stripe,
|
||||
drawStartY,
|
||||
drawEndY,
|
||||
spriteHeight,
|
||||
texture,
|
||||
texX,
|
||||
transformY,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Darkens a 32-bit 0xAABBGGRR color by roughly 30% without touching Alpha
|
||||
int shadeColor(int color) {
|
||||
int r = (color & 0xFF) * 7 ~/ 10;
|
||||
int g = ((color >> 8) & 0xFF) * 7 ~/ 10;
|
||||
int b = ((color >> 16) & 0xFF) * 7 ~/ 10;
|
||||
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user