この記事では、OpenGLでドロップをレンダリングし、反射と透明度の法線をオンザフライで計算する方法を見ていきます。 メタボールとは何か、グラフィックチップセットのバグ、およびモバイルデバイスの60 FPSに適用できる最適化のトリック。
内容
パート1. モバイルクロスプラットフォームエンジン
パート2. SDFフォントを使用したUTF-8テキストのレンダリング
パート3.透明度と反射のあるドロップのレンダリング
メタボール
ドロップレンダリングは、 メタボールテクニックに基づいています。
このテクニックの意味をテキストで伝えるのは難しいので、明確に示します。 さらに、この手法は、 前の記事でSDFフォントで使用した手法と非常によく似ています 。
グラフィックエディターを開きます。
- ぼやけたブラシでポイントを描画します。
- わずかに重なるように、このようなポイントをさらに追加します。
- レベル(レベル)を目的の効果に合わせてツイストします。
最も簡単な形で、Metaballsは準備ができています。 ご想像のとおり、液体は多くのポイントで構成されており、物理学を破壊し、美しさのためのシェーダーを追加するだけです。
単一のドロップをレンダリングするにはトリックがあります。 通常の放射状グラデーションでドロップをレンダリングすると、永遠に丸いエンドウが得られます。これは、ダイナミクスでは非常に奇妙に見えます。 したがって、個々のドロップの速度を考慮し、勾配の中心を加速に向かってわずかにシフトします。 その結果、移動時に孤独な水滴でさえ少し伸び、写真を著しく活気づけます。
OpenGLを準備する
ドロップをレンダリングするには、最初にテクスチャの中間ステップをレンダリングする必要があります。 これは、 FBO ( Framebuffer Object )を使用して実行できます。 さらに、画面サイズの1/2または1/4の小さなテクスチャを使用できます。 品質はほとんど影響を受けません。
width= ; height= ; // FBO glGenFramebuffers(1, &framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); // glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); // FBO glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
次に、テクスチャでのレンダリングに切り替えるには、次を実行します。
glBindFramebuffer(GL_FRAMEBUFFER、フレームバッファー);
glClear(GL_COLOR_BUFFER_BIT);
そして再び画面にレンダリングするには:
glBindFramebuffer(GL_FRAMEBUFFER、0);
リソースを節約するために、透明度のないRGBテクスチャを使用することは論理的です。 そしてほとんどの場合、すべてがうまくいきます。 ただし、 Adrenoチップセットを搭載したAndroidではそうではありません。 まれなデバイスでは、ノイズまたは黒一色がテクスチャに表示されます。 したがって、 GL_RGBA形式を使用することをお勧めします。
レンダリングを落とす
最初のパスは、速度と素材を考慮して、テクスチャ内のすべてのドロップをレンダリングします。
以下は擬似コードです 元のコードには、特定のエンジン機能へのリンクが多すぎます。
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); glClear(GL_COLOR_BUFFER_BIT); bindShader(metaBalls); for( - ){ // "" vx = ; vy = Y; vLength = length(vx, vy); if(vLength > vMax) { // "", vx *= vMax/vLength; vy *= vMax/vLength; } setUniforms( vx, vy, . ); renderQuadAt( .x, .y ); }
次のようなものが得られるはずです。
注意深い読者は尋ねます:
「なぜ赤なのか、緑はどこから来たのか、そしてこれらはどのような境界線なのか?」
素材
一度に複数のマテリアルのドロップをドロップするとします。 そして、彼らがスムーズにミックスできるようにします。 これを行うために、速度とともに、ドロップのマテリアルも転送します。 これは0から1までの通常のフロートであり、あるマテリアルから別のマテリアルへの移行を意味します。 テクスチャのREDチャンネルではグラデーション自体を記述し、 GREENではマテリアルを記述します。
ボーダー
記事のヘッダーのgifをもう一度見てください。 縁石の隣に、滴が少し広がるが、縁石自体には登らないことがわかります。 この効果を得るには、RチャンネルとGチャンネルの情報が保存されている場所に、事前に作成したマスクを追加する必要があります-追加するものと削除するもの。
数式は次のように書くことができます:(滴のあるテクスチャ+赤)*青。
つまり 境界のぼやけたエッジに沿って、ドロップをわずかに強化し、境界自体に直接、反対に、ドロップを削除します。
コントラストのあるグレースケールテクスチャを上に描画してみてください。 たとえば、これ:
滴がテクスチャの棚の周りを突然流れ始めます。
もちろん、これは物理学とは関係なく、視覚的なトリックにすぎません。
マスターシェーダー
次に、画面全体のサイズの1つのクワッド(2つの三角形/ポリゴン)を出力する必要があります。 このシェーダーでは、メタボールテクニックを適用して、わずかにぼやけたエッジを取得する必要があります。 これにより、ドロップのエッジで3D効果が得られます。
次に、隣接するグラデーションテクセルを読み取り、反射の法線を計算します。これにより、Matcapテクスチャから値を取得します。
以下は、読みやすくするためのクリーンなシェーダーコードです。
// OpenGL ES precision #ifdef DEFPRECISION precision mediump float; #endif varying mediump vec2 outTexCord; //FBO , uniform lowp sampler2D tex0; //Matcap uniform lowp sampler2D tex1; #define limitMin 0.4 #define limitMax 1.6666 #define levels(a,b,c) (ab)*c void main(void){ // Metaballs float tex = texture2D(tex0, outTexCord).r; float gradient = levels(tex, limitMin, limitMax); // , Adreno #ifndef ADRENO if(gradient<=0.0){ discard; } #endif // 4 vec2 step=vec2(0.002, 0.002); vec2 cord=outTexCord; cord.x+=step.x; float right=texture2D(tex0, cord).r; cord.x-=step.x*2.0; float left=texture2D(tex0, cord).r; cord+=step; float bottom=texture2D(tex0, cord).r; cord.y-=step.y*2.0; float top=texture2D(tex0, cord).r; // vec3 normal; normal.z=gradient; normal.x=(right-left)*(1.0-gradient); normal.y=(bottom-top)*(1.0-gradient); normal=normalize(normal); // vec3 ref=vec3(outTexCord-0.5, 0.5); ref = normalize(reflect(ref, normal)); // Matcap cord=(ref.xy+0.5)*0.5; vec4 matcap=texture2D(tex1, cord); // matcap.a*=min(1.0, gradient*10.0); // , matcap.a*=1.0-gradient*0.2; gl_FragColor = matcap; }
アドレノ
ほとんどの問題は、 Adrenoチップセットを搭載したAndroidデバイスによってもたらされました 。
- Adrenoでは、すべてのテクスチャが読み取られるまで破棄はできません。 そして、一般的に廃棄を拒否する方が良いです。
- FBOの場合、RGBAテクスチャのみを使用する必要があります。 RGBはノイズまたは黒を表示します。
破棄する必要がありますか?
はい、必要です。 特に、FPSの増加はタブレットで顕著です。大きなろ過液があり、ピクセルごとに戦わなければなりません。
透明性
透明度については、単純なトリックを使用します。ドロップのエッジに近づくほど、法線がより大きくずれます。つまり、エッジの透明度が低下します。 一種の最大限に単純化されたフレネル効果 。
マットキャップ
Matcapテクスチャを置き換えるだけで、水、銀、金、溶岩、牛乳、コーヒーなど、まったく異なる素材を入手できます。
インターネット上に十分な無料のMatcapテクスチャがあるのは良いことです。 確かに、大きなドロップでは、テクスチャの中心が非常に引き伸ばされることを考慮する価値があります。 したがって、良い結果を得るには、多くのMatcapテクスチャをソートする必要があります。
いくつかの材料
FBOで液滴をレンダリングするときに、マテリアルの値も記録したことを覚えていますか?
シェーダーに小さな変更を追加し、1つのマテリアルから別のマテリアルへのスムーズな移行を取得します。
そして、私たちが持っているマテリアルの価値は個々のドロップに結びついているので、各ドロップのマテリアルを順番に変えると、液体が溢れたり混ざり合ったりする効果が得られます。
ところで、私たちは2つの材料だけに限定されていません。 もう1つのマテリアルトランジションがBLUEチャンネルに記録されている場合、4つのマテリアルを一度に補間できます。
最適化
現在の形式では、すべてのデバイスで60 FPSを取得しても機能しません。 iPad2のメインシェーダーは特に大変でした。 破棄は保存されませんでしたが、ピクセルの80%で機能しました。 これらの空の80%を取り除きましょう。
画面をセルに分割します。 私にとっては、screenWidth / 20セルサイズが最適でした。 彼らは四角形をセルに分割し、これらの小さな四角形のインデックスを構成しました。 次に、どのセルがドロップで満たされているか(さらに隣接セルを追加する)を確認し、ラティスが変更されるたびにインデックスを更新する必要があります。
glBufferData(GL_ELEMENT_ARRAY_BUFFER、サイズ、データ、GL_DYNAMIC_DRAW);
液滴を含むセルのみが表示されます。
物理学
物理学についてはさりげなく触れます。 これは非常に興味深いが、1つの記事の広範なトピックです。
わずかな変更を加えたVerlet統合に基づいています。 Verletに関するすべては、このようなコードブロックで囲まれています。
//fric - , float dt2,tmp; dt2=dt*dt; tmp = 2.0f * x - prevX + accelX * dt2; prevX += (x - prevX)*fric; x = tmp; tmp = 2.0f * y - prevY + accelY * dt2; prevY += (y - prevY)*fric; y = tmp;
液滴間の距離を確認し、壁との衝突を処理し、表面張力を考慮して、液滴が拡散しないようにすることができます。
表面張力でカンニングしなければなりませんでした。 どのフレームが互いに接触しているかを調べて、それらをグループに分けます。 次に、各グループの中心を取得し、各ドロップをグループの中心に少し引き出します。 これにより、非常に受け入れやすく迅速な結果が得られました。
お団子!
ドロップを背景にうまくフィットさせるには、影を追加します。 GPUに大きな負荷をかけることなく、メインシェーダーで取得することもできます。
影については、わずかに大きいメタボールを使用してください。 エッジでドロップを暗くし、エッジの後ろで黒い色にし、アルファマスクをわずかに拡大します。 このようなもの:
float shadow = levels(tex, ); float body=min(1.0, gradient*10.0); matcap.rgb*=min(1.0, body*5.0); matcap.a*=body+shadow;