実装メモ:ParticleEmitterComponent でパーティクルを Entity 自身に吸収させる

パーティクル演出は基本的に放出されるようなイメージがあるが、visionOS の ParticleEmitterComponent では内向きに集約させることもできるのでメモ。

mainEmitter.attractionCenter に吸収先の座標を指定すればよく、emitterShapeSize を Entity のサイズよりも大きく指定した上で、エミッターを持つ Entity 中心座標を指定すれば、自分自身にパーティクルを吸収させることができる。

var p = entity.components[ParticleEmitterComponent.self]!
p.emitterShape = .sphere
p.emitterShapeSize = .init(x: 0.1, y: 0.1, z: 0.1)
p.fieldSimulationSpace = .local
p.mainEmitter.attractionCenter = entity.position(relativeTo: entity)

entity.components[ParticleEmitterComponent.self] = p

検証環境:visionOS 26.0.1

実装メモ:visionOS でパーティクルのプロパティをランタイムで変更する

Reality Composer Pro で定義したパーティクル(前回記事参照)を、Entity に対し ParticleEmitterComponent で設定したあと、その属性をアプリ動作中に動的に変更することができる。

単純に、ParticleEmitterComponent を取り出して値を変更し、再度入れ直せば良い。

struct ImmersiveView: View {
    @State var model: Entity?
    @ObservedObject var someTracking: SomeTracking

    var body: some View {
        RealityView { content in
            model = ModelEntity(...
            await attachParticleEffect(to: model!) // https://p0dee.com/blog/2025/11/03/define-particle-on-reality-composer-pro-and-attach-to-entity/
        } update: { content in
            guard let model else { return }
            model.position = someTracking.location
            updateParticleEmitterProperties(for: model, value: someTracking.strength)
        }
    }

    func updateParticleEmitterProperties(for model: Entity, value: Float) {
        guard var particleEmitterComp = centerMarker.components[ParticleEmitterComponent.self] else {
            return
        }
        let birthRate = Float(value * 100)
        particleEmitterComp.mainEmitter.birthRate = birthRate
        model.components[ParticleEmitterComponent.self] = particleEmitterComp
    }
}

他にも、RealityKit.System を使って、シーンの毎更新でパーティクルを自動的に更新する方法もありそう(出来た)。この手法は、例えば Apple のサンプルだと惑星や飛行機などの物体を周回軌道に乗せて動かし続ける、といった表現に用いられている。以下のコードだと、ParticleEmitterComponent を持ったすべての Entity を更新対象としいる。雑なので、Entity Component System に乗っ取り、カスタム Component を定義して、属性変更に必要な値もこれを経由して受け渡す設計にすると良いかもしれない。

// https://developer.apple.com/videos/play/wwdc2023/10081/
public class ParticleTransitionSystem: RealityKit.System {
    public required init(scene: RealityKit.Scene) { }
    
    private static let query = EntityQuery(where: .has(ParticleEmitterComponent.self))

    public func update(context: SceneUpdateContext) {
        let entities = context.scene.performQuery(Self.query)
        for entity in entities {
            updateParticles(entity: entity)
        }
    }
    
    public func updateParticles(entity: Entity) {
        guard var particleEmitterComp = entity.components[ParticleEmitterComponent.self] else {
            return
        }
        // スケールに応じてパーティクルの放出量を変化させる
        let scale = entity.scale(relativeTo: nil).x
        let birthRate = Float(scale * 100)
        particleEmitterComp.mainEmitter.birthRate = birthRate
        entity.components[ParticleEmitterComponent.self] = particleEmitterComp
    }
}

実装メモ:Reality Composer Pro で定義したパーティクルを Reality View のエンティティにアタッチする

visionOS/App プロジェクトを作成したら初期状態で存在する Packages/RealityKitContent/Package を Reality Composer Pro で開き、以下のブログに従ってパーティクルを追加する。

How to make a fire effect using particles and Reality Composer Pro

これを実装で抽出し、任意のエンティティに追加する。

func attachParticleEffect(to entity: Entity) async throws {
    let particlesRoot = try await Entity(
        named: "ParticleEffect",
        in: realityKitContentBundle // already defined in Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.swift
    )
    guard
        let emitterSourceEntity = particlesRoot.findEntity(named: "ParticleEmitter"),
        let emitterComponent = emitterSourceEntity.components[ParticleEmitterComponent.self]
    else {
        assertionFailure("Emitter entity or ParticleEmitterComponent not found")
        return
    }
    entity.components.set(emitterComponent)
}

検証環境 Xcode 26.0.1 (17A400)