聪明绝顶Shader教程[The Art of Code(1-4)]

聪明绝顶Shader教程[The Art of Code(1-4)]

2024-07-04
Shader, Shadertoy

PART1.JUST A CIRCLE #

Shadertoy 是一个通过 WebGL 创建和共享着色器的在线社区和工具,用于在网络浏览器中学习和教授 3D 计算机图形。

Shadertoy中只有片段着色器,通过输入 像素坐标(fragCoord) 并输出对应像素的 rgba值(fragcolor) 来生成图像。

Shadertoy 上新建项目后,将本部分完整代码粘贴到shadertoy中,按下代码编辑器下方的播放键(或Alt + Enter),图像输出区会显示一个边缘模糊的圆形。

阅读代码不难理解主函数通过输入像素坐标(fragCoord),和输出像素颜色(fragColor)来逐一处理每一个像素。

坐标系的常见操作:

  • vec2 uv = fragCoord/iResolution.xy; 将屏幕像素坐标归一化,方便后续操作,iResolution是Shadertoy定义的屏幕尺寸变量
  • uv -= 0.5; 将uv坐标系的原点移动到屏幕中心
  • uv.x *= iResolution.x/iResolution.y; 拉伸uv.x坐标,如果屏幕比为1,则对uv坐标的x轴无影响,如果屏幕比大于或小于1,会将uv.x拉伸或压缩,以适应不同的屏幕分辨率

float d = length(uv); 取uv矢量的长度,并使用smoothstep函数float c = smoothstep(r+0.1,r,d); 实现圆形距离场。

PART1完整代码 #

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;

    uv -= 0.5;// -0.5 to 0.5
    uv.x *= iResolution.x/iResolution.y;

    float d = length(uv);
    float r =0.3;
    float c = smoothstep(r+0.1,r,d);
    fragColor = vec4(vec3(c),1.0);
}
  • PART1截图: shadertoyTutorialPart1.png

PART2.BUILDING STUFF WITH CIRCLE #

  • 把上一部分的Circle函数整理出来
float Circle(vec2 uv, vec2 p, float r, float blur)
{
    float d = length(uv-p);
    float c = smoothstep(r+blur,r,d);
    return c;
}
  • 使用Circle函数画出脸和眼睛(多个圆相减)
  • 可以尝试改变圆的位置和改变符号,比如mask += Circle(), 因为圆的部分值为1,两个圆相减后交集值为0,从而实现相减效果
    float mask = Circle(uv, vec2(0.,0.), .4, .03);
    mask -= Circle(uv, vec2(-.15,.12), .07, .02);
    mask -= Circle(uv, vec2(.15,.12), .07, .02);
  • 创建一个颜色,然后与musk取交集
  • mask是0到1之间的数,rgb值也是0到1之间的数,相乘后即可在屏幕中留下mask>0的部分
    vec3 col = vec3(.5,0.4,.6)*mask;
  • 创建嘴巴,经过标准化处理后加入musk中
    float mouth = Circle(uv, vec2(0.,0.), .3, .03);
    mouth -= Circle(uv, vec2(0.,0.45), .5, .03);
    mouth = clamp(mouth, 0., 1.); //将值置于0到1之间
    mask -= mouth;

PART2完整代码 #

float Circle(vec2 uv, vec2 p, float r, float blur)
{
    float d = length(uv-p);
    float c = smoothstep(r+blur,r,d);
    return c;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;

    uv -= 0.5;//remap to -0.5 to 0.5
    uv.x *= iResolution.x/iResolution.y;

    float mask = Circle(uv, vec2(0.,0.), .4, .03);
    mask -= Circle(uv, vec2(-.15,.12), .07, .02);
    mask -= Circle(uv, vec2(.15,.12), .07, .02);
    float mouth = Circle(uv, vec2(0.,0.), .3, .03);
    mouth -= Circle(uv, vec2(0.,0.45), .5, .03);
    mouth = clamp(mouth, 0., 1.);

    mask -= mouth;
    vec3 col = vec3(.5,0.4,.6)*mask;
    fragColor = vec4(col,1.0);
}

PART2截图: shadertoyTutorialPart2.png

PART3.ROTO-ZOOMING SMILEY & MAKING A RECTANGLE #

Roto-Zooming #

  • 详细的Roto-Zooming原理查看 The Art of Demomaking - Issue 10 - Roto-Zooming
  • 首先打包笑脸函数,并实现位置、大小、旋转角度的控制
  • 要注意旋转算法分为两步,都需要原始的uv值,如果计算uv.y时使用了更新后的uv.x,会导致结果出错,图像失真拉伸。
