#前置き
色々あって直線、曲線を引こうとしたが、最初やり方がわからなかったのでメモ。
UI/NSBezierPathをSCNShapeで表示すると、分厚くなったりして、「これ線じゃなくて箱じゃん」状態になったので、やりました。
この記事は、SCNVector3を使って3DなBezier曲線を描写するものです。
2DなUI/NSBezierPathをSCNShapeで表示とかではありません。
#やり方
概要
- 与えられたSCNVector3から曲線を構成する点の座標を求める。
- 点から曲線を近似する。
- SCNGeometorySource/Elementを使って表示する。
##SCNVector3を拡張
計算しやすいようにSCNVector3を拡張します
infix operator • // ⌥ + 8
infix operator × // かける[変換]
extension SCNVector3{
static func - (lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3{
return SCNVector3(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z)
}
static func + (lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3{
return SCNVector3(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z)
}
static func * (lhs: SCNVector3, rhs: CGFloat) -> SCNVector3{
return SCNVector3(lhs.x * rhs, lhs.y * rhs, lhs.z * rhs)
}
static func / (lhs: SCNVector3, rhs: CGFloat) -> SCNVector3{
return SCNVector3(lhs.x / rhs, lhs.y / rhs, lhs.z / rhs)
}
static func • (lhs: SCNVector3, rhs: SCNVector3) -> CGFloat{
return (lhs.x * rhs.x) + (lhs.y * rhs.y) + (lhs.z * rhs.z)
}
static func × (lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3{
return SCNVector3(
(lhs.y*rhs.z) - (lhs.z*rhs.y),
(lhs.z*rhs.x) - (lhs.x*rhs.z),
(lhs.x*rhs.y) - (lhs.y*rhs.x)
)
}
private var length²: CGFloat{
return (x*x) + (y*y) + (z*z)
}
var length: CGFloat{
return length².squareRoot()
}
var normalized: SCNVector3{
let length = self.length
return SCNVector3(x: x/length, y: y/length, z: z/length)
}
}
##SCNPathという、SCNVector3からBezier曲線を計算するクラスを作成する
いざ計算します。
class SCNPath{
private var current: SCNVector3 = SCNVector3(0, 0, 0)
var points: [SCNVector3] = [] //曲線を構成する点の座標を保存する
/// 始点を設定します。pointsは上書きされます。デフォルトでは(0, 0, 0)です。
func start(from point: SCNVector3) -> SCNPath{
current = point
points = [point]
return self
}
func addLine(to point: SCNVector3) -> SCNPath{
var rtn = [SCNVector3]()
points.append(current)
rtn.append(current)
current = point
return self
}
func addQuadCurve(to point: SCNVector3, control: SCNVector3) -> SCNPath{
var rtn = [SCNVector3]()
let n = Int((control - current).length + (point - control).length) * 12 //この係数を変えると、カクカク度と計算量が反比例して変化する。
for i in 0..<n{
let t = CGFloat(i) / CGFloat(n)
let q1 = current + (control - current) * t
let q2 = control + (point - control) * t
let r = q1 + (q2 - q1) * t
rtn.append(r)
}
points += rtn
current = point
return self
}
func addCurve(to point: SCNVector3, control1: SCNVector3, control2: SCNVector3) -> SCNPath{
var rtn = [SCNVector3]()
let n = Int((control1 - current).length + (control2 - control1).length + (point - control2).length) * 12 //この係数を変えると、カクカク度と計算量が反比例して変化する。
for i in 0..<n{
let t = CGFloat(i) / CGFloat(n)
let q1 = current + (control1 - current) * t
let q2 = control1 + (control2 - control1) * t
let q3 = control2 + (point - control2) * t
let r1 = q1 + (q2 - q1) * t
let r2 = q2 + (q3 - q2) * t
let s = r1 + (r2 - r1) * t
rtn.append(s)
}
points += rtn
current = point
return self
}
func end(){
points.append(current)
}
func close() -> SCNPath{
_ = addLine(to: self.points[0])
if let last = points.last, last == current{}else{
points.append(current)
}
current = self.points[0]
return self
}
}
extension SCNVector3: Equatable{
public static func == (lhs: SCNVector3, rhs: SCNVector3) -> Bool{
return (lhs.x == rhs.x) && (lhs.y == rhs.y) && (lhs.z == rhs.z)
}
}
##SCNLineという、線を描写するクラスを作成する。
SCNNodeを継承したSCNLineというクラスを作成して、このクラスを使ってNodeを作成します。
class SCNLine: SCNNode{
override init(){
super.init()
}
// Pathを指定してBezier曲線を引く
init(path: SCNPath){
super.init()
let source = SCNGeometrySource(vertices: path.points)
let indices: [UInt32] = {var rtn = [UInt32]();for i in 0..<path.points.count-1{rtn += [UInt32(i), UInt32(i+1)]};return rtn}()
let element = SCNGeometryElement(indices: indices, primitiveType: .line)
let geometry = SCNGeometry(sources: [source], elements: [element])
self.geometry = geometry
let material = SCNMaterial()
material.diffuse.contents = NSColor.white.cgColor
self.geometry!.insertMaterial(material, at: 0)
}
// オマケ 直線を引く
init(from : SCNVector3, to : SCNVector3){
super.init()
let source = SCNGeometrySource.init(vertices: [from, to])
let indices : [UInt8] = [0, 1]
let data = Data.init(bytes: indices)
let element = SCNGeometryElement.init(data: data, primitiveType: .line, primitiveCount: 1, bytesPerIndex: 1)
let geometry = SCNGeometry.init(sources: [source], elements: [element])
// super.init(geometry: geometry)
self.geometry = geometry
let material = SCNMaterial.init()
material.diffuse.contents = NSColor.white.cgColor
self.geometry!.insertMaterial(material, at: 0)
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
#実際に使ってみる
//見やすくするために
func v(_ x: CGFloat, _ y: CGFloat, _ z: CGFloat) -> SCNVector3{
return SCNVector3(x: x, y: y, z: z)
}
// Kotlin function
infix operator =>
public func => <T,U> (lhs: T, rhs: (T) throws -> U) rethrows -> U{
return try rhs(lhs)
}
//-----
let toz: CGFloat = -2.0 // toz < 0 の範囲で変えてみてください
let zpercurve = (toz)/4.0
let bezierCircle: CGFloat = CGFloat((sqrtf(2) - 1) * 4 / 3)
let taned = (zpercurve * bezierCircle) => {$0>=0 ? $0 : -1 * $0}
let path = SCNPath()
_ = path.start(from: v(0, 0, 1))
.addLine(to: v(0, 0, 0))
.addCurve(to: v(-1, 1, zpercurve), control1: v(-1 * bezierCircle, 0, 0), control2: v(-1, 1 - bezierCircle, zpercurve + taned))
.addCurve(to: v(0, 2, toz/2.0), control1: v(-1, 1 + bezierCircle, zpercurve - taned), control2: v(-1 * bezierCircle, 2, toz/2))
.addCurve(to: v(1, 1, zpercurve * 3.0), control1: v(bezierCircle, 2, toz/2), control2: v(1, 1 + bezierCircle, (zpercurve * 3.0) + taned))
.addCurve(to: v(0, 0, toz), control1: v(1, 1 - bezierCircle, (zpercurve * 3.0) - taned), control2: v(bezierCircle, 0, toz))
.addLine(to: v(0, 0, -5))
.end()
let lineNode = SCNLine(path: path) // このNodeをscene上で表示して見てください。
少しいじるだけで色々変わってしまいます。ミスるとぐにゃぐにゃのよくわからないものになります。気をつけましょう。
#コメント
3DでBezier曲線を使って例に挙げたような綺麗なぐるぐるを書こうとすると、一部のcontrolPointを傾けないといけないので、そこが難点でした。なかなかに計算を要するネタでした。
もしすでにあったようなネタだったとしても、これは自分にとっていい勉強になったと思います。
近似させてるだけなので、ズームアップするとカクカクしてます。そこは場合によって計算の精度をあげるとかしましょう。
(9/25)
SCNNodeなのでARKitでほぼそのまま表示できますが、ARKitで表示させるととんでもなく大きくなりました。プログラム上の1が、ARでの1メートルだそうです。ARで表示させるときは小さくしましょう。
Bezier以外にも媒介変数表示の式を計算して曲線を描画する方法などもあります。(宣伝)->Swift3,4 文字列を計算
#参考サイト
非常に参考にさせていただきました。
SceneKitでカスタムジオメトリの作り方+おまけ - Qiita
Swift で計算して Bezier 曲線を描く - Qiita
SceneKit で 線を描く line - なるたるなるなる