mcpField
S
scenekit

scenekit

github.com/dpearson2699开发1.2k3.1k 次安装13 周前更新
代码仓库

关于 scenekit

scenekit 是面向维护现有 SceneKit 场景的开发者的技能,涵盖场景构建、渲染、动画、物理和资源加载。内容侧重现有项目维护,并指出新项目及 USD/USDZ 流程应转向 RealityKit。

主要功能

  1. 01设置场景、节点与几何体
  2. 02配置材质、灯光与摄像机
  3. 03创建动作和显式动画
  4. 04设置物理体与碰撞
  5. 05制作粒子效果
  6. 06加载模型并集成 SwiftUI

适用场景

  1. 01维护使用 SceneKit 的现有应用
  2. 02为场景配置灯光、视角和材质
  3. 03为游戏场景添加动画与物理交互
  4. 04检查并加载 .scn 或 .dae 资源

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill scenekit

安装后在 Claude Code 中输入/scenekit确认已加载。

附带文件evals/evals.jsonreferences/scenekit-patterns.md

详细介绍

SceneKit

仅维护现有的 SceneKit 场景。Apple 在 WWDC 2025 上弃用了 SceneKit,并将其限制为维护用途;新项目、重大现代化改造和 USD/USDZ 工作流应转向 RealityKit。现有应用仍可继续运行。

目录

场景设置

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()                                          // Empty
guard let scene = SCNScene(named: "art.scnassets/ship.scn")     // .scn in .scnassets
    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 unit above parent
parentNode.addChildNode(childNode)
变换
node.position = SCNVector3(x: 0, y: 2, z: -5)
node.eulerAngles = SCNVector3(x: 0, y: .pi / 4, z: 0)  // 45-degree Y rotation
node.scale = SCNVector3(2, 2, 2)
node.simdPosition = SIMD3<Float>(0, 2, -5)  // Prefer simd for performance
内置图元

SCNBox、SCNSphere、SCNCylinder、SCNCone、SCNTorus、SCNCapsule、SCNTube、SCNPlane、SCNFloor、SCNText、SCNShape(拉伸的贝塞尔路径)。

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     // Solid color
material.diffuse.contents = UIImage(named: "brick") // Texture
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       // Scalar or texture
pbr.roughness.contents = 0.2       // Scalar or texture
pbr.normal.contents = UIImage(named: "normal")
pbr.ambientOcclusion.contents = UIImage(named: "ao")
光照模型

.physicallyBased(金属度/粗糙度)、.blinn(默认)、.phong、.lambert(仅漫反射)、.constant(不受光照影响)、.shadowOnly。

每个材质属性都是一个 SCNMaterialProperty,接受 UIColor、UIImage、CGFloat 标量、SKTexture、CALayer 或 AVPlayer。

透明度
material.transparency = 0.5
material.transparencyMode = .dualLayer
material.isDoubleSided = true

光照

将 SCNLight 附加到节点。光线方向与节点的负 Z 轴一致。

光源类型
// Ambient: uniform, no direction
let ambient = SCNLight()
ambient.type = .ambient
ambient.color = UIColor(white: 0.3, alpha: 1)

// Directional: parallel rays (sunlight)
let directional = SCNLight()
directional.type = .directional
directional.castsShadow = true

// Omni: point light, all directions
let omni = SCNLight()
omni.type = .omni
omni.attenuationEndDistance = 20

// Spot: cone-shaped
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)
类别位掩码
light.categoryBitMask = 1 << 1     // Category 2
node.categoryBitMask = 1 << 1      // Only lit by category-2 lights

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                        // Degrees
camera.zNear = 0.1
camera.zFar = 500
camera.automaticallyAdjustsZRange = true

// Orthographic
camera.usesOrthographicProjection = true
camera.orthographicScale = 10

景深(wantsDepthOfField、focusDistance、fStop)和 HDR 效果(wantsHDR、bloomIntensity、bloomThreshold、screenSpaceAmbientOcclusionIntensity)直接在 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]))

// Sequential
node.runAction(.sequence([.fadeOut(duration: 0.3), .removeFromParentNode()]))

// Infinite loop
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")

物理

