Programmer sur Nintendo 3DS

Votre premier homebrew

Alexandre Laurent

Un tutoriel de découverte sur la réalisation de programmes et jeux pour la Nintendo 3DS et ses variantes. Ainsi, vous y apprendrez à installer les logiciels nécessaires pour compiler le programme et vous réaliserez votre première application 3D pouvant fonctionner sur la console.

Commentez

- Titre : Programmer sur Nintendo 3DS

- Auteur : Alexandre Laurent

- Parution : 23 août 2018

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2018 Alexandre Laurent. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Caractéristiques techniques

- II. Installation

- III. Première compilation

- III-A. Tester

- IV. Découverte de l’écosystème

- IV-A. Shaders

- IV-B. Organisation d’un projet

- IV-B-1. Personnalisation du Makefile

- IV-B-2. Shaders

- IV-B-3. Textures

- V. Première application

- V-A. Hello World

- V-A-1. Détection des entrées utilisateur

- V-B. Cube

- V-B-1. Double écran

- V-B-2. Effet 3D

- V-C. Lumière

- V-D. Texture

- V-D-1. Mise en place de la texture

- V-D-2. Initialisation de la texture

- V-D-3. Utilisation

- VI. Conclusion

- VII. Pour aller plus loin

- VIII. Remerciements

I - Introduction

Afin de pouvoir exécuter ses propres programmes sur la Nintendo 3DS (ou ses variantes), il est nécessaire d’avoir une console pouvant exécuter le Homebrew Launcher (ou un linker). Toutefois, vous pouvez utiliser l’émulateur Citra pour tester vos programmes sur votre ordinateur.

L’auteur utilise un PC sous Linux comme machine de développement. Il est totalement possible d’utiliser un Mac ou un PC sous Windows : ce choix n’est qu’une préférence d’utilisation.

I-A - Caractéristiques techniques

Voici un petit rappel des caractéristiques techniques de la console. La Nintendo 3DS (et la Nintendo 2DS) embarque un ARM11 MPCore cadencé à 268 MHz (contre un quad core cadencé à 804 MHz pour les versions « New ») sur lequel s’exécute les programmes. De plus, les graphismes sont gérés par une puce PICA200 de DMP cadencée à 204 MHz. Finalement, les versions classiques embarquent 128 Mo de FCRAM avec 6 Mo de VRAM, contre 256 Mo et 10 Mo de VRAM pour les versions « New ».

Plusieurs détails sont à retenir quant à la programmation graphique avec le PICA200. Celui-ci est compatible OpenGL ES 1.1 et supporte les vertex shader. Toutefois, le fragment shader n’est pas programmable mais simplement configurable. La puce supporte l’éclairage par pixel calculé à partir du modèle Phong, les textures procédurales, le rendu des ombres et des objets gazeux.

Il est nécessaire de connaître la programmation avec OpenGL afin de réaliser des applications graphiques sur la Nintendo 3DS. Je vous conseille la lecture de la première partie de ce guide.

II - Installation

Sous Linux, il suffit d’exécuter le script devkitARMupdate.pl, servant aussi bien de script d’installation que de script de mise à jour. Le kit de développement s’installera dans votre répertoire personnel.

À la fin, vous devez définir les variables DEVKITPRO pour pointer sur le dossier ~/devkitPro et DEVKITARM sur le dossier ${DEVKITPRO}/devkitARM.

Sans ces variables, la compilation à travers les Makefile ne fonctionnera pas, car il ne pourra pas retrouver les outils de compilation.

III - Première compilation

Avec le devkitARM, vous avez aussi récupéré des exemples (~/devkitPro/examples) et parmi ceux-ci, un template pour une application (~/devkitPro/examples/3ds/templates/application) et un autre pour une bibliothèque (~/devkitPro/examples/3ds/templates/library). En lançant le Makefile du template pour l’application, vous aurez un programme, dans plusieurs formats :

- elf : format binaire, qui peut être lancé avec l’émulateur ;

