Carbon Workshop

le blog de carbon14.

Carbon WS - Level 7 WIP

Vendredi 29 mars 2013 à 17:45

Depuis un moment j’hésite à poster un article. Le projet continue d’avancer mais le lighting étant un dossier complexe, je préfère publier quelque chose de complet le moment venu. En attendant, un petit historique des réalisations des dernières semaines en vidéo :

YouTube Preview Image

Carbon WS - Level 6

Jeudi 24 janvier 2013 à 10:32

Vous l’attendiez depuis des semaines, le voilà enfin : le level 6 !

Pour commencer l’année, je voulais vous dire que je continuer de m’amuser sur ce projet. Même s’il est loin de l’idéal participatif que j’imaginais au début, je découvre et redécouvre un tas de trucs en construisant ce moteur. Et j’espère que toi lecteur tu prends plaisir à lire ces lignes, tout au moins y trouves-tu de l’intérêt.

Dans l’atelier d’aujourd’hui, il est question de colle. En effet pas de nouveauté, il s’agira avant tout d’utiliser de la glu pour assembler ce qui existe déjà.

YouTube Preview Image

Infos générales :

Dépôt : git://github.com/carbon-14/Carbon.git

Le système de ressources

Première couche de colle qui fait tenir le cycle de vie des meshes, textures, etc : gérer la création et la destruction des ressources. Ce qui est fait à la main pour l’instant.

Une ressource est une unité de donnée (data) préparée pour être utilisée par le programme. De la data se stocke en général en dehors du programme sous forme de fichier qu’on charge à la création de la ressource.

La caractéristique principale d’une ressource est qu’elle se partage. Deux objets différents peuvent utiliser une même texture par exemple. Pour ça, on utilise :

  • Les compteurs de ref, pour que la ressource connaisse le “nombre de ses utilisateurs”. Le principe : tant qu’une ressource est utilisée elle ne peut être détruite. En général ça va de pair avec la panoplie des “pointeurs intelligents”. J’ai ajouté par réflexe un SharedPtr qui s’occupe d’incrémenter/décrémenter le compteur de la ressource.

  • Une HashTable. Pour retrouver rapidement une ressources existante. La table fonctionne en deux temps : un accès direct via un hash-code dans un tableau de buckets puis une recherche linéaire dans le bucket. L’insertion et la suppression coûtent le prix d’un push dans un Array, et il y a la possibilité de dumper le contenu de la table.

Le chargement/déchargement des ressources s’effectue en différé via des stacks. Ça permet de regrouper le traitement des fichiers et de résoudre les cas “je détruis une ressource et je la charge pendant la même trame”.

Cas qui risque de devenir sensible : les dépendances de ressources. Un mesh va tirer les materials associés, qui vont tirer les textures associés, etc. Pour l’instant tout va bien puisque le chargement se fait en cascade. Ça se compliquera s’il devient important de threader le chargement des ressources.

Le système de materials

Autre type de colle, celle qui lie la géométrie (mesh) à ce qui y est dessiné (shader, paramètres de shader, textures) et qui constitue un material.

J’ai choisi un système assez fermé : à un shader correspond un ensemble de configurations fixes. Côté graphiste, ça signifie utiliser des banques de materials préétablies. Côté codeur, à un shader donné est associé une liste d’uniform buffers possibles.

Ces configurations sont définies dans des fichiers xml (data/materials/program.xsd et data/materials/level6.xml). Le MaterialCompiler (nouveau tool) prépare les uniforms buffers qui seront chargés en même temps que les shaders. Le système est prévu pour fonctionner avec 16 configs par shader, ce qui me semble suffisant, la diversité des materials étant amenée par les textures (les graphistes qui lisent confirmeront ?).

Au final, le material est une ressource référençant une config de program et des textures. Elle est générée à la compilation du mesh.

Pour supporter le système, on modifie le ProgramCache pour qu’il gère les configs : binder les samplers et l’uniform buffer d’une config à l’utilisation d’un program.

Deux objectifs : faciliter la réutilisation des uniforms buffers et cadrer le futur lighting.

Le RenderCache

Moins on dérange le RenderDevice et plus il est content. Pour éviter de l’appeler pour rien, par exemple en bindant des buffers déjà bindés ou des render states déjà utilisés, j’utilise un cache qui centralise ces appels. Actuellement, il gère quasi tout ce qui transite par le device : programs, render states, textures, uniforms et le clear. Vraiment utile pour les states, à tester pour le reste.

L’Application

Dernière couche de colle : la couche Application. Dans UnitTest, maintenant que le cadre est bien formé (et devient plutôt conséquent), il était utile de créer une classe Application de base qui initialise la fenêtre et le moteur.

Démo

Même scène qu’au level précédent, des materials appliqués à la cathédrale en plus.

La démo se télécharge ici : Level6.

Décompressez l’archive et lancez bin/win32/Retail/UnitTest.exe .

Config des touches :

  • F1, F2, F3 : mode demo, free cam, FPS
  • F4 : recompile les shaders
  • Z, Q, S, D : se déplacer
  • SPACE : sauter
  • SHIFT : courir
  • F : flash light

C’est une version Windows qui nécessite une carte supportant OpenGL 4.2.

Maintenant que le moteur graphique tient debout de lui-même grâce à toute cette bonne colle, les travaux d’expansion peuvent reprendre. Prochaine étape : le lighting.

YouTube Preview ImageYouTube Preview Image

Carbon WS - Dark World #1

Lundi 24 décembre 2012 à 1:07

