Three.js案例
Three.js和GSAP来实现 3D 翻转揭示动画

Three.js 和 GSAP 来实现 3D 翻转揭示动画

使用 Three.js (opens in a new tab)GSAP(TweenMax) (opens in a new tab) 来实现一个简单的 3D 翻转揭示动画。当点击页面中的 Trigger 按钮时,屏幕上会出现很多 3D 方块并逐渐翻转、移出视野,同时使背景内容逐渐显现。 主要流程可以简单分为以下几个步骤:

  1. Three.js 场景初始化
  2. 在场景中添加灯光
  3. 批量创建 3D 方块并排布到屏幕覆盖的区域
  4. 通过 GSAP 设置动画(翻转 + 移动 + 透明度变化)
  5. 动画结束后切换页面状态

接下来让我们来看看代码结构,边学边理解。

二、HTML 结构

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <!-- 引入 Three.js 和 TweenMax -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/100/three.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script>
    <style>
      /* 相关的样式省略,主要控制页面布局、字体样式以及动画显示 */
    </style>
  </head>
  <body class="text-center">
    <div id="page">
      <div class="cover-container d-flex h-100 p-3 mx-auto flex-column">
        <header class="masthead mb-auto">
          <div class="inner">
            <h3 class="masthead-brand">Reveal #1</h3>
            <nav class="nav nav-masthead justify-content-center">
              <a class="nav-link active" href="#">Home</a>
              <a
                class="nav-link"
                href="https://codepen.io/soju22/"
                target="_blank"
                >Codepen Profile</a
              >
              <a
                class="nav-link"
                href="https://codepen.io/collection/AGZywR"
                target="_blank"
                >ThreeJS Collection</a
              >
            </nav>
          </div>
        </header>
 
        <main role="main" class="inner cover">
          <h1 class="cover-heading">Simple 3D Reveal Effect</h1>
          <p class="lead">
            This simple effect is made with ThreeJS and TweenMax.
          </p>
          <p class="lead">
            <a href="#" id="trigger" class="btn btn-lg btn-secondary"
              >Trigger</a
            >
          </p>
        </main>
 
        <footer class="mastfoot mt-auto">
          <div class="inner">
            <p></p>
          </div>
        </footer>
      </div>
    </div>
 
    <!-- 用来做揭示效果的画布 -->
    <canvas id="reveal-effect"></canvas>
 
    <script>
      // 接下来是核心的 JavaScript 逻辑
    </script>
  </body>
</html>

关键点解析

  • <canvas id="reveal-effect"></canvas>
    这是 Three.js 用来渲染 3D 场景的画布。所有 3D 效果最终会绘制到这个元素上。
  • <a href="#" id="trigger">Trigger</a>
    点击这个按钮可以触发并重新播放 3D 翻转效果。

三、核心脚本

下面的脚本主要包含了一系列函数,依次完成场景初始化、灯光和对象的创建、动画效果以及浏览器窗口自适应等工作。

let renderer, scene, camera, cameraCtrl;
let width, height, cx, cy, wWidth, wHeight;
const TMath = THREE.Math;
 
let conf = {
  color: 0xffffff,
  objectWidth: 12,
  objectThickness: 3,
  ambientColor: 0x808080,
  light1Color: 0xffffff,
  shadow: false,
  perspective: 75,
  cameraZ: 75,
};
 
let objects = [];
let geometry, material;
let hMap, hMap0, nx, ny;

1. 全局变量与配置

  • renderer, scene, camera:Three.js 中的核心对象,分别代表渲染器、场景和相机。
  • width, height, cx, cy:分别表示浏览器窗口的宽/高以及中心点。
  • wWidth, wHeight:渲染区域(视锥体)内可视宽度和高度。
  • conf:封装了一些可调整的配置,如物体宽度、高度、相机参数、灯光颜色等等。
  • objects:存储所有创建的 3D 方块,方便统一管理动画。
  • geometry, material:3D 网格(geometry)和材质(material)在创建方块时被复用。
  • nx, ny:横纵方向上方块的数量。

2. init():初始化函数

