Skip to content

搭建开发环境 ​

引入three.js ​

type="importmap"配置——扩展库引入

html
<script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.126.1/build/three.module.js", //自己更换版本
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.126.1/examples/jsm/"  
      }
    }
</script>
<script type="module">
     import * as THREE from 'three'; //这里的three和 配置addons/等价于examples/jsm/。
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    console.log(OrbitControls);
</script>

概念 ​

前置知识 ​

  • 场景(Scene):是物体、光源等元素的容器,可以配合 chrome 插件使用,抛出 window.scene即可实时调整 obj 的信息和材质信息。
  • 相机(Camera):场景中的相机,代替人眼去观察,场景中只能添加一个,一般常用的是透视相机(PerspectiveCamera)
  • 物体对象(Mesh):包括二维物体(点、线、面)、三维物体,模型等等
  • 光源(Light):场景中的光照,如果不添加光照场景将会是一片漆黑,包括全局光、平行光、点光源等
  • 渲染器(Renderer):场景的渲染方式,如webGL\canvas2D\Css3D。
  • 控制器(Control): 可通过键盘、鼠标控制相机的移动

相机(Camera) ​

  • 正交(orthographic)相机:物品的渲染尺寸与它距离镜头的远近无关。也就是说在场景中移动一个物体,其大小不会变化。正交镜头适合2D游戏。
  • 透视(perspective)相机:模拟人眼的视觉特点,距离远的物体显得更小。透视镜头通常更适合3D渲染
  • THREE.PerspectiveCamera(fov,aspect,near,far)
    • fov视野角度:FOV取值在60-90度之间较好,默认值为60
    • aspect渲染区域的宽高比:一般是rendder.domElement.clientWidth/render.domElement.clientHeight
    • near最近离镜头的距离
    • far 远离镜头的距离
  • camera.position:控制相机在整个3D环境中的位置
  • camera.lookAt:控制相机的焦点位置,决定相机的朝向

渲染器(renderer) ​

  • 设置渲染器的大小:renderer.setSize(width,height)
  • 设置启用抗锯齿: renderer.antialias = true
  • 设置设备像素比: renderer.setPixelRatio(window.devicePixelRatio)
  • 设置背景颜色: renderer.setClearColor(0x444444, 1);
  • 开始渲染:renderer.render(scene,camera)
  • 获取渲染元素:renderer.domElement,本质是<canvas>

光源 ​

光源说明
AmbientLight环境光,其颜色均匀的应用到场景及其所有对象上,这种光源为场景添加全局的环境光。这种光没有特定的方向,不会产生阴影。通常不会把AmbientLight作为唯一的光源,而是和SpotLight、DirectionalLight等光源结合使用,从而达到柔化阴影、添加全局色调的效果。指定颜色时要相对保守,例如#0c0c0c。设置太亮的颜色会导致整个画面过度饱和,什么都看不清:
PointLight3D空间中的一个点光源,向所有方向发出光线
SpotLight产生圆锥形光柱的聚光灯,台灯、天花板射灯通常都属于这类光源,这种光源的使用场景最多,特别是在你需要阴影效果的时候。
DirectionalLight也就无限光,光线是平行的。典型的例子是日光,用于模拟遥远的,类似太阳那样的光源。该光源与SpotLight的主要区别是,它不会随着距离而变暗,所有被照耀的地方获得相同的光照强度。
HemisphereLight特殊光源,用于创建户外自然的光线效果,此光源模拟物体表面反光效果、微弱发光的天空,模拟穹顶(半球)的微弱发光效果,让户外场景更加逼真。使用DirectionalLight + AmbientLight可以在某种程度上来模拟户外光线,但是不够真实,因为无法体现大气层的散射效果、地面或物体的反射效果
AreaLight面光源,指定一个发光的区域
LensFlare不是光源,用于给光源添加镜头光晕效果

加载外部模型 ​

  • 通过Three.js加载器(Loader)实现
  • 加载器把文本/二进制的模型文件转化为Three.js对象结构。 每个加载器理解某种特定的文件格式。
  • 需要注意的是,由于贴图的尺寸必须是(2的幂数)X (2的幂数),如:1024X512,所以为了防止贴图变形,平面的宽度比例需要与贴图的比例一致。
  • 支持的格式
    格式说明
    JSONThree.js自定义的、基于JSON的格式。可以声明式的定义一个Geometry或者Scene.利用该格式,你可以方便的重用复杂的Geometry或Scene
    OBJ / MTLOBJ是Wavefront开发的一种简单3D格式,此格式被广泛的支持,用于定义Geometry,MTL用于配合OBJ,它指定OBJ使用的材质
    Collada(dae)基于XML的格式,被大量3D应用程序、渲染引擎支持
    STLSTereoLithography的简写,在快速原型领域被广泛使用。3D打印模型通常使用该格式定义Three.js提供了STLExporter.js,使用它可以把Three.js模型导出为STL格式
    CTMopenCTM定义的格式,以紧凑的格式存储基于三角形的Mesh
    VTKVisualization Toolkit定义的格式,用于声明顶点和面。此格式有二进制/ASCII两种变体,Three.js仅支持ASCII变体
    AWD3D场景的二进制格式,主要被away3d引擎使用,Three.js不支持AWD压缩格式
    Assimp开放资产导入库(Open asset import library)是导入多种3D模型的标准方式。使用该Loader你可以导入多种多样的3D模型格式
    VRML虚拟现实建模语言(Virtual Reality Modeling Language)是一种基于文本的格式,现已经被X3D格式取代尽管Three.js不直接支持X3D,但是后者很容易被转换为其它格式
    Babylon游戏引擎Babylon的私有格式
    PLY常用于存储来自3D扫描仪的信息

基本使用 ​

使用模板 ​

js
   class Scene {
      constructor(renderTarget) {
        this.scene = new THREE.Scene();
        this.camera = null;
        this.renderer = null;
        this.controls = null;
        this.mesh = null
        this.renderTarget = renderTarget; // 渲染目标

        this.initRenderer();
        this.initCamera();
        this.initControls();
        this.initLights();
        this.initHelper();
        this.addMesh()
        this.Stats = new Stats(); //性能监视器
        this.render();
        window.addEventListener('resize', this.onWindowResize.bind(this), false);
        document.body.appendChild(this.Stats.domElement);
      }

      initRenderer() {
        this.renderer = new THREE.WebGLRenderer({ antialias: true });
        this.renderer.setSize(this.renderTarget.clientWidth, this.renderTarget.clientHeight);
        this.renderer.setPixelRatio(window.devicePixelRatio); // 屏幕像素比
        this.renderer.setClearColor(0x444444, 1); // 背景颜色
        this.renderTarget.appendChild(this.renderer.domElement);
      }

      initCamera() {
        this.camera = new THREE.PerspectiveCamera(30, this.renderTarget.clientWidth / this.renderTarget.clientHeight, 1, 3000);
        this.camera.position.set(3, 3, 3);
        this.camera.lookAt(new THREE.Vector3(0, 0, 0));
      }

      initControls() {
        this.controls = new OrbitControls(this.camera, this.renderer.domElement);
        this.controls.enableDamping = true; // 启用阻尼
        this.controls.dampingFactor = 0.1; // 阻尼惯性
        this.controls.minDistance = 5;
        this.controls.maxDistance = 20;
        this.controls.target.set(0, 0, 0) //注意和lookAt保持一致
        this.controls.addEventListener('change', () => {
          this.renderer.render(this.scene, this.camera);
        });
        this.controls.zoomSpeed = 0.8; // 缩放速度
      }

      initLights() {
        const ambientLight = new THREE.AmbientLight(0x404040); // (color ,intensity) 光照强度
        this.scene.add(ambientLight);

        const pointLight = new THREE.PointLight(0xffffff, 0.1); // (color, intensity, distance, decay) 光照衰减
        pointLight.position.set(-3, -6, -5);
        this.scene.add(pointLight);

        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
        directionalLight.position.set(3, 6, 5);
        this.scene.add(directionalLight);
      }

      initHelper() { /* 辅助对象 */
        const size = 100; // 网格尺寸
        const divisions = 100; // 网格分段数
        const gridHelper = new THREE.GridHelper(size, divisions, 0xCCCCCC, 0xCCCCCC);
        gridHelper.material.opacity = 0.2;
        gridHelper.material.transparent = true;
        gridHelper.name = 'gridHelper'
        this.scene.add(gridHelper);

        const axesHelper = new THREE.AxesHelper(5);
        axesHelper.name = 'axesHelper'
        this.scene.add(axesHelper);
      }

      addMesh(){
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        const material = new THREE.MeshLambertMaterial({ color: 0x00ff00 });
        this.mesh = new THREE.Mesh(geometry, material);
        this.scene.add(this.mesh);
      }

      onWindowResize() {
        // 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
        // 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
        // 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
        this.camera.aspect = this.renderTarget.clientWidth / this.renderTarget.clientHeight; //更新摄像头的宽高比
        this.camera.updateProjectionMatrix();//更新投影矩阵
        this.renderer.setSize(this.renderTarget.clientWidth, this.renderTarget.clientHeight);//重置渲染器输出画布canvas尺寸
        this.renderer.setPixelRatio(window.devicePixelRatio) //设置渲染器的像素比
        this.renderer.render(this.scene, this.camera);
      }

      render() {
        requestAnimationFrame(this.render.bind(this)); // 周期性执行
        this.Stats.update()
        this.controls.update(); //update()函数内会执行camera.lookAt(controls.target)
        this.renderer.render(this.scene, this.camera); // 渲染场景,类似于拍照
      }
    }

控制画面全屏 ​

js
window.addEventListener('dblclick', () => {
    const fullScreenElement = document.fullscreenElement
    console.log(this.renderer.domElement)
    if (!fullScreenElement) {
        this.renderer.domElement.requestFullscreen() //让画布对象全屏
    } else {
        document.exitFullscreen()//让文档对象退出全屏
    }
})

GUI调试库 ​

.add()方法

  • gui.add(控制对象,对象具体属性,参数3,参数4 ...)
    • 参数3和参数4,分别是一个数字,交互界面是一个鼠标可以拖动的拖动条,可以在一个区间改变属性的值
    • 参数3:array -- 生成交互界面是下拉菜单
    • 参数3:object -- 生成交互界面是下拉菜单
    • 如果.add()改变属性的对应的数据类型如果是布尔值,那么交互界面就是一个单选框。

.name()方法.add()创建的交互界面,会默认显示所改变属性的名字,为了通过交互界面更好理解你改变的某个对象属性,你可以通过.name()方法改变gui生成交互界面显示的内容。

js
gui.add(ambient, 'intensity', 0, 2.0).name('环境光强度');

.step()方法-步长 步长.step()方法可以设置交互界面每次改变属性值间隔是多少。

js
gui.add(ambient, 'intensity', 0, 2.0).name('环境光强度').step(0.1);

.onChange()方法 当gui界面某个值的时候,.onChange()方法就会执行,这时候你可以根据需要通过.onChange()执行某些代码。

