Threejs开发指南(第三篇 模型导入与分析)

3D模型处理是Web3D程序的一个重要话题,threejs支持很多种格式的3D模型,大概可以被分成这几类:

  1. 3D编辑器格式:用于特定应用程序(主要是3D编辑器):blend(Blender)、max(3d Studio Max)、mb/ma(Maya)等等。
  2. 交换格式:比如OBJ、DAE(Collada)、FBX等等,用于3D编辑器之间交换信息,通常文件会比较大(内含3D编辑器所需要的信息)。
  3. 应用程序格式:比如IFC格式,在BIM项目中被广泛运用。
  4. 传输格式:GLTF格式,被设计出来专门用于传输的,体积小、易于渲染、二进制存储,是threejs推荐使用的模型格式。

Threejs为很多种模型提供了解析类(位于jsm/loaders目录下,约40多种),无论哪种格式的模型,经过threejs解析后,都会转换成各种Object3D对象的嵌套组合(动画数据除外),之后的模型分析,其原理都是相同的,他们之间的区别仅在于导入模型时的API函数略有不同。

模型的外观尺寸可能很大(比如建筑模型),也可能很小(比如手枪),这个尺寸程序员未必事先知道,编程时需要反复调整场景大小、摄像机位置,以保证模型以合理的方式显示出来,如果程序需要动态地加载多个模型,可能会导致模型显示异常,比如只显示了模型的一部分,或者是模型看起来很小、与场景不协调。

还有一个问题,很多模型不是围绕原点Vector3(0,0,0)进行建模的,他可能在离原点坐标很远很远的地方,载入场景后什么都不显示,其实模型可能只是在摄像机的视角范围之外,不能渲染到屏幕上而已。

这些反复调试的工作相当繁琐,可以开发一个通用类对这些代码进行封装和重用,本章我们介绍这个封装方法,同时也介绍一些后期的模型处理技术,比如模型动画开发。

目录

3.1 模型分析的一般方法

3.1.1 初始化全局变量

3.1.2 定义应用程序类(MyApp)

3.1.3 定义子类(MyObject)

3.1.4 运行程序

3.2 深度分析与模型动画

3.2.1 子类的定位与材质修改

3.2.2 通过GUI修改颜色

3.2.3 3D文本类(TextObject)

3.2.4 轮胎旋转动画

3.2.5 车门开关动画

3.2.5.1 定位车门锚点

3.2.5.2 定义车门动画

3.3 构建通用的模型分析类

3.3.1 定义GLB模型分析类(ModelAnalysis_GLB)

3.3.2 利用ModelAnalysis_GLB开发网页

3.3.2.1 网页文件(Model_Show_GLB.html)

3.3.2.2 脚本文件(Model_Show_GLB.js)

1、导入相关类

2、定义全局对象

3、定义MyApp类

4、重定义userAction方法

5、运行程序

3.4 IFC模型解析


3.1 模型分析的一般方法

无论什么格式的模型,由于其导入场景之后的处理方法都是相同的,我们就以OBJ模型(也称为Wavefront OBJ,由Wavefront Technologies在1980年代创建)为例来说明模型分析的一般方法,他是最古老、最简单的模型之一,使用文本存储。

这是一台挖掘机的OBJ模型,如下图所示,其中红、绿、蓝三条线分别代表了X轴、Y轴和Z轴,模型的中心位于坐标原点处。

3.1.1 初始化全局变量

import * as THREE from 'three';
import { Logger } from './js_threesim/logger.js';
import { ThreeApp, ThreeObject } from './js_threesim/threesim.js';
import { OrbitControls } from './js_three/jsm/controls/OrbitControls.js';
import { MTLLoader } from './js_three/jsm/loaders/MTLLoader.js';
import { OBJLoader } from './js_three/jsm/loaders/OBJLoader.js';

let model = {
	model_name: '挖掘机(OBJ格式)',
	file_path: './models/obj/',
	mtl_file: 'wjj.mtl',
	model_file: 'wjj.obj'
};

