scenekit

scenekit

热门

维护和扩展现有的 SceneKit 3D 场景与可视化项目。适用于处理 SCNView、SCNScene、SCNNode 场景图,SceneKit 几何体/材质/光源/相机,SCNAction 动画,SCNPhysicsBody 物理,SCNParticleSystem 粒子特效,.scn/.dae/.abc 格式的 SceneKit 资源,着色器修改器(shader modifiers)或 SwiftUI SceneView 的场景。SceneKit 已被软废弃(soft-deprecated)并进入仅维护状态;新应用开发、重大功能更新、USD/USDZ 管线及迁移计划应转向 RealityKit。

967Star
49Fork
更新于 2026/7/31
SKILL.md
只读
名称
scenekit
描述

维护和扩展现有的 SceneKit 3D 场景与可视化项目。适用于处理 SCNView、SCNScene、SCNNode 场景图,SceneKit 几何体/材质/光源/相机,SCNAction 动画,SCNPhysicsBody 物理,SCNParticleSystem 粒子特效,.scn/.dae/.abc 格式的 SceneKit 资源,着色器修改器(shader modifiers)或 SwiftUI SceneView 的场景。SceneKit 已被软废弃(soft-deprecated)并进入仅维护状态;新应用开发、重大功能更新、USD/USDZ 管线及迁移计划应转向 RealityKit。

SceneKit

仅用于维护现有的 SceneKit 场景。Apple 已在 WWDC 2025 上宣布废弃 SceneKit 并将其限制为仅维护模式;新项目、重大架构更新以及 USD/USDZ 工作流管线均应转向 RealityKit。现有 App 可继续正常运行。

目录

场景搭建

UIKit 中的 SCNView

import SceneKit

let sceneView = SCNView(frame: view.bounds)
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = .black
view.addSubview(sceneView)

allowsCameraControl 开启后会内置轨道旋转、平移和缩放手势。在需要自定义相机控制的生产环境中,通常会禁用该选项。

创建 SCNScene

let scene = SCNScene()                                          // 空场景
guard let scene = SCNScene(named: "art.scnassets/ship.scn")     // 位于 .scnassets 中的 .scn 文件
    else { fatalError("Missing scene asset") }
let url = Bundle.main.url(forResource: "ship", withExtension: "dae")!
let scene = try SCNScene(url: url, options: [.checkConsistency: true])

节点与几何体

每个场景都有一个 rootNode(根节点)。所有内容都作为其子孙节点存在。节点在父节点的坐标系中定义了位置、朝向和缩放。SceneKit 使用右手坐标系:+X 轴向右,+Y 轴向上,+Z 轴朝向相机。

let parentNode = SCNNode()
scene.rootNode.addChildNode(parentNode)

let childNode = SCNNode()
childNode.position = SCNVector3(0, 1, 0)  // 高于父节点 1 个单位
parentNode.addChildNode(childNode)

变换(Transforms)

node.position = SCNVector3(x: 0, y: 2, z: -5)
node.eulerAngles = SCNVector3(x: 0, y: .pi / 4, z: 0)  // 沿 Y 轴旋转 45 度
node.scale = SCNVector3(2, 2, 2)
node.simdPosition = SIMD3<Float>(0, 2, -5)  // 出于性能考虑推荐使用 simd

内置几何原型(Primitives)

SCNBoxSCNSphereSCNCylinderSCNConeSCNTorusSCNCapsuleSCNTubeSCNPlaneSCNFloorSCNTextSCNShape(贝塞尔路径挤出图形)。

let node = SCNNode(geometry: SCNSphere(radius: 0.5))

查找节点

let maxNode = scene.rootNode.childNode(withName: "Max", recursively: true)
let enemies = scene.rootNode.childNodes { node, _ in
    node.name?.hasPrefix("enemy") == true
}

材质

SCNMaterial 定义表面外观。使用 firstMaterial 处理单材质几何体,或使用 materials 数组处理多材质几何体。

颜色与纹理

let material = SCNMaterial()
material.diffuse.contents = UIColor.systemBlue     // 纯色
material.diffuse.contents = UIImage(named: "brick") // 纹理贴图
material.normal.contents = UIImage(named: "brick_normal")
sphere.firstMaterial = material

基于物理的渲染(PBR)

let pbr = SCNMaterial()
pbr.lightingModel = .physicallyBased
pbr.diffuse.contents = UIImage(named: "albedo")
pbr.metalness.contents = 0.8       // 标量或纹理贴图
pbr.roughness.contents = 0.2       // 标量或纹理贴图
pbr.normal.contents = UIImage(named: "normal")
pbr.ambientOcclusion.contents = UIImage(named: "ao")

