Web3D编程入门总结——面向对象的基础Web3D框架

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Web3D编程入门总结——面向对象的基础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 : null, normal : null };
452     }
453 }
454 
455 Sim.App.prototype.findObjectFromIntersected = function以上是关于Web3D编程入门总结——面向对象的基础Web3D框架的主要内容,如果未能解决你的问题,请参考以下文章

Web3D|基于WebGL的Three.js框架|入门篇

Web3D发展趋势以及Web3D应用场景

three.js入门-一些基础理论|大帅老猿threejs特训

three.js入门-一些基础理论|大帅老猿threejs特训

Web3D框架——选threejs还是thingjs?

Web3D应用开发在线IDE中文版