布景
在体验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(沉溺式与窗口之间的切换)
手势滚动地球
手势拖拽地球旋转,地球保持在原地
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.增加手势
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
接收拖拽手势,在手势的onChanged
、onEnded
中去改变核算旋转视点。
这里便是一些视点核算:
yaw
是核算x方向上的旋转视点
pitch
是核算y方向上的旋转视点
content
.rotation3DEffect(.radians(yaw == 0 ? 0.01 : yaw), axis: .y)
.rotation3DEffect(.radians(pitch == 0 ? 0.01 : pitch), axis: .x)
yawLimit
、pitchLimit
最大视点的约束,咱们这里只约束了y方向上的视点,x没有约束。