Couleurs & Soleil
Sur internet les couleurs se situent sur la plage RGBA, dans tes projets MSL c'est du P3, par défaut sur Blender c'est du Rec.2020. Les couleurs sont plus vivent, plus précise — c'est à dire que tu as non seulement tu as une plage de couleurs plus grandes mais aussi plus de couleurs disponibles entre le Rouge et le Bleu comme vu dans le chapitre précédent.
Héxadécimal (base 16)
Un des formats couramment utilisé pour représenter les couleurs en informatique fonctionne de la manière suivante :
# RR GG BB AA → canal alpha optionnel — pair de 2
│ │ │ │
│ │ │ └─── quantité de transparence/quantity of transparent
│ │ └────── quantité de bleu/quantity of blue
│ └───────── quantité de vert/quantity of green
└──────────── quantité de rouge/quantity of red
→ Chaque pair est un nombre sur 8 bits (0 à 255) écrit en base 16 (héxa)
→ 0 1 2 3 4 5 6 7 8 9 A B C D E F
→ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 → total 16 valeurs différentes
→ 00 : minimum (0) → FF : maximum (255 = 15 * 16 + 15) → 0F = 15, 10 = 16, 11 = 17, …
→ #000000 = noir → #FFFFFF = blanc
→ float3(0.0, 0.0, 0.0) → ou float4(1.0, 1.0, 1.0, 1.0)
→ BF = 11 * 16 + 15 = 191 = 191/255 = ~0.749096 → normalisé : float = entier / 255.0
→ 0.749096 * 255 = 191 = 191 / 11 reste 15 = BF
Hex → float4 converter
Paste any hex color. The converter applies the sRGB → linear transfer function so values are ready to drop into Metal.
Colors are linear RGBA — not sRGB. If you pick a hex value from a color picker (which lives in sRGB), convert it first.
Linéaire → Hex converter
L'inverse : colle une couleur linéaire telle qu'elle apparaît dans ton code (float4(…), { 1.0f, 0.2f, … }, tuple Blender…). Le convertisseur applique la fonction de transfert linéaire → sRGB et te rend le hex.
Roue chromatique & palette GPU
Choisis une harmonie, déplace les poignées sur la roue (la grosse = couleur de base), puis envoie les couleurs dans ta palette (20 max). Tu peux renommer, réordonner (glisser-déposer), importer des hex depuis Adobe Color / Coolors, générer un dégradé, puis exporter le code prêt à coller pour MSL, C++, Blender, GLSL ou HLSL. La palette est sauvegardée dans ton navigateur.
Clique sur une cellule pour la copier. Linéaire sRGB = ce que tu passes à un shader qui écrit dans un bgra8Unorm_srgb ou un buffer HDR. P3 linéaire si ta CAMetalLayer est en Display P3 (colorspace). HSV [0..1] = l'entrée de hsv_to_rgb().
Réflexion des couleurs
Quelques opérations entre vecteurs servant pour stocker nos couleurs normalisées :
// réflexion du blanc
float4 white = (1.0, 1.0, 1.0, 1.0);
float4 purple = (0.78, 0.57, 0.92, 1.0);
float4 result = white * purple; // (0.78, 0.57, 0.92, 1.0)
// réflexion du bleu — seul ce canal est conservé et ainsi de suite
float4 blue = (0.0, 0.0, 1.0, 1.0);
float4 purple = (0.78, 0.57, 0.92, 1.0);
float4 result = blue * purple; // (0.0, 0.0, 0.92, 1.0)
// en pratique
float4 orange = (0.96, 0.64, 0.29, 1.0);
float4 purple = (0.78, 0.57, 0.92, 1.0);
float4 result = blue * purple; // orange foncé / brun
Dans notre projet
Modifies la couleur du cube
/// MSL — Perspective.metal — update:18/05/26
half4 fragment fragment_main_perspective(v2f in [[stage_in]])
{
half4 orangeClair = half4(0.96, 0.64, 0.29, 1.0);
half4 purple = half4(0.78, 0.57, 0.92, 1.0);
return purple * orangeClair;
}
/// C++ — Perspective.hpp — update:18/05/26
private:
static constexpr int kCubeCount = 8;
CubeInstance m_instances[kCubeCount];
/// C++ — Mesh_shared.h — update:18/05/26
struct CubeInstance // on peut très bien utiliser VertexIn3DMesh
{
simd::float4 position;
simd::float4 color;
};
/// C++ — Perspective.cpp — update:18/05/26 — buildGeometry()
void Perspective::buildGeometry(MTL::Device* device)
{
mesh = meshes::cubeMesh(0.23f, device);
const simd::float4 colors[kCubeCount] = { // couleurs MSL normalisées [0..1]
{ 1.0f, 0.18f, 0.18f, 1.0f }, // rouge
{ 1.0f, 0.55f, 0.0f, 1.0f }, // orange
{ 1.0f, 0.92f, 0.1f, 1.0f }, // jaune
{ 0.15f, 0.85f, 0.3f, 1.0f }, // vert
{ 0.1f, 0.55f, 1.0f, 1.0f }, // bleu
{ 0.5f, 0.2f, 0.9f, 1.0f }, // violet
{ 1.0f, 0.35f, 0.75f, 1.0f }, // rose
{ 0.9f, 0.9f, 0.9f, 1.0f }, // blanc
};
for (int i = 0; i < kCubeCount; ++i)
{
float x = (i % 4) * 1.1f - 1.65f;
float y = (i < 4) ? 0.6f : -0.6f; // opération ternaire
m_instances[i].position = { x, y, 0.0f, 0.f };
m_instances[i].color = colors[i];
}
}
si i < 4 alors
y ← 0.6
sinon
y ← -0.6
fin si
/// C++ — Perspective.cpp — update:18/05/26 — draw()
void Perspective::draw(MTL::RenderCommandEncoder* renderCommandEncoder, GlobalUniforms globalUniforms)
{
renderCommandEncoder->setRenderPipelineState(m_renderPipelineState.get());
renderCommandEncoder->setDepthStencilState(m_depthStencilState.get());
renderCommandEncoder->setVertexBuffer(mesh.vertices, 0, 0);
renderCommandEncoder->setVertexBytes(&globalUniforms, sizeof(GlobalUniforms), 1);
renderCommandEncoder->setFragmentBytes(&globalUniforms, sizeof(GlobalUniforms), 1);
renderCommandEncoder->setVertexBytes(m_instances, sizeof(m_instances), 2); // slot 2 = instances
renderCommandEncoder->drawIndexedPrimitives(MTL::PrimitiveTypeTriangle, mesh.numIndices,
mesh.indexType, mesh.indices, 0,
kCubeCount); // instanceCount
};
/// MSL — Perspective.metal — update:18/05/26
struct v2f
{
float4 position [[position]];
float4 normal;
float3 worldPosition; // position avant projection
half3 color;
};
/// MSL — Perspective.metal — update:18/05/26
v2f vertex vertex_main_perspective(constant VertexInWithNormal* vertexIn [[buffer(0)]],
constant GlobalUniforms& uniforms [[buffer(1)]],
constant CubeInstance* instances [[buffer(2)]],
uint vertexId [[vertex_id]],
uint iid [[instance_id]])
{
CubeInstance cube = instances[iid];
float3 worldPos = vertexIn[vertexId].position.xyz + cube.position.xyz;
v2f out;
out.position = uniforms.cameraUniforms.viewProjectionMatrix * float4(worldPos, 1.0);
out.normal = vertexIn[vertexId].normal;
out.worldPosition = worldPos;
out.color = half3(cube.color.rgb);
return out;
}
/// MSL — Perspective.metal — update:18/05/26
half4 fragment fragment_main_perspective(v2f in [[stage_in]])
{
return half4(half3(in.color), 1.0);
}
→ Mets en commentaire toute la passe qui permet d'afficher le buffer qui stocke les données de texture de profondeur pour retrouver les couleurs.
Scène avec lumière
Déclarer un soleil.
On l'affichera plus tard. Pour l'instant le shader lit sa position.
Re-structure les uniforms globaux en y ajoutant les données de soleil/lumière :
/// C++ — Renderer_shared.h — update:18/05/26
struct alignas(16) Sun
{
simd::float4 sunDirection;
simd::float4 sunColor;
simd::float4 cameraPosition;
float frameTime = 0.f;
};
/// C++ — Renderer_shared.h — update:18/05/26
struct alignas(16) GlobalUniforms
{
CameraUniforms cameraUniforms;
Sun sun;
};
Déclares une fonction qui va modifier la couleur du soleil à chaque frame.
/// C++ — Renderer.hpp — update:18/05/26 — avant la déclaration de la classe ou dans le .cpp
static inline simd::float3 mix3(simd::float3 a, simd::float3 b, float t)
{
return a + t * (b - a);
}
/// C++ — Renderer.hpp — update:18/05/26
private:
static simd::float3 sunColorForAngle(float sunAngle);
/// C++ — Renderer.cpp — update:18/05/26
simd::float3 Renderer::sunColorForAngle(float sunAngle)
{
constexpr simd::float3 kSunrise = { 1.0f, 0.4f, 0.2f };
constexpr simd::float3 kDay = { 1.0f, 0.95f, 0.9f };
constexpr simd::float3 kNight = { 0.1f, 0.1f, 0.2f };
if (sunAngle > M_PI)
{
float p = (sunAngle - M_PI) / M_PI;
if (p < 0.3f)
return mix3(kSunrise, kNight, simd::smoothstep(0.0f, 1.0f, p / 0.3f));
if (p < 0.7f)
return kNight;
return mix3(kNight, kSunrise, simd::smoothstep(0.0f, 1.0f, (p - 0.7f) / 0.3f));
}
float p = sunAngle / M_PI;
if (p < 0.2f)
return mix3(kSunrise, kDay, simd::smoothstep(0.0f, 1.0f, p / 0.2f));
if (p < 0.8f)
return kDay;
return mix3(kDay, kSunrise, simd::smoothstep(0.0f, 1.0f, (p - 0.8f) / 0.2f));
}
Mets tout ça à jour dans update()
/// C++ — Renderer.cpp — update:18/05/26 — update()
void Renderer::update(float delta)
{
m_game2d.update(delta, m_input);
const float speed = 3.0f * delta;
m_globalUniforms.sun.cameraPosition.xyz = m_camera.getPosition();
if (m_input.keyZ) m_globalUniforms.sun.cameraPosition.xyz += m_camera.getForward() * speed;
if (m_input.keyS) m_globalUniforms.sun.cameraPosition.xyz -= m_camera.getForward() * speed;
if (m_input.keyQ) m_globalUniforms.sun.cameraPosition.xyz += m_camera.getLeft() * speed;
if (m_input.keyD) m_globalUniforms.sun.cameraPosition.xyz -= m_camera.getLeft() * speed;
if (m_input.keyA) m_globalUniforms.sun.cameraPosition.xyz += m_camera.getUp() * speed;
if (m_input.keyE) m_globalUniforms.sun.cameraPosition.xyz -= m_camera.getUp() * speed;
m_camera.setPosition(m_globalUniforms.sun.cameraPosition.xyz);
m_cursor3D.updateUniforms(m_globalUniforms.cameraUniforms.invViewProjectionMatrix, m_windowSize, m_input.mousePosition);
m_globalUniforms.sun.frameTime += delta;
float timeOfDay = fmod(m_globalUniforms.sun.frameTime, 120.f) / 120.f;
float sunAngle = timeOfDay * 2.0f * M_PI;
float tilt = M_PI * 0.1f; // 18° d'inclinaison, comme la réalité
simd::float3 direction = simd::normalize(simd::float3{ cos(sunAngle) * sin(tilt), sin(sunAngle), cos(sunAngle) * cos(tilt) });
m_globalUniforms.sun.sunDirection = { direction.x, direction.y, direction.z, 0.f };
simd::float3 color = sunColorForAngle(sunAngle);
m_globalUniforms.sun.sunColor = { color.x, color.y, color.z, 0.f };
m_globalUniforms.cameraUniforms = m_camera.updateUniforms();
// rotation de la caméra :
m_camera.rotateOnAxis({0.0f, 1.0f, 0.0f}, m_input.mouseDelta.x);
m_camera.rotateOnAxis(-m_camera.getLeft(), m_input.mouseDelta.y);
};
/// MSL — Perspective.metal — update:18/05/26
half4 fragment fragment_main_perspective(v2f in [[stage_in]],
constant GlobalUniforms& uniforms [[buffer(1)]])
{
float3 normal = normalize(in.normal.xyz);
float3 lightDir = normalize(uniforms.sun.sunDirection.xyz);
// dot product : 0.0 = perpendiculaire, 1.0 = face à la lumière
float diff = max(dot(normal, lightDir), 0.0);
float3 ambient = 0.15f * float3(in.color);
float3 diffuse = diff * float3(in.color) * float3(uniforms.sun.sunColor.rgb);
return half4(half3(ambient + diffuse), 1.0);
}
Un refresh de notre fonction draw() principale
Ta fonction devrait ressemble à ça :
/// C++ — Renderer.cpp — update:18/05/26
void Renderer::draw(MTK::View* view, double timeStamp, float delta)
{
NS::AutoreleasePool* autoreleasePool = NS::AutoreleasePool::alloc()->init();
update(delta);
frame += 1;
MTL::CommandBuffer* commandBuffer = m_commandQueue->commandBuffer();
MTL::RenderPassDescriptor* renderPassDescriptor = view->currentRenderPassDescriptor();
renderPassDescriptor->colorAttachments()->object(0)->setClearColor(MTL::ClearColor(0.9, 0.9, 0.9, 1.0));
renderPassDescriptor->depthAttachment()->setTexture(m_depth.get());
renderPassDescriptor->depthAttachment()->setLoadAction(MTL::LoadActionClear);
renderPassDescriptor->depthAttachment()->setClearDepth(1.0f); // loin
renderPassDescriptor->depthAttachment()->setStoreAction(MTL::StoreActionStore);
{
MTL::RenderCommandEncoder* renderCommandEncoder = commandBuffer->renderCommandEncoder(renderPassDescriptor);
renderCommandEncoder->setViewport(m_viewport);
renderCommandEncoder->setDepthStencilState(m_depthStencilState.get());
renderCommandEncoder->setRenderPipelineState(m_renderPipelineState.get());
renderCommandEncoder->setVertexBuffer(m_windowSizeBuffer.get(), 0, 1);
renderCommandEncoder->setFragmentBuffer(m_windowSizeBuffer.get(), 0, 1);
renderCommandEncoder->setFragmentBytes(&m_windowSize, sizeof(m_windowSize), 1);
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));
m_game2d.draw(renderCommandEncoder, timeStamp, delta, m_windowSize);
m_perspective.draw(renderCommandEncoder, m_globalUniforms);
m_text.clearBatch();
TextRendering::TextRenderOptions opts;
opts.color = {1.0f, 0.37254901960784315f, 0.12156862745098039, 1.0f};
opts.scale = 128.f;
opts.thickness = 0.5f;
opts.outlineWidth = 0.23f;
opts.outlineColor = {0.0f, 0.0f, 0.0f, 1.0f}; // Contour noir
opts.alignment = TextRendering::TextRenderOptions::Alignment::Center;
m_text.generateTextMesh("LearnMSL.dev",
m_viewport.width / 2, m_viewport.height / 2 - 50,
opts, TextRendering::FontID::Syne);
opts.color = {1.0f, 1.0f, 0.7f, 0.82f};
opts.scale = 129.f;
m_text.generateTextMesh("LearnMSL.dev",
m_viewport.width / 2, m_viewport.height / 2 - 50,
opts, TextRendering::FontID::Syne);
m_text.drawLogo(m_viewport.width * 0.5f, m_viewport.height * 0.5f + 50, 128.f);
m_text.render(renderCommandEncoder, m_viewport.width, m_viewport.height);
if (frame % 5000 == 10)
m_audioEngine->playSoundEvent("mrSpoiler.mp3");
if (m_input.keySpace)
m_audioEngine->playSoundEvent("Test.m4a");
renderCommandEncoder->endEncoding();
}
// {
// auto gbufferRenderPassDesc= NS::TransferPtr(MTL::RenderPassDescriptor::alloc()->init());
// gbufferRenderPassDesc->colorAttachments()->object(0)->setTexture(view->currentDrawable()->texture());
// gbufferRenderPassDesc->colorAttachments()->object(0)->setLoadAction(MTL::LoadActionLoad);
// gbufferRenderPassDesc->colorAttachments()->object(0)->setStoreAction(MTL::StoreActionStore);
// auto gbufferRenderCmdEnc = commandBuffer->renderCommandEncoder(gbufferRenderPassDesc.get());
// gbufferRenderCmdEnc->setRenderPipelineState(m_depthDebugPipelineState.get());
// gbufferRenderCmdEnc->setViewport(m_viewport);
// gbufferRenderCmdEnc->setFragmentTexture(m_depth.get(), 0);
// gbufferRenderCmdEnc->setFragmentBytes(&m_windowSize, sizeof(m_windowSize), 1);
// gbufferRenderCmdEnc->setVertexBuffer(m_windowQuad.vertices, 0, 0);
// gbufferRenderCmdEnc->setVertexBytes(&m_windowSize, sizeof(m_windowSize), 1);
// simd::float2 origin = { 0.f, 0.f };
// gbufferRenderCmdEnc->setVertexBytes(&origin, sizeof(origin), 2);
// gbufferRenderCmdEnc->drawIndexedPrimitives(MTL::PrimitiveTypeTriangle, m_windowQuad.numIndices,
// m_windowQuad.indexType, m_windowQuad.indices, NS::UInteger(0));
// gbufferRenderCmdEnc->endEncoding();
// }
m_cursor3D.pick(commandBuffer, m_depth.get());
commandBuffer->presentDrawable(view->currentDrawable());
commandBuffer->commit();
commandBuffer->waitUntilCompleted();
PickResult result = m_cursor3D.getResult();
m_cursorPosition3D.x = roundf(result.worldPosition.x);
m_cursorPosition3D.y = roundf(result.worldPosition.y);
m_cursorPosition3D.z = roundf(result.worldPosition.z);
// if (frame % 240 == 0)
// printf("mouse2D : x = %.10f, y = %.10f\n", m_input.mousePosition.x, m_input.mousePosition.y);
// if (frame % 240 == 0)
// printf("mouse3D : x = %.10f, y = %.10f, z = %.10f\n", m_cursorPosition3D.x, m_cursorPosition3D.y, m_cursorPosition3D.z);
autoreleasePool->release();
}
4 % 4 = 0
5 modulo 4 = 1
Il s'agit du reste de la division.
Structure potentielle de ton dossier
MSLearn/
├── MSLearn.entitlements
├── Application/
│ ├── main.m
│ ├── AppViewController.h
│ ├── AppViewController.mm
│ └── macOS/
│ ├── AppIcon.icns
│ └── macOSInfo.plist
├── Frameworks/
│ ├── metal-cpp/ …
│ └── metal-cpp-extensions/ …
├── Renderer/
│ ├── TextRendering.hpp
│ ├── TextRendering.cpp
│ ├── NonCopyable.hpp
│ ├── Audio.hpp
│ ├── Audio.mm
│ ├── Mesh.cpp
│ ├── Mesh.hpp
│ ├── Perspective.cpp
│ ├── Perspective.hpp
│ ├── Math.cpp
│ ├── Math.hpp
│ ├── Cursor3D.cpp
│ ├── Cursor3D.hpp
│ ├── Camera.hpp
│ ├── Camera.cpp
│ ├── Game2D.cpp
│ └── Game2D.hpp
│ ├── Renderer.cpp
│ └── Renderer.hpp
├── Shaders/
│ ├── Shapes.metal
│ ├── Radial.metal
│ ├── GBuffer.metal
│ ├── Cursor3D.metal
│ ├── Fullscreen.metal
│ ├── Perspective.metal
│ └── TextRendering.metal
├── includes/
│ ├── SharedGPU/
│ │ ├── Mesh_shared.h
│ │ ├── Cursor3D_shared.h
│ │ ├── Renderer_shared.h
│ │ └── TextRendering_shared.h
│ ├── extern/
│ │ └── stb-image.h
│ └── SharedAPP/
│ └── InputState.h
├── Test.m4a
├── mrSpoiler.mp3
├── syne2048xBold.png
├── ibmplexmonosemibold4096.png
├── metrics.csv
├── metricsibmplexmonosemibold4096.csv
├── .gitignore
├── .git
└── Makefile