SceneKit是苹果为iOS、macOS和tvOS提供的高层3D图形框架。它把复杂的OpenGL或Metal渲染细节封装成场景图、节点、几何体、材质、灯光和相机等对象,开发者用Swift或Objective-C即可快速搭建3D界面。与SpriteKit面向2D相比,SceneKit更关注三维空间中的模型变换、光照计算和纹理采样。要构建一个基础但完整的3D应用,需要依次处理四件事:创建渲染视图与场景、设置光源与光照模型、为几何体配置材质纹理、实现相机视角控制。下面先从场景和几何体开始,逐步构建一个可交互的3D展示页面。

一、创建SCNView与基础几何体
SCNView是UIView的子类,负责将场景渲染到屏幕上。创建时可以直接使用Storyboard拖入一个UIView并修改类名为SCNView,也可以在代码中实例化并添加到视图层级。设置scene属性后,SCNView会自动建立渲染循环。通常还会设置allowsCameraControl为true来快速获得相机旋转和缩放手势,不过该属性会启用系统默认的相机控制,后面讲解自定义手势时再关闭。backgroundColor用于设置场景清屏颜色,默认可能为nil显示空白,建议明确指定为深色,以便突出几何体与光照效果。
SCNScene相当于容器,rootNode作为根节点。所有几何体、灯光、相机都是SCNNode,通过addChildNode挂载。几何体由SCNGeometry子类描述,如SCNBox、SCNSphere、SCNCylinder、SCNPlane等。几何体本身只定义形状和拓扑结构,外观由材质SCNMaterial决定。可以将几何体赋给节点,再把节点加入场景。注意坐标轴:SceneKit使用右手坐标系,默认相机放在z轴正方向看向原点。
下面的代码创建一个蓝色立方体,并加入相机和一个点光源,形成一个最小可运行的3D场景。
import SceneKit
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let sceneView = SCNView(frame: view.bounds)
sceneView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(sceneView)
let scene = SCNScene()
sceneView.scene = scene
sceneView.backgroundColor = UIColor.black
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
scene.rootNode.addChildNode(cameraNode)
let box = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.05)
box.firstMaterial?.diffuse.contents = UIColor.systemBlue
let boxNode = SCNNode(geometry: box)
scene.rootNode.addChildNode(boxNode)
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light?.type = .omni
lightNode.position = SCNVector3(x: 2, y: 3, z: 5)
scene.rootNode.addChildNode(lightNode)
}
}
二、光照模型与光源类型
SceneKit中SCNLight有四种常用类型:环境光ambient、点光源omni、平行光directional、聚光灯spot。环境光没有具体位置,均匀照亮所有面,适合补光;点光源从一个点向所有方向发射,随距离衰减,用来模拟灯泡;平行光所有光线平行,类似太阳,没有距离衰减;聚光灯形成锥形光斑,适合舞台或手电筒效果。每种光源可以单独设置颜色、强度(intensity)和温度(temperature,可选),其中温度用于模拟钨丝灯或日光等色彩倾向。
除了光源类型,几何体材质还有一个lightingModel属性,决定光照计算模型。常见的有constant(不受光,纯色)、lambert(只考虑漫反射)、blinn(漫反射加高光)、phong(类似blinn,高光算法略有差异)、physicallyBased(基于物理的PBR模型)。Blinn-Phong高光更集中,适合塑料或光滑表面;Lambert没有高光,适合墙面、布料;PBR结合粗糙度和金属度更真实,渲染开销也更高。当光源为平行光且材质为lambert时,立方体表面明暗对比由法线与光线方向决定,边缘较暗,中间较亮。
阴影效果需要光源的castsShadow设为true,并且节点也要设置castsShadow。阴影贴图的大小和模糊半径通过shadowRadius、shadowMapSize等属性调整。阴影在静态场景中代价不高,但过多动态阴影会增大GPU负担。下面代码配置一个平行光和一个点光源,让立方体同时接受主光与补光。
let directionalLightNode = SCNNode() directionalLightNode.light = SCNLight() directionalLightNode.light?.type = .directional directionalLightNode.light?.color = UIColor.white directionalLightNode.light?.intensity = 1000 directionalLightNode.eulerAngles = SCNVector3(x: -Float.pi / 4, y: 0, z: 0) directionalLightNode.light?.castsShadow = true directionalLightNode.light?.shadowRadius = 4 directionalLightNode.light?.shadowMapSize = CGSize(width: 1024, height: 1024) scene.rootNode.addChildNode(directionalLightNode) let fillLightNode = SCNNode() fillLightNode.light = SCNLight() fillLightNode.light?.type = .omni fillLightNode.light?.color = UIColor.systemBlue fillLightNode.light?.intensity = 300 fillLightNode.position = SCNVector3(x: -2, y: 1, z: 2) scene.rootNode.addChildNode(fillLightNode) box.firstMaterial?.lightingModel = .blinn boxNode.castsShadow = true
三、材质纹理与PBR属性
SCNMaterial的contents属性可以接受UIColor、UIImage、NSData以及纹理字符串等。diffuse.contents是基础颜色或漫反射贴图,normal.contents用于法线贴图,通过扰乱表面法线模拟凹凸细节,roughness.contents控制粗糙度,metalness.contents控制金属度,这两个属性主要对physicallyBased光照模型生效。如果使用图片,SceneKit会自动处理坐标映射,但要注意图片尺寸最好为2的幂,避免某些旧设备纹理采样问题。
将一张木纹图片设置为diffuse.contents后,几何体表面会显示纹理。配合roughness为0.2、metalness为0.0可以得到哑光木材质;把diffuse设为深灰色、metalness设为1.0、roughness设为0.3,再配合环境光和点光源,就能模拟抛光金属。还可以使用multiply.contents对基础颜色进行乘法混合,或使用emission.contents让材质自发光,这些属性在制作霓虹灯或显示器效果时很实用。
纹理的平铺模式由wrapS和wrapT控制,默认Clamp。如果要无缝平铺,设置为Mirror或Repeat,并调整contentsTransform缩放。以下代码演示加载一张纹理并配置PBR材质。
let material = SCNMaterial()
material.lightingModel = .physicallyBased
if let image = UIImage(named: "woodTexture") {
material.diffuse.contents = image
}
material.roughness.contents = 0.2
material.metalness.contents = 0.0
material.normal.contents = UIImage(named: "woodNormal")
material.diffuse.wrapS = .repeat
material.diffuse.wrapT = .repeat
let transform = SCNMatrix4MakeScale(4, 4, 1)
material.diffuse.contentsTransform = transform
box.firstMaterial = material
四、相机视角控制与自定义手势
SCNCamera作为节点的camera属性,可以配置fieldOfView视角大小(默认60度)、zNear和zFar裁剪面、是否正交投影等。相机节点的position和orientation决定观察位置和方向。默认情况下,如果没有设置相机,场景使用系统提供的默认相机,但无法控制视角。将相机节点放到z轴正方向并指向原点,可以观察场景中心。
SCNLookAtConstraint可以让节点永远朝向另一个节点,适合实现相机跟随或轨道观察。设置约束后,改变相机位置时相机会自动旋转朝向目标。配合UIPanGestureRecognizer,把手指拖动映射为相机绕y轴和x轴的角度,再用UIPinchGestureRecognizer调整相机到目标的距离,就实现了常见的3D查看器交互。如果只想快速原型,可以开启sceneView.allowsCameraControl = true;系统会提供单指旋转、双指缩放和平移。但这会覆盖自定义手势,且相机行为固定。对于正式应用,建议关闭该属性,自行处理手势。
下面给出一个简单的旋转与缩放手势实现:在viewDidLoad中添加手势,在回调中更新cameraNode的位置,并依赖SCNLookAtConstraint保持朝向。为了保持画面稳定,通常将相机放在以目标为圆心的球面上。
let targetNode = SCNNode()
targetNode.position = SCNVector3Zero
scene.rootNode.addChildNode(targetNode)
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
let lookAt = SCNLookAtConstraint(target: targetNode)
cameraNode.constraints = [lookAt]
scene.rootNode.addChildNode(cameraNode)
var rotationAngle: Float = 0
var distance: Float = 5
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
sceneView.addGestureRecognizer(panGesture)
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
sceneView.addGestureRecognizer(pinchGesture)
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: sceneView)
rotationAngle += Float(translation.x) * 0.01
updateCameraPosition()
gesture.setTranslation(.zero, in: sceneView)
}
@objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
distance /= Float(gesture.scale)
distance = max(2, min(distance, 20))
gesture.scale = 1
updateCameraPosition()
}
func updateCameraPosition() {
let x = distance * sin(rotationAngle)
let z = distance * cos(rotationAngle)
cameraNode.position = SCNVector3(x: x, y: 2, z: z)
}
通过以上四个部分的配置,一个基础但可扩展的SceneKit 3D应用就搭建完成。接下来可以根据实际需求调整几何体复杂度、光源细节和材质贴图,逐步增加更丰富的交互与动画效果。