代码实例
估计之前的文章,写得比较抽象?所以还是有朋友看不懂,那就放上我的demo,边看边印证。
项目主目录
我这里是把rollup.config.js
和vue.config,js
改成.mjs
后缀了。
——按步骤来的朋友,可以先不改这个后缀,如果后续报错了,再改。
package.json
{
"name": "three-ifc",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "concurrently \"rollup -c ./rollup.config.mjs -w -m inline\" \"vue-cli-service serve\" ",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"ifcServe": "http-server . -p 8000",
"ifcWatch": "rollup -c ./rollup.config.mjs -w -m inline",
"ifcBuild": "rollup -c ./rollup.config.mjs"
},
"dependencies": {
"and": "^0.0.3",
"three": "^0.150.1",
"three-mesh-bvh": "^0.5.23",
"vue": "^2.6.10",
"web-ifc": "^0.0.39",
"web-ifc-three": "^0.0.122"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.1",
"@vue/cli-plugin-babel": "^3.11.0",
"@vue/cli-plugin-eslint": "^3.11.0",
"@vue/cli-service": "^3.11.0",
"babel-eslint": "^10.0.1",
"browserslist": "^4.21.4",
"caniuse-lite": "^1.0.30001418",
"copy-webpack-plugin": "^5.1.2",
"core-js": "^2.6.5",
"css-loader": "^2.1.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"exports-loader": "^4.0.0",
"imports-loader": "^4.0.1",
"rollup": "^3.18.0",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-vue": "^6.0.0",
"vue-template-compiler": "^2.6.10"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {
},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {
}
}
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}
rollup.config.mjs
// import vue from 'rollup-plugin-vue';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
// import resolve from '@rollup/plugin-node-resolve';
export default {
input: "./public/js/ifc/threeIfcScene.js", // 入口文件路径
output: [
{
format: "umd", // 输出格式
file: "./public/js/ifc/bundle.js", // 输出文件路径
name: "threeIfcScene"
//当入口文件有export时,'umd'格式必须指定 name
//这样,在通过<script>标签引入时,才能通过 name 访问到 export 的内容。
},
],
plugins: [
// vue(), // 处理vue组件
babel({
exclude: 'node_modules/**', // 不编译node_modules目录下的代码
runtimeHelpers: true, // 配置runtime,不设置会报错
}),
commonjs(), // 将CommonJS模块转换为ES6模块
resolve() // 解析模块路径
]
}
声明:
vue.config.mjs
,我就是改了个后缀,里面没有任何与 ifc.js 相关的配置,所以不用追问这里面的代码是啥。
public文件夹
如下图所示,是public文件夹的内容:
- 蓝色:是
.ifc
的模型数据存放的地方。这个地方是demo暂时存放的,后续要布到生产上的话,建议模型直接放到服务器,引用服务器地址。 - 绿色:是根据IFC.js官方文档上进行的工作线程尝试,如果不需要,可以找到对应的代码删去。
- 黄色:是调用IFC.js 、渲染模型、模型交互等集大成的公用js。很重要。
- 红色:是根据IFC.js官方文档上建议的,把wasm单独从node_modules里面提取出来存放。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>three-ifc</title>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0px;
padding: 0px;
}
</style>
</head>
<body>
<noscript>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script src="./js/ifc/bundle.js"></script>
</body>
</html>
注意:这里要记得引用
rollup.config.js
转换后的bundle.js
threeIfcScene.js
重中之重,不要眨眼!!!
import * as THREE from 'three';
import {
Raycaster,
Vector2,
AmbientLight,
AxesHelper,
DirectionalLight,
GridHelper,
PerspectiveCamera,
Scene,
WebGLRenderer,
MeshLambertMaterial,
} from 'three';
import {
OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import {
IFCLoader } from 'web-ifc-three/IFCLoader.js';
import {
IFCSPACE,
IFCWALLSTANDARDCASE,
IFCSLAB,
IFCFURNISHINGELEMENT,
IFCDOOR,
IFCWINDOW,
IFCPLATE,
IFCMEMBER,
} from 'web-ifc';
import {
acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh';
window.scene = null; // 场景
window.mesh = null; // 网格模型对象Mesh
window.renderer = null; // 渲染器
window.camera = null; // 相机
window.controls = null; // 控制器
window.threeCanvas = null; // 渲染场景的canvas元素
//Sets up the IFC loading
const ifcModels = [];
window.ifcLoader = new IFCLoader(); // 创建web-ifc-three渲染器
const raycaster = new Raycaster(); // 光线投射,用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)
raycaster.firstHitOnly = true;
const mouse = new Vector2(); // 鼠标的桌面二维坐标
let animationFrameId = null; // 渲染动画ID,为了方便关闭动画
const subsets = {
}; // 存储已创建的子集
// List of categories names 图层类别名称
const categories = {
IFCSPACE,
IFCWALLSTANDARDCASE,
IFCSLAB,
IFCFURNISHINGELEMENT,
IFCDOOR,
IFCWINDOW,
IFCPLATE,
IFCMEMBER,
};
// 创建子集材料
const preselectMat = new MeshLambertMaterial({
transparent: true,
opacity: 0.6,
color: 0xff88ff,
depthTest: false,
});
const ifc = ifcLoader.ifcManager;
// Reference to the previous selection
let preselectModel = {
id: -1 };
/**
* 场景
*/
function initScene() {
scene = new THREE.Scene(); // 创建Three.js场景
scene.background = new THREE.Color(0x8cc7de);
}
/**
* 光线
*/
function initLights() {
const directionalLight1 = new THREE.DirectionalLight(0xffeeff, 0.8);
directionalLight1.position.set(1, 1, 1);
// 告诉平行光需要开启阴影投射
// directionalLight1.castShadow = true;
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight2.position.set(-1, 0.5, -1);
// 告诉平行光需要开启阴影投射
// directionalLight2.castShadow = true;
scene.add(directionalLight2);
const ambientLight = new THREE.AmbientLight(0xffffee, 0.25);
scene.add(ambientLight);
}
/**
* 配置IFCLoader
*/
async function setUpIFCLoader() {
// const ifcLoader = new IFCLoader(); // 创建web-ifc-three渲染器
// await ifcLoader.ifcManager.setWasmPath( 'https://unpkg.com/[email protected]/', true );
await ifcLoader.ifcManager.setWasmPath('./');
// 渲染的时候是否显示空间图层
// 设置可选类别图层
await ifcLoader.ifcManager.parser.setupOptionalCategories({
[ IFCSPACE ]: false,
});
await ifcLoader.ifcManager.applyWebIfcConfig({
USE_FAST_BOOLS: true
});
// Sets up optimized picking
await ifcLoader.ifcManager.setupThreeMeshBVH(
computeBoundsTree,
disposeBoundsTree,
acceleratedRaycast
);
// const ifcURL = URL.createObjectURL(changed.target.files[0]);
// ifcLoader.load(ifcURL, (ifcModel) => scene.add(ifcModel));
try {
loadIfcModel();
} catch(err) {
console.log(err);
loadIfcModel();
}
}
/**
* 加载IFC模型
*/
function loadIfcModel() {
ifcLoader.load('../../data/ifc/rac_advanced_sample_project.ifc', async (ifcModel) => {
// IFC文件地址
ifcModels.push(ifcModel);
// mesh = ifcModel.mesh ? addMaterial(ifcModel.mesh, '../../data/ifc/textures/woods-plastics.finishcarpentry.wood.redbirch_1.png') : null;
mesh = ifcModel.mesh ? ifcModel.mesh : null;
scene.add(mesh || ifcModel); // 将IFC模型添加到Three.js场景中
// render();
if (mesh) {
console.log(ifcModel);
console.log(mesh);
setCameraView(mesh);
}
await setupAllCategories();
});
}
// Gets all the items of a category
async function getAll(category) {
return ifcLoader.ifcManager.getAllItemsOfType(0, category, false);
}
// Creates a new subset containing all elements of a category
async function newSubsetOfType(category) {
const ids = await getAll(category);
return ifcLoader.ifcManager.createSubset({
modelID: 0,
scene,
ids,
removePrevious: true,
customID: category.toString(),
});
}
async function setupAllCategories() {
const allCategories = Object.values(categories); // 获取类型对应的ID集合
for (let i = 0; i < allCategories.length; i++) {
const category = allCategories[i];
await setupCategory(category, Object.keys(categories)[i]);
// const newCategory = addMaterial(category, '../../data/ifc/textures/124_1.png');
// await setupCategory(newCategory);
}
}
// Creates a new subset and configures the checkbox
async function setupCategory(category, type) {
subsets[category] = await newSubsetOfType(category);
if (type == 'IFCSPACE') {
subsets[category].removeFromParent(); // 把IFCSPACE图层踢出去
}
// setupCheckBox(category);
}
// Gets the name of a category 从categories图层类别对象中获取对应的name
function getName(category) {
const names = Object.keys(categories);
return names.find(name => categories[name] === category);
}
/**
* 模型上材质
*/
function addMaterial(mesh, materialUrl) {
const geometry = mesh.geometry; // 获取立方体的几何体
var material;
if (!materialUrl) {
const texture = new THREE.TextureLoader().load(materialUrl); // 创建立方体的材质,并将材质数组传入构造函数
material = new THREE.MeshBasicMaterial({
map: texture });
} else {
material = new THREE.MeshBasicMaterial({
color: 0x00ffff, // 设置材质颜色为蓝色
opacity: 0.5, // 设置材质透明度为0.5
transparent: true, // 开启材质透明
blending: THREE.AdditiveBlending, // 设置材质混合模式为加法混合
depthWrite: false, // 关闭深度写入,避免发光部分被遮挡
map: null, // 关闭材质贴图,不需要使用纹理贴图
emissive: 0x00ffff, // 设置发光颜色为蓝色
emissiveIntensity: 0.2 // 设置发光强度为0.2
});
}
// 创建新的立方体的网格模型,并将几何体和材质传入构造函数
const newMesh = new THREE.Mesh(geometry, material);
return newMesh;
}
/**
* 元素高亮
*/
function highlight(event, material, model) {
const found = cast(event)[0];
if (found) {
// 获取模型ID
model.id = found.object.modelID;
// 获取快递ID
const index = found.faceIndex;
const geometry = found.object.geometry;
const id = ifc.getExpressId(geometry, index);
// 创建子集
ifcLoader.ifcManager.createSubset({
modelID: model.id,
ids: [id],
material: material,
scene: scene,
removePrevious: true,
});
} else {
// 移除之前的高亮部分
ifc.removeSubset(model.id, material);
}
}
/**
* 渲染
*/
function initRender(elemId) {
// antialias 抗锯齿 || alpha 控制默认的透明值
renderer = new THREE.WebGLRenderer({
antialias: true, alpha: true }); // 创建Three.js渲染器 antialias: false,
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
const threeElementId = elemId || 'three-canvas';
// renderer.domElement.id = threeElementId;
// document.body.appendChild(renderer.domElement);
document.getElementById(threeElementId).appendChild(renderer.domElement);
// 给three场景所在的canvas增加一个双击事件
window.threeCanvas = document.getElementById(threeElementId);
window.threeCanvas.ondblclick = pick;
}
/**
* 辅助工具
*/
function initHelper() {
// grids辅助网格
const grid = new GridHelper(50, 30);
scene.add(grid);
// Axes辅助轴线
// axes轴(右手坐标系):红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
const axes = new AxesHelper();
axes.material.depthTest = false;
axes.renderOrder = 1;
scene.add(axes);
// 相机辅助线
const cameraHelp = new THREE.CameraHelper(camera);
scene.add(cameraHelp);
}
/**
* 控制器
*/
function initControls() {
controls = new OrbitControls(camera, renderer.domElement);
// controls.addEventListener('change', render); // addEventListener 第二个参数 是一个方法,不能直接把renderer写进去 || 不能写成 render()
/**
* 设置控制器角度
* phi 是俯仰角(0 俯视 --> 大于0,趋于仰视)
* theta 是水平旋转角(小于0,逆时针转;大于0,顺时针转)
* distance 是距离
*
*/
controls.setAngle = function (phi, theta, distance) {
var r = distance || controls.object.position.distanceTo(controls.target);
var x = r * Math.cos(phi - Math.PI / 2) * Math.sin(theta) + controls.target.x;
var y = r * Math.sin(phi + Math.PI / 2) + controls.target.y;
var z = r * Math.cos(phi - Math.PI / 2) * Math.cos(theta) + controls.target.z;
controls.object.position.set(x, y, z);
controls.object.lookAt(controls.target);
};
controls.enableDamping = true; // 使动画循环使用时阻尼或自转 意思是否有惯性
// controls.autoRotate = true; // 自动旋转
controls.rotateSpeed = 0.5; // 旋转速度(ORBIT的旋转速度,鼠标左键),默认1
controls.panSpeed = 0.5; // 位移速度(ORBIT的位移速度,鼠标右键),默认1
// 请注意,可以通过将 polarAngle 或者 azimuthAngle 的min和max设置为相同的值来禁用单个轴, 这将使得垂直旋转或水平旋转固定为所设置的值。
// 你能够垂直旋转的角度的上、下限,范围是0到Math.PI,其默认值为Math.PI。
// Tips:如下图设置,是限制相机向下旋转,但是可以向上旋转
controls.maxPolarAngle = Math.PI / 2;
controls.minPolarAngle = 0;
controls.target.set(0, 0, 0);
// controls.minDistance = 1;// 设置相机距离原点的最近距离
controls.maxDistance = 200;// 设置相机距离原点的最远距离
}
/**
* 控制器动画
*/
function animate() {
//更新控制器
render();
controls.update();
animationFrameId = requestAnimationFrame(animate); // 使用requestAnimationFrame可以让浏览器根据自身的渲染节奏调整动画的帧率,从而避免过度渲染,优化three.js渲染性能
}
/**
* 停止requestAnimationFrame动画
*
*/
export function stopAnimationFrame() {
window.cancelAnimationFrame(animationFrameId);
}
/**
* 渲染场景
*/
export function render() {
renderer.render(scene, camera);
}
/**
* 视窗的尺寸重新变化
*/
export function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
render();
}
function initCamera() {
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
// camera.position.set(0, 40, 100);
camera.lookAt(new THREE.Vector3(0, 0, 0));
}
/**
* 获取模型的高度,并设置相机初始视角(也初始化控制器的视角)
*
*/
function setCameraView(mesh) {
// 获取模型边界框
const boundingBox = new THREE.Box3().setFromObject(mesh);
// 获取中心位置
const center = boundingBox.getCenter(new THREE.Vector3());
// 获取模型高度
const height = boundingBox.max.y - boundingBox.min.y;
// 计算相机位置
const cameraPosition = new THREE.Vector3(0, height / 2.8, height * 2); // x, y, z
// 计算相机目标点
const target = new THREE.Vector3(center.x, center.y, center.z);
// 计算模型绕 y 轴逆时针旋转 45 度的四元数。
const angle = Math.PI / 4; // 将相机向左偏转 45 度
const axis = new THREE.Vector3(0, 1, 0); // 绕 y 轴旋转
// const quaternion = new THREE.Quaternion().setFromAxisAngle(axis, angle);
// mesh.applyQuaternion(quaternion);
// mesh.rotateY(angle); // 模型本身旋转,这效果与上面注释的两行代码一样
// 将模型偏移原先坐标位置,可能会引发一系列的问题
// const offset = center.clone().negate(); // 计算模型在屏幕中心的偏移量,即将模型平移到屏幕中心的向量。
// mesh.position.add(offset); // 将模型平移到屏幕中心。
controls.setAngle(1.4, -0.8, height * 2); // 控制器视角:有点俯视,且逆时针旋转30°左右
controls.target.copy(center); // 将视角的目标点移到包围盒的中心点
controls.update(); // 更新视角的位置
// 设置相机位置和目标点
// camera.position.set(cameraPosition);// 设置相机位置 —— 这里好像没有发挥作用
// camera.position.copy(cameraPosition); // 将源摄像机的属性复制到新摄像机中。—— 这个去掉导致会看不到模型
camera.lookAt(target); // camera.lookAt 与 orbitcontrol冲突不能使用,要用controls.target代替 //controls.target = new THREE.Vector3(0,-100,0);
// camera.rotation.y = angle; //物体的均匀从左到又平移可以用相机旋转Y轴来实现 —— 这里好像没有发挥作用
// camera.fov = 100; // 摄像机视锥体垂直视野角度,从视图的底部到顶部,以角度来表示。默认值是50。这个值是用来确定相机前方的垂直视角,角度越大,我们能够查看的内容就越多。
console.log('Height:' + height);
}
/**
* 设置开启多进程
*
*/
async function setUpMultiThreading() {
await ifcLoader.ifcManager.useWebWorkers(true, "js/ifc/IFCWorker.js"); // Uncaught SyntaxError: Unexpected token '<' (at IFCWorker.js:1:1)
await ifcLoader.ifcManager.setWasmPath("./");
}
/**
* 设置模型加载进度
*
*/
function setupProgressNotification() {
const progressText = document.getElementById('progress-text');
ifcLoader.ifcManager.setOnProgress((event) => {
const result = Math.trunc(event.loaded / event.total * 100);
if (result == '100') {
setTimeout(() => {
progressText.innerText = result.toString();
}, 3000);
} else {
progressText.innerText = result.toString();
}
});
}
/**
* 计算鼠标在屏幕上的位置
*
*/
function cast(event) {
const bounds = window.threeCanvas.getBoundingClientRect();
const x1 = event.clientX - bounds.left;
const x2 = bounds.right - bounds.left;
mouse.x = (x1 / x2) * 2 - 1;
const y1 = event.clientY - bounds.top;
const y2 = bounds.bottom - bounds.top;
mouse.y = -(y1 / y2) * 2 + 1;
// 将其放置在指向鼠标的相机上
raycaster.setFromCamera(mouse, camera);
// 投射射线
return raycaster.intersectObjects(ifcModels);
}
/**
* 鼠标拾取(双击)
*
*/
async function pick(event) {
const found = cast(event)[ 0 ];
if (found) {
const index = found.faceIndex;
const geometry = found.object.geometry;
const ifc = ifcLoader.ifcManager;
const id = ifc.getExpressId(geometry, index);
console.log(id);
const modelID = found.object.modelID;
const props = await ifc.getItemProperties(modelID, id); // 直接属性信息(在 IFC 模式中,有两种类型的属性:直接和间接。间接属性(psets、qsets 和类型属性))
const type = await ifc.getIfcType(modelID, id); // 指定元素的IFC类型
const materials = await ifc.getMaterialsProperties(modelID, id); // 材质信息
// const spaces = await ifc.getSpatialStructure(modelID);
const typeProps = await ifc.getTypeProperties(modelID, id); // 类型属性
const propSets = await ifc.getPropertySets(modelID, id, true); // 同时获取了属性集和数量集
const output = document.getElementById('output');
output.innerHTML = JSON.stringify(props, null, 2);
console.log(props);
console.log(type);
console.log(materials);
// console.log(spaces);
console.log(typeProps);
console.log(propSets);
// logAllSlabs( modelID );
}
}
/**
* 释放所有的IFCLoader内存
*
*/
// 设置内存处理
// const button = document.getElementById("memory-button");
// button.addEventListener(`click`, () => releaseMemory());
export async function releaseMemory() {
// 这将释放所有的IFCLoader内存
await ifcLoader.ifcManager.dispose();
ifcLoader = null;
ifcLoader = new IFCLoader();
// 如果之前设置了wasm路径,
// 我们需要重置它
await ifcLoader.ifcManager.setWasmPath('./');
// 如果IFC模型是一个数组或对象,
// 你也必须在那里释放它们。
// 否则,它们将不会被垃圾回收。
ifcModels.length = 0;
}
function initModel() {
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshPhongMaterial({
color: 0xffffff });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
}
/**
* Three 初始化渲染场景
*
* @param {Object} comp vue组件的this对象
* @param {String} elemId 渲染模型的元素Id
* @param {Object} config 额外增加的一些配置
* @param {Object} offsetOption 模型渲染偏移配置
* @return {void} 无
*/
export async function initThree(elemId, comp, config, offsetOption) {
//Scene
initScene();
//Camera
initCamera();
//Lights
initLights();
//Setup IFC Loader
setUpIFCLoader();
//Renderer
initRender(elemId);
//Helper
initHelper();
//Controls
initControls();
setUpMultiThreading();
setupProgressNotification();
//Animation loop
animate();
window.addEventListener('resize', onWindowResize);
window.onclick = (event) => highlight(event, preselectMat, preselectModel);
}
这个js里面我已经放了很多东西了,有工作线程、材质、相机视角调整等等,可以根据注释去删掉自己不需要的代码。(我才不会说,我自己是懒得删了…)
styles.css
* {
padding: 0;
margin: 0;
}
html, body {
overflow: hidden;
}
#threeCanvas {
position: fixed;
top: 0;
left: 0;
outline: none;
}
src文件夹
只有一个.vue做demo页面,名字随心意定。
使用的vue页面
<template>
<div class="model">
<div id="threeContainer">
<div style="position: absolute;width: 300px;background: rgba(255,255,255, 0.9);color: #333;left: 1px;border-radius: 5px;height: 350px;overflow: auto;text-align: left;">
<p>Properties:</p>
<p id="output" style="padding: 0 5px;"></p>
</div>
<!-- 模型加载进度 -->
<div style="position: absolute;bottom: 0;left: 10px;">
<p>Progress:<span id="progress-text">0</span> %</p>
</div>
</div>
</div>
</template>
<script>
// 1 创建视图容器的vue组件
var EarthComp = {
data() {
return {
};
},
watch: {
},
created() {
},
// 1.1 资源创建
mounted() {
threeIfcScene.initThree('threeContainer');
},
// 1.2 资源销毁
beforeDestroy() {
},
methods: {
},
};
export default EarthComp;
</script>
<style scoped>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
position: relative;
}
</style>
运行
直接运行
start 命令
就行了。
它会去把rollup.config.mjs里面相关的js压缩转化成bundle.js,并启动项目。