🦜

Go&Ebitengineで3D描画をする

に公開

はじめに

Ebitengineは2Dゲームエンジンであり、3D用の機能は持ち合わせていないが、なぜか世の中にはガチな人がいてガチな3Dライブラリを作っていたりする。
https://github.com/SolarLune/tetra3d
でもこれは使わず、座標計算から自前でやってみたのでそんな感じのお話。

Githubに置く

こちらで実行できる。
https://mirichi.github.io/Ebiten3DTest/001/
このような感じのものが動く。マウスやタッチでぐるぐるできる。

コードはこちらに置いてある。
https://github.com/mirichi/Ebiten3DTest/tree/main/docs/001

これは何か

Ebitengineで3Dをやってみようと思い立って調べていたら凄そうなやつが出てきたので参考にしつつ、とりあえず座標計算から描画までを一通り実装してみた。
特に設計もなく順次実装していったので雑な部分も多いが、最低限このような描画をするのに必要なコードはこの程度なのだと、シンプルな3Dの描画だけなら言うほど難しくないのだと知ってもらえれば幸いである。

内容

全体像

このプログラムの中心になるのはdice.goである。だいたい3Dの全部がここに書かれている。
https://github.com/mirichi/Ebiten3DTest/blob/main/docs/001/dice.go
残念ながら最後まで表示できないようだ。全部で220行程度なのでgithubで見てくださいな。
手作業で頂点情報を構築し(座標を並べ)て、サイコロの画像はebitenの関数でコツコツ描画して。地味に頑張ったところ。
3Dの計算はDrawDice関数でやっていて、各種座標変換、法線の計算、背面カリング、頂点バッファ作成、描画という流れになっている。陰影とパースペクティブコレクト用にシェーダを用意した。
計算に使うVector、Matrixについてはtetra3Dを参考にしながら必要なぶんだけ作った。決まりきった計算をするだけなので誰が作っても列ベクトルか行ベクトルかの違いぐらいでだいたい似たようなものになるはず。

座標の表現など

3Dなのに右に+X、下に+Y、奥に+Zという変則的な座標系になっている。2DライブラリのEbitenを拡張しているのだという主張である。このへんは座標変換行列の作り方でどうにでもなるので、この手のものを作りたい人は自分の好みに合わせて作ればよい。
ベクトルは行ベクトル。画面上値が横に並んでるのに列ベクトルとか頭おかしいだろうと日頃から思っているので行ベクトルである。どっちかというとDirectX出身ぎみだし。このへんもVectorとMatrixの作り方でどうにでもなるので好みに合わせて作ればよい。tetra3dは列ベクトルだった。

座標計算について

基本に忠実に行列を作って計算しているだけである。DirectX的な計算式になっているが。この辺の計算式は検索すると式と考え方の解説サイトがたくさん出てくる。射影変換行列を自分で作る人とかそんなにいないと思うのだがな。
今回はビュー変換は省略してある。これは単に面倒だったからで、サイコロの座標をカメラの位置(0,0,0)の正面(+Z方向)に配置してあるので不要、ということだ。世界の中を移動するとか、拡大縮小とか、そういうのがしたいならカメラを用意してビュー変換することになる。
各種パラメータは画面の640*480ベースの数値にしてある。これは単にイメージしやすかったからで、実際にはすべてカメラとの相対表現であり、射影変換で-W~+Wに落とし込まれて、ビューポート変換で画面サイズになるので、どんな値にしようが自由である。一般的な3Dゲームがどのようにしているのかを知らないので適当。

描画について

立方体を描画するのでポリゴンのZソートなりZバッファなりでも対応できそうだが、手を抜いて背面カリングだけでお茶を濁している。ポリゴンの頂点が右回りか左回りかを判定して、立体形状では裏を向いたポリゴンは表向きのポリゴンに隠れるので描画しない、というのが背面カリングで、Ebitenは2Dらしくカリングをしない(カリングすると左右反転描画とかができなくなる)ので、自前で判定している。
あと、パースペクティブコレクト(パースペクティブ補正テクスチャマッピング)というのを(半分)自力でやっている。Ebitenで3D座標を計算してポリゴン描画、と言ってもそのままでは三角形を変形して描画しているだけで、テクスチャを貼るとただ単純に変形された画像が描画される。奥行きがある場合、奥のほうが小さくなるように描画しないといけないのだが、EbitenはZ座標を持たないのでそういう機能が無い。
そういうわけで、射影変換後のW(実質Z座標)を使ってテクスチャ座標に補正をかける。頂点情報にはテクスチャ座標があり、通常はGPUが線形補間することでポリゴン内の画像を綺麗に描画するのだが、3Dの場合はそのまま線形補間されると歪んでしまうので、頂点情報に格納するテクスチャ座標はWで割っておいて、その値をGPUに線形補間してもらって、同様に頂点情報に入れたWの値もGPUに線形補間してもらって、シェーダ内で計算して戻すことでパースをつけるわけだ(あまりわかってない)。
この効果はアプリの左上のボタンで処理のON/OFFができるので、パースペクティブコレクトするしないの違いを見て実感することができるはず。ちょっとわかりにくいかもだが。
コンパイルできる環境がある人なら、dice.goのサイコロ画像作成の後ろにコメントしてるやつを有効にするとパースペクティブコレクトの効果が見えやすくなる。ああ、画像貼っておけばいいのか。こうなる。

EbitenのShader

今回やってみようと思ったのは、EbitenにShaderがあったからだ。フラグメントシェーダ(DirectXではピクセルシェーダ)だけしか扱えないが、これがあるだけで表現力はかなり上がる。3D描画ができるのもShaderのおかげで、これが無いとパースペクティブコレクトができない。
頂点シェーダが使えないので座標は自前で計算するハメにはなったが、実際のところ頂点が少なければ計算は大した負荷ではないので問題にならない。3D初期の頃はGPUによっては頂点シェーダが実装されていなくてソフトウェアでエミュレーションなんて時代もあったぐらいだ。現代の3Dは頂点がとても多そうなのでテッセレータとかも使ってハードウェアで処理しないとやってられないかもしれない。
今回のハマりポイントは座標計算を除けばやはりシェーダ部分で、テクスチャ座標の計算が狂うと画像が予想できない描画になるし、なんかテクスチャの画像データが壊れたような挙動をすることもあった。シェーダのコードはデバッグができないし、変数の中身を知る手段も無いので、バグるとキツい。
いまいちわからないのはimageSrc0Origin()で、SubImageを使ってるわけでもないのにこれが返す値を使って計算しないとうまくいかない(おそらく0,0以外が返ってきている)というところで、テクスチャアトラスとかミップマップあたりの関連で特殊なことをやってるのかな?などと思っているが詳細がわからない。どんな値が取れているのかが見れればいいんだけど。

その他

前に作ったボタンとラベル、適当にコードを持ってきて生成して配置したら動いたのでちょー便利(自画自賛)。

おしまい

3Dの描画ができるとやってみたいことが色々とでてきてしまうわけだが。例えばシャドウマッピングみたいな表現とか。3D物理演算のライブラリを使ってみるとか。複雑なモデルを描画してみるとか。
まあ今回の部分だけでも結構苦労したので更にやるかはちょっとわからないが、面白そうではある。気がしないでもない。

Discussion