本篇主要經過分析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碰撞檢測功能。