CAIM-Metal:03
Metalでパーティクルアニメーション
演習講師:渡邉 賢悟 (渡辺電気株式会社)
東京工科大学メディア学部
CAIM-Metalの準備
•
caimmetal03.zipをダウンロードして展開
03:1 毎フレームでの座標値更新
(アニメーションをつくる)
Metal演習(1-1) :パーティクル構造体の追加
import UIKit
// バッファID番号
let ID_VERTEX:Int = 0 let ID_PROJECTION:Int = 1 // 1頂点情報の構造体
struct VertexInfo : Initializable { var pos:Vec4 = Vec4()
var uv:Vec2 = Vec2()
var rgba:CAIMColor = CAIMColor() }
// パーティクル情報 struct Particle {
var pos:Vec2 = Vec2() // 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 pl_circle:CAIMMetalRenderPipeline?
// GPU:バッファ
private var mat_buf:CAIMMetalBuffer? // 行列バッファ
private var circle_quads_buf:CAIMMetalBuffer? // 頂点バッファ(円を描く四角形)
// CPU:形状メモリ
private var circle_quads = CAIMQuadrangles<VertexInfo>() // 円用メモリ
// パーティクル情報配列
private var circle_parts = [Particle]() // 円用パーティクル情報
…(続く)…
•
DrawingViewController.swift
にパーティクル用の構造体(struct Particle)をつくる
Metal演習(1-2) : パーティクル準備関数の作成と呼び出し
…(前ページからのつづき)…
// 円描画の準備関数
private func setupCircles() {
// シェーダを指定してパイプラインの作成
pl_circle = CAIMMetalRenderPipeline(vertname:"vert2d", fragname:"fragCircleCosCurve")
pl_circle?.blend_type = .alpha_blend
// (GPUバッファ)頂点バッファ(四角形)の作成
circle_quads_buf = CAIMMetalBuffer(vertice:circle_quads)
// パーティクル情報配列を作る
let wid:Float = Float(CAIMScreenPixel.width)
let hgt:Float = Float(CAIMScreenPixel.height)
for _ in 0 ..< 100 {
var p:Particle = Particle()
p.pos = Vec2(CAIMRandom(wid), CAIMRandom(hgt))
p.rgba = CAIMColor(R: CAIMRandom(), G: CAIMRandom(), B: CAIMRandom(), A: CAIMRandom())
p.radius = CAIMRandom(100.0)
p.life = CAIMRandom()
circle_parts.append(p)
}
}
override func setup() {
// (GPUバッファ)ピクセルプロジェクション行列バッファの作成(画面サイズに合わせる)
mat_buf
=
CAIMMetalBuffer
(
Matrix4x4
.
pixelProjection
(
CAIMScreenPixel
))
// 円描画の準備
setupCircles()
}
…(続く)…
•
パーティクル構造体配列(circle_parts)を準備する
setupCircles関数
を作成
•
シェーダはCAIM-Metal02のShader.metalで作った
vert2d
と
fragCircleCosCurve
をそのまま使う(後述)
•
従来のsetup関数
内でsetupCircles()を呼ぶ
Metal演習(1-3) : パーティクル情報の更新
…(前ページからのつづき)…
// 円情報の更新
private func updateCircles() {
// パーティクル情報の更新
let wid:
Float
=
Float
(
CAIMScreenPixel
.
width
)
let hgt:
Float
=
Float
(
CAIMScreenPixel
.
height
)
for i:
Int
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 = Vec2(CAIMRandom(wid), CAIMRandom(hgt))
circle_parts[i].rgba = CAIMColor(R: CAIMRandom(), G: CAIMRandom(), B: CAIMRandom(), A: CAIMRandom())
circle_parts[i].radius = CAIMRandom(100.0)
circle_parts[i].life = 1.0
}
}
}
…(続く)…
•
update関数内で以下の作業を行いたい
•
アニメーションさせるため、パーティクル情報(circle_parts)を更新
•
パーティクル情報から頂点CPUメモリの作成 (circle_parts → circle_quads)
•
頂点CPUメモリからGPUバッファへ転送 (circle_quads → circle_quads_buf)
•
更新したGPUバッファで画面描画
•
上記処理を小さな関数に分割し、整理する
•
パーティクルを更新する
updateCircles()関数
を作る
Metal演習(1-4) : CPUメモリ,GPUバッファの更新
…(前ページからのつづき)…// 円情報からCPUメモリの更新、GPUメモリに転送
private func genCirclesBuffer() {
// パーティクル配列からCPUメモリの作成(circle_parts -> circle_quads)
circle_quads.resize(count: circle_parts.count)
let p_circle_quads = circle_quads.pointer
for i:Int in 0 ..< circle_quads.count { // パーティクル情報を展開する
let p:Particle = circle_parts[i]
let x:Float = p.pos.x // x座標
let y:Float = p.pos.y // y座標
let r:Float = p.radius * (1.0 - p.life) // 半径(ライフが短いと半径が大きくなるようにする)
var rgba:CAIMColor = p.rgba // 色
rgba.A *= p.life // アルファ値の計算(ライフが短いと薄くなるようにする)
// 四角形頂点v0
p_circle_quads[i].v0.pos = Vec4(x-r, y-r, 0, 1) p_circle_quads[i].v0.uv = Vec2(-1.0, -1.0)
p_circle_quads[i].v0.rgba = rgba // 四角形頂点v1
p_circle_quads[i].v1.pos = Vec4(x+r, y-r, 0, 1) p_circle_quads[i].v1.uv = Vec2(1.0, -1.0)
p_circle_quads[i].v1.rgba = rgba // 四角形頂点v2
p_circle_quads[i].v2.pos = Vec4(x-r, y+r, 0, 1) p_circle_quads[i].v2.uv = Vec2(-1.0, 1.0)
p_circle_quads[i].v2.rgba = rgba // 四角形頂点v3
p_circle_quads[i].v3.pos = Vec4(x+r, y+r, 0, 1) p_circle_quads[i].v3.uv = Vec2(1.0, 1.0)
p_circle_quads[i].v3.rgba = rgba } // GPUバッファの内容を更新(circle_quads -> circle_quads_buf) circle_quads_buf?.update(vertice: circle_quads) } …(続く)…
•
円パーティクル情報(circle_parts)からCPUメモリ&GPUバッファを更新する
genCirclesBuffer()関数
を作成
•
Particle内のlife値
を使って、
半径(r)
と
アルファ値(rgba.A)
を変え、アニメーションを実現する
Metal演習(1-5) : 描画関数の作成・update関数の記述
…(前ページからのつづき)…
// 円の描画
private func drawCircles(renderer:CAIMMetalRenderer)
{
// パイプライン(シェーダ)の切り替え
renderer.
use
(
pl_circle
)
// 使用するバッファと番号をリンクする
renderer.
link
(
circle_quads_buf
!, to:.
vertex
, at:
ID_VERTEX
)
renderer.
link
(
mat_buf
!, to:.
vertex
, at:
ID_PROJECTION
)
// GPU描画実行(circle_quadsを渡すと四角形を描く)
renderer.
draw
(
circle_quads
)
}
// 繰り返し処理関数
override
func
update(renderer:
CAIMMetalRenderer
) {
// 円情報の更新
updateCircles()
// 円情報からGPUバッファを生成
genCirclesBuffer()
// 円の描画
drawCircles(renderer: renderer)
}
}
•
更新したGPUバッファを使って、円を描画する
drawCircles(renderer:)関数
を作成
Metal演習(1-6) : シェーダの準備
…(省略)…
// バッファID番号
constant int ID_VERTEX = 0;
constant int ID_PROJECTION = 1;
// 入力頂点情報 struct VertexIn { packed_float4 pos; packed_float2 uv; packed_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 vid [[vertex_id]])
{
VertexOut vout;
vout.pos = proj_matrix * float4(vin[vid].pos); vout.uv = vin[vid].uv;
vout.rgba = vin[vid].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距離の二乗が1.0以上 = 円の外 (discard_fragment()を呼ぶとピクセルが破棄される)
if(dist2 >= 1.0) { discard_fragment(); } // 新しい色情報をつくる
float4 rgba = vout.rgba;
rgba[3] = vout.rgba[3] * (1.0 + cos(M_PI_F * dist2)) / 2.0; return rgba;
}
Metal演習(1-7) : 結果
•
アニメーションする100個のパーティクルが描画される
03:2 複数パイプラインによる描画
(シェーダを切り替える)
Metal演習(2-1) :リング描画の追加
import
UIKit
…(省略)…
// CAIM-Metalを使うビューコントローラ
class
DrawingViewController :
CAIMMetalViewController
{
private
var
pl_circle:
CAIMMetalRenderPipeline
?
// 円用のパイプライン
private var pl_ring:CAIMMetalRenderPipeline?
// リング用のパイプライン
// GPU:バッファ
private
var
mat_buf:
CAIMMetalBuffer
?
// 行列バッファ
private
var
circle_quads_buf:
CAIMMetalBuffer
?
// 頂点バッファ(円を描く四角形)
private var ring_quads_buf:CAIMMetalBuffer?
// 頂点バッファ(リングを描く四角形)
// CPU:形状メモリ
private
var
circle_quads =
CAIMQuadrangles
<
VertexInfo
>()
// 円用メモリ
private var ring_quads = CAIMQuadrangles<VertexInfo>()
// リング用メモリ
// パーティクル情報配列
private
var
circle_parts = [
Particle
]()
// 円用パーティクル情報
private var ring_parts = [Particle]()
// リング用パーティクル情報
…(続く)…
•
DrawingViewController.swift
にパイプラインを円に追加してリングを描画できるようにする
•
パイプライン、GPUバッファ、CPUメモリ、パーティクル情報をそれぞれ追加する
Metal演習(2-2) : パーティクル準備関数の作成と呼び出し
…(前ページからのつづき)…
private
func
setupCircles() {
…
(省略)…
}
// リング描画の準備関数
private func setupRings() {
// リング用のパイプラインの作成
pl_ring =
CAIMMetalRenderPipeline
(vertname:
"vert2d"
, fragname:
"fragRing"
)
pl_ring?.
blend_type
= .
alpha_blend
// (GPUバッファ)頂点バッファ(四角形)の作成
ring_quads_buf = CAIMMetalBuffer(vertice:ring_quads)
// リング用のパーティクル情報配列を作る
let
wid:
Float
=
Float
(
CAIMScreenPixel
.
width
)
let
hgt:
Float
=
Float
(
CAIMScreenPixel
.
height
)
for
_
in
0
..<
100
{
var
p:
Particle
=
Particle
()
p.
pos
=
Vec2
(
CAIMRandom
(wid),
CAIMRandom
(hgt))
p.
rgba
=
CAIMColor
(R:
CAIMRandom
(), G:
CAIMRandom
(), B:
CAIMRandom
(), A:
CAIMRandom
())
p.
radius
=
CAIMRandom
(
100.0
)
p.
life
=
CAIMRandom
()
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
を使う(※フラグメントシェーダが異なるパイプラインになる)
•
setup関数
内でsetupCircles()とともに
setupRings()も呼び出す
Metal演習(2-3) : パーティクル情報の更新
…(前ページからのつづき)…
// リング情報の更新
private func updateRings()
{
// パーティクル情報の更新
let
wid:
Float
=
Float
(
CAIMScreenPixel
.
width
)
let
hgt:
Float
=
Float
(
CAIMScreenPixel
.
height
)
// リング用のパーティクル情報の更新
for
i:
Int
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 = Vec2(CAIMRandom(wid), CAIMRandom(hgt))
ring_parts[i].rgba = CAIMColor(R: CAIMRandom(), G: CAIMRandom(), B: CAIMRandom(), A: CAIMRandom())
ring_parts[i].radius = CAIMRandom(100.0)
ring_parts[i].life = 1.0
}
}
}
…(続く)…
•
update関数内でringの更新処理を加える
•
作り方は基本的にCircleと同じ。updateRings()関数を作成
•
ring_parts[i].lifeの値を少しずつ減らし、0になったとき新たなパーティクルを再度設定するしくみ
Metal演習(2-4) : CPUメモリ,GPUバッファの更新
…(前ページからのつづき)…// リング情報からCPUメモリの更新、GPUバッファへの転送
private func genRingsBuffer() {
// パーティクル配列からCPUメモリの作成(parts -> quads)
ring_quads.resize(count: ring_parts.count)
let p_ring_quads = ring_quads.pointer
for i:Int in 0 ..< ring_quads.count { // パーティクル情報を展開する
let p:Particle = ring_parts[i]
let x:Float = p.pos.x // x座標
let y:Float = p.pos.y // y座標
let r:Float = p.radius * (1.0 - p.life) // 半径(ライフが短いと半径が大きくなるようにする)
var rgba:CAIMColor = p.rgba // 色
rgba.A *= p.life // アルファ値の計算(ライフが短いと薄くなるようにする)
// 四角形頂点v0
p_ring_quads[i].v0.pos = Vec4(x-r, y-r, 0, 1) p_ring_quads[i].v0.uv = Vec2(-1.0, -1.0)
p_ring_quads[i].v0.rgba = rgba // 四角形頂点v1
p_ring_quads[i].v1.pos = Vec4(x+r, y-r, 0, 1) p_ring_quads[i].v1.uv = Vec2(1.0, -1.0)
p_ring_quads[i].v1.rgba = rgba // 四角形頂点v2
p_ring_quads[i].v2.pos = Vec4(x-r, y+r, 0, 1) p_ring_quads[i].v2.uv = Vec2(-1.0, 1.0)
p_ring_quads[i].v2.rgba = rgba // 四角形頂点v3
p_ring_quads[i].v3.pos = Vec4(x+r, y+r, 0, 1) p_ring_quads[i].v3.uv = Vec2(1.0, 1.0)
p_ring_quads[i].v3.rgba = rgba } // GPUバッファの内容を更新(quads -> quads_buf) ring_quads_buf?.update(vertice: ring_quads) } …(続く)…