3.1.2 定义应用程序类(MyApp)

class MyApp extends ThreeApp {
	static sceneSize = 100; //场景大小
	constructor() {
		super();
	}
	init(param) {
		super.init(param);

		this.model_name = param.model.model_name; //模型名称
		this.file_path = param.model.file_path; //传入模型文件路径
		this.mtl_file = param.model.mtl_file; //传入MTL文件名
		this.model_file = param.model.model_file; //传入OBJ文件名

		//根据模型位置调整原点坐标
		this.axis = new THREE.Vector3(0, 0, 0);
		//根据模型大小调整显示比例
		this.scale = 0;

		const directionalLight = new THREE.DirectionalLight(0xffffff, 5);
		directionalLight.position.set(1, 1, 1);
		this.scene.add(directionalLight);	//平行光
		const ambientLight = new THREE.AmbientLight(0x505050, 1);
		this.scene.add(ambientLight);	//环境光

		let that = this;
		this.childrens = 0; //统计模型子类数量

		//添加坐标轴
		const axesHelper = new THREE.AxesHelper(MyApp.sceneSize);
		this.scene.add(axesHelper);

		//载入obj文件,显示载入进度
		let onProgress = function(xhr) {
			if(xhr.lengthComputable) {
				var percentComplete = xhr.loaded / xhr.total * 100;
				document.getElementById('waitting').innerText = percentComplete.toFixed(2) + '%';
			}
		};
		//错误处理
		let onError = function(xhr) {
			logger.log('error:', xhr);
			logger.render();
		};

		new MTLLoader()
			.setPath(that.file_path)
			.load(that.mtl_file, function(materials) {
				materials.preload();
				new OBJLoader()
					.setMaterials(materials)
					.setPath(that.file_path)
					.load(that.model_file, function(model) {
						//(1)计算模型的外围立方体包围盒
						let box = new THREE.BoxHelper(model);
						box.geometry.computeBoundingBox();
						let modelBox_max = box.geometry.boundingBox.max;
						let modelBox_min = box.geometry.boundingBox.min;

						//(2)将模型的中心坐标拉回原点
						that.axis.set(-(modelBox_min.x + modelBox_max.x) / 2, -(modelBox_min.y + modelBox_max.y) / 2, -(modelBox_min.z + modelBox_max.z) / 2);

						//(3)取包围盒的最长边并计算显示比例
						let lenX = (modelBox_max.x - modelBox_min.x);
						let lenY = (modelBox_max.y - modelBox_min.y);
						let lenZ = (modelBox_max.z - modelBox_min.z);
						let s = lenX > lenZ ? lenX : lenZ;
						s = s > lenY ? s : lenY;
						//根据最长边调整模型的显示比例
						that.scale = MyApp.sceneSize / s;
						that.axis.multiplyScalar(that.scale);

						//(4)遍历模型子类,实例化为ThreeObject对象后加入场景
						model.traverse(function(obj) {
							if(obj instanceof THREE.Mesh) {
								let object = new MyObject();
								object.init(obj, that.axis, that.scale);
								that.addObject(object);
								that.childrens++;
							}
						});

						logger.log('Model-name:', that.model_name);
						logger.log('Model-lenX:', lenX.toFixed(2));
						logger.log('Model-lenY:', lenY.toFixed(2));
						logger.log('Model-lenZ:', lenZ.toFixed(2));
						logger.log('Scale-ratio:', that.scale.toFixed(2));
						logger.log('Sub-models:', that.childrens);
						logger.render();

					}, onProgress, onError);
			});

		this.camera.position.set(1, 1, MyApp.sceneSize);
		this.camera.lookAt(new THREE.Vector3(0, 0, 0));

		this.controls = new OrbitControls(this.camera, this.renderer.domElement);
		this.focus();
	}

	update() {
		super.update(this);
	}

	onWindowResize() {
		super.onWindowResize(this);
	}
}

这段代码的核心功能包括以下四部分:

