使用着色器绘制图案

2860 字
14 分钟
使用着色器绘制图案

What is UV#

在创建着色器时,我们需要绘制特定的图案,如星星、圆圈、光透镜、波浪等。虽然可以使用纹理,但绘制形状可以让我们有更多的控制权;我们可以对形状参数进行动画处理,并且没有要加载的纹理。需要在fragment shader中用到uv坐标。

什么是uv坐标?

UV坐标是用来将2D纹理贴图到3D模型表面的坐标系。这里的U和V代表的是2D空间的两个轴,类似于平面中的X和Y,但为了避免与3D空间中的X、Y、Z混淆,所以用U和V来命名。

在Threejs中UV坐标的范围是0到1,覆盖整个纹理图像。原点(0,0)在左下角,而(1,1)在右上角。uv坐标的count取决于material的分段数

如何将uv坐标发送到fragment shader?

fragment shader无法直接访问顶点数据,所以我们需要通过 vertex shader 创建vUv变量发送给 fragment shader。

vertex.glsl
varying vec2 vUv;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
vUv = uv;
}
// fragment.glsl
varying vec2 vUv;
void main()
{
// use vUv
gl_FragColor = vec4(vUv, 1.0, 1.0)
}

Pattern 1#

直接使用 uv 坐标,原点的颜色为(0.0, 0.0, 1.0, 1.0)到右上角(1.0, 1.0, 1.0, 1.0)的渐变

varying vec2 vUv;
void main() {
gl_FragColor = vec4(vUv, 1.0, 1.0);
}

image-20250422155105545
image-20250422155105545

Pattern 2#

同样的使用 uv 坐标,但b设置为0.0,为(0.0, 0.0, 0.0, 1.0)到右上角(1.0, 1.0, 0.0, 1.0)的渐变

void main() {
gl_FragColor = vec4(vUv, 0.0, 1.0);
}

image-20250422155853192
image-20250422155853192

Pattern 3#

只使用 uv 坐标的x值,得到一个在x轴上的黑白渐变。颜色在x轴上y轴上每个点都是(0.0, 0.0, 0.0, 1.0)到(1.0, 1.0, 1.0, 1.0)变化

varying vec2 vUv;
void main()
{
float strength = vUv.x;
gl_FragColor = vec4(vec3(strength), 1.0);
}

image-20250422160614995
image-20250422160614995

Pattern 4#

和 pattern 3 完全相同,不过应用在y轴上。

varying vec2 vUv;
void main()
{
float strength = vUv.y;
gl_FragColor = vec4(vec3(strength), 1.0);
}

image-20250422160752938
image-20250422160752938

Pattern 5 (1.0 - …)#

将 pattern 4 的值通过 1.0 - ...反转。(1.0, 1.0, 1.0, 1.0) 到(0.0, 0.0, 0.0, 1.0)变化

varying vec2 vUv;
void main()
{
float strength = 1.0 - vUv.y;
gl_FragColor = vec4(vec3(strength), 1.0);
}

image-20250422161048251
image-20250422161048251

Pattern 6#

将 uv.y 乘以 10,gl_FragColor将很快的到达(1.0, 1.0, 1.0, 1.0),后面大于1.0也显示白色

varying vec2 vUv;
void main()
{
float strength = vUv.x;
gl_FragColor = vec4(vec3(strength), 1.0);
}

image-20250422161448435
image-20250422161448435

Pattern 7 mod(…)#

通过取模运算 mod(...),重复某一段的变化。

将y的值乘以10.0,模1.0,分为10段,每段都是(0.0, 0.0, 0.0, 1.0)到(1.0, 1.0, 1.0, 1.0)变化。如果模0.5,最大值为0.5,结果就是(0.0, 0.0, 0.0, 1.0)到(0.5, 0.5, 0.5, 1.0)

varying vec2 vUv;
void main()
{
float strength = mod(vUv.y * 10.0, 1.0);
gl_FragColor = vec4(vec3(strength), 1.0);
}

image-20250422162044088
image-20250422162044088

Pattern 8 step(…)#

在 pattern 7 的基础上实现斑马条纹,如果颜色在0.5以下,设为0.0;在0.5以上,设置1.0。

可以通过if()判断实现,但是会影响性能。

