three学习笔记(1-9)

307 阅读44分钟

Three.js零基础入门 (B站有同步视频,大佬讲的很清晰)

环境

threejs官方所有版本文件包:github,截至当前最新版本是r160

three中文/英文文档: 中文网

学习环境

html文件中即可

<script src="./build/three.js"></script>
/
<script type="module">
// 现在浏览器支持ES6语法,自然包括import方式引入js文件
import * as THREE from './build/three.module.js';
</script>
​
// 推荐importmap写法
​
<script type="importmap">
{
    "imports":{
         "three": "https://unpkg.com/three@0.160.1/build/three.module.js",
         "three/addons/": "https://unpkg.com/three@0.160.1/examples/jsm/"
    } 
}
<script>
<script type="module">
    import * as THREE from 'three' // three上面配置的名字
</script>

开发环境:

npm install three@xxxx--save
​
// 引入主库
import * as THREE from 'three';
​
// 引入扩展库addons文件夹下,比如旋转缩放:
import {xxx} from 'three/addons/loaders/......'

创建场景

认识三个基本概念:场景,相机,渲染器

image-20240130144015936.png

三维场景Scene

可以把三维场景Scene对象理解为虚拟的3D场景,用来模拟真实的三维世界

const Scene = new THREE.Scene()  // 创建3D场景对象 Scene

物体形状Geometry

xxxxGeometry(几何体) 创建各种形状,以Geometry结尾的各个方法,创建几何对象

const geometry = new THREE.BoxGeometry(100,100,100) // 实例化一个长宽高分别为100的长方体对象

image-20240130171750346.png

外观材质Material

xxxxMaterial 定义物体的外观效果,创建材质对象

const material = new THREE.MeshBasicMaterial({  // 实例化一个材质对象
    color: 0xff0000,//0xff0000设置材质颜色为红色
}); 

image-20240130171801928.png

物体 网格模型Mesh

通过网格模型Mesh表示一个虚拟物体,比如鼠标,桌子等

两个参数: 形状geometry 材质material

const mesh = new THREE.Mesh(geometry,material) // 网格模型对象 表示一个物体

位置 .position

通过位置属性.position定义网格模型Mesh在虚拟场景Scene中的位置,默认为坐标原点

mesh.position.set(x,y,z)

场景中添加物体 .add

将创建的网格模型添加进场景中

scene.add(mesh)

相机

场景有了,需要有一个相机才能在网页中渲染

透视投影相机

PerspectiveCamera透视投影相机比较常用,,本质是模拟眼睛观察世界

const camera = new THREE.PerspectiveCamer() // 实例化一个相机

相机实例化好了,接下来看看相机的其他属性,来配置一下相机的位置,观察目标等

相机位置 .position

实例化好的相机可以通过.position对象设置相机在Three.js三维坐标系中的位置,相机位置不同拍的照片也不同

camera.position.set(x,y,z)

相机观察目标 .lookAt

设置相机的拍照目标,具体来说就是镜头对准的哪个位置

//相机观察目标指向Threejs 3D空间中某个位置
camera.lookAt(0, 0, 0); //坐标原点
camera.lookAt(mesh.position);//指向mesh对应的位置

物体和相机的位置关系判断:

根据相机在坐标系的位置和物体在坐标系的位置可以判断出两者的相对位置关系:

mesh.position
camera.position

canvas画布

定义相机渲染输出的画布尺寸,虚拟相机渲染场景在浏览器网页上呈现的结果称为canvas画布,他决定在网页上输出的Canvas画布的大小,将来图像会投影到画布上

// 单位px
const width = 800;
const height = 500;

视锥体

相机的拍摄范围,,通过四个参数来定义一个四棱台,这个四棱台就叫做视锥体,物体必须在视锥体空间,才能渲染出来不在视锥体之内的物体不会渲染到画布上

// 定义canvas画布的大小
const width = 800;
const height = 500;
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000)
​
参数解释:
PerspectiveCamera(fov, aspect, near, far)
fov: 相机视锥体垂直方向视野角度
aspect: 视锥体宽度和高度的比值,一般设置为canvas画布的宽高比
near: 相机视锥体近截面相对相机的具体,
far: 相机视锥体远截面相对相机的距离,far-near构成了视锥体高度方向

image-20240130172503480.png

渲染器

场景有了,相机有了,通过渲染器就可以将这两者结合生成图像了

WedGL渲染器

通过WebGLRenderer实例化一个渲染器

const renderer = new THREE.WebGLRenderer();

设置canvas画布尺寸 .setSize

设置three.js渲染区域的尺寸 单位px

const width = 800;
const height = 500;
renderer.setSize(width,heigth)

这里的画布和定义相机的canvas画布不同:

  • 相机的canvas画布,影响的是投影的效果,确保投影在画布上不会出现形变
  • 渲染器的canvas画布,影响的是渲染结果的呈现,确保渲染结果正确显示在画布上

一般情况下需要确保相机的视锥体宽高比与渲染器的画布宽高比匹配

渲染 .render

生成一个canvas画布,相当于拍照时'咔'的动作,并将三维场景Scene呈现在canvas画布上

renderer.render(scene,camera);

获取画布 .domElement

.domElement属性可以获取.render生成的canvas画布的DOM

const canvasDom = renderer.domElement

插入页面

获取到canvas画布的DOM之后便可以将其插入页面中的任意一个元素中了

document.getElementById('webgl').appendChild(renderer.domElement);

三维坐标系

辅助观察坐标系 .AxesHelper

.AxesHelper参数表示坐标轴尺寸大小,

x,y,x轴分别对应红R,绿G,蓝B,默认y轴朝上,也可以通过.setColors()进行设置

const  axesHelper = new THREE.AxesHelper(150)
scene.add(axesHelper)

设置材质半透明

将材质设置为半透明即可以看到完整的坐标轴

const material = new THREE.MeshBasicMaterial({
    transparent: true, // 开启透明
    opacity: 0.5 // 设置透明度
})

光源

受光照影响的材质

有的材质受光照的影响表明会有所差异,有的材质不会;

image-20240131114146531.png

只有基础网格材质表面不会受到影响,其他受光照影响的材质,在没有光源的情况下是看不到的

创建光源

和现实世界相同,光源分为多种类型,环境光,点光源,聚光灯光源等:

image-20240131114912385.png

他们的光线区别如下:

image-20240131114949274.png

以点光源为例创建一个光源:

const pointLight = new THREE.Pointlight('fff',1.0)
参数: 
fff: 光源颜色
1.0: 光照强度

直接设置光源强度:
pointLight.intensity = 1.0

光源衰减 .decay

通过该属性可以直接设置光源的随着距离增加的衰减程度,为0表示不衰减

pointLight.decay = 0

光源位置 .position

光源相当于电灯泡,灯泡位置不同,照在物体上光的角度和强弱也不同

pointLight.position.set(x,y,z) // 光源的位置要放在物体外面才能照到物体

添加光源

scene.add(pointLight)

相机控件 OrbitControls

展示模型时通过OrbitControls相机轨道控制器,实现旋转缩放平移的效果,需要注意的是:相机控件的初始化会将相机的lookAt初始化为(0,0,0)因此之后还要手动设置一下控件的target为相机的lookAt值

