359 lines
11 KiB
Dart
359 lines
11 KiB
Dart
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';
|
|
|
|
class SoftwareRasterizer {
|
|
// Pre-calculated VGA colors for ceiling and floor
|
|
final int ceilingColor = ColorPalette.vga32Bit[25];
|
|
final int floorColor = ColorPalette.vga32Bit[29];
|
|
|
|
void render(WolfEngine engine, FrameBuffer buffer) {
|
|
// 1. Wipe the screen clean with ceiling and floor colors
|
|
_clearScreen(buffer);
|
|
|
|
// 2. We need a Z-Buffer (1D array mapping to screen width) so sprites
|
|
// know if they are hiding behind a wall slice.
|
|
List<double> zBuffer = List.filled(buffer.width, 0.0);
|
|
|
|
// 3. Do the math and draw the walls, filling the Z-Buffer as we go
|
|
_castWalls(engine, buffer, zBuffer);
|
|
|
|
// 4. Draw the entities/sprites
|
|
_castSprites(engine, buffer, zBuffer);
|
|
}
|
|
|
|
void _clearScreen(FrameBuffer buffer) {
|
|
int halfScreen = (buffer.width * buffer.height) ~/ 2;
|
|
buffer.pixels.fillRange(0, halfScreen, ceilingColor);
|
|
buffer.pixels.fillRange(halfScreen, buffer.pixels.length, floorColor);
|
|
}
|
|
|
|
void _castWalls(WolfEngine engine, FrameBuffer buffer, List<double> zBuffer) {
|
|
final double fov = math.pi / 3;
|
|
final Player player = engine.player;
|
|
final SpriteMap map = engine.currentLevel;
|
|
|
|
// Fetch dynamic states from the managers
|
|
final Map<String, double> doorOffsets = engine.doorManager
|
|
.getOffsetsForRenderer();
|
|
final Pushwall? activePushwall = engine.pushwallManager.activePushwall;
|
|
|
|
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 < buffer.width; x++) {
|
|
double cameraX = 2 * x / buffer.width - 1.0;
|
|
Coordinate2D rayDir = dir + (plane * cameraX);
|
|
|
|
int mapX = player.x.toInt();
|
|
int mapY = player.y.toInt();
|
|
|
|
double sideDistX;
|
|
double sideDistY;
|
|
double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs();
|
|
double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs();
|
|
double perpWallDist = 0.0;
|
|
|
|
int stepX;
|
|
int stepY;
|
|
bool hit = false;
|
|
bool hitOutOfBounds = false;
|
|
int side = 0;
|
|
int hitWallId = 0;
|
|
|
|
double textureOffset = 0.0;
|
|
bool customDistCalculated = false;
|
|
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;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
// STANDARD WALL
|
|
else {
|
|
hit = true;
|
|
hitWallId = map[mapY][mapX];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hitOutOfBounds) continue;
|
|
|
|
if (!customDistCalculated) {
|
|
if (side == 0) {
|
|
perpWallDist = (sideDistX - deltaDistX);
|
|
} else {
|
|
perpWallDist = (sideDistY - deltaDistY);
|
|
}
|
|
}
|
|
|
|
// Log the distance so sprites know if they are occluded
|
|
zBuffer[x] = perpWallDist;
|
|
|
|
double wallX = (side == 0)
|
|
? player.y + perpWallDist * rayDir.y
|
|
: player.x + perpWallDist * rayDir.x;
|
|
wallX -= wallX.floor();
|
|
|
|
_drawTexturedColumn(
|
|
x,
|
|
perpWallDist,
|
|
wallX,
|
|
side,
|
|
hitWallId,
|
|
engine.data.walls, // Wall textures
|
|
textureOffset,
|
|
buffer,
|
|
player.angle,
|
|
);
|
|
}
|
|
}
|
|
|
|
void _drawTexturedColumn(
|
|
int x,
|
|
double distance,
|
|
double wallX,
|
|
int side,
|
|
int hitWallId,
|
|
List<Sprite> textures,
|
|
double textureOffset,
|
|
FrameBuffer buffer,
|
|
double playerAngle,
|
|
) {
|
|
if (distance <= 0.01) distance = 0.01;
|
|
|
|
int lineHeight = (buffer.height / distance).toInt();
|
|
|
|
int drawStart = -lineHeight ~/ 2 + buffer.height ~/ 2;
|
|
if (drawStart < 0) drawStart = 0;
|
|
|
|
int drawEnd = lineHeight ~/ 2 + buffer.height ~/ 2;
|
|
if (drawEnd >= buffer.height) drawEnd = buffer.height - 1;
|
|
|
|
int texNum;
|
|
if (hitWallId >= 90) {
|
|
texNum = 98.clamp(0, textures.length - 1);
|
|
} else {
|
|
texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2);
|
|
if (side == 1) texNum += 1;
|
|
}
|
|
|
|
int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63);
|
|
if (side == 0 && math.cos(playerAngle) > 0) texX = 63 - texX;
|
|
if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX;
|
|
|
|
double step = 64.0 / lineHeight;
|
|
double texPos = (drawStart - buffer.height / 2 + lineHeight / 2) * step;
|
|
|
|
Sprite texture = textures[texNum];
|
|
|
|
for (int y = drawStart; y < drawEnd; y++) {
|
|
int texY = texPos.toInt() & 63;
|
|
texPos += step;
|
|
|
|
int colorByte = texture.pixels[texX * 64 + texY];
|
|
int color32 = ColorPalette.vga32Bit[colorByte];
|
|
|
|
buffer.pixels[y * buffer.width + x] = color32;
|
|
}
|
|
}
|
|
|
|
void _castSprites(
|
|
WolfEngine engine,
|
|
FrameBuffer buffer,
|
|
List<double> zBuffer,
|
|
) {
|
|
final Player player = engine.player;
|
|
final List<Entity> activeSprites = List.from(engine.entities);
|
|
|
|
// Sort entities 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);
|
|
|
|
if (transformY > 0) {
|
|
int spriteScreenX = ((buffer.width / 2) * (1 + transformX / transformY))
|
|
.toInt();
|
|
int spriteHeight = (buffer.height / transformY).abs().toInt();
|
|
|
|
// In 1x1 buffer pixels, the width of the sprite is equal to its height
|
|
int spriteWidth = spriteHeight;
|
|
|
|
int drawStartY = -spriteHeight ~/ 2 + buffer.height ~/ 2;
|
|
if (drawStartY < 0) drawStartY = 0;
|
|
|
|
int drawEndY = spriteHeight ~/ 2 + buffer.height ~/ 2;
|
|
if (drawEndY >= buffer.height) drawEndY = buffer.height - 1;
|
|
|
|
int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
|
|
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
|
|
|
|
int clipStartX = math.max(0, drawStartX);
|
|
int clipEndX = math.min(buffer.width - 1, drawEndX);
|
|
|
|
int safeIndex = entity.spriteIndex.clamp(
|
|
0,
|
|
engine.data.sprites.length - 1,
|
|
);
|
|
Sprite spritePixels = engine.data.sprites[safeIndex];
|
|
|
|
for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
|
|
// Z-Buffer Check! Only draw the vertical stripe if it's in front of the wall
|
|
if (transformY < zBuffer[stripe]) {
|
|
int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63);
|
|
|
|
double step = 64.0 / spriteHeight;
|
|
double texPos =
|
|
(drawStartY - buffer.height / 2 + spriteHeight / 2) * step;
|
|
|
|
for (int y = drawStartY; y < drawEndY; y++) {
|
|
int texY = texPos.toInt() & 63;
|
|
texPos += step;
|
|
|
|
int colorByte = spritePixels.pixels[texX * 64 + texY];
|
|
|
|
if (colorByte != 255) {
|
|
// 255 is transparent
|
|
buffer.pixels[y * buffer.width + stripe] =
|
|
ColorPalette.vga32Bit[colorByte];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|