Ah les fêtes de fin d’année. Qu’il fait bon coder en slip au coin du feu, une bière de Noël à la main, entre deux parties de Tribes ou NS2. En ces moments de paix universelle, il est bon se rappeler que dans le monde du jeu vidéo beaucoup n’ont pas la chance du codeur dont le boulot est essentiellement de promouvoir l’élévation de l’esprit par la mise au point de technos toujours plus sophistiqués. Je parle ici des designers, ces pauvres créatures démentes perdues dans le labyrinthe obscure des règles du jeu ( une pensée également pour tous les graphistes, esclaves ad vitam æternam bisous ).

Pourquoi la question du design tout à coup ? Et bien parce que petit à petit les choix côté dev seront de plus en plus dictés par le design. Exemple tout con : comment organiser la structure d’une scène si on ne connait pas sa composition ? Mal ou pas du tout en général.

Pour préparer le terrain, il n’y a donc d’autre choix que de franchir les portes du Dark World, le monde corrompu du game design.

La base

Honnêtement, je n’y connais rien en game design. Hormis mon sens du fun ( ha ha ), je ferai appel à des méthodes de codeur. Ce qui risque d’être épique… ou de se transformer en triste farce je ne vous le cache pas. Résumons les faits :

  • type de jeu : FPS

Mouais, léger. Mais c’est la base, l’essence même du jeu, et donc déjà tout un tas de règles préétablies.

On connait donc le point de vue du joueur. La première personne implique d’être au plus proche des sensations du joueur : connecter son cerveau au personnage… Pour moi, une partie du fun que procure un FPS vient de là : INCARNER un personnage virtuel et ce de manière VISCÉRALE. Pour imager, c’est le fun que je tire d’un Tribes quand je contrôle parfaitement la course de mon jugger d’un bout à l’autre de la carte en mettant une piche deux trois lights et placer mes mortars pile comme je voulais au moment prévu. L’interface homme/machine s’efface pour ne laisser que les sensations de jeu.

On connait également le type d’action à faire : SHOOTER ! Culturellement ça a des tas d’implications. Mais c’est démontrer le contrôle de mon avatar qui m’intéresse le plus. Vaincre un adversaire implique d’une certaine manière ma maîtrise du jeu. Mon fun viendrait du plaisir simple de posséder le jeu et d’en faire la preuve. Par extension, me dépasser, prouver mon intelligence, mes capacités d’anticipation, de réaction… Bref la base du jeu pour nous autres animaux.

En partant de ça, le FPS que je trouverais fun devrait me proposer de contrôler un avatar et d’en démontrer ma maîtrise au long d’un série de “tests”. C’est un peu là que ça se gâte. Cette définition simpliste ne s’applique pas vraiment à la réalité. La motivation du joueur dans le temps, au fil de ses expériences est obscure. Et le designer dont l’ultime but est de porter le fun au cœur de chacun s’avère, je le crains, peine perdue. Aussi les designers du monde entier s’évertuent jour après jour, élaborant maintes théories, sacrifiant maints poulets, à créer du bon jeu, avec pour objectif le saint Graal des quelques heures de jeu bien équilibré qui égaieront les mornes journées des pauvres quidams que nous sommes.

Tout cela fait abstraction de la composante “esthétique” et “narrative” des jeux contemporains qui pèse tout de même dans le plaisir prit lors d’une partie.

Voilà pour le blabla théorique. Vous excuserez la bière de noël…

Le concept

Là c’est un peu le quart d’heure confessions intimes. Je vais vous dévoiler mes inspirations, mes rêves, mes espoirs. Merci de ne pas trop rire dans les coms ( ha ha, sens du fun… ).

Si la théorie que j’ai développé plus haut est un point de départ, le jeu fun de BASE est un ensemble d’épreuves mettant à mal les capacités du joueur à contrôler son avatar et maîtriser le jeu afin de prouver sa “valeur” aux yeux “du monde” ( mon psy me souffle “de lui-même”… ). Et bien partons de là :

Primo, créer l’avatar, le système permettant au joueur d’incarner un personnage virtuel. Après deux décennies de FPS, le modèle de contrôle n’a que très peu évolué et on a aujourd’hui une jouabilité “standard”. Il faut toutefois nuancer. Ce n’est pas à vous lecteur de nofrag que je vais révéler la subtilité des déplacements d’un Quake 3 CPMA. Sans partir dans des extrêmes, mon FPS fun doit proposer plusieurs niveaux de maîtrise pour ses contrôles dans le but d’accentuer les sensations induites par le point de vue à la première personne. Donc un control set standard mais pensé pour pouvoir dépasser les capacités basiques de l’avatar via un ensemble de techniques. Ça me semble être une conclusion logique au vu des bases établies plus haut.

Secundo, les “défis” proposés au joueur. Le néant, la fin du monde tangible, l’immensité des possibles, LA question que se pose le designer : quel système de jeu ? Je ne suis pas designer, j’ai alors regardé l’infini droit dans les yeux et lui ai dis : je suis un singe qui tape sur un clavier. Et ainsi naquit la génération procédurale. Ça tient autant de l’expérience du programmeur fou que du manque de moyen : j’ai envie de tester plein de trucs et je n’ai absolument ni le temps, ni le talent de tout faire alors autant rester en terrain “connu” et coder un générateur de défis. Et tant qu’à faire, le mettre au centre du système de jeu.

Début type : je lance un partie seul ou je rejoins des potes. Une fois les critères de départs définis ( nombre de joueur, coop/compétitif, durée approximative de la partie ou d’autres ), le générateur de défis propose différents “scénarios”. Le scénar est un assemblage de briques de gameplay : type d’environnement, architecture, objectifs, arsenal, ennemis, items, bonus, malus, etc. Le tout est associé de manière cohérente, “narrative”, guidant la progression des joueurs. Ça peut paraître assez flou mais imaginez un Binding of Isaac sauce FPS et vous aurez ce qui se rapproche le plus de mon idée.