js
const obj = {
    x: 30,
};
// 当obj的x属性变化的时候,就把此时obj.x的值value赋值给mesh的x坐标
gui.add(obj, 'x', 0, 180).onChange(function(value){
    mesh.position.x = value;
	// 你可以写任何你想跟着obj.x同步变化的代码
	// 比如mesh.position.y = value;
});

.addColor()颜色值改变 .addColor()生成颜色值改变的交互界面

js
const obj = {
    color:0x00ffff,
};
// .addColor()生成颜色值改变的交互界面
gui.addColor(obj, 'color').onChange(function(value){
    mesh.material.color.set(value);
});

.addFolder()分组 通过gui对象的.addFolder()方法可以创建一个子菜单

  • .addFolder()返回的子文件夹对象,同样具有gui对象的.add()、.onChange()、.addColor()等等属性
  • 关闭.close()和展开.open()交互界面
  1. 材质(Meterial)

几何体(BufferGeometry) ​

概念: ​

threejs的长方体BoxGeometry、球体SphereGeometry等几何体都是基于BufferGeometry类构建的,BufferGeometry是一个没有任何形状的空几何体,你可以通过BufferGeometry自定义任何几何形状,具体一点说就是定义顶点数据。

自定义几何体 ​

定义顶点位置数据

js
const geometry = new THREE.BufferGeometry() 
const vertices = new Float32Array([ //类型化数组创建顶点数据
    0, 0, 0,
    1, 0, 0,
    0, 1, 0,
    1, 1, 0,
    0, 0, 1,
    1, 0, 1,
    0, 1, 1,
    1, 1, 1,
]);
// const attributes = new THREE.BufferAttribute(vertices, 3)
// geometry.attributes.position = attributes
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); // 创建属性缓冲区对象,并设置位置属性

顶点索引数据

js
    // Uint16Array类型数组创建顶点索引数据
    const indexes = new Uint16Array([
        // 下面索引值对应顶点位置数据中的顶点坐标
        0, 1, 2, 0, 2, 3,
    ])
    // 索引数据赋值给几何体的index属性
    // geometry.index = new THREE.BufferAttribute(indexes, 1); 
     geometry.setIndex(new THREE.BufferAttribute(index, 1))//1个为一组

定义顶点法线数据

js
    //使用受光照影响的材质,几何体BufferGeometry需要定义顶点法线数据。
    const norlmals = new Float32Array([
        0, 0, 1, 
        0, 0, 1,
        0, 0, 1,
        0, 0, 1,
        0, 0, 1,
        0, 0, 1,
    ])
    geometry.attributes.normal = new THREE.BufferAttribute(norlmals, 3)

定义顶点颜色数据

js
    //定义顶点颜色数据
    const colors = new Float32Array([
        1, 0, 0, 
        1, 0, 0,
        1, 0, 0,
    ]);
    geometry.attributes.color = new THREE.BufferAttribute(colors, 3)
    const material = new THREE.LineBasicMaterial({
        vertexColors:true,//使用顶点颜色渲染
    });
    const line = new THREE.Line(geometry, material);

几何体细分数

js
    //矩形几何体PlaneGeometry的参数3,4表示细分数,默认是1,1
    const geometry = new THREE.PlaneGeometry(100,50,1,1);
    //球体SphereGeometry参数2、3分别代表宽、高度两个方向上的细分数,默认32,16,
    const geometry = new THREE.SphereGeometry( 50, 32, 16 );

旋转、平移、缩放

js
    geometry.scale(2, 2, 2); // 几何体xyz三个方向都放大2倍
    geometry.translate(50, 0, 0); // 几何体沿着x轴平移50
    geometry.rotateX(Math.PI / 4); // 几何体绕着x轴旋转45度
    geometry.center();// 居中:已经偏移的几何体居中,执行.center(),你可以看到几何体重新与坐标原点重合

常用几何体 ​

  • 2D
    图形说明
    矩形 THREE.PlaneGeometry外观上是一个矩形new THREE.PlaneGeometry(width, height, widthSegments, heightSegments);
    THREE.CircleGeometry外观上是一个圆形或者扇形 // 半径为3的圆new THREE.CircleGeometry(3, 12); // 半径为3的半圆new THREE.CircleGeometry(3, 12, 0, Math.PI);第三个参数和第四个分别是起始角度和结束角度,默认0-2*PI
    THREE.RingGeometry外观上是一个圆环或者扇环 new THREE.RingGeometry(innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength)
    THREE.ShapeGeometry该形状允许你创建自定义的二维图形,其操作方式类似于SVG/Canvas中的画布
  • 3D
    图形说明
    THREE.BoxGeometry这是一个具有长宽高的盒子 BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments)
    THREE.SphereGeometry这是一个三维球体/不完整球体 SphereGeometry(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength)
    THREE. CylinderGeometry可以绘制圆柱、圆筒、圆锥或者截锥 new THREE.CylinderGeometry(radiusTop,radiusBottom,height,radialSegments,heightSegments,openEnded)

数学库 ​

三维向量Vector3 ​

  • 常用方法
    • set(x,y,z) 三维向量的xyz
    • add(v3) 将传入的向量v和这个向量相加。
    • sub(v3) 从该向量减去向量v。
    • multiply(s) 将该向量与所传入的标量s进行相乘。
    • normalize() 将该向量转换为单位向量(unit vector), 也就是说,将该向量的方向设置为和原向量相同,但是其长度(length)为1。
js
    const v3 = new THREE.Vector3(10,10,10) //三维向量 x,y,z
    //可以用set方法修改三维向量的xyz
    //网格模型的position,scale就是一个三维向量的实例
    v3.normalize()//转化为单位向量
    mesh.translateOnAxis(v3,100)  //沿着向量方向平移100

角度属性.rotation(欧拉Euler对象) ​

欧拉角用三个角度来描述3D空间中的旋转,通常表示为(x, y, z)或(α, β, γ)。在Three.js中,欧拉角使用弧度制。 基础使用

js
// 创建欧拉角
let euler = new THREE.Euler(0, Math.PI/2, 0, 'XYZ');

// 设置欧拉角
object.rotation.set(Math.PI/4, 0, Math.PI/2);

// 获取欧拉角
let currentRotation = object.rotation;

const a = new THREE.Euler( 0, 1, 1.57, 'XYZ' );
const b = new THREE.Vector3( 1, 0, 1 );
b.applyEuler(a);

mesh.rotation.y += Math.PI/3; //绕y轴的角度设置为60度
mesh.rotateX(Math.PI/4);//绕x轴旋转π/4
const axis = new THREE.Vector3(0,1,0);//向量axis
mesh.rotateOnAxis(axis,Math.PI/8);//绕axis轴旋转π/8

四元数Quaternion ​

四元数用四个数值(x, y, z, w)来表示3D旋转。虽然不如欧拉角直观,但在数学运算和插值方面有优势。 欧拉角和四元数可以互相转换 基础使用

js
// 创建四元数
let quaternion = new THREE.Quaternion();

// 从欧拉角设置四元数
quaternion.setFromEuler(new THREE.Euler(Math.PI/4, 0, Math.PI/2));

// 应用四元数到物体
object.quaternion.copy(quaternion);

// 获取四元数
let currentQuaternion = object.quaternion;
const q = new THREE.Quaternion(0, 0, 0, 1); //四元数
q.setFromAxisAngle(axis, Math.PI/4); //绕axis轴旋转π/4
mesh.quaternion.multiply(q); //绕四元数旋转
mesh.rotateOnWorldAxis(axis,Math.PI/8); //绕世界轴旋转

注意事项

  • 确保使用的是单位四元数(长度为1)。Three.js通常会自动处理这个问题。
  • 使用 .slerp() 方法进行平滑的旋转插值。
  • 避免直接修改四元数的 x, y, z, w 值:最好使用提供的方法如 .setFromEuler(), .setFromAxisAngle() 等。 选择建议
  • 如果需要直观地设置或显示旋转,使用欧拉角。
  • 对于动画、插值或需要进行大量旋转计算的情况,使用四元数。
  • 在存储或传输旋转数据时,考虑使用四元数以避免万向节死锁问题。(当其中一个旋转轴与另一个旋转轴重合时,会失去一个自由度,这就是万向节死锁。)

模型材质颜色(Color对象) ​

以下创建方法得到的都是一个Color对象:包含rgb以及isColor属性

js
//empty constructor - will default white
const color1 = new THREE.Color();

//Hexadecimal color (recommended)
const color2 = new THREE.Color( 0xff0000 );

//RGB string
const color3 = new THREE.Color("rgb(255, 0, 0)");
const color4 = new THREE.Color("rgb(100%, 0%, 0%)");

//X11 color name - all 140 color names are supported.
//Note the lack of CamelCase in the name
const color5 = new THREE.Color( 'skyblue' );

//HSL string
const color6 = new THREE.Color("hsl(0, 100%, 50%)");

//Separate RGB values between 0 and 1
const color7 = new THREE.Color( 1, 0, 0 );

克隆.clone()和复制.copy() ​

js
    /* 克隆.clone()、复制.copy()是threejs很多对象都具有的方法,比如三维向量对象Vector3、网格模型Mesh、几何体、材质。 */
    const v1 = new THREE.Vector3(1, 2, 3);
    const v2 = v1.clone();//v2是一个新的Vector3对象,和v1的.x、.y、.z属性值一样
    const v1 = new THREE.Vector3(1, 2, 3);
    const v3 = new THREE.Vector3(4, 5, 6);
    v3.copy(v1);//读取v1.x、v1.y、v1.z的赋值给v3.x、v3.y、v3.z

    //用代码方式解决mesh共享材质问题
    gltf.scene.traverse(function (obj) {
        if (obj.isMesh) {
            // .material.clone()返回一个新材质对象,和原来一样,重新赋值给.material属性
            obj.material = obj.material.clone();
        }
    });

深度克隆 ​

js
deepClone(scene) {
    const defaultMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff });
    if (scene.isMesh) {
        const geometryCopy = scene.geometry.clone();
        let materialCopy;
        if (Array.isArray(scene.material)) {
            materialCopy = scene.material.map(mat => mat.clone());
        } else {
            materialCopy = scene.material.clone();
        }
        return new THREE.Mesh(geometryCopy, materialCopy);
    } else {
        const group = new THREE.Group();
        scene.traverse(mesh => {
            if (mesh.isMesh) {
            const geometryCopy = mesh.geometry.clone();
            let materialCopy;
            if (Array.isArray(mesh.material)) {
                materialCopy = mesh.material.map(mat => mat.clone());
            } else {
                materialCopy = mesh.material.clone();
            }
            const meshCopy = new THREE.Mesh(geometryCopy, materialCopy);
            group.add(meshCopy);
            }
        });
        return group;
    }
}

材质(Material) ​

概念 ​

材质描述了对象objects的外观。它们的定义方式与渲染器无关, 因此,如果您决定使用不同的渲染器,不必重写材质。

常用属性以及方法

  • color 材质的颜色
  • opacity 材质的透明度
  • transparent 设置为true时,通过设置材质的opacity属性来控制材质透明的程度。
  • side 默认为THREE.FrontSide。其他选项有THREE.BackSide 和 THREE.DoubleSide。
  • needsUpdate 指定需要重新编译材质。
  • .visible 可以控制是否隐藏该材质对应的模型对象。
  • dispose () 处理材质。材质的纹理不会被处理。需要通过Texture处理。

常用材质分类: ​

材质说明
MeshBasicMaterial基本的材质,显示为简单的颜色或者显示为线框。不考虑光线的影响
MeshDepthMaterial使用简单的颜色,但是颜色深度和距离相机的远近有关
MeshNormalMaterial基于面Geometry的法线(normals)数组来给面着色
MeshFacematerial容器,允许为Geometry的每一个面指定一个材质
MeshLambertMaterial考虑光线的影响,哑光材质
MeshPhongMaterial考虑光线的影响,光泽材质
ShaderMaterial允许使用自己的着色器来控制顶点如何被放置、像素如何被着色
LineBasicMaterial用于THREE.Line对象,创建彩色线条
LineDashMaterial用于THREE.Line对象,创建虚线条
RawShaderMaterial仅和THREE.BufferedGeometry联用,优化静态Geometry(顶点、面不变)的渲染

物体 ​

组(Group / Object3D) ​

它几乎和Object3D是相同的(历史遗留问题),其目的是使得组中对象在语法上的结构更加清晰。

常用属性以及方法 ​

  • 坐标(Position)
    • 本地坐标(局部):就是模型的.position属性。
    • 世界坐标:模型自身.position和所有父对象.position累加的坐标。
    • .getWorldPosition(target)获取世界坐标 结果将被复制到这个Vector3中。
    • 局部坐标系:mesh.add(new THREE.AxesHelper(50));
  • .visible,通过该属性可以隐藏或显示一个模型。
  • .add(...) 添加对象到这个对象的子级,可以添加任意数量的对象。
  • .remove(...) 从当前对象的子级中移除对象。可以移除任意数量的对象。
  • .traverse(callback):遍历当前对象及其子对象,并执行回调函数。
js
    const group = new THREE.Group();//创建一个组对象
    const obj = new THREE.Object3D() //历史遗留问题,相当于Group,表示模型对象节点
    group.add(mesh1,mesh2); //把 mesh1型 mesh2型插入到组group中,作为group的子对象
    group.name='小区房子'; //模型对象通过.name属性命名进行标记
    scene.add(group);//把group插入到场景中作为场景子对象
    group.translateY(50) //通过移动父对象,使子对象都移动

    mesh1.add(mesh2); //threejs默认mesh也可以添加子对象,mesh基类也是Object3D

    // 递归遍历model包含所有的模型节点
    model.traverse(function(obj) {
        console.log('所有模型节点的名称',obj.name);
        // obj.isMesh:if判断模型对象obj是不是网格模型'Mesh'
        if (obj.isMesh) {//判断条件也可以是obj.type === 'Mesh'
            obj.material.color.set(0xffff00);
        }
    });

    // 返回名.name为"4号楼"对应的对象  类似查找Dom
    const nameNode = scene.getObjectByName ("4号楼");

点(Points) ​

js
    const material = new THREE.PointsMaterial({
        color: 0xffff00,
        size: 10.0, //点对象像素尺寸
        sizeAttenuation: false,// 是否根据相机距离而衰减
    }); 
    const points = new THREE.Points(geometry, material); //点模型对象

点模型sizeAttenuation注意点:

  • 当不使用大小衰减(sizeAttenuation: false)时,size仅是一个固定的像素大小,适用于需要点大小与距离无关的场景。
  • 当使用大小衰减(sizeAttenuation: true)时,size定义了相对大小,其最终大小取决于点相对于相机位置的透视效果。

线(Line) ​

js
    // 线材质对象
    const material = new THREE.LineBasicMaterial({
        color: 0xff0000 //线条颜色
    }); 
    // 创建线模型对象
    const line = new THREE.Line(geometry, material);
    const line = new THREE.LineLoop(geometry, material);  // 闭合线条
    const line = new THREE.LineSegments(geometry, material);//非连续的线条

网格(Mesh)(三角形概念) ​

  • 注意点 : 网格模型三角形:三个顶点顺序是逆时针的方向视为正面
js
    const material = new THREE.MeshBasicMaterial({
        color: 0x0000ff, //材质颜色
        side: THREE.FrontSide, //默认只有正面可见 DoubleSide两面可见  BackSide背面可见
    });
    const points = new THREE.Mesh(geometry, material)

纹理(Texture) ​

创建一个纹理贴图,将其应用到一个表面,或者作为反射/折射贴图。

纹理加载器(TextureLoader) ​

  • .load (url, onLoad, onProgress, onError ) : Texture:
    • 加载一张图片可以返回一个纹理对象Texture
    • 纹理对象Texture可以作为模型材质颜色贴图.map属性的值。
    • 颜色贴图map和color属性颜色值会混合,如果没有特殊需要,设置了颜色贴图.map,不用设置color的值,color默认白色0xffffff
js
    const geometry = new THREE.PlaneGeometry(200, 100); 
    //纹理贴图加载器TextureLoader
    const texLoader = new THREE.TextureLoader();
    // .load()方法加载图像,返回一个纹理对象Texture
    const texture = texLoader.load('./earth.jpg');
    const material = new THREE.MeshLambertMaterial({
        // 设置纹理贴图:Texture对象作为材质map属性的属性值
        map: texture,//map表示材质的颜色贴图属性
    });

纹理贴图颜色偏差解决 ​

纹理对象Texture颜色空间编码属性.encoding

  • THREE.LinearEncoding:线性颜色空间 (默认值3000)
  • THREE.sRGBEncoding:sRGB (opens new window)颜色空间 (3001)

threejs加载gltf模型,颜色贴图map属性.encoding的默认值是sRGB颜色空间THREE.sRGBEncoding。outputEncoding的默认值是线性空间THREE.LinearEncoding,和纹理对象.encoding默认值一样,如果颜色贴图.encoding的值是THREE.sRGBEncoding,为了避免颜色偏差,.outputEncoding的值也需要设置为THREE.sRGBEncoding。

js
//解决加载gltf格式模型纹理贴图和原图不一样问题
renderer.outputEncoding = THREE.sRGBEncoding;

注意!!最新版本属性名字有改变。渲染器属性名.outputEncoding已经变更为.outputColorSpace.纹理对象属性名.encoding已经变更为.colorSpace

js
//新版本,加载gltf,不需要执行下面代码解决颜色偏差
renderer.outputColorSpace = THREE.SRGBColorSpace;//设置为SRGB颜色空间

texture.colorSpace  = THREE.SRGBColorSpace;//设置为SRGB颜色空间

纹理贴图错位问题解决 ​

直接给gltf模型材质设置.map属性更换贴图,会出现纹理贴图错位的问题,这主要和纹理对象Texture的翻转属性.flipY有关。.flipY表示是否翻转纹理贴图在Mesh上的显示位置

js
loader.load("../手机模型.glb", function (gltf) {
    const mesh = gltf.scene.children[0]; //获取Mesh
    console.log('.flipY', mesh.material.map.flipY);
})

//是否翻转纹理贴图
texture.flipY = false;

顶点UV坐标 ​

顶点UV坐标可以在0~1.0之间任意取值,纹理贴图左下角对应的UV坐标是(0,0),右上角对应的坐标(1,1)。

查看UV坐标

js
    const geometry = new THREE.PlaneGeometry(200, 100); //矩形平面
    // const geometry = new THREE.BoxGeometry(100, 100, 100); //长方体
    // const geometry = new THREE.SphereGeometry(100, 30, 30);//球体
    console.log('uv',geometry.attributes.uv);

自定义顶点UVgeometry.attributes.uv

js
    /**纹理坐标0~1之间随意定义*/
    const uvs = new Float32Array([
        0, 0, //图片左下角   2个为一组 表示一个顶点的uv坐标
        1, 0, //图片右下角
        1, 1, //图片右上角
        0, 1, //图片左上角
    ]);
    // 设置几何体attributes属性的位置uv属性
    geometry.attributes.uv = new THREE.BufferAttribute(uvs, 2);

获取纹理贴图四分之一

js
    const uvs = new Float32Array([
        0, 0, 
        0.5, 0, 
        0.5, 0.5, 
        0, 0.5, 
    ]);

纹理对象Texture阵列

js
    // .load()方法加载图像,返回一个纹理对象Texture
    const texture = texLoader.load('./瓷砖.jpg');
    // 设置阵列模式
    texture.wrapS = THREE.RepeatWrapping; 
    texture.wrapT = THREE.RepeatWrapping;
    // uv两个方向纹理重复数量
    texture.repeat.set(12,12);//注意选择合适的阵列数量
    mesh.rotateX(-Math.PI/2);// 旋转矩形平面

矩形Mesh+背景透明png贴图

  • 创建一个矩形平面,设置颜色贴图.map,注意选择背景透明的.png图像作为颜色贴图,同时材质设置transparent: true,这样png图片背景完全透明的部分不显示。
  • 如果你不想矩形平面Mesh与地面网格线重合,可以通过位置属性.position适当偏移。mesh.position.y = 1;

UV动画

  • 纹理对象Texture的.offset的功能是偏移贴图在Mesh上位置,本质上相当于修改了UV顶点坐标。
js
    /* 纹理对象.wrapS或.wrapT与.offset组合使用
        + 当你通过.offset设置了纹理映射偏移后,把.wrapS或.wrapT设置为重复映射模式THREE.RepeatWrapping 
    */
    texture.offset.x +=0.5;//纹理U方向偏移
    // 设置.wrapS也就是U方向,纹理映射模式(包裹模式)
    texture.wrapS = THREE.RepeatWrapping;//对应offste.x偏移

    //通过阵列纹理贴图设置.map,这样的话贴图像素可以更小一些。
    // 设置U方向阵列模式
    texture.wrapS = THREE.RepeatWrapping;
    // uv两个方向纹理重复数量
    texture.repeat.x=50;//注意选择合适的阵列数量
    // 渲染循环
    function render() {
        texture.offset.x +=0.1;//设置纹理动画:偏移量根据纹理和动画需要,设置合适的值
        renderer.render(scene, camera);
        requestAnimationFrame(render);
    }
    render();

加载外部三维模型(gltf) ​

GLTF格式简介 (Web3D领域JPG) ​

  • 相比较obj、stl等格式而言,.gltf格式可以包含更多的模型信息。
  • 不仅three.js,其它的WebGL三维引擎cesium、babylonjs都对gltf格式有良好的的支持
  • GLTF文件就是通过JSON的键值对方式来表示模型信息,比如meshes表示网格模型信息,materials表示材质信息...
  • 有些glTF文件会关联一个或多个.bin文件,.bin文件以二进制形式存储了模型的顶点数据等信息。 .bin文件中的信息其实就是对应gltf文件中的buffers属性
  • .glb就是gltf格式的二进制文件。

GLTF加载器(GLTFLoader) ​

js
    // 引入gltf模型加载库GLTFLoader.js
    import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
    // 创建GLTF加载器对象
    const loader = new GLTFLoader()
    loader.load( 'gltf模型.gltf', function ( gltf ) {
    console.log('控制台查看加载gltf文件返回的对象结构',gltf);
    console.log('gltf对象场景属性',gltf.scene);
    // 返回的场景对象gltf.scene插入到threejs场景中
    scene.add( gltf.scene );
    })

MeshSurfaceSampler ​

MeshSurfaceSampler是Three.js库中提供的一个工具,用于从三维模型表面均匀地抽样位置。这种方法主要用于散点生成、粒子效果和任何需要在模型表面随机分布元素的场合。

原理 ​

MeshSurfaceSampler的工作原理基于对三角形网格模型表面的采样。简单来说,它会根据模型的面积分布,在模型表面随机抽取点。这一过程包含几个关键步骤:

  1. **计算每个三角形的面积:**首先,MeshSurfaceSampler会遍历模型的所有三角形,计算它们的面积。这一步骤是基于三角形顶点坐标来完成的。

  2. **构建累积面积数组:**随后,它会构建一个累积面积数组,数组中每个元素的值等于从第一个三角形到当前三角形的所有面积之和。这样可以根据三角形面积在模型上创建一个概率分布,从而确保点的抽样与三角形的面积成比例。

  3. **随机抽样:**当需要生成一个点时,MeshSurfaceSampler会生成一个随机数,这个随机数对应于累积面积数组中的某个值。通过比较随机数与累积面积数组的元素值,可以选择对应的三角形进行采样。

  4. **在选定的三角形内生成点:**一旦选定了三角形,MeshSurfaceSampler会在这个三角形内随机生成一个点。这一步通常涉及到在三角形内使用重心坐标或其他方法来保证点的均匀分布。

用法 ​

js
import * as THREE from 'three';
import { MeshSurfaceSampler } from 'three/examples/jsm/math/MeshSurfaceSampler.js';

convertPointCloudBySphere(mesh, totalSum = 2000) {
    const group = new THREE.Group();
    const sampler = new MeshSurfaceSampler(mesh).build();
    const tempPosition = new THREE.Vector3();
    for (let i = 0; i < totalSum; i++) {
    sampler.sample(tempPosition);
    // tempPosition.multiplyScalar(normalizationInfo.scaleFactor);
    const sphereGeometry = new THREE.SphereGeometry(0.02, 0.05, 0.05);
    const sphereMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, emissive: 0x404040 });
    const sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);

    sphereMesh.position.copy(tempPosition);
    group.add(sphereMesh);
    }

    this.isAnimation = false;
    this.loadingcb && this.loadingcb();

    return group;
}

计算每个模型的表面积 ​

js
calculateSurfaceArea(mesh) {
    const geometry = mesh.geometry;
    let area = 0;
    const vertices = geometry.attributes.position.array;
    const indices = geometry.index ? geometry.index.array : undefined;
    for (let i = 0; i < (indices ? indices.length : vertices.length / 3); i += 3) {
    const id0 = indices ? indices[i] * 3 : i * 3;
    const id1 = indices ? indices[i + 1] * 3 : (i + 1) * 3;
    const id2 = indices ? indices[i + 2] * 3 : (i + 2) * 3;

    const v0 = new THREE.Vector3(vertices[id0], vertices[id0 + 1], vertices[id0 + 2]);
    const v1 = new THREE.Vector3(vertices[id1], vertices[id1 + 1], vertices[id1 + 2]);
    const v2 = new THREE.Vector3(vertices[id2], vertices[id2 + 1], vertices[id2 + 2]);

    // 通过计算三角形面积来计算总面积
    const edge1 = new THREE.Vector3().subVectors(v1, v0);
    const edge2 = new THREE.Vector3().subVectors(v2, v0);
    const crossProduct = new THREE.Vector3().crossVectors(edge1, edge2);
    area += crossProduct.length() * 0.5;
    }
    return area;
}

归一化处理 ​

合并几何体并归一化处理 ​

注意点:

  • mesh.updateMatrixWorld(true);当你调整了对象的变换属性(如position、scale或rotation)后,需要调用updateMatrixWorld来更新世界矩阵。传递true作为参数会强制更新该对象以及它的所有后代的世界矩阵。

  • geometry.applyMatrix4(mesh.matrixWorld); 这个函数将指定的矩阵应用于几何体中的所有顶点。

  • finalGeometry.computeVertexNormals();这个函数重新计算几何体的顶点法线。

js
   this.fileLoaderMap[extension].load(event.target.result, (loaded) => {
        if (loaded.scene) loaded = loaded.scene;
        let mesh = this.makeSingleMeshAndNormalize(loaded, extension)
    })

          makeSingleMeshAndNormalize(object, source_extension) {
        let finalGeometry;

        if (object.isMesh)
          finalGeometry = object.geometry;
        else if (object.isBufferGeometry) { // stl, ply
          const geometry = object;
          if (source_extension == "stl")
            geometry.rotateX(-Math.PI / 2);
          finalGeometry = geometry;
        }
        else if (object.isGroup) { // glb, dae, obj, fbx
          const scene = object;
          scene.updateMatrixWorld(true);

          const geometries = [];
          scene.traverse((object) => {
            if (object.isMesh) {
              const mesh = object;
              const geometry = mesh.geometry.clone();

              function retainOnlyAttribute(obj, keyToKeep) {
                for (const key in obj) {
                  if (obj.hasOwnProperty(key) && key !== keyToKeep) {
                    delete obj[key];
                  }
                }
              }
              retainOnlyAttribute(geometry.attributes, "position");

              // console.log("merging", geometry)
              geometry.applyMatrix4(mesh.matrixWorld);
              geometries.push(geometry);
            }
          });

          finalGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);
        }
        finalGeometry.computeVertexNormals(); // not necessary
        this.normalizeGeometry(finalGeometry);

        const finalMesh = new THREE.Mesh(finalGeometry, new THREE.MeshStandardMaterial({ color: 0xffffff }));
        return finalMesh;
      }

    normalizeGeometry(geometry) {
        const mesh = new THREE.Mesh(geometry);

        let scaleFactor = 1;
        const center = new THREE.Vector3();
        const boundingBox = new THREE.Box3().setFromObject(mesh);
        if (!boundingBox.isEmpty()) {
          const maxDimension = Math.max(
            boundingBox.max.x - boundingBox.min.x,
            boundingBox.max.y - boundingBox.min.y,
            boundingBox.max.z - boundingBox.min.z
          );
          console.log(maxDimension, 'maxDimension');
          scaleFactor = 2 / maxDimension * 0.95;
          mesh.scale.set(scaleFactor, scaleFactor, scaleFactor);
          boundingBox.getCenter(center).multiplyScalar(scaleFactor);
          mesh.position.sub(center);
        } else {
          console.error('Bounding box is empty.');
        }
        mesh.updateMatrixWorld(true);
        geometry.applyMatrix4(mesh.matrixWorld);
    }

单独针对每个几何体归一化处理 ​

js
    extension === 'glb' ? (this.originMesh = loaded.scene) : (this.originMesh = loaded;)

    normalizeAndVoxelize() {
        let scaleFactor = 1;
        const center = new THREE.Vector3();
        const boundingBox = new THREE.Box3().setFromObject(this.originMesh);
        if (!boundingBox.isEmpty()) {
        const maxDimension = Math.max(
            boundingBox.max.x - boundingBox.min.x,
            boundingBox.max.y - boundingBox.min.y,
            boundingBox.max.z - boundingBox.min.z
        );
        console.log(maxDimension,'maxDimension');
        scaleFactor = 2 / maxDimension * 0.95;
        this.originMesh.scale.set(scaleFactor, scaleFactor, scaleFactor);
        boundingBox.getCenter(center).multiplyScalar(scaleFactor);
        this.originMesh.position.sub(center);
        } else {
        console.error('Bounding box is empty.');
        }
        return { scaleFactor: scaleFactor, center: center };
    }

缩放 ​

在 Three.js 中,你可以使用 Vector3 类对向量进行缩放。以下是有关 Vector3 缩放的方法,包括如何独立缩放各个轴:

  1. 统一缩放 使用 multiplyScalar 方法,可以将向量的三个分量(x, y, z)按相同的比例缩放。 multiplyScalar(scalar: number): Vector3:将每个分量乘以指定的标量。
js
const vector = new THREE.Vector3(1, 2, 3);
const scaleFactor = 2;
vector.multiplyScalar(scaleFactor);
console.log(vector); // Vector3 { x: 2, y: 4, z: 6 }
  1. 非统一缩放 使用 multiply 方法,可以将向量的各个分量按不同的比例进行缩放。 multiply(v: Vector3): Vector3:将每个分量乘以另一个向量的对应分量。
js
const vector = new THREE.Vector3(1, 2, 3);
const scaleVector = new THREE.Vector3(2, 3, 4);
vector.multiply(scaleVector);
console.log(vector); // Vector3 { x: 2, y: 6, z: 12 }
  1. 手动设置每个分量 你也可以手动设置向量的每个分量来实现缩放。 set(x: number, y: number, z: number): Vector3:将向量的分量设置为指定的值。
js
const vector = new THREE.Vector3(1, 2, 3);
const scaleX = 2;
const scaleY = 3;
const scaleZ = 4;
vector.set(vector.x * scaleX, vector.y * scaleY, vector.z * scaleZ);
console.log(vector); // Vector3 { x: 2, y: 6, z: 12 }
  1. 使用 applyMatrix4 方法 对于更复杂的变换(包括缩放),你可以使用变换矩阵。 applyMatrix4(matrix: Matrix4): Vector3:将4x4变换矩阵应用到向量上。
js
const vector = new THREE.Vector3(1, 2, 3);
const matrix = new THREE.Matrix4();
matrix.makeScale(2, 3, 4);
vector.applyMatrix4(matrix);
console.log(vector); // Vector3 { x: 2, y: 6, z: 12 }

生成曲线、几何体 ​

手动生成圆弧顶点 ​

生成圆弧顶点数据 以坐标原点为中心,在XOY平面上生成圆弧上的顶点数据。

绘制圆弧线,本质就是绘制一个正n边形,n越大,圆弧细分数或者说精度越高。

通过for循环沿着圆弧线,通过三角函数计算顶点坐标,批量生成圆弧上顶点数据。

js
const geometry = new THREE.BufferGeometry(); //创建一个几何体对象
const R = 1; //圆弧半径
const N = 100; //分段数量
const sp = 2 * Math.PI / N;//两个相邻点间隔弧度
const arr = [];
const cx = 0;
const cy = 0;
for (let i = 0; i < N; i++) {
    const angle = sp * i;//当前点弧度 以(cx,cy)为中心,在XOY平面上生成圆弧上的顶点数据 
    const x = cx + R * Math.cos(angle);
    const y = cy + R * Math.sin(angle); 
    arr.push(x, y, 0);
}
const vertices = new Float32Array(arr); // 创建属性缓冲区对象 //3个为一组,表示一个顶点的xyz坐标 
const attribue = new THREE.BufferAttribute(vertices, 3); // 设置几何体attributes属性的位置属性
geometry.attributes.position = attribue; 
const material = new THREE.LineBasicMaterial({ // 线材质  
    color: 0xffffff 
}); // 创建线模型对象 构造函数:Line、LineLoop、LineSegments  
// const line=new THREE.Line(geometry, material); 
const line = new THREE.LineLoop(geometry, material);//线条模型对象
this.scene.add(line);

