手把手带你入门 Three.js Shader 系列(四)

通过前两篇文章的学习,相信大家对片元着色器里的 uv 纹理坐标、GLSL 里的 sin、step、distance 等内置函数,以及如何应用这些知识绘制颜色渐变与突变、重复条纹、圆圈等效果都有了一定了解。如果还不了解可以看看「手把手带你入门 Three.js Shader 系列(二) - 古柳 - 20230716」「手把手带你入门 Three.js Shader 系列(三) - 古柳 - 20230725」

但有个问题还没解决,那些条纹圆圈都是黑白的,而实际中我们希望应用上想要的颜色,比如类似下图两种彩色交替的条纹小球,又该如何实现?(看到这张图想到棋盘格子的效果后续也可以加进来讲解下)

内置函数 mix 线性插值

其实上篇文章末尾有"剧透",我们用 mix 内置函数对两种颜色进行插值即可轻松实现,之前的代码仍可使用。

mix(x, y, a) 为线性插值,结果为 x*(1-a)+y*a,浮点数 a 的范围是0.0到1.0,根据其数值大小对 x、y 进行插值。当 a=0.0 时,mix 的结果为 x;a=1.0 时,结果为 y;a=0.5 时,结果为 (x+y)/2.0 ... 其中 x、y 可以是 float/vec2/vec3/vec4 等数据类型,只要两者类型一致就行,插值后的结果也是同一类型。

C# 复制代码
// linear interpolation 线性插值
mix(x, y, 0.0) => x
mix(x, y, 1.0) => y
mix(x, y, 1.0) => (x + y) / 2.0
mix(x, y, a) => x * (1 - a) + y * a

比如当 x、y 为两种 vec3 颜色,用 vUv.x 去插值时,结果就是分别对 rgb 各分量套下上述公式,就能算出具体颜色值。

C# 复制代码
mix(color1, color2, vUv.x);
x = color1 = vec3(1.0, 0.0, 0.0) = red
y = color2 = vec3(0.0, 1.0, 0.0) = green
a = vUv.x = 0.0 - 1.0 范围

mix(x, y, a) = x * (1-a) + y * a
当 a = 0.25 =>
x * (1 - 0.25) + y * 0.25 
= x * 0.75 + y * 0.25
= vec3(1.0, 0.0, 0.0) * 0.75 + vec3(0.0, 1.0, 0.0) * 0.25 
= vec3(0.75, 0.25, 0.00) // 分别对 rgb 分量套下公式得到对应数值

知道了 mix 怎么使用后,我们将前两篇文章里各种方式生成的 0-1 数值再去 mix 两种想要的颜色即可解决上文提到的问题。

不过毕竟这篇文章又是过去快4个月才更新(群里倒是更活跃些,欢迎加入「可视化交流群」进行交流,加古柳微信「xiaoaizhj」备注「可视化加群」即可,也有机会围观古柳朋友圈,实时追踪最新动态!),可能大家也都忘了前面学过的知识,加上大家自己去实现难免会出现些奇奇怪怪的问题,所以还是古柳带大家一起实现下各种效果,并穿插讲解些新的内容。话不多说,进入正题。

黑白渐变与突变

首先从熟悉的黑白渐变讲起,准备好两种 vec3 格式的黑白颜色,通过 vUv.x 去插值颜色,效果和直接用 vec3(vUv.x) 一样。结合 step 就能实现黑白突变的效果。注意 color 变量也是 vec3 具体要和被插值数据的类型一致。

C# 复制代码
varying vec2 vUv;

void main() {
  vec3 color1 = vec3(0.0);
  vec3 color2 = vec3(1.0);
  // 黑白渐变
  vec3 color = mix(color1, color2, vUv.x);
  // vec3 color = vec3(vUv.x); // 相同效果
  // 黑白突变
  // vec3 color = mix(color1, color2, step(0.5, vUv.x));
  // vec3 color = vec3(step(0.5, vUv.x));
  gl_FragColor = vec4(color, 1.0);
}

红绿渐变和突变