- 3dsx : l’exécutable, qui peut être lancé avec l’Homebrew Launcher (ou l’émulateur) ;

- smdh : fichier de métadonnées et pour l’icône (peut être intégré au fichier .3dsx).

III-A - Tester

Vous avez deux possibilités pour tester. La première, rapide est d’utiliser l’émulateur 3DS Citra. Avec l’émulateur, vous pouvez aussi bien charger le fichier .3dsx que le fichier .elf.

Sinon, vous pouvez copier les fichiers .3dsx et .smdh sur une carte SD et les charger avec l’Homebrew Launcher.

IV - Découverte de l’écosystème

Lors de l’installation de devkitARM, le script a récupéré deux importantes bibliothèques :

- libcrtu : bibliothèque pour écrire des applications ARM11 dans l’espace utilisateur de la Nintendo 3DS. Grâce à celle-ci, vous aurez accès à des fonctions pour récupérer les interactions avec l’utilisateur (écran tactile, boutons…), pour le système de fichiers, pour le lecteur NFC… La documentation est disponible en ligne ;

- Citro3D : bibliothèque pour accéder au GPU PICA200 reprenant le design d’OpenGL. La documentation doit être générée par vous-même avec Doxygen en récupérant les sources.

La documentation existe, mais pour Citro3D, elle est plutôt légère. Il sera donc nécessaire d’avoir une connaissance d’OpenGL et la bonne méthode pour apprendre sera d’explorer les programmes d’exemples accompagnant devkitARM.

IV-A - Shaders

Dans les exemples fournis avec devkitARM, on remarque que les shaders sont en assembleur et non dans un langage haut niveau comme le HLSL ou le GLSL. Ces fichiers sont traités par picasso (installé avec devkitARM) et la documentation du langage supporté est consultable sur GitHub.

IV-B - Organisation d’un projet

Les sources doivent être dans le dossier source et peuvent aussi bien avoir l’extension .c ou .cpp. Vous pouvez mettre vos fichiers d’en-têtes dans un dossier include. Les shaders doivent avoir l’extension v.pica (vertex shader) ou g.pica (geometry shader).
Les fichiers de données binaires doivent utiliser l’extension .bin et être dans un dossier data.

Les fichiers de données doivent être intégrés à l’exécutable, faute de moyen de les charger à partir de la carte SD.

IV-B-1 - Personnalisation du Makefile

Le Makefile proposé dans les programmes d’exemples est aussi générique que possible. Il compilera vos fichiers sources et vos shaders en suivant les règles énoncées ci-dessus.
Il est toutefois utile de modifier quelques variables, que l’on retrouvera documentées, dans la première partie du fichier :

#---------------------------------------------------------------------------------
# TARGET is the name of the output
# BUILD is the directory where object files & intermediate files will be placed
# SOURCES is a list of directories containing source code
# DATA is a list of directories containing data files
# INCLUDES is a list of directories containing header files
#
# NO_SMDH: if set to anything, no SMDH file is generated.
# ROMFS is the directory which contains the RomFS, relative to the Makefile (Optional)
# APP_TITLE is the name of the app stored in the SMDH file (Optional)
# APP_DESCRIPTION is the description of the app stored in the SMDH file (Optional)
# APP_AUTHOR is the author of the app stored in the SMDH file (Optional)
# ICON is the filename of the icon (.png), relative to the project folder.
#   If not set, it attempts to use one of the following (in this order):
#     - <Project name>.png
#     - icon.png
#     - <libctru folder>/default_icon.png
#---------------------------------------------------------------------------------
TARGET        :=    test
BUILD        :=    build
SOURCES        :=    source
DATA        :=    data
INCLUDES    :=    include
#ROMFS        :=    romfs
APP_TITLE    :=  cube
APP_DESCRIPTION := Description
APP_AUTHOR  :=  Author