几何体方法.setFromPoints() .setFromPoints()是几何体BufferGeometry的一个方法,通过该方法可以把数组pointsArr中坐标数据提取出来赋值给几何体。具体说就是把pointsArr里面坐标数据提取出来,赋值给geometry.attributes.position属性

js
const pointsArr = [
    // 三维向量Vector3表示的坐标值
    new THREE.Vector3(0,0,0),
    new THREE.Vector3(0,100,0),
    new THREE.Vector3(0,100,100),
    new THREE.Vector3(0,0,100),
];
geometry.setFromPoints(pointsArr);

利用曲线Curve生成圆弧 ​

Curve是Three.js中用于生成曲线的基类,Curve3是Curve的子类,用于生成3维曲线。Curve2是Curve的子类,用于生成2维曲线。 曲线父类Curve:

  • 2D:
    • 直线:LineCurve
    • 圆弧:ArcCurve
    • 椭圆:EllipseCurve
    • 二维样条曲线:SplineCurve
    • 二次贝塞尔曲线:QuadraticBezierCurve
    • 三次贝塞尔曲线:CubicBezierCurve
  • 3D:
    • 直线:LineCurve3
    • 三维样条曲线:CatmullRomCurve3
    • 三维二次贝塞尔曲线:QuadraticBezierCurve3
    • 三维三次贝塞尔曲线:CubicBezierCurve3

曲线Curve方法.getPoints() && .getSpacedPoints()

js
const arc = new  THREE.EllipseCurve(0,0,4,3)  

//.getPoints()会考虑曲线斜率变化,斜率变化快的位置返回的顶点更密集
const points = arc.getPoints(50) //将曲线划分为的分段数 返回51个点
//.getSpacedPoints()按照曲线长度等间距返回顶点数据
const points = arc.getSpacedPoints(50) //将曲线划分为的分段数 返回50个点

const geometry = new THREE.BufferGeometry().setFromPoints(points)
const material = new THREE.LineBasicMaterial({ color: 0xffffff });
const mesh = new THREE.Line(geometry, material);
this.scene.add(mesh);

ArcCurve弧线

js
EllipseCurve( aX, aY, xRadius,yRadius, aStartAngle, aEndAngle, aClockwise )
参数描述
aX, aY椭圆中心点坐标
xRadius, yRadius椭圆半径
aStartAngle, aEndAngle弧线起始弧度,弧线结束弧度
aClockwise弧线是顺时针还是逆时针 默认值为false
js
// 完整圆弧
const arc = new THREE.ArcCurve(0, 0, 100, 0, 2 * Math.PI);
// 半圆弧
const arc = new THREE.ArcCurve(0, 0, 100, 0, Math.PI);
// 四分之一圆弧
const arc = new THREE.ArcCurve(0, 0, 100, 0, Math.PI/2);

样条、贝塞尔曲线应用 ​

  • 注意:
    • 样条曲线new THREE.CatmullRomCurve3(arr) 接受的是一个点数组
    • 贝塞尔曲线new THREE.QuadraticBezierCurve3(p1, p2, p3) 接受的是三个点

三维样条曲线CatmullRomCurve3实现飞线轨迹 下面曲线的起始点设置在XOZ平面上,y方向为曲线高度方向。

js
// p1、p3轨迹线起始点坐标
const p1 = new THREE.Vector3(-100, 0, -100);
const p3 = new THREE.Vector3(100, 0, 100);
// 计算p1和p3的中点坐标
const x2 = (p1.x + p3.x)/2;
const z2 = (p1.z + p3.z)/2;
const h = 50;
const p2 = new THREE.Vector3(x2, h, z2);

const arr = [p1, p2, p3];
// 三维样条曲线
const curve = new THREE.CatmullRomCurve3(arr);

三维二次贝赛尔曲线QuadraticBezierCurve3实现飞线轨迹

js
// p1、p3轨迹线起始点坐标
const p1 = new THREE.Vector3(-100, 0, -100);
const p3 = new THREE.Vector3(100, 0, 100);
// 计算p1和p3的中点坐标
const x2 = (p1.x + p3.x)/2;
const z2 = (p1.z + p3.z)/2;
const h = 100;
const p2 = new THREE.Vector3(x2, h, z2);
// 三维二次贝赛尔曲线
const curve = new THREE.QuadraticBezierCurve3(p1, p2, p3);

组合曲线CurvePath拼接曲线 ​

整体思路很简单,LineCurve创建两条直线线段,ArcCurve绘制一段圆弧线,然后把两段直线和一段圆弧线,通过组合曲线的CurvePath.curves属性拼接起来。

js
const R = 80;//圆弧半径
const H = 200;//直线部分高度
// 直线1
const line1 = new THREE.LineCurve(new THREE.Vector2(R, H), new THREE.Vector2(R, 0));
// 圆弧
const arc = new THREE.ArcCurve(0, 0, R, 0, Math.PI, true);
// 直线2
const line2 = new THREE.LineCurve(new THREE.Vector2(-R, 0), new THREE.Vector2(-R, H));

// CurvePath创建一个组合曲线对象
const CurvePath = new THREE.CurvePath();
//line1, arc, line2拼接出来一个U型轮廓曲线,注意顺序
CurvePath.curves.push(line1, arc, line2);

曲线几何体 ​

曲线路径管道TubeGeometry ​

js
TubeGeometry(path, tubularSegments, radius, radiusSegments, closed)
参数描述
path扫描路径,路径要用三维曲线
tubularSegments路径方向细分数,默认64
radius管道半径,默认1
radiusSegments管道圆弧细分数,默认8数
closed是否闭合管道

旋转成型LatheGeometry ​

js
LatheGeometry(points, segments, phiStart, phiLength)
参数描述
pointsVector2表示的坐标数据组成的数组
segments扫描路径方向细分数,默认12
phiStart扫描路径起始弧度,默认0
phiLength扫描路径结束弧度,默认2*Math.PI

轮廓填充ShapeGeometry ​

js
ShapeGeometry(shapes, curveSegments)
参数描述
shapesShape对象组成的数组
curveSegments扫描路径方向细分数,默认12

拉伸/扫描成型ExtrudeGeometry ​

js
ExtrudeGeometry(shapes, options)
参数描述
shapes形状或者一个包含形状的数组
options包含有下列参数的对象
depthfloat,挤出的形状的深度,默认值为1。
bevelEnabledboolean,是否开启斜角,默认值为true。
bevelThicknessfloat,斜角的深度,默认值为0.2。
bevelSizefloat,斜角的尺寸,默认值为0.1。
bevelSegmentsint,斜角的分段数,默认值为3。
extrudePathTHREE.Curve对象,扫描路径,默认值为null。
stepsint,扫描路径细分数,默认值为1。

多边形轮廓Shape ​

参数描述
.currentPoint属性Vector2对象,当前点,默认值为Vector2(0,0)。
.holes属性Vector2对象组成的数组,用来设置多边形轮廓的洞。
.moveTo()方法设置当前点,参数为Vector2对象。
.lineTo()方法绘制直线,参数为Vector2对象。
.absarc()方法绘制绝对圆弧,参数为x, y, radius, startAngle, endAngle, clockwise。
.arc()方法绘制圆弧,参数为x, y, radius, startAngle, endAngle, clockwise。 注意圆心坐标是相对当前.currentPoint而言,而不是坐标原点

绘制一个矩形轮廓Shape

js
const shape = new THREE.Shape();
shape.moveTo(10, 0); //.currentPoint变为(10,0)
// 绘制直线线段,起点(10,0),结束点(100,0)
shape.lineTo(100, 0);//.currentPoint变为(100, 0)
shape.lineTo(100, 100);//.currentPoint变为(100, 100)
shape.lineTo(10, 100);//.currentPoint变为(10, 100)

模型边界线EdgesGeometry ​

js
EdgesGeometry(geometry , thresholdAngle)
参数描述
geometryTHREE.BufferGeometry对象
thresholdAnglefloat,仅当相邻面的法线之间的角度(单位为角度)超过这个值时,才会渲染边缘。默认值为1。

Color 颜色类 ​

Color颜色渐变插值 ​

.lerpColors() ​

执行.lerpColors(Color1,Color2, percent)通过一个百分比参数percent,可以控制Color1和Color2两种颜色混合的百分比,Color1对应1-percent,Color2对应percent。

js
const c1 = new THREE.Color(0xff0000); //红色
const c2 = new THREE.Color(0x0000ff); //蓝色
const c = new THREE.Color();
c.lerpColors(c1,c2, 0);
console.log('颜色插值结果',c); //颜色插值结果,和c1一样rgb(1,0,0),100% c1 + 0% c2混合

.lerp() ​

c1与c2颜色混合,混合后的rgb值,赋值给c1的.r、.g、.b属性。

js
const c1 = new THREE.Color(0xff0000); //红色
const c2 = new THREE.Color(0x0000ff); //蓝色
c1.lerp(c2, percent);

Color颜色插值应用

js
// 根据顶点距离起点远近进行颜色插值计算
const c1 = new THREE.Color(0x00ffff); //曲线起点颜色 青色
const c2 = new THREE.Color(0xffff00); //曲线结束点颜色 黄色
for (let i = 0; i < count; i++) {
    const percent = i / count; //点索引值相对所有点数量的百分比
    //根据顶点位置顺序大小设置颜色渐变
    const c = c1.clone().lerp(c2, percent);//颜色插值计算
    colorsArr.push(c.r, c.g, c.b); 
}

相机基础 ​

正投影相机 ​

一句话描述,正投影相机OrthographicCamera和透视投影相机PerspectiveCamera的区别就是,透视投影可以模拟人眼观察世界的视觉效果,正投影相机不会。 正投影相机一般用于2D渲染,比如平面地图。

js
OrthographicCamera( left, right, top, bottom, near, far )

// 正投影相机
const width = window.innerWidth; //canvas画布宽度
const height = window.innerHeight; //canvas画布高度
const k = width / height; //canvas画布宽高比
const s = 600;//控制left, right, top, bottom范围大小
const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 8000);
参数描述
leftfloat,相机的左视图。
rightfloat,相机的右视图。
topfloat,相机的上视图。
bottomfloat,相机的下视图。
nearfloat,相机的近裁剪面。
farfloat,相机的远裁剪面。

正投影相机OrthographicCamera的left、 right属性受到canvas画布宽高比影响,所以需要随着canvas画布更新。

js
// Canvas画布跟随窗口变化
window.onresize = function () {
    const width = window.innerWidth; //canvas画布宽度
    const height = window.innerHeight; //canvas画布高度
    // 1. WebGL渲染器渲染的Cnavas画布尺寸更新
    renderer.setSize(width, height);
    // 2.1.更新相机参数
    const k = width / height; //canvas画布宽高比
    camera.left = -s*k;
    camera.right = s*k;
    // 2.2.相机的left, right, top, bottom属性变化了,通知threejs系统
    camera.updateProjectionMatrix();
};

包围盒Box3(min:Vector3, max:Vector3) ​

