NEUROMANTIC

自分でC/C++/UE4/Graphics/ゲームなどをやったことをメモするブログ

ShaderToyで線が引きたくて実装・メモを書いてみた。

ShaderToy又はKodeLifeなど、glslを使っているところでシェーダーで線が引きたくて実装をやってみました。

私が作った線分レンダリング関数は、

  • 線分の太さが調整可能
  • 線分の位置を任意に配置可能
  • 線分の色を単色かグラデーションで指定可能

の機能を持っています。

実装まではShaderToy特有のガクガクしているバグがあって、それの調整で時間がかかりました。
ちなみに線分を利用して、時計っぽく描画させるようにしました。やったね(時間帯はUTC+0です)

説明

X、Y軸に平行する線のレンダリング

vec3 DrawLine(float pos, float line, float intensity, vec3 col, vec3 compColor)
{
    float lineOffset = step(0.2, WeightSmoothV(line, intensity, pos));
    return mix(col, compColor, lineOffset);
}
  • DrawLine関数はとある軸の値をインプットとして受け取り、指定した部分の軸の位置に線を引く関数です。 線を引く位置はlineの値で決まります。compColorは以前までのレンダリングしていたピクセルの色(fragColor)のRGB部分などが入ります。
  • この関数ではWeightSmoothVという関数を呼び出して、posの値がintensityが加わったline値にどの程度近いかを[0, 1]のWeight値で返します。 最後はmixですが、lineOffsetは0か12つの中で一つとなるので、線を引くとしたらcolがリターン値として選ばれます。
float WeightSmoothV(float o, float scale, float p)
{
    return 
        smoothstep(o - scale / 2.0, o - scale, p) 
      + smoothstep(o + scale / 2.0, o + scale, p);
}
  • WeightSmoothV関数は大まかに言うと、以下の図でのリターン値の推移を持ちます。smoothstep関数は[0, 1]の値しか持たない、またインプットによって推移が反転するという点を活用して、スムースにVを描くようにしました。

f:id:neuliliilli:20190524174020p:plain
`WeightSmoothV`関数の大まかなグラフ推移

result = DrawLine(uv.y, 0.f, .004f, vec3(1), result);
result = DrawLine(uv.x, 0.f, .002f, vec3(1), result);
  • スクリーンの原点を元としてX軸・Y軸に白い線を描画します。スクリーンのサイズによってちゃんと出てない可能性があります。

スクリーン領域で任意の線分のレンダリング

Main関数

result = DrawLineExt2(vec2(0.0, -0.25), vec2(0, 0.25), 
                      vec3(.2, .2, .2), vec3(1, 1, 1),
                      .25f, uv, result);
// ...
result = DrawLineExt2(vec2(0, 0), Rotate(vec2(0.25, 0.4), iTime), 
                      vec3(0, 0, 1), vec3(1, 1, 0),
                      .008f, uv, result);
result = DrawLineExt2(vec2(0, 0), Rotate(vec2(-0.3, -0.25), iTime * 0.75),
                      vec3(1, 0, 0), vec3(0, 1, 0),
                      .016f, uv, result);
// ...
  • DrawLineExt2関数で任意の線分を任意のカラーグラデーション、そして任意の太さでレンダリングします。ShaderToyのコードとはちょっと違うかもしれませんが、上のコードでは単純にiTime時間変数で反時計回りで線分を回しています。
  • この関数もDrawLineのように以前のレンダリングした色変数を必要とします。

DrawLineExt2関数

vec3 DrawLineExt2(vec2 start, vec2 end, vec3 colStart, vec3 colEnd, float intensity, vec2 pos, vec3 compColor);
  • DrawLineExt2関数は「線分の両端の位置」、「線分の両端の色」、「太さ」、「現在レンダリングするポイント」、「以前の色」を引数として持ちます。
  • DrawLineExt2は以下の順番で色を塗るか、ないかを判断します。
    1. 線分のサイドベクトルを計算する。これでintensityと掛け算して線分が塗られる領域の角の4つのポイントの座標を求む。
    2. 最終レンダリング領域の線分4つに対してUV位置をインプットし、領域の中にあるかを判断する。
    3. ピタゴラスの定理を用いて、[0, 1]のmixオフセット値を計算する。
    4. mixをし、UV座標に塗られる色を設定しながら、UV座標が領域に入っていない場合には以前の色をリターンする。
vec4 upLine = vec4(start, end) - vec4(side, side) * intensity;
vec4 dnLine = vec4(end, start) + vec4(side, side) * intensity;
vec4 reLine = vec4(upLine.zw, dnLine.xy);
vec4 leLine = vec4(dnLine.zw, upLine.xy);

// Check pos is inside of area. using intersection checking.
// Dirty Way... Is there anyone who can improve this?
float intersected = 0.0;
intersected += IsDown(upLine, pos);
intersected += IsDown(dnLine, pos);
intersected += IsDown(reLine, pos);
intersected += IsDown(leLine, pos);
float isInside = mod(intersected, 2.0);
  • 各線分の両端のXYポイントをvec4に格納します。これでIsDownという関数を使用して、左から右へと放つ光線から見てposが線分のY領域に入りながらも下に位置しているかを判断します。(だったらIsDownじゃなくてIsRightが良かったのでは)
  • IsDownのアルゴリズムはリンクのものを使用しました。
  • isInsideは領域にposポイントが入っているかを示します。1だったら入っている状態を、0だったら入ってない状態を示します。
// Get lerp color with trigonometric function.
// pos * isInside is important to avoid artifact dot line.
float hypot  = length(end - pos);
float dist   = DistLineToPoint(start, normalize(end - start), pos * isInside);
float offset = sqrt((hypot * hypot) - (dist * dist)) / length(end - start);
    
// Mix.
vec3 pointCol = mix(colEnd, colStart, offset) * isInside;
return mix(compColor, pointCol, isInside);
  • ピタゴラスの定理を使用して、ポイントから線分までの最短距離、endまでの距離を求め、残り一つの部分線分の長さを計算します。求めた値は標準化されてoffsetとして扱います。
  • 最終のカラー値を計算するためにmixを使用します。* isInsideはShaderToyのレンダリングバグを回避するために付けられたものとなります。
  • isInsideは0か1になります。mixを使う時には、最初の引数の掛け算の係数は(1 - \text{isInside})となることにご注意を。
  • DistLineToPointは以前に説明した関数を使い回ししました。今のシーンではvec3ではなく、vec2です。しかしGLSLはちゃんとオーバーローディングしてくれますので別の名前で作ったりすることはありませんね。

IsDown関数

float IsDown(vec2 start, vec2 end, vec2 pos)
{
    // Compare point.Y is in [start.Y, end.Y]
    float insideY = step(abs((end.y - pos.y) + (start.y - pos.y)), abs(end.y - start.y));

    // Get intersection.
    float dx   = end.x - start.x;
    float grad = float(end.y - start.y) / dx;
    float resX = abs(dx) > 0.0 ? (pos.y + grad * start.x - start.y) / grad : end.x;
    return float(resX < pos.x) * insideY;
}
  • リンクのアルゴリズムを使いました。しかし、もしかしてdxが0の場合に対して、最終のリターン値がINF(無限)になることを防ぐためにresXを記録する時にはdxが0であればただのx座標を返すようにしました。