本质是轨道控制器修改的是相机的各种参数,如位置,距离等

引入

import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

使用

const orbitControls = new OrbitControls(camera,renderer.domElement)
orbitControls.target.set(x,y,x)
orbitControls.update(); // update函数内会执行 camera.lookAt(controls.target)
orbitControls.addEventListener('change',function(){ // 监听到变化重新渲染
    renderer.render(scene,camera)
})
参数: 
camera: 改变哪个相机
renderer.domElement: 监控哪个区域
  • 鼠标左键加平移: 旋转模型
  • 鼠标右键加平移: 移动模型
  • 鼠标滚轮: 缩放模型

平型光和环境光

点光源

光线从一个点发射:

image-20240131151809409.png

点光源的辅助观察器

正常情况下是看不到光源的具体位置的,借助PointLightHelper可以用球形网格辅助对象来模拟光源,从而查看光源的位置

const pointLightHelper = new THREE.PointLightHelper(pointLight,10,"#fff")
scene.add(pointLightHelper)
​
// 参数说明:
pointLight: 要模拟的光源
10: 可选 球形网格辅助对象的直径
#fff: 可选 颜色,默认为模拟光源颜色

环境光

这种光源没有特定方向,只是整体场景的变亮或者暗

//环境光:没有特定方向,整体改变场景的光照明暗
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);

平行光

光线沿着特定方向发射:

image-20240131152822203.png

平行光是有方向的,其方向的是由光源的position和指向的目标的position计算的.

// 平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
// 设置光源的方向:通过光源position属性和目标指向对象的position属性计算
directionalLight.position.set(80, 100, 50);
// 方向光指向对象网格模型mesh,可以不设置,默认的位置是0,0,0
directionalLight.target = mesh;
scene.add(directionalLight);

平行光辅助观察器

与点光源类似

// DirectionalLightHelper:可视化平行光
const dirLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5,0xff0000);
scene.add(dirLightHelper);

平行光反射规律

平行光照射到网格模型Mesh表面,光线和模型表面构成一个入射角度,入射角度不同,对光照的反射能力不同。

光线照射到漫反射网格材质MeshLambertMaterial (opens new window)对应Mesh表面,Mesh表面对光线反射程度与入射角大小有关

image-20240131154823703.png

动画循环渲染

使用HTML5提供的帧动画requestAnimationFrame,在浏览器下一次重绘之前调用给定的函数,默认每秒钟60次,不过这也不一定,跟执行的任务性能和电脑性能有关

一个简单的旋转动画

const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
// renderer.render(scene, camera); //执行渲染操作
document.body.appendChild(renderer.domElement);

// 渲染函数
function render() {
    renderer.render(scene, camera); //执行渲染操作
    mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(render);//请求再次执行渲染函数render,渲染下一帧
}
render();

计算两帧之间的时间间隔和帧率

// 渲染循环
const clock = new THREE.Clock();
function render() {
    const spt = clock.getDelta()*1000;//毫秒
    console.log('两帧渲染时间间隔(毫秒)',spt);
    console.log('帧率FPS',1000/spt);
    renderer.render(scene, camera); //执行渲染操作
    mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(render);//请求再次执行渲染函数render,渲染下一帧
}
render();

动态调整画布

在视口大小变化时自动调整画布大小:

需要注意: canvas画布宽高度动态变化,需要更新相机和渲染的参数updateProjectionMatrix(),否则无法正常渲染。

window.addEventListener('resize',function(){
    // 设置相机视锥体的宽高比
    camera.aspect(window.innerWidth / window.innerHeight)
    // 设置渲染器canvas画布的大小
    renderer.setSize(window.innerWidth,window.innerHeight)
    // 更新相机的数据
    // 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
    // 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
    // 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
    camera.updateProjectionMatrix();
})

监控threejs的渲染性能

通过stats拓展库可以查看渲染性能

引入

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

stats使用

// 创建stats对象
const stats = new Stats();
//stats.domElement 监控结果,一个div,可以插入页面中
document.body.append(stats.domElement)

function render(){
    stats.update()
    
    renderer.render(scene,camera);
    requestAnimationFrame(render)
}
render()

添加后的效果:

image-20240131171439104.png

设置显示模式setMode

通过setMode设置首次打开页面的显示模式,鼠标单击可以显示更换不同的模式

// stats.domElement显示:渲染帧率  刷新频率,一秒渲染次数 
stats.setMode(0);//默认模式
//stats.domElement显示:渲染周期 渲染一帧多长时间(单位:毫秒ms)
  stats.setMode(1);

常见几何体简介

常见形状

// 正方体
const box = new THREE.BoxGeometry(100, 100, 100);
// 胶囊模型
const capsule = new THREE.CapsuleGeometry(100, 150, 4, 10);
//圆锥模型
const cone = new THREE.ConeGeometry(100, 100, 40);
//球
const sphere = new THREE.SphereGeometry(100);
// CylinderGeometry:圆柱
const cylinder = new THREE.CylinderGeometry(50, 50, 100);
// PlaneGeometry:矩形平面
const plane = new THREE.PlaneGeometry(100, 50);
// CircleGeometry:圆形平面
const circle = new THREE.CircleGeometry(50);

材质双面可见

材质默认正面可见,反面不可见,对于矩形圆形平面,如果想看到两个面,可以设置:

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

高光网络材质