用 mix 去插值黑白颜色多少有些"脱裤子放屁------多此一举",那么替换成实际的两种彩色,比如红色、绿色......嗯,虽然 mix 简单好用的特性开始显现,但这配色也是一言难尽,所以还是赶紧换掉吧!

C# 复制代码
void main() {
  vec3 color1 = vec3(1.0, 0.0, 0.0);
  vec3 color2 = vec3(0.0, 1.0, 0.0);
  vec3 color = mix(color1, color2, vUv.x);
  // vec3 color = mix(color1, color2, step(0.5, vUv.x));
  gl_FragColor = vec4(color, 1.0);
}

粉色青色黄色渐变和突变

当然这里的目的不在于挑选合适的颜色,而是为了后续讲解方便,所以古柳简单地从 rgb 任意两个分量为 1.0 的三种颜色------黄色、青色、粉色------里选了一组配色,比如黄色搭配青色的看着不错,作为后续演示的配色!另外这里将插值的数值改用 mixer 变量表示,后续公式复杂了抽离出来更直观。

C# 复制代码
void main() {
  // vec3(1.0, 0.0, 1.0) // pink 粉色
  vec3 color1 = vec3(1.0, 1.0, 0.0); // yellow 黄色
  vec3 color2 = vec3(0.0, 1.0, 1.0); // cyan 青色
  float mixer = vUv.x;
  // float mixer = step(0.5, vUv.x);
  vec3 color = mix(color1, color2, mixer);
  gl_FragColor = vec4(color, 1.0);
}

重复条纹+插值颜色

敲定好颜色后,我们对之前实现的重复条纹再应用上颜色,将 vUv.x 乘以3.0再取小数后变成重复3次的0.0-1.0数值作为 mixer 去插值两种颜色即可。替换几何体后彩色条纹小球也就一并实现出来了!

C# 复制代码
void main() {
  vec3 color1 = vec3(1.0, 1.0, 0.0);
  vec3 color2 = vec3(0.0, 1.0, 1.0); 
  float mixer = fract(vUv.x * 3.0);
  // float mixer = step(0.5, fract(vUv.y * 3.0));
  vec3 color = mix(color1, color2, mixer);
  gl_FragColor = vec4(color, 1.0);
}

圆圈+插值颜色

在圆圈效果上应用同样很简单,将离中心的距离数值作为 mixer 去插值颜色即可。有了前几篇的讲解,相信大家对这里的实现应该不会有什么疑问了。重复圆圈和替换几何体后的效果也一并附上。

C# 复制代码
void main() {
  vec3 color1 = vec3(1.0, 1.0, 0.0);
  vec3 color2 = vec3(0.0, 1.0, 1.0); 
  float mixer = length(vUv - vec2(0.5));
  // float mixer = length(fract(vUv * 3.0) - vec2(0.5));
  mixer = step(0.25, mixer);
  vec3 color = mix(color1, color2, mixer);
  gl_FragColor = vec4(color, 1.0);
}

对角线上颜色渐变

前几个例子都是炒炒冷饭,所以讲解得很快,这里古柳突然想到可以讲下如何在对角线上应用颜色渐变,并借此讲解些新内容。

比如我们想在左下角到右上角的对角线上进行渐变,直接用 vUv.x+vUv.y 作为 mixer 会发现当到达 (0.5,0.5) 后再往右上方数值都是大于1.0,所以插值出来的颜色都会是 color2 即青色。

C# 复制代码
void main() {
  vec3 color1 = vec3(1.0, 1.0, 0.0);
  vec3 color2 = vec3(0.0, 1.0, 1.0); 
  float mixer = vUv.x + vUv.y;
  vec3 color = mix(color1, color2, mixer);
  // vec3 color = vec3(mixer);
  gl_FragColor = vec4(color, 1.0);
}

解决办法很简单,因为此时从左下角到右上角 mixer 范围是 0.0-2.0,那么除以2.0缩小一倍变回0.0-1.0即可。

C# 复制代码
float mixer = (vUv.x + vUv.y) / 2.0;

如何想将颜色反过来,可以把 mixer 数值用 1.0 减去,这样就从 0.0-1.0 变化到 1.0-0.0;或者直接在 mix 时调换两种颜色顺序也行。

