0%

three基础篇

基础

材质

基类:Material

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

1702368339909

1702368442450

整体上来看,就是渲染表现能力越强,占用的计算机硬件资源更多。

  • 占用渲染资源 MeshBasicMaterial < MeshLambertMaterial < MeshPhongMaterial < MeshStandardMaterial < MeshPhysicalMaterial
  • 渲染表现能力 MeshBasicMaterial < MeshLambertMaterial < MeshPhongMaterial < MeshStandardMaterial < MeshPhysicalMaterial
材质或集合体共享

我们可以直接访问并修改物体的材质对象,因为材质事共享的如果对其进行修改该用到该材质的物体都会发生改变

我们同时也可以直接访问并修改物体的集合体对象,若有其他物体也是用到这个集合体 修改后也会造成影响。

1702370190692

三维变量的克隆和复制方法

let v1 = new three.Vector(1,2,3)

letv2 = v1.clone() //此时会克隆一个与v1一样属性与值得对象

let v3 = {}

v3.copy(v1)可将v1得值赋值给v3

若是物体得克隆 他的材质与集合体不单独进行克隆得话 那么材质和集合体还是存在共享问题

1702368996979

顶点:

顶点的每个坐标对应一个顶点法线 ,法线关系光照后阴影的角度。

物体的属性有一些列的属性:position,rotation,scalr等属性 还有相关的方法,其都会继承父级构造函数的属性和方法

向量:normalize 将向量转为单位向量 长度为1

1702368060275

组对象group

1702435513665

子对象:group得children属性下可以查看到这个组对象下面得所有子对象。

子对象任何变化始终都是根据着父节点进行的。我们也可以直接使用Object3D创建一个节点对象,也可以给网格添加子对象。

通过设置name 可以给相应的物体设置指定的名字。

Object3D.traverse遍历所有的子节点

Object3D.getObjectByName() 获取指定名称的节点

本地坐标:当前物体的的坐标就是position的位置

Object3D.postion 表示物体的局部位置

世界坐标:本地坐标加上所有上级的坐标累加坐标,是为子集物体始终是相对于父级物体进行偏移的

Object3D.getWordPosition(v3):将当前物体的三维坐标存在一个三维向量中,可以获取当这个物体的世界坐标

当我们通过translate修改了集合体顶点坐标的时候,该物体的局部坐标系并没有发生改变,所有我们对这个物体(object3d)的一些列操作(如rotate)这个物体模型会相对于坐标原点进行

物体隐藏visible:物体object3d 隐藏或显示一个模型。 隐藏父级物体 他的子集物体也会被隐藏

材质隐藏visible:材质materail 通过该属性可以控制是否隐藏该材质对应的模型对象。 相同材质的隐藏的话 所有使用到此材质的物体的模型都会被隐藏

object3d.remove: scene , mesh ,都有删除这个方法 与add添加模型对立,可以删除他的子代模型 如“senec.remove(light)删除环境光,因为光照也是通过add添加的 ,场景模型多的话可以遍历子代模型条件删除一些模型。在进行删除尝试中发现了一个微妙的问题,就是在遍历traverse中不建议直接删除当前的属性。会出现一些奇怪的问题

1702544424552

模型数的遍历

object3D.traverse() 遍历了下面的所有子子节点。

object3D.getObjectByName() 根据Object.name来查找对应的名字的模型

object3D.visible 控制模型的显示与隐藏

模型:gltf

包含内容:几乎包含了该模型的所有数据 模型的层级关系,rgb材质,纹理材质,骨骼动画,变形动画…

格式信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"asset": {
"version": "2.0",
},
...
// 模型材质信息
"materials": [
{
"pbrMetallicRoughness": {//PBR材质
"baseColorFactor": [1,1,0,1],
"metallicFactor": 0.5,//金属度
"roughnessFactor": 1//粗糙度
}
}
],
// 网格模型数据
"meshes": ...
// 纹理贴图
"images": [
{
// uri指向外部图像文件
"uri": "贴图名称.png"//图像数据也可以直接存储在.gltf文件中
}
],
"buffers": [
// 一个buffer对应一个二进制数据块,可能是顶点位置 、顶点索引等数据
{
"byteLength": 840,
//这里面的顶点数据,也快成单独以.bin文件的形式存在
"uri": "data:application/octet-stream;base64,AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/.......
}
],
}

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

load(’模型地址’,加载成功后的函数,加载时的相关信息):我们可以在加载函数中再次处理这个模型,如修改模型的相关属性,可以通过第三个参数来设置相关进度条…

查询模型节点:getObjectName() 根据模型的name属性来查询谋和模型对象

遍历模型traverse:

查看gltf的默认材质

材质相同问题:模型材质共享的问题判断材质的name 是否相同,

解决方案:1,三维建模软件中设置,需要代码改变材质不要共享,要独享材质。2,代码批量更改,克隆材质对象,重新赋值给mesh的材质属性.

1
2
3
4
5
6
gltf.scne.getObjectByName('汽车').traverse(function(item){
if(item.isMesh){
item.material = obj.material.clone()
}
})

纹理

如果没有特殊的需求,一般为了正常渲染,避免色差,three.js中贴图的颜色空间编码属性encoding要与webGL渲染器的outputEncoding属性(最新版本中属性名已改为 outputColorSpace )保持一致。

纹理对象的encoding有多个属性:THREE.LinearEncoding(线性颜色空间:3000 ),THREE.sRGBEncoding (sRGB颜色空间:3001)

如果webGL的渲染器的色彩空间手动设置了后面的添加的纹理也需要设置成相关的色彩空间(最新版中纹理对象的色彩空间属性名已由encoding改为colorSpace)

给gltf模型更换颜色贴图:TextureLoader()加载新的模型数据 如果直接给gltf的模型材质map设置新的纹理会出现错位的情况,这和贴图的texture的翻转flipY属性有关。

纹理对象的flipY默认值为trun ,gltf的贴图flipY默认值为false,跟换模型的贴图时需要设置为false.

纹理的列阵wrapS wrapT 分别是在UV轴上的如何进行包裹。repeat在uv轴上重复的数量

环境贴图:

立方体纹理加载器cubetextureLoader

这个加载器加载六个图片作为空间的上下左右前后,这个用来加载环境贴图,

1
2
3
4
5
6
7
8
9
10
// 加载环境贴图
// 加载周围环境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

材质MeshStandardMaterial的envMap环境贴图属性,通过pbr材质的贴图属性反射周围的景物。

envMapIntensity属性控制了反光率 roughness:控制粗糙度

环境贴图的色彩空间编码应该与渲染器的保持一致

//pbr材质的相关属性:

clearcoat:可以给物体表面设置一层透明膜,就像一层透明的漆,范围在0-1.

clearcoatRoughness:0-1 ;清漆层的粗糙层度。

metalness: 0.9,//车外壳金属度
roughness: 0.5,//车外壳粗糙度

transmission 玻璃透明度

Ior:折光率

场景的背景:

透明度:new Three.WebGLRenderer({alpha:true})或 render.setColorAlpha:0-1

背景色:render.setClearColor(颜色,透明度)

导出图片:

1.创建a标签

2.设置a标签的herf的 为场景的canvas元素,可以通过render.domElement获取

3.渲染器设置preserveDrawingBuffer为true ,开启缓存。

4.通过canvas的toDataURL(‘image/png’)获取话不上的像素信息,将以png的格式保存。

mesh重叠时候会出现闪缩的情况叫做深度冲突(z-fighting)
自定义划线:顶点数据来画

