iOS开发:如何用MetalKit设置3D正方形正反面不同颜色?
嘿,这个需求在MetalKit里实现起来其实挺直观的,核心就是要让Metal允许渲染正方形的正反两面,然后在着色器里区分正反面来设置颜色。我给你拆解下步骤,再附上完整的代码示例:
先搞懂Metal的默认行为
Metal默认会开启背面剔除(Backface Culling)——也就是只渲染正面朝向相机的面,背面会被自动丢弃。所以第一步我们要修改渲染管道状态,让它允许渲染双面(或者单独渲染背面)。
两种实现思路
这里有两种常见的实现方式,我推荐第二种,更高效:
- 方式一:给顶点添加法向量数据,在片元着色器里通过法向量和视图方向的点积判断正反面
- 方式二:直接用Metal的内置属性
[[front_facing]]判断当前渲染的是正面还是背面(不用额外传递顶点数据,更省心)
完整代码示例
1. Metal着色器代码(.metal文件)
这部分是核心,通过[[front_facing]]变量区分正反面,设置不同颜色:
#include <metal_stdlib> using namespace metal; // 顶点输入结构体 struct VertexIn { float3 position [[attribute(0)]]; }; // 顶点输出结构体 struct VertexOut { float4 position [[position]]; }; // 顶点着色器:处理顶点变换 vertex VertexOut vertexShader(const VertexIn in [[stage_in]], constant float4x4& modelViewProjectionMatrix [[buffer(0)]]) { VertexOut out; out.position = modelViewProjectionMatrix * float4(in.position, 1.0); return out; } // 片元着色器:根据正反面返回不同颜色 fragment float4 fragmentShader(bool isFrontFace [[front_facing]]) { if (isFrontFace) { return float4(1.0, 0.0, 0.0, 1.0); // 正面设为红色 } else { return float4(0.0, 1.0, 0.0, 1.0); // 背面设为绿色 } }
2. Swift代码关键部分
配置渲染管道状态(关闭背面剔除)
在创建MTLRenderPipelineDescriptor时,我们需要修改剔除模式,让Metal渲染正反两面:
guard let device = MTLCreateSystemDefaultDevice(), let library = device.makeDefaultLibrary() else { fatalError("Metal is not supported on this device") } let pipelineDescriptor = MTLRenderPipelineDescriptor() pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertexShader") pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragmentShader") pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm // 关键:设置剔除模式为.none,允许渲染正反两面 // 如果只想单独渲染背面,可以把cullMode设为.front(剔除正面,只留背面) pipelineDescriptor.cullMode = .none // 可选:如果你的顶点绕序是顺时针,需要设置windingOrder为.clockwise,否则正反面判断会反 // pipelineDescriptor.windingOrder = .clockwise do { pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor) } catch { print("Failed to create pipeline state: \(error)") }
定义正方形顶点数据
我们需要用两个三角形组成一个正方形(Metal基于三角形图元渲染):
// 正方形顶点坐标(x,y,z),包含两个三角形的6个顶点 let squareVertices: [Float] = [ -0.5, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.5, 0.5, 0.0, -0.5, 0.5, 0.0, 0.5, -0.5, 0.0 ] // 创建顶点缓冲 let vertexBuffer = device.makeBuffer(bytes: squareVertices, length: squareVertices.count * MemoryLayout<Float>.stride, options: [])
渲染循环中的绘制逻辑
这里添加了一个旋转的MVP矩阵,让正方形可以转动,方便我们看到背面:
guard let renderPassDescriptor = view.currentRenderPassDescriptor, let commandQueue = device.makeCommandQueue(), let commandBuffer = commandQueue.makeCommandBuffer(), let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { return } renderEncoder.setRenderPipelineState(pipelineState) renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) // 生成旋转的MVP矩阵,让正方形绕Y轴旋转 let currentTime = Float(Date().timeIntervalSinceReferenceDate) let modelMatrix = float4x4(rotationAngle: currentTime * .pi, axis: float3(0, 1, 0)) let viewMatrix = float4x4(translation: float3(0, 0, -2)) // 把正方形移到相机前方 let projectionMatrix = float4x4(perspectiveProjectionFov: .pi/3, aspectRatio: Float(view.bounds.width / view.bounds.height), nearZ: 0.1, farZ: 100) let mvpMatrix = projectionMatrix * viewMatrix * modelMatrix // 传递MVP矩阵到顶点着色器 let mvpBuffer = device.makeBuffer(bytes: &mvpMatrix, length: MemoryLayout<float4x4>.stride, options: []) renderEncoder.setVertexBuffer(mvpBuffer, offset: 0, index: 0) // 绘制正方形(6个顶点,组成2个三角形) renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: squareVertices.count / 3) renderEncoder.endEncoding() commandBuffer.present(view.currentDrawable!) commandBuffer.commit()
单独设置背面颜色的特殊情况
如果你只想给背面设置颜色,正面保持默认(或者不渲染正面):
- 把渲染管道的
cullMode设为.front,这样Metal会剔除正面,只渲染背面 - 片元着色器里可以直接返回背面颜色,不用判断
isFrontFace:
fragment float4 fragmentShader() { return float4(0.0, 1.0, 0.0, 1.0); // 直接返回背面颜色 }
内容的提问来源于stack exchange,提问作者whataday
相关产品推荐
相关产品推荐

