手把手带你入门 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,备注「可视化加群」即可。

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

相关推荐
起司锅仔2 天前
OpenGL ES 着色器(5)
android·安卓·opengl·着色器
不惑_2 天前
最佳ThreeJS实践 · 实现赛博朋克风格的三维图像气泡效果
javascript·node.js·webgl
起司锅仔2 天前
OpenGL ES MVP/变换投影矩阵(8)
android·安卓·opengl
小彭努力中3 天前
50. GLTF格式简介 (Web3D领域JPG)
前端·3d·webgl
起司锅仔3 天前
OpenGL ES 之EGL(6)
android·安卓·opengl
小彭努力中4 天前
52. OrbitControls辅助设置相机参数
前端·3d·webgl
起司锅仔4 天前
OpenGL ES 纹理(7)
android·安卓·opengl
幻梦丶海炎4 天前
【Threejs进阶教程-着色器篇】8. Shadertoy如何使用到Threejs-基础版
webgl·threejs·着色器·glsl
小彭努力中5 天前
43. 创建纹理贴图
前端·3d·webgl·贴图
优雅永不过时·5 天前
three.js 通过着色器实现热力图效果
前端·javascript·智慧城市·three.js·热力图·着色器