普通视图

发现新文章,点击刷新页面。
昨天 — 2025年8月15日首页
昨天以前首页

2025年项目中是怎么初始化Three.js三维场景的

作者 入秋
2025年8月13日 15:40

Three.js 是一个强大的 JavaScript 3D 库,可以创建令人惊叹的 Web 3D 应用程序。本文将深入分析一个完整的 Three.js 场景管理类 useScene,它封装了场景初始化、模型加载、事件处理等核心功能。

依赖与导入

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import {KTX2Loader} from 'three/examples/jsm/loaders/KTX2Loader';
import {DRACOLoader} from 'three/examples/jsm/loaders/DRACOLoader'
import {MeshoptDecoder} from 'three/examples/jsm/libs/meshopt_decoder.module';
import Stats from "stats-gl";
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
import { gsap } from "gsap";
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
  • three:核心 3D 引擎。

  • OrbitControls:相机轨道控制器(旋转/平移/缩放)。

  • GLTFLoader:加载 glTF/GLB 模型。

  • KTX2Loader:加载 KTX2 压缩纹理(Basis Universal)。

  • DRACOLoader:加载 Draco 压缩几何。

  • MeshoptDecoder:Meshopt 顶点/索引压缩解码。

  • stats-gl:FPS/GPU 性能面板。

  • RGBELoader:HDR 贴图加载器(环境光照)。

  • gsap:动画库(用于相机/目标缓动)。

  • lil-gui:轻量 GUI 面板(调试、调参)。

useScene总览与成员

export class useScene {
  constructor(params){ ... }
  onListener(event, handler){ ... }
  init(){ ... }
  initStates(){ ... }
  initScene(){ ... }
  initCamera(){ ... }
  initLight(){ ... }
  createPanel(){ ... }
  initControls(){ ... }
  initRenderer(){ ... }
  startRender(){ ... }
  cancelAnimation(){ ... }
  startAnimation(){ ... }
  addRender(name, fun){ ... }
  removeRender(name){ ... }
  initSize(width, height){ ... }
  initLoading(){ ... }
  loadModelFile(url){ ... }
  mergeModelGeometry(model, bufferName='position'){ ... }
  getModelObject(){ ... }
  setPosition(model){ ... }
  getPosition(model){ ... }
  getModelPosition = (name) => { ... }
}

构造器与核心字段

constructor(params){
    this.gsap = gsap
    this.eventTarget = new EventTarget(),
    this.loadingManager = new THREE.LoadingManager()//加载资源管理器
    this.scene = null
    this.camera = null
    this.controls = null
    this.renderer = null
    this.stats = null
    this.raycaster = new THREE.Raycaster();
    this.width = params.width || window.innerWidth
    this.height = params.height || window.innerHeight
    this.container = params.container
    this.renderFunctions = {}
    this.init()
    this.initStates()
}
  • eventTarget:统一事件总线,向外派发加载进度、点击拾取等事件。

  • loadingManager:集中监听资源加载生命周期。

  • raycaster:用于点击拾取(鼠标射线相交)。

  • renderFunctions:自定义渲染回调注册表(按帧执行)。

  • container:渲染画布挂载的 DOM 容器(也是监听点击的区域)。

初始化流程

init (){
  this.initScene()
  this.initCamera()
  this.initControls()
  this.initRenderer()
  this.startRender()
  this.initLight()
  
  this.getModelObject()
  this.initLoading()
  window.addEventListener('resize', ()=> this.initSize());
}

顺序要点:

  • 先建场景/相机/控制器,再创建渲染器并立即开始渲染循环
  • 加上环境光、点击拾取与加载器事件。
  • 监听 resize,自适应窗口或容器尺寸。

性能状态面板

initStates(){
  const stats = new Stats({ horizontal: false, trackGPU: true });
  this.container.appendChild(stats.dom);
  this.stats?.init(this.renderer);
  this.stats = stats
}

  • stats-gl 面板插入容器。

  • trackGPU: true 开 GPU 指标(依赖浏览器支持)。

场景初始化

initScene() 方法创建了一个基础 Three.js 场景,并设置了环境贴图

initScene(){
        const scene = new THREE.Scene();
        scene.name = "scene"
        scene.receiveShadow = true;
        scene.castShadow = true
        scene.background = new THREE.Color('#000F25')
        scene.environment = new RGBELoader().load('/hdr/drachenfels_cellar_1k.hdr' ,(texture)=>{
            scene.environment.mapping = THREE.EquirectangularReflectionMapping;
            scene.environmentIntensity = 1
            scene.environment = texture;
            
        })
        this.scene = scene;
       
    }

关键点:

  • 设置了阴影接收和投射
  • 使用 HDR 环境贴图增强场景真实感
  • 通过 EquirectangularReflectionMapping 实现环境反射

相机初始化

 initCamera(){
     const camera = new THREE.PerspectiveCamera( 45, this.width / this.height, 0.01, 100000 );
     camera.position.set(0,0,0)
     this.camera = camera
 }
  • 45° 视角,近平面 0.01,远平面 10w(配合对数深度缓冲)。
  • 初始相机位置在原点 (0,0,0);通常实际会在加载模型后用 setPositiongetModelPosition 重新布置。

控制器设置

initControls(){
    const controls = new OrbitControls( this.camera, this.container);
    controls.enableDamping = true;
    controls.dampingFactor = 0.5;
    controls.zoomSpeed = 2;
    controls.enableZoom = true;//是否缩放
    controls.autoRotate = false;
    controls.minPolarAngle = 0;
    controls.maxPolarAngle = (Math.PI / 180) * 80;
    controls.minAzimuthAngle = -Infinity;
    controls.maxAzimuthAngle = Infinity;
    controls.update();
    this.controls = controls
 }
  • 阻尼开启,阻尼系数 0.5(手感偏重)。

  • 缩放速度 2,禁止自动旋转

  • 俯仰角限制 0° ~ 80°方位角无限制。

  • 重要:第二参数绑定在 container 上,即控制器监听容器事件。通常也可绑定 renderer.domElement;此处选 container 便于拾取控制共用同一区域。

初始化灯光

initLight(){
    const lightsGroup = new THREE.Group();
    lightsGroup.name = "lights"
    const light = new THREE.AmbientLight('#b0ccf0',1.0);
    light.name = '环境光'
    lightsGroup.add(light)
    this.scene.add(lightsGroup)
        
}
  • 仅添加了环境光(浅蓝、强度 1.0)。

  • 将所有灯光放入 lights 组,方便统一管理/销毁。

GUI 面板

createPanel(){
  return new GUI({ width: 250 });
}

返回一个 lil-gui 面板实例,未默认挂载任何参数,供上层按需使用。

渲染器初始化

initRenderer(){
        const renderer = new THREE.WebGLRenderer( {
            antialias: true,
            alpha: true,
            powerPreference: 'high-performance',
            premultipliedAlpha: true,
            preserveDrawingBuffer: true, // 允许截图
            logarithmicDepthBuffer: true, // 是否使用对数深度缓存。如果要在单个场景中处理巨大的比例差异
        });
        renderer.setPixelRatio( window.devicePixelRatio );
        renderer.setSize(this.width, this.height );
        renderer.sortObjects = true;//启用对渲染对象的排序。这通常用于确保渲染顺序正确,以避免深度排序问题。
        renderer.autoClear =  true;//每个渲染帧之前自动清除渲染目标
        renderer.autoClearColor =  true;//自动清除渲染目标的颜色缓冲区。
        renderer.autoClearDepth = true;//自动清除渲染目标的深度缓冲区。
        renderer.autoClearStencil = true;// 自动清除渲染目标的模板缓冲区。
        renderer.localClippingEnabled = true;// 启用局部剪裁,允许使用
        renderer.outputColorSpace = THREE.SRGBColorSpace;//定义渲染器的输出编码。默认为THREE.SRGBColorSpace  LinearSRGBColorSpace outputEncoding
        renderer.toneMapping = THREE.LinearToneMapping;//禁用色调映射。色调映射用于模拟相机的曝光。
        renderer.toneMappingExposure = 1.5;
        renderer.shadowMap.enabled = true
        // renderer.shadowMap.type = THREE.PCFShadowMap;
        renderer.physicallyCorrectLights = true;//启用物理正确的光照计算
        this.container.appendChild( renderer.domElement );
        this.renderer = renderer
    }

