type
Post
status
Published
date
Mar 12, 2026
slug
summary
片段着色器上色案例:
百叶窗效果
百叶窗2(阶跃函数)
棋盘格
点阵图
同样的思想,不同的效果
中心渐变扩散
四周扩散
四周扩散2
四周扩散3
百叶窗3
噪点(随机性函数)
噪点(伪随机性)
噪点2(伪随机性)
取长度
中心光束
拉伸效果
拉伸效果叠加
旋转
画圆
渐变圆
渐变圆2
正弦变化
反正切
柏林噪声
柏林噪声2
颜色混合
实现水面波浪效果
tags
Three.js
category
技术学习笔记
icon
password
片段着色器上色案例
之前在片段着色器里通过下面的代码给平面上了红色
注意片段着色器是针对每个片段(像素)去计算的,也可以理解为有多少个顶点就会执行多少次片段着色器代码
所以只要想办法给每个顶点设置不同的
r、g、b 值,就可以实现简单的渐变颜色了在几何体中存在一个
uv 属性,这个属性记录的是几何体的 uv 坐标,通常在纹理映射时会用到:
在这里插入图片描述
所以,可以直接利用这个值来实现渐变,在顶点着色器中通过
varying 将 uv 传给片段着色器:顶点着色器的uv无需声明是因为threejs中使用了ShaderMaterial而不是RawShaderMaterial
效果:

每一个
uv 坐标都是一个 Vector2 ,所以创建时可以使用 vec4(vUV, 1.0, 1.0) 快捷创建一个 vec4也可以将
vUV 的位置放在 rgb 中 g 和 b 的位置来得到不同的渐变,比如:
在这里插入图片描述
再比如:

在这里插入图片描述
那如果需要一个黑到白的渐变色呢?
要实现这个效果,就要先知道
是怎么映射的;
这个平面几何体的
坐标实际上是以下面这个坐标系来定值的:


在这里插入图片描述
而
rgb 值均为0时是黑色, rgb 值均为1时则是白色,所以从左到右的黑白渐变就可以写成:
在这里插入图片描述
如果想要实现从上到下的白到黑渐变,则只需要使用
vUV.y 来实现;同时,可以通过用 vUV * 一定的数值 来控制渐变的数值,如:
在这里插入图片描述
这是因为本身
uv 坐标就是从0到1的,越靠近1那乘以一定的数值就越接近1或大于1,所以可以得到上面的效果,自己稍微思考一下即可百叶窗效果

在这里插入图片描述
从白到黑的渐变本质就是控制
rgb 颜色从(1,1,1)到(0,0,0),很容易想到多层渐变的效果通过取余就可以实现,但是 glsl 里的取余不是 % ,而是通过 mod 函数实现:百叶窗2(阶跃函数)

在这里插入图片描述
如果要实现”逐层切分“的效果,实际要实现的是一个限制条件:
大于某值时,取1;小于某值时,取0 ,比如:也可以使用三元表达式来判断:
但是,使用条件判断实际上会导致的一定的性能问题。因为着色器代码是运行在
GPU 中的,每个顶点都会跑一次,条件判断对GPU内的”分支发散“、”指令缓存“、”动态调度“等计算操作都会有影响,感兴趣的可以再深入了解所以这里最好的办法是使用
glsl 中提供的 step 函数来解决, step 函数支持传入两个值,第一个值是 阈值 ,第二个值是输入值;当输入值大于等于指定阈值时,函数返回1,否则返回0棋盘格
接着上面的代码,在上面我们实现了垂直方向百叶窗的效果,那水平方向百叶窗的效果无非就是对X轴的
uv 坐标取余和跃进,比如:
在这里插入图片描述
那棋盘格效果该怎么实现呢?可以自己思考一下,怎么实现下面这个效果:

实际上就是横向百叶窗和纵向百叶窗效果的叠加,也就是将横向和纵向的坐标计算结果加起来即可:
你可以自己分析思考一下,为什么两者加起来就能实现这样的效果,笔者当时是想了很久的(提示:打印出每个uv坐标的值和strenth的计算值来分析)
点阵图
只需要修改一个地方就可以实现下面的效果:

将垂直与竖直方向的
取余跃进结果 相乘即可:为什么是这样?我们从下图红色箭头所指的地方开始分析