C# 复制代码
float mixer = 1.0 - (vUv.x + vUv.y) / 2.0;
// 或者调换颜色顺序,任选其一
// vec3 color = mix(color2, color1, mixer);

对角线上颜色突变

将 mixer 数值通过 step 函数处理就能得到颜色突变的三角形拼接效果。

C# 复制代码
void main() {
  vec3 color1 = vec3(1.0, 1.0, 0.0);
  vec3 color2 = vec3(0.0, 1.0, 1.0); 
  float mixer = (vUv.x + vUv.y) / 2.0;
  // float mixer = 1.0 - (vUv.x + vUv.y) / 2.0;
  mixer = step(0.5, mixer);
  vec3 color = mix(color1, color2, mixer);
  gl_FragColor = vec4(color, 1.0);
}

对角线上渐变过去再渐变回来(来去之间x)

前面的渐变都是有去无回的,那么如何实现这种从黄色渐变到青色再渐变回黄色的效果呢?这里古柳建议大家在此暂停阅读、先尝试下自己实现,如果之前没接触过下面所讲的方法,应该还是蛮难想到该如何实现的吧!

希望大家真的尝试过后再看这里的讲解,那样印象更深,当然毫无头绪也很正常,不卖关子了,新知识来袭!

从左下角到右上角 vUv.x+vUv.y 数值从 0.0-2.0 变化,在中间位置数值就是1.0为青色,这说明左下方没什么问题,想渐变回去,就是要把右上方 1.0-2.0 变到 1.0-0.0,此时数值就是和左下方沿着对角线对称。

也就是说我们的目的是得到图中第3个效果0.0到1.0再到0.0,目前有了第1个效果 vUv.x+vUv.y,第2个效果结合上面颜色反过来的操作,可以通过 2.0-(vUv.x+vUv.y) 得到,接下来的问题就是如何把这两个效果进行结合?

C# 复制代码
float mixer1 = vUv.x + vUv.y;
float mixer2 = 2.0 - (vUv.x + vUv.y);
float mixer = ???

一般涉及这种结合的,比较常见的就是相加、相减、相乘、相除、或取最小值、取最大值。我们将一些位置的uv值带入上述公式去进行验证,比如左下角(0.0,0.0)、右上角(1.0,1.0)、左下方某点(0.1,0.3)、右上方某点(0.8,0.7)...可以快速排除掉加减乘除都不符合,而取两者的最小值正好符合要求。

ini 复制代码
左下方某点 (0.1,0.3) 
  mixer1 => vUv.x+vUv.y=0.4
  mixer2 => 2.0-(vUv.x+vUv.y)=1.6
  mixer => 0.4 <= min(0.4, 1.6)
右上方某点 (0.8,0.7)
  mixer1 => vUv.x+vUv.y=1.5
  mixer2 => 2.0-(vUv.x+vUv.y)=0.5
  mixer => 0.5 <= min(1.5, 0.5)

于是完整代码如下。

C# 复制代码
void main() {
  vec3 color1 = vec3(1.0, 1.0, 0.0);
  vec3 color2 = vec3(0.0, 1.0, 1.0); 
  float mixer1 = vUv.x + vUv.y;
  float mixer2 = 2.0 - (vUv.x + vUv.y);
  float mixer = min(mixer1, mixer2);
  // mixer = step(0.5, mixer);
  vec3 color = mix(color1, color2, mixer);
  // vec3 color = vec3(mixer);
  gl_FragColor = vec4(color, 1.0);
}

当然如果大家懒得去验证,也可以在代码里直接修改组合方式,通过图形直观地看是不是想要的效果这样也行。下图依次为加、减、乘、除、取最小值对应的情况。

C# 复制代码
float mixer1 = vUv.x + vUv.y;
float mixer2 = 2.0 - (vUv.x + vUv.y);

float mixer = mixer1 + mixer2;
// float mixer = mixer1 - mixer2;
// float mixer = mixer1 * mixer2;
// float mixer = mixer1 / mixer2;
// float mixer = min(mixer1, mixer2);