关键项解读:

  • logarithmicDepthBuffer:解决超大场景 z-fighting(更耗性能)。
  • preserveDrawingBuffer:允许截图(会降低性能;截图完可以改回 false)。
  • sortObjects:启用对象排序,半透明/复杂层次时更安全。
  • localClippingEnabled:允许局部裁剪。
  • 色彩空间SRGBColorSpace(现代 three 写法)。
  • 色调映射:线性 + 曝光 1.5(你可按需求换 ACESFilmic)。
  • 物理光照physicallyCorrectLights = true,配合 PBR/环境贴图更真实

渲染循环

startRender(){
  Object.keys(this.renderFunctions).forEach(key => {
    this.renderFunctions[key]()
  })
  this.renderer.render(this.scene, this.camera);
  this.controls.update();
  this.stats?.update();
  this.animationId = requestAnimationFrame(this.startRender.bind(this))
}

cancelAnimation(){
  cancelAnimationFrame(this.animationId)
}

// 别名:用于“替换环境贴图后重新启动”
startAnimation(){
  this.startRender()
}

  • 每帧先执行自定义渲染任务renderFunctions),再渲染画面。

  • 更新控制器阻尼、更新性能面板。

  • cancelAnimation() 用于暂停,startAnimation() 用于启动渲染

插拔式帧回调

addRender(name, fun){
  this.renderFunctions[name] = fun;
}
removeRender(name){
  this.renderFunctions[name] ? delete this.renderFunctions[name] : false;
}

名字注册/移除帧级任务,避免重复与泄漏,适合粒子、后期、动画等模块化更新。

自适应尺寸

initSize(width, height){
  this.width = width || this.container.clientWidth || window.innerWidth;
  this.height = height || this.container.clientHeight || window.innerHeight
  this.camera.aspect = this.width / this.height;
  this.camera.updateProjectionMatrix();
  this.renderer.setSize(this.width, this.height);
}

  • 支持外部传参或自动从容器/窗口取值。

  • 更新相机投影与渲染器大小。

资源加载事件

initLoading(){
  this.loadingManager.onStart = (url, loaded, total) => {
    this.eventTarget?.dispatchEvent(new CustomEvent('onStart', {
      detail: { url, loaded, total, progress: parseFloat(loaded / total * 100).toFixed(2) }
    }))
  }
  this.loadingManager.onProgress = (url, loaded, total) => {
    this.eventTarget?.dispatchEvent(new CustomEvent('onProgress', {
      detail: { url, loaded, total, progress: parseFloat(loaded / total * 100).toFixed(2) }
    }))
  }
  this.loadingManager.onLoad = () => {
    this.eventTarget?.dispatchEvent(new CustomEvent('onLoad'))
  }
}

通过 EventTarget 对外派发 onStart / onProgress / onLoad,便于 UI 进度条与日志。

模型加载

loadModelFile(url){
  const dracoLoader = new DRACOLoader();
  const ktx2Loader = new KTX2Loader();

  dracoLoader.setDecoderPath(`${location.href}/draco/gltf/`);
  dracoLoader.setDecoderConfig({ type: "js" });
  dracoLoader.preload();

  const loader = new GLTFLoader(this.loadingManager);
  loader.setKTX2Loader(ktx2Loader);
  loader.setMeshoptDecoder(MeshoptDecoder);
  loader.setDRACOLoader(dracoLoader);

  return loader.loadAsync(url, ({ loaded, total }) => {
    this.eventTarget?.dispatchEvent(new CustomEvent('progress', {
      detail: { url, loaded, total, progress: parseFloat(loaded / total * 100).toFixed(2) }
    }))
  })
}

  • Draco:设置解码路径(需放置 decoder js/wasm 资源)。

  • KTX2:直接设置给 GLTFLoader,用于解析 KTX2 纹理。

  • Meshopt:减少模型体积并加速加载。

  • 返回 Promisegltf 对象),并在 progress 回调继续派发自定义进度事件。

点击拾取

getModelObject(){
  this.container.addEventListener('click', (e)=>{
    e.stopPropagation();
    const mouse = {x: 0, y: 0}
    mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;

    this.raycaster.setFromCamera(mouse, this.camera);
    const intersects = this.raycaster.intersectObjects(this.scene.children, true);

    if (intersects.length > 0) {
      const clickedObject = intersects[0];
      this.eventTarget?.dispatchEvent(new CustomEvent('click', {
        detail: {
          model: clickedObject.object,
          point: clickedObject.point,
          info: clickedObject
        }
      }));
    }
  });
}

  • container 上监听 click

  • 将屏幕坐标映射到 NDC,通过 Raycaster 与场景递归相交

  • 命中后派发 click 事件,返回:

    • model:命中的 Mesh/Object3D
    • point:射线命中点世界坐标
    • info:three.js 相交结果对象

