CAIM-Metal:03
Metalでパーティクルアニメーション
演習講師:渡邉 賢悟 (渡辺電気株式会社)
東京工科大学メディア学部
CAIM-Metalの準備
•
caimmetal03.zipをダウンロードして展開
03:1 毎フレームでの座標値更新
(アニメーションをつくる)
Metal演習(1-1) :パーティクル構造体の追加
import UIKit import simd // バッファID番号 let ID_VERTEX:Int = 0 let ID_PROJECTION:Int = 1 // 1頂点情報の構造体 struct Vertex {var pos:Float2 = Float2() var uv:Float2 = Float2() var rgba:Float4 = Float4() }
// パーティクル情報
struct Particle {
var pos:Float2 = Float2() // xy座標
var radius:Float = 0.0 // 半径
var rgba:CAIMColor = CAIMColor() // パーティクル色
var life:Float = 0.0 // パーティクルの生存係数(1.0~0.0)
}
// CAIM-Metalを使うビューコントローラ
class DrawingViewController : CAIMMetalViewController {
private var render_circle:CAIMMetalRenderer?
private var mat:Matrix4x4 = .identity // 変換行列
private var circles = CAIMQuadrangles<Vertex>(count: 100) // 円用4頂点メッシュ群
// パーティクル情報配列
private var circle_parts = [Particle]() // 円用パーティクル情報
•
DrawingViewController.swift
にパーティクル用の構造体(struct Particle)をつくる
Metal演習(1-2) : パーティクル準備関数の作成と呼び出し
…(前ページからのつづき)…
// 円描画の準備関数
private func setupCircles() {
// シェーダを指定してパイプラインレンダラの作成
render_circle = CAIMMetalRenderer(vertname:"vert2d", fragname:"fragCircleCosCurve") // アルファブレンドを有効にする
render_circle?.blendType = .alphaBlend
// デプスを無効にする
render_circle?.depthCompare = .always
render_circle?.depthEnabled = false
// 円のパーティクル情報配列を作る
let wid = Float(CAIM.screenPixel.width) let hgt = Float(CAIM.screenPixel.height) for _ in 0 ..< circles.count {
var p:Particle = Particle()
p.pos = Float2(CAIM.random(wid), CAIM.random(hgt))
p.rgba = CAIMColor(CAIM.random(), CAIM.random(), CAIM.random(), CAIM.random()) p.radius = CAIM.random(100.0) p.life = CAIM.random() circle_parts.append(p) } } // 準備関数
override func setup() {
// ピクセルプロジェクション行列バッファの作成(画面サイズに合わせる)
mat = Matrix4x4.pixelProjection(CAIM.screenPixel) // 円描画の準備
setupCircles() }
…(続く)…
•
パーティクル構造体配列(circle_parts)を準備する
setupCircles関数
を作成
•
シェーダはCAIM-Metal02のShader.metalで作った
vert2d
と
fragCircleCosCurve
をそのまま使う(後述)
Metal演習(1-3) : パーティクル情報の更新
…(前ページからのつづき)…
// 円のパーティクル情報の更新
private func updateCircles() { // パーティクル情報の更新
let wid = Float(CAIM.screenPixel.width) let hgt = Float(CAIM.screenPixel.height)
for i in 0 ..< circle_parts.count {
// パーティクルのライフを減らす(3秒間)
circle_parts[i].life -= 1.0 / (3.0 * 60.0)
// ライフが0以下になったら、新たなパーティクル情報を設定する
if(circle_parts[i].life <= 0.0) {
circle_parts[i].pos = Float2(CAIM.random(wid), CAIM.random(hgt))
circle_parts[i].rgba = CAIMColor(CAIM.random(), CAIM.random(), CAIM.random(), CAIM.random()) circle_parts[i].radius = CAIM.random(100.0) circle_parts[i].life = 1.0 } } } …(続く)…
•
update関数内で以下の作業を行いたい
•
アニメーションさせるため、パーティクル情報(circle_parts)を更新
•
パーティクル情報から頂点情報の作成 (circle_parts → circles)
•
頂点CPUメモリからGPUバッファへ転送 (circles → circles.metalBuffer)
•
GPUバッファで画面描画
•
上記処理を小さな関数に分割し、整理する
•
パーティクルを更新する
updateCircles()関数
を作る
Metal演習(1-4) : メッシュ情報の更新
…(前ページからのつづき)…
// 円のパーティクル情報から頂点メッシュ情報を更新
private func genCirclesMesh() {
for i in 0 ..< circles.count {
// パーティクル情報を展開して、メッシュ情報を作る材料にする
let p = circle_parts[i]
let x = p.pos.x // x座標
let y = p.pos.y // y座標
let r = p.radius * (1.0 - p.life) // 半径(ライフが短いと半径が大きくなるようにする)
var rgba = p.rgba // 色
rgba.A *= p.life // アルファ値の計算(ライフが短いと薄くなるようにする)
// 四角形メッシュi個目の頂点0
circles[i][0].pos = Float2(x-r, y-r) circles[i][0].uv = Float2(-1.0, -1.0) circles[i][0].rgba = rgba.float4
// 四角形メッシュi個目の頂点1
circles[i][1].pos = Float2(x+r, y-r) circles[i][1].uv = Float2(1.0, -1.0) circles[i][1].rgba = rgba.float4
// 四角形メッシュi個目の頂点2
circles[i][2].pos = Float2(x-r, y+r) circles[i][2].uv = Float2(-1.0, 1.0) circles[i][2].rgba = rgba.float4
// 四角形メッシュi個目の頂点3
circles[i][3].pos = Float2(x+r, y+r) circles[i][3].uv = Float2(1.0, 1.0) circles[i][3].rgba = rgba.float4 } } …(続く)…
•
円パーティクル情報(circle_parts)からメッシュ情報(circles)を更新する
genCirclesMesh()関数
を作成
•
Particle内のlife値
を使って、
半径(r)
と
アルファ値(rgba.A)
を変え、アニメーションを実現する
Metal演習(1-5) : 描画関数の作成・update関数の記述
…(前ページからのつづき)…
// 円の描画
private func drawCircles(on metalView:CAIMMetalView)
{
// パイプライン(シェーダ)の切り替え
render_circle?.beginDrawing(on: metalView)
// CPUメモリからGPUバッファを作成し、シェーダ番号をリンクする
render_circle?.link(circles.metalBuffer, to:.vertex, at:ID_VERTEX)
render_circle?.link(mat.metalBuffer, to:.vertex, at:ID_PROJECTION)
// GPU描画実行(circlesを渡すと四角形メッシュの中に丸を描く)
render_circle?.draw(circles)
}
// 繰り返し処理関数
override
func
update(metalView:
CAIMMetalView
) {
// 円情報の更新
updateCircles()
// 円情報で頂点メッシュ情報を更新
genCirclesMesh()
// 円の描画
drawCircles(on:metalView)
}
}
•
更新したメッシュ情報を使って、円を描画する
drawCircles(on metalView:)関数
を作成
Metal演習(1-6) : シェーダの準備
#include <metal_stdlib> using namespace metal;
// バッファID番号
constant int ID_VERTEX = 0;
constant int ID_PROJECTION = 1;
// 入力頂点情報 struct VertexIn { float2 pos; float2 uv; float4 rgba; }; // 出力頂点情報 struct VertexOut {
float4 pos [[position]]; float2 uv;
float4 rgba; };
// 頂点シェーダー(2Dピクセル座標系)
vertex VertexOut vert2d(device VertexIn *vin [[ buffer(ID_VERTEX) ]],
constant float4x4 &proj_matrix [[ buffer(ID_PROJECTION) ]], uint idx [[ vertex_id ]]) {
VertexOut vout;
// float2に、z=0,w=1を追加 → float4を作成し、行列を使って座標変換
vout.pos = proj_matrix * float4(vin[idx].pos, 0, 1); vout.uv = vin[idx].uv;
vout.rgba = vin[idx].rgba; return vout;
}
// フラグメントシェーダー(Cosカーブを使って滑らかな変化の円を描く)
fragment float4 fragCircleCosCurve(VertexOut vout [[ stage_in ]]) { // 中心からのuv距離の二乗
float dist2 = vout.uv[0] * vout.uv[0] + vout.uv[1] * vout.uv[1]; // uv距離
float dist = sqrt(dist2);
// uv距離が1.0以上 = 円の外 (discard_fragment()を呼ぶとピクセルが破棄される)
if(dist >= 1.0) { discard_fragment(); }
// cosを用いて新しいアルファをもつ色情報をつくる(rgba[3]=アルファ)
float4 rgba = vout.rgba;
rgba[3] = vout.rgba[3] * (1.0 + cos(M_PI_F * dist)) / 2.0; return rgba;
}
Metal演習(1-7) : 結果
•
アニメーションする100個のパーティクルが描画される
03:2 複数パイプラインによる描画
(シェーダを切り替える)
Metal演習(2-1) :リング描画の追加
import
UIKit
import
simd
…(省略)…
// CAIM-Metalを使うビューコントローラ
class
DrawingViewController :
CAIMMetalViewController
{
private
var
render_circle:
CAIMMetalRenderer
?
private var render_ring:CAIMMetalRenderer?
private
var
mat:
Matrix4x4
= .
identity
// 変換行列
private
var
circles =
CAIMQuadrangles
<
Vertex
>(count:
100
)
// 円用4頂点メッシュ群
private var rings = CAIMQuadrangles<Vertex>(count: 100)
// リング用4頂点メッシュ群
// パーティクル情報配列
private
var
circle_parts = [
Particle
]()
// 円用パーティクル情報
private var ring_parts = [Particle]()
// リング用パーティクル情報
…(続く)…
•
DrawingViewController.swift
にパイプラインを円に追加してリングを描画できるようにする
Metal演習(2-2) : パーティクル準備関数の作成と呼び出し
…(前ページからのつづき)…
private func setupCircles() {
…(省略)…
}
// リング描画の準備関数
private func setupRings() { // リング用のレンダラの作成
render_ring = CAIMMetalRenderer(vertname:"vert2d", fragname:"fragRing") // アルファブレンドを有効にする
render_ring?.blendType = .alphaBlend
// デプスを無効にする
render_ring?.depthCompare = .always
render_ring?.depthEnabled = false
// リング用のパーティクル情報を作る
let wid = Float(CAIM.screenPixel.width) let hgt = Float(CAIM.screenPixel.height) for _ in 0 ..< rings.count {
var p = Particle()
p.pos = Float2(CAIM.random(wid), CAIM.random(hgt))
p.rgba = CAIMColor(CAIM.random(), CAIM.random(), CAIM.random(), CAIM.random()) p.radius = CAIM.random(100.0)
p.life = CAIM.random()
ring_parts.append(p) }
}
override func setup() {
// (GPUバッファ)ピクセルプロジェクション行列バッファの作成(画面サイズに合わせる)
mat_buf = CAIMMetalBuffer(Matrix4x4.pixelProjection(CAIMScreenPixel)) // 円描画の準備 setupCircles() // リング描画の準備 setupRings() } …(続く)…
•
パーティクル構造体配列(rings_parts)を準備する
setupRings関数
を作成
•
シェーダはCAIM-Metal02のShader.metalで作った
vert2d
と
fragRing
を使う(※フラグメントシェーダが異なるパイプラインになる)
Metal演習(2-3) : パーティクル情報の更新
…(前ページからのつづき)…
// リングのパーティクル情報の更新
private func updateRings()
{
let
wid =
Float
(
CAIM
.
screenPixel
.
width
)
let
hgt =
Float
(
CAIM
.
screenPixel
.
height
)
// リング用のパーティクル情報の更新
for
i
in
0
..<
ring_parts.count
{
// パーティクルのライフを減らす(3秒間)
ring_parts[i].life -= 1.0 / (3.0 * 60.0)
// ライフが0以下になったら、新たなパーティクル情報を設定する
if(ring_parts[i].life <= 0.0) {
ring_parts[i].pos = Float2(CAIM.random(wid), CAIM.random(hgt))
ring_parts[i].rgba = CAIMColor(CAIM.random(), CAIM.random(), CAIM.random(), CAIM.random())
ring_parts[i].radius = CAIM.random(100.0)
ring_parts[i].life = 1.0
}
}
}
…(続く)…
•
update関数内でringの更新処理を加える
•
作り方は基本的にCircleと同じ。updateRings()関数を作成
•
ring_parts[i].lifeの値を少しずつ減らし、0になったとき新たなパーティクルを再度設定するしくみ
Metal演習(2-4) : メッシュ情報の更新
…(前ページからのつづき)…
// リングのパーティクル情報から頂点メッシュ情報を更新
private func genRingsMesh() { // リングの全ての点の情報を更新
for i in 0 ..< rings.count {
// パーティクル情報を展開して、メッシュ情報を作る材料にする
let p = ring_parts[i]
let x = p.pos.x // x座標
let y = p.pos.y // y座標
let r = p.radius * (1.0 - p.life) // 半径(ライフが短いと半径が大きくなるようにする)
var rgba = p.rgba // 色
rgba.A *= p.life // アルファ値の計算(ライフが短いと薄くなるようにする)
// 四角形メッシュi個目の頂点0
rings[i][0].pos = Float2(x-r, y-r) rings[i][0].uv = Float2(-1.0, -1.0) rings[i][0].rgba = rgba.float4
// 四角形メッシュi個目の頂点1
rings[i][1].pos = Float2(x+r, y-r) rings[i][1].uv = Float2(1.0, -1.0) rings[i][1].rgba = rgba.float4
// 四角形メッシュi個目の頂点2
rings[i][2].pos = Float2(x-r, y+r) rings[i][2].uv = Float2(-1.0, 1.0) rings[i][2].rgba = rgba.float4
// 四角形メッシュi個目の頂点3
rings[i][3].pos = Float2(x+r, y+r) rings[i][3].uv = Float2(1.0, 1.0) rings[i][3].rgba = rgba.float4 } } …(続く)…
•
Ringパーティクル情報(ring_parts)からメッシュ情報(rings)を更新する
genRingsMesh()関数
を作成
•
Particle内のlife変数の値を使って半径(r)とアルファ値(rgba.A)を変え、アニメーションを実現する
Metal演習(2-5) : 描画関数の作成・update関数の記述
…(前ページからのつづき)…
// リングの描画
private func drawRings(on metalView:CAIMMetalView) { // パイプライン(シェーダ)の切り替え
render_ring?.beginDrawing(on: metalView)
// CPUメモリからGPUバッファを作成し、シェーダ番号をリンクする
render_ring?.link(rings.metalBuffer, to:.vertex, at:ID_VERTEX) render_ring?.link(mat.metalBuffer, to:.vertex, at:ID_PROJECTION)
// GPU描画実行(ringsを渡すと四角形メッシュの中に丸を描く)
render_ring?.draw(rings)
}
// 繰り返し処理関数
override func update(metalView: CAIMMetalView) { // 円情報の更新
updateCircles()
// 円情報で頂点メッシュ情報を更新
genCirclesMesh() // 円の描画
drawCircles(on: metalView) // リング情報の更新 updateRings() // リング情報で頂点メッシュ情報を更新 genRingsMesh() // リングの描画 drawRings(on: metalView) } }