几何体setFromPoints(v3)将v3顶点数据设为几何体的attribute.position的顶点位置属性。除了可以是三维向量数组还可以是二位向量数组。

曲线:基类cuver

1712822846881

管道几何体

1
TubeGeometry(path, tubularSegments, radius, radiusSegments, closed)

旋转成型几何体

LatheGeometry:通过一个圆形轮廓坐标点 旋转得到一个几何体曲面

1
2
LatheGeometry(points, segments, phiStart, phiLength)
points为一个二维矢量组成的一个线段 segments为细分数, phiStart开始角度 phiLength旋转的角度默认值2π

形状几何体

ShapeGeometry:通过一个集合的轮廓得到一个多边形几何体平面。

先通过 Shape 将这些点生成一个二维平面,shape([二维矢量点数组…])

用ShapeGeometry 将这个二位平面装维平面几何体 ShapeGeometry (shape)

多边形轮廓shape(重点)

使用一些列路径传入shape生成一个多边形的形状,它可以和ExtrudeGeometryShapeGeometry一起使用,获取点,或者获取三角,也可以获得一个该形状的几何体,非常Nice.

1
2
父类为Path,所以也是继承了父类的相关方法与属性,如画线,画圆,画椭圆,等等获取当前点等属性。
传入点组成opints,可构造一个自定任意形状的多边形

1713148838218

若线与圆弧组合的时候,第一次的结束点自动衍生至第二个圆弧的起点。当前一次线画完时会采取以当前的currentPoint为中心点。

shape的线与圆共同使用的时候,他们的起点会相连接

1713149897591

内孔:holes

在创建的shape内挖出对应的孔洞,这些孔洞的坐标位置相对原点。

1713153199017

边缘几何体