Ainsi, on retrouve le nom de l’exécutable dans la variable TARGET, les dossiers à prendre en compte pour compiler le projet (SOURCES, DATA, INCLUDES), et des options facultatives pour fournir un titre, une description, un auteur et une icône qui seront affichés dans le Homebrew Launcher.
Finalement, si vous souhaitez intégrer des données à votre application (autre que les .bin basique), vous devez ajouter vos propres règles de compilation.

IV-B-2 - Shaders

Les shaders sont traités au travers du Makefile et doivent avoir l’extension .v.pica (ou g.pica pour le geometry shader). Une règle du Makefile permettra de les assembler et générera un fichier .h à inclure dans votre code afin de pouvoir charger le shader.

IV-B-3 - Textures

Sachant qu’il n’y a pas d’accès au système de fichiers, il est aussi nécessaire d’intégrer les ressources telles que les textures dans l’exécutable. Le Makefile convertira les fichiers .bin en .o afin de les intégrer tels quels. Pour accéder à votre fichier, il suffira d’inclure un fichier _bin.h correspondant à votre ressource et les données seront accessibles à travers une variable du même nom que le fichier suffixé par _bin.

V - Première application

V-A - Hello World

Dans les exemples accompagnant devkitARM, vous trouverez le classique Hello World :

#include <string.h>
#include <stdio.h>

#include <3ds.h>

int main(int argc, char **argv) {

gfxInitDefault();
consoleInit(GFX_TOP, NULL);

printf("Hello 3DS World!");

// Main loop
while (aptMainLoop()) {

gspWaitForVBlank();
hidScanInput();

// Your code goes here

u32 kDown = hidKeysDown();
if (kDown & KEY_START)
break; // break in order to return to hbmenu

// Flush and swap framebuffers
gfxFlushBuffers();
gfxSwapBuffers();
}

gfxExit();
return 0;
}

Pour initialiser les tampons d’images des écrans, on appelle glxInitDefault() (et à la fin, gfxExit() pour libérer la mémoire). La fonction consoleInit() permet d’initialiser la console sur l’écran (ici, du haut) afin d’afficher des informations grâce à printf().

Ensuite, on retrouve d’ailleurs la boucle principale, permettant ici de vérifier les boutons appuyés et de rafraîchir l’écran.

V-A-1 - Détection des entrées utilisateur

La détection des entrées utilisateur se fait en deux temps :

- lecture de l’état de la console grâce à hidScanInput() ;

- la récupération de l’état d’une entrée utilisateur :

- hidKeysDown/hidKeysHeld()/hidKeysUp() pour les boutons,

- hidTouchRead() pour l’écran tactile,

- hidCircleRead() pour le circle pad,

- hidAccelRead() pour l’accéléromètre,

- hidGyroRead() pour le gyroscope ;

Finalement, vous pouvez attendre un événement particulier avec hidWaitForEvent().

V-B - Cube

Le code suivant permet d’afficher un cube coloré grâce à un tampon indexé, passant de l’écran inférieur à l’écran supérieur :

#include <stdio.h>
#include <string.h>
#include <math.h>
#include <3ds.h>
#include <citro3d.h>
#include "vshader_shbin.h"

#define CLEAR_COLOR 0x58D14AFF
#define CLEAR_COLOR2 0xFF9898FF

#define DISPLAY_TRANSFER_FLAGS \
(GX_TRANSFER_FLIP_VERT(0) | GX_TRANSFER_OUT_TILED(0) | GX_TRANSFER_RAW_COPY(0) | \
GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGBA8) | GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB8) | \
GX_TRANSFER_SCALING(GX_TRANSFER_SCALE_NO))

typedef struct { float x, y, z; } vertex;