Techniquement c’est une nouveauté pour moi. Ce qui explique la naïveté avec laquelle j’exprime les choses. Ça promet d’être intéressant…

Premier article design. Ça lève le voile sur mes ambitions et ma petite vision du jeu final. Vos commentaires sont évidemment les bienvenus en particulier si vous avez des docs qui attestent ou réfutent ce que je dis.

Ps : pour rester dans la thématique, si vous postez un com vous devez proposer une brique de gameplay. A titre d’exemple, l’auto-degen : si vous restez sur place votre vie diminue. Elle descend d’autant plus si vous êtes accroupi.

Carbon WS - Level 5

Vendredi 14 décembre 2012 à 22:44

Dernier atelier de l’année consacré au MeshCompiler. Comme pour le TextureCompiler vu précédemment, c’est un outil permettant de préparer des données pour le moteur. Il s’agit cette fois de modèles 3D.

Infos générales :

Dépôt : git://github.com/carbon-14/Carbon.git

De Max à la carte graphique

Le parallèle entre la texture est le mesh se fait naturellement. Dans les deux cas la ressource de base est créé via un éditeur externe ( 3dsMax, Maya, Blender, etc ). On utilise, pour les mêmes raisons qui pousse à utiliser le png, un format d’export portable : COLLADA. Le COLLADA est un schéma XML qui contient toutes les infos nécessaires à la construction de vertex buffer et d’index buffer. Ces tableaux de données binaires sont directement exploitables par la carte graphique. Le MeshCompiler comme le TextureCompiler se charge de convertir le fichier exporté ( dae ) en un fichier de données bruts ( bmh, pour Binary MesH ). Et de même on en profite pour vérifier et optimiser le contenu du mesh :

Le MeshCompiler

Pas de surprise, le MeshCompiler reprend la même structure que le TextureCompiler : une appli console, indépendante du reste du moteur. Première étape : charger le fichier COLLADA. J’ai utilisé libxml qui tout comme la lib png est un bon gros standard. Je ne rentre pas dans les détails du parsing du fichier ( SAX toussa toussa ). Vient ensuite la phase de préparation de la ressource. Je crée le vertex buffer et les index buffers en tenant compte de différentes options : la compression des formats de vertex, la suppression des doublons et la génération des tangentes/binormales. Pour finir, je passe les indices dans une moulinette d’optimisation.

La compression de format, c’est diminuer le poids du mesh et adapter la précision de ses données à leur utilisation. Par exemple, on peut raisonnablement utiliser du float16 (half) à la place du float32 pour encoder les positions d’un vertex de la plupart des modèles 3D. Et cela s’applique à toutes les composantes du vertex : UVs ( 2 x int16 ), normales ( 2_10_10_10 ), couleur ( uint32 ). Un modèle avec position, coordonnées de texture, normales et couleur passe de 48 octets ( 12 + 8 + 12 + 16 ) à 18 octets ( 6 + 4 + 4 + 4 ) par vertex. De même les indices sont encodés dans un format adapté au nombre de vertices. Pour info, un lien vers les best practices dans l’utilisation des vertex buffers d’OpenGL.

La génération du tangent space, permet d’appliquer des coordonnées de texture dans l’espace du mesh ( cf. normal mapping ).

L’optimisation de l’ordre des triangles tente de trouver le meilleur ordre dans lequel sont rendus les polygones. En effet, la carte garde dans un cache les informations des derniers polygones utilisés. Du coup, il existe des algos qui vont essayer de maximiser l’usage de ce cache ( algo Forsyth ou Tipsify par exemple ).

Envoyer le mesh au shader

Là rien ne change par rapport à l’envoi de géométrie des précédents ateliers. On déclare correctement les vertex et index buffers et on récupère le tout au vertex shader avec le bon layout.

Maintenant qu’on a des textures, des meshes et un semblant de pipeline graphique, on peut commencer à s’amuser. Il restait à ajouter la gestion des constant buffers ( ou uniform buffers dans OpenGL ) pour envoyer des paramètres au shader ( matrice de camera entre autre ). Du coup démo :

YouTube Preview Image

Pour info, il s’agit du modèle de la cathédrale de Sibenik. Un standard à côté de l’atrium de Sponza pour tout ce qui est scène de test de global illum ( d’autres modèles ici ). Pour rajouter du détail, j’ai calculé avec Blender une valeur d’AO au vertex. Au centre de la scène, une sphère avec un lighting “complet” : diffuse, spéculaire, ambiante et émissive.

Vous pouvez tenter de l’essayer : Level 5 . Décompressez l’archive et lancez bin/win32/Retail/UnitTest.exe .

C’est une version Windows qui nécessite une carte supportant OpenGL 4.2. Il risque de vous manquer le vcredist 2010 pour la faire tourner ( x86, x64 ).

Config des touches :

  • F1, F2, F3 : mode demo, free cam, FPS
  • F4 : recompile les shaders
  • Z, Q, S, D : se déplacer
  • SPACE : sauter
  • SHIFT : courir
  • F : flash light

Voilà pour les outils de la lib graphique. Bosser sur cette partie est souvent gratifiant : le résultat est en général immédiat et visuel. Bref ça fait plaisir de voir des choses bouger à l’écran. Mais derrière ça, tout tient encore avec pas mal de scotch et de bouts de ficelle. Pour le prochain atelier, j’espère proposer une construction plus solide. Ce ne sera pas avant un mois, histoire de passer tranquillement la fin d’année. D’ici là, passez de bonnes fêtes et à plus.

