Threejs开发指南(第一篇 构建ThreeSim框架)

本人曾于2017年撰写了《WebGL开发与应用》一书,比较全面的介绍了threejs引擎的基本框架(如坐标系、矩阵、场景、相机等)和编程基础(如材质、灯光、网格、法线、UV坐标、动画、模型分析、用户交互、场景控制等),当时,浏览器对WebGL的支持还不是很普及(当时大量的用户还在使用微软的IE,而微软对WebGL一直不太友好),因此国内从事threejs(https://threejs.org/)开发的人员很少,相关的书籍或资料也比较少。

如今,形式已经发生了很大的变化,浏览器方面,几乎没有人再使用IE(edge基本取代了IE),而主流的浏览器均已完全支持WebGL;脚本方面,从ECMAScript 6.0开始已经支持类(class)的概念,从而可以对JavaScript进行面向对象编程;引擎方面,threejs目前一枝独秀,在国内外都得到了广泛的应用,特别是在游戏开发、虚拟现实、建筑可视化、数据可视化等领域,许多知名公司和开发者都开始使用threejs来创建高质量的Web应用,这正是作者再次撰写此书的原因,当然,此书的目标不再是介绍threejs的编程基础,而是提供一个更直观的threejs开发框架。

Threejs的原生代码是面向过程的,并不适合用于开发业务逻辑复杂的大型应用程序,作者结合自己多年的编程经验,开发了一套基于threejs的面向对象开发框架-ThreeSim,该框架对大量的threejs底层操作进行了封装(比如三维场景的初始化、空间目标选取、模型导入与分析、动画生成等),这些底层代码对于每一个threejs应用程序来说都是差不多的,但却往往消耗我们很多的调试精力,有了这个框架,我们就可以忽略各种底层的实现细节(如矩阵变换、向量计算等),将更多的精力集中在具体的业务逻辑上,代码会变得直观很多、简单很多。

ThreeSim框架的核心由三个类组成:(1)ThreeBase:基类,处理异步执行问题;(2)ThreeApp:应用程序类;(3)ThreeObject:3D对象类。通常情况下,一个ThreeSim应用程序会由一个继承自ThreeApp的应用程序类和多个继承自ThreeObject的3D对象类组成,通过在应用程序中添加3D对象,构建完整的Web3D场景,在用户交互方面,可以通过对应用程序或3D对象进行鼠标或键盘事件编程来完成。

本书的所有代码基于以下规范编写:(1)基于ECMAScript 6.0规范;(2)基于threejs R163版本;(3)类名使用大驼峰命名法(Upper Camel Case,即每个单词的首字母都采用大写字母,例如ThreeApp、ThreeObject);(4)变量、函数名使用小驼峰命名法(Lower Camel Case,即第一个单词以小写字母开始,后续单词的首字母大写,例如userName、addObject())。

目录

1.程序调试类(Logger)

2.异步处理类(ThreeBase)

1.构造函数(constructor)

2.订阅消息(subScribe)

3.取消订阅消息(unSubScribe)

4.发布消息(publish)

3.代码重构类

3.1应用程序类(ThreeApp)

1.导入映射(importMap)

2.构造函数(constructor)

3.初始化(init)

4.运行(run)

5.不渲染运行(runNorender)

6.更新场景(update)

7.增加对象(addObject)

8.删除对象(removeObject)

9.初始化鼠标事件(initMouse)

10.初始化键盘事件(initKeyboard)

11.初始化窗口事件(initDomHandlers)

12.处理鼠标移动事件(onDocumentMouseMove)

13.处理鼠标按下事件(onDocumentMouseDown)

14.处理鼠标松开事件(onDocumentMouseUp)

15.处理键盘按下事件(onKeyDown)

16.处理键盘松开事件(onKeyUp)

17.处理键盘按键事件(onKeyPress)

18.获取鼠标位置的对象(objectFromMouse)

19.反向搜索ThreeObject对象(findObjectFromIntersected)

20.处理窗体事件(onWindowResize)

21.设置焦点(focus)

3.2 3D对象类(ThreeObject)

1.构造函数(constructor)

2.设置3D对象(setObject3D)

3.增加子类(addChild)

4.删除子类(removeChild)

5.更新对象(update)

6.设置位置(setPosition)

7.设置旋转(setRotation)

8.设置比例(setScale)

9.设置是否隐藏(setVisible)

10.设置层(setLayers)

11.获取场景(getScene)

12.获取ThreeApp对象(getApp)

13.用户自定义方法(userAction)

3.3导出ThreeApp和ThreeObject类

4.第一个ThreeApp程序

4.1网页文件

4.2脚本文件


1.程序调试类(Logger)

如何将大量的数据高效的展示在页面上,这是一个很繁重且很重要的工作,通常我们可以使用innerHTML、innerText、textContent等属性向DOM元素中输出数据,典型的代码如下:

const myDiv = document.getElementById('myDiv');
let someText = "here is some text";
myDiv.innerText = someText;

这种原生的写法会有两个问题,一是程序繁琐,准备数据时需要时刻关心DOM元素的名称;二是只能显示基本的数据类型,比如数字、字符串等,如果要调试的变量是一个对象,JavaScript默认会显示为“[object Object]”,从而导致无法看到对象的具体内容。

在JavaScript编程实践当中,经常会遇到需要显示或者调试一个对象的具体内容的情况,如果仅仅是调试,那么可以通过console.log的方法向浏览器的控制台输出对象,控制台允许用户逐级展开对象,从而看到对象的详细内容,但如果需要在页面中显示该对象的具体内容,就需要通过递归的方式遍历对象,因为对象的层级深度是不确定的。

我们把这些数据展示的工作整合成一个类,代码如下。

class Logger {
	constructor(elem) {
		this.elem = elem;
		this.lines = [];
}
//调试基本类型的变量
	log() {
		this.lines.push([...arguments].join(' '));
}
//调试对象变量
	logObject(obj){
		for (let [key, value] of Object.entries(obj)) {
			this.log(key+":",value);
		}
	}
//递归调试对象的所有key和value
	logObjectAll(obj,c){		//c表示层级缩进的字符
		let chr = c || ' ';	//默认使用全角空格缩进
		for (let [key, value] of Object.entries(obj)) {
			if (typeof value === 'object' && value !== null){
				this.log(chr,key,":","[Object]");
				this.logObjectAll(value,chr + chr);	//进入下层循环
			}
			else
				this.log(chr,key,":",value);
		}
	}
//向页面输出内容
	render() {
		this.elem.innerHTML = this.lines.join('<br />');
		this.lines = [];
	}
}
export {Logger};

下面的例子展示了如何利用Logger类进行程序调试。

let logger = new Logger(document.querySelector('#info'));
let obj={
	a:{a1:'this is a1' , a2:{a21:'this is a21' , a22:'this is a22'}},
	b:{b1:'this is b1' , b2:'this is b2'}
};
logger.logObjectAll(obj);
logger.render();

效果如图所示。

由于大多数的3D模型都包含层级关系,当我们在做模型分析的时候,经常需要一种能够快捷直观的查看模型层级关系的方法,Logger类可以很好的胜任这项工作。下图展示的是一只鹦鹉的GLB模型及其结构数据(mesh.userData),我们会在后续的章节中详细讲解具体的实现方法。

 

需要注意一点,模型文件可能很大(几何、材质、动画、矩阵等数据),递归输出的层级数据也就很多,向页面上输出这些数据时有可能导致页面崩溃(out of memory),编程时应注意这个问题。

2.异步处理类(ThreeBase)

利用threejs进行3D编程时,经常会遇到需要进行异步处理的情况,比如等待材质文件载入后才开始载入3D模型文件、等待3D模型载入后才开始做模型分析和渲染、等待前一个关键帧动画结束后再播放另一个关键帧动画等等,为保证这些异步的代码能够被直观的定义和有效的执行,我们创建一个基于消息订阅/发布模式的异步执行基类ThreeBase,并约定所有的threejs程序和对象类应从该类继承而来,这就可以简化后续的异步编程工作,提高代码的可读性。

class ThreeBase {
	constructor() {
		this.messageCenter = {};
	}
	subScribe(message, callback) {
		let fc = this.messageCenter[message];
		if(fc) return;
		fc = callback;
		this.messageCenter[message] = fc;
	}
	unSubScribe(message) {
		let fc = this.messageCenter[message];
		if(fc)
			delete this.messageCenter[message];
	}
	publish(message) {
		let fc = this.messageCenter[message];
		if(fc) {
		let args = Array.prototype.slice.call(arguments);
			fc.apply(this, args);
		}
	}
}
export {ThreeBase}

ThreeBase类共定义了4个方法。

1.构造函数(constructor)

构造函数,该函数定义了一个消息中心,每条消息都是一个JSON对象。

2.订阅消息(subScribe)

订阅消息,该函数接受两个参数,消息本身、回调函数,作用在于向消息中心追加一个JSON消息,当该消息被发布时(一般会在subScribe之后,由publish函数发布),可以调用callback函数,比如:

let a1 = new ThreeBase();
a1.subScribe("Loading", handle);
a1.subScribe("Building", handle);
console.log(a1);

则消息中心(即messageCenter)为: Object { Loading: handle(), Building: handle() },如图所示。

3.取消订阅消息(unSubScribe)

取消订阅一个消息。

4.发布消息(publish)

发布一个消息。该函数的目的是为了立刻执行该消息所对应的回调函数。

请注意这里有一个JavaScript如何传递函数参数的问题,尽管此处的publish函数只定义了一个参数message,但实际执行时是没有这个限制的,可以传递多个参数,比如:

a1.publish("Loading" , 50);

这些参数被保存在一个特殊的对象中,即arguments,arguments参数在JavaScript中是一个特殊的对象,它包含了函数运行时传递的所有参数。arguments对象类似于数组,但它不是一个数组,可以通过arguments[n]来访问对应的单个参数的值,并且拥有一个length属性,表示参数的个数。与普通数组不同的是,arguments是一个对象,而不是数组,因此它不具有数组的许多方法,如push、pop等。

对于上面的例子,arguments对象为["Loading" , 50],即arguments [0]="Loading",arguments[1]=50。

该函数的关键语句fc.apply(this, args)则把这些实际的参数传递给了回调函数(比如上述例子中的handle),JavaScript约定apply函数的第二个参数必须是一个数组或类数组对象,因此下面的两行代码也可以直接写成这样:fc.apply(this, arguments);

let args = Array.prototype.slice.call(arguments);
fc.apply(this, args);

下面的例子完整地说明了如何使用该类。

let a1 = new ThreeBase();
a1.subScribe("Loading", handle);
a1.subScribe("Building", handle);
//回调函数
function handle(){
	let t = document.querySelector('#container');
	let v = [...arguments];	
	t.innerText = t.innerText + "\n" + v[0] + ":" + v[1];
}
setTimeout(()=>{a1.publish("Loading" , 0);},1000);
setTimeout(()=>{a1.publish("Loading" , 100);},2000);
setTimeout(()=>{a1.publish("Building" , 'step-1');},3000);
setTimeout(()=>{a1.publish("Building" , 'step-2');},4000);

这段代码每隔1秒钟输出一行文本,效果如图所示。

这段代码的回调函数中运用到了JavaScript中的三点运算符。

let v = [...arguments];

三点运算符是JavaScript的一个扩展运算法,常用于解构数组或形参,比如当执行下面的代码时,数组v的内容为["Loading" , 0],即v[0]="Loading",v[1]=0 。

setTimeout(()=>{a1.publish("Loading" , 0);},1000);

三点运算符的一个典型应用是数组的合并,比如:

const boys = ['Bob', 'Charlie'];
const girls = ['Alice', 'Diana'];
const all = [...boys, ...girls];
console.log(all); // ["Bob", "Charlie", "Alice", "Diana"]

在大型3D应用程序中,由于模型本身可能很大,下载模型需要的时间也会很长,为改善应用程序的用户体验,通常会在主界面中设计一个进度条,实时显示当前的下载进度,下面的代码是实现该功能的关键部分。

//载入模型时,订阅“loadding”消息
obj.subScribe("loadding", (...v)=>{
	logger.log("Loadding:",v[1].toFixed(2) + '%');
	logger.render();
});

//载入过程中,发布“loadding”消息
let onProgress = function (xhr) {
if (xhr.lengthComputable) {
		let percentComplete = xhr.loaded / xhr.total * 100;
		that.publish('loadding',percentComplete);
	}
};

请注意在这个例子中,“loadd

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值