本系列文章是对 metalkit.org 上面MetalKit内容的全面翻译和学习.
上一节我说我们将学习Metal shading language
.在学之前,我们先做一些代码清理和重构.从下载前一节的源代码 source code开始.我们将从重构render() 函数开始.所以让我们取出vertex buffer和render pipeline state,并创建3个新的函数放进去,这样我们的旧函数就减少到这样:
var vertex_buffer: MTLBuffer!
var rps: MTLRenderPipelineState! = nil
func render() {
device = MTLCreateSystemDefaultDevice()
createBuffer()
registerShaders()
sendToGPU()
}
复制代码
我们先对createBuffer() 函数做一些改变.回忆上一节vertex data是Float
类型的数组,像这样:
let vertex_data:[Float] = [-1.0, -1.0, 0.0, 1.0,
1.0, -1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0]
复制代码
让我们把它转换成更好的格式,一个带有两个vector_float4
类型成员的结构体,一个position另一个是color:
struct Vertex {
var position: vector_float4
var color: vector_float4
}
复制代码
你可能会好奇vector_float4到底是什么样的数据类型.从苹果官方文档中我们发现,这种向量类型是一种clang基础类型,比传统的SIMD
类型更适合向量-向量和向量-标量的算术运算.它可以通过类似数组下标来访问向量的成员分量,具体作法是用 . 操作符和组件名称访问(x
,y
,z
,w
,或它们的组合).除了 .xyzw组件名外,下面的子向量也能通过:.lo / .hi(向量的前半部分和后半部分)来轻松访问,还有奇偶位的 .even / .odd子向量:
vector_float4 x = 1.0f; // x = { 1, 1, 1, 1 }.
vector_float3 y = { 1, 2, 3 }; // y = { 1, 2, 3 }.
x.xyz = y.zyx; // x = { 1/3, 1/2, 1, 1 }.
x.w = 0; // x = { 1/4, 1/3, 1/2, 0 }.
复制代码
让我们返回到createBuffer()
用新的结构体来替换vertex-data:
func createBuffer() {
let vertex_data = [Vertex(position: [-1.0, -1.0, 0.0, 1.0], color: [1, 0, 0, 1]),
Vertex(position: [ 1.0, -1.0, 0.0, 1.0], color: [0, 1, 0, 1]),
Vertex(position: [ 0.0, 1.0, 0.0, 1.0], color: [0, 0, 1, 1])]
vertex_buffer = device!.newBufferWithBytes(vertex_data, length: sizeof(Vertex) * 3, options:[])
}
复制代码
你看,通过简单地将它转成结构体数组,我们可以轻易创建顶点数据.
同时,我们保持顶点位置仍在上次的位置上,并且我们为每个顶点添加单独的颜色(红,绿,蓝).接下来,是registerShaders() 函数.我们无需改变旧代码,只需要将它移动到新的地方:
func registerShaders() {
let library = device!.newDefaultLibrary()!
let vertex_func = library.newFunctionWithName("vertex_func")
let frag_func = library.newFunctionWithName("fragment_func")
let rpld = MTLRenderPipelineDescriptor()
rpld.vertexFunction = vertex_func
rpld.fragmentFunction = frag_func
rpld.colorAttachments[0].pixelFormat = .BGRA8Unorm
do {
try rps = device!.newRenderPipelineStateWithDescriptor(rpld)
} catch let error {
self.print("\(error)")
}
}
复制代码
最后,我们对sendToGPU() 函数也做同样的操作,不改变旧代码只移动到新地方:
func sendToGPU() {
if let rpd = currentRenderPassDescriptor, drawable = currentDrawable {
rpd.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 0.5, 0.5, 1.0)
let command_buffer = device!.newCommandQueue().commandBuffer()
let command_encoder = command_buffer.renderCommandEncoderWithDescriptor(rpd)
command_encoder.setRenderPipelineState(rps)
command_encoder.setVertexBuffer(vertex_buffer, offset: 0, atIndex: 0)
command_encoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
command_encoder.endEncoding()
command_buffer.presentDrawable(drawable)
command_buffer.commit()
}
}
复制代码
接下来让我们转移到Shaders.metal文件.这时我们做两处修改.首先,给我们的Vertex
结构体添加一个color成员,这样我们就可以在CPU
和GPU
之间来回传递数据:
struct Vertex {
float4 position [[position]];
float4 color;
};
复制代码
其次,我们替换上次在fragment着色器中使用的硬编码的颜色:
fragment float4 fragment_func(Vertex vert [[stage_in]]) {
return float4(0.7, 1, 1, 1);
}
复制代码
替换为每个顶点自带的实际颜色(通过vertex_buffer传递到GPU
):
fragment float4 fragment_func(Vertex vert [[stage_in]]) {
return vert.color;
}
复制代码
如果你运行程序,你看到一个更漂亮的彩色三角形:
你也许会奇怪,为什么我们只传递给三个顶点对应颜色,但顶点之间的颜色却是渐变的?要理解这些,就必须先理解两种着色器的不同及它们在图形管线中角色的不同.让我们看看任一个着色器的语法(这里先顶点着色器作例子):
vertex Vertex vertex_func(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]])
复制代码
第一个关键词,是函数限定符只能使用vertex, fragment和kernel.下一个关键词是返回值类型.接下来是带有圆括号参数的函数name名称.Metal shading language
限定了指针的使用,必须用device,threadgroup或constant修饰符来声明,这些修饰符指定了函数变量或参数分配到的内存区域.[[...]] 语法是用来声明属性,例如资源位置,着色器输入,以及在着色器与CPU之间来回传递的内置变量.
Metal
使用 [[ buffer(index)]] 属性来标识出位置,让device
和constant buffer
的参数类型能够区分.内置的输入变量和输出变量被用来在图形函数(顶点和片段)与固定图形管线流程之间传递数据.在我们例子中 [[vertex_id]] 是传递过程中每个顶点的标识符.Metal
接收顶点函数和光栅产生的片段的输出,来产生输入到片段函数的各个片段.每个片段输入依靠 [[stage_in]] 属性修饰符来标识.
vertex shader
用指向顶点列表的指针作为第一个参数.我们可以用第2个参数vid来索引vertices顶点,其中的vid被赋值成vertex_id,它告诉Metal
插入当前正在被处理的顶点的索引作为第2个参数.然后只需传递每个顶点(包括位置和颜色)给fragment shader片段着色器
去处理.fragment shader片段着色器
所作的操作是,取出从vertex shader顶点着色器
中传过来的顶点,直接传给每个像素而无需改变输入数据.顶点着色器运行频率不高(本例中只需3次-每个顶点1次),但fragment shader片段着色器
运行几千次-每个需要绘制的像素一次.
所以你可能仍然会问:"ok,但是颜色渐变到底怎么回事呢?" 现在你理解了每个着色器的作用及运行频率,你可以认为任一个像素点的颜色都是它的附近像素颜色的平均值.例如,在red红
和green绿
颜色像素正中间的像素颜色将会是yellow黄
,只是因为fragment shader片段着色器
用平均数来产生颜色插值:0.5 * red + 0.5 * green.同样的,在red红
和blue蓝
正中间的颜色会是magenta品红
,在blue蓝
和green绿
正中间的颜色会是cyan青
.就这样,剩余部分像素都是用初始颜色的插值,最终结果就是你看到的渐变范围.
源代码source code 已发布在Github上.
下次见!