我之前解析过Starling的核心渲染流程,相比Away3D而言Starling真的是足够简单,不过幸运的是两者的渲染流程是大体上相似的;Starling的渲染是每帧调用Starling类中的render方法,类似的Away3D的渲染是每帧调用View3D类中的render方法,那我们要了解Away3D的渲染就需要从这个方法入手了。
View3D的render方法源码:
1 /** 2 * Renders the view. 3 */ 4 public function render():void 5 { 6 //if context3D has Disposed by the OS,don't render at this frame 7 if (!stage3DProxy.recoverFromDisposal()) { 8 _backBufferInvalid = true; 9 return; 10 } 11 12 // reset or update render settings 13 if (_backBufferInvalid) 14 updateBackBuffer(); 15 16 if (_shareContext && _layeredView) 17 stage3DProxy.clearDepthBuffer(); 18 19 if (!_parentIsStage) { 20 var globalPos:Point = parent.localToGlobal(_localPos); 21 if (_globalPos.x != globalPos.x || _globalPos.y != globalPos.y) { 22 _globalPos = globalPos; 23 _globalPosDirty = true; 24 } 25 } 26 27 if (_globalPosDirty) 28 updateGlobalPos(); 29 30 updateTime(); 31 32 updateViewSizeData(); 33 34 _entityCollector.clear(); 35 36 // collect stuff to render 37 _scene.traversePartitions(_entityCollector); 38 39 // update picking 40 _mouse3DManager.updateCollider(this); 41 _touch3DManager.updateCollider(); 42 43 if (_requireDepthRender) 44 renderSceneDepthToTexture(_entityCollector); 45 46 // todo: perform depth prepass after light update and before final render 47 if (_depthPrepass) 48 renderDepthPrepass(_entityCollector); 49 50 _renderer.clearOnRender = !_depthPrepass; 51 52 if (_filter3DRenderer && _stage3DProxy._context3D) { 53 _renderer.render(_entityCollector, _filter3DRenderer.getMainInputTexture(_stage3DProxy), _rttBufferManager.renderToTextureRect); 54 _filter3DRenderer.render(_stage3DProxy, camera, _depthRender); 55 } else { 56 _renderer.shareContext = _shareContext; 57 if (_shareContext) 58 _renderer.render(_entityCollector, null, _scissorRect); 59 else 60 _renderer.render(_entityCollector); 61 62 } 63 64 if (!_shareContext) { 65 stage3DProxy.present(); 66 67 // fire collected mouse events 68 _mouse3DManager.fireMouseEvents(); 69 _touch3DManager.fireTouchEvents(); 70 } 71 72 // clean up data for this render 73 _entityCollector.cleanUp(); 74 75 // register that a view has been rendered 76 stage3DProxy.bufferClear = false; 77 }
在进入渲染代码的解读之前,我们应该需要大概的解读一下render方法实现的功能;
1 /** 2 * 渲染 View3D 对象. 3 */ 4 public function render():void 5 { 6 //判断当前的 context3D 对象是否可以使用, 不能使用则取消本次渲染 7 if (!stage3DProxy.recoverFromDisposal()) { 8 _backBufferInvalid = true; 9 return; 10 } 11 12 //如果 View3D 的尺寸改变则更新后台缓冲区的大小 13 if (_backBufferInvalid) 14 updateBackBuffer(); 15 16 //清除深度缓冲 17 if (_shareContext && _layeredView) 18 stage3DProxy.clearDepthBuffer(); 19 20 //如果父级不是 stage 对象则需要获取 View3D 对象的舞台坐标 21 if (!_parentIsStage) { 22 var globalPos:Point = parent.localToGlobal(_localPos); 23 if (_globalPos.x != globalPos.x || _globalPos.y != globalPos.y) { 24 _globalPos = globalPos; 25 _globalPosDirty = true; 26 } 27 } 28 29 //更新舞台坐标 30 if (_globalPosDirty) 31 updateGlobalPos(); 32 33 //获取当前帧和上一帧之间的间隔时间 34 updateTime(); 35 36 //更新视口尺寸数据, 主要是更新当前摄像机的属性 37 updateViewSizeData(); 38 39 //清除实体收集器 40 _entityCollector.clear(); 41 42 //对当前渲染的场景进行实体收集, 收集到的对象会在后面进行渲染 43 _scene.traversePartitions(_entityCollector); 44 45 //鼠标及触摸事件的处理 46 _mouse3DManager.updateCollider(this); 47 _touch3DManager.updateCollider(); 48 49 // ----- 渲染代码 begin ----- 50 51 if (_requireDepthRender) 52 renderSceneDepthToTexture(_entityCollector); 53 54 // todo: perform depth prepass after light update and before final render 55 if (_depthPrepass) 56 renderDepthPrepass(_entityCollector); 57 58 _renderer.clearOnRender = !_depthPrepass; 59 60 if (_filter3DRenderer && _stage3DProxy._context3D) { 61 _renderer.render(_entityCollector, _filter3DRenderer.getMainInputTexture(_stage3DProxy), _rttBufferManager.renderToTextureRect); 62 _filter3DRenderer.render(_stage3DProxy, camera, _depthRender); 63 } else { 64 _renderer.shareContext = _shareContext; 65 if (_shareContext) 66 _renderer.render(_entityCollector, null, _scissorRect); 67 else 68 _renderer.render(_entityCollector); 69 70 } 71 72 // ----- 渲染代码 end ----- 73 74 //不共享 context3D 对象就直接渲染, 共享需要手动调用 present 方法 75 if (!_shareContext) { 76 //呈现 3D 画面 77 stage3DProxy.present(); 78 79 //释放收集的鼠标和触摸事件 80 _mouse3DManager.fireMouseEvents(); 81 _touch3DManager.fireTouchEvents(); 82 } 83 84 //清除实体收集器的数据 85 _entityCollector.cleanUp(); 86 87 //标记已经渲染完毕 88 stage3DProxy.bufferClear = false; 89 }
撇开诸如鼠标事件的处理,我们可以知道Away3D的核心渲染是分为两个步骤的:
- 收集需要渲染的实体;
- 根据收集到的实体开始进行真正的渲染;
收集需要渲染的实体:
我们知道在Starling中是直接采用深度优先遍历的方法来遍历显示列表中的所有显示对象,然后一一进行渲染,并没有分为收集和渲染两个步骤;那么在Away3D中3D显示列表也是树形结构,也可以采用Starling的方法来遍历绘制,特别的是Starling采用画家算法,所以需要得到谁先绘制谁后绘制的正确顺序,而Away3D使用的是ZBuffer算法,无论谁先绘制最终都会呈现一样的结果,那么是不是说Away3D的渲染就更加简单了呢?当然不是,Away3D由于是存在一个3D空间中,所以最终的绘制对象需要结合其摄像机的镜头对准的区域来决定,不在可视区域的对象就不进行绘制,即视锥剔除,可以大大的提高渲染效率;那么Away3D的实体收集其核心就是得到需要渲染的3D对象,去掉不需要渲染的3D对象的过程。
我们看看收集实体对象的代码:
1 // collect stuff to render 2 _scene.traversePartitions(_entityCollector);
查看这个方法:
1 public function traversePartitions(traverser:PartitionTraverser):void 2 { 3 var i:uint; 4 var len:uint = _partitions.length; 5 6 traverser.scene = this; 7 8 while (i < len) 9 _partitions[i++].traverse(traverser); 10 }
我们发现一个陌生的类型Partition3D,而几乎所有的实体收集都是由该类接手处理的,那么这个类究竟是什么呢?每一个Scene3D在初始化的时候都会创建一个_partitions:Vector.<Partition3D>。Partition3D是一个空间分区系统的核心,它用于将三维场景分级成多个互不重叠的子空间,从而形成一个树型数据结构。
接着查看traverse方法:
1 public function traverse(traverser:PartitionTraverser):void 2 { 3 if (_updatesMade) 4 updateEntities(); 5 6 ++PartitionTraverser._collectionMark; 7 8 _rootNode.acceptTraverser(traverser); 9 }
我们发现了一个_rootNode对象,该对象是记录Partition3D包含的所有对象的树形结构的root,我们接下来看看acceptTraverser方法:
1 public function acceptTraverser(traverser:PartitionTraverser):void 2 { 3 if (_numEntities == 0 && !_debugPrimitive) 4 return; 5 6 if (traverser.enterNode(this)) { 7 var i:uint; 8 while (i < _numChildNodes) 9 _childNodes[i++].acceptTraverser(traverser); 10 11 if (_debugPrimitive) 12 traverser.applyRenderable(_debugPrimitive); 13 } 14 }
注意参数traverser就是我们的实体收集对象的实例_entityCollector,调用的方法enterNode即视锥剔除,会去掉不需要渲染的对象,而实际上添加需要渲染的对象是NodeBase的子类EntityNode的子类MeshNode,我们分别看看EntityNode和MeshNode的acceptTraverser方法:
EntityNode:
1 override public function acceptTraverser(traverser:PartitionTraverser):void 2 { 3 traverser.applyEntity(_entity); 4 }
MeshNode:
1 override public function acceptTraverser(traverser:PartitionTraverser):void 2 { 3 if (traverser.enterNode(this)) { 4 super.acceptTraverser(traverser); 5 var subs:Vector.<SubMesh> = _mesh.subMeshes; 6 var i:uint; 7 var len:uint = subs.length; 8 while (i < len) 9 traverser.applyRenderable(subs[i++]); 10 } 11 }
最终需要渲染的对象都会被收集,交给下一步的渲染代码进行渲染。
Partition3D将我们的3D空间切割为多个不重合的区域,那么如果一个实体对象移动到另一个Partition3D对象的区域,或改变尺寸跨越多个Partition3D对象时Away3D又是如何处理的呢?
我们看看实体类Entity的notifySceneBoundsInvalid方法,当我们的实体对象位置或尺寸改变时会调用该方法:
1 private function notifySceneBoundsInvalid():void 2 { 3 if (_scene) 4 _scene.invalidateEntityBounds(this); 5 }
这个方法会通知到我们的场景对象调用invalidateEntityBounds方法:
1 arcane function invalidateEntityBounds(entity:Entity):void 2 { 3 entity.implicitPartition.markForUpdate(entity); 4 }
markForUpdate方法会重新将我们的实体对象分配到对应的Partition3D对象中去。
另外有一个大神发现了实体回收的bug,链接贴出来: