最近做一个机泵项目,要求导入一个已有的机泵3D模型在浏览器中显示。前端使用vue来完成。现在将模型封装成一个组件pump.vue. 首先确保项目已经安装了three. (npm install three).
其中label是标签,可以在模型中完成对象的标注,这一功能使用three.js中的sprite来完成。有一个问题暂时还没有解决,就是在函数updateLabel以及函数genCanvas中存在内存泄漏的问题,即在更新label的时候并没有释放掉上一个label的空间,导致浏览器的GPU内存占用持续上涨。初步认为是genCanvas中的生成canvas画布产生的泄露,但这个功能被弃用所以暂时没有解决。
有一个高亮的需求使用 RenderPass, EffectComposer, OutlinePass 来完成,引入时需要安装 three-outlinepass (当然可以直接引入three中的这些包,但是我懒得找)。测试时在添加FBX模型那个函数中把需要高亮显示的对象添加到 selectedLists 列表中。然后在outlineObj函数中处理。最后在animate函数中对composer进行渲染。这里有个坑,我后面注释了一行 renderer.render(scene, camera); 如果没有讲这句话去掉,高亮的composer就不会显示。
<template>
<div ref="screen" style="flex: 1">
<div id="container"></div>
</div>
</template>
<script>
import * as THREE from 'three';
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader';
// import {MTLLoader, OBJLoader} from 'three-obj-mtl-loader';
import {FBXLoader} from 'three/examples/jsm/loaders/FBXLoader';
import elementResizeDetectorMaker from 'element-resize-detector';
import { RenderPass, EffectComposer, OutlinePass } from 'three-outlinepass'
let camera = '',
scene = '',
renderer = '';
let container = '';
let controls = '';
let gridHelper = '';
let axisHelper = '';
let modelGroup = '';
let mixer = '';
let clock = '';
let ctxes = [];
let composer = '';
let outlinePass = '';
let renderPass = '';
export default {
name: "pump",
props: {
'modelFilename': {
type: String,
default: 'static/models/mx/mx.FBX'
},
'metFilename': {
type: String,
default: 'static/models/mx/mx.mtl'
},
'configs': {
type: Object,
default: () => {
return {
bgColor: '#000000',
bgAlpha: 0.8
}
}
}
},
data() {
return {
models: [],
lights: [],
labels: [],
labelFont: '120px "华文彩体"',
aniPlay: true, // 控制动画是否播放
labelScale: 0.12, // 标签模型缩放比例
laCvsWidth: 1024,
laCvsHeight: 512,
selectedList: [], //选中的物体列表
}
},
methods: {
init(modelFilename, metFilename, modelType) {
if (modelType === null || modelType === 'fbx') {
this.sceneInit()
// console.log(scene)
// this.addObjModel(modelFilename, 1, metFilename);
// this.addGltfModel('static/models/Bee.glb', 0.05, '')
return this.addFBXModel(modelFilename, 0)
} else {
if (modelType === 'obj') {
this.addObjModel(modelFilename, metFilename, 1)
}
}
},
sceneInit() {
container = document.getElementById('container');
scene = new THREE.Scene(); //创建场景
camera = new THREE.PerspectiveCamera //创建相机
(65, container.clientWidth / container.clientHeight, 0.1, 100000);
camera.position.set(-120, 0, 400);
renderer = new THREE.WebGLRenderer({antialias: true, alpha: true}); //创建渲染器,antialias抗锯齿
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(container.clientWidth, container.clientHeight); //设置渲染器的尺寸
// renderer.setClearColor(this.configs.bgColor, this.configs.bgAlpha); //设置背景颜色
container.appendChild(renderer.domElement); //渲染器加入到dom中,这个元素是一个canvas
// renderer.render(scene, camera)
let ambient = this.addLight('AmbientLight', {color: 0x555555})
let pointLight = this.addLight('PointLight', {color: 0xaaaaaa})
let pointLight2 = this.addLight('PointLight', {color: 0xaaaaaa})
pointLight.position.set(600, 800, 1000);
pointLight2.position.set(-600, 800, 1000)
},
openGrid(isOpen) {
if (isOpen === true) {
gridHelper = new THREE.GridHelper(1000, 100, 0x888888, 0x444444);
gridHelper.position.y = 0;
gridHelper.position.x = 0;
gridHelper.name = "Grid";
scene.add(gridHelper);
} else
scene.remove(gridHelper)
},
openAxis(isOpen) {
if (isOpen === true) {
axisHelper = new THREE.AxisHelper(1000);
scene.add(axisHelper);
} else
scene.remove(axisHelper)
},
addLight(type, params) {
let light = new THREE[type](params.color)
this.lights.push(light)
scene.add(light)
return light
},
// 添加Obj模型
addObjModel(modelFilename, metFilename, scale) {
let mtlLoader = new MTLLoader();
let objLoader = new OBJLoader();
mtlLoader.load(metFilename, (materials) => {
materials.preload()
objLoader.setMaterials(materials)
objLoader.load(modelFilename, (object) => {
// object.position.set(-13.2, -0.3, 0)
// object.scale.set(scale, scale, scale);
// group.add(object)
scene.add(object)
console.log('obj模型加载成功');
console.log(object)
// camera.lookAt(new THREE.Vector3(10, 0, 0))
})
}, () => {
console.log('obj正在加载');
})
// let loader = new OBJLoader()
// // obj.setMaterials(new THREE.MeshNormalMaterial().clone())
// loader.load(modelFilename , obj => {
// obj.position.set(-6.7, 0, 0.2);
// obj.scale.set(scale, scale, scale);
// let material = new THREE.MeshLambertMaterial({color: '#ffffff'});
// // console.log(material)
// // 加载完obj文件是一个场景组Group,遍历它的子元素,每一个元素是一个Mesh
// obj.children.forEach(child => {
// child.material = material;
// child.geometry.computeFaceNormals();
// child.geometry.computeVertexNormals();
// });
// scene.add(obj);
//
// console.log('模型添加完成');
//
// this.models.push(obj)
// })
},
// 添加FBX模型
addFBXModel(modelFilename, scale) {
let _this = this
return new Promise((resolve, reject) => {
let fLoader = new FBXLoader()
fLoader.load(modelFilename, object => {
mixer = new THREE.AnimationMixer(object);
let action = mixer.clipAction(object.animations[0]);
action.play();
object.traverse(function (child) {
if (child instanceof THREE.Mesh) {
child.castShadow = true;
child.receiveShadow = true;
// 添加高亮的位置
if (Math.random() < 0.5)
_this.selectedList.push(child)
}
});
scene.add(object);
// console.log(object)
this.models.push(object);
// this.getPosition();
// // 添加标签
// let label1 = this.addLabel({x: -190, y: 170, z: 0},
// {
// title: '电机非驱动端',
// hori: '未知',
// vert: '未知',
// axis: '未知'
// }, this.labelFont)
// let label2 = this.addLabel({x: -80, y: 200, z: 0},
// {
// title: '电机驱动端',
// hori: '未知',
// vert: '未知',
// axis: '未知'
// }, this.labelFont)
// let label3 = this.addLabel({x: 130, y: 200, z: 0},
// {
// title: '水泵驱动端',
// hori: '未知',
// vert: '未知',
// axis: '未知'
// }, this.labelFont)
// let label4 = this.addLabel({x: 240, y: 160, z: 0},
// {
// title: '水泵非驱动端',
// hori: '未知',
// vert: '未知',
// axis: '未知'
// }, this.labelFont)
resolve(); // 加载完成后的Promise要调用resolve函数
})
})
},
// 添加Gltf模型
addGltfModel(modelFilename, metFilename, scale) {
let loader = new GLTFLoader();
loader.load(modelFilename, obj => {
// console.log(obj);
obj.scene.scale.set(scale, scale, scale)
obj.scene.position.set(-5, 0, -1);
scene.add(obj.scene);
});
},
addLabel(pos, text) {
// 这个scale参数暂时没有用,函数内部已经写死了
// console.log(this.models)
let canvas = this.genCanvas(text)
let texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true
let spriteMaterial = new THREE.SpriteMaterial(
{map: texture, sizeAttenuation: false});
let label = new THREE.Sprite(spriteMaterial);
let scaleY = this.labelScale;
let scaleX = scaleY * canvas.width / canvas.height;
label.position.set(pos.x, pos.y, pos.z);
// sprite默认会令canvas变形 需要通过scale调整比例
label.scale.set(scaleX, scaleY, 1);
scene.add(label);
this.labels.push(label);
// label.materialObj = spriteMaterial;
// label.textureObj = texture;
label.textCanvas = canvas;
texture.dispose();
spriteMaterial.dispose();
return label;
},
rmLabel(label) {
// console.log(label)
scene.remove(label)
console.log(label)
label.materialObj.dispose()
label.textureObj.dispose()
label.geometry.dispose()
label.textCanvas = null
label = null
// console.log(label)
// label.dispose()
// console.log(label)
},
updateLabel(label, newText) {
if (newText) {
// let pos = label.position
// let newLabel = this.addLabel(pos, newText, this.labelFont)
//将原来的列表中对应的标签替换掉
// let index = this.labels.indexOf(label)
// this.labels.splice(index, 1, newLabel)
// this.rmLabel(label)
console.log(label)
// let newCanvas = this.genCanvas(newText, this.labelFont)
let newCanvas =
// label.textureObj.dispose()
// label.materialObj.dispose()
// label.textCanvas = null
// window.gc()
// console.log(label.material.map)
label.textCanvas = null
// label.material.map.dispose()
// label.material.map.image.dispose()
// label.material.map = null
// window.gc()
label.material.map.image = newCanvas
label.material.map.dispose()
newCanvas.remove()
newCanvas = null
// label.textureObj = texture
// label.materialObj.map = texture
// label.textCanvas = newCanvas
// console.log(new Date())
//
// console.log(this.labels)
//
// console.log(this.labels)
}
},
genCanvas(text) {
// 绘制canvas作为sprite的贴图
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
ctxes.push(ctx) // 将当前的ctx保存
let title = text.title;
if (ctx !== null) {
let textWidth =
title.length * Number(this.labelFont.split(' ')[0].replace('px', ''))
let textHeight =
Number(this.labelFont.split(' ')[0].replace('px', ''))
// console.log(textWidth + ' ' + textHeight)
canvas.width = this.laCvsWidth;
canvas.height = this.laCvsHeight;
// console.log(canvas.width + ' ' + canvas.height)
// ctx.fillStyle = "#333";
// ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.font = this.labelFont;
ctx.fillStyle = "#478dc6";
ctx.fillText(title, 50, textHeight);
ctx.fillStyle = "#fff";
ctx.font = '90px Arial'
ctx.fillText('垂直:' + text.vert, 50, 2 * textHeight + 10);
ctx.fillText('水平:' + text.hori, 50, 3 * textHeight + 10);
if (text.axis !== '')
ctx.fillText('轴向:' + text.axis, 50, 4 * textHeight + 10);
}
return canvas
},
testLabel() {
let newText1 = {
title: '电机非驱动端',
hori: Math.random().toFixed(2) + '',
vert: Math.random().toFixed(2) + '',
axis: Math.random().toFixed(2) + '',
}
let newText2 = {
title: '电机驱动端',
hori: Math.random().toFixed(2) + '',
vert: Math.random().toFixed(2) + '',
axis: Math.random().toFixed(2) + '',
}
let newText3 = {
title: '水泵驱动端',
hori: Math.random().toFixed(2) + '',
vert: Math.random().toFixed(2) + '',
axis: Math.random().toFixed(2) + '',
}
let newText4 = {
title: '水泵非驱动端',
hori: Math.random().toFixed(2) + '',
vert: Math.random().toFixed(2) + '',
axis: Math.random().toFixed(2) + '',
}
this.updateLabel(this.labels[0], newText1)
this.updateLabel(this.labels[1], newText2)
this.updateLabel(this.labels[2], newText3)
this.updateLabel(this.labels[3], newText4)
},
getPosition() {
let meshSet = this.models[0].children
let posSet = meshSet.map(item => item.position)
// for (let item of meshSet) {
// posSet.push(item.position)
// }
console.log('--------获取的模型坐标集合-------')
console.log(posSet)
console.log('--------------------------------')
},
aniControl(flag) {
if (flag !== null) {
this.aniPlay = flag
} else {
this.aniPlay = !this.aniPlay
// let tip = '动画' + (this.aniPlay === true ? '开始播放' : '停止播放')
// console.log(tip)
}
},
outlineObj(selectedObjects) {
composer = new EffectComposer(renderer); // 特效组件
renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass); // 特效渲染
outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
composer.addPass(outlinePass); // 加入高光特效
outlinePass.pulsePeriod = 2; //数值越大,律动越慢
outlinePass.visibleEdgeColor.set('#c413e8'); // 高光颜色
outlinePass.hiddenEdgeColor.set('#585353');// 阴影颜色
outlinePass.usePatternTexture = false; // 使用纹理覆盖?
outlinePass.edgeStrength = 5; // 高光边缘强度
outlinePass.edgeGlow = 2; // 边缘微光强度
outlinePass.edgeThickness = 2; // 高光厚度
outlinePass.selectedObjects = selectedObjects; // 需要高光的obj
},
animate() {
requestAnimationFrame(this.animate);
// this.Mesh.rotation.x += 0.01;
// this.Mesh.rotation.y += 0.01;
let delta = clock.getDelta();
if (this.aniPlay) {
if (mixer !== '') mixer.update(delta);
}
if (composer !== '')
composer.render(delta)
// renderer.render(scene, camera);
},
},
mounted() {
let div = this.$refs.screen
this.init(this.modelFilename, this.metFilename, null).then(() => {
clock = new THREE.Clock();
camera.position.x = -250
camera.position.y = 50
camera.lookAt(-100, 0, 0)
// this.openGrid(true);
// this.openAxis(true);
// controls = new OrbitControls(camera, renderer.domElement) //加入轨道控制
for (let item of this.models) {
item.position.x = -90
item.position.y = -20
item.position.z = 20
}
console.log('当前的已选择列表')
console.log(this.selectedList)
this.outlineObj(this.selectedList)
this.animate();
setInterval(this.aniControl, 2000, null)
// setInterval(this.testLabel, 1000)
// resolve()
})
let erd = elementResizeDetectorMaker()
erd.listenTo(div, (element) => {
// console.log(element.clientHeight + ' ' + element.clientWidth)
camera.aspect = (element.clientWidth) / (element.clientHeight);
camera.updateProjectionMatrix();
renderer.setSize((element.clientWidth), (element.clientHeight));
});
// window.addEventListener('resize', () => {
// camera.aspect = (window.innerWidth - 30) / (window.innerHeight - 100);
// camera.updateProjectionMatrix();
// renderer.setSize((window.innerWidth - 30), (window.innerHeight - 100));
// });
},
}
</script>
<style lang="scss" scoped>
#container {
margin: 0;
overflow: hidden;
background: url("../../../src/assets/comImgs/bigBg.png") center no-repeat;
background-size: cover;
}
/*.tap {*/
/* position: absolute;*/
/* background-color: MidnightBlue;*/
/* background-color: rgba(0, 10, 40);*/
/* border-top-left-radius: 10px;*/
/* border-bottom-right-radius: 10px;*/
/* opacity: 0.5;*/
/* font-size: 4px;*/
/* !*color: aqua;*!*/
/* !*width: 36px;*!*/
/* !*height: 44px;*!*/
/* padding: 1px 1px 1px;*/
/*}*/
</style>