`MeshPhongMaterial高光网络材质属于受光照影响的材质之一,他的效果就像阳光下看一辆光滑的表面,在特定角度某个区域特别亮,也可以称为镜面泛着效果,而前面提到的漫反射只会向四周反射光线,不会在特定角度反射光线

// 模拟镜面反射,产生一个高光效果
const material = new THREE.MeshPhongMaterial({
    color: 0xff0000,
});

高光亮度属性 .shininess

该属性可以控制高光反射效果:

// 模拟镜面反射,产生一个高光效果
const material = new THREE.MeshPhongMaterial({
    color: 0xff0000,
    shininess: 20, //高光部分的亮度,默认30
});

高光颜色属性 .spcular

颜色属性.specular表示高光的颜色

// 模拟镜面反射,产生一个高光效果
const material = new THREE.MeshPhongMaterial({
    color: 0xff0000,
    shininess: 20, //高光部分的亮度,默认30
    specular: 0x444444, //高光部分的颜色
});

WebGL渲染器设置(锯齿模糊)

一些渲染器的基础设置

渲染器锯齿属性 .antialias

是否执行抗锯齿,该属性可以在创建的时候设置也可以执行通过.antialias一样设置,默认为false

const renderer = new THREE.WebGLRenderer({antialias: true})
或者
renderer.antialias = true

false:

image-20240201150057177.png

true:

image-20240201150012924.png

设置设备像素比 .setPixelRatio

如果渲染器的设备像素比和浏览器的设备像素比不同,则canvas画布输出可能模糊,这时候需要将渲染器的设备像素比设置为屏幕的设备像素比:

// 将渲染器的设备像素比设置的和屏幕相同,不管高清不高清为了保险都执行一遍
renderer.setPixelRatio(window.devicePixelRatio)

设置背景颜色

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

gui.js库

一个辅助调试的库

安装

gihtub地址:https://github.com/dataarts/dat.gui

npm地址:https://www.npmjs.com/package/dat.gui

官方案例扩展库中给的有,直接引入即可
// 引入dat.gui.js的一个类GUI
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

创建GUI对象

const gui = new GUI()

实例化GUI之后页面上会多一个操作界面,可以通过操作界面调整three的各种参数实时看到效果

.domElement

通过gui.domElement可以拿到页面上GUI界面的DOM对象,进而对GUI界面进行各种调整:

const guiDom = gui.domElement
guiDom.style.width = '200px' // 调整gui界面宽度

添加控制项 .add

刚开始的GUI页面中是没有控制项的,需要通过.add方法添加控制项,gui会自动生成对应页面,用来改变js对象的某个属性值

gui.add(控制对象,对象属性名,min,max,step)
​
控制对象: 需要改变属性的对象
对象属性名: 改变对象的哪个属性
min和max: 数值改变的范围
step: 步长

根据参数3,参数4值的类型不同,自动生成的控制项不同:

  • 进度条交互

    • 参数3和参数4都是数字,创建拖动条
  • 下拉菜单

    • min是一个数组: [-100, 0, 100]
    • min是一个对象: {left: -100,center: 0,right: 100}键名可以是中文
  • 单选框

    • 改变的属性对应类型是Boolean类型

控制项名称

默认为属性名,可以通过.name方法指定:

gui.add().name('光照强度')

监控变化

.onChange方法在属性变化的时候执行内部回调函数

gui.onChange(() => {
    renderer,render(scene,carmera)
})

生成颜色值

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

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

分组

需要控制的属性比较多的时候,为了避免混合,可以适当分组管理

new GUI()实例化一个gui对象,默认创建一个总的菜单,通过gui对象的.addFolder()方法可以创建一个子菜单,当你通过GUI控制的属性比较多的时候,可以使用.addFolder()进行分组。

.addFolder()返回的子文件夹对象,同样具有gui对象的.add().onChange().addColor()等等属性。

// 环境光子菜单
const ambientFolder = gui.addFolder('环境光');
// 环境光强度
ambientFolder.add(ambient, 'intensity',0,2);

关闭和打开

const gui = new GUI(); //创建GUI对象 
gui.close();//关闭菜单c
gui.open(); // 打开

几何体

缓冲类型几何体

上文中提到的长方体,球体等都是继承自缓冲类型几何体BufferGeometry类,可以理解为BufferGeomertry是一个没有任何形状的空几何体,可以通过BufferGeometry定义任何形状的几何体.具体的定义方式是从定于顶点数据开始的:

//创建一个空的几何体对象
const geometry = new THREE.BufferGeometry(); 

定义几何体顶点

要求使用类型化数组Float32Array,每三个元素为一组xyz来表示几何体的顶点坐标,

//类型化数组创建顶点数据
const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    50, 0, 0, //顶点2坐标
    0, 100, 0, //顶点3坐标
    0, 0, 10, //顶点4坐标
    0, 0, 100, //顶点5坐标
    50, 0, 10, //顶点6坐标
]);

设置了类型化数组之后需要通过BufferAttribute类将顶点数据转换成three的几何顶点数据:

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

设置几何体顶点

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

点模型 .points

点模型与网格模型都属于模型的一种,网格模型渲染出来的是一个面,而点模型渲染出来的是一个个的点

const points = new Points(geometry,material)

点材质

网格模型有对应的漫反射材质,高亮材质等,点模型有自己对应的点材质PointsMaterial:

const material = new THREE.PointsMaterial({
    color: 0xffff00,
    size: 10.0 //点对象像素尺寸
}); 

通过点模型可以将上述定义的顶点数据渲染出来看效果:

image-20240202113617604.png

线模型

使用线模型可以将顶点数据渲染成线条,不同的线模型连接顶点的规则不同

线材质

const material = new THREE.LineBasicMaterial({
    color: 0xff0000 //线条颜色
});

Line模型

从第一个点到最后一个点依次连接成线

const line = new THREE.Line(geometry, material);

下面是顶点数据为:[
  0, 0, 0, 100, 0, 100, 100, 0, 0, 0, 0, 100, 0, 100, 0, 100, 100, 0, 100, 100,100, 0, 100, 100,
] 的连接结果

image-20240202143223370.png

LineLoop模型

从第一个点到最后一个点依次相连接,并且首尾相连

const line = new THREE.LineLoop(geometry, material);

image-20240202143434459.png

LineSegments模型

间断的连接各个顶点,如第一个和第二个相连接,第三个和第四个相连接,依次这样

const line = new THREE.LineSegments(geometry, lineMaterial);

image-20240202143619274.png

网格模型

通过网格模型渲染顶点数据时,将会每三个顶点为一组渲染成一个小三角形,多个小三角形拼起来就形成了一个任意的形状,

const geometry = new THREE.BufferGeometry();
// 设置顶点坐标
const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    50, 0, 0, //顶点2坐标
    0, 100, 0, //顶点3坐标
    0, 0, 10, //顶点4坐标
    0, 0, 100, //顶点5坐标
    50, 0, 10, //顶点6坐标
]);
// 顶点坐标转换成threejs的顶点数据
const attribue = new THREE.BufferAttribute(vertices, 3);
// 设置形状的几何顶点信息
geometry.attributes.position = attribue;
// 网格材质
const meshMaterial = new THREE.MeshBasicMaterial({ color: "#fff" });
const mesh = new THREE.Mesh(geometry, meshMaterial);
scene.add(mesh);    

上述代码渲染结果为:

image-20240202145918085.png

可以看到,三组顶点组成了一个三角形,后三个顶点组成了另一个三角形

网格模型的本质(重要)

实际上网格模型就是一个个三角形拼接构成的:

image-20240202150112916.png

正面和反面

three中材质是具有正反面之分的,默认正面可见,反面不可见,通过材质的side属性控制:

//正面可见:在背面看不到模型
const material = new THREE.MeshBasicMaterial({
    side: THREE.FrontSide,
});
//两面可见: 任何一面都能看到模型
const material = new THREE.MeshBasicMaterial({
    side: THREE.DoubleSide,
});
//背面可见: 在正面看不到模型
const material = new THREE.MeshBasicMaterial({
    side: THREE.BackSide,
});

那么three是如何区分正面和反面的呢?

通过当前观察三角形的三个顶点的顺时针或逆时针顺序:

  • 正面:逆时针
  • 反面:顺时针

相机对着三角形的一个面,如果三个顶点的顺序是逆时针方向,该面视为正面,如果三个顶点的顺序是顺时针方向,该面视为反面。

使用缓冲类型几何形状自定义一个长方体:

注意: 每个三角形的正面反面问题,确保正面朝外

const vertices = new Float32Array([
  0, 0, 0, 0, 200, 0, 100, 0, 0, 0, 200, 0, 100, 200, 0, 100, 0, 0,

  0, 0, 100, 100, 0, 100, 0, 200, 100, 0, 200, 100, 100, 0, 100, 100, 200, 100,

  0, 0, 0, 100, 0, 0, 0, 0, 100, 0, 0, 100, 100, 0, 0, 100, 0, 100,

  0, 200, 0, 0, 200, 100, 100, 200, 0, 0, 200, 100, 100, 200, 100, 100, 200, 0,

  0, 0, 0, 0, 0, 100, 0, 200, 100, 0, 200, 100, 0, 200, 0, 0, 0, 0,

  100, 0, 0, 100, 200, 100, 100, 0, 100, 100, 0, 0, 100, 200, 0, 100, 200, 100,
]);

顶点索引

不难发现上面我们实现立方体的时候,出现了很多重复坐标,我们可以通过给各个顶点建立索引从而减少重复书写,整体步骤如下:

  • 定义顶点数据时,删除重复的顶点数据
  • 通过Uint16Array创建索引数据: 使用每组顶点数据的索引值,排列出新的顶点顺序
  • 将顶点索引赋值到缓冲几何体index属性上

具体实操一下:

// 定义顶点数据 将长方体八个顶点坐标写好,
const vertices = new Float32Array([
  0,0,0, // 索引为0的顶点
  100,0,0, // 索引为1的顶点
  100,0,100, //索引为2的顶点
  0,0,100, //索引为3的顶点
  0,200,0, //索引为4的顶点
  0,200,100, // 索引为5的顶点
  100,200,100, // 索引为6的顶点,
  100,200,0, // 索引为7的顶点
]);
// 定义索引顺序
//定义索引数据 其实就是拿着顶点索引,排列出来三角形,索引值代表的就是对应的顶点
const indexs = new Uint16Array([
  0,4,7, // 第一个
  0,7,1, // 第二个

  1,7,2, //
  2,7,6, //

  3,5,4, //
  3,4,0, //

  0,1,2, //
  0,2,3, //

  4,6,7, //
  4,5,6, //

  2,6,3, //
  3,6,5,
]);

// 转换为几何数据
const attribut = new THREE.BufferAttribute(vertices, 3);
// 设置几何数据
geometry.attributes.position = attribut;
// 转换为几何体顶点数据
const attrIndex = new THREE.BufferAttribute(indexs, 1); // 1表示每一个索引都为一个顶点数据
// 设置索引数据
geometry.index = attrIndex;

原理就是为每个顶点建立 索引,每个索引代表一个顶点,然后由这些顶点组成三角形

顶点法线

上文中定义的缓冲几何体使用的材质为MeshBasicMaterial材质,它不受光照影响,如果我们将材质换成受光照影响的材质如MeshLambertMaterial,会发现模型无法正常渲染了(非环境光),这是因为我们没有定义形状的法线数据,

什么法线

数学上法线的概念是: 平面的垂线,如果是光滑的曲线,一个点的垂线就是该点切面的垂线

法线的作用:

当物体受光照的时候,根据入射角的不同,three会做出不同的反射,那么如何确定入射角度呢,答案就是通过照射到的三角平面的法线来确定,

一个平面的法线由组成这个平面的顶点法线决定,三角形是组成平面的最小单位,因此只需要知道三个顶点的法线就可以知道整个平面的法线了

image-20240202173121735.png

定义顶点法线

和顶点的坐标数据一样,每个顶点都有一个对应的法线数据,下面是定义的案例:

const normals = new Float32Array([
    0, 0, 1, //顶点1法线( 法向量 ) x,y,z
    0, 0, 1, //顶点2法线
    0, 0, 1, //顶点3法线
    0, 0, 1, //顶点4法线
]);
// 设置几何体的顶点法线属性.attributes.normal
geometry.attributes.normal = new THREE.BufferAttribute(normals, 3);

注意: 顶点法线数据是和顶点坐标数据一一对应的,

案例(将顶点法线修改成其他值便可以看到法线对光线反射的影响):

const geometry = new THREE.BufferGeometry();
// 定义顶点数据 矩形
const vertices = new Float32Array([0, 0, 0, 100, 0, 0, 0, 200, 0, 100, 200, 0]);
//定义索引数据 其实就是拿着顶点索引,排列出来三角形
const indexs = new Uint16Array([0, 1, 2, 2, 1, 3]);
​
// 定义顶点法线
const normals = new Float32Array([
  0,0,1, //顶点1法线
  0,0,1, //顶点2法线
  0,0,1, //顶点3法线
  0,0,1, //顶点4法线
]);
​
geometry.attributes.normal = new THREE.BufferAttribute(normals, 3);

自带几何体的几个属性

材质属性 .wireframe

材质添加这个属性之后可以显示出几何体的三角形结构

const material = new THREE.MeshBasicMaterial({
  wireframe: true,
});

几何体细分数

形状的构造函数中还有可选参数用来设置几何体的细分数,不同形状的几何体参数个数可能不同,具体产看文档即可;这里拿立方体BoxGeometry和球SphereGeometry举例子:

BoxGeometry:

构造函数参数:
width — X 轴上面的宽度,默认值为 1。
height — Y 轴上面的高度,默认值为 1。
depth — Z 轴上面的深度,默认值为 1。
widthSegments — (可选)宽度的分段数,默认值是 1。
heightSegments — (可选)高度的分段数,默认值是 1。
depthSegments — (可选)深度的分段数,默认值是 1。
​
const box = new THREE.BoxGeometry(100, 100, 100, 2, 2); // 宽度分为两份,高度分为两份
const material = new THREE.MeshBasicMaterial({
  wireframe: true,
});
const mesh = new THREE.Mesh(box, material);

image-20240202183700640.png

SphereGeometry:

const box = new THREE.SphereGeometry(100, 32, 16);
const material = new THREE.MeshBasicMaterial({
  wireframe: true,
});
const mesh = new THREE.Mesh(box, material);

下面设置细分数为(3,3,)和(32,16)的效果区别:

image-20240202184106539.png

细分数的影响

从上文球体的例子可以看出,对于曲面而言,细分数越多表面越光滑,

细分数越大,三角形越多,顶点数就越多,这些数量直接影响three的性能因此: 在不影响渲染效果的前提下细分数越少越好,

旋转缩放平移

BufferGeometry缓冲几何体通过scale(),translate(),rotateX()等方法可以对几何体本身进行缩放,平移,旋转,本质上是改变几何体的顶点数据,

image-20240202232626467.png

其他继承自BufferGeometry的几何体也具有相同方法

scale缩放

bufferGeomotry.scale(x, y, z);
//x,y,z 在三个方向上缩放多少倍

translate平移

buggerGeomotry.translate(x,y,z)
// x,y,z在三个方向上平移的距离

rotateX rotateY rotateZ

bufferGeomotry.rotateX(Math.PI / 2); // 设置在X轴方向上旋转45度

模型对象和材质

Vector3三维向量类

上一小节尝试了几何体的旋转,缩放,平移,几何体之所以可以使用scale,translate,rotate属性,是因为这些属性方法在几何体的父类BufferGeomotry中定义了,接下来实验一下模型的缩放,平移和旋转.mesh,line,point都有一个父类Object3D,父类中定义了相关方法,因此可以直接用:

mesh.translateX(100);
mesh.rotateX(0.1);
mesh.scale.set(2, 1, 1);

三维向量

Vector3这个类称为三维向量,three将一些属性的值也封装成了类,比如position,scale等这些属性的值都是Vector3的实例,因此可以从上面的例子看到设置scale属性的时候,我们调用的是set方法,之所以能调用,是应为scale属性值为Vector3的实例,Vector3类中定义了这些方法.

三维向量对象有三个属性x,y,z代表三个方向的分量.该对象本身还有很多方法,这里举几个例子,具体的可以查阅文档.

.set方法

设置x,y,z的值

.normalize ()

在平移时可以通过该方法让模型按照自定义的方向移动:

const axis = new THREE.Vector3(3,1,1) // 自定义的三个方向向量
axis.normalize() // 向量归一化,可以理解长方向
mesh.translateOnAxis(axis,100) // 指定哪个向量,并移动多少方向

Euler 类

模型的位置可以通过position属性知道,同样的模型的角度也可以通过某个属性知道:

image-20240203212636602.png

模型的角度属性有两种表示方法:

  • .rotation: 值为欧拉对象Euler
  • .quaternion: 值为四元数对象Quaternion

欧拉对象Euler

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

由于角度属性的值为欧拉对象,那么需要修改这个值的时候可以通过Euler中的方法,

对于rotateX等旋转方法,实际上是更改的rotation属性的值

绕自定义轴旋转

通过Vector3自定义一个方向,通过rotateOnAxis执行旋转

const axis = new THREE.Vector3(0,1,0);//向量axis
mesh.rotateOnAxis(axis,Math.PI/8);//绕axis轴旋转π/8

Color类

Color类是材质颜色属性的值,用来控制模型颜色,如果想修改颜色的值,可以查看Color类提供了哪些方法

材质的父类Material

基础材质,漫反射材质,高光材质,物理材质的父类,

模型的材质和几何体

模型是由几何体+材质组成的,在模型上可以通过geometry和material属性直接访问模型的几何体和材质对象,并重新设置属性.修改时要注意如果两个模型共用同一个材质或几何体对象,那么修改其中一个另一个也会跟着改变

克隆和复制

三维向量,模型,材质,几何体等都具有clonecopy方法

clone

复制一个和原来对象一样的对象

const mesh = new THREE.Mesh(box,material);
const newMesh = mesh.clone() // 克隆mesh赋值给newMesh

copy

将一个对象的属性的属性值赋值给另一个对象

const v1 = new THREE.Vector3(1,1,1);
const v2 = new THREE.Vector3(1,2,2);

v2.copy(v1) // 将v1的属性值赋值给v2

Mesh的克隆

模型克隆之后,新模型和旧模型的材质和几何体是共用的,一个模型改变了材质或几何体的属性,另一个会跟着变,如果想彻底切断联系,需要将几何体和材质也克隆下来

const mesh = new THREE.Mesh(box, material);

const mesh2 = mesh.clone();
mesh2.material = mesh.material.clone();  // 材质也克隆一下,并赋值给新模型

层级模型

Group层级模型

Group层级模型可以把多个模型进行分组,通过Group类创建一个层级模型对象group,通过add方法将其他模型添加到层级模型中,场景对象中直接添加组对象便可以把组对象中的所有模型都添加到场景中

const mesh1 = new THREE.Mesh(geometry,material)
const mesh2 = new THREE.Mesh(geometry,material)

const group = new THREE.Group()
group.add(mesh1,mesh2)

scene.add(group)

上述代码最终形成如下树形结构:这也是Group做的事情,将子对象添加进来构成树结构

image-20240204093056288.png

group也属于模型的一种和其他模型一样父类为Object3D

.children

模型添加到组中之后,实际上是添加到了group.children属性上

.add方法

该方法继承自object3D,依次可以添加一个,也可以添加多个

组对象的变换

组对象变换时,内部的子对象会跟着一起变化

object3D作为模型节点

object3D也可以和Group一样作为模型分组来使用,毕竟group的父类就是object3D,但Group更有语义性

遍历层级模型

.name

那么属性是object3D上的属性,用于对模型,分组等进行标记,下面简单构建一个小区的group并命名,结构大概如下:

image-20240204102104672.png

​
const group = new THREE.Group();
group.name = "小区";
​
const heigh = new THREE.Group();
heigh.name = "高层";
const cottages = new THREE.Group();
cottages.name = "别墅";
​
const floor = new THREE.BoxGeometry(100, 100, 100);
const houses = new THREE.BoxGeometry(100, 200, 100);
const material = new THREE.MeshBasicMaterial();
​
for (let i = 0; i < 10; i++) {
  const mesh = new THREE.Mesh(floor, material);
  mesh.position.set(120 * (i + 1), 0, 0);
  mesh.name = `第${i + 1}层`;
  heigh.add(mesh);
​
  const mesh2 = new THREE.Mesh(houses, material);
  mesh2.name = `第${i + 1}栋别墅`;
  mesh2.position.set(0, 0, 120 * (i + 1));
  cottages.add(mesh2);
}
​
group.add(cottages, heigh);

层级模型的遍历

通过.traverse方法可以遍历层级模型的树形结构,参数是一个回调函数,回调函数的参数是每一个节点对象

group.traverse(function (obj) {
  if (obj.isMesh) {
    console.log(obj.material.color.set("#333"));
  }
});

.getObjectByName

通过name属性查找树形结构中对应的节点

const a = group.getObjectByName("第1层");
a.material.color.set("#fff");

本地坐标和世界坐标

本地坐标:模型的.position属性

世界坐标: 模型的.position属性和所有父对象的.position属性的和

.getWorldPosition

获取模型的世界坐标,这样我们旧不用一个一个累加了

// 首先创建一个三维向量,用来存储获取的世界坐标
const vector3 = new THREE.Vector3()
//获取模型的世界坐标
mesh.getWorldPosition(vector3)

console.log("本地坐标", mesh.position);
console.log("世界坐标", vector3);

子对象辅助局部坐标系

一个以自身为原点的坐标系,无论模型怎么移动一直跟随着模型

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

修改相对局部坐标系的位置

我们知道局部坐标系随着模型的位置改变而改变,并以模型中心为原点,那么如何修改模型相对局部坐标的位置呢?

答案就是通过修改几何体顶点位置实现

const box = new THREE.BoxGeometry(100, 100, 100);
const material = new THREE.MeshBasicMaterial();
const mesh = new THREE.Mesh(box, material);

// 添加局部坐标系,正常情况下模型和局部坐标系是重合的
const meshAxesHelper = new THREE.AxesHelper(200);
mesh.add(meshAxesHelper);

// 修改几何体的顶点位置,会发现模型相对局部坐标的位置变化了
box.translate(50, 80, 0);

再旋转一下看看:

const box = new THREE.BoxGeometry(100, 100, 100);
const material = new THREE.MeshBasicMaterial();
const mesh = new THREE.Mesh(box, material);

// 添加局部坐标系,正常情况下模型和局部坐标系是重合的
const meshAxesHelper = new THREE.AxesHelper(200);
mesh.add(meshAxesHelper);

// 修改几何体的顶点位置,会发现模型相对局部坐标的位置变化了
box.translate(50, 80, 0);

// 添加旋转动画 绕x轴旋转 ,由于修改了几何体位置,可以看到模型旋转并没有绕x轴旋转,
function render() {
  mesh.rotateX(0.1);
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}
render();

通过实验可以知道:模型的移动实际作用到了局部坐标系的移动,而几何体的移动才是相对局部坐标系的移动

.remove

与add相反,从父对象中删除子对象

模型的隐藏和显示

模型的父类object3D中封装了一个.visible,该方法可以控制模型的隐藏和显示

mesh.visible = true;
mesh.visible = false;

材质的visible属性

材质的父类Material也有.visible属性,可以控制使用了该材质的所有模型的隐藏和显示

mesh.material.visible = false; //使用同一个材质的模型将一同隐藏

顶点UV坐标和纹理贴图

纹理贴图

效果: 通过纹理贴图,将一张图片贴在模型上

实现:

通过TextureLoader类实例化一个加载器对象,通过加载器对象的load方法生成纹理贴图对象,将生成的纹理贴图对象赋值给材质map属性即可:

const box = new THREE.BoxGeometry(100, 100, 100);

// 创建一个纹理贴图加载器
const texLoader = new THREE.TextureLoader();
//通过加载器加载图片,生成贴图
const texture = texLoader.load("./1434094552.png");

const material = new THREE.MeshBasicMaterial({
  map: texture,
});
const mesh = new THREE.Mesh(box, material);

效果:

image-20240204152117626.png

注意: 设置了材质的颜色属性会影响最终的效果,最终显示贴图的颜色和材质颜色混合的结果

UV坐标

UV坐标的作用: 将贴图中像素的坐标提取出来,映射到网格模型的几何体表面,

UV坐标系

UV坐标系,以V为竖轴,U为横轴,范围(0,1),通过坐标来描述一张图片,纹理贴图左下角对应的UV坐标是(0,0)右上角对应的坐标(1,1):

坐标(0,0),(1,0),(1,1),(0,1)就表示的是将整张图贴到物体表面

image-20240204154808086.png

自定义UV坐标

顶点UV坐标geometry.attributes.uv和顶点位置坐标geometry.attributes.position是一一对应的,自定义uv坐标可以自由的将图片的一部分映射Mesh的几何表面上

当顶点位置和uv坐标对应时:

const bufferGeometry = new THREE.BufferGeometry();
​
const buffer = new Float32Array([0, 0, 0, 100, 0, 0, 100, 100, 0, 0, 100, 0]);
const indexs = new Uint16Array([0, 1, 2, 0, 2, 3]);
const attrIndexs = new THREE.BufferAttribute(indexs, 1);
const attribute = new THREE.BufferAttribute(buffer, 3);
bufferGeometry.attributes.position = attribute;
bufferGeometry.index = attrIndexs;
​
const uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]); // 自定义uv坐标
const attrUvs = new THREE.BufferAttribute(uvs, 2); // 两个数字为一个坐标
bufferGeometry.attributes.uv = attrUvs;
​
const textureLoder = new THREE.TextureLoader();
const texture = textureLoder.load("./public/img/砖墙.jpg");
​
const material = new THREE.MeshBasicMaterial({ map: texture });
​
const mesh = new THREE.Mesh(bufferGeometry, material);

贴图是一整张图片:

image-20240204164418680.png

当顶点坐标对应的uv坐标不是整张图片时,只将下半部分图片贴到物体上:

const uvs = new Float32Array([0, 0, 1, 0, 1,0.5, 0,0.5 ]); // 自定义uv坐标

效果:

对于多面体,每个面都会使用uv坐标去贴图

圆形平面纹理贴图

如果想将一个图片裁剪成圆形渲染,可以利用圆形几何体,圆形几何体的UV坐标会对颜色纹理贴图.map提取,自动提取一个圆形轮廓

贴图阵列

将一个贴图在模型表面重复,只会在模型范围内重复指定数值:

const plane = new THREE.PlaneGeometry(100, 100);

const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("./public/img/瓷砖.jpg");

// 开启阵列, 无限平铺
texture.wrapS = THREE.RepeatWrapping; // U方向
texture.wrapT = THREE.RepeatWrapping; // V方向
//设置阵列数
texture.repeat.set(12, 12);

const material = new THREE.MeshBasicMaterial({ map: texture });
const mesh = new THREE.Mesh(plane, material);

透明贴图

如果有的贴图背景是透明的,如果直接贴到模型上,即使背景透明也会渲染出来,这时需要将开启材质的透明属性才可以完全渲染成透明背景

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

网格地面辅助观察器

const gridHelper = new THREE.GridHelper(300, 25, 0x004444, 0x004444);
scene.add(gridHelper);
参数: 坐标尺寸,网格细分数,中线颜色,坐标格网格线的颜色

用offset实现uv动画

offset: vector2D 表示纹理贴图在模型上的偏移量,实现动画的原理:

  1. 设置贴图无限平铺
  2. 累加/减贴图的offset值,
  3. 当贴图在mesh上偏移的时候,mesh上就有贴不到的表面,这时会自动repeat

代码:

// 开启阵列 无限平铺
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;

//修改offset
function render() {
  texture.offset.x += 0.01;
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}
render();

效果:

offset动画.gif

加载外部模型

加载流程

  1. 实例化GLTFLoader加载器,可以加载gltf,glbl格式的文件,加载其他格式的文件需要使用其他加载器
  2. 设置加载模型的路径,在加载器对象的onload参数中获取到加载的模型对象,添加早到场景中
  3. 根据模型尺寸,设置相机的位置,保证正常显示
  4. 设置webgl渲染器的编码方式(新版本的不用设置)

加载一个模型示例:

import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
​
const group = new THREE.Group();
const gltfLoader = new GLTFLoader();
gltfLoader.load(
  "../public/three.js-master/examples/models/gltf/Flamingo.glb",
  (gltf) => {
    group.add(gltf.scene); // 模型对象的scene是要给组,包含该模型的所有模型
  }
);
​
export { group };

尺寸的概念

对于模型来说一般需要一个尺寸,不用太精确,需要有个大概把握就可以,可以使用blender软件测量一下

单位问题

three中没有单位,只有尺寸,但在正常协作开发中,一般都会定义一个单位比如m,做到单位统一,方便协作沟通

相机的选择和设置

透视投影相机符合近大远小的规律,不需要模拟人眼的近大远小的投影规律可以使用正投影相机.大部分3D项目都是用透视投影相机

想要模型在canvas画布上居中显示,需要将相机的lookAt设置为相机的位置即可,对于相机控件也会影响相机的lookAt的值,需要在设置控件的target值为相机的lookAt的值

controls.target.set(camera,position);
controls.update()

通过相机控件设置相机参数

相机控件本质上就是修改的相机位置参数,因此在不知道如何设置合适的相机参数时,可以通过相机控件打印相机的位置信息等在页面上找到合适的位置后,将当前的位置信息赋值给相机对应的参数

control.addEventListener('change',function(){
    console.log(camera.position)
    console.log(control.target) // 相机控件移动时,.target属性会变化,该属性对应的就是相机的`lookAt`
})

模型命名和查找

  1. 命名:模型的名称在前端和美工之间就像特殊的API接口,需要提前商议,便于之后的工作展开,

  2. 查找模型: 一个gltf模型导入后如何查找整个模型中某个子模型呢

    1. gteObjectByName()可以通过模型名称在gltf.scene中直接获取
    2. 通过分组管理,可以快速获取某一个层级的模型实现批量修改

外部模型共享材质

加载外部模型时,可能模型a模型b共用了一种材质,也可能不共用

如何判断

  1. .name标记材质,给一个模型材质添加name作为标记,查看另一个模型的材质是否也有相同的标记,若相同那么说明两模型共用一个材质

    mesh1.material = '这是模型1的材质' // 给材质添加标记
    mesh2.material.name === '这时模型1的材质'
    
  2. 直接修改一个模型的材质属性,如颜色等,看有哪些模型的颜色跟着变化了,变化的即共享材质的模型

如何解决

  1. clone通过遍历模型将模型的材质设置为clone后的材质,保证每个模型的材质是独立的
  2. 在3D建模软件中直接修改

颜色空间编码属性

纹理贴图对象,渲染器,gltf模型都有一个属性: 颜色空间编码属性,渲染时三者该属性的值要保持一致,否则会有颜色偏差

  • THREE.LinearEncoding:线性颜色空间
  • THREE.sRGBEncoding:sRGB颜色空间

现在版本:

THREE.NoColorSpace = "" // 空
THREE.SRGBColorSpace = "srgb" // srgb空间
THREE.LinearSRGBColorSpace = "srgb-linear" // 线性空间

默认值:

纹理贴图对象: texture.encoding: 默认值THREE.NoColorSpac;// 默认是""
渲染器: renderer..outputColorSpace 默认值: BColorSpace;//srgb

PBR材质和纹理贴图

PBR材质就是基于物理渲染的材质,three提供了两个APIMeshStandardMaterialMeshPhysicalMaterial,MeshPhysicalMaterialMeshStandardMaterial扩展的子类,提供了更多功能属性。物理材质具有更复杂的光照模型,渲染出的模型更加真实

  • 占用渲染资源 MeshBasicMaterial < MeshLambertMaterial < MeshPhongMaterial < MeshStandardMaterial < MeshPhysicalMaterial
  • 渲染表现能力 MeshBasicMaterial < MeshLambertMaterial < MeshPhongMaterial < MeshStandardMaterial < MeshPhysicalMaterial

材质金属度和粗糙程度

讲解MeshStandardMaterial材质的金属度和粗糙属性

金属度属性

.metalness,表示像金属的程度,取值范围0.0~1.0,默认值0.5,非金属使用0.0,金属使用1.0,生锈的金属用0.0-1.0表示

const mate = new THREE.MeshStandardMaterial({
    metalness: 0.5;
})

粗糙度属性

.roughness,表示粗糙程度,取值0.0-1.0,越光滑反射能力越强,越粗糙反射能力越弱,0.0表示平滑的镜面反射,1.0为漫反射,默认0.5

const mate = new THREEMeshSrandar({
    roughness: 0.5
})

环境贴图

环境贴图就是一个物体周围的六个面的环境是怎么样的,对PBR材质的渲染效果影响很大,最好设置一下.

立方体纹理加载器

一个可以一次性加载六张图片的纹理加载器:CubeTextureLoader,用以加载物体的环境贴图,

// 加载周围环境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 

设置环境纹理贴图

赋值给材质的`.envMap属性
​
material.envMap = textureCube

为什么影响较大?

真实场景中物体的表面效果受所处环境反射的光的影响,用环境贴图就是模拟这种物体所在环境

环境贴图反射率

MeshStandardMaterial.envMapIntensity属性表示模型表面反射周围环境的能力,或者说这个属性可以控制环境贴图对模型表面的影响,该值相当于系数,环境贴图像素值乘以该系数后作用于模型表面.取值范围0-1

material.envMapIntensity = 0.0; // 相当于没有设置环境贴图

场景环境贴图

一个一个模型设置环境贴图envmap比较繁琐,可以通过scene场景的环境贴图属性给全部模型设置环境贴图

scene.environment = cubTexture

物理材质

物理材质属于PBR材质的一种,是标准材质的子类,其中扩展了很多属性: 清漆属性clearcoat,透光率.transmission,反射率.reflectivity,光泽.sheen,折射率.ior等,下面对上述属性逐一实验

清漆属性

一层透明图层,像在表面刷了一层清漆一样,0~1,

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

透光率

用以模拟半透明材质,如玻璃,塑料,与透明度.opacity不同,透光率.transmission在透明情况下依然保持反射率,取值范围0~1,默认0

mesh.material = new THREE.MeshPhysicalMaterial({
    transmission: 1.0, //玻璃材质透光率,transmission替代opacity 
})

