Viewport
撰写时间:2023-09-13
修订时间:2023-09-13
Viewport
在本应用框架中,Viewport是非常重要的一个类,可以说是应用框架中心枢纽部分。能否科学、合理地设计此类,对整个应用框架的功能与效率有着重要的影响。
ViewportManager是所有Viewport的管理类,其doOnResize方法的代码如下:
委派各个Viewport类来具体处理resize事件。因此,我们先来看Viewport类的实现情况。
状态图
在有视口的环境下进行渲染之前,必须确保以下的状态已经准备好。

状态共分为两种,一种是当画布大小发生改变时的状态,二是当相机视图发生改变时的状态。其中,当画布大小发生改变时,应同时设置第一种状态及第二种状态。
首先,当画布的大小改变时,需更新Viewport的rect。其结构为:
正对应于视口的坐标系。
cssBorderDOM属性值的类型是一个HTMLDivElement,用于在界面上通过绘制其边框来标识各个视口的边框。此属性值应随画布大小的改变而改变。
projectionMatrix属性值的类型是一个表示矩阵的数据类型mat4,由于我们使用透视视图,透视矩阵的计算与视口的宽高比相关:
因此,它也应随之重新计算。
现在,再来看第二种状态,即当相机取景发生改变时,应更新哪些状态。这种状态,实际上是每次渲染时应更新的状态。
当我们在视口进行平移、旋转、缩放等变换时,将由Camera类来记住这些状态,并反馈给Viewport类。因此,我们应根据这些矩阵,先更新着色器中的uProjectionMatrix及uViewMatrix。
之后,根据rect的值,调用gl的viewport方法,scissor方法及clear方法对本视口进行清屏。
这就是在渲染之前我们需要更新的状态。之后,将交由Scene类来读取模型的矩阵,并予以渲染。
类图

源代码解析
构造方法
构造方法需要两个参数,一个是表示相机的camera。另一个是viewportOpts,用于动态获取视口大小。将这两个参数都存储为类的属性。
初始化
这里重现上面的状态图进行讲解。

初始化的作用是需要对左图中的cssBorderDOM及projectionMatrix这两个属性分配内存。
首先,为cssBorderDOM分配内存。
上面的代码,创建了一个HTMLDivElement,并将其添加为网页中id为canvas-container
的div元素的子元素。对于4个视口的网页,在运行时将会形成下面的DOM树:
- body
- div id="canvas-container"
- canvas
- div class="viewport-border"
- div class="viewport-border"
- div class="viewport-border"
- div class="viewport-border"
- div id="canvas-container"
后面,将使用它们来动态标识出各个视口的边框线。最后,将此新生成的元素存储为类的cssBorderDOM属性值。
其次,为projectionMatrix分配内存:
这就完成了cssBorderDOM及projectionMatrix两个属性的内存分配。它们的值将在确定rect的属性值之后才随之确定。
响应事件
对应状态图,代码很容易编写了。
当画布大小改变时,依序更新状态图左图中的3个属性,然后再调用响应相机视图变换的事件处理代码。
首先更新rect属性值。
先调用WebGLUtils的syncCanvasSize方法确定整个画布的大小:
然后调用ViewportLayoutManager的GetViewportRect方法根据视口布局的类型及其该视口在此布局中的id
来获取该视口的尺寸大小。因为布局较多,因此该代码较长,但均非常容易理解。例如,对于上下左右4个视口的布局,其代码如下:
这样,各个视口的rect属性就存储了该视口的尺寸值。
然后据此更新cssBorderDOM的尺寸值。
因为我们使用了显示器的高分辨率的特性,而CSS属性值无视于此,因此,需将rect的各个属性值均除以devicePixelRatio。
再依据rect的属性值更新projectionMatrix的属性值:
这就完成了当画布尺寸改变时必须更新3个属性值的工作。下面该处理因相机视图变动而需做的工作了。
第一步,将更新后的投影矩阵及相机的视图矩阵更新至着色器中的uProjectionMatrix及uViewMatrix中。
当我们在当前视口平移、旋转、缩放场景时,Controls将以事件的参数调用camera的相应方法,最终更新camera的viewMatrix属性值。而我们只需调用其getViewMatrix方法即可获取该矩阵值。具体细节详见Camera一节。
第二步,清除当前视口区域。
我们之前已在WebGL的渲染管道中激活裁剪测试:
激活该功能后,gl的clear方法只清除viewport方法所确定的区域与scissor所确定的区域的重叠部分。上面的代码,这两部分的区域都使用了rect的值,因此只能清除本视口的区域,不会清除其他视口的内容。
至此,我们已处理投影矩阵、视图矩阵及其清屏。这些工作均与视口的区域大小有关,因此均集中由Viewport类处理。但我们尚未处理模型矩阵。
对于所有视口,不管该视口使用何种相机拍摄,都是拍摄同一群演员,只不过是拍摄角度不同而已。因此,模型矩阵与视口的职责无关,所有视口均共享同一模型矩阵。因此,在最后一步,我们将更新模型矩阵的任务交由Scene类来完成。
renderMeshesInViewport可以根据各个视口独有的viewportRenderMode属性值来进行渲染,这样在不同视口中可以独立地渲染为实体、框线、实体及框线等不同的渲染模式。详见Scene一节。
