WWDC Quick Look 💓 By SwiftGGTeam
Dive deep into volumes and immersive spaces

Dive deep into volumes and immersive spaces

观看原视频

Highlight

visionOS 2 为 Volume 新增自动底板、角落调整大小手柄、工具栏和自定义装饰件,同时增强了沉浸式空间的沉浸度控制和空间跟踪能力。


核心内容

visionOS 有三种场景类型:Window、Volume 和 Immersive Space。这场 Session 聚焦在 Volume 和 Immersive Space 上,尤其是 visionOS 2 对这两者的增强。

Volume 在 visionOS 2 中有了明显改进。当你注视它时,底部会自动显示底板(Baseplate),帮助用户感知 Volume 的边界范围。如果内容已经填满了整个 Volume 或自己绘制了底部表面,可以用 .volumeBaseplateVisibility(.hidden) 关闭底板。

另一个重要变化是 Volume 现在有了角落的调整大小手柄。默认情况下,Volume 的最小/最大尺寸继承自内容视图的 frame。如果你给视图设置了固定尺寸,Volume 就无法调整大小。改为设置最小/最大值后,拖动手柄就能平滑缩放。你也可以通过代码动态改变状态变量来驱动 Volume 尺寸变化。

工具栏(Toolbar)可以浮动在 Volume 底部的装饰件中。visionOS 2 的工具栏会自动跟随用户站立的方位移动到最近的侧面,窗口控件也一样。除了工具栏,visionOS 2 还允许 Volume 添加自定义装饰件,可以放在 Volume 周围的任意位置,并且会根据距离自动缩放保持可读性。

当用户在 Volume 周围移动时,机器人和内容需要做出响应。每个 Volume 的侧面都是一个视点(Viewpoint),系统会自动将窗口控件和装饰件移动到离用户最近的侧面。你可以用 onVolumeViewpointChange 监听视点变化,让内容也跟随调整。如果只想支持特定侧面,可以用 supportedVolumeViewpoints 限制。

Immersive Space 部分增加了更多控制能力。visionOS 2 支持自定义沉浸度范围,可以指定渐进式沉浸的初始值和最小/最大值。用户旋转数码表冠调整沉浸度时,你可以用 onImmersionChange 监听并做出反应。

新的 SpatialTrackingSession API 让你能够跟踪平面锚点。创建 floor anchor 后,可以用 SpatialTapGesture 检测用户在沉浸空间中的点击,然后将植物放置在用户点击的位置。最后,你可以用 SurroundingsEffect 动态改变现实透视的颜色,比如当机器人碰撞到植物时,用对应的花盆颜色来改变环境色调。


详细内容

Volume 底板

visionOS 2 默认为 Volume 启用底板。它会在用户注视时淡入显示,帮助感知 Volume 的边界。

// Baseplate

WindowGroup(id: "RobotExploration") {
    ExplorationView()
        .volumeBaseplateVisibility(.visible) // Default!
}
.windowStyle(.volumetric)
  • volumeBaseplateVisibility(.visible) 是默认行为,底板会在注视时自动淡入
  • 如果内容已填满 Volume 边界或自定义了底部表面,设置为 .hidden 避免视觉冲突
  • 底板帮助用户找到角落的调整大小手柄

Volume 调整大小

Volume 现在有角落的调整大小手柄,需要正确设置 frame 的最小/最大值。

// Enabling resizability

WindowGroup(id: "RobotExploration") {
    let initialSize = Size3D(width: 900, height: 500, depth: 900)

    ExplorationView()
        .frame(minWidth: initialSize.width, maxWidth: initialSize.width * 2,
               minHeight: initialSize.height, maxHeight: initialSize.height * 2)
        .frame(minDepth: initialSize.depth, maxDepth: initialSize.depth * 2)
}
.windowStyle(.volumetric)
.windowResizability(.contentSize) // Default!
  • 设置 minWidth/maxWidthminHeight/maxHeightminDepth/maxDepth 允许 Volume 调整大小
  • 固定 frame 值会禁用调整大小
  • windowResizability(.contentSize) 是默认行为,Volume 尺寸继承自内容

代码驱动 Volume 尺寸

你可以通过状态变量动态改变 Volume 尺寸(06:10)。

// Programmatic resize

struct ExplorationView: View {
    @State private var levelScale: Double = 1.0

    var body: some View {
        RealityView { content in
            // Level code here
        } update: { content in
            appState.explorationLevel?.setScale(
                [levelScale, levelScale, levelScale], relativeTo: nil)
        }
        .frame(width: levelSize.value.width * levelScale,
               height: levelSize.value.height * levelScale)
        .frame(depth: levelSize.value.depth * levelScale)
        .overlay { Button("Change Size") { levelScale = levelScale == 1.0 ? 2.0 : 1.0 } }
    }
}
  • 状态变量 levelScale 控制缩放比例
  • frame 值随 scale 变化,Volume 自动调整大小以适应新尺寸
  • RealityKit entity 也需要同步缩放

工具栏装饰件

工具栏可以浮动在 Volume 底部(07:39)。

// Toolbar ornament

ExplorationView()
.toolbar {
		ToolbarItem {
      	Button("Next Size") {
          	levelScale = levelScale == 1.0 ? 2.0 : 1.0
        }
    }
  	ToolbarItemGroup {
      	Button("Replay") {
          	resetExploration()
        }
      	Button("Exit Game") {
          	exitExploration()
          	openWindow(id: "RobotCreation")
        }
    }
}
  • ToolbarItemToolbarItemGroup 用于组织按钮
  • 工具栏会自动跟随用户移动到最近的侧面
  • 工具栏会根据 Volume 距离自动缩放保持可读性

自定义装饰件

除了工具栏,你可以在 Volume 周围添加自定义装饰件(10:41)。

// Ornaments

WindowGroup(id: "RobotExploration") {
    ExplorationView()
    .ornament(attachmentAnchor: .scene(.topBack)) {
        ProgressView()
    }
}
.windowStyle(.volumetric)
  • attachmentAnchor: .scene(.topBack) 将装饰件放置在 Volume 后上方
  • 装饰件会自动跟随视点移动
  • 装饰件会根据距离自动缩放

监听视点变化

让内容响应用户在 Volume 周围的移动(12:08)。

// Volume viewpoint

struct ExplorationView: View {
    var body: some View {
        RealityView { content in
            // Some RealityKit code
        }
        .onVolumeViewpointChange { oldValue, newValue in
            appState.robot?.currentViewpoint = newValue.squareAzimuth
        }
    }
}
  • onVolumeViewpointChange 在活动视点改变时触发
  • newValue.squareAzimuth 返回四个标准化值之一:front、left、right、back
  • 可以用这个值让角色转向用户

限制支持的视点

如果你的内容只适合从某些侧面观看,可以限制支持的视点(13:43)。

// Supported viewpoints
struct ExplorationView: View {
  	let supportedViewpoints: Viewpoint3D.SquareAzimuth.Set = [.front, .left, .right]

  	var body: some View {
      	RealityView { content in
        		// Some RealityKit code
        }
      	.supportedVolumeViewpoints(supportedViewpoints)
      	.onVolumeViewpointChange { _, newValue in
        		appState.robot?.currentViewpoint = newValue.squareAzimuth
        }
    }
}
  • supportedVolumeViewpoints 接受一个选项集,指定支持的侧面
  • 未支持的侧面不会触发装饰件和窗口控件移动
  • 默认支持所有四个侧面

视点更新策略

updateStrategy 控制是否收到不支持视点的更新(14:30)。

// Viewpoint update strategy

struct ExplorationView: View {
    let supportedViewpoints: Viewpoint3D.SquareAzimuth.Set = [.front, .left, .right]

