快速上手React+threejs实战开发炫酷3D动画(入门篇)

2,369 阅读7分钟

前言

大家好,我叫虚竹。最近一直在学习threejs,感觉很有意思就梳理了一下自己学习笔记分享出来。以备日后项目需要,后续还会不断完善丰富我的threejs案例库,希望能在学习的路上帮到大家。

为什么要学它,市场有需求。近年来web得到了快速的发展,随着HTML5的普及,网页的表现能力越来越强大。尽管网页已经实现了很多精美的展示效果与优雅的交互效果,但人总是贪婪的,web开发已经不满足于2D效果的实现,更加炫酷的3D效果更能吸引人的目光。Three.js是用于实现web3D效果的JS库,它的出现让3D应用开发更简单。本文将通过Three.js的介绍及示例带我们走进3D的奇妙世界。

Web3研习社:54web3.cc/

效果截图如下所示:

1.png

优势

  • TS支持
  • 面向对象编程(继承)
  • 文档相对完善(入门简单)
  • 丰富的开源案例
  • 基于浏览器显示,用户访问相对方便
  • 生态全,行业认可度高,WebGL封装完善

劣势

  • 学习曲线比较高,特别在一些比较复杂的动画时,需要三维空间想象能力和较好的数学基础
  • 性能优化存在瓶颈,由于threejs是基于浏览器的3D,浏览的内存较小,导致在复杂模型和动画时内存溢出
  • 对设备要求较高,低设备会导致3D模型渲染较慢,动画卡顿
  • 没有提供一些基础建模软件的插件

初识 threejs

官网介绍:Javascript 3D library(JavaScript 3D 库)。

Three.js是一款基于webGL(Web Graphics Library)的封装,简单易用且轻量级的3D库。Three.js对WebGL提供的接口进行了非常好的封装,简化了很多细节,大大降低了学习成本,极大地提高了性能,功能也非常强大。用户不需要详细地学习WebGL,就能轻松创作出三维图形,是前端开发者研发3D绘图的主要工具。

Three.js是纯渲染引擎,而且代码易读,容易作为学习WebGL、3D图形、3D数学应用的平台,也可以做中小型的重表现的Web项目。

用最简单的一句话概括:WebGL 和 Three.js 的关系,相当于 JavaScript 和 jQuery 的关系。

1.png

技术栈

  • vite v2.9
  • react v18
  • threejs v140
  • less
  • hooks

目录结构

image.png

准备工作

  • 下载安装 nodejs v12+
  • 代码编辑器工具 VS Code
  • 推荐谷歌浏览器

基础知识点

  • 场景(Scene):是物体、光源等元素的容器。
  • 相机(Camera):场景中的相机,可以通过操作相机的方式来改变观察者的位置和朝向,场景中只能添加一个,决定哪些物体将在屏幕上渲染。
  • 渲染器(Renderer):场景的渲染方式,如 WebGL/canvas2D/CSS3D。
  • 物体对象(Mesh):包括二维物体(点、线、面)、三维物体,模型等等。
  • 光源(light):场景中的光照,如果不添加光照场景将会是一片漆黑,包括全局光、平行光、点光源等。
  • 材质(material):材质就像是物体的皮肤,决定物体外表的样子,例如物体的颜色,看起来是否光滑,是否有贴图等等。
  • 加载器(Loader):用于加载纹理、图片、模型、音频等资源。
  • 控制器(Control):可通过键盘、鼠标控制相机的移动。

THREEJS 三大要素.png

大体实现思路

  • 构建一个场景,也就是一个三维空间
  • 创建一个相机,也就是一个观察点,并且定义观察的位置和角度
  • 定义形状和材质,把他们合起来之后放到场景中
  • 使用指定的渲染器将整体渲染到屏幕上

代码实现

构建本地服务

Vite 是一种新型前端构建工具,能够显著提升前端开发体验。Vite 需要 Node.js 版本 >= v12.0.0,然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。

npm create vite@latest three-demo
cd three-demo
npm install
npm run dev
# 或者 yarn 安装启动项目
yarn create vite@latest three-demo
cd three-demo
yarn
yarn start

image.png

image.png

image.png

image.png

安装threejs库

npm i three

搭建3D场景步骤

导入threejs核心库

import * as THREE from "three";

创建场景

//  创建场景对象Scene
const scene = new THREE.Scene();

创建渲染器

// 参数:antialias 是否执行抗锯齿。默认为false
const renderer = new THREE.WebGLRenderer({ antialias: true }); 

