Scissoring By API
在OpenGL或者D3D中,在fragment被写入frame buffer之前,会进行per fragment的操作,OpenGL中的操作顺序如下:
- Scissor Test
- Alpha Test
- Stecil Test
- Depth Test
- Blend
- Dither
- Logic operation
要进行Scissor Test,我们可以
glEnable(GL_SCISSOR_TEST);
glScissor(left, bottom, with, height);
默认情况下Scissor Test是禁用的,并且Scissor的大小是整个window。
在D3D中,我们可以
g_pD3Device->SetRenderState(D3DRS_SCISSORTESTENABLE, TRUE);
g_pD3Device->SetScissorRect(&rect);
Scissoring By Projection Matrix
我们也可以通过修改projection matrix,来达到相同的目的,当引擎不提供相关的api来直接设置scissor rect的时候,比如unity3d。
注意到,要想在屏幕的某个区域只渲染world中的一部分,那么我们首先应该修改viewport,使得和scissor rect一致。又因为我们没有办法只渲染NDC中一部分,我们唯一的选择就是将我们需要渲染的那一部分,缩放/平移,使得那部分占满整个NDC区域,这样,就达到了只渲染scissor rect的目的。
其中蓝色的矩形(下面简称NDC')是我们需要实际显示的部分,白色矩形就是原始的NDC坐标系。我们只需要将NDC坐标系中坐标转换到NDC'中即可,这样在NDC'之外的vertex就会被clipping,最后达到scissor的目的。
注意到,在坐标进行投影之后,实际上是在齐次坐标空间,但是因为在透视除后的欧式空间中变换和在齐次坐标空间中变换不影响最终的结果,所以我们选择在NDC中进行,否则我们需要知道齐次坐标的w分量,因为
-w <= x <= w
-w <= y <= w
对于齐次坐标[x y z w],对应的欧氏空间的坐标是[x/w y/w z/w]。
假设Scissor Test以Normalized Screen Coordinate指定,为(l, t, w, h)。NDC中的坐标为[x y z],考虑到变换只影响x, y,所以以后的讨论不涉及z的变换。
在Normalized screen space中,对于已知的一个点P,有以下等式成立:
其中坐标系[O, u0, u1] 是原始NDC在Normalized Screen Space中的表示,坐标系[O', v0, v1]是变换后的NDC在Normalized Screen Space中的表示。我们已知[x y],进而可以得到,
我们只需要求出[O, u0, u1]在[O', v0, v1]中的坐标即可得到变换矩阵。
有
O = (1/2, 1/2)
u0 = (1/2, 0)
u1 = (0, -1/2)
O' = (l+w/2, t+h/2)
v0 = (w/2, 0)
v1 = (0, -h/2)
所以我们可以得到
m00 = 1/w
m01 = 0
m10 = 0
m11 = 1/h
m20 = (1-2l-w)/w
m21 = -(1-2t-h)/h
所以最终我们将原始的投影矩阵,再乘以如下的变换矩阵,即可达到Scissor的目的
在Unity3D中,我们可以
// Set the viewport to the scissor rect
Camera.main.rect = rcScissor;
// Transform so that the scissor rect occupies the NDC
Matrix4x4 mat = new Matrix4x4();
mat.SetColumn(0, new Vector4(1 / rcScissor.width, 0, 0, 0));
mat.SetColumn(1, new Vector4(0, 1 / rcScissor.height, 0, 0));
mat.SetColumn(2, new Vector4(0, 0, 1, 0));
float tx = (1.0f - (2 * rcScissor.x + rcScissor.width)) / rcScissor.width;
float ty = (-1.0f + (2 * rcScissor.y + rcScissor.height)) / rcScissor.height;
mat.SetColumn(3, new Vector4(tx, ty, 0, 1));