HT图形组件设计之道(四)

By | 2014年10月8日

在《HT图形组件设计之道(二)》我们展示了HT在2D图形矢量的数据绑定功能,这种机制不仅可用于2D图形,HT的通用组件甚至3D引擎都具备这种数据绑定机制,此篇我们将构建一个3D飞机模型,展示如果将数据绑定机制运用于3D模型,同时会运用到HT的动画机制,以及OBJ 3D模型加载等技术细节,正巧赶上刚发布的iOS8我们终于能将基于HT for Web开发的HTML5 3D应用跑在iOS系统了。

Screen Shot 2014-10-08 at 7.45.25 PM

首选我们需要一个飞机模型,采用HT for Web构建3D模型可采用API组合各种基础模型的方式,但今天我们将采用读入OBJ的方式,毕竟网上已有很多不错的现成模型素材,搜查了一番后我在www.turbosquid.com选择了的这款免费的飞机模型,这个飞机模型是3dsmax格式,飞机模型是一体化的,由于我还需要控制机头的螺旋桨,因此我用3dsmax做了点改造,将螺旋桨分离了机身独立作为一个材质,同时导出成HT for Web可读取的OBJ格式,接下来就没美工设计师什么事了,剩下就全靠我们程序员自己的代码手艺活了。

Screen Shot 2014-10-08 at 8.26.18 PM

读取OBJ文件一般采用AJAX的方式远程加载,这对于喜欢纯前端的程序员来说很不爽,开发或演示个例子还得启服务,我喜欢本地文件打开就能跑不受跨域安全限制,因此我们需要将OBJ的文本信息放在在HTML或者JS代码中。解决这类问题有很多种方式,例如对于WebGL开发来说vertex shader和fragment shader代码同样面临这个问题,一种方式是写成一堆的string的array然后进行join的方式,另一种方式是增加<script id=”shader-vs” type=”x-shader/x-vertex”></script>和<script id=”shader-fs” type=”x-shader/x-fragment”></script>的自定义类似script块,然后读取相应DOM元素的textContent来获取文本内容。

但这两种方式都不适合OBJ内容,因为OBJ内容太长,采用数组方式对于成千上万行的OBJ文件行行加引号是不可思议的工作量(当然你可以再写个工具干这事),而采用<script>的方式会使得页面的HTML代码太长不易阅读编辑,我喜欢采用下面代码所示的这种方式,obj和mtl文件就像普通的js文件,可分离HTML页面代码,可给多个例子复用,且没有跨域安全问题,当然代码有点tricky,将function转换成字符串再截取中间文本内容:

var flight_mtl = getRawText(function(){/*
	newmtl body
	Ns 10.0000
	Ni 1.5000
	d 1.0000
	Tr 0.0000
	Tf 1.0000 1.0000 1.0000 
	illum 2
	Ka 0.3608 0.4353 0.2549
	Kd 0.3608 0.4353 0.2549
	Ks 0.0000 0.0000 0.0000
	Ke 0.0000 0.0000 0.0000
	...
*/});

var flight_obj = getRawText(function(){/*
	v  -21.7990 -2.5094 -157.4279
	v  -34.5972 -20.3459 -42.9317
	v  -36.7638 -6.2029 -43.0833
	...
*/});			

function getRawText(obj){
    var text = String(obj); 
    return text.substring(14, text.length-3);
}

以下为注册飞机模型的代码,通过代码的注解可知我们对飞机模型做了调整,通过r3: [0, -Math.PI/2, 0]我将整体飞机模型沿着y轴旋转了-Math.PI弧度使之朝向右边,通过s3:[0.1, 0.1, 0.1]将飞机模型缩小了10倍。

ht.Default.loadObj(flight_obj, flight_mtl, {                    
	center: true,
	r3: [0, -Math.PI/2, 0], // make plane face right
	s3: [0.1, 0.1, 0.1], // make plane smaller
	finishFunc: function(modelMap, array, rawS3){
		if(modelMap){                            
			modelMap.propeller.r3 = {
				func: function(data){
					return [data.a('angle'), 0, 0]; 
				}                                
			};                             
			// make propeller a litter bigger
			modelMap.propeller.s3 = [1, 1.2, 1.2]; 
			modelMap.propeller.color = 'yellow';

			// add a sphere model as an indicator light
			array.push({
				shape3d: ht.Default.createSmoothSphereModel(),
				t3: [-40, 10, 0],
				s3: [6, 6, 6],
				color: {
					func: function(data){
						return data.a('light') ? 'red': 'black';
					}
				}
			});
			ht.Default.setShape3dModel('plane', array);

			createPlane(rawS3);
			createFormPane();  
		} 
	}
});

