Three.js

0x01 概述

(1)关于

以下内容使用 Three@0.148.0

(2)安装

a. CDN

<script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@<version>/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@<version>/examples/jsm/"
    }
  }
</script>

b. Node.js

  1. 使用命令 npm install three@<version> --save 安装指定版本的 Three.js

  2. 在入口 JavaScript 文件中导入 Three.js

    import * as THREE from 'three';
    

c. Vue 3

  1. 使用命令 npm create vite@latest 并选择安装 Vue + JavaScript

  2. 使用命令 npm install -S three@<version> 安装指定版本的 Three.js

  3. 在页面中导入并使用 Three.js

    <script setup>
    import * as THREE from "three";
    </script>
    
    <template></template>
    
    <style scoped></style>
    

d. React 18

  1. 使用命令 npx create-react-app <project-name> 创建 React 应用

  2. 使用命令 npm install -S three@<version> 安装指定版本的 Three.js

  3. 在页面中导入并使用 Three.js

    import * as THREE from "three";
    
    function App() {
      return <div></div>;
    }
    
    export default App;
    

0x02 基本概念

(1)场景 Scene

graph LR 场景 & 相机-->渲染器==>投影图

a. 几何体 Geometry

b. 材质 Material

c. 物体 Objects

d. 三维坐标系 AxesHelper

  • Three.js 支持通过 AxesHelper 导入三维坐标系到页面中

    const axesHelper = new THREE.AxesHelper(150);
    scene.add(axesHelper);
    
    • 设置物体的材质为半透明

      const material = new THREE.MeshBasicMaterial({
        color: 0xff0000,
        transparent: true, // 允许透明
        opacity: 0.5,	// 透明度
      });
      
  • 导入的三维坐标系中,颜色与轴对应关系如下:

    红色:X 轴

    绿色:Y 轴

    蓝色:Z 轴

e. 光源 Light

  • Three.js 提供的部分材质受光照影响,如:

    graph LR 网格材质-->A[不受光照影响] & B[受光照影响] A-->MeshBasicMaterial B-->漫反射 & 高光 & 物理 漫反射-->MeshLambertMaterial 高光-->MeshPhongMaterial 物理-->MeshStandardMaterial & MeshPhysicalMaterial
  • Three.js 提供光源相关的 API

    构造器 含义 官方文档
    AmbientLight( color : Color, intensity : Float ) 环境光 https://threejs.org/docs/#api/zh/lights/AmbientLight
    DirectionalLight( color : Color, intensity : Float ) 平行光 https://threejs.org/docs/#api/zh/lights/DirectionalLight
    HemisphereLight( skyColor : Integer, groundColor : Integer, intensity : Float ) 半球光 https://threejs.org/docs/#api/zh/lights/HemisphereLight
    Light( color : Color, intensity : Float ) 光源基类 https://threejs.org/docs/#api/zh/lights/Light
    LightProbe( sh : SphericalHarmonics3, intensity : Float ) 光照探针 https://threejs.org/docs/#api/zh/lights/LightProbe
    PointLight( color : Color, intensity : Float, distance : Number, decay : Float ) 点光源 https://threejs.org/docs/#api/zh/lights/PointLight
    RectAreaLight( color : Color, intensity : Float, width : Float, height : Float ) 平面光光源 https://threejs.org/docs/#api/zh/lights/RectAreaLight
    SpotLight( color : Color, intensity : Float, distance : Float, angle : Radians, penumbra : Float, decay : Float ) 聚光灯 https://threejs.org/docs/#api/zh/lights/SpotLight
  • 使用环境光(改变场景整体的明暗)

    const ambient = new THREE.AmbientLight(0xffffff, 0.2);
    scene.add(ambient);
    
  • 使用点光源结合 MeshLambertMaterial 漫反射材质

    const material = new THREE.MeshLambertMaterial({
      color: 0xff0000,
    });
    
    const pointLight = new THREE.PointLight(0xffffff);
    pointLight.intensity = 1.0; // 光照强度
    pointLight.decay = 0.0; // 光照衰减
    pointLight.position.set(150, 100, 50); // 光源位置
    scene.add(pointLight); // 添加光源
    
    // ...
    
    • 通过使用 PointLightHelper 对点光源可视化

      const pointLightHelper = new THREE.PointLightHelper(pointLight, 10);
      scene.add(pointLightHelper);
      
  • 使用平行光结合 MeshLambertMaterial 漫反射材质

    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
    directionalLight.position.set(-50, -100, -150); // 光源位置
    directionalLight.target = mesh; // 光源指向目标对象
    scene.add(directionalLight);
    
    • 通过使用 DirectionalLightHelper 对平行光源可视化

      const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 10);
      scene.add(directionalLightHelper);
      

