试玩 visionOS 2 上的 LowLevelMesh 与 Compute Shader:模拟布料旗子

WWDC2024 上,苹果为 RealityKit 推出了 LowLevelMesh 功能,具体好处有:

  • 可以自定义顶点结构体,想传什么就传什么
  • 可以方便对顶点进行原地修改,CPU 与 GPU(Compute Shader)都可以

本文通过两种方式,创建自定义的 MeshResource,并使用计算着色器来模拟布料被风吹动效果,带大家了解新推出的 LowLevelMesh的强大功能。

RealityKit 中的 MeshResource

RealityKit 中 3D 模型的表示方法就是 MeshResource类型,它包含了一系列内置的基本几何体生成 API,及创建自定义几何的 API。

内置几何类型与 API

原本内置的 BoxPlaneSphereText 等基本几何体生成功能一直不太够用,于是今年新增了 extruding(挤压拉伸)式生成 API,同时今年还可以直接从包含 MeshAnchor.Geometry 的 Anchor 中生成几何体,不用像以前一样绕道生成: MeshAnchor.Geometry(ARKit 识别的现实物体几何形状) -> ShapeResource(物理碰撞用的形状) -> MeshResource(RealityKit 显示用的网格几何体):

swift 复制代码
// 新的拉伸式创建方式,可以根据 `SwiftUI.Path` 和 `AttributedString` 拉伸挤压出几何
convenience init(
    extruding path: Path,
    extrusionOptions: MeshResource.ShapeExtrusionOptions = ShapeExtrusionOptions()
) throws

convenience init(
    extruding string: AttributedString,
    textOptions: MeshResource.GenerateTextOptions = GenerateTextOptions(),
    extrusionOptions: MeshResource.ShapeExtrusionOptions = ShapeExtrusionOptions()
) throws

// 直接从 Anchor 中创建,无需再绕道 ShapeResource
convenience init(from planeAnchor: PlaneAnchor) async throws
convenience init(from meshAnchor: MeshAnchor) async throws

通过 MeshDescriptor 创建自定义几何体

这是创建自定义几何体最简单的方法,也是最易理解的方法,对程序员比较友好。只需要把 顶点位置顶点索引贴图坐标 这些数组传进去 MeshDescriptor 就可以了。因为传入的参数,经过了 RealityKit 的处理,所以我们可以省略一些参数,比如形状简单的法线可以自动生成。

不过缺点也有,RealityKit 处理的过程会将传入的多边形重新三角化为三角形,这样不仅运行效率低,还造成了最终生成的几何体和传入参数不一致,后续几乎无法修改。

swift 复制代码
var vertices: [SIMD3<Float>] = [] //顶点位置
var indices: [UInt32] = [] //顶点索引
var uvs: [SIMD2<Float>] = [] //贴图坐标

......
......
var descr = MeshDescriptor()
descr.primitives = .triangles(indices)
descr.positions = MeshBuffer(vertices)
descr.textureCoordinates = MeshBuffers.TextureCoordinates(uvs)
return try .generate(from: [descr])

通过 MeshResource.Contents 创建自定义几何体

MeshResource.Contents 的结构与 USD 等 3D 格式文件的格式高度相似,又因为它不会对传入参数进行三角化,所以和最终显示在屏幕上几何体也一致,因此就可以后续通过代码进行修改。

它的内部结构包含:modelsinstances。前者是几何体的结构定义,而后者则是实例化的对象,特别像 对象 的区别。

swift 复制代码
var vertices: [SIMD3<Float>] = [] //顶点位置
var normals: [SIMD3<Float>] = [] //法线
var indices: [UInt32] = [] //顶点索引
var uvs: [SIMD2<Float>] = [] //贴图坐标

......
......

var part = MeshResource.Part(id: "MeshPart", materialIndex: 0)
part.triangleIndices = .init(indices)
part.textureCoordinates = .init(uvs)
part.normals = .init(normals)
part.positions = .init(vertices)

let model = MeshResource.Model(id: "MeshModel", parts: [part])
let instance = MeshResource.Instance(id: "MeshModel-0", model: "MeshModel")

var contents = MeshResource.Contents()
contents.instances = .init([instance])
contents.models = .init([model])
return try MeshResource.generate(from: contents)

通过 LowLevelMesh 创建自定义几何体

今年新增了 LowLevelMesh 来创建几何体,它的好处一是可以自定义顶点的数据结构,二是通过 CPU 指针和 GPU 的计算着色器进行高效修改。

缺点就是,它的定义也复杂很多,你需要掌握更多的内存操作知识。

swift 复制代码
var vertices: [SIMD3<Float>] = [] //顶点位置
var normals: [SIMD3<Float>] = [] //法线
var indices: [UInt32] = [] //顶点索引
var uvs: [SIMD2<Float>] = [] //贴图坐标

......
......

//描述内存布局
var desc = LowLevelMesh.Descriptor()
desc.vertexAttributes = [......]
desc.vertexLayouts = [......]
desc.vertexCapacity = vertices.count
desc.indexType = .uint32
desc.indexCapacity = indices.count


let mesh = try? LowLevelMesh(descriptor: desc)
mesh?.withUnsafeMutableBytes(bufferIndex: 0, { rawBufferPointer in
    //用 Swift 指针写入顶点、法线和 uv 数据
    ......
})
mesh?.withUnsafeMutableIndices { rawIndices in
    //用 Swift 指针写入顶点索引
    ......
}

// 设置 BoundingBox 和拓扑格式
let meshBounds = BoundingBox(min: [0, -0.1, 0], max: [Float(width), 0.1, Float(height)])
mesh?.parts.replaceAll([
    LowLevelMesh.Part(
        indexCount: indices.count,
        topology: .triangle,
        materialIndex: 0,
        bounds: meshBounds
    )
])

return try MeshResource(from: mesh!)

三种方法对比

从使用方便的角度讲:
MeshDescriptor > MeshResource.Contents >> LowLevelMesh

而从 CPU 单次生成效率角度讲:
LowLevelMesh > MeshResource.Contents >> MeshDescriptor

而从 GPU 连续计算与修改的角度讲:
LowLevelMesh >> MeshResource.Contents

简单总结:

  • MeshDescriptor:为方便程序员使用而生。
  • MeshResource.Contents:为方便对接 USD 文件,及高效提交给 GPU 渲染而生。
  • LowLevelMesh:为自由内存布局,及 Compute Shader 而生。

Compute Shader 与模拟布料

为什么 LowLevelMesh 与计算着色器 Compute Shader 关系这么密切?难道没有它就无法使用 Compute Shader 了吗?那倒也不是,Compute Shader 擅长处理并行计算问题,比如模拟布料,风吹动旗子的效果几乎就是为 Compute Shader 而生的。

在本文对应的 Demo:RealityComputeShader_Flag 中,V1 版就是通过 MeshResource.ContentsCompute Shader 联合使用,来不断刷新旗子几何体的形状来实现的,可以看到最大问题就是:Compute Shader 计算完成后,数据需要回到 CPU,CPU 读取数据并重新构建一个 MeshResource.Contents,因为是新的几何体,所以只能将数据发送回 GPU 进行渲染。

而 Demo 中的 V2 版,则是 LowLevelMeshCompute Shader 联合使用,来不断刷新旗子几何体的形状来实现的,可以看到最大好处就是:Compute Shader 计算完成后,数据无需回到 CPU,CPU 只需要知道计算完成,然后直接调用 GPU 进行渲染即可,因为数据一直都在 GPU 上。

性能对比

经过测试,V1 版本在模拟器上 CPU 占用一直在 25% 左右,而 V2 版 CPU 占用仅有 14% 左右。考虑到模拟器不够精确,Compute Shader 代码也没有进行过多优化,相信实机效果会更好。

不过,让人好奇的是,Vision Pro 真机使用的 M2 处理器,GPU 为了提供 3D 渲染已经竭尽全力了,在大型沉浸式场景中还能有多少余力来供大家跑 Compute Shader 代码?

LowLevelMesh 实际使用的问题

经过使用发现,像苹果在 WWDC Build a spatial drawing app with RealityKit 中讲的那样,自定义一个结构体来组织内存是最方便的使用方式,可以方便后续拼接更多数据,也方便在 Shader 中进行数据操作。

