8

Metal 示例之画 3D

 3 years ago
source link: http://blog.danthought.com/programming/2018/07/03/metal-by-example-3d/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文主要通过自己对 Metal By Example 理解编写,这一篇文章讲解 Metal 如何画 3D。

Metal By Example Cover

定义立方体

立方体需要 8 个顶点:

static const MBEVertex vertices[] =
{
  { .position = { -1,  1,  1, 1 }, .color = { 0, 1, 1, 1 } },
  { .position = { -1, -1,  1, 1 }, .color = { 0, 0, 1, 1 } },
  { .position = {  1, -1,  1, 1 }, .color = { 1, 0, 1, 1 } },
  { .position = {  1,  1,  1, 1 }, .color = { 1, 1, 1, 1 } },
  { .position = { -1,  1, -1, 1 }, .color = { 0, 1, 0, 1 } },
  { .position = { -1, -1, -1, 1 }, .color = { 0, 0, 0, 1 } },
  { .position = {  1, -1, -1, 1 }, .color = { 1, 0, 0, 1 } },
  { .position = {  1,  1, -1, 1 }, .color = { 1, 1, 0, 1 } }
};

self.vertexBuffer = [self.device newBufferWithBytes:vertices
                                             length:sizeof(vertices)
                                            options:MTLResourceCPUCacheModeDefaultCache];

立方体的每一个平面都是由三角形绘制而成,需要一个 Index Buffer 包含每个三角形对应顶点的下标:

typedef uint16_t MBEIndex;
const MTLIndexType MBEIndexType = MTLIndexTypeUInt16;

static const MBEIndex indices[] =
{
  3, 2, 6, 6, 7, 3,
  4, 5, 1, 1, 0, 4,
  4, 0, 3, 3, 7, 4,
  1, 5, 6, 6, 2, 1,
  0, 1, 2, 2, 3, 0,
  7, 6, 5, 5, 4, 7
};

self.indexBuffer = [self.device newBufferWithBytes:indices
                                            length:sizeof(indices)
                                           options:MTLResourceOptionCPUCacheModeDefault];
self.indexBuffer.label = @"Indices";

物体空间(object space)转换为世界空间(world space)

物体空间就是立方体各顶点坐标相对立方体中心的本地坐标体系,世界空间就是真实世界坐标体系:

@property (nonatomic, assign) float rotationX, rotationY, time;

self.time += duration;
self.rotationX += duration * (M_PI / 2);
self.rotationY += duration * (M_PI / 3);
float scaleFactor = sinf(5 * self.time) * 0.25 + 1;
const vector_float3 xAxis = { 1, 0, 0 };
const vector_float3 yAxis = { 0, 1, 0 };
const matrix_float4x4 xRot = matrix_float4x4_rotation(xAxis, self.rotationX);
const matrix_float4x4 yRot = matrix_float4x4_rotation(yAxis, self.rotationY);
const matrix_float4x4 scale = matrix_float4x4_uniform_scale(scaleFactor);
const matrix_float4x4 modelMatrix = matrix_multiply(matrix_multiply(xRot, yRot), scale);

世界空间(world space)转换为相机空间(camera space、view space、eye space),再转换为裁剪空间(clip space)

相机空间就是从相机或者观察者的眼睛角度看物体时的坐标体系,物体在下面这个截头锥体中,就是相机或眼睛中能看见物体:

Metal By Example View Frustum

相机空间这一步的转换可以简单的理解为调整相机的位置,这里调整方式是世界空间的右手系,Y 轴向上,Z 轴垂直于屏幕向外:

const vector_float3 cameraTranslation = { 0, 0, -5 };
const matrix_float4x4 viewMatrix = matrix_float4x4_translation(cameraTranslation);

前面说到物体在截头锥体中才能被看见,物体不在截头锥体的部分怎么剪裁掉呢?通过裁剪空间,裁剪空间被 GPU 用来决定三角形的可见性,如果三角形的三个顶点都在裁剪空间之外,三角形就不会被渲染(culled),三角形的至少一个顶点在裁剪空间之内,三角形会根据边界被剪裁掉,相机空间转换为裁剪空间通过 perspective projection 矩阵的运算,可以向 matrix_float4x4_perspective 运算方法提供 field of view 改变视锥体竖直方向的张开角度、near 和 far 控制视锥体的近裁剪平面和远裁剪平面距离摄像机的远近:

const CGSize drawableSize = view.metalLayer.drawableSize;
const float aspect = drawableSize.width / drawableSize.height;
const float fov = (2 * M_PI) / 5;
const float near = 1;
const float far = 100;
const matrix_float4x4 projectionMatrix = matrix_float4x4_perspective(aspect, fov, near, far);

我们可以计算出 model-view-projection (MVP) 矩阵:

MBEUniforms uniforms;
uniforms.modelViewProjectionMatrix = matrix_multiply(projectionMatrix, matrix_multiply(viewMatrix, modelMatrix));

裁剪空间(clip space)转换为 NDC 空间,再转换为窗口坐标

这里不需要自己写代码转换,MVP 坐标除以 w 转换为规格化设备坐标(normalized device coordinates), Metal 的 NDC 空间就是这样一个立方体 [−1, 1]×[−1, 1]×[0, 1],最后 NDC 再转换为窗口坐标。

vertex shader 中每一个顶点坐标和 MVP 矩阵相乘得到转化后的坐标,fragment shader 只是单纯地返回颜色,注意 device 前缀表明地址空间是 per-vertex 或 per-fragment,constant 前缀表明地址空间在每次调用都不会改变:

#include <metal_stdlib>

using namespace metal;

struct Vertex
{
  float4 position [[position]];
  float4 color;
};

struct Uniforms
{
  float4x4 modelViewProjectionMatrix;
};

vertex Vertex vertex_project(device Vertex *vertices [[buffer(0)]],
                             constant Uniforms *uniforms [[buffer(1)]],
                             uint vid [[vertex_id]])
{
  Vertex vertexOut;
  vertexOut.position = uniforms->modelViewProjectionMatrix * vertices[vid].position;
  vertexOut.color = vertices[vid].color;
  
  return vertexOut;
}

fragment half4 fragment_flatcolor(Vertex vertexIn [[stage_in]])
{
  return half4(vertexIn.color);
}

配置 Render Pass,向 Render Pass 中传递对应的 Buffer,根据 Index 获取 Buffer 中的三角形顶点来绘制三角形:

id<MTLRenderCommandEncoder> renderPass = [commandBuffer renderCommandEncoderWithDescriptor:passDescriptor];
[renderPass setRenderPipelineState:self.pipeline];
[renderPass setDepthStencilState:self.depthStencilState];
[renderPass setFrontFacingWinding:MTLWindingCounterClockwise];
[renderPass setCullMode:MTLCullModeBack];

const NSUInteger uniformBufferOffset = sizeof(MBEUniforms) * self.bufferIndex;

[renderPass setVertexBuffer:self.vertexBuffer offset:0 atIndex:0];
[renderPass setVertexBuffer:self.uniformBuffer offset:uniformBufferOffset atIndex:1];

[renderPass drawIndexedPrimitives:MTLPrimitiveTypeTriangle
                       indexCount:[self.indexBuffer length] / sizeof(MBEIndex)
                        indexType:MBEIndexType
                      indexBuffer:self.indexBuffer
                indexBufferOffset:0];

[renderPass endEncoding];

代码和效果

danjiang / MetalByExample / 3D


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK