Web3D編程入門總結——面向對象的基礎Web3D框架

本篇主要經過分析Tony Parisi的sim.js庫(原版代碼託管於:https://github.com/tparisi/WebGLBook/tree/master/sim),總結基礎Web3D框架的編寫方法。在上一篇的基礎上,要求讀者具備簡短英文閱讀或者查字典的能力。html

限於水平和時間,本文不免出現錯誤與遺漏,您在閱讀過程當中若是遇到錯誤或者疑問請在評論區中指出,我將盡快回復。html5

爲提升JavaScript編程效率,建議使用WebStorm工具進行網頁程序編寫,WebStorm官網:http://www.jetbrains.com/webstorm/。 git

上一篇中,咱們把程序的全部文件放在同一個目錄下,這種文件組織方式適用於簡單的功能測試,但當文件數量更多時則會變得混亂不堪,咱們在編寫通常規模的Web3D程序時可參考下圖進行文件組織:github

 

該組織方式把JavaScript文件分爲LIB和PAGE兩部分,LIB保存通常不作修改的庫文件,PAGE保存爲特定頁面編寫的js文件,若是頁面js較多可在PAGE中再分離出子文件夾。web

MODEL下的每一個文件夾都是一個JSON類型的模型,能夠看到其中有保存紋理信息的jpg文件和保存頂點數組、法線向量、紋理座標的文本文件。編程

上一篇的代碼中,咱們把全部須要屢次調用的對象設爲了全局變量和全局函數,當代碼量增多時這種「全局管理」方式將面臨巨大的挑戰,隨然咱們能夠用規範的變量命名或者變量數組來儘量避免變量名重複,但全局管理方式仍缺乏對變量間關係的描述方法,這時使用「面向對象」的變量管理方法彷佛是惟一的選擇。canvas

下面進入正題:數組

  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 
526 // Sim.Object - base class for all objects in our simulation
527 //物體的「基類」
528 Sim.Object = function()
529 {
530     Sim.Publisher.call(this);
531     
532     this.object3D = null;
533     this.children = [];
534 }
535 
536 Sim.Object.prototype = new Sim.Publisher;
537 
538 Sim.Object.prototype.init = function()//物體自己沒有init時,纔會到「基類」裏找
539 {
540 }
541 
542 Sim.Object.prototype.update = function()
543 {
544     this.updateChildren();//驅動子物體的update
545 }
546 
547 // setPosition - move the object to a new position
548 //把物體移到到另外一個位置
549 Sim.Object.prototype.setPosition = function(x, y, z)
550 {
551     if (this.object3D)
552     {
553         this.object3D.position.set(x, y, z);
554     }
555 }
556 
557 //setScale - scale the object
558 //成比例的放大縮小這個物體
559 Sim.Object.prototype.setScale = function(x, y, z)
560 {
561     if (this.object3D)
562     {
563         this.object3D.scale.set(x, y, z);
564     }
565 }
566 
567 //setScale - scale the object
568 //用遞歸的方式設置物體及其子物體的可見性
569 Sim.Object.prototype.setVisible = function(visible)
570 {
571     function setVisible(obj, visible)
572     {
573         obj.visible = visible;
574         var i, len = obj.children.length;
575         for (i = 0; i < len; i++)
576         {
577             setVisible(obj.children[i], visible);
578         }
579     }
580     
581     if (this.object3D)
582     {
583         setVisible(this.object3D, visible);
584     }
585 }
586 //@@寫到這裏做者也累了,因此附近出現了錯誤代碼
587 // updateChildren - update all child objects
588 Sim.Object.prototype.updateChildren = function()
589 {
590     var i, len;
591     len = this.children.length;
592     for (i = 0; i < len; i++)
593     {
594         this.children[i].update();
595     }
596 }
597 
598 Sim.Object.prototype.setObject3D = function(object3D)
599 {
600     object3D.data = this;//創建雙向鏈表,能夠相互調用
601     this.object3D = object3D;//將這個咱們本身定義的Sim.Object和Three.js的object3D對象關聯在一塊兒
602 }
603 
604 //Add/remove children
605 //添加/刪除子物體
606 Sim.Object.prototype.addChild = function(child)
607 {
608     this.children.push(child);//Sim.js設置
609     
610     // If this is a renderable object, add its object3D as a child of mine
611     if (child.object3D)//Three.js設置
612     {
613         this.object3D.add(child.object3D);
614     }
615 }
616 
617 Sim.Object.prototype.removeChild = function(child)
618 {
619     var index = this.children.indexOf(child);
620     if (index != -1)
621     {
622         this.children.splice(index, 1);
623         // If this is a renderable object, remove its object3D as a child of mine
624         if (child.object3D)
625         {
626             this.object3D.remove(child.object3D);
627         }
628     }
629 }
630 
631 // Some utility methods
632 //從物體返回到場景,若是沒有這個方法就只能用全局變量去引用camera了
633 Sim.Object.prototype.getScene = function()
634 {
635     var scene = null;
636     if (this.object3D)
637     {
638         var obj = this.object3D;
639         while (obj.parent)
640         {
641             obj = obj.parent;
642         }
643         
644         scene = obj;
645     }
646     
647     return scene;
648 }
649 
650 Sim.Object.prototype.getApp = function()
651 {
652     var scene = this.getScene();
653     return scene ? scene.data : null;//若是scene不具有data屬性,說明scene不對應App
654 }
655 
656 // Some constants
657 
658 /* key codes
659 37: left
660 38: up
661 39: right
662 40: down
663 */
664 Sim.KeyCodes = {};
665 Sim.KeyCodes.KEY_LEFT  = 37;
666 Sim.KeyCodes.KEY_UP  = 38;
667 Sim.KeyCodes.KEY_RIGHT  = 39;
668 Sim.KeyCodes.KEY_DOWN  = 40;
669 //幾個經常使用按鍵的鍵值

 下面經過部分示例代碼演示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的示例代碼: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 
 55 //下面是對鼠標鍵盤的響應,其中的監聽配置由Sim庫完成
 56 //SolarSystemApp對象的鼠標移動處理
 57 SolarSystemApp.prototype.handleMouseMove = function(x, y)
 58 {
 59     if (this.mouseDown)//若是如今鼠標是按下的狀態
 60     {
 61         var dx = x - this.lastX;//鼠標在x軸上相對於原位置的位移
 62         if (Math.abs(dx) > SolarSystemApp.MOUSE_MOVE_TOLERANCE)//這個位移大於必定程度纔可以生效,避免了鼠標微小震動的影響
 63         {
 64             this.root.rotation.y -= (dx * 0.01);//太陽系繞y軸旋轉必定角度
 65         }
 66         this.lastX = x;//更新x軸原位置
 67         
 68         //return;
 69         
 70         var dy = y - this.lastY;
 71         if (Math.abs(dy) > SolarSystemApp.MOUSE_MOVE_TOLERANCE)
 72         {
 73             this.root.rotation.x += (dy * 0.01);
 74             
 75             // Clamp to some outer boundary values
 76             if (this.root.rotation.x < 0)
 77                 this.root.rotation.x = 0;
 78             
 79             if (this.root.rotation.x > SolarSystemApp.MAX_ROTATION_X)//達到必定角度以後禁止繼續旋轉
 80                 this.root.rotation.x = SolarSystemApp.MAX_ROTATION_X;
 81             
 82         }
 83         this.lastY = y;
 84         
 85     }    
 86 }
 87 
 88 SolarSystemApp.prototype.handleMouseDown = function(x, y)
 89 {
 90     this.lastX = x;
 91     this.lastY = y;
 92     this.mouseDown = true;
 93 }
 94 
 95 SolarSystemApp.prototype.handleMouseUp = function(x, y)
 96 {
 97     this.lastX = x;
 98     this.lastY = y;
 99     this.mouseDown = false;
100 }
101 
102 SolarSystemApp.prototype.handleMouseScroll = function(delta)
103 {//鼠標滾輪控制相機遠近
104     var dx = delta;
105 
106     this.camera.position.z -= dx*10;
107     
108     // Clamp to some boundary values
109     if (this.camera.position.z < SolarSystemApp.MIN_CAMERA_Z)
110         this.camera.position.z = SolarSystemApp.MIN_CAMERA_Z;
111     if (this.camera.position.z > SolarSystemApp.MAX_CAMERA_Z)
112         this.camera.position.z = SolarSystemApp.MAX_CAMERA_Z;
113 }
114 //用currentlyPressedKeys數組保存鍵盤全部按鍵的狀態
115 SolarSystemApp.prototype.handleKeyDown= function(keyCode,charCode)
116 {
117     this.currentlyPressedKeys[keyCode] = true;
118 }
119 SolarSystemApp.prototype.handleKeyUp= function(keyCode,charCode)
120 {
121     this.currentlyPressedKeys[keyCode] = false;
122 }
123 
124 SolarSystemApp.prototype.update = function()
125 {
126     showFocus();
127     var speed=0//先後速度
128     var adspeed=0//左右速度
129     if (this.currentlyPressedKeys[65]) {
130         // A鍵-橫向左
131         adspeed = -0.009;
132     } else if ( this.currentlyPressedKeys[68]) {
133         // D鍵-橫向右
134         adspeed = +0.009;
135     } 
136 
137     if (this.currentlyPressedKeys[87]) {
138         // W鍵-縱向上
139         speed = 0.003;
140     } else if ( this.currentlyPressedKeys[83]) {
141         // S鍵-縱向下
142         speed = -0.003;
143     } 
144     var timeNow = new Date().getTime();
145     if (this.lastTime != 0) {
146         var elapsed = timeNow - this.lastTime;
147 
148         if (speed != 0||adspeed!=0) {
149             this.camera.position.x+=adspeed* elapsed;
150             this.camera.position.y+=speed* elapsed;
151         }
152 
153         // adjust yaw and pitch by their respective rates of change
154     }
155     this.lastTime = timeNow;
156     
157     Sim.App.prototype.update.call(this);//驅動Sim中的「this.objects[i].update();」
158 }
159 //按照planet_specs數組批量生成行星
160 SolarSystemApp.prototype.createPlanets = function ()
161 {        
162     var i, len = SolarSystemApp.planet_specs.length;
163     for (i = 0; i < len; i++)
164     {
165         var spec = SolarSystemApp.planet_specs[i];//這個行星的參數
166         var planet = spec.type ? new spec.type : new Planet;//除了地球和土星比較特殊,其餘星球都建立爲Planet對象,Planet繼承自Sim.Object
167         
168         planet.init({animateOrbit:true, animateRotation: true, showOrbit:true,        
169             distance:spec.distance * SolarSystemApp.EARTH_DISTANCE + Sun.SIZE_IN_EARTHS, 
170             size:spec.size * SolarSystemApp.EXAGGERATED_PLANET_SCALE, 
171             period : spec.period,
172             revolutionSpeed : 0.002,
173             map : spec.map});
174         this.addObject(planet);
175         this.planets.push(planet);
176 
177         var orbit = new Orbit();//行星軌道
178         orbit.init(spec.distance * SolarSystemApp.EARTH_DISTANCE + Sun.SIZE_IN_EARTHS);
179         this.addObject(orbit);        
180         this.orbits.push(orbit);
181     }
182 }
183 
184 SolarSystemApp.MOUSE_MOVE_TOLERANCE = 4;
185 SolarSystemApp.MAX_ROTATION_X = Math.PI / 2;
186 SolarSystemApp.MAX_CAMERA_Z = Sun.SIZE_IN_EARTHS * 50;
187 SolarSystemApp.MIN_CAMERA_Z = Sun.SIZE_IN_EARTHS * 2;//鏡頭最小z值
188 SolarSystemApp.EARTH_DISTANCE = 50;
189 SolarSystemApp.PLUTO_DISTANCE_IN_EARTHS = 77.2;
190 SolarSystemApp.EARTH_DISTANCE_SQUARED = 45000;
191 SolarSystemApp.EXAGGERATED_PLANET_SCALE = 5.55;
192 SolarSystemApp.planet_specs = [
193     //大小,距離,週期,紋理圖片
194    // Mercury
195    { size : 1 / 2.54, distance : 0.4, period : 0.24, map : "../IMAGE/SOLAR/Mercury.jpg" },
196    // Venus
197    { size : 1 / 1.05, distance : 0.7, period : 0.62, map : "../IMAGE/SOLAR/venus.jpg"  },
198    // Earth
199    { type : Earth, size : 1 , distance : 1, period : 1, map : "../IMAGE/SOLAR/earth_surface_2048.jpg"  },
200    // Mars
201    { size : 1 / 1.88, distance : 1.6, period : 1.88, map : "../IMAGE/SOLAR/MarsV3-Shaded-2k.jpg"  },
202    // Jupiter
203    { size : 11.1, distance : 5.2, period : 11.86, map : "../IMAGE/SOLAR/realj2k.jpg"  },
204    // Saturn
205    { type : Saturn, size : 9.41, distance : 10, period : 29.46, map : "../IMAGE/SOLAR/saturn_bjoernjonsson.jpg"  },
206    // Uranus
207    { size : 4, distance : 19.6, period : 84.01, map : "../IMAGE/SOLAR/uranus.jpg"  },
208    // Neptune
209    { size : 3.88, distance : 38.8, period : 164.8, map : "../IMAGE/SOLAR/neptune.jpg"  },
210    // Pluto - have to exaggerate his size or we'll never see the little guy
211    { size : 10 / 5.55, distance : 77.2, period : 247.7, map : "../IMAGE/SOLAR/pluto.jpg"  },
212                                ];

接下來是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 
59 Planet.prototype.update = function() //物體的update
60 {    
61     // Simulate the orbit
62     if (this.animateOrbit)
63     {
64         this.object3D.rotation.y += this.revolutionSpeed / this.period;
65     }
66     
67     Sim.Object.prototype.update.call(this);
68 }
69 
70 Planet.REVOLUTION_Y = 0.003;

最終效果:

上面的代碼簡單演示了基於Three.js的面向對象框架使用方法,在使用過程當中咱們發現Three.js的更傾向於3D場景構建,其自己並不具有做爲「遊戲引擎」的完整功能,不少功能須要用戶本身編寫。與其相對babylon.js是一個面向3D遊戲應用編寫的WebGL封裝(http://www.babylonjs.com/),具備碰撞檢測、物理模擬等功能,但渲染性能較Three.js略低。

下一步準備編寫基於Three.js的3D碰撞檢測功能。

相關文章
相關標籤/搜索