如何在SceneKit中向Metal片元函数传入系统时间?
在SceneKit的Metal片元函数中获取系统时间的实现方案
我之前也碰到过这个需求,其实核心就是把SceneKit这边的时间数据作为uniform变量传递给Metal shader——因为Metal片元函数无法直接访问系统时间,必须由CPU端主动传递。下面是具体的实现步骤,亲测有效:
1. 修改Metal片元函数,添加时间uniform变量
首先在你的.metal文件里,给片元函数添加一个uniform float类型的时间参数,比如:
#include <metal_stdlib> #include <SceneKit/scn_metal> using namespace metal; struct VertexOut { float4 position [[position]]; float2 uv; }; // 声明时间uniform变量 uniform float time; fragment float4 customFragmentShader(VertexOut in [[stage_in]], texture2d<float> colorTexture [[texture(0)]], sampler colorSampler [[sampler(0)]]) { // 这里就可以使用time变量了,比如做一个随时间变化的颜色偏移 float4 baseColor = colorTexture.sample(colorSampler, in.uv); baseColor.r += sin(time) * 0.1; return baseColor; }
2. 在SceneKit代码中配置SCNProgram并传递时间
接下来在iOS/macOS的代码里,你需要:
- 初始化
SCNProgram并关联你的Metal文件 - 在每次渲染时,把当前系统时间传递给Metal的uniform变量
Swift示例:
import SceneKit import QuartzCore class ViewController: UIViewController, SCNSceneRendererDelegate { var scnView: SCNView! var customProgram: SCNProgram! // 记录初始时间,用于计算累计运行时间(可选,避免每次取当前时间重置) var startTime: TimeInterval = 0 override func viewDidLoad() { super.viewDidLoad() // 初始化SCNView scnView = SCNView(frame: view.bounds) view.addSubview(scnView) scnView.delegate = self // 加载自定义SCNProgram customProgram = SCNProgram() customProgram.vertexFunctionName = "customVertexShader" // 你的顶点函数名 customProgram.fragmentFunctionName = "customFragmentShader" // 对应Metal里的片元函数名 customProgram.isOpaque = false // 给场景中的节点设置这个program let cubeNode = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)) cubeNode.geometry?.program = customProgram scnView.scene = SCNScene(rootNode: cubeNode) // 记录初始时间 startTime = CACurrentMediaTime() } // 每次渲染前更新时间uniform func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { // 计算累计运行时间(也可以直接用传入的time参数,根据需求选择) let elapsedTime = time - startTime // 将时间传递给Metal的uniform变量 customProgram.setValue(elapsedTime, forKey: "time") } }
Objective-C示例:
#import <UIKit/UIKit.h> #import <SceneKit/SceneKit.h> #import <QuartzCore/QuartzCore.h> @interface ViewController : UIViewController <SCNSceneRendererDelegate> @property (nonatomic, strong) SCNView *scnView; @property (nonatomic, strong) SCNProgram *customProgram; @property (nonatomic, assign) CFTimeInterval startTime; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.scnView = [[SCNView alloc] initWithFrame:self.view.bounds]; [self.view addSubview:self.scnView]; self.scnView.delegate = self; self.customProgram = [[SCNProgram alloc] init]; self.customProgram.vertexFunctionName = @"customVertexShader"; self.customProgram.fragmentFunctionName = @"customFragmentShader"; self.customProgram.opaque = NO; SCNBox *cube = [SCNBox boxWithWidth:1 height:1 length:1 chamferRadius:0]; cube.program = self.customProgram; SCNNode *cubeNode = [SCNNode nodeWithGeometry:cube]; self.scnView.scene = [SCNScene sceneWithRootNode:cubeNode]; self.startTime = CACurrentMediaTime(); } - (void)renderer:(id<SCNSceneRenderer>)renderer updateAtTime:(CFTimeInterval)time { CFTimeInterval elapsedTime = time - self.startTime; [self.customProgram setValue:@(elapsedTime) forKey:@"time"]; } @end
3. 注意事项
- 确保Metal文件中uniform变量的名字(
time)和代码中setValue:forKey:的key完全一致,否则无法传递成功 - 如果你需要的是累计运行时间,建议用初始时间的差值,而不是直接用
CACurrentMediaTime(),这样不会因为App进入后台再前台而重置时间 SCNSceneRendererDelegate的renderer:updateAtTime:方法会在每一帧渲染前调用,正好适合更新这种需要实时变化的uniform变量
内容的提问来源于stack exchange,提问作者anton
相关产品推荐
相关产品推荐

