在两点之间绘制 SceneKit 对象
Posted
技术标签:
【中文标题】在两点之间绘制 SceneKit 对象【英文标题】:Draw SceneKit object between two points 【发布时间】:2016-05-02 07:56:31 【问题描述】:在geometry side 方面取得了一些进展,我正着手将整个场景组合在一起。该场景有几十个对象,每个对象由一个边界立方体定义,其角由两个 SCNVector3(最初是两组 x、y、z)指定。
这是我目前所拥有的一个示例 - 它是一个 11 元素对数周期天线,就像 70 年代的老式电视天线一样。每条灰线都是一个“元素”,通常由铝棒制成。我使用了从 +ve 到 -ve Y 的 SCNCylinders,整个内容不到 100 行(SK 非常棒)。
问题是如果元素在 X 上不对称并且因此必须旋转 SCNCylinder 会发生什么情况。我找到了this example,但我无法理解细节......它似乎利用了球体是对称的这一事实,因此角度有点“消失”。
有没有人有一个通用的函数,它将获取两个 3D 点并返回适合设置节点的 eulerAngle 的 SCNVector3,或类似的解决方案?
【问题讨论】:
您找到解决方案了吗?谢谢! 恐怕运气不好。我转到了应用程序的其他部分。 【参考方案1】:上面提到的两种解决方案都非常有效,我可以为这个问题提供第三个解决方案。
//extension code starts
func normalizeVector(_ iv: SCNVector3) -> SCNVector3
let length = sqrt(iv.x * iv.x + iv.y * iv.y + iv.z * iv.z)
if length == 0
return SCNVector3(0.0, 0.0, 0.0)
return SCNVector3( iv.x / length, iv.y / length, iv.z / length)
extension SCNNode
func buildLineInTwoPointsWithRotation(from startPoint: SCNVector3,
to endPoint: SCNVector3,
radius: CGFloat,
color: UIColor) -> SCNNode
let w = SCNVector3(x: endPoint.x-startPoint.x,
y: endPoint.y-startPoint.y,
z: endPoint.z-startPoint.z)
let l = CGFloat(sqrt(w.x * w.x + w.y * w.y + w.z * w.z))
if l == 0.0
// two points together.
let sphere = SCNSphere(radius: radius)
sphere.firstMaterial?.diffuse.contents = color
self.geometry = sphere
self.position = startPoint
return self
let cyl = SCNCylinder(radius: radius, height: l)
cyl.firstMaterial?.diffuse.contents = color
self.geometry = cyl
//original vector of cylinder above 0,0,0
let ov = SCNVector3(0, l/2.0,0)
//target vector, in new coordination
let nv = SCNVector3((endPoint.x - startPoint.x)/2.0, (endPoint.y - startPoint.y)/2.0,
(endPoint.z-startPoint.z)/2.0)
// axis between two vector
let av = SCNVector3( (ov.x + nv.x)/2.0, (ov.y+nv.y)/2.0, (ov.z+nv.z)/2.0)
//normalized axis vector
let av_normalized = normalizeVector(av)
let q0 = Float(0.0) //cos(angel/2), angle is always 180 or M_PI
let q1 = Float(av_normalized.x) // x' * sin(angle/2)
let q2 = Float(av_normalized.y) // y' * sin(angle/2)
let q3 = Float(av_normalized.z) // z' * sin(angle/2)
let r_m11 = q0 * q0 + q1 * q1 - q2 * q2 - q3 * q3
let r_m12 = 2 * q1 * q2 + 2 * q0 * q3
let r_m13 = 2 * q1 * q3 - 2 * q0 * q2
let r_m21 = 2 * q1 * q2 - 2 * q0 * q3
let r_m22 = q0 * q0 - q1 * q1 + q2 * q2 - q3 * q3
let r_m23 = 2 * q2 * q3 + 2 * q0 * q1
let r_m31 = 2 * q1 * q3 + 2 * q0 * q2
let r_m32 = 2 * q2 * q3 - 2 * q0 * q1
let r_m33 = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3
self.transform.m11 = r_m11
self.transform.m12 = r_m12
self.transform.m13 = r_m13
self.transform.m14 = 0.0
self.transform.m21 = r_m21
self.transform.m22 = r_m22
self.transform.m23 = r_m23
self.transform.m24 = 0.0
self.transform.m31 = r_m31
self.transform.m32 = r_m32
self.transform.m33 = r_m33
self.transform.m34 = 0.0
self.transform.m41 = (startPoint.x + endPoint.x) / 2.0
self.transform.m42 = (startPoint.y + endPoint.y) / 2.0
self.transform.m43 = (startPoint.z + endPoint.z) / 2.0
self.transform.m44 = 1.0
return self
//extension ended.
//in your code, you can like this.
let twoPointsNode1 = SCNNode()
scene.rootNode.addChildNode(twoPointsNode1.buildLineInTwoPointsWithRotation(
from: SCNVector3(1,-1,3), to: SCNVector3( 7,11,7), radius: 0.2, color: .cyan))
//end
可以参考http://danceswithcode.net/engineeringnotes/quaternions/quaternions.html
顺便说一句,当您使用圆柱在上述 3 种方法中的两点之间画一条线时,您将得到相同的结果。但实际上,它们会有不同的法线。换句话说,如果你在两点之间使用盒子,盒子的边,除了顶部和底部,将面向与上述3种方法不同的方向。
如果您需要进一步解释,请告诉我。
【讨论】:
当我使用 ARImageTrackingConfiguration 而不是 ARWorldTrackingConfiguration 时,是否将线路放在固定位置?【参考方案2】:编辑:对于低于或等于 ios 11
我要告诉你一个好消息!您可以链接两个点并在此 Vector 上放置一个 SCNNode !
拿着这个,享受在两点之间画线吧!
class CylinderLine: SCNNode
init( parent: SCNNode,//Needed to add destination point of your line
v1: SCNVector3,//source
v2: SCNVector3,//destination
radius: CGFloat,//somes option for the cylinder
radSegmentCount: Int, //other option
color: UIColor )// color of your node object
super.init()
//Calcul the height of our line
let height = v1.distance(v2)
//set position to v1 coordonate
position = v1
//Create the second node to draw direction vector
let nodeV2 = SCNNode()
//define his position
nodeV2.position = v2
//add it to parent
parent.addChildNode(nodeV2)
//Align Z axis
let zAlign = SCNNode()
zAlign.eulerAngles.x = Float(M_PI_2)
//create our cylinder
let cyl = SCNCylinder(radius: radius, height: CGFloat(height))
cyl.radialSegmentCount = radSegmentCount
cyl.firstMaterial?.diffuse.contents = color
//Create node with cylinder
let nodeCyl = SCNNode(geometry: cyl )
nodeCyl.position.y = -height/2
zAlign.addChildNode(nodeCyl)
//Add it to child
addChildNode(zAlign)
//set contrainte direction to our vector
constraints = [SCNLookAtConstraint(target: nodeV2)]
override init()
super.init()
required init?(coder aDecoder: NSCoder)
super.init(coder: aDecoder)
private extension SCNVector3
func distance(receiver:SCNVector3) -> Float
let xd = receiver.x - self.x
let yd = receiver.y - self.y
let zd = receiver.z - self.z
let distance = Float(sqrt(xd * xd + yd * yd + zd * zd))
if (distance < 0)
return (distance * -1)
else
return (distance)
【讨论】:
您可以使用scene.rootNode.addChildNode() 将此节点添加到您的scenekit 视图中!我已经使用这个类来模拟两个原子之间的蛋白质连接。【参考方案3】:@maury-markowitz 的回答对我有用,这是它的最新 (Swift4) 版本。
对于在 Swift 中使用 SCNVector3
的任何人,我只能建议在代码中的某处添加 +-*/
运算符重载 (e.g. from here)。
extension SCNNode
static func lineNode(from: SCNVector3, to: SCNVector3, radius: CGFloat = 0.25) -> SCNNode
let vector = to - from
let height = vector.length()
let cylinder = SCNCylinder(radius: radius, height: CGFloat(height))
cylinder.radialSegmentCount = 4
let node = SCNNode(geometry: cylinder)
node.position = (to + from) / 2
node.eulerAngles = SCNVector3.lineEulerAngles(vector: vector)
return node
extension SCNVector3
static func lineEulerAngles(vector: SCNVector3) -> SCNVector3
let height = vector.length()
let lxz = sqrtf(vector.x * vector.x + vector.z * vector.z)
let pitchB = vector.y < 0 ? Float.pi - asinf(lxz/height) : asinf(lxz/height)
let pitch = vector.z == 0 ? pitchB : sign(vector.z) * pitchB
var yaw: Float = 0
if vector.x != 0 || vector.z != 0
let inner = vector.x / (height * sinf(pitch))
if inner > 1 || inner < -1
yaw = Float.pi / 2
else
yaw = asinf(inner)
return SCNVector3(CGFloat(pitch), CGFloat(yaw), 0)
【讨论】:
请注意,如果您正在使用 Swift 4(因此可能针对 iOS 11 / macOS 10.13 / 等),您可以更轻松地使用 SIMD 向量类型而不是SCNVector
,从而得到东西像免费的运算符重载(因为它们使用 CPU 内在函数而更快)。此外,SCNNode.look(at:)
等新的便利功能也可能是相关的。【参考方案4】:
为了另一种方法,我通过三角函数实现了这一点。这使得代码非常少。这是最终结果:
在我的例子中,节点总是放置在一个固定的平面上,该平面切割 Y 轴。
// Create Cylinder Geometry
let line = SCNCylinder(radius: 0.002, height: node1.distance(to: node2))
// Create Material
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
material.lightingModel = .phong
line.materials = [material]
// Create Cylinder(line) Node
let newLine = SCNNode()
newLine.geometry = line
newLine.position = posBetween(first: node1, second: node2)
// This is the change in x,y and z between node1 and node2
let dirVector = SCNVector3Make(node2.x - node1.x, node2.y - node1.y, node2.z - node1.z)
// Get Y rotation in radians
let yAngle = atan(dirVector.x / dirVector.z)
// Rotate cylinder node about X axis so cylinder is laying down
currentLine.eulerAngles.x = .pi / 2
// Rotate cylinder node about Y axis so cylinder is pointing to each node
currentLine.eulerAngles.y = yAngle
这是获取两个节点之间位置的函数,将它放在你的类中:
func posBetween(first: SCNVector3, second: SCNVector3) -> SCNVector3
return SCNVector3Make((first.x + second.x) / 2, (first.y + second.y) / 2, (first.z + second.z) / 2)
这是获取圆柱高度节点之间距离的扩展,将其放置在类之外的某个位置:
extension SCNVector3
func distance(to destination: SCNVector3) -> CGFloat
let dx = destination.x - x
let dy = destination.y - y
let dz = destination.z - z
return CGFloat(sqrt(dx*dx + dy*dy + dz*dz))
如果你没有像我这样的固定轴,那么你可以做额外的三角来使用这种方法。
【讨论】:
【参考方案5】:这是一个使用 simd 和四元数进行旋转的解决方案。我基于@Bersaelor 的答案扩展。
我使用这个推导 (https://***.com/a/1171995/6693924) 从两个向量创建四元数。希望这会有所帮助。
extension SCNNode
static func lineNode(from: simd_float3, to: simd_float3, radius : CGFloat = 0.25) -> SCNNode
let vector = to - from
let height = simd_length(vector)
//cylinder
let cylinder = SCNCylinder(radius: radius, height: CGFloat(height))
cylinder.firstMaterial?.diffuse.contents = UIColor.white
//line node
let lineNode = SCNNode(geometry: cylinder)
//adjust line position
let line_axis = simd_float3(0, height/2, 0)
lineNode.simdPosition = from + line_axis
let vector_cross = simd_cross(line_axis, vector)
let qw = simd_length(line_axis) * simd_length(vector) + simd_dot(line_axis, vector)
let q = simd_quatf(ix: vector_cross.x, iy: vector_cross.y, iz: vector_cross.z, r: qw).normalized
lineNode.simdRotate(by: q, aroundTarget: from)
return lineNode
【讨论】:
我需要使用 lineNode.simdRotate(by: q, aroundTarget: (to+from)/2) 但除此之外,这对我来说非常有效,而且非常简洁!谢谢!【参考方案6】:Sprout 的(哇,自动更正不允许我实际输入他的名字!)帖子确实是一个解决方案,但我在我的代码中实现了一个非常不同的解决方案。
我所做的是根据两端的 X、Y 和 Z 位置计算直线和两个端点的长度:
let w = SCNVector3(x: CGFloat(x2m-x1m), y: CGFloat(y2m-y1m), z: CGFloat(z2m-z1m))
let l = w.length()
长度只是 pythag。现在我创建一个SCNNode
来保存SCNCylinder
,并将它放在行的中间:
let node = SCNNode(geometry: cyl)
node.position = SCNVector3(x: CGFloat((x1m+x2m)/2.0), y: CGFloat((y1m+y2m)/2.0), z: CGFloat((z1m+z2m)/2.0))
现在是讨厌的部分,我们计算欧拉角并旋转节点:
let lxz = (Double(w.x)**2 + Double(w.z)**2)**0.5
var pitch, pitchB: Double
if w.y < 0
pitchB = M_PI - asin(Double(lxz)/Double(l))
else
pitchB = asin(Double(lxz)/Double(l))
if w.z == 0
pitch = pitchB
else
pitch = sign(Double(w.z)) * pitchB
var yaw: Double
if w.x == 0 && w.z == 0
yaw = 0
else
let inner = Double(w.x) / (Double(l) * sin (pitch))
if inner > 1
yaw = M_PI_2
else if inner < -1
yaw = M_PI_2
else
yaw = asin(inner)
node.eulerAngles = SCNVector3(CGFloat(pitch), CGFloat(yaw), 0)
我怀疑有一种更简单的方法可以使用其他旋转输入之一来执行此操作,但这很有效,而且工作是一项功能!
【讨论】:
【参考方案7】:在两个节点之间画线:
func generateLine( startPoint: SCNVector3, endPoint: SCNVector3) -> SCNGeometry
let vertices: [SCNVector3] = [startPoint, endPoint]
let data = NSData(bytes: vertices, length: MemoryLayout<SCNVector3>.size * vertices.count) as Data
let vertexSource = SCNGeometrySource(data: data,
semantic: .vertex,
vectorCount: vertices.count,
usesFloatComponents: true,
componentsPerVector: 3,
bytesPerComponent: MemoryLayout<Float>.size,
dataOffset: 0,
dataStride: MemoryLayout<SCNVector3>.stride)
let indices: [Int32] = [ 0, 1]
let indexData = NSData(bytes: indices, length: MemoryLayout<Int32>.size * indices.count) as Data
let element = SCNGeometryElement(data: indexData,
primitiveType: .line,
primitiveCount: indices.count/2,
bytesPerIndex: MemoryLayout<Int32>.size)
return SCNGeometry(sources: [vertexSource], elements: [element])
如何使用
let line = generateLine(startPoint: SCNVector3Make(1, 1, 1), endPoint: SCNVector3Make(8, 8, 8))
let lineNode = SCNNode(geometry: line)
lineNode.position = SCNVector3Make(15, 15, 10)
scene.rootNode.addChildNode(lineNode)
线条的粗细需要实现SCNSceneRendererDelegate,特别是:
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval)
glLineWidth(10)
【讨论】:
'glLineWidth' 已弃用:首先在 iOS 12.0 中弃用 - OpenGLES API 已弃用。【参考方案8】:Winchill 答案的 Objective-C 版本:
-(void)lineNodeFrom:(SCNVector3)to to:(SCNVector3)from radius:(float)radius
SCNVector3 w = SCNVector3Make(to.x - from.x, to.y - from.y, from.z - to.z);
float l = sqrtf(powf(w.x, 2) + powf(w.y, 2) + powf(w.z, 2.0f));
SCNCylinder * cylinder = [SCNCylinder cylinderWithRadius:radius height:l];
SCNMaterial * material = [SCNMaterial material];
material.diffuse.contents = [[UIColor darkGrayColor] colorWithAlphaComponent:0.75f];
cylinder.materials = @[material];
[self setGeometry:cylinder];
//original vector of cylinder above 0,0,0
SCNVector3 ov = SCNVector3Make(0, l/2.0,0);
//target vector, in new coordination
SCNVector3 nv = SCNVector3Make((from.x - to.x)/2.0, (from.y - to.y)/2.0, (from.z-to.z)/2.0);
// axis between two vector
SCNVector3 av = SCNVector3Make((ov.x + nv.x)/2.0, (ov.y+nv.y)/2.0, (ov.z+nv.z)/2.0);
//normalized axis vector
SCNVector3 av_normalized = [self normaliseVector:av];
float q0 = 0.0f; //cos(angel/2), angle is always 180 or M_PI
float q1 = av_normalized.x; // x' * sin(angle/2)
float q2 = av_normalized.y; // y' * sin(angle/2)
float q3 = av_normalized.z; // z' * sin(angle/2)
float r_m11 = q0 * q0 + q1 * q1 - q2 * q2 - q3 * q3;
float r_m12 = 2 * q1 * q2 + 2 * q0 * q3;
float r_m13 = 2 * q1 * q3 - 2 * q0 * q2;
float r_m21 = 2 * q1 * q2 - 2 * q0 * q3;
float r_m22 = q0 * q0 - q1 * q1 + q2 * q2 - q3 * q3;
float r_m23 = 2 * q2 * q3 + 2 * q0 * q1;
float r_m31 = 2 * q1 * q3 + 2 * q0 * q2;
float r_m32 = 2 * q2 * q3 - 2 * q0 * q1;
float r_m33 = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3;
SCNMatrix4 transform;
transform.m11 = r_m11;
transform.m12 = r_m12;
transform.m13 = r_m13;
transform.m14 = 0.0;
transform.m21 = r_m21;
transform.m22 = r_m22;
transform.m23 = r_m23;
transform.m24 = 0.0;
transform.m31 = r_m31;
transform.m32 = r_m32;
transform.m33 = r_m33;
transform.m34 = 0.0;
transform.m41 = (to.x + from.x) / 2.0;
transform.m42 = (to.y + from.y) / 2.0;
transform.m43 = (to.z + from.z) / 2.0;
transform.m44 = 1.0;
self.transform = transform;
-(SCNVector3)normaliseVector:(SCNVector3)iv
float length = sqrt(iv.x * iv.x + iv.y * iv.y + iv.z * iv.z);
if (length == 0)
return SCNVector3Make(0.0, 0.0, 0.0);
return SCNVector3Make(iv.x / length, iv.y / length, iv.z / length);
【讨论】:
以上是关于在两点之间绘制 SceneKit 对象的主要内容,如果未能解决你的问题,请参考以下文章
在 SceneKit/modelIO 中测量 3D-Obj 文件上两点之间的距离并显示它们
如何在 iOS 11 中将 SKVideoNode 添加到 ARKit-SceneKit?