Chapitre -117 · Perspective

Le point de vue en perspective

Une fois le chapitre Caméra suivi, nous voulons afficher notre premier cube en fullscreen.

Des souvenirs te reviennent ? On a codé le cube dans la bibliothèque Mesh sans l'utiliser car le sujet est proche des figures 2D.

Structure du projet

MSLearn/
├── MSLearn.entitlements
├── Application/ …
├── Frameworks/ …
├── Renderer/
│   ├── Renderer.cpp
│   ├── Renderer.hpp
│   ├── Mesh.cpp
│   ├── Mesh.hpp
│   ├── Perspective.cpp
│   ├── Perspective.hpp
│   ├── Math.cpp
│   ├── Math.hpp
│   ├── Camera.hpp
│   ├── Camera.cpp
│   ├── Game2D.cpp
│   └── Game2D.hpp
├── Shaders/
│   ├── Fullscreen.metal
│   ├── Perspective.metal       ← ajoutes
│   └── Shapes.metal/
├── includes/
│   ├── SharedAPP/
│   │   └── InputState.h
│   └── SharedGPU/
│       ├── Mesh_shared.h
│       └── Render_shared.h
└── Makefile

Le shader avec projection

Le cube est au centre, là où la caméra regarde.

Pour chaque vertices, multiplication de la matrice viewProjectionMatrix x le vecteur.

/// MSL — Perspective.metal
#include <metal_stdlib>

#include "../includes/SharedGPU/Mesh_shared.h"
#include "../includes/SharedGPU/Renderer_shared.h"

using namespace metal;

struct v2f
{
    float4 position [[position]];
    float4 normal;
    half3  color;
};

v2f vertex vertex_main_perspective(constant VertexIn3DMesh* vextexIn [[buffer(0)]],
                                   constant GlobalUniforms& uniforms [[buffer(1)]],
                                   uint                     vertexId [[vertex_id]])
{
    v2f out;
    out.position = uniforms.cameraUniforms.viewProjectionMatrix * float4(vextexIn[vertexId].position.xyz, 1.0);
    out.normal = vextexIn[vertexId].texcoord; // Même struct mais gère les normales et c'est ok
    return out;
}

half4 fragment fragment_main_perspective(v2f in [[stage_in]])
{
    return half4(1.0h, 0.23h, 0.23h, 1.0h);
}

viewProjectionMatrix — la matrice du vertex shader. C'est la matrice qu'on utilise dans 99% des vertex shaders. Elle combine view et projection en une seule multiplication.

half4

En pratique pour ce qui est envoyé au GPU, Apple Silicon a des unités ALU optimisées pour le calcul en précision 16 bits. Un flottant égal à 4 bits c’est pourquoi il est préférable de travailler avec des valeurs de 16 bits qui seront alignés sans aucun doute.

Throughput doublé sur les opérations arithmétiques en half.

Pour le depth du chapitre suivant pour l'exemple, half manque de précision et/ou peut dériver (lighting PBR, HDR, depth reconstruction).

rappel: float4 = 32 bits

/// C++ — Perspective.hpp — update:18/05/26
#ifndef Perspective_hpp
#define Perspective_hpp

#include "Mesh.hpp"

#include "../includes/SharedAPP/InputState.h"
#include "../includes/SharedGPU/Renderer_shared.h"

class Perspective
{
public:
    Perspective(MTL::Device*, MTL::Library*,
                MTL::PixelFormat colorPixelFormat, MTL::PixelFormat depthPixelFormat,
                const std::string& resourcePath);
    ~Perspective();

    void draw(MTL::RenderCommandEncoder* renderCommandEncoder, GlobalUniforms globalUniforms);
private:
    NS::SharedPtr<MTL::Buffer>              m_vertexBuffer;
    NS::SharedPtr<MTL::DepthStencilState>   m_depthStencilState;
    NS::SharedPtr<MTL::RenderPipelineState> m_renderPipelineState;

    Mesh3D mesh;

    void buildPipeline(MTL::Device*, MTL::Library*,
                       MTL::PixelFormat colorPixelFormat, MTL::PixelFormat depthPixelFormat);
    void buildGeometry(MTL::Device* device);
};

#endif /* Perspective_hpp */

La logique

///  C++ — Perspective.cpp — update:18/05/26
#include "Perspective.hpp"

Perspective::Perspective(MTL::Device* device, MTL::Library* shaderLibrary,
                         MTL::PixelFormat colorPixelFormat, MTL::PixelFormat depthPixelFormat,
                         const std::string& resourcePath)
{
    memset(&mesh, 0x0, sizeof(Mesh3D)); // remplis le buffer avec des zéros (0xF = 1)
    buildGeometry(device); // et build/construit les buffers
    buildPipeline(device, shaderLibrary, colorPixelFormat, depthPixelFormat);
}

Perspective::~Perspective()
{
    meshes::releaseMesh(&mesh);
}

