--[[ B O F E N S T E I N 3 D ]] -- -- plus du ray marching que du ray casting à la Wolfenstein 3D -- aurait terriblement ramé sur un PC de l'époque même écrit dans un langage performant -- car le calcul des intersections rayon/map dans Wolf3D était bien plus efficace que ça (+ n'utilisait que des -- calculs sur des entiers) -- io.stdout:setvbuf("no") W = 320 H = 240 -- structure pour un vecteur 2D function vec2() v = { x = 0, y = 0 } return v end -- seuille un valeur entre un min et un max function clamp(val, min, max) return math.max(min, math.min(val, max)); end -- joueur: position (vecteur) et rotation en radians player = { pos = vec2(), rot = 0 } -- champ de vision fov = math.pi / 4 -- la map : 1 seul type de mur mais il serait facile d'en définir d'autres MAP_WIDTH = 16 MAP_HEIGHT = 16 -- chaque case de la map mesure 1 x 1 (unité de map) -- hauteur des murs en unités de map WALL_HEIGHT = 0.6 -- couleur des murs (ici blancs) WALL_COLOR = { r = 1, g = 1, b = 1 } -- plan de la map map = { { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1 }, { 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, { 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 } } -- pas de progression pour le ray marching step = 0.01 -- distance max pour le ray marching MAX_DIST = math.max(MAP_WIDTH, MAP_HEIGHT) * 1.5 function love.load() love.graphics.setDefaultFilter("nearest", "nearest") love.window.setMode(1280, 960) love.window.setTitle("B o f e n s t e i n 3 D") -- position initiale du joueur dans la map player.pos.x = 1.5 player.pos.y = 1.5 end function love.update(dt) local oldPos = vec2() local row, col -- mémoriser la position précédente du joueur en cas de collision avec un mur oldPos.x = player.pos.x oldPos.y = player.pos.y -- rotation vers la droite if love.keyboard.isDown("right") then player.rot = player.rot + 2 * dt if player.rot > 2 * math.pi then player.rot = 0 end end -- rotation vers la gauche if love.keyboard.isDown("left") then player.rot = player.rot - 2 * dt if player.rot < 0 then player.rot = 2 * math.pi end end -- avancer if love.keyboard.isDown("up") then player.pos.x = player.pos.x + math.cos(player.rot) * 2 * dt player.pos.y = player.pos.y + math.sin(player.rot) * 2 * dt col = math.floor(player.pos.x) + 1 row = math.floor(player.pos.y) + 1 if map[row][col] > 0 then player.pos.x = oldPos.x player.pos.y = oldPos.y end end -- reculer if love.keyboard.isDown("down") then player.pos.x = player.pos.x - math.cos(player.rot) * 2 * dt player.pos.y = player.pos.y - math.sin(player.rot) * 2 * dt col = math.floor(player.pos.x) + 1 row = math.floor(player.pos.y) + 1 if map[row][col] > 0 then player.pos.x = oldPos.x player.pos.y = oldPos.y end end -- déplacement de côté vers la droite if love.keyboard.isDown("x") then player.pos.x = player.pos.x - math.sin(player.rot) * 2 * dt player.pos.y = player.pos.y + math.cos(player.rot) * 2 * dt col = math.floor(player.pos.x) + 1 row = math.floor(player.pos.y) + 1 if map[row][col] > 0 then player.pos.x = oldPos.x player.pos.y = oldPos.y end end -- déplacement de côté vers la gauche if love.keyboard.isDown("w") then player.pos.x = player.pos.x + math.sin(player.rot) * 2 * dt player.pos.y = player.pos.y - math.cos(player.rot) * 2 * dt col = math.floor(player.pos.x) + 1 row = math.floor(player.pos.y) + 1 if map[row][col] > 0 then player.pos.x = oldPos.x player.pos.y = oldPos.y end end end function love.draw() love.graphics.scale(2, 2) -- dessiner la map en 2D (arrière plan) -- décalage x,y de la map 2D à l'écran local map2d = vec2() map2d.x = W + 10 map2d.y = 0 -- taille des cases de la map 2D à l'écran local cellsize = 8 -- dessiner les murs local r,c for r = 1,16 do for c = 1, 16 do if map[r][c] > 0 then love.graphics.setColor(1, 1, 1) love.graphics.rectangle("fill", (c - 1) * cellsize + map2d.x, (r - 1) * cellsize + map2d.y, cellsize, cellsize) else love.graphics.setColor(0, 0, 0) love.graphics.rectangle("fill", (c - 1) * cellsize + map2d.x, (r - 1) * cellsize + map2d.y, cellsize, cellsize) end end end -- calculer et afficher la vue FPS en 3D -- dessiner le sol et le plafond local x, y, shade for y = 1,H/2 do -- on obscurcit petit à petit pour simuler la distance (dégradé linéaire) shade = (1 - y / (H / 2)) -- plafond love.graphics.setColor(0, 0, 0.5 * shade) love.graphics.line(0, y, W, y) -- sol (mirroir du plafond par rapport au milieu de l'écran verticalement) love.graphics.setColor(0.3 * shade, 0.3 * shade, 0.3 * shade) love.graphics.line(0, H - y, W, H - y) end -- pas d'incrément de l'angle du rayon relatif à la largeur de l'écran local inc = fov / W -- angle de départ par rapport à la direction où regarde le joueur local angle = player.rot - fov / 2 -- calculer le vecteur de la direction où regarde le joueur local lookat = vec2() lookat.x = math.cos(player.rot) lookat.y = math.sin(player.rot) -- un rayon est juste un vecteur qui représente sa direction local ray = vec2() -- distance parcourue le long du rayon local t -- point le long du rayon pour la distance parcourue t local march = vec2() -- coordonnées en ligne, colonne dans la map pour le test d'intersection rayon/mur local row, col -- pour chaque colonne de l'écran -- les murs sont dessinés en lamelles verticales for x = 0, W - 1 do -- lancer un rayon: calculer sa direction à partir de l'angle de lancé courant ray.x = math.cos(angle) ray.y = math.sin(angle) -- intialiser la distance parcourue t = 0 -- ray marching while ( t < MAX_DIST ) do -- avancer d'une distance d'un pas le long du rayon march.x = player.pos.x + ray.x * t march.y = player.pos.y + ray.y * t -- si on a dépassé les limite de la map if march.x < 0 or march.x > MAP_WIDTH or march.y < 0 or march.y > MAP_HEIGHT then -- out of map boundaries march.x = clamp(march.x, 0, W) march.y = clamp(march.y, 0, H) break end -- calculer la position actuelle sur le rayon en coordonnées dans la table map col = math.floor(march.x) + 1 row = math.floor(march.y) + 1 -- y a t-il un mur à cet endroit ? local hit = map[row][col] if hit > 0 then -- oui -- calculer la distance. d = t fonctionne mais donne un effet "fishbowl" (comme vu dans un aquarium) -- la multiplication par math.cos(angle - player.rot) supprime cet effet en corrigeant la distance local d = t * math.cos(math.abs(angle - player.rot)) -- l'effet de projection perspective est inversement proportionnel à la distance local persp = 1 / d -- la hauteur de cette lamelle de mur est calculée en deux parties symétriques par rapport au milieu de l'écran local ceiling = H / 2 - H * WALL_HEIGHT * persp -- partie supérieure ceiling = clamp(ceiling, 0, H - 1) -- clamp pour ne pas dépasser la hauteur de l'écran local floor = H - ceiling -- partie inférieure, mirroir de la partir supérieure -- calculer la luminosité du mur (attenuation en fonction de la distance) shade = persp * persp + 0.5 * persp + 0.1 -- dessiner la lamelle de mur love.graphics.setColor(0.7 * shade, 0.7 * shade, 0.7 * shade) love.graphics.line(x, ceiling, x, floor) break end -- avancer d'un pas t = t + step if t > MAX_DIST then t = MAX_DIST break end end -- incrémenter l'angle de lancer pour le rayon suivant angle = angle + inc -- dessiner la map en 2D (avant plan) -- dessiner le champ de vision du joueur if ( x == 0 or x == W - 1 ) then love.graphics.setColor(0, 1, 1) local x1, y1, x2, y2 x1 = (player.pos.x * cellsize) + map2d.x y1 = (player.pos.y * cellsize) + map2d.y x2 = math.floor(march.x + 0.5) * cellsize + map2d.x y2 = math.floor(march.y + 0.5) * cellsize + map2d.y love.graphics.line(x1, y1, x2, y2) end end -- fin pour chaque colonne de l'écran -- dessiner la map en 2D (avant plan, fin) -- afficher un point rouge à l'emplacement du joueur love.graphics.setColor(1, 0, 0) love.graphics.rectangle("fill", (player.pos.x * cellsize) + map2d.x, player.pos.y * cellsize + map2d.y, 1, 1) -- afficher les contrôles love.graphics.setColor(1, 1, 1) love.graphics.print("Contrôles:", 0, H) love.graphics.print("- flèches gauche-droite : rotation", 0, H + 12) love.graphics.print("- flèches haut-bas : déplacement avant-arrière", 0, H + 24) love.graphics.print("- touches W et X : déplacement latéral", 0, H + 36) -- afficher quelques variables pour info sous la vue 3D love.graphics.setColor(1, 1, 0) love.graphics.print("Debug :", 0, H + 60) -- position du joueur love.graphics.print(" x=" .. player.pos.x .. " y=" .. player.pos.y, 0, H + 72) -- emplacement en ligne,colonne en coordonnées dans la table map local c = math.floor(player.pos.x) + 1 local r = math.floor(player.pos.y) + 1 love.graphics.print(" row=" .. r .. " col=" .. c, 0, H + 84) -- rotation du joueur love.graphics.print(" rot=" .. player.rot, 0, H + 96) end function love.keypressed(key) if key == "escape" then love.event.quit() end end