シェーダーを使って二値化を再実装しました

Boltzmanです. 魚眼でやりたいAR, 4回目です.
少しですが進捗が出たので進捗報告です.
以前二値化を実装した際にあまりに重かったので, サークル仲間の助言に従ってシェーダーを使って実装しなおすことにしました.

使ったシェーダーはCompute Shaderです. Standerd Surface Shader, Unlit Shader, Image Effect Shader, Compute Shaderのうちどのシェーダーを使うのか選ぶ時点でかなり難航しました. 様々なサイトをめぐって情報をあさって, 最終的には消去法で選択したような記憶があります.
選んだシェーダーがあっていたのかはわかりませんが, とりあえず実装はできました.
以下にコードを記します. まずはCompute Shaderのほうから

<br />
Texture2D&lt;float4&gt; Input;<br />
float1 thresh;<br />
RWTexture2D&lt;float4&gt; Result;</p>
<p>[numthreads(8,8,1)]<br />
void CSMain (uint3 id : SV_DispatchThreadID)<br />
{<br />
	//二値化<br />
	Result[id.xy] = lerp(float4(1, 1, 1, 0), float4(0, 0, 0, 0),<br />
                      ceil( clamp( -(Input[id.xy].r+Input[id.xy].g+Input[id.xy].b)/3 + thresh, 0, 1) ));<br />
}<br />

(注:ここからなぜか口調が変わりますが書いている人は同じです. 本来なら直すべきですが, どうしても気力がわかないということと, 内容に影響はないということからそのままにします. 申し訳ない. )
今見返すとlerpの1番目と2番目の引数がfloat4であるのに対して3番目の引数がfloat1になっているのでどうして動いているのか不思議である. (自動的に変換しているのだろうか).
(2016/3/5追記:こちらのページに”スカラー昇格”なる記述があり, それによるものかと考えられます)
まずuint3 idは三つの符号なし整数をパラメーターとして持つ変数であるが, このような変数はx,y,zやr,g,bなどを用いて要素を取り出すことができる.(詳細)
これから, Input[id.xy]ではid.xyによってテクスチャの座標を指定して, その座標の色を求めている.(たぶん)
Input[id.xy]の値はfloat4でrgbaらしいので, rgbの和を取って3で割って濃度値を算出し, threshからその値を引くと, threshより大きい値の時は結果が負, threshより小さな値の時は結果が正になる. これをclampすると, 負の値は0に, 正の値は0より大きく1以下の値になる. さらにceilによってこれを0と1に二分する. lerpで線形補間すると, 0のときは後ろの値, 1の時は前の値が採用され, 画像を二値化することができる.
と, 私は思っているのだが, 実際どうなってコードがうまくいっている(ように見える)のかはわからない.

次にC#スクリプト

<br />
using UnityEngine;<br />
using System.Collections;</p>
<p>public class Test : MonoBehaviour {<br />
    public ComputeShader cs;<br />
    public int width = 1080;<br />
    public int height = 720;<br />
    public int FPS = 30;<br />
    [Range(0, 1)]<br />
    public float thresh = 0.5f;</p>
<p>    private RenderTexture text;<br />
    private WebCamTexture wtex;</p>
<p>	// Use this for initialization<br />
	void Start () {<br />
        //WebCam準備<br />
        var devices = WebCamTexture.devices;<br />
        if (devices.Length &gt; 0)<br />
        {<br />
            wtex = new WebCamTexture(width, height, FPS);<br />
            wtex.Play();<br />
        }<br />
        else<br />
        {<br />
            Debug.LogError(&quot;Could not Find WebCam.&quot;);<br />
            return;<br />
        }<br />
        text = new RenderTexture(wtex.width, wtex.height, 0);<br />
        text.enableRandomWrite = true;<br />
        text.Create();<br />
        transform.localScale = new Vector3(width, 1, height);<br />
        transform.GetComponent&lt;Renderer&gt;().material.mainTexture = text;<br />
        cs.SetTexture(0, &quot;Input&quot;, wtex);<br />
        cs.SetFloat(&quot;thresh&quot;, thresh);<br />
        cs.SetTexture(0, &quot;Result&quot;, text);<br />
    }</p>
<p>	// Update is called once per frame<br />
	void Update () {<br />
        if (!SystemInfo.supportsComputeShaders)<br />
        {<br />
            Debug.LogError(&quot;Computer Shader is not Supported.&quot;);<br />
            return;<br />
        }<br />
        cs.Dispatch(0, width, height, 1);<br />
    }<br />
}<br />

cs.Set~で値を設定する. WebcamTextureを入力画像として渡し, 結果をRenderTextureで受け取っている.
cs.DispatchでCompute Shaderを実行する. (Compute Shaderの詳細)
ちゃんとRandomWriteをtrueにしておくことが重要らしいとどこかで読んだ.
(2016/02/29追記:ソースを発見したので載せます->詳細)
途中まで実装したのが今月の頭くらいで, 完成したのが2,3日前とずいぶん間が空いてしまったために, どこから引っ張ってきた情報なのかわからず大変忍びない.
やっていて感じたのは, シェーダー関係は情報を探すのが大変だということ.
シェーダーその物もそうだが, CgやHLSLの書き方も公式のリファレンスに見つけられただけでCやC++、C#と比べるとずっと調べにくかったように思う.
まあ私の見落としが多いだけなのだろうが…

前回のコードが大津の手法まで実装したものであったため, 正確な比較ではありませんが, 前回のコードのMonoBehaviourが300ms以上かかっていたのに対し, 今回のコードではCompute ShaderのDispatshは0.01ms以下となっています. 目に見えてわかるほど大きく改善しました.

次回は(必要があれば)大津の手法をシェーダーで実装することを目標にします.

Posted on