聪明绝顶Shader教程[The Art of Code(1-4)]
2024-07-04
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);
实现圆形距离场。
- 更多2D距离场: 2D distance functions
- 函数使用方法查看 内置函数库
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截图:
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截图:
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截图
Roto-Zooming Smiley
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截图
变形和重映射的理解
最终效果
总结 #
这个教程是The Art Of Code的系列教程 Shadertoy Tutorial 播放列表中的第一个小部分,之后会把这个系列的所有视频的笔记都发上来。本篇的重点总结:
- 搞懂Shader是什么,以片元着色器来说,对每一个像素,传入该像素的坐标值,传出该像素的颜色值,组成图像。
- 对坐标系的基础操作,比如归一化、中心化、屏幕比例调整。以及代码中uv的具体含义。
- 重映射是常用操作,将一个值从一个域映射到另一个域,归一化01映射和线性映射后,可以操作映射后的值实现非线性映射。
Shadertoy( https://www.shadertoy.com/new )
- 保留所有版权
- 首次上传:2024年07月04日
- 最后修改:2024年07月05日