Ecrire un moteur de Raycasting avec View
Article paru dans le magazine Login HS 14 Rebol (septembre 2002)
Reproduction interdite. Tous droits réservés à Posse Press et Olivier Auverlot
Télécharger
le code source du raycaster
Rebol est un langage généraliste. Vous pouvez pratiquement tout programmer en Rebol. Il est léger, manipule efficacement les données et possède d'indéniables qualités dans le domaine de programmation réseau. C'est également un langage doué dans le domaine du graphisme et de l'animation. Le GCS (Graphical Compositing System) est une véritable librairie multimédia indépendante de la plate-forme d'exécution. Pour illustrer ses capacités, vous allez réaliser un véritable moteur de raycasting avec seulement 3 Ko de code. Entre-nous, je ne serai pas étonné dans quelques mois de rencontrer des jeux on-line écrits en Rebol.
C'est quoi le raycasting ?
Par sa simplicité, le raycasting est l'une des technologies les plus intrigantes dans le domaine des jeux vidéo. Pour mieux la cerner, je vais faire appel à votre mémoire... Certains d'entre vous se souviennent probablement d'un jeu nommé Wolfstein, apparut au début des années 90. Il s'agissait du premier jeu dans lequel le joueur était immergé dans son environnement. Le personnage se déplacait dans un labyrinthe et l'animation des murs et PNJ (personnages non jouables) était réalisée en temps réel. Pour les performances des machines de l'époque (Intel 286 et 386), c'était tout simplement incroyable ! Comment John Carnac, l'auteur du jeu, avait-il réalisé ce prodige ? Et bien la réponse est simple : tout simplement en trichant.
On m'aurai trompé à l'insu de mon joystick ?
En fait, le raycasting est une énorme et géniale supercherie puisqu'il n'utilise aucun algorythme 3D. Le joueur se déplace dans un univers 2D dont la visualisation à l'écran est réalisé à l'aide d'un "lancé de rayon". Le joueur est un point se déplaçant dans un tableau à deux dimensions et dans lequel chaque case, peut être vide ou représenter un cube virtuel définie par une couleur ou une texture. Il vous suffit de définir un champ de vision (Field of view) dont la largeur est le plus souvent fixée à 90 degrès afin de respecter les capacités minimales d'un être humain. Le cone ainsi créé, vous allez lancer 90 rayons afin de rechercher la première intersection avec un mur. Il ne vous reste plus qu'à déterminer la hauteur du segment du mur selon la distance et à le dessiner sur l'écran. Dans Wolfstein (et également dans notre moteur), les murs sont dessinés symétriquement par rapport à l'axe des abscisses (représentant l'horizon). Cet algorythme présente l'avantage de la simplicité mais ne vous permet pas de lever ou baisser la tête (le moteur du célèbre Doom doit déjà être un peu plus complexe... je n'ose même pas penser à Quake III).
Commençons par les fondations ...
Avant de jeter quoique ce soit, vous devez définir la surface du jeu ainsi qu'un certain nombre de variables et de fonctions utilitaires. Le labyrinthe (LABY) est défini par une liste de bloc (chaque bloc correspont à une ligne du tableau). Si la valeur d'une case est différente de 0, cela signifie que la case contient un cube.
Pour retrouver la couleur des murs de ce cube, il suffit d'allez la rechercher dans la liste PALETTE initialisée à l'aide des 16 célèbres couleurs de l'antique Qbasic. Vous avez également besoin de fixer la position du joueur (PX et PY), la distance parcourue en un pas (STRIDE), la direction du déplacement en degrès (HEADING) et le nombre de degrès lors d'une rotation (TURN). Pour accélérer les calculs, vous définissez également une liste contenant les valeurs de cosinus (CTABLE) pour un certain nombre d'angles. La fonction GET-ANGLE permet d'extraire une valeur de cette table.
REBOL [
subject: "moteur de raycasting"
version: 0.7
]
px: 9 * 1024
py: 11 * 1024
stride: 5
heading: 0
turn: 10
laby: [
[ 8 7 8 7 8 7 8 7 8 7 8 7 ]
[ 7 0 0 0 0 0 0 0 13 0 0 8 ]
[ 8 0 0 0 12 0 0 0 14 0 9 7 ]
[ 7 0 0 0 12 0 4 0 13 0 0 8 ]
[ 8 0 4 11 11 0 3 0 0 0 0 7 ]
[ 7 0 3 0 12 3 4 3 4 3 0 8 ]
[ 8 0 4 0 0 0 3 0 3 0 0 7 ]
[ 7 0 3 0 0 0 4 0 4 0 9 8 ]
[ 8 0 4 0 0 0 0 0 0 0 0 7 ]
[ 7 0 5 6 5 6 0 0 0 0 0 8 ]
[ 8 0 0 0 0 0 0 0 0 0 0 7 ]
[ 8 7 8 7 8 7 8 7 8 7 8 7 ]
]
ctable: []
for a 0 (359 + 180) 1 [ append ctable to-integer (((cosine a ) * 1024) / 10) ]
palette: [
0.0.128 0.128.0 0.128.128
0.0.128 128.0.128 128.128.0 192.192.192
128.128.128 0.0.255 0.255.0 255.255.0
0.0.255 255.0.255 0.255.255 255.255.255
]
get-angle: func [ v ] [ pick ctable (v + 1) ]
Plusieurs de ces valeurs sont "mises à l'échelle" par une multiplication par 1024. Cela permet au joueur de se déplacer sur chaque case plutôt que de sauter de case en case à chaque mouvement.
... avant de batir les murs
Pour dessiner à l'écran, vous avez besoin d'un fenêtre contenant une zone image. Il s'agit ici d'un LAYOUT nommé ecran. L'image utilisé comme surface de dessin se nomme DISPLAY et mesure 360 sur 200 pixels. Vous pouvez déjà en déduire que chaque rayon lancé aura une largeur de 4 pixels (360 / 90). Un effet GRADIENT permet de définir un superbe dégradé représentant le sol et le plafond de votre labyrinthe.