但是那样就需要修改 Compute Shader,造成两者不一致,于是我就想挑战一下,使用默认的布局来创建一个几何体。

其中遇到的最大困难有两个:

  • 初始化时,需要使用 Swift 指针来向 LowLevelMesh 填充数据;
  • 每次计算完成后,需要使用 Blit 命令,将数据复制到指定位置的内存位置上;

我们先来演示第一个,在 CPU 上初始化,使用 Swift 指针来向 mesh 填充数据:

swift 复制代码
var vertices: [SIMD3<Float>] = []
var normals: [SIMD3<Float>] = []
var uvs: [SIMD2<Float>] = []

var desc = LowLevelMesh.Descriptor()

let mesh = try? LowLevelMesh(descriptor: desc)
mesh?.withUnsafeMutableBytes(bufferIndex: 0, { rawBufferPointer in
    let _ = rawBufferPointer.withMemoryRebound(to: SIMD3<Float>.self) { buffer in
        let offset = buffer.update(fromContentsOf: vertices+normals)
        buffer.suffix(from: offset).withMemoryRebound(to: SIMD2<Float>.self) { buffer2 in
            let _ = buffer2.update(fromContentsOf: uvs)
        }
    }
})

下面是每次 GPU 计算完成后的操作,命令 GPU 复制数据到 lowLevelMesh 对应 Buffer 的指定位置:

swift 复制代码
let blitBuffer = commandQueue.makeCommandBuffer()
let blitEncoder = blitBuffer?.makeBlitCommandEncoder()
guard let vb = mesh.lowLevelMesh?.replace(bufferIndex: 0, using: blitBuffer!) else {
    return
}
// 命令 GPU 复制数据到 lowLevelMesh 对应 Buffer 的指定位置
blitEncoder?.copy(from: mesh.vb1, sourceOffset: 0, to: vb, destinationOffset: 0, size: MemoryLayout<SIMD3<Float>>.size * mesh.vertexCount)
blitEncoder?.copy(from: mesh.normalBuffer, sourceOffset: 0, to: vb, destinationOffset: MemoryLayout<SIMD3<Float>>.size * mesh.vertexCount, size: MemoryLayout<SIMD3<Float>>.size * mesh.vertexCount)
blitEncoder?.copy(from: mesh.uvBuffer, sourceOffset: 0, to: vb, destinationOffset: MemoryLayout<SIMD3<Float>>.size * mesh.vertexCount * 2, size: MemoryLayout<SIMD2<Float>>.size * mesh.vertexCount)

blitEncoder?.endEncoding()
blitBuffer?.commit()

参考

相关推荐
虹科数字化与AR5 天前
安宝特分享 | AR技术重塑工业:数字孪生与沉浸式培训的创新应用
ar·数字孪生·ar眼镜·增强现实·工业ar
JovaZou10 天前
Snap 发布新一代 AR 眼镜,有什么特别之处?
ai·ar·虚拟现实·华为snap·增强现实
学步_技术20 天前
利用AI增强现实开发:基于CoreML的深度学习图像场景识别实战教程
人工智能·深度学习·ar·增强现实·coreml
斯裕科技22 天前
新升级|优化航拍/倾斜模型好消息,支持处理多套贴图模型!
unity·ue5·3dsmax·虚拟现实·maya·增强现实
Successssss~1 个月前
【高校主办,EI稳定检索】2024年人机交互与虚拟现实国际会议(HCIVR 2024)
计算机视觉·人机交互·vr·虚拟现实·增强现实
学步_技术2 个月前
增强现实系列—深入探索ARKit:平面检测、三维模型放置与增强现实交互
机器学习·计算机视觉·交互·增强现实·虚拟现实技术·平面检测·vr/ar
Uncertainty!!2 个月前
初识增强现实(AR)
ar·增强现实
VRARvrnew3d3 个月前
轨道交通AR交互教学定制公司优选深圳华锐视点
ar·增强现实·轨道交通·ar公司·ar教学
VRARvrnew3d3 个月前
AR增强现实汽车装配仿真培训系统开发降低投入费用
ar·增强现实·汽车导航·ar仿真培训
苹果API搬运工3 个月前
试玩 RealityComposerPro 中的 Shader Graph:不同噪声图像 Noise 可视化
增强现实