创建相机

// 创建透视相机,带四个参数
const camera = new THREE.PerspectiveCamera(for, aspect, near, far);

初始化相机

const initCamera = useCallback(() => {
    camera.aspect = window.innerWidth / window.innerHeight; // 设置场景的宽高比
    camera.for = 45; // 相机的视角
    camera.near = 1; // 相机的近端面
    camera.far = 1000; // 相机的远端面
    camera.position.set(0, 10, 20); // 设置相机位置
    camera.lookAt(0, 0, 0); // 设置相机面向(0, 0, 0)xyz坐标观察
    camera.updateProjectionMatrix(); // 更新相机
}, [camera]);

初始化渲染场景

const domRef = useRef(); // 创建 domRef 对象,并通过 domRef.current 访问对应的 DOM 对象
  
const initRenderer = useCallback(() => {
    renderer.setPixelRatio(window.devicePixelRatio); // 设置分辨率为当前设备的分辨率,解决场景模糊,抗锯齿的一种很好的方法
    renderer.setSize(window.innerWidth, window.innerHeight); // 设置画布大小
    renderer.shadowMap.enabled = true; // 开启渲染阴影效果
    domRef.current.appendChild(renderer.domElement); // 挂载 DOM
}, [renderer, domRef]);

初始化灯光

const lights = useRef([]).current; // 创建 lights 空数组

const createLight = useCallback(() => {
    // 太阳光
    // const dirLight = new THREE.DirectionalLight("#fff", 0.5);
    // dirLight.position.set(100, 200, 200);
    // 环境光
    const ambientLight = new THREE.AmbientLight("#fff", 0.3);
    // 点光源
    const pointLight = new THREE.PointLight("#fff", 1, 8);
    pointLight.position.set(0, 5, 0); // 设置灯光xyz坐标位置

    scene.add(pointLight, ambientLight); // 将灯光添加到场景中
    lights.push(pointLight, ambientLight);
}, []);

渲染函数

// 渲染函数
const renders = useCallback(() => {
    renderer.clear();
    renderer.render(scene, camera);
}, [renderer, scene, camera]);

初始化用户交互

const initControls = useCallback(() => {
    controls.enableDamping = true; // 是否有惯性
    controls.enableZoom = true; // 是否可以缩放
    // controls.autoRotate = true; // 是否自动旋转
    controls.autoRotateSpeed = 2; // 设置自动旋转速度
    controls.enablePan = true; // 是否开启右键拖拽
}, []);

创建基础立方体

const meshs = useRef([]).current; // 创建 meshs 空数组

const createRect = useCallback(() => {
    // 设置正方体宽、高、深分别为2、2、2
    const rectGeometry = new THREE.BoxGeometry(2, 2, 2); // 创建几何体对象
    const rectMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); // 创建材质 - MeshBasicMaterial基础网格材质
    const rect = new THREE.Mesh(rectGeometry, rectMaterial); // 网格物体由几何形状和材质组成不同的物体
    rect.position.set(4, 0, 0); // 设置物体xyz坐标位置
    scene.add(rect); // 将物体添加到场景中
    meshs.push(rect);
}, []);

image.png

创建彩色立方体

创建彩色立方体只需将基础网格材质MeshBasicMaterial改成法线网格材质MeshNormalMaterial,代码如下:

const createRect2 = useCallback(() => {
    const cubeGeometry = new THREE.BoxGeometry(2, 2, 2);
    const cubeMaterial = new THREE.MeshNormalMaterial(); // 创建法线网格材质
    const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
    cube.position.set(0, 0, 0);
    scene.add(cube);
    meshs.push(cube);
}, []);

image.png

创建线条几何体

const points = [];
const colors = [];
  
const createLine = useCallback(() => {
    const lineGeometry = new THREE.BufferGeometry(); // 是点、线、面几何体的有效表述
    const lineMaterial = new THREE.LineBasicMaterial({ vertexColors: true }); // 基础线条材质
    for (let i = 0; i < 10000; i++) {
      const x = Math.random() * 2 - 1;
      const y = Math.random() * 2 - 1;
      const z = Math.random() * 2 - 1;
      points.push(x, y, z);
      colors.push(Math.random());
      colors.push(Math.random());
      colors.push(0xff0000);
    }
    lineGeometry.setAttribute(
      "position",
      new THREE.Float32BufferAttribute(points, 3)
    );
    lineGeometry.setAttribute(
      "color",
      new THREE.Float32BufferAttribute(colors, 3)
    );
    const line = new THREE.Line(lineGeometry, lineMaterial); // 连续的线组成的物体
    line.position.set(8, 0, 0);
    scene.add(line);
    meshs.push(line);
}, []);