// Liste des sommets (position et couleur)
static const vertex vertex_list[] =
{
// Avant
{ 0.5f, 0.5f, 0.5f }, { 1.0f, 0.0f, 0.0f },
{ -0.5f, 0.5f, 0.5f }, { 0.0f, 0.0f, 1.0f },
{ -0.5f, -0.5f, 0.5f }, { 0.0f, 1.0f, 0.0f },
{ 0.5f, -0.5f, 0.5f }, { 0.0f, 1.0f, 1.0f },
// Arrière
{ 0.5f, 0.5f, -0.5f }, { 0.0f, 1.0f, 1.0f },
{ -0.5f, 0.5f, -0.5f }, { 1.0f, 1.0f, 0.0f },
{ -0.5f, -0.5f, -0.5f }, { 1.0f, 0.0f, 1.0f },
{ 0.5f, -0.5f, -0.5f }, { 0.0f, 0.0f, 0.0f },
};
#define vertex_list_count (sizeof(vertex_list)/sizeof(vertex_list[0]))

// Index des sommets pour l’affichage
static const u16 index_list[] =
{
0, 2, 1,
0, 3, 2,

0, 4, 3,
4, 7, 3,

4, 5, 6,
4, 6, 7,

6, 2, 3,
6, 3, 7,

1, 6, 5,
1, 2, 6,

0, 1, 5,
0, 5, 4
};
#define index_list_count (sizeof(index_list)/sizeof(index_list[0]))

static DVLB_s* vshader_dvlb;
static shaderProgram_s program;
static int uLoc_mvp; // Variable uniforme pour la matrice MVP
// Matrices
static C3D_Mtx projectionTop;
static C3D_Mtx projectionBot;
static C3D_Mtx view;
static C3D_Mtx model;
static C3D_Mtx mvp;

static void* vbo_data;
static void* index_data;

void updateMatrix(bool isTop)
{
Mtx_Identity(&mvp);
Mtx_Multiply(&mvp, &model, &mvp);
Mtx_Multiply(&mvp, &view, &mvp);

if(isTop)
{
Mtx_Multiply(&mvp, &projectionTop, &mvp);
}
else
{
Mtx_Multiply(&mvp, &projectionBot, &mvp);
}
}

void initScene()
{
// Charge le vertex shader et crée le program shader puis le lie
vshader_dvlb = DVLB_ParseFile((u32*)vshader_shbin, vshader_shbin_size);
shaderProgramInit(&program);
shaderProgramSetVsh(&program, &vshader_dvlb->DVLE[0]);
C3D_BindProgram(&program);

// Récupère l’emplacement de la variable uniforme
uLoc_mvp = shaderInstanceGetUniformLocation(program.vertexShader, "mvp");

// Configure les attributs de sommets du vertex shader
C3D_AttrInfo* attrInfo = C3D_GetAttrInfo();
AttrInfo_Init(attrInfo);
AttrInfo_AddLoader(attrInfo, 0, GPU_FLOAT, 3); // v0=position
AttrInfo_AddLoader(attrInfo, 1, GPU_FLOAT, 3); // v1=color

// Calcul des matrices de projection et de vue
Mtx_OrthoTilt(&projectionTop, -2.0, 2.0, -1.0, 1.0, 0.0, 10.0, true);
Mtx_OrthoTilt(&projectionBot, -2.0f, 2.0f, -3.1f, -1.1f, 0.0f, 10.0f, true);
Mtx_LookAt(&view, FVec3_New(0, 0, -5), FVec3_New(0, 0, 0), FVec3_New(0, 1, 0), true);

Mtx_Identity(&model);

// Créer le VBO
vbo_data = linearAlloc(sizeof(vertex_list)*2);
memcpy(vbo_data, vertex_list, sizeof(vertex_list));

// et l’IBO
index_data = linearAlloc(sizeof(index_list));
memcpy(index_data, index_list, sizeof(index_list));

// Configure les tampons
C3D_BufInfo* bufInfo = C3D_GetBufInfo();
BufInfo_Init(bufInfo);
BufInfo_Add(bufInfo, vbo_data, sizeof(vertex)*2, 2, 0x10);

// Configure la première passe de la rasterization
// https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml
C3D_TexEnv* env = C3D_GetTexEnv(0);
C3D_TexEnvSrc(env, C3D_Both, GPU_PRIMARY_COLOR, 0, 0);
C3D_TexEnvOp(env, C3D_Both, 0, 0, 0);
C3D_TexEnvFunc(env, C3D_Both, GPU_REPLACE);
}

