数字孪生近几年被提到的越来越多,基于此来看看学习记录一下vue+three.js学习中的一些问题
一款运行在浏览器中的3D引擎。three.js是一个webgl为基础的库,对webGL的3D渲染工具方法与渲染循环封装的js库,省去与繁琐底层接口的交互,通过threeJS就可以快速生成三维模型。
npm install --s three
import * as THREE from “three”;
<template>
<div>
<div id="container">div>
div>
template>
<script>
import * as THREE from "three";
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
export default {
data() {
return {
camera: null,
scene: null,
renderer: null,
mesh: null,
controls:null
};
},
mounted() {
this.init();
this.animate();
},
methods: {
//初始化
init: function() {
// 创建场景对象Scene
this.scene = new THREE.Scene();
//网格模型添加到场景中
let geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
let material = new THREE.MeshNormalMaterial({
color: "white"
});
this.mesh = new THREE.Mesh(geometry, material);
this.scene.add(this.mesh);
/**
* 相机设置
*/
let container = document.getElementById("container");
this.camera = new THREE.PerspectiveCamera(
70,
container.clientWidth / container.clientHeight,
0.01,
10
);
this.camera.position.z = 1;
/**
* 创建渲染器对象
*/
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(this.renderer.domElement);
//创建控件对象
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
},
// 动画
animate: function() {
requestAnimationFrame(this.animate);
this.mesh.rotation.x += 0.01;
this.mesh.rotation.y += 0.02;
this.renderer.render(this.scene, this.camera);
}
}
};
script>
<style>
#container {
position: absolute;
width: 100%;
height: 100%;
}
style>
引用自简书,点击链接跳转
需要场景(scene)、相机(camera)和渲染器(renderer),他们是图形渲染得重要部分
承载所有模板的容器,允许渲染模型和位置
new THREE.Scene()
场景中人眼的角色,决定场景中模型的远近、高度角度等参数。
提供正投影相机、透视相机、立体相机等多种相机模式,常用的为前两种
正投影相机
new THREE.OrthographicCamera( left, right, top, bottom, near, far )
分别设置相机的左边界,右边界,上边界,下边界,远面,近面
透视相机
new THREE.PerspectiveCamera( fov, aspect, near, far )
分别设置相机的视场角度,长宽比,近面,远面
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(container.clientWidth,
container.clientHeight);
this.renderer.render(scene, camera)
渲染器决定了渲染的结果应该画在页面的什么元素上面,并且以怎样的方式来绘制
项目中为了代码可读性,最好保证一定的场景创建顺序
this.initRender(); //初始化场景渲染器
this.initScene(); //加载场景容器
this.initCamera(); //加载摄像机
this.initLight(); //加载灯光
this.initControls(); //鼠标操作镜头画面
this.animate();
//加载gltf模型
快捷获取到坐标方式:绑定双击事件,在模型中调整好视角之后双击,获取到当前坐标
this.container.addEventListener('dblclick', this.onDoubleClick, false); //双击
//添加双击事件
onDoubleClick() {
console.log('newT =', JSON.stringify(controls.target).replace(/\"/g, ''));
console.log('newP =', JSON.stringify(camera.position).replace(/\"/g, ''));
cssScene.traverse(item => console.log(item.name))
},
使用tween实现
animateCamera(newP, newT, callBack, time = 2000, flag = '0') {
let campos = camera.position, target = controls.target
const tween = new TWEEN.Tween({x1: campos.x, y1: campos.y, z1: campos.z, x2: target.x, y2: target.y, z2: target.z});
tween.to({x1: newP.x, y1: newP.y, z1: newP.z, x2: newT.x, y2: newT.y, z2: newT.z}, isProduct ? time : 0);
tween.onUpdate(object => {
camera.position.x = object.x1;
camera.position.y = object.y1;
camera.position.z = object.z1;
controls.target.x = object.x2;
controls.target.y = object.y2;
controls.target.z = object.z2;
controls.update();
});
tween.onComplete(() => this.callBack(flag));
tween.easing(TWEEN.Easing.Cubic.InOut);
tween.start();
},
高亮显示模型(呼吸灯)https://wow.techbrood.com/fiddle/56603
OutlineObj(selectedObjects, color = 0x00ffff) {
//移除发光标记
this.iShine = JSON.stringify(selectedObjects) !== '[]';
// 创建一个EffectComposer(效果组合器)对象,然后在该对象上添加后期处理通道。
this.composer = new EffectComposer(renderer);
// 新建一个场景通道 为了覆盖到原理来的场景上
this.renderPass = new RenderPass(scene, camera);
this.composer.addPass(this.renderPass);
// 物体边缘发光通道
let object = new THREE.Vector2(this.out.offsetWidth, this.out.offsetHeight);
this.outlinePass = new OutlinePass(object, scene, camera, selectedObjects);
this.outlinePass.selectedObjects = selectedObjects;
this.outlinePass.edgeStrength = 10; // 边框的亮度,最大10
this.outlinePass.edgeGlow = 1; // 光晕[0,1]//,最大1
this.outlinePass.usePatternTexture = false; // 是否使用父级的材质,纯色的可以使用
this.outlinePass.edgeThickness = 1; // 边框宽度//最大4
this.outlinePass.downSampleRatio = 1.5; // 边框弯曲度//之前为2,,1比较合适
this.outlinePass.pulsePeriod = 1; // 呼吸闪烁的速度//5
this.outlinePass.visibleEdgeColor.set(color); // 呼吸显示的颜色16进制,0x00ffff
this.outlinePass.hiddenEdgeColor = new THREE.Color(0, 0, 0); // 呼吸消失的颜色(0,0,0)
this.outlinePass.clear = true;
this.composer.addPass(this.outlinePass);
// 自定义的着色器通道 作为参数
const effectFXAA = new ShaderPass(FXAAShader);
effectFXAA.uniforms.resolution.value.set(1 / this.out.offsetWidth, 1 / this.out.offsetHeight);
effectFXAA.renderToScreen = true;
this.composer.addPass(effectFXAA);
//用于更新轨道控制器
clock = new THREE.Clock();
//是否可以缩放
controls.enableZoom = true;
//是否自动旋转
controls.autoRotate = false;
//最大纵向旋转角度
controls.maxPolarAngle = Math.PI / 2;
//是否开启右键拖拽
controls.enablePan = true;
},
//line12是一半蓝色一半透明图片,line11是一半灰色一半透明图片
pipelineCurve:textureLoader.load(`${path}/组态图/line12.png`),
pipelineCurve2:textureLoader.load(`${path}/组态图/line12.png`),
//曲线路径以及在路径上重复铺几次
pipelineCurvePoints:[
[
new THREE.Vector3(-933,-144,-203), //1
new THREE.Vector3(-933,-143,-243),
new THREE.Vector3(-933,-142,-283),
new THREE.Vector3(-933,-141,-323),
new THREE.Vector3(-933,-140,-363),
new THREE.Vector3(-933,-140,-373),
new THREE.Vector3(-933,-140,-383),
new THREE.Vector3(-933,-140,-393),
new THREE.Vector3(-933,-140,-400),
new THREE.Vector3(-933,-140,-408), //2
new THREE.Vector3(-893,-140,-408),
new THREE.Vector3(-853,-140,-408),
new THREE.Vector3(-813,-140,-408),
new THREE.Vector3(-773,-140,-408),
new THREE.Vector3(-733,-140,-408),
new THREE.Vector3(-693,-140,-408),
new THREE.Vector3(-683,-140,-408),
new THREE.Vector3(-673,-140,-408),
new THREE.Vector3(-663,-140,-408),
new THREE.Vector3(-654,-140,-408),//3
],
[
new THREE.Vector3(-654,-144,-203), //1
new THREE.Vector3(-654,-140,-408), //2
]
],
pipelineCurveRepeatX:[10, 5]
//调用
this.pipeline(this.pipelineCurve, this.pipelineCurvePoints[0], this.pipelineCurveRepeatX[0], "pipelineCurve");
this.pipeline(this.pipelineCurve2, this.pipelineCurvePoints[1], this.pipelineCurveRepeatX[1], "pipelineCurve2");
/* region 组态图管道 */
pipeline(pipelineCurve, points, repeatX = 10, meshName) {
let pipelineCurveClone = new THREE.CatmullRomCurve3(
points,
false
);
//曲线,路径,即管道的形状|管道分成多少段|管道的半径|管道口分成多少段,即管道口是几边形|是否闭合管道,首尾相接
let tubeGeometry = new THREE.TubeGeometry(
pipelineCurveClone,
80,
8,
40,
false
); //6 0.1
// 设置阵列模式为 RepeatWrapping
pipelineCurve.wrapS = THREE.RepeatWrapping;
pipelineCurve.wrapT = THREE.RepeatWrapping;
// 设置x方向的偏移(沿着管道路径方向),y方向默认1
//等价texture.repeat= new THREE.Vector2(20,1)
pipelineCurve.repeat.x = repeatX; //此路径上重复铺几次
pipelineCurve.repeat.y = 1; //此路径上重复铺几次
//this.pipelineCurve.offset.y = 1.5;//贴图旋转
let tubeMaterial = new THREE.MeshPhongMaterial({
map: pipelineCurve,
transparent: true,
//opacity: 1,//透明的
side: THREE.DoubleSide //两面可见
});
let mesh = new THREE.Mesh(tubeGeometry, tubeMaterial);
mesh.name = meshName;
this.diagramGroup.add(mesh);
},
/* endregion */
//绑定数据以及流动效果
let arr = [];
this.loadPipeState(this.pipelineCurve, 'pvOnBuilding1', 0, 'pipelineCurve', arr)
this.loadPipeState(this.pipelineCurve2, 'chargeInBuilding1', 1, 'pipelineCurve2', arr)
if (arr.length > 0){
arr.forEach(one => this.diagramGroup.remove(one));
}
//pipelineCurveOffset是这一整块的封装
setTimeout(() => levels === 'Diagram' && this.pipelineCurveOffset(), 60);
//加载每条管道的状态
loadPipeState(pipelineCurve, valueName, number, meshName, arr){
if (parseFloat(this.diagramData[valueName]) < 0){
pipelineCurve.offset.x -= 0.08;
if (this.pipelineCurveLine[number] === 11){
this.diagramGroup.traverse(child => child instanceof THREE.Mesh && child.name === meshName&&arr.push(child));
pipelineCurve = textureLoader.load(`${path}/组态图/line12.png`);
this.pipeline(pipelineCurve, this.pipelineCurvePoints[number], this.pipelineCurveRepeatX[number], meshName);
this.pipelineCurveLine[number] = 12;
}
}else if (parseFloat(this.diagramData[valueName]) > 0) {
pipelineCurve.offset.x += 0.08;
if (this.pipelineCurveLine[number] === 11){
this.diagramGroup.traverse(child => child instanceof THREE.Mesh && child.name === meshName&&arr.push(child));
pipelineCurve = textureLoader.load(`${path}/组态图/line12.png`);
this.pipeline(pipelineCurve, this.pipelineCurvePoints[number], this.pipelineCurveRepeatX[number], meshName);
this.pipelineCurveLine[number] = 12;
}
}else {
if (this.pipelineCurveLine[number] === 12){
this.diagramGroup.traverse(child => child instanceof THREE.Mesh && child.name === meshName&&arr.push(child));
pipelineCurve = textureLoader.load(`${path}/组态图/line11.png`);
this.pipeline(pipelineCurve, this.pipelineCurvePoints[number], this.pipelineCurveRepeatX[number], meshName);
this.pipelineCurveLine[number] = 11;
}
}
},
纹理中包含的颜色信息(.map, .emissiveMap, 和 .specularMap)在glTF中总是使用sRGB颜色空间,而顶点颜色和材质属性(.color, .emissive, .specular) 则使用线性颜色空间。在典型的渲染工作流程中,纹理会被渲染器转换为线性颜色空间,进行光照计算,然后最终输出会被转换回 sRGB 颜色空间并显示在屏幕上。在render中加入以下代码
renderer.outputEncoding = THREE.sRGBEncoding;
//不加下面这句,背景也会变亮
scene.background.encoding = THREE.sRGBEncoding;
另一个方案:材质丢失,再赋值
this.diagramGroup.traverse(child =>{
//材质丢失,再赋值
if(child instanceof THREE.Mesh){
child.material.emissive = child.material.color;
child.material.emissiveMap = child.material.map
}
})
它根据场景的明暗对比, 把 HDR 高动态范围光照非线性的 ToneMapping 映射到显示器能显示的 LDR 低动态光照范围,尽可能的保存了明暗对比细节,使最终渲染效果更加逼真。
pmremGenerator = new THREE.PMREMGenerator( renderer );
pmremGenerator.compileEquirectangularShader();
this.getCubeMapTexture();
getCubeMapTexture () {
new RGBELoader()
.setDataType( UnsignedByteType )
.load( `${path}/组态图/venice_sunset_1k.hdr`, ( texture ) => {
console.log(texture)
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
console.log(envMap)
pmremGenerator.dispose();
scene.environment = envMap;
});
},
使用的是gltf-pipeline进行文件压缩
npm install -g gltf-pipeline
gltf-pipeline -i demo.gltf -d -s
//引入
import {DRACOLoader} from 'three/examples/jsm/loaders/DRACOLoader'
//创建一个gltf加载器
const loader = new GLTFLoader();
//这里是因为我将加载gltf整体封装了一个函数,compressed是true/false,代表传进来的是不是压缩过的gltf文件
if(compressed){
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath(`${publicPath}draco/gltf/`);
//设置解压库文件路径
loader.setDRACOLoader(dracoLoader);
}
//loader加载gltf,省略
......
项目里使用时是将页面嵌入到标签页中展示,使用的框架会对打开过的页面进行缓存,在切换标签时,触发不了beforeDestroy。所以首先需要解决页面缓存问题,之后解决释放内存问题。
首先是在缓存时增加判断,给title为"数字孪生"的项设置notCache为true
这里增加上是否需要缓存的判断,之后页面标签切换时可以正常触发beforeDestroy
下面是释放掉内存的方法, 不过存在一点问题,释放之后打印renderer.info其中的memory,依旧不是0。在释放以后再次加载时如果环境光等出现问题,可以scene先置null后new试一下
beforeDestroy() {
//清除定时器
const lastTimeoutId = setTimeout(null);
for (let i = 0; i <= lastTimeoutId._id; i++) {
clearTimeout(i);
}
scene.remove();
if (renderer){
renderer.dispose();
renderer.forceContextLoss();
renderer.content = null;
let gl = renderer.domElement.getContext('webgl');
gl && gl.getExtension('WEBGL_lose_context').loseContext();
}
THREE.Cache.clear();
window.removeEventListener('resize', this.onWindowResize);
this.container.removeEventListener('mousedown', this.onMouseDown, false) // 鼠标按下
this.container.removeEventListener('click', this.onMouseClick, false); //单击
this.container.removeEventListener('dblclick', this.onDoubleClick, false); //双击
cancelAnimationFrame(ranimationID);
},
加载之前:
DRACOLoader加载器的问题,这个加载器在加载时会创建四个Dedicated Worker,而且并不会自动释放,数量少的时候感知不明显,当不停的关闭打开数字孪生页面时,这些Dedicated Worker会累加,越来越多,必须进行手动释放
LoadGLTFOnly(modelArray, compressed = false) {
const loader = new GLTFLoader();
if(compressed){
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath(`${publicPath}draco/gltf/`);
//设置解压库文件路径
loader.setDRACOLoader(dracoLoader);
let number = 0;
//加载gltf文件
modelArray.forEach(one => {
loader.load(one.gltfUrl, gltf => {
number = number + 1;
const model = gltf.scene;
model.traverse(child => {
if (child instanceof THREE.Mesh) {
child.name = one.name;
}
});
//场景中添加模型文件
one.level === 'Cabinet' && this.cabinetGroup.add(model)
if (number === modelArray.length ){
for ( let i = 0; i < loader.dracoLoader.workerPool.length; ++ i ) {
//释放
loader.dracoLoader.workerPool[ i ].terminate();
}
}
});
});
return;
}
//加载gltf文件
modelArray.forEach(one => {
loader.load(one.gltfUrl, gltf => {
const model = gltf.scene;
model.traverse(child => {
if (child instanceof THREE.Mesh) {
child.name = one.name;
}
});
//场景中添加模型文件
one.level === 'Cabinet' && this.cabinetGroup.add(model)
});
});
},
vue集成three.js加载外部模型-一路从容
three.js模型压缩/gltf/glb,gltf-pipeline
Three.js中文网
create.js-主要使用到了tween.js
Web Workers
3D渲染图形是一个很好玩的东西,欢迎大家一起交流