image.png

让立方体动起来

const id = useRef(null);

// 循环动画
const animate = useCallback(() => {
    meshs.forEach((item) => {
      item.rotation.x += (0.5 / 180) * Math.PI;
      item.rotation.y += (0.5 / 180) * Math.PI;
    });
    ......
    id.current = window.requestAnimationFrame(animate);
}, []);

useEffect(() => {
    ......
    animate();
    return () => {
      window.cancelAnimationFrame(id.current); // 取消动画
      meshs.forEach((item) => {
        scene.remove(item); // 删除场景
        item.geometry.dispose(); // 释放内存
        item.material.dispose();
      });
      scene.remove();
      renderer.dispose(); // 释放内存
    };
}, [renderer, scene, camera]);

引入控制器

可以使用鼠标对场景进行操作,比如旋转场景,移动场景,缩放场景。定义一个轨道控制器,要想使轨道控制器生效,必须循环渲染场景requestAnimationFrame,也就是在动画循环里调用controls.update()方法。

// 导入轨道控制器JS库
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

// 创建控制器
const controls = new OrbitControls(camera, renderer.domElement);

// 更新动画
const animate = useCallback(() => {
    ......
    controls.update(); // 更新控制器
    renders();
    id.current = window.requestAnimationFrame(animate);
}, []);

添加3D字体

导入FontLoaderJS库用于加载JSON格式的字体类,可以将.ttf格式字体转换为json类型,可以使用facetype.js来在线转换字体。

import { FontLoader } from "three/examples/jsm/loaders/FontLoader";

// 创建加载字体对象
const loader = new FontLoader();

导入TextGeometryJS库将显示的文本生成单一几何体类。

import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry";

创建字体函数

const fonts = useRef([]).current;

const createFont = useCallback(() => {
    loader.load("fonts/STKaiti_Regular.json", (font) => {
      const textGeometry = new TextGeometry("@ 懒 人 码 农", {
        font: font, // 字体实例
        size: 0.5, // 字体大小
        height: 0.5, // 字体厚度
        curveSegments: 0.05, // 文本的曲线上点的数量
        bevelThickness: 0.05, // 文本上斜角的深度
        bevelSize: 0.05, // 斜角与原始文本轮廓之间的延伸距离
        // bevelSegments: 20, // 斜角的分段数
        bevelEnabled: true, // 是否开启斜角
      });
      const textMaterial = new THREE.MeshNormalMaterial({
        flatShading: true, // 是否使用平面着色进行渲染
      });
      const text = new THREE.Mesh(textGeometry, textMaterial);
      // text.position.set(-6, 6, 0);
      scene.add(text); // 将中文字体添加到场景中
      fonts.push(text);
    });
}, []);

让文字动起来

const clock = new THREE.Clock();

const animate = useCallback(() => {
    const elapsed = clock.getElapsedTime();
    ......
    fonts.forEach((item) => {
      item.position.set(Math.sin(elapsed) * -6, 6, Math.cos(elapsed) * -6);
    });
    controls.update();
    renders();
    id.current = window.requestAnimationFrame(animate);
}, []);

image.png

浏览器窗口变动自适应

const onWindowResize = useCallback(() => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
    renders();
}, []);

useEffect(() => {
    ......
    window.addEventListener("resize", onWindowResize, false);
    return () => {
        window.removeEventListener("resize", onWindowResize, false);
    }
}, [])

参考资料

  1. threejs官网:https://threejs.org/
  2. vite 官方文档: https://cn.vitejs.dev/
  3. github源码地址:https://github.com/mrdoob/three.js/
  4. threejs基础教程: http://www.yanhuangxueyuan.com/Three.js/
  5. 在线字体转换工具:https://gero3.github.io/facetype.js/
  6. 在线提取字体工具:https://www.fontke.com/tool/subfont/

结语

本文就写到这吧,以上内容是Three.js中提供的基础功能并结合示例阐述。希望大家看完后能对Three.js有个初步的了解,并能上手使用Three.js绘制出基础的3D图形动画。如果觉得有用,赶紧点赞收藏起来吧,说不定哪天就用上啦~