function init() {
  // 创建渲染器
  renderer = new THREE.WebGLRenderer({
    canvas: document.getElementById("reveal-effect"),
    antialias: true,
    alpha: true,
  });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);
 
  // 创建透视相机
  camera = new THREE.PerspectiveCamera(
    conf.perspective,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );
  camera.position.z = conf.cameraZ;
 
  // 创建场景
  scene = new THREE.Scene();
 
  // 创建方块的基础几何体
  geometry = new THREE.BoxGeometry(
    conf.objectWidth,
    conf.objectWidth,
    conf.objectThickness
  );
 
  // 事件监听
  window.addEventListener("load", initScene);
  document.getElementById("trigger").addEventListener("click", initScene);
 
  // 开始动画循环
  animate();
}

解析

  1. new THREE.WebGLRenderer
    使用 WebGL 渲染器将 3D 对象绘制到 <canvas> 上。
  2. renderer.setSize(window.innerWidth, window.innerHeight)
    设置渲染器大小为浏览器窗口大小。
  3. camera = new THREE.PerspectiveCamera(...)
    采用透视相机(PerspectiveCamera),让场景有远近透视效果。
  4. scene = new THREE.Scene()
    创建一个空的场景容器,后续的灯光、模型都要加到这个场景里。
  5. geometry = new THREE.BoxGeometry(...)
    BoxGeometry 创建一个方块的几何形状,后续会在循环里实例化多个方块。
  6. 事件绑定
    • window.addEventListener("load", initScene):当页面加载完成后执行 initScene()
    • document.getElementById("trigger").addEventListener("click", initScene):点击按钮可以重新执行 initScene(),从而重复动画。
  7. animate()
    开始渲染循环,让 Three.js 持续更新场景。

3. initScene():重新初始化场景内容

function initScene() {
  onResize();
  scene = new THREE.Scene();
  initLights();
  initObjects();
}

每次触发 initScene() 时,会做以下事情:

  1. onResize():根据当前浏览器窗口大小,更新渲染尺寸和相机参数。
  2. 重新创建场景:将 scene 重置为空场景。
  3. initLights():往场景中添加灯光。
  4. initObjects():批量创建并排布 3D 方块。

4. initLights():初始化灯光

function initLights() {
  scene.add(new THREE.AmbientLight(conf.ambientColor));
  let light = new THREE.PointLight(0xffffff);
  light.position.z = 100;
  scene.add(light);
}
  • 环境光(AmbientLight):为整个场景提供基础照明,让物体不会一片漆黑。
  • 点光源(PointLight):类似于一个灯泡,从一个点向各个方向发出光线。这里让它位于相机附近。

5. initObjects():批量创建方块

function initObjects() {
  objects = [];
  nx = Math.round(wWidth / conf.objectWidth) + 1;
  ny = Math.round(wHeight / conf.objectWidth) + 1;
  let mesh, x, y;
  for (let i = 0; i < nx; i++) {
    for (let j = 0; j < ny; j++) {
      material = new THREE.MeshLambertMaterial({
        color: conf.color,
        transparent: true,
        opacity: 1,
      });
      mesh = new THREE.Mesh(geometry, material);
      x = -wWidth / 2 + i * conf.objectWidth;
      y = -wHeight / 2 + j * conf.objectWidth;
      mesh.position.set(x, y, 0);
      objects.push(mesh);
      scene.add(mesh);
    }
  }
  document.body.classList.add("loaded");
  startAnim();
}
  1. nx, ny:根据浏览器的可视宽高以及方块的大小,计算横纵两个维度放多少个方块才可以铺满画面。
  2. 循环创建方块
    • 使用 MeshLambertMaterial 创建可投射光照的材质。
    • 将每个方块的位置设到对应的坐标,铺满整个场景。
    • 将方块储存在 objects 数组里,方便后续动画调用。
  3. document.body.classList.add("loaded"):给 <body> 加上一个类名,用于 CSS 的过渡。
  4. startAnim():创建完所有方块后,立即开始动画。

6. startAnim():启动动画