折射率

ior非金属材料折射率在1~2.333之间,默认0.5,不同材料的折射率百度搜索即可

new THREE.MeshPhysicalMaterial({
    ior:1.5,//折射率
})

玻璃材质的设置

new Three.MeshPhysicalMaterial({
        metalness: 0.0,//玻璃非金属 
    roughness: 0.0,//玻璃表面光滑
    envMap:textureCube,//环境贴图
    envMapIntensity: 1.0, //环境贴图对Mesh表面影响程度
    transmission: 1.0, //玻璃材质透光率,transmission替代opacity 
    ior:1.5,//折射率
})

转换为图片

可以通过canvas将canvas转换成图片,步骤如下:

  1. 开启渲染器的preserveDrawingBuffer是否保留缓存直到手动清除或被覆盖
  2. 通过toDataURL获取cnavas元素的数据
  3. 通过a标签点击下载
  // 创建a标签
  const a = document.createElement("a");
  //赋值a标签的href属性
  a.href = canvasDom.toDataURL("image/png"); // 点击时,会下载href的东西
  //设置下载名称
  a.download = "three.png";
  //点击a标签
  a.click();

画布背景透明图

默认的canvas背景是黑色的,通过设置透明度,可以显示出canvas后的元素,制造一种3D模型悬浮的感觉,通过设置渲染器的相关属性可以达到这个效果

