本篇主要通過分析Tony Parisi的sim.js庫(原版代碼托管於:https://github.com/tparisi/WebGLBook/tree/master/sim),總結基礎Web3D框架的編寫方法。在上一篇的基礎上,要求讀者具有簡短英文閱讀或者查字典的能力。 限於水平和時間,本文難免
本篇主要通過分析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 : nul