Shader Model 2.0でのクアッドツリーリアルタイム可視化

プロローグ


良い一日! 友人が私の仕事に来て、私が彼に書いたばかりのシェーダーを見せたとき、それは彼らとの最初の深刻な経験でした。 このマイクロプログラムは、カメラからの画像をニットセーターの画像に変換しました。



開発時には、携帯電話のカメラから画像が撮影され、ニットの形で表示されましたが、その効果は異常でした。 そして、友人が二次ツリーの形でカメラのフィルターを作るというアイデアを投げました。



一般に、この瞬間から、二次ツリーをすばやく構築する方法を研究するために座って、 HistoPyramidのアイデアに落ち着きました

実装


最初に、シェーダーの入力パラメーターを準備する必要があります。 私はUnity3Dを使用したので、C#のコード例です。

アルゴリズムのプロセスは、2つの段階に分けられます。

  1. 木を造る
  2. a。 シェーダーパラメーターの準備
    b。 シェーダーの実装
  3. 木から画像を作成する

木を造る


シェーダーパラメーターの準備


すべての計算は、正方形の最大サイズが256ピクセルに等しいという条件で実行されます。

int quadSize = 256; 

最初に、構築されたツリーの例を示します。



ツリーのレベル数は次のように計算されます。

 float levelsAmount = Mathf.Log(quadSize, 2f); 

次の各レベルは、前のレベルの半分です。 ツリー要素は右側に配置されるため、ツリー画像のサイズは次のように計算されます。

 Vector2 histoSize = new Vector2((float)quadSize + (float)quadSize / 2f, (float)quadSize); 

シェーダーパラメーターは、ブロックの先頭の座標、ブロックの末尾の座標、およびブロックのサイズを渡す必要があります。 また、親ブロックにも同じパラメーターが必要です。 現在のブロックよりも大きいブロック。

 float _cellSize = Mathf.Pow (2f, levelsLog - (float)currentLevel); Vector2 size = new Vector2 (_cellSize / histoSize.x, _cellSize / histoSize.y); Vector2 start = new Vector2 (0.6666666667f, size.y); Vector2 end = new Vector2 (0.6666666667f + size.x, 2f *size.y); builderMaterial [i].SetVector ("_startBlock", start); builderMaterial [i].SetVector ("_endBlock", end); builderMaterial [i].SetVector ("_sizeBlock", size); 

指定されたコードは、 currentLevelが 0より大きい場合、つまり開始、終了、ブロックサイズの座標を計算します。 元の画像の右側にあるツリーのすべての要素。 これから、開始と終了の座標の「マジックナンバー」0.6666666667f(2/3が元の画像です)。 _cellSizeは、ブロックサイズをピクセル単位で格納します。その後、開始と終了については、相対単位に変換する必要があります。

 if (i == 1) { parentStart = Vector4.zero; parentSize = new Vector4 (0.6666666667f, 1f, 0, 0); } else { _cellSize = Mathf.Pow (2f, levelsLog - (float)currentLevel + 1f); parentSize = new Vector4 (_cellSize / histoSize.x, _cellSize / histoSize.y, 0, 0); parentStart = new Vector4 (0.6666666667f, parentSize.y, 0, 0); } 

親ブロックの場合、すべてが同じです。

シェーダーの実装


シェーダーは、 levelsAmount回実行されます。 ツリーの各レベルについて、現在のレベルに対応するパラメーターが送信されます。

シェーダーの入力パラメーターは次のとおりです。

 Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _BaseStep ("Pixel size parent block", Vector) = (0, 0, 0, 0) _Level ("Current level", Float) = 1 _startBlock («Start current block", Vector) = (0, 0, 0, 0) _endBlock («End current block", Vector) = (0, 0, 0, 0) _sizeBlock ("Size current block", Vector) = (0, 0, 0, 0) _parentEndBlock ("Start parent block", Vector) = (0, 0, 0, 0) _parentSizeBlock ("Size parent block", Vector) = (0, 0, 0, 0) } 


_MainTexで送信された元の画像:



このシェーダーのタスクは次のとおりです。


レベル0の場合、テクスチャは次のように作成されます。

 if (_Level == 0) { if (vo.uv.x < _sizeBlock.x) { float2 uv = float2(vo.uv.x / _sizeBlock.x, vo.uv.y); return tex2D(_MainTex, uv); } else { return fixed4(0,0,0,1); } } 

最終サイズに比例して、それを挿入し、残りのピクセルを黒でペイントします。

