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

本系列教程的代码将开源到该仓库,前几篇文章的代码也会陆续补上,欢迎大家 Star:github.com/DesertsX/th...

本系列的代码同时放到了 Codepen Collection,欢迎学习:codepen.io/collection/...

此外,之前和之后的所有文章的例子都将更新到这里,方便大家和古柳一起见证本系列内容的不断壮大与完善过程 www.canva.com/design/DAF3...

继续放新加上的群友的赞扬!看到群友把前面七篇全认真看了并每篇都点赞,甚至发现了此前没人发现的 bug,值得表扬👍!

正文

在前两篇关于顶点着色器的文章里,古柳教大家应用 sin、random、noise 等函数来移动顶点、改变几何体形状,并介绍了将 noise 数值作为颜色的一些方法。

本文古柳将教大家如何对顶点进行旋转,不过因为球体的顶点怎么旋转球体形状都不变,所以我们用长方体讲解更直观。通过对顶点旋转不同角度可以实现类似"扭麻花"的效果。

长方体

下面是简单的一些设置,我们使用 BoxGeometry 并且在 shader 里用 vUv 作为颜色,相机在 z=3 的位置看向场景中心、正对长方体。

js 复制代码
const camera = new THREE.PerspectiveCamera(75, 1, 0.01, 100);
camera.position.set(0, 0, 3);

const vertexShader = /* GLSL */ `
  uniform float uTime;
  varying vec2 vUv;

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

const fragmentShader = /* GLSL */ `
  varying vec2 vUv;

  void main() {
    gl_FragColor = vec4(vUv, 0.0, 1.0);
  }