光照模型

.physicallyBased(金属度/粗糙度)、.blinn(默认值)、.phong.lambert(仅漫反射)、.constant(无光照)、.shadowOnly

每个材质属性都是一个 SCNMaterialProperty,可接收 UIColorUIImageCGFloat 标量、SKTextureCALayerAVPlayer

透明度

material.transparency = 0.5
material.transparencyMode = .dualLayer
material.isDoubleSided = true

光照

SCNLight 挂载到节点上。光照方向沿节点的 Z 轴负方向。

光照类型

// 环境光(Ambient):均匀漫射,无固定方向
let ambient = SCNLight()
ambient.type = .ambient
ambient.color = UIColor(white: 0.3, alpha: 1)

// 平行光(Directional):平行光线(如阳光)
let directional = SCNLight()
directional.type = .directional
directional.castsShadow = true

// 点光源(Omni):向所有方向辐射的点光源
let omni = SCNLight()
omni.type = .omni
omni.attenuationEndDistance = 20

// 聚光灯(Spot):圆锥形光束
let spot = SCNLight()
spot.type = .spot
spot.spotInnerAngle = 20
spot.spotOuterAngle = 60

挂载到节点:

let lightNode = SCNNode()
lightNode.light = directional
lightNode.eulerAngles = SCNVector3(-Float.pi / 3, 0, 0)
lightNode.position = SCNVector3(0, 10, 10)
scene.rootNode.addChildNode(lightNode)

阴影

light.castsShadow = true
light.shadowMapSize = CGSize(width: 2048, height: 2048)
light.shadowSampleCount = 8
light.shadowRadius = 3.0
light.shadowColor = UIColor(white: 0, alpha: 0.5)

分类掩码(Category Bit Masks)

light.categoryBitMask = 1 << 1     // 分类 2
node.categoryBitMask = 1 << 1      // 仅会被分类 2 的光源照亮

SceneKit 每个节点最多渲染 8 个光源。对点光源/聚光灯使用 attenuationEndDistance(衰减结束距离),以便 SceneKit 自动忽略对远端节点的光照计算。

相机

SCNCamera 挂载到节点上以定义视角。

let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 5, 15)
cameraNode.look(at: SCNVector3Zero)
scene.rootNode.addChildNode(cameraNode)
sceneView.pointOfView = cameraNode

属性配置

camera.fieldOfView = 60                        // 视场角(度)
camera.zNear = 0.1
camera.zFar = 500
camera.automaticallyAdjustsZRange = true

// 正交投影
camera.usesOrthographicProjection = true
camera.orthographicScale = 10

景深特效(wantsDepthOfFieldfocusDistancefStop)和 HDR 特效(wantsHDRbloomIntensitybloomThresholdscreenSpaceAmbientOcclusionIntensity)均可在 SCNCamera 上直接配置。

动画

SceneKit 提供了三种动画实现方式。

SCNAction(声明式,面向游戏)

挂载到节点上的可复用、可组合的动画对象。

let move = SCNAction.move(by: SCNVector3(0, 2, 0), duration: 1)
let rotate = SCNAction.rotateBy(x: 0, y: .pi, z: 0, duration: 1)
node.runAction(.group([move, rotate]))

// 顺序动画
node.runAction(.sequence([.fadeOut(duration: 0.3), .removeFromParentNode()]))

// 无限循环动画
let pulse = SCNAction.sequence([
    .scale(to: 1.2, duration: 0.5),
    .scale(to: 1.0, duration: 0.5)
])
node.runAction(.repeatForever(pulse))

SCNTransaction(隐式动画)

SCNTransaction.begin()
SCNTransaction.animationDuration = 1.0
node.position = SCNVector3(5, 0, 0)
node.opacity = 0.5
SCNTransaction.completionBlock = { print("Done") }
SCNTransaction.commit()

显式动画(Core Animation)

let animation = CABasicAnimation(keyPath: "rotation")
animation.toValue = NSValue(scnVector4: SCNVector4(0, 1, 0, Float.pi * 2))
animation.duration = 2
animation.repeatCount = .infinity
node.addAnimation(animation, forKey: "spin")

物理引擎

物理体(Physics Body)

node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)   // 受力 + 碰撞
floor.physicsBody = SCNPhysicsBody(type: .static, shape: nil)    // 不可移动
platform.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) // 代码驱动