void exitScene()
{
// Libère les VBO
linearFree(index_data);
linearFree(vbo_data);

// Libère le program shader
shaderProgramFree(&program);
DVLB_Free(vshader_dvlb);
}

static void sceneRender(bool isTop)
{
updateMatrix(isTop);

// Mise à jour de la variable uniforme
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, uLoc_mvp, &mvp);

// Affichage
C3D_DrawElements(GPU_TRIANGLES, index_list_count, C3D_UNSIGNED_SHORT, index_data);
}

void sceneUpdate(float angle)
{
Mtx_Identity(&model);

Mtx_RotateX(&model, C3D_AngleFromDegrees(angle), false);
Mtx_RotateY(&model, C3D_AngleFromDegrees(angle/3.f), false);
Mtx_Translate(&model, 0, cosf(angle / 100.f) * 1.2f - 1, 0, false);
}

int main(int argc, char **argv) {

const float speed = 2;
float angle = 0;

gfxInitDefault();
C3D_Init(C3D_DEFAULT_CMDBUF_SIZE);

// Initialisations des cibles de rendu
C3D_RenderTarget* top_target = C3D_RenderTargetCreate(240, 400, GPU_RB_RGBA8, GPU_RB_DEPTH24_STENCIL8);
C3D_RenderTargetSetClear(top_target, C3D_CLEAR_ALL, CLEAR_COLOR, 0);
C3D_RenderTargetSetOutput(top_target, GFX_TOP, GFX_LEFT, DISPLAY_TRANSFER_FLAGS);

C3D_RenderTarget* bot_target = C3D_RenderTargetCreate(240, 320, GPU_RB_RGBA8, GPU_RB_DEPTH24_STENCIL8);
C3D_RenderTargetSetClear(bot_target, C3D_CLEAR_ALL, CLEAR_COLOR2, 0);
C3D_RenderTargetSetOutput(bot_target, GFX_BOTTOM, GFX_LEFT, DISPLAY_TRANSFER_FLAGS);

initScene();

// Boucle principale
while (aptMainLoop()) {
angle += speed;

hidScanInput();
u32 kDown = hidKeysDown();
if (kDown & KEY_START)
break; // fin de la boucle pour retourner au menu HomeBrew

sceneUpdate(angle);

// Affichage de la scène
C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
C3D_FrameDrawOn(top_target);
sceneRender(true);
C3D_FrameDrawOn(bot_target);
sceneRender(false);
C3D_FrameEnd(0);
}

exitScene();

C3D_Fini();
gfxExit();
return 0;
}

Et le code du vertex shader correspondant :

; Example PICA200 vertex shader

; Variables uniformes
.fvec mvp[4]

; Constantes
.constf myconst(0.0, 1.0, -1.0, 0.1)
.constf myconst2(0.3, 0.0, 0.0, 0.0)
.alias  zeros myconst.xxxx ; Vector full of zeros
.alias  ones  myconst.yyyy ; Vector full of ones

; Sorties
.out outpos position
.out outclr color

; Entrées (alias pour aider la lecture)
.alias inpos v0
.alias inclr v1

.bool test

.proc main
; Force the w component of inpos to be 1.0
mov r0.xyz, inpos
mov r0.w,   ones

; outpos = projectionMatrix * inpos
dp4 outpos.x, mvp[0], r0
dp4 outpos.y, mvp[1], r0
dp4 outpos.z, mvp[2], r0
dp4 outpos.w, mvp[3], r0

; outclr = inclr
mov r1.xyz, inclr
mov r1.w,   ones
mov outclr, r1

; Nous avons fini
end
.end