setPosition

setPosition(model){
  const boundingBox = new THREE.Box3().setFromObject(model);
  const boundingSphere = new THREE.Sphere()
  boundingBox.getBoundingSphere(boundingSphere)

  const [leng,width,height] = [
    boundingBox.max.x - boundingBox.min.x,
    boundingBox.max.z - boundingBox.min.z,
    boundingBox.max.y - boundingBox.min.y
  ]

  const diagonal = Math.sqrt(Math.sqrt(leng ** 2 + width ** 2) ** 2 + height ** 2);
  const { x,y,z } = boundingSphere.center

  this.camera.position.set(boundingBox.max.x + diagonal, diagonal, boundingBox.max.z + diagonal);
  this.controls.target.set(x,y,z)
  this.controls.object.updateProjectionMatrix()
  this.camera.updateProjectionMatrix()
}

计算 包围盒/包围球,将相机放到盒子对角线延伸位置,target 指向模型中心,确保整体入镜。

getPosition

getPosition(model){
  const boundingBox = new THREE.Box3().setFromObject(model);
  const boundingSphere = new THREE.Sphere()
  boundingBox.getBoundingSphere(boundingSphere)
  return boundingSphere.center
}

返回给定模型中心点

getModelPosition

getModelPosition = (name) => {
  const model = this.scene.getObjectByName(name)
  if(!model){ return }

  const box = new THREE.Box3().setFromObject(model)
  const size = new THREE.Vector3()
  box.getSize(size)
  const maxSize = Math.max(size.x, size.y, size.z)
  const distance = maxSize * 2

  const center = new THREE.Vector3()
  box.getCenter(center)

  const direction = new THREE.Vector3(0, 0, 1)
  const newCameraPos = new THREE.Vector3().copy(center).add(direction.multiplyScalar(distance))


  // gsap.to(this.camera.position,{ x: newCameraPos.x, y: newCameraPos.y + 1, z: newCameraPos.z, duration:1, onUpdate: () => this.controls.update() })
  // gsap.to(this.controls.target,{ x: center.x, y: center.y, z: center.z, duration:1 })
}

  • 通过名称定位目标,计算其中心与尺寸,沿 +Z 方向推远到合适距离。

  • 缓动动画已注释,打开即可实现平滑对焦。

事件对外接口

onListener (event, handler){
  this.eventTarget?.addEventListener(event, handler);
}

可监听的事件包括:

  • 加载onStart / onProgress / onLoadinitLoading()
  • 模型加载进度progressloadModelFile() 的第二个回调)
  • 点击拾取clickgetModelObject()

使用示例

init.js

import { useScene } from '@/hooks/useThreeModel';
export const initScene = (params) => {
    const app = new useScene({
        width: params.container.clientWidth,
        height: params.container.clientHeight,
        container: params.container,
    })
    //全局变量
    window.ThreeModel = app;
    // 载入进度
    app.onListener('onStart',() => {
        info.progress = 0;
        info.isloadingManager = true
    })
    app.onListener('onProgress',({detail:{loaded,total,progress}}) => {
        info.Loadtext = "解析中..."
        const num = (loaded / total * 100).toFixed(2);
        info.progress = parseFloat(num);
    })
    app.onListener('onLoad', () => {
        info.isloadingManager = false;
        info.Loadtext = "加载完成"
    })
    app.onListener('progress',({detail:{progress}})=>{
        info.Loadtext = "下载中..."
        info.progress = parseFloat(progress);
    })
    loadModelAsync('/model/Michelle.glb')
}
export const loadModelAsync = async (url) => {
    try {
        const { scene } = await ThreeModel.loadModelFile(url);
      
        ThreeModel.scene.add(scene)
       
    } catch (error) { console.log(error) }
}

index.vue

<script setup>
import { nextTick, onMounted, reactive, useTemplateRef } from 'vue';
import {initScene} from '@/three/init.js';
const modeView = useTemplateRef('modeView');
const info = reactive({

})
onMounted(()=>{
    nextTick(()=>{
        initScene({
            container: modeView.value
        })
    })
})
</script>
<template>
    <div class="model-view" ref="modeView"></div>