varying vec2 vUv;
void main()
{
float strength = mod(vUv.y * 10.0, 1.0);
if(strength < 0.5) {
strength = 0.0;
} else {
strength = 1.0;
}
// or
strength = strength < 0.5 ? 0.0 : 1.0; // 三元运算符
gl_FragColor = vec4(vec3(strength), 1.0);
}

glsl 提供了step(edge, x)函数。

  • 参数:edge 是阈值,x 是输入值。
  • 返回值:若 x >= edge 返回 1.0,否则返回 0.0。
varying vec2 vUv;
void main()
{
float strength = mod(vUv.y * 10.0, 1.0);
strength = step(0.5, strength); // 超过阈值0.5,返回1.0,否则返回0.0
gl_FragColor = vec4(vec3(strength), 1.0);
}

image-20250422163923388
image-20250422163923388

Pattern 9#

通过调整 step()的阈值,更改条纹的粗细。

float strength = mod(vUv.y * 10.0, 1.0);
strength = step(0.8, strength);

image-20250422164320581
image-20250422164320581

Pattern 10#

将 pattern 9 中修改vUv.x,在x轴上分段(竖条纹)

float strength = step(0.8, mod(vUv.x * 10.0, 1.0));

image-20250422164543169
image-20250422164543169

Pattern 11 +#

将 pattern 9 和 pattern 10 的图案叠加,可以使用+取并集

float strength = step(0.8, mod(vUv.x * 10.0, 1.0));
strength += step(0.8, mod(vUv.y * 10.0, 1.0));

image-20250422164920441
image-20250422164920441

Pattern 12 *#

将 pattern 9 和 pattern 10 的图案取相交处,可以使用*取交集

float strength = step(0.8, mod(vUv.x * 10.0, 1.0));
strength *= step(0.8, mod(vUv.y * 10.0, 1.0)); // 只能看到交点处

image-20250422165050274
image-20250422165050274

Pattern 13#

在 pattern 12 上,将x轴上的分段阈值缩小,x轴方向上条纹更粗。然后取交集。

float strength = step(0.4, mod(vUv.x * 10.0, 1.0));
strength *= step(0.8, mod(vUv.y * 10.0, 1.0));

image-20250422165421888
image-20250422165421888

Pattern 14#

将 pattern 13 的图案分别按照水平方向(barX)和竖直方向(barY)实现,然后再取并集。

float barX = step(0.4, mod(vUv.x * 10.0, 1.0));
barX *= step(0.8, mod(vUv.y * 10.0, 1.0));
float barY = step(0.4, mod(vUv.y * 10.0, 1.0));
barY *= step(0.8, mod(vUv.x * 10.0, 1.0));
float strength = barX + barY;

image-20250422170934207
image-20250422170934207

Pattern 15#

基于 pattern 14 将 barX 和 barY 分别沿各自的方向偏移0.2,得到 + 图案

float barX = step(0.4, mod(vUv.x * 10.0, 1.0));
barX *= step(0.8, mod(vUv.y * 10.0 + 0.2, 1.0));
float barY = step(0.4, mod(vUv.y * 10.0, 1.0));
barY *= step(0.8, mod(vUv.x * 10.0 + 0.2, 1.0));
float strength = barX + barY;

image-20250422171220785
image-20250422171220785

Pattern 16 abs()#

绘制x轴方向,0.5到0.0到0.5的渐变。abs()取绝对值

float strength = abs(vUv.x - 0.5);

image-20250423134807735
image-20250423134807735

Pattern 17 min()#

在水平方向和竖直方向取图案的最小值min()

float strength = min(abs(vUv.x - 0.5), abs(vUv.y - 0.5));

image-20250423135138735
image-20250423135138735

Pattern 18 max()#

在水平方向和竖直方向取最大值max()

float strength = max(abs(vUv.x - 0.5), abs(vUv.y - 0.5));

image-20250423135305648
image-20250423135305648

Pattern 19#

基于 pattern 18 ,使用 step()分段显示,实现矩形环。

float strength = max(abs(vUv.x - 0.5), abs(vUv.y - 0.5));
strength = step(0.2, strength);

image-20250423135630483
image-20250423135630483

Pattern 20#

基于 pattern 19,通过取反(1.0 - ...)操作实现矩形square2,再与square1相乘取交集,构成更细的矩形环。

float square1 = step(0.2, max(abs(vUv.x - 0.5), abs(vUv.y - 0.5)));
float square2 = 1.0 - step(0.25, max(abs(vUv.x - 0.5), abs(vUv.y - 0.5)));
float strength = square1 * square2;

