はじめに
前回のエントリはかなりXamarin.Andoridよりだったので、今回のはSecondLifeよりのものです。 今年(2016)のはじめにRadegastの3DViewをAndroidに移植してみたので、それを思い出して書いていきます。
このエントリで使われている技術的な項目
- OpenTK
OpenGLおよびOpenGLESなどをC#でラップしたものです。
- LibOpenMetaverse
Metaverse(SecondLife,OpenSimなど)用のライブラリです。
- Xamarin.Android
Radegastって
C#で書かれたクロスプラットフォーム(Windows,macOS,Linux)のMetaverse Viewerでメインはテキストベースですが、3DViewも実装しており、機能をプラグインで追加できるものです。SecondLife的視点から言えばサードパーティのViewerです。 開発は、Latif Khalifaさんがリーダーとなって行っていましたが、彼の病気のため2014年の11月に開発終了のアナウンスされました。
しかし、リポジトリをのぞくと細かい修正は行われています。私もPRしてます。
なお、LibOpenMetaverseのメインで活動されていて、Radegastもそのリーダーとして制作されていたLatif Khalifaさんですが
において記述したように泉下の客になられました。 ここで記述する第1段階としての移植をすでに終えていて、次の段階に取り掛かっていたころでしたのでかなりショックでした。
動機
4年ほど前に
Android向けのLibOpenMetaverseもある程度動くようになった
勉強も兼ねてAndrodで3D部も実装してみたい
と思うようになり、少々調査し、
ということがわかり、じゃ実装するかと少し手を動かしたところで、実機やPCを長時間触ると体調が悪くなるようになり、放置していました。
今年(2016)の初めごろに体調もだいぶ治ってきて、体調不良への対応方法もある程度確立できたので、再度手を付けました。
実際やってみて
前提
今回の移植は
ソースコードについては、Radegastの3Dviewを https://github.com/radegastdev/radegast/tree/master/Radegast/GUI/Rendering
ごそっと移植しました。
そこではOpenGL1.3ベースで書かれており、それをOpenGLES1.1にあわせて移植しました。 そのため、OpenGL1.3およびOpenGLES1.1はプログラマブルシェーダーではなく固定機能パイプラインですので、比較的違和感なく移植することが可能でした。
なお、固定機能パイプラインは、レンダリングの方式として、あらかじめ用意されたものを使って簡便に行うものです。それに対してプログラマブルシェーダーと呼ばれるものはシェーダーを使って自由にレンダリングする方式のことで、OpenGLでは1.5以降、OpenGLESでは2.0以降となっています。
また、LibOpenMetaverseにおいて、RigidMeshのデータを取得する実装がされていないため、Radegastでも対応していないため、いわゆるRiggid MeshやFittedMeshは描画できません。 Mesh/Mesh Asset Format - Second Life Wiki
対応した内容
OpenGLとOpenGLESの違い(Androidへの対応)
- 実装されているものの差異対応
実装されているものの差異対応として、RadegastのOpenGL1.3では実装されていますが、OpenGLES1.1にはオクルージョンカリングが実装されていません。OpenGLES3.0からオクルージョンカリングは実装されました。そのためその処理は削除しました。オクルージョンカリングは、あるオブジェクトが他のオブジェクトによって遮蔽されている場合、そのオブジェクトを描画しないカリングです。それが実装されていないため、オブジェクトが見えなくても描画され、速度が遅くなるという問題が発生します。
また、GL.Begin,GL.Endで実装されているところがありましたが、OpenGLES1.1には実装されておりません。配列を使って書き換えました。通常は変更できませんが、ボーンを描画する機能が実装されていました。
https://github.com/radegastdev/radegast/blob/master/Radegast/GUI/Rendering/Rendering.cs#L1709
- 操作部分の変更
操作部分の変更をしました。そもそもマウス操作で、カメラのコントロールやズームなどが実装されていますが、それをタッチ操作に変更しました。 タッチしている間に動かすとそれに合わせてカメラが動き、ピンチイン・ピンチアウトでズームするようにしました。Panの実装を入れるべきか悩んだのですが、どういう操作にするか決めるのも面倒だったので実装しませんでした。何かフラグを立てればよかったかも。。。
public override bool OnTouchEvent (MotionEvent e) { base.OnTouchEvent (e); mScaleDetector.OnTouchEvent (e); float pixelToM = 1f / 75f; switch (e.Action & MotionEventActions.Mask) { case MotionEventActions.Down: if (!mScaleDetector.IsInProgress) { prevx = e.GetX (); prevy = e.GetY (); mActivePointerId = e.GetPointerId (0); } break; case MotionEventActions.Pointer1Down: if (mScaleDetector.IsInProgress) { float gx = mScaleDetector.FocusX; float gy = mScaleDetector.FocusY; mLastGestureX = gx; mLastGestureY = gy; } break; case MotionEventActions.Move: if (!mScaleDetector.IsInProgress) { int pointerIdx = e.FindPointerIndex (mActivePointerId); float x = e.GetX (pointerIdx); float y = e.GetY (pointerIdx); float dx = x - prevx; float dy = y - prevy; //Rotate Camera.Rotate (-dx*pixelToM, true); Camera.Rotate (-dy*pixelToM, false); mPosX += dx; mPosY += dy; Invalidate (); prevx = x; prevy = y; } else { float gx = mScaleDetector.FocusX; float gy = mScaleDetector.FocusY; float gdx = gx - mLastGestureX; float gdy = gy - mLastGestureY; //zoom float scale = mScaleFactor; Camera.MoveToTarget (scale-mprevScale); mprevScale = scale; mPosX += gdx; mPosY += gdy; Invalidate (); mLastGestureX = gx; mLastGestureY = gy; } break; case MotionEventActions.Up: mActivePointerId = INVALID_POINTER_ID; break; case MotionEventActions.Cancel: mActivePointerId = INVALID_POINTER_ID; break; case MotionEventActions.PointerUp: int pointerIdx2 = (int)(e.Action & MotionEventActions.PointerIndexMask) >> (int)MotionEventActions.PointerIndexShift; int pointerId = e.GetPointerId (pointerIdx2); if (pointerId == mActivePointerId) { int NewPointerIndex = pointerIdx2 == 0 ? 1 : 0; prevx = e.GetX (NewPointerIndex); prevy = e.GetY (NewPointerIndex); mActivePointerId = e.GetPointerId (NewPointerIndex); } else{ int TempPointerIdx = e.FindPointerIndex(mActivePointerId); prevx = e.GetX(TempPointerIdx); prevy = e.GetY(TempPointerIdx); } break; } return true; }
- テクスチャを貼る際のBitmapの処理
画像の座標が逆(原点が左下と右上の違い)なので、bitmap上下反転する必要がありました。それとAndroidのAPIで実装されているGLUtils.TexSubImage2D を使いました。Bitmapで直接渡せるからという理由です。
Android.Graphics.Matrix mtx = new Android.Graphics.Matrix (); //上下反転 mtx.PreScale(1,-1); img = Bitmap.CreateBitmap (img, 0, 0,img.Width, img.Height, mtx, false); GL.BindTexture (TextureTarget.Texture2D, item.TextureID); // 作成したテクスチャをバインド Android.Opengl.GLUtils.TexSubImage2D ((int)TextureTarget.Texture2D,0,0,0,img);
次へ向けて
- マトリックスパレットによる実装
Radegastの実装だとキーフレームアニメーションにおいて、フレームごとにアバターの座標の変更の計算をCPUで行っており、Androidの実機で試すとFPSが1を超えない程度になってしまい、とても残念な描画になってしまいます。これを解決する手段として、各ボーンの姿勢の行列をGPUに渡して、GPU側で計算させ描画すると速度が改善されるので、その対応を行いましたが、後述するshader絡みでまだ途中となっております。
- LibOpenMetaverseはRigidMeshのデータ取得に対応
LibOpenMetaverseでは、RigidMeshのデータを格納していないので、それを対応する必要がありました。また、描画においても修正が必要ですので、それへの対応をRadegastのソースを修正することで行っています。これも後述するshader絡みでまだ途中となっております。
- OpenGLES2.1以降への変更
より快適に描画できるようにOpenGLES3.0のオクルージョンカリングを使用したいのですが、その前にOpenGLES1.1からOpenGLES2.1へ移行するには、固定機能パイプラインからプログラマブルシェーダへと移行する必要がありました。
- shaderとか
現状、固定機能パイプラインからプログラマブルシェーダへと移行できましたが、実装するシェーダー特に光(Normal関係)がうまく実装できず、放置状態になっております。
- Bento対応とか
ボーンが増えたので、GPU側のメモリ消費が心配ですが、avatar_skeleton.xmlを変更して、テストすればいいのかなと思っていますが、Rigidmesh対応時にボーン名のマッピングに泣かされたのでそれがまたあるかもしれないと思っております。
- 最適化について
一番の問題は遅くて使用に耐えられないということで、最適化する方法はあり試してみる価値はあるとは思っています。
モチベーションの問題とかで中途半端な実装で紹介するのもという段階です。 PC版のRadegastでは現状こんな感じです。
まとめ
3D周りのプログラミング経験がある方にとって、今回記述したことろの移植は、数日もあれば十分な範囲です。私にとっては3D周りの勉強から入らなければならなかったのでちょっと難儀しました。しかし、次へ向けてのところでも数学的な勉強をする必要がありましたので、良い勉強となりました。
次へ向けてと言いつつも、LibOpenMetaverse for Androidの準備とかで秋口から全然手を付けられていません。 あんまり最新事情を追えていないのですが、ボーン周りの仕様が追加されたということは確認しています。 しかし、体調のこともありゆっくりやっていきます。 ここで書いたようなAndroidでのViewerのリリースはいろいろな事情から諦めたので、そのビジネスロジックを担当するLibOpenMetaverse for Androidをリリースしようと思っております。ですので、個人的にはプライオリティは低いという状況です。
今回記述したものについては、iOSでもおそらく可能だと思いますし、LibOpenMetaverseにおいてもPocketMetaverseが使用していましたから、移植は可能です。 SecondLifeのiOS向けの3DViewerがほしいという方で、Xamarin.iOSで移植してみようという方はいらっしゃいませんか?
いつもの締めになってしまいますが、健康は大切です。何気ない日常を送れるということ、そのことこそが幸せなことだということを心に刻んで日々を送りましょう。