shapenil 时,SceneKit 会自动根据几何体计算物理碰撞形状。出于性能考虑,建议使用简化形状:

let shape = SCNPhysicsShape(
    geometry: SCNBox(width: 1, height: 2, length: 1, chamferRadius: 0),
    options: nil
)
node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: shape)
node.physicsBody?.mass = 2.0
node.physicsBody?.restitution = 0.3

施加力

node.physicsBody?.applyForce(SCNVector3(0, 10, 0), asImpulse: false) // 持续施力
node.physicsBody?.applyForce(SCNVector3(0, 5, 0), asImpulse: true)   // 瞬间冲量
node.physicsBody?.applyTorque(SCNVector4(0, 1, 0, 2), asImpulse: true)

碰撞检测

struct PhysicsCategory {
    static let player:     Int = 1 << 0
    static let enemy:      Int = 1 << 1
    static let ground:     Int = 1 << 2
}

playerNode.physicsBody?.categoryBitMask = PhysicsCategory.player
playerNode.physicsBody?.collisionBitMask = PhysicsCategory.ground | PhysicsCategory.enemy
playerNode.physicsBody?.contactTestBitMask = PhysicsCategory.enemy

scene.physicsWorld.contactDelegate = self

func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
    handleCollision(between: contact.nodeA, and: contact.nodeB)
}

重力

scene.physicsWorld.gravity = SCNVector3(0, -9.8, 0)
node.physicsBody?.isAffectedByGravity = false

粒子系统

SCNParticleSystem 可用于创建火焰、烟雾、雨滴和火花等特效。

let particles = SCNParticleSystem()
particles.birthRate = 100
particles.particleLifeSpan = 2
particles.particleSize = 0.1
particles.particleColor = .orange
particles.emitterShape = SCNSphere(radius: 0.5)
particles.particleVelocity = 2
particles.isAffectedByGravity = true
particles.blendMode = .additive

let emitterNode = SCNNode()
emitterNode.addParticleSystem(particles)
scene.rootNode.addChildNode(emitterNode)

使用 SCNParticleSystem(named: "fire.scnp", inDirectory: nil) 从 Xcode 粒子编辑器加载。粒子可通过 colliderNodes 与几何体发生碰撞。

加载模型

SceneKit 官方文档中支持的场景资源格式为 .scn.dae.abc。对于随 App 打包的资源,请将场景文件放在 .scnassets 文件夹中,将纹理贴图图片放在资源目录(asset catalogs)中,以便 Xcode 针对目标设备进行压缩优化。

USD/USDZ 是迁移到 RealityKit 的路线,并非 SceneKit 的默认加载路径。对于新项目、重大架构更新或 SCN 转换为 USD 的资源迁移,请转交 RealityKit skill 处理。

enum SceneAssetError: Error { case missingResource, missingNode(String) }

func loadCheckedScene() throws -> SCNScene {
    guard let url = Bundle.main.url(forResource: "model", withExtension: "dae")
    else { throw SceneAssetError.missingResource }

    let scene = try SCNScene(url: url, options: [.checkConsistency: true])
    guard scene.rootNode.childNode(withName: "mesh", recursively: true) != nil
    else { throw SceneAssetError.missingNode("mesh") }
    return scene
}

将上述逻辑作为资源创作/导入的检查关卡:遇到一致性校验失败或缺少必需节点时及时拦截并报错,修正源资源文件或导入参数后再重复校验。对于导出的 .scn 文件,需参考 场景序列化 规范,且必须在提交前确保导出成功并重新加载校验通过。

对于大型模型,可结合 .onDemand 按需加载策略使用 SCNReferenceNode。若需要在导入时转换单位,请使用 SCNSceneSource.LoadingOption

let source = SCNSceneSource(url: url, options: nil)!
let scene = try source.scene(options: [.convertUnitsToMeters: 1.0])

请勿使用 SCNScene.Attribute.unitUnitMetersPerUnitSCNScene.Attribute 仅包含元数据信息:.startTime.endTime.frameRate 以及 .upAxis

集成到 SwiftUI

使用 SceneView 在 SwiftUI 中嵌入 SceneKit:

import SwiftUI
import SceneKit

struct SceneKitView: View {
    let scene: SCNScene = {
        let scene = SCNScene()
        let sphere = SCNNode(geometry: SCNSphere(radius: 1))
        sphere.geometry?.firstMaterial?.lightingModel = .p

<!-- truncated for translation batch; full body continues in source -->