image-20250423140032193
image-20250423140032193

Pattern 21 floor()#

水平方向的渐变条纹,floor()向下取整,分成x段就乘x除x。

float strength = floor(vUv.x * 10.0) / 10.0;

image-20250423140221139
image-20250423140221139

Pattern 22#

基于 pattern 21, 将水平方向和竖直方向取交集得到,对角线渐变的条纹

float strength = floor(vUv.x * 10.0) / 10.0;
strength *= floor(vUv.y * 10.0) / 10.0;

image-20250423140455616
image-20250423140455616

Pattern 23 random()#

目的是每个 uv 坐标的颜色都取随机数。但是 glsl 中没有内置的随机数方法,只能通过自定义函数得到一个看起来随机的[0, 1]的数。下面的方法通过接收 vec2返回一个相对随机的值。

float random(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}

在main()中使用,形成雪花屏。因为random()是固定的,每次渲染都是相同的图案。

float strength = random(vUv);

image-20250423141028588
image-20250423141028588

Pattern 24#

基于 pattern 22 和 23,每个区域的坐标都取随机值。

vec2 gridUv = vec2(floor(vUv.x * 10.0) / 10.0, floor(vUv.y * 10.0) / 10.0);
float strength = random(gridUv);

image-20250423151504622
image-20250423151504622

Pattern 25#

基于 Pattern 24, 给竖直方向设置x偏移

vec2 gridUv = vec2(floor(vUv.x * 10.0) / 10.0, floor(vUv.y * 10.0 + vUv.x * 5.0) / 10.0);
float strength = random(gridUv);

image-20250423154626184
image-20250423154626184

Pattern 26 length()#

根据uv坐标上该点的模长。原点的值 0.0 到右上角的 √2

∥a⃗∥=x2+y2{\displaystyle \left\|{\vec {a}}\right\|={\sqrt {x^{2}+y^{2}}}}
float strength = length(vUv);

image-20250423154905082
image-20250423154905082

Pattern 27 distance()#

计算uv坐标中各点,到(0.5, 0.5)的距离,从(0.5,0.5)的 0.0 到顶点的 √0.5

∥a⃗∥=(x−0.5)2+(y−0.5)2{\displaystyle \left\|{\vec {a}}\right\|={\sqrt {\left(x-0.5\right)^{2}+\left(y-0.5\right)^{2}}}}
float strength = distance(vUv, vec2(0.5));

等价于向量减去0.5的模长。

float strength = length(vUv - 0.5);

image-20250423160137579
image-20250423160137579

Pattern 28#

基于 pattern 27 取反

float strength = 1.0 - distance(vUv, vec2(0.5));

image-20250423160229323
image-20250423160229323

Pattern 29#

基于 pattern 27,坐标距离(0.5, 0.5)越近,strength的值越大。可以做星星效果,更改分子的值。但是平面的边缘不等于0,可能在特定的角度可以看到平面

float strength = 0.015 / distance(vUv, vec2(0.5)) + 0.1;

image-20250423160502397
image-20250423160502397

Pattern 30#

在 pattern 29 上,变换vu坐标

vec2 lightUv = vec2(vUv.x * 0.1 + 0.45, vUv.y * 0.5 + 0.25);
float strength = 0.015 / distance(lightUv, vec2(0.5));

image-20250423161053035
image-20250423161053035

Pattern 31#

将 pattern 30 的水平方向和竖直方向取交集,构成十字

vec2 lightUvX = vec2(vUv.x * 0.1 + 0.45, vUv.y * 0.5 + 0.25);
float lightX = 0.015 / distance(lightUvX, vec2(0.5));
vec2 lightUvY = vec2(vUv.y * 0.1 + 0.45, vUv.x * 0.5 + 0.25);
float lightY = 0.015 / distance(lightUvY, vec2(0.5));
float strength = lightX * lightY;

image-20250423161242287
image-20250423161242287

Pattern 32 rotate()#

将 pattern 31 旋转。

glsl 中没有内置旋转的函数,通过自定义rotate()实现

参数: uv:uv坐标,rotation:旋转角度,mid:旋转中心点

