axiom-realitykit by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-realitykit目的:使用 RealityKit 的实体-组件-系统架构构建 3D 内容、AR 体验和空间计算应用 iOS 版本:iOS 13+(基础版),iOS 18+(iOS 上的 RealityView),visionOS 1.0+ Xcode:Xcode 15+
在以下情况使用此技能:
在以下情况不要使用此技能:
axiom-scenekit)axiom-spritekit)axiom-metal-migration-ref)在 SceneKit 中,节点拥有其属性。一个节点就是一个可渲染、可碰撞、可动画的对象。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
在 RealityKit 中,实体是空容器。组件添加数据。系统处理这些数据。
Entity (身份 + 层级)
├── TransformComponent (位置, 旋转, 缩放)
├── ModelComponent (网格 + 材质)
├── CollisionComponent (碰撞形状)
├── PhysicsBodyComponent (质量, 模式)
└── [YourCustomComponent] (游戏特定数据)
System (每帧处理具有特定组件的实体)
为什么 ECS 重要:
| 场景图思维 | ECS 思维 |
|---|---|
| "玩家节点移动" | "移动系统处理具有 MovementComponent 的实体" |
| "在节点子类中添加方法" | "添加一个组件,创建一个系统" |
"在节点中重写 update(_:)" | "注册一个查询组件的系统" |
| "节点知道其生命值" | "HealthComponent 保存数据,DamageSystem 处理它" |
// 空实体
let entity = Entity()
entity.name = "player"
// 带有组件的实体
let entity = Entity()
entity.components[ModelComponent.self] = ModelComponent(
mesh: .generateBox(size: 0.1),
materials: [SimpleMaterial(color: .blue, isMetallic: false)]
)
// ModelEntity 便捷方式(内置 ModelComponent)
let box = ModelEntity(
mesh: .generateBox(size: 0.1),
materials: [SimpleMaterial(color: .red, isMetallic: true)]
)
// 父子关系
parent.addChild(child)
child.removeFromParent()
// 查找实体
let found = root.findEntity(named: "player")
// 遍历
for child in entity.children {
// 处理子节点
}
// 克隆
let clone = entity.clone(recursive: true)
// 局部变换(相对于父节点)
entity.position = SIMD3<Float>(0, 1, 0)
entity.orientation = simd_quatf(angle: .pi / 4, axis: SIMD3(0, 1, 0))
entity.scale = SIMD3<Float>(repeating: 2.0)
// 世界空间查询
let worldPos = entity.position(relativeTo: nil)
let worldTransform = entity.transform(relativeTo: nil)
// 设置世界空间变换
entity.setPosition(SIMD3(1, 0, 0), relativeTo: nil)
// 看向一个点
entity.look(at: targetPosition, from: entity.position, relativeTo: nil)
| 组件 | 用途 |
|---|---|
Transform | 位置、旋转、缩放 |
ModelComponent | 网格几何体 + 材质 |
CollisionComponent | 用于物理和交互的碰撞形状 |
PhysicsBodyComponent | 质量、物理模式(动态/静态/运动学) |
PhysicsMotionComponent | 线速度和角速度 |
AnchoringComponent | AR 锚点附着 |
SynchronizationComponent | 多人同步 |
PerspectiveCameraComponent | 相机设置 |
DirectionalLightComponent | 定向光 |
PointLightComponent | 点光源 |
SpotLightComponent | 聚光灯 |
CharacterControllerComponent | 角色物理控制器 |
AudioMixGroupsComponent | 音频混合 |
SpatialAudioComponent | 3D 定位音频 |
AmbientAudioComponent | 非定位音频 |
ChannelAudioComponent | 多声道音频 |
OpacityComponent | 实体透明度 |
GroundingShadowComponent | 接触阴影 |
InputTargetComponent | 手势输入(visionOS) |
HoverEffectComponent | 悬停高亮(visionOS) |
AccessibilityComponent | VoiceOver 支持 |
struct HealthComponent: Component {
var current: Int
var maximum: Int
var percentage: Float {
Float(current) / Float(maximum)
}
}
// 使用前注册(通常在应用初始化时)
HealthComponent.registerComponent()
// 附加到实体
entity.components[HealthComponent.self] = HealthComponent(current: 100, maximum: 100)
// 读取
if let health = entity.components[HealthComponent.self] {
print(health.current)
}
// 修改
entity.components[HealthComponent.self]?.current -= 10
组件是值类型。当你读取一个组件,修改它,然后写回时,你是在替换整个组件:
// 读取-修改-写入模式
var health = entity.components[HealthComponent.self]!
health.current -= damage
entity.components[HealthComponent.self] = health
反模式:持有对组件的引用并期望修改能传播。组件在读取时被复制。
struct DamageSystem: System {
// 定义此系统需要哪些组件
static let query = EntityQuery(where: .has(HealthComponent.self))
init(scene: RealityKit.Scene) {
// 一次性设置
}
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: Self.query,
updatingSystemWhen: .rendering) {
var health = entity.components[HealthComponent.self]!
if health.current <= 0 {
entity.removeFromParent()
}
}
}
}
// 注册系统
DamageSystem.registerSystem()
// 订阅碰撞事件
scene.subscribe(to: CollisionEvents.Began.self) { event in
let entityA = event.entityA
let entityB = event.entityB
// 处理碰撞
}
// 订阅场景更新
scene.subscribe(to: SceneEvents.Update.self) { event in
let deltaTime = event.deltaTime
// 每帧逻辑
}
struct ContentView: View {
var body: some View {
RealityView { content in
// make 闭包 —— 调用一次
let box = ModelEntity(
mesh: .generateBox(size: 0.1),
materials: [SimpleMaterial(color: .blue, isMetallic: false)]
)
content.add(box)
} update: { content in
// update 闭包 —— 当 SwiftUI 状态改变时调用
}
}
}
在 iOS 上,RealityView 提供了一个相机内容参数,用于配置 AR 或虚拟相机:
RealityView { content, attachments in
// 加载 3D 内容
if let model = try? await ModelEntity(named: "scene") {
content.add(model)
}
}
RealityView { content in
// 从包中加载
if let entity = try? await Entity(named: "MyScene", in: .main) {
content.add(entity)
}
// 从 URL 加载
if let entity = try? await Entity(contentsOf: modelURL) {
content.add(entity)
}
}
// 简单的 3D 模型显示(无交互)
Model3D(named: "toy_robot") { model in
model
.resizable()
.scaledToFit()
} placeholder: {
ProgressView()
}
RealityView { content, attachments in
let entity = ModelEntity(mesh: .generateSphere(radius: 0.1))
content.add(entity)
if let label = attachments.entity(for: "priceTag") {
label.position = SIMD3(0, 0.15, 0)
entity.addChild(label)
}
} attachments: {
Attachment(id: "priceTag") {
Text("$9.99")
.padding()
.glassBackgroundEffect()
}
}
struct GameView: View {
@State private var score = 0
var body: some View {
VStack {
Text("Score: \(score)")
RealityView { content in
let scene = try! await Entity(named: "GameScene")
content.add(scene)
} update: { content in
// 响应状态变化
// 注意:update 在 SwiftUI 状态改变时调用,
// 而不是每帧。对于每帧逻辑,请使用系统。
}
}
}
}
// 水平平面
let anchor = AnchorEntity(.plane(.horizontal, classification: .table,
minimumBounds: SIMD2(0.2, 0.2)))
// 垂直平面
let anchor = AnchorEntity(.plane(.vertical, classification: .wall,
minimumBounds: SIMD2(0.5, 0.5)))
// 世界位置
let anchor = AnchorEntity(world: SIMD3<Float>(0, 0, -1))
// 图像锚点
let anchor = AnchorEntity(.image(group: "AR Resources", name: "poster"))
// 面部锚点(前置摄像头)
let anchor = AnchorEntity(.face)
// 身体锚点
let anchor = AnchorEntity(.body)
let session = SpatialTrackingSession()
let configuration = SpatialTrackingSession.Configuration(tracking: [.plane, .object])
let result = await session.run(configuration)
if let notSupported = result {
// 处理此设备上不支持的追踪
for denied in notSupported.deniedTrackingModes {
print("Not supported: \(denied)")
}
}
.table、.floor、.wall)来适当放置内容// 启用拖拽、旋转、缩放手势
entity.components[ManipulationComponent.self] = ManipulationComponent(
allowedModes: .all // .translate, .rotate, .scale
)
// 还需要 CollisionComponent 进行命中测试
entity.generateCollisionShapes(recursive: true)
// visionOS 手势输入所需
entity.components[InputTargetComponent.self] = InputTargetComponent()
entity.components[CollisionComponent.self] = CollisionComponent(
shapes: [.generateBox(size: SIMD3(0.1, 0.1, 0.1))]
)
RealityView { content in
let entity = ModelEntity(mesh: .generateBox(size: 0.1))
entity.generateCollisionShapes(recursive: true)
entity.components.set(InputTargetComponent())
content.add(entity)
}
.gesture(
TapGesture()
.targetedToAnyEntity()
.onEnded { value in
let tappedEntity = value.entity
// 处理点击
}
)
.gesture(
DragGesture()
.targetedToAnyEntity()
.onChanged { value in
value.entity.position = value.convert(value.location3D,
from: .local, to: .scene)
}
)
// 从屏幕点进行光线投射
if let result = arView.raycast(from: screenPoint,
allowing: .estimatedPlane,
alignment: .horizontal).first {
let worldPosition = result.worldTransform.columns.3
// 将实体放置在 worldPosition
}
| 材质 | 用途 | 自定义项 |
|---|---|---|
SimpleMaterial | 纯色或纹理 | 颜色、金属度、粗糙度 |
PhysicallyBasedMaterial | 完整 PBR | 所有 PBR 贴图(基础色、法线、金属度、粗糙度、AO、自发光) |
UnlitMaterial | 无光照响应 | 颜色或纹理,始终完全照亮 |
OcclusionMaterial | 不可见但会遮挡 | AR 内容隐藏在真实物体后面 |
VideoMaterial | 表面视频播放 | AVPlayer 驱动 |
ShaderGraphMaterial | 自定义着色器图 | Reality Composer Pro |
CustomMaterial | Metal 着色器函数 | 完整的 Metal 控制 |
var material = PhysicallyBasedMaterial()
material.baseColor = .init(tint: .white,
texture: .init(try! .load(named: "albedo")))
material.metallic = .init(floatLiteral: 0.0)
material.roughness = .init(floatLiteral: 0.5)
material.normal = .init(texture: .init(try! .load(named: "normal")))
material.ambientOcclusion = .init(texture: .init(try! .load(named: "ao")))
material.emissiveColor = .init(color: .blue)
material.emissiveIntensity = 2.0
let entity = ModelEntity(
mesh: .generateSphere(radius: 0.1),
materials: [material]
)
// 隐藏其后方 3D 内容的不可见平面
let occluder = ModelEntity(
mesh: .generatePlane(width: 1, depth: 1),
materials: [OcclusionMaterial()]
)
occluder.position = SIMD3(0, 0, 0)
anchor.addChild(occluder)
// 基于图像的光照
if let resource = try? await EnvironmentResource(named: "studio_lighting") {
// 通过 RealityView 内容应用
}
// 从网格生成(精确但昂贵)
entity.generateCollisionShapes(recursive: true)
// 手动形状(性能优先)
entity.components[CollisionComponent.self] = CollisionComponent(
shapes: [
.generateBox(size: SIMD3(0.1, 0.2, 0.1)), // 盒子
.generateSphere(radius: 0.1), // 球体
.generateCapsule(height: 0.3, radius: 0.05) // 胶囊体
]
)
// 动态 —— 物理模拟控制运动
entity.components[PhysicsBodyComponent.self] = PhysicsBodyComponent(
massProperties: .init(mass: 1.0),
material: .generate(staticFriction: 0.5,
dynamicFriction: 0.3,
restitution: 0.4),
mode: .dynamic
)
// 静态 —— 不可移动的碰撞表面
ground.components[PhysicsBodyComponent.self] = PhysicsBodyComponent(
mode: .static
)
// 运动学 —— 代码控制,参与碰撞
platform.components[PhysicsBodyComponent.self] = PhysicsBodyComponent(
mode: .kinematic
)
// 定义组
let playerGroup = CollisionGroup(rawValue: 1 << 0)
let enemyGroup = CollisionGroup(rawValue: 1 << 1)
let bulletGroup = CollisionGroup(rawValue: 1 << 2)
// 过滤器:玩家与敌人和子弹碰撞
entity.components[CollisionComponent.self] = CollisionComponent(
shapes: [.generateSphere(radius: 0.1)],
filter: CollisionFilter(
group: playerGroup,
mask: enemyGroup | bulletGroup
)
)
// 在 RealityView 的 make 闭包或系统中订阅
scene.subscribe(to: CollisionEvents.Began.self, on: playerEntity) { event in
let otherEntity = event.entityA == playerEntity ? event.entityB : event.entityA
handleCollision(with: otherEntity)
}
if var motion = entity.components[PhysicsMotionComponent.self] {
motion.linearVelocity = SIMD3(0, 5, 0) // 向上冲量
entity.components[PhysicsMotionComponent.self] = motion
}
// 在持续时间内动画到位置
entity.move(
to: Transform(
scale: SIMD3(repeating: 1.5),
rotation: simd_quatf(angle: .pi, axis: SIMD3(0, 1, 0)),
translation: SIMD3(0, 2, 0)
),
relativeTo: entity.parent,
duration: 2.0,
timingFunction: .easeInOut
)
if let entity = try? await Entity(named: "character") {
// 播放所有可用动画
for animation in entity.availableAnimations {
entity.playAnimation(animation.repeat())
}
}
let controller = entity.playAnimation(animation)
controller.pause()
controller.resume()
controller.speed = 2.0 // 2 倍播放速度
controller.blendFactor = 0.5 // 与当前状态混合
// 加载音频资源
let resource = try! AudioFileResource.load(named: "engine.wav",
configuration: .init(shouldLoop: true))
// 创建带有空间音频的实体
let audioEntity = Entity()
audioEntity.components[SpatialAudioComponent.self] = SpatialAudioComponent()
let controller = audioEntity.playAudio(resource)
// 在 3D 空间中定位音频源
audioEntity.position = SIMD3(2, 0, -1)
entity.components[AmbientAudioComponent.self] = AmbientAudioComponent()
entity.playAudio(backgroundMusic)
// 在许多实体间共享网格和材质
let sharedMesh = MeshResource.generateSphere(radius: 0.01)
let sharedMaterial = SimpleMaterial(color: .white, isMetallic: false)
for i in 0..<1000 {
let entity = ModelEntity(mesh: sharedMesh, materials: [sharedMaterial])
entity.position = randomPosition()
parent.addChild(entity)
}
RealityKit 会自动批处理具有相同网格和材质资源的实体。
反模式:每帧创建和替换组件。
// 错误 —— 每帧分配组件
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: query, updatingSystemWhen: .rendering) {
entity.components[ModelComponent.self] = ModelComponent(
mesh: .generateBox(size: 0.1),
materials: [newMaterial] // 每帧新分配
)
}
}
// 正确 —— 修改现有组件
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: query, updatingSystemWhen: .rendering) {
// 仅在真正需要时更新
if needsUpdate {
var model = entity.components[ModelComponent.self]!
model.materials = [cachedMaterial]
entity.components[ModelComponent.self] = model
}
}
}
generateCollisionShapes(recursive: true) 很方便但昂贵使用 Xcode 的 RealityKit 调试器:
// 如果组件遵循 Codable 协议,则自动同步
struct ScoreComponent: Component, Codable {
var points: Int
}
// SynchronizationComponent 控制同步内容
entity.components[SynchronizationComponent.self] = SynchronizationComponent()
let service = try MultipeerConnectivityService(session: mcSession)
// 带有 SynchronizationComponent 的实体在节点间自动同步
时间成本:与架构对抗导致数小时的挫败感
// 错误 —— 为行为而子类化 Entity
class PlayerEntity: Entity {
func takeDamage(_ amount: Int) { /* 逻辑在实体中 */ }
}
// 正确 —— 组件保存数据,系统包含逻辑
struct HealthComponent: Component { var hp: Int }
struct DamageSystem: System {
static let query = EntityQuery(where: .has(HealthComponent.self))
func update(context: SceneUpdateContext) {
// 在此处理伤害
}
}
时间成本:不可测试、不灵活的架构
不要将所有游戏逻辑放在一种实体类型中。拆分成可以混合搭配的组件。
时间成本:错过帧更新,行为不一致
// 错误 —— 基于定时器的更新
Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { _ in
entity.position.x += 0.01
}
// 正确 —— 系统更新
struct MovementSystem: System {
static let query = EntityQuery(where: .has(VelocityComponent.self))
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: Self.query,
updatingSystemWhen: .rendering) {
let velocity = entity.components[VelocityComponent.self]!
entity.position += velocity.value * Float(context.deltaTime)
}
}
}
时间成本:15-30 分钟调试“为什么点击无效”
手势需要 CollisionComponent。如果一个实体有 InputTargetComponent 或 ManipulationComponent 但没有 CollisionComponent,手势永远不会触发。
时间成本:因引用过时而崩溃
// 错误 —— 实体可能在帧之间被移除
struct BadSystem: System {
var playerEntity: Entity? // 引用过时风险
func update(context: SceneUpdateContext) {
playerEntity?.position.x += 0.1 // 可能崩溃
}
}
// 正确 —— 每帧查询
struct GoodSystem: System {
static let query = EntityQuery(where: .has(PlayerComponent.self))
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: Self.query,
updatingSystemWhen: .rendering) {
entity.position.x += Float(context.deltaTime)
}
}
}
registerComponent() 注册registerSystem() 注册CollisionComponentInputTargetComponent[weak self](如果保留视图/控制器)压力:团队想避免学习 ECS,只需要显示一个 3D 模型
错误方法:跳过 ECS,将所有逻辑塞进 RealityView 闭包。
正确方法:即使是简单的应用也能从 ECS 中受益。RealityView 中的单个 ModelEntity 已经在使用 ECS —— 你只是还没有添加自定义组件。从简单开始,随着复杂性增长添加组件。
反驳模板:"我们已经在使用 ECS —— Entity 和 ModelComponent。这种模式是可扩展的。当我们需要行为时添加一个自定义组件,只是一个结构体定义,而不是架构变更。"
压力:团队有 SceneKit 经验,RealityKit 不熟悉
错误方法:在 SceneKit 中构建新功能。
正确方法:SceneKit 已处于软弃用状态。不会添加新功能。现在投资 RealityKit —— 如果需要,ECS 概念可以转移到其他游戏引擎(Unity、Unreal、Bevy)。
反驳模板:"SceneKit 处于维护模式 —— 没有新功能,只有安全补丁。我们写的每一行 SceneKit 代码都是迁移债务。RealityKit 的概念(Entity、Component、System)是行业标准的 ECS。"
压力:截止日期,碰撞形状设置看起来很复杂
错误方法:跳过碰撞形状,使用基于位置的接近检测。
正确方法:entity.generateCollisionShapes(recursive: true) 只需一行代码。没有它,手势将无法工作,物理也不会碰撞。这个"捷径"比它节省的时间创造了更多的调试时间。
反驳模板:"手势和物理需要碰撞形状。只需一行代码:entity.generateCollisionShapes(recursive: true)。跳过它意味着手势会静默失败 —— 这是一个更难诊断的错误。"
WWDC:2019-603, 2019-605, 2021-10074, 2022-10074, 2023-10080, 2023-10081, 2024-10103, 2024-10153
文档:/realitykit, /realitykit/entity, /realitykit/realityview, /realitykit/modelentity, /realitykit/anchorentity, /realitykit/component
技能:axiom-realitykit-ref, axiom-realitykit-diag, axiom-scenekit, axiom-scenekit-ref
每周安装
115
仓库
GitHub 星标
678
首次出现
2026年2月5日
安全审计
安装于
opencode110
gemini-cli107
codex106
github-copilot105
amp103
kimi-cli103
Purpose : Build 3D content, AR experiences, and spatial computing apps using RealityKit's Entity-Component-System architecture iOS Version : iOS 13+ (base), iOS 18+ (RealityView on iOS), visionOS 1.0+ Xcode : Xcode 15+
Use this skill when:
Do NOT use this skill for:
axiom-scenekit)axiom-spritekit)axiom-metal-migration-ref)In SceneKit, nodes own their properties. A node IS a renderable, collidable, animated thing.
In RealityKit, entities are empty containers. Components add data. Systems process that data.
Entity (identity + hierarchy)
├── TransformComponent (position, rotation, scale)
├── ModelComponent (mesh + materials)
├── CollisionComponent (collision shapes)
├── PhysicsBodyComponent (mass, mode)
└── [YourCustomComponent] (game-specific data)
System (processes entities with specific components each frame)
Why ECS matters :
| Scene Graph Thinking | ECS Thinking |
|---|---|
| "The player node moves" | "The movement system processes entities with MovementComponent" |
| "Add a method to the node subclass" | "Add a component, create a system" |
"Override update(_:) in the node" | "Register a System that queries for components" |
| "The node knows its health" | "HealthComponent holds data, DamageSystem processes it" |
// Empty entity
let entity = Entity()
entity.name = "player"
// Entity with components
let entity = Entity()
entity.components[ModelComponent.self] = ModelComponent(
mesh: .generateBox(size: 0.1),
materials: [SimpleMaterial(color: .blue, isMetallic: false)]
)
// ModelEntity convenience (has ModelComponent built in)
let box = ModelEntity(
mesh: .generateBox(size: 0.1),
materials: [SimpleMaterial(color: .red, isMetallic: true)]
)
// Parent-child
parent.addChild(child)
child.removeFromParent()
// Find entities
let found = root.findEntity(named: "player")
// Enumerate
for child in entity.children {
// Process children
}
// Clone
let clone = entity.clone(recursive: true)
// Local transform (relative to parent)
entity.position = SIMD3<Float>(0, 1, 0)
entity.orientation = simd_quatf(angle: .pi / 4, axis: SIMD3(0, 1, 0))
entity.scale = SIMD3<Float>(repeating: 2.0)
// World-space queries
let worldPos = entity.position(relativeTo: nil)
let worldTransform = entity.transform(relativeTo: nil)
// Set world-space transform
entity.setPosition(SIMD3(1, 0, 0), relativeTo: nil)
// Look at a point
entity.look(at: targetPosition, from: entity.position, relativeTo: nil)
| Component | Purpose |
|---|---|
Transform | Position, rotation, scale |
ModelComponent | Mesh geometry + materials |
CollisionComponent | Collision shapes for physics and interaction |
PhysicsBodyComponent | Mass, physics mode (dynamic/static/kinematic) |
PhysicsMotionComponent | Linear and angular velocity |
AnchoringComponent |
struct HealthComponent: Component {
var current: Int
var maximum: Int
var percentage: Float {
Float(current) / Float(maximum)
}
}
// Register before use (typically in app init)
HealthComponent.registerComponent()
// Attach to entity
entity.components[HealthComponent.self] = HealthComponent(current: 100, maximum: 100)
// Read
if let health = entity.components[HealthComponent.self] {
print(health.current)
}
// Modify
entity.components[HealthComponent.self]?.current -= 10
Components are value types (structs). When you read a component, modify it, and write it back, you're replacing the entire component:
// Read-modify-write pattern
var health = entity.components[HealthComponent.self]!
health.current -= damage
entity.components[HealthComponent.self] = health
Anti-pattern : Holding a reference to a component and expecting mutations to propagate. Components are copied on read.
struct DamageSystem: System {
// Define which components this system needs
static let query = EntityQuery(where: .has(HealthComponent.self))
init(scene: RealityKit.Scene) {
// One-time setup
}
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: Self.query,
updatingSystemWhen: .rendering) {
var health = entity.components[HealthComponent.self]!
if health.current <= 0 {
entity.removeFromParent()
}
}
}
}
// Register system
DamageSystem.registerSystem()
// Subscribe to collision events
scene.subscribe(to: CollisionEvents.Began.self) { event in
let entityA = event.entityA
let entityB = event.entityB
// Handle collision
}
// Subscribe to scene update
scene.subscribe(to: SceneEvents.Update.self) { event in
let deltaTime = event.deltaTime
// Per-frame logic
}
struct ContentView: View {
var body: some View {
RealityView { content in
// make closure — called once
let box = ModelEntity(
mesh: .generateBox(size: 0.1),
materials: [SimpleMaterial(color: .blue, isMetallic: false)]
)
content.add(box)
} update: { content in
// update closure — called when SwiftUI state changes
}
}
}
On iOS, RealityView provides a camera content parameter for configuring the AR or virtual camera:
RealityView { content, attachments in
// Load 3D content
if let model = try? await ModelEntity(named: "scene") {
content.add(model)
}
}
RealityView { content in
// Load from bundle
if let entity = try? await Entity(named: "MyScene", in: .main) {
content.add(entity)
}
// Load from URL
if let entity = try? await Entity(contentsOf: modelURL) {
content.add(entity)
}
}
// Simple 3D model display (no interaction)
Model3D(named: "toy_robot") { model in
model
.resizable()
.scaledToFit()
} placeholder: {
ProgressView()
}
RealityView { content, attachments in
let entity = ModelEntity(mesh: .generateSphere(radius: 0.1))
content.add(entity)
if let label = attachments.entity(for: "priceTag") {
label.position = SIMD3(0, 0.15, 0)
entity.addChild(label)
}
} attachments: {
Attachment(id: "priceTag") {
Text("$9.99")
.padding()
.glassBackgroundEffect()
}
}
struct GameView: View {
@State private var score = 0
var body: some View {
VStack {
Text("Score: \(score)")
RealityView { content in
let scene = try! await Entity(named: "GameScene")
content.add(scene)
} update: { content in
// React to state changes
// Note: update is called when SwiftUI state changes,
// not every frame. Use Systems for per-frame logic.
}
}
}
}
// Horizontal plane
let anchor = AnchorEntity(.plane(.horizontal, classification: .table,
minimumBounds: SIMD2(0.2, 0.2)))
// Vertical plane
let anchor = AnchorEntity(.plane(.vertical, classification: .wall,
minimumBounds: SIMD2(0.5, 0.5)))
// World position
let anchor = AnchorEntity(world: SIMD3<Float>(0, 0, -1))
// Image anchor
let anchor = AnchorEntity(.image(group: "AR Resources", name: "poster"))
// Face anchor (front camera)
let anchor = AnchorEntity(.face)
// Body anchor
let anchor = AnchorEntity(.body)
let session = SpatialTrackingSession()
let configuration = SpatialTrackingSession.Configuration(tracking: [.plane, .object])
let result = await session.run(configuration)
if let notSupported = result {
// Handle unsupported tracking on this device
for denied in notSupported.deniedTrackingModes {
print("Not supported: \(denied)")
}
}
.table, .floor, .wall) to place content appropriately// Enable drag, rotate, scale gestures
entity.components[ManipulationComponent.self] = ManipulationComponent(
allowedModes: .all // .translate, .rotate, .scale
)
// Also requires CollisionComponent for hit testing
entity.generateCollisionShapes(recursive: true)
// Required for visionOS gesture input
entity.components[InputTargetComponent.self] = InputTargetComponent()
entity.components[CollisionComponent.self] = CollisionComponent(
shapes: [.generateBox(size: SIMD3(0.1, 0.1, 0.1))]
)
RealityView { content in
let entity = ModelEntity(mesh: .generateBox(size: 0.1))
entity.generateCollisionShapes(recursive: true)
entity.components.set(InputTargetComponent())
content.add(entity)
}
.gesture(
TapGesture()
.targetedToAnyEntity()
.onEnded { value in
let tappedEntity = value.entity
// Handle tap
}
)
.gesture(
DragGesture()
.targetedToAnyEntity()
.onChanged { value in
value.entity.position = value.convert(value.location3D,
from: .local, to: .scene)
}
)
// Ray-cast from screen point
if let result = arView.raycast(from: screenPoint,
allowing: .estimatedPlane,
alignment: .horizontal).first {
let worldPosition = result.worldTransform.columns.3
// Place entity at worldPosition
}
| Material | Purpose | Customization |
|---|---|---|
SimpleMaterial | Solid color or texture | Color, metallic, roughness |
PhysicallyBasedMaterial | Full PBR | All PBR maps (base color, normal, metallic, roughness, AO, emissive) |
UnlitMaterial | No lighting response | Color or texture, always fully lit |
OcclusionMaterial | Invisible but occludes | AR content hiding behind real objects |
VideoMaterial |
var material = PhysicallyBasedMaterial()
material.baseColor = .init(tint: .white,
texture: .init(try! .load(named: "albedo")))
material.metallic = .init(floatLiteral: 0.0)
material.roughness = .init(floatLiteral: 0.5)
material.normal = .init(texture: .init(try! .load(named: "normal")))
material.ambientOcclusion = .init(texture: .init(try! .load(named: "ao")))
material.emissiveColor = .init(color: .blue)
material.emissiveIntensity = 2.0
let entity = ModelEntity(
mesh: .generateSphere(radius: 0.1),
materials: [material]
)
// Invisible plane that hides 3D content behind it
let occluder = ModelEntity(
mesh: .generatePlane(width: 1, depth: 1),
materials: [OcclusionMaterial()]
)
occluder.position = SIMD3(0, 0, 0)
anchor.addChild(occluder)
// Image-based lighting
if let resource = try? await EnvironmentResource(named: "studio_lighting") {
// Apply via RealityView content
}
// Generate from mesh (accurate but expensive)
entity.generateCollisionShapes(recursive: true)
// Manual shapes (prefer for performance)
entity.components[CollisionComponent.self] = CollisionComponent(
shapes: [
.generateBox(size: SIMD3(0.1, 0.2, 0.1)), // Box
.generateSphere(radius: 0.1), // Sphere
.generateCapsule(height: 0.3, radius: 0.05) // Capsule
]
)
// Dynamic — physics simulation controls movement
entity.components[PhysicsBodyComponent.self] = PhysicsBodyComponent(
massProperties: .init(mass: 1.0),
material: .generate(staticFriction: 0.5,
dynamicFriction: 0.3,
restitution: 0.4),
mode: .dynamic
)
// Static — immovable collision surface
ground.components[PhysicsBodyComponent.self] = PhysicsBodyComponent(
mode: .static
)
// Kinematic — code-controlled, participates in collisions
platform.components[PhysicsBodyComponent.self] = PhysicsBodyComponent(
mode: .kinematic
)
// Define groups
let playerGroup = CollisionGroup(rawValue: 1 << 0)
let enemyGroup = CollisionGroup(rawValue: 1 << 1)
let bulletGroup = CollisionGroup(rawValue: 1 << 2)
// Filter: player collides with enemies and bullets
entity.components[CollisionComponent.self] = CollisionComponent(
shapes: [.generateSphere(radius: 0.1)],
filter: CollisionFilter(
group: playerGroup,
mask: enemyGroup | bulletGroup
)
)
// Subscribe in RealityView make closure or System
scene.subscribe(to: CollisionEvents.Began.self, on: playerEntity) { event in
let otherEntity = event.entityA == playerEntity ? event.entityB : event.entityA
handleCollision(with: otherEntity)
}
if var motion = entity.components[PhysicsMotionComponent.self] {
motion.linearVelocity = SIMD3(0, 5, 0) // Impulse up
entity.components[PhysicsMotionComponent.self] = motion
}
// Animate to position over duration
entity.move(
to: Transform(
scale: SIMD3(repeating: 1.5),
rotation: simd_quatf(angle: .pi, axis: SIMD3(0, 1, 0)),
translation: SIMD3(0, 2, 0)
),
relativeTo: entity.parent,
duration: 2.0,
timingFunction: .easeInOut
)
if let entity = try? await Entity(named: "character") {
// Play all available animations
for animation in entity.availableAnimations {
entity.playAnimation(animation.repeat())
}
}
let controller = entity.playAnimation(animation)
controller.pause()
controller.resume()
controller.speed = 2.0 // 2x playback speed
controller.blendFactor = 0.5 // Blend with current state
// Load audio resource
let resource = try! AudioFileResource.load(named: "engine.wav",
configuration: .init(shouldLoop: true))
// Create entity with spatial audio
let audioEntity = Entity()
audioEntity.components[SpatialAudioComponent.self] = SpatialAudioComponent()
let controller = audioEntity.playAudio(resource)
// Position the audio source in 3D space
audioEntity.position = SIMD3(2, 0, -1)
entity.components[AmbientAudioComponent.self] = AmbientAudioComponent()
entity.playAudio(backgroundMusic)
// Share mesh and material across many entities
let sharedMesh = MeshResource.generateSphere(radius: 0.01)
let sharedMaterial = SimpleMaterial(color: .white, isMetallic: false)
for i in 0..<1000 {
let entity = ModelEntity(mesh: sharedMesh, materials: [sharedMaterial])
entity.position = randomPosition()
parent.addChild(entity)
}
RealityKit automatically batches entities with identical mesh and material resources.
Anti-pattern : Creating and replacing components every frame.
// BAD — component allocation every frame
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: query, updatingSystemWhen: .rendering) {
entity.components[ModelComponent.self] = ModelComponent(
mesh: .generateBox(size: 0.1),
materials: [newMaterial] // New allocation every frame
)
}
}
// GOOD — modify existing component
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: query, updatingSystemWhen: .rendering) {
// Only update when actually needed
if needsUpdate {
var model = entity.components[ModelComponent.self]!
model.materials = [cachedMaterial]
entity.components[ModelComponent.self] = model
}
}
}
generateCollisionShapes(recursive: true) is convenient but expensiveUse Xcode's RealityKit debugger:
// Components sync automatically if they conform to Codable
struct ScoreComponent: Component, Codable {
var points: Int
}
// SynchronizationComponent controls what syncs
entity.components[SynchronizationComponent.self] = SynchronizationComponent()
let service = try MultipeerConnectivityService(session: mcSession)
// Entities with SynchronizationComponent auto-sync across peers
Time cost : Hours of frustration from fighting the architecture
// BAD — subclassing Entity for behavior
class PlayerEntity: Entity {
func takeDamage(_ amount: Int) { /* logic in entity */ }
}
// GOOD — component holds data, system has logic
struct HealthComponent: Component { var hp: Int }
struct DamageSystem: System {
static let query = EntityQuery(where: .has(HealthComponent.self))
func update(context: SceneUpdateContext) {
// Process damage here
}
}
Time cost : Untestable, inflexible architecture
Don't put all game logic in one entity type. Split into components that can be mixed and matched.
Time cost : Missed frame updates, inconsistent behavior
// BAD — timer-based updates
Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { _ in
entity.position.x += 0.01
}
// GOOD — System update
struct MovementSystem: System {
static let query = EntityQuery(where: .has(VelocityComponent.self))
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: Self.query,
updatingSystemWhen: .rendering) {
let velocity = entity.components[VelocityComponent.self]!
entity.position += velocity.value * Float(context.deltaTime)
}
}
}
Time cost : 15-30 min debugging "why taps don't work"
Gestures require CollisionComponent. If an entity has InputTargetComponent (visionOS) or ManipulationComponent but no CollisionComponent, gestures will never fire.
Time cost : Crashes from stale references
// BAD — entity might be removed between frames
struct BadSystem: System {
var playerEntity: Entity? // Stale reference risk
func update(context: SceneUpdateContext) {
playerEntity?.position.x += 0.1 // May crash
}
}
// GOOD — query each frame
struct GoodSystem: System {
static let query = EntityQuery(where: .has(PlayerComponent.self))
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: Self.query,
updatingSystemWhen: .rendering) {
entity.position.x += Float(context.deltaTime)
}
}
}
registerComponent() before useregisterSystem() before scene loadsCollisionComponentInputTargetComponent[weak self] in closure-based subscriptions if retaining view/controllerPressure : Team wants to avoid learning ECS, just needs one 3D model displayed
Wrong approach : Skip ECS, jam all logic into RealityView closures.
Correct approach : Even simple apps benefit from ECS. A single ModelEntity in a RealityView is already using ECS — you're just not adding custom components yet. Start simple, add components as complexity grows.
Push-back template : "We're already using ECS — Entity and ModelComponent. The pattern scales. Adding a custom component when we need behavior is one struct definition, not an architecture change."
Pressure : Team has SceneKit experience, RealityKit is unfamiliar
Wrong approach : Build new features in SceneKit.
Correct approach : SceneKit is soft-deprecated. New features won't be added. Invest in RealityKit now — the ECS concepts transfer to other game engines (Unity, Unreal, Bevy) if needed.
Push-back template : "SceneKit is in maintenance mode — no new features, only security patches. Every line of SceneKit we write is migration debt. RealityKit's concepts (Entity, Component, System) are industry-standard ECS."
Pressure : Deadline, collision shape setup seems complex
Wrong approach : Skip collision shapes, use position-based proximity detection.
Correct approach : entity.generateCollisionShapes(recursive: true) takes one line. Without it, gestures won't work and physics won't collide. The "shortcut" creates more debugging time than it saves.
Push-back template : "Collision shapes are required for gestures and physics. It's one line: entity.generateCollisionShapes(recursive: true). Skipping it means gestures silently fail — a harder bug to diagnose."
WWDC : 2019-603, 2019-605, 2021-10074, 2022-10074, 2023-10080, 2023-10081, 2024-10103, 2024-10153
Docs : /realitykit, /realitykit/entity, /realitykit/realityview, /realitykit/modelentity, /realitykit/anchorentity, /realitykit/component
Skills : axiom-realitykit-ref, axiom-realitykit-diag, axiom-scenekit, axiom-scenekit-ref
Weekly Installs
115
Repository
GitHub Stars
678
First Seen
Feb 5, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode110
gemini-cli107
codex106
github-copilot105
amp103
kimi-cli103
Ralph AI任务完成保障工具 - 结构化PRD驱动开发与强制验证流程
253 周安装
财务摘要报告生成器 - 基于S&P Global MCP实时数据自动生成专业Word文档
257 周安装
Android现代架构与模块化指南:整洁架构、分层设计与Hilt依赖注入
255 周安装
McKinsey Consultant V3.1:AI驱动的麦肯锡风格商业问题解决框架与技能
260 周安装
drawio 图表生成器:Markdown 中创建专业图表与架构图的完整指南与规则
262 周安装
代码库审计员 - 协调9个工作器进行全面的代码质量分析与最佳实践检查
254 周安装
| AR anchor attachment |
SynchronizationComponent | Multiplayer sync |
PerspectiveCameraComponent | Camera settings |
DirectionalLightComponent | Directional light |
PointLightComponent | Point light |
SpotLightComponent | Spot light |
CharacterControllerComponent | Character physics controller |
AudioMixGroupsComponent | Audio mixing |
SpatialAudioComponent | 3D positional audio |
AmbientAudioComponent | Non-positional audio |
ChannelAudioComponent | Multi-channel audio |
OpacityComponent | Entity transparency |
GroundingShadowComponent | Contact shadow |
InputTargetComponent | Gesture input (visionOS) |
HoverEffectComponent | Hover highlight (visionOS) |
AccessibilityComponent | VoiceOver support |
| Video playback on surface |
| AVPlayer-driven |
ShaderGraphMaterial | Custom shader graph | Reality Composer Pro |
CustomMaterial | Metal shader functions | Full Metal control |