本系列教程的代码将开源到该仓库,前几篇文章的代码也会陆续补上,欢迎大家 Star:github.com/DesertsX/th…
本系列的代码同时放到了 Codepen Collection,欢迎学习:codepen.io/collection/…
此外,之前和之后的所有文章的例子都将更新到这里,方便大家和古柳一起见证本系列内容的不断壮大与完善过程 www.canva.com/design/DAF3…
前言
上一篇文章「手把手带你入门 Three.js Shader 系列(十一) - 20250427」里讲到通过在 plane 平面的水平方向上划分不同区域、生成不同数值,然后用该数值去偏移 uv 就能做出图片错位移动的漂亮效果。
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.01, 100);
camera.position.set(0, 0, 1);
const loader = new THREE.TextureLoader();
const texture = loader.load("./assets/flower.jpg");
const vertex = /* GLSL */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragment = /* GLSL */ `
uniform float uTime;
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
float block = 6.0;
float x = floor(vUv.x * block) / block;
vec2 newUV = vUv;
newUV.x += x + uTime;
gl_FragColor = texture2D(uTexture, newUV);
}
`;
const geometry = new THREE.PlaneGeometry(1, 1);
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uTexture: { value: texture },
},
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;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
本文将在此基础上继续讲解更多图片错位效果的实现。
方向随机
首先上述效果里的各个区域都往一个方向移动,略显单调,可以引入随机值、改变左右移动方向,来增加变化。
借助 hash 函数
对每个区域的 x 生成0-1的随机数值 rand,再减去0.5使正负值各有一半概率,然后用内置函数 sign 将小于0.0的转换成-1.0,大于0.0的转换成1.0
,用这个值 dir 表示向左还是向右移动。先将 rand、dir 分别变成颜色看看,右图里黑色的是 -1.0,白色的是1.0。
// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
float hash(float n) { return fract(sin(n) * 1e4); }
vec2 hash(vec2 p){
p = vec2( dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3)));
return fract(sin(p)*43758.5453);
}
void main(){
vec2 newUV = vUv;
float block = 6.0;
float x = floor(vUv.x * block) / block;
float rand = hash(x);
float dir = sign(rand - 0.5);
// gl_FragColor = vec4(vec3(rand), 1.0);
gl_FragColor = vec4(vec3((dir)), 1.0);
}
这里用 hash 函数而非一般的 random 函数,是因为 hash 函数可以对相同的输入得到相同的输出,虽然每个区域里的 x 数值相同,乍看起来是对一个区域得到一个随机值、6个区域得到6个值就行。
但大家别忘了着色器代码是在每个像素上执行的,还是得执行成千上万次,因此每个区域里的像素的相同 x,需要有相同输出的随机值来确保最后方向也相同,所以必须用 hash 函数。
另外古柳谷歌搜索 glsl hash function
时没怎么找到单独的 hash 函数的实现,反而又是在这个 Noise 函数里找到了相关的实现,上面复制了2个 hash 函数,分别支持 float 输入 float 输出和 vec2 输入 vec2 输出,后者将在后续 xy 2维图片错位移动时用到。
将 dir 乘上 uTime 就能改变移动方向。具体随机的效果可能和 block 区域个数,以及大家网上找的 hash 函数的实现等都有关。
newUV.x += x + dir * uTime;
gl_FragColor = texture2D(uTexture, newUV);
水平垂直方向都错位
前面只在水平方向划分区域和进行错位,还可以在垂直方向上也一起错位。此时只需对 vUv 直接进行操作,就能得到 xy 上都离散化后的偏移数值 offset,然后加到 uv 上就能看到格子状的错位效果。
void main() {
float block = 6.0;
vec2 offset = floor(vUv * block) / block;
// gl_FragColor = vec4(offset, 0.0, 1.0);
vec2 newUV = vUv;
newUV += offset;
gl_FragColor = texture2D(uTexture, newUV);
}
用每个格子的 offset 数值计算2维的随机值,再计算出代表上下左右方向的 dir——(-1.0,-1.0) / (-1.0,1.0) / (1.0,-1.0) / (1.0,1.0)
——右图可以看到 dir 最终有红绿黄黑4种颜色,对应4个对角线上的移动方向。(如果大家对这里2维计算有些困惑的话,也可以将 xy 拆开进行表示和计算,方便理解)
void main() {
vec2 rand = hash(offset);
vec2 dir = sign(rand - 0.5);
// vec2 dir = vec2(sign(rand.x-0.5), sign(rand.y-0.5));
// gl_FragColor = vec4(rand, 0.0, 1.0);
gl_FragColor = vec4(dir, 0.0, 1.0);
}
将 dir 乘上 uTime 后就能看到每个格子里随机移动的错位视觉效果,一步步实现下来是不是蛮有趣的。
newUV += offset + dir * uTime;
以上就是古柳将“折射”小球里用到的部分知识点抽离出来后,结合 plane 上不同区域的离散化数值,来实现图片格子错位效果时所想到的一些好玩的例子。
圆周运动
此外还有一种效果,在「手把手带你入门 Three.js Shader 系列(五) - 20231126」一文里就提到过,早就想分享给大家的,正好和这两篇内容契合,因此在这里一并讲下。
这个效果和上面水平垂直方向上的移动不同,此时需要让像素按圆周运动旋转起来。
将 uTime 作为角度 angle,并且通过 sin、cos 转换成2维的坐标 coords,此时该坐标随时间变化总是在圆圈上运动,然后乘以运动半径0.1,最后加到 uv 上,就能看到图片整体做圆周运动。
void main() {
vec2 newUV = vUv;
float angle = uTime;
// float angle = uTime * 3.14 * 2.0;
vec2 coords = vec2(sin(angle), cos(angle));
newUV += 0.1 * coords;
gl_FragColor = texture2D(uTexture, newUV);
}
如果不希望图片是整块一起运动的,就可以把每个区域的不同数值 offset 作为角度偏移量加上去,这样就能打破步调的一致性。
float block = 6.0;
vec2 offset = floor(vUv * block) / block;
// vec2 coords = vec2(sin(angle), cos(angle));
vec2 coords = vec2(
sin(angle + offset.x),
cos(angle + offset.x)
);
把水平垂直的 offset 都加上后,一个个格子以不同角度错位旋转的效果就成功了。由于相邻格子的偏移角度接近,所以整体上有波浪般的效果。
vec2 coords = vec2(
sin(angle + offset.x + offset.y),
cos(angle + offset.x + offset.y)
);
调整不同参数,效果也会不同,大家可自行尝试,直到调出自己满意的效果。
vec2 coords = vec2(
sin(angle + offset.x * 3.0 + offset.y * 2.0),
cos(angle + offset.x * 3.0 + offset.y * 2.0)
);
几年前古柳刚接触 Shader 时,看到这种效果后就非常喜欢,了解实现原理后发现并不复杂。利用简单的 sin cos 三角函数,再结合强大的 Shader 在成千上万的像素上并行执行,就能做出一般前端里无法实现的酷炫丝滑的效果。时隔许久,终于在今天教给大家了,希望大家会喜欢!
小结
讲完了 plane 上的图片错位效果如何实现,下一篇会教大家如何实现这种漂亮的“折射”小球效果。
最后照旧是本文的所有例子合集。本文代码见 codepen。
相关阅读
「Three.js Shader 系列文章」目录如下:
照例
如果你喜欢本文内容,欢迎以各种方式支持,这也是对古柳输出教程的一种正向鼓励!
最后欢迎加入「可视化交流群」
,进群多多交流,目前5群刚刚建立;对本文任何地方有疑惑的可以群里提问。加古柳微信:xiaoaizhj
,备注「可视化加群」
即可。
欢迎关注古柳的公众号「牛衣古柳」
,并设置星标,以便第一时间收到更新。