[VisionOS] 拆分HelloWorld的功能点 - 手势转动地球

1,175 阅读3分钟

背景

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

环境

Xcode:15.1 beta

VisionOS:1.0

梳理功能

graph LR;
    功能点-->A(设置光照);
    style A fill:#bbf,color:#fff
    click A "https://juejin.cn/post/7298690615046651943"
    
    功能点-->B(手势转动地球)
    style B fill:#bbf,color:#fff
    click B "https://juejin.cn/post/7298765809290706983"
    
    功能点-->C(地球自转)
    style C fill:#bbf,color:#fff
    click C "https://juejin.cn/post/7298775642261569575"
    
    功能点-->D(地球跟随鼠标拖动)
    style D fill:#bbf,color:#fff
    click D "https://juejin.cn/post/7299037876637351975"
    
    功能点-->E(卫星围绕地球转动)
    style E fill:#bbf,color:#fff
    click E "https://juejin.cn/post/7300431522255241250"
    
    功能点-->G(沉浸式与窗口之间的切换)
    style G fill:#bbf,color:#fff
    click G "https://juejin.cn/spost/7300816733525901352"

手势转动地球

Nov-08-2023 13-42-20.gif

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

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,也就是可输入的组件。

image.png

如果还想要碰撞、辅助功能,就可以在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没有限制。

3.代码

DragEarth.swift