Three.js一篇就够了[持续更新]

使用

下载
yarn add three
引入
import * as THREE from 'three'

常用开发工具

辅助观察坐标系

坐标轴颜色红R、绿G、蓝B分别对应坐标系的xyz轴。

// AxesHelper:辅助观察的坐标系
const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);
渲染帧率

通过stats.js库可以查看three.js当前的渲染性能,具体说就是计算three.js的渲染帧率(FPS),所谓渲染帧率(FPS),简单说就是three.js每秒钟完成的渲染次数,一般渲染达到每秒钟60次为最佳状态。

//引入性能监视器stats.js
import Stats from 'three/addons/libs/stats.module.js';

//创建stats对象
const stats = new Stats();

// 渲染函数
function render() {
	//requestAnimationFrame循环调用的函数中调用方法update(),来刷新时间
	stats.update();
	renderer.render(scene, camera); //执行渲染操作
	requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
}
render();
GUI库

threejs三维空间的很多参数,不是心算出来的,往往需要可视化的方式调试出来,因此需要GUI库进行调试。

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

// 实例化一个gui对象
const gui = new GUI();

具体用法请参考Three.js官网或者这个文档

网格地面辅助观察
const gridHelper = new THREE.GridHelper(300, 25, 0x004444, 0x004444);
三维模型加载器
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const loader = new GLTFLoader();

loader.load( 'gltf模型.gltf', function ( gltf ) {
  scene.add( gltf.scene );
})
后处理通道扩展库

threejs文件包目录examples/jsm/postprocessing/,你可以看到threejs提供了很多后处理通道,想实现什么样的后期处理效果,需要调用threejs对应的后处理通道扩展库。

  • OutlinePass.js:高亮发光描边
  • UnrealBloomPass.js:Bloom发光
  • GlitchPass.js:画面抖动效果

基本三要素

3D的基本三要素就是:场景相机渲染器

在这里插入图片描述

场景

你可以把三维场景Scene (opens new window)对象理解为虚拟的3D场景,用来表示模拟生活中的真实三维场景,或者说三维世界。

// 添加到场景
scene.add(model);//模型对象被添加到场景

// 从场景中移除
scene.remove(model,mesh2);//移除场景中多个模型对象

// 查看场景结构
console.log('查看Scene的子对象',scene.children);

// 查看场景某个具体模型
const nameNode = scene.getObjectByName ("4号楼");

// 隐藏或显示 适用任何东西
mesh.visible =false;// 隐藏一个网格模型,visible的默认值是true
group.visible =false;// 隐藏一个包含多个模型的组对象group
坐标
关系坐标

任何一个模型的本地坐标(局部坐标)就是模型的.position属性。

一个模型的世界坐标,说的是,模型自身.position和所有父对象.position累加的坐标。

获取世界坐标

mesh.getWorldPosition(Vector3)读取一个模型的世界坐标,并把读取结果存储到参数Vector3中。

// 声明一个三维向量用来表示某个坐标
const worldPosition = new THREE.Vector3();
// 获取mesh的世界坐标,你会发现mesh的世界坐标受到父对象group的.position影响
mesh.getWorldPosition(worldPosition);
console.log('世界坐标',worldPosition);
console.log('本地坐标',mesh.position);
添加局部坐标

mesh.add(坐标系)给mesh添加一个局部坐标系。

//可视化mesh的局部坐标系
const meshAxesHelper = new THREE.AxesHelper(50);
mesh.add(meshAxesHelper);
UV坐标

学习自定义顶点UV坐标之前,首先保证你对BufferGeometry的顶点数据、纹理贴图都有一定的理解。

顶点UV坐标的作用是从纹理贴图上提取像素映射到网格模型Mesh的几何体表面上。

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

在这里插入图片描述

代码演示

const geometry = new THREE.PlaneGeometry(200, 100); //矩形平面
console.log('uv',geometry.attributes.uv);

在这里插入图片描述

屏幕坐标

.offsetX.offsetY表示鼠标单击位置的坐标,单位是像素px,以点击的HTML元素左上角为坐标原点,水平向右方向为x轴,竖直向下方向为y轴。

addEventListener('click',function(event){
    const px = event.offsetX;
    const py = event.offsetY;
    const cx = event.clientX;
    const cy = event.clientY;
})
标准设备坐标

Three.js Canvas画布具有一个标准设备坐标系,该坐标系的坐标原点在canvas画布的中间位置,x轴水平向右,y轴竖直向上。

标准设备坐标系的坐标值不是绝对值,是相对值,范围是[-1,1]区间,也是说canvas画布上任何一个位置的坐标,如果用标准设备坐标系去衡量,那么坐标的所有值都在-1到1之间。

在这里插入图片描述

(屏幕坐标转标准设备坐标)

可以用.offsetX.offsetY当做canvas画布的屏幕坐标。

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

在这里插入图片描述

// 坐标转化公式
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;
})
相机

相机常用的有三种:正投相机 OrthographicCamera透视相机 PerspectiveCamera立体相机 CubeCamera

正投相机

一句话描述,正投影相机OrthographicCamera和透视投影相机PerspectiveCamera的区别就是,透视投影可以模拟人眼观察世界的视觉效果,正投影相机不会。

比如前面工厂的gltf模型加载案例,就是用透视投影相机模拟人在空中俯视地面的效果,如果使用正投影相机渲染效果就不太自然。

在这里插入图片描述

/*
left	渲染空间的左边界
right	渲染空间的右边界
top		渲染空间的上边界
bottom	渲染空间的下边界
near	near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 默认值0.1
far		far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小小,会有部分场景看不到。 默认值2000
*/
const camera = new THREE.OrthographicCamera( left, right, top, bottom, near, far )
透视相机

透视投影相机PerspectiveCamera本质上就是在模拟人眼观察这个世界的规律。

在这里插入图片描述

/*
fov	相机视锥体竖直方向视野角度
aspect	相机视锥体水平方向和竖直方向长度比,一般设置为Canvas画布宽高比width / height
near	相机视锥体近裁截面相对相机距离
far	相机视锥体远裁截面相对相机距离,far-near构成了视锥体高度方向
*/
const camera = new THREE.PerspectiveCamera( fov, aspect, near, far )
相机的选择

对于大部分需要模拟人眼观察效果的场景,需要使用透视投影相机,比如人在场景中漫游,或是在高处俯瞰整个园区或工厂。

正投影没有透视效果,也就是不会模拟人眼观察世界的效果。在一些不需要透视的场景你可以选择使用正投影相机,比如整体预览一个中国地图的效果,或者一个2D可视化的效果。

相机控件
  • 地图导航相机控件MapControls
  • 开发或者展示控件 OrbitControls
渲染器

生活中如果有了景物相机,那么如果想获得一张照片,就需要你拿着相机,按一下,咔,完成拍照。对于threejs而言,如果完成“咔”这个拍照动作,就需要一个新的对象,也就是WebGL渲染器WebGLRenderer (opens new window)

// 创建渲染器对象
const renderer = new THREE.WebGLRenderer({
     antialias:true,// 设置渲染器锯齿
     preserveDrawingBuffer:true, //想把canvas画布上内容下载到本地,需要设置为true
});

// 定义threejs输出画布的尺寸(单位:像素px)
const width = 800; //宽度
const height = 500; //高度
renderer.setSize(width, height); //设置three.js渲染区域的尺寸(像素px)

// 获取你屏幕对应的设备像素比.devicePixelRatio告诉threejs,以免渲染模糊问题
renderer.setPixelRatio(window.devicePixelRatio);

renderer.setClearColor(0x444444, 1); //设置背景颜色

renderer.setClearAlpha(0.8);// 改变背景透明度值

renderer.setClearColor(0xb9d3ff, 0.4); //设置背景颜色和透明度

renderer.render(scene, camera); //执行渲染操作
document.getElementById('webgl').appendChild(renderer.domElement);// Canvas画布插入到任意HTML元素中
使用代码案例
// 创建3D场景对象Scene
const scene = new THREE.Scene();

// 实例化一个透视投影相机对象 
const camera = new THREE.PerspectiveCamera(30, 800 / 500, 1, 3000);
camera.position.set(200, 200, 200); 
camera.lookAt(0, 0, 0);

// 创建渲染器对象
const renderer = new THREE.WebGLRenderer();
renderer.setSize(800, 300);
renderer.render(scene, camera);
document.body.appendChild(renderer.domElement);

物体

实体(模型) = 几何体 + 材质

网页 = HTML + CSS

还是那句话,把一些抽象组装在一起就会变成实体。

几何体
基础几何体 Geometry

Three.js提供了各种各样的几何体API,用来表示三维物体的几何形状。

  • 长方体 BoxGeometry
  • 圆柱体 CylinderGeometry
  • 球体 SphereGeometry
  • 圆锥 ConeGeometry
  • 矩形平面 PlaneGeometry
  • 圆平面 CircleGeometry
缓冲几何体 BufferGeometry

用法:

在这里插入图片描述

// 定义顶点数据:通过javascript创建一组xyz坐标数据用来表示几何体的顶点坐标。
const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    80, 0, 0, //顶点2坐标
    80, 80, 0, //顶点3坐标

    0, 0, 0, //顶点4坐标   和顶点1位置相同
    80, 80, 0, //顶点5坐标  和顶点3位置相同
    0, 80, 0, //顶点6坐标
]);

// 创建属性缓冲区对象
//3个为一组,表示一个顶点的xyz坐标
const attribue = new THREE.BufferAttribute(vertices, 3); 

// 设置几何体attributes属性的位置属性
geometry.attributes.position = attribue;

// 设置索引数据 .index 数据
const indexes = new Uint16Array([
    // 下面索引值对应顶点位置数据中的顶点坐标
    0, 1, 2, 0, 2, 3,
])
// 索引数据赋值给几何体的index属性
geometry.index = new THREE.BufferAttribute(indexes, 1); //1个为一组
常用方法
缩放 .scale()
// 几何体xyz三个方向都放大2倍
geometry.scale(2, 2, 2);
平移 .translate()
// 几何体沿着x轴平移50
geometry.translate(50, 0, 0);
旋转
  • .rotateX()
  • .rotateY()
  • .rotateZ()
// 几何体绕着x轴旋转45度
geometry.rotateX(Math.PI / 4);
居中.center()
geometry.translate(50, 0, 0);//偏移
// 居中:已经偏移的几何体居中,执行.center(),你可以看到几何体重新与坐标原点重合
geometry.center();
克隆 .clone()

