Mode7效果:世界坐标转屏幕空间的片段着色器逆向求解
逆向还原Mode7变换的思路
嘿,这个问题我之前做复古风格3D渲染的时候也踩过坑!Mode7这种变换本质是透视投影+仿射变换的组合,没法直接靠矩阵求逆一键搞定,得手动拆解正向流程,一步步倒推回去。我给你理清楚具体怎么操作:
第一步:先把你的正向变换拆得明明白白
逆变换的前提是你得完全清楚自己的片段着色器里,世界坐标是怎么一步步变成屏幕坐标的。举个经典Mode7地面渲染的例子,正向流程一般是这样的:
// 输入:世界坐标 (worldX, worldY) // 已知参数:旋转角angle,透视偏移offset,屏幕缩放/中心参数 float rotX = worldX * cos(angle) - worldY * sin(angle); float rotY = worldX * sin(angle) + worldY * cos(angle); // 透视投影计算屏幕坐标 float perspectiveDivisor = rotY + offset; // 防止除以0的偏移量 float screenX = (rotX / perspectiveDivisor) * screenScaleX + screenCenterX; float screenY = (rotY / perspectiveDivisor) * screenScaleY + screenCenterY;
不管你的实现细节有多不一样,先把每一步的数学公式列出来,这是逆变换的基础。
第二步:从屏幕坐标反向走每一步
知道了正向的每一步,我们就可以用逆运算一步步推回世界坐标:
1. 先把屏幕坐标还原成透视除法前的归一化值
正向最后一步是把透视除法后的结果缩放平移到屏幕空间,那逆向第一步就是把屏幕坐标转回去:
// 输入:屏幕坐标(x_screen, y_screen) float normX = (x_screen - screenCenterX) / screenScaleX; float normY = (y_screen - screenCenterY) / screenScaleY;
这时候normX就是rotX / perspectiveDivisor,normY就是rotY / perspectiveDivisor。
2. 求解透视除法的除数
回忆正向里perspectiveDivisor = rotY + offset,把rotY = normY * perspectiveDivisor代入这个式子,就能解出perspectiveDivisor:
perspectiveDivisor = (normY * perspectiveDivisor) + offset // 整理一下: perspectiveDivisor * (1 - normY) = offset perspectiveDivisor = offset / (1 - normY)
把这个写成GLSL代码,记得加个小偏移防止除以0:
float divisor = offset / (1.0 - normY + 0.0001); // +0.0001避免除数为0
3. 算出旋转后的坐标
有了除数,就能还原出旋转变换后的rotX和rotY:
float rotX = normX * divisor; float rotY = normY * divisor;
4. 逆向旋转变换得到世界坐标
旋转矩阵的逆矩阵就是它的转置,所以逆旋转的公式很容易推导:
正向旋转是:
rotX = worldX*cosθ - worldY*sinθ rotY = worldX*sinθ + worldY*cosθ
那逆旋转就是:
worldX = rotX*cosθ + rotY*sinθ worldY = -rotX*sinθ + rotY*cosθ
写成代码就是:
float cosAngle = cos(angle); float sinAngle = sin(angle); float worldX = rotX * cosAngle + rotY * sinAngle; float worldY = -rotX * sinAngle + rotY * cosAngle;
第三步:适配你的自定义变换
如果你的Mode7还有额外操作(比如先平移世界坐标、非均匀缩放、甚至自定义扭曲),只需要对应着逆向:
- 比如正向先做了
worldX -= originX,那逆向最后要加回去:worldX += originX - 如果正向给
rotX乘了自定义缩放scaleX,那逆向就要除以scaleX:rotX /= scaleX
完整的逆向代码示例
把上面的步骤整合起来,就是一个可直接用的GLSL片段:
// 输入:屏幕坐标(x_screen, y_screen) // 已知参数:angle, offset, screenScaleX, screenScaleY, screenCenterX, screenCenterY // 1. 还原归一化坐标 float normX = (x_screen - screenCenterX) / screenScaleX; float normY = (y_screen - screenCenterY) / screenScaleY; // 2. 计算透视除数 float divisor = offset / (1.0 - normY + 0.0001); // 3. 得到旋转后坐标 float rotX = normX * divisor; float rotY = normY * divisor; // 4. 逆旋转得到世界坐标 float cosA = cos(angle); float sinA = sin(angle); float worldX = rotX * cosA + rotY * sinA; float worldY = -rotX * sinA + rotY * cosA;
几个要注意的点
- 必须严格匹配正向流程:如果你的正向有不可逆操作(比如
clamp或者取整),那逆变换可能会有歧义,得针对性处理边界情况。 - 精度问题:如果渲染时出现抖动,可以给变量加上
highp精度限定符,提升计算精度。 - 除数保护:一定要给可能为0的除数加个极小的偏移,不然会出现NaN导致渲染错误。
内容的提问来源于stack exchange,提问作者mfl
相关产品推荐
相关产品推荐