`;
  
// const geometry = new THREE.BoxGeometry(1, 1, 1);
const geometry = new THREE.BoxGeometry(3, 1, 1);
const material = new THREE.ShaderMaterial({
  uniforms: {
    uTime: { value: 0 },
  },
  vertexShader,
  fragmentShader,
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

const clock = new THREE.Clock();
function render() {
  const time = clock.getElapsedTime();
  material.uniforms.uTime.value = time;
  // mesh.rotation.y = time;
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

render();

当长宽高都为1时,可以看到立方体每个面都有各自的 (0,0)-(1,1) uv 值,看着像由6个 plane 组成。不同几何体的 uv 效果会很不一样,这点大家替换几何体看看就会发现,不过数值范围都是0-1不会变化。

将长度设为3,方便后续沿x轴扭麻花。这里可以看到长度拉长后,uv 青红颜色效果也只是相应的拉长,4个角的颜色不变,背后数值范围也是不变的。

之前讲解片元着色器时选用 1:1 的 plane 作为例子是为了不使大家对 uv 产生困惑,实际上不论是这里的 3:1,还是任何比例,uv 各自都是0-1的范围,只是图形不是 1:1 时咋看起来会有些奇怪。

条纹效果

知道长方体上 uv 的效果后,我们可以将 vUv.y 剩以3.0再通过 fract 取小数然后对0.5取 step 从而使得数值在0/1/0/1/0/1之间变化,最后用该数值来 mix 插值两种颜色从而实现重复条纹效果。

这里的红色是 #d8345f,可以通过该链接将16进制格式直接转化成 GLSL 里的 rgb 数值,即 vec3(0.847, 0.204, 0.373)。链接不用记也没事,要用时直接谷歌 glsl hex to rgb 即可。

C# 复制代码
varying vec2 vUv;

void main() {
  vec3 color1 = vec3(0.847, 0.204, 0.373);
  vec3 color2 = vec3(1.0);
  float mixer = step(0.5, fract(vUv.y * 3.0));
  // vec3 color = vec3(mixer);
  vec3 color = mix(color1, color2, mixer);
  gl_FragColor = vec4(color, 1.0);
}

如果大家是跟着本系列教程一篇篇学下来的,相信这里红白条纹的实现闭着眼睛也能写出来了吧。如果有不理解的地方,可以看前面文章复习下:「手把手带你入门 Three.js Shader 系列(二) - 牛衣古柳 - 20230716」

旋转顶点

完成上述准备工作后,我们就可以开始对顶点进行旋转。谷歌搜索 glsl rotate 后从这个链接拷贝 glsl-rotation-3d 部分的函数到顶点着色器里。rotate() 函数接收待旋转的三维点坐标、旋绕的轴、旋转角度这三个参数,返回旋转后点的坐标位置。

C# 复制代码
// glsl-rotation-3d
mat4 rotationMatrix(vec3 axis, float angle) {
    axis = normalize(axis);
    float s = sin(angle);
    float c = cos(angle);
    float oc = 1.0 - c;
    
    return mat4(oc * axis.x * axis.x + c,           oc * axis.x * axis.y - axis.z * s,  oc * axis.z * axis.x + axis.y * s,  0.0,
                oc * axis.x * axis.y + axis.z * s,  oc * axis.y * axis.y + c,           oc * axis.y * axis.z - axis.x * s,  0.0,
                oc * axis.z * axis.x - axis.y * s,  oc * axis.y * axis.z + axis.x * s,  oc * axis.z * axis.z + c,           0.0,
                0.0,                                0.0,                                0.0,                                1.0);
}

vec3 rotate(vec3 v, vec3 axis, float angle) {
	mat4 m = rotationMatrix(axis, angle);
	return (m * vec4(v, 1.0)).xyz;
}

长方体沿x轴拉长,那么就让顶点 position 都绕x轴旋转,即 axis 用 (1.0, 0.0, 0.0) 表示;旋转角度用弧度值表示,需要用到 PI 值,可以通过 const float PI 声明和赋值。先让所有顶点统一绕x轴旋转 PI/4.0 即45度,看起来已经成功生效。

C# 复制代码
uniform float uTime;
varying vec2 vUv;

const float PI = 3.1415925;

// vec3 rotate(...){ ... }

void main() {
  vUv = uv;
  // vec3 newPos = position;
  vec3 axis = vec3(1.0, 0.0, 0.0);
  float angle = PI / 4.0;
  vec3 newPos = rotate(position, axis, angle);
  gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}

让角度随时间变化,就能实现 mesh.rotation.y=time 同样的效果。

C# 复制代码
// float angle = PI / 4.0;
float angle = uTime;

改变 axis 就能绕其他轴旋转。

C# 复制代码
// vec3 axis = vec3(1.0, 0.0, 0.0);
vec3 axis = vec3(0.0, 1.0, 0.0);
// vec3 axis = vec3(0.0, 0.0, 1.0);
// vec3 axis = vec3(1.0, 1.0, 1.0);

旋转角度随x值而变化

当然这里还是绕x轴旋转所以 axis 不用变。为了实现前文所说的沿x轴扭麻花的效果,我们需要使旋转的角度随顶点坐标里的x值而变化,而不是所有顶点都用统一的数值。此时会发现长方体形状变得挺别扭、挺奇怪,中间区域过渡地不够丝滑。

C# 复制代码
float angle = position.x;

原因想来大家也能猜到,就是长方体的细分数不够、顶点不够多,中间没有顶点、没有操作的空间,只有两端才能旋转,所以扭转得很生硬。通过设置 material 的 wireframe:true 就能应证我们的想法。

js 复制代码
const geometry = new THREE.BoxGeometry(3, 1, 1);
const material = new THREE.ShaderMaterial({
  uniforms: {
    uTime: { value: 0 },
  },
  vertexShader,
  fragmentShader,
  wireframe: true,
});

增加长方体的细分数

我们以2、4、8、16、32、64......等2的倍数来增加长方体的细分数,会发现差不多在32以后中间扭转过渡就很丝滑了。毕竟图中长方体就这么点长度,切成32份已经很细了。这里采用64也没什么特别的缘故,反正 shader 里都是同样轻松拿捏,用大些的数值也无妨。

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

随着细分数增加、顶点数增多,中间顶点随着x坐标的变化以不同角度发生旋转,从而有了丝滑的扭曲效果。当我们把 position.x + uTime 作为 angle 时就能使扭转效果随之动起来。

C# 复制代码
// ...

void main() {
  vUv = uv;
  vec3 axis = vec3(1.0, 0.0, 0.0);
  // float angle = position.x;
  float angle = position.x + uTime;
  vec3 newPos = rotate(position, axis, angle);
  gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}

其他变体

除此之外我们还可以随意在 angle 加些内容来实现不同的扭曲动画效果,大家可自由发挥,没准会有意外之喜。

C# 复制代码
float angle = position.x + sin(uTime) * 3.0 + uTime;
C# 复制代码
float angle = position.x * sin(uTime) - uTime;

Kinetic Typography

以上就是"扭麻花"效果的全部内容,并不复杂、并不难懂。

古柳之所以想讲这个效果,一方面是因为想教大家顶点旋转,一方面是想到这个 Kinetic Typography 动态文字排版/文字动效的实际例子(也这是上面 #d8345f 红色的出处,并非凭空冒出来的)。

扭转长方体后再结合文字会使人眼前一亮,后续讲解到 shader 里的纹理贴图、纹理采样时,古柳会再带大家把结合文字部分补上,敬请期待!

其他文字效果同样不错,有机会古柳会在教程里多讲些例子。

暂时舍弃的 WebGL Blob

其实一开始古柳设想的讲解顶点旋转的例子是下面这个效果,即在 noise 值偏移顶点后,加上旋转顶点,再加上 Cosine Palette(另一种 shader 里的配色方法),从而实现出这种酷炫的 WebGL Blob 效果。不过一些细节古柳觉得解释不好,远不如长方体"扭麻花"直观好懂,故而进行了取舍。当然其中的 Cosine Palette 古柳后续还是会教大家的。

小结

本文的内容比较简单,没啥好总结的。最后照旧是本文的所有例子合集,大家要好好复习哈。

相关阅读

「手把手带你入门 Three.js Shader 系列」目录如下:

照例

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

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

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

相关推荐
答案—answer2 天前
开源项目:Three.js3D模型可视化编辑系统
javascript·3d·开源·开源项目·three.js·three.js编辑器
贝格前端工场2 天前
困在像素里:我的可视化大屏项目与前端价值觉醒
前端·three.js
全栈王校长3 天前
Three.js 材质进阶
webgl·three.js
全栈王校长3 天前
Three.js Geometry进阶
webgl·three.js
OliverZhao3 天前
探索 iPhotron 如何利用 OpenGL 实现照片毫秒级调色渲染
opengl
烛阴3 天前
3D字体TextGeometry
前端·webgl·three.js
全栈王校长4 天前
Three.js 开发快速入门
three.js
全栈王校长4 天前
Three.js 环境搭建与开发初识
three.js
图素4 天前
Cesium 深入浅出 《一》WGS84、ECEF、经纬高:Cesium 世界坐标到底是什么?
webgl
supermapsupport4 天前
SuperMap GIS基础产品FAQ集锦(20260112)
webgl·supermap·iserver·idesktopx