包围盒尺寸.getSize() 返回包围盒具体的长宽高尺寸

js
const scale = new THREE.Vector3()
// getSize()计算包围盒尺寸
// 获得包围盒长宽高尺寸,结果保存在参数三维向量对象scale中
box3.getSize(scale)
console.log('模型包围盒尺寸', scale);

包围盒几何中心.getCenter() Box3方法.getCenter()计算返回包围盒几何中心

js
// 计算包围盒中心坐标
const center = new THREE.Vector3()
box3.getCenter(center)
console.log('模型中心坐标', center);

.expandByObject 和 .setFromObject 是 Three.js 中 Box3 类的两个方法,它们在使用上有一些重要的区别:

  1. .setFromObject(object)
    • 这个方法会重置包围盒,然后根据给定的对象及其所有后代设置新的包围盒。
    • 它会清除现有的包围盒数据,完全基于给定对象重新计算。
    • 通常用于初始化包围盒或者完全重置包围盒。
  2. .expandByObject(object)
    • 这个方法会扩展现有的包围盒以包含给定的对象及其所有后代。
    • 它不会重置现有的包围盒,而是在现有基础上进行扩展。
    • 通常用于逐步添加对象到现有的包围盒中。

光源和阴影 ​

平行光阴影计算 ​

平行光阴影相机属性.shadow.camera(阴影渲染范围) 平行光DirectionalLight的.shadow属性是平行光阴影对象DirectionalLightShadow,平行光阴影对象有一个相机属性shadow.camera,属性值是一个正投影相机对象OrthographicCamera。所以.shadow.camera属性的用法可以参考前面讲解过的正投影相机OrthographicCamera。

平行光DirectionalLight阴影步骤

  1. .castShadow设置产生阴影的模型对象
  2. .castShadow设置产生阴影的光源对象
  3. .receiveShadow设置接收阴影效果的模型
  4. .shadowMap.enabledWebGl渲染器允许阴影渲染
  5. .shadow.camera设置光源阴影渲染范围
js
// 设置产生投影的网格模型
mesh.castShadow = true;
// 平行光设置产生阴影的光源对象,开启光源阴影的计算功能
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.castShadow = true;
// 设置接收阴影的投影面
planeMesh.receiveShadow = true;
// 设置渲染器,允许光源阴影渲染
renderer.shadowMap.enabled = true;  
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 或者 THREE.PCFShadowMap

directionalLight.shadow.mapSize.width = 2048; // 分辨率
directionalLight.shadow.mapSize.height = 2048;

directionalLight.shadow.radius = 3; //阴影边缘逐渐弱化或模糊化

// 设置三维场景计算阴影的范围
directionalLight.shadow.camera.left = -50;
directionalLight.shadow.camera.right = 50;
directionalLight.shadow.camera.top = 200;
directionalLight.shadow.camera.bottom = -100;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 600;

// 更新投影相机
directionalLight.shadow.camera.updateProjectionMatrix();

// 设置偏移
directionalLight.shadow.bias = -0.0001;

CameraHelper可视化.shadow.camera

js
// 可视化平行光阴影对应的正投影相机对象
const cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
scene.add(cameraHelper);
//平行光阴影相机的位置.shadow.camera.position默认就是平行光的位置属性directionalLight.position。
console.log('shadow.camera位置',directionalLight.shadow.camera.position);

注意事项

  1. 阴影贴图分辨率:
    • 更高的分辨率可以产生更清晰的阴影,但也会影响性能。
    • 如果发现阴影模糊或有锯齿,可以尝试增加分辨率。
  2. 投影范围:
    • 设置投影相机的范围时要考虑到场景的大小,以确保所有的阴影都被正确捕捉。
    • 如果投影范围设置得过小,可能会导致某些阴影丢失。
    • 如果设置得过大,则可能导致阴影细节丢失。
  3. 阴影偏移:
    • 设置正确的偏移值可以减少自阴影问题,即物体自身对其表面产生阴影的情况。
    • 偏移值通常很小,例如 -0.0001,以避免物体穿透自身的阴影。
  4. 阴影过滤:
    • 可以使用 PCF (Percentage Closer Filtering) 或者 PCF Soft 来改善阴影边缘的质量。
    • 通过设置 THREE.Light.prototype.shadow.mapNeedsUpdate 为 true 来启用。
  5. 性能考虑:
    • 大量使用阴影可能会降低渲染性能,尤其是在移动设备上。
    • 如果发现性能瓶颈,可以考虑减少阴影贴图的分辨率,或者只对某些重要的光源启用阴影。

精灵模型Sprite ​

创建精灵模型Sprite ​

创建精灵模型对象Sprite和创建网格模型对象一样需要创建一个材质对象,不同的地方在于创建精灵模型对象不需要创建几何体对象Geometry。

js
// 创建精灵材质对象SpriteMaterial
const spriteMaterial = new THREE.SpriteMaterial({color:0x00ffff});
// 创建精灵模型对象,不需要几何体geometry参数
const sprite = new THREE.Sprite(spriteMaterial);
  • Sprite形状是矩形,PlaneGeometry创建的网格模型Mesh的形状也是矩形。
  • 通过相机控件OrbitControls旋转测试,Sprite矩形平面会始终平行于Canvas画布或者说屏幕
  • sprite的大小默认是111,可以通过scale来控制,sprite.scale只需要设置x、y两个分量就可以,z方向默认值就行。

精灵模型标注场景(贴图) ​

精灵材质对象SpriteMaterial和普通的网格材质一样具有颜色贴图.map、开启透明.transparent、透明度.opacity等属性。

js
const texture = new THREE.TextureLoader().load("./光点.png");
const spriteMaterial = new THREE.SpriteMaterial({
  map: texture, //设置精灵纹理贴图
});
const spriteMaterial = new THREE.SpriteMaterial({
  transparent:true,//SpriteMaterial默认是true
});

Sprite模拟下雨、下雪 ​

js
create(){
    const spriteMaterial = new THREE.SpriteMaterial({ color: 0x00ffff });
    this.sgroup = new THREE.Group();
    for (let i = 0; i < 16000; i++) {
        // 精灵模型共享材质
        const sprite = new THREE.Sprite(spriteMaterial);
        this.sgroup.add(sprite);
        sprite.scale.set(1, 1, 1);
        // 设置精灵模型位置,在长方体空间上上随机分布
        const x = 1000 * (Math.random() - 0.5);
        const y = 600 * Math.random();
        const z = 1000 * (Math.random() - 0.5);
        sprite.position.set(x, y, z)
    }
    this.scene.add(this.sgroup);
    this.loop()
}

loop() {
    // loop()每次执行都会更新雨滴的位置,进而产生动画效果
    this.sgroup.children.forEach(sprite => {
        // 雨滴的y坐标每次减1
        sprite.position.y -= 1;
        if (sprite.position.y < -20) {
        // 如果雨滴落到地面,重置y,从新下落
        sprite.position.y = 600;
        }
    });
    requestAnimationFrame(this.loop.bind(this));
}

相机镜头附近的雨滴偏大

js
//near调整大一些,避免距离相机非常近的雨滴渲染非常大的现象。
const camera = new THREE.PerspectiveCamera(30, width / height, 50, 3000);

后处理EffectComposer ​

发光描边OutlinePass ​

引入后处理扩展库EffectComposer.js three.js WebGL渲染器执行渲染方法.render()会得到一张图像,如果你需要对一个webgl渲染器的渲染结果进行后期处理,就把它作为EffectComposer的参数。

js
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';

// 创建后处理对象EffectComposer,WebGL渲染器作为参数
const composer = new EffectComposer(renderer);

渲染器通道RenderPass 渲染器通道RenderPass的作用是指定后处理对应的相机camera和场景scene。

js
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
// 创建一个渲染器通道,场景和相机作为参数
const renderPass = new RenderPass(scene, camera);

// 设置renderPass通道
composer.addPass(renderPass);

OutlinePass通道

js
import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js';

// OutlinePass第一个参数v2的尺寸和canvas画布保持一致
const v2 = new THREE.Vector2(window.innerWidth, window.innerHeight);
// const v2 = new THREE.Vector2(800, 600);
const outlinePass = new OutlinePass(v2, scene, camera);
// 一个模型对象
outlinePass.selectedObjects = [mesh];
// 多个模型对象
outlinePass.selectedObjects = [mesh1,mesh2,group];
composer.addPass(outlinePass);

渲染循环执行EffectComposer.render() 渲染循环中后处理EffectComposer执行.render(),会调用webgl渲染器执行.render(),也就是说renderer.render(scene, camera)不用再执行。

js
// 渲染循环
function render() {
    composer.render();
    // renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

gltf后处理颜色异常(伽马校正) ​

如果你使用threejs后处理功能EffectComposer,renderer.outputEncoding会无效,自然会出现颜色偏差。

js
import {GammaCorrectionShader} from 'three/addons/shaders/GammaCorrectionShader.js';
// ShaderPass功能:使用后处理Shader创建后处理通道
import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
// 创建伽马校正通道
const gammaPass= new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);

注意: 从 Three.js r130 版本开始,引入了新的色彩管理系统。这个系统默认使用线性色彩空间进行所有的光照计算,然后在最终输出到显示器时进行适当的伽马校正。 考虑到这些改进,在大多数情况下,你不再需要使用 GammaCorrectionShader 来解决 GLTF 模型的颜色偏差问题。然而,还是有一些注意事项:

  1. 确保正确设置
js
renderer.outputEncoding = THREE.sRGBEncoding;
  1. 如果你的场景中混合了不同来源的内容(例如,一些是 GLTF 模型,一些是手动创建的几何体),可能需要手动设置一些材质的编码:
js
material.encoding = THREE.sRGBEncoding;
  1. 对于纹理,确保设置:
js
texture.encoding = THREE.sRGBEncoding;
  1. 如果使用后处理,确保在 EffectComposer 的最后添加 OutputPass:
js
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';

const outputPass = new OutputPass();
composer.addPass(outputPass);
  1. 在极少数情况下,如果你发现颜色仍然不正确,可以尝试手动应用伽马校正:
js
renderer.outputEncoding = THREE.LinearEncoding;
const gammaCorrectionPass = new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaCorrectionPass);

数学库-射线 ​

射线Ray(origin:Vector3,direction:Vector3) ​

基础属性方法 ​

射线起点.origin

js
ray.origin = new THREE.Vector3(1,0,3);
ray.origin.set(1, 0, 3);

射线方向.direction 射线Ray的方向.direction通用用一个三维向量Vector3表示,向量长度保证为1,也就是单位向量。

js
ray.direction = new THREE.Vector3(0,0,-1);
ray.direction = new THREE.Vector3(5,0,0).normalize();

.intersectTriangle()方法 计算一个射线和一个三角形在3D空间中是否交叉。 执行.intersectTriangle()方法,如果相交返回交点坐标,不相交返回空值null。

