开篇
我在Shadertoy或者WegGL中编写着色器程序时,经常需要用到许多绘制2D或者3D图形学公式。和数学公式一样,这些公式大多数时候是需要记忆的。为了后续方便记忆和查阅,本博文总结了一些我在平时开发绘制图形(尤其是3D图形)时常用的计算公式。这其中大多数都是前人的思想结晶,我会在每一份说明文字中注明算法的来源,他们有些来自书籍,有些来自视频,更多的是来自互联网上博客文字(虽然也都不是他们原创,但可以知道这些技巧被使用时的上下文)。本博文只作为记录和总结功能,并不提供创新的想法,并且后续会持续地更新。
Shapes(图形)
Remap(重分)
不知道用“重分”一词是否准确,这个公式用在2D绘制的场景中,在需要对固定的区域内进行再次归一化处理,以便处理图形。此算法是在youtube频道上看到的。
vec2 Remap(vec2 p, float t, float r, float b, float l) {
return vec2((p.x - r)/ (l -r), (p.y - b) / (t - b));
}
Plot(线条)
虽然在HTML中画一条线很容易,但在shader中画一条线却没那么简单。需要用到smoothstep函数来处理。在The Book of Shaders的基础知识中看到。
float plot_x(vec2 p, float start, float end, float blur) {
return smoothstep(start, start + blur, p.x)
- smoothstep(end, end + blur, p.x)
}
float plot_y(vec2 p, float start, float end, float blur) {
return smoothstep(start, start + blur, p.y)
- smoothstep(end, end + blur, p.y)
}
Grid(表格)
这是基础分块技术,旨在把画布分成若干等分,是很多酷炫效果的基础。在The Book of Shaders的基础知识中学习到,
vec2 uv = fract(uv * 10.0);
Signed field distance (基础2D和3D的图像算法)
符号距离场,这个名字翻译实在有些别扭,但是确实非常基础和实用的创造基础图形的方法。这些方法由著名Inigo Quilez原创,他是Shadertoy的创始人,专注图形领域二十多年,是一位真正的大神级别人物。该系列公式在他的博客中有总结,总共分2D图形和3D图像两篇。此处只做一些常用的基础的图形:
Sphere(球体)
float sdSphere( vec3 p, float s )
{
return length(p)-s;
}
Cube (立方体)
float sdBox( vec3 p, vec3 b )
{
vec3 q = abs(p) - b;
return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}
Cylinder (圆柱体)
float sdRoundedCylinder( vec3 p, float ra, float rb, float h )
{
vec2 d = vec2( length(p.xz)-2.0*ra+rb, abs(p.y) - h );
return min(max(d.x,d.y),0.0) + length(max(d,0.0)) - rb;
}
颜色
RGB和HSV颜色互转
vec3 rgb2hsv(vec3 c){
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c){
vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0), 6.0)-3.0)-1.0, 0.0, 1.0);
rgb = rgb * rgb * (3.0 - 2.0 * rgb);
return c.z * mix(vec3(1.0), rgb, c.y);
}
View(视角)
RayMarch(光线步进)
在Webgl或者OpenGL,可以直接用顶点着色器构建三维空间中的物体,而在只能使用片元着色器(fragment shader)的环境中,RayMarch算法是进入3D世界的基础算法公式。它的核心原理是利用一束光线逐步去探索一个物体的边缘的每个点,使用这个方法才能在2D绘制3D物体。它的计算非常“昂贵”,在GPU能力较差的机器上,帧数会降低。该方法在 Nathan Vaughn's Shaders Language Tutorial 有详细说明。
#define MAX_STEP 255
#define PRECISION 0.01
float RayMarch(vec2 ro, vec2 rd, float start, float end) {
float depth = start;
for(int i=0; i<MAX_STEP; i++) {
vec2 p = ro + rd * depth;
float d = (length(p) - 1.0);
depth += d;
if(depth < PRECISION || depth > end) break;
}
return depth;
}
Camera Matrix(相机矩阵)
相机矩阵也是在无法使用顶点着色器(vertex shader)的环境中使用,它规定了一个相机的方向,视线方向从而固定了一个观察者的位置。该方法也是在Nathan Vaughn's Shaders Language Tutorial 博文中有说明。
mat3 camera(vec3 ro, vec3 lp) {
vec3 camera_direction = normalize(lp - ro);
vec3 camera_right = normalize(cross(vec3(0.0, 1.0, 0.0), camera_direction));
vec3 camera_up = normalize(cross(camera_xx, camera_direction));
return mat3(
-camera_direction,
camera_right,
-camera_up
);
}
光线碰撞
抗阴影锯齿(anti aslising)
WebGL在绘制阴影的过程中,阴影会在边缘显示不平滑的效果。抗阴影锯齿使用平均取值过渡算法,让阴影变得丝滑。最初在Learn OpenGl 教程中学习到:
float shadows = 0.0;
float opacity= .4;// 阴影alpha值, 值越小暗度越深
float texelSize= 1.0 / 2028.0;// 阴影像素尺寸,值越小阴影越逼真
vec4 rgbaDepth;
// 消除阴影边缘的锯齿 去平局差值四周围的
for(float y=-2.0; y <= 2.0; y++){
for(float x=-2.0; x <=2.0; x++){
rgbaDepth = texture(u_texture, depth.xy + vec2(x, y) * texelSize);
shadows += (depth.z - bias > rgbaDepth.r) ? 1.0 : 0.0;
}
}
shadows /= 9.0;// 4*4的样本
float visibility = min(opacity + (1.0 - shadows), 1.0); // 抗锯齿阴影
Noise(噪声)
噪声属于图形技术中较为高级的图像算法。是一种有效的模拟现实世界随机现象的方法:
Random(随机算法)
Fractal(分形)
Matrix(矩阵)
rotate(旋转)
3D的旋转矩阵虽然经常使用却对于人脑来说有些记忆成本,不过为了使的记忆简化,记住基础的2D也是一种非常方便的手段。例如处理一个3D物体在一个方向上进行旋转,就可以只用2D矩阵来处理,该方法最初是在The Book Oif Shaders中学习到,但真正的用法是在youtube中。
mat2 rotate2D(float angle) {
return mat2(cos(angle), -sin(angle),
sin(angle), cos(angle));
}
缩放与旋转是异曲同工,不再赘述:
scale(缩放)
mat2 scale2D(vec2 r) {
return mat2(r.x, 0.0,
0.0, r.y
);
}
Materials & Light(材质和光照)
3D场景的基础光照模型---冯式光照模型也称为ADS光照模型,是让虚拟世界中的无图看起来跟家真实的有效光照模型。最初是在《自顶而下的图像学设计》中看到。
Ambient(环境光)
gl_FragColor = ambientColor * a_color;
diffuse(漫反射)
float fDot = dot(a_normal, u_lightDirection);
gl_FragColor = diffuseColor * fDot * a_color;
specular(镜面反射)
vec2 ref = reflect(a_normal, u_lightDirection);
float fDot = clamp(dot(ref, -u_viewDirection), 0.0, 1.0);
float n = pow(fDot, shiness);
gl_FragColor = specularColor * n * a_color;
法线
法线其实就是一个表面的倾斜率。在webgl中,我们可以直接指定buffer中的数据每个顶点的法线。而在缺少顶点着色器的片元着色器中,我们需要通过取两个点之间连线的倾斜率来得到一个表面的倾斜率:
vec3 calcNormal(vec3 p, float d) {
vec2 e = vec2(1.0, -1.0) * 0.0005; // epsilon
float r = 1.; // radius of sphere
return normalize(
e.xyy * d +
e.yyx * d +
e.yxy * d +
e.xxx * d);
}
Position(定位)
Canvas(坐标转换)
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y;
极坐标
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y
vec2 polarCoordinate = (atan(uv.x, uv.y), length(uv));