はじめに
こんにちは、わたよです。(現在時刻午前2時) 自分はよくopenFrameworks(以下oF)でCG表現やシミュレーションなどを試したりしています。 まぁ、いろいろあるんですが今回はglslでシェーディング処理をしてみたので共有します。CGとかに興味ある人は是非
目次
・oFとかGLSLってなに? ・今回の例の概要 ・実装について
oFとかGLSLってなに?
oF
oFはc++による様々なライブラリを簡潔にまとめたものと言えます。グラフィックスやセンサー、アクチュエーターなどを扱って創造的なものを開発するためのハブのような存在です。 詳しくは公式サイトをみてください。
GLSL
グラフィックスプログラミングをする上で欠かせないのがこのGLSLです。openGLのシェーディング言語です。グラフィックスパイプラインを直接制御して表現力を増すことができます。 GLSLに関しては doxasさんの記事が楽しいです。 あっもちろんoFはopenGLのプログラミングインターフェースを使用しています。
今回の例の概要
今回は.obj形式のモデルをoFで読み込んでリムライティングという処理をGLSLで実装しました。
いままで自分はoFならoFだけ、GLSLならGLSLだけでいじいじしていました。 oFのCPU計算だけだと処理自体に限界がありますし、GLSLだけだとちょっと何かを表示するだけでも本当に本当に難しいです。 そこでoFで頂点情報を指定して、並列処理だけをGLSLで計算する的なことをしたくなるわけです。
実装
モデルの読み込み
モデルの読み込み自体は簡単でofxAssimpleModelLoaderを使用します。【参考】
ここで問題なのは、GLSLでは頂点のさまざまな情報をやりとりして計算するのですが.obj形式のままではとにかく扱いにくい(色情報が無かったり)のでoF上で扱いやすいようにofMesh
クラスで変換します。
座標変換
CGやると絶対出てきます。3DCGでは頂点の位置を4×4の行列で表します。拡大縮小、平行移動、回転を表す行列をかけていくことで座標を表現します。 モデル行列、ビュー行列、射影行列でうまく処理を分けることができます。 詳しくはこれよんで🙏【参考】 (現在午前3時30、途中でだらけたとは言え流石に眠いぞー)
分ける理由はレンダラーもそういう風に作られているからです。
モデル行列はワールド座標系からの相対的な位置を表現します。回転は原点中心に行われるため、はじめに掛ける必要があります。
射影行列は理解しづらいのですが実用上ふんふんなるほどで大丈夫です。きっと
ビュー行列は、ワールド座標系を視点座標系に変換します。なんでモデルと違って視点座標系に変換しなきゃいけないんでしょうか。
カメラが動くとカメラが動くと、モデルはその方向とは逆に動いたように見えます。しかし、実際にはレンダラーはレンダリング処理に特化したいためカメラの位置と向きは固定したいです。そこでワールド座標系からの相対位置を計算してその逆行列を返せばよいことがわかります。(まぁ結局glm::lookAt
ってのがあるんですけどね)
これがわかりやすいです🙏【参考】
これらを掛け合わせて最終的にmvp行列を生成します。モデル・ビュー・プロジェクション行列です。p、v、mの順に掛け合わせていくことでできる行列です。
生成できたmvp行列をGLSLに渡していくんですね。
今までのoFではofMatrix3x3
, ofMatrix4x4
という行優先形式のクラスが用意されていましたが、glm
を使うとopenGLでも採用されている列優先形式で計算できるので理解しやすいです。列優先ということは単に掛ける順序を逆に考えれば良いだけです。
void ofApp::draw() {
using namespace glm;
DirectionalLight dirLight;
float time = ofGetElapsedTimef() * 0.5;
cam.pos = vec3(0, 20.0f, 15.0f);
cam.fov = radians(60.0f);
float cAngle = radians(0.0f);
vec3 right = vec3(0, 1, 0);
//ビュー行列
mat4 view = inverse(translate(cam.pos));
//モデル行列
mat4 model = rotate(time, right) * scale(vec3(0.03, 0.03, 0.03));
//法線情報
mat3 normalMatrix = (transpose(inverse(mat3(model))));
//射影行列
float aspect = 1024.0f / 768.0f;
mat4 proj = perspective(cam.fov, aspect, 0.1f, 110.0f);
mat4 mvp = proj * view * model;
//光の方向ベクトルを計算
dirLight.direction = normalize(vec3(-1, 0, 1));
dirLight.color = vec3(1, 1, 1);
dirLight.intensity = 1.0f;
diffuseShader.begin();
diffuseShader.setUniformMatrix4f("model", model);
diffuseShader.setUniformMatrix4f("mvp", mvp);
diffuseShader.setUniform3f("meshCol", glm::vec3(1, 0.4, 0.8));
diffuseShader.setUniform3f("lightDir", getLightDirection(dirLight));
diffuseShader.setUniform3f("lightCol", getLightColor(dirLight));
diffuseShader.setUniform3f("cameraPos", cam.pos);
diffuseShader.setUniformMatrix3f("normal", normalMatrix);
mesh.draw();
diffuseShader.end();
}
リムライティング
ここまで来てやっと、GLSLを書くことができます。詳しいGLSLの文法はbook of shadersに任せます。 リムライティングはモデルの輪郭からうっすらと露光する光を表現する照明効果です。詳しい解説などはここに任せます🙏【参考】 必要なのは、光の方向ベクトル、視線ベクトル、モデルの法線ベクトルです。あ〜だからmvp行列を渡す必要があるんですね。 重要なのはフラグメントシェーダーなのでソースコード載せておきます。
void main()
{
vec3 normal = normalize(fragNrm);
vec3 toCam = normalize(cameraPos - fragWorldPos);
float rimAmt = 1.0 - clamp(dot(normal, toCam), 0.0, 1.0);
rimAmt = pow(rimAmt, 3);
float lightAmt = max(0.0,dot(normal, lightDir));
vec3 fragLight = lightCol * lightAmt;
outCol = vec4(meshCol * fragLight + rimAmt, 1.0);
}
完成!
輪郭をみると露光している状態がシミュレートされているのが確認できます。
まとめ
終わってみれば対した量のコードは書いていないですが、いろんなところでつまづきましたなーという感じでした。 しかし今回の実装が理解できればいろんなシェーディング処理をoF上で試すことができますし、かなり学びは多かったです。 (特に座標変換!!) GLSLは他にもたくさんできることがあるのでこれからが楽しみです。 そしてdoxasさんのサイト本当に神です。
最後に
これを書くにあたって自分は説明うまくないし、新入生向けにこんな個人の趣味全開のもの書いてもな〜と思って乗り気じゃ無かったんですけど、書いてみると思い出せたり、文章を書くこと自体が難しいと気づいたりして楽しかったです。 新入生に伝わればいいなってことは、概念として理解できてもコードに落とし込むのは大変ってことです。
自分は本などを読んで概念から理解しようとするばっかりで、どうやらコードから何をいってるのか理解することを避けていたなと思いました。 どちらも大事ですが、コードは書きまくり読みまくった方がいいでしょう。 (現在時刻午前5時、普通に時間かかる...)