Contrairement au programme précédent, nous n’initialisons plus de console, car nous utilisons les deux écrans pour la 3D grâce à Citro3D. Cette bibliothèque s’initialise avec C3D_Init() et se clôt avec C3D_Fini().

La largeur et la hauteur des cibles de rendu sont inversées par rapport à la résolution de l’écran. En réalité, l’écran est tourné de 90°, ce qui explique cette inversion.

Le reste de l’application ressemble à une application OpenGL moderne classique. Dans cet exemple, nous utilisons :

- un vertex shader avec une variable uniforme et deux attributs de sommets ;

- un VBO ;

- un IBO ;

- les matrices.

L’allocation mémoire des tampons se fait à travers la fonction linearAlloc() et la libération à travers linearFree().

V-B-1 - Double écran

La gestion du double écran n’a rien de particulier en soi : cela revient à gérer deux zones de rendu à la fois. Par conséquent, il est nécessaire d’avoir deux tampons de rendu (un pour chaque écran) et de réaliser l’affichage sur l’un puis l’autre écran.

Les écrans n’ont pas la même résolution.

Dans le programme d’exemple, l’astuce est de définir une matrice de projection différente pour chaque écran. Certes, la scène est affichée deux fois (et un frustrum culling aurait permis d’éviter des rendus inutiles), mais le cube n’est visible qu’une fois : sa position dans le monde détermine sur quel écran il sera affiché.

Les deux écrans sont séparés physiquement. Il peut être une bonne idée de recréer cet espacement entre les deux zones de rendu afin de donner une meilleure immersion au joueur.

V-B-2 - Effet 3D

Pour activer l’effet 3D stéréoscopique, vous devez appeler gfxSet3D() après gfxInit() (ou gfxInitDefault()).
Ensuite, vous devez utiliser deux cibles de rendu de 400x240 pixels, une pour chaque œil. Dans la configuration de celles-ci, vous devez indiquer quelle cible utiliser pour la gauche et laquelle pour la droite à travers la fonction C3D_RenderTargetSetOutput() :

C3D_RenderTargetSetOutput(leftTarget, GFX_TOP, GFX_LEFT, COMMON_DISPLAY_TRANSFER_FLAGS); // Écran supérieur, œil gauche
C3D_RenderTargetSetOutput(rightTarget, GFX_TOP, GFX_RIGHT, COMMON_DISPLAY_TRANSFER_FLAGS); // Écran supérieur, œil droit

Finalement, lors du rendu, vous devez prendre en compte la configuration de l’effet 3D de l’utilisateur (qu’il a défini avec le bouton glissoir). Vous pouvez obtenir sa valeur avec la fonction osGet3DSliderState(). Cette valeur doit être passée à la fonction Mtx_PerspStereoTilt() pour générer la matrice de projection.

Le développeur de Picasso conseille de diviser la valeur du bouton glissoir par 3.

V-C - Lumière

Même si le GPU ne propose pas de fragment shader, il est possible de configurer des lumières et ainsi avoir un éclairage reposant sur le modèle de Phong.
Pour cela, il faudra utiliser les fonctions :

- C3D_LightEnvInit() : pour initialiser la structure des propriétés de la lumière ;

- C3D_LightEnvBind() : pour définir des propriétés comme actives ;

- C3D_LightEnvMaterial() : pour spécifier les propriétés de l’objet ;

- LightLut_Phong() : pour indiquer que nous souhaitons le modèle de Phong ;

- C3D_LightEnvLut() : pour configurer comment la lumière est calculée ;

- C3D_LightInit() : pour initialiser la lumière ;

- C3D_LightColor() : pour donner une couleur à la lumière ;

- C3D_LightPosition() : pour donner la position à la lumière.

Voici un exemple d’utilisation, tiré des exemples provenant du devkitARM :

static const C3D_Material material =
{
{ 0.2f, 0.2f, 0.2f }, //ambient
{ 0.4f, 0.4f, 0.4f }, //diffuse
{ 0.8f, 0.8f, 0.8f }, //specular0
{ 0.0f, 0.0f, 0.0f }, //specular1
{ 0.0f, 0.0f, 0.0f }, //emission
};

