🦜

Go&Ebitengineで環境マッピングしてみる

に公開

はじめに

Ebitenでシェーダをいじっていて、ちょっと頑張ったら環境マッピングとかもできるんだろうかと思ったのでやってみた。なにぶん初めての試みであるので色々イケてないのだが、ともあれこんなようなものが動くようにはなった。ので今回はそんなお話。

ちょっと見た目に地味過ぎた気がするので、派手ではないが少し極端な見た目に修正してみた。
サイコロが画面に収まっていないが、前に作ったサイコロぐるぐるのやつをベースにしている。
https://zenn.dev/mirichi/articles/93624c7269f56a

GitHubに置く

実行はこちら。
https://mirichi.github.io/Ebiten3DTest/002/
コードはこちら。
https://github.com/mirichi/Ebiten3DTest/tree/main/docs/002

これは何か

2DライブラリのEbitengineで3D描画を試すネタの一つとして環境マッピングにチャレンジしてみた。
Ebitengineにはフラグメントシェーダ(ピクセルシェーダ)があるので座標計算さえ自前で頑張ればどうにかなるのである。シェーダのほうはどのみち頑張る必要がある。

内容

環境マッピングとは

鏡的なものとか、金属的なもの、水面的なものは風景などが反射して映りこむ。これを3D描画で再現しようというのが環境マッピングである。
小さくて複雑な形状であれば映っているものは適当でもわからないので適当なテクスチャを歪めて張り付ければそれっぽくなるが、鏡、球体、水面なんかではそうもいかない。こういったものを描画するには反射する物体に合わせてカメラを置いて、そこから見た画像を描画して作る必要がある。今回はサイコロ1個なのでこのやり方で描画してみた。

Ebitengineの機能では

Ebitenはもともと画像に対して画像を描画するという思想で作られていて、画面に描画する際も描画先はebiten.Imageである。この考え方は作者のこだわりのようで、以前からここだけは終始一貫しているらしい。GPUを使った描画であればRenderTargetTextureを使うことになるが、いかなる画像(画面、新規で作ったImage、ファイルから読み込んだ画像など)に対しても透過的に同様の扱いができるとなれば、これは言うほど簡単なことでは無いだろうと思える。
ともあれ、環境マップ用のebiten.Imageを作って、これに対してシェーダを使った描画ができればサイコロが描画できるので反射する画像が作れる。あとはこれをシェーダに食わせてそれっぽく描画するだけだ。

コード的な話

今回は全体的に書き直してファイルを細かく分けた。主に計算ミスを主因としたバグの調査が大変で、一部分を置き換えたり、内部情報を可視化したりといったことがしやすいように作り直していたらこうなったというわけだ。
一応、サイコロと球体を描画する必要があったのと、環境マッピング用にカメラを用意して別視点で描画できるようにするというのも目的にある。
ところで球体の描画について、3Dで球体と言うと頂点をたくさん使って立体の球体を描画するというのがよくあって、最初そんな感じで描画しようと思っていたのだが、よくよく考えたら球体てどの角度で見ても円なんじゃね?ということで今回はポリゴン2枚で矩形を作って、そこにシェーダだけで球体を描画している。
このポリゴン2枚を何もないところから描画しようとすると頂点の計算やバッファ作成で面倒なのだが、EbitenのDrawRectShaderはなぜか画像と引数で渡す描画のサイズが一致していないとエラーになるので、わざわざ40行ほどのコードを追加した。別にサイズが違ってもいい気がするのだけどな。三角形2枚なら問題なく描画できるわけだし、どうせ内部では三角形2枚なんだろうし。
https://github.com/mirichi/Ebiten3DTest/blob/main/docs/002/sphere.go#L112-L157

描画に関する話

球体の表面の反射を表現する場合、手法としては球体の中心にカメラを置いて、元のカメラのほうを向いて描画することで画像を作る。この時、描画面をカメラ原点と一致させてやる(zn=0)と前方上下左右に180度範囲の描画ができそうだが、テクスチャサイズが無限になってしまうので実際には描画できない。テクスチャサイズを有限にすると画像自体が無限に縮小される。
従って、通常の環境マッピングでは球体中心から前後左右上下の6方向にカメラを向けてキューブ状の描画データを作成する。これがよく言われるキューブ環境マップというやつである。描画するときは前方180度だけでいいのだが省略できるのは後ろの1面のみでなんとなく嬉しくない。
そういうわけでリアルタイムの環境マッピングは大変なのだが、今回は簡易的なものということで、球体中心に置いたカメラをこっちに向けて、適当なznに適当なサイズで1回だけ描画している。球体表面にサイコロが描画できればいいので描画できる程度の範囲に絞って雑に調整した。実際、映り込んでいるサイコロをよ~く見ると球体の外周近辺でサイコロが少し欠けるように見えるような気がしないでもない。そのぐらいの雑さ。

シェーダの話

今回は最初から最後まで試行錯誤の連続で、シェーダも同様に苦労した。
https://github.com/mirichi/Ebiten3DTest/blob/main/docs/002/sphere.go#L20-L60
なんとなくそれっぽい描画ができてはいるが、実際きちんと正しい描画ができているのかは微妙に怪しい。いろいろおかしいところがありそうだ。
手法としてはテクスチャ座標ベースで計算していて、球体の中心はUniformsで渡している半径から求めている。テクスチャ画像のサイズから算出できたかもしれない。
描画されるものは半球であるとの前提で法線を計算して、表面に描画される色を環境マッピング用テクスチャから取得、スペキュラとかライト、アンビエントを適当に組み入れて算出するという感じになっている。
ちなみにサイコロの1の面、赤い丸にハイトマップを適用して凹んで見えるようにしてみた。こっちは更に輪をかけて適当。球体もハイトマップでやれるかとも思ったが、法線計算すれば済むのでやらなかった。

おしまい

コードを全体的に整理してはみたが、おそらくとても非効率なので、これをベースに何か凄いものが作れるとかそういうことは無いだろうと考えている。
Ebitenで3D描画すること自体がどうなのかと思わないこともなくもないが、いずれにせよ凄いものを作るのは凄い人にお任せする次第である。

Discussion