Screen Shot 2014-10-08 at 9.46.53 PM

飞机的螺旋桨模型绑定了data.a(‘angle’)属性,原始螺旋桨模型有点小,通过modelMap.propeller.s3 = [1, 1.2, 1.2];在yz面做了1.2倍的放大,通过modelMap.propeller.color = ‘yellow’;将原始模型的颜色改成更显眼的黄色,当然你也可以通过修改mtl文件实现,甚至再将该属性绑定数据模型进行动态变化。

飞机尾部原始模型并没有指示灯,我们通过ht.Default.createSmoothSphereModel()用API创建了一个模型,与OBJ的模型进行了组合,指示灯的颜色通过return data.a(‘light’) ? ‘red’: ‘black’;的函数逻辑进行数据绑定,后续我们将在飞机运行过程动态变化data.a(‘light’)参数,实现飞机飞行过程指示灯的闪烁效果。

Screen Shot 2014-10-08 at 10.01.56 PM

飞行路线是通过ht.Polyline类型构建的,上图的几个黄色球是飞行路线Polyline对象的部分控制点,通过这几个控制点我们甚至可以在飞机飞行过程动态改变飞行路线。

params = {
      delay: 1500,
      duration: 20000,
      easing: function(t){
           return (t *= 2) < 1 ? 0.5 * t * t : 0.5 * (1 - (--t) * (t - 2));                     
      },
      action: function(v, t){
           var point = getPoint(v),
                px = point.x,
                py = point.y,
                pz = point.z,
                tangent = getTangent(v),
                tx = tangent.x,
                ty = tangent.y,
                tz = tangent.z;
           plane.p3(px, py, pz);
           plane.lookAt([px + tx, py + ty, pz + tz], 'right');  

           var camera = formPane.v('Camera');
           if(camera === 'Look At'){
                g3d.setCenter(px, py, pz);
           }
           else if(camera === 'First Person'){                           
                g3d.setEye(px - tx * 400, py - ty * 400 + 30, pz - tz * 400);
                g3d.setCenter(px, py, pz);                           
           }

           plane.a('angle', v*Math.PI*120);                       
           if(this.duration * t % 1000 > 500){
                plane.a('light', false);
           }else{
                plane.a('light', true);
           }                       
      },
      finishFunc: function(){
           animation = ht.Default.startAnim(params);
           plane.a('light', false);
      }                 
 };                              

 animation = ht.Default.startAnim(params);

以上为飞行动画的相关代码,ht.Default.startAnim可启动Frame-Based和Time-Based两种方式的动画,本例中我们需要动态改变飞行的周期,同时Frame-Based的方式会导致不同硬件设备总体运行周期差异太大,因此我们采用设置Duration的Time-Based的动画方式。

动画过程主要要改变飞机的位置,以及保持机头朝向切线方向,同时在Look At的模式下,我们不断让HT的Graph3dView的eye属性盯着飞机的位置,First Person模式下我们还需要改变Graph3dView的center属性。通过if(this.duration * t % 1000 > 500)的代码逻辑,实现了半秒钟改变一次light属性的闪烁效果。

为了达到更逼真的现实效果我们定义了Easing函数,采用了easeBoth这种起始结束较慢中间过程较快的动画函数,可参考《透过WebGL 3D看动画Easing函数本质》文章,从而实现飞机逐渐加速启动启动,慢慢减速着落的效果,螺旋桨的旋转角度也在动画过程中根据Easing相关参数值设置,因此螺旋桨的旋转速度也一致的放映了这种动画效果。

Screen Shot 2014-10-08 at 10.36.35 PM

该例子综合运用了HT for Web的多种技术功能,大家能体会到HT这种数据绑定机制灵活且强大的特点,通过数据绑定机制,我们可以动态修改从2D拓扑图、到通用组件渲染,甚至到3D引擎的数据模型,所有图形元素的颜色、大小和角度等参数皆可灵活控制,并且以最直观易用的方式供程序员二次开发与实际业务数据绑定关联。

最后上段该HTML5例子在iOS、Android和Mac等多平台下的运行视频和抓图,有兴趣的同学还可对该例子做更多有意思的改造扩展。

Screen Shot 2014-10-08 at 7.44.54 PM