ShadowToy-Smooth Mouse Drawing

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 内置全局变量

posted @ 2024-04-01 15:35  KKKKevin  阅读(11)  评论(0编辑  收藏  举报