(1)计算模型的外围立方体包围盒。根据载入的模型(model)生成一个外围立方体包围盒(box),计算包围盒大小(computeBoundingBox),该包围盒代表着模型的原始尺寸和坐标位置。

(2)将模型的中心坐标拉回原点。为保证模型能出现在摄像机视角中,我们将模型拉到原点坐标处,注意boundingBox.max和boundingBox.min分别存储着包围盒的极大值和极小值,他们都是Vector3类型的,比如一个边长为1的正方体,假设其中心点位于原点处,则boundingBox.max.x为0.5,boundingBox.min.x为-0.5。

(3)取模型包围盒的最长边并计算显示比例。无论模型的原始尺寸有多大,我们总按一定的比例让其合理的显示在场景中,此处我们定义场景大小为100(static sceneSize = 100;),根据包围盒的最长边计算显示比例,让其适应我们的场景。

(4)遍历模型子类,实例化为ThreeObject对象后加入场景。使用traverse方法(Object3D类的原生方法)遍历模型,找到所有的子类,并将这些子类实例化成ThreeObject对象加入场景,这么做的目的是为了方便我们对子类进行事件编程,比如单击某个子类后执行某种操作。

由于OBJ模型将材质单独保存为一个材质文件(mtl),因此模型载入的脚本看起来复杂很多,但其实大多数的模型都是单文件的,模型载入的脚本非常简单,比如:

//载入GLTF模型
let gltfLoader = new GLTFLoader();
gltfLoader.load(model_file, function (gltf) {
	…
});

//载入IFC模型
let ifcLoader = new IFCLoader();
ifcLoader.ifcManager.setWasmPath( './js_three/jsm/loaders/ifc/');
ifcLoader.load(model_file, function (ifc) {
	…
});

3.1.3 定义子类(MyObject)

class MyObject extends ThreeObject {
	constructor() {
		super();
	}

	init(mesh, axis, scale) {
		this.mesh = mesh.clone();
		this.name = mesh.name;
		this.setObject3D(this.mesh);
		this.mesh.position.copy(axis);
		this.mesh.scale.set(scale, scale, scale);

		//原始材质
		this.material_old = this.mesh.material;

		//鼠标滑过时的材质
		this.material_over = new THREE.MeshLambertMaterial({
			color: 0xffff00,
			transparent: true,
			opacity: 0.9
		});

		//鼠标选中时的材质
		this.material_selected = new THREE.MeshLambertMaterial({
			color: 0xff0000,
			transparent: true,
			opacity: 0.9
		});

		//当前的选中状态
		this.selected = false;
	}

	handleMouseUp(x, y, point, normal, event) {
		//0:左键;1:中键;2:右键
		let btn = event.button;
		//鼠标左键用于控制场景,比如转动镜头
		if(btn == 2) {
			if(this.selected)
				this.unselect();
			else
				this.select();
			logger.log("子类名称:", this.name);
			logger.render();
		}
	}

	handleMouseOver(x, y, point, normal) {
		this.overCursor = "pointer";
		if(this.selected) return;
		this.mesh.material = this.material_over;
	}

	handleMouseOut(x, y, point, normal) {
		if(this.selected) return;
		this.mesh.material = this.material_old;
	}

	select() {
		this.selected = true;
		this.mesh.material = this.material_selected;
	}

	unselect() {
		this.selected = false;
		this.mesh.material = this.material_old;
	}
}

这段代码接受模型的子类、坐标、显示比例三个参数并生成ThreeObject对象,定义了两种新的材质分别用于鼠标滑过和被选中时的状态(如下图所示),由于鼠标左键往往用于控制场景(比如控制场景的转动),此处使用右键来进行选中操作。

 

请注意几何体(geometry)和材质(material)是可以被多个网格(mesh)对象共用的,当我们修改其中的一个网格时,可能会意外的修改了另一个网格,导致一些意想不到的后果,因此我们在ThreeObject对象中对原始网格做了一份复制(this.mesh = mesh.clone())。

3.1.4 运行程序

let logger = new Logger(document.querySelector('#debug pre'));
let container = document.getElementById("container");
let ap
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值