    var body: some View {
        RealityView { content in
            // Some RealityKit code
        }
        .supportedVolumeViewpoints(supportedViewpoints)
        .onVolumeViewpointChange(updateStrategy: .all) { _, newValue in
            appState.robot?.currentViewpoint = newValue.squareAzimuth
            if !supportedViewpoints.contains(newValue) {
                appState.robot?.animationState.transition(to: .annoyed)
            }
        }
    }
}
  • updateStrategy: .all 确保所有视点变化都会触发回调
  • 可以检测用户是否在不支持的侧面,并触发提示动画
  • 默认只在支持的视点之间切换时触发

自定义沉浸度范围

visionOS 2 支持自定义渐进式沉浸的初始值和范围(23:54)。

// Customizing immersion
struct BotanistApp: App {
    // Custom immersion amounts
    @State private var immersionStyle: ImmersionStyle = .progressive(0.2...1.0, initialAmount: 0.8)

    var body: some Scene {
        // Immersive Space
        ImmersiveSpace(id: "ImmersiveSpace") {
            ImmersiveSpaceExplorationView()
        }
        .immersionStyle(selection: $immersionStyle, in: .mixed, .progressive, .full)
    }
}
  • .progressive(range, initialAmount:) 创建自定义渐进式沉浸
  • 0.2...1.0 是最小到最大沉浸度范围
  • initialAmount: 0.8 设置初始沉浸度为 80%

响应沉浸度变化

监听数码表冠调整沉浸度的事件(25:17)。

// Reacting to immersion
struct ImmersiveView: View {
    @State var immersionAmount: Double?

    var body: some View {
        ImmersiveSpaceExplorationView()
            .onImmersionChange { context in
                immersionAmount = context.amount
            }
            .onChange(of: immersionAmount) { oldValue, newValue in
                handleImmersionAmountChanged(newValue: newValue, oldValue: oldValue)
            }
    }
}
  • onImmersionChange 提供新的沉浸度值
  • onChange 检测沉浸度变化并触发响应
  • 可以比较新旧值判断沉浸度是增加还是减少

处理沉浸度变化

根据沉浸度变化让机器人做出反应(25:39)。

// Reacting to immersion
func handleImmersionAmountChanged(newValue: Double?, oldValue: Double?) {
    guard let newValue, let oldValue else {
        return
    }

    if newValue > oldValue {
        // Move the robot outward to react to increasing immersion
        moveRobotOutward()
    } else if newValue < oldValue {
        // Move the robot inward to react to decreasing immersion
        moveRobotInward()
    }
}
  • 沉浸度增加时,机器人向外移动
  • 沉浸度减少时,机器人向内移动
  • 这种响应增强用户对沉浸度变化的感知

空间跟踪会话

使用 SpatialTrackingSession 跟踪平面锚点(26:57)。

// Create and run spatial tracking session
struct ImmersiveExplorationView {
    @State var spatialTrackingSession: SpatialTrackingSession
        = SpatialTrackingSession()

    var body: some View {
        RealityView { content in
            // ...
        }
        .task {
            await runSpatialTrackingSession()
        }
    }
}
  • SpatialTrackingSession 管理空间跟踪
  • .task 在沉浸空间打开时启动会话
  • 需要用户授权才能访问平面锚点

配置并运行跟踪

设置平面跟踪配置并运行会话(27:11)。

// Create and run the spatial tracking session
func runSpatialTrackingSession() async {
    // Configure the session for plane anchor tracking
    let configuration =
        SpatialTrackingSession.Configuration(tracking: [.plane])

    // Run the session to request plane anchor transforms
    let _ = await spatialTrackingSession.run(configuration)
}
  • Configuration(tracking: [.plane]) 配置平面跟踪
  • run(configuration) 请求授权并开始跟踪
  • 返回值表示授权是否成功

创建地面锚点

创建一个水平地面的锚点实体(27:32)。

// Create a floor anchor to track
struct ImmersiveExplorationView {
    @State var spatialTrackingSession: SpatialTrackingSession
        = SpatialTrackingSession()