</template>
<style lang="less" scoped>
.model-view{
    width: 100%;
    height: 100%;
    background-color: #000F25;
}
</style>

一款解压的3D方块加载动画效果

作者 谢小飞
2025年8月11日 09:35

  这不最近我司设计师又给我整了点活,实现一个加载进度的动画,刚开始我还以为放两个圆圈转一下就可以了;但是设计说我们是一个3D项目,得搞点高端大气的,于是就有了这篇文章。

环境准备

  首先我们还是将三维场景的四个要素准备好,这里笔者为了简化代码,使用了自己封装的工具来创建:

export default class Index {
  scene: Scene;
  camera: PerspectiveCamera;
  renderer: WebGLRenderer;
  controls: OrbitControls;
  constructor() {
    this.scene = initScene();
    this.camera = initCamera(new Vector3(60, 50, 70), 55, 0.01, 200);
    this.renderer = initRenderer({
      antialias: true,
      alpha: true
    });
    // 设置透明背景
    this.renderer.setClearColor(0x000000, 0);
    this.controls = initOrbitControls(this.camera, this.renderer, false);
  }
}

  这里有一个我们可能会不太常见的函数:renderer.setClearColor,它的主要作用是用于设置渲染器的清除颜色,也就是场景中的背景色;这个函数接收两个参数,第一个是一个十六进制的颜色,默认是0x000000,即黑色;第二个参数是其透明度alpha值;我们后期可以在背景中插入一些预加载图等,因此可以通过设置这个函数的alpha为0来清除背景的颜色。

  环境搭建好了,神说要有光;于是我们创建两种光,环境光和平行光:

export default class Index {
  initLight() {
    {
      // 环境光
      this.ambientLight = new AmbientLight(0xffffff, 1)
      this.ambientLight.castShadow = false
      this.scene.add(this.ambientLight)
    }

    {
      // 平行光
      this.directionalLight = new DirectionalLight(0xffffff, 1.2)
      this.directionalLight.position.set(0, 10, 0)
      this.directionalLight.castShadow = true
      this.scene.add(this.directionalLight)
    }
  }
}

  接着,就需要我们最重要的物体,三个方块了,我们定义一个函数来批量地创建方块:

export default class Index {
  createCube(x: number, y: number, z: number, size: number, color: number): 
  Mesh<BufferGeometry, MeshPhongMaterial> 
  {
    const material = new MeshPhongMaterial({
      color,
    })
    const mesh = new Mesh(new BoxGeometry(size, size, size), material)
    mesh.castShadow = true
    mesh.receiveShadow = true
    mesh.position.set(x, y, z)
    mesh.name = "cube"

    this.scene.add(mesh)
    return mesh
  }
}

  然后按照初始化的位置创建三个方块:

const SIZE = 4.8
export default class Index {
  cube1: Mesh<BufferGeometry, MeshPhongMaterial>;
  cube2: Mesh<BufferGeometry, MeshPhongMaterial>;
  cube3: Mesh<BufferGeometry, MeshPhongMaterial>;
  constructor() {
    this.cube1 = this.createCube(0, 0, 0, SIZE, 0xf0fafc)
    this.cube2 = this.createCube(0, 0, SIZE, SIZE, 0xf0fafc)
    this.cube3 = this.createCube(0 - SIZE, 0, SIZE, SIZE, 0xf0fafc)
  }
}

这里我们定义了一个全局的SIZE变量,用于定义方块的尺寸,后续方块的位置更新也都会使用这个尺寸变量。

  从代码上可能很难看出来,实际上方块按照这样的位置进行排布了:

初始化位置

翻滚把!方块

  三个方块出场了,首先我们就来看一下如何先让一个方块向前翻滚,向前翻滚其实包含了两个动作,一个是前进,一个是旋转;因此这里还是引入我们常用的GSAP库,我们让cube3先沿着-Z轴的方向运动看一下效果:

import gsap from "gsap";
export default class Index {
  constructor() {
    const tl = gsap.timeline();
    tl.to(this.cube3.position, {
      z: 0,
      duration: 1,
    })
    .to(
      this.cube3.rotation,
      {
        x: -Math.PI / 2,
        duration: 1,
      },
      "<"
    );
  }
}

  上面代码中,我们创建了时间线,然后对Cube的position位置和rotation旋转角度进行了动画设置;由于两个动作是同时进行的,在这里我们使用了gsap的<参数,将rotation插入到position动画执行前,表示两个动作之间的时间间隔为0秒;我们允许代码,就能看到方块翻滚运动起来了:

方块翻滚运动

  但是如果每个方块运动我们都写一遍这样的代码,比较费时费力;我们发现方块不是沿着X轴运动就是沿着Z轴运动,因此我们可以将方块的移动代码封装到一个函数中去:

const DURATION = 0.8;
/**
 * 将方块移动并且旋转到指定地方
 * @param {Mesh} cube 方块
 * @param {'x' | 'z'} direction 方向,在x轴或者z轴
 * @param {number} pos 移动到的最终位置
 * @param {rotation} rotation 旋转的角度
 * @param {rotationAxes} rotationAxes 绕着哪个轴旋转
 * @returns {gsap.core.Timeline}
 */
rollCube(
  cube: Mesh,
  direction: "x" | "z",
  pos: number,
  rotation: number,
  rotationAxes: "x" | "y" | "z"
) {
  const tl = gsap.timeline();
  if (direction === "x") {
    tl.to(cube.position, {
      x: pos,
      duration: DURATION,
    }).to(
      cube.rotation,
      {
        [rotationAxes]: rotation,
        duration: DURATION,
      },
      "<"
    );
  } else if (direction === "z") {
    tl.to(cube.position, {
      z: pos,
      duration: DURATION,
    }).to(
      cube.rotation,
      {
        [rotationAxes]: rotation,
        duration: DURATION,
      },
      "<"
    );
  }
  return tl;
}

  我们封装一个rollCube函数,它定义了一个timeline时间线,然后根据传入的方向、位置、旋转角度、旋转轴等参数,将方块的移动和旋转进行设置;对方块的单次运动方式实现完毕之后,那我们下面就要来看下如何让他们一起运动起来呢?我们如何将单次运动的时间线进行串起来?

  这里我们使用gsap的一个小技巧:嵌套时间线;嵌套时间线的方式可以改变我们代码的组织逻辑,让我们实现各种复杂的动画逻辑,同时保持代码的简洁和可读性;那么嵌套时间线如何来做呢?

  由于rollCube中定义了一个一个小的时间线,因此,我们可以在构造函数中定义一个主时间线,然后通过add方法将rollCube返回的小时间线添加到主时间线中:

export default class Index {
  mainTimeline: gsap.core.Timeline;
  constructor() {
    this.mainTimeline = gsap.timeline({ repeat: -1 });
  }
}

  这里我们设置repeat: -1,表示这个主时间线会无限重复执行,这样我们对方块移动只需要进行四次,就可以实现效果了,而不需要将所有方块移动到原来的位置;下面我们就可以把每个小方块的运动添加到主时间线中:

const ROTATION_UNIT = Math.PI / 2;
export default class Index {
  mainTimeline: gsap.core.Timeline;
  constructor() {
    this.mainTimeline.add(
      this.rollCube(this.cube3, "z", 0, -ROTATION_UNIT, "x")
    );
    this.mainTimeline.add(
      this.rollCube(this.cube2, "x", -SIZE, ROTATION_UNIT, "z")
    );
    this.mainTimeline.add(
      this.rollCube(this.cube1, "z", SIZE, ROTATION_UNIT, "x")
    );
    this.mainTimeline.add(
      this.rollCube(this.cube3, "x", 0, ROTATION_UNIT, "y")
    );
  }
}

  我们通过一张图来更好的理解这个运动过程:

方块运动过程

  我们看下最后的实现效果,也可以点击这个地址查看,能看一整天,相当的解压:

effetc.gif

本文所有源码敬请关注gzh【前端壹读】,后台回复关键词【3D方块加载动画】即可获取。

总结

  本文我们实现了一个有趣的3D方块加载动画的效果,相比于2D加载效果,3D效果通过空间运动和旋转带来了更加丰富的视觉体验。我们学习了通过setClearColor方法设置透明背景,以及通过gsap嵌套时间线的方式将多个方块动画串联起来,形成无限循环的加载动画效果,这种3D动画的实现方式不仅代码结构清晰,而且通过空间运动为加载效果增添了更多趣味性。

如果觉得写得还不错,敬请关注我的掘金主页。更多文章敬请请访问谢小飞的博客

❌
❌