はじめに
こんにちは!システム開発部のYです。
担当した企業案件で、Full Immersive空間内である場所に移動させたいという要件がありました。
ユーザー自身のポジションを直接変更することはできなかったため、別のアプローチで「それらしく見せる」方法を検討し、実現しました。今回はその手法についてまとめていきたいと思います。
環境
- Swift Version: 5.9.2
- Xcode: 15.2
- visionOS: 1.0〜1.3
- Reality Composer Pro: 1.0
- macOS: 14.5以降
空間内の移動について
前置きでも述べたように、ユーザー自身のポジションを直接変更することはできません。
しかし、別のアプローチとして、空間そのものを動かすことで、ユーザーが移動しているように見せることが可能です。
ただし、注意点があります。以下のガイドラインに記載されている通り、空間全体を移動させることで、ユーザーが酔いやすくなる可能性があります。
固定された座標系を用意することを検討する。固定された領域の中で完結している視覚的な動きの方が、人間は楽に処理することができます。反対に、例えばプレイヤーに自動的に空間を移動させるゲームで、周囲のエリア全体が動いているように見えると、気分が悪くなってしまうことがあります。
https://developer.apple.com/jp/design/human-interface-guidelines/motion#visionOS
実際の動画をご覧ください
※モデルをタップすると、モデルの前に移動します
実装について
指定ポジションへの移動
1. プロジェクトの作成
まず、Xcodeを開き、新規プロジェクト作成時に「visionOS」を選択します。その際、Immersive Space の設定は「Full」にしてください。

作成したプロジェクトをビルドして実行すると、次のようなアプリが起動します。
ウィンドウが表示され、その中央に「Show Immersive Space」というボタンが配置されています。このボタンを押すと、Full Immersive Spaceに入り、2つの球体が表示されます。
2. Reality Composer Proの変更
Reality Composer Proを開き、「Immersive.usda」を選択して編集を行います。
「Sphere_Left」と「Sphere_Right」を削除し、移動がわかりやすいように、Cubeを使用して床を作成し、目の前にモデルを配置します。

今回使用するモデルは、サンプルにある「Drummer.usdz」です。
このモデルをタップで検知できるようにするため、以下の設定として、CollisionとInputTargetを追加します。

3. ImmersiveView.swiftの変更
プロジェクト作成後の、コードは以下になっています。
import SwiftUI
import RealityKit
import RealityKitContent
struct ImmersiveView: View {
@State private var immersiveContentEntity = Entity()
var body: some View {
RealityView { content in
// Add the initial RealityKit content
if let immersiveContentEntity = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
content.add(immersiveContentEntity)
// Add an ImageBasedLight for the immersive content
guard let resource = try? await EnvironmentResource(named: "ImageBasedLight") else { return }
let iblComponent = ImageBasedLightComponent(source: .single(resource), intensityExponent: 0.25)
immersiveContentEntity.components.set(iblComponent)
immersiveContentEntity.components.set(ImageBasedLightReceiverComponent(imageBasedLight: immersiveContentEntity))
// Put skybox here. See example in World project available at
// https://developer.apple.com/
}
}
}
}
Entityのタップ検知をするために、tapGestureを追加します。
以下のように、変更していきます。
ここまででビルドして実行し、モデルをタップするとDrummerという文字列が出力されます。
import SwiftUI
import RealityKit
import RealityKitContent
struct ImmersiveView: View {
var body: some View {
RealityView { content in
// Add the initial RealityKit content
if let immersiveContentEntity = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
content.add(immersiveContentEntity)
// Add an ImageBasedLight for the immersive content
guard let resource = try? await EnvironmentResource(named: "ImageBasedLight") else { return }
let iblComponent = ImageBasedLightComponent(source: .single(resource), intensityExponent: 0.25)
immersiveContentEntity.components.set(iblComponent)
immersiveContentEntity.components.set(ImageBasedLightReceiverComponent(imageBasedLight: immersiveContentEntity))
}
}
.gesture(tapGesture)
}
var tapGesture: some Gesture {
TapGesture()
.targetedToAnyEntity()
.onEnded { value in
switch value.entity.name {
case "Drummer":
print("Drummer")
default: break
}
}
}
}タップした時に、空間が移動しているように見える実装を行います。
読み込んだEntityを操作したいので、immersiveContentEntityという変数を作成し、保持するように変更します。
次では、immersiveContentEntityをmoveさせています。
import SwiftUI
import RealityKit
import RealityKitContent
struct ImmersiveView: View {
@State private var immersiveContentEntity = Entity()
var body: some View {
RealityView { content in
// Add the initial RealityKit content
if let immersiveContentEntity = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
self.immersiveContentEntity = immersiveContentEntity
content.add(immersiveContentEntity)
// Add an ImageBasedLight for the immersive content
guard let resource = try? await EnvironmentResource(named: "ImageBasedLight") else { return }
let iblComponent = ImageBasedLightComponent(source: .single(resource), intensityExponent: 0.25)
immersiveContentEntity.components.set(iblComponent)
immersiveContentEntity.components.set(ImageBasedLightReceiverComponent(imageBasedLight: immersiveContentEntity))
}
}
.gesture(tapGesture)
}
var tapGesture: some Gesture {
TapGesture()
.targetedToAnyEntity()
.onEnded { value in
switch value.entity.name {
case "Drummer":
// 動かすPosition
let newPosition = SIMD3<Float>(0, 0, 7)
var newTransform = self.immersiveContentEntity.transform
newTransform.translation = newPosition
self.immersiveContentEntity.move(to: newTransform, relativeTo: nil, duration: 1.0, timingFunction: .linear)
default: break
}
}
}
}ここまででタップすると、immersiveContentEntityのポジションが変わって空間が移動し、あたかも自分が移動しているように見えます。
指定したアンカーへの移動
先ほどは指定したポジションへの移動でしたが、Reality Composer Proで設置したポジションへ移動させたいという事があると思います。
1. Reality Composer Proを変更
Reality Composer Proを開き、「Immersive.usda」を選択し変更します。
新しいTransform「DrummerAnchor」を作成します。
作成したTransformを次のように、Drummerの前辺りに設置します。

2. ImmersiveView.swiftの変更
Reality Composer Proで設置した、「DrummerAnchor」を取得し移動します。
取得するコードと、読み込んだDrummerAnchorを使用するので、
drummerAnchorという変数を作成し、保持するように変更します。
import SwiftUI
import RealityKit
import RealityKitContent
struct ImmersiveView: View {
@State private var immersiveContentEntity = Entity()
@State private var drummerAnchor = Entity()
var body: some View {
RealityView { content in
// Add the initial RealityKit content
if let immersiveContentEntity = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
self.immersiveContentEntity = immersiveContentEntity
if let drummerAnchor = immersiveContentEntity.findEntity(named: "DrummerAnchor") {
self.drummerAnchor = drummerAnchor
}
content.add(immersiveContentEntity)
// Add an ImageBasedLight for the immersive content
guard let resource = try? await EnvironmentResource(named: "ImageBasedLight") else { return }
let iblComponent = ImageBasedLightComponent(source: .single(resource), intensityExponent: 0.25)
immersiveContentEntity.components.set(iblComponent)
immersiveContentEntity.components.set(ImageBasedLightReceiverComponent(imageBasedLight: immersiveContentEntity))
// Put skybox here. See example in World project available at
// https://developer.apple.com/
}
}
.gesture(tapGesture)
}
}また、移動する処理をEntityのExtensionに移動させました。
moveToEntityが指定されたエンティティに移動させる処理です。指定されたエンティティのポジションを取得し、ポジションを反転させて移動させています。
extension Entity {
/// 指定されたエンティティに移動する
@discardableResult
func moveToEntity(_ movePointEntity: Entity, relativeTo parent: Entity?, duration: TimeInterval = 1.0, timingFunction: AnimationTimingFunction = .linear) -> AnimationPlaybackController? {
let movePointTranslation = movePointEntity.transform.translation
let newTranslation = movePointTranslation * -1
return self.moveToPosition(newTranslation, relativeTo: parent, duration: duration, timingFunction: timingFunction)
}
/// アニメーションしながらエンティティを指定箇所に動かす
@discardableResult
func moveToPosition(_ position: SIMD3<Float>, relativeTo parent: Entity?, duration: TimeInterval = 1.0, timingFunction: AnimationTimingFunction = .linear) -> AnimationPlaybackController {
return self.move(
to: Transform(scale: self.transform.scale, rotation: self.transform.rotation, translation: position),
relativeTo: parent,
duration: duration,
timingFunction: timingFunction
)
}
}
以下全コードです。
import SwiftUI
import RealityKit
import RealityKitContent
struct ImmersiveView: View {
@State private var immersiveContentEntity = Entity()
@State private var drummerAnchor = Entity()
var body: some View {
RealityView { content in
// Add the initial RealityKit content
if let immersiveContentEntity = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
self.immersiveContentEntity = immersiveContentEntity
if let drummerAnchor = immersiveContentEntity.findEntity(named: "DrummerAnchor") {
self.drummerAnchor = drummerAnchor
}
content.add(immersiveContentEntity)
// Add an ImageBasedLight for the immersive content
guard let resource = try? await EnvironmentResource(named: "ImageBasedLight") else { return }
let iblComponent = ImageBasedLightComponent(source: .single(resource), intensityExponent: 0.25)
immersiveContentEntity.components.set(iblComponent)
immersiveContentEntity.components.set(ImageBasedLightReceiverComponent(imageBasedLight: immersiveContentEntity))
}
}
.gesture(tapGesture)
}
var tapGesture: some Gesture {
TapGesture()
.targetedToAnyEntity()
.onEnded { value in
switch value.entity.name {
case "Drummer":
self.immersiveContentEntity.moveToEntity(self.drummerAnchor, relativeTo: nil, duration: 1, timingFunction: .linear)
default: break
}
}
}
}
extension Entity {
/// 指定されたエンティティに移動する
@discardableResult
func moveToEntity(_ movePointEntity: Entity, relativeTo parent: Entity?, duration: TimeInterval = 1.0, timingFunction: AnimationTimingFunction = .linear) -> AnimationPlaybackController? {
let movePointTranslation = movePointEntity.transform.translation
let newTranslation = movePointTranslation * -1
return self.moveToPosition(newTranslation, relativeTo: parent, duration: duration, timingFunction: timingFunction)
}
/// アニメーションしながらエンティティを指定箇所に動かす
@discardableResult
func moveToPosition(_ position: SIMD3<Float>, relativeTo parent: Entity?, duration: TimeInterval = 1.0, timingFunction: AnimationTimingFunction = .linear) -> AnimationPlaybackController {
return self.move(
to: Transform(scale: self.transform.scale, rotation: self.transform.rotation, translation: position),
relativeTo: parent,
duration: duration,
timingFunction: timingFunction
)
}
}
まとめ
空間内を移動する演出の実装方法についてまとめました。
コード自体は非常にシンプルに実装できましたが、実際に空間内を移動する体験はユーザーに大きな負担を与えやすく、酔いを引き起こす可能性があります。そのため、適切な場面で慎重に活用することが重要です。
この記事が皆様のお役に立てれば幸いです。また、誤りや改善点があれば、ぜひ教えていただけると幸いです。








