You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 09:08:07