setClearAlpha

修改透明度的值

renderer.setClearAlpha(0) // 完全透明 取值0-1

alpha

直接设置背景透明,默认dalse

new WebGLRenderer({
    alpha: true
})

.setClearColor()

该方法的第二个参数可以设置渲染器背景颜色的透明度

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

深度冲突

如果两个模型挨得太近,或者摄像机离得太远就可能出现闪烁的问题,下图是两个矩形平面模型距离为0.01时的效果,

image-20240207170709659.png

产生闪烁的原因: 两个模型离的太近了,电脑GPU分不清谁在前谁在后

解决方式

  1. 适当拉开两个模型距离

  2. 建模时避免两个模型挨得太近

  3. 避免相机离的太远,相机遵从近大远小的规律,相机离模型越远,对相机来说两个模型越近

  4. 设置渲染器的深度缓冲区,正常情况下是有效的,但设置的距离过小,也会导致该设置失效

    // WebGL渲染器设置
    const renderer = new THREE.WebGLRenderer({
        // 设置对数深度缓冲区,优化深度冲突问题
        logarithmicDepthBuffer: true
    });
    

手写模型加载进度条

模型记载进度可以通过GLTF加载器的回调函数拿到,加载器第三个回调onProgress,加载模型时触发,参数包含.loaded和total

