SwiftUI SceneViewによる3D表現
こんにちは。株式会社ZOZO 計測プラットフォーム開発本部 計測アプリ部 iOSブロックの@na9ainです。
みなさんは、SwiftUIで3Dモデルを使った表現をしたいと思ったことはありませんか?
iOS 14.0からSwiftUIではSceneView
というものが使えるようになり、手軽にSceneKitによる3D表現ができるようになりました。
この記事では、使用例やコードを交えながら、SceneView
の使い方を説明していきます。
SceneView
の使用例
早速、SceneView
を実際に使ってみた例から紹介します。
惑星の3Dモデルを、スワイプしたり、ピンチしたりすることで回転・拡大・移動させるビューを作ってみました。
SceneView
の使い方
複雑でとっつきづらい印象があるSceneKitですが、SceneView
の使い方は意外と簡単なんです。
例えば、このような数行のコードから
import SwiftUI
import SceneKit
struct ContentView: View {
var body: some View {
SceneView(
scene: SCNScene(named: "Earth.usdz"),
options: [.autoenablesDefaultLighting, .allowsCameraControl]
)
}
}
このようなビューができあがります。
上記のコードでは、scene: SCNScene(named: "Earth.usdz")
の部分でEarth.usdz
という3Dモデルのファイルをロードしたシーンを生成し、options: [.autoenablesDefaultLighting, .allowsCameraControl]
で3Dモデルの照明に関する設定と、ジェスチャによる視点の回転・拡大・移動を有効にしています。
SceneView
で実現できること
さて、数行のコードで3Dモデルをいじることができるビューができあがりましたが、SceneView
でできることをさらに掘り下げてみましょう。
SceneView
のイニシャライザがこちらになります。
init(
scene: SCNScene? = nil,
pointOfView: SCNNode? = nil,
options: SceneView.Options = [],
preferredFramesPerSecond: Int = 60,
antialiasingMode: SCNAntialiasingMode = .multisampling4X,
delegate: SCNSceneRendererDelegate? = nil,
technique: SCNTechnique? = nil
)
基本的には、これらの引数によって設定できる範囲が、 SceneView
でできる表現の範囲になります。
それぞれの引数について、簡単に説明していきます。
-
scene: SCNScene?
シーンを設定 -
pointOfView: SCNNode?
初期視点を設定 -
options: SceneView.Options
-
allowsCameraControl
ジェスチャによるシーン内のカメラ操作を有効にする設定 -
autoenablesDefaultLighting
デフォルト照明を有効にする設定(照明がないシーンではモデルが真っ暗なので、これを使うと便利) -
jitteringEnabled
視点をわずかにジッタリングすることでシーン内の3Dモデルのエッジを滑らかにする設定 -
rendersContinuously
シーンに変化があったときのみレンダリングする設定(バッテリー消費を抑えられる) -
temporalAntialiasingEnabled
一時的なアンチアイリアスを有効にする設定
-
-
preferredFramesPerSecond: Int
1秒あたりにレンダリングするフレーム数を設定 -
antialiasingMode: SCNAntialiasingMode
アンチエイリアスのモードを設定 -
delegate: SCNSceneRendererDelegate?
以下で説明 -
technique: SCNTechnique?
以下で説明
SCNSceneRendererDelegate
について
SCNSceneRendererDelegate
に準拠したクラスを用意し、デリゲートメソッドにお好みの実装することで表現の幅を広げられます。
例えば、簡単な例ですが、以下のようにデリゲートメソッドの中でレンダラーにSCNDebugOptions
を設定することで
import SceneKit
class SceneCoordinator: NSObject, SCNSceneRendererDelegate {
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
renderer.debugOptions = [.renderAsWireframe]
}
}
import SwiftUI
import SceneKit
struct ContentView: View {
var sceneCoordinator = SceneCoordinator()
var body: some View {
SceneView(
scene: SCNScene(named: "Earth.usdz"),
options: [.autoenablesDefaultLighting, .allowsCameraControl],
delegate: sceneCoordinator
)
}
}
3Dモデルのワイヤーフレームを表示させることができます。
SCNTechnique
について
SCNTechnique
では、レンタリングをカスタマイズすることができます。
具体的には、MetalかOpenGLのシェーダーとDeveloper Documentationで確認できる定義済みのKeyとValueを持つ辞書型のSCNTechniqueを用意し、引数として与えることでカスタマイズができます。
こちらの記事を参考に、MetalのシェーダーとSCNTechnique
を用意したところ、ファミコンを彷彿とさせるレトロな感じのレンダリングにカスタマイズすることができました。
SCNTechnique 実装例
- 以下の2ファイルを作成
#include <metal_stdlib>
using namespace metal;
#include <SceneKit/scn_metal>
struct custom_vertex_t
{
float4 position [[attribute(SCNVertexSemanticPosition)]];
};
struct out_vertex_t
{
float4 position [[position]];
float2 uv;
};
constexpr sampler s = sampler(coord::normalized,
address::repeat,
filter::nearest);
vertex out_vertex_t pixelate_pass_through_vertex(custom_vertex_t in [[stage_in]],
constant SCNSceneBuffer& scn_frame [[buffer(0)]])
{
out_vertex_t out;
out.position = in.position;
out.uv = float2((in.position.x + 1.0) * 0.5 , (in.position.y + 1.0) * -0.5);
return out;
};
fragment half4 pixelate_pass_through_fragment(out_vertex_t vert [[stage_in]],
texture2d<float, access::sample> colorSampler [[texture(0)]])
{
float4 fragment_color = colorSampler.sample( s, vert.uv);
return half4(fragment_color);
};
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>passes</key>
<dict>
<key>pixelate_scene</key>
<dict>
<key>draw</key>
<string>DRAW_SCENE</string>
<key>inputs</key>
<dict/>
<key>outputs</key>
<dict>
<key>color</key>
<string>color_scene</string>
</dict>
<key>colorStates</key>
<dict>
<key>clear</key>
<true/>
<key>clearColor</key>
<string>sceneBackground</string>
</dict>
</dict>
<key>resample_pixelation</key>
<dict>
<key>draw</key>
<string>DRAW_QUAD</string>
<key>program</key>
<string>doesntexist</string>
<key>metalVertexShader</key>
<string>pixelate_pass_through_vertex</string>
<key>metalFragmentShader</key>
<string>pixelate_pass_through_fragment</string>
<key>inputs</key>
<dict>
<key>colorSampler</key>
<string>color_scene</string>
</dict>
<key>outputs</key>
<dict>
<key>color</key>
<string>COLOR</string>
</dict>
</dict>
</dict>
<key>sequence</key>
<array>
<string>pixelate_scene</string>
<string>resample_pixelation</string>
</array>
<key>targets</key>
<dict>
<key>color_scene</key>
<dict>
<key>type</key>
<string>color</string>
<key>size</key>
<string>64x114</string>
</dict>
</dict>
<key>symbols</key>
<dict/>
</dict>
</plist>
-
sample.plist
からロードした[String: Any]
型の辞書をもとにイニシャライズした'SCNTechnique'をSceneView
の引数に渡す
import SwiftUI
import SceneKit
struct ContentView: View {
var body: some View {
SceneView(
scene: SCNScene(named: "Earth.usdz"),
options: [.autoenablesDefaultLighting, .allowsCameraControl],
technique: SCNTechnique(dictionary: techniqueDict!)
)
}
}
extension ContentView {
var techniqueDict: [String: Any]? {
if let path = Bundle.main.path(forResource: "sample", ofType: "plist"),
let dict = NSDictionary(contentsOfFile: path) as? [String : Any] {
return dict
}
return nil
}
}
SceneView
で実現できないこと
シンプルな実装ができる一方で、SCNView
にできても、SceneView
にはできないことがあります。
発見した中でも、使用するにあたって特に考慮すべきだと感じたものはこちらです。
- 背景色の変更
-
SCNView
のように.backgroundColor
から背景を透過させる、みたいなことができない
-
- カメラ操作設定の変更
-
SCNView
のように.cameraControlConfiguration
からカメラの操作感度などをカスタマイズすることができない
-
まとめ
とっつきづらい印象があるSceneKitですが、SceneView
を使えば簡単に3D表現を行うことができます。また、SCNSceneRendererDelegate
のデリゲートメソッドやSCNTechnique
から凝った表現をすることもできます。
みなさんもぜひ、SceneView
を使ってみてください。
Discussion