本篇主要通过分析Tony Parisi的sim.js库(原版代码托管于:https://github.com/tparisi/WebGLBook/tree/master/sim),总结基础Web3D框架的编写方法。在上一篇的基础上,要求读者具有简短英文阅读或者查字典的能力。

限于水平和时间,本文难免出现错误与遗漏,您在阅读过程中如果遇到错误或者疑问请在评论区中指出,我将尽快回复。

为提高JavaScript编程效率,建议使用WebStorm工具进行网页程序编写,WebStorm官网:http://www.jetbrains.com/webstorm/。

上一篇中,我们把程序的所有文件放在同一个目录下,这种文件组织方式适用于简单的功能测试,但当文件数量更多时则会变得混乱不堪,我们在编写一般规模的Web3D程序时可参考下图进行文件组织:

该组织方式把JavaScript文件分为LIB和PAGE两部分,LIB保存一般不做修改的库文件,PAGE保存为特定页面编写的js文件,如果页面js较多可在PAGE中再分离出子文件夹。

MODEL下的每个文件夹都是一个JSON类型的模型,可以看到其中有保存纹理信息的jpg文件和保存顶点数组、法线向量、纹理坐标的文本文件。

上一篇的代码中,我们把所有需要多次调用的对象设为了全局变量和全局函数,当代码量增多时这种“全局管理”方式将面临巨大的挑战,随然我们可以用规范的变量命名或者变量数组来尽可能避免变量名重复,但全局管理方式仍缺少对变量间关系的描述方法,这时使用“面向对象”的变量管理方法似乎是唯一的选择。

下面进入正题:

  1. //代码截取自https://github.com/tparisi/WebGLBook/tree/master/sim,在那里Tony Parisi的Sim库依照旧版Three.js库编写,为了使用新版本Three.js库我对Sim.js进行了部分修改,修改点附近以“@@”标记
  2. // Sim.js - A Simple Simulator for WebGL (based on Three.js)
  3. //Sim.js是一个基于Three.js的WebGL简单框架
  4. Sim = {};//Sim是一个自包含对象,库中的其他变量和函数都是这个自包含对象的属性,可以在库的外部通过“Sim.”的方式调用库内的方法。
  5.  
  6. // Sim.Publisher - base class for event publishers
  7. //Publish/Subscribe消息通信,用来优化多个对象之间的消息传递,事实上Tony Parisi的WebGL著作里并没有真正使用这种消息传递方法,关于Publish/Subscribe的简单例程可以参考:http://www.mamicode.com/info-detail-502782.html
  8. Sim.Publisher = function() {
  9. this.messageTypes = {};
  10. }
  11.  
  12. Sim.Publisher.prototype.subscribe = function(message, subscriber, callback) {
  13. var subscribers = this.messageTypes[message];
  14. if (subscribers)
  15. {
  16. if (this.findSubscriber(subscribers, subscriber) != -1)
  17. {
  18. return;
  19. }
  20. }
  21. else
  22. {
  23. subscribers = [];
  24. this.messageTypes[message] = subscribers;
  25. }
  26.  
  27. subscribers.push({ subscriber : subscriber, callback : callback });
  28. }
  29.  
  30. Sim.Publisher.prototype.unsubscribe = function(message, subscriber, callback) {
  31. if (subscriber)
  32. {
  33. var subscribers = this.messageTypes[message];
  34.  
  35. if (subscribers)
  36. {
  37. var i = this.findSubscriber(subscribers, subscriber, callback);
  38. if (i != -1)
  39. {
  40. this.messageTypes[message].splice(i, 1);
  41. }
  42. }
  43. }
  44. else
  45. {
  46. delete this.messageTypes[message];
  47. }
  48. }
  49.  
  50. Sim.Publisher.prototype.publish = function(message) {
  51. var subscribers = this.messageTypes[message];
  52.  
  53. if (subscribers)
  54. {
  55. for (var i = 0; i < subscribers.length; i++)
  56. {
  57. var args = [];
  58. for (var j = 0; j < arguments.length - 1; j++)
  59. {
  60. args.push(arguments[j + 1]);
  61. }
  62. subscribers[i].callback.apply(subscribers[i].subscriber, args);
  63. }
  64. }
  65. }
  66.  
  67. Sim.Publisher.prototype.findSubscriber = function (subscribers, subscriber) {
  68. for (var i = 0; i < subscribers.length; i++)
  69. {
  70. if (subscribers[i] == subscriber)
  71. {
  72. return i;
  73. }
  74. }
  75.  
  76. return -1;
  77. }
  78.  
  79. // Sim.App - application class (singleton)
  80. //Sim.App属性对“绘制环境”的封装(这里认为一个canvas里只有一个绘制环境)
  81. Sim.App = function()
  82. {
  83. Sim.Publisher.call(this);
  84. //call表示this(Sim.App)继承自Sim.Publisher,意指在Sim.App的上下文环境使用Sim.Publisher的“构造方法”,也就是使用Sim.App与Sim.Publisher重叠的属性(这里没有)执行了this.messageTypes = {};语句,为App对象建立了消息一个队列。
  85.  
  86. this.renderer = null;
  87. this.scene = null;
  88. this.camera = null;
  89. this.objects = [];
  90. //可见App对象包含了canvas的上下文、与显卡的交互接口、相机设置、物体数组
  91. }
  92.  
  93. Sim.App.prototype = new Sim.Publisher;
  94. //prototype表示Sim.App扩展自new Sim.Publisher,当调用Sim.App中的某个未定义的方法时,编译器会尝试到prototype中去寻找,如App.subscribe
  95. //prototype.init表示使用init方法对Sim.App进行原型拓展,这样所有的var myApp=new Sim.App都会自动具有init方法(找不到时去prototype中找);这与"Sim.App.init"是不同的,如果后着的init不在App的“构造方法”中定义,myApp是不会具有init方法的。
  96. Sim.App.prototype.init = function(param)//绘图环境初始化
  97. {
  98. param = param || {};
  99. var container = param.container;
  100. var canvas = param.canvas;
  101.  
  102. // Create the Three.js renderer, add it to our div
  103. //@@这一段是我自己改的,加入了没有显卡时的软件渲染选择,可惜CanvasRenderer只支持部分的Three.js功能,并且没有找到去除图元边线的方法。
  104.  
  105. function webglAvailable()//是否可用webgl
  106. {
  107. try{
  108. var canvas=document.createElement("canvas");
  109. return !!(window.WebGLRenderingContext
  110. &&(canvas.getContext("webgl")||canvas.getContext("experimental-webgl"))
  111. );
  112. }catch(e){
  113. return false;
  114. }
  115. }
  116. if(webglAvailable()){
  117. var renderer=new THREE.WebGLRenderer({ antialias: true, canvas: canvas });
  118. }else{
  119. var renderer=new THREE.CanvasRenderer({ antialias: true, canvas: canvas });//对于支持html5但不支持webgl的情况,使用更慢一些的2Dcanvas来软件实现webgl的效果
  120. }
  121. //var renderer = new THREE.WebGLRenderer( { antialias: true, canvas: canvas } );
  122. //@@
  123.  
  124. renderer.setClearColor( 0xffffff );//@@旧版本中这个是默认的
  125. renderer.setSize(container.offsetWidth, container.offsetHeight);
  126. container.appendChild( renderer.domElement );
  127. container.onfocus=function(){
  128. renderer.domElement.focus();//@@保持焦点!!
  129. }
  130. //在部分浏览器中canvas不具备保持焦点的能力,点击canvas时焦点会被设置在外面的container上,影响交互效果
  131.  
  132. // Create a new Three.js scene
  133. var scene = new THREE.Scene();
  134. scene.add( new THREE.AmbientLight( 0x505050 ) );
  135. scene.data = this;
  136.  
  137. // Put in a camera at a good default location
  138. camera = new THREE.PerspectiveCamera( 45, container.offsetWidth / container.offsetHeight, 1, 10000 );
  139. camera.position.set( 0, 0, 3.3333 );
  140.  
  141. scene.add(camera);
  142.  
  143. // Create a root object to contain all other scene objects
  144. //建立了一个“根物体”,来存放场景中的其他物体,也就是根物体移动时所有其他物体会和它一同移动
  145. var root = new THREE.Object3D();
  146. scene.add(root);
  147.  
  148. // Create a projector to handle picking
  149. //建立一个“投影器”来处理三维空间中的点选,@@新版本中去掉了这个属性,这里的定义是多余的
  150. var projector = new THREE.Projector();
  151.  
  152. // Save away a few things
  153. //把上面的属性设为App对象的“公有”属性,var则是App对象的“私有”属性
  154. this.container = container;
  155. this.renderer = renderer;
  156. this.scene = scene;
  157. this.camera = camera;
  158. this.projector = projector;
  159. this.root = root;
  160.  
  161. // Set up event handlers
  162. //启动事件响应功能
  163. this.initMouse();
  164. this.initKeyboard();
  165. this.addDomHandlers();
  166. }
  167.  
  168. //Core run loop
  169. //核心循环
  170. Sim.App.prototype.run = function()
  171. {
  172. this.update();
  173. this.renderer.render( this.scene, this.camera );
  174. var that = this;//之所以使用that是为了保存此时的this状态,requestAnimationFrame会在“浏览器认为合适”的时候重调,而那时的“this”可能已经发生变化了。
  175. //requestAnimationFrame(function() { that.run(); });
  176. requestAnimFrame(function() { that.run(); });//@@换用了另一个帧动画库
  177. }
  178.  
  179. // Update method - called once per tick
  180. //场景更新方法,这里的代码逻辑运行在浏览器端,是CPU资源的主要消耗者
  181. Sim.App.prototype.update = function()
  182. {
  183. var i, len;
  184. len = this.objects.length;
  185. for (i = 0; i < len; i++)
  186. {//将App的update转化为其所包含的objects的update
  187. this.objects[i].update();
  188. }
  189. }
  190.  
  191. // Add/remove objects
  192. //在场景中添加或删除一个物体
  193. //添加
  194. Sim.App.prototype.addObject = function(obj)
  195. {
  196. this.objects.push(obj);//将物体对象添加到前面建立的物体数组里
  197.  
  198. // If this is a renderable object, add it to the root scene
  199. //Three.js对于场景中object3D类型的对象提供了“parent/children ”式的关联链,Sim.js封装了这一关联
  200. if (obj.object3D)
  201. {
  202. this.root.add(obj.object3D);
  203. }
  204. }
  205. //删除
  206. Sim.App.prototype.removeObject = function(obj)
  207. {
  208. var index = this.objects.indexOf(obj);
  209. if (index != -1)
  210. {
  211. this.objects.splice(index, 1);
  212. // If this is a renderable object, remove it from the root scene
  213.  
  214. if (obj.object3D)
  215. {
  216. this.root.remove(obj.object3D);
  217. }
  218. }
  219. }
  220.  
  221. // Event handling
  222. //事件处理
  223. //初始化鼠标响应
  224. Sim.App.prototype.initMouse = function()
  225. {
  226. var dom = this.renderer.domElement;//取得canvas
  227.  
  228. //添加监听
  229. var that = this;
  230. dom.addEventListener( 'mousemove',
  231. function(e) { that.onDocumentMouseMove(e); }, false );
  232. dom.addEventListener( 'mousedown',
  233. function(e) { that.onDocumentMouseDown(e); }, false );
  234. dom.addEventListener( 'mouseup',
  235. function(e) { that.onDocumentMouseUp(e); }, false );
  236.  
  237. //中键滚动
  238. $(dom).mousewheel(
  239. function(e, delta) {
  240. that.onDocumentMouseScroll(e, delta);
  241. }
  242. );
  243.  
  244. //鼠标悬停的物体
  245. this.overObject = null;
  246. //被点击到的物体
  247. this.clickedObject = null;
  248. }
  249. //初始化键盘响应
  250. Sim.App.prototype.initKeyboard = function()
  251. {
  252. var dom = this.renderer.domElement;
  253.  
  254. var that = this;
  255. dom.addEventListener( 'keydown',
  256. function(e) { that.onKeyDown(e); }, false );
  257. dom.addEventListener( 'keyup',
  258. function(e) { that.onKeyUp(e); }, false );
  259. dom.addEventListener( 'keypress',
  260. function(e) { that.onKeyPress(e); }, false );
  261.  
  262. // so it can take focus
  263. //这样设置之后canvas可以通过Tab键获得焦点,@@但这个设置并不完美,仍需要修改
  264. dom.setAttribute("tabindex", 1);
  265. dom.style.outline='none';
  266. dom.focus();
  267. }
  268.  
  269. Sim.App.prototype.addDomHandlers = function()
  270. {
  271. var that = this;
  272. //监听浏览器窗口大小的变化
  273. window.addEventListener( 'resize', function(event) { that.onWindowResize(event); }, false );
  274. }
  275.  
  276. //如果监听到鼠标移动
  277. Sim.App.prototype.onDocumentMouseMove = function(event)
  278. {
  279. event.preventDefault();//阻止浏览器的默认响应
  280.  
  281. if (this.clickedObject && this.clickedObject.handleMouseMove)
  282. {//如果已经有选中的物体,并且被选中的物体具有自己的handleMouseMove方法
  283. var hitpoint = null, hitnormal = null;//三维空间中的“点击点”和“点击法线”(鼠标在3D物体上的点击方向)设为空
  284. var intersected = this.objectFromMouse(event.pageX, event.pageY);
  285. //在三维空间中通过浏览器中的二维坐标,找到鼠标所在的物体,稍后详细分析该方法
  286. if (intersected.object == this.clickedObject)
  287. {//如果鼠标所在的物体确实是被选中的物体,
  288. hitpoint = intersected.point;
  289. hitnormal = intersected.normal;
  290. }
  291. this.clickedObject.handleMouseMove(event.pageX, event.pageY, hitpoint, hitnormal);
  292. //执行这个被选中的物体的鼠标移动方法,比如拖拽变形之类
  293. }
  294. else
  295. {//如果没有被选中的物体
  296. var handled = false;
  297.  
  298. var oldObj = this.overObject;//暂存旧的“悬停物体”
  299. var intersected = this.objectFromMouse(event.pageX, event.pageY);
  300. this.overObject = intersected.object;//将悬停物体设为鼠标所在的物体
  301.  
  302. if (this.overObject != oldObj)//如果这是一个新物体,也就是说鼠标从一个物体上移到另一物体上
  303. {
  304. if (oldObj)
  305. {//如果存在旧的物体,则要触发旧物体的“鼠标移出”事件
  306. this.container.style.cursor = 'auto';//取巧用CSS来处理光标变化,是2D网页和3Dcanvas的结合运用
  307.  
  308. if (oldObj.handleMouseOut)
  309. {
  310. oldObj.handleMouseOut(event.pageX, event.pageY);
  311. }
  312. }
  313.  
  314. if (this.overObject)
  315. {
  316. if (this.overObject.overCursor)
  317. {
  318. this.container.style.cursor = this.overObject.overCursor;//光标设置
  319. }
  320.  
  321. if (this.overObject.handleMouseOver)
  322. {
  323. this.overObject.handleMouseOver(event.pageX, event.pageY);
  324. }
  325. }
  326.  
  327. handled = true;//表示物体的handleMouseOver执行完毕
  328. }
  329.  
  330. if (!handled && this.handleMouseMove)
  331. {
  332. this.handleMouseMove(event.pageX, event.pageY);
  333. //如果物体没有执行handleMouseOver,且环境(App)能够响应handleMouseOver,则执行环境的鼠标移动响应,在应用中可体现为移动视角之类
  334. }
  335. }
  336. }
  337. //鼠标按下
  338. Sim.App.prototype.onDocumentMouseDown = function(event)
  339. {
  340. event.preventDefault();
  341.  
  342. var handled = false;
  343.  
  344. var intersected = this.objectFromMouse(event.pageX, event.pageY);
  345. if (intersected.object)
  346. {
  347. if (intersected.object.handleMouseDown)
  348. {
  349. intersected.object.handleMouseDown(event.pageX, event.pageY, intersected.point, intersected.normal);
  350. this.clickedObject = intersected.object;
  351. handled = true;
  352. }
  353. }
  354.  
  355. if (!handled && this.handleMouseDown)
  356. {
  357. this.handleMouseDown(event.pageX, event.pageY);
  358. }
  359. }
  360.  
  361. Sim.App.prototype.onDocumentMouseUp = function(event)
  362. {
  363. event.preventDefault();
  364.  
  365. var handled = false;
  366.  
  367. var intersected = this.objectFromMouse(event.pageX, event.pageY);
  368. if (intersected.object)
  369. {
  370. if (intersected.object.handleMouseUp)
  371. {
  372. intersected.object.handleMouseUp(event.pageX, event.pageY, intersected.point, intersected.normal);
  373. handled = true;
  374. }
  375. }
  376.  
  377. if (!handled && this.handleMouseUp)
  378. {
  379. this.handleMouseUp(event.pageX, event.pageY);
  380. }
  381.  
  382. this.clickedObject = null;
  383. }
  384.  
  385. Sim.App.prototype.onDocumentMouseScroll = function(event, delta)
  386. {
  387. event.preventDefault();
  388.  
  389. if (this.handleMouseScroll)
  390. {
  391. this.handleMouseScroll(delta);
  392. }
  393. }
  394.  
  395. Sim.App.prototype.objectFromMouse = function(pagex, pagey)
  396. {
  397. // Translate page coords to element coords
  398. //把浏览器页面中的位置转化为canvas中的坐标
  399. var offset = $(this.renderer.domElement).offset();
  400. var eltx = pagex - offset.left;
  401. var elty = pagey - offset.top;
  402.  
  403. // Translate client coords into viewport x,y
  404. //把canvas中的坐标转化为3D场景中的坐标
  405. var vpx = ( eltx / this.container.offsetWidth ) * 2 - 1;
  406. var vpy = - ( elty / this.container.offsetHeight ) * 2 + 1;
  407.  
  408. var vector = new THREE.Vector3( vpx, vpy, 0.5 );//补充一个z轴坐标,形成三维空间中靠原点外侧的一个点(在Three.js中“点”分为Points和Vector两种,前者具有颜色、大小、材质是真正可以被显示出来的物体,后着是数学意义上的点或者向量)
  409.  
  410. //this.projector.unprojectVector( vector, this.camera );
  411. vector.unproject(this.camera);//@@新版本中去掉投影矩阵影响的方法,不要忘记3D场景中看到的东西都是经过投影矩阵变形过的,所以要先把“看到的位置”转化为“实际的位置”再进行位置计算
  412.  
  413. //@@这里是Sim.js中版本差异最大的地方
  414. //在三维空间中取得物体的原理:从相机到“鼠标所在的点”画一条射线,通过Three.js封装的方法取得这条射线穿过的所有物体,第一个穿过的物体被认为是“鼠标所在的物体”
  415.  
  416. //var ray = new THREE.Ray( this.camera.position, vector.subSelf( this.camera.position ).normalize() );
  417. //var intersects = ray.intersectScene( this.scene );
  418. var raycaster = new THREE.Raycaster(this.camera.position,vector.subVectors(vector,this.camera.position).normalize());
  419. //@@Raycaster是新版Three.js专门为“穿过检测”定义的一种对象,与Ray分别开来,第一个参数是射线的端点,第二个参数是一个标准化(长度为一)的向量
  420. var intersects = raycaster.intersectObjects(this.scene.children,true);
  421. //true表示考虑物体的子物体,这里必须加上,被“穿过到”的物体被存入了一个数组
  422.  
  423. if ( intersects.length > 0 ) {
  424.  
  425. /*var i = 0;
  426. while(!intersects[i].object.visible)
  427. {
  428. i++;
  429. }
  430.  
  431. var intersected = intersects[i];
  432. var mat = new THREE.Matrix4().getInverse(intersected.object.matrixWorld);
  433. var point = mat.multiplyVector3(intersected.point);
  434.  
  435. return (this.findObjectFromIntersected(intersected.object, intersected.point, intersected.face.normal)); */
  436. //@@
  437. for(var i=0;i<intersects.length;i++)
  438. {
  439. if(intersects[i].object.visible&&intersects[i].face)
  440. {//物体可见并且”有面“(剔除了穿过线物体和点物体的情况)
  441. var intersected = intersects[i];
  442. var mat = new THREE.Matrix4().getInverse(intersected.object.matrixWorld);
  443. var point=intersected.point.applyMatrix4( mat );//可见intersected.point是相对坐标,加上物体所在的姿态矩阵之后变成了3D空间中的绝对坐标
  444. return (this.findObjectFromIntersected(intersected.object, intersected.point, intersected.face.normal));
  445. }
  446. }
  447. return { object : null, point : null, normal : null };//没有找到符合条件的物体
  448. }
  449. else
  450. {
  451. return { object : null, point : null, normal : null };
  452. }
  453. }
  454.  
  455. Sim.App.prototype.findObjectFromIntersected = function(object, point, normal)
  456. {//回溯子物体的parent/children链,找到距它最近的具有data属性的父物体,这样的物体是使用Sim.Object定义的。这种回溯保持了复杂物体的整体性:拉一个人的手使得整个人移动,而非手脱离了人自己移动。
  457.  
  458. if (object.data)
  459. {
  460. return { object: object.data, point: point, normal: normal };
  461. }
  462. else if (object.parent)
  463. {
  464. return this.findObjectFromIntersected(object.parent, point, normal);
  465. }
  466. else
  467. {
  468. return { object : null, point : null, normal : null };
  469. }
  470. }
  471.  
  472. //键盘按键被按下
  473. Sim.App.prototype.onKeyDown = function(event)
  474. {
  475. // N.B.: Chrome doesn't deliver keyPress if we don't bubble... keep an eye on this
  476. //作者说的是浏览器兼容性的问题,是否可用JQuery弥补?
  477. event.preventDefault();
  478.  
  479. if (this.handleKeyDown)
  480. {
  481. this.handleKeyDown(event.keyCode, event.charCode);
  482. }
  483. }
  484.  
  485. Sim.App.prototype.onKeyUp = function(event)
  486. {
  487. // N.B.: Chrome doesn't deliver keyPress if we don't bubble... keep an eye on this
  488. event.preventDefault();
  489.  
  490. if (this.handleKeyUp)
  491. {
  492. this.handleKeyUp(event.keyCode, event.charCode);
  493. }
  494. }
  495.  
  496. Sim.App.prototype.onKeyPress = function(event)
  497. {
  498. // N.B.: Chrome doesn't deliver keyPress if we don't bubble... keep an eye on this
  499. event.preventDefault();
  500.  
  501. if (this.handleKeyPress)
  502. {
  503. this.handleKeyPress(event.keyCode, event.charCode);
  504. }
  505. }
  506.  
  507. //浏览器窗口大小变化
  508. Sim.App.prototype.onWindowResize = function(event) {
  509.  
  510. this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight);
  511.  
  512. this.camera.aspect = this.container.offsetWidth / this.container.offsetHeight;//宽高比
  513. this.camera.updateProjectionMatrix();//投影矩阵
  514.  
  515. }
  516. //给环境定义了一个取得焦点的方法
  517. Sim.App.prototype.focus = function()
  518. {
  519. if (this.renderer && this.renderer.domElement)
  520. {
  521. this.renderer.domElement.focus();
  522. }
  523. }
  524.  
  525. // Sim.Object - base class for all objects in our simulation
  526. //物体的“基类”
  527. Sim.Object = function()
  528. {
  529. Sim.Publisher.call(this);
  530.  
  531. this.object3D = null;
  532. this.children = [];
  533. }
  534.  
  535. Sim.Object.prototype = new Sim.Publisher;
  536.  
  537. Sim.Object.prototype.init = function()//物体本身没有init时,才会到“基类”里找
  538. {
  539. }
  540.  
  541. Sim.Object.prototype.update = function()
  542. {
  543. this.updateChildren();//驱动子物体的update
  544. }
  545.  
  546. // setPosition - move the object to a new position
  547. //把物体移到到另一个位置
  548. Sim.Object.prototype.setPosition = function(x, y, z)
  549. {
  550. if (this.object3D)
  551. {
  552. this.object3D.position.set(x, y, z);
  553. }
  554. }
  555.  
  556. //setScale - scale the object
  557. //成比例的放大缩小这个物体
  558. Sim.Object.prototype.setScale = function(x, y, z)
  559. {
  560. if (this.object3D)
  561. {
  562. this.object3D.scale.set(x, y, z);
  563. }
  564. }
  565.  
  566. //setScale - scale the object
  567. //用递归的方式设置物体及其子物体的可见性
  568. Sim.Object.prototype.setVisible = function(visible)
  569. {
  570. function setVisible(obj, visible)
  571. {
  572. obj.visible = visible;
  573. var i, len = obj.children.length;
  574. for (i = 0; i < len; i++)
  575. {
  576. setVisible(obj.children[i], visible);
  577. }
  578. }
  579.  
  580. if (this.object3D)
  581. {
  582. setVisible(this.object3D, visible);
  583. }
  584. }
  585. //@@写到这里作者也累了,所以附近出现了错误代码
  586. // updateChildren - update all child objects
  587. Sim.Object.prototype.updateChildren = function()
  588. {
  589. var i, len;
  590. len = this.children.length;
  591. for (i = 0; i < len; i++)
  592. {
  593. this.children[i].update();
  594. }
  595. }
  596.  
  597. Sim.Object.prototype.setObject3D = function(object3D)
  598. {
  599. object3D.data = this;//建立双向链表,可以相互调用
  600. this.object3D = object3D;//将这个我们自己定义的Sim.Object和Three.js的object3D对象关联在一起
  601. }
  602.  
  603. //Add/remove children
  604. //添加/删除子物体
  605. Sim.Object.prototype.addChild = function(child)
  606. {
  607. this.children.push(child);//Sim.js设置
  608.  
  609. // If this is a renderable object, add its object3D as a child of mine
  610. if (child.object3D)//Three.js设置
  611. {
  612. this.object3D.add(child.object3D);
  613. }
  614. }
  615.  
  616. Sim.Object.prototype.removeChild = function(child)
  617. {
  618. var index = this.children.indexOf(child);
  619. if (index != -1)
  620. {
  621. this.children.splice(index, 1);
  622. // If this is a renderable object, remove its object3D as a child of mine
  623. if (child.object3D)
  624. {
  625. this.object3D.remove(child.object3D);
  626. }
  627. }
  628. }
  629.  
  630. // Some utility methods
  631. //从物体返回到场景,如果没有这个方法就只能用全局变量去引用camera了
  632. Sim.Object.prototype.getScene = function()
  633. {
  634. var scene = null;
  635. if (this.object3D)
  636. {
  637. var obj = this.object3D;
  638. while (obj.parent)
  639. {
  640. obj = obj.parent;
  641. }
  642.  
  643. scene = obj;
  644. }
  645.  
  646. return scene;
  647. }
  648.  
  649. Sim.Object.prototype.getApp = function()
  650. {
  651. var scene = this.getScene();
  652. return scene ? scene.data : null;//如果scene不具备data属性,说明scene不对应App
  653. }
  654.  
  655. // Some constants
  656.  
  657. /* key codes
  658. 37: left
  659. 38: up
  660. 39: right
  661. 40: down
  662. */
  663. Sim.KeyCodes = {};
  664. Sim.KeyCodes.KEY_LEFT = 37;
  665. Sim.KeyCodes.KEY_UP = 38;
  666. Sim.KeyCodes.KEY_RIGHT = 39;
  667. Sim.KeyCodes.KEY_DOWN = 40;
  668. //几个常用按键的键值

