通常,单独动画顶点位置是一个棘手的问题。但是在 SceneKit 中有很好的方法来处理它。
GPU 确实希望在开始渲染帧之前将所有顶点数据上传到一个块中。这意味着,如果您不断地在 CPU 上计算新的顶点位置/法线/等,那么每次即使只有部分数据发生变化,您都会遇到将所有数据传输到 GPU 的问题。
因为您已经在数学上描述了您的表面,所以您可以在 GPU 本身上完成这项工作。如果每个顶点位置是某个变量的函数,您可以在着色器中编写该函数,并找到一种方法来传递每个顶点的输入变量。
您可以查看以下几个选项:
着色器修改器。从具有所需拓扑的虚拟几何开始(顶点数量以及它们如何连接为多边形)。将您的输入变量作为额外的纹理传递,并在您的着色器修改器代码(用于几何入口点)中,查找纹理,执行您的函数,并使用结果设置顶点位置。
金属计算着色器。创建一个由 Metal buffer 支持的几何源,然后在渲染时,将一个计算着色器排入队列,该着色器根据您的函数将顶点数据写入该缓冲区。(链接中有部分代码。)
更新:从您的评论看来,您可能处于更轻松的位置。
如果你所拥有的是由相对于自身是静态的并且相对于彼此移动的部分组成的几何体——就像魔方的立方体——在渲染时计算顶点是多余的。相反,您可以将几何体的静态部分上传到 GPU 一次,然后使用变换将它们相对于彼此定位。
在 SceneKit 中执行此操作的方法是创建单独的节点,每个节点都有自己的(静态)几何图形,然后设置节点变换(或位置/方向/比例)以相对于彼此移动节点。要一次移动多个节点,请使用节点层次结构——将其中的几个设为另一个节点的子节点。如果某个时刻需要一起移动,而稍后需要一起移动不同的子集,则可以更改层次结构。
这是魔方概念的一个具体例子。首先,创建一些立方体:
// convenience for creating solid color materials
func materialWithColor(color: NSColor) -> SCNMaterial {
let mat = SCNMaterial()
mat.diffuse.contents = color
mat.specular.contents = NSColor.whiteColor()
return mat
}
// create and arrange a 3x3x3 array of cubelets
var cubelets: [SCNNode] = []
for x in -1...1 {
for y in -1...1 {
for z in -1...1 {
let box = SCNBox()
box.chamferRadius = 0.1
box.materials = [
materialWithColor(NSColor.greenColor()),
materialWithColor(NSColor.redColor()),
materialWithColor(NSColor.blueColor()),
materialWithColor(NSColor.orangeColor()),
materialWithColor(NSColor.whiteColor()),
materialWithColor(NSColor.yellowColor()),
]
let node = SCNNode(geometry: box)
node.position = SCNVector3(x: CGFloat(x), y: CGFloat(y), z: CGFloat(z))
scene.rootNode.addChildNode(node)
cubelets += [node]
}
}
}
接下来,做一个旋转的过程。这是一个特定的旋转,但您可以将其概括为对立方体的任何子集进行任何变换的函数:
// create a temporary node for the rotation
let rotateNode = SCNNode()
scene.rootNode.addChildNode(rotateNode)
// grab the set of cubelets whose position is along the right face of the puzzle,
// and add them to the rotation node
let rightCubelets = cubelets.filter { node in
return abs(node.position.x - 1) < 0.001
}
rightCubelets.map { rotateNode.addChildNode($0) }
// animate a rotation
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(2)
rotateNode.eulerAngles.x += CGFloat(M_PI_2)
SCNTransaction.setCompletionBlock {
// after animating, remove the cubelets from the rotation node,
// and re-add them to the parent node with their transforms altered
rotateNode.enumerateChildNodesUsingBlock { cubelet, _ in
cubelet.transform = cubelet.worldTransform
cubelet.removeFromParentNode()
scene.rootNode.addChildNode(cubelet)
}
rotateNode.removeFromParentNode()
}
SCNTransaction.commit()
神奇的部分在动画之后的清理中。立方体从场景根节点的子节点开始,我们暂时将它们重新设置为另一个节点,以便我们可以将它们一起转换。在将它们再次返回为根节点的子节点时,我们将每个节点的本地transform
设置为其worldTransform
,以便它保持临时节点变换更改的效果。
然后,您可以重复此过程以获取(新)一组世界空间位置中的任何一组节点,并使用另一个临时节点来转换它们。
我不太确定你的问题有多像魔方,但听起来你可能可以从这样的事情中概括出一个解决方案。