他のレベルでは、現在のブロックのヒットを確認する必要があります。

 if (vo.uv.y >= _startBlock.y && vo.uv.y <= _endBlock.y && vo.uv.x >= _startBlock.x && vo.uv.x <= _endBlock.x) { 

計算を簡単にするために、オリジナルとの色の違いは最初のレベルでのみ考慮されます。 ブロックの相対座標は次のように計算されます。

 float2 uv = (vo.uv - _startBlock)/_szeBlock; float2 suv = float2(0.6666666667 * uv.x, uv.y); 

次に、親ブロックの4色を取得し、平均値と誤差を計算する必要があります。

 fixed4 col1 = tex2D(_MainTex, suv); fixed4 col2 = tex2D(_MainTex, float2(min(suv.x + delta.x, _endBlock.x), suv.y)); fixed4 col3 = tex2D(_MainTex, float2(suv.x, min(suv.y - delta.y, _startBlock.y))); fixed4 col4 = tex2D(_MainTex, float2(suv.x, max(suv.y - delta.y, _startBlock.y))); col = (col1 + col2 + col3 + col4)/4; 

エラーは簡単な方法で計算されます。ピクセルの色が異なる場合、エラーに(1 / n)が加算されます。ここで、nは色比較の組み合わせの数です。

1つの組み合わせの例を示します。

 float rgbError = 0; if (abs(DecodeFloatRGBA(col2) - DecodeFloatRGBA(col1)) > 0) { rgbError += 0.1666666667; } 

関数の結果は平均ピクセルカラー値です。アルファチャネルはエラーです。 これはアルゴリズムの主な単純化です。なぜなら、ヒストグラムを通して色偏差を計算するのに十分ではなかったからです。

 return fixed4(col.rgb, errorColor); 

他のレベルでも、原理は似ており、親ブロックのピクセルの合計からエラーのみが考慮されます。

 float errorColor = min((col1.a + col2.a + col3.a + col4.a), 1.0); 

その結果、すべてのレベルを経て、ツリーが形成されます。 次のステップでは、画面に合わせてツリーを表示し、画面に合わせてツリーを拡大します。

ツリーから画像を作成する


このシェーダーのタスクは次のとおりです。各レベルのエラーを判断し、指定したレベルを超えない場合は、ツリー内のピクセルの色で必要なレベルを描画します。 シェーダーオプション:

 Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _TileTex ("Tile (RGB)", 2D) = "white" {} } 


最終的な画像の品質はこのテクスチャに依存し、背景は透明で、正方形に刻まれた図形は任意です。

画像出力の主な機能は次のとおりです。

 fixed4 getLevelColor(float level, float2 uv, float2 cellSize, float outSize) { float2 suv = float2(0.6666666667 + uv.x*cellSize.x, cellSize.y+uv.y*cellSize.y); fixed4 color = tex2D(_MainTex, suv); if (color.a > 0.59-(0.08*(9-level))) { return fixed4(0,0,0,0); } float2 scaledUV = frac(uv/outSize); float2 patternUV = float2(2*scaledUV.x*outSize, outSize+scaledUV.y*outSize); fixed4 tileColor = tex2D(_TileTex, patternUV) * fixed4(color.rgb, 1.0); fixed4 outcolor = lerp(tileColor, fixed4(0,0,0,1), 1 - tileColor.a); return fixed4(outcolor.rgb, 1); } 

入力パラメーター:


アイデアは次のとおりです。最終画像の各ピクセルについて、ツリーの最小ブロックから大きなブロックに(下から上に)渡す必要があります。

エラーが指定された値(しきい値)より大きくない場合:

 if (color.a > 0.59-(0.08*(9-level))) //      . 

次に、 _TileTexテクスチャから目的の画像を表示します。

その結果、必要なレベルを超えるために、サイクルなしで条件をハードライトする必要がありました。

 fixed4 value = float4(0,0,0,0); value = getLevelColor(7, vo.uv, float2(0.005208333333, 0.0078125), 0.5); if (value.a > 0) { return value; } value = getLevelColor(6, vo.uv, float2(0.01041666667, 0.015625), 0.25); if (value.a > 0) { return value; } value = getLevelColor(5, vo.uv, float2(0.02083333333, 0.03125), 0.125); if (value.a > 0) { return value; } value = getLevelColor(4, vo.uv, float2(0.04166666667, 0.0625), 0.0625); if (value.a > 0) { return value; } value = getLevelColor(3, vo.uv, float2(0.08333333333, 0.125), 0.03125); if (value.a > 0) { return value; } value = getLevelColor(2, vo.uv, float2(0.1666666667, 0.25), 0.015625); if (value.a > 0) { return value; } value = getLevelColor(1, vo.uv, float2(0.3333333333, 0.5), 0.0078125); if (value.a > 0) { return value; } return value; 

出力では、目的の結果が得られます。



エピローグ


その結果、25〜30以上のフレームレートの弱いモバイルデバイスでシェーダーが動作するようになりました。 この記事では、「純粋な」色での画像処理について触れましたが、さらに進んで色偏差エラーの計算をより難しくすることができます。その後、複雑なテクスチャを持つ「ノイズの多い」画像でこのスクリプトを使用できます。

例(44秒から):



私は記事の多くの魔法の数字を謝罪したかった。 ご清聴ありがとうございました!

使用した資料へのリンク:

Source: https://habr.com/ru/post/J280674/


All Articles