import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:wolf_dart/classes/color_palette.dart'; import 'package:wolf_dart/classes/linear_coordinates.dart'; import 'package:wolf_dart/classes/matrix.dart'; import 'package:wolf_dart/classes/sprite.dart'; class RaycasterPainter extends CustomPainter { final Matrix map; final List> textures; final LinearCoordinates player; final double playerAngle; final double fov; final Map doorOffsets; final List> sprites; final List entities; RaycasterPainter({ required this.map, required this.textures, required this.player, required this.playerAngle, required this.fov, required this.doorOffsets, required this.sprites, required this.entities, }); @override void paint(Canvas canvas, Size size) { // 1. Draw Ceiling & Floor canvas.drawRect( Rect.fromLTWH(0, 0, size.width, size.height / 2), Paint()..color = Colors.blueGrey[900]!, ); canvas.drawRect( Rect.fromLTWH(0, size.height / 2, size.width, size.height / 2), Paint()..color = Colors.brown[900]!, ); int screenWidth = size.width.toInt(); // The 1D Z-Buffer List zBuffer = List.filled(screenWidth, 0.0); double dirX = math.cos(playerAngle); double dirY = math.sin(playerAngle); double planeX = -dirY * math.tan(fov / 2); double planeY = dirX * math.tan(fov / 2); // --- 1. CAST WALLS --- for (int x = 0; x < screenWidth; x++) { double cameraX = 2 * x / screenWidth - 1.0; double rayDirX = dirX + planeX * cameraX; double rayDirY = dirY + planeY * cameraX; int mapX = player.x.toInt(); int mapY = player.y.toInt(); double sideDistX; double sideDistY; double deltaDistX = (rayDirX == 0) ? 1e30 : (1.0 / rayDirX).abs(); double deltaDistY = (rayDirY == 0) ? 1e30 : (1.0 / rayDirY).abs(); double perpWallDist; int stepX; int stepY; bool hit = false; bool hitOutOfBounds = false; int side = 0; int hitWallId = 0; double doorOffset = 0.0; Set ignoredDoors = {}; if (rayDirX < 0) { stepX = -1; sideDistX = (player.x - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1.0 - player.x) * deltaDistX; } if (rayDirY < 0) { stepY = -1; sideDistY = (player.y - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1.0 - player.y) * deltaDistY; } 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 doorKey = '$mapX,$mapY'; if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(doorKey)) { double currentOffset = doorOffsets[doorKey] ?? 0.0; if (currentOffset > 0.0) { double perpWallDistTemp = (side == 0) ? (sideDistX - deltaDistX) : (sideDistY - deltaDistY); double wallXTemp = (side == 0) ? player.y + perpWallDistTemp * rayDirY : player.x + perpWallDistTemp * rayDirX; wallXTemp -= wallXTemp.floor(); if (wallXTemp < currentOffset) { ignoredDoors.add(doorKey); continue; } } doorOffset = currentOffset; } hit = true; hitWallId = map[mapY][mapX]; } } if (hitOutOfBounds) continue; if (side == 0) { perpWallDist = (sideDistX - deltaDistX); } else { perpWallDist = (sideDistY - deltaDistY); } // STORE THE DISTANCE IN THE Z-BUFFER! zBuffer[x] = perpWallDist; double wallX; if (side == 0) { wallX = player.y + perpWallDist * rayDirY; } else { wallX = player.x + perpWallDist * rayDirX; } wallX -= wallX.floor(); _drawTexturedColumn( canvas, x, perpWallDist, wallX, side, size, hitWallId, textures, doorOffset, ); } // --- 2. DRAW SPRITES --- // Sort sprites from furthest to closest (Painter's Algorithm) List activeSprites = List.from(entities); activeSprites.sort((a, b) { double distA = math.pow(player.x - a.x, 2) + math.pow(player.y - a.y, 2).toDouble(); double distB = math.pow(player.x - b.x, 2) + math.pow(player.y - b.y, 2).toDouble(); return distB.compareTo(distA); }); for (Sprite sprite in activeSprites) { // Translate sprite position to relative to camera double spriteX = sprite.x - player.x; double spriteY = sprite.y - player.y; // Inverse camera matrix (Transform to screen space) double invDet = 1.0 / (planeX * dirY - dirX * planeY); double transformX = invDet * (dirY * spriteX - dirX * spriteY); double transformY = invDet * (-planeY * spriteX + planeX * spriteY); // print("Sprite at ${sprite.x}, ${sprite.y} transformY: $transformY"); // Is the sprite in front of the camera? if (transformY > 0) { // print( // "Attempting to draw Sprite Index: ${sprite.spriteIndex} (Total Sprites Loaded: ${sprites.length})", // ); int spriteScreenX = ((screenWidth / 2) * (1 + transformX / transformY)) .toInt(); // Calculate height and width (Sprites are 64x64 squares) int spriteHeight = (size.height / transformY).abs().toInt(); int spriteWidth = spriteHeight; int drawStartX = -spriteWidth ~/ 2 + spriteScreenX; int drawEndX = spriteWidth ~/ 2 + spriteScreenX; // Clip to screen boundaries int clipStartX = math.max(0, drawStartX); int clipEndX = math.min(screenWidth - 1, drawEndX); for (int stripe = clipStartX; stripe < clipEndX; stripe++) { // THE Z-BUFFER CHECK! // Only draw if the sprite is closer to the camera than the wall at this pixel column if (transformY < zBuffer[stripe]) { int texX = ((stripe - drawStartX) * 64 / spriteWidth).toInt().clamp( 0, 63, ); double startY = (size.height / 2) - (spriteHeight / 2); double stepY = spriteHeight / 64.0; // Safeguard against bad sprite indices int safeIndex = sprite.spriteIndex.clamp(0, sprites.length - 1); Matrix spritePixels = sprites[safeIndex]; for (int ty = 0; ty < 64; ty++) { int colorByte = spritePixels[texX][ty]; // Only draw if the pixel is NOT 255 (Magenta Transparency) if (colorByte != 255) { double endY = startY + stepY; if (endY > 0 && startY < size.height) { canvas.drawLine( Offset(stripe.toDouble(), startY), Offset(stripe.toDouble(), endY), Paint() ..color = ColorPalette.vga[colorByte] ..strokeWidth = 1.1, ); } } startY += stepY; } } } } } } void _drawTexturedColumn( Canvas canvas, int x, double distance, double wallX, int side, Size size, int hitWallId, List> textures, double doorOffset, ) { if (distance <= 0.01) distance = 0.01; double wallHeight = size.height / distance; int drawStart = ((size.height / 2) - (wallHeight / 2)).toInt(); // TEXTURE MAPPING // Wolf3D stores textures in pairs. Even = N/S (Light), Odd = E/W (Dark). int texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2); int texX = (wallX * 64).toInt().clamp(0, 63); // INTERCEPT DOORS if (hitWallId >= 90) { // Safely clamp the door texture index so it never crashes the paint loop! texNum = 98.clamp(0, textures.length - 1); texX = ((wallX - doorOffset) * 64).toInt().clamp(0, 63); } else { // Standard wall texture pairing texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2); if (side == 1) texNum += 1; } if (side == 0 && math.cos(playerAngle) > 0) texX = 63 - texX; if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX; double startY = drawStart.toDouble(); double stepY = wallHeight / 64.0; for (int ty = 0; ty < 64; ty++) { int colorByte = textures[texNum][texX][ty]; // 2. NO MORE SHADOW MATH // Because we selected the correct dark texture above, we just draw the raw color! Color pixelColor = ColorPalette.vga[colorByte]; double endY = startY + stepY; if (endY > 0 && startY < size.height) { canvas.drawLine( Offset(x.toDouble(), startY), Offset(x.toDouble(), endY), Paint() ..color = pixelColor ..strokeWidth = 1.1, ); } startY += stepY; } } @override bool shouldRepaint(covariant RaycasterPainter oldDelegate) { return oldDelegate.player != player || oldDelegate.playerAngle != playerAngle; } }