布景

在体验HelloWorld时,很好奇每个功用是怎么完成的,可是这个demo复用了很多功用、数据模型,刚开始了解起来就比较困难。所以我就先从功用点来看,将复用的功用、数据模型都剔除掉,确保单一功用能解藕独自运行。

环境

Xcode:15.1 beta

VisionOS:1.0

整理功用

graph LR;
    功用点-->A(设置光照);
    style A fill:#bbf,color:#fff
    click A "https://www.6hu.cc/post/7298690615046651943"
    功用点-->B(手势滚动地球)
    style B fill:#bbf,color:#fff
    click B "https://www.6hu.cc/post/7298765809290706983"
    功用点-->C(地球自转)
    style C fill:#bbf,color:#fff
    click C "https://www.6hu.cc/post/7298775642261569575"
    功用点-->D(地球跟随鼠标拖动)
    功用点-->E(卫星围绕地球滚动)
    功用点-->F(月球围绕地球滚动)
    功用点-->G(沉溺式与窗口之间的切换)

手势滚动地球

[VisionOS] 拆分HelloWorld的功用点 - 手势滚动地球

手势拖拽地球旋转,地球保持在原地

import SwiftUI
import RealityKit
import RealityKitContent
struct DragEarth: View {
    var body: some View {
        RealityView { content in
            guard let earth = await RealityKitContent.entity(named: "Globe") else {
                return
            }
            content.add(earth)
            earth.setSunlight(intensity: 14)
            earth.scale = SIMD3(repeating: 0.3)
        }
        .dragRotation(pitchLimit: .degrees(90))
    }
}
#Preview {
    DragEarth()
}

这里代码就很简略,加载了一个3D资源,然后增加了一个拖拽旋转的手势。

1.加载3D资源

和一般的3D资源没有任何区别,可是因为咱们要增加手势,所以这个Entity必须要增加一个InputComponent,也便是可输入的组件。

[VisionOS] 拆分HelloWorld的功用点 - 手势滚动地球

如果还想要碰撞、辅佐功用,就可以在Reality Composer Pro中增加系统为咱们提供好的组件,如果有些特殊的功用,比如光照,就可以自定义组件,增加到Entity

2.增加手势


import SwiftUI
import RealityKit
extension View {
    /// Enables people to drag an entity to rotate it, with optional limitations
    /// on the rotation in yaw and pitch.
    func dragRotation(
        yawLimit: Angle? = nil,
        pitchLimit: Angle? = nil,
        sensitivity: Double = 10,
        axRotateClockwise: Bool = false,
        axRotateCounterClockwise: Bool = false
    ) -> some View {
        self.modifier(
            DragRotationModifier(
                yawLimit: yawLimit,
                pitchLimit: pitchLimit,
                sensitivity: sensitivity,
                axRotateClockwise: axRotateClockwise,
                axRotateCounterClockwise: axRotateCounterClockwise
            )
        )
    }
}
/// A modifier converts drag gestures into entity rotation.
private struct DragRotationModifier: ViewModifier {
    var yawLimit: Angle?
    var pitchLimit: Angle?
    var sensitivity: Double
    var axRotateClockwise: Bool
    var axRotateCounterClockwise: Bool
    @State private var baseYaw: Double = 0
    @State private var yaw: Double = 0
    @State private var basePitch: Double = 0
    @State private var pitch: Double = 0
    func body(content: Content) -> some View {
        content
            .rotation3DEffect(.radians(yaw == 0 ? 0.01 : yaw), axis: .y)
            .rotation3DEffect(.radians(pitch == 0 ? 0.01 : pitch), axis: .x)
            .gesture(DragGesture(minimumDistance: 0.0)
                .targetedToAnyEntity()
                .onChanged { value in
                    // Find the current linear displacement.
                    let location3D = value.convert(value.location3D, from: .local, to: .scene)
                    let startLocation3D = value.convert(value.startLocation3D, from: .local, to: .scene)
                    let delta = location3D - startLocation3D
                    // Use an interactive spring animation that becomes
                    // a spring animation when the gesture ends below.
                    withAnimation(.interactiveSpring) {
                        yaw = spin(displacement: Double(delta.x), base: baseYaw, limit: yawLimit)
                        pitch = spin(displacement: Double(delta.y), base: basePitch, limit: pitchLimit)
                    }
                }
                .onEnded { value in
                    // Find the current and predicted final linear displacements.
                    let location3D = value.convert(value.location3D, from: .local, to: .scene)
                    let startLocation3D = value.convert(value.startLocation3D, from: .local, to: .scene)
                    let predictedEndLocation3D = value.convert(value.predictedEndLocation3D, from: .local, to: .scene)
                    let delta = location3D - startLocation3D
                    let predictedDelta = predictedEndLocation3D - location3D
                    // Set the final spin value using a spring animation.
                    withAnimation(.spring) {
                        yaw = finalSpin(
                            displacement: Double(delta.x),
                            predictedDisplacement: Double(predictedDelta.x),
                            base: baseYaw,
                            limit: yawLimit)
                        pitch = finalSpin(
                            displacement: Double(delta.y),
                            predictedDisplacement: Double(predictedDelta.y),
                            base: basePitch,
                            limit: pitchLimit)
                    }
                    // Store the last value for use by the next gesture.
                    baseYaw = yaw
                    basePitch = pitch
                }
            )
            .onChange(of: axRotateClockwise) {
                withAnimation(.spring) {
                    yaw -= (.pi / 6)
                    baseYaw = yaw
                }
            }
            .onChange(of: axRotateCounterClockwise) {
                withAnimation(.spring) {
                    yaw += (.pi / 6)
                    baseYaw = yaw
                }
            }
    }
    /// Finds the spin for the specified linear displacement, subject to an
    /// optional limit.
    private func spin(
        displacement: Double,
        base: Double,
        limit: Angle?
    ) -> Double {
        if let limit {
            return atan(displacement * sensitivity) * (limit.degrees / 90)
        } else {
            return base + displacement * sensitivity
        }
    }
    /// Finds the final spin given the current and predicted final linear
    /// displacements, or zero when the spin is restricted.
    private func finalSpin(
        displacement: Double,
        predictedDisplacement: Double,
        base: Double,
        limit: Angle?
    ) -> Double {
        // If there is a spin limit, always return to zero spin at the end.
        guard limit == nil else { return 0 }
        // Find the projected final linear displacement, capped at 1 more revolution.
        let cap = .pi * 2.0 / sensitivity
        let delta = displacement + max(-cap, min(cap, predictedDisplacement))
        // Find the final spin.
        return base + delta * sensitivity
    }
}

代码里主要用rotation3DEffect方法做旋转。

DragGesture接收拖拽手势,在手势的onChangedonEnded中去改变核算旋转视点。

这里便是一些视点核算:

yaw是核算x方向上的旋转视点

pitch 是核算y方向上的旋转视点

content
    .rotation3DEffect(.radians(yaw == 0 ? 0.01 : yaw), axis: .y)
    .rotation3DEffect(.radians(pitch == 0 ? 0.01 : pitch), axis: .x)

yawLimitpitchLimit最大视点的约束,咱们这里只约束了y方向上的视点,x没有约束。