阴阳图

此图使用纯shader代码画出来的,我很好奇,是怎么做到的,代码也很精炼不多

yin yang ShaderToy地址

Unity Shader

Shader"Yin Yang"{
    Properties{

    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #include "UnityCG.cginc"
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed2 p = 1.1 * (2.0 * i.uv-1);

                fixed h = dot(p,p);
                fixed d = abs(p.y)-h; 
                fixed a = d - 0.23;
                fixed b = h - 1.00;
                fixed c = sign(a*b*(p.y+p.x + (p.y-p.x)*sign(d)));

                c = lerp( c, 0.0, smoothstep(0.98,1.00,h));
                c = lerp( c, 0.6, smoothstep(1.00,1.02,h));
                return  fixed4( c, c, c, 1.0 );
            }ENDCG
        }
    }
}

分析

vert阶段我们在漫反射(六)中已经提到了,画太极阴阳图的操作,主要是在frag操作中,必须逐像素才能完成,逐顶点操作是完成不了的

1. 从集合[0,1]到集合[-1,1]

(2.0 * i.uv-1)实际上是将[0,1]范围内的连续数值。变成了[-1,1]的连续数值
最终*1.1 将结果放大了1.1倍 fixed2 p = 1.1 * (2.0 * i.uv-1);

2. 斜边的平方

dot(p,p)实际和p.x*p.x + p.y*p.y结果是一样的数值

3. 圆内外取正负

观察d = 0.5 - (0.5*0.5 + 0.5*0.5)这时正好是d = 0,意思就是当xy都为0.5时,sign(d)处于正负之间的临界点,如果此时我们将0.5认为是一个圆的半径,那么就有以下几何意义了
几何意义: 半径为0.5的圆内d为负,圆外d为正
fixed d = abs(p.y)-h;

4.画太极阴阳图

sign(a*b*(p.y+p.x + (p.y-p.x)*sign(d)))

我还原了一下这个公式:

(|y| - x*x + y*y) * (x*x + y*y - 1) * (x + y + (y-x)*(圆内-1或圆外1))

4.1 画圆

我先改了一下代码,先画圆,代码很容易理解:圆处h为0(黑色),离圆心越远,越接近1(白色)

return  fixed4( h, h, h, 1.0 ); // return  fixed4( c, c, c, 1.0 );
fixed2 p = 1.1 * (2.0 * i.uv-1);
此时为什么1.1倍xy大小也能理解了,p的值其实是放大了xy的值也就是放大平方和,圆半径平方越大时圆看上去越小,因为平方后大于1的颜色都最终为白色了

4.2 画灰底实心描黑边的白心圆

不直接使用h作为颜色,我们改用处理后的h,即c来做颜色值,如下:

 fixed c = sign(h);//sign(a*b*(p.y+p.x + (p.y-p.x)*sign(d)))
c = lerp( c, 0.0, smoothstep(0.98,1.00,h));
c = lerp( c, 0.6, smoothstep(1.00,1.02,h));
return  fixed4( c, c, c, 1.0 );

lerp:lerp( c, 0.0, h),h的范围被限定在1到0之间。函数将h按0,1逐渐返回c到0的值
    函数原型为:Lerp(a,b,t);内部实现为y = b+(1-t)*a;其中t被clamp在了[0,1],也就是说最小为0,最大为1.
smoothstep:Hermite插值,圆出现边的关键函数,具体可以去查看这个插值的绘图
    函数原型为: t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
     return t * t * (3.0 - 2.0 * t);

smoothstep的返回值:

返回值  条件
0       x < a < b 或 x > a > b
1       x < b < a 或 x > b > a
某个值  根据x在域 [a, b] (或者[b, a])中的位置, 返回某个在 [0, 1] 内的值

理解两次lerp和smoothstep的使用:

圆内白色,圆外灰色是因为两次lerp
第一次lerp时:p点离圆越远则越黑(接近lerp第二个参数),p点离圆越近越白(接近lerp第一个参数c,此时的第一参数为1),由于lerp之前h使用smoothstep处理,所以第一次lerp时,lerp( c, 0.0, smoothstep(0.98,1.00,h))将渐变的范围限定在了[0.98,1.00]范围,其他地方要么黑,要么白,在[0.98,1.00]范围内时,取白到黑渐变
第二次lerp时:将第一次lerp远离圆心的p点由黑改灰(0.6)(因为smoothstep的限制,其他非[1.00,1.02]范围内,要么灰色,要么白色),在[1.00,1.02]范围内时,取黑到灰渐变