Gradient applique un dégradé à la surface de dessin
La fonction REFRESH-DISPLAY appelle le moteur de raycasting et fait apparaître le contenu de la zone de dessin.
refresh-display: does [
retrace
show display
]
ecran: layout [
backtile %marbre.jpg
display: box 360x200 effect [
gradient 0x1 0.0.0 128.128.128
draw []
] edge [
size: 1x1
color: 255.255.255
]
]
refresh-display
view/title ecran join "Raycaster " system/script/header/version
Le coeur de notre programme se trouve dans la fonction RETRACE. Une boucle FOR permet de parcourir les 90 angles pour lesquels vous réaliser un lancé de rayon. Une droite est tracée virtuellement à partir de la position du joueur et selon l'angle actif. A la première case non vide (sa valeur est différente de 0), la fonction calcule la hauteur du segment de mur et détermine la position et les dimensions du rectangle le représentant.

La hauteur est calculé symétriquement à la ligne d'horizon
Il ne vous reste plus qu'à récupérer la couleur dans PALETTE et à ajouter dans l'attribut EFFECT/DRAW de l'image, les instructions de dessin.
retrace: does [
clear display/effect/draw
xy1: xy2: 0x0
angle: remainder (heading - 44) 360
if angle < 0 [ angle: angle + 360 ]
for a angle (angle + 89) 1 [
xx: px
yy: py
stepx: get-angle a + 90
stepy: get-angle a
l: 0
until [
xx: xx - stepx
yy: yy - stepy
l: l + 1
colonne: make integer! (xx / 1024)
ligne: make integer! (yy / 1024)
laby/:ligne/:colonne <> 0
]
h: make integer! (900 / l)
xy1/y: 100 - h
xy2/y: 100 + h
xy2/x: xy1/x + 3
color: pick palette laby/:ligne/:colonne
append display/effect/draw reduce [
'pen color
'fill-pen color
'box xy1 xy2
]
xy1/x: xy2/x + 1
]
]

Votre labyrinthe vu de l'intérieur
Terminons par l'interactivité
Il ne vous reste plus qu'à gérer le clavier de façon à pouvoir vous déplacer dans votre labyrinthe. Pour cela, vous définissez une fonction EVT-KEY qui est insérée dans le gestionnaire d'événements. Selon la touche pressée (UP, DOWN, LEFT et RIGHT), vous appelez la fonction BOUGE-PERSO. Celle-ci valide le déplacement du joueur en vérifiant qu'il ne rencontre pas un mur et provoque le raffraichissement de la surface de dessin par l'appel de la fonction REFRESH-DISPLAY.
bouge-perso: function [ /recule ] [ mul ] [
either recule [ mul: -1 ] [ mul: 1 ]
newpx: px - ((get-angle (heading + 90)) * stride * mul)
newpy: py - ((get-angle heading) * stride * mul)
c: make integer! (newpx / 1024)
l: make integer! (newpy / 1024)
if laby/:l/:c = 0 [
px: newpx
py: newpy
refresh-display
]
]
evt-key: function [ f event ] [] [
if (event/type = 'key) [
switch event/key [
up [ bouge-perso ]
down [ bouge-perso/recule ]
left [
heading: remainder (heading + (360 - turn)) 360
refresh-display
]
right [
heading: remainder (heading + turn) 360
refresh-display
]
]
]
event
]
insert-event-func :evt-key
Conclusion
L'algorythme présenté ici n'est pas le plus efficace mais est probablement l'un des plus simples. Il reste de nombreuses possibilités d'optimisation et surtout, l'aspect visuel peut être grandement amélioré par l'utilisation de textures.
Olivier Auverlot