像素着色器与顶点着色器缩放差异:为何除法与乘法并存?
这问题问得非常好!咱们来拆解下为什么这两个着色器一个用除法、一个用乘法做缩放,结果却完全一致——核心在于它们的变换方向正好相反:一个是从屏幕像素反向推导纹理坐标,另一个是从纹理顶点正向映射到屏幕空间。
先明确两个着色器的核心工作逻辑:
- 像素着色器:逐像素处理,计算当前屏幕像素需要采样纹理的哪个坐标才能得到旋转缩放后的效果。如果计算出的纹理坐标超出0-1范围,就输出黑色。
- 顶点着色器:变换全屏四边形(或纹理四边形)的顶点位置,定义旋转缩放后的纹理在屏幕上的显示区域,然后依靠GPU的光栅化和纹理坐标插值来填充像素。
接下来咱们一步步看坐标变换的细节:
坐标变换的共同步骤
两个着色器一开始都会把输入坐标(像素着色器是纹理坐标,顶点着色器是顶点位置)转换为以图像中心为原点的绝对坐标空间:
ptOrigin.x = (ptOrigin.x - 0.5) * fWidthSrc; ptOrigin.y = (ptOrigin.y - 0.5) * fHeightSrc;
这一步把原点移到图像中心,坐标范围变成了-0.5*宽度到0.5*宽度(x轴),y轴同理。
然后两者都会用标准的旋转矩阵做旋转变换——这部分逻辑完全一致。
接下来就是关键的差异:缩放的方向,这完全取决于变换是正向还是反向:
像素着色器的缩放(除法)
旋转完成后,像素着色器会把坐标转回以中心为原点的归一化空间(范围-0.5到0.5),然后用除法处理fScale:
ptPos.x = ptPos.x / fScale + 0.5; ptPos.y = ptPos.y / fScale + 0.5;
用除法的原因很简单:像素着色器是在做反向推导——从当前屏幕像素的坐标,找到它对应的原始纹理坐标。要得到更小的输出图像(也就是你说的缩放效果),我们只需要采样原始纹理的中心一小块区域。当fScale<1时,除以它会扩大归一化坐标的范围,大部分屏幕像素计算出的纹理坐标会超出0-1范围(被裁剪为黑色),只有屏幕中心占比为fScale的区域能映射到有效的纹理坐标,最终显示出缩小后的旋转图像。
顶点着色器的缩放(乘法)
顶点着色器则是正向映射——从纹理/四边形的顶点坐标,变换到屏幕位置。旋转并转回归一化中心空间后,它用乘法处理fScale:
ptPos.x = ptPos.x * fScale + 0.5; ptPos.y = ptPos.y * fScale + 0.5;
当fScale<1时,乘以它会把原本-0.5到0.5的归一化坐标缩小到-0.5*fScale到0.5*fScale,加上0.5后,屏幕坐标范围就变成了0.5-0.5*fScale到0.5+0.5*fScale——也就是屏幕中心一个占比为fScale的正方形区域。GPU光栅化这个正方形时,会把原始纹理坐标插值到整个正方形内,这和像素着色器计算出的有效纹理坐标完全一致。正方形外的像素不会被绘制(自然显示为黑色,和像素着色器的裁剪效果一样)。
为什么两者效果等价
从数学上看,这两种方式是互逆操作,所以结果完全一致:
- 对于中心有效区域内的任意像素,像素着色器计算出的采样坐标,和顶点着色器方案中插值得到的纹理坐标完全相同。
- 区域外的像素要么被像素着色器裁剪为黑色,要么被顶点着色器跳过绘制,效果一致。
不管fScale是0.77还是0.1,这种等价性都成立——因为缩放fScale的逆操作就是缩放1/fScale,而除以fScale正好就是乘以1/fScale,两者在数学上完全等价。
内容的提问来源于stack exchange,提问作者Olga Pshenichnikova

