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

背景

在体验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(地球跟随鼠标拖动) 功能点-->E(卫星围绕地球转动) 功能点-->F(月球围绕地球转动) 功能点-->G(沉浸式与窗口之间的切换)

手势转动地球

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

swift 复制代码
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,也就是可输入的组件。

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

2.添加手势

swift 复制代码
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方向上的旋转角度

ini 复制代码
content
    .rotation3DEffect(.radians(yaw == 0 ? 0.01 : yaw), axis: .y)
    .rotation3DEffect(.radians(pitch == 0 ? 0.01 : pitch), axis: .x)

yawLimitpitchLimit最大角度的限制,我们这里只限制了y方向上的角度,x没有限制。

相关推荐
HarderCoder5 个月前
Apple Vision Pro 学习资料
visionos
苹果API搬运工5 个月前
试玩 RealityComposerPro 中的 Shader Graph:用圆环制作一个 Meta Logo
visionos·增强现实
苹果API搬运工5 个月前
我开源了个手势匹配框架,让你在模拟器调试 visionOS 手部追踪功能!
visionos·增强现实
苹果API搬运工5 个月前
只需三板斧!带你入门 visionOS 空间计算的数学与几何基础
visionos·增强现实
-九月新辰-6 个月前
Unity VisionOS开发流程
unity·游戏引擎·visionos
XR基地6 个月前
XR 世界导览021 | AVP App Store上线网页版、如何在AVP上快速匹配手势、AVP开发岗位
visionos
XR基地6 个月前
XR 世界导览#020-visionOS 1.1/Xcode 17.3/PICO 5.9.0 更新了!
unity3d·visionos
xChester6 个月前
visionOS 应用图标设计
前端·ios·visionos
XR基地6 个月前
XR 世界导览#019-Let's visionOS 大会/在 AVP 上玩手机或查看Spline的3D模型/用代码写ShaderGraph
unity3d·visionos
xChester7 个月前
VisionPro开发 - 轻松实现天空盒并添加光照
前端·ios·visionos