提示
教程例子都可以到下面网址进行运行,不需要另外安装软件环境:
官方提供在线编写shader工具:https://thebookofshaders.com/edit.php
glslsandbox网站:http://glslsandbox.com/
shadertoy网站:https://www.shadertoy.com/
长方形
想象我们有张数学课上使用的方格纸,而我们的作业是画一个正方形。纸的大小是10 10而正方形应该是8 8。你会怎么做?
你是不是会涂满除了第一行第一列和最后一行和最后一列的所有格点?
这和着色器有什么关系?方格纸上的每个小方形格点就是一个线程(一个像素)。每个格点有它的位置,就想棋盘上的坐标一样。在之前的章节我们将x和y映射到rgb通道,并且我们学习了如何将二维边界限制在0和1之间。我们如何用这些来画一个中心点位于屏幕中心的正方形?
我们从空间角度来判别的 if 语句伪代码开始。这个原理和我们思考方格纸的策略异曲同工。
if ( (X GREATER THAN 1) AND (Y GREATER THAN 1) )
paint white
else
paint black
现在我们有个更好的主意让这个想法实现,来试试把if语句换成step(),并用0到1代替10 * 10的范围。
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 resolution;
void main(){
vec2 st = gl_FragCoord.xy/resolution.xy;
vec3 color = vec3(0.0);
// Each result will return 1.0 (white) or 0.0 (black).
float left = step(0.1,st.x); // Similar to ( X greater than 0.1 )
float bottom = step(0.1,st.y); // Similar to ( Y greater than 0.1 )
// The multiplication of left*bottom will be similar to the logical AND.
color = vec3( left * bottom );
gl_FragColor = vec4(color,1.0);
}
step()函数会让没每一个小于10%
的像素变成黑色(vec3(0.0))并将其与的变成白色(vec3(1.0))。left 乘 bottom 效果相当于逻辑 AND —— 当 x y 都为 1.0 时乘积才能是 1.0。这样做的效果就是画了两条黑线,一个在画布的底边另一个在左边。
vec2 bl = step(vec2(0.1),st); // 左下角留黑
vec2 tr = step(vec2(0.1),1.0-st); // 右上角留黑
color = vec3(bl.x * bl.y * tr.x * tr.y);
- 用 smoothstep() 函数代替 step() 函数,试试在相同的代码下会有什么不同。注意通过改变取值,你可以不仅可以得到模糊边界也可以由漂亮的顺滑边界。
- 应用 floor() 做个另外的案例。
- 想一下如何在一个画板上移动并放置不同的长方形?如果你做出来了,试着像Piet Mondrian一样创作以长方形和色彩的图画。
圆形
有几种方法来计算距离。最简单的是用
distance()
函数,这个函数其实内部调用length()
函数,计算不同两点的距离(在此例中是像素坐标和画布中心的距离)。length()函数内部只不过是用平方根(sqrt()
)计算斜边的方程。
你可以使用distance()
,length()
或sqrt()
到计算屏幕的中心的距离。下面的代码包含着三个函数,毫无悬念的他们返回相同的结果。
// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
void main(){
vec2 st = gl_FragCoord.xy/u_resolution;
float pct = 0.0;
// a. The DISTANCE from the pixel to the center
pct = distance(st,vec2(0.5));
// b. The LENGTH of the vector
// from the pixel to the center
// vec2 toCenter = vec2(0.5)-st;
// pct = length(toCenter);
// c. The SQUARE ROOT of the vector
// from the pixel to the center
// vec2 tC = vec2(0.5)-st;
// pct = sqrt(tC.x*tC.x+tC.y*tC.y);
vec3 color = vec3(pct);
gl_FragColor = vec4( color, 1.0 );
}
代码 | 效果 |
---|---|
pct = distance(st,vec2(0.5)); | |
pct = step(distance(st,vec2(0.5)),.5); | |
其他代码图案相同 |
距离场
我们可也可以从另外的角度思考上面的例子:把它当做海拔地图(等高线图)——越黑的地方意味着海拔越高。想象下,你就在圆锥的顶端,那么这里的渐变就和圆锥的等高线图有些相似。到圆锥的水平距离是一个常数0.5。这个距离值在每个方向上都是相等的。通过选择从那里截取这个圆锥,你就会得到或大或小的圆纹面。
- 用
step()
函数把所有大于0.5的像素点变成白色,并把小于的变成黑色(0.0) - 反转前景色和背景色。
- 调戏下
smoothstep()
函数,用不同的值来试着做出一个边界顺滑的圆 - 一旦遇到令你满意的应用,把他写成一个函数,这样将来就可以调用了
- 给这个圆来些缤纷的颜色吧!
- 再加点动画?一闪一闪亮晶晶?或者是砰砰跳动的心脏?(或许你可以从上一章汲取一些灵感)
- 让它动起来?能不能移动它并且在同一个屏幕上放置多个圆?
- 如果你结合函数来混合不同的距离场,会发生什么呢?
pct = distance(st,vec2(0.4)) + distance(st,vec2(0.6));
pct = distance(st,vec2(0.4)) * distance(st,vec2(0.6));
pct = min(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
pct = max(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
pct = pow(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
- 用这种技巧制作三个元素,如果它们是运动的,那就再好不过啦!
就计算效率而言,
sqrt()
函数,以及所有依赖它的运算,都耗时耗力。dot()
点乘是另外一种用来高效计算圆形距离场的方式。
// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
float circle(in vec2 _st, in float _radius){
vec2 dist = _st-vec2(0.5);
return 1.-smoothstep(_radius-(_radius*0.01),
_radius+(_radius*0.01),
dot(dist,dist)*4.0);
}
void main(){
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec3 color = vec3(circle(st,.8));
gl_FragColor = vec4( color, 1.0 );
}
就计算效率而言,
sqrt()
函数,以及所有依赖它的运算,都耗时耗力。dot()
点乘是另外一种用来高效计算圆形距离场的方式。
算式 | 推倒 |
---|---|
dot(dist)*2 | 并不等于sqrt, 它的递增幅度比sqrt和length大,而用它来代替length or sqrt是为了效率 |
1.-smoothstep | 获得反色 |
sqrt or length | |
dot(dist)*2 | |
- | 这两个图像只是在0~1之间有交集 |
距离场的特点
距离场几乎可以用来画任何东西。显然,图形越复杂,方程也越复杂。但是一旦你找到某个特定图形的公式,就很容易添加图形或应用像过渡边界的效果。正因如此,距离场经常用于字体渲染,例如Mapbox GL Labels, Matt DesLauriers Material Design Fonts 和 as is describe on Chapter 7 of iPhone 3D Programming, O’Reilly.
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
void main(){
vec2 st = gl_FragCoord.xy/u_resolution.xy;
st.x *= u_resolution.x/u_resolution.y;
vec3 color = vec3(0.0);
float d = 0.0;
// Remap the space to -1. to 1.
st = st *2.-1.;
// Make the distance field
d = length( abs(st)-.3 );
// d = length( min(abs(st)-.3,0.) );
// d = length( max(abs(st)-.3,0.) );
// Visualize the distance field
gl_FragColor = vec4(vec3(fract(d*10.0)),1.0);
// Drawing with the distance field
// gl_FragColor = vec4(vec3( step(.3,d) ),1.0);
// gl_FragColor = vec4(vec3( step(.3,d) * step(d,.4)),1.0);
// gl_FragColor = vec4(vec3( smoothstep(.3,.4,d)* smoothstep(.6,.5,d)) ,1.0);
}
试试注释中的代码
公式 | 说明 |
---|---|
step(a,d) | 如果d<a返回0,如果d>a返回1 |
step(d, a) | 如果d>a返回0,如果d<a返回1,相当于 1.0-step(a,d) |
极坐标下的图形
在关于颜色的章节我们通过如下的方程把每个像素的 半径 和 角度 笛卡尔坐标映射到极坐标。
vec2 pos = vec2(0.5)-st;
float r = length(pos)*2.0;
float a = atan(pos.y,pos.x);
试试下面图形
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
void main(){
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec3 color = vec3(0.0);
vec2 pos = vec2(0.5)-st;
float r = length(pos)*2.0;
float a = atan(pos.y,pos.x);
float f = cos(a*3.);
// f = abs(cos(a*3.));
// f = abs(cos(a*2.5))*.5+.3;
// f = abs(cos(a*12.)*sin(a*3.))*.8+.1;
// f = smoothstep(-.5,1., cos(a*10.))*0.2+0.5;
color = vec3( smoothstep(f,f+0.028,r) );
gl_FragColor = vec4(color, 1.0);
}
其中:
- pos 将坐标系从左下角移动到了图像中样
- r/a 为所有像素点距离图像中央的距离和角度
-
f 为cos(a)的值获得距离中心点-1~1的值,其中负值被舍。为什么是三瓣,因为a乘了3,这样在一个周期里就有了三次高潮。一次的样子:
- 试试让它动起来
- 试试弄个❄️形状
整合的魅力
到目前为止,我们知道如何用
atan()
函数来根据角度调整半径以获得不同的图形,以及如何用atan()
结合所以和距离场有关的技巧得到可能的效果。
看下下面来自Andrew Baldwin的例子。这里的技巧是用极坐标的方式通过定义多边形的边数来构建一个距离场。
#ifdef GL_ES
precision mediump float;
#endif
#define PI 3.14159265359
#define TWO_PI 6.28318530718
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
// Reference to
// http://thndl.com/square-shaped-shaders.html
void main(){
vec2 st = gl_FragCoord.xy/u_resolution.xy;
st.x *= u_resolution.x/u_resolution.y;
vec3 color = vec3(0.0);
float d = 0.0;
// Remap the space to -1. to 1.
st = st *2.-1.;
// Number of sides of your shape
int N = 3;
// Angle and radius from the current pixel
float a = atan(st.x,st.y)+PI+u_time;
float r = TWO_PI/float(N);
// Shaping function that modulate the distance
d = cos(floor(.5+a/r)*r-a)*length(st);
color = vec3(1.0-smoothstep(.4,.41,d));
// color = vec3(d);
gl_FragColor = vec4(color,1.0);
}
- a 还是所有像素相对中心的角度
- r 变为了多边形一条边对应的中心夹角大小
- ceil(a/r) 将-pi~pi分割成n分,分布成阶梯状
-
ceil(a/r-0.5)*r 将图像右移0.5个单位, 乘r只增加y轴倍率
-
d 图像,也许在极坐标系下是个三角形,数学模型不会建的小白
- 试试改变多边形边数
方形区域
float box(in vec2 _st, in vec2 _size){
_size = vec2(0.5) - _size*0.5;
vec2 uv = smoothstep(_size,
_size+vec2(0.001),
_st);
uv *= smoothstep(_size,
_size+vec2(0.001),
vec2(1.0)-_st);
return uv.x*uv.y;
}
- smoothstep(_size, _size+vec2(0.001), _st); 为上角留白
- smoothstep(_size, _size+vec2(0.001), vec2(1.0)-_st); 为左下角留白
- 两个区域有公共交叉的部分,交集用乘法可得到也就是两个值都为1相乘才为1,否则为0,这个就像之前写的电路里的或非门nor
加号
float cross(in vec2 _st, float _size){
return box(_st, vec2(_size,_size/4.)) +
box(_st, vec2(_size/4.,_size));
}
- 将两个形状所占的所有面积叠加,只要为1的地方就都为1,除非两个面积标注的地方都为0,这个像门电路里的与门and
- 接着可以推导出面积相减就是通过减法公式来处理