(2)相机 Camera

a. 画布尺寸与布局

  • 使用 Three.js 创建的图像作为 <canvas /> 嵌入页面中

  • 全浏览器显示

    const width = window.innerWidth;
    const height = window.innerHeight;
    document.body.style.margin = "0";
    document.body.style.overflow = "hidden";
    
    // ...
    
    renderer.setSize(width, height);
    
  • 动态填充

    window.onresize = () => {
      renderer.setSize(window.innerWidth, window.innerHeight); // 重置画布尺寸
      camera.aspect = window.innerWidth / window.innerHeight; // 重置相机观察范围比例
      camera.updateProjectionMatrix(); // 更新相机投影矩阵
    };
    

b. 轨道控制器 OrbitControls

  • 通过 OrbitControls 可以使用鼠标控制相机

    • 左键:切换视角
    • 中键:调整远近
    • 右键:改变位置
  • 原生 HTML 中导入 OrbitControls

    1. index.html

      <script type="importmap">
        {
          "imports": {
            "three": "./node_modules/three/build/three.module.js",
            "three/addons/": "./node_modules/three/examples/jsm/"
          }
        }
      </script>
      
    2. main.js

      // ...
      import { OrbitControls } from "three/addons/controls/OrbitControls.js";
      
      const controls = new OrbitControls(camera, renderer.domElement);	// 创建控制器对象
      controls.addEventListener("change", () => renderer.render(scene, camera));	// 监听改变事件并重新渲染
      

(3)渲染器 Renderer

a. 动画渲染循环效果

  • 通过 HTML5 的 requestAnimationFrame 方法实现

  • 举例:自旋

    // 周期性渲染,按屏幕刷新率执行
    function render() {
      mesh.rotateY(0.01); // 沿 Y 轴旋转 0.01 rad
      renderer.render(scene, camera); // 渲染
      requestAnimationFrame(render);
    }
    render();
    

b. 阵列

  • 完成定义几何体及材质后,通过 for 循环批量创建物体,并分配其所在位置

  • 举例:立方体阵列

    import * as THREE from "three";
    
    const scene = new THREE.Scene(); // 创建场景
    const geometry = new THREE.BoxGeometry(10, 10, 10);	// 创建立方几何体
    const material = new THREE.MeshBasicMaterial({
      color: 0xff0000,
      transparent: true,
      opacity: 0.5,
    }); // 创建半透明材质
    
    // for 循环构建阵列
    for (let i = 0; i < 10; i++) {
      for (let j = 0; j < 10; j++) {
        const mesh = new THREE.Mesh(geometry, material);
        mesh.position.set(i * 20, 0, j * 20);
        scene.add(mesh);
      }
    }
    
    // 全屏显示
    const width = window.innerWidth;
    const height = window.innerHeight;
    document.body.style.margin = "0";
    document.body.style.overflow = "hidden";
    
    // 调整相机
    const camera = new THREE.PerspectiveCamera(30, width / height, 0.1, 3000);
    camera.position.set(460, 200, 460);
    camera.lookAt(0, 0, 0);
    
    // 渲染
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(width, height);
    renderer.render(scene, camera);
    document.body.appendChild(renderer.domElement);
    

c. stats.js

  • stats.js 可以查看 Three.js 当前的渲染性能,即计算渲染帧率(Frames per Second,FPS)

    • Three.js 每次执行 WebGL 渲染器的 render() 方法,就会得到一帧图像
  • 导入并使用

    //...
    import Stats from "three/addons/libs/stats.module.js";
    
    const stats = Stats();
    document.body.appendChild(stats.domElement);
    
    function render() {
      stats.update(); // 更新统计信息
      mesh.rotateY(0.01);
      renderer.render(scene, camera);
      requestAnimationFrame(render);
    }
    render();
    

