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 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 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 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 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 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 zBuffer, ) { final Player player = engine.player; final List 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]; } } } } } } } }