问题
If I have two points in SceneKit (e.g. (1,2,3)
and (-1,-1,-1)
). How do I draw a line between the two?
I see that there is a SCNBox object I may be able to use, but that only allows me to specify the center (e.g. via simdPosition
). The other ways to modify it are the transform (which I don't know how to use), or the Euler angles (which I'm not sure how to calculate which ones I need to use).
回答1:
Just build a custom geometry using SCNGeometryPrimitiveType.line
:
let vertices: [SCNVector3] = [
SCNVector3(1, 2, 3),
SCNVector3(-1, -1, -1)
]
let linesGeometry = SCNGeometry(
sources: [
SCNGeometrySource(vertices: vertices)
],
elements: [
SCNGeometryElement(
indices: [Int32]([0, 1]),
primitiveType: .line
)
]
)
let line = SCNNode(geometry: linesGeometry)
scene.rootNode.addChildNode(line)
回答2:
You can draw a line between two points using the following approach:
import SceneKit
extension SCNGeometry {
class func line(vector1: SCNVector3,
vector2: SCNVector3) -> SCNGeometry {
let sources = SCNGeometrySource(vertices: [vector1,
vector2])
let index: [Int32] = [0,1]
let elements = SCNGeometryElement(indices: index,
primitiveType: .line)
return SCNGeometry(sources: [sources],
elements: [elements])
}
}
...and then feed it to addLine
function in ViewController:
class ViewController: UIViewController {
// Some code...
func addLine(start: SCNVector3, end: SCNVector3) {
let lineGeo = SCNGeometry.line(vector1: start,
vector2: end)
let lineNode = SCNNode(geometry: lineGeo)
sceneView.scene.rootNode.addChildNode(lineNode)
}
}
As we all know line's width can't be changed (cause there's no property to do it), so you can use
cylinder
primitive geometry instead of aline
:
extension SCNGeometry {
class func cylinderLine(from: SCNVector3,
to: SCNVector3,
segments: Int) -> SCNNode {
let x1 = from.x
let x2 = to.x
let y1 = from.y
let y2 = to.y
let z1 = from.z
let z2 = to.z
let distance = sqrtf( (x2-x1) * (x2-x1) +
(y2-y1) * (y2-y1) +
(z2-z1) * (z2-z1) )
let cylinder = SCNCylinder(radius: 0.005,
height: CGFloat(distance))
cylinder.radialSegmentCount = segments
cylinder.firstMaterial?.diffuse.contents = UIColor.green
let lineNode = SCNNode(geometry: cylinder)
lineNode.position = SCNVector3(x: (from.x + to.x) / 2,
y: (from.y + to.y) / 2,
z: (from.z + to.z) / 2)
lineNode.eulerAngles = SCNVector3(Float.pi / 2,
acos((to.z-from.z)/distance),
atan2((to.y-from.y),(to.x-from.x)))
return lineNode
}
}
...then feed it the same way to ViewController:
class ViewController: UIViewController {
// Some code...
func addLine(start: SCNVector3, end: SCNVector3) {
let cylinderLineNode = SCNGeometry.cylinderLine(from: start,
to: end,
segments: 3)
sceneView.scene.rootNode.addChildNode(cylinderLineNode)
}
}
回答3:
First you'll need to calculate the heading and pitch between the two points. Full post is here and this answer explains how to do it between any arbitrary two points.
Once you have your two angles, if you attempt to use the Euler angles on an SCNBox, you'll notice that when you only modify the pitch (eulerAngles.x
), or only modify the heading (eulerAngles.y
), everything works fine. However, the moment you try to modify both, you'll run into issues. One solution is to wrap one node inside another.
This seemed like such a useful suggestion, that I create a handy wrapper node that should handle rotations upon all 3 axes:
import Foundation
import SceneKit
struct HeadingPitchBank {
let heading: Float
let pitch: Float
let bank: Float
/// returns the heading and pitch (bank is 0) represented by the vector
static func from(vector: simd_float3) -> HeadingPitchBank {
let heading = atan2f(vector.x, vector.z)
let pitch = atan2f(sqrt(vector.x*vector.x + vector.z*vector.z), vector.y) - Float.pi / 2.0
return HeadingPitchBank(heading: heading, pitch: pitch, bank: 0)
}
}
class HeadingPitchBankWrapper: SCNNode {
init(wrappedNode: SCNNode) {
headingNode = SCNNode()
pitchNode = SCNNode()
bankNode = SCNNode()
_wrappedNode = wrappedNode
super.init()
addChildNode(headingNode)
headingNode.addChildNode(pitchNode)
pitchNode.addChildNode(bankNode)
bankNode.addChildNode(wrappedNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var heading: Float {
get {
return headingNode.eulerAngles.y
}
set {
headingNode.eulerAngles.y = newValue
}
}
var pitch: Float {
get {
return pitchNode.eulerAngles.x
}
set {
pitchNode.eulerAngles.x = newValue
}
}
var bank: Float {
get {
return bankNode.eulerAngles.z
}
set {
bankNode.eulerAngles.z = newValue
}
}
var headingPitchBank: HeadingPitchBank {
get {
return HeadingPitchBank(heading: heading, pitch: pitch, bank: bank)
}
set {
heading = newValue.heading
pitch = newValue.pitch
bank = newValue.bank
}
}
var wrappedNode: SCNNode {
return _wrappedNode
}
private var headingNode: SCNNode
private var pitchNode: SCNNode
private var bankNode: SCNNode
private var _wrappedNode: SCNNode
}
You could then use this to easily draw a line between two points:
func createLine(start: simd_float3 = simd_float3(), end: simd_float3, color: UIColor, opacity: CGFloat? = nil, radius: CGFloat = 0.005) -> SCNNode {
let length = CGFloat(simd_length(end-start))
let box = SCNNode(geometry: SCNBox(width: radius, height: radius, length: length, chamferRadius: 0))
box.geometry!.firstMaterial!.diffuse.contents = color
let wrapper = HeadingPitchBankWrapper(wrappedNode: box)
wrapper.headingPitchBank = HeadingPitchBank.from(vector: end - start)
wrapper.simdPosition = midpoint(start, end)
if let opacity = opacity {
wrapper.opacity = opacity
}
return wrapper
}
来源:https://stackoverflow.com/questions/58470229/how-to-draw-a-line-between-two-points-in-scenekit