ShadowToy-Smooth Mouse Drawing 源码分析
简概
Smooth Mouse Drawing 源码分析学习。
源码
Image
// A recreation of https://lazybrush.dulnan.net/
// Controls:
// - Mouse to draw
// - L: toggle between quadratic bezier curves and line segments
// - S: toggle SDF visualisation
// - P: toggle mouse points
// Settings in Buffer B
// Modified sdBezier() function originally from
// Quadratic Bezier SDF With L2 - Envy24
// https://www.shadertoy.com/view/7sGyWd
#define LINE_WIDTH (iResolution.y * 0.01)
#define POINT_RADIUS (iResolution.y * 0.007)
const int KEY_L = 76;
const int KEY_S = 83;
const int KEY_P = 80;
bool keyToggled(int keyCode) {
return texelFetch(iChannel1, ivec2(keyCode, 2), 0).r > 0.0;
}
// 即前景颜色与背景颜色的混合后的透明度。
vec4 blendOver(vec4 front, vec4 back) {
float a = front.a + back.a * (1.0 - front.a);
return a > 0.0
? vec4((front.rgb * front.a + back.rgb * back.a * (1.0 - front.a)) / a , a)
: vec4(1.0);
}
void blendInto(inout vec4 dst, vec4 src) {
dst = blendOver(src, dst);
}
void mainImage(out vec4 fragColor, vec2 fragCoord) {
fragColor = vec4(1.0);
// 二次贝塞尔曲线 SDF值
float qd = texture(iChannel0, fragCoord / iResolution.xy).x;
// 线段 SDF值
float ld = texture(iChannel0, fragCoord / iResolution.xy).y;
// 鼠标点 SDF值
float pd = texture(iChannel0, fragCoord / iResolution.xy).z;
// 根据按键状态选择用什么
float sd = (keyToggled(KEY_L) ? ld : qd) - LINE_WIDTH / 2.0;
// 将 sd 作为透明度与现在的颜色混合,也就是说,距离越近, sd 越小, 0.5-sd 越大,图像越不透明
blendInto(fragColor, vec4(0.0, 0.0, 0.0, clamp(0.5 - sd, 0.0, 1.0)));
if (!keyToggled(KEY_S)) {
float spacing = iResolution.y * 0.02;
float thickness = max(iResolution.y * 0.002, 1.0);
float opacity = clamp(
0.5 + 0.5 * thickness -
abs(mod(sd - (spacing - thickness) * 0.5, spacing) - spacing * 0.5),
0.0, 1.0
) * 0.5 * exp(-sd / iResolution.y * 8.0);
blendInto(fragColor, vec4(0.0, 0.0, 0.0, opacity));
}
if (keyToggled(KEY_P)) {
blendInto(fragColor, vec4(1.0, 0.0, 0.0, 0.0));
}
}
blendInTo and blendOver
vec4 blendOver(vec4 front, vec4 back) {
// `front.a` 是前景颜色的 alpha 值,表示前景颜色的透明度。
// `back.a` 是背景颜色的 alpha 值,表示背景颜色的透明度。
// `(1.0 - front.a)` 表示前景颜色的不透明度,
// 即前景颜色的 alpha 值的补数,表示背景颜色中不受前景颜色影响的部分。
// `back.a * (1.0 - front.a)` 表示背景颜色中不受前景颜色影响的部分的透明度。
// `front.a + back.a * (1.0 - front.a)` 表示合成后的颜色的 alpha 值,
// 即前景颜色与背景颜色的混合后的透明度。
float a = front.a + back.a * (1.0 - front.a);
// 如何混合之后的透明度大于0,也就是有不透明的显示,那么取混合后的颜色,否则取黑色
return a > 0.0
// 这部分是关于颜色的感知,颜色如何按照透明度划分的方式来显示,透明度也可以划分么?
// 仔细想想也不是不可能。
// 总之,混合一个颜色时,要保证颜色和透明度都是混合之后的,a是混合后的透明度,颜色要对应加权
// 得到“混合后”的颜色。
? vec4((front.rgb * front.a + back.rgb * back.a * (1.0 - front.a)) / a , a)
: vec4(1.0);
}
BufferB
// This buffer maintains the SDF for the drawing.
// .x: SDF with quadratic bezier curves
// .y: SDF with linear segments
// .z: SDF for mouse points
float sdSegment(vec2 p, vec2 a, vec2 b) {
vec2 ap = p - a;
vec2 ab = b - a;
// clamp(dot(ap, ab) / dot(ab, ab), 0.0, 1.0) 投影向量的长度,即||ap -> ab||
// ab * clamp(dot(ap, ab) / dot(ab, ab), 0.0, 1.0) 长度*方向,得到投影向量 ap -> ab
// distance(ap, ab * clamp(dot(ap, ab) / dot(ab, ab), 0.0, 1.0)); 得到 p 到 ab 的垂直距离
return distance(ap, ab * clamp(dot(ap, ab) / dot(ab, ab), 0.0, 1.0));
}
void mainImage(out vec4 fragColor, vec2 fragCoord) {
float qd = 1e30;
float ld = 1e30;
float pd = 1e30;
// 取上一帧的二次贝塞尔值,线段值,鼠标点距离
if (iFrame != 0) {
qd = texelFetch(iChannel1, ivec2(fragCoord), 0).r;
ld = texelFetch(iChannel1, ivec2(fragCoord), 0).g;
pd = texelFetch(iChannel1, ivec2(fragCoord), 0).b;
}
// 鼠标在前两帧的位置
vec4 mouseA = iFrame > 0 ? texelFetch(iChannel0, ivec2(0, 0), 0) : vec4(0.0);
vec4 mouseB = iFrame > 0 ? texelFetch(iChannel0, ivec2(1, 0), 0) : vec4(0.0);
// 鼠标现在帧的位置
vec4 mouseC = iFrame > 0 ? texelFetch(iChannel0, ivec2(2, 0), 0) : iMouse;
// A: mouse from previous previous frame
// B: mouse from previous frame
// C: mouse from this frame
mouseA.xy += 0.5;
mouseB.xy += 0.5;
mouseC.xy += 0.5;
// 鼠标点
// 现在,鼠标按键按下
if (mouseC.z > 0.0) {
pd = min(pd, distance(fragCoord, mouseC.xy));
}
// 线段
// 现在,鼠标按下,且上一帧按下
if (mouseB.z > 0.0 && mouseC.z > 0.0) {
// 三个位置:当前纹理位素,上一帧鼠标位置,当前鼠标位置
// ld 为当前像素点到鼠标移动方向的距离
ld = min(ld, sdSegment(fragCoord, mouseB.xy, mouseC.xy));
} else if (mouseC.z > 0.0) {
// ld 为当前像素点到鼠标位置的距离
ld = min(ld, distance(fragCoord, mouseC.xy));
}
// 二次贝塞尔曲线
// 现在,鼠标按下,且上一帧未按下
if (mouseB.z <= 0.0 && mouseC.z > 0.0) {
qd = min(qd, distance(fragCoord, mouseC.xy));
} else if (mouseA.z <= 0.0 && mouseB.z > 0.0 && mouseC.z > 0.0) {
// 现在按下,上一帧按下,上上帧未按下
qd = min(qd, sdSegment(fragCoord, mouseB.xy, mix(mouseB.xy, mouseC.xy, 0.5)));
} else if (mouseA.z > 0.0 && mouseB.z > 0.0 && mouseC.z > 0.0) {
// 三帧全部按下
qd = min(qd, abs(sdBezier(fragCoord, mix(mouseA.xy, mouseB.xy, 0.5), mouseB.xy, mix(mouseB.xy, mouseC.xy, 0.5))));
} else if (mouseA.z > 0.0 && mouseB.z > 0.0 && mouseC.z <= 0.0) {
// 现在松开,上一帧按下,上上帧按下
qd = min(qd, sdSegment(fragCoord, mix(mouseA.xy, mouseB.xy, 0.5), mouseB.xy));
}
// 保存
fragColor.r = qd;
fragColor.g = ld;
fragColor.b = pd;
}
BufferA
// This buffer tracks smoothed mouse positions over multiple frames.
// See https://lazybrush.dulnan.net/ for what these mean:
#define RADIUS (iResolution.y * 0.015)
#define FRICTION 0.05
void mainImage(out vec4 fragColor, vec2 fragCoord) {
if (fragCoord.y != 0.5 || fragCoord.x > 3.0) {
return;
}
if (iFrame == 0) {
if (fragCoord.x == 2.5) {
fragColor = iMouse;
} else {
fragColor = vec4(0.0);
}
return;
}
vec4 iMouse = iMouse;
const float magic = 1e25;
if (iMouse == vec4(0.0)) {
float t = iTime * 3.0;
iMouse.xy = (vec2(cos(3.14159 * t) + sin(0.72834 * t + 0.3), sin(2.781374 * t + 3.47912) + cos(t)) * 0.25 + 0.5) * iResolution.xy;
iMouse.z = magic;
}
vec4 mouseA = texelFetch(iChannel0, ivec2(1, 0), 0);
vec4 mouseB = texelFetch(iChannel0, ivec2(2, 0), 0);
vec4 mouseC;
mouseC.zw = iMouse.zw;
float dist = distance(mouseB.xy, iMouse.xy);
if (mouseB.z > 0.0 && (mouseB.z != magic || iMouse.z == magic) && dist > 0.0) {
vec2 dir = (iMouse.xy - mouseB.xy) / dist;
float len = max(dist - RADIUS, 0.0);
float ease = 1.0 - pow(FRICTION, iTimeDelta * 10.0);
mouseC.xy = mouseB.xy + dir * len * ease;
} else {
mouseC.xy = iMouse.xy;
}
if (fragCoord.x == 0.5) {
fragColor = mouseA;
} else if (fragCoord.x == 1.5) {
fragColor = mouseB.z == magic && iMouse.z != magic ? vec4(0.0) : mouseB;
} else {
fragColor = mouseC;
}
}
Common
// solveQuadratic(), solveCubic(), solve() and sdBezier() are from
// Quadratic Bezier SDF With L2 - Envy24
// https://www.shadertoy.com/view/7sGyWd
// with modification. Thank you! I tried a lot of different sdBezier()
// implementations from across Shadertoy (including trying to make it
// myself) and all of them had bugs and incorrect edge case handling
// except this one.
int solveQuadratic(float a, float b, float c, out vec2 roots) {
// Return the number of real roots to the equation
// a*x^2 + b*x + c = 0 where a != 0 and populate roots.
float discriminant = b * b - 4.0 * a * c;
if (discriminant < 0.0) {
return 0;
}
if (discriminant == 0.0) {
roots[0] = -b / (2.0 * a);
return 1;
}
float SQRT = sqrt(discriminant);
roots[0] = (-b + SQRT) / (2.0 * a);
roots[1] = (-b - SQRT) / (2.0 * a);
return 2;
}
int solveCubic(float a, float b, float c, float d, out vec3 roots) {
// Return the number of real roots to the equation
// a*x^3 + b*x^2 + c*x + d = 0 where a != 0 and populate roots.
const float TAU = 6.2831853071795862;
float A = b / a;
float B = c / a;
float C = d / a;
float Q = (A * A - 3.0 * B) / 9.0;
float R = (2.0 * A * A * A - 9.0 * A * B + 27.0 * C) / 54.0;
float S = Q * Q * Q - R * R;
float sQ = sqrt(abs(Q));
roots = vec3(-A / 3.0);
if (S > 0.0) {
roots += -2.0 * sQ * cos(acos(R / (sQ * abs(Q))) / 3.0 + vec3(TAU, 0.0, -TAU) / 3.0);
return 3;
}
if (Q == 0.0) {
roots[0] += -pow(C - A * A * A / 27.0, 1.0 / 3.0);
return 1;
}
if (S < 0.0) {
float u = abs(R / (sQ * Q));
float v = Q > 0.0 ? cosh(acosh(u) / 3.0) : sinh(asinh(u) / 3.0);
roots[0] += -2.0 * sign(R) * sQ * v;
return 1;
}
roots.xy += vec2(-2.0, 1.0) * sign(R) * sQ;
return 2;
}
int solve(float a, float b, float c, float d, out vec3 roots) {
// Return the number of real roots to the equation
// a*x^3 + b*x^2 + c*x + d = 0 and populate roots.
if (a == 0.0) {
if (b == 0.0) {
if (c == 0.0) {
return 0;
}
roots[0] = -d/c;
return 1;
}
vec2 r;
int num = solveQuadratic(b, c, d, r);
roots.xy = r;
return num;
}
return solveCubic(a, b, c, d, roots);
}
float sdBezier(vec2 p, vec2 a, vec2 b, vec2 c) {
vec2 A = a - 2.0 * b + c;
vec2 B = 2.0 * (b - a);
vec2 C = a - p;
vec3 T;
int num = solve(
2.0 * dot(A, A),
3.0 * dot(A, B),
2.0 * dot(A, C) + dot(B, B),
dot(B, C),
T
);
T = clamp(T, 0.0, 1.0);
float best = 1e30;
for (int i = 0; i < num; ++i) {
float t = T[i];
float u = 1.0 - t;
vec2 d = u * u * a + 2.0 * t * u * b + t * t * c - p;
best = min(best, dot(d, d));
}
return sqrt(best);
}
ShaderToy 内置成员
iMouse
iMouse
:用于获取鼠标的位置和状态信息。
vec4(x, y, z, w),其中(x, y)表示鼠标在屏幕上的坐标位置,(z, w)表示鼠标左右按键按下状态。
fragCoord
gl_FragCoord
:contains the window-relative coordinates of the current fragment
https://registry.khronos.org/OpenGL-Refpages/gl4/html/gl_FragCoord.xhtml
texelFetch
texelFetch
:在纹理中执行单个纹素的查找
if (iFrame != 0) {
qd = texelFetch(iChannel1, ivec2(fragCoord), 0).r;
ld = texelFetch(iChannel1, ivec2(fragCoord), 0).g;
pd = texelFetch(iChannel1, ivec2(fragCoord), 0).b;
}
https://registry.khronos.org/OpenGL-Refpages/gl4/html/texelFetch.xhtml
distance
distance
:calculate the distance between two points
https://registry.khronos.org/OpenGL-Refpages/gl4/html/distance.xhtml
参考资料
Smooth Mouse Drawing
Radiance Cascades
Outrun
OpenGL & Metal Shader 编程:ShaderToy 内置全局变量