[Unity] サークルプログレスバーをシェーダで書いてみる


モバイルのVRではキーボードやマウスなどが使えないケースが多く、視点による選択を行うことが少なくありません。
ただ、一瞬見ただけで選択されてしまうと問題があるので、一定時間見つめたのちに選択、ということをします。

そして見つめた時間の経過を円形のプログレスバーで表示しようと思ったんですが、テクスチャで行うと荒れてしまったり太さなどを自由に変更できなかったのでシェーダだけで実現しようと書いたのが今回の内容です。

ちなみに実行結果はこんな感じになります↓

透明の扱いを変更する

まず、透明度を物理ベースのレンダリングで行わないように変更する必要があります。
(Unity5以降では透明オブジェクトは現実世界のプラスチックやガラスのように、透明だけどライティングの影響を受けて、「そこに物体がある」ことが分かるようにレンダリングされるためです)

そのためには以下のようにTags#pragmaを少し変更します。

Tags { "RenderType"="Opaque" "Queue"="Transparent" }

// ... 中略

#pragma surface surf Standard fullforwardshadows alpha:fade

"Queue"="Transparent"alpha:fadeを追加しました。

シェーダを書く

さて、ここが本題のところです。
考え方はテクスチャの中心からの角度と距離を元に計算を行います。

それぞれの計算の意味についてはコメントに記載しています。

サーフェースシェーダ部分

以下はサーフェースシェーダ部分です。

void surf (Input IN, inout SurfaceOutputStandard o) {
     // 中央からの位置を-1 〜 1の範囲に正規化
    float2 pos = (IN.uv_MainTex - float2(0.5)) * 2.0;

    // 上記の位置から角度を算出(0〜1の範囲)
    // また今回は上端から(つまり90度)のところから時計回りに徐々に透明にしたいので
    // 90度だけ回転した状態に変換しておく
    float angle = (atan2(pos.y, pos.x) - atan2(1.0, 0.0)) / (PI * 2);

    // atan2では-180〜180度の値が返されるので、0〜360になるように正規化する
    // 角度を0〜1にしているため、実質足すのは1。
    if (angle < 0) {
        angle += 1.0;
    }

    float len = length(pos);

    // 縁の長さ。極端に短いとエッジがギザギザになる
    float edge = 0.03;
    float width = 1 - _Width;

    // サークルの内円
    float inner = smoothstep(width, width + edge, len);

    // サークルの外円
    float outer = smoothstep(1.0 - edge, 1.00, len);

    // 上記を合算して円のための数値を計算する
    float opaque = inner - outer;

    fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;

    o.Albedo = c.rgb;
    o.Metallic = 0;
    o.Smoothness = 0;

    // 指定された`cutoff`の値がしきい値以上の場合のみ透明度を有効にする
    float cutoff = angle < _Cutoff ? 0 : 1;
    o.Alpha = _Color.a * opaque * cutoff;
}

全体のコード

プロパティなど、全体のコードを載せておきます。

[2016.02.01追記]

最初に公開していたコードだと、Unityの標準のライティングが適用されてちょっと色味がおかしなことになるので若干
修正しました。

具体的には #pragma でのライティングの指定の変更とライティング関数の追加です。

Shader "Custom/CircleProgress" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Cutoff ("Cutoff", Range(0, 1)) = 1.0
        _Width ("Width", Range(0, 1)) = 0.6
    }
    SubShader {
        Tags { "RenderType"="Opaque" "Queue"="Transparent" }
        LOD 200

        CGPROGRAM

        // カスタムの「Original」ライティング関数を指定
        #pragma surface surf Original fullforwardshadows alpha:fade

        #pragma target 3.0

        static const float PI = 3.14159265f;

        sampler2D _MainTex;

        struct Input {
            float2 uv_MainTex;
        };

        fixed4 _Color;
        float _Cutoff;
        float _Width;

        // カスタムライティングを適用する
        half4 LightingOriginal (SurfaceOutput s, half3 lightDir, half atten) {
            // ライティングの影響を受けさせないため、受け取った色情報をそのまま返す
            return half4(s.Albedo, s.Alpha);
        }

        void surf (Input IN, inout SurfaceOutput o) {
            float2 pos = (IN.uv_MainTex - float2(0.5, 0.5)) * 2.0;
            float angle = (atan2(pos.y, pos.x) - atan2(1.0, 0.0)) / (PI * 2);

            if (angle < 0) {
                angle += 1.0;
            }

            float len = length(pos);
            float edge = 0.03;
            float width = 1 - _Width;
            float inner = smoothstep(width, width + edge, len);
            float outer = smoothstep(1.0 - edge, 1.00, len);
            float opaque = inner - outer;

            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            float cutoff = angle < _Cutoff ? 0 : 1;
            o.Alpha = _Color.a * opaque * cutoff;
        }
        ENDCG
    } 
    FallBack "Diffuse"
}