Three.js教程
进阶
图片切换

什么是 gl-transitions

gl-transitions 是一个基于 WebGL 的轻量级开源转场动画库,专为开发者实现高性能的图片/视频过渡效果而生。

通过 GLSL 着色器语法,它提供了超过[官方收录的 60+]种预设转场效果(如淡入淡出、溶解、像素化切换、马赛克切换、波纹切换等),能够帮助开发者轻松实现专业级的视觉过渡效果。

访问地址:https://gl-transitions.com/ (opens in a new tab)

我们先来看几个效果 水波过渡 斑点过渡 水纹过渡

gl-transition 的基本原理

要理解 gl-transition,先得对 WebGL/OpenGL 中的着色器工作流程有一个大致认识。以 WebGL 环境为例:

  1. 顶点着色器(Vertex Shader):接收每个顶点的坐标信息,计算变换后的坐标,一般与转场关系不大,更多是做基础的二维或三维坐标变换。
  2. 片元着色器(Fragment Shader):对屏幕上每一个像素执行颜色计算。转场效果的核心逻辑就往往写在此处。

在 gl-transition 中,片元着色器通常需要接收以下几个关键输入:

  • 上一个画面纹理(from / source1):表示转场前的画面。
  • 下一个画面纹理(to / source2):表示转场后的画面。
  • 进度参数(progress):代表转场进度,取值一般从 0.0 到 1.0。
    • progress=0.0 时,画面完全是“上一个画面”;
    • progress=1.0 时,完全切换至“下一个画面”;
    • 中间的 [0.0, 1.0] 区间就是转场特效的动画过程。

片元着色器根据 progress 的变化,来对“上一个画面”与“下一个画面”在像素层面进行混合、变换或各种视觉处理,产生过渡动画。

gl-transition 的优势

  1. 丰富的转场效果
    许多项目或库都收录了数十种甚至上百种不同类型的转场脚本,帮助开发者快速实现各种视觉效果,而无需从头编写复杂的 GLSL 逻辑。
  2. 性能高
    基于 GPU(WebGL/OpenGL),可以充分利用硬件加速,渲染效率远高于纯 CPU 图像处理。
  3. 跨平台
    只要目标环境支持 OpenGL 或 WebGL,就可以通过 gl-transition 获取同样的转场效果。对于有多端需求的视频项目来说,移植成本相对较低。
  4. 易于定制
    因为转场的核心是 GLSL 着色器脚本,开发者可以在已有脚本的基础上进行微调或二次开发,也可以通过参数来控制转场速度、颜色、方向等。

在 Three.js 中使用 gl-transition

思路概览

  1. 准备要切换的两张纹理或场景
    • 可以是两张图片、两段视频,也可以是两个 3D 场景的渲染结果。
  2. 准备 gl-transition 的转场片元着色器
  3. 在 Three.js 中创建自定义 ShaderMaterial
    • 将 gl-transition 的片元着色器直接嵌入到 ShaderMaterial 中;
    • 顶点着色器(Vertex Shader)可以用一个简单的全屏或平面顶点着色器即可。
  4. 将两张纹理(from/to)和进度(progress)作为 uniform 传递给 ShaderMaterial
    • 随着时间推进改变 progress 值,从 0.0 插值到 1.0,实现转场动画。
  5. 渲染并在画面中展示
    • 可选择“全屏叠加渲染”或“后期处理 Pass”两种思路来实现最终效果。

下面这段代码展示了如何在 Three.js 中使用自定义着色器(ShaderMaterial)来实现图片之间的转场动画,且利用鼠标悬停(mouseenter、mouseleave)控制转场的开始与回退。我们将通过对代码的各个部分进行详细解读,帮助你理解如何将 gl-transition 风格的片元着色器逻辑整合到 Three.js 中,实现酷炫的动态过渡。

Three.js 场景与渲染器初始化

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 600 / 400, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
 
renderer.setSize(600, 400);
const container = document.getElementById("scene-container");
container.appendChild(renderer.domElement);
 
camera.position.z = 5;
  • 创建了一个 75° 视场角(FOV=75)的透视相机(PerspectiveCamera),并设置宽高比为 600/400。
  • 创建了 WebGLRenderer,渲染尺寸为 600×400,并将其 DOM 元素插入到页面中(scene-container 容器内)。
  • 将相机放在 z=5 的位置,以便能看到放置在场景中央的平面。

ShaderMaterial:自定义转场着色器

通过PlaneGeometry创建一个平面,因为 gl-transition 是一个 glsl 的过渡,所以这里我们需要设置materialShaderMaterial, 关于 fragmentShader 我们先设置为空串,后面进行讲解。

