SelixyCARD
Jeu de cartes type UNO en 3D dans le navigateur, moteur Rust en wasm
Je voulais un jeu de cartes dont les cartes soient celles des joueurs, et un rendu 3D sans passer par un moteur existant : j’ai écrit le mien. Le projet est en cours, le premier commit date du 19 octobre 2026 et l’essentiel du moteur de table s’est fait dans les jours qui ont suivi.
Le jeu et les sets
Un set compte 108 emplacements aux identifiants fixes, comme un paquet de UNO. Une page de bibliothèque permet de les illustrer avec un éditeur à recadrage 2:3, et une carte sans image reçoit un visuel par défaut. Un cadre commun (bord de couleur, valeur dans les coins) est dessiné par-dessus chaque illustration, donc toutes les cartes restent lisibles.
Les règles de base et les options sont écrites une seule fois, dans un module Rust (commun) exécuté par le serveur, qui est le seul juge de la partie. Les options couvrent entre autres le cumul des +2 et +4, la contestation du +4, le saut en cours de tour (jump_in), la règle du 7 et du 0 et la pénalité de UNO. Le générateur de mélange est un xorshift maison : à graine égale, la partie se rejoue à l’identique, ce qui sert à reproduire un bug.
Le salon permet de créer ou de rejoindre une partie par WebSocket, la connexion se rétablit seule en cas de coupure. Des robots peuvent compléter la table : ils tournent dans le navigateur de l’hôte du salon et jouent par la même WebSocket qu’un joueur.
Le moteur 3D en Rust et wasm
Le moteur ne s’occupe que du rendu, de l’animation et du pointage : le JavaScript garde la WebSocket, l’état de la partie et l’interface. Le rendu passe par wgpu, avec des shaders en WGSL, en WebGPU quand le navigateur le permet et en WebGL2 sinon. Le JSON de l’état de la table est lu par un analyseur écrit à la main, pour ne pas embarquer serde dans le wasm.
Quelques choix :
- toutes les cartes sont posées à plat et se superposent par décalage en hauteur, avec une caméra de table basse : un éventail de cartes inclinées se traverserait au z-buffer ;
- chaque carte rejoint sa pose cible par lissage exponentiel, indépendant du débit d’images, avec des trajets en arc pour les longs déplacements ;
- le pointage ne lance pas de rayon : il projette les coins des cartes et teste le quadrilatère, de la carte du dessus à celle du dessous ;
- l’éventail de la main s’adapte à la place visible, notamment en portrait sur téléphone, où la caméra se rapproche ;
- les textures sont un tableau de 128 couches de 256×384, plutôt qu’un atlas : pas de calcul d’UV par case, pas de bavure entre cartes.
Rendu et décor
Le plan de travail est une ardoise générée dans le shader, avec des traits de craie, sans texture, donc sans étirement à l’écran. Les cartes projettent des ombres (shadow mapping en 2048, filtrées en PCF), et une carte jouée soulève un nuage de poussière de sa couleur à l’impact. Un joker posé en souffle un plus dense et laisse un dépôt de poudre de la couleur choisie.
La lueur autour de la main ou de la pioche est calculée dans l’espace écran : on dessine les formes dans un masque, on en tire une distance au contour, puis on peint un dégradé qui part du contour. Il n’y a ainsi aucun trait entre deux cartes voisines. Limite connue : près de la frontière, un objet incliné presque à la même profondeur que la sélection peut être mal classé.
Les fonds des pages sont des rendus du même moteur, bakés par outils/cuire-fond.js. Un second module, graphe_pbr, décrit des matériaux sous forme de graphe de nœuds, traduit en GLSL et en WGSL.
Serveur et déploiement
Le serveur est en Rust (Axum) avec PostgreSQL : comptes, sets, images et salons. Les images sont envoyées en WebP 512×768 et adressées par leur SHA-256, ce qui permet de les servir avec un cache immuable. Le tout est livré dans un pod Podman (serveur et base) derrière nginx, par un simple git push.
Le dépôt suit des règles strictes que je m’impose : 200 lignes maximum par fichier, 5 fonctions par fichier, aucun commentaire dans le code, le pourquoi vivant dans le README de chaque module. Un script d’audit les vérifie à chaque commit, avec les tests Rust et JavaScript.