WWDC Quick Look 💓 By SwiftGGTeam
Bring your world into augmented reality

Bring your world into augmented reality

观看原视频

Highlight

Object Capture 团队的 Hao Tang 和 Risa 介绍了 Object Capture 技术的最新进展和最佳实践。Object Capture 是 Apple 的摄影测量 API,可以将真实物体的照片转换为详细的 3D 模型。

核心内容

很多 AR 体验的难点不在 AR 本身,而在素材。一个真实木制棋子要进入游戏,过去通常要 3D 建模师和材质专家参与。形状、纹理、比例都要手工处理,成本很高。

Object Capture 把这个流程压缩成两步:先从多个角度拍摄真实物体,再把照片交给 macOS 上的 Photogrammetry API 生成 USDZ 模型。RealityKit 会输出几何网格和材质贴图,模型可以直接进入 AR 或游戏项目。

这场 session 用一个 oversized wooden chess pieces 的例子串起完整流程。先拍摄棋子,处理成 USDZ;再把模型导入 Reality Converter 调整纹理;最后放进 Xcode 的 RealityKit 项目,做成可以点击、移动、发光、被吃掉的 AR 棋盘。

Apple 在 WWDC 2022 还补上了一个关键环节:ARKit 可以在 ARSession 运行中拍摄原生相机分辨率的照片。开发者可以继续显示 3D 引导 UI,同时拿到更适合 Object Capture 的高分辨率图像。

详细内容

在 ARSession 中拍摄高分辨率照片

05:15)Object Capture 的输入质量会直接影响 3D 模型质量。Hao 明确说,图片分辨率越高,Object Capture 能生成的模型质量越好。

06:20)ARKit 6 提供了 high-resolution background photos API。它允许 App 在 ARSession 继续运行时,按原生相机分辨率拍摄照片。iPhone 13 的 Wide camera 对应 1200 万像素。

if let hiResCaptureVideoFormat = ARWorldTrackingConfiguration.recommendedVideoFormatForHighResolutionFrameCapturing {
    // Assign the video format that supports hi-res capturing.
    config.videoFormat = hiResCaptureVideoFormat
}

// Run the session.
session.run(config)

session.captureHighResolutionFrame { frame, error in
    if let frame = frame {
        // save frame.capturedImage
        // …
    }
}

关键点:

  • recommendedVideoFormatForHighResolutionFrameCapturing 查询当前设备是否有支持高分辨率照片捕捉的视频格式。
  • config.videoFormat = hiResCaptureVideoFormat 把 ARWorldTrackingConfiguration 切换到该格式。
  • session.run(config) 用新的配置启动 ARSession。
  • captureHighResolutionFrame 异步返回高分辨率 ARFrame。
  • frame.capturedImage 是后续保存并送入 Object Capture 流程的图像数据。

这个 API 的价值在于拍摄流程不中断。用户仍然看到 AR 覆盖层,App 可以用 3D 引导 UI 提醒用户哪些角度还没拍到。

选择适合 Object Capture 的拍摄对象

07:44)Object Capture 适合表面有足够纹理的物体。透明、反光、缺少纹理的区域,重建细节会变差。

拍摄环境也很重要。Hao 建议使用均匀、漫射的光,保持背景稳定,并给物体周围留出足够空间。如果房间较暗,可以使用照明良好的转台。

09:24)示例中的 pirate ship 放在干净桌面中央。拍摄者缓慢绕物体移动,从不同高度拍摄,让物体始终占据画面中心,并保持相邻照片之间有高重叠度。

10:15)这次示例先拍约 80 张照片,再把船翻到侧面拍约 20 张,用来重建底部。Hao 也提醒,只有物体翻转后仍保持刚性,才适合这样处理。

用 PhotogrammetrySession 生成 USDZ 模型

11:00)拍摄完成后,照片会被复制到 Mac,并由 Object Capture API 处理。输出细节级别有 reduced、medium、full、raw 四种。

reduced 和 medium 面向 Web、移动端和 AR Quick Look,三角面和材质通道更少,内存占用更低。full 和 raw 面向游戏、后期制作等高端交互场景,几何细节更高,也需要更多内存。

14:17)在 AR 棋盘示例中,Risa 使用 2021 年引入的 PhotogrammetrySession 处理 rook 的照片,并选择 reduced detail level。原因很直接:AR 游戏需要稳定运行,模型不能过重。

这个 session 没有给出 PhotogrammetrySession 的代码片段。官方资源中给出了配套入口:Creating a photogrammetry command-line appUsing object capture assets in RealityKit,适合作为实现起点。