我们假设一个像素就是图中所指的方块这么大,横向的百叶窗在此处是白色,也就是
strengthX 求得的值是 1纵向的百叶窗在此处也是白色,那么
strengthY 值也是 1 ,所以最终这个像素的颜色就是 vec4(1*1, 1*1, 1*1, 1) 也就是白色那么黑色的点就相当于是
vec4(1*0, 1*0, 1*0, 1) ,即黑色同样的思想,不同的效果
这里再贴几个使用同样的思想来实现的效果:
横向线阵:

横纵向线阵叠加:

另一个线阵叠加:

中心渐变扩散

在这里插入图片描述
从左到右的黑白渐变是通过取x轴
uv 坐标当作顶点颜色rgb的值来实现的,那从中心进行发散呢?实际上只需要通过一个取绝对值即可实现:
坐标在
轴的区间是
,
后就是
,那么取绝对值后,就可以实现从中间0到两边0.5的渐变

四周扩散

在这里插入图片描述
这个怎么实现?上面已经实现了横向的中心渐变扩散,纵向的中心渐变扩散只需要将输入值从
vUV.x 改为 vUV.y 即可,也许将他们叠加就可以实现?比如:然而得到的效果:

得到这样的效果其实也很好理解,如果是叠加的话,则横纵方向从中心到两边就是
[0, 0.5] + [0, 0.5] ,在中心是 0 + 0 ,在四个对角就是 0.5 + 0.5那我们要的效果到底该怎么实现?


在这里插入图片描述
我们将横纵方向的中心渐变图放在一起就不难看出来,黑色的部分实际上取的是各自方向上的黑色部分,比如:

越黑意味着值越接近0,那么只需要对那个顶点的
x、y 方向的 uv 坐标取最小值即可:四周扩散2

在这里插入图片描述
这个效果其实和上面的类似,只需要在每个顶点取
vUV - 0.5 坐标的最大值即可:这个图看着像个深不见底的走廊,所以其实一些静态的走廊场景就是用这种原理实现的
四周扩散3
这个应该不难想:

在四周扩散2中我们取的最大值的区间仍然是
[0, 0.5] ,那么我们只需要给定一个 0 - 0.5 的阈值即可,大于它取1,小于它取0:百叶窗3

在这里插入图片描述
这是一个横向的渐变 + 百叶窗效果,分析这张图的话大体可以得到10个预估值:

正常来说,可以这么写:
但是之前讲过在GPU中运行条件判断是不合适的,应该尽量使用数学方法
glsl 里的四舍五入也是 floor 方法,对于 floor(1.2) 会得到 1 , floor(2.2) 会得到 2 ,以此类推,如果对其再除以10,就可以得到 0.1、0.2... ,所以,可以写出这样的代码:然后可以思考一下下面这个怎么实现的:

噪点(随机性函数)

在这里插入图片描述
GLSL 不像JS一样提供 Math.random 来创造随机的数值,它本身并不存在随机函数在
book of shader 社区有对在 glsl 里使用随机性的方案说明,地址是: https://thebookofshaders.com/10/ ,感兴趣的可以到这里了解其原理的实现总的来说是社区的一位大佬利用正弦函数和小数点提取函数创造出了随机函数,它接收一个
vec2 坐标来返回一个随机的浮点数:噪点(伪随机性)

在这里插入图片描述
可以想一下,为什么上面的代码可以得到这样的结果?
第一个噪点案例的随机性实际上是因为“所有的uv坐标都不相同”,而在这个案例中,传入的uv坐标在x轴和y轴上的取值均为
0.1、0.2、0.3... ,这本身就降低了整体的随机性,所以会得到上述图的结果噪点2(伪随机性)
在上例的基础上添加倾斜效果:

纵向的倾斜实际上只需要给纵向的值上加上一个横向随机值控制即可实现:
取长度

在这里插入图片描述
实际上我们很容易可以发现这个例子的规律:

取
vUV 坐标向量的长度当作 r、g、b 的值即可:然后可以思考一下这个怎么实现:

答案:
其实也很好理解,
相当于将
坐标的取值区间放在了
,原本的
变成了
,
函数取得的值为
,
也是一样,等同于将原点移动到了
坐标系的中心:

除了
length 函数可以实现计算长度外, distance 函数也可以计算长度,他接收两个 vec2 坐标,用于计算两个坐标之间的长度,所以上面的写法也可以改成:所以,有了
的计算原理,也可以试着将中心放在右下角,比如使用

中心光束
如果要实现下面这样的效果,该怎么设置:

float strenth = distance(vUV,vec2(.5)) 呈现的效果是中心暗(值小),两边亮(值大),取值区间在 [0, 1 / 根号2] ,用1减去该值,取值区间就变为: [1 - 1 / 根号2, 1] ,获得下面的效果:
在这里插入图片描述
这样确实是实现了中心亮两边暗,但是中心的白色明显密度不够,我们需要实现更快的值衰减速度,其实通过简单的除法就可以实现,比如:

约靠近
(0.5, 0.5) ,它的 distance 约趋于 0 ,当值小于 0.1 时, 0.1 除以该值得到的值越趋近于 1 ,所以亮度越大,需要缩小中心白色的范围则只需要将被除数缩小即可,比如 0.1 改为 0.015 即可得到理想的效果拉伸效果
在上例的基础上,可以调整对应的比例以实现下面的拉伸效果:

首先以基本的
uv 坐标创建一个新的二维向量,方便我们调整 x、y 的坐标先实现x轴方向的拉伸,给x设置一个缩放倍数,比如
0.5 :现在得到了下面的效果:

乘法会造成一定的偏移量(实际上是整个x轴的方向都存在拉伸,但看起来是偏移导致的),可以通过加减法来将其移动回来,这里需要在原来的基础上加上
0.25 :现在可以得到下面的效果:

显然拉伸的效果不够,将x轴的乘数改为
0.1 ,修改拉伸的比例:得到下面这样的效果:

在这里插入图片描述
亮点被移动到了屏幕右边外,所以我们的偏移量也应该增加调整,比如:
这样就成功将其移回了屏幕中心:

原图中,纵向也有一定的拉伸,所以经过适当调整,最后的数值为:
所以,我们要实现某个效果时,不一定能一次就成功,往往需要不断地尝试不同值的组合来得到我们最终要的效果。
拉伸效果叠加

在这里插入图片描述
注意需要用乘法而不是加法:
旋转
在
glsl 里实现旋转就比较复杂了,建议还是在 threejs 中旋转整个 mesh 来处理,这里直接记录一下公式:
在这里插入图片描述
完整代码:
旋转的角度,通常要与
PI 挂钩, GLSL 里不存在已声明的 PI ,需要我们自行声明,比如:注意,这里
#define 声明常量的结尾不需要加分号!类似于C语言,可以通过
#define 来声明一个常量使用,所以如果要旋转45度,则:一个PI对应的是一个半圆
画圆

在这里插入图片描述
实际上就是上面的取长度例子中加上跃进函数处理即可:
渐变圆

在这里插入图片描述
比较好理解,不写出来了
渐变圆2

在这里插入图片描述
正弦变化

在这里插入图片描述
振幅改为20(
),可以获得更奇怪的效果:

当然,设置不同的数值可以得到不同的效果,这里就自行尝试了,比如:

在这里插入图片描述
反正切

在这里插入图片描述
实现代码:
atan 是一个内置的反正切函数,对点 (vUV.x,vUV.Y) 取反正切值,得到的是一个角度值,范围在-Π到Π之间,这个角度值表示从原点到 (vUV.x,vUV.Y) 的方向与正X轴之间的夹角与
的取值区间均为
,所以在
度角的那条分割线上,求得
值均为
,比如(
)

柏林噪声
Perlin Noise 是一种由Ken Perlin发明的自然噪声生成算法,通常用于生成逼真的纹理、地形、云层、水底倒影等效果,感兴趣的可以自行搜索了解
在这里插入图片描述
为了使用该算法,可以前往这个网站:
,往下划会看到一个”Classic PerlinNoise”:

这个算法里用到了一个
算法,但是没有写在这里,你需要在页面中搜索该关键字,然后找到这一句:

将它们一同复制到
glsl 代码里并使用,即可得到图上的效果同理,可以使用跃进函数来控制噪声值:

在这里插入图片描述
柏林噪声2

在这里插入图片描述

