坐标变换 Coordinate Transformations
在计算机图形学中,坐标变换是渲染过程中不可或缺的一部分,它涉及一系列几何体从模型空间到最终屏幕空间的转换。以下是一般的坐标变换流程:
-
模型变换 (Model Transformation):
- 将物体从其本地坐标系(模型空间)转换至全局坐标系(世界空间)。这个过程可能包括平移、旋转和缩放操作,以放置模型在合适的世界位置并调整其大小和方向。
-
视图变换 (View Transformation):
- 也称为摄像机变换,将整个场景从世界空间转换至相机空间或眼睛空间。这通常涉及到将摄像机的位置反向移动(平移),然后根据摄像机的方向和上方向进行旋转,使摄像机位于原点,并且面向负Z轴方向。
-
投影变换 (Projection Transformation):
- 投影变换进一步将相机空间中的三维坐标转换为裁剪空间下的二维坐标。有两种主要类型的投影:
- 正交投影:保持平行线不变,常用于工程制图或二维游戏界面。
- 透视投影:模拟真实世界的视觉效果,随着距离增加,物体显得越来越小。这是大多数三维场景的标准做法。
- 投影变换进一步将相机空间中的三维坐标转换为裁剪空间下的二维坐标。有两种主要类型的投影:
-
裁剪变换 (Clipping Transformation):
- 在完成投影后,所有的顶点必须通过裁剪测试,确保它们位于视口的可见区域内(即裁剪体积内)。超出此区域的几何体部分会被剔除。
-
窗口变换 (Window Transformation):
- 裁剪后的坐标会进一步被映射到实际的显示设备(如屏幕)的二维坐标系统上,这个过程包括将标准化设备坐标(NDC,范围在[-1, 1]之间)转换成像素坐标,并考虑视口的位置和大小。
-
深度值转换与归一化:
- 在透视投影中,裁剪空间的Z值还需要转换成深度缓冲区能使用的深度值,通常进行正向归一化(0到1之间)或者特定范围内的深度值映射。
综上所述,这些连续的坐标变换共同确保了三维模型能够正确地呈现在二维屏幕上,并且每个步骤都有助于优化渲染性能以及实现各种视觉效果。
视口控制 Controlling the Viewport
为多个视口设置深度范围
void glDepthRangeArrayv( uint first, sizei count, const double *v );
void glDepthRangeIndexed( uint index, double n, double f );
void glDepthRange( double n, double f );
void glDepthRangef( float n, float f );
在计算机图形学和帧缓冲对象的上下文中,深度值可以使用定点表示或浮点表示来存储。
-
定点表示:
- 在定点格式的深度缓冲区中,深度值被表示为一个整数,其中有一部分固定的二进制位用于小数部分。例如,如果有一个m位的定点表示,那么可表示的深度范围是从0到(2^m - 1),这些数值会根据近裁剪面和远裁剪面进行缩放。每一步增量在这个范围内代表均匀的步长。二进制表示中所有位都用来编码该范围内的精度,也就是说,最高有效位(MSB)通常代表符号(如果是有符号数),其余位则代表数值大小。
-
浮点表示:
- 浮点深度缓冲区允许更大的动态范围以及深度值之间的更细腻梯度,特别是在处理近距离物体或者深度范围极大的场景时。这种表示遵循IEEE-754标准,每个数字包含符号位、指数位和尾数位。与定点表示不同的是,它可以以不同的精度来表示非常小和非常大的值。
“如果绘制帧缓冲区具有浮点深度缓冲,则必须使用浮点表示”这句话意味着,当你的帧缓冲配置支持使用浮点数来存储深度值时,你必须使用浮点数据写入那个缓冲区。否则,如果你将定点值写入浮点缓冲区,结果将是不准确或无意义的。
例如,如果帧缓冲区配置了32位浮点深度缓冲区(如GL_DEPTH_COMPONENT32F),那么深度值将以单精度浮点数形式编码。相反,如果缓冲区设置为定点格式,比如GL_DEPTH_COMPONENT24,那么深度值会被表示为整数,并没有显式的小数点,但隐含地基于近裁剪距离和远裁剪距离进行缩放。
void glDepthRangeArrayv(
uint first, // 指定要修改的第一个视口的索引
sizei count, // 指定要修改的连续视口的数量
const double *v // 指向一个包含深度范围数据的双精度浮点数数组的指针
);
在 v
指向的数组中,每个连续的子数组对(n, f)代表了对应视口的深度范围,其中 n 是近裁剪面在归一化设备坐标系下的值,f 是远裁剪面对应的值。这两个值都会被自动限制在 [0, 1] 的范围内。
例如,如果要为从索引为 first
到 first + count - 1
的视口分别设置不同的深度范围,你需要提供一个大小为 2 * count
的数组,其中每一对相邻元素代表一个视口的深度范围。通过这种方式,你可以灵活地为多视口配置不同的深度缓冲区行为,以满足不同场景的需求。
void glDepthRangeIndexed( uint index, double n, double f );
// 等价于
double v[] = { n, f };
glDepthRangeArrayv(index, 1, v);
- 为
index
视口配置深度范围。
void glDepthRange( double n, double f );
// 等价于
for (uint i = 0; i < MAX_VIEWPORTS; i++)
{
glDepthRangeIndexed(i, n, f);
}
- 为所有视口的深度范围设置为相同的值。
void glDepthRangef( float n, float f );
glDepthRange
的float版本。
设置视口数组(Viewport Array)
void glViewportArrayv( uint first, sizei count, const float *v );
void glViewportIndexedf( uint index, float x, float y, float w, float h );
void glViewportIndexedfv( uint index, const float *v );
void glViewport( int x, int y, sizei w, sizei h );
在OpenGL中,视口变换的实现依赖于一系列状态变量。每个视口有四个整数和两个钳制浮点值作为其参数:
-
视口的位置(ox, oy):ox = x + w/2,oy = y + h/2,其中(x, y)为视口左下角在窗口坐标系中的位置,w和h分别为视口的宽度和高度。
-
视口尺寸(px, py):px = w,py = h。
在初始状态下,若渲染到默认帧缓冲区且存在关联的显示窗口,则w和h会被设置为窗口的实际宽度和高度;若无关联的默认帧缓冲区,则初始化为零。
-
深度范围(n, f):n 初始化为 0.0,f 初始化为 1.0,分别代表深度缓冲区近裁剪面和远裁剪面在归一化设备坐标下的值。
视口的位置及尺寸会受到实施相关的边界限制,可以通过调用
GetFloatv
函数并传入VIEWPORT_BOUNDS_RANGE
来获取允许的范围。同时,视口的宽度和高度也会被钳制在实施所支持的最大尺寸内,可通过查询MAX_VIEWPORT_DIMS
得到。视口变换处理浮点边界的精度是与具体实现有关的,可以通过查询实现定义的常量
VIEWPORT_SUBPIXEL_BITS
来了解该精度。
void glViewportArrayv( uint first, sizei count, const float *v );
first
: 表示视口数组的起始索引。在支持视口数组的上下文中,可以同时定义多个连续的视口,并且通过这个参数指定要开始配置的第一个视口的位置。count
: 指定要设置的视口数量。它代表从first
开始连续的视口个数。v
: 指向一个浮点型数组的指针,该数组包含了一系列的视口参数。每个视口由四个连续的浮点数(x, y, width, height)
描述,其中(x, y)
定义了视口在窗口坐标系中的左下角位置,而width
和height
则定义了视口的大小。因此,数组应至少包含4 * count
个元素。
void glViewportIndexedf( uint index, float x, float y, float w, float h );
// 等价于
float v[4] = { x, y, w, h };
ViewportArrayv(index, 1, v);
- 设置
index
视口。
void glViewportIndexedfv( uint index, const float *v );
// 等价于
glViewportArrayv(index, 1, v);
- 设置
index
视口。
void glViewport( int x, int y, sizei w, sizei h );
// 等价于
for (uint i = 0; i < MAX_VIEWPORTS; i++)
{
glViewportIndexedf(i, 1, (float)x, (float)y, (float)w, (float)h);
}
- 为所有视口设置相同的值。