
孙其瑞:Go+ Spx 引擎坐标与绘制体系详解
大家好,我今天的分享主题是「Go+ Spx 引擎坐标与绘制体系详解」。之所以分享这一主题,在于 Spx 是跨平台的引擎,在不同终端和平台中屏幕和 DPI 会存在不一致的问题,如何将 Spx 引擎坐标对应到屏幕中便是一个需要讨论的问题。
另外,Spx 具体如何绘制?地图、相机、精灵间的关系是什么?坐标的属性如何判断?等等细节问题,今天都会和大家一起聊一聊。
分享将分为如下四部分:
坐标系概述
Spx 坐标系
Spx 绘制机制
练习时间
一. 坐标系概述
1. 窗口
坐标系中要理解的第一个概念是窗口。比如我们打开 QQ 或者一个 Excel 表格,首先便会弹出一个窗口。这里的窗口是物理窗口,用户可以不断的控制它进行缩放或拉伸,在这个过程中像素可能也会随之发生变化,像素也就是屏幕坐标系中最常用的单位。
在对精灵与物体进行描述时,通常会问在窗口的哪个「位置」,比如距离窗口坐上角 100*100 的位置通常会表述为离窗口左上角 100*100 个像素。
但这里所说的像素并不等价于绘制的像素。所以这里有一个很重要的概念,绘制窗口是如何映射到屏幕中?屏幕的大小变化同时绘制会如何变化?
2. 裁剪区域
所谓的逻辑坐标(基础坐标系)是用户定义的一个坐标体系,在这个坐标体系中用户可以设定和控制当前能够显示在可视化窗口上的内容。告诉绘制的 API、OpenGL 等如何将坐标映射到屏幕中。
3. 视口(视窗)
绘制通常是在 ViewPort 中完成,比如假设 ViewPort 的大小是从左上角的 (0,0) 开始,宽高是 200*200,这就代表视窗范围是一个 200*200 的一个区域。我们通常会将viewport作为窗口,但这里的窗口又不等同于上述所说坐屏幕窗口,这就会产生逻辑坐标到物理坐标映射的问题。
我们以 openGL 为例,如下图所示:
中心点 (0,0) 必然会归一化到 (-1,1) 区域,裁减的区域会映射到 ViewPort 中,通过逻辑坐标系到物理坐标系的映射关系,在 Windows 窗口进行拉伸或变形,完成像素的映射。
上图中远端的小型区域便是裁减面积是所谓的视口(viewport),窗口客户区域也就是 Windows 窗口,我们通过将每个像素与窗口的物理坐标系进行匹配,便可得到当中的映射关系。
在 Spx 中经常会发生一件有意思的事情,比如启动一个 300*200 的窗口,将窗口拉大后当中的精灵也会等比例放大,背后的原因便是锁死了一个视口(viewport),固定了当中的坐标系。也就是说 Windows 窗口的调整不会影响整体坐标系。
另外就是视口(viewport)的映射方式。
如上图中的矩阵所示。首先有一个裁剪区域 Xw,代表左上角至右上角的一个区域,通过缩放实现从 ViewPort 到 Windows 的转化。从矩阵中我们可以看出首先平移至做小角,进行等比缩放,然后再平移回来。
关于坐标系我们进行一个简单的总结。
上图左侧为世界坐标体系,也是常说的逻辑坐标体系,是在视口(viewport)下的坐标体系。图中的红色为裁减窗口,也就是显示区域。在逻辑坐标体系中,会将显示区域归一化到视口体系,再映射到 Windows 的坐标体系。
整体的流程便是通过使用物体的坐标构建世界坐标场景。比如一个精灵「猴子」,位置是 20*20,这是一个物体的坐标,是给逻辑坐标系定的尺度。有了这个尺度后,需要将世界坐标体系转化到视口(viewport)体系中,归一化到一个区域中。
归一化的目的是为了更好的映射到设备坐标。归一化后的好处是,随意拖拽窗口,视口(viewport)坐标都不会变化。
二. Spx 坐标系
在坐标系概述中我们讲到了三个概念:Windows 窗口、裁剪区域和 viewport 视口,这三个概念组成了从逻辑坐标到物理坐标系的映射关系,也是绘制体系最基础的基石。在这个基础上,理解 Spx 会更加容易。
1. 概念
熟悉 Spx 的朋友对上面三个配置文件应该不陌生。
第一个为窗口坐标的配置文件,也是 Spx 的工程文件。逻辑是启动以诚程序,资源来自 res,窗口大小是 640*480,那也就代表游戏的画面尺寸为 640*480。
窗口中的世界长什么样,由第二张图来决定,也就是世界坐标的配置文件。其中,map 是所谓的世界地图,也就是世界的大小。当没有 map 时会选择场景的背景作为默认的世界大小。
对于这两个概念,我们可以设想成我们站在房间中,投过窗户看世界。窗户便是我们的窗口,世界便是绘制的场景。
在这个世界中会有精灵。有一个有意思的地方,精灵有一个相对于自己的坐标,叫做局部坐标。假设精灵自身的坐标为 (100,100),如果位置为 (50,50),那么就可以判定精灵的位置是居中的,也就是我们常说的旋转中心,在世界的位置坐标是(-100,-250)。
2. 窗口概念
在上图中,Spx 坐标体系是居中的,向上和向右为正方向的坐标体。假设我们指定了窗口大小,那么窗口的大小便是 Windows 指定的大小。当我们将窗口拉大,会发现窗口中的画布被等比例整体拉大,但世界大小并未发生变化,这意味着窗口变化不改变世界大小,不会出现将窗口拉大后,可以看到世界中更多区域的情况。
比如视窗窗口下,左上角为(-100,200),右下角是 (200,-100)。无论如何拉伸画布,坐标间的关系是固定不变的。窗口是视口的映射,是一个展示区域,跟坐标体系无关。
3. 坐标体
如上图所示,黄色部分的 windows 无论放大多少倍,都不会超过蓝色 world 的范围,会随之进行等比例的放大或缩小。但坐标体系因为归一化,所以不会发生变化,坐标原点保持在世界中间 (0,0) 的位置。
这个逻辑对应到代码中是如何实现的?
其实只有一行代码。当一个窗口变化时(从 windows 传入宽和高),返回的还是初始的窗口大小。如果这里不进行指定,背景大小便会等同于世界的大小,窗口便会等价于世界的大小,从而展示整个世界。
对于世界的定义,也是很重要的部分。这里经常会遇到一个让人困惑的地方,windows 和世界是如何对应的?
当我们没有指定世界大小时,会给世界设定一个默认值,大小则等同于背景的大小。这当中有两种情况:
windows 大于世界
当 windows 大于世界时,世界在 windows 中只显示于局部区域,其余地方显示为黑色,也就是不可见区域。
windows 小于世界
当 windows 小于世界时,我们无法通过一个窗口观察完整的世界,需要通过相机的方式移动窗口,进行探索。当指定 map 大小后,map 便是世界的大小。所以说世界的大小是固定不变的,通过改变窗口来改变世界的大小,是不可能的。
所以,当世界大小比窗口小时,便采用绘制策略,将其他透明区域填黑;当世界大小比窗口大时,便利用相机进行探索。
4. 世界概念
一个精灵是如何出现在世界上,首先要将精灵的局部坐标绘制到世界坐标体系下,然后再映射到 windows 窗口下。当显示区域中看不到精灵时,我们要借助相机去探索世界,移动可视区域寻找精灵。
5. 精灵坐标概念
精灵的中心点便是精灵的局部坐标。
"costumes": [ { "name": "calf-0", "path": "1.png", "x": 55, "y": 50 } ],
如上段代码所示,精灵的中心点在 (55,50),这个坐标指的是相对于精灵本身的坐标,而不是相对于世界的坐标。所以局部坐标的概念是对象所在的坐标空间,也就是对象本身的坐标空间。
举个例子,精灵的像素为 200*200,设置 x 为 100,y 为 100,那么精灵 (0,0) 的位置就会位于精灵的正中间。
下面我们来看精灵的世界坐标。
如上图配置,包含猴子和鳄鱼两个动物,猴子在世界的 (0,0) 位置。当没有设置局部坐标的位置时,(0,0) 位置会出现在猴子的左上角,但这不符合我们的预期,我们希望 (0,0) 位于猴子本身的正中间,所以我们需要先去修改它的局部坐标,修改中心点的坐标到猴子的中心点,也就是将世界的 (0,0) 和猴子的中心点保持一致,从而让猴子出现在世界正中间。
所以物体变换到的最终空间就是世界坐标系,世界坐标体系呈现的是精灵和精灵、物体和物体间的关系。
比如人的脑袋依附于我们的身体,它相对身体有一个坐标,而脑袋相对于世界而言还会有一个世界坐标。
这两个坐标之间转换,包含从世界坐标转换为窗口坐标和从窗口坐标转换到世界坐标。
将世界坐标转移到窗口中,可以通过矩阵,通过精灵点的信息乘以所在世界的矩阵信息,也就是平移和旋转,从而映射到窗口中。
其中最常见的是窗口转换为世界坐标,这是很重要的一部分内容。比如,想知道鼠标点击的精灵是什么?鼠标渠道的是屏幕的坐标系,当点击 (0,0) 位置时,首先通过屏幕转换到世界上,也就是逆矩阵。逆矩阵的概念,比如往左移动 20 步,那么它的逆矩阵就是往右移动 20 步。
三. Spx 绘制机制
在前面的内容中我们讲解了 Spx 的坐标体系,讲清楚了精灵在世界的什么位置、世界在窗口的什么位置,了解这个体系后,精灵、窗口、世界的信息就有了基础的了解,了解了房间的布局。
清楚房间布局后的第二步,就是如何通过坐标进行绘制。
1. 一般绘制机制
在讲具体绘制前,先和大家讲一下渲染的概念,也就是光栅的渲染。
上图为 openGL 中的渲染过程。图中最左侧的三角形、圆形、长方形等为图元,也可以认为是精灵。精灵由什么组成?网格通过光栅化的方式呈现到屏幕上。假设想呈现一个三角形,应该如何上色、添加效果?这就需要经过第二个阶段,使用着色器。
使用着色器,也就是选择使用什么样的方式将图源进行呈现。如果熟悉 Shader 语言就会比较容易理解这个概念。Shader 是一种着色器,解决了物体用什么样的姿势去展现。
我们的绘制其实就是模拟真实的世界。我举一个例子,假设世界中有一个房子,房子由钢筋、水泥等构造而成,那么钢筋和水泥就是我们的顶点信息,也就是三角面片的信息。
具备三角面片信息后,世界是不可见的,还需要有太阳光作为光源,墙面可能还要刷层白漆,光源和白漆组成一个着色器,进行绘制并呈现出来。
最终呈现在 camera,也就是我们的眼睛中。最终我们看到的可能是墙面的白漆和太阳光融合出的墙面。
2. Spx 绘制流程
在 Spx 中,绘制流程大致可简化为 5 步:
首先是一张 Image 的位图数据,比如一个精灵的 PNG 或者 JPG 图片,或者一张矢量图的数据(SVG 的绘制体系会有一点细微区别)。
加入我们将一个绘图数据防止在世界的 (0,0) 位置,并旋转 45°,就生成了一个矩阵数据,这个过程也就是几何运算 Geo。
第三步是对图片进行 UV 化,也就是三角面片化,绘制出四个顶点,两个三角面片。之所以需要三角面片,是为了解决性能的问题。假设一个物体有两个面或者四个面,面越多就代表模型越精细,所以面片在一定程度上可以简化运算。
当每一个像素都需要进行着色时,如果顶点信息太多,渲染能力是跟不上的,这是会进行 UV 化,添加 Shader,也就是增加一些简单的特效,比如透明度等,从而绘制出来。
绘制其实是一种管道渲染,上述便是绘制的一个管道。但在场景中,我们应该怎样去绘制场景树?
整个世界是有组织的进行由上而下的划分,比如宇宙中有地球、地球中有中国、中国中有杭州北京等等,按照层级逐级进行划分。如果地球相对于宇宙在运动,那么地球上的人相对于宇宙也是在运动的。这种组合间具有连带的机制。
上图左侧即为一棵渲染树。windows 中有一个刷新的绘制机制,通过一个时间片不断的将渲染树中的数据绘制到屏幕中,逐帧、无状态、不间断的绘制。
在 Spx 1.0 版本中,绘制树一般在世界中包含很多精灵,如下图所示。
精灵分布于世界不同的位置,从而绘制出对应的世界与场景树,这个世界就是前面提到的 map。
当我们想知道精灵的位置时,需要先通过世界去寻找,确定坐标系,否则就无法知道精灵的具体位置。比如“我在杭州”,当我们设定坐标系为中国,就可以确定出杭州是在中国的某个地方,这里的坐标系有一个参考系的属性。
如上图所示,精灵下面还连带着其他的精灵,这是组合精灵的功能,预计会在 Spx 1.1 版本中正式完成实现。之所以需要这个功能,我们还是通过一个例子来说明。
对于一个人而言,我们可以把手臂当成一个精灵,把头、脚、身体当成精灵,但当我们把这些精灵组合在一起时可以生成另一个精灵,也就是「人」,这就是组合精灵的概念。
前面我们讲的是精灵的概念。那么世界地图又该如何进行绘制?当地图比窗口场景图大时,地图就会呈现不同的模式:
重复贴图
比如一个 100*100 的草坪图片,在 800*600 的地图中可以通过重复贴图来实现画面的填充。
比例填充
比例填充的意思是窗口不出边界。当图片比世界小时,通过等比例放大到世界中,其他地方通过黑色区域进行填充。
填充裁减
裁减填充是比例填充的另一种形态。通过等比例放大铺满屏幕后,将多余的地方裁减掉。
精灵的绘制相对世界绘制而言,较为复杂:
上图为精灵绘制的一小段代码。在精灵绘制前,需要先进行几个事情。
精灵的中心点,也就是旋转中心。第 14 行有个平移中心点(centerX,centerY),设置中心点后,会把整个对世界的变化平移到中心点,告诉世界所有的变化是从中心点开始的,而不是从 (0,0) 开始。
比如将精灵放大两倍,是从中心点向外放大两倍;旋转 90°,也是从中心点开始进行旋转。所以,中心点就是整个图形变换的尺度,变化的一个规则。
缩放旋转后,我们可能还需要将精灵移动到世界的某一个位置上,从而完成精灵在世界上位置的绘制。
绘制完成后,还需要考虑是以怎样的一种形态来进行绘制,也就是前面讲到的着色器,用 Geo 和网格来呈现整个精灵。
具体的着色过程,其实就是 Shader 语言。我们来做一下详细的讲解。
上图中代码的第 69 行,是 shader 的入口,它传递了重要的两个信息,精灵的位置和贴图的位置,我们可以通过这个信息取得图片的尺寸与大小。
第 71 行到 86 行,解决的是抗锯齿的问题。因为 Spx 绘制是 UV 化的,也就是三角面片化,因此通过某一个点拿到的像素往往是带小数点的,所以要对小数点进行周围四个点的采样。
第 79 行到 82 行,便是左上、右上、左下、右下四个点。对这四个点进行采样,乘上位置信息得到其应该的像素信息,如第 86 行所示。如果对应的像素信息是透明的,那么直接全部返回 0。
第 91 行中的 Color 就是颜色特效。在新版的 Spx 中,增加了颜色的特效功能,可以实现变红、变黄等颜色的变化。比如一个人大怪物打到热血沸腾时,脸部可以通过颜色特效变成红色,或者身体变成红色体现血气方刚的感觉。
跳过中间的算法,直接看第 105 行。将着色器计算出的颜色给到面片,从而得到面片的颜色信息。
通过 Spx 的 5 步绘制流程,我们可以将精灵在世界中绘制出来。当世界和精灵都绘制好后,需要通过相机来进行观察。这里有种特殊情况,当世界和窗口一样大时,我们似乎不需要借助相机,但这个世界中依然存在相机,只是相机的窗口和世界一样大。
所以无论哪种情况,都离不开相机视窗的绘制,下图即为相机视窗绘制的一段代码。
第 61 行到 66 行,主要是完成将相机约束在世界中;第 70 行到 71 行,是用来适配坐标体系;第 73 行到 74 行,是在进行平移操作,让相机的中心平移到世界中心,让相机的坐标体系依附在世界中心的最中间。
第 76 行到 79 行,是通过移动相机中心,进行缩放、旋转相机等操作,最后再平移回相机中心,完成全部相机的绘制。