PS : Comment je peux me faire spammer de pubs par cheap fake ray ban et consort alors qu’il n’y a pas moyen de poster un com “anonyme” avec le formulaire ci-dessous ?

Carbon WS - Level 4

Mercredi 28 novembre 2012 à 4:36

Suite de la lib graphique avec un article consacré aux textures. Et plus particulièrement à la préparation d’une image avant son utilisation par le moteur. Pour ça on ouvre un nouveau pan de la programmation de jeu : les outils.

Infos générales

Dépôt : git://github.com/carbon-14/Carbon.git

De ‘Toshop à la carte graphique

Je vais commencer par la fin : que mange la carte graphique ? Du tableau de pixels (ex: RGB standard = 3 x 8-bits de couleurs, encodés sur des char) ou plus communément appelé bitmap. Créer une texture OpenGL ou DirectX c’est créer une bitmap. Suffit ensuite de l’envoyer au shader (voir fin de l’article).

C’est bien gentil du bitmap brut mais on ne travaille pas avec ça. C’est inutilement lourd (ex : une image 512×512 RGBA = 512 x 512 x 4 octets = 1Mo!). Comme c’est un format directement accepté par la carte, ça reste utile comme fichier cache. Mais pour une image source, j’utiliserai le png qui, en plus de son offre de compression, accepte les formats de pixels utiles à un jeu (grayscale, rgb, rgb + alpha, 8-16 bits/canal) et est géré par la plupart (tous?) des éditeurs. Les formats spécifiques à un éditeur (psd ou xcf) sont exclus car ni portable, ni forcement ouvert, ni utile or contexte d’édition.

On a quasiment une chaîne de prod. Reste à faire le lien entre l’image source (png) et l’image cache (btx, pour Binary TeXture. Si vous avez mieux ? Tout format commencant par bite sera refusé… et dds c’est déjà pris…). Cette opération peut se faire offline via un tool : le TextureCompiler. Outre la conversion, cet outil va permettre de préparer, spécialiser les données pour leur utilisation future. Comme c’est un passage obligé pour toute texture, il sert également à certifier les données : contraintes de taille, de format, etc.

Le TextureCompiler

Ma conception des tools est qu’ils sont par nature totalement indépendants du jeu à l’exception de l’éditeur du jeu lui-même qui serait plus un mode de jeu “augmenté” ou un “god mode”. Pourquoi une séparation si stricte ? Parce que ce sont deux monde différents avec des objectifs et des priorités souvent opposés. Même la lib Core, comme j’ai insisté dans les premiers ateliers, est designée pour exécuter un jeu le plus efficacement possible. Dans le cas d’un outil, on utilise la lib standard qui elle a été conçue pour faire des applis robustes, fiables, normalisés, etc. Le lien entre tool et jeu est purement conventionnel. Dans le cas du texture compiler, le jeu utilise des textures dans un certain format et l’outil de son côté s’engage à produire des textures au format correspondant.

Le TextureCompiler prend la forme d’une appli console. Sa première tache est de convertir un png en bitmap. Le boulot est assuré par la lib png (j’ai repris texto la routine de chargement livrée avec les sources). Viens ensuite la phase de préparation proprement dîtes. Le rôle de certification est confié à OpenGL. L’idée est de créer l’objet GL texture puis de dumper son contenu dans l’image sortante. De cette manière, on s’assure qu’elle pourra être relue par le jeu. J’ai inclus trois “options” de compilation : le profil d’utilisation (couleur, linéaire et normale), la génération des mip-maps, et la compression de la texture.

Le profile d’utilisation permet de gérer à la fois la prise en compte du gamma grâce au format de texture sRGB (profile couleur) et le formatage des normal maps (profile normale). Pour info, la correction gamma permet d’adapter la précision de l’intensité du pixel en fonction de la perception humaine. En gros, notre œil perçoit mieux les détails dans les sombres que dans les clairs donc on code plus d’infos sur les bits “sombres” que sur les clairs en se servant d’une “fonction gamma”. Jusqu’à récemment, on négligeait la correction gamma sur toutes les opérations de blending, ce qui faussait les calculs et dégradait la qualité de l’image. Maintenant la carte s’occupe elle-même (si on lui précise) de corriger les couleurs au sampling d’une texture ou au blending.

La génération des mip-maps active la création des niveaux de détail de la texture. Pour info, le principe des mip-maps c’est de créer une collection de copies de l’image de base à des résolutions inférieures (1/2×1/2, 1/4×1/4, …). Pourquoi ? Pour respecter le pixel ratio ! Pour déterminer la couleur d’un pixel de l’écran grâce à une texture, il faut qu’un seul pixel de la texture ou deux au maximum y correspondent. Si pour un pixel de l’écran correspond plus de deux pixels, la carte va choisir au mieux mais visuellement ça se traduira par du bruit ou du moiré (sous-échantillonnage). La solution dans ce cas c’est d’aller regarder dans les mip-maps de plus faible résolution jusqu’à trouver un pixel convenable. Une pensée pour tous les “moddeurs” qui s’imaginent que plus grande est la texture meilleure sera l’image…

Enfin l’option de compression de texture active la compression par blocs (anciennement DXT sur D3D9, nouvellement BC sur les versions suivantes et encore d’autres noms dans l’équivalent OpenGL). Le principe c’est d’encoder l’image non pas pixel par pixel mais par blocs de 4×4 pixels. On diminue le poids de l’image, on perd en qualité de couleur/d’information mais on gagne en perfs au shader (le bloc est utilisé comme un cache localisé). Il est conseillé de compresser ses textures.