d. dat.gui

  • dat.gui 用于快速创建控制三维场景的 UI 交互界面

  • 导入并使用

    import * as THREE from "three";
    import { GUI } from "three/addons/libs/lil-gui.module.min.js"; // 导入 dat.gui
    
    const scene = new THREE.Scene();
    const geometry = new THREE.BoxGeometry(50, 50, 50);
    const material = new THREE.MeshBasicMaterial({
      color: 0xff0000,
    });
    
    const mesh = new THREE.Mesh(geometry, material);
    mesh.position.set(0, 0, 0);
    scene.add(mesh);
    
    const axesHelper = new THREE.AxesHelper(100);
    scene.add(axesHelper);
    
    const width = window.innerWidth;
    const height = window.innerHeight;
    document.body.style.margin = "0";
    document.body.style.overflow = "hidden";
    
    const camera = new THREE.PerspectiveCamera(30, width / height, 0.1, 3000);
    camera.position.set(200, 200, 200);
    camera.lookAt(0, 0, 0);
    
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(width, height);
    renderer.render(scene, camera);
    document.body.appendChild(renderer.domElement);
    
    // 使用 dat.gui
    const gui = new GUI();
    gui.domElement.style.backgroundColor = "#0bf"; // 设置 GUI DOM 元素背景色
    
    const positionFolder = gui.addFolder("位置"); // 创建子菜单
    positionFolder.open(); // 默认打开菜单
    
    // 添加控制项
    positionFolder
      .add(mesh.position, "x", -50, 50) // add(目标对象, 属性名, 最小值, 最大值) 拖动条
      .name("x 坐标"); // name() 设置名称
    positionFolder
      .add(mesh.position, "y", [-50, -25, 0, 25, 50]) // add(目标对象, 属性名, 枚举值列表) 下拉菜单
      .name("y 坐标")
      .step(2); // step() 设置步长
    positionFolder
      .add(mesh.position, "z", {
        小: -50,
        中: -25,
        大: 0,
        大中: 25,
        大大: 50,
      }) // add(目标对象, 属性名, 枚举对象) 下拉菜单(命名)
      .name("z 坐标")
      .onChange((value) => console.log(`Z 坐标:${value}`)); // onChange() 监听属性值改变
    
      gui
      .addColor(material, "color") // 修改材质颜色
      .name("颜色");
    gui
      .add(mesh, "visible") // add(目标对象, 属性名) 复选框(布尔值)
      .name("是否可见");
    
    // 渲染循环
    function render() {
      renderer.render(scene, camera);
      requestAnimationFrame(render);
    }
    render();
    

0x03 顶点数据

(1)渲染顶点数据

  • 缓冲类型几何体 BufferGeometry 用于定义顶点数据

    • BoxGeometrySphereGeometry 等几何体均基于 BufferGeometry 类构建
  • 举例:

    import * as THREE from "three";
    
    const geometry = new THREE.BufferGeometry(); // 创建一个空几何体对象
    const vertices = new Float32Array([
      0, 0, 0, // 第一个顶点
      50, 0, 0, // 第二个顶点
      0, 100, 0, // 第三个顶点
      0, 0, 10, // 第四个顶点
      0, 0, 100, // 第五个顶点
      50, 0, 10, // 第六个顶点
    ]); // 设置顶点坐标类型化数组
    
    const attribute = new THREE.BufferAttribute(vertices, 3); // 创建顶点属性对象, 3 表示每三个元素为一组, 对应 xyz 三个属性
    geometry.attributes.position = attribute; // 将顶点位置属性设置给几何体
    

a. 点模型渲染顶点数据

  • Mesh 模型类似,点模型对象 Points 将几何体渲染为点,而 Mesh 将几何体渲染为面

  • 点模型对象的材质是点材质 PointsMaterial

  • 将上一节的顶点数据通过点模型对象进行可视化渲染

    import * as THREE from "three";
    import { OrbitControls } from "three/addons/controls/OrbitControls.js";
    
    const geometry = new THREE.BufferGeometry();
    const vertices = new Float32Array([
      0, 0, 0, 50, 0, 0, 0, 100, 0, 0, 0, 10, 0, 0, 100, 50, 0, 10,
    ]);
    
    const attribute = new THREE.BufferAttribute(vertices, 3);
    geometry.attributes.position = attribute;
    
    const material = new THREE.PointsMaterial({
      color: 0xff0000,
      size: 10.0, // 点的像素尺寸
    });
    
    const points = new THREE.Points(geometry, material);
    
    const axesHelper = new THREE.AxesHelper(150);
    
    const scene = new THREE.Scene();
    scene.add(points);
    scene.add(axesHelper);
    
    const width = window.innerWidth;
    const height = window.innerHeight;
    document.body.style.margin = "0";
    document.body.style.overflow = "hidden";
    
    const camera = new THREE.PerspectiveCamera(30, width / height, 0.1, 3000);
    camera.position.set(200, 400, 200);
    camera.lookAt(0, 0, 0);
    
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(width, height);
    renderer.render(scene, camera);
    document.body.appendChild(renderer.domElement);
    
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.addEventListener("change", () => renderer.render(scene, camera));
    

b. 线模型渲染顶点数据

  • 相比 Points 模型,线模型对象 Line 将顶点连成线

  • 使用线模型渲染上述顶点数据

    const material = new THREE.LineBasicMaterial({
      color: 0xff0000,
      size: 10.0, // 点的像素尺寸
    });
    
    const line = new THREE.Line(geometry, material);
    scene.add(line);
    
  • 除了上述线模型,Three.js 还提供了其他线模型:

    • LineLoop 模型构成闭合线条
    • LineSegments 模型构成非连续的线条

c. 网格模型渲染顶点数据

  • 网格模型 Mesh 本质上是由多个三角形(面)拼接而成

  • 使用网格模型渲染上述顶点数据

    const material = new THREE.MeshBasicMaterial({
      color: 0xff0000,
    });
    
    const mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
    

(2)构建平面

绘制平面需要使用 Mesh 模型,而 Mesh 模型根据顶点数据构建三角形面,因此需要通过三角顶点构建所需要的平面,如矩形平面

注意:必须将三角平面的正面(背面)统一

a. 构建矩形平面几何体

  • Mesh 模型构建矩形平面的顶点顺序如下:

    1. 第一个三角平面
      1. 左下角
      2. 右下角
      3. 右上角
    2. 第二个三角平面
      1. 左下角
      2. 右上角
      3. 左上角
  • 举例:在 \(XOY\) 平面,边长为 50 的正方形

    const geometry = new THREE.BufferGeometry();
    const vertices = new Float32Array([
      0, 0, 0,
      50, 0, 0,
      50, 50, 0,
      0, 0, 0,
      50, 50, 0,
      0, 50, 0,
    ]);
    geometry.attributes.position = new THREE.BufferAttribute(vertices, 3);
    
    const material = new THREE.MeshBasicMaterial({
      color: 0xff0000,
    });
    
    const mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
    

b. 顶点索引

  • 顶点索引数据用于移除在构建平面时重复的顶点

    • 上述案例中,如 \((0, 0, 0)\)\((50, 50, 0)\) 两个顶点
  • 使用顶点索引

    const geometry = new THREE.BufferGeometry();
    const indexes = new Uint16Array([0, 1, 2, 0, 2, 3]);
    const vertices = new Float32Array([
      0, 0, 0,
      50, 0, 0,
      50, 50, 0,
      0, 50, 0
    ]);
    
    geometry.attributes.position = new THREE.BufferAttribute(vertices, 3);
    geometry.index = new THREE.BufferAttribute(indexes, 1);	// 导入索引并设置每个索引元素代表一个点
    

c. 顶点法线

  • Three.js 中的法线是广义上的法线,比数学上的法线定义更宽泛

  • 对于需要受光照影响的材质,必须引入法线

    • MeshLambertMaterial
  • 法线通过顶点定义,每个顶点都有一个法线数据

  • 定义顶点法线

    // 顶点法线数据与顶点一一对应,支持索引
    const normals = new Float32Array([
      0, 0, 1, // 顶点一法线
      0, 0, 1, // 顶点二法线
      0, 0, 1, // 顶点三法线
      0, 0, 1, // 顶点四法线
    ])
    
    geometry.attributes.normals = new THREE.BufferAttribute(normals, 3)
    