这个比较通用,几何体、材质、模型、三维向量等都可以使用。

const mesh2 = mesh.clone();
mesh2.position.x = 100;
复制 .copy()

这个比较通用,几何体、材质、模型、三维向量等都可以使用。

mesh.position.copy(mesh2.position);
材质 Material

如果你想定义物体的外观效果,比如颜色,就需要通过材质Material相关的API实现。

  • 网格基础材质 MeshBasicMaterial (不受光照影响)
  • 网格漫反射材质 MeshLamberMaterial (受光照影响 漫反射)
  • 网格高光材质 MeshPhongMaterial (受光照影响 漫反射 镜面反射)
  • 物理材质 PBR (受光照影响)
    • MeshStandardMaterial
    • MeshPhysicalMaterial
  • 点材质 PointsMaterial
  • 线基础材质 LineBasicMaterial
  • 精灵材质 SpriteMaterial
常用属性
// 透明设置
material.transparent = true;//开启透明
material.opacity = 0.5;//设置透明度

// 可见设置
material.side = THREE.BackSide;//背面可以看到
material.side = THREE.DoubleSide;//双面可见
物理材质
金属度

金属度属性 .metalness 表示材质像金属的程度, 非金属材料,如木材或石材,使用0.0,金属使用1.0。

threejs的PBR材质,.metalness默认是0.5,0.0到1.0之间的值可用于生锈的金属外观。

new THREE.MeshStandardMaterial({
    metalness: 1.0,//金属度属性
})
mesh.material.metalness = 1.0;//金属度
粗糙度

生活中不同物体表面的粗糙程度不同,比如地面比较粗糙,比如镜子表面就非常非常光滑。

粗糙度roughness表示模型表面的光滑或者说粗糙程度,越光滑镜面反射能力越强,越粗糙,表面镜面反射能力越弱,更多地表现为漫反射。

粗糙度roughness,0.0表示平滑的镜面反射,1.0表示完全漫反射,默认0.5。

new THREE.MeshStandardMaterial({
    roughness: 0.5,//表面粗糙度
})
mesh.material.roughness = 0.5;//表面粗糙度
mesh.material.roughness = 0.0;//完全镜面反射,像镜子一样
反射周围环境

实际生活中,一个物体表面,往往会反射周围的环境。人的眼睛看到的东西,往往反射有周围景物,所以three.js渲染模型,如果想渲染效果更好看,如果想更符合实际生活情况,也需要想办法让模型反射周围景物。

MeshStandardMaterial材质的环境贴图属性是.envMap,通过PBR材质的贴图属性可以实现模型表面反射周围景物,这样渲染效果更好。

// 加载环境贴图
const textureCube = new THREE.CubeTextureLoader()
    .setPath('./环境贴图/环境贴图0/')
    .load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']);
new THREE.MeshStandardMaterial({
    metalness: 1.0,
    roughness: 0.5,
    envMap: textureCube, //设置pbr材质环境贴图
})
obj.material.envMap = textureCube; //设置环境贴图 
环境贴图反射率

MeshStandardMaterial.envMapIntensity属性主要用来设置模型表面反射周围环境贴图的能力,或者说环境贴图对模型表面的影响能力。具体说.envMapIntensity相当于环境贴图的系数,环境贴图像素值乘以该系数后,在用于影响模型表面。

// envMapIntensity:控制环境贴图对mesh表面影响程度
//默认值1, 设置为0.0,相当于没有环境贴图
obj.material.envMapIntensity = 1.0;
清漆层

MeshPhysicalMaterial是在MeshStandardMaterial基础上扩展出来的子类,除了继承了MeshStandardMaterial的金属度、粗糙度等属性,还新增了清漆.clearcoat、透光率.transmission、反射率.reflectivity、光泽.sheen、折射率.ior等等各种用于模拟生活中不同材质的属性。

清漆层属性.clearcoat可以用来模拟物体表面一层透明图层,就好比你在物体表面刷了一层透明清漆,喷了点水。.clearcoat的范围0到1,默认0。

清漆层粗糙度.clearcoatRoughness属性表示物体表面透明涂层.clearcoat对应的的粗糙度,.clearcoatRoughness的范围是为0.0至1.0。默认值为0.0。

const material = new THREE.MeshPhysicalMaterial( {
	clearcoat: 1.0,//物体表面清漆层或者说透明涂层的厚度
	clearcoatRoughness: 0.1,//透明涂层表面的粗糙度
} );
透光率

为了更好的模拟玻璃、半透明塑料一类的视觉效果,可以使用物理透明度.transmission属性代替Mesh普通透明度属性.opacity

使用.transmission属性设置Mesh透明度,即便完全透射的情况下仍可保持高反射率。

物理光学透明度.transmission的值范围是从0.0到1.0。默认值为0.0。

const mesh = gltf.scene.getObjectByName('玻璃01')
mesh.material = new THREE.MeshPhysicalMaterial({
    transmission: 1.0, //玻璃材质透光率,transmission替代opacity 
})
折射率

非金属材料的折射率从1.0到2.333。默认值为1.5。

new THREE.MeshPhysicalMaterial({
    ior:1.5,//折射率
})
精灵材质
基本使用

精灵材质对象SpriteMaterial和普通的网格材质一样可以设置颜色.color、颜色贴图.map、开启透明.transparent、透明度.opacity等属性,精灵材质对象SpriteMaterial的父类是材质Material

精灵模型Sprite默认是一个矩形形状,默认长宽都是1,默认在坐标原点位置。Sprite默认尺寸为1,如果你在画布上看不太清,可以适当调整相机参数。

对于透视投影相机而言,SpriteMesh一样遵循远小近大的投影规律。

const spriteMaterial = new THREE.SpriteMaterial({
  rotation:Math.PI/4,// 旋转精灵对象45度,弧度值
});

// 创建精灵模型对象,不需要几何体geometry参数
const sprite = new THREE.Sprite(spriteMaterial);

sprite.scale.set(50, 25, 1); //只需要设置x、y两个分量就可以

sprite.position.set(0,50,0);
场景标注

基本使用

根据需要选择Sprite或矩形Mesh标注场景,如果希望矩形始终平行于canvas画布,就选择Sprite,如果希望矩形标注姿态角度能跟着场景旋转,就使用矩形Mesh标注场景。

const texture = new THREE.TextureLoader().load("./光点.png");
const spriteMaterial = new THREE.SpriteMaterial({
  map: texture, //设置精灵纹理贴图
    
  // 网格材质.transparent属性默认是false,如果贴图是背景透明的png贴图,需要把.transparent设置为true,对于SpriteMaterial而言,.transparent默认是true。
  transparent:true,//SpriteMaterial默认是true
});

HTML标签标注

threejs的扩展库CSS2DRenderer.js,通过CSS2DRenderer.js可以把HTML元素作为标签标注三维场景。

threejs扩展库CSS2DRenderer.js提供了两个类CSS2渲染器CSS2DRenderer、CSS2模型对象CSS2DObject

CSS3渲染器CSS3DRenderer和CSS2渲染器CSS2DRenderer整体使用流程基本相同,只是在HTML标签渲染效果方面不同,比如CSS3渲染的标签会跟着场景相机同步缩放,而CSS2渲染的标签默认保持自身像素值。

使用:

// 1.引入
import { CSS2DRenderer,CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import {CSS3DRenderer,CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';

// 2.HTML元素转化为threejs的CSS2模型对象
const div = document.getElementById('tag');
const tag = new CSS2DObject(div);

// 3.添加到场景
tag.position.set(50,0,50);
scene.add(tag);

// 4.渲染
const css2Renderer = new CSS2DRenderer();
css2Renderer.render(scene, camera);
css2Renderer.setSize(width, height);
document.body.appendChild(css2Renderer.domElement);

注意:

HTML元素标签<div id="tag"></div>外面div父元素遮挡了Canvas画布鼠标事件,会造成相机控件OrbitControls的旋转、缩放等操作无效,也有可能会影响你的射线拾取,等等任何与canvas画布有关的鼠标事件都有可能收到影响,不过这算是普通web前端内容,选择前端方式解决即可。

// 方式1:
css2Renderer.domElement.style.pointerEvents = 'none';

// 方式2:
renderer.domElement.style.zIndex = 1;
css2Renderer.domElement.style.zIndex = -1;

CSS3DSprite

CSS3精灵模型CSS3DSprite对应的HTML标签,可以跟着场景缩放,位置可以跟着场景旋转,但是自身的姿态角度始终平行于canvas画布,不受旋转影响,就像精灵模型一样Sprite

// 引入CSS3精灵模型对象CSS3DSprite
import { CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js';

const div = document.getElementById('tag');
// HTML元素转化为threejs的CSS3精灵模型`CSS3DSprite`
const tag = new CSS3DSprite(div);
//标签tag作为mesh子对象,默认标注在模型局部坐标系坐标原点
mesh.add(tag);
// 相对父对象局部坐标原点偏移80,刚好标注在圆锥
tag.position.y += 80;

Canvas转为贴图

Canvas转为贴图的核心是需要使用CanvasTexture吧Canvas转为纹理对象。

canvas画布作为CanvasTexture的参数创建一个纹理对象,本质上你可以理解为CanvasTexture把canvas画布当做图片,读取参数canvas画布上的像素值,创建纹理贴图Texture

loader.load("../工厂.glb", function (gltf) {
  model.add(gltf.scene);
  const canvas = createCanvas('设备A');//创建一个canvas画布
  // canvas画布作为CanvasTexture的参数创建一个纹理对象
  // 本质上你可以理解为CanvasTexture读取参数canvas画布上的像素值
  const texture = new THREE.CanvasTexture(canvas);
  const spriteMaterial = new THREE.SpriteMaterial({
    map: texture,
  });
  const sprite = new THREE.Sprite(spriteMaterial);
})

也可以使用外部图片转到canvas上:

const img = new Image();
img.src = "./标签箭头背景.png";
const canvas = createCanvas(img,'设备A');//创建一个canvas画布
// 图片加载完成后,读取canvas像素数据创建CanvasTexture
const texture = new THREE.CanvasTexture(canvas);
雪和雨

如果你想模拟下雨效果,一个雨滴用一个3D水滴形曲面表示,假设一个水滴用40个三角形表示,1万个雨滴,就是40万个三角形,精灵模型Sprite在threejs内部就像相当于两个三角形构成的矩形,1万个精灵模型,相当于2万个三角形,Sprite模拟雨滴相比比3D曲面几何体表示雨滴顶点数量就会少很多,这样threejs渲染性能就更好。

const texture = new THREE.TextureLoader().load("./雨滴.png");
const spriteMaterial = new THREE.SpriteMaterial({
    map: texture, 
});

// 雨滴在3D空间随机分布
const group = new THREE.Group();
for (let i = 0; i < 16000; i++) {
    // 精灵模型共享材质
    const sprite = new THREE.Sprite(spriteMaterial);
    group.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)
}

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

// 根据时间计算Sprite位置
const clock = new THREE.Clock();
function loop() {
    // loop()两次执行时间间隔
    const t = clock.getDelta();
    group.children.forEach(sprite => {
        // 雨滴的y坐标每次减t*60
        sprite.position.y -= t*60;
        if (sprite.position.y < 0) {
            sprite.position.y = 600;
        }
    });
    requestAnimationFrame(loop);
}
loop();

相机附近雨滴偏大

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

模型
  • 点模型 Points
  • 线模型
    • 闭合线条 LineLoop
    • 非连续的线条 LineSegments
  • 网格模型 Mesh
  • 精灵模型 Sprite
三维向量 Vector3

三维向量Vector3有xyz三个分量,threejs中会用三维向量Vector3表示很多种数据,本节课提到的比较简单,就是位置.position和缩放.scale属性。

案例代码:

// new THREE.Vector3()实例化一个三维向量对象
const v3 = new THREE.Vector3(0,0,0);
v3.set(10,0,0);// set方法设置向量的值
v3.x = 100;// 访问x、y或z属性改变某个分量的值

常用方法

// 改变位置
mesh.position.y = 80;
mesh.position.set(80,2,10);

// 平移
mesh.translateX(100);//沿着x轴正方向平移距离100

// 缩放
mesh.scale.set(0.5, 1.5, 2)// 网格模型xyz方向分别缩放0.5,1.5,2倍

// 改变角度
mesh.rotation.y += Math.PI/3;//绕y轴的角度设置为60度

// 旋转 .rotateX() .rotateY() .rotateZ()
mesh.rotateX(Math.PI/4);//绕x轴旋转π/4
欧拉 Euler

模型的角度属性.rotation和四元数属性.quaternion都是表示模型的角度状态,只是表示方法不同,.rotation属性值是欧拉对象Euler (opens new window),.quaternion属性值是是四元数对象Quaternion

// 创建一个欧拉对象,表示绕着xyz轴分别旋转45度,0度,90度
const Euler = new THREE.Euler( Math.PI/4,0, Math.PI/2);
颜色对象 Color

颜色对象Color文档,可以看到颜色对象有三个属性,分别为.r.g.b,表示颜色RGB的三个分量。

// 创建一个颜色对象
const color = new THREE.Color();//默认是纯白色0xffffff。

// 通过 .r .g .b 改变颜色
color.r = 0.0;
color.b = 0.0;

color.setRGB(0,1,0);//RGB方式设置颜色
color.setHex(0x00ff00);//十六进制方式设置颜色
color.setStyle('#00ff00');//前端CSS颜色值设置颜色

material.color.set(0x00ffff);
material.color.set('#00ff00');
material.color.set('rgb(0,255,0)');
网格模型

网格模型Mesh其实就一个一个三角形(面)拼接构成。使用网格模型Mesh渲染几何体geometry,就是几何体所有顶点坐标三个为一组,构成一个三角形,多组顶点构成多个三角形,就可以用来模拟表示物体的表面。

在这里插入图片描述

查看网格模型代码示例:

// 参数:PlaneGeometry(长度,宽度,把一个物体分成几段,每段分成几段)
  const geometry = new THREE.PlaneGeometry(100, 50, 2, 1);
  const material = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    wireframe: true,//线条模式渲染mesh对应的三角形数据
  });
  const plane = new THREE.Mesh(geometry, material);
  scene.add(plane);
可见面

空间中一个三角形有正反两面,那么Three.js的规则是如何区分正反面的?非常简单,你的眼睛(相机)对着三角形的一个面,如果三个顶点的顺序是逆时针方向,该面视为正面,如果三个顶点的顺序是顺时针方向,该面视为反面。

演示:

const material = new THREE.MeshBasicMaterial({
    color: 0x0000ff, //材质颜色
    side: THREE.FrontSide, //默认只有正面可见
});

const material = new THREE.MeshBasicMaterial({
    side: THREE.DoubleSide, //两面可见
});

const material = new THREE.MeshBasicMaterial({
    side: THREE.BackSide, //设置只有背面可见
});
组 Group
基本使用

下面代码创建了两个网格模型mesh1、mesh2,通过THREE.Group类创建一个组对象group,然后通过add方法把网格模型mesh1、mesh2作为设置为组对象group的子对象,然后在通过执行scene.add(group)把组对象group作为场景对象的scene的子对象。也就是说场景对象是scene是group的父对象,group是mesh1、mesh2的父对象。

//创建两个网格模型mesh1、mesh2
const geometry = new THREE.BoxGeometry(20, 20, 20);
const material = new THREE.MeshLambertMaterial({color: 0x00ffff});
const group = new THREE.Group();
const mesh1 = new THREE.Mesh(geometry, material);
const mesh2 = new THREE.Mesh(geometry, material);
mesh2.translateX(25);
group.add(mesh1);
group.add(mesh2);
scene.add(group);

// 查看子对象
console.log('查看group的子对象',group.children);

光源和阴影

光学基本知识
  • 折射
  • 镜面反射
  • 漫反射
光源

Three.js提供了多种模拟生活中光源的API,文档搜索关键词light就可以看到。

  • 环境光 AmbientLight
  • 点光源 PointLight
  • 聚光灯光源 SpotLight
  • 平行光 DirectionalLight

在这里插入图片描述

常用属性

// 以点光源为例 两个参数分别表示光源颜色和光照强度
const pointLight = new THREE.PointLight(0xffffff, 1.0);

// 光源衰减
pointLight.decay = 0.0;//设置光源不随距离衰减

// 光源位置
pointLight.position.set(400, 0, 0);//点光源放在x轴上

// 添加到场景
scene.add(pointLight);

辅助工具

// 点光源辅助观察
const pointLightHelper = new THREE.PointLightHelper(pointLight, 10);
scene.add(pointLightHelper);

// 平行光辅助观察
const dirLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5,0xff0000);
scene.add(dirLightHelper);
阴影

贴图

纹理贴图
// 创建纹理贴图加载器
const texLoader = new THREE.TextureLoader();

// .load()方法加载图像,返回一个纹理对象Texture
const texture = texLoader.load('./earth.jpg');

// 添加到物体
const material = new THREE.MeshLambertMaterial({
    map: texture,//map表示材质的颜色贴图属性
});

注意 :材质的颜色贴图属性.map设置后,模型会从纹理贴图上采集像素值,这时候一般来说不需要再设置材质颜色.color.map贴图之所以称之为颜色贴图就是因为网格模型会获得颜色贴图的颜色值RGB。


环境贴图

环境贴图对PBR材质渲染效果影响还是比较大,一般渲染PBR材质的模型,最好设置一个合适的环境贴图。

立方体纹理加载器CubeTextureLoader

立方体纹理加载器CubeTextureLoader.load()方法是加载6张图片,返回一个立方体纹理对象CubeTexture

立方体纹理对象CubeTexture的父类是纹理对象Texture

// 加载环境贴图
// 加载周围环境6个方向贴图
// 上下左右前后6张贴图构成一个立方体空间
// 'px.jpg', 'nx.jpg':x轴正方向、负方向贴图  p:正positive  n:负negative
// 'py.jpg', 'ny.jpg':y轴贴图
// 'pz.jpg', 'nz.jpg':z轴贴图
const textureCube = new THREE.CubeTextureLoader()
    .setPath('./环境贴图/环境贴图0/')
    .load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']);
    // CubeTexture表示立方体纹理对象,父类是纹理对象Texture 

// 环境贴图需要保持一致
textureCube.encoding = THREE.sRGBEncoding;   

加载三维模型

gltf 与 glb

相比较obj、stl等格式而言,.gltf格式可以包含更多的模型信息。.gltf格式文件几乎可以包含所有的三维模型相关信息的数据,比如模型层级关系、PBR材质、纹理贴图、骨骼动画、变形动画。

gltf格式文件不一定就是以扩展名.gltf结尾,.glb就是gltf格式的二进制文件。比如你可以把.gltf模型和贴图信息全部合成得到一个.glb文件中,.glb文件相对.gltf文件体积更小,网络传输自然更快。

基本使用
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 );
})
加载进度
loader.load("../工厂.glb", function (gltf) {
    model.add(gltf.scene);
    // 加载完成,隐藏进度条
    // document.getElementById("container").style.visibility ='hidden';
    document.getElementById("container").style.display = 'none';
}, function (xhr) { 
    const percent = xhr.loaded / xhr.total;
    // console.log('加载进度' + percent);
    percentDiv.style.width = percent * 400 + "px"; //进度条元素长度
    percentDiv.style.textIndent = percent * 400 + 5 + "px"; //缩进元素中的首行文本
    // Math.floor:小数加载进度取整
    percentDiv.innerHTML = Math.floor(percent * 100) + '%'; //进度百分比
})
解决贴图颜色偏差

three.js加载gltf模型的时候,可能会遇到three.js渲染结果颜色偏差,对于这种情况,你只需要修改WebGL渲染器默认的编码方式.outputEncoding即可

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

最新版本属性名字有改变。渲染器属性名.outputEncoding已经变更为.outputColorSpace

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

如果没有特殊需要,一般为了正常渲染,避免颜色偏差,threejs代码中需要颜色贴图.encoding和渲染器.outputEncoding属性值保持一致。

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

注意!!!最新版本,纹理对象属性名.encoding已经变更为.colorSpace

纹理对象Texture颜色空间 (opens new window)编码属性.encoding有多个属性值,默认值是线性颜色空间THREE.LinearEncoding

const texture = new THREE.TextureLoader().load('./earth.jpg');
texture.encoding = THREE.LinearEncoding;//默认值
texture.encoding = THREE.sRGBEncoding;

//解决加载gltf格式模型颜色偏差问题
renderer.outputEncoding = THREE.sRGBEncoding;

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

射线

射线Ray和三维向量Vector3一样属于数学几何计算相关的API,可以进行射线交叉计算。

基本使用
// 创建射线对象Ray
const ray = new THREE.Ray()

// 设置射线起点
ray.origin = new THREE.Vector3(1,0,3);
ray.origin.set(1, 0, 3);

// 设置射线方向
// 注意: .direction的值需要是单位向量,不是的话可以执行.normalize()归一化或者说标准化。
ray.direction = new THREE.Vector3(1,0,0);// 表示射线沿着x轴正方向
ray.direction = new THREE.Vector3(-1,0,0);// 表示射线沿着x轴负方向
ray.direction = new THREE.Vector3(1,1,0).normalize();// 表示射线沿着xy坐标轴的中间线
判断一个射线和一个三角形在3D中是否交叉

.intersectTriangle()计算射线和三角形是否相交叉,相交返回交点,不相交返回null

// 三角形三个点坐标
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();//用来记录射线和三角形的交叉点
const result = ray.intersectTriangle(p1,p2,p3,false,point);
console.log('交叉点坐标', point);
console.log('查看是否相交', result);
射线投射器

射线投射器可拾取模型。

代码案例:

// 1.初始化模型
const geometry = new THREE.SphereGeometry(25, 50, 50);
const material = new THREE.MeshLambertMaterial({
    color: 0x009999,
});
const mesh1 = new THREE.Mesh(geometry, material);
const mesh2 = mesh1.clone();
mesh2.position.y = 100;
const mesh3 = mesh1.clone();
mesh3.position.x = 100;
const model = new THREE.Group();
// 注意更新下模型的世界矩阵,你设置的mesh.position生效,再进行射线拾取计算
model.updateMatrixWorld(true);
// 三个网格模型mesh1,mesh2,mesh3用于射线拾取测试
model.add(mesh1, mesh2, mesh3);
model.updateMatrixWorld(true);

// 2.使用射线投射器
const raycaster = new THREE.Raycaster();
// 设置射线起点
raycaster.ray.origin = new THREE.Vector3(-100, 0, 0);
// 设置射线方向射线方向沿着x轴
raycaster.ray.direction = new THREE.Vector3(1, 0, 0);

// 3.计算出来与自身射线.ray相交的网格模型
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
console.log("射线器返回的对象", intersects);
// intersects.length大于0说明,说明选中了模型
if (intersects.length > 0) {
     // 选中模型的第一个模型,设置为红色
    intersects[0].object.material.color.set(0xff0000);
    
    console.log("交叉点坐标", intersects[0].point);
    console.log("交叉对象",intersects[0].object);
    console.log("射线原点和交叉点距离",intersects[0].distance);
}

选中模型案例

注意:如果画布变化了,需要重新计算画布的尺寸大小。

renderer.domElement.addEventListener('click', function (event) {
    // .offsetY、.offsetX以canvas画布左上角为坐标原点,单位px
    const px = event.offsetX;
    const py = event.offsetY;
    //屏幕坐标px、py转WebGL标准设备坐标x、y
    //width、height表示canvas画布宽高度
    const x = (px / width) * 2 - 1;
    const y = -(py / height) * 2 + 1;
    //创建一个射线投射器`Raycaster`
    const raycaster = new THREE.Raycaster();
    //.setFromCamera()计算射线投射器`Raycaster`的射线属性.ray
    // 形象点说就是在点击位置创建一条射线,射线穿过的模型代表选中
    raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
    //.intersectObjects([mesh1, mesh2, mesh3])对参数中的网格模型对象进行射线交叉计算
    // 未选中对象返回空数组[],选中一个对象,数组1个元素,选中两个对象,数组两个元素
    const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
    console.log("射线器返回的对象", intersects);
    // intersects.length大于0说明,说明选中了模型
    if (intersects.length > 0) {
        // 选中模型的第一个模型,设置为红色
        intersects[0].object.material.color.set(0xff0000);
    }
})

动画

Jsap.js

GSAP 中文文档

Tween.js

tween.js 中文文档

后处理

这些通道需要选择性的组合一起使用。

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

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

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

通过EffectComposer(renderer)指定了需要后处理的渲染器WebGLRenderer,渲染器通道RenderPass的作用是指定后处理对应的相机camera和场景scene

// 引入渲染器通道RenderPass
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';

// 创建一个渲染器通道,场景和相机作为参数
const renderPass = new RenderPass(scene, camera);

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

// 渲染循环
function render() {
    composer.render();
    // renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();
高亮描边通道

OutlinePass可以给指定的某个模型对象添加一个高亮发光描边效果。

// 引入OutlinePass通道
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.visibleEdgeColor.set(0xffff00); 
//高亮发光描边厚度
outlinePass.edgeThickness = 4; 
//高亮描边发光强度
outlinePass.edgeStrength = 6; 
 //模型闪烁频率控制,默认0不闪烁
outlinePass.pulsePeriod = 2;

// 选择一个或者多个发光模型
// 一个模型对象
outlinePass.selectedObjects = [mesh];
// 多个模型对象
outlinePass.selectedObjects = [mesh1,mesh2,group];

// 设置OutlinePass通道
composer.addPass(outlinePass);
GlitchPass通道

OutlinePassGlitchPass通道组合使用,一个通道就像流水线上的一个工位一样,负责完成一个后处理功能,然后交给下一个通道,完成新的后处理功能。

// 引入GlitchPass通道
import { GlitchPass } from 'three/addons/postprocessing/GlitchPass.js';

// 设置glitchPass通道
const glitchPass = new GlitchPass();
composer.addPass(glitchPass);
伽马校正通道

threejs并没有直接提供伽马校正的后处理通道,提供了一个伽马校正的Shader对象GammaCorrectionShader,这时候可以把Shader对象作为ShaderPass的参数创建一个通道。

// ShaderPass功能:使用后处理Shader创建后处理通道
import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';

// 创建伽马校正通道
const gammaPass= new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);
抗锯齿处理通道