html结构:

<style>
.progress {
    position: fixed;
    bottom: 50px;
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
  }
  .wrap {
    width: 500px;
    height: 10px;
    border: none;
    border-radius: 5px;
    background-color: #d4dee7;
  }
  .child {
    width: 0;
    height: 10px;
    border: none;
    border-radius: 5px;
    background-color: #ee8e1e;
  }
  .number {
    padding-left: 10px;
    color: #fff;
  }
  .hidden {
    display: none;
  }
</style>
<div class="progress">
  <div class="wrap"><div class="child"></div></div>
  <p class="number">0.00%</p>
</div>

js:

import * as Three from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
​
const pro = document.querySelector(".progress");
const wrap = document.querySelector(".wrap");
const child = document.querySelector(".child");
const number = document.querySelector(".number");
const wrapWidth = wrap.clientWidth;
console.log("wrapWidth:", child.clientWidth);
​
export const group = new Three.Group();
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath(
  "../public/Three.js-master/examples/jsm/libs/draco/"
);
loader.setDRACOLoader(dracoLoader);
loader.load(
  "../案例/Three.js视频教程源码b站/7.PBR材质与纹理贴图/轿车.glb",
  (gltf) => {
    group.add(gltf.scene);
    pro.classList.add("hidden");
  },
  (progress) => {
    pro.classList.remove("hidden");
    const { loaded, total } = progress;
    const proportions = loaded / total;
    child.style.width = `${proportions * wrapWidth}px`;
    number.innerHTML = `${(proportions * 100).toFixed(2)}%`;
  }
);

