http://www.cnblogs.com/pursues/p/5226807.html

第一次使用threejs到实际项目中,开始的时候心情有点小激动,毕竟是第一次嘛,然而做着做着就感受到这玩意水好深,满满的都是坑,填都填不过来。经过老板20天惨无人道的摧残,终于小有成就。

因为第一次搞这玩意,相对的遇到的问题也是大把的,让我来一一诉说一路上遇到的各种问题。

开发使用: C4D、Blender2.75、[threejs-r72](http://threejs.org/)

万事开头难,第一个问题就是怎么才能把3d软件中做好的模型显示在浏览器中。

一、模型在软件中的导入与导出。

这个项目中涉及到单个模型和动画模型,而不同模型的导入导出有差异,下面就告诉大家我是如何将坑填平的。

1、单个模型:

因为自己不会使用3D软件建模,只能求助公司大神设计师来一起搞。刚开始的想法是直接用3d软件建模然后直接导出obj格式来用,然后设计师用C4D做好了一个测试模型,发现模型数量少的话网页的大小还可以接受,但是由于项目的模型数量比较多,然后粗算了一下模型总的大小,发现超出了预想,所以得另寻它法。

接着在网上搜索发现Blender这玩意,由于设计师对C4D情有独钟不会Blender软件,所以决定用C4D做好模型然后导出obj格式接着再导入到Blender里面,再经由Blender导出需要的格式。

因为是第一次倒腾这个软件,所以并不会导出。然后就在网上搜素了怎么用Blender导出的json、js文件。经过测试js文件导出比较大,最后果断选择json。

在软件中如何导入导出如图所示:

图一 (左边为导入obj,右边导出json)

2、动画模型:

由于设计师出一个动画模型也没有这么快,就没法进行导出测试。于是看到threejs官网里有demo中使用的动画模型,我就拿过来进行测试,发现动画模型跟单个模型导出选择有差异然后发现更单个模型的导出有出入,经过反复的测试,得到导动画模型需要注意的几点,

(1)选择好动画的帧数,如果没选择,导出的json文件会有空帧。并且文件也会相对增大。

(2)选择好导出选项中Animation,一般就选择Morph Animation、Embed Animation选项。

单个模型以及动画模型导出选项如下图所示:


图二 (左边为导出单个模型,右边导出动画模型)

注:导出单个带材质模型需要在导出选项的时候需要在shading选项中选择Face Materials。

拿着设计师做好的动画模型导出json格式后碰到了一些问题,虽然json格式的大小相比obj格式的要小一点,不过项目中有人物的动画模型导出的json格式大小还是太大。然后为了解决这个问题,跟设计师进行讨论,然后得到以下解决方案:

(1)将模型的面和顶点在不影响正常显示的情况下进行删减

(2)对动画模型的帧数、面、顶点也进行删减

经过反复的修改和测试终于将动画模型控制在500-1000KB左右,单个模型控制在100K左右。

模型的导出问题解决了然后是对模型进行导入到页面中去。

二、模型加载到页面

在threejs官网上看到利用obj格式加载的demo比较多,所以就直接使用的是obj的格式模型进行加载,根据demo利用THREE.OBJLoader()、THREE.OBJMTLLoader()进行加载。然后设计大神给了我一个带材质的模型让我进行测试,发现两问题:

(1)模型材质丢失

(2)模型的大小太大(模型量少大小还可以接受,考虑到此次项目中的模型量多,估算了一下大概有70-80M左右)

由于模型大小太大所以放弃。所以改选用THREE.JSONLoader()进行加载。

在这一步由于是直接导出带材质的json格式,材质对应到模型的各个面是一个问题。然后在官网的demo上看到THREE.MeshFaceMaterial()方法,查看了一下文档,然后迅速解决这个了这个问题。

另外threejs提供了各种模型的加载方法具体可去threejs.org查询。

虽然解决了模型加载和面的问题,但是模型在网页的表现与软件中渲染的差别太大。刚开始以为是模型方面以及导入导出的方式不对,于是和设计师进行各种修改然后反复的测试,发现没什么变化。然后求助stackoverflow找到了答案。是由于模型的shading的原因造成的,然后根据网上提供的解决方案在材质中加上materials.shading
=
THREE.FlatShading来解决。不过有些Android手机上会出现材质无法解析的错误。而且在r72的版本中MeshLambertMaterial已经移除了shading这个属性。

下图就是在网页中渲染的结果对比。


图三 (左边为未加shading,右边加了shading)

静态模型的导入没有问题了,然后是动画模型的导入,参考官网demo,直接套用基本的动画没有太大问题,只是项目中有一个人物运球的动画模型比较难折腾,刚开始的时候直接是按照动画模型的导出直接导,然后同步到页面中发现只能导出一个动画,另外一个丢失了,定位了一下问题好像是Blender不能同时导出多个动画(具体是不是待研究)。最后想了一个办法就是采取分开导出再创建一个obj包裹两个动画,操作这个obj。来解决多个动画问题(如有更好的办法求大神指导)。

至此将模型放到页面中的准备工作都做完了。接着就是模型上的事件与动画,模型上的各种事件整的头都大了,然而到现在我还是有一些东西没有弄清楚原理还得继续研究。

三、模型的圆周运动

刚开始项目中有个需求就是进入页面中模型需要做一个圆周运动,圆周运动以前在数学中学过,但是一直没用所以就忘了,然后就在网上找有关圆周运动计算的方法。这里不做过多的解释,用下面一张图来完整解释怎么来计算圆周运动。

图四 (圆周运动计算)

在页面中测试的结果如下图所示:


图五 (圆周运动测试结果)

功能代码如下:

         var clock = new THREE.Clock(); //时间跟踪
//圆周运动
var time = clock.getElapsedTime() * 1;
loadMesh.position.x = Math.cos( time ) * 10;
loadMesh.position.y = Math.sin( time ) * 10;

老板看了一下,然后脑补一下整个页面的效果说还是不要这样子,很多模型都这样运动的话画面太乱了,最后决定的是简单点直接把所有模型摆放成一个球体形状,然后模型不单独运动,而是整体绕中心转,这个实现起来比较简单思路是直接设置外层模型y轴旋转就可以了。

四、所有模型在空间里的位置

整体的运动效果描绘出来了,接着就是开始实施了。接着遇到了一个算是比较坑的问题。那就是模型在空间位置的确认了,由于对3D场景的不熟悉,将所有模型摆出一个球体就有点困难了。只能求助设计大神了,然后他在C4D中将所有模型摆成一个球体之后,然后像操作模型一样导了一份obj给我。然后我利用Blender打开(如下图六所示),然后我看了下,每个模型在软件中都存在一个x,y,z值,我抱着侥幸的心里把所有模型的x,y,z记录下来然后填到页面中。最后发现球体的形状出来了,只是距离有点差异,接着想了个投机取巧的办法把所有的x,y,z进行等比的放大缩小,改完效果还不错。最后就是拉着设计师疯狂调整细节方面的问题。效果如下图七所示。


图六 (球体模型)

图七 (页面中渲染效果)

页面的基本样子出来了,剩下的就是页面的交互了,整个难点基本都在这里了。

项目需求:在页面中选中模型,然后选择模型现在在屏幕中间,然后用手指进行360度滑动,点击关闭按钮回到原型原有的位置。

思路:选中模型-->移动某个东西-->绑定旋转事件-->回位。

可以说项目大部分的时间都花在实现这个操作过程中。下面就简单说一下我是怎么去填这些坑的。

五、在页面中选中模型

之后的所有操作都要基于这个模型去做,所有第一步就需要选中这个模型。这个跟以前做的完全不同,然后在官网demo和stackoverflow游荡,因为涉及到屏幕坐标和世界坐标这个概念有种完全懵逼的感觉。还好官网上有demo的支持,参考了demo之后发现,首先获取屏幕坐标的x,y然后想办法转换成向量,接着标准化向量,通过raycaster.intersectObjects的检测来获取选中的模型。功能代码如下:

        mouse.x = ( e.clientX / window.innerWidth ) * 2 - 1; //鼠标的x到屏幕y轴的距离与屏幕宽的一半的比值 绝对值不超过1
mouse.y = - ( e.clientY / window.innerHeight ) * 2 + 1; //鼠标的y到屏幕x轴的距离与屏幕宽的一半的比值 绝对值不超过1 //新建一个三维变换半单位向量 假设z方向就是0.5,这样左右移的时候,还会有前后移的效果
//屏幕和场景转换工具根据照相机,把这个向量从屏幕转化为场景中的向量
var vector = new THREE.Vector3( mouse.x, mouse.y, 0.5 ).unproject( camera ); //变换过后的向量vector减去相机的位置向量后标准化
var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() ); //新建一条从相机的位置到vector向量的一道光线
var intersects = raycaster.intersectObjects( objects ); if ( intersects.length > 0 ) { //把选中的对象放到全局变量SELECTED中
SELECTED = intersects[ 0 ].object;
}

参考: http://threejs.org/examples/webgl_octree_raycasting.html

模型选中之后然后开始下一步。

六、相机的移动

因为要让选中的模型显示在屏幕中间,于是想了两种方案:

(1)改变选中物体的x,y,z值

经过反复的测试发现改变x,y,z值,模型会在空间中乱窜,把握不好位置。于是就思考其它的方案。

(2)移动相机

移动相机这一块被坑了无数次,因为刚开始对这个相机的原理不是很清楚,就随意试了几个值(可以利用threejs提供的相机辅助线来操作具体参考:http://threejs.org/docs/index.html#Reference/Extras.Helpers/CameraHelper),来证明自己的方向是正确的,发现模型确实出现在不同的位置了,然后就继续往下深挖。首先在google中寻找有关threejs中相机的原理,具体参考:http://www.flowers1225.com/lessons/2015/12/08/1,对原理有一定了解之后,然后就想怎么让相机出现在自己想要的位置上。

首先获取到选中模型的世界坐标,然后再根据当前的坐标值改变相机的x,y,z让相机直接照在当前模型上。测试了一下发现选中模型出现在屏幕中间,不过根据模型的不同位置上有偏差,这个后来设置了默认值来修正偏差。效果下图所示:


图八 (相机移动)

功能实现了,然后发现选择之后出现的太突然了,没有体现相机移动的效果,然后就想到使用TweenMax这个动画库来实现平滑过渡的动画。代码如下:

    TweenMax.to(camera.position, 1, {
x: x,
y: y,
z: z,
ease:Expo.easeInOut,
onComplete: function (){}
})

加上之后效果如下图所示:

图九 (相机平滑移动)

模型已经放大显示在屏幕中,然后点击关闭按钮让模型回到原位这个就直接用TweenMax将相机的position值设置为初始值就可以了。

接下来就是开发用手指操作模型旋转的功能了。

七、操作模型旋转

这个功能我卡了好久,里面涉及到数学中的矩阵、四元数、欧拉角、向量乘积、轴-角啥的,完全都忘了。在二维中旋转可以通过角度来控制,而在三维空间中需要通过四元数或者矩阵来实现,万般无奈只能求助万能的google来了解怎么在三维空间中对物体进行旋转操作。
通过了解在3D中表现旋转有三种方法,矩阵、欧拉角、四元组。
最后选用四元组来实现旋转的方法,简单点说原因就是四元组的是围绕一个轴来做旋转,而且在threejs中也提供了THREE.Quaternion()方法,然后在threejs的包中找到了一些写好的鼠标控制的类(TrackballControls.js、OrbitControls.js等),然后参考着源码,将方法虽然写出来了,但是有时候操作起来会有意向不到的bug,所以里面的细节还有待深挖。下面简述一下旋转的思路。

首先四元数控制旋转需要的是一个旋转轴和一个旋转弧度,直接上图清楚明了


图十

然后就想办法得到这个两个东西,接着开始想怎么弄到旋转弧度,首先获取到点击开始和结束的x,y值,然后得到两个向量之间的夹角,得到一个弧度,然后在设置一个默认的旋转系数,两者相乘得到弧度。接着通过开始和结束的向量乘积得到旋转轴,最后通过setFromAxisAngle(axis,
angle)得到旋转四元数。

核心代码如下:

      function rotateMatrix(rotateStart, rotateEnd){
var axis = new THREE.Vector3(),
quaternion = new THREE.Quaternion(); //得到开始和结束向量间的夹角
var angle = Math.acos(rotateStart.dot(rotateEnd) / rotateStart.length() / rotateEnd.length()); if (angle){ //如果夹角等于0, 说明物体没有旋转
axis.crossVectors(rotateStart, rotateEnd).normalize(); //rotateStart,rotateEnd向量乘积 标准化 得到旋转轴
angle *= _that.rotationSpeed; //rotationSpeed旋转系数 得到旋转弧度
quaternion.setFromAxisAngle(axis, angle); //从一个旋转轴和旋转弧度得到四元组, 如果要让物体相反方向旋转 设置angle为负
}
return quaternion; //返回一个旋转的四元数
} this.handleRotation = function(object){
_that.rotateEndPoint = _that.projectOnTrackball(_that.deltaX, _that.deltaY);
var rotateQuaternion = rotateMatrix(_that.rotateStartPoint, _that.rotateEndPoint);
var curQuaternion = object.quaternion;
curQuaternion.multiplyQuaternions(rotateQuaternion, curQuaternion); //设置四元组 a x b
curQuaternion.normalize();
object.setRotationFromQuaternion(curQuaternion); //方法通过规范化的旋转四元数直接应用旋转 参数必须normalize()
};

在这里有个小坑,就是所有模型的外观大小不同,当旋转的时候,可能会出现误操作,然后用了一个小技巧就是用一个透明的方体包裹模型,这样做就相当于旋转一个cube了,而且设置方体有网格时对排除bug有帮助。如下图所示:


图十一 (外层包裹框)

虽然旋转的效果做出来了,但是旋转里面涉及的东西还有一些理解的不是很清楚,还需要继续深入研究,等我研究透彻了再重新整理一下(还望大神指点一下)。

参考:四元数旋转cube旋转、TrackballControls.js源码

八、 灯光

最后就是灯光的控制,因为NBA的主色调是红蓝色,设计大神就想模型在页面中有红蓝光打在模型上的感觉,于是照着这个方向,他开始在软件中调试灯光,调整好之后我按照设计师在软件中调整好的位置摆放灯光,发现跟预想的有点差异,页面中的颜色显的太深了,于是在整个空间中加上了一个白色的全局光来提亮整体的亮度,然后对灯光进行反复的调整,就到了现在页面中呈现的样子了。灯光调整的过程如图所示:


图十二 (灯光调整过程)

注: threejs提供很多种灯光具体可以在threejs文档中查看http://threejs.org/docs/index.html#Reference/Lights,而灯光调整可以借助threejs提供的辅助线来调整,THREE.PointLightHelper()、THREE.SpotLightHelper()等。这样有助于迅速定位到问题。

最后就是手机兼容性的测试了(因为是微信的活动页,所以其它浏览器未测试)。在iPhone下整体体验较好,在Android下使用r71版本发现模型会出现菱角不分明的情况如图三所示,之后改用r72版本,用高版本的Android测试发现问题解决了,然后拿我自己的mx2测试时出现另外一个问题,直接卡在加载页面进不去页面,然后通过调试工具发现在控制台中有方法报错(小米2也是同等情况),倒腾了好久,然而并没有什么卵用,因为是threejs内部报错,无奈只能放大招,做一个Android版本来解决这个问题。

最后附上体验地址(页面中意想不到的bug肯定是还有的):


扫码体验

第一次在实战中使用threejs开发,还有很多的不足需要弥补,希望下一次能做的更好。自己也继续的在webgl的坑中挣扎,挣扎,挣扎(重要的要说三遍),还望有大神指点迷津!!

附上公司大神设计师的站酷:http://www.zcool.com.cn/u/738186

另外附上一些自己在填坑中找到的对3D学习有帮助的网站、博客、书籍:

各种计算  http://www.euclideanspace.com/maths/index.htm
许多有趣的东西  http://www.natural-science.or.jp/article/laboratory/cat467/
大叔很厉害的 http://learningthreejs.com/
threejs源码注释 https://github.com/omni360/three.js.sourcecode

关于threejs的书籍只有英文原版的:

《Three.Js Essentials》、《threejs-cookbook》、《Learning Three.js》

首个threejs项目-前端填坑指南【转】的更多相关文章

  1. 首个threejs项目-前端填坑指南

    第一次使用threejs到实际项目中,开始的时候心情有点小激动,毕竟是第一次嘛,然而做着做着就感受到这玩意水好深,满满的都是坑,填都填不过来.经过老板20天惨无人道的摧残,终于小有成就. 因为第一次搞 ...

  2. Android项目开发填坑记-so文件引发的攻坚战

    故事的最初 我负责的项目A要求有播放在线视频的功能,当时从别人的聊天记录的一瞥中发现百度有相关的SDK,当时找到的是Baidu-T5Player-SDK-Android-1.4s,项目中Demo的so ...

  3. Flutter 开发填坑指南

    引言 第一次在使用Flutter是在Ubuntu机器上,但是因为Android Studio还有Sdk配置问题,flutter doctor总是在这一步报错...最近又在win10上配了一下环境(真香 ...

  4. Android项目开发填坑记-Fragment的onBackPressed

    Github版 CSDN版 知识背景 Fragment在当前的Android开发中,有两种引用方式,一个是 Android 3.0 时加入的,一个是supportV4包中的.这里简称为Fragment ...

  5. Android项目开发填坑记-Fragment的onAttach

    背景 现在Android开发多使用一个Activity管理多个Fragment进行开发,不免需要两者相互传递数据,一般是给Fragment添加回调接口,让Activity继承并实现. 回调接口一般都写 ...

  6. Kubernetes Fluentd+Elasticsearch+Kibana统一日志管理平台搭建的填坑指南

    在初步完成Kubernetes集群架构的建立后,通过搭建一些监控组件,我们已经能够实现 图形化的监控每个node,pod的状态信息和资源情况 通过scale进行replicateSet的扩展和伸缩 通 ...

  7. Hive 填坑指南

    Hive 填坑指南 目录 Hive 填坑指南 数据表备份 数据表备份 方法1:create table 表名_new as select * from 原表 create table 表名_new a ...

  8. 微信支付中分账功能 填坑指南V1

    公司是做电商的,近期开发了一款小程序,准备线上线下同步销售玩具.这里就涉及到微信支付的功能,网上有很多教程,官方也有文档和Demo,因此微信支付还是比较容易实现的. 由于我们公司是和其他公司合作运营的 ...

  9. Hexo博客skapp主题部署填坑指南

    相信大家都很喜欢 hexo skapp 的主题,由于作者采用结巴分词,加上需要依赖各种各样的环境 所以可能大家踩过很多坑,也许每个人踩得坑不一样,这里使用 Docker 容器 centos 来部署, ...

随机推荐

  1. 多线程学习笔记七之信号量Semaphore

    目录 简介 数据结构 示例 实现分析 构造方法 信号量的获取(公平方式) 信号量的释放(公平方式) nonfairTryAcquireShared(int acquires) 总结 简介   Sema ...

  2. 使用Synchronized块同步方法

    synchronized关键字有两种用法.第一种就是在<使用Synchronized关键字同步类方法>一文中所介绍的直接用在方法的定义中.另外一种就是synchronized块.我们不仅可 ...

  3. Web服务器指纹识别工具httprint

    Web服务器指纹识别工具httprint   在Web渗透测试中,准确判断服务器的类型直接影响后期渗透测试的成功率.Kali Linux提供了专门的Web服务器指纹识别工具Httprint.该工具根据 ...

  4. 运行程序,解读this指向---case6

    function Parent() { this.a = 1; this.b = [1, 2, this.a]; this.c = { ckey: 5 }; this.show = function ...

  5. CentOS 7设置KVM硬盘模式为SCSI

    找到一下节点,把target节点的dev改成s开头,bus改成scsi即可,并删除address节点: 以此内推,如果要修改为ide需要修改dev为h开头,bus改成ide. 参考: https:// ...

  6. “一次编写,随处运行” Intel HTML5技术研讨会

    HTML5 是一种开放标准.通过使用此标准,开发人员只需编写一次程序,即可在任何地方运行. 这一跨平台方法能够显著增加开发人员的总体潜在受众,同时在不同屏幕尺寸上为用户带来更一致的体验.因此,HTML ...

  7. NXP LPC-Link LPC3154

    LPC-Link: LPC-Link调试探针由恩智浦.Code Red和Embedded Artists三方共同开发, 该探针可与目标板断开,利用板载10针JTAG/SWD连接器直接用于客户自己的设计 ...

  8. 学一点 MYSQL 双机异地热备份—-MYSQL主从,主主备份原理及实践

    简单介绍mysql双机,多机异地热备简单原理实战. 双机热备的概念简单说一下,就是要保持两个数据库的状态自动同步.对任何一个数据库的操作都自动应用到另外一个数据库,始终保持两个数据库数据一致. 这样做 ...

  9. 使用HttpClient对ASP.NET Web API服务实现增删改查

    本篇体验使用HttpClient对ASP.NET Web API服务实现增删改查. 创建ASP.NET Web API项目 新建项目,选择"ASP.NET MVC 4 Web应用程序&quo ...

  10. [erlang] mnesia

    原文地址: http://www.cnblogs.com/bluefrog/archive/2012/05/16/2504625.html 本来是项目合作的,可是你却一而再再而三的使用这招,我处理愤怒 ...