js
// 三角形三个点坐标
const p1 = new THREE.Vector3(100, 25, 0);
const p2 = new THREE.Vector3(100, -25, 25);
const p3 = new THREE.Vector3(100, -25, -25);
const point = new THREE.Vector3();//用来记录射线和三角形的交叉点
// `.intersectTriangle()`计算射线和三角形是否相交叉,相交返回交点,不相交返回null
const result = ray.intersectTriangle(p1,p2,p3,false,point);
console.log('交叉点坐标', point);
console.log('查看是否相交', result);

.intersectTriangle()参数4表示是否进行背面剔除,设为true,表示进行背面剔除,虽然从几何空间上讲,该案例源码射线和三角形虽然交叉,但在threejs中,三角形背面对着射线,视为交叉无效,进行背面剔除,返回值r是null。

Raycaster(射线拾取模型) ​

.intersectObjects(objects:Array,recursive:Boolean,optionalTarget:Array) ​

  • objects —— 检测和射线相交的一组物体。
  • recursive —— 若为true,则同时也会检测所有物体的后代。否则将只会检测对象本身的相交部分。默认值为true。
  • optionalTarget —— (可选)设置结果的目标数组。如果不设置这个值,则一个新的Array会被实例化;如果设置了这个值,则在每次调用之前必须清空这个数组(例如:array.length = 0;)。

返回结果时,相交部分将按距离进行排序,最近的位于第一个。该方法返回一个包含有交叉部分的数组:

.setFromCamera(coords:Vector2,camera:Camera) ​

  • coords —— 在标准化设备坐标中鼠标的二维坐标 —— X分量与Y分量应当在-1到1之间。
  • camera —— 射线所来源的摄像机。

鼠标点击选中模型 ​

threejs canvas画布上一个点,可以用.offsetX、.offsetY绝对值表示,同样也可以用标准设备坐标系去表达。

屏幕坐标转标准设备坐标

js
// 坐标转化公式
addEventListener('click',function(event){
    const px = event.offsetX;
    const py = event.offsetY;
    //屏幕坐标px、py转标准设备坐标x、y
    //width、height表示canvas画布宽高度
    const x = (px / width) * 2 - 1;
    const y = -(py / height) * 2 + 1;
})

拾取模型

js
  const raycaster = new THREE.Raycaster();
    // 形象点说就是在点击位置创建一条射线,射线穿过的模型代表选中
    raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
    const intersects = raycaster.intersectObjects(mainScene.scene.children, true);
    if (intersects.length > 0) {
    const intersect = intersects[0];
    const object = intersect.object;
    if (object.type === 'Mesh') {
        object.material = new THREE.MeshPhongMaterial({ color: Math.random() * 0xffffff });
    }
    }

拾取层级模型

js
const cunchu = model.getObjectByName('存储罐');
// 射线拾取模型对象(包含多个Mesh)
// 可以给待选对象的所有子孙后代Mesh,设置一个祖先属性ancestors,值指向祖先(待选对象)    
for (let i = 0; i < cunchu.children.length; i++) {
    const group = cunchu.children[i];
    //递归遍历chooseObj,并给chooseObj的所有子孙后代设置一个ancestors属性指向自己
    group.traverse(function (obj) {
        if (obj.isMesh) {
            obj.ancestors = group;
        }
    })
}
// 射线交叉计算拾取模型
const intersects = raycaster.intersectObjects(cunchu.children);
console.log('intersects', intersects);
if (intersects.length > 0) {
    // 通过.ancestors属性判断那个模型对象被选中了
    outlinePass.selectedObjects = [intersects[0].object.ancestors];
}

射线拾取Sprite控制场景

js
//自定义一个方法.change()。
sprite.change = function(){
  mesh.material.color.set(0xffffff);
}
addEventListener('click', function (event) {
    ...
    // 射线交叉计算拾取精灵模型
    const intersects = raycaster.intersectObjects([sprite,sprite2]);
    if (intersects.length > 0) {
        intersects[0].object.change();//执行选中sprite绑定的change函数
    }
})

场景标注标签信息 ​

CSS2DRenderer(HTML标签) ​

CSS2模型对象CSS2DObject:可以把一个HTML元素转化为一个CSS2模型对象CSS2DObject,然后添加到场景中。这个对象可以被附加到任何 Object3D 实例上,包括网格、光线等。它会根据元素在三维空间中的位置、旋转和缩放自动调整 HTML 元素的位置。

CSS2DRenderer:负责渲染 CSS2DObject。它会在每次渲染 Three.js 场景时更新这些对象的位置。

js
// 1.创建CSS2DRenderer
this.cssRenderer = new CSS2DRenderer();
this.cssRenderer.setSize(this.renderTarget.clientWidth, this.renderTarget.clientHeight);
this.cssRenderer.domElement.style.position = 'absolute';
this.cssRenderer.domElement.style.zIndex = 1;
this.cssRenderer.domElement.style.top = '0px';
this.cssRenderer.domElement.style.left = '0px';
this.cssRenderer.domElement.style.pointerEvents = 'none';
this.renderTarget.appendChild(this.cssRenderer.domElement);

// 2.把对应的HTML元素转化为CSS2模型对象CSS2DObject,并添加给相关Mesh/Sence
for (let i = 0; i < this.modoleG.children.length; i++) {
    const tag = document.querySelector('.biaoqian').cloneNode(true);
    const CSS2D = new CSS2DObject(tag);
    const tags = CSS2D;
    this.modoleG.children[i].add(tags);
    tags.position.set(0, 0, 0);
}

// 3.持续render
 this.cssRenderer.render(this.scene, this.camera)

 //4. resize
 onWindowResize(){
    this.camera.aspect = this.renderTarget.clientWidth / this.renderTarget.clientHeight;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(this.renderTarget.clientWidth, this.renderTarget.clientHeight);
    this.renderer.render(this.scene, this.camera);

    this.cssRenderer.setSize(this.renderTarget.clientWidth, this.renderTarget.clientHeight);
    this.cssRenderer.render(this.scene, this.camera);  
 }

CSS3DObject && CSS3DRenderer

  1. CSS3DObject会同步旋转、缩放
  2. CSS2渲染的标签默认保持自身像素值,CSS3DRenderer渲染的HTML元素尺寸值会换算成mesh里面的size
  3. 禁止CSS3DObject标签对应HTMl元素背面显示:backface-visibility: hidden

关键帧动画 ​

AnimationClip ​

使用步骤

  1. 创建关键帧动画AnimationClip
js
// 给需要设置关键帧动画的模型命名
mesh.name = "Box";
const times = [0, 3, 6]; //时间轴上,设置三个时刻0、3、6秒
// times中三个不同时间点,物体分别对应values中的三个xyz坐标
const values = [0, 0, 0, 100, 0, 0, 0, 0, 100];
// 0~3秒,物体从(0,0,0)逐渐移动到(100,0,0),3~6秒逐渐从(100,0,0)移动到(0,0,100)
const posKF = new THREE.KeyframeTrack('Box.position', times, values);
// 从2秒到5秒,物体从红色逐渐变化为蓝色
const colorKF = new THREE.KeyframeTrack('Box.material.color', [2, 5], [1, 0, 0, 0, 0, 1]);
// 1.3 基于关键帧数据,创建一个clip关键帧动画对象,命名"test",持续时间6秒。
const clip = new THREE.AnimationClip("test", 6, [posKF, colorKF]);
  1. 播放关键帧动画AnimationClip
js
//传入包含关键帧动画的mesh/scene创建一个播放器mixer
this.mixer = new THREE.AnimationMixer(mesh);
//AnimationMixer的`.clipAction()`返回一个AnimationAction对象
const clipAction = this.mixer.clipAction(clip); 
//.play()控制动画播放,默认循环播放
clipAction.play();
  1. 更新播放器AnimationMixer时间
js
this.clock = new THREE.Clock();
render() {
    requestAnimationFrame(this.render.bind(this)); // 递归调用
    this.Stats.update()
    this.controls.update(); //update()函数内会执行camera.lookAt(controls.target)
    // this.composer.render();
    this.renderer.render(this.scene, this.camera); // 渲染场景,类似于拍照

    this.cssRenderer.render(this.scene, this.camera)
    const frameT = this.clock.getDelta();
    // 更新播放器相关的时间
    this.mixer.update(frameT);
}

动画播放(暂停、倍速、循环)

  • AnimationAction的循环属性.loop
js
const clipAction = mixer.clipAction(clip);
//.play()控制动画播放,默认循环播放
clipAction.play();
//不循环播放
clipAction.loop = THREE.LoopOnce; // THREE.LoopRepeat  THREE.LoopPingPong
  • AnimationAction的.clampWhenFinished属性
js
//不循环播放,执行一次后默认回到动画开头
clipAction.loop = THREE.LoopOnce; 
// 物体状态停留在动画结束的时候
clipAction.clampWhenFinished = true;
  • 停止结束动画.stop():不是暂停,是动画彻底终止,回到初始状态。
js
//动画停止结束,回到开始状态
clipAction.stop();
  • 是否暂停播放.paused
js
const bu = document.getElementById('bu');
bu.addEventListener('click',function(){
    // AnimationAction.paused默认值false,设置为true,可以临时暂停动画
    if (clipAction.paused) {//暂停状态
        clipAction.paused = false;//切换为播放状态
        bu.innerHTML='暂停';// 如果改变为播放状态,按钮文字设置为“暂停”
      } else {//播放状态
        clipAction.paused = true;//切换为暂停状态
        bu.innerHTML='继续';// 如果改变为暂停状态,按钮文字设置为“继续”
      }
})
  • 倍速播放.timeScale
js
clipAction.timeScale = 1;//默认
clipAction.timeScale = 2;//2倍速
  • 控制动画播放特定时间段
js
//AnimationAction设置开始播放时间:从1秒时刻对应动画开始播放
clipAction.time = 1; 
//AnimationClip设置播放结束时间:到5秒时刻对应的动画状态停止
clip.duration = 5;

动画库tweenjs ​

基本使用 ​

js
//创建一段mesh平移的动画
const tween = new TWEEN.Tween(mesh.position);
//经过2000毫秒,pos对象的x和y属性分别从零变化为100、50
tween.to({x: 100,y: 50}, 2000);
//tween动画开始执行
tween.start(); 

// 渲染循环
function render() {
    TWEEN.update();
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

twwenjs库提供了onStart、onUpdate、onComplete等用于控制动画执行的回调函数。

  • onStart:动画开始执行时触发
  • onUpdate:动画执行过程中,一直被调用执行
  • onComplete:动画执行完毕时触发

封装一个相机动画函数 ​

js
// 相机动画函数,从A点飞行到B点,A点表示相机当前所处状态
// pos: 三维向量Vector3,表示动画结束相机位置
// target: 三维向量Vector3,表示相机动画结束lookAt指向的目标观察点
function createCameraTween(endPos,endTarget){
    new TWEEN.Tween({
        // 不管相机此刻处于什么状态,直接读取当前的位置和目标观察点
        x: camera.position.x,
        y: camera.position.y,
        z: camera.position.z,
        tx: controls.target.x,
        ty: controls.target.y,
        tz: controls.target.z,
    })
    .to({
        // 动画结束相机位置坐标
        x: endPos.x,
        y: endPos.y,
        z: endPos.z,
        // 动画结束相机指向的目标观察点
        tx: endTarget.x,
        ty: endTarget.y,
        tz: endTarget.z,
    }, 2000)
    .onUpdate(function (obj) {
        // 动态改变相机位置
        camera.position.set(obj.x, obj.y, obj.z);
        // 动态计算相机视线
        // camera.lookAt(obj.tx, obj.ty, obj.tz);
        controls.target.set(obj.tx, obj.ty, obj.tz);//有了这个就不用camera.lookAt
        controls.update();//内部会执行.lookAt()
    })
    .start();
}

缓动算法.easing ​

easing函数和easing类型都有多种方式,可以自由组合使用(Linear除外)。

js
// 动画开始缓动方式(类比加速启动)
tween.easing(TWEEN.Easing.Sinusoidal.In);
// 动画结束缓动方式(类比减速刹车)
tween.easing(TWEEN.Easing.Sinusoidal.Out);
// 同时设置In和Out
tween.easing(TWEEN.Easing.Sinusoidal.InOut);

模型淡入淡出 ​

js
// 模型淡入
material.transparent = true;//开启透明计算
material.opacity = 0.0;//完全透明

// new TWEEN.Tween(material)
new TWEEN.Tween({opacity:material.opacity})
.to({opacity:1.0}, 3000)
.onUpdate(function(obj){
    material.opacity = obj.opacity
})
.onComplete(function(){
    //动画结束:关闭允许透明,恢复到模型原来状态
    material.transparent = false;
})
.start();

// 模型淡出
// new TWEEN.Tween(material)
new TWEEN.Tween({opacity:material.opacity})
.to({opacity:0.0}, 3000)
.onStart(function(){
    //动画开始:允许透明opacity属性才能生效
    material.transparent = true;
})
.onUpdate(function(obj){
    material.opacity = obj.opacity
})
.start();

数学几何计算 ​

Vector3 ​

方法描述
A.add(B)向量A与向量B相加,结果保存在向量A中
A.sub(B)向量A与向量B相减,结果保存在向量A中
A.multiplyScalar(B)向量A与"标量B"相乘,结果保存在向量A中
A.divideScalar(B)向量A与"标量B"相除,结果保存在向量A中
A.dot(B)向量A与向量B的点积,结果保存在向量A中
A.cross(B)向量A与向量B的叉积,结果保存在向量A中
A.length()向量A的长度(模长)
A.distanceTo(B)向量A与向量B的距离
A.angleTo(B)向量A与向量B的夹角
A.normalize()向量A归一化,长度为1
A.set(x, y, z)设置向量A的x、y、z坐标
C.addVectors(A, B)向量A与向量B相加,结果保存在向量C中
C.subVectors(A, B)向量A与向量B相减,结果保存在向量C中
mesh.position.copy(A)将向量A复制到mesh的position属性中

向量点乘 叉乘 ​

计算向量夹角余弦值

js
const cos =  a.clone().normalize().dot(b.clone().normalize());

//反余弦计算向量夹角弧度
const rad = Math.acos(cos);
const angle = THREE.MathUtils.radToDeg(rad); //弧度转角度

点乘判断物体再人前还是人后

js
// a向量:人的正前方
const a = new THREE.Vector3(0,0,-1);
// 人指向物体的向量
const b = mesh.position.clone().sub(person.position);
const dot = a.dot(b);//向量a和b点乘
if (dot > 0) {
    console.log('物体在人前面');
} else if (dot < 0) {
    console.log('物体在人后面');
}

点乘判断是否在扇形内

js
// 已知数据
person.position.set(0, 0, 2);//人位置
mesh.position.set(2, 0, -3);//物体位置
// a向量:人的正前方沿着z轴负半轴
const a = new THREE.Vector3(0, 0, -1);
// 扇形范围
const R = 20;//人前方扇形半径
const angle = 60;//人前方扇形角度
// 物体坐标减去人坐标,创建一个人指向物体的向量
const b = mesh.position.clone().sub(person.position);
const L = b.length();//物体与人的距离
if (L < R) {//物体与人的距离在半径R以内
    console.log('物体与人距离小于扇形半径');
}else{
    console.log('不在扇形区域内');
}

点乘判断平行向量方向异同

js
const a = new THREE.Vector3(10, 10, 0);
const b = new THREE.Vector3(20, 0, 0);
// a、b向量归一化后点乘
// a、b向量归一化后点乘
const cos =  a.normalize().dot(b.normalize());

//注意判断的前提是,两个向量是平行的,余弦值要么1,要么-1
if(cos>0.5){
    console.log('方向相同');
}else{
    console.log('方向相反');
}

叉乘判断物体在人左右

js
const c = a.clone().cross(b);
c.normalize()
if (c.y > 0) {
    console.log('物体在人左边');
} else if (c.y < 0) {
    console.log('物体在人右边')
}

判断两个点是否在线段同一侧

js
// 已知条件
// 一条线段两点坐标A、B
const A = new THREE.Vector3(0, 0, 10);
const B = new THREE.Vector3(100, 0, 10);

// 判断p1、p2两点位于线段AB同一侧,还是异侧
const p1 = new THREE.Vector3(20, 0, 40);
const p2 = new THREE.Vector3(80, 0, 40);//与p1同侧

// p1分别向线段AB两点创建两条向量a1、b1
const a1 = A.clone().sub(p1);
const b1 = B.clone().sub(p1);
// p2分别向线段AB两点创建两条向量a2、b2
const a2 = A.clone().sub(p2);
const b2 = B.clone().sub(p2);

// 通过c1、c2方向是否相同来推断两点是否位于线段同一侧
const c1 = a1.clone().cross(b1);
const c2 = a2.clone().cross(b2);

// 向量c1与c2夹角余弦值:用来推断向量c1与c2方向是否相同
const cos =  c1.normalize().dot(c2.normalize());
if(cos>0.5){//方向相同时候,余弦值1>0.5
    console.log('方向相同,两点在线段同侧');
}else{//方向相反时候,余弦值-1<0.5
    console.log('方向相反,两点在线段异侧');
}

叉乘结果

  • 向量方向 :向量c垂直于向量a和b构成的平面
  • 向量长度 :向量c的长度等于a长度乘b长度乘夹角θ的正弦值
js
c.crossVectors(a,b);
c.length() = a.length()*b.length()*sin(θ)

叉乘计算三角形面积

  • L1 x L2 x sin(θ)= h x L2 = 三角形面积2倍
js
// 三角形两条边构建两个向量
const a = p2.clone().sub(p1);
const b = p3.clone().sub(p1);
// 两个向量叉乘结果c的几何含义:a.length()*b.length()*sin(θ)
const c = a.clone().cross(b);

// 三角形面积计算
const S = 0.5*c.length();
console.log('S',S);

计算物体的表面积

  • Geometry有顶点索引数据
js
const pos = geometry.attributes.position;
const index = geometry.index;
console.log('geometry',geometry);
let S = 0;//表示物体表面积
for (var i = 0; i < index.count; i += 3) {
    // 获取当前三角形对应三个顶点的索引
    const i1 = index.getX(i);
    const i2 = index.getX(i + 1);
    const i3 = index.getX(i + 2);

    //获取三个顶点的坐标 
    const p1 = new THREE.Vector3(pos.getX(i1), pos.getY(i1), pos.getZ(i1));
    const p2 = new THREE.Vector3(pos.getX(i2), pos.getY(i2), pos.getZ(i2));
    const p3 = new THREE.Vector3(pos.getX(i3), pos.getY(i3), pos.getZ(i3));
    S += AreaOfTriangle(p1, p2, p3); 
}
console.log('S',S);

//三角形面积计算
function AreaOfTriangle(p1, p2, p3) {
    // 三角形两条边构建两个向量
    const a = p2.clone().sub(p1);
    const b = p3.clone().sub(p1);
    // 两个向量叉乘结果c的几何含义:a.length()*b.length()*sin(θ)
    const c = a.clone().cross(b);
    // 三角形面积计算
    const S = 0.5 * c.length();
    return S
}
  • Geometry没有顶点索引数据
js
const pos = geometry.attributes.position;
let S = 0;//表示物体表面积
for (let i = 0; i < pos.count; i += 3) {
    const p1 = new THREE.Vector3(pos.getX(i), pos.getY(i), pos.getZ(i));
    const p2 = new THREE.Vector3(pos.getX(i + 1), pos.getY(i + 1), pos.getZ(i + 1));
    const p3 = new THREE.Vector3(pos.getX(i + 2), pos.getY(i + 2), pos.getZ(i + 2));
    S += AreaOfTriangle(p1, p2, p3);//所有三角形面积累加
}
console.log('S', S);
//三角形面积计算
function AreaOfTriangle(p1, p2, p3) {
    // 三角形两条边构建两个向量
    const a = p2.clone().sub(p1);
    const b = p3.clone().sub(p1);
    // 两个向量叉乘结果c的几何含义:a.length()*b.length()*sin(θ)
    const c = a.clone().cross(b);
    // 三角形面积计算
    const S = 0.5 * c.length();
    return S;
}

点到直线的距离

js
// 已知条件
// 直线经过两点坐标A、B
const A = new THREE.Vector3(0, 0, 0);
const B = new THREE.Vector3(100, 0, 0);
// 直线外一点p
const p = new THREE.Vector3(50, 0, 30);

const AB = B.clone().sub(A);
const width = AB.length();//AB两点距离
// ApB构建一个三角形,其中两条边构建向量a、向量b
const a = A.clone().sub(p);
const b = B.clone().sub(p);
const c = a.clone().cross(b);
const H  = c.length()/width;//点到直线的距离

常见问题解决方案 ​

模型表面一闪一闪(z-fighting/深度冲突问题) ​

这种现象发生在两个或多个表面非常接近或重叠时,渲染器难以确定哪个表面应该显示在前面

解决方案:

  1. 重叠几何体:
    • 检查模型,确保没有重叠的几何体。
    • 如果有意为之的重叠,可以稍微调整几何体的位置,使它们有微小的间隔。
  2. 相机近平面和远平面设置不当:
    • 调整相机的near和far值,使其更适合你的场景尺寸:
    js
        this.camera = new THREE.PerspectiveCamera(
            30,
            this.container.clientWidth / this.container.clientHeight,
            1,  // 增加近平面距离
            3000
        );
  3. 深度缓冲精度不足:
    • 对于大场景,默认的深度缓冲可能不够精确。
    js
        this.renderer = new THREE.WebGLRenderer({ 
            antialias: true,
            logarithmicDepthBuffer: true  // 添加这一行
        });
  4. 材质问题:
    • 调整材质的polygonOffset属性:
    js
        object.material.polygonOffset = true;
        object.material.polygonOffsetFactor = 1;
        object.material.polygonOffsetUnits = 1;
    • 或者,如果是双面材质,可以尝试设置为单面:
    js
        object.material.side = THREE.FrontSide;  // 或 THREE.BackSide
  5. 环境贴图和材质交互:
    • 尝试调整材质的roughness和metalness值:
    • 或者,调整环境贴图的强度:
  6. 渲染设置:
    • 启用或禁用某些渲染特性:
    js
        this.renderer.shadowMap.enabled = true;
        this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;