Envoyer la texture au shader

Cette partie est encore en wip. Elle ne concerne pas uniquement les textures mais tous les types de paramètres que l’on peut envoyer au shader. Pour l’atelier j’ai implémenté la création des textures et des samplers (objet GL qui permet d’”échantilloner” la texture, d’extraire une valeur selon des uvs). J’ai rajouté un tableau de textures et samplers dans le RenderElement. Avant l’appel au Draw, je bind les textures/samplers. L’adressage (layout) se fait au shader (je profite des améliorations d’OpenGL 4.2 pour pas me fouler cette fois). Le reste c’est du GLSL (level4.fs).

Le prochain atelier portera également sur un tool : le MeshCompiler qui s’occupe de préparer les géométries. C’est peut-être un peu relou de passer par ces étapes mais je tenais à montrer qu’il est important de préparer ses datas, de prendre en considération l’organisation d’une chaîne de prod et de tout ce que cela implique avant de foncer tête baisser dans la lib Graphic. En passant, je me fais spammer de commentaires “Indésirables” mais pas moyen de poster en “invité” sur le blog. J’ai loupé un truc ? Sinon comme d’hab toute réaction est la bienvenue. A plus.

Carbon WS - Level 3

Vendredi 16 novembre 2012 à 20:14

Au vu du succès du précédent atelier, je transforme la formule en devblog classique, toujours orienté prog, et rien n’empêche un débat de commencer si le sujet et la motivation le permettent. En gros, je posterai plus régulièrement pour commenter l’avancement du projet dont l’objectif, lui, ne change pas.

Cette semaine on passe littéralement au niveau supérieur puisqu’on commencera la librairie graphique. Et pour clore les ateliers précédents sur la librairie Core, je vous conseille la lecture de Game Engine Architecture, un excellent bouquin avec l’essentiel de la base par Jason Gregory le directeur technique chez Naughty Dog depuis Uncharted.

Infos générales

Dépôt : git://github.com/carbon-14/Carbon.git

La lib Graphic c’est quoi ?

La lib graphique ( ou lib rendu ) concentre toutes les fonctionnalités spécifiques à l’affichage. Quel est l’intérêt d’en faire une librairie autonome ? Ça permet de développer en faisant abstraction du reste du jeu. La librairie est donc réutilisable pour d’autres applications ( un éditeur ou un autre jeu/moteur par exemple ). De même Le jeu peut utiliser la lib rendu sans se soucier de son implémentation : on utilise la même “interface” quelque soit la plateforme par exemple. Tout ça sans compter les avantages liés au quotidien du dev : temps de compil’ et de link (dll). Et ce principe s’applique aux autres domaines : audio, physique, réseau, IA…

Pour résumer le modèle :

  • Application : Là où est codé le jeu. ( UnitTest )
  • Specific : Les libs spécifiques comme la librairie graphique. ( Graphic )
  • Core : La lib “utilitaire” qui sert pour tous les niveaux au-dessus. ( Core )

Comment afficher un truc ?

Pour afficher un truc à l’écran, il faut savoir parler à votre carte graphique. C’est ce que font OpenGL et DirectX : proposer une interface homogène pour parler à tous les modèles de carte. Pour des raisons de portabilité, on se servira d’OpenGL ( car comme vous le savez, DX n’est pris en charge que par Windows et encore si vous tournez toujours sous XP, vous ne bénéficiez pas des dernières versions ). Aujourd’hui, il y a peu de différences fonctionnelles entre ces deux libs. Aussi si vous passez de l’une à l’autre vous retrouverez plus ou moins les mêmes notions.

Voilà grosso-modo le process d’affichage d’une carte graphique :

  • En entrée, des vertices ( un vertex, des vertices ) en provenance de votre ram via le bus PCIe. C’est généralement un tableau de “positions” mais ça peut être un tableau de n’importe quelle info ( couleur, uvs, normale, etc ).
  • Le vertex shader est un programme qui transforme chaque élément du tableau de vertices en une position en coordonnées écran.
  • Primitive assembly est l’étape qui regroupe les vertices transformés en triangles.
  • La rasterization transforme un triangle en fragments ( correspond à un “super pixel” ).
  • Le fragment shader, est un programme qui transforme le “fragment entrant” en un “fragment final” contenant la valeur du pixel de l’image ( une couleur par exemple ).
  • Raster operations désigne des tests ( culling, stencil ) et le blending ( mixage avec l’image finale ).
  • En sortie, un tableau de pixels ( aka image ) qui peut être envoyé à l’écran.

Et concrètement ça donne quoi ?

Premièrement, on doit créer une “application fenêtrée”. C’est toujours la même chose sous Windows ( allez voir Level3.cpp ).

Ensuite on veut afficher un truc simple : les coordonnées de l’image. En rouge selon l’axe X, en vert selon l’axe Y.

Initialiser OpenGL

Comme Windows a arrêté le support d’OpenGL à la version 1.1 ( on en est aujourd’hui à la version 4.3 ), son initialisation est souvent confié à d’autres libs. Mais si on prend la peine d’y regarder de plus près, on se rend compte qu’il n’y a aucune difficulté à le faire soi-même ( OpenGL.inl et OpenGL.cpp ). Ça se passe en 2 étapes : créer le context ( fonction CreateContext/DestroyContext ), et charger les fonctions manquantes en appelant wglGetProcAddress. Rien de compliqué, c’est juste chiant d’appeler toutes les fonctions dont a besoin l’appli.

L’interface