#define PI 3.1415926535897932384626433832795
vec2 rotate(vec2 uv, float rotation, vec2 mid) {
return vec2(
cos(rotation) * (uv.x - mid.x) + sin(rotation) * (uv.y - mid.y) + mid.x,
cos(rotation) * (uv.y - mid.y) - sin(rotation) * (uv.x - mid.x) + mid.y
);
}

main()旋转45°

vec2 rotatedUv = rotate(vUv, PI * 0.25, vec2(0.5));
vec2 lightUvX = vec2(rotatedUv.x * 0.1 + 0.45, rotatedUv.y * 0.5 + 0.25);
float lightX = 0.015 / distance(lightUvX, vec2(0.5));
vec2 lightUvY = vec2(rotatedUv.y * 0.1 + 0.45, rotatedUv.x * 0.5 + 0.25);
float lightY = 0.015 / distance(lightUvY, vec2(0.5));
float strength = lightX * lightY;

Pattern 33#

基于 pattern 27,取阈值,可以得到圆形

float strength = step(0.25, distance(vUv, vec2(0.5)));

image-20250423161640441
image-20250423161640441

pattern 34#

基于 pattern 27 取绝对值

float strength = abs(distance(vUv, vec2(0.5)) - 0.25);

image-20250423161819875
image-20250423161819875

Pattern 35#

pattern 33 与 pattern 34 结合,得到圆环

float strength = step(0.01, abs(distance(vUv, vec2(0.5)) - 0.25));

image-20250423161946930
image-20250423161946930

Pattern 36#

pattern 35 取反

float strength = 1.0 - step(0.01, abs(distance(vUv, vec2(0.5)) - 0.25));

image-20250423162051274
image-20250423162051274

Pattern 37#

将 pattern 36 的圆环,设置竖直方向的变化

vec2 wavedUv = vec2(vUv.x, vUv.y + sin(vUv.x * 30.0) * 0.1);
float strength = 1.0 - step(0.01, abs(distance(wavedUv, vec2(0.5)) - 0.25));

image-20250423165630788
image-20250423165630788

Pattern 38#

将 pattern 37 同样设置水平和竖直的变化

vec2 wavedUv = vec2(vUv.x + sin(vUv.y * 30.0) * 0.1, vUv.y + sin(vUv.x * 30.0) * 0.1);
float strength = 1.0 - step(0.01, abs(distance(wavedUv, vec2(0.5)) - 0.25));

image-20250423165801595
image-20250423165801595

Pattern 40#

将 pattern 38 的变化周期增大。

vec2 wavedUv = vec2(vUv.x + sin(vUv.y * 100.0) * 0.1, vUv.y + sin(vUv.x * 100.0) * 0.1);
float strength = 1.0 - step(0.01, abs(distance(wavedUv, vec2(0.5)) - 0.25));

image-20250423165911909
image-20250423165911909

Pattern 41#

uv坐标取反正切atan(),f(x)=tan^(-1)(x)的取值范围是[-π, π],uv坐标没有负值取值为[0, π]

float angle = atan(vUv.x, vUv.y);
float strength = angle;

image-20250423171135603
image-20250423171135603

Pattern 42#

pattern 41 的uv坐标置于中心 -0.5

float angle = atan(vUv.x - 0.5, vUv.y - 0.5 );
float strength = angle;

image-20250423171322719
image-20250423171322719

Pattern 43#

由(0.5, 0.5)开始的角度渐变纹理

float angle = atan(vUv.x - 0.5, vUv.y - 0.5);
angle /= PI * 2.0; // 映射到 [0, 1]
angle += 0.5;
float strength = angle;

image-20250423172014681
image-20250423172014681

Pattern 44#

pattern 43 取模分段

float angle = atan(vUv.x - 0.5, vUv.y - 0.5);
angle /= PI * 2.0;
angle += 0.5;
angle *= 20.0;
angle = mod(angle, 1.0);
float strength = angle;

image-20250423172130152
image-20250423172130152

Pattern 45#

pattern 40 和 Pattern 43 组合

float angle = atan(vUv.x - 0.5, vUv.y - 0.5);
angle /= PI * 2.0;
angle += 0.5;
float sinusoid = sin(angle * 100.0);
float radius = 0.25 + sinusoid * 0.01;
// 计算到中心点(0.5, 0.5)的距离,sqrt((x-0.5)^2 + (y-0.5)^2)
float strength = 1.0 - step(0.01, abs(distance(vUv, vec2(0.5)) - radius));