float Smiley(vec2 uv, vec2 p, float size, float angle)
{
    //这里的uv相当于小元素自己的坐标系统
    vec2 uv_orig = uv;//旋转算法分为两步,需要保存原始uv值备用
    uv.x = uv_orig.x*cos(angle) - uv_orig.y*sin(angle);//rotate
    uv.y = uv_orig.y*cos(angle) + uv_orig.x*sin(angle);
    
    uv -= p;//translating
    uv /= size;//scaling
    float mask = Circle(uv, vec2(0.,0.), .4, .03);
    mask -= Circle(uv, vec2(-.15,.12), .07, .02);
    mask -= Circle(uv, vec2(.15,.12), .07, .02);
    
    float mouth = Circle(uv, vec2(0.,0.), .3, .03);
    mouth -= Circle(uv, vec2(0.,0.45), .5, .03);
    mouth = smoothstep(0.,1.,mouth);
    mask -= mouth;
    
    return mask;
}
  • 在Smiley函数中添加Scale,可以实现Roto-Zooming效果
Smiley():
	uv /= scale;

mainImage():
	float scale = 1.+.5*sin(iTime);//简单的周期函数,返回值随时间变化,实现“Zooming”部分
    //角度参数传入iTime(Shadertoy定义的时间变量,等于显示区下方的那个数字,实现“Roto-”部分)
    float mask = Smiley(uv, vec2(0., .0), 1., iTime, scale);
  • 实现矩形函数:
  • 带子函数:
    • smoothstep(a, b, t), t < a, 返回0.0, t > b, 返回1.0, 否则返回Hermite插值,当a > b时,smoothstep函数将反转
    • 运用两个smoothstep函数结果求交集,将显示给定边缘的“带子”
  • 矩形函数:
    • 运用两个band函数结果求交集,将显示给定边缘的矩形
//带子函数
float Band(float t, float start, float end, float blur)
{
    //PART4中为了显示效果,blur不再/2.
    float step1 = smoothstep(start-blur/2., start+blur/2., t);
    float step2 = smoothstep(end+blur/2., end-blur/2., t);
    
    return step1*step2;
}
//矩形函数
float Rect(vec2 uv, vec2 p, vec2 size, float blur)
{
    uv -= p;
    float band1 = Band(uv.x, -size.x/2., size.x/2., blur);
    float band2 = Band(uv.y, -size.y/2., size.y/2., blur);
    
    return band1 * band2;
}

//也可以传入上下左右来定义矩形
//下一部分会使用这个版本的矩形函数
float Rect(vec2 uv, float left, float right, float bottom, float top, float blur)
{
    float band1 = Band(uv.x, left, right, blur);
    float band2 = Band(uv.y, bottom, top, blur);
    
    return band1 * band2;
}

PART3完整代码 #

float Circle(vec2 uv, vec2 p, float r, float blur)
{
    float d = length(uv-p);
    float c = smoothstep(r+blur,r,d);
    return c;
}

float Band(float t, float start, float end, float blur)
{
    float step1 = smoothstep(start-blur/2., start+blur/2., t);
    float step2 = smoothstep(end+blur/2., end-blur/2., t);
    
    return step1*step2;
}
float Rect(vec2 uv, vec2 p, vec2 size, float blur)
{
    uv -= p;
    float band1 = Band(uv.x, -size.x/2., size.x/2., blur);
    float band2 = Band(uv.y, -size.y/2., size.y/2., blur);
    
    return band1 * band2;
}

float Smiley(vec2 uv, vec2 p, float size, float angle, float scale)
{
    //这里的uv相当于小元素自己的坐标系统
    
    uv -= p;//translating
    uv /= size;//scaling
    
    uv /= scale;
    vec2 uv_orig = uv;//旋转算法分为两步,需要保存原始uv值备用
    uv.x = uv_orig.x*cos(angle) - uv_orig.y*sin(angle);//rotate
    uv.y = uv_orig.y*cos(angle) + uv_orig.x*sin(angle);
    float mask = Circle(uv, vec2(0.,0.), .4, .03);
    mask -= Circle(uv, vec2(-.15,.12), .07, .02);
    mask -= Circle(uv, vec2(.15,.12), .07, .02);
    
    float mouth = Circle(uv, vec2(0.,0.), .3, .03);
    mouth -= Circle(uv, vec2(0.,0.45), .5, .03);
    mouth = smoothstep(0.,1.,mouth);
    mask -= mouth;
    
    return mask;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;

    uv -= 0.5;//remap to -0.5 to 0.5
    uv.x *= iResolution.x/iResolution.y;
    float scale = 1.+.5*sin(iTime);
    //float mask = Smiley(uv, vec2(0., .0), 1., iTime, scale);//Roto-Zooming
    float mask = Rect(uv, vec2(.4,0.), vec2(.4, .3), .01);
    vec3 col = vec3(.5,0.4,.6)*mask; 
    
    fragColor = vec4(col,1.0);
}

