使用 SVG 和 fragment shader
在设置页面背景时,使用 Pattern 这种平铺的效果是很常见的做法,其中使用 CSS 属性是最常规的了:
body {
background-image: url("paper.gif");
background-repeat: repeat;
}
除此之外,使用 SVG 和 WebGL 也能实现这种效果。
SVG Patterns
SVG 中填充对应的属性是 fill
,取值除了简单的颜色,也可以通过 url 关联到某个静态资源,甚至是我们定义好的 Pattern:
<rect fill="url(#Pattern)" stroke="black" x="0" y="0" width="200" height="200"/>
声明一个 Pattern 也并不复杂,其中可以包含一些常规的 SVG 元素。例如这里我们定义每一个 Pattern 包含一个天蓝色的正方形。 唯一让人困惑的是宽高的单位。
<pattern id="Pattern" x="0" y="0" width=".25" height=".25">
<rect x="0" y="0" width="50" height="50" fill="skyblue"/>
</pattern>
单元系统
Pattern 有自己的单元系统,体现在 patternUnits
这个属性上。默认情况下,取值为 objectBoundingBox
。
在这样的单元系统下,宽高的取值范围就是 0-1。例如我们想让 Pattern 在 xy 方向各平铺四次:
<pattern id="Pattern" x="0" y="0" width=".25" height=".25" patternUnits="objectBoundingBox">
patternUnits
的另一个取值 userSpaceOnUse
,在使用时需要根据最终画布的尺寸计算 Pattern 的宽高。例如在 200*200 的场景中,要实现同样平铺四次的效果:
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<pattern id="Pattern" x="0" y="0" width="50" height="50" patternUnits="userSpaceOnUse">
See the Pen SVG Patterns by xiaop (@xiaoiver) on CodePen.
下面让我们用 WebGL 来实现更加复杂的效果。
WebGL Shader
仅用 fragment shader 就可以实现 Pattern 效果。
首先,需要将整个空间切割成同样大小的块,使用内置 fract
函数可以实现这一点。
例如,将初始坐标空间放大到 3 倍,然后通过 fract
取得小数部分。这样后续只需要考虑坐标范围在 0-1 的小块即可。
uniform vec2 u_resolution;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution;
vec3 color = vec3(0.0);
st *= 3.0; // Scale up the space by 3
st = fract(st); // Wrap arround 1.0
// Now we have 3 spaces that goes from 0-1
color = vec3(circle(st,0.5));
gl_FragColor = vec4(color,1.0);
}
至于画圆形,这里使用 step
函数根据每个 frag 到圆心 (0.5, 0.5) 的距离决定是否涂色。
由于背景是黑色,需要用 1-
得到反色白色。使用 smoothstep
能让圆形边缘稍微圆滑一些。
float circle(in vec2 _st, in float _radius){
vec2 l = _st-vec2(0.5);
return 1.-smoothstep(
_radius-(_radius*0.01),
_radius+(_radius*0.01),
dot(l,l)*4.0);
}
交错的平铺效果
利用简单的平移变换,可以得到交错的平铺效果。将之前的圆形向上下左右四个方向平移:
float circlePattern(vec2 st, float radius) {
return circle(st+vec2(0.,-.5), radius)+
circle(st+vec2(0.,.5), radius)+
circle(st+vec2(-.5,0.), radius)+
circle(st+vec2(.5,0.), radius);
}
修改背景和圆形图案的颜色也很容易,利用内置函数 mix
可以实现:
color += mix(vec3(0.075,0.114,0.329),vec3(0.973,0.843,0.675),circlePattern(grid1,0.224));
增大半径让四个圆互相重叠,可以得到如下效果:
值得一提的是,我在 shader toy 上得到了一位名叫 FabriceNeyret2 的开发者指点,比如:
- 1-smooth(a,b) = smooth(b,a), 所以上面的 circle 函数可以改写
- 关于抗锯齿。上面原始的例子中,如果仔细观察,圆形边缘的锯齿感很强。这是由于选择了 _radius*0.01 这样的 Magic Number,当分辨率变高半径变大时,smoothstep 的插值范围也就变大了。选择基于分辨率的两个临界值能优化这一点:
float circle(vec2 U, float r) { float p = 9./iResolution.y; return smoothstep( p, - p, length(U) -r ); }
- 原本需要平移四个圆,通过 U=abs(U) 的映射,只需要两个就够了。
- 相较于在 0-1 范围内进行操作,可以采用放大两倍的方式,避免 0.5 这样的坐标值。一来是操作方便,二来减少字符总量呀。但是要注意放大之后平移到原点:
U = fract( U ) * 2. - 1. ;
短短几行就有这么大的优化空间,而且最后这位开发者提供了一个最精简的版本更是只有 180 个字符。相比我的原始 500+ 版本,不知道高到哪里去了。
圆环
知晓了圆形的画法,圆环其实就是两个圆相剪得到的区域:
color += mix(
vec3(0.075,0.114,0.329),
vec3(0.973,0.843,0.675),
// 相减
circlePattern(grid1,0.23)-circlePattern(grid1,0.170));
更加复杂的效果
结合 time 变量和时间函数,可以得到更加复杂的动画效果
其他基本图形
更多基本图形的画法,例如正方形,三角形和线段,可以参考:
都是只使用 fragment shader 完成,不需要 vertex shader 过多的参与。
总结
以上例子都是在 shader toy 上写的。参考了「the book of shaders」,原站点也提供了可在线编辑的 playground,强烈推荐阅读。