4.3 画太极阴阳

我们先画一个太极阴阳图,不带鱼眼的那种

fixed4 frag(v2f i) : SV_Target
{
    // (2.0 * i.uv-1)实际上是将[0,1]范围内的连续数值。变成了[-1,1]的连续数值
    // 1.1 先放大了1.1倍
    fixed2 p = 1.1 * (2.0 * i.uv-1);

    // 这里dot P的点积,可能不是取的几何意义的点积的意思,而是获取p.x*p.x + p.y*p.y的数值平方和
    // fixed h = dot(p,p);
    fixed h = p.x*p.x + p.y*p.y;
     // 观察d = 0.5 - (0.5*0.5 + 0.5*0.5)这时正好是d = 0,意思就是当xy都为0.5时,sign(d)处于正负之间的临界点
     // 几何意义: 半径为0.5的圆内d为负,圆外d为正
    fixed d = abs(p.y)-h; 
    // d集合为[0,2]直接减少0.23,  a则为[-0.23, 1.77]
    // fixed a = d - 0.23;
    // b取斜边h的平方减去1,实际为[0,2]缩小为[-1,1]
    // fixed b = h - 1.00;
    //  (p.y-p.x)*sign(d), 圆内*-1 ???
    //  (|y| - x*x + y*y) * (x*x + y*y - 1) * (x + y + (y-x)*(圆内-1或圆外1))
    fixed c = sign((p.y+p.x +  (p.y-p.x)*sign(d)));

    // 两次lerp画出来灰底黑描边的白心圆,注意smoothstep和lerp两处内置shader函数的使用技巧
    c = lerp( c, 0.0, smoothstep(0.98, 1.00, h));//smoothstep(0.98,1.00,h));
    c = lerp( c, 0.6, smoothstep(1.00,1.02,h));

    return  fixed4( c, c, c, 1.0 );
}

我通过调试以下代码发现了一些情况

fixed c = 1; // 圆白色
fixed c = -1;// 圆黑色
fixed c = sign(p.x);// 圆左黑右白
fixed c = sign(p.y);// 圆上黑下白
fixed c = sign(p.y+p.y);// 圆斜着的一黑一白
fixed c = sign(p.x*p.y);// 圆四均分黑白(黑白相间)
重要:c这个值, 在两次lerp的过程中,有一个很重要的特征, 那就是可以控制圆的黑白,因为lerp两次时,完全限制了c在圆边,圆外的作用,所以圆圈里,c的值,1则为白色,-1则为黑色,如果稍加对c的值施加控制, 那么可以生成我们想要的图形了

公式分解:
!!!注意因为只取正负值绘制像素,所以数值大小是没有意义的,仅符号有意义(正就是白,负就是黑)

sign((abs(p.y)-p.x*p.x - p.y*p.y - 0.23)*(p.x*p.x + p.y*p.y - 1.00)) * sign( sign(abs(p.y)-p.x*p.x - p.y*p.y));
进一步拆解:
1. (p.x*p.x + p.y*p.y - 1.00): 将所有像素点进行反色
2. sign((abs(p.y)-p.x*p.x - p.y*p.y - 0.23): 画了两个鱼眼
3. sign( sign(abs(p.y)-p.x*p.x - p.y*p.y)): 这里画了鱼眼的外同心圆

这里画了4个圆,稍加看一下上面的公式,我们就能得知4个圆的范围是怎么限定的出来的

最后一步:

1. sign( sign(abs(p.y)-p.x*p.x - p.y*p.y)*(p.y-p.x) + p.y + p.x); 
2. sign( sign(abs(p.y)-p.x*p.x - p.y*p.y));
1是最终公式,仅仅比2的值多处理了一个乘(p.y-p.x)和加p.y + p.x的操作
A反色操作:乘(p.y-p.x)是将整体颜色沿着p.y-p.x线,降线下方也就是赋值的时候,取了个反色
加p.x再加p.y是为了限制A的反色操作,向圆心靠拢,这个操作只对鱼眼的外圆进行即可

可以看反色,+p.y后的变化,最后是结果

1.沿着y=-x的线,将线下方进行反色

2.+p.y缩小反色区域

3.最终结果

sign( 两个鱼眼的同心圆 *(p.y-p.x) + p.y + p.x);