(3)几何体变换

  • BufferGeometry 通过一系列 Three.js 提供的 API,基于改变顶点数据,实现对几何体的变换

    • 缩放:scale()

    • 平移:translate()

    • \(x\) 轴旋转:rotateX()

      \(y\) 轴旋转:rotateY()

      \(z\) 轴旋转:rotateZ()

    • 居中:center()

  • 将几何体 \(x\) 坐标放大 2 倍,\(y\) 坐标缩小一半,\(z\) 坐标不变

    geometry.scale(2, 1 / 2, 0);
    

    将几何体沿 \(z\) 轴平移 50

    geometry.translate(0, 0, 50);
    

    将几何体沿 \(y\) 轴旋转 45°

    geometry.rotateY(Math.PI / 4);
    

    将几何体居中

    geometry.center();
    

0x04 模型对象

(1)三维向量 Vector3

  • Three.js 提供的模型对象,如 PointsLineMesh,均继承于 Object3D,而 Object3D 中坐标位置 position 等属性,均采用 Vector3 类型

  • 三维向量 Vector3xyz 三个分量,可以表示坐标等变量

    const vector3 = new THREE.Vector3(100, 200, 300); // 声明一个三维向量
    console.log(vector3.x, vector3.y, vector3.z); // 获取三维向量的x,y,z分量
    
    vector3.set(200, 300, 400); // 重新设置三维向量的x,y,z分量
    console.log(vector3.x, vector3.y, vector3.z);
    
  • 基于三维向量,可以对模型对象进行缩放或平移变换,如将立方体放大 2 倍:

    const geometry = new THREE.BoxGeometry(50, 50, 50);
    const material = new THREE.MeshBasicMaterial({
      color: 0xff0000,
      transparent: true,
      opacity: 0.5,
    });
    const mesh = new THREE.Mesh(geometry, material);
    mesh.scale.set(2, 2, 2);
    

(2)欧拉 Euler

  • 模型对象的角度属性 rotation 采用 Euler 类型

  • 特别地,order 分量表示旋转顺序的字符串,默认为 'XYZ'(必须是大写)

  • 基于欧拉,可以对模型对象进行选择变换,如将立方体沿 \(x\) 轴旋转 45°

    const mesh = new THREE.Mesh(geometry, material);
    mesh.rotateX(Math.PI / 4);
    // 或
    // mesh.rotation.x += Math.PI / 4;
    

(3)颜色 Color

  • 模型材质的颜色属性 color 采用 Color 类型

  • 当分量 gb 被定义后,r 表示红色分量,否则为十六进制颜色值

    const red = new THREE.Color(255, 0, 0);
    const green = new THREE.Color(0x00ff00);
    const blue = new THREE.Color("#0000ff");
    

(4)模型材质属性

  • MeshBasicMaterial 等材质均继承于 Material
  • 透明属性:
    • 透明开关:transparent(布尔类型)
    • 透明度:opacity(浮点类型,\(0\) 表示全透明,\(1\) 表示不透明)
  • 面属性: side(整型 / 枚举)
    • 前面可见:THREE.FrontSide
    • 背面可见:THREE.BackSide
    • 双面可见:THREE.DoubleSide

(5)克隆与复制

  • 克隆 clone():获得调用该方法的对象的副本

  • 复制 copy():将传入的参数覆盖到调用该方法的对象相应的属性

  • 举例:

    const mesh = new THREE.Mesh(geometry, material);
    const mesh2 = mesh.clone(); // 克隆 mesh 模型
    mesh2.position.set(100, 0, 0);
    
    mesh.rotation.x += Math.PI / 4;
    mesh2.rotation.copy(mesh.rotation); // 复制角度属性
    
    scene.add(mesh);
    scene.add(mesh2);
    

0x05 层级模型

(1)组对象 Group

  • 组对象 Group 继承于 Object3D,用于将模型对象进行分组管理

    官方文档:https://threejs.org/docs/#api/zh/objects/Group

    const group = new THREE.Group(); // 声明一个组对象
    group.add(mesh); // 向组中添加模型对象
    group.add(mesh2);
    
    scene.add(group); // 将组作为子场景加入场景中
    
    • Mesh 等模型对象也可以通过 add() 方法添加子对象,如为子对象添加坐标系

      const axesHelper = new THREE.AxesHelper(50);
      mesh2.add(axesHelper);
      
    • 相对地,remove() 方法可以移除子对象,如移除 group 中的 mesh2

      group.remove(mesh2);
      
  • Object3D 提供 children 属性来查看其模型树结构

    const scene = new THREE.Scene();
    scene.add(group);
    scene.add(axesHelper);
    
    console.log(scene.children);
    
    flowchart TB Scene-->Group & AxesHelper Group-->1[Mesh] & 2[Mesh]