three.js提供了多种抗锯齿的后处理,下面给演示下FXAAShaderSMAAPass两种抗锯齿的后处理。

FXAA抗锯齿通道

FXAA减弱了锯齿,但是并不完美。

// ShaderPass功能:使用后处理Shader创建后处理通道
import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
// FXAA抗锯齿Shader
import { FXAAShader } from 'three/addons/shaders/FXAAShader.js';

// 设置设备像素比,避免canvas画布输出模糊
renderer.setPixelRatio(window.devicePixelRatio);

const FXAAPass = new ShaderPass( FXAAShader );
// `.getPixelRatio()`获取`renderer.setPixelRatio()`设置的值
const pixelRatio = renderer.getPixelRatio();//获取设备像素比 
// width、height是canva画布的宽高度
FXAAPass.uniforms.resolution.value.x = 1 /(width*pixelRatio);
FXAAPass.uniforms.resolution.value.y = 1 /(height*pixelRatio);
composer.addPass( FXAAPass );
SMAA抗锯齿通道

SMAA相比较FXAA抗锯齿效果更好一些。

// SMAA抗锯齿通道
import {SMAAPass} from 'three/addons/postprocessing/SMAAPass.js';

//获取.setPixelRatio()设置的设备像素比
const pixelRatio = renderer.getPixelRatio();
// width、height是canva画布的宽高度
const smaaPass = new SMAAPass(width * pixelRatio, height * pixelRatio);
composer.addPass(smaaPass);

Shader

想要学习Shader请先掌握 GLSL ES 语言。

CannonJS物理引擎

所谓物理引擎,就是通过代码模拟物理世界。举个简单例子,比如你初高中都学过物理学,其中速度加速度位移都是比较常见的物理量,咱们通过CannonJS等物理引擎,都可以辅助你计算生活中物体的速度位移,比如计算一个小球在地球重力的作用下,下落的速度和位置。

安装
yarn add cannon-es -S
基本使用
import * as THREE from 'three'
import * as CANNON from "cannon-es"

// 创建3D场景对象Scene
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(10, 10, 10);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.render(scene, camera);
new OrbitControls(camera, renderer.domElement)
document.getElementById('view').appendChild(renderer.domElement);

// 坐标辅助
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

// 设置环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

// 创建物理世界
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0); //单位:m/s²

// 创建一个平面
const groundShape = new CANNON.Plane();
const groundBody = new CANNON.Body({
    mass: 0, // 质量为0,表示静态物体
    shape: groundShape,
});
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(groundBody);
const groundGeometry = new THREE.BoxGeometry(10, 10, 0);
const groundMaterial = new THREE.MeshBasicMaterial({ color: 0x808080 });
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
groundMesh.rotation.x = -Math.PI / 2;
scene.add(groundMesh);

// 创建一个球
const body = new CANNON.Body({
mass: 0.3,
position: new CANNON.Vec3(0, 100, 0),
shape: new CANNON.Sphere(1),
});
world.addBody(body);
const geometry = new THREE.SphereGeometry(1);
const material = new THREE.MeshLambertMaterial({
color: 0x00ffff,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 动画函数
function animate() {
requestAnimationFrame(animate);
world.step(1 / 60);//更新物理计算
mesh.position.copy(body.position);
renderer.render(scene, camera);
}
animate();
物理世界
// CANNON.World创建物理世界对象
const world = new CANNON.World();

// 设置物理世界重力加速度
world.gravity.set(0, -9.8, 0); //单位:m/s²

// 物理世界更新计算
function render() {
    world.step(1/60);//更新物理计算
     mesh.position.copy(body.position);// 渲染循环中,同步物理球body与网格球mesh的位置
    requestAnimationFrame(render);
}
render()
碰撞体

通过CANNON.Body类,可以创建一个用于物体物理模拟计算,比如用Body表示一个球、一个箱子、一张桌子,你可以计算Body的位置、速度等物理量。

// 1m半径球体
const bodyShape = new CANNON.Sphere(1);

const body = new CANNON.Body({
    mass: 0.3, // 碰撞体质量0.3kg
     // 碰撞体的三维空间中位置
    position: new CANNON.Vec3(0, 100, 0),
    shape: bodyShape,//碰撞体的几何体形状
});

// 把物体添加到物理世界
world.addBody(body);
碰撞事件
const audio = new Audio('./碰撞声.wav');
body.addEventListener('collide', (event) => {
    console.log('碰撞碰撞信息', event.contact);
    audio.volume = 1;
    audio.play();
})
  • 24
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值