Chapitre -109 · reflexion

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.

float4 (linear)
—
float4 (raw ÷ 255)
—

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.

Hex—
Hex + alpha—
float4 sRGB encodé—
RGB 8 bits—
HSV—

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.

Luminosité (V) 100%
Couleur sélectionnée
Palette 0/20
Dégradé 1re → dernière en étapes

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().

Red vertex
{1.0, 0.2, 0.2, 1.0}
Green vertex
{0.2, 1.0, 0.4, 1.0}
Blue vertex
{0.2, 0.5, 1.0, 1.0}
Uniform white
{1.0, 1.0, 1.0, 1.0}
Amber tones
{0.96, 0.64, 0.29, 1.0}
Purple
{0.78, 0.57, 0.92, 1.0}

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;
}
⬡CheckPoint Compilation — aperçu dot product
/// 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];
    }
}
Opération ternaire en pseudo code
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.

⬡CheckPoint Compilation — sans gérer le soleil

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);
}
⬡CheckPoint Compilation — gestion du soleil

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();
}
Opération modulo — Exemples
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