void Perspective::buildPipeline(MTL::Device* device, MTL::Library* shaderLibrary,
                                MTL::PixelFormat colorPixelFormat, MTL::PixelFormat depthPixelFormat)
{
    auto vertexFunction = NS::TransferPtr(shaderLibrary->newFunction(MTLSTR("vertex_main_perspective")));
    auto fragmentFunction = NS::TransferPtr(shaderLibrary->newFunction(MTLSTR("fragment_main_perspective")));
    auto renderPipelineDescriptor = NS::TransferPtr(MTL::RenderPipelineDescriptor::alloc()->init());
    renderPipelineDescriptor->setVertexFunction(vertexFunction.get());
    renderPipelineDescriptor->setFragmentFunction(fragmentFunction.get());
    renderPipelineDescriptor->colorAttachments()->object(0)->setPixelFormat(colorPixelFormat);
    renderPipelineDescriptor->setDepthAttachmentPixelFormat(depthPixelFormat);
    NS::Error* error = nullptr;
    m_renderPipelineState = NS::TransferPtr(device->newRenderPipelineState(renderPipelineDescriptor.get(), &error));
    assert(!error && "Pipeline creation failed");
    auto _depthStencilDescriptor = NS::TransferPtr(MTL::DepthStencilDescriptor::alloc()->init());
    _depthStencilDescriptor->setDepthCompareFunction(MTL::CompareFunction::CompareFunctionGreaterEqual);
    _depthStencilDescriptor->setDepthWriteEnabled(true); // on veut que notre cube soit en avant plan
    m_depthStencilState = NS::TransferPtr(device->newDepthStencilState(_depthStencilDescriptor.get()));
}

void Perspective::buildGeometry(MTL::Device* device)
{
    mesh = meshes::cubeMeshWithoutTexture(0.5, device);
}

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->drawIndexedPrimitives(MTL::PrimitiveTypeTriangle, mesh.numIndices, mesh.indexType, mesh.indices, 0);
};

ENFIN les essais

/// C++ — Renderer.hpp — update:18/05/26
#include "Camera.hpp"
#include "Perspective.hpp"

private:
    MTL::PixelFormat            m_depthPixelFormat;
    Perspective                 m_perspective;
    Camera                      m_camera;
    GlobalUniforms              m_globalUniforms;
/// C++ — Renderer.cpp — update:18/05/26
m_game2d(m_device, m_shaderLibrary, m_pixelFormat, m_depthPixelFormat),
m_perspective(m_device, m_shaderLibrary, m_pixelFormat, m_depthPixelFormat, resourcePath),
/// C++ — Renderer.cpp — update:18/05/26
{
    // initPerspectiveWithPosition attend une DIRECTION, pas une target.
    // Dans notre cas ça a été calculé dans le chapitre Camera (0,0,1)
    const simd::float3 eye    = { 0.f, 0.f, -3.f };
    const simd::float3 target = { 0.f, 0.f, 0.f };
    const simd::float3 dir    = simd::normalize(target - eye); // évite NaN si std::normalized(0, 0, 0);
    m_camera.initPerspectiveWithPosition(eye, { 0.f, 0.f, 1.f },
                                         { 0.f, 1.f, 0.f }, // attention au NaN
                                         M_PI / 3.f, // 60°
                                         (float)((float)width / (float)height), 0.01f, 100.f);

    m_globalUniforms.cameraUniforms = m_camera.updateUniforms(); // build toutes les matrices
}
    /// C++ — Renderer.cpp — update:18/05/26 — update()
    m_game2d.update(delta, m_input);
    const float speed = 3.0f * delta; // Le delta du Quartz (CACurrentMediaTime())
    simd::float3 position = m_camera.getPosition();

    // si la touche Z est OUI, ajoute le vecteur de devant à la position xyz de la caméra
    if (m_input.keyZ) position += m_camera.getForward() * speed;
    if (m_input.keyS) position -= m_camera.getForward() * speed; // reculer
    if (m_input.keyQ) position += m_camera.getLeft()    * speed; // gauche
    if (m_input.keyD) position -= m_camera.getLeft()    * speed;
    if (m_input.keyA) position += m_camera.getUp()      * speed; // monter
    if (m_input.keyE) position -= m_camera.getUp()      * speed;

    m_camera.setPosition(position); // c'est la caméra qui change de position, pas le cube.
    
    m_globalUniforms.cameraUniforms = m_camera.updateUniforms(); // à chaque frame, si m_needRebuild = oui
        /// C++ — Renderer.cpp — update:18/05/26 — draw()
        m_game2d.draw(renderCommandEncoder, timeStamp, delta, m_windowSize);
        m_perspective.draw(renderCommandEncoder, m_globalUniforms);
    /// Objective-C++ — AppViewController.mm — update:18/05/26 — on map les touches
    InputState state;
    if ([_pressedKeys containsObject:@(0x0C)]) { state.keyA = true; }
    if ([_pressedKeys containsObject:@(0x0D)]) { state.keyZ = true; }
    if ([_pressedKeys containsObject:@(0x0E)]) { state.keyE = true; }
    if ([_pressedKeys containsObject:@(0x00)]) { state.keyQ = true; }
    if ([_pressedKeys containsObject:@(0x01)]) { state.keyS = true; }
    if ([_pressedKeys containsObject:@(0x02)]) { state.keyD = true; }
/// C++ — Renderer.cpp — update:18/05/26 — resizeViewWindow()
    updateViewportWindow(width, height);
    m_camera.setAspectRatio((float)((float)width / (float)height)); // ajoutes
⬡CheckPoint Compilation — essaies à ta manière