把 Object Capture 模型变成 RealityKit 游戏对象

15:26)棋子模型导入 Xcode 后,Risa 用 RealityKit 创建棋盘。棋盘格来自缩放后的 primitive cubes,颜色在黑白之间交替。

17:00)启动动画分两步:先把棋盘格沿 y 轴上移 10 厘米,再用 move(to:relativeTo:duration:) 回到原来的 transform。边框 USDZ 自带动画,直接调用 playAnimation

// Board Animation
class Chessboard: Entity {
    func playAnimation() {
        checkers
            .forEach { entity in
                let currentTransform = entity.transform
                // Move checker square 10cm up
                entity.transform.translation += SIMD3<Float>(0, 0.1, 0)
                entity.move(to: currentTransform,
                            relativeTo: entity.parent,
                            duration: BoardGame.startupAnimationDuration)
            }

        // Play built-in animation for board border
        border.availableAnimations.forEach {
            border.playAnimation($0)
        }
    }
}

关键点:

  • checkers.forEach 遍历每一个棋盘格实体。
  • currentTransform 保存最终落点。
  • translation += SIMD3<Float>(0, 0.1, 0) 先把格子抬高 10 厘米。
  • move(to:relativeTo:duration:) 让实体在指定时间内回到保存的 transform。
  • border.availableAnimations 读取 USDZ 中已有的动画。
  • border.playAnimation($0) 播放边框模型内置动画。

这个片段没有改变模型资产本身,只在 RealityKit 场景中操作 Entity 的 transform。

用 raycast 选择真实棋子模型

18:00)棋盘要能互动,第一步是选中棋子。Risa 已经把 UITapGestureRecognizer 加到 ARView。用户点击屏幕后,App 从相机原点向点击位置发出一条 ray,再对 3D 场景做 raycast。

// Select chess piece
class ChessViewport: ARView {
    @objc
    func handleTap(sender: UITapGestureRecognizer) {
        guard let ray = ray(through: sender.location(in: self)) else { return }

        // No piece is selected yet, we want to select one
        guard let raycastResult = scene.raycast(origin: ray.origin,
                                                direction: ray.direction,
                                                length: 5,
                                                query: .nearest,
                                                mask: .piece).first,
              let piece = raycastResult.entity.parentChessPiece else {
            return
        }
        boardGame.select(piece)
        gameManager.selectedPiece = piece
    }
}

关键点:

  • sender.location(in: self) 取得用户在 ARView 中点击的 2D 坐标。
  • ray(through:) 把 2D 点击点转换成从相机出发的 3D ray。
  • scene.raycast 在 3D 场景中查找命中的实体。
  • length: 5 限制 raycast 的距离。
  • query: .nearest 只取最近的命中结果。
  • mask: .piece 只检测棋子的 collision group。
  • parentChessPiece 从命中的子实体回到棋子对象。
  • boardGame.select(piece)gameManager.selectedPiece 更新游戏状态。

Risa 特别提醒,raycast 会忽略没有 CollisionComponent 的实体。导入 Object Capture 模型后,如果想点击它,碰撞组件不能漏掉。

用 CustomMaterial 在 CPU 和 GPU 之间传递动画进度

20:38)吃子动画使用 RealityKit custom materials 的 geometry modifiers。它们在 RealityKit 的 vertex shader 中逐顶点调用,可以改变位置、法线、纹理坐标等顶点数据。

21:16capturedProgress 表示棋子被吃掉的动画进度,范围是 0 到 1。CPU 侧把这个值写进 CustomMaterial.custom.value,Metal 侧再通过 uniforms 的 custom parameter 读取。

// Capture Geometry Modifier
class ChessPiece: Entity, HasChessPiece {
    var capturedProgress: Float {
        get {
            (pieceEntity?.model?.materials.first as? CustomMaterial)?.custom.value[0] ?? 0
        }
        set {
            pieceEntity?.modifyMaterials { material in
                guard var customMaterial = material as? CustomMaterial else {
                    return material
                }
                customMaterial.custom.value = SIMD4<Float>(newValue, 0, 0, 0)
                return customMaterial
            }
        }
    }
}

关键点:

  • capturedProgress 是棋子实体上的状态属性。
  • getter 从第一个材质读取 CustomMaterial.custom.value[0]
  • 读取失败时返回 0,表示动画还没开始。
  • modifyMaterials 遍历并替换实体材质。
  • guard var customMaterial = material as? CustomMaterial 只处理自定义材质。
  • SIMD4<Float>(newValue, 0, 0, 0) 把动画进度放进 custom value 的第一个通道。
  • 返回修改后的 customMaterial,RealityKit 会把新值送给着色器。