PART3截图 shadertoyTutorialPart3.1.png

Roto-Zooming Smiley

shadertoyTutorialPart3.2.png

Rectangle

PART4.DOMAIN DISTORTION #

  • 通过操作坐标系,实现变形效果
  • 注:这一PART使用的是上下左右传参的Rect()(上一部分已给出)
//为了操作方便
	float x = uv.x;
	float y = uv.y;

//以下操作可以实现剪切,使x值上的线倾斜,y同理
	x += y*0.2;
//对边进行操作可以实现不同的四边形
	float mask = Rect(vec2(x, y),-.3+y*.2, .3-y*.2, -.2, .2, .01);//比如梯形

下面的代码实现了矩形的弯曲

  • 使用图形计算器 Desmos 可以方便的找到图形函数,应用到shader中
  • 因为y减去对应的m,图形在屏幕上的位置会对应上移
  • 比如点uv.y:(0.5,0), 减去该位置m后,y:(0.5,-0.25), 而矩形函数中这个点应该画在(0.5,0)位置,而原先的位置已经变为(0.5,-0.25),所以图形上移。
    float m = x*x;
    float y = uv.y - m;
  • 使用sin()和iTime,操作矩形周期运动
  • 设定sin函数的幅值和频率,找到好的显示效果
    float m = sin(iTime+x*8.)*.1;
    float y = uv.y - m;

重映射 #

  • 重映射是一个常见的操作,从一个域映射一个值到另一个域
  • 归一化和重映射:(很简单,但为了理解这个我还画了一个图)
float remap01(float a, float b, float t)//映射到01之间
{
    return (t-a)/(b-a);
}
float remap(float a, float b, float c, float d, float t)//映射到目标域
{
    return c + (d-c)*remap01(a, b, t);
}
//可以化简为一个函数
float remap(float a, float b, float c, float d, float t)
{
    return c+ (d-c)*(t-a)/(b-a);
}
  • 将矩形设置为长条状并让它像旗子一样🚩飘起来
    float m = sin(iTime+x*8.)*.1;
    float mask = Rect(vec2(x,y),-.5, .5, -.1, .1, blur);
  • 将像素坐标x值(-.5, .5)从坐标轴映射到模糊程度(.01, .25)
    float blur = remap(-.5, .5, .01, .25, x);//线性映射
    blur = pow(blur*4., 2.);//非线性映射

PART4完整代码 #

float Circle(vec2 uv, vec2 p, float r, float blur)
{
    float d = length(uv-p);
    float c = smoothstep(r+blur,r,d);
    return c;
}

float Band(float t, float start, float end, float blur)
{
    //float step1 = smoothstep(start-blur/2., start+blur/2., t);
    //float step2 = smoothstep(end+blur/2., end-blur/2., t);
    float step1 = smoothstep(start-blur, start+blur, t);
    float step2 = smoothstep(end+blur, end-blur, t);
    return step1*step2;
}
float Rect(vec2 uv, float left, float right, float bottom, float top, float blur)
{
    float band1 = Band(uv.x, left, right, blur);
    float band2 = Band(uv.y, bottom, top, blur);
    
    return band1 * band2;
}

float remap01(float a, float b, float t)
{
    return (t-a)/(b-a);
}
float remap(float a, float b, float c, float d, float t)
{
    return c + (d-c)*remap01(a, b, t);
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;

    uv -= 0.5;//remap to -0.5 to 0.5
    uv.x *= iResolution.x/iResolution.y;
    float x = uv.x;
    
    float m = sin(iTime+x*8.)*.1;
    float y = uv.y - m;
    
    float blur = remap(-.5, .5, .01, .25, x);
    blur = pow(blur*4., 2.);
    float mask = Rect(vec2(x,y),-.5, .5, -.1, .1, blur);
    vec3 col = vec3(.5,0.4,.6)*mask; 
    
    fragColor = vec4(col,1.0);
}

PART4截图

shadertoyTutorialPart4.1.png

变形和重映射的理解

shadertoyTutorialPart4.2.png

最终效果

总结 #

这个教程是The Art Of Code的系列教程 Shadertoy Tutorial 播放列表中的第一个小部分,之后会把这个系列的所有视频的笔记都发上来。本篇的重点总结:

  1. 搞懂Shader是什么,以片元着色器来说,对每一个像素,传入该像素的坐标值,传出该像素的颜色值,组成图像。
  2. 对坐标系的基础操作,比如归一化、中心化、屏幕比例调整。以及代码中uv的具体含义。
  3. 重映射是常用操作,将一个值从一个域映射到另一个域,归一化01映射和线性映射后,可以操作映射后的值实现非线性映射。

Shadertoy( https://www.shadertoy.com/new )


  • 保留所有版权
  • 首次上传:2024年07月04日
  • 最后修改:2024年07月05日