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

文章更新(可能 ❌ / 一定 ✅)没那么频繁,欢迎加入「可视化交流群」进行交流。加古柳微信「xiaoaizhj」备注「可视化加群」即可,也有机会围观古柳朋友圈,实时追踪最新动态!另外,大家可以点下"赞"和"在看",这样每次新文章推送,就会第一时间出现在你的订阅号列表里。

上篇文章「手把手带你入门 Three.js Shader 系列(二) - 牛衣古柳 - 20230716」古柳教大家如何在片元着色器里使用 uv 纹理坐标,并结合 GLSL 里一些内置函数,制作出颜色渐变、颜色突变、重复条纹等效果,最后带大家看了下不同几何体上的条纹效果是什么样的,相信大家对 shader 编程有了更进一步的了解!

js 复制代码
const vertex = `
  varying vec2 vUv;

  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

const fragment = `
  varying vec2 vUv;

  void main() {    
    vec3 color = vec3(step(0.5, fract(vUv.x * 3.0)));
    gl_FragColor = vec4(color, 1.0);
  }
`;

// const material = new THREE.MeshBasicMaterial({ color: 0x0ca678 });
const material = new THREE.ShaderMaterial({
  vertexShader: vertex,
  fragmentShader: fragment
});

看到有群友认真看了文章并指出几处小笔误,并对古柳的教程加以赞许,还是很开心的。(加古柳微信:xiaoaizhj,备注「可视化加群」即,欢迎进群交流)

绘制圆形

本文将继续介绍些 GLSL 内置函数并结合 uv 来实现些其他效果,首先教教大家如何绘制圆形。

在之前古柳自学 shader 时曾发现不少教程都会有一章(Shaping functions)专门讲如何在 shader 里绘制不同的基本形状,如圆形、矩形、三角形、多边形、线段等,但其实这些代码理解起来还是蛮抽象的而且不少图形相对不常用,因而古柳觉得这部分不太需要那么早以及那么大篇章的讲解,所以在自己的教程中会适当舍弃,这背后的取舍都是为了使该入门教程尽可能简单、大家学起来不觉得晦涩难懂。(如果当初古柳自学时也能有这样的教程想来应该会轻松许多!)

当然相对来说圆形的绘制既简单又常用得多,所以先进行介绍。

通过 length() 内置函数可以获取向量的长度,这里用 vUv 计算每个像素离原点(0.0, 0.0)位置的距离 dist,将其设置到颜色上,会得到圆心在左下角的1/4渐变圆形效果,左上角(0.0, 1.0)和右下角(1.0, 0.0)离原点距离都是1,对应颜色正好是白色,当dist>1后,颜色仍为白色。

C# 复制代码
varying vec2 vUv;

void main() {
  // 绘制渐变圆形
  float dist = length(vUv);
  vec3 color = vec3(dist);
  gl_FragColor = vec4(color, 1.0);
}

结合上一篇提到的 step() 函数,当 dist<0.5为0,dist>0.5为1,则能得到半径为0.5的1/4圆形。

C# 复制代码
varying vec2 vUv;

void main() {
  // 绘制圆形
  float dist = length(vUv);
  vec3 color = vec3(step(0.5, dist));
  gl_FragColor = vec4(color, 1.0);
}

通过 vUv - vec2(0.5) 将所有坐标整体移动,也就是将坐标原点移动到 plane 的正中心,然后就能绘制出完整的圆形;另外也可以用 distance() 函数计算两个点的距离来代替 length(),作用相同。

C# 复制代码
varying vec2 vUv;

void main() {
  // 先居中,再绘制圆形
  float dist = length(vUv - vec2(0.5));
  // float dist = distance(vUv, vec2(0.5));
  float radius = 0.5; // 0.25
  vec3 color = vec3(step(radius, dist));
  gl_FragColor = vec4(color, 1.0);
}

除了手动改变半径大小 radius,还可以传入时间来动态控制半径大小。

ShaderMaterial 里可以通过 uniforms 从主程序 js 里传入所需的变量,其在顶点着色器和片元着色器里都能获取到,且对于每个顶点或片元数值统一相同,比如这里设置 uTime,并且在 animate() 函数里将不断变化的时间数值复制更新上去。注意设置时不是 uTime: 0 这样的格式,后面必须是对象格式 uTime: { value: 0 },其他 uniform 变量同理。

js 复制代码
const material = new THREE.ShaderMaterial({
  uniforms: { 
    uTime: 
      { value: 0 } 
  },
  vertexShader: vertex,
  fragmentShader: fragment,
});

// Animation
let time = 0;
function animate() {
  time += 0.05;
  material.uniforms.uTime.value = time;
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

在片元着色器里用 uniform 修饰符声明变量,因为 uTime 是不断增大的数值,可以通过 sin() 正弦函数将数值规范到-1-1之间并使其周期变化,再乘以0.5后加上0.5,将范围变化到0-1,确保设置到半径上时不会是负数,于是就能实现圆圈半径大小随时间周期变化的效果。同样的方式还可以用到球体上改变顶点位置,使球体半径大小不断变化,后续讲到顶点着色器时会介绍。

C# 复制代码
varying vec2 vUv;

uniform float uTime;

void main() {
  // 先居中,再绘制圆形
  float dist = length(vUv - vec2(0.5));
  // 半径大小随时间周期变化
  float radius = 0.5 * (sin(uTime) * 0.5 + 0.5);
  vec3 color = vec3(step(radius, dist);
  gl_FragColor = vec4(color, 1.0);
}

接着结合上篇文章所学,对 uv 重复5次 fract(vUv * 5.0),由于每个圆圈都是相对各自中心位置绘制,所以就有一组重复的圆圈效果。

C# 复制代码
varying vec2 vUv;

uniform float uTime;

void main() {
  // 先重复 uv,再居中,再绘制圆形
  float dist = length(fract(vUv * 5.0) - vec2(0.5));
  // 半径大小随时间周期变化
  float radius = 0.5 * (sin(uTime) * 0.5 + 0.5);
  vec3 color = vec3(step(radius, dist);
  gl_FragColor = vec4(color, 1.0);
}

但此时每个圆圈都是通过 sin(uTime) 控制动画,半径变化同步进行,很统一也很单调,这里可以通过 sin(uTime + vUv.x) 将不同水平值作为偏差值加进去,于是会有水平波浪起伏的效果

C# 复制代码
varying vec2 vUv;

uniform float uTime;

void main() {
  // 先重复 uv,再居中,再绘制圆形
  float dist = length(fract(vUv * 5.0) - vec2(0.5));
  // 半径大小随时间周期变化
  float radius = 0.5 * (sin(uTime + vUv.x) * 0.5 + 0.5);
  vec3 color = vec3(step(radius, dist);
  gl_FragColor = vec4(color, 1.0);
}

如果再把 vUv.y 也一起加上,变化效果更有趣丰富。

C# 复制代码
varying vec2 vUv;

uniform float uTime;

void main() {
  // 先重复 uv,再居中,再绘制圆形
  float dist = length(fract(vUv * 5.0) - vec2(0.5));
  // 半径大小随时间周期变化
  float radius = 0.5 * (sin(uTime + vUv.x + vUv.y) * 0.5 + 0.5);
  vec3 color = vec3(step(radius, dist);
  gl_FragColor = vec4(color, 1.0);
}

切换到立方体同样是蛮有趣的效果。

js 复制代码
// const geometry = new THREE.PlaneGeometry(1, 1);
const geometry = new THREE.BoxGeometry(1, 1, 1);

需要注意的是,大家自己敲的代码可能动画快慢效果略有不同,可通过 uTime 乘以较大或较小的值进行调整,比如 sin(uTime * 0.1 + vUv.x)

接着让我们回到最初绘制一个完整圆形的例子。

C# 复制代码
varying vec2 vUv;

void main() {
  // 先居中,再绘制圆形
  float dist = length(vUv - vec2(0.5));
  float radius = 0.5;
  vec3 color = vec3(step(radius, dist));
  gl_FragColor = vec4(color, 1.0);
}

当我们有了每个 uv 离中心的距离后,可以对它运用翻倍再取小数的操作进行重复,这样就能做出由里向外一圈圈黑白交错的径向条纹效果。

C# 复制代码
varying vec2 vUv;

void main() {
  // 先居中,后重复,再绘制圆形
  float dist = fract(length(vUv - vec2(0.5)) * 5.0);
  float radius = 0.5;
  vec3 color = vec3(step(radius, dist));
  gl_FragColor = vec4(color, 1.0);
}

有个小细节需要注意,上面我们翻了5倍,但看效果里一黑一白为1组,其实只有3组多点,究其原因是 length(vUv - vec2(0.5)) 一开始的范围并不是0到1,最大值是四个角离中心的距离,也就是 (1.0, 1.0) 离 (0.5, 0.5) 的距离,即 √2/2=0.707,因而我们可以先对其除以 0.707 再去翻倍取小数进行重复,这样就能如愿想有几组就几组、想重复几次就重复几次。

C# 复制代码
float dist = fract(length(vUv - vec2(0.5)) / 0.707 * 5.0);

然后将 uTime 加到变换到0-1范围后的数值上,使得径向条纹动起来。此时 dist 的公式其实已经有些复杂了,大家虽然可能看古柳写得很自然,但如果第一次尝试自己写,没准计算的顺序、括号的位置保不齐花样百出。所以大家务必好好理解每一步的顺序是怎么样的,力求能清楚地自行实现出来。

C# 复制代码
varying vec2 vUv;

void main() {
  // 先居中,后重复,再绘制圆形
  float dist = fract((length(vUv - vec2(0.5)) /0.707 + uTime * 0.5) * 5.0);
  float radius = 0.5;
  vec3 color = vec3(step(radius, dist));
  gl_FragColor = vec4(color, 1.0);
}

反向运动可以减去 uTime,运动速率可以通过 uTime 的倍数来控制。

C# 复制代码
float dist = fract((length(vUv - vec2(0.5)) / 0.707 - uTime * 0.5) * 5.0);

切换到立方体上就是这样的效果。

一下子又讲了不少内容,通过这两篇教程的学习,相信大家也见识到结合 GLSL 的内置函数以及简单的 sin 正弦函数,我们就能对 uv 做出不少酷炫的效果。

不知道大家是否越来越觉得 shader 其实蛮有意思的,这些内容并不是特别难,当然教程的背后古柳做了一些取舍,也力求结合更多有趣的例子使大家在学的过程中不觉得枯燥乏味、晦涩难懂。当然更精彩的内容还在后面,希望大家能坚持一起学下去。

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

照例

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

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

相关推荐
一个处女座的程序猿O(∩_∩)O1 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
燃先生._.7 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖8 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
black^sugar9 小时前
纯前端实现更新检测
开发语言·前端·javascript
2401_8576009510 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_8576009510 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL10 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js
小白学大数据10 小时前
如何使用Selenium处理JavaScript动态加载的内容?
大数据·javascript·爬虫·selenium·测试工具
2402_8575834911 小时前
基于 SSM 框架的 Vue 电脑测评系统:照亮电脑品质之路
前端·javascript·vue.js