生成曲线和几何体

生成圆弧

利用顶点坐标和算法生成圆弧

const R = 10; // 半径
const N = 100; // 分段数量
const sp = (2 * Math.PI) / N; // 每段的弧度
​
let arr = []; // 存放计算出的顶点坐标
for (let i = 0; i <= N; i++) {
  const nSp = sp * i;
  const x = Math.sin(nSp) * R;
  const y = Math.cos(nSp) * R;
  arr.push(x, y, 0);
}
​
const vertices = new Float32Array(arr);
const attribut = new Three.BufferAttribute(vertices, 3);
const geometry = new Three.BufferGeometry({
  color: 0xff000,
});
//设置几何数据
geometry.attributes.position = attribut;
//材质
const material = new Three.MeshBasicMaterial();
const mesh = new Three.LineLoop(geometry, material);

曲线

可以用来实现飞行轨迹之类的效果

上面我们使用了算法生成曲线,其实three自带了很多种曲线,可以直接使用,他们都有一个共同的父类Curve:

image-20240219112803087.png

通过曲线类,可以直接获取到曲线的顶点坐标,不需要再计算:

实例化椭圆

const curve = new Three.EllipseCurve(0, 0, 10, 10, 0, 2 * Math.PI)

获取椭圆顶点数据

参数为曲线分段数,50时将返回51个顶点坐标

通过.getSpacedPoints()是按照曲线长度等间距返回顶点数据,.getPoints()获取点的方式并不是按照曲线等间距的方式,而是会考虑曲线斜率变化,斜率变化快的位置返回的顶点更密集。

const pointArr = curve.getPoints(50);

生成椭圆曲线形状

通过缓冲区几何体的setFromPoints方法,可以将pointArr中的坐标赋值给geometry.attributes.position属性

const geometry = new Three.BufferGeometry();
geometry.setFromPoints(pointArr);
​
// 创建材质
const material = new Three.LineBasicMaterial({ color: "#997122" });
// 创建模型
const mesh = new Three.Line(geometry, material);

样条曲线

//样条曲线
const arr = [
  new Three.Vector3(-50, 20, 90),
  new Three.Vector3(-10, 40, 40),
  new Three.Vector3(0, 0, 0),
  new Three.Vector3(60, -60, 0),
  new Three.Vector3(70, 0, 80),
];
// 三维样条曲线
const curve = new Three.CatmullRomCurve3(arr);
const pointArr = curve.getPoints(6);
//设置顶点坐标
const geometry = new Three.BufferGeometry();
geometry.setFromPoints(pointArr);

贝塞尔曲线

// 贝塞尔曲线
// p1、p2、p3表示三个点坐标
// p1、p3是曲线起始点,p2是曲线的控制点
const p1 = new Three.Vector2(-80, 0);
const p2 = new Three.Vector2(100, 80);
const p3 = new Three.Vector2(80, 0);
const curve = new Three.QuadraticBezierCurve(p1, p2, p3);
​
const pointsArr = curve.getPoints(100); //曲线上获取点
const geometry = new Three.BufferGeometry();
geometry.setFromPoints(pointsArr);

管道 TubeGeometry

沿着曲线生成一个管道

const path = new THREE.CatmullRomCurve3([
  new THREE.Vector3(-50, 20, 90),
  new THREE.Vector3(-10, 40, 40),
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(60, -60, 0),
  new THREE.Vector3(70, 0, 80),
]);
​
// path:路径   40:沿着轨迹细分数  2:管道半径   25:管道截面圆细分数
const geometry = new THREE.TubeGeometry(path, 40, 2, 25);
​
const material = new THREE.MeshLambertMaterial({
  side: THREE.DoubleSide, //双面显示看到管道内壁
});
​
const mesh = new THREE.Mesh(geometry, material);

旋转成型LatheGeometry

基于曲线旋转得到几何体曲面

LatheGeometry(points, segments, phiStart, phiLength)
points: 坐标数据数组
segments: 圆周方向细分数 默认12
phiStart: 开始角度,默认0
phiLength: 旋转角度,默认2PI

拉伸成型ExtrudeGeometry

将一个平面通过平移生成几何体

// Shape表示一个平面多边形轮廓
const shape = new THREE.Shape([
  // 按照特定顺序,依次书写多边形顶点坐标
  new THREE.Vector2(-50, -50), //多边形起点
  new THREE.Vector2(-50, 50),
  new THREE.Vector2(50, 50),
  new THREE.Vector2(50, -50),
]);
​
const geometry = new THREE.ExtrudeGeometry(shape, {
  depth: 20,
  bevelThickness: 5, //倒角尺寸:拉伸方向
  bevelSize: 5, //倒角尺寸:垂直拉伸方向
  bevelSegments: 20, //倒圆角:倒角细分精度,默认3
});
const material = new THREE.MeshBasicMaterial();
​
const mesh = new THREE.Mesh(geometry, material);

除此之外,拉伸还可以沿着设定的曲线进行:

// 轨迹:创建轮廓的扫描轨迹(3D样条曲线)
const curve = new THREE.CatmullRomCurve3([
    new THREE.Vector3( -10, -50, -50 ),
    new THREE.Vector3( 10, 0, 0 ),
    new THREE.Vector3( 8, 50, 50 ),
    new THREE.Vector3( -5, 0, 100)
]);
const geometry = new THREE.ExtrudeGeometry(shape, {
    extrudePath:curve,//扫描轨迹 属性值为三D样条曲线
});
​