Des figures 2D colorées!
Quadrilatères, triangles et notre premier cube.
Dans le précédent chapitre nous avons affiché un triangle recouvrant toute la surface de notre fenêtre d'application, ce qui veut dire que nous pouvons déjà créer pas mal de choses.
L'idée :
Dans ce chapitre tu vas construire une bibliothèque de meshes (rappel, une texture est projetée sur un mesh), ce qui peut t'ouvrir quelques portes.
Structure du projet attendue
MSLearn/
├── MSLearn.entitlements
├── Application/
│ ├── main.m
│ ├── AppViewController.h
│ ├── AppViewController.mm
│ └── macOS/
│ ├── macOSInfo.plist
│ └── AppIcon.icns ← si tu as déjà exploré l'onglet setup
├── Frameworks/
│ ├── metal-cpp/
│ └── metal-cpp-extensions/
├── Renderer/
│ ├── Mesh.cpp
│ ├── Mesh.hpp
│ ├── Renderer.cpp
│ └── Renderer.hpp
├── Shaders/
│ ├── Fullscreen.metal
│ └── Shapes.metal/ ← Ajoute
├── includes/
│ └── SharedGPU/
│ ├── Renderer_shared.h
│ └── Mesh_shared.h ← Ajoute (normalement tu l'as déjà)
└── Makefile ← toujours optionnel
Tu as vu que pour créer des vertices, tu dois les déclarer comme étant des nombres à virgules flottantes (dans un vecteur float2 ou float4) et les stocker dans tableau, pour finalement afficher le rendu en 3D et avec une composante w. Ça se passait dans le shader, maintenant tu vas envoyer ces vertices via un buffer, comme les uniforms (taille fenêtre).
/// C++ — Mesh_shared.h — update:18/05/26 — partagé CPU & GPU
#ifndef Mesh_shared_h
#define Mesh_shared_h
#include <simd/simd.h>
struct alignas(16) VertexIn2DMesh // des informations pour un point dans l'espace 2D + couleur RGBA
{
simd::float2 position;
simd::float4 color;
};
#endif /* Mesh_shared_h */
Dans le chapitre précédent, le vertex shader retourne une coordonnée de texture pour une couleur unie stockée dans un float2 {x, y} ou {u, v}. Les données pour remplir le vecteur que le GPU attend a été directement déclaré en MSL dans le fragment shader.
Tout ce que tu vois à l'écran est triangulaire.
Buffers & mémoire GPU
Un MTL::Buffer (MTLBuffer) est un bloc de mémoire partagé entre le CPU et le GPU.
Sur Apple Silicon, c'est littéralement la même RAM physique — pas de transfert de bus, pas de copie PCIe.
Créer un buffer
/// C++ — Renderer.hpp — update:18/05/26
// #include <simd/simd.h> // Inclus dans Mesh_shared.h
#include "../includes/SharedGPU/Mesh_shared.h"
private:
void buildGeometry();
void buildPipeline();
NS::SharedPtr<MTL::Buffer> m_vertexBuffer; // NS::SharedPtr → destruction automatique.
NS::SharedPtr<MTL::Buffer> m_vertexBufferQuad;
Faisons un petit point ;
buildGeometry()→ au lieu de créer ces fonctions, tu pourrais exécuter son contenu dans le constructeur car c'est juste de l'initialisation/construction, la géométrie fait partie de ton objet/classe.buildPipeline()→ construit le PSO (Pipeline State Object) : associe les fonctions vertex/fragment au format de pixel du drawable.
On va construire un triangle, puis on construira un carré comme suit :
En commençant par le haut gauche, ordre d'enroulement (winding order), et continuer en sens horaire (clockwise).
Face 0 Top-Left > Top-Right > Bottom-Left
Face 1 Top-Right > Bottom-Right > Bottom-Left
/// C++ — Renderer.cpp — update:18/05/26 — Constructeur — buildGeometry → buildPipeline → newBuffer
{
buildGeometry(); // ← Ajoute
buildPipeline(); // ← Appelle de ces fonctions une fois, dans l'ordre
}
/// C++ — Renderer.cpp — update:18/05/26
void Renderer::buildPipeline() // ← Notre fonction qui construit le pipeline
{ // Change les noms selon tes fonctions MSL
auto vertexFunction = NS::TransferPtr(m_shaderLibrary->newFunction(MTLSTR("vertex_main_shapes")));
auto fragmentFunction = NS::TransferPtr(m_shaderLibrary->newFunction(MTLSTR("fragment_main_shapes")));
auto renderPipelineDescriptor = NS::TransferPtr(MTL::RenderPipelineDescriptor::alloc()->init());
renderPipelineDescriptor->setVertexFunction(vertexFunction.get());
renderPipelineDescriptor->setFragmentFunction(fragmentFunction.get());
renderPipelineDescriptor->colorAttachments()->object(0)->setPixelFormat(m_pixelFormat);
renderPipelineDescriptor->setDepthAttachmentPixelFormat(m_depthPixelFormat);
NS::Error* error = nullptr;
m_renderPipelineState = NS::TransferPtr(m_device->newRenderPipelineState(renderPipelineDescriptor.get(), &error));
assert(!error && "Pipeline creation failed");
}
void Renderer::buildGeometry()
{ // R G B Alpha (transparence -> 1.0f = maximum = opaque)
const simd::float4 red = { 1.0f, 0.0f, 0.0f, 1.0f };
const simd::float4 green = { 0.0f, 1.0f, 0.0f, 1.0f };
const simd::float4 blue = { 0.0f, 0.0f, 1.0f, 1.0f };
const simd::float4 white = { 1.0f, 1.0f, 1.0f, 1.0f };
const simd::float4 black = { 0.0f, 0.0f, 0.0f, 1.0f };
const VertexIn2DMesh triangle[] = {
{ {-0.89f, 0.89f }, blue }, // Top-Left
{ { 0.0f, 0.89f }, red }, // Top-Right
{ {-0.89f, -0.89f }, green }, // Bottom-Left
};
const VertexIn2DMesh square[] = {
{ {-0.5f, 0.5f }, red }, // TL
{ { 0.5f, 0.5f }, green }, // TR
{ {-0.5f, -0.5f }, blue }, // BL
{ { 0.5f, 0.5f }, green }, // TR
{ { 0.5f, -0.5f }, black }, // BR
{ {-0.5f, -0.5f }, blue }, // BL
};
// Buffers initialisés avec data
m_vertexBuffer = NS::TransferPtr(m_device->newBuffer(triangle, sizeof(triangle), MTL::ResourceStorageModeShared));
m_vertexBufferQuad = NS::TransferPtr(m_device->newBuffer(square, sizeof(square), MTL::ResourceStorageModeShared));
}
/// C++ — Exemple — update:18/05/26
// Données CPU (rendent un carré 2D)
4 vertex (points) dans un espace 3D
x, y, x, y, x, y, x, y, x, y, x, y
float vertices[] = { -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f };
// Buffer de vertices — données statiques (Private = GPU seul)
MTL::Buffer* _vertexBuffer = device->newBuffer(
vertices, // pointeur vers le tableau ([], array) des données CPU ci-dessus
sizeof(vertices), // taille en bytes (ici 48 octets → sizeof(float) = 4 bytes * 12 valeurs)
MTL::ResourceStorageModeShared);
Mémoire GPU sur Apple Silicon
Sur architecture mémoire unifiée, il n'y a pas de VRAM séparée. CPU et GPU accèdent au même pool de RAM. Metal expose trois modes de stockage pour les buffers :
// Shared : CPU et GPU peuvent lire/écrire → idéal pour données dynamiques
MTL::ResourceStorageModeShared
// Private : GPU seulement → performances maximales, pas d'accès CPU direct
MTL::ResourceStorageModePrivate
// Managed : (Intel/discret) copies explicites CPU ↔ GPU — inutile sur M-series
MTL::ResourceStorageModeManaged
Shared pour les buffers que le CPU met à jour chaque frame (uniforms, positions de caméra), et Private pour les données statiques (mesh, textures baked). Le mode Managed n'a aucun intérêt sur Apple Silicon.
/// C++ — Renderer.cpp — update:18/05/26
renderCommandEncoder->setRenderPipelineState(m_renderPipelineState.get());
renderCommandEncoder->setVertexBuffer(m_vertexBufferQuad.get(), 0, 0);
renderCommandEncoder->drawPrimitives(MTL::PrimitiveTypeTriangle, NS::UInteger(0), NS::UInteger(6));
renderCommandEncoder->setVertexBuffer(m_vertexBuffer.get(), 0, 0);
renderCommandEncoder->drawPrimitives(MTL::PrimitiveTypeTriangle, NS::UInteger(0), NS::UInteger(3));
Le carré a été envoyé à la file d'attente en premier, il sera donc en arrière plan par rapport au triangle.
Don't Panic !!
setRenderPipelineState()→ méthode appartenant à l'encoder.setVertexBuffer(buffer, 0, 0)→ buffer[0] dans le code MSLdrawPrimitives()→ prototype Metal → retourne void = la méthode ne renvoit rien, elle dessine. → MTL:: = namespace → RenderCommandEncoder = le nom de la classe (Metal).
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* + + */
/* File: Shapes.metal +++ +++ */
/* + + */
/* By: laboitederemdal + + */
/* + + */
/* Created: 21/04/2026 13:55:51 + + + + + + */
/* Updated: 18/05/2026 */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include <metal_stdlib>
#include "../includes/SharedGPU/Mesh_shared.h"
using namespace metal;
struct VertexOut2DMesh
{
float4 position [[position]];
float4 color;
};
vertex VertexOut2DMesh vertex_main_shapes(uint vid [[vertex_id]],
constant VertexIn2DMesh* vertices [[buffer(0)]])
{
VertexOut2DMesh out;
out.position = float4(vertices[vid].position, 0.0, 1.0);
out.color = vertices[vid].color;
return out;
}
fragment float4 fragment_main_shapes(VertexOut2DMesh in [[stage_in]])
{
return in.color;
}
Pour une fenêtre (window) de 2560 x 1440 pixels et les coordonnées en local space ci-dessous, le GPU fait ces calculs automatiquement pour positionner le point :
\( \text{NDC} = \frac{\text{clip}}{w} = \frac{(0.0,0.33,0.0)}{1.0} = (0.0,0.33,0.0) \)
\( x_{\text{px}} = \frac{(x_{\text{NDC}} + 1)}{2} \times \text{width} = \frac{(0.0 + 1)}{2} \times 2560 = 1280 \text{ px} \)
\( y_{\text{px}} = \frac{(1 - y_{\text{NDC}})}{2} \times \text{height} = \frac{(1 - 0.33)}{2} \times 1440 = 482 \text{ px} \)
Y est inversé — en NDC +1 = haut, −1 = bas, mais en pixels 0 = haut, 1440 = bas. D'où le (1 - y).
w = 1.0 — sans matrice de projection, la division par w ne change rien. Dès que tu utilises une projectionMatrix, w devient −z_view et là la perspective entre en jeu — les objets lointains rapetissent.
/// C++ — Exemple — update:18/05/26
{ 0.0f, 0.33f }, { 0.3f, 0.7f, 0.3f, 1.0f } // vert pastel
/// MSL — Exemple — update:18/05/26
// vertices[index] → le sommet, la fn est invoquée en parallèle pour chaque sommet par le GPU.
// Ici vid/index = 1 = 2ième vertex → On récupère la première valeur d'un tableau [] via l'index 0.
// .position → x = 0, y = 0.33, z = 0
// [[position]] → Clip Space
out.position = float4(vertices[vid].position, 0.0, 1.0);
// float4 out.position = float4(0, 0.33, 0, 1); → xyzw
Quelques 'meshes' utiles pour la suite du projet :
/// C++ — Mesh.hpp — update:18/05/26
#ifndef Mesh_hpp
#define Mesh_hpp
#include <vector>
#include <Metal/Metal.hpp>
#include "../includes/SharedGPU/Mesh_shared.h"
struct Mesh3D
{
MTL::Buffer* vertices;
MTL::Buffer* indices;
uint32_t numIndices;
MTL::IndexType indexType;
MTL::Winding winding;
};
namespace meshes
{
NS::SharedPtr<MTL::Buffer> squareMesh(MTL::Device* device, const simd::float4 color, float horizontalScale = 1.0f, float verticalScale = 1.0f);
NS::SharedPtr<MTL::Buffer> triangleEquilateralMesh(MTL::Device* device, const simd::float4 color, float scale = 1.0f);
NS::SharedPtr<MTL::Buffer> triangleRectangleMesh(MTL::Device* device, const simd::float4 color, float scale = 3.f);
NS::SharedPtr<MTL::Buffer> circleMesh(MTL::Device* device, float radius, int segments);
NS::SharedPtr<MTL::Buffer> trunkWoodMesh(MTL::Device* device, float width, float height);
NS::SharedPtr<MTL::Buffer> cloudMesh(MTL::Device* device, float width, float height, float corner);
NS::SharedPtr<MTL::Buffer> groundMesh(MTL::Device* device);
Mesh3D cubeMesh(float size, MTL::Device* device);
Mesh3D newWindowQuad(MTL::Device* device, float horizontalScale = 1.0f, float verticalScale = 1.0f);
Mesh3D cubeMeshWithoutTexture(float size, MTL::Device* device);
void releaseMesh(Mesh3D* mesh);
}
#endif /* Mesh_hpp */
/// C++ — Mesh_shared.h — update:18/05/26
struct alignas(16) VertexIn2DMesh
{
simd::float2 position;
simd::float4 color;
};
struct alignas(16) VertexInWithNormal
{
simd::float4 position; // tout ce qui nous intéresse c'est d'envoyer des nombres au GPU
simd::float4 normal; // color/normal/texCoord sont du même type, si tu redéfinis
simd::float4 texcoord; // une structure plutôt que de t'adapter au nom → boilerplate
};
struct alignas(16) VertexIn3DMesh
{
simd::float4 position;
simd::float4 texcoord;
};
static_assert(sizeof(VertexIn2DMesh) == 32);
static_assert(sizeof(VertexInWithNormal) == 48);
static_assert(sizeof(VertexIn3DMesh) == 32);
/// C++ — Mesh.cpp — update:18/05/26
#include "Mesh.hpp"
namespace meshes
{
NS::SharedPtr<MTL::Buffer> triangleEquilateralMesh(MTL::Device* device, const simd::float4 color, float scale)
{
const simd::float4 red = { 1.0f, 0.0f, 0.0f, 1.0f };
const simd::float4 green = { 0.0f, 1.0f, 0.0f, 1.0f };
const simd::float4 blue = { 0.0f, 0.0f, 1.0f, 1.0f };
if (scale > 0.16f)
{
const VertexIn2DMesh triangle[] = {
{ { 0.0f, scale }, red }, // TT
{ { scale, -scale }, blue }, // BR
{ {-scale, -scale }, green },
};
return NS::TransferPtr(device->newBuffer(triangle, sizeof(triangle), MTL::ResourceStorageModeShared));
}
else
{
const VertexIn2DMesh triangle[] = {
{ { 0.0f, scale }, color }, // TT
{ { scale, -scale }, color }, // BR
{ {-scale, -scale }, color },
};
return NS::TransferPtr(device->newBuffer(triangle, sizeof(triangle), MTL::ResourceStorageModeShared));
}
}
/// C++ — Mesh.cpp — update:18/05/26
NS::SharedPtr<MTL::Buffer> triangleRectangleMesh(MTL::Device* device, const simd::float4 color, float scale)
{
const VertexIn2DMesh triangle[] = {
{ { -scale, scale * 3 }, color }, // TL
{ { scale * 3, -scale }, color }, // BR
{ { -scale, -scale }, color },
};
return NS::TransferPtr(device->newBuffer(triangle, sizeof(triangle), MTL::ResourceStorageModeShared));
}
/// C++ — Mesh.cpp — update:18/05/26
NS::SharedPtr<MTL::Buffer> groundMesh(MTL::Device* device)
{
const simd::float4 greenGrass = { 0.2f, 0.55f, 0.15f, 1.0f };
const VertexIn2DMesh verts[] = {
{ { -3.f, -0.23f }, greenGrass }, // TL
{ { 3.23f, -0.23f }, greenGrass }, // TR
{ { -3.f, -3.23f }, greenGrass }, // BR
};
return NS::TransferPtr(device->newBuffer(verts, sizeof(verts), MTL::ResourceStorageModeShared));
}
/// C++ — Mesh.cpp — update:18/05/26
NS::SharedPtr<MTL::Buffer> squareMesh(MTL::Device* device, const simd::float4 color, float horizontalScale, float verticalScale)
{
const VertexIn2DMesh square[] = {
{ {-horizontalScale, verticalScale }, color }, // TL
{ { horizontalScale, verticalScale }, color }, // TR
{ {-horizontalScale, -verticalScale }, color }, // BL
{ { horizontalScale, verticalScale }, color }, // TR
{ { horizontalScale, -verticalScale }, color }, // BR
{ {-horizontalScale, -verticalScale }, color }, // BL
};
return NS::TransferPtr(device->newBuffer(square, sizeof(square), MTL::ResourceStorageModeShared));
}
/// C++ — Mesh.cpp — update:18/05/26
NS::SharedPtr<MTL::Buffer> circleMesh(MTL::Device* device, float radius, int segments)
{
const simd::float4 yellow = { 1.0f, 0.9f, 0.0f, 1.0f };
std::vector<VertexIn2DMesh> verts;
verts.reserve(segments * 3);
for (int i = 0; i < segments; i++)
{
float a0 = (float)i / segments * M_PI * 2.f;
float a1 = (float)(i + 1) / segments * M_PI * 2.f;
verts.push_back({ { 0.f, 0.f }, yellow });
verts.push_back({ { cosf(a0) * radius, sinf(a0) * radius }, yellow }); // clockwise
verts.push_back({ { cosf(a1) * radius, sinf(a1) * radius }, yellow });
}
return NS::TransferPtr(device->newBuffer(verts.data(), verts.size() * sizeof(VertexIn2DMesh), MTL::ResourceStorageModeShared));
}
/// C++ — Mesh.cpp — update:18/05/26
NS::SharedPtr<MTL::Buffer> cloudMesh(MTL::Device* device, float w, float h, float corner)
{
const simd::float4 c = { 0.85f, 0.85f, 0.85f, 1.0f };
const float x = w, y = h, k = corner;
// rectangle principal + 4 coins en triangle vide = rect avec coins coupés
const VertexIn2DMesh verts[] = {
// rect central large
{ {-x + k, y }, c }, { { x - k, y }, c }, { { x - k, -y }, c },
{ {-x + k, y }, c }, { { x - k, -y }, c }, { {-x + k, -y }, c },
// rect central haut
{ {-x, y - k }, c }, { { x, y - k }, c }, { { x, -(y - k) }, c },
{ {-x, y - k }, c }, { { x, -(y - k) }, c }, { {-x, -(y - k) }, c },
};
return NS::TransferPtr(device->newBuffer(verts, sizeof(verts), MTL::ResourceStorageModeShared));
}
→ sizeof(VertexIn2DMesh) nous renvoie la taille d'une copie de la structure.
→ sizeof(vertices) nous renvoie la taille de toutes les vertices déclarées.
vertices = pluriel, vertex = singulier (point/sommet)
Découvrons les meshes 3D.
Normales — direction perpendiculaire d’une surface
En géométrie et en rendu 3D, une normale est un vecteur perpendiculaire à une surface. Dans un moteur graphique, les normales servent principalement à calculer l’éclairage.
Sans normales, une surface ne “sait” pas dans quelle direction elle regarde. Le GPU ne peut donc pas déterminer si elle reçoit beaucoup ou peu de lumière.
Une normale est généralement normalisée :
\[ ||\vec{n}|| = 1 \]
Cela signifie que sa longueur vaut exactement 1. Les calculs de lumière deviennent alors stables et prévisibles.
Normales dans un cube
left-handed coordinate systemDans un mesh, chaque vertex contient généralement :
Position Normale (v⃗) Texture Coordinates (UV)
{ -s, +s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f }, { 0.f, 1.f, 0.f, 0.f },
{ +s, +s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f }, { 1.f, 1.f, 0.f, 0.f },
{ +s, -s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f }, { 1.f, 0.f, 0.f, 0.f },
{ -s, -s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f }, { 0.f, 0.f, 0.f, 0.f },
\[ s = 1 \qquad \vec{v} = \begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix} \]
La face regarde vers +Z → Face arrière
Tous les vertices de cette face utilisent la même normale. Le rendu produit donc une face plate : flat shading.
Si chaque triangle possède une normale constante : Flat shading
Si les normales sont interpolées entre les vertices : Smooth shading
Le GPU interpole automatiquement les normales dans le rasterizer.
Pour une sphère, les normales pointent généralement depuis le centre :
\[ \vec{n} = \frac{\vec{p}}{||\vec{p}||} \]
où \( \vec{p} \) est la position du vertex.
Une normale ne doit pas être transformée exactement comme une position.
Pour une position :
\[ \vec{p} ' = M \cdot \vec{p} \]
Mais pour une normale :
\[ \vec{n}' = (M^{-1})^T \cdot \vec{n} \]
/// C++ — Mesh.cpp — update:18/05/26 — Right-Handed ! — Convention non-standard
Mesh3D cubeMeshWithoutTexture(float size, MTL::Device* device)
{
const float s = size * 0.5f;
VertexIn3DMesh vertices[] = {
// Positions Normals
// Face arrière Z+ x y z
{ { -s, -s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f } }, // BR
{ { +s, -s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f } }, // BL
{ { +s, +s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f } }, // TL
{ { -s, +s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f } }, // TR
{ { +s, -s, +s, 1.f }, { 1.f, 0.f, 0.f, 0.f } },
{ { +s, -s, -s, 1.f }, { 1.f, 0.f, 0.f, 0.f } },
{ { +s, +s, -s, 1.f }, { 1.f, 0.f, 0.f, 0.f } },
{ { +s, +s, +s, 1.f }, { 1.f, 0.f, 0.f, 0.f } },
{ { +s, -s, -s, 1.f }, { 0.f, 0.f, -1.f, 0.f } },
{ { -s, -s, -s, 1.f }, { 0.f, 0.f, -1.f, 0.f } },
{ { -s, +s, -s, 1.f }, { 0.f, 0.f, -1.f, 0.f } },
{ { +s, +s, -s, 1.f }, { 0.f, 0.f, -1.f, 0.f } },
{ { -s, -s, -s, 1.f }, { -1.f, 0.f, 0.f, 0.f } },
{ { -s, -s, +s, 1.f }, { -1.f, 0.f, 0.f, 0.f } },
{ { -s, +s, +s, 1.f }, { -1.f, 0.f, 0.f, 0.f } },
{ { -s, +s, -s, 1.f }, { -1.f, 0.f, 0.f, 0.f } },
{ { -s, +s, +s, 1.f }, { 0.f, 1.f, 0.f, 0.f } },
{ { +s, +s, +s, 1.f }, { 0.f, 1.f, 0.f, 0.f } },
{ { +s, +s, -s, 1.f }, { 0.f, 1.f, 0.f, 0.f } },
{ { -s, +s, -s, 1.f }, { 0.f, 1.f, 0.f, 0.f } },
{ { -s, -s, -s, 1.f }, { 0.f, -1.f, 0.f, 0.f } },
{ { +s, -s, -s, 1.f }, { 0.f, -1.f, 0.f, 0.f } },
{ { +s, -s, +s, 1.f }, { 0.f, -1.f, 0.f, 0.f } },
{ { -s, -s, +s, 1.f }, { 0.f, -1.f, 0.f, 0.f } },
};
uint16_t indices[] = {
0, 1, 2, 2, 3, 0, /* front */
4, 5, 6, 6, 7, 4, /* right */
8, 9, 10, 10, 11, 8, /* back */
12, 13, 14, 14, 15, 12, /* left */
16, 17, 18, 18, 19, 16, /* top */
20, 21, 22, 22, 23, 20, /* bottom */
};
Mesh3D cubeMesh
{
.vertices = device->newBuffer(sizeof(vertices), MTL::ResourceStorageModeShared),
.indices = device->newBuffer(sizeof(indices), MTL::ResourceStorageModeShared),
.numIndices = sizeof(indices) / sizeof(indices[0]),
.indexType = MTL::IndexTypeUInt16,
.winding = MTL::WindingCounterClockwise // pour exemple, règle de la main droite
};
memcpy(cubeMesh.vertices->contents(), vertices, sizeof(vertices));
memcpy(cubeMesh.indices->contents(), indices, sizeof(indices));
return cubeMesh;
}
Winding order et normales
L’ordre des sommets détermine l’orientation d’un triangle.
Avec un winding counter-clockwise :
\[ \vec{n} = (\vec{b} - \vec{a}) \times (\vec{c} - \vec{a}) \]
Le produit vectoriel donne une normale orientée selon la règle de la main droite. Si l’ordre des vertices est inversé :
- la normale s’inverse
- le back-face culling peut supprimer la face
- la lumière devient incorrecte
On utilisera la technique pour des rendus spéciaux.
/// C++ — Mesh.cpp — update:18/05/26
Mesh3D cubeMesh(float size, MTL::Device* device) // Cube avec le centre xyz (0, 0, 0), identity matrix
{
const float s = size * 0.5f;
VertexInWithNormal vertices[] = {
// Position Normale (v⃗) Texture Coordinates (UV)
// Face avant Z- (vue de l'extérieur → -Z)
{ { -s, +s, -s, 1.f }, { 0.f, 0.f, -1.f, 0.f }, { 0.f, 0.f, 0.f, 0.f } }, // TL
{ { +s, +s, -s, 1.f }, { 0.f, 0.f, -1.f, 0.f }, { 1.f, 0.f, 0.f, 0.f } }, // TR
{ { +s, -s, -s, 1.f }, { 0.f, 0.f, -1.f, 0.f }, { 1.f, 1.f, 0.f, 0.f } }, // BR
{ { -s, -s, -s, 1.f }, { 0.f, 0.f, -1.f, 0.f }, { 0.f, 1.f, 0.f, 0.f } }, // BL
// Face arrière Z+ x y z
{ { +s, +s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f }, { 0.f, 0.f, 0.f, 0.f } }, // TL
{ { -s, +s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f }, { 1.f, 0.f, 0.f, 0.f } }, // TR
{ { -s, -s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f }, { 1.f, 1.f, 0.f, 0.f } }, // BR
{ { +s, -s, +s, 1.f }, { 0.f, 0.f, 1.f, 0.f }, { 0.f, 1.f, 0.f, 0.f } }, // BL
// Face droite X+
{ { +s, +s, -s, 1.f }, { 1.f, 0.f, 0.f, 0.f }, { 0.f, 0.f, 0.f, 0.f } }, // TL
{ { +s, +s, +s, 1.f }, { 1.f, 0.f, 0.f, 0.f }, { 1.f, 0.f, 0.f, 0.f } }, // TR
{ { +s, -s, +s, 1.f }, { 1.f, 0.f, 0.f, 0.f }, { 1.f, 1.f, 0.f, 0.f } }, // BR
{ { +s, -s, -s, 1.f }, { 1.f, 0.f, 0.f, 0.f }, { 0.f, 1.f, 0.f, 0.f } }, // BL
// Face gauche X-
{ { -s, +s, +s, 1.f }, { -1.f, 0.f, 0.f, 0.f }, { 0.f, 0.f, 0.f, 0.f } }, // TL
{ { -s, +s, -s, 1.f }, { -1.f, 0.f, 0.f, 0.f }, { 1.f, 0.f, 0.f, 0.f } }, // TR
{ { -s, -s, -s, 1.f }, { -1.f, 0.f, 0.f, 0.f }, { 1.f, 1.f, 0.f, 0.f } }, // BR
{ { -s, -s, +s, 1.f }, { -1.f, 0.f, 0.f, 0.f }, { 0.f, 1.f, 0.f, 0.f } }, // BL
// Face dessus Y+
{ { -s, +s, +s, 1.f }, { 0.f, 1.f, 0.f, 0.f }, { 0.f, 0.f, 0.f, 0.f } }, // TL
{ { +s, +s, +s, 1.f }, { 0.f, 1.f, 0.f, 0.f }, { 1.f, 0.f, 0.f, 0.f } }, // TR
{ { +s, +s, -s, 1.f }, { 0.f, 1.f, 0.f, 0.f }, { 1.f, 1.f, 0.f, 0.f } }, // BR
{ { -s, +s, -s, 1.f }, { 0.f, 1.f, 0.f, 0.f }, { 0.f, 1.f, 0.f, 0.f } }, // BL
// Face dessous Y-
{ { -s, -s, -s, 1.f }, { 0.f, -1.f, 0.f, 0.f }, { 0.f, 0.f, 0.f, 0.f } }, // TL
{ { +s, -s, -s, 1.f }, { 0.f, -1.f, 0.f, 0.f }, { 1.f, 0.f, 0.f, 0.f } }, // TR
{ { +s, -s, +s, 1.f }, { 0.f, -1.f, 0.f, 0.f }, { 1.f, 1.f, 0.f, 0.f } }, // BR
{ { -s, -s, +s, 1.f }, { 0.f, -1.f, 0.f, 0.f }, { 0.f, 1.f, 0.f, 0.f } }, // BL
};
uint16_t indices[] = {
0, 1, 2, 2, 3, 0, /* front */
4, 5, 6, 6, 7, 4, /* back */
8, 9, 10, 10, 11, 8, /* right */
12, 13, 14, 14, 15, 12, /* left */
16, 17, 18, 18, 19, 16, /* top */
20, 21, 22, 22, 23, 20, /* bottom */
};
Mesh3D cubeMesh
{
.vertices = device->newBuffer(sizeof(vertices), MTL::ResourceStorageModeShared),
.indices = device->newBuffer(sizeof(indices), MTL::ResourceStorageModeShared),
.numIndices = sizeof(indices) / sizeof(indices[0]),
.indexType = MTL::IndexTypeUInt16,
.winding = MTL::WindingClockwise // ← CW vu de l'extérieur
}; // MTLWindingCounterClockwise vu de l'intérieur
memcpy(cubeMesh.vertices->contents(), vertices, sizeof(vertices));
memcpy(cubeMesh.indices->contents(), indices, sizeof(indices));
return cubeMesh;
}
Top-Left > Top-Right > Bottom-Right > Bottom-Left
/// C++ — Mesh.cpp — update:18/05/26
Mesh3D newWindowQuad(MTL::Device* device, float horizontalScale, float verticalScale)
{
const float hs = horizontalScale / 2;
const float vs = verticalScale / 2;
std::vector<VertexIn3DMesh> vertexIn
{
VertexIn3DMesh {
.position = simd::float4{ -1.f * hs, 1.f * vs, 0.0, 1.0 },
.texcoord = simd::float4{ 0.0, 0.0 }
},
VertexIn3DMesh {
.position = simd::float4{ -1.f * hs, -1.f * vs, 0.0, 1.0 },
.texcoord = simd::float4{ 0.0, 1.0 }
},
VertexIn3DMesh {
.position = simd::float4{ +1.f * hs, -1.f * vs, 0.0, 1.0 },
.texcoord = simd::float4{ 1.0, 1.0 }
},
VertexIn3DMesh {
.position = simd::float4{ +1.f * hs, +1.f * vs, 0.0, 1.0 },
.texcoord = simd::float4{ 1.0, 0.0 }
}
};
std::vector<uint16_t> indexData
{
0, 1, 2,
2, 3, 0
};
Mesh3D mesh
{
.vertices = device->newBuffer(vertexIn.size() * sizeof(VertexIn3DMesh), MTL::ResourceStorageModeShared),
.indices = device->newBuffer(indexData.size() * sizeof(uint16_t), MTL::ResourceStorageModeShared),
.numIndices = (uint32_t)indexData.size(),
.indexType = MTL::IndexTypeUInt16,
.winding = MTL::WindingCounterClockwise
};
memcpy(mesh.vertices->contents(), vertexIn.data(), vertexIn.size() * sizeof(VertexIn3DMesh));
memcpy(mesh.indices->contents(), indexData.data(), indexData.size() * sizeof(uint16_t));
return mesh;
}
/// C++ — Mesh.cpp — update:18/05/26
void releaseMesh(Mesh3D* mesh)
{
mesh->vertices->release();
mesh->indices->release();
mesh->vertices = nullptr;
mesh->indices = nullptr;
}
} // namespace meshes
Principes appliqués :
- TL / TR / BR / BL uniquement (face par face)
- chaque face = 4 sommets indépendants (pas de partage → normales propres)
- winding CW vu de l’extérieur
- Z+ = arrière écran (Metal LH classique) — Left-handed — Z non flippé
- indices cohérents par groupe de 4
/// C++ — Renderer.hpp — update:18/05/26
#include "Mesh.hpp"
Indices & optimisation
Un cube a 8 vertices mais 12 triangles (6 faces × 2 triangles). Sans indices : 36 vertices à envoyer au GPU
Chaque coin du cube est partagé par 3 faces — sans les indices tu envoies le même vertex 3 fois :
/// C++ — Exemple — update:18/05/26 — Indices/Index des vectices
// Sans indices — vertex dupliqués
{ -0.5, -0.5, 0.5 }, // coin 0 — face avant
{ -0.5, -0.5, 0.5 }, // coin 0 — face gauche (même données)
{ -0.5, -0.5, 0.5 }, // coin 0 — face bas (même données)
// Avec indices — 8 vertices uniques + 36 indices
vertices[0] = { -0.5, -0.5, 0.5 }; // stocké une seule fois
indices = { 0, 1, 2, // triangle 1 face avant
0, 2, 3, // triangle 2 face avant
0, 3, 4, // triangle 1 face gauche
... };
Le gain mémoire sur un cube est modeste, mais sur un mesh complexe (personnage, terrain) avec des milliers de vertices partagés c'est critique — et le GPU peut aussi réutiliser son post-transform cache : si le même index apparaît deux fois, le vertex shader n'est pas réexécuté.