Encore une fois, plutôt que d’appeler directement les fonctions GL natives, je passe par une interface : RenderDevice. C’est toujours pour garder la possibilité d’avoir des implémentations différentes mais aussi pour écrire des fonctions plus haut niveau et ajouter une couche de gestion ( Init, Update, Destroy ).

Les vertex arrays

Le vertex array est un objet GL qui est exactement ce qui le désigne : un tableau de vertices. Leur création/destruction se fait via CreateVertexArray/DestroyVertexArray dans RenderDevice. Je ne rentre pas dans le détail, le wiki officiel le fait très bien.

Les shaders

L’étape suivante dans le pipeline de rendu. Comme vu plus haut, les shaders sont des programmes faits pour tourner sur la carte graphique. Pour afficher l’image, il faut un vertex shader et un fragment shader. Ce sont des fichiers sources et comme les sources d’un programme C, il faut les compiler. C’est ce que font CreateProgram/DeleteProgram dans RenderDevice. Comme il s’agit de manipulation de fichiers et d’opérations potentiellement couteuses, j’ai regroupé la gestion des shaders dans un ProgramCache. Le ProgramCache centralise le chargement des shaders, la génération et l’accès aux programmes. Plus, le résultat de la génération du programme est stocké sous forme binaire dans un fichier cache pour être rechargé directement au lancement suivant.

Le rendu

Un vertex array, un programme (shader) et un type de primitive (triangle) forment une “unité de rendu”, un objet affichable par la carte graphique. Et dans un jeu, il y en a un paquet à afficher. En prévision, je regroupe ces objets dans une structure RenderElement et j’utilise la classe RenderList pour les stacker, les trier et les afficher.

Pour cet atelier, j’utilise la classe FullScreenQuadRenderer qui permet de :

  • Créer le RenderElement pour afficher un rectangle plein écran.
  • Ajouter l’élément à une RenderList globale.

L’image dessinée est actualisée en permanence dans une boucle infinie répétant jusqu’au signal d’interruption la même séquence :

  • FullScreenQuadRenderer ajoute le rectangle plein écran à la liste.
  • La liste affiche son contenu puis se vide.

Voilà pour une entrée en matière de la lib graphique. Ça fait un peu cours magistral mais pour le prochain atelier il devrait y avoir quelque chose de mieux qu’un dégradé de couleur à afficher. Vous pouvez poster vos coms ici, j’ai découvert l’option pour les rendre accessibles à tous…

Carbon WS - Level 2

Mercredi 31 octobre 2012 à 20:15

Nouvel atelier cette semaine consacré à la lib de math. Comme pour le premier, il s’agit d’un sujet de programmation général.

Infos générales

Le principe : 2 semaines pour aborder ce thème du point de vu du programmeur de jeu. Se poser les questions nécessaires. Et y répondre de façon concrète dans le code.

De quoi a-t-on besoin ?

En terme de maths, on manipule essentiellement des réels donc des valeurs de type flottant. Et en particulier des float 32-bits. A mon sens, les seuls cas d’utilisation de double (64 bits) concernent des problèmes de précision dans les calculs physiques et la définition de positions sur une scène gigantesque. En général 32 bits suffisent amplement.

En plus des opérations de base sur les flottants (arithmétiques, comparaisons), on a besoin des fonctions trigonométrique (sin, cos, tan et réciproques) et exponentielles (sqrt, exp, log). La trigo ça sert partout… Sqrt sert à calculer des normes de vecteur. Exp et Log sont beaucoup moins utiles mais il arrive d’avoir besoin d’une échelle logarithmique ou de faire des calculs de dispersion…

Faire un FPS en 3D implique des objets mathématiques appliqués à la 3D : vecteurs et matrices. Plus les quaternions qu’on utilise en physique et en animation.

Et… c’est tout. Enfin plus ou moins. Que pensez-vous de la sphère, du plan, ou de la box ? Est-ce que ce sont des objets mathématiques à part entière ou bien une application directe des objets cités ci-dessus ? Par exemple : la sphère peut être définie comme une distance entre son centre et tout autre point. Partant de là, est-ce qu’un frustum (qui en 3D définit le volume vu par une caméra) fait parti de la lib de math ? D’après mon expérience : ça dépend. Ça dépend essentiellement de l’utilisation qu’on en fait. Si on reprend notre sphère (qui êtes aux cieux), pour définir un objet “physique/dynamique” j’aurais besoin d’une sphère orienté dans l’espace, tandis qu’une sphère englobante pour des tests d’occlusion peut se contenter d’être une distance par rapport à un point. Et tout ça impacte aussi l’organisation des données qui peut être très ouverte s’il y a besoin d’un accès régulier ou bien au contraire fermée/compacte/optimisée pour un calcul particulier. Bref j’aurais tendance à dire : si tu peux pas créer un objet global satisfaisant, ne fait pas un objet global.

Mon point de vue

Il est essentiel de concevoir une lib simple. Rien de plus facile que se perdre dans le formalisme des maths. On a vite fait de coder une lib à base de template qui, à partir du paradigme math, permet de générer une foultitude d’objets inutiles. C’est beau les maths…

J’ai implémenté une première version de la lib. Le code est disponible sur le dépôt : Math.inl, Intrinsics.inl, Vector.h, Matrix.h, Quaternion.h.

J’ai pris comme modèle la lib math utilisée pour les shaders (hlsl, glsl) : uniquement les types de base et une API C-style infaillible. Je vais encore plus loin dans le concept en ne laissant comme type que le float 32-bits, le Vector 4 x 32-bits et la Matrix 4 x 4 x 32-bits. Ça évite un tas de conversions potentielles et comme on le verra ci-dessous, le format des données est important.