物理体
node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)   // Forces + collisions
floor.physicsBody = SCNPhysicsBody(type: .static, shape: nil)    // Immovable
platform.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) // Code-driven

当 shape 为 nil 时,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) // Continuous
node.physicsBody?.applyForce(SCNVector3(0, 5, 0), asImpulse: true)   // Instant
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。 对于打包的资源,请将场景文件放在 .scnassets 文件夹中,并将纹理 图像放在资源目录中,以便 Xcode 针对目标设备优化它们。

USD/USDZ 是迁移到 RealityKit 的路径,而非默认的 SceneKit 加载 路径。对于新项目、重大更新或 SCN 到 USD 的资源转换, 请交由 RealityKit 技能处理。

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.unit 或 UnitMetersPerUnit。SCNScene.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 = .physicallyBased
        sphere.geometry?.firstMaterial?.diffuse.contents = UIColor.systemBlue
        sphere.geometry?.firstMaterial?.metalness.contents = 0.8
        scene.rootNode.addChildNode(sphere)
        return scene
    }()

    var body: some View {
        SceneView(scene: scene,
                  options: [.allowsCameraControl, .autoenablesDefaultLighting])
    }
}

选项:.allowsCameraControl、.autoenablesDefaultLighting、 .jitteringEnabled、.temporalAntialiasingEnabled。

如需控制渲染循环,请使用 UIViewRepresentable 包装 SCNView,并配合 SCNSceneRendererDelegate 协调器。请参阅 references/scenekit-patterns.md。

常见错误

未添加摄像机或灯光
// DON'T: Scene renders blank or black -- no camera, no lights
sceneView.scene = scene

// DO: Add camera + lights, or use convenience flags
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 5, 15)
scene.rootNode.addChildNode(cameraNode)
sceneView.pointOfView = cameraNode
sceneView.autoenablesDefaultLighting = true
物理形状使用精确几何体
// DON'T
node.physicsBody = SCNPhysicsBody(type: .dynamic,
    shape: SCNPhysicsShape(geometry: complexMesh, options: nil))

// DO: Simplified primitive
node.physicsBody = SCNPhysicsBody(type: .dynamic,
    shape: SCNPhysicsShape(
        geometry: SCNBox(width: 1, height: 2, length: 1, chamferRadius: 0),
        options: nil))
修改动态刚体的变换
// DON'T: Resets physics simulation
dynamicNode.position = SCNVector3(5, 0, 0)

// DO: Use forces/impulses
dynamicNode.physicsBody?.applyForce(SCNVector3(10, 0, 0), asImpulse: true)

审查清单

  • 场景至少包含一个设为 pointOfView 的摄像机节点
  • 场景具有适当的灯光(或在原型开发时使用 autoenablesDefaultLighting)
  • 物理形状使用简化几何体,而非完整的网格细节
  • 需要碰撞回调的刚体已设置 contactTestBitMask
  • SCNPhysicsContactDelegate 已分配给 scene.physicsWorld.contactDelegate
  • 通过力/冲量而非直接设置位置来更改动态刚体的变换
  • 每个节点的灯光数量不超过 8 个;点光源/聚光灯已设置 attenuationEndDistance
  • 材质使用 .physicallyBased 光照模型,以实现逼真渲染
  • SceneKit 资源使用文档列出的 .scn、.dae 或 .abc 场景源格式
  • 导入和导出的资源在提交前通过一致性和必需节点检查
  • 打包的 SceneKit 纹理/图像使用资源目录或经 Xcode 优化的资源
  • 场景元数据/导入选项使用文档规定的 API;不使用虚构的 SCNScene.Attribute.unit
  • 新的 USD/USDZ 流程或重大更新交由 RealityKit 处理
  • Game Center 身份验证、排行榜、成就或多人游戏交由 GameKit 处理
  • 大型模型使用 SCNReferenceNode 以启用延迟加载
  • 平衡粒子的 birthRate 和 particleLifeSpan 以控制粒子数量
  • 使用 categoryBitMask 将灯光和摄像机限定到相关节点
  • SwiftUI 场景使用 SceneView 或由 UIViewRepresentable 包装的 SCNView
  • 已了解弃用情况;针对新项目评估 RealityKit

参考资料

评论

支持 Markdown