在这里插入图片描述
颜色混合
glsl 中的 mix 函数可以实现线性插值,通常用来实现两种颜色的混合mix 函数接收3个参数: genType mix(genType x, genType y, genType a)- x:第一个值。
- y:第二个值。
- a:插值因子,范围通常在 0.0 到 1.0 之间。
x、y 可以是 float 、 vec2 、 vec3 等返回值是 x 和 y 之间的线性插值结果,计算公式为:
x * (1.0 - a) + y * a比如对上面柏林噪声的示例进行插值计算颜色:

在这里插入图片描述
总结:各种不同的计算方式组合就可以得到各种各样的形状,核心在于不断地尝试,无法直接想到是很正常的。基本的试验思路就是,找一个大概的实现,在其基础上不断修改参数,直到获得预期效果
实现水面波浪效果

在这里插入图片描述
初始化
首先搭建一个基本场景,创建一个平面

在这里插入图片描述
替换材质为着色器材质
在
src 目录里创建 shaders 文件夹、顶点着色器、片段着色器文件,添加基本的代码如下:将基础材质换为着色器材质:

在这里插入图片描述
创造Y轴的波浪
modelPosition 是通过模型矩阵转换而来的世界坐标系的坐标,它是我们在 threejs 里对这个平面 mesh 的旋转、平移、缩放等操作过后算出来的每个顶点的坐标,修改 modelPosition 就等同于直接修改顶点最终在X、Y、Z轴的坐标比如们在顶点着色器里执行
modelPosition.y+=1.0; 的操作:它实际上就是对所有顶点在世界坐标系上往Y轴正半轴移动1.0个单位:

理解了这一点后,要添加波浪效果实际上就是对每个顶点的Y轴坐标创建一定量的偏移,所以使用
sin 函数可以很轻松地达到这点:
在这里插入图片描述
为什么?接下来以实际的值来举例解释:
我们创建的平面是宽高均为
的平面,并且通过旋转-90°
使其与Y轴垂直,这些变换被
内部计算成对应的矩阵输出,也就是
,然后每个顶点经过变换得到了下面的图:

比如上图中的
modelPosition = (1, 0, 0) 对应的原本的 position 就是 (1,0,0)modelPosition = (1, 0, -1) 对应的原本 position 就是 (1, -1, 0)这是未旋转时的坐标:

当
modelPosition = (1, 0, 0) 进行 modelPosition.y+=sin(modelPosition.x) 计算时,实际上是:其他点的计算以此类推,最终得到一个波浪的形状
我们可以在
threejs 里把平面的宽高改为 PI * 2 进行测试:那么,顶点坐标未变化前就是:

经过
modelPosition.y+=sin(modelPosition.x) 的变化后:所以此时图像上得到:

创建平面部分的最终代码:
控制Y轴波浪的频率
到目前为止,平面的宽高均为2,
modelPosition.x 的取值范围在 [-1, 1] ,所以 sin(modelPosition.x) 的取值范围在 [-0.841, 0.841]如果对
modelPosition.x 乘 5 , sin(modelPosition.x) 的取值范围就会在 (-1, 1) ,正弦函数 [-5, 5] 区间的图像会被表示出来,并且呈现一个压缩的状态:
在这里插入图片描述
正弦函数图像:

代码:
所以,我们可以通过一个
uniform 自定义变量来控制其振动频率(波峰的个数):在顶点着色器中获取该值:

在这里插入图片描述
创造Y轴的波浪(使用Z坐标)
在当前例子中,平面原本的Y轴坐标恒为0,每个点只有x、z轴坐标有值,所以我们还可以通过
Z 轴坐标来创建 Z 方向的 Y 轴波浪:
在这里插入图片描述
控制振幅
水面的波浪不需要这么猛烈,也就是说需要控制一定的振幅,减小振幅实际上就是减小
sin(modelPosition) 的值,所以直接使用一个新的变量来控制即可,比如:现在,就可以通过修改
来控制振幅了:

波浪通常不可能存在一定的规律,所以Z方向和X方向的震动频率我们可以分别用两个值来控制:

在这里插入图片描述
这样就完成了一个基本的波浪效果
添加动画
先在片段着色器中把波浪的颜色改一下,改成蓝色,看着没那么辣眼睛:
要实现动画,实际上就是需要一个随时间不断变化的值,在
threejs 里通常使用 elapsetime 记录从第一次渲染到此时此刻经过了多少秒,所以直接使用这个值控制动画即可:在顶点着色器中获取该值,用其控制函数周期即可:

在这里插入图片描述
控制速度
所谓控制速度实际上就是控制随机值的变化速度,也就是控制
uTime 的值,所以添加一个变量来控制其比例即可:控制颜色
这里要实现的是通过gui面板控制颜色,然后将颜色参数传给片段着色器,片段着色器使用对应的颜色进行渲染
首先实现GUI面板控制颜色,需要另外创建一个Object来保存gui修改的值,在修改的回调中更新颜色值:
然后在片段着色器中获取
uSurfaceColor 的值修改波浪颜色:THREE.Color 传到片段着色器后实际上是一个vec3类型的数据,可以通过r、g、b或x、y、z访问值,也可以使用 swizzle 操作快速生成 vec4
在这里插入图片描述
混合颜色,构建层次感
波浪颜色通常不会是固定的蓝色,而是有深蓝、浅蓝、白等颜色组成的:

所以我们可以再添加一个颜色,通过点的深度(偏移量)来控制着色器对某片段在两种颜色中进行混合着色
首先多创建一个
uDepthColor 变量,以同样的方式使用控制面板操作颜色的变化:
在这里插入图片描述
在顶点着色器里,每个
modelPosition.y 发生一定量的偏移使得波浪的形状被构建出来,那么这个偏移量就是我们需要用来控制颜色混合的值,通过 varying 就可以将其传给片段着色器进行着色:在片段着色器中获取该变量并使用:
这里使用了
内置的
函数,
函数的解释如下:

所以当
趋近于1时,就会取
,反之亦然,最终得到的效果:

回过头来看
vElevation 的值,默认情况下,它的值求出来的实际区间大致在 [-0.4, 0.4] , mix 函数要求的区间是0到1,所以我们也可以多添加两个变量来纠正(或者说控制)片段着色器的 mix 值区间,这一步就不写出来了,读者可以自己试着操作一下现在,我们就可以修改颜色以获得不同的波浪颜色混合的效果了:

在这里插入图片描述
添加细节 - 小波浪
目前的波浪还是有迹可循的,即不够真实,真实世界中的波浪,存在很大的随机性,所以,我们可以通过柏林噪声算法来构建出这种随机性
打开这个网站:
,把3D柏林噪声算法的代码拷贝到片段着色器中

代码太多,这里就不贴代码了
然后,给顶点的偏移值加上
cnoise 算法控制:cnoise 函数接收一个 vec3 ,如果直接传 modelPosition 那必然创造不出随机性,因为每个点的 modelPosition 都是固定的,所以 vec3 的第三个值我们使用 uTime ,让每个点在不同时刻的偏移量都不相同,这样操作下来的得到结果就是:
在这里插入图片描述
以现在的情况看来,明显还不是所谓的小波浪,因为有两个问题:
- 波浪的震动频率不够快(
modelPosition.xz)
- 波浪的震动速度太快(
uTime)
试着对
modelPosition.xz 和 uTime 加上一定比例的控制,比如:这样就得到了如下效果:

在这里插入图片描述
但是现在整体的振幅又太大了,所以只需要对整体得到的柏林噪声振幅值缩小即可:

在这里插入图片描述
波纹反向
目前我们得到的波纹是既有向上突起的波纹,也有向下凹陷的波纹

这是因为
cnoise 运算后的结果是存在负数的,其函数图像是不规则的波峰波谷交替图像,所以我们可以通过 abs 来控制水面的形状:这样,就相当于将所有波峰给翻转成了波谷:

这个颜色可能看的不够明显,下面修改一下颜色,可以得到:

总结
从宏观来看,实现水面波纹的效果实际就是正弦函数图像在z方向、x方向上的绘制;
正弦函数图像的特点是波峰、波谷、振动频率、振幅,使用变量对其进行操控,加上时间的变化值即可实现动画
柏林噪声算法用于创建随机性,创建出水面波纹各异的效果,为了将图像波峰反向,使用了
-abs 来实现在实现过程中,需要不断地测试值来达到我们的理想效果,
lil-gui 就是一个不错的调试的工具