MonoGameの3Dモデルを管理描画するクラスを作る

提供: MonoBook
ナビゲーションに移動 検索に移動

MonoGameでの3Dモデルの「表示」について解説しているサイトは多数あるが、「回転」や「位置」について記述しているサイトはほとんどないようだ。そもそもモデルと位置情報などを切り離して管理すること自体が馬鹿げているので、それらを管理するクラスを作っておくと捗る。

また、Drawコールの中でBasicEffect周りを毎回設定している記述も多い。PCのようにCPUが高速な環境では問題にならないのだろうが、Androidなどではそれだけで結構な負担になる。Android端末の多くが「OpenGL ES 2.0」であるためGeometry Instancingのようなジオメトリシェーダーを使った技術も使えないのでDrawコールを極力軽くする努力が求められる。 

追記:小手先の最適化より自前Modelクラスを実装してメッシュ結合できるようにした方が速そう。なお純正のModelクラスはsealed指定されているので継承できない。

    using System;
    using System.Collections.Generic;

    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    using Microsoft.Xna.Framework.Input;

    public class Model3D
    {
        public static Dictionary<string, Model> ModelsCache = new Dictionary<string, Model>();

        public Game Game;

        public Model Model;

        public Matrix[] Bones;

        /// <summary>
        /// 回転(単位:degree)
        /// </summary>
        public Vector3 Rotation;

        /// <summary>
        /// 位置
        /// </summary>
        public Vector3 Position;

        public Model3D(Game game, string modelName)
        {
            // game
            this.Game = game;

            // model
            if (!ModelsCache.ContainsKey(modelName))
            {
                var model = game.Content.Load<Model>(modelName);
                ModelsCache.Add(modelName, model);
            }
            this.Model = ModelsCache[modelName];

            // boneを抽出しておく。
            this.Bones = new Matrix[this.Model.Bones.Count];
            this.Model.CopyAbsoluteBoneTransformsTo(this.Bones);

            // BasicEffectの基本設定をDrawメソッドの中で毎回やるのは非常に無駄なので事前に設定しておく。
            // Androidでは実質的にGeometry Instancingの様な技法が使えないのでDrawコールの軽量化は重要になる。
            // (ジオメトリシェーダーに対応したOpenGL ES 3.2対応端末が広く普及するのは10年後だろう)
            // これだけでもショボいAndroid(F-04G)では300モデルでフレームレートが20%くらい違ってくる。
            foreach (var mesh in this.Model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.LightingEnabled = true;
                    effect.PreferPerPixelLighting = true;
                }
            }
        }
        // Camera引数は「MonoGameのカメラを作る」を参照
        public void Draw(Camera camera)
        {
            var meshes = this.Model.Meshes;
            var bones  = this.Bones;

            // 回転をMatrixに変換
            var yaw   = MathHelper.ToRadians(this.Rotation.Y);
            var pitch = MathHelper.ToRadians(this.Rotation.X);
            var roll  = MathHelper.ToRadians(this.Rotation.Z); 
            var rotation = Matrix.CreateFromYawPitchRoll(yaw, pitch, roll);

            // 位置をMatrixに変換
            var postion = Matrix.CreateTranslation(this.Position);

            // この手の定番計算はCameraクラスを作っておくと捗る。
            var view = camera.View;
            var projection = camera.Projection;
          
            foreach (var mesh in meshes)
            {
                // 一般的な四則演算と異なり、行列の乗算除算は順番が重要。
                // そのため「rotation」と「position」を入れ替えると残念な結果になる。
                var world = bones[mesh.ParentBone.Index] * rotation * postion;

                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.World = world;
                    effect.View = view;
                    effect.Projection = projection;
                }
                mesh.Draw();
            }
        }
    }

関連項目[編集 | ソースを編集]