    let floorAnchor = AnchorEntity(
        .plane(.horizontal, classification: .floor, minimumBounds: .init(x: 0.01, y: 0.01))
    )

    var body: some View {
        RealityView { content in
            content.add(floorAnchor)
        }
        .task {
            await runSpatialTrackingSession()
        }
    }
}
  • .plane(.horizontal, classification: .floor, ...) 创建地面锚点
  • minimumBounds 设置最小检测尺寸
  • 将锚点添加到 RealityView 以启用跟踪

检测空间点击

用 SpatialTapGesture 检测用户在沉浸空间中的点击(27:54)。

// Detect taps on entities in immersive space
RealityView { content in
    // ...
}
.gesture(
    SpatialTapGesture(
        coordinateSpace: .immersiveSpace
    )
    .targetedToAnyEntity()
    .onEnded { value in
        handleTapOnFloor(value: value)
    }
)
  • SpatialTapGesture(coordinateSpace: .immersiveSpace) 检测 3D 空间中的点击
  • .targetedToAnyEntity() 让手势作用于任何实体
  • onEnded 处理点击事件

处理点击放置植物

将点击位置转换为地面锚点坐标并放置植物(28:09)。

// Handle tap event
func handleTapOnFloor(value: EntityTargetValue<SpatialTapGesture.Value>) {
    let location =
        value.convert(value.location3D, from: .immersiveSpace, to: floorAnchor)

    plantEntity.position = location
    floorAnchor.addChild(plantEntity)
}
  • convert(location3D, from: .immersiveSpace, to: floorAnchor) 转换坐标系
  • 将转换后的位置设置为植物的位置
  • 将植物添加为地面锚点的子实体

环境透视着色

用 SurroundingsEffect 改变现实透视的颜色(30:48)。

// Apply effect to tint passthrough
struct ImmersiveExplorationView: View {
    var body: some View {
        RealityView { content in
            // ...
        }
        .preferredSurroundingsEffect(surroundingsEffect)
    }

    // The resolved surroundings effect based on tint color
    var surroundingsEffect: SurroundingsEffect? {
        if let color = appModel.tintColor {
            return SurroundingsEffect.colorMultiply(color)
        } else {
            return nil
        }
    }
}
  • SurroundingsEffect.colorMultiply(color) 创建颜色乘法效果
  • preferredSurroundingsEffect 应用环境效果
  • 当机器人碰撞到植物时,用对应颜色改变环境色调

核心启发

  • 为 Volume 启用调整大小:如果 Volume 内容不是固定尺寸,使用 frame(minWidth:maxWidth:minDepth:maxDepth:) 而不是固定 frame,让用户可以通过拖动角落手柄调整大小。对于需要动态调整的场景,可以用状态变量驱动 frame 变化,Volume 会自动适应。

  • 用装饰件减少主视图混乱:将辅助 UI(如进度显示、状态信息)从主视图中提取到自定义装饰件中。装饰件会自动跟随用户移动到最近的侧面,并根据距离自动缩放,保持可读性。主视图只保留核心 3D 内容。

  • 让内容响应用户视角:使用 onVolumeViewpointChange 监听用户在 Volume 周围的移动,让角色或内容转向用户。如果只支持特定侧面,用 supportedVolumeViewpoints 限制,并用 updateStrategy: .all 在用户走到不支持侧面时给出提示。

  • 自定义沉浸度体验:visionOS 2 允许指定渐进式沉浸的初始值和范围。如果你的体验需要更强的沉浸感,可以设置更高的初始值(如 80%)。用 onImmersionChange 监听用户调整沉浸度,让内容做出相应反应。

  • 用空间跟踪实现交互:使用 SpatialTrackingSession 跟踪平面锚点,配合 SpatialTapGesture 检测 3D 空间中的点击。这能实现”点击地面放置物体”之类的交互,让用户在真实环境中布置内容。


关联 Session

评论

GitHub Issues · utterances