Raycasting à la Wolfenstein 3D
Salut
Lors du dernier Mastermind, David parlait des sujets des prochains ateliers et masterclass.
Parmi eux, du raycasting comme utilisé dans le jeu Wolfenstein 3D.
J'ai écrit un raycaster en Love2D l'an dernier, très simple (c'est plus du ray marching que du ray casting).
Environ 300 lignes de codes et ça affiche la map en 2D et la vue 3D.
ça intéresse du monde ? si oui, je colle le code dans le forum.
a+
AlbanR
Sujet déplacé dans "Vos progrès".
Bravo pour ta réalisation, je vais bientôt travailler sur le sujet, en partant de la base et en essayant de vulgariser chaque étape, comme je l'ai fait pour les maps hexagonales, on verra ce que j'arrive à pondre :).
Salut,
Perso je suis toujours preneur pour voir du code, j'aime bien voir sous le capot 🙂
Hello,
je pensais que l'on recevait des notifications lors de réponses sur le forum ...
Voici le code en question; fonctionne sur la dernière version de love 2D
Enjoy !
--[[ 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 avec l'algo DDA) -- io.stdout:setvbuf("no") W = 320 H = 240 -- structure pour un vecteur 2D function vec2() v = { x = 0, y = 0 } return v end -- seuille une 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 (incrément) 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
Je poste le code en attachement car il ne s'affiche pas correctement en inline
Je viens de voir qu'il y avait un bouton pour recevoir les réponse par email. Sorry, j'avais pas vu.
- 6 Forums
- 257 Sujets
- 899 Posts
- 0 En ligne
- 45.6 {numéro}K Membres