image-20250423172902748
image-20250423172902748

Pattern 46 perlin noise#

perlin noise是自然噪声生成算法,具有在函数上的连续性,并可在多次调用时给出一致的数值。用来生成噪声图样,生成随机性、云朵,水面的倒影等等。perlin noise

下面是其中一种算法, Classic Perlin 2D Noise

// Classic Perlin 2D Noise
// by Stefan Gustavson (https://github.com/stegu/webgl-noise)
//
vec2 fade(vec2 t) {
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
vec4 permute(vec4 x) {
return mod(((x * 34.0) + 1.0) * x, 289.0);
}
float cnoise(vec2 P) {
vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
Pi = mod(Pi, 289.0); // To avoid truncation effects in permutation
vec4 ix = Pi.xzxz;
vec4 iy = Pi.yyww;
vec4 fx = Pf.xzxz;
vec4 fy = Pf.yyww;
vec4 i = permute(permute(ix) + iy);
vec4 gx = 2.0 * fract(i * 0.0243902439) - 1.0; // 1/41 = 0.024...
vec4 gy = abs(gx) - 0.5;
vec4 tx = floor(gx + 0.5);
gx = gx - tx;
vec2 g00 = vec2(gx.x, gy.x);
vec2 g10 = vec2(gx.y, gy.y);
vec2 g01 = vec2(gx.z, gy.z);
vec2 g11 = vec2(gx.w, gy.w);
vec4 norm = 1.79284291400159 - 0.85373472095314 *
vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11));
g00 *= norm.x;
g01 *= norm.y;
g10 *= norm.z;
g11 *= norm.w;
float n00 = dot(g00, vec2(fx.x, fy.x));
float n10 = dot(g10, vec2(fx.y, fy.y));
float n01 = dot(g01, vec2(fx.z, fy.z));
float n11 = dot(g11, vec2(fx.w, fy.w));
vec2 fade_xy = fade(Pf.xy);
vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
return 2.3 * n_xy;
}

main()使用

float strength = cnoise(vUv * 10.0);

image-20250423174617343
image-20250423174617343

Pattern 47#

cnoise()返回的是正数和负数,step()取值

float strength = step(0.0, cnoise(vUv * 10.0));

image-20250423174831480
image-20250423174831480

Pattern 48#

ads()

float strength = 1.0 - abs(cnoise(vUv * 10.0));

image-20250423174923302
image-20250423174923302

pattern 49#

线条变化

float strength = sin(cnoise(vUv * 10.0) * 20.0);

image-20250423174959444
image-20250423174959444

pattern 50#

pattern 49 取 step()

float strength = step(0.9, sin(cnoise(vUv * 10.0) * 20.0));

image-20250423175040761
image-20250423175040761

Mix Color#

混合颜色,通过mix()混合颜色

float strength = step(0.9, sin(cnoise(vUv * 10.0) * 20.0));
vec3 blackColor = vec3(0.0); // black
vec3 uvColor = vec3(vUv, 1.0);
vec3 mixedColor = mix(blackColor, uvColor, strength); // 混合颜色
gl_FragColor = vec4(mixedColor, 1.0);

image-20250423175503213
image-20250423175503213

但是在 pattern 11 中,图案叠加时strength超过了1,混合是第三个值会超过1,颜色的值被计算到更大的范围,所以需要限制界限

float strength = step(0.8, mod(vUv.x * 10.0, 1.0));
strength += step(0.8, mod(vUv.y * 10.0, 1.0));
vec3 blackColor = vec3(0.0); // black
vec3 uvColor = vec3(vUv, 1.0);
vec3 mixedColor = mix(blackColor, uvColor, strength); // 混合颜色
gl_FragColor = vec4(mixedColor, 1.0);

交点处颜色明显更亮,因为 strength > 1.0

image-20250423175559671
image-20250423175559671

strength = clamp(strength, 0.0, 1.0); // 限制范围在0-1之间

image-20250423175613542
image-20250423175613542

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

使用着色器绘制图案
https://sukksa.github.io/posts/shi-yong-zhe-se-qi-hui-zhi-tu-an/
作者
Sukksa
发布于
2025-05-07
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Sukksa
记录前端学习与日常
公告
博客刚刚上线,内容还在慢慢补齐。欢迎随便逛逛~
分类
标签
最新动态
站点统计
文章
69
分类
16
标签
17
总字数
178,517
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录