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
値にどの程度近いかを]の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
関数は]の値しか持たない、またインプットによって推移が反転するという点を活用して、スムースにVを描くようにしました。
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
は以下の順番で色を塗るか、ないかを判断します。- 線分のサイドベクトルを計算する。これで
intensity
と掛け算して線分が塗られる領域の角の4つのポイントの座標を求む。 - 最終レンダリング領域の線分4つに対してUV位置をインプットし、領域の中にあるかを判断する。
- ピタゴラスの定理を用いて、]の
mix
オフセット値を計算する。 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
を使う時には、最初の引数の掛け算の係数はとなることにご注意を。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
座標を返すようにしました。