EdgesGeometry:给指定几何体添加外边框线查看其边缘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    // 集合体的边界线EdgesGeometry
const geometryModle = new THREE.CylinderGeometry(2,2,32)
const materialModle = new THREE.MeshLambertMaterial({
color: 0x004444,
transparent:true,
opacity:0.5,
})
const mesh2 = new THREE.Mesh(geometryModle,materialModle)
scene.add(mesh2)
const edgesGeometry = new THREE.EdgesGeometry(geometryModle,30)//第二次参数时发现角度大于 多少时才会渲染
const edgesMaterial = new THREE.LineBasicMaterial({
color: 0x00ffff,
})
const lineModle = new THREE.LineSegments(edgesGeometry,edgesMaterial)
scene.add(lineModle)
// 同理相关模型也可以在加载完成后也可以添加边缘线。
modelLoader.load('./bwm/scene.gltf',(gltf)=>{
gltf.scene.position.set(5,0,5)
gltf.scene.traverse((item)=>{
if(item.isMesh){
item.material = new THREE.MeshLambertMaterial({//设置新的材质属性
color: 0x004444,
transparent: true,
opacity: 0.5,
})
const itemEdge = new THREE.EdgesGeometry(item.geometry)//创建外框线
const itemLineMaterial = new THREE.LineBasicMaterial({//线性材质
color: 0x00ffff,
})
const itemLine = new THREE.Line(itemEdge,itemLineMaterial)
item.add(itemLine)
}
})
scene.add(gltf.scene)//模型添加到场景中

拉伸几何体

**ExtrudeGeometry :**也是通过一个平面坐标拉成一个几何体,

参数:

1
2
3
4
5
6
depth: 20,
bevelThickness: 5, //倒角尺寸:拉伸方向
bevelSize: 5, //倒角尺寸:垂直拉伸方向
bevelSegments: 20, //倒圆角:倒角细分精度,默认3
extrudePath:cuverpath//通过一个三维曲线让这个多变对其沿着这个曲线拉伸
steps:100//沿着路径细分精度,越大越光滑

1712907318826

弯曲水管流动效果:

贴图版本:创建一个贴图,然后将贴图设置一个阵列模式。再render函数中给贴图的x轴的点进行+1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const pointsV = new THREE.CatmullRomCurve3([
new THREE.Vector3(20,10,5),
new THREE.Vector3(0,0,0),
new THREE.Vector3(-20,-15,-50),
])
const pipeBackgroundImage = new THREE.TextureLoader().load(img)
// pipeBackgroundImage.encoding = THREE.SRGBColorSpace
// 设置阵列模式
pipeBackgroundImage.wrapS = THREE.RepeatWrapping;
pipeBackgroundImage.wrapT = THREE.RepeatWrapping;
// uv两个方向纹理重复数量
pipeBackgroundImage.repeat.set(1,1);//注意选择合适的阵列数量
pipeBackgroundImage.encoding = THREE.SRGBColorSpace
console.log(pipeBackgroundImage,'tuoian');
const pipeline = new THREE.TubeGeometry(pointsV,65, 2, 25)
const pipeMaterial = new THREE.MeshPhysicalMaterial({emissive:'#3f7b9d',side:THREE.DoubleSide,map:pipeBackgroundImage,clearcoat:1})
const pipeMesh = new THREE.Mesh(pipeline,pipeMaterial)
scene.add(pipeMesh)
function animation(){
pipeBackgroundImage.offset.x += 0.0001; //uv动画
renderer.render(scene,camera)
requestAnimationFrame(animation)
}
animation(){

几何体的顶点属性

顶点位置数据geometry.attributes.position

顶点法向量数据geometry.attributes.normal

顶点UV数据geometry.attributes.uv

顶点颜色数据geometry.attributes.color

如何使用顶点颜色: 创建相关几何体时,通过设置起这个几何体顶点数量的颜色。

*若要添加顶点颜色需要在材质中开启vertexColors ,材质的color属性可以不用设置了

*rgb 范围1-0

1
2
3
4
5
6
7
const colors = new Float32Array([
1, 0, 0, //顶点1颜色
0, 0, 1, //顶点2颜色
0, 1, 0, //顶点3颜色
])
geotry.attributes.color = new three.BufferAttribute(colors,3) //三个一组

颜色的rgb范围如下图 ↓

1713170230091

绘制彩色曲线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1.创建一个三维贝塞尔曲线或者其他曲线
const peakCover = new three.CubicBezierCurve3(
new THREE.Vector3(4,4,4),
new THREE.Vector3(4,5,6),
new THREE.Vector3(7,6,6),
)

//2.获取这个曲线上的所有点位 .getpoints() 参数将曲线分割为多少个点
const peackPoints = peackCover.getpoints(20)

//3.设置这些点位的颜色
let peackColors = []
for(let i =0;i<peackPoints.length;i++){
const itemValue = i / peackPoints.length //等到这个点位对应的rgb
peackColors.push(itemValue,0,1)
}

//4.设置集合体的顶点数据
const peackCoverGeotry = new three.BufferGeometry().setFromPoints(new Float32Array(peackColors),3)

//5.创建曲线材质
const peackCoverMaterial = new three.LineBasicMaterial({
vertexColors:true,//使用顶点颜色渲染
// color:'red'
})

//创建线条
const coverLine = new three.Line(peackCoverGeotry,peackCoverMaterial)
scene.add(coverLine)

Color的使用

THREE.Color(r,g,b) 当所有参数被定义时,r是红色分量,g是绿色分量,b是蓝色分量。
当只有 r 被定义时:

  • 它可用一个十六进制 hexadecimal triplet 值表示颜色(推荐)。
  • 它可以是一个另一个颜色实例。
  • 它可以是另外一个CSS样式。例如:
    • ‘rgb(250, 0,0)’
    • ‘rgb(100%,0%,0%)’
    • ‘hsl(0, 100%, 50%)’
    • ‘#ff0000’
    • ‘#f00’
    • ‘red’
1
const color = new THREE.Color(r,g,b)
color的插值
1
2
3
4
5
6
7
8
9
10
11
//lerpColors
const c1 = new THREE.Color(0xff0000); //红色
const c2 = new THREE.Color(0x0000ff); //蓝色
const c = new THREE.Color();
// 颜色3.lerpColors(颜色1,颜色2,占比) 占比为两种颜色各占多少 但占比为0时到的颜色就是颜色1 占比为1时得到的颜色就是颜色2。
c = c.lerpColors(c1,c2,占比Value)
// lerp(颜色1,占比value)
c1.lerp(c2,1)// 颜色1.lerp(颜色2,占比) 占比为颜色1与颜色2的占比 占比为0为颜色1, 占比为2时为颜色2

let c3 = c1.clone().lerColor(c1,c2,占比)//这里是先克隆在用插值修改这个颜色

颜色插值的应用:不用自己用相关逻辑来写颜色的属性数组,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let peakCoverPoint = peakCover.getPoints(20)//获取曲线上的点
let colorArr = []
for(let i=0;i< peakCoverPoint.length ;i++){
const percent = i / peakCoverPoint.length; //点索引值相对所有点数量的百分比
//根据顶点位置顺序大小设置颜色渐变
// 红色分量从0到1变化,蓝色分量从1到0变化
colorArr.push(percent, 0, 1); //蓝色到红色渐变色
}
// 优化↓
let c1 = new three.Color(0x00ffff)
let c2 = new three.Color(0xffff00)
for(let i=0;i< peakCoverPoint.length ;i++){
const percent = i / peakCoverPoint.length; //点索引值相对所有点数量的百分比
//根据顶点位置顺序大小设置颜色渐变
let c = c1.clone(c2,percent)
colorArr.push(c.r, c.g, c.b); //颜色插值计算
}

模型的顶点及数据处理

引入的模型有些在返回的模型json对象的scne.children[0]中有geometry属性。有的会在其下面继续拓展多个物体。此时我们就需要遍历这个对象,去获取他的顶点数据。

BufferAttribute具有getX(), getY() ,getZ() 方法获取x(1维) y(2维) z(3维) 指定项的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//通过顶点数据 来将山体根据高度着色
modelLoader.load('shan2.glb',(gltf)=>{
gltf.scene.position.set(5,0,5)
console.log( gltf.scene);
gltf.scene.scale.set(25,25,25)

const zArr = [] //顶点数据组
//遍历所有物体的顶点将塞入zArr
gltf.scene.traverse(item=>{
let itemAttributes = item.geometry.attributes//顶点属性
let itemPosition = item.geometry.attributes.position//顶点位置数据
//在位置数据中有一个count表示这个物体有多少个顶点
for(let i = 0;i<itemPosition.length;i++){
zArr.push(itemPosition.getZ(i))
}
})
zArr.sort()//升序排序
let minHeight = zArr[0]//山脚高度
let maxHeight = zArr[zArr.length - 1]//山顶高度
let height = maxHeight - minHeight //山体的高度
let minColor = new THREE.Color(0x0000ff)//山脚颜色
let maxColor = new THREE.Color(0xff0000)//山顶颜色
//再次根据当前顶点个数进行循环 设置当前的顶点颜色
gltf.scene.traverse(item=>{
let itemAttributes = item.geometry.attributes//顶点属性
let itemPosition = item.geometry.attributes.position//顶点位置数据
let colorArr = []
for(let i = 0;i<itemPosition.count;i++){
let colorVal = (itemPosition.getZ(I) - minHeight) / height
let c = minColor.clone().lerp(maxColor,colorVal)
colorArr.push(c.r,c.g,c.b)
}
let float32Arr = new three.Float32Array(colorArr)
itemAttributes.geometry.color = new three.BufferAttribute(float32Arr,3)

item.material = new three.MeshLambertMaterial({
vertexColors:true//开启顶点颜色
})
})

})

1713253141908

相机camera

相机种类:正投影相机 OrthographicCamera ,透视投影相机 PerspectiveCamera ,立方体相机CubeCamera

正投影相机:相机所看到的是一个四边形衍生后的空间。 正投影没有透视效果,也就是不会模拟人眼观察世界的效果。

1
2
3
4
5
6
7
8
9
OrthographicCamera( left, right, top, bottom, near, far )
left 渲染空间的左边界
right 渲染空间的右边界
top 渲染空间的上边界
bottom 渲染空间的下边界
near near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 默认值0.1
far far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小小,会有部分场景看不到。 默认值2000
left 与 top 为负值

相机的position位置若在物体位置左边范围里面 ,会渲染范围外的 ,涉及到范围的坐标的物体这回被切割一般

1713321813858

1713321203283

透视投影相机:具有一定角度的锥型视野空间。特点:模拟人眼所看到的物体特征

视觉差异:同样渲染一个物体,正投影渲染的没有透视相机看着的自然。

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

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

WebGL渲染器更新Canvas画布尺寸

1
2
3
4
5
6
7
解释// onresize 事件会在窗口被调整大小时发生
window.onresize = function () {
const width = window.innerWidth; //canvas画布宽度
const height = window.innerHeight; //canvas画布高度
// 重置渲染器输出画布canvas尺寸
renderer.setSize(width, height);
};

也可以在创建的时候就设置render.setSize(window.innerWidth,window.innerHeight)

透视相机需要更新一下宽高比

1
2
3
4
5
6
7
window.onresize = function () {
// width、height表示canvas画布宽高度
const width = canvas.clientWidth //canvas画布宽度
const height = canvas.clientHeight //canvas画布高度
camera.aspect = canvas.clientWidth/canvas.clientHeight
camera.updateProjectionMatrix();
};

正投相机受到宽高影响left 和 right的影响。

1
2
3
4
const k = width / height; //canvas画布宽高比
const s = 50;
//控制left, right, top, bottom范围大小
const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 8000);
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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();
};

包围盒

包围一个物体外层包围的几何体,就如手办的透明收纳盒。

1
const box3 = new THREE.Box3();

expandByObject(mesh):方法获取这个物体的最小包围盒.

getSize(target:vector3):获取到这个包围盒的大小并赋值给一个v3.

getCenter(target:vector3):获取到这个包围盒的中心点,并赋给一个v3.

包围盒地图案例:

在这个案例中涉及到几个点

1.我们如何让地图居中,也就是摄像机如何对着地图,。

2.轨道控制器中心点对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import cq from 'map.json' //地图的json数据

//地图使用的正投影相机
const width = canvas.clientWidth; //canvas画布宽度
const height = canvas.clientHeight; //canvas画布高度
const k = width / height; //canvas画布宽高比
const s = 5;//控制left, right, top, bottom范围大小
console.log(k*s,'ks');
const camera = new THREE.OrthographicCamera(-k*s,k*s,s,-s,0.001,8000)
camera.position.set(10,10,10)
camera.lookAt(0,0,0)
scene.add(camera)

let depth = 0.2//地图的深度
//生成区县模块以及区县的边框
cq.map(item=>{
let v2 = []
let v3 = []
item.path.forEach(el => {
v2.push(new THREE.Vector2(el[0],el[1]))
v3.push(new THREE.Vector3(el[0],el[1],depth+0.0001))
});

let shape = new THREE.Shape(v2)
let ShapeGeo = new THREE.ExtrudeGeometry(shape,{depth:depth,curveSegments:v3.length,bevelEnabled:false})
let shapeMaterial = new THREE.MeshPhongMaterial({color:'#0a4287',opacity:1})
let shapeMaterial2 = new THREE.MeshPhongMaterial({color:'#1b6ef3',opacity:1})
let shaeMesh = new THREE.Mesh(ShapeGeo,[shapeMaterial,shapeMaterial2])
shaeMesh.position.z = 0

const areaLineGeo = new THREE.BufferGeometry().setFromPoints(v3)
const areaLineMaterial = new THREE.MeshPhongMaterial({color:'#fff',})
const areaLine = new THREE.Line(areaLineGeo,areaLineMaterial)
console.log(shaeMesh);
scene.add(areaLine)
scene.add(shaeMesh)
})
console.log(cqoutLine);
//创建一个重庆地图轮廓 以便于让地图居中
let outLine = []
cqoutLine.features[0].geometry.coordinates.forEach(item=>{
console.log(item);
item[0].forEach(val=>{
outLine.push(new THREE.Vector3(val[0],val[1],depth+0.0001))
})
})
console.log(outLine,'新');
const outLineGeo = new THREE.BufferGeometry().setFromPoints(outLine)
const outLIneMaterial = new THREE.MeshBasicMaterial({color:'red'})
const outLineMesh = new THREE.Line(outLineGeo,outLIneMaterial)
// scene.add(outLineMesh)
//包围盒 通过包围盒获取到物体的中心点
camera.lookAt(0,0,0)
camera.position.set(300,300,300)
const mapBox = new THREE.Box3()
let outLineBox = mapBox.expandByObject(outLineMesh)
const center = new THREE.Vector3() //获取地图中心点
outLineBox.getCenter(center)
console.log(center,'center');
//重新设置相机的观察中心点以及相机的位置。
camera.lookAt(center.x,center.y,center.z)
camera.position.set(center.x,center.y,50)
//重新设置轨道控制器的中心点。需要手动update一下配置
Controls.target.set(center.x,center.y,center.z);
Controls.update();
let mapBoxSize = new THREE.Vector3()//获取地图包围盒的大小
outLineBox.getSize(mapBoxSize)
const obj = new THREE.Object3D()
obj.position.set(center.x,center.y,center.z)
cqLight.target = obj
1713348789513

相机的动画效果:

主要是通过移动相机的position 完成的。

如在animation中不断更新相机的位置,相机观察的中心点始终为某个点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function animation(){

ridius+=0.001

camera.position.z = r * Math.sin(ridius)

camera.position.x = r * Math.cos(ridius)

camera.position.x += 0.003

camera.position.z -= 0.003

camera.lookAt(5,1.2,-5)

renderer.render(scene,camera)

requestAnimationFrame(animation)

}

animation()

相机的**.up**属性控制哪个坐标轴朝上:

x轴朝上:camera.up.set(1,0,0)

y轴朝上:camera.up.set(0,1,0)

z轴朝上:camera.up.set(0,0,1)

管道穿梭案例:

思路创建一个曲线,生成对应的管道模型。获取这个曲线上的点,相机位置设置在第当前点位上 然后相机的朝向为当前点位的下一个点的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//相机选择透视相机,相机的仰角调试大一点管道中看的更多。45设置为90
cosnt coverLine = new three.CubicBezierCurve3(
new THREE.Vector3( -10, 0, 0 ),
new THREE.Vector3( -5, 15, 0 ),
new THREE.Vector3( 20, 15, 0 ),
new THREE.Vector3( 10, 0, 0 )
)
//创建纹理
const textrue = new three.TextrueLoader().load('water.png')
textrue.warpS = three.RepeatWrapping //开启矩阵模式 水平平铺
textrue.repeate.x = 1

const coverMaterial = new three.MeshBasicMaterial({
map:textrue,
side: THREE.DoubleSide//双面显示
})

//获取曲线上的点
const coverPoints = coverLine.getPoints(500)

let i = 0
function animation(){
i+=1
if(i < coverPoints.length - 1){
camera.position.copy(coverPoints[i])//相机位置在当前项
camera.lookAt(coverPoints[i + 1])//相机朝向下一项
}else{
i = 0
}
renderer.render(scene,camera)
requestAnimationFrame(animation)

}

animation()

相机控件OrbitControls
  • 旋转:拖动鼠标左键。enableRotate:true 开启旋转
  • 缩放:滚动鼠标中键。 enableZoom :true 开启缩放
  • 平移:拖动鼠标右键 。enablePan:true 开启平移

设置控件的平移,缩放,旋转就是控制相机的位置变化。

如果修改了空间的target的目标点 则也修改了相机的camera。修改控件的属性时也需要更新控件update()

控制缩放范围:

1
2
3
4
5
6
透视投影相机看见为物体效果问近大远小,控件提供了两个属性minDistance 与 maxDistance, 
controls.minDistance = 5
controls.maxDistance = 15

正投影相机则是minZoom,maxZoom

控制相机的旋转范围:

1
2
3
4
5
6
7
8
控制相机旋转范围:
minPolarAngle / maxPolarAngle 控制相机的上下旋转范围
controls.minPolarAngle = 0
controls.maxPolarAngle = Math.PI / 4

minAzimuthAngle / maxAzimuthAngle 控制相机左右旋转范围
controls.minAzimuthAngle = 0
controls.maxAzimuthAngle = Math.PI /2

除了OrbitControls控件外 还有一个MapOrbitControls 模拟地图的拖拽。

光源:light

光源基类light:平行光:DirectionalLight 点光源:PointLight 聚光灯:SpotLight ;这光源三种都能产生阴影。

环境光源:AmbientLight 。不会产生阴影

调试注意:调试阴影的时候记得开启相机的辅助工具 以便快速找到相机的范围; 以及将光源可视化确定光源的具体位置。

光源如何产生阴影:

1.光源需要设置.castShadow 为true

2.产生阴影的模型需要设置.castShadow 为true

3.渲染器的允许渲染 renderder. shadowMap.enabled 为true

4.设置渲染范围.shadow.camera 属性值始终为正投影相机OrthographicCamera , 限制阴影的范围,如果给的不合适看不到或者看不全,模糊不清等

光源的shadow.mapSize

提高阴影的渲染效果,值越大越清楚。默认值512

1713778296664

注意:

在能覆盖包含阴影渲染范围的情况下, .shadow.camera的尺寸尽量小。

如果你增加.shadow.camera的长方体视野尺寸范围,阴影模糊齿感,可以适当提升.shadow.mapSize的大小。

光源的shadow.radius

改变阴影边缘的过度效果。

可以适当的提升.shadow.radius让边缘的过度效果更明显。或者说边缘逐渐弱化或者模糊化,没有明显的边界干。

1713778832957
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
 //光
// 设置渲染器,允许光源阴影渲染
renderer.shadowMap.enabled = true;
const light = new THREE.AmbientLight( 0x404040,10 ); // 柔和的白光
const gui = new GUI();//创建GUI对象
gui.add(light, 'intensity', 0, 100).name('环境光.intensity');
scene.add( light );
//平行光
const directionalLight = new THREE.DirectionalLight( 0xffffff,5)
directionalLight.position.set(100,100,100),
directionalLight.target.position.set(0,0,0)
directionalLight.castShadow = true
directionalLight.receiveShadow = true;
console.log('阴影相机属性',directionalLight.shadow.camera);
const directionalLightHelp = new THREE.DirectionalLightHelper(directionalLight,0xffffff)
directionalLight.shadow.mapSize.set(2540,2540) //边缘的颗粒度
directionalLight.shadow.radius = 5
directionalLight.shadow.camera.left = -100;
directionalLight.shadow.camera.right = 100;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.bottom = -100;
directionalLight.shadow.camera.near = 0.01;
directionalLight.shadow.camera.far = 5000;

// scene.add(directionalLightHelp)
// 可视化平行光阴影对应的正投影相机对象
const cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
scene.add(cameraHelper);
// 可视化平行光
const dirHelper = new THREE.DirectionalLightHelper( directionalLight, 5);
scene.add( dirHelper );

const load = new GLTFLoader('模型1.gltf',gltf=>{
gltf.scene.castShadow = true
scene.add(gltf.scene)
})


// gui调节阴影
// 阴影子菜单
const shadowFolder = gui.addFolder('平行光阴影');
const cam = directionalLight.shadow.camera;
// 相机left、right等属性变化执行.updateProjectionMatrix();
// 相机变化了,执行CameraHelper的更新方法.update();
shadowFolder.add(cam,'left',-500,0).onChange(function(v){
cam.updateProjectionMatrix();//相机更新投影矩阵
cameraHelper.update();//相机范围变化了,相机辅助对象更新
});
shadowFolder.add(cam,'right',0,500).onChange(function(v){
cam.updateProjectionMatrix();
cameraHelper.update();
});
shadowFolder.add(cam,'top',0,500).onChange(function(v){
cam.updateProjectionMatrix();
cameraHelper.update();
});
shadowFolder.add(cam,'bottom',-500,0).onChange(function(v){
cam.updateProjectionMatrix();
cameraHelper.update();
});
shadowFolder.add(cam,'far',0,1000).onChange(function(v){
cam.updateProjectionMatrix();
cameraHelper.update();
});
// *做一个太阳的升起与降落:原理光源在绕着z轴进行旋转
let r = 100 //光源旋转半径
let angle = 0 //光源的角度
function animation(){
angle+=0.01
directionalLight.position.y = r * Math.sin(angle)
directionalLight.position.x = r * Math.cos(angle)

renderer.render(scene,camera)
requestAnimationFrame(animation)
}


精灵:sprite

spriteMaterial:材质与普通网格材质一样可以设置相关的.map envMap 啥的。

Sprite:始终朝向摄像机,旋转轨道也不会让精灵图旋转不会产生阴影。即使设置了castShadow为true也不会生效。与其他材质不同的是精灵不需要设置几何体 Geometry ,精灵图的默认尺寸都是 1 ,

透视相机中:也遵循远小近大的原理

正投影相机如何验证精灵的尺寸:如果精灵尺寸为1, 给相机top , buttom设置为0.5 则整个精灵回撑满画布。

精灵与矩形mesh区别:他们都是一个矩形,区别在于精灵始终平行于画布,而mesh的矩形则始终跟着旋转。在尺寸上,mesh可以通过geometry和mesh.scale来定义,然而精灵则只能通过sprite.scale来定义。

模拟下雨和下雪的效果:如果要做下雨效果3d的曲面几何体,一个曲面几何体需要4个三角形的话,如果有3千个水滴,则需要3万个三角形;如果用精灵的话,一个精灵只需要两个三角形,减少了很多三角形的生成。渲染性能要好很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// const texture = new THREE.TextureLoader().load('img/wa3.jpg')
const texture = new THREE.TextureLoader().load('img/xue.png')
const spriteMaterial = new THREE.SpriteMaterial({
map:texture,
// rotation:Math.PI/4,//旋转精灵对象45度,弧度值
transparent:true,//SpriteMaterial默认是true
})
const group = new THREE.Object3D()
for(let i = 0;i < 6000;i++){
const sprite = new THREE.Sprite(spriteMaterial)
sprite.scale.set(5, 5, 1)
sprite.position.x = 500 * (Math.random() - 0.5)
sprite.position.y = 250 * (Math.random())
sprite.position.z = 500 * (Math.random() - 0.5)
group.add(sprite)
}
scene.add(group)

const clock = new THREE.Clock()
function loop(){
// 通过tranvers来循环修改比较卡
// group.traverse(item=>{
// console.log(item,'w');
// item.position.y -=0.1
// if(item.position.y<0){
// item.position.y = 100
// }
// })

// 根据时间来设置相关位置 很卡
// const t = clock.getDelta();// loop()两次执行时间间隔
// group.children.forEach(item=>{
// console.log(t);
// item.position.y -= t * 60
// if(item.position.y < 0){
// item.position.y = 100
// }
// })

// 周期性改变雨滴位置
group.children.forEach(item=>{
//近点的雨滴 雪比较大 可以将相机的近点参数设置到一点
item.position.y -= 0.1
item.material.rotation += 0.000001// 雪花旋转需要旋转材质中的rotation
if(item.position.y < 0){
item.position.y = 250
}
})
requestAnimationFrame(loop)
}
loop()
function animation(){

renderer.render(scene,camera)
requestAnimationFrame(animation)
}
animation()

后处理: EffectComposer

定义:可以理解为处理照片后期处理一个 ‘ps’。

这是一个拓展库需要单独引入EffectComposer.js 和相关的后期处理通道。

后期处理库:

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

…等,通道可以组合使用:组合使用的话,先处理一个通道再处理另外一个。

threejs本地部署的服务文件包examples文件目录,全文检索关键词EffectComposer,可以找到后处理的很多案例。

设置后期处理后,模型会出现色差, renderer.outputEncoding色彩空间不会生效,此时我们需要设置伽马校正通道。GammaCorrectionShader.js 提供了伽马校正的对象,可以处理模型后处理后颜色的色彩。没有伽马矫正的通道 还需要引入一个shaderPass.js库 配合使用 将伽马校正的对象作为参数创建校验通道。

1714356663966

模型添加后会出现锯齿:后处理器多种抗锯齿配置, FXAAShader 和 SMAAShader。SMAA效果比FXAA的效果好写

如何使用呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
   import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';//渲染器通道RenderPass
import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js';//物体模型外描边通道RenderPass
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';// 引入UnrealBloomPass通道
// 引入GlitchPass通道
import { GlitchPass } from 'three/addons/postprocessing/GlitchPass.js';

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

//首先需要使用后期处理器
const composer = new EffectComposer(renderder) //参数为需要处理的webgl渲染器

//创建渲染器通道。这个必须要有不然后面可能不会出现效果
const renderPass = new RenderPass(scene,camera)//参数为场景与相机
comporser.addPass(renderPass)//给后期处理器添加渲染器通道
//例子 物体的外边缘发光通道


//创建一个二维矢量 大小与画布一直
let v2 = new three.Vector2(canvas.innerWidth,canvas.innerHeight)
const outLinePass = new OutlinePass(v2,scene,camera)
outLinePass.selectedObjects = [mesh1] //selectedObjects属性为添加物体 值为数组所以可以添加多个
composer.addPass(outLinePass)

//outLinePass的相关属性
outLinePass.visibleEdgeColor.set(0xffff00) //控制描边的颜色
outLinePass.edgeThickness = 4 //描边的厚度
outLinePass.edgeStrength = 6 //描边的亮度
outLinePass.pulsPeriod = 2 //描边的闪烁频率

const bloom = new UnrealBloomPass(v2) //控制物体本身发光
bloom.strength = 2 //发光强度
// composer.addPass(bloom)

const glitchPass = new GlitchPass()//闪屏效果,类型以前黑白电视信号不好的闪屏
composer.addPass(glitchPass)

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

//首先需要设置像素比,避免canvas画布数据模糊
renderer.setPixelRatio(window.devicePixelRatio);
const pixelRatio = renderer.getPixelRatio()
console.log(pixelRatio,'像素比');

//FXAA抗锯齿
const fxaa = new ShaderPass(FXAAShader)
fxaa.uniforms.resolution.value.x = 1/(canvas.clientWidth*pixelRatio)
fxaa.uniforms.resolution.value.y = 1/(canvas.clientHeight*pixelRatio)
// composer.addPass(fxaa)

//smaa抗锯齿
const smaa = new SMAAPass(canvas.clientWidth*pixelRatio,canvas.clientHeight*pixelRatio)
composer.addPass(smaa)

//还需要设循环渲染中调用一下后期渲染器 后期渲染器执行时也可以不用执行webgl渲染器的渲染了
function animation(){
//renderer.render(scene,camera)
composer.render()
requestAnimationFrame(animation)
}

animation()

射线Ray(重点)

射线起点:origin为一个三维向量

射线方向:direction 是一个三维向量单位,向量长度始终为1,在设置了三维向量后我们一般使用 .normalize() 将其向量进行归一化。归一化就是将这个线长度变为1。

射线的真实方向:射线方向归一后,得到的方向就是射线的防线,简单点来说我们将射线方向起点移动到射线起点就会得到射线的真实方向了。

intersectObjects(meshArr):与哪些物体相交。注意点:我们在执行这个方法前先更新一下坐标,因为修改了相关位置信息后会在下一次render时才生效,所以我们得updateMatrixWorld()手动更新.

17143751853581714375233084

屏幕坐标转three.js设备坐标:我们可以用offset与client来进行转换,两种坐标点转换逻辑其实都差不多。设备作标以canvas中心点为坐标原点,xy坐标的范围在[-1,1]。

计算逻辑:设备坐标范围[-1,1],所以我们需要一个范围。首先求出每个方向上的占比,此时得到的就是一个小于1

的小数。因为-1 到 1范围是2 ,所以得到的值乘2,再-1 结果永远都在 - 1 跟 1 之间了。唯一注意点是y轴方向相反 ,占比值取负*2 + 1。

offset转换:建议

1
2
3
//获取canvas画布的宽高 width,height
let x = (width/e.offsetX) * 2 - 1
let y = -(height/e.offsetY)*2 + 1

client转换:需要注意的是,client坐标是相对浏览器的,所以canvas画布距离浏览器上边与左边的距离需要自己删减一波,如果开全屏的画就不需要了。

1
2
3
4
5
6
7
8
9
   let canvasWidth = canvas.clientWidth
let canvasHeight = canvas.clientHeight
// 用client坐标转换设备坐标
let cX = ((e.clientX - 8)/canvasWidth) * 2 - 1
let cY = -((e.clientY-20.8-8)/canvasHeight) * 2 +1
console.log(cX,cY,'client');
//全屏
let cX = (e.clientX/canvasWidth) * 2 - 1
let cY = -(e.clientY/canvasHeight) * 2 + 1

1714442633565

射线拾取器Raycaster:创建Raycaster实例。他的ray属性就是射线,设置ray的origin,direction就是改变射线拾取器的起点与方向,也可以通过拾取器的Set方法来进行修改。

.intersectObject( mesh ):方法 判断是否包含某个物体

.intersectObjects( [ mesh1, mesh2 ] ):检索是否经过这些物体,将经过的物体返回成一个数组,没有就是空数组

.setFromCamera( v2 , camera) : v2作为方向, 在标准化设备坐标中鼠标的二维坐标 —— X分量与Y分量应当在-1到1之间。 射线所来源的摄像机。

.set(v3,direction): v3 作为起点,是个三维坐标。direction是一个方向也是三维坐标。

如果画布容器canvas尺寸发生变化,需要更新的设置:相机的宽高比需要更新camera.aspect=w/h ; 渲染器更新画布尺寸render.setSize(w,h) ; 相机参数变化,相机必须更新camera.updateProjectionMatrix();

精灵图也可以进行拾取,操作方法与mesh一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
	const ray = new THREE.Ray()
ray.origin = new THREE.Vector3(1,0,0)
// ray.direction = new THREE.Vector3(5,5,5).normalize()
ray.direction = new THREE.Vector3(5,5,5).normalize();
//数据归一化 将向量转化为方向向量 向量的长度变为1
// let L = ray.direction.length()
// ray.direction.multiplyScalar(1/L) //multiplyScalar将该方向的三维向量归一化 1除向量的长度得到这个比例,然后xyz三个点都会乘以这个值。

//判断射线是否相交某个三角形.intersectTriangle(v1,v2,v3,backfaceCulling:boolean,target) 相交时将值付给target.

//通过坐标拾取器来获取鼠标交互的模型
const raycaster = new Raycaster()
document.body.addEventListener('click',e=>{
let canvasWidth = canvas.clientWidth
let canvasHeight = canvas.clientHeight
//获取canvas画布的宽高 width,height
let x = (width/e.offsetX) * 2 - 1
let y = -(height/e.offsetY)*2 + 1
let v2 = new three.Vector2(x,y)
raycaster.setFromCamera(v2,camera) //更新一个起点为相机 方向为v2的射线。
const arr = rayCaster.intersectObjects([mesh3,mesh1,mesh2])//intersectObjects()经过其中物体中的哪些呢
if(arr.length > 0){
arr[0].object.material.color.set('red')//修改经过模型的颜色
}

// 如果画布的大小发生了改变那么 我们地重新设置相机的参数 及重新设置渲染器 画布大小 更新摄像机投影矩阵
document.querySelector('#change').addEventListener('click',()=>{
vue.flog = !vue.flog
if(vue.flog){
vue.dom.style.width = '500px'
vue.dom.style.height = '500px'
camera.aspect = canvas.clientWidth/canvas.clientHeight //设置相机宽高比
renderer.setSize(canvas.clientWidth,canvas.clientHeight)//设置画布大小
camera.updateProjectionMatrix()//更新摄像机投影矩阵。在任何参数被改变以后必须被调用。
// if(this.model&&this.model){
// console.log('处理');
// // this.model.renderer.setSize(500, 500);
// this.model.camera.aspect = 500 / 500;
// this.model.camera.updateProjectionMatrix()
// }
}else{
vue.dom.style.width = '400px'
vue.dom.style.height = '400px'
camera.aspect = canvas.clientWidth/canvas.clientHeight
renderer.setSize(canvas.clientWidth,canvas.clientHeight)
camera.updateProjectionMatrix()
// if(this.model){
// console.log('调用');
// // this.model.renderer.setSize(400, 400);
// this.model.camera.aspect = 400 / 400;
// this.model.camera.updateProjectionMatrix()
// }
}
})


})

html2D标注CSS2DRender/CSS2DObject

在某些场景中我们需要标注一些场景中的模块信息,此时我们需要自己设置信息的排版。CSS2DRender及CSS2DObject就可以实现。

原理:因为创建的始终为html,所以我们创建的css2dRender是一个html元素,但是这个元素要与three的画布的位置始终保持一致,这样通过CSS2DObject所创建的标注才不会偏移。

CSS2DRender:场景标注的渲染器,跟webgl渲染器的用法一致,但是需要注意位置与three画布保持一致

CSS2DObject: 将html转为网格模型的类,转化这个标注也可以设置相关的属性,如position等,因为基类是object3D

标签的位置设置:需要将标注添加到某个物体附近。1.将标注添加到该模型的子级;2.获取该模型的世界坐标也是可以的; 3. 标签模型对象作为需要标注mesh的子对象,然后获取mesh几何体某个顶点的坐标,作为标签模型对象局部坐标position;

标签偏移:可以通过设置标签的css定位属性来设置属性的偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
const btn = document.querySelector('.tip2D')
const btn2 = document.querySelector('.tip3D')

const btn2D = new CSS2DObject(btn)
//如果需要将这个标注设置在 一个物体上要么获取物体的世界坐标系 或者将这个标注添加到这个物体里面作为子级。
let v3 = new THREE.Vector3(50,30,30)
// 将标注设置为世界坐标系位置
// boxMesh.getWorldPosition(v3)
// 将标注设置为定点位置
console.log( boxPosition.getX(0),'某个位置');
btn2D.position.copy(v3)
// scene.add(btn2D)

const glbLoader = new GLTFLoader()
let n = 5

const rayCaster = new Raycaster()
const renderPass = new RenderPass(scene,camera)
const composer = new EffectComposer(renderer)
composer.addPass(renderPass)
const outLinePass = new OutlinePass(new THREE.Vector2(canvas.clientWidth,canvas.clientHeight),scene,camera)
//模型描边颜色
outLinePass.visibleEdgeColor.set(0xffff00)
//高亮发光描边厚度
outLinePass.edgeThickness = 4
//高亮发光区域的强度
outLinePass.edgeStrength = 6
composer.addPass(outLinePass)
glbLoader.load('/零件.glb',gltf=>{
console.log(gltf,'模型');
gltf.scene.traverse(val=>{
if(val.isMesh||val.isLine){
// val.getWorldPosition(v3)
n+=30
if(n==185){
camera.position.set(n+10,n+10,n+10)
camera.lookAt(n,n,n)
controls.target.set(n,n,n)
controls.update()
console.log('相机的位置',camera);
}
val.position.set(n,n,n)
console.log(val.position,'???');
}
})
// btn.style.top = '20px'
// btn.style.left = '20px'
const tag2D = new CSS2DObject(btn)
console.log(tag2D,tag3D,'css对象');
let changeObj = null
//添加css2d标签
document.body.addEventListener('click',e=>{
let x = (e.offsetX/canvas.clientWidth)*2 - 1
let y = -(e.offsetY/canvas.clientHeight)*2 + 1
console.log(x,y);
let v2 = new THREE.Vector2(x,y)

rayCaster.setFromCamera(v2,camera)
let obj = rayCaster.intersectObjects(gltf.scene.children[0].children[0].children)
console.log(obj);
if(obj.length>0){
let a = obj[0].object
outLinePass.selectedObjects = [a]
changeObj = a
if(a.isLine) a.add(tag2D)//将标注添加到这个对象里面
}else{
// if(changeObj) changeObj.remove(tag)
// outLinePass.selectedObjects = []
}

})
//因为css2Drender 的事件被屏蔽了所以无法触发事件,我们可以单独给某个元素将事件触发开启
document.querySelector('.close1').style.pointerEvents = 'auto';
document.querySelector('.close1').addEventListener('click',()=>{
if(changeObj) changeObj.remove(tag2D)//关闭弹窗及外边缘发光
outLinePass.selectedObjects = []
})

scene.add(gltf.scene)

})


//css3D 渲染器
const css3DRender = new CSS3DRenderer()
css3DRender.domElement.style.position = 'absolute'//设置定位是让css3d这个元素与画布重叠
css3DRender.domElement.style.top = '0px'
css3DRender.domElement.style.pointerEvents = 'none'
css3DRender.setSize(canvas.clientWidth,canvas.clientHeight)
document.querySelector('.screen').appendChild(css3DRender.domElement)
window.addEventListener('resize',()=>{
css3DRender.setSize(canvas.clientWidth,canvas.clientHeight)
})

//css2D 渲染器
const css2Drender = new CSS2DRenderer()
css2Drender.domElement.style.position = 'absolute'//设置定位是让css3d这个元素与画布重叠
css2Drender.domElement.style.top = '0px'
css2Drender.domElement.style.pointerEvents = 'none'//关闭渲染的事件 避免相机或者场景无法点击的问题
css2Drender.setSize(canvas.clientWidth,canvas.clientHeight)
document.querySelector('.screen').appendChild(css2Drender.domElement)
window.addEventListener('resize',()=>{
//场景的大小也需要更新
css2Drender.setSize(canvas.clientWidth,canvas.clientHeight)
})


function animation(){
css2Drender.render(scene,camera)
css3DRender.render(scene,camera)
// renderer.render(scene,camera)
composer.render()
requestAnimationFrame(animation)
}
animation()

html3D标注CSS3DRender/CSS3DObject/CSS3DSprite

CSS3DRender与2D的使用基本上一致。不论是css2d还是3d 所创建的标注都不会被物体压住,因为他在一个canvas画布的一个html里面。

**2d与3d的区别:**2d始终朝上 就是普通的Html. 3d是可以跟着旋转与缩放的就如Mesh物体一样,css3d精灵则跟three精灵一样始终始终朝向相机, CSS3精灵模型CSS3DSprite尺寸、位置、缩放等渲染规律和CSS3对象模型CSS3DObject基本一致。

CSS2渲染的标签和CSS3渲染的标签偏移方式不同,CSS3标签,直接按照threejs模型尺寸修改方式改变,比用HTML像素方式更方便准确。

1
2
3
4
5
6
7
8
css2采取的是设置css定位属性,而css3采取的是网格的模型位置
css2:
const div = document.getElementById('tag');
// id="tag"元素高度322px,默认标签中心与标注点
div.style.top = '-161px'; //平移-161px,指示线端点和标注点重合
css3:
tag.scale.set(0.5,0.5,1);//缩放标签尺寸
tag.position.y += 10;//累加标签高度一半,标签底部和圆锥顶部标注位置重合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
  
const btn2 = document.querySelector('.tip3D')
const glbLoader = new GLTFLoader()
let n = 5
const rayCaster = new Raycaster()
const renderPass = new RenderPass(scene,camera)
const composer = new EffectComposer(renderer)
composer.addPass(renderPass)
const outLinePass = new OutlinePass(new THREE.Vector2(canvas.clientWidth,canvas.clientHeight),scene,camera)
//模型描边颜色
outLinePass.visibleEdgeColor.set(0xffff00)
//高亮发光描边厚度
outLinePass.edgeThickness = 4
//高亮发光区域的强度
outLinePass.edgeStrength = 6
composer.addPass(outLinePass)
glbLoader.load('/零件.glb',gltf=>{
console.log(gltf,'模型');
gltf.scene.traverse(val=>{
if(val.isMesh||val.isLine){
// val.getWorldPosition(v3)
n+=30
if(n==185){
camera.position.set(n+10,n+10,n+10)
camera.lookAt(n,n,n)
controls.target.set(n,n,n)
controls.update()
console.log('相机的位置',camera);
}
val.position.set(n,n,n)
console.log(val.position,'???');
}
})
const tag3D = new CSS3DObject(btn2)
//禁止CSS3DObject标签对应HTMl元素背面显示
btn2.style.backface-visibility = 'hidden'

const tagSprite = new CSS3DSprite(btn2)
tag3D.scale.set(0.5,0.5,1);//缩放标签尺寸
tag3D.position.x += 60
console.log(tag2D,tag3D,'css对象');

let changeObj = null
//添加css2d标签
document.body.addEventListener('click',e=>{
let x = (e.offsetX/canvas.clientWidth)*2 - 1
let y = -(e.offsetY/canvas.clientHeight)*2 + 1
console.log(x,y);
let v2 = new THREE.Vector2(x,y)

rayCaster.setFromCamera(v2,camera)
let obj = rayCaster.intersectObjects(gltf.scene.children[0].children[0].children)
console.log(obj);
if(obj.length>0){
let a = obj[0].object
outLinePass.selectedObjects = [a]
changeObj = a
if(a.isLine) a.add(tag2D)
// if(a.isMesh) a.add(tag3D) //css3D 具有跟着缩放的特点,类似于Mesh ,始终是html创建的元素 旋转场景始终在模型上面。
if(a.isMesh) a.add(tagSprite) //css3D精灵 类似于精灵图,始终朝着向摄像机。
}else{
// if(changeObj) changeObj.remove(tag)
// outLinePass.selectedObjects = []
}

})
document.querySelector('.close1').style.pointerEvents = 'auto';
document.querySelector('.close1').addEventListener('click',()=>{
if(changeObj) changeObj.remove(tag2D)
outLinePass.selectedObjects = []
})
document.querySelector('.close2').style.pointerEvents = 'auto';
document.querySelector('.close2').addEventListener('click',()=>{
if(changeObj) changeObj.remove(tagSprite)
outLinePass.selectedObjects = []
})



scene.add(gltf.scene)

})


//css3D 渲染器
const css3DRender = new CSS3DRenderer()
css3DRender.domElement.style.position = 'absolute'//设置定位是让css3d这个元素与画布重叠
css3DRender.domElement.style.top = '0px'
css3DRender.domElement.style.pointerEvents = 'none'
css3DRender.setSize(canvas.clientWidth,canvas.clientHeight)
document.querySelector('.screen').appendChild(css3DRender.domElement)
window.addEventListener('resize',()=>{
css3DRender.setSize(canvas.clientWidth,canvas.clientHeight)
})

//css2D 渲染器
const css2Drender = new CSS2DRenderer()
css2Drender.domElement.style.position = 'absolute'//设置定位是让css3d这个元素与画布重叠
css2Drender.domElement.style.top = '0px'
css2Drender.domElement.style.pointerEvents = 'none'
css2Drender.setSize(canvas.clientWidth,canvas.clientHeight)
document.querySelector('.screen').appendChild(css2Drender.domElement)
window.addEventListener('resize',()=>{
css2Drender.setSize(canvas.clientWidth,canvas.clientHeight)
})


function animation(){
css2Drender.render(scene,camera)
css3DRender.render(scene,camera)
// renderer.render(scene,camera)
composer.render()
requestAnimationFrame(animation)
}
animation()

添加多i个标注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
    let num = 0

for(let i=0;i<5;i++){
glbLoader.load('tree2.glb',glb=>{
console.log(glb,'shu');
glb.scene.traverse(item=>{
if(item.isMesh){
console.log(item,'shuju ');
// item.material.emissive.set('red')
}
})

glb.scene.scale.set(0.1,0.1,0.1)
glb.scene.position.set(num,0,num)

// 创建树的标签 css2D 如果需要将标签的位置设置在某个物体顶上的话 如果只设置css定位,缩放时标签会偏移;建议还是用精准定位标签的position属性
const treeDom = document.querySelector('.treeName').cloneNode()
treeDom.innerHTML = '第'+(i+1)+'棵树'
const treeTip = new CSS2DObject(treeDom)
//方案1:将标签添加到该模型中 单独设置y的坐标
// treeTip.position.y = 500
// glb.scene.add(treeTip)
//方案2:获取模型的世界坐标,在修改y的坐标
// let adr = new THREE.Vector3()
// glb.scene.getWorldPosition(adr)
// adr.y += 50
// treeTip.position.copy(adr)
// // treeTip.position.x = 50
// console.log(treeTip);
// scene.add(treeTip)


// 添加css3d精灵 css3d的精灵不能不能被物体遮挡
// const iconDom = document.querySelector('.icon').cloneNode()
// const iconSprite = new CSS3DSprite(iconDom)
// iconSprite.position.y = 400
// glb.scene.add(iconSprite)

// 添加精灵模型 精灵模型可以被物体遮挡。
// const textureLoad = new THREE.TextureLoader()
// let spriteImg = textureLoad.load('/警戒.png')
// const spriteM = new THREE.SpriteMaterial({map:spriteImg})
// const sprite = new THREE.Sprite(spriteM)
// sprite.position.y = 450
// sprite.scale.set(100,100,100)
// glb.scene.add(sprite)


//通过canvas来作为精灵的贴图绘制标注
let tipcanvas = createCanvas()
const canvasTexture = new THREE.CanvasTexture(tipcanvas)
const spriteM2 = new THREE.SpriteMaterial({map:canvasTexture})
const sprite2 = new THREE.Sprite(spriteM2)
sprite2.position.y = 450
sprite2.scale.set(canvas.width/canvas.height*100,100,100)
glb.scene.add(sprite2)

scene.add(glb.scene)
num+=20
})
}

function createCanvas(src='house/nz (1).png'){
const img = new Image()
img.src = src
//通过canvas来绘制精灵模型的贴图
const tipCan = document.createElement('canvas')
let str = '设备A'
let arr = str.split('')
const arc = /[\u4e00-\u9fa5]/
let strLength = 0 //字符串的长度
for(let i = 0;i<arr.length;i++){
if(arc.test(arr[0])){//判断的是否为汉字 是汉字+1
strLength+=1
}else{
strLength+=0.0 //是应为或者其他符号 则+0.5
}
}
// 根据字体符号类型和数量,文字font-size大小来设置canvas画布的宽度
const h = 80
const w = h + strLength*32
tipCan.width = w
tipCan.height = h

const h1 = h*0.8
const c = tipCan.getContext('2d')//获取canvas的上下文 才能绘制
const r = h1/2
c.arc(r,r,r,-Math.PI/2,Math.PI/2,true)//顺势针旋转半圆
c.arc(w-r,r,r,Math.PI/2,-Math.PI/2,true)//顺势针旋转半圆
// c.fillStyle = "rgba(0,0,0,0.0)"; //背景透明
// c.fillRect(0, 0, w, h);
c.fillStyle = "rgba(255,255,255,1)";
c.fill()
// c.drawImage(img, 0, 0, w, h);//图片绘制到canvas画布上
// 箭头
c.beginPath()
const h2 = h-h1
c.moveTo(w/2-h2*0.6,h1)
c.lineTo(w/2+h2*0.6,h1)
c.lineTo(w/2,h)
c.fill()
// 文字
c.beginPath()
c.translate(w/2,h1/2)
c.fillStyle = '#000000'//文本填充色
c.font = 'normal 32px 宋体'//文本的字体
c.textBaseline = 'middle'//文本与fillText定义的纵坐标,对齐线
c.textAlign = "center"; //文本居中(以fillText定义的横坐标)
c.fillText(str,0,0)

return tipCan
}

着色器材质(重要)

着色器是 WebGL 的重要组件之一, shader 它是一种使用 GLSL 语言编写的运行在 GPU 上的小程序。顾名思义,着色器用于定位几何体的每个顶点,并为几何体的每个可见像素进行着色 。着色器是屏幕上呈现画面之前的最后一步,用它可以实现对先前渲染结果进行修改,如颜色、位置等,也可以对先前渲染的结果做后处理,实现高级的渲染效果。

资源网站

欢迎关注我的其它发布渠道