GP15 PDF

GAME PROGRAMMING #15
ゲームプログラミング ( 第 15 回 )
講師 尾形 薫
スキニングの理解 CPU 編 (gp15-001)
スキニングを理解するために、どんな計算が行われているかを検証するプログラムを作ってみます。
ジョイント 1
右の図のように、2 つのジョイントを持つ 1 つのボーンと、このジョイ
ントによって変形する立方体を考えます。底部の頂点はジョイント 0 の
影響のみ、上部の頂点はジョイント 1 の影響のみ、中間の頂点は双方の
ジョイントの影響をうける、という状態です。
ここでジョイント1が回転すると、上部頂点と中間の頂点が引きずられ
てひねることになります。ではジョイント0が回転や移動するとどうな
るでしょうか?一般的にはジョイント0と1は親子関係が組まれるた
ジョイント 0
め、ジョイント0が動くとジョイント1も一緒に動くことになります。
ウェイト値
各ジョイントの回転状態は行列によって表現されます。頂点とジョイントの回転行列との間の関係は、ウェ
イト値という数値で表現できます。
例えば頂点 0 番は、ジョイント 0 に 1.0 のウェイト、ジョイント 1 に 0.0 のウェイトを持っているとします。
この場合、ジョイント 1 をどれだけ回転させても頂点 0 番は何の影響も受けません。もし頂点 5 番がジョ
イント 0 と 1 に対してそれぞれ 0.5 のウェイトを持っていれば、両方のジョイントの影響を均等に受けるこ
とになります。
頂点、ウェイト値、行列の関係
変形後の頂点の座標位置 Pnew は、元の座標位置 Porig と行列、ウェイト値により、
Pnew = Porig * (w0 * M0 + w1 * M1)
と記述できます。このとき、w0 + w1 = 1.0 となるべきことに注意して下さい。総和が 1.0 にならないとその
頂点はジョイントに追従せず、骨格の動きからはずれたような動きになってしまいます。
また、M1 と M0 の間には通常親子関係があるため、M1 は M0 を積算されたものになります。常に、子は親
の行列を掛けたものになります。
プログラム内部でのポリゴン生成
今回のプログラムでは FBX ファイルを使わずにポリゴンを表示しています。プログラム内部で頂点座標と
頂点の結び方を指定する方法を採っています。
インデックス
ポリゴンの基本は三角形です。3 頂点の情報があれば三角形を 1 枚描画できます。それでは 2 枚の場合は 6
頂点の情報が必要でしょうか?実際には 2 枚の三角形はくっついて四角形を構成する場合が多いでしょう。
この場合、2 枚の三角形の間でいくつかの頂点は共有されていると考えることができます。つまり、4 頂点
あれば 2 枚の三角形を描いて 1 枚の四角形のように見せることができるはずです。
この考え方を実現するため、三角形の頂点座標を直接指定して描画させるのではなく、頂点座標配列を用意
OGATA Kaoru 2012
page. 1
GAME PROGRAMMING #15
しておいて、その配列の位置を指定する、インデックス法を使います。
頂点配列
v0: xyz
v1: xyz
v2: xyz
v3: xyz
v1
インデックス配列
0
1
2
1
2
3
v3
t0
t1
t0
t1
v0
v2
描画を行う際にはまずインデックス配列から最初の三角形 t0 のための3つのデータを取り出します。頂点
配列からそれぞれの頂点座標位置を取得し、それぞれを頂点シェーダに送ります。さらにインデックス配列
から三角形 t1 のためのデータを3つ取り出し、と繰り返し処理をしていきます。
インデックス配列から 3 つずつずらしながら、順番に取り出していくこの方法はトライアングルリスト
(Triangle List) と呼ばれます
トライアングルストリップ
インデックス配列からのデータの取り出し方は三頂点ずつでした。上の図を見ると、v1 と v2 は二つの三角
形で共有されていて、かつ、連続して並んでいることがわかります。この事実をうまく使うことはできない
でしょうか?
トライアングルストリップ (Trianle Strip) と呼ぶ考え方では、インデックス配列からの
データの取り出し方を変えて、1つずつずらしながら三頂点のデータを取り出します。
ちょうど一筆書きで三角形を連続して描画していくような感じになります。
インデックス配列
t1
この方法では隣接する三角形が多いほどインデックス配列の大きさを減らすことができ
0
1
2
3
t0
ます。しかし一筆書きで複雑な形状を描くのは実は困難です。縮退三角形と呼ぶ面積がゼロの三角形を挟む
ことで一筆書きを維持する必要があります。
最近では GPU のメモリも増え処理速度も向上しているのでトライアングルリストでも十分でしょう。
プログラムの実行
マウス左ドラッグでカメラが回転します。1キーで下の行列(ジョイント0)を選択、2キーで上の行列(ジョ
イント1)を選択し、右ドラッグで行列を回転させます。行列に追従して頂点が変形すること確認します。
このとき、頂点を直接回転させているのではなく行列を操作していることに注意してください。
課題
・ウェイト値を操作し、例えば 0.5 を 0.8 にするとどうなるか、試してみよ。
・ジョイント間の親子関係を切った場合どのようになるか、試してみよ。
・頂点数を増やし、三角ポリゴンの数を増やしてみよ。
・Y 軸回転以外にも、X 軸回転ならどのように変形するか、試してみよ。
・ジョイントを増やすにはどうすればよいか考察し、実装してみよ。
・トライアングルストリップの方法で、立方体を描くにはどのようにインデックスを設定すればよいか。考
えて見よ。
OGATA Kaoru 2012
page. 2
GAME PROGRAMMING #15
スキニングの理解 GPU 編 (gp15-002)
実際のスキニング処理は大抵 GPU で行われます。これには以下のような理由があります:
・CPU変形では、
毎フレーム頂点データをGPUに送る必要がある
・GPUでのスキニング変形であれば、
事前に頂点データを送ることができる
・GPU処理の方が高速になりうる
GPU 処理を行うためには、シェーダにジョイント行列配列を渡す必要があります。また頂点シェーダに送
る頂点の情報も増やす必要があります。すなわち、各頂点ごとの「インフルエンス行列の番号」と「ウェ
イト値」です。
ジ ョ イ ン ト 行 列 配 列 を 渡 す の は 簡 単 で す。C# プ ロ グ ラ ム 側 か ら 通 常 の シ ェ ー ダ 変 数 と 同 様 に
EffectParameter を使って SetValue により配列を設定するだけです。
EffectParameter jointMatrix;
// "jointMatrix[]"
jointMatrix = effect.Parameters["jointMatrix"];
Matrix[] jointMtx;
jointMatrix = effect.Parameters["jointMatrix"];
// ジョイント行列をシェーダに送る
jointMatrix.SetValue(jointMtx);
では、各頂点ごとのインフルエンス行列の番号とウェイト値はどのようにして渡しましょう。そもそも、
どうやって頂点シェーダには頂点座標や UV、法線ベクトルが渡されているのでしょうか?これらの情報に、
ウェイト値を追加することはできるようでしょうか? もちろん可能です。
カスタム頂点構造体
XNA に は い く つ か の 頂 点 フ ォ ー マ ッ ト が 事 前 に 登 録 さ れ て い ま す。 頂 点 座 標 と 頂 点 カ ラ ー の
VertexPositionColor 構造体、テクスチャ UV を追加した VertexPositionColorTexture 構造体などです。これ
らの構造体には VertexDeclaration(頂点説明)が設定されており、構造体にどのような情報(座標、UV、
法線など)が含まれているかを頂点シェーダに与えることができます。
同じような構造体を自前で作成できれば、頂点シェーダに好きな情報を渡すことができるようになります。
このために、IVertexType インターフェースを継承した構造体を定義します。
struct VertexColor : IVertexType {
public Vector3 xyz;
public Color color;
public short b0, b1; // インフルエンス行列の番号
public float w0, w1; // 各行列への影響度
public VertexInfo(Vector3 xyz, Color color) { this.xyz = xyz; this.color = color; }
public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration (
new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
new VertexElement(sizeof(float)*3, VertexElementFormat.Color, VertexElementUsage.Color, 0)
new VertexElement(sizeof(byte)*4+..., VertexElementFormat.Short2, VertexElementUsage.BlendIndices, 0),
new VertexElement(sizeof(short)*2+..., VertexElementFormat.Vector2, VertexElementUsage.BlendWeight, 0)
);
VertexDeclaration IVertexType.VertexDeclaration { get { return VertexDeclaration; } }
};
VertexDeclaration は構造体でのデータの並び位置を説明するものです。頂点シェーダに渡したい各項目に
ついて、VertexElement により「どのメンバ変数を何のセマンティクスに対応させるか」を指定していきま
す。やや泥臭いですが、メンバ変数の指定には先頭からの byte 位置での指定になります。
この方式の利点は、構造体のメンバに「頂点シェーダに渡さないメンバ変数」があっても構わないという
ことです。
OGATA Kaoru 2012
page. 3
GAME PROGRAMMING #15
シェーダでスキニング
スキニングは頂点シェーダで行います。
基本の World View Projection 変換の前に
(つまりローカル空間)
、
ジョ
イント行列による変形処理を行います。
VertexOutput vertexColorSkinning (
float4 position: POSITION,
float4 color: COLOR0,
float4 index: BLENDINDICES,
float4 weight: BLENDWEIGHT
) {
float4x4 skinTransform = 0;
skinTransform += jointMatrix[index.x] * weight.x;
skinTransform += jointMatrix[index.y] * weight.y;
float4 pos = mul(position, skinTransform);
...
}
頂点シェーダにはセマンティクスの指定通りにデータが渡されます。構造体で float w0, w1 となっていた
メンバ変数はまとめて float4 weight に渡されており、それぞれ weight.x, weight.y でアクセスできます。
weight は float4 なので float2 つ分不足していますが自動的に調整されています。
頂点バッファとインデックスバッファ
GPU でのスキニングを行うということは、頂点配列とインデックス配列を CPU 側で操作することもない、
ということです。従って、これらの配列は事前に GPU 側(VRAM)に転送しておくことができます。これ
を実現するのが頂点バッファとインデックスバッファです。
VertexInfo[] vtx = new VertexInfo[] {
new VertexInfo(new Vector3(-20, -40, 0), Color.White, 1.0f, 0.0f),
.....
};
GraphicsDevice gd = graphics.GraphicsDevice;
// 頂点バッファとインデックスバッファを作成
VertexBuffer vbuffer = new VertexBuffer(gd, typeof(VertexInfo), vtx.Length, BufferUsage.None);
vbuffer.SetData(vtx);
IndexBuffer ibuffer = new IndexBuffer(gd, typeof(short), ix.Length, BufferUsage.None);
ibuffer.SetData(ix);
// ストリームにセットして、
描画
gd.SetVertexBuffer(vbuffer, 0);
gd.Indices = ibuffer;
// Trianle Stripで描画、
頂点数vtx.Length、
ポリゴン数4
gd.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0, vtx.Length, 0, 4);
このように、実際に描画する際にはどのバッファを使うかを指定するだけになります。
もし複数個のモデルを描画したい場合にはそれぞれ描画前バッファを設定することになります。
プログラムの入力と実行
gp15-001 と描画方法を変えただけで、操作方法は変わりません。
小課題
・頂点数を増やし、三角ポリゴンの数を増やしてみよ。
・ジョイントの数を3つに増やす場合、VertexDeclaration の設定がどうなるか考察せよ。さらに実装せよ。
OGATA Kaoru 2012
page. 4
GAME PROGRAMMING #15
シェーダ応用:トゥーン(gp15-003)
トゥーンタッチのレンダリングについて考えてみましょう。トゥーンタッチとは、アニメ調の面の塗り方で、
滑らかな面において、ある角度を堺に色が変わる、という塗り方です。
面 の 明 る さ が ど の よ う に 決 ま っ て い る か を 思 い 出 し ま し ょ う。
ライト
Lambert の余弦則により、面の明るさは法線方向 N とライトへの方
向 L のなす角度で決まります。シェーダ的には N と L の内積を求め
て計算します。
法線ベクトル N
ライトへの
方向ベクトル L
点P
この内積の値 (N.L) を用いて、ある一定値を堺に 1 と 0 に分けてし
まえばどうでしょう。
float diff = saturate(dot(lightDir, input.wnormal));
float3 diffColor = (diff > 0.4) ? 0.8 : 0.4;
ある角度で色が変わります。アニメ調になりました。
しかしこれでは 2 階調なので、もっと階調数を増やしたい場合どうしましょう。単純に IF 文を並べるので
は効率が悪いでしょう。また、エッジ部分を柔らかくしたいということになると計算が複雑になります。
そこで、閾値計算を止めてテクスチャを利用してみます。
都合がいいことに、saturate したあとの N.L は 0.0 〜 1.0 の範囲の値になっています。これはそのままテク
スチャの U 座標として使うことができます。その U 座標に相当する場所の色を、N.L の値の代わりに使います。
float diff = saturate(dot(lightDir, input.wnormal));
float4 lambert = tex2D(LambertTextureSampler, float2(diff, input.uv.y));
float3 diffColor = lambert.rgb * LightColor.rgb;
エッジ部分が柔らかくなりました。
このように、テクスチャは単にカラーテクスチャとして利用するだ
けでなく、パラメータ配列として用いることもできます。
小課題
・キー1と2でトゥーン効果の ON/OFF ができる。またキー2と3
は適用するテクスチャが異なる (lambertMap1.png と lambertMap2.
png)ので試してみよ。
・5階調で塗りつぶしをするようなテクスチャを作成せよ。
・テクスチャの V 座標の使い道について検討せよ。
OGATA Kaoru 2012
page. 5
GAME PROGRAMMING #15
gp15-001.cs
1 /// gp15-001-print.cs, スキニングの理解 CPUでスキニング
2 namespace gp15_001 {
3
public class Game1 : Microsoft.Xna.Framework.Game {
4
///_ 略
5
float[][] joint;
6
Matrix[] jointMtx;
7
// 頂点データを表す構造体
8
struct VertexInfo {
9
public Vector3 xyz;
10
public Color color;
11
public float w0, w1;
// 3つの行列への影響度
12
public VertexInfo(Vector3 xyz, Color color, float w0, float w1) {
13
this.xyz = xyz; this.color = color; this.w0 = w0; this.w1 = w1;
14
}
15
};
16
VertexInfo[] vtx = new VertexInfo[] {
17
new VertexInfo(new Vector3(-20, -40, 0), Color.White, 1.0f, 0.0f),
18
new VertexInfo(new Vector3(20, -40, 0), Color.Red, 1.0f, 0.0f),
19
///_ 略
20
};
21
// VertexInfoのままでは余計なデータがあって描画できないので、
22
// 描画時にはVertexPositionColor型にデータをコピーする。
23
VertexPositionColor[] vertexPos;
24
short[] vertexIndex = new short[] { 0, 1, 2, 3, 4, 5 };
25
26
protected override void LoadContent() {
27
///_ 略
28
// jointを初期化 { TX, TY, TZ, RX, RY, RZ }
29
joint = new float[][] {
30
new float[] {0, -40, 0, 0, 0, 0 },
31
new float[] {0, 40, 0, 0, 0, 0 },
32
};
33
jointMtx = new Matrix[2];
// Matrixは構造体なので個別にnewしなくてよい
34
jointMtx[0] = Matrix.Identity;
35
jointMtx[1] = Matrix.Identity;
36
37
// vpを初期化
38
vertexPos = new VertexPositionColor[vtx.Length];
39
for (int ii = 0; ii < vtx.Length; ii++) {
40
vertexPos[ii] = new VertexPositionColor();
41
vertexPos[ii].Position = vtx[ii].xyz;
42
vertexPos[ii].Color = vtx[ii].color;
43
}
44
}
45
protected override void Draw(GameTime gameTime) {
46
///_ 略
47
// 各行列を作成する。
ここでは回転のみを反映させる
48
for (int ii = 0; ii < 2; ii++) {
49
jointMtx[ii] = Matrix.CreateRotationZ(MathHelper.ToRadians(joint[ii][5]));
50
jointMtx[ii] *= Matrix.CreateRotationY(MathHelper.ToRadians(joint[ii][4]));
51
jointMtx[ii] *= Matrix.CreateRotationX(MathHelper.ToRadians(joint[ii][3]));
52
}
53
// コメントを外すと、
ジョイントが親子する
54
//jointMtx[1] *= jointMtx[0];
55
56
// 頂点を再計算する。
各行列へのウェイト値に基づいて頂点位置をTransformする
57
for (int ii = 0; ii < vtx.Length; ii++) {
58
// newPos = origPos * (w0 * M0 + w1 * M1)
59
Matrix j0 = jointMtx[0] * vtx[ii].w0;
60
Matrix j1 = jointMtx[1] * vtx[ii].w1;
61
vertexPos[ii].Position = Vector3.Transform(vtx[ii].xyz, (j0+j1));
62
}
63
// モデルデータを表示する
64
effect.CurrentTechnique.Passes[0].Apply();
65
gd.DrawUserIndexedPrimitives<VertexPositionColor>(PrimitiveType.TriangleStrip,
66
vertexPos, 0, vertexPos.Length, vertexIndex, 0, 4);
67
///_ 略
68
}
69
}
70 }
OGATA Kaoru 2012
page. 6
GAME PROGRAMMING #15
gp15-002.cs
1 /// gp15-002-print.cs, スキニングの理解 GPUでスキニング
2 namespace gp15_002 {
3
public class Game1 : Microsoft.Xna.Framework.Game {
4
///_ 略
5
EffectParameter jointMatrix;
// "jointMatrix[]"
6
// 頂点バッファとインデックスバッファ
7
VertexBuffer vbuffer;
8
IndexBuffer ibuffer;
9
float[][] joint;
10
Matrix[] jointMtx;
11
12
struct VertexInfo : IVertexType {
13
public Vector3 xyz;
14
public Color color;
15
public short b0, b1; // インフルエンス行列の番号
16
public float w0, w1; // 各行列への影響度
17
public VertexInfo(Vector3 xyz, Color color, float w0, float w1) {
18
this.xyz = xyz; this.color = color; this.w0 = w0; this.w1 = w1;
19
this.b0 = 0; this.b1 = 1;
20
}
21
public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration (
22
new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
23
new VertexElement(sizeof(float)*3, VertexElementFormat.Color, VertexElementUsage.Color, 0),
24
new VertexElement(sizeof(byte)*4+sizeof(float)*3, VertexElementFormat.Short2, VertexElementUsage.
BlendIndices, 0),
25
new VertexElement(sizeof(short)*2+sizeof(byte)*4+sizeof(float)*3, VertexElementFormat.Vector2,
VertexElementUsage.BlendWeight, 0)
26
);
27
VertexDeclaration IVertexType.VertexDeclaration { get { return VertexDeclaration; } }
28
};
29
30
VertexInfo[] vtx = new VertexInfo[] {
31
new VertexInfo(new Vector3(-20, -40, 0), Color.White, 1.0f, 0.0f),
32
new VertexInfo(new Vector3(20, -40, 0), Color.Red, 1.0f, 0.0f),
33
///_ 略
34
};
35
36
protected override void LoadContent() {
37
///_ 略
38
jointMatrix = effect.Parameters["jointMatrix"];
39
///_ 略
40
// 頂点バッファ、
インデックスバッファの作成
41
vbuffer = new VertexBuffer(graphics.GraphicsDevice, typeof(VertexInfo), vtx.Length, BufferUsage.None);
42
vbuffer.SetData(vtx);
43
ibuffer = new IndexBuffer(graphics.GraphicsDevice, typeof(short), ix.Length, BufferUsage.None);
44
ibuffer.SetData(ix);
45
}
46
47
protected override void Draw(GameTime gameTime) {
48
///_ 略
49
// ジョイント行列をシェーダに送る
50
jointMatrix.SetValue(jointMtx);
51
52
// モデルデータを表示する
53
effect.CurrentTechnique.Passes[0].Apply();
54
gd.SetVertexBuffer(vbuffer, 0);
55
gd.Indices = ibuffer;
56
gd.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0, vtx.Length, 0, 4);
57
///_ 略
58
}
59
}
60 }
OGATA Kaoru 2012
page. 7
GAME PROGRAMMING #15
colorSkinning.fx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// colorSkinning-complete.fx, effect sample
// OGATA Kaoru
// 下記の行列の値は、
プログラム側から与えられる
float4x4 world, view, projection;
float4x4 jointMatrix[2];
// スキニング用マトリクス
struct VertexOutput {
float4 position : POSITION;
float4 color: COLOR0;
};
VertexOutput vertexColorSkinning (
float4 position: POSITION,
float4 color: COLOR0,
float4 index: BLENDINDICES,
float4 weight: BLENDWEIGHT
) {
VertexOutput output;
float4x4 skinTransform = 0;
skinTransform += jointMatrix[index.x] * weight.x;
skinTransform += jointMatrix[index.y] * weight.y;
float4 pos = mul(position, skinTransform);
float4x4 wvp = mul(world, mul(view, projection));
output.position = mul(pos, wvp);
output.color = color;
return output;
}
// ピクセルシェーダを定義する
// 戻り値はCOLORセマンティクス
float4 pixelColor (
VertexOutput input
) : COLOR {
return input.color;
}
technique MyShader {
pass P0 {
VertexShader = compile vs_2_0 vertexColorSkinning();
PixelShader = compile ps_2_0 pixelColor();
}
}
OGATA Kaoru 2012
page. 8
GAME PROGRAMMING #15
LambertPS.fx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// LambertPS-print.fx, Pixel ShaderでLambert計算
struct VertexOutput {
float4 position : POSITION;
float4 color: COLOR0;
float2 uv : TEXCOORD0;
float4 wposition: TEXCOORD1;
float3 wnormal: TEXCOORD2;
};
VertexOutput MyVertexShader (
float4 position : POSITION,
float3 normal : NORMAL,
float2 uv : TEXCOORD0
) {
VertexOutput output;
output.wposition = mul(position, world);
float4x4 vp = mul(view, projection);
output.position = mul(output.wposition, vp);
output.wnormal = mul(normal, world);
output.color = float4(1, 1, 1, 1);
output.uv = uv;
return output;
}
float4 MyPixelShader(VertexOutput input) : COLOR {
float3 lightDir = normalize(lightPos.xyz - input.wposition.xyz);
float diff = saturate(dot(lightDir, input.wnormal));
float3 diffColor = diff * LightColor.rgb;
float4 color = float4(diffColor, 1);
color *= tex2D(ModelTextureSampler, input.uv);
return color;
}
texture lambertTexture;
sampler LambertTextureSampler =
Texture = <lambertTexture>;
AddressU = Mirror;
AddressV = Mirror;
};
sampler_state {
float4 MyPixelShaderLambertMap(VertexOutput input) : COLOR {
float3 lightDir = normalize(lightPos.xyz - input.wposition.xyz);
float diff = saturate(dot(lightDir, input.wnormal));
float4 lambert = tex2D(LambertTextureSampler, float2(diff, input.uv.y));
float3 diffColor = lambert.rgb * LightColor.rgb;
float4 color = float4(diffColor, 1);
color *= tex2D(ModelTextureSampler, input.uv);
return color;
}
technique MyShader {
pass P0 {
VertexShader = compile vs_2_0 MyVertexShader();
PixelShader = compile ps_2_0 MyPixelShader();
}
}
technique MyShaderLambertMap {
pass P0 {
VertexShader = compile vs_2_0 MyVertexShader();
PixelShader = compile ps_2_0 MyPixelShaderLambertMap();
}
}
OGATA Kaoru 2012
page. 9