兔年就是要画一只可爱的小兔子啦

1,886 阅读7分钟

我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛

前言

2023年就是兔年啦~ 在这个兔年开始的时候当然是要画一只可爱的小兔子来保佑我们今年顺顺利利、和和美美,在文章的开头就先祝大家新年快乐!!!

在新的一年中,祝大家事业“兔”飞猛进,大展宏“兔”,前“兔”无量!

废话就不多说了,阅读完本文你将收获下面这只可爱的小兔子。

20230113-102250.gif

需要注意的是,本文主要给出实现的具体思路,但是其中涉及到的一些GLSL有关的基础知识,就不会在本文中详细介绍了,在文中会给出相应的链接,读者可以自行查看。

分析

首先我们来对这个图像进行一波简单的分析。可以很容易的看出来这幅图主要分为 “背景” + “头像” 两个部分。背景中的兔子头实际上就是前面的大的兔子头的缩小版。所以我们还是需要聚焦于头像的绘制。

image.png

通过上图我们可以看出,整个头像基本上都是由圆(椭圆)和线段构成的,所以从几何元素上来说还是比较简单的。接下来我们将逐步的实现它。

开始编码

首先,我们的运行环境当然是作者喜欢的Shadertoy!!!如果还不知道shadertoy及运行环境的小伙伴,可以查看这篇专栏里的文章。 Shader从入门到放弃 - 鹤云云的专栏 - 掘金 (juejin.cn)

轮廓 —— 头

首先我们先画一个圆。

    float y = uv.y;
    vec2 nuv = vec2(uv.x, y);
    float d = length(nuv);
    float blur = 0.005;
    float m = S(0.2, 0.2 - blur, d);
    float size = 0.;
   
    col.rgb = mix(col.rgb, vec3(1.0, 0.976, 0.96), m);

其中,S 函数是是给 smoothstep 函数定义的别名,为了少打几个字母而已。 有关 smoothstep 方面的知识,可以在Shader从入门到放弃(二) —— 常见GLSL内置函数 - 掘金 (juejin.cn)这篇文章中查看。在本文中就不再过多的进行赘述了。

mix 函数的作用是用于将背景色与我们的前景色进行叠加,其具体原理也可以在Shader从入门到放弃(二) —— 常见GLSL内置函数 - 掘金 (juejin.cn) 中查看。

image.png

但是!!!哪里头这么圆的兔子,所以我得让这个兔子头稍微的变形一丢丢。。。我们可以通过以下代码来实现。

    float y = uv.y + (uv.x * uv.x) * 0.5;

image.png

结果如上,这是什么意思呢? 这里是Shader绘图中一个很重要的技巧。就是将坐标系进行变形,(domain distortion)

我们绘制一下 uv.x * uv.x * 0.5 这个函数图像。

image.png

而我们通过这样的运算,我们在绘制这个圆时的坐标系就会变成下面这样,所以在此坐标系下绘制的圆理所当然的也就会变形咯~

image.png

轮廓 —— 耳朵

接下来是兔子🐰的灵魂 —— 耳朵,实际上我们也是画一个圆,只是这里我们要让这个圆变得更加“扁”一点。

nuv = uv;
nuv *= vec2(4.0, 1.0);
d = length(nuv);

size = 0.4;
blur = 0.01;
m = S(size, size - blur, d);

col.rgb = mix(col.rgb, vec3(1.0, 0.976, 0.96), m);

iShot_2023-01-15_16.17.12.png

接着,类似的我们运用与上面绘制兔头轮廓的方式使其变形一点。

nuv = uv;
nuv = vec2(nuv.x, nuv.y - (nuv.x * nuv.x) * 20.);
nuv *= vec2(4.0, 1.0);
d = length(nuv);

size = 0.4;
blur = 0.01;
m = S(size, size - blur, d);

col.rgb = mix(col.rgb, vec3(1.0, 0.976, 0.96), m);

image.png

withInBox

接下来有一个比较重点的知识需要注意了,我们需要用到一个叫做 withInBox的函数,该函数的作用就是将某个坐标转换到一个矩形内,进行坐标的重映射。该函数如下:

vec2 withInBox(vec2 uv, vec4 rect) {
    return (uv - rect.xy) / (rect.zw - rect.xy);
}

很容易看出来,其实就是将画布上的坐标转换到了某个矩形内。简而言之,就是用这个矩形区域替代了原来的画布区域。

旋转

在Shader中旋转一个坐标同样是运用的旋转公式,只不过我们可以使用矩阵来简化这一操作,下面给出一个旋转矩阵。我们如果要旋转一个点,那么我们使用这个矩阵来乘以这个坐标就可以了。

mat2 Rot(float a) {
    a = a / 180. * PI;
    float c = cos(a);
    float s = sin(a);
    return mat2(c, s, -s, c);
}

代码如下:

vec2 ruv = uv * Rot(-15.);
vec4 rect = vec4(0.0, 0.0, 0.5, 0.5);
nuv = withInBox(ruv - vec2(0.08, 0.2), rect);
nuv = vec2(nuv.x, nuv.y - (nuv.x * nuv.x) * 20.);
nuv *= vec2(4.0, 1.0);
d = length(nuv);

此时,我们的画面如下: image.png

我们将之前的兔头也显示出来: image.png

对称作画技巧

此时,我们只绘制了一只耳朵,可能读者已经想到我再重复做一遍这个操作不就好了吗??

事实上的确可以做,但是在shader中我们有更加巧妙的办法,对于轴对称的图形,我们通常可以使用取绝对值的方法来快速的绘制对称的图形。所以,此时我们想要绘制另一半的耳朵,只需要取x的绝对值就可以了!

截止目前,main函数中的代码如下:

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    vec4 col = vec4(vec3(0.0), 0.);

    float y = uv.y + (uv.x * uv.x) * 0.5;
    vec2 nuv = vec2(uv.x, y);
    float d = length(nuv);
    float blur = 0.005;
    float m = S(0.2, 0.2 - blur, d);
    float size = 0.;
    col.rgb = mix(col.rgb, vec3(1.0, 0.976, 0.96), m);
    
    float side = sign(uv.x);
    uv.x = abs(uv.x);a
    vec2 ruv = uv * Rot(-15.);
    vec4 rect = vec4(0.0, 0.0, 0.5, 0.5);
    nuv = withInBox(ruv - vec2(0.08, 0.2), rect);
    nuv = vec2(nuv.x, nuv.y - (nuv.x * nuv.x) * 20.);
    nuv *= vec2(4.0, 1.0);
    d = length(nuv);

    size = 0.4;
    blur = 0.01;
    m = S(size, size - blur, d);
    
    col.rgb = mix(col.rgb, vec3(1.0, 0.976, 0.96), m);
    
    fragColor = col;
}

可以看出,我们已经初步的具备了一个兔子的轮廓了。

image.png

到现在为止,我们基本上已经学到了所有的绘制兔子的方法,接下来就是依葫芦画瓢,方法都是相同的,只不过我们绘制的图形和大小不同罢了,下面的内容就会说的比较粗略了。如果你有任何疑问,可以在评论区发表你的问题。

后续绘制兔头的代码我就不一一贴在文章中,在文章的末尾,我会讲完整的代码放在“码上掘金”中,供读者自行查看。

背景

现在让我们快进到绘制完兔头的部分,我们现在要做的就是绘制我们的背景。

image.png

首先,我们可以看出,在文章开头的图里,我们的背景分成两块,一个是背景的渐变色,另一个则是许多的小兔子。

渐变色

渐变色比较简单,就是根据我们的一个y坐标来混合两种颜色。

float gradient = smoothstep(-0.5, 0.2, uv.y);
vec3 col = mix(vec3(1.0, 0.8, 0.9), vec3(0.7, 0.9, 1.0), gradient);

许多小兔子

许多的小兔子则是利用了另一种shader编程中常用的技巧。我们可以利用下面的代码来将我们的画布划分为一个个的小格子。

vec2 st = fract(uv * 3.0);
vec2 id = floor(uv * 3.0);

上面的代码中的st 表示的是每个小格子中新的uv坐标。而id则代表每个格子的唯一id。

读者可以查看这一篇文章进一步的加深理解。Shader从入门到放弃(四) —— 绘制闪耀星际 - 掘金 (juejin.cn)

那么,那么我只需要将这个新的坐标套入刚刚我们绘制兔头的函数中,就可以在这一个个的小格子中绘制出小的兔子头。

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    float gradient = smoothstep(-0.5, 0.2, uv.y);
    vec3 col = mix(vec3(1.0, 0.8, 0.9), vec3(0.7, 0.9, 1.0), gradient);
    
    vec4 head = Head(uv, iTime);
    uv *= 5.;
    vec2 id = floor(uv);
    float n = Hash21(id);
    vec2 st = fract(uv) - 0.5;
    
    vec4 fHead = Head(st, 0.0);
    col.rgb = mix(col, fHead.rgb, fHead.a);
    col.rgb = mix(col, head.rgb, head.a);

    fragColor = vec4(col, 1.0);
}

结果如下:

image.png

最后,我们可以根据每个格子的id来生成一个hash值,再根据这个hash值来生成一个旋转角度,这样每个格子的旋转初始值都不一样,这样就可以形成一种错落有致的美。

为避免啰嗦,最后的代码就不再贴出来了,这块大家可以自行思考一下应该怎样去做。

完整的代码如下:

总结

OK,今天我们学习到了如何绘制一只可爱的小兔子,你有没有被这个小兔子所萌化呢?作者最后发现,这只兔子还与“那年那兔那些事”中的兔子竟然有几分神似呢。

最后祝大家新年快乐,兔年大吉~~~ 如果你觉得本文有用,别忘了给作者点赞哦。