この記事はシェーダーアドベントカレンダー18日目の記事として書かれています。
https://qiita.com/advent-calendar/2019/shader-advent-calender-2019
#はじめに
シェーダーの数値って色として見ることは多いのだけど、普通(?)のプログラムのように数字として見ることは少ないと思います。
VR空間でVJをするときに、パラメータの値を数字として目で見たかったので数値を数字として表示するシェーダーを作りました。
今回はその数字シェーダーを何を考えながらどういう風に作っていったか書きたいと思います。
完成したサンプル。機能として桁数指定、マイナス表示など。
パラメータの数値を数字として表示するシェーダーを書いた~。小数点以下の深いところの動きは怪しいけど。シェーダーではじめてまともにfor文使ったかもしれない。#Unity #Shader pic.twitter.com/Sw9PncoCzU
— noriben🌓 (@noriben327) October 23, 2019
-100から100まで表示してみた動画。
数字シェーダー完成図(アドベントカレンダー記事用)
— noriben🌗 (@noriben327) December 17, 2019
-100から100まで表示してみる。あと桁数を増やしてみたり。#Unity #Shader pic.twitter.com/MjZa0UUsaP
完成したコードとテクスチャです。
https://github.com/noriben327/DisplayNumber/blob/master/DisplayNumber.shader
#既に数字シェーダーを作っている方々
同じようなことを思っている方はたくさんいて、シェーダーを配布している方もいます。
BUTADIENE WORKS
ブタジエンさん
https://twitter.com/butadiene121/status/1063451198194413568
シェーダ内の任意の数値を任意の桁数を指定して表示するシェーダ(というか関数とテクスチャの組み合わせ)できた~ pic.twitter.com/IvxuqsFs4Z
— ブタジエン (@butadiene121) October 27, 2018
Position Shader
オノッチさん
https://onotchi.booth.pm/items/996827
座標シェーダー進捗
— オノッチ (@onotchi_) August 29, 2018
・背景画像を任意で設定可能に
・全体の透過率を設定可能に
もちろん数字テクスチャも好きなのが使えます。
基本機能はこれで十分かな。特に問題が見つからなかったら、今晩でもリリースします。 #VRChat pic.twitter.com/B66tj1KZTM
少しコードを読んでみたものの自分にはわからなかったので、1から考えながら作ることにしました。
#基本の考え
数字を書いたテクスチャを用意して、例えば数値が1ならテクスチャの1が書いてある部分にスクロールして表示する。これだけです。
今回作った数字のテクスチャはこれです。
Y座標を10分割して数字を書いているので、uv
がfloat2(0, 0.1)
で1
、float2(0, 0.7)
で7
のテクスチャにそのままスクロールできます。
説明用に2桁で、隠している部分も表示してみたもの。数値にあわせてUVスクロールしてるのがわかるんじゃないかな~と思います。0.0から2.0を表示させています。
数字シェーダー説明図(アドベントカレンダー記事用)
— noriben🌗 (@noriben327) December 17, 2019
2桁だけで表示。説明用に隠していた部分も表示。数値にあわせてUVスクロールしてる。#Unity #Shader pic.twitter.com/kJRdqSdaFp
#数値の抽出
数値そのままではどうにも扱えないので、1桁ずつ切り取って0~9の値にして、1/10にすることで0.0~1.0のUV値として使える値にする。それを全桁に実行することで最終的に数字として表示する。
今回は整数部と小数部を分けてfor
文にして処理した。
###整数部の抽出
まず1の位だけやってみる。
-
数値は仮に123.141592とする。
-
1/10する。
-
12.3141592
-
frac
で小数点のみにする。 -
0.3141592
-
10倍する。
-
3.141592
-
floor
で小数点以下を切り捨てる。 -
3
これで1の位の数値である3のみを取り出せる。
あとは1/10して0.3とすれば0~1の範囲になるのでそのままUVの値として使えるようになる。
実際のコード。
for(j = 1; j < _IntDigits + 1; j++)
{
multi = pow(10, j);
val = numVal;
com = val * 1 / multi;
val = frac(com) * 10;
val = floor(val) * 0.1;
uv = i.uv;
uv.y += val; //数字移動
uv.x += -0.04 + 0.05 * j; //桁移動
numCol = numCol + tex2D(_NumTex, uv);
}
###小数部の抽出
次に小数点第1位だけ取り出してみる。
-
数値は仮に123.141592とする。
-
frac
で小数点のみにする -
0.141592
-
10倍する
-
1.141592
-
floor
で小数点以下を切り捨てる -
1
これで小数点第1位の1という数字を取り出せる。
1/10すれば0.1とそのままUVの値として使える。
実際のコード。
おまじないとして元の値に0.00001
を足しているが、これは例えば0.3
が0.299999
と表示されてしまうときがある場合のとりあえずの対策である(本来は必要ない)
for(j = 0; j < _DecimalDigits; j++)
{
multi = pow(10, j);
val = numVal + 0.00001;//おまじない
com = val * multi;
val = frac(com) * 10;
val = floor(val) * 0.1;
uv = i.uv;
uv.y += val; //数字移動
uv.x -= 0.06 + 0.05 * j; //桁移動
numCol = numCol + tex2D(_NumTex, uv);
}
#負号
if
でマイナス以外のときは負号を消すようにした。
桁数の増加にあわせて常に先頭に表示するようUV.x値を移動している。(動画参照)
(_IntDigits(整数部の桁数)の1つ上の桁に負号を表示)
float2 muv = i.uv;
muv.x += -0.005 + 0.05 * (_IntDigits + 1);
#見た目を整えて完成
カンマを描画したり、見せたくない部分をマスクするなど。
#あとがき
プログラムの知識も経験も少ないのですが、それでもシェーダーを書くのは楽しいです。このコードも楽しく書けたのでせっかくなので解説の記事を書こうかな~と思ってアドベントカレンダーに参加しました。
決め打ちの数字が多かったり、数字の表示部分を関数にして他のコードで使いやすくすべきだよなーと反省点はあるものの、コードの一部の数値を確認したり、オブジェクトの座標を表示してみたり、
Unityの時間の流れ(_Time.y)を可視化してみたりと結構実用的なシェーダーとして使っています。
これは可視化されたUnityの時の流れ(_Time.y) pic.twitter.com/lj2lTEWuZM
— noriben🌗 (@noriben327) October 26, 2019
#全コード
RenderTextureの値を取得するコードが一部入っていますが、今回は_Test
の値を表示するようにしています。
Shader "Noriben/DisplayNumber"
{
Properties
{
_NumTex ("NumTex", 2D) = "white" {}
_RenderTex ("RenderTex", 2D) = "white" {}
_Index ("Index", float) = 0
_IntDigits("Int Digits", int) = 2
_DecimalDigits("Decimal Digits", int) = 3
_Test("Test", Range(-100,100)) = 0
[Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull", float) = 0
}
SubShader
{
Tags { "RenderType"="TransparentCutout" "Queue" = "AlphaTest" "DisableBatching" = "True"}
LOD 100
Pass
{
Cull [_Cull]
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _NumTex;
float4 _NumTex_ST;
sampler2D _RenderTex;
float _Test;
float _Index;
int _IntDigits;
int _DecimalDigits;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _NumTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//数値取得
float4 renderTex = tex2D(_RenderTex, float2((_Index + 0.5) * 0.1, 0.5));
//表示位置をセンターにオフセット
i.uv.x += -0.37;
//カンマの描画
float left = step(0.076, i.uv.x);
float right = 1 - step(0.085, i.uv.x);
float bottom = step(0.0234, i.uv.y);
float top = 1 - step(0.033, i.uv.y);
float comma = left * right * top * bottom;
//数字表示
//float4 objPos = mul ( unity_ObjectToWorld, float4(0, 0, 0, 1));
//float minusCheck = renderTex.x;
float minusCheck = _Test;
//float minusCheck = -_Time.y; //表示する数値
float numVal = abs(minusCheck);
fixed4 numCol = fixed4(0,0,0,0);
float multi, val, com;
float2 uv = i.uv;
//1未満
int j = 0;
for(j = 0; j < _DecimalDigits; j++)
{
multi = pow(10, j);
val = numVal + 0.00001;//おまじない
com = val * multi;
val = frac(com) * 10;
val = floor(val) * 0.1;
uv = i.uv;
uv.y += val; //数字移動
uv.x -= 0.06 + 0.05 * j; //桁移動
numCol = numCol + tex2D(_NumTex, uv);
}
//1以上
for(j = 1; j < _IntDigits + 1; j++)
{
multi = pow(10, j);
val = numVal;
com = val * 1 / multi;
val = frac(com) * 10;
val = floor(val) * 0.1;
uv = i.uv;
uv.y += val;
uv.x += -0.04 + 0.05 * j;
numCol = numCol + tex2D(_NumTex, uv);
}
//負号
float2 muv = i.uv;
muv.x += -0.005 + 0.05 * (_IntDigits + 1);
float mleft = step(0.076, muv.x);
float mright = 1 - step(0.1, muv.x);
float mbottom = step(0.044, muv.y);
float mtop = 1 - step(0.054, muv.y);
float mcol = mleft * mright * mbottom * mtop;
//0以上のときは負号消す
if(minusCheck >= 0)
{
mcol = 0;
}
//mix
fixed4 col = numCol;
col += float4(comma, comma, comma, 1);
col += float4(mcol, mcol, mcol, 1);
col = clamp(col, 0, 0.8); //明るさちょっとさげる
//使わない部分黒塗り
top = 1 - step(0.1, i.uv.y);
float3 black = float3(top, top, top);
col *= float4(black.xyz, 1);
//黒部分透明化
clip(col.x - 0.5);
return col;
}
ENDCG
}
}
}