C3D_LightEnvInit(&lightEnv);
C3D_LightEnvBind(&lightEnv);
C3D_LightEnvMaterial(&lightEnv, &material);

LightLut_Phong(&lut_Phong, 30);
C3D_LightEnvLut(&lightEnv, GPU_LUT_D0, GPU_LUTINPUT_LN, false, &lut_Phong);

C3D_FVec lightVec = FVec4_New(0.0f, 0.0f, -0.5f, 1.0f);

C3D_LightInit(&light, &lightEnv);
C3D_LightColor(&light, 1.0, 1.0, 1.0);
C3D_LightPosition(&light, &lightVec);

Évidemment, pour un meilleur résultat, il est nécessaire d’attribuer des normales à vos sommets.

V-D - Texture

V-D-1 - Mise en place de la texture

La texture doit être placée dans un fichier binaire qui sera intégré à l’exécutable. Un fichier binaire contenant la texture sans compression est plus simple à utiliser, sachant qu’il suffira de passer les données au GPU. Vous pouvez utiliser cette page Web pour convertir vos images en image brute, non compressée.

V-D-2 - Initialisation de la texture

Quatre fonctions permettent d’arriver à l’initialisation de la texture :

- C3D_TextInit() : pour initialiser la structure représentant la texture ;

- C3D_TexUpload() : pour charger les données de la texture ;

- C3D_TexSetFilter() : pour définir les filtres utilisés lors d’une réduction ou d’un agrandissement ;

- C3D_TexBind() : pour assigner la texture à un canal.

Exemple :

C3D_TexInit(&kitten_tex, 64, 64, GPU_RGBA8);
C3D_TexUpload(&kitten_tex, kitten_bin);
C3D_TexSetFilter(&kitten_tex, GPU_LINEAR, GPU_NEAREST);
C3D_TexBind(0, &kitten_tex);

V-D-3 - Utilisation

L’utilisation de la texture se fait au travers de la configuration de la rasterization :

C3D_TexEnv* env = C3D_GetTexEnv(0);
C3D_TexEnvSrc(env, C3D_Both, GPU_TEXTURE0, GPU_PRIMARY_COLOR, 0);
C3D_TexEnvOp(env, C3D_Both, 0, 0, 0);
C3D_TexEnvFunc(env, C3D_Both, GPU_MODULATE);

Dû à l’absence du fragment shader, on ne fait que spécifier comment le GPU doit mélanger le résultat de l’application de la texture avec le résultat du vertex shader. Ici on mélange la sortie de la couleur primaire (soit, la sortie du vertex shader) avec la texture au canal 0.

VI - Conclusion

Nous avons vu que l’installation et la mise en place d’un environnement de travail et de test sont rapides. De plus, les bases de la programmation 3D, ainsi que les particularités de la plateforme ont été décrites afin de rendre le processus de création d’homebrew bien plus aisé. En effet, la documentation présente mais limitée demande quelques prérequis en programmation graphique et une bonne autonomie.

J’espère ainsi vous avoir donné envie d’aller plus loin et d’avoir enlevé le plus gros obstacle quant au démarrage d’un tel projet. Ainsi, vous aussi vous pourrez ressentir la joie de voir vos propres programmes sur votre console et d’utiliser ces fonctionnalités spécifiques.

VII - Pour aller plus loin

Même si la documentation est éparse, elle est suffisante. Voici où la trouver :

- les exemples accompagnant devkitARM (couvrant tous les besoins que l’on peut avoir à travers des programmes simples) ;

- la documentation de libCTRU ;

- le tutoriel de Thompson Lee ;

- le Wiki dédié aux homebrews sur la Nintendo 3DS.

VIII - Remerciements

Je tiens à remercier chrtophe pour sa relecture de cet article, ainsi qu’à f-leb pour ses corrections orthographiques.