Concrètement, la librairie standard (cmath) propose les fonctions auxquelles on s’attend sur des flottants (et rien de plus ni de moins pour une fois). J’ai juste interfacé les fonctions que je voulais. NB. Par principe, portabilité oblige, j’interface toujours les fonctions provenant d’autres libs.

Pour les vecteurs, j’utilise les instructions SIMD qui permettent de faire du calcul vectoriel sur : des tableaux de 4 floats (sse2)… Les choses sont bien faites ! Pour faire très simple, il existe une instruction (et bien d’autres) qui additionne les quatre floats du tableau pour le prix d’une. Voilà pour les avantages. Pour les inconvénients : les données doivent être alignés sur 16 octets, l’accès individuel aux éléments du tableau ne va pas de soi et pour finir on est dépendant de l’archi. Ça reste tout de même très avantageux. Même principe pour les matrices qui sont des tableaux de quatre vecteurs.

J’ai décidé de ne pas encapsuler le vecteur dans une classe. Je conserve le concept de manipuler directement un type intrinsèque. L’API reprend les fonctions standards pour les flottants plus des fonctions spécifiques ( produit scalaire, produit vectoriel, normalisation… comme pour les langages de shader ). J’ai laissé les fonctions de trigo et exponentielles en attente (elles ne sont pas dispo de manière native et les fonctions de la lib standard utilisent déjà les instructions SSE… à voir). Je vous renvoie vers cet article qui explique quelques subtilités (plus forcément d’actualité d’ailleurs mais toujours intéressant).

Enfin, la matrice est un tableau de quatre vecteurs (colonnes) dans une struct. Quant aux quaternions, ce sont des vecteurs déguisés.

Les maths c’est funtastique !

L’heure est venue de mettre cette lib au service du jeu vidéo.

Pour tester ces objets mathématiques, j’ai mis en place la chaîne d’opérations qui transforme un polygone, tel qu’il existe dans un modeleur 3D, en un polygone tel qu’il est à l’écran. C’est grosso modo ce que fait la lib rendu et un vertex shader sur votre carte vidéo une chiée de fois par seconde : convertir des coordonnées locales en coordonnées écran.

Vous pouvez trouver ce code dans le fichier Level2.cpp.

J’espère que ce second atelier saura susciter votre intérêt. Vos questions, commentaires, réactions, tout ça dans le nouveau forum. Et comme je sais qu’il est difficile à cet instant de se concentrer sur ce que j’écris, je vous laisse.

Carbon WS - Level 1 - Checkpoint #2

Lundi 29 octobre 2012 à 19:35

Après une semaine un mois sans nouvelle, je fais le point sur l’atelier.

Tout d’abord, merci à la dizaine de courageux participants. Pas évident de démarrer sur ce genre de sujet. Le débat a d’ailleurs plutôt tourné autour (quel est l’intérêt de réinventer la roue :)) et n’est jamais vraiment rentré dans le vif : quelle STL et quels allocateurs mémoire pour un jeu vidéo ? De mon point de vu c’est dû à un manque de préparation, un sujet pas forcement sexy et un manque précision dans l’énoncé. J’essaierai de mieux amener les choses à l’avenir. L’expérience est tout de même instructive et permet d’adapter la méthode :

  • Plus de préparation. Plutôt que balancer le sujet uniquement soutenu par une vision large du problème, je les traiterai en amont pour proposer en même temps une version du code et la réflexion qui m’y a mené. J’espère que ça permettra de mieux cadrer l’atelier. Je démarre avec un sujet d’avance. On verra si ça permet de tenir le rythme d’un sujet toute les deux semaines.
  • Des sujets “orientés” jeu vidéo. Comme on est encore au fond du moteur, il n’est pas évident de trouver un lien entre les sujets et la finalité du projet. Je vais m’employer à présenter les sujets et mettre en pratique les éléments développés tels qu’ils le seraient dans un jeu.

Pour soutenir votre participation, j’ai ouvert le site http://carbon.alwaysdata.net. Il s’agit d’un forum ouvert à tous. Si vous souhaitez vous exprimer sur le projet, faites-le là bas. Ça permet de centraliser les choses. Merci.

Une rapide présentation de ce qui a été développé pour ce premier atelier :

  • Concernant la STL, je suis resté sur mon idée de base : 1 container, le tableau dyna. Ça nous donne la classe générique Array dont l’implem est une version simplifiée du vector de la EASTL. J’ai également rajouté la classe FixedArray, la version d’Array qui utilise un tableau de taille fixe et ne requière pas d”allocation dynamique. Quant aux algos, on complètera au fur et à mesure.
  • Pour les allocs custom, j’ai uniquement implémenté l’allocateur rapide qui est flushé en fin de trame. Pour l’allocateur lent, j’ai conservé les fonctions natives. Le tout peut être appelé via le MemoryManager qui centralise les allocations et permettra plus tard de prévenir les fuites de mémoire.
  • A cela s’ajoute des devs secondaires utiles pour les objets ci-dessus ou pour le débogage : MemoryUtils, TimeUtils, StringUtils…

Je vais maintenant répondre à la question qui a hanté le forum tout au long de l’atelier : “Pourquoi tu t’acharnes à refaire ce qui a déjà été fait par des générations de codeur avant toi ? Utilises la EASTL t’en as bien recopié la moitié dans ton code moisi !” Et c’est tout à fait vrai ! J’aurai très bien pu intégrer cette lib aux sources. Mais c’est oublier la raison d’être de ce projet : faire partager le point de vu du dev de jeu. Le but “caché” était qu’à travers la conception de ces éléments de base de la programmation, on arrive nécessairement à plusieurs règles d’or sur sa façon de coder comme : toujours analyser ses besoins et privilégier une solution adaptée à une solution full générique (prévenir l’over-engineering), évaluer sa consommation en mémoire et éviter les allocations dynamiques inutiles. Je trouvais que pour un premier atelier, ce genre de rappel était bonne idée. Désolé de ne pas avoir réussi à vous y amener.

