ThreeJS 里元素如下:
1.场景(Scene):是物体、光源等元素的容器, 2.相机(Camera):控制视角的位置、范围以及视觉焦点的位置,一个3D环境中只能存在一个相机 3.物体对象(Mesh):包括二维物体(点、线、面)、三维物体、粒子 4.光源(Light):包括全局光、平行光、点光源 5.渲染器(Renderer):指定渲染方式,如webGL\canvas2D\Css2D\Css3D等。 6.控制器(Control): 相机控件,可通过键盘、鼠标控制相机的移动
一、camera
1.三维里的相机分为两种,一种是透视相机,一种是正交相机。
//透视投影相机:
var camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );
//正交投影相机:
THREE.OrthographicCamera(left, right, top, bottom, near, far)
2.属性
fov
— 摄像机视锥体垂直视野角度-aspect
— 摄像机视锥体长宽比near
— 摄像机视锥体近端面far
— 摄像机视锥体远端面zoom
—设置摄像机的缩放倍数
3.两个重要参数
camera.position:控制相机在整个3D环境中的位置(取值为3维坐标对象-THREE.Vector3(x,y,z)) camera.lookAt:控制相机的焦点位置,决定相机的朝向(取值为3维坐标对象-THREE.Vector3(x,y,z))
camera.position.set(0, 0, 1000);
4.方法
.updateProjectionMatrix () : null
更新摄像机投影矩阵。在任何camer属性参数被改变以后必须被调用。
.toJSON () : JSON
使用JSON格式来返回摄像机数据。
二、model
(一)Mesh
mesh包括形状Geometry
和材质Material
1.属性:
name:可以通过scene.getObjectByName(name)获取该物体对象;
userData:可以存放用户自定义的信息;
id:是mesh实例唯一标识
2.方法
clone() / copy()
.copy()
方法简单的说就是复制一个对象的属性值赋值给给另一个对象对应的属性
.clone()
是相当于新建一个对象,然后复制原对象的属性值赋值给新的对象对应属性,创建一个和原来对象完全一样的对象
网格模型对象
Mesh
调用clone(),也会返回一个新对象,但是两mesh共享模型几何体和材质对象,修改其中一个mesh的几何体属性,另一个也会跟着变化。几何体Geometry克隆或复制,
Geometry.vertices
不是获得对象的索引值,而是深拷贝属性的值,你修改其中一个Geometry.vertices
的值,另一个不会发生变化。
getObjectById / getObjectByName
根据唯一ID或name获取mesh实例
注意:mesh 缩放使用 mesh.scale.copy(10, 10, 10);
不可以直接使用 mesh.scale=***
cubeGeo = new THREE.CircleGeometry(1, 64);
cubeMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
});
this.circleMesh = new THREE.Mesh(cubeGeo, cubeMaterial);
(二)Geometry
包括平面Plane、圆形Circle、立方体Cube、球体Sphere、圆柱Cylinder、多面体Polyhedron等
除去这些已经定义好的几何体模型外,还可以利用ShapeGeometry实现自定义,例如:
//triangle
this.triangleShape = new THREE.Shape()
.moveTo(0, 0)//Move the .currentPoint to x, y
.lineTo(1, 1)//Connects a LineCurve from .currentPoint to x, y onto the path.
.lineTo(2, 0)
.lineTo(0, 0); // close path
this.triangleGeometry = new THREE.ShapeGeometry(this.triangleShape);
this.triangleMesh = new THREE.Mesh(
this.triangleGeometry,
new THREE.MeshPhongMaterial({ color: 0x0000ff })
);
BoxBufferGeometry
、SphereBufferGeometry
可以分别用来创建长方体、球体
BoxGeometry
、SphereGeometry
也可以用来分别创建长方体、球体
上文提到的几何体模型的基类为BufferGeometry(缓冲区几何对象)和Geometry(普通几何对象),他俩的区别是使用BufferGeometry比Geometry性能更好点。
// create a simple square shape. We duplicate the top left and bottom right
// vertices because each vertex needs to appear once per triangle.
const vertices = new Float32Array( [
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, -1.0, 1.0
] );
// itemSize = 3 because there are 3 values (components) per vertex
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
//或者
//geometry.setFromPoints([...pointPositions])//pointPositions为[vector3,vector3...]
1.将几何转换为BufferGeometry
var bufferGeometry = new THREE.BufferGeometry().fromGeometry( geometry );
2.几何体位置转换
(1)父子关系,父对象的位置发生改变子对象也会跟着改变,但子对象的位置是相对父对象的;子对象的位置发生改变,父对象的位置不会改变。
父对象位置改变,若想得到子对象mesh的世界坐标,则需要执行以下方法:
for (let i = 0; i < children.length; i++) {
let vec = children[i].position;
vec.applyMatrix4(group.matrix); // 应用网格的矩阵变换
children[i].position.copy(vec);
}
//或者
(2)网格模型位置发生改变,但是几何体Geometry的vertices不会立即发生变化,具体情况如下:
this.lineMesh = new THREE.LineLoop(
this.lineGeometry.clone().setFromPoints(this.pointPositions),
this.lineMaterial
);
this.lineMesh位置发生改变,但是this.lineGeometry的vertices不会立即发生改变,需要执行以下方法
for (let i = 0; i < length; i++) {
vec = new THREE.Vector3();
attribute = obj.geometry.attributes.position; // 我们想要位置数据
vec.fromBufferAttribute(attribute, i); // 提取x,y,z坐标
vec.applyMatrix4(obj.matrix); // 应用网格的矩阵变换
children[i].position.copy(vec);//vec即为位置改变后vertices里点的值
}
(三)Material
Mesh基本材质MeshBasicMaterial
//纯色材质
material = new THREE.MeshBasicMaterial({
color: 0xff0000
}),
//贴图
let texture = await new THREE.TextureLoader().load(currentPlatformImgInfo.img);
let texture = await new THREE.TextureLoader().load(currentPlatformImgInfo.img);
let geometry = new THREE.PlaneGeometry(currentPlatformImgInfo.width, currentPlatformImgInfo.height);
//transparent: true
let material = new THREE.MeshBasicMaterial({ map: texture, name: 'material-background',transparent: true });
//或者在loader的回调函数里完成贴图
const loader = new THREE.TextureLoader();
loader.load(
url,
// 加载完贴图后的回调函数
function (texture) {}
)
注意:图片是异步加载的,必须在加载完成之后在进行贴图
PointsMaterial 点的材质
LineBasicMaterial 线的基础材质 LineDashedMaterial 虚线的基础材质
MeshBasicMaterial 网格基础材质
MeshDepthMaterial 网格深度材质 根据网格到相机距离 染色
材质修改
mesh.material = newMaterial;
三、Light
全局光:
THREE.AmbientLight,影响整个scene的光源,一般是为了弱化阴影或调整整体色调,可设置光照颜色,以颜色的明度确定光源亮度 平行光:
THREE.DirectionalLight,模拟类似太阳的光源,所有被照射的区域亮度是一致的,可设置光照颜色、光照方向(通过向量确定方向),以颜色的明度确定光源亮度 点光源:
THREE.PointLight:单点发光,照射所有方向,可设置光照强度,光照半径和光颜色
四、渲染器 renderer
渲染器决定了渲染的结果应该画在元素的什么元素上面,并且以怎样的方式来绘制。
Threejs中提供了很多的渲染方式,主要介绍 CanvasRenderer 、WebGLRenderer两种。
注:CanvasRenderer 和 WebGLRenderer 都是使用HTML5的
(一)WebGLRenderer
WebGL渲染器使用WebGL来绘制场景(如果设备支持),使用WebGL能够利用GPU硬件加速从而提高渲染性能
//开启反锯齿
var renderer = new THREE.WebGLRenderer({antialias: true});
由于在3D图像中,受分辨的制约,物体边缘总会或多或少的呈现三角形的锯齿,而抗锯齿就是指对图像边缘进行柔化处理,使图像边缘看起来更平滑,更接近实物的物体。
(二)CanvasRenderer
Canvas渲染器不使用WebGL来绘制场景,使用相对较慢的Canvas 2D Context API。
var renderer = new THREE.CanvasRenderer();
在不确定浏览器是否支持WebGL渲染器的时候,可以通过以下代码来实现渲染器的选择:
function webglAvailable() {
try {
var canvas = document.createElement( 'canvas' );
return !!( window.WebGLRenderingContext && (canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ));
} catch ( e ) {
return false;
}
}
if ( webglAvailable() ) {
renderer = new THREE.WebGLRenderer();
} else {
renderer = new THREE.CanvasRenderer();
}
五、控制器
1.TransformControls
用来转换 3D 空间中的对象。与其他控件不同,它不用于变换场景的相机。
//实例化
transformControl = new TransformControls(
camera,
renderer.domElement
);
//属性更改可以添加事件侦听器的单独事件。事件类型是“propertyname-changed”。
transformControl.addEventListener(
'dragging-changed',
function (event) {
controls.enabled = !event.value;
}
);
//受控 3D 对象发生更改,则触发。
transformControl.addEventListener('objectChange', () => {
this.isMove = true;
this.updateSplineOutline();
});
//何为受控
transformControl.attach(object);
transformControl.getRaycaster (); //射线投射器 返回用于用户交互的Raycaster对象
transformControl.setSize();//设置助手 UI 的大小。
propertyname有axis 、camera、domElement、enabled(是否启用控件,在于其他操作冲突时可用) 、object 、showX 、size 、translationSnap
怎么让 transformControl 助手 UI 的大小跟随画布缩放,这个问题我还没解决
2.OrbitControls
轨道控制允许相机围绕目标运行,一般用于全景
//实例化
const controls = new OrbitControls(camera, renderer.domElement);
controls.damping = 0.2;
//当相机被控件转换时触发。
controls.addEventListener('change', this.render);
3.TrackballControls
TrackballControls 类似于OrbitControls。但是,它不保持恒定的相机向上矢量。这意味着如果相机围绕“北极”和“南极”运行,它不会翻转以保持“正面朝上”。
TrackballControls可设置参数比较多,意味着功能会比OrbitControls更强大
4.DragControls
dragControsl可以用来拖动Group需要设置 dragControls.transformGroup = true;
let draggableObjects = dragControls.getObjects();
//draggableObjects为只读属性,所以只能通过设置length = 0来删除之前操作对象
draggableObjects.length = 0;
draggableObjects.push(group);
当对象被移除scene后,也需要从draggableObjects里移除该对象
dragControls = new DragControls(
curbeObj.children,
this.camera,
this.renderer.domElement
);
//移入模型
dragControls.addEventListener('hoveron', () => {
//选中模型
this.orbitControls.enabled = false; // 关闭orbitControls 控制器
});
//移出模型
dragControls.addEventListener('hoveroff', () => {
//选中模型
this.orbitControls.enabled = true; // 关闭orbitControls 控制器
});
//拖拽启动
dragControls.addEventListener('dragstart', (event) => {
this.transformControl.attach(event.object);//将选中对象绑定transformControl
console.log(event.object);
});
//拖过程
dragControls.addEventListener('drag', () => {
this.render();//
});
//拖完
dragControls.addEventListener('dragend', () => {
this.transformControl.detach();//解除transformControl绑定
// event.object.material.emissive.set(0x000000);
});
5.MapControls
地图控件,如果查看类似地图模型或者不希望用户对模型进行反转的时候可以使用这个控件。
6.CameraControls
优势:放大缩小,鼠标点击位置不变
export default class CameraControl {
private readonly cameraControls: CameraControls;
constructor(renderer: THREE.WebGL1Renderer, scene: THREE.Scene, camera: THREE.PerspectiveCamera, dom: HTMLElement) {
this.cameraControls = new CameraControls(camera, dom);
this.init(renderer, scene, camera);
}
enable() {
this.cameraControls.enabled = true;
}
disable() {
this.cameraControls.enabled = false;
}
private init(renderer: THREE.WebGL1Renderer, scene: THREE.Scene, camera: THREE.PerspectiveCamera): void {
this.cameraControls.dollyToCursor = true;
this.cameraControls.mouseButtons.left = CameraControls.ACTION.TRUCK;
this.cameraControls.mouseButtons.right = CameraControls.ACTION.TRUCK;
// default disable this control
this.cameraControls.enabled = false;
const clock = new THREE.Clock();
let anim = () => {
const delta = clock.getDelta();
const hasControlsUpdated = this.cameraControls.update(delta);
requestAnimationFrame(anim);
if (hasControlsUpdated) {
renderer.render(scene, camera);
}
};
anim();
}
}
以上控件不再需要时需要调用dispose () 方法处理掉
六、鼠标选取物体
1.使用GPU选取物体
创建选取材质,将场景中的每个模型的材质替换成不同的颜色。
读取鼠标位置像素颜色,根据颜色判断鼠标位置的物体。
pick() {
//render the picking scene off-screen
// set the view offset to represent just a single pixel under the mouse
//window.devicePixelRatio是设备上物理像素和设备独立像素(device-independent pixels (dips))的比例。
this.camera.setViewOffset(
this.renderer.domElement.width,
this.renderer.domElement.height,
(hoverPosition.x * window.devicePixelRatio) | 0,
(hoverPosition.y * window.devicePixelRatio) | 0,
1,
1
);
// console.log(camera);
// render the scene
//指定下面渲染所在位置为 pickingTexture,不填默认为cavans画布
this.renderer.setRenderTarget(this.pickingTexture);
this.renderer.render(this.pickingScene, this.camera);
// clear the view offset so rendering returns to normal
// console.log(renderer);
this.camera.clearViewOffset();
//create buffer for reading single pixel
const pixelBuffer = new Uint8Array(4);
// console.log(pickingTexture);
//read the pixel
//从 renderTarget 读取像素数据到你传入的缓冲区中。这是一个围绕WebGLRenderingContext.readPixels ()的包装器。
this.renderer.readRenderTargetPixels(
this.pickingTexture,
0,
0,
1,
1,
pixelBuffer
);
// if (pixelBuffer[0] !== 0)
console.log(pixelBuffer);
//interpret the pixel as an ID
const id =
(pixelBuffer[0] << 16) | (pixelBuffer[1] << 8) | pixelBuffer[2];
const data = pickingData[id];
if (data) {
//move our highlightBox so that it surrounds the picked object
if (data.position) {
// console.log(data.position);
highlightBox.position.copy(data.position);
// highlightBox.rotation.copy(data.rotation);
highlightBox.visible = true;
// console.log(highlightBox);
}
} else {
highlightBox.visible = false;
}
}
2.光线投射法
将鼠标在屏幕上的位置转化为标准设备坐标,与camera连接形成一条射线,射线穿过Mesh即为选中的目标。
此法缺点:当模型非常大,比如说有40万个面,通过遍历的方法选取物体和计算碰撞点位置将非常慢,用户体验不好。
let { clientWidth, clientHeight } = this.container;
pointer.x = (event.offsetX / clientWidth) * 2 - 1;
pointer.y = -(event.offsetY / clientHeight) * 2 + 1;
raycaster.setFromCamera(pointer, this.camera);
const curbIntersects = raycaster.intersectObjects(
curbeObj.children
);
return curbIntersects.length == 0 ? null : curbIntersects[0].object;
参考用three.js开发三维地图实例、Three.js 拾取之GPU Picking的理解和思考
七、性能优化
删除模型对象(.remove()
和·dispose()
方法)
remove / removeFromParent
一个网格模型Mesh是包含几何体geometry和材质对象Material的,几何体geometry本质上就是顶点数据,Three.js通过WebGL渲染器解析几何体的时候会调用WebGL API创建顶点缓冲区来存储顶点数据。
如果仅仅执行scene.remove(Mesh)
只是把网格模型从场景对象的.children
属性中删除,解析网格模型Mesh几何体的顶点数据通过WebGL API创建的顶点缓冲区占用的内存并不会释放。
处理材质对象. 材质的纹理不能得到处理. 材质的纹理需要通过纹理对象Texture的dispose方法实现
Material·dispose()
删除场景对象中Scene
一个子对象Group
,并释放组对象Group
中所有网格模型几何体的顶点缓冲区占用内存
// 递归遍历组对象group释放所有后代网格模型绑定几何体占用内存
group.traverse(function(obj) {
if (obj.type === 'Mesh') {
obj.geometry.dispose();
obj.material.dispose();
}
})
// 删除场景对象scene的子对象group
scene.remove(group);
八、ThreeJS 三种坐标系
1.分类
-
世界坐标(右手坐标)
世界坐标系默认就是对Threejs整个场景Scene建立一个坐标系
- 屏幕坐标系
ThreeJS 是使用了 canvas
画布绘制图形的,因此屏幕坐标系就是 canvas
中的坐标系,也就是左上角是坐标原点:
-
标准设备坐标系
标准设备坐标系是三维的,其原点默认在屏幕中心,且
x y z
的范围是[-1,1]
,因此其x
、y
轴在屏幕坐标系中的表示就是:
2.坐标系转换
(1)屏幕坐标转世界坐标
屏幕坐标转空间坐标需要经过两个步骤:屏幕坐标 -> 标准设备坐标 -> 世界坐标
设屏幕一点A(x,y),A点对应标准设备中点O点A^的坐标为(x1,y1),可得两坐标之间关系为 x1 = x - width / 2, y1 = - (y - height / 2)
再标准化到[-1,1]之间,即得A对应标准设备坐标系中的点坐标( x1 / (width / 2) , - (y - height / 2) / height / 2 )
然后,再通过 Vector3.unproject(camera)
方法将标准设备坐标转为世界坐标:
代码如下:
const x = event.clientX;//鼠标单击坐标X
const y = event.clientY;//鼠标单击坐标Y
// 屏幕坐标转标准设备坐标
const x1 = ( x / window.innerWidth ) * 2 - 1;
const y1 = -( y / window.innerHeight ) * 2 + 1;
//标准设备坐标(z=0.5这个值并没有一个具体的说法)
const stdVector = new Vector3(x, y, 0.5);
const worldVector = stdVector.unproject(camera);
(2)世界坐标转屏幕坐标
屏幕坐标转空间坐标需要经过两个步骤:世界> 标准设备坐标 -> 屏幕坐标
先将世界坐标系使用 project
方法转换到标准设备坐标系,再转换到屏幕坐标系中:
const standardVec = worldVector.project(camera);
//世界坐标转换屏幕坐标偏差问题
function wordPosToScreen(object,camera) {
var vector = new THREE.Vector3();
var widthHalf = 0.5 * window.innerWidth;
var heightHalf = 0.5 * window.innerHeight;
object.updateMatrixWorld(); /*这段代码是重要的在获取前先更新下对象的世界坐标/世界矩阵*/
vector.setFromMatrixPosition(object.matrixWorld);
vector.project(camera);
vector.x = (vector.x * widthHalf) + widthHalf;
vector.y = -(vector.y * heightHalf) + heightHalf;
return {
x: vector.x,
y: vector.y
};
}
九、边缘库 stats.js
this.stats = new Stats();
this.stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(this.stats.dom);
......
animate() {
// this.stats.begin();
this.stats.update();
..............
// this.stats.end();
requestAnimationFrame( animate );
}
最后一秒渲染的FPS帧数。数字越高越好。
MS毫秒需要渲染的帧。数字越小越好。
MB已分配内存的MB。(用 运行 Chrome
--enable-precise-memory-info
)
十、ThreeJS里的一些方法
计算a向量到所传入的v间的距离a.distanceTo ( v : Vector3 ) : Float
利用ShapeGeometry形成的mesh设置mesh的position属性不是中心点,mesh旋转中心也不是自己的中心点,怎么解决这个问题?
如何将mesh的position对准mesh中心点
//必须先调用computeBoundingBox方法计算
scene.children[0].geometry.computeBoundingBox();
// 把对象放到坐标原点
scene.children[0].geometry.center();
如何解决旋转中心不是中心点问题
let center = new THREE.Vector3();
scene.children[0].geometry.computeBoundingBox();
scene.children[0].geometry.boundingBox.getCenter(center);
let x = center.x;
let y = center.y;
let z = center.z;
// 把对象放到坐标原点
scene.children[0].geometry.center();
// 绕轴旋转
scene.children[0].rotation.z = Date.now() * 0.001;
// 再把对象放回原来的地方
scene.children[0].geometry.translate(x, y, z);
//最后调用render
查看某对象是否在group里
group.children.includes( object ) === true
ThreeJS bufferGeometry位置属性在应用转换时不更新
mesh = new THREE.Mesh(geometry, material);
mesh.position.set(10, 10, 10);
mesh.rotation.set(- Math.PI/2, 0, 0);
mesh.scale.set(1, 1, 1);
scene.add(mesh);
mesh.updateMatrix(); // make sure the mesh's matrix is updated
var vec = new THREE.Vector3();
var attribute = mesh.geometry.attributes.position; // we want the position data
var index = 1; // index is zero-based, so this the the 2nd vertex
vec.fromAttribute(attribute, index); // extract the x,y,z coordinates
vec.applyMatrix4(mesh.matrix); // apply the mesh's matrix transform
由点连成线,当point作为在line的children时,使用dragControl平移了线,但是点的坐标不变时,使用下面方法
for (let i = 0; i < obj.userData.positions.length; i++) {
vec = new THREE.Vector3();
attribute = obj.geometry.attributes.position; // 我们想要位置数据
vec.fromBufferAttribute(attribute, i); // 提取x,y,z坐标
vec.applyMatrix4(obj.matrix); // 应用网格的矩阵变换
children[i].position.copy(vec);
pointLists.push(vec);
}
十一、Three.js的渲染机制
renderer = new THREE.WebGLRenderer({ antialias: true }),
antialias - 是否执行抗锯齿。默认为false。
three的渲染器是基于webGL的。它的渲染机制是根据物体离照相机的距离来控制和进行渲染的。对于透明的物体,则是按照从最远到最近的顺序进行渲染。也就是说,它根据物体的空间位置进行排序,然后根据这个顺序来渲染物体。
1.renderer.sortObjects = false;
物体的渲染顺序将会由他们添加到场景中的顺序所决定。适合大部分场景。
2.renderer.sortObjects = true;
并且给特定的物体设置object.renderOrder 指定它的渲染顺序。
3.material1.depthWrite = false;
对于透明物体
如果发现场景中的透明物体显示有问题,例如旋转摄像机的时候会出现闪烁等问题。可以尝试以下几种方法:
1.设置
material.transparent = false;1
注意:不透明物体将会优先渲染。所以这也可以作为控制渲染顺序的一个方法。
2.设置
material.alphaTest = 0.1;1
自己尝试改变不同的alpha测试值,以适合你自己的场景。 3.尝试改变sortObject和depthWrite的值等等。
十二、WebGLRenderTarge
WebGLRenderTarget,它是一个缓冲,就是在这个缓冲中,视频卡为正在后台渲染的场景绘制像素。 它用于不同的效果,例如把它做为贴图使用或者图像后期处理。
WebGLRenderTarget的构造器有三个参数,分别是width,height和options。宽高就是RenderTarget的高,设置的同时也会把它们赋值给texture.image的width和height属性。
WebGLRenderTarget的属性有
width | 渲染目标宽度 |
---|---|
height | 渲染目标高度 |
scissor | 渲染目标视口内的一个矩形区域,区域之外的片元将会被丢弃 |
scissorTest | 表明是否激活了剪裁测试 |
viewport | 渲染目标的视口 |
texture | 纹理实例保存这渲染的像素,用作进一步处理的输入值 |
depthBuffer | 渲染到深度缓冲区。默认true |
stencilBuffer | 渲染到模具缓冲区。默认false |
depthTexture | 如果设置,那么场景的深度将会被渲染到此纹理上。默认是null |
WebGLRenderTarget的方法
方法 | 描述 |
---|---|
setSize | 设置渲染目标的大小 |
clone | 创建一个渲染目标副本 |
copy | 采用传入的渲染目标的设置 |
dispose | 发出一个处理事件 |
十三、Three.js使用dat.GUI简化试验流程
使用这个插件的最省事的地方在于,调试很方便的调节相关的值,从而影响最后绘制的结果。而dat.GUI实现的东西也很简单,理解起来也很好理解。
最后附上之前的参考案例:
通过鼠标点击平面实现任意画线功能
ThreeJS借助插件可以加载的文件类型