(2)模型树结构

  • 模型树结构由层级模型节点构成模型树,每个层级模型节点均可进行单独操作

    • 类似 DOM 树
  • 命名:name

    const group = new THREE.Group();
    
    group.name = "立方体组";
    mesh.name = "立方体 A";
    mesh2.name = "立方体 B";
    
    group.add(mesh);
    group.add(mesh2);
    
  • 递归:traverse()

    scene.traverse((item) => console.log(item.name, item));
    
  • 查找:
    如按模型名称查找:getObjectByName()

    console.log(scene.getObjectByName("立方体 A"));
    

(3)坐标

  • Three.js 中,坐标分为本地(局部)坐标、世界坐标

    • 本地(局部)坐标:模型自身的 position 属性值
    • 世界坐标:模型自身及其所有父模型的 position 属性值之和
  • 现象:当改变对象的 position 属性值时,模型位置发生改变;当改变其父对象的 position 属性值时,其位置也会改变

  • 获取模型对象坐标

    scene.position.set(50, 50, 50);
    scene.add(mesh);
    
    const worldPosition = new THREE.Vector3();
    mesh.getWorldPosition(worldPosition);
    console.log("世界坐标", worldPosition.toArray()); // [50, 50, 50]
    console.log("本地坐标", mesh.position.toArray()); // [0, 0, 0]
    

(4)模型显隐

  • Object3D 封装了 visible 属性,用于控制模型对象的显隐,其值为布尔类型

    mesh2.visible = false;
    
  • Material 等也支持通过 visible 属性控制显隐

0x06 纹理贴图

(1)纹理贴图

  • 纹理贴图:将一张图片作为纹理对象 Texture 导入到材质并映射到模型中

  • 创建纹理贴图

    const textureLoader = new THREE.TextureLoader(); // 声明一个纹理贴图加载器
    const texture = textureLoader.load("./public/image.jpg"); // 加载图像并返回纹理对象
    
    const geometry = new THREE.PlaneGeometry(192, 108);
    
    const material = new THREE.MeshBasicMaterial({
      map: texture, // 导入纹理对象
    });
    

(2)顶点 UV 坐标

  • 顶点 UV 坐标用于从纹理贴图上提取像素映射到模型的几何体表面上

    • 获取几何体的顶点 UV 坐标

      console.log(geometry.attributes.uv);
      
  • 顶点 UV 坐标的 \(u\)\(v\) 取值范围均为 \([0, 1]\),其中左下角是 \((0, 0)\),右上角是 \((1, 1)\)

    • 顶点 UV 坐标系相当于二维坐标系,\(u\)\(x\) 轴,\(v\)\(y\)
  • 自定义顶点 UV 坐标

    const geometry = new THREE.PlaneGeometry(192, 108);
    const uvs = new Float32Array([
      0, 0, // 左下角
      1, 0, // 右下角
      1, 1, // 右上角
      0, 1, // 左上角
    ]);
    geometry.attributes.uv = new THREE.BufferAttribute(uvs, 2);
    

(3)圆形平面设置纹理贴图

  • 用于将矩形图片裁剪为圆形并渲染

    const textureLoader = new THREE.TextureLoader();
    const texture = textureLoader.load("./public/image.jpg");
    
    const geometry = new THREE.CircleGeometry(60, 100);
    
    const material = new THREE.MeshBasicMaterial({
      map: texture,
    });
    
  • 本质上,圆形平面的顶点 UV 坐标就是一个圆形,从而实现对图片进行圆形裁剪

(4)阵列

纹理对象 Texture 提供了阵列功能,用于在几何体内重复纹理,而非全填充

const texture = textureLoader.load("./public/image.jpg");
texture.wrapS = THREE.RepeatWrapping; // 水平方向重复
texture.wrapT = THREE.RepeatWrapping; // 垂直方向重复
texture.repeat.set(10, 5); // 水平方向重复 10 次, 垂直方向重复 5 次

(5)背景透明图片纹理

  • 常用于场景标注

  • 在导入透明 PNG 纹理贴图的同时开启透明

    const material = new THREE.MeshBasicMaterial({
      map: texture,
      transparent: true,
    });
    