const geometry = new THREE.PlaneGeometry(2, 2);
const material = new THREE.ShaderMaterial({
  uniforms: {},
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = vec4(position, 1.0);
    }
  `,
  fragmentShader: ``,
});
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
  • PlaneGeometry(2, 2):创建一个覆盖大约全屏的平面,具体能否真正满屏取决于相机位置和透视设置。

加载与切换纹理

  1. 我们提前准备好两张图,分别是 1.jpg 和 2.jpg
  2. 通过THREE.TextureLoader 进行图片加载,因为图片加载的逻辑都一样只是名称不一样,所以封装loadRandomTexture用于加载图片
  3. gl-transition 需要有一个 from 代表初始图片,to 代表过渡到什么图片,所以我们这两要设置 material 的 uniforms.from = currentTextureuniforms.to.value = nextTexture
const textureLoader = new THREE.TextureLoader();
let currentTexture, nextTexture;
 
function loadRandomTexture(num) {
  return new Promise((resolve) => {
    textureLoader.load(`/src/${num}.jpg`, (texture) => {
      texture.minFilter = THREE.LinearFilter;
      texture.magFilter = THREE.LinearFilter;
      resolve(texture);
    });
  });
}
 
// 初始化纹理
async function initTextures() {
  currentTexture = await loadRandomTexture(1);
  nextTexture = await loadRandomTexture(2);
  material.uniforms.from.value = currentTexture;
  material.uniforms.to.value = nextTexture;
}
  • loadRandomTexture:返回一个 Promise 异步加载指定路径的图片纹理,并设置了缩小与放大滤镜(minFiltermagFilter)以提升图片显示质量。
  • initTextures:初始时加载 1.jpg2.jpg,将它们分别赋值给 fromto

注意:在当前的例子里,transitionToNextImage 并没有被直接调用来做点击切换,而是示例中预留的方法,未来可扩展到点击或自动轮播等场景。

鼠标悬停控制进度

  • 当鼠标进入渲染器区域时,isHovered 设为 true,表示需要开始转场动画。
  • 鼠标离开后,isHovered = false,表示需要让转场回退到原状。
let transitioning = false;
let isHovered = false;
 
renderer.domElement.addEventListener("mouseenter", () => {
  isHovered = true;
  transitioning = true;
});
 
renderer.domElement.addEventListener("mouseleave", () => {
  isHovered = false;
  transitioning = true;
});

引入 gl-transition

我试了几种使用方法,最后觉得还是直接打开 glsl 里面的文件代码然后将其复制到我们的代码中直接使用是自由度最高的,而且最方便。

这里,我们拿一开始拿这个过渡效果为例 我们找到这个效果的文件代码,可以在官网的效果列表页找到名字

点进去就有它的 glsl 的代码了

直接复制过来的代码是没有办法直接使用的,他只有 transition 函数,但是我们还需要其他的一些变量,我们补齐这些变量,之后哪怕换 transition 只用找到对应的代码复制替换到我们的代码中就可以了。

const material = new THREE.ShaderMaterial({
  uniforms: {
    progress: { value: 0.0 },
    from: { value: null },
    to: { value: null },
    dots: { value: 20.0 },
    center: { value: new THREE.Vector2(0.5, 0.5) },
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    varying vec2 vUv;
    uniform sampler2D from;
    uniform sampler2D to;
    uniform float progress;
 
    vec4 getToColor(vec2 uv) {
      return texture2D(to, uv);
    }
 
    vec4 getFromColor(vec2 uv) {
      return texture2D(from, uv);
    }
 
    //----- 复制的代码 start -----
    const float SQRT_2 = 1.414213562373;
    uniform float dots;// = 20.0;
    uniform vec2 center;// = vec2(0, 0);
 
    vec4 transition(vec2 uv) {
      bool nextImage = distance(fract(uv * dots), vec2(0.5, 0.5)) < ( progress / distance(uv, center));
      return nextImage ? getToColor(uv) : getFromColor(uv);
    }
    //----- 复制的代码 end ----
 
    void main() {
      gl_FragColor = transition(vUv);
    }
  `,
});

注意: 上面的代码复制过来了,我们可以看到,有两个 uniforms,一个是 dots 用来控制斑点的数量,一个是 center 用来控制斑点的中心位置。 所以我们需要在初始化的时候设置这两个值,这里我们设置 dots = 20center = vec2(0.5, 0.5)

这两个变量,只是这个 transition 用到了,如果换别的 transition,就需要看对应的代码,然后设置对应的变量。

动画循环(Animate)

最后,在增加 hover 事件的时候,我们需要在动画循环中不断的更新 progress 的值,这样才能让转场动画进行。

function animate() {
  requestAnimationFrame(animate);
 
  if (transitioning) {
    if (isHovered && material.uniforms.progress.value < 1.0) {
      material.uniforms.progress.value += 0.02;
      if (material.uniforms.progress.value >= 1.0) {
        material.uniforms.progress.value = 1.0;
        transitioning = false;
      }
    } else if (!isHovered && material.uniforms.progress.value > 0.0) {
      material.uniforms.progress.value -= 0.02;
      if (material.uniforms.progress.value <= 0.0) {
        material.uniforms.progress.value = 0.0;
        transitioning = false;
      }
    }
  }
 
  renderer.render(scene, camera);
}
  • requestAnimationFrame 中持续监测 transitioning 状态:
    1. 如果鼠标在容器内且 progress < 1.0,则不断增加 progress,直到 1.0 停止。
    2. 如果鼠标离开且 progress > 0.0,则不断减小 progress,直到 0.0 停止。
  • 随着 progress 值从 0 ~ 1 的变化,片元着色器中的过渡函数会逐渐切换显示比例,从而形成转场动画效果。

响应窗口大小变化

window.addEventListener("resize", () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  material.uniforms.ratio.value = window.innerWidth / window.innerHeight;
});
  • 在窗口大小变化时同步更新相机的宽高比,调整渲染器大小,并更新 Shader 中的 ratio 值,避免变形或裁剪问题。

总结

通过以上代码与讲解,我们可以看到 Three.js + 自定义 ShaderMaterial 为转场动画带来了极大的灵活性和表现力。借助片元着色器中的 transition 函数,你能够自由定义想要的视觉过渡方式;通过鼠标悬停事件(或任何自定义触发条件),你可以在浏览器端顺畅地实现类似 gl-transition 风格的动态切换。

代码

github

https://github.com/calmound/threejs-demo/tree/main/gltransition (opens in a new tab)

gitee

https://gitee.com/calmound/threejs-demo/tree/main/gltransition (opens in a new tab)