ProcessingのコードをSwiftで写経する - その2: Solar System 3D
前回に引き続き、ProcessingのコードをSwiftで写経してみます。今回は前回の3D版です。成果物はこちら。
写経元は Coding Challenge #8 で解説されているProcessingのソースコードです。
ちなみに使用するフレームワークとしては前回SpriteKitをチョイスしたわけですが、今回は3DということでSceneKitを利用しました。
では以下で実装解説をしていきます。なお、文章はすべて無料で、サンプルコードのダウンロードだけ有料となっております。
プロジェクトの雛形
カメラやライトのセットアップは、XcodeのSceneKitテンプレート(GameテンプレートでTechnologyとしてSceneKitを選択)で生成されるそのままを利用しました。
SpriteKitの2D版コードの単純置き換えで済む部分
基本的にはアルゴリズムの「考え方」は同じなので、前回のSpriteKitを用いた実装の単純置き換えでSceneKitの3D実装に変えられる部分も多くあります。
・Planetクラスの親クラスを SKNode → SCNNode に
// 2D
class Planet: SKNod {
...
}
// 3D
class Planet: SCNNode {
...
}
・ノードの色
// 2D
shapeNode.fillColor = UIColor.white
// 3D
geometryNode.geometry?.firstMaterial?.diffuse.contents = UIColor.white
・addChild
// 2D
addChild(shapeNode)
// 3D
addChildNode(geometryNode)
などなど。
2Dの概念を3Dに置き換える必要がある部分
形状や位置、回転等を扱う場合は当然2Dで考えていたものを3D用に書き換える必要があります。
・「形状」を持つノードを SKShapeNode → SCNGeometryNode に
// 2D
private let shapeNode: SKShapeNode
// 3D
SCNSphereprivate let geometryNode: SCNNode
geometryNode = SCNNode(geometry: sphere)
// 2D(円/楕円)
let rect = CGRect(x: 0, y: 0, width: radius*2, height: radius*2)
shapeNode = SKShapeNode(ellipseIn: rect)
// 3D(球)
let sphere = SCNSphere(radius: radius)
geometryNode = SCNNode(geometry: sphere)
・「中心座標」の計算
2D版では太陽(恒星)を画面の中心に置いていたわけですが、3Dではそれを「シーンの中心」と考える必要があります。
// 2D
sun.position = CGPoint(x: size.width/2 - sunRadius,
y: size.height/2 - sunRadius)
// 3D
sun.position = SCNVector3Zero
要は「原点」になるので、計算としてはむしろ簡単になります。
・translation
変位させるベクトルが2次元から3次元になるわけですが、2Dの際はx方向にだけ変位させて後は回転させていたところが、3Dの場合はある3次元ベクトル分変位させます。どのように回転させるかについては後述。
// 2D
shapeNode.position = CGPoint(x: distance, y: 0)
// 3D
geometryNode.position = SCNVector3(x: v.x, y: v.y, z: v.z)
SceneKitでの毎フレームのレンダリングループ
Processingではdraw()関数、SpriteKitではSKSceneのupdate(_ :)メソッドがレンダリングループとなっていたわけですが、SceneKitではどうするのでしょうか。
SceneViewはSCNSceneRendererプロトコルに準拠しているので、SceneViewを管理しているクラスでSCNSceneRendererDelegateへの準拠を宣言し、
class ViewController: UIViewController, SCNSceneRendererDelegate
SceneViewのdelegateプロパティにオブジェクトをセットします。
guard let scnView = self.view as? SCNView else { fatalError() }
scnView.delegate = self
なお、この際、SceneViewのisPlayingにtrueをセットするのを忘れないでください。
scnView.isPlaying = true
これをセットしないとレンダリングループが回りません。
ランダムな3次元ベクトルを生成する
Processingで3次元ベクトルをあらわずPVectorに相当する型はSceneKitではSCNVector3になります。ここが弱冠面倒なところで、足りない機能が多くあり、なかなか単純な置き換えでは済みません。
たとえばあrandom3DというPVectorクラスのメソッドがありますが、SceneKitにはありません。
// Processing
v = PVector.random3D();
そこで、自前実装します。
// SceneKit
func randomVector3() -> SCNVector3 {
let rand = SCNVector3(random(min: -1.0, max: 1.0),
random(min: -1.0, max: 1.0),
random(min: -1.0, max: 1.0))
return rand.normalized
}
なお、ランダムな3次元ベクトルを求めるには色々なアプローチがあるようですが、こちらの記事を読みとりあえず上記のシンプルな方法でもよさそうと判断しました。
地軸の傾きと、それに沿った回転
ここの移植が個人的には今回の山場でした。(コードにコメントがないので、計算の意図を理解するのに時間がかかりました)
// Processing
PVector v2 = new PVector(1, 0, 1);
PVector p = v.cross(v2);
rotate(angle, p.x, p.y, p.z);
ランダム3次元ベクトルと、x-z平面上のベクトルとのクロス積を求め、それを地軸としています。y軸が傾いてない地軸なので、ランダムなベクトルとx-zベクトルのクロス積を求めることで「傾いた地軸」が表現できるわけですね。(※繰り返しますが独自解釈です。認識が間違ってたらすみません。)
クロス積はこちらを参考に、次のように実装しました。
extension SCNVector3 {
...
func crossProduct(_ vectorB:SCNVector3) -> SCNVector3 {
let computedX = (y * vectorB.z) - (z * vectorB.y)
let computedY = (z * vectorB.x) - (x * vectorB.z)
let computedZ = (x * vectorB.y) - (y * vectorB.x)
return SCNVector3(computedX, computedY, computedZ)
}
}
「傾いた地軸」の計算と、その周りに沿った回転はSceneKitでは次のように実装しました。
let v2 = SCNVector3(0, 1, 0) // x-z平面上のベクトル
let p = v.crossProduct(v2)
rotation = SCNVector4Make(p.x, p.y, p.z, angle)
今回の写経での学び
・Processingの3D表現も、SceneKitを利用すればある程度は簡単に写経できる
・SpriteKitで書いた2D表現を3D表現に置き換えるのも結構簡単
・3次元的に動かすのが自分はまだまだ引き出しが足りないようなので、こういう3D表現のコードを写経するのは大変学びがある。今後もどんどんやっていきたい
ソースコードのダウンロード
Xcode 9.3、iOS 11.3で動作確認しています。なお、基本的にトラブルシューティング等のサポートは行っていません。ご容赦ください。
ここから先は
¥ 100
最後まで読んでいただきありがとうございます!もし参考になる部分があれば、スキを押していただけると励みになります。 Twitterもフォローしていただけたら嬉しいです。 https://twitter.com/shu223/