(6)UV 动画

  • UV 动画基于纹理对象 Texture 的偏移属性 offset 以及阵列功能,通过循环渲染实现

    texture.offset.x += 0.5; // U 轴正方向偏移
    texture.offset.y += 0.5; // V 轴正方向偏移
    
  • 举例:

    // 导入贴图
    const texture = textureLoader.load("./public/image.png");
    
    // 设置阵列
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.repeat.set(10, 1);
    
    // ...
    
    // 循环渲染
    function render() {
      texture.offset.x += 0.05;	// 偏移
      renderer.render(scene, camera);
      requestAnimationFrame(render);
    }
    render();
    

0x07 模型拾取

(1)坐标归一化

  • Three.js 的坐标系原点在屏幕正中央,而浏览器页面的坐标系原点在屏幕的左上角

  • 由于原点不同,同一位置的坐标值存在差异,需要归一化

    const pointer = new THREE.Vector2();
    window.addEventListener("pointermove", (event) => {
      pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
      pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
    });
    

(2)射线与光线投射

  • Three.js 提供射线 Ray 类型,需要指定原点和方向

    const ray = new THREE.Ray(
      new THREE.Vector3(0, 0, 0),
      new THREE.Vector3(1, 0, 0)
    );
    
  • 光线投射 Raycaster 用于进行鼠标拾取,即在三维空间中计算出鼠标移过了什么物体

    // 坐标归一化
    
    const raycaster = new THREE.Raycaster();
    function render() {
      raycaster.setFromCamera(pointer, camera); // 通过摄像机和鼠标位置更新射线
      const intersects = raycaster.intersectObjects(scene.children); // 计算物体和射线的交点
      for (let i = 0; i < intersects.length; i++) {
        intersects[i].object.material.color.set(0xff0000);
      }
      renderer.render(scene, camera);
      requestAnimationFrame(render);
    }
    render();
    

(3)点击交互

  1. 坐标归一化

    const pointer = new THREE.Vector2();
    window.addEventListener("click", (event) => {
      pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
      pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
    });
    
  2. 计算射线

    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(pointer, camera);
    
  3. 计算交点

    const intersects = raycaster.intersectObjects(scene.children);
    if (intersects.length > 0) {
        intersects[0].object.material.color.set(0xff0000);
    }
    

0x08 导入外部三维模型

  • 对于复杂的三维模型,仅通过 Three.js 提供的 API 很难实现,需要依赖其他专业三维建模工具实现,并导入到 Three.js 中渲染至 HTML 页面
  • 常见三维建模软件工具包括:

(1)加载模型

  • Three.js 可以加载 GLTF 格式的三维模型数据

    • GLTF 全称 GL Transmission Format,是基于 Web 端通用的 JSON 格式与二进制格式数据
      • .bin 文件以二进制形式存储模型的顶点数据等
      • .glb 文件是 GLTF 格式的二进制文件,包含模型和贴图信息
    • GLTF 2.0 于 2017 年发布
  • 借助 GLTFLoader 实现 GLTF 格式模型导入

    import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
    
    // ...
    
    const loader = new GLTFLoader();
    loader.load("./public/scene.gltf", (gltf) => scene.add(gltf.scene));
    
    • GLB 格式的模型也可以用上述方法导入
  • 通过 OrbitControls 调整相机至合适的位置

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.addEventListener("change", () => {
      console.log(camera.position); // 调整相机并输出位置参数
      renderer.render(scene, camera);
    });
    

(2)处理模型

  • 对于导入到 Three.js 的三维模型,均采用模型树结构,即可以通过第五章第二节的方法处理模型
  • 导入的模型中,一些外观一致的模型一般会共享材质
    • 美术:设置模型材质独占
    • 开发:批量克隆材质

(3)进度可视

  • 当导入的模型较大时,不可避免的需要一定时间加载

  • GLTFLoaderload() 方法的第三个参数需要一个回调函数,来获取模型的加载信息,实现进度可视

    loader.load(
      "./public/scene.gltf",
      (gltf) => scene.add(gltf.scene),
      (xhr) => console.log(xhr.loaded / xhr.total)
    );
    
posted @ 2024-10-05 17:11  SRIGT  阅读(142)  评论(0编辑  收藏  举报