21:50)Metal 侧根据这个进度缩放 y 方向,并在 x 方向加入波浪偏移。Risa 提到这些修改是 transient 的,不会改变实体原始顶点数据。

用 surface shader 和 bloom 标出可移动位置

22:30)为了帮助新手看懂棋子可以移动到哪里,示例给潜在落点的棋盘格加了脉冲效果。每个棋盘格都是独立 Entity,都有自己的 Model Component,因此可以单独套用 surface shader。

23:00)CPU 侧传入布尔值,Metal surface shader 读取 custom parameter。如果当前格子是可能落点,就修改 emissive color。

// Checker animation to show potential moves
void checkerSurface(realitykit::surface_parameters params,
                    float amplitude,
                    bool isBlack = false)
{
    // ...
    bool isPossibleMove = params.uniforms().custom_parameter()[0];
    if (isPossibleMove) {
        const float a = amplitude * sin(params.uniforms().time() * M_PI_F) + amplitude;
        params.surface().set_emissive_color(half3(a));
        if (isBlack) {
            params.surface().set_base_color(half3(a));
        }
    }
}

关键点:

  • checkerSurface 是 RealityKit surface shader 使用的 Metal 函数。
  • params.uniforms().custom_parameter()[0] 读取 CPU 侧传来的自定义值。
  • isPossibleMove 决定当前棋盘格是否需要高亮。
  • sin(params.uniforms().time() * M_PI_F) 用时间生成脉冲变化。
  • set_emissive_color(half3(a)) 改变自发光颜色。
  • 黑色棋盘格额外调用 set_base_color,让高亮更明显。

23:20)为了让发光效果更明显,示例又在 ARView 上加了 bloom 后处理。实现入口是 renderCallbacks.postProcess,具体图像处理使用 Metal Performance Shaders。

import MetalPerformanceShaders

class ChessViewport: ARView {
    init(gameManager: GameManager) {
        /// ...
        renderCallbacks.postProcess = postEffectBloom
    }

    func postEffectBloom(context: ARView.PostProcessContext) {
        let brightness = MPSImageThresholdToZero(device: context.device,
                                                 thresholdValue: 0.85,
                                                 linearGrayColorTransform: nil)
        brightness.encode(commandBuffer: context.commandBuffer,
                          sourceTexture: context.sourceColorTexture,
                          destinationTexture: bloomTexture!)
        /// ...
    }
}

关键点:

  • import MetalPerformanceShaders 引入 Metal Performance Shaders。
  • renderCallbacks.postProcess = postEffectBloom 把自定义后处理接到 ARView 渲染流程。
  • ARView.PostProcessContext 提供后处理所需的 device、commandBuffer 和纹理。
  • MPSImageThresholdToZero 用阈值筛出较亮区域。
  • thresholdValue: 0.85 表示低于阈值的颜色会被压掉。
  • brightness.encode 把处理命令编码到当前 command buffer。
  • sourceColorTexture 是 ARView 渲染出的颜色纹理。
  • bloomTexture 接收亮度筛选后的中间结果。

核心启发

  • 做一个商品 AR 预览工具:用 Object Capture 生成商品 USDZ,再用 RealityKit 放进用户空间。适合鞋、植物、摆件等有纹理、低反光的物体。起点是拍摄规范、PhotogrammetrySession 和 RealityKit 模型加载。

  • 做一个带拍摄引导的 3D 扫描 App:在 ARSession 中显示覆盖层,提示用户从不同高度和角度拍摄。使用 captureHighResolutionFrame 保存原生分辨率照片,再把照片传到 Mac 端处理。

  • 做一个真实玩具变 AR 角色的游戏原型:先用 Object Capture 扫描玩具,再在 RealityKit 中给模型添加碰撞组件、点击选择和移动动画。示例中的 scene.raycastmove(to:relativeTo:duration:) 可以直接作为交互入口。

  • 做一套 RealityKit 自定义材质效果库:把选中发光、吃子压扁、潜在位置脉冲这类效果抽成可复用组件。CPU 侧通过 CustomMaterial.custom.value 传递状态,GPU 侧用 surface shader 或 geometry modifier 表现动画。

  • 做一个 AR 教学棋盘:根据规则计算可移动格子,把结果写入棋盘格材质的 custom parameter,再用 emissive color 和 bloom 高亮。这个方向适合象棋、国际象棋、桌游规则教学。

关联 Session

评论

GitHub Issues · utterances