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