function startAnim() {
  document.body.classList.remove("revealed");
  objects.forEach((mesh) => {
    mesh.rotation.set(0, 0, 0);
    mesh.material.opacity = 1;
    mesh.position.z = 0;
 
    let delay = TMath.randFloat(1, 2);
    let rx = TMath.randFloatSpread(2 * Math.PI);
    let ry = TMath.randFloatSpread(2 * Math.PI);
    let rz = TMath.randFloatSpread(2 * Math.PI);
 
    // 使用 TweenMax 为每个方块添加动画
    TweenMax.to(mesh.rotation, 2, { x: rx, y: ry, z: rz, delay: delay });
    TweenMax.to(mesh.position, 2, {
      z: 80,
      delay: delay + 0.5,
      ease: Power1.easeOut,
    });
    TweenMax.to(mesh.material, 2, { opacity: 0, delay: delay + 0.5 });
  });
  setTimeout(() => {
    document.body.classList.add("revealed");
  }, 4500);
}
  1. 重置每个方块的初始属性:将旋转、透明度、z 坐标都归零,以便能重新播放动画。
  2. 随机延迟与随机旋转
    • TMath.randFloat(1, 2):为每个方块生成 1 ~ 2 秒不等的延迟,增加整体翻转过程的层次感。
    • TMath.randFloatSpread(2 * Math.PI):在 -π 到 π 的范围内随机生成旋转角度,让方块在翻转时方向各不相同。
  3. 方块移出视野 & 渐隐
    • TweenMax.to(mesh.position, 2, { z: 80 }):把方块往 Z 轴正方向移动一段距离。
    • TweenMax.to(mesh.material, 2, { opacity: 0 }):方块逐渐变得透明。
  4. 添加 “revealed” 类
    • 动画约在 4.5 秒后结束,给 <body> 再加上一个类,表示已经完成揭示。这时可以移除不需要的元素或让背景内容完全呈现。

7. animate():渲染循环

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
  • requestAnimationFrame(animate):浏览器专门用于动画的方法,保证尽可能平滑。
  • renderer.render(scene, camera):每帧都用相机去渲染当前场景。如果场景或相机发生改变,则能实时更新。

8. onResize():自适应窗口大小

function onResize() {
  width = window.innerWidth;
  cx = width / 2;
  height = window.innerHeight;
  cy = height / 2;
 
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  renderer.setSize(width, height);
 
  size = getRendererSize();
  wWidth = size[0];
  wHeight = size[1];
}
  1. 更新相机宽高比:当浏览器的宽度或高度改变后,需要更新 camera.aspect 并调用 updateProjectionMatrix(),否则画面会变形。
  2. 更新渲染器大小:同步渲染器的大小以适配新的窗口尺寸。
  3. 记录可视宽高wWidthwHeight 用于后续铺设方块时计算网格数量。

9. getRendererSize():计算可视区域大小

function getRendererSize() {
  const cam = new THREE.PerspectiveCamera(conf.perspective, camera.aspect);
  const vFOV = (cam.fov * Math.PI) / 180;
  const height = 2 * Math.tan(vFOV / 2) * Math.abs(conf.cameraZ);
  const width = height * cam.aspect;
  return [width, height];
}
  • 通过三角函数计算摄像机所能看到的“真实宽高”。
  • 这里将 conf.cameraZ 作为距离来推算,在这个距离上能看到的宽度与高度是多少。
  • 最终用于决定方块在 X、Y 方向上要排列多少行列。

四、使用与效果

  1. 打开页面:当页面加载完毕后,init() 会自动执行。
  2. 点击 Trigger 按钮:将会再次执行 initScene(),重新生成方块并播放翻转动画。
  3. 动画过程:方块随机旋转后,向 Z 方向移动并逐渐消失;此时页面背景从白色逐渐过渡到深色,最后页面上内容“显现”出来。

你可以根据自己的需求,修改下列内容以实现不同效果:

  • objectWidth:方块的尺寸大小。
  • objectThickness:方块的厚度。
  • cameraZ:相机初始位置,影响视觉上的远近。
  • color:方块的颜色。
  • 动画细节:比如 TweenMax 的持续时间、延迟范围、旋转范围等。

五、总结

  1. Three.js 场景搭建:渲染器、相机、场景是最基础的“三件套”,接着再添加对象和灯光。
  2. GSAP 动画:通过对方块的旋转、透明度和位置进行缓动,可以打造出丰富的动画效果。
  3. 事件与响应:加载时、窗口大小改变时、按钮点击时,各自触发对应的函数,让程序更具交互性。
  4. 布局与过渡:结合 CSS,可以更好地完成前后场景的切换。

六、代码

https://codepen.io/soju22/pen/zbjNWy (opens in a new tab)