// vec3 color = vec3(mixer);
vec3 color = mix(color1, color2, mixer);

可以看出来相乘时的效果和实际所需的取最小值的效果最为接近,一开始古柳看下面这张图前两种效果时,把其中白色的位置数值误认为都是1.0,然后代入几个位置的数值后发现,相乘好像也可以,比如 (0.3,0.4) => mixer1=0.7、mixer2=白色=1.0 => mixer=0.7*1.0=0.7 那确实也行,而忘了白色部分是 vUv.x+vUv.y 或 2.0-(vUv.x+vUv.y) 数值里1.0-2.0的范围,相乘后数值就不对了。这样的错误其实很真实、也很有意思,所以古柳觉得写出来和大家分享下也不错。

限制最大值后再相乘

当然题外话,沿着上面的思路,将错就错,我们只需把数值1.0-2.0的值限制到最大值1.0,借助内置函数 clamp(x, min, max) 将 x 值限制到 min-max 之间,那么将 mixer1、mixer2 的值限制到0.0-1.0后,再相乘就行了。

C# 复制代码
float mixer1 = vUv.x + vUv.y;
mixer1 = clamp(mixer1, 0.0, 1.0);
float mixer2 = 2.0 - (vUv.x + vUv.y);
mixer2 = clamp(mixer2, 0.0, 1.0);
float mixer = mixer1 * mixer2;

下图依次为最初相乘、限制0.0-1.0后相乘、取最小值的效果,可以看出后两者效果一致。所以有时候出错了反而会尝试出别的可行方法,倒也是意外之喜!

图形化的思路(暂时不理解也没关系)

不过上面的讲解思路是古柳在写文章时才想到的,对于其他类似两种数值进行结合的操作,这种代入特殊位置的数值并快速验证下加减乘除或取最小值最大值的方法,或者直接去应用看实际效果的方法更通用。

但回到这里的例子,古柳自己一开始的思路并不是那样的。

因为之前在其他地方学到过类似操作,所以印象里就是以直线的图形化效果去理解该怎样组合出 mixer。这里用到 desmos 这个图形计算器网站(别问我"图形计算器"是什么意思,我也是谷歌copy过来的,反正就是个能画函数图的网站,大家看其他人的 shader 讲解有时也会用到借助该网站讲某些数值变化情况),我们将 vUv.x+vUv.y 看成是个整体作为 x,那么一开始从 0.0-2.0 的过程其实就是 y=x 红色直线这样变化,我们希望在中间(0.5,0.5)也就是 x=1.0=y=mixer 时后续直线下降回到0.0,其实这也是 y=2-x 这条直线在 1.0-2.0 这段的变化。

  • 链接:https://www.desmos.com/calculator?lang=zh-CN

那么只要大家还有点中学数学的印象,看到这样的直线,可能就会想起直接取两个直线的最小值 min(x, 2-x) 也就是图中绿线部分,就是我们所需的结合两者的方式。

当然这里的讲解大家暂时没搞懂也没关系,有这么个印象,没准哪天看到0到1再回到0的变化方式,能想起这种直线图,然后直接反应过来可以用 min 取最小值那也不错。

本文例子配图合集

最后照旧是本篇文章所有例子的配图,方便大家对照着去自行实现出每个具体的效果。另外每篇文章几十个配图,也是古柳希望以这种方式让大家在学习抽象的 shader 过程中能更直观好懂的去理解每一步的效果,希望大家觉得很有用!

如果你喜欢本文内容,欢迎以各种方式支持,这也是对古柳输出教程的一种正向鼓励!

照例

最后欢迎加入「可视化交流群」,进群多多交流,对本文任何地方有疑惑的可以群里提问。加古柳微信:xiaoaizhj,备注「可视化加群」即可。

欢迎关注古柳的公众号「牛衣古柳」,并设置星标,以便第一时间收到更新。

相关推荐
雯0609~5 分钟前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ8 分钟前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z14 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
彭世瑜37 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund40438 分钟前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish39 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five40 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序40 分钟前
vue3 封装request请求
java·前端·typescript·vue
临枫54141 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
前端每日三省42 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript