WPF3Dで影
WFPの壁を破ろうシリーズ第6段?
WPF 3Dで平面投影シャドウを作ってみましょう。平面投影シャドウというのは3Dのジオメトリをペシャンコにして地面の上に配置する影です。影ですが、れっきとしたジオメトリ オブジェクトです。
実は、XZ平面(Z=0)に落とされた影を作ることはそれほど難しくありません。ジオメトリに次の行列(列優先)を適用するだけです。ここで l は光源の位置です。
ly -lx 0 0
0 0 0 0
0 -lz ly 0
0 -1 0 ly
任意の平面への影も、もう少し複雑な4x4の行列を適用するだけです。詳しくはReal-Time RenderingなどのCGの教科書を参照してください。
まず、次のように、シーン内にXAMLでティーポットを2つ配置します。もちろん一方は影なので色を黒色にします。
<ModelVisual3D x:Name="myScene">
<ModelVisual3D x:Name="myShadow">
<ModelVisual3D.Content>
<GeometryModel3D Geometry="{StaticResource myTeapot}">
<GeometryModel3D.Material>
<DiffuseMaterial Brush="Black" />
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial Brush="Black" />
</GeometryModel3D.BackMaterial>
<GeometryModel3D.Transform>
<MatrixTransform3D>
<MatrixTransform3D.Matrix>
<Matrix3D M11 ="10" M33="10" M44="10" M24="-1"/>
</MatrixTransform3D.Matrix>
</MatrixTransform3D>
</GeometryModel3D.Transform>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D x:Name="myTeapot">
<ModelVisual3D.Transform>
<MatrixTransform3D x:Name="myGeometryMatrix3D">
<MatrixTransform3D.Matrix>
<Matrix3D OffsetY="2" />
</MatrixTransform3D.Matrix>
</MatrixTransform3D>
</ModelVisual3D.Transform>
<ModelVisual3D.Content>
<GeometryModel3D x:Name="myGeometry"
Geometry="{StaticResource myTeapot}" >
<GeometryModel3D.Material>
<MaterialGroup x:Name="myMaterialGroup">
<DiffuseMaterial Brush="White" />
</MaterialGroup>
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
コードでは、光源位置やティーポットの位置や向きが変更されたとき、先ほどの座用変換を適用するようにコールバックを作成します。
private void OnGeometrySliderValueChanged
(object sender, RoutedEventArgs e)
{
if (this.IsLoaded)
{
myGeometryMatrix3D.Matrix = GeometryMatrix
(myAngleX.Value, myAngleY.Value, myAngleZ.Value,
myOffsetX.Value, myOffsetY.Value, myOffsetZ.Value);
CreateShadow();
}
}
private void PointValueChanged
(object sender, RoutedEventArgs e)
{
if (this.IsLoaded)
{
Transform3DGroup tg = new Transform3DGroup();
tg.Children.Add(new ScaleTransform3D(0.1, 0.1, 0.1));
tg.Children.Add(new TranslateTransform3D
(PointLightPositionX.Value,
PointLightPositionY.Value,
PointLightPositionZ.Value));
mySphere.Transform = tg;
myPointLight.Position =
new Point3D(PointLightPositionX.Value,
PointLightPositionY.Value,
PointLightPositionZ.Value);
CreateShadow();
}
}
private void CreateShadow()
{
Matrix3D mat = new Matrix3D();
Point3D l = myPointLight.Position;
Vector3D n = planeNormal;
double d = planeDepth;
mat.M11=l.Y; mat.M21=-l.X; mat.M31=0.0; mat.OffsetX=0.0;
mat.M12=0.0; mat.M22=0.0; mat.M32=0.0; mat.OffsetY=0.0;
mat.M13=0.0; mat.M23=-l.Z; mat.M33=l.Y; mat.OffsetZ=0.0;
mat.M14=0.0; mat.M24=-1.0; mat.M34=0.0; mat.M44=l.Y;
myShadow.Content.Transform =
new MatrixTransform3D
(Matrix3D.Multiply(myGeometryMatrix3D.Value, mat));
}
この影にはいくつかの制限があります。まず、平面にしか影を落とせません。でこぼこした地形や他のジオメトリには使えません(ペシャンコにしたジオメトリなので当然ですが...)。影が落ちる平面とおなじZ値を持つと、影と平面のどちらが上になるかが不定になり、いわゆるZファイティングが発生します。影か地形に少しオフセットを設定して必ず影が上にくるようにする必要があります。
RenderToBitmapを使って画像を生成すれば、WPFでも「シャドウ テクスチャ」くらいはできそうな気がします。シャドウテクスチャは地形に影のテクスチャを適用するテクニックなので、でこぼこした地形にも適用できます。さすがに、ピクセル単位で光源からの距離の比較を行う「シャドウマッピング」はWPFでは難しそうです。
マイクロソフト㈱エバンジェリスト。北海道大学理学部物理学科卒。リアルタイム3Dグラフィックスを専門とし、グラフィックスやシェーダに関する技術文章を執筆・講演。 DirectX SDK日本語ドキュメントの開発に携わるとともに、Windows Presentation Foundation プログラミング(オーム社)、Game Programming Gemsシリーズ、リアルタイム レンダリング第2版(ボーンデジタル)、Texturing & Modeling, A Procedural Approach などを翻訳・監修、XAMLプログラミング(ソフトバンク クリエイティブ)を執筆。趣味は薪割り。