Quoiqu’il en soit, je clos ce Level 1. Dites-moi s’il est nécessaire de faire un article de synthèse plus consistant. Le prochain atelier ouvrira cette semaine avec comme sujet la lib de maths. J’attends vos retours sur http://carbon.alwaysdata.net . Comme d’hab, tout commentaire est le bienvenu.

Carbon WS - Level 1 - Checkpoint

Mardi 25 septembre 2012 à 16:42

Bonsoir [STOP]
Fin de mois => rush [STOP]
Article suivra dans semaine [STOP]
http://carbon.alwaysdata.net [FIN DE TRANSMISSION]

Carbon WS - Level 1

Mardi 18 septembre 2012 à 16:19

Premier atelier cette semaine, et on commence dans la violence tranquillement avec des classiques de la prog: la STL et les allocateurs mémoire. Des thèmes tellement discutés que la matière abonde sur le net et qu’on n’aura aucun mal, je l’espère, à remplir le forum. Bref de quoi se roder avant d’attaquer des problématiques propres au jeu vidéo.

The journey so far.

L’atelier se déroule sur deux semaines:

  • Semaine 1 - Discussion dans le forum devant aboutir à une première version du code.
  • Repoussage de la version sur la base en ligne
  • Semaine 2 - Itération sur le code.

La chaîne de dev mise en place la semaine dernière (Level 0) est dispo pour tester vos idées et venir les partager sur le forum. Quelques modifications depuis: l’appli de test (UnitTest) est maintenant structurée en “levels” pour y faire correspondre les ateliers et j’ai également rajouté quelques macros d’assert.

Infos générales

Dépôt: git://github.com/carbon-14/Carbon.git
Forum: http://forum.nofrag.com/forums/debats/topics/621804

Go Go Go !!!

Ce que je vous propose de coder dans ce premier atelier n’est pas un exercice de style. Au contraire, les containers STL et les allocateurs sont vraiment des objets fondamentaux. On y fait appel souvent et de partout alors mieux vaut qu’ils soient bien conçus. C’est aussi une bonne manière de mettre en pratique les règles de base de la prog “bas niveau” et celle qui prévaut devant toutes dans un jeu: FAUT QUE CA BOURRE!!!

La STL

Je ne vais pas vous apprendre ce qu’est la Standard Template Library. Vous utilisez sûrement sans y faire attention celle de Microsoft ou consultez-vous les références de la SGI ou encore connaissez-vous l’implémentation d’EA. Bref, la STL existe dans le standard (ou imaginaire collectif) mais en réalité chacun fait sa propre cuisine. Et je ne dérogerai pas à la règle avec ma proposition d’implémentation de la STL:

  • 1 seul container: Array ou tableau dynamique, alias vector

Le seul objet de la STL que l’on utilise réellement d’après mon expérience. Enfin, je considère que string et hash_map sont des cas particuliers d’Array (string étant un tableau de caractères + des fonctions utilitaires, et hash_map un tableau de pairs clef/valeur avec des fonctions d’insertion et de recherche spéciales). Les avantages proviennent du fait qu’on alloue un gros bloc mémoire: pas ou peu d’allocs, pas de fragmentation de la mémoire, indexation des données. Pour résumer, c’est une couche de gestion des tableaux C (dynamiques ou statiques) et à mon humble avis ça suffit pour un jeu.

Les allocateurs mémoires

Pourquoi faire des allocateurs mémoire alors que le système fait déjà tout ça? PARCE QUE CA BOURRE!!! Plus sérieusement, même si les codeurs de kernel savent ce qu’ils font, un syscall coûtera plus cher que des allocateurs adaptés au contexte d’utilisation. En particulier dans le jeu vidéo où la taille disponible est souvent limitée et où une partie des objets ont la durée de vie d’une trame. On ne va pas non plus réinventer la roue et se restreindre à ce qui se passe au-dessus d’un appel à malloc: j’alloue un gros bloc mémoire et je le gère de manière à nourrir mon appli au mieux (au plus vite). Ma proposition:

  • Allocation de toute la mémoire nécessaire au jeu.
  • Un allocateur qui utilise le début du buffer et un autre la fin.
  • Le 1er est l’allocateur “classique”, “lent”, avec le comportement générique du malloc (à base de pools ou de pages).
  • Le 2nd est l’allocateur “rapide” pour les objets de trame. Il se comporte comme une stack (LIFO) et est flushé en fin de trame.

Voilà pour un comportement général qui tient compte des restrictions typiques. L’idée est de trouver l’allocateur convenant le mieux à chaque grand type d’utilisation sans tomber dans l’ultra spécialisation et la multiplication d’allocateurs.


Je tiens à préciser que mes propositions ne sont que des points de départ pour la première phase de l’atelier. N’hésitez pas proposer vos propres solutions et à partager vos sources de doc. De même si vous trouvez un bout de code russe révolutionnaire ou que vous venez de craker le code de Carmack, viendez nous en faire profiter! Tous les coups sont permis. On en discute et on fait progresser les choses.

Premier test pour ces ateliers. La formule est en rodage. On se retrouve sur le forum pour parler de tout ça. Tout commentaire, remarque, participation est le bienvenu.