scenekit

scenekit

熱門

維護與擴充現有的 SceneKit 3D 場景及視覺化效果。當處理 SCNView、SCNScene、SCNNode 場景圖、SceneKit 幾何體/材質/光源/攝影機、SCNAction 動畫、SCNPhysicsBody 物理效果、SCNParticleSystem 粒子效果、.scn/.dae/.abc 等 SceneKit 資產、Shader Modifier 或 SwiftUI SceneView 時使用。SceneKit 已進入軟性棄用與維護模式;請將新 App、重大更新、USD/USDZ 流程與遷移計畫轉向 RealityKit。

967星標
49分支
更新於 2026/7/31
SKILL.md
唯讀
名稱
scenekit
描述

維護與擴充現有的 SceneKit 3D 場景及視覺化效果。當處理 SCNView、SCNScene、SCNNode 場景圖、SceneKit 幾何體/材質/光源/攝影機、SCNAction 動畫、SCNPhysicsBody 物理效果、SCNParticleSystem 粒子效果、.scn/.dae/.abc 等 SceneKit 資產、Shader Modifier 或 SwiftUI SceneView 時使用。SceneKit 已進入軟性棄用與維護模式;請將新 App、重大更新、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 會啟用內建的旋轉(orbit)、平移(pan)與縮放(zoom)手勢。在需要自訂攝影機控制的正式上線環境中,通常會停用此設定。

建立 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

內建基元幾何體

SCNBoxSCNSphereSCNCylinderSCNConeSCNTorusSCNCapsuleSCNTubeSCNPlaneSCNFloorSCNTextSCNShape(擠壓的 Bezier 路徑)。

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 軸方向(-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                        // 視角角度(Degrees)
camera.zNear = 0.1
camera.zFar = 500
camera.automaticallyAdjustsZRange = true

// 正交投影 (Orthographic)
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 Bodies)

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
對於隨附資產,請將場景檔案放置於 .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 檔案,請參考 Scene Serialization,並要求在 Commit 提交前必須同時通過匯出成功與檢核載入。

針對大型模型,請使用設有 .onDemand 載入原則的 SCNReferenceNode。若需要在匯入時進行單位轉換,請使用 SCNSceneSource.LoadingOption

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

請勿使用 SCNScene.Attribute.unitUnitMetersPerUnitSCNScene.Attribute 僅屬於元資料(metadata):.startTime.endTime.frameRate 以及 .upAxis

整合 SwiftUI

SceneView 能將 SceneKit 嵌入至 SwiftUI 中:

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 -->