下面通过部分示例代码演示Sim框架的使用方法。原始示例引用自(T)的第三章,时间有限,我并没有对全部代码进行Three.js新版本修改,选取的两个JS文件主要用来体现这种面向对象的调用方法。如果需要旧版本的完整示例代码请到(T)的github下载(https://github.com/tparisi/WebGLBook),如果想研究新版本方法请到Three.js官网阅读官方文档(http://threejs.org/docs/index.html#Manual/Introduction/Creating_a_scene)。

Sim.App的示例代码:

  1. //这是一个模拟太阳系的3D场景,这里是solarSystem2.js文件
  2. // Constructor
  3. SolarSystemApp = function()
  4. {
  5. Sim.App.call(this);//用Sim.App构造SolarSystemApp
  6. }
  7.  
  8. // Subclass Sim.App
  9. SolarSystemApp.prototype = new Sim.App();//原型扩展
  10.  
  11. // Our custom initializer
  12. //“太阳系对象”的初始化方法(“构造”是“初始化”的基础)
  13. SolarSystemApp.prototype.init = function(container)
  14. {
  15. // Call superclass init code to set up scene, renderer, default camera
  16. //在this的情况下调用Sim.js库中的Sim.App.prototype.init方法,实现了代码重用,container是init方法的参数
  17. Sim.App.prototype.init.call(this, container);
  18. //除了调用库中的init,初始化还要做这些:
  19. this.planets = [];//行星数组
  20. this.orbits = [];//行星轨道
  21. this.lastX = 0;
  22. this.lastY = 0;
  23. this.mouseDown = false;
  24. this.lastTime = 0;//上次渲染时间
  25. this.currentlyPressedKeys=[];//当前键盘数组
  26.  
  27. // Let there be light!
  28. var sun = new Sun();//建立Sun对象
  29. sun.init();//sun对象的初始化方法
  30. this.addObject(sun);//很自然的把Sun对象添加到SolarSystemApp的objects数组中
  31.  
  32. // Are the stars out tonight...?
  33. var stars = new Stars();//太阳系外的遥远恒星
  34. // Push the stars out past Pluto
  35. //括号里是到太阳的最小距离
  36. stars.init(Sun.SIZE_IN_EARTHS + SolarSystemApp.EARTH_DISTANCE * SolarSystemApp.PLUTO_DISTANCE_IN_EARTHS);
  37. this.addObject(stars);
  38.  
  39. // And on the third day...
  40. this.createPlanets();//建立行星
  41.  
  42. // Move the camera back so we can see our Solar System
  43. this.camera.position.set(0, 0, Sun.SIZE_IN_EARTHS * 8);//将相机向外移动一些,看到太阳系的全貌
  44.  
  45. var amb = new THREE.AmbientLight(0x676767);//环境光
  46. this.scene.add(amb);
  47.  
  48. // Tilt the whole solar system toward the camera a bit
  49. //将整个太阳系绕x轴旋转一些
  50. this.root.rotation.x = Math.PI / 8;
  51.  
  52. }
  53.  
  54. //下面是对鼠标键盘的响应,其中的监听配置由Sim库完成
  55. //SolarSystemApp对象的鼠标移动处理
  56. SolarSystemApp.prototype.handleMouseMove = function(x, y)
  57. {
  58. if (this.mouseDown)//如果现在鼠标是按下的状态
  59. {
  60. var dx = x - this.lastX;//鼠标在x轴上相对于原位置的位移
  61. if (Math.abs(dx) > SolarSystemApp.MOUSE_MOVE_TOLERANCE)//这个位移大于一定程度才能够生效,避免了鼠标微小震动的影响
  62. {
  63. this.root.rotation.y -= (dx * 0.01);//太阳系绕y轴旋转一定角度
  64. }
  65. this.lastX = x;//更新x轴原位置
  66.  
  67. //return;
  68.  
  69. var dy = y - this.lastY;
  70. if (Math.abs(dy) > SolarSystemApp.MOUSE_MOVE_TOLERANCE)
  71. {
  72. this.root.rotation.x += (dy * 0.01);
  73.  
  74. // Clamp to some outer boundary values
  75. if (this.root.rotation.x < 0)
  76. this.root.rotation.x = 0;
  77.  
  78. if (this.root.rotation.x > SolarSystemApp.MAX_ROTATION_X)//达到一定角度之后禁止继续旋转
  79. this.root.rotation.x = SolarSystemApp.MAX_ROTATION_X;
  80.  
  81. }
  82. this.lastY = y;
  83.  
  84. }
  85. }
  86.  
  87. SolarSystemApp.prototype.handleMouseDown = function(x, y)
  88. {
  89. this.lastX = x;
  90. this.lastY = y;
  91. this.mouseDown = true;
  92. }
  93.  
  94. SolarSystemApp.prototype.handleMouseUp = function(x, y)
  95. {
  96. this.lastX = x;
  97. this.lastY = y;
  98. this.mouseDown = false;
  99. }
  100.  
  101. SolarSystemApp.prototype.handleMouseScroll = function(delta)
  102. {//鼠标滚轮控制相机远近
  103. var dx = delta;
  104.  
  105. this.camera.position.z -= dx*10;
  106.  
  107. // Clamp to some boundary values
  108. if (this.camera.position.z < SolarSystemApp.MIN_CAMERA_Z)
  109. this.camera.position.z = SolarSystemApp.MIN_CAMERA_Z;
  110. if (this.camera.position.z > SolarSystemApp.MAX_CAMERA_Z)
  111. this.camera.position.z = SolarSystemApp.MAX_CAMERA_Z;
  112. }
  113. //用currentlyPressedKeys数组保存键盘所有按键的状态
  114. SolarSystemApp.prototype.handleKeyDown= function(keyCode,charCode)
  115. {
  116. this.currentlyPressedKeys[keyCode] = true;
  117. }
  118. SolarSystemApp.prototype.handleKeyUp= function(keyCode,charCode)
  119. {
  120. this.currentlyPressedKeys[keyCode] = false;
  121. }
  122.  
  123. SolarSystemApp.prototype.update = function()
  124. {
  125. showFocus();
  126. var speed=0//前后速度
  127. var adspeed=0//左右速度
  128. if (this.currentlyPressedKeys[65]) {
  129. // A键-横向左
  130. adspeed = -0.009;
  131. } else if ( this.currentlyPressedKeys[68]) {
  132. // D键-横向右
  133. adspeed = +0.009;
  134. }
  135.  
  136. if (this.currentlyPressedKeys[87]) {
  137. // W键-纵向上
  138. speed = 0.003;
  139. } else if ( this.currentlyPressedKeys[83]) {
  140. // S键-纵向下
  141. speed = -0.003;
  142. }
  143. var timeNow = new Date().getTime();
  144. if (this.lastTime != 0) {
  145. var elapsed = timeNow - this.lastTime;
  146.  
  147. if (speed != 0||adspeed!=0) {
  148. this.camera.position.x+=adspeed* elapsed;
  149. this.camera.position.y+=speed* elapsed;
  150. }
  151.  
  152. // adjust yaw and pitch by their respective rates of change
  153. }
  154. this.lastTime = timeNow;
  155.  
  156. Sim.App.prototype.update.call(this);//驱动Sim中的“this.objects[i].update();”
  157. }
  158. //按照planet_specs数组批量生成行星
  159. SolarSystemApp.prototype.createPlanets = function ()
  160. {
  161. var i, len = SolarSystemApp.planet_specs.length;
  162. for (i = 0; i < len; i++)
  163. {
  164. var spec = SolarSystemApp.planet_specs[i];//这个行星的参数
  165. var planet = spec.type ? new spec.type : new Planet;//除了地球和土星比较特殊,其他星球都创建为Planet对象,Planet继承自Sim.Object
  166.  
  167. planet.init({animateOrbit:true, animateRotation: true, showOrbit:true,
  168. distance:spec.distance * SolarSystemApp.EARTH_DISTANCE + Sun.SIZE_IN_EARTHS,
  169. size:spec.size * SolarSystemApp.EXAGGERATED_PLANET_SCALE,
  170. period : spec.period,
  171. revolutionSpeed : 0.002,
  172. map : spec.map});
  173. this.addObject(planet);
  174. this.planets.push(planet);
  175.  
  176. var orbit = new Orbit();//行星轨道
  177. orbit.init(spec.distance * SolarSystemApp.EARTH_DISTANCE + Sun.SIZE_IN_EARTHS);
  178. this.addObject(orbit);
  179. this.orbits.push(orbit);
  180. }
  181. }
  182.  
  183. SolarSystemApp.MOUSE_MOVE_TOLERANCE = 4;
  184. SolarSystemApp.MAX_ROTATION_X = Math.PI / 2;
  185. SolarSystemApp.MAX_CAMERA_Z = Sun.SIZE_IN_EARTHS * 50;
  186. SolarSystemApp.MIN_CAMERA_Z = Sun.SIZE_IN_EARTHS * 2;//镜头最小z值
  187. SolarSystemApp.EARTH_DISTANCE = 50;
  188. SolarSystemApp.PLUTO_DISTANCE_IN_EARTHS = 77.2;
  189. SolarSystemApp.EARTH_DISTANCE_SQUARED = 45000;
  190. SolarSystemApp.EXAGGERATED_PLANET_SCALE = 5.55;
  191. SolarSystemApp.planet_specs = [
  192. //大小,距离,周期,纹理图片
  193. // Mercury
  194. { size : 1 / 2.54, distance : 0.4, period : 0.24, map : "../IMAGE/SOLAR/Mercury.jpg" },
  195. // Venus
  196. { size : 1 / 1.05, distance : 0.7, period : 0.62, map : "../IMAGE/SOLAR/venus.jpg" },
  197. // Earth
  198. { type : Earth, size : 1 , distance : 1, period : 1, map : "../IMAGE/SOLAR/earth_surface_2048.jpg" },
  199. // Mars
  200. { size : 1 / 1.88, distance : 1.6, period : 1.88, map : "../IMAGE/SOLAR/MarsV3-Shaded-2k.jpg" },
  201. // Jupiter
  202. { size : 11.1, distance : 5.2, period : 11.86, map : "../IMAGE/SOLAR/realj2k.jpg" },
  203. // Saturn
  204. { type : Saturn, size : 9.41, distance : 10, period : 29.46, map : "../IMAGE/SOLAR/saturn_bjoernjonsson.jpg" },
  205. // Uranus
  206. { size : 4, distance : 19.6, period : 84.01, map : "../IMAGE/SOLAR/uranus.jpg" },
  207. // Neptune
  208. { size : 3.88, distance : 38.8, period : 164.8, map : "../IMAGE/SOLAR/neptune.jpg" },
  209. // Pluto - have to exaggerate his size or we'll never see the little guy
  210. { size : 10 / 5.55, distance : 77.2, period : 247.7, map : "../IMAGE/SOLAR/pluto.jpg" },
  211. ];

接下来是Sim.Object的示例代码:

  1. //Sim.Object的使用方式
  2. // Custom Planet class
  3. Planet = function()
  4. {
  5. Sim.Object.call(this);
  6. }
  7.  
  8. Planet.prototype = new Sim.Object();
  9.  
  10. Planet.prototype.init = function(param)
  11. {
  12. param = param || {};
  13.  
  14. // Create an orbit group to simulate the orbit - this is the top-level Planet group
  15. var planetOrbitGroup = new THREE.Object3D();//planetOrbitGroup是一个Three.js中的3D对象,Three.js为它准备了各种相关的方法和属性
  16.  
  17. // Tell the framework about our object
  18. this.setObject3D(planetOrbitGroup);//将Sim.js定义的Sim.object与Three.js定义的Object3D关联起来
  19.  
  20. // Create a group to contain Planet and Clouds meshes
  21. var planetGroup = new THREE.Object3D();
  22. var distance = param.distance || 0;
  23. var distsquared = distance * distance;
  24. planetGroup.position.set(Math.sqrt(distsquared/2), 0, -Math.sqrt(distsquared/2));//设置行星位置
  25. planetOrbitGroup.add(planetGroup);//planetGroup是planetOrbitGroup在Three.js层面的子物体
  26.  
  27. this.planetGroup = planetGroup;
  28. var size = param.size || 1;
  29. this.planetGroup.scale.set(size, size, size);//设置大小比例
  30.  
  31. var map = param.map;//纹理图片
  32. this.createGlobe(map);//使用纹理图片建立行星
  33.  
  34. this.animateOrbit = param.animateOrbit;//行星是否沿着轨道运动
  35. this.period = param.period;//公转周期
  36. this.revolutionSpeed = param.revolutionSpeed ? param.revolutionSpeed : Planet.REVOLUTION_Y;//公转速度
  37. }
  38.  
  39. Planet.prototype.createGlobe = function(map)
  40. {
  41. // Create our Planet with nice texture
  42. var geometry = new THREE.SphereGeometry(1, 32, 32);//建立一个多面体(球)
  43. //var texture = THREE.ImageUtils.loadTexture(map);
  44. //@@新版加载纹理方法
  45. var texture = new THREE.TextureLoader().load(map);
  46. //var material = new THREE.MeshPhongMaterial( {map: texture, ambient: 0x333333} );
  47. //@@
  48. var material = new THREE.MeshPhongMaterial( {map: texture} );
  49. var globeMesh = new THREE.Mesh( geometry, material );
  50.  
  51. // Add it to our group
  52. this.planetGroup.add(globeMesh);//globeMesh是planetGroup的子物体
  53.  
  54. // Save it away so we can rotate it
  55. this.globeMesh = globeMesh;
  56. }
  57.  
  58. Planet.prototype.update = function() //物体的update
  59. {
  60. // Simulate the orbit
  61. if (this.animateOrbit)
  62. {
  63. this.object3D.rotation.y += this.revolutionSpeed / this.period;
  64. }
  65.  
  66. Sim.Object.prototype.update.call(this);
  67. }
  68.  
  69. Planet.REVOLUTION_Y = 0.003;

最终效果:

上面的代码简单演示了基于Three.js的面向对象框架使用方法,在使用过程中我们发现Three.js的更倾向于3D场景构建,其本身并不具备作为“游戏引擎”的完整功能,很多功能需要用户自己编写。与其相对babylon.js是一个面向3D游戏应用编写的WebGL封装(http://www.babylonjs.com/),具有碰撞检测、物理模拟等功能,但渲染性能较Three.js略低。

下一步准备编写基于Three.js的3D碰撞检测功能。

Web3D编程入门总结——面向对象的基础Web3D框架的更多相关文章

  1. VS2010/MFC编程入门之四(MFC应用程序框架分析)

    VS2010/MFC编程入门之四(MFC应用程序框架分析)-软件开发-鸡啄米 http://www.jizhuomi.com/software/145.html   上一讲鸡啄米讲的是VS2010应用 ...

  2. Web三维编程入门总结之二:面向对象的基础Web3D框架

    本篇主要通过分析Tony Parisi的sim.js库(原版代码托管于:https://github.com/tparisi/WebGLBook/tree/master/sim),总结基础Web3D框 ...

  3. Web3D编程入门总结——WebGL与Three.js基础介绍

    /*在这里对这段时间学习的3D编程知识做个总结,以备再次出发.计划分成“webgl与three.js基础介绍”.“面向对象的基础3D场景框架编写”.“模型导入与简单3D游戏编写”三个部分,其他零散知识 ...

  4. Python快速编程入门,打牢基础必须知道的11个知识点 !

    Python被誉为全世界高效的编程语言,同时也被称作是“胶水语言”,那它为何能如此受欢迎,下面我们就来说说Python入门学习的必备11个知识点,也就是它为何能够如此受欢迎的原因. Python 简介 ...

  5. kotlin函数式编程入门及图片处理

    函数式编程入门: 对于面向对象编程[OOP]和函数式编程[FP] 由于在JAVA8的学习中系统的学习过了,所以这里对其概念就不过多解释了,下面直接用代码来看下在kotlin中函数式编程是如何编写的: ...

  6. VS2010/MFC编程入门教程之目录和总结

    鸡啄米的这套VS2010/MFC编程入门教程到此就全部完成了,虽然有些内容还未涉及到,但帮助大家进行VS2010/MFC的入门学习业已足够.以此教程的知识为基础,学习VS2010/MFC较为深入的内容 ...

  7. (转)VS2010-MFC编程入门教程之目录和总结

     目前该教程可以到鸡啄米编程课堂去学习,阅读体验更好,更适合在线学习. 原文目录及链接: 一.VS2010/MFC编程入门教程之目录 第一部分:VS2010/MFC开发环境 VS2010/MFC编程入 ...

  8. [Java入门笔记] 面向对象编程基础(二):方法详解

    什么是方法? 简介 在上一篇的blog中,我们知道了方法是类中的一个组成部分,是类或对象的行为特征的抽象. 无论是从语法和功能上来看,方法都有点类似与函数.但是,方法与传统的函数还是有着不同之处: 在 ...

  9. JavaScript基础入门12 - 面向对象编程

    目录 JavaScript 面向对象编程 前言 构造函数创建对象 instanceof constructor 返回值 原型对象 关于对象的属性查找 in hasOwnProperty() JS当中实 ...

随机推荐

  1. 关于C# WinForm 边框阴影窗体(一)

    using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using Sy ...

  2. petapoco定制,比较SQL事务,存储过程,分布式事务(MSDTC)的区别和场景

    使用分布式事务时 就锁死了,而且是只锁编辑的行 使用.netSQL事务一定要执行了一个CUD的SQL才会锁死,而且也是锁行,但是也锁读的行 .netSQL事务要在这里才锁死 结论,对于产品要求细粒度的 ...

  3. 【Go语言】集合与文件操作

    本文目录 1.数据集合的主要操作 1_1.字典的声明 1_2.字典的初始化和创建 1_3.字典的访问和操作 1_4.其他类型的数据集 2.文件操作 2_1.文件操作概述os包和path包 2_2.文件 ...

  4. jquery输入框按下回车提交表单

    jQuery on()方法是官方推荐的绑定事件的一个方法 $('#password').on('keydown', function(e) { // 短路语法,当e.keyCode == 13成立的时 ...

  5. EhCache WebCache 与 SpringMVC集成时 CacheManager冲突的问题

    转自:点击打开链接 http://www.cnblogs.com/daxin/p/3560989.html EhCache WebCache 与 SpringMVC集成时 CacheManager冲突 ...

  6. Server Tomcat v7.0 Server at localhost was unable to start within 45 seconds 解决方法

    Server Tomcat v6.0 Server at localhost was unable to start within 45 seconds. If the server requires ...

  7. apache中.htaccess不起作用

    找到apache的配置文件httpd.conf文件,找到:  代码如下 复制代码 #LoadModule rewrite_module modules/mod_rewrite.so 去掉前面的#号. ...

  8. HDU4325 树状数组

    Flowers Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Total Sub ...

  9. "我爱记单词"测试报告兼功能展示

    "我爱记单词"测试报告兼功能展示 前言: 我们大部分的测试都是一边开发一边完成的,这里给出软件开发基本完成后在使用时的一些测试例子. 一.背景介绍 我们的数据库中一共有10个表: ...

  10. spring security 匿名登录

    匿名登录,即用户尚未登录系统,系统会为所有未登录的用户分配一个匿名用户,这个用户也拥有自己的权限,不过他是不能访问任何被保护资源的. 设置一个匿名用户的好处是,我们在进行权限判断时,可以保证Secur ...