如何在 vertex/fragment 着色器(金属)中使用 3x3 2D 变换
How to use a 3x3 2D transformation in a vertex/fragment shader (Metal)
我有一个本应很简单的任务,但显然我仍然不明白投影在着色器中是如何工作的。我需要对纹理四边形(2 个三角形)进行 2D 透视变换,但在视觉上它看起来不正确(例如,梯形比 CPU 版本中的略高或拉伸得更多)。
我有这个结构:
struct VertexInOut
{
float4 position [[position]];
float3 warp0;
float3 warp1;
float3 warp2;
float3 warp3;
};
在顶点着色器中我做了类似的事情(texCoords
是四边形角的像素坐标,单应性是在像素坐标中计算的):
v.warp0 = texCoords[vid] * homographies[0];
然后在片段着色器中像这样:
return intensity.sample(s, inFrag.warp0.xy / inFrag.warp0.z);
结果不是我所期望的。我花了几个小时在这上面,但我无法弄清楚。 发泄
更新:
这些是 CPU 的代码和结果(又名 预期结果 ):
// _image contains the original image
cv::Matx33d h(1.03140473, 0.0778113901, 0.000169219566,
0.0342947133, 1.06025684, 0.000459250761,
-0.0364957005, -38.3375587, 0.818259298);
cv::Mat dest(_image.size(), CV_8UC4);
// h is transposed because OpenCV is col major and using backwarping because it is what is used on the GPU, so better for comparison
cv::warpPerspective(_image, dest, h.t(), _image.size(), cv::WARP_INVERSE_MAP | cv::INTER_LINEAR);
这些是 GPU 的代码和结果(又名错误结果):
// constants passed in buffers, image size 320x240
const simd::float4 quadVertices[4] =
{
{ -1.0f, -1.0f, 0.0f, 1.0f },
{ +1.0f, -1.0f, 0.0f, 1.0f },
{ -1.0f, +1.0f, 0.0f, 1.0f },
{ +1.0f, +1.0f, 0.0f, 1.0f },
};
const simd::float3 textureCoords[4] =
{
{ 0, IMAGE_HEIGHT, 1.0f },
{ IMAGE_WIDTH, IMAGE_HEIGHT, 1.0f },
{ 0, 0, 1.0f },
{ IMAGE_WIDTH, 0, 1.0f },
};
// vertex shader
vertex VertexInOut homographyVertex(uint vid [[ vertex_id ]],
constant float4 *positions [[ buffer(0) ]],
constant float3 *texCoords [[ buffer(1) ]],
constant simd::float3x3 *homographies [[ buffer(2) ]])
{
VertexInOut v;
v.position = positions[vid];
// example homography
simd::float3x3 h = {
{1.03140473, 0.0778113901, 0.000169219566},
{0.0342947133, 1.06025684, 0.000459250761},
{-0.0364957005, -38.3375587, 0.818259298}
};
v.warp = h * texCoords[vid];
return v;
}
// fragment shader
fragment int4 homographyFragment(VertexInOut inFrag [[stage_in]],
texture2d<uint, access::sample> intensity [[ texture(1) ]])
{
constexpr sampler s(coord::pixel, filter::linear, address::clamp_to_zero);
float4 targetIntensity = intensityRight.sample(s, inFrag.warp.xy / inFrag.warp.z);
return targetIntensity;
}
原图:
更新 2:
与应该在片段着色器中完成透视分割的普遍看法相反,如果我在顶点着色器中分割(并且没有失真或三角形之间的接缝),但为什么呢?
更新 3:
如果出现以下情况,我会得到相同(错误)的结果:
- 我将透视划分移动到片段着色器
- 我只是从代码中删除了除法器
很奇怪,好像没有发生分裂。
好的,解决方案当然是一个非常小的细节:simd::float3
的 除法 表现得非常疯狂。事实上,如果我像这样在片段着色器中进行透视划分:
float4 targetIntensity = intensityRight.sample(s, inFrag.warp.xy * (1.0 / inFrag.warp.z));
有效!
这让我发现乘以预除浮点数与除以浮点数不同。其原因至今我不得而知,如果有人知道为什么我们可以解开这个谜。
我有一个本应很简单的任务,但显然我仍然不明白投影在着色器中是如何工作的。我需要对纹理四边形(2 个三角形)进行 2D 透视变换,但在视觉上它看起来不正确(例如,梯形比 CPU 版本中的略高或拉伸得更多)。
我有这个结构:
struct VertexInOut
{
float4 position [[position]];
float3 warp0;
float3 warp1;
float3 warp2;
float3 warp3;
};
在顶点着色器中我做了类似的事情(texCoords
是四边形角的像素坐标,单应性是在像素坐标中计算的):
v.warp0 = texCoords[vid] * homographies[0];
然后在片段着色器中像这样:
return intensity.sample(s, inFrag.warp0.xy / inFrag.warp0.z);
结果不是我所期望的。我花了几个小时在这上面,但我无法弄清楚。 发泄
更新:
这些是 CPU 的代码和结果(又名 预期结果 ):
// _image contains the original image
cv::Matx33d h(1.03140473, 0.0778113901, 0.000169219566,
0.0342947133, 1.06025684, 0.000459250761,
-0.0364957005, -38.3375587, 0.818259298);
cv::Mat dest(_image.size(), CV_8UC4);
// h is transposed because OpenCV is col major and using backwarping because it is what is used on the GPU, so better for comparison
cv::warpPerspective(_image, dest, h.t(), _image.size(), cv::WARP_INVERSE_MAP | cv::INTER_LINEAR);
这些是 GPU 的代码和结果(又名错误结果):
// constants passed in buffers, image size 320x240
const simd::float4 quadVertices[4] =
{
{ -1.0f, -1.0f, 0.0f, 1.0f },
{ +1.0f, -1.0f, 0.0f, 1.0f },
{ -1.0f, +1.0f, 0.0f, 1.0f },
{ +1.0f, +1.0f, 0.0f, 1.0f },
};
const simd::float3 textureCoords[4] =
{
{ 0, IMAGE_HEIGHT, 1.0f },
{ IMAGE_WIDTH, IMAGE_HEIGHT, 1.0f },
{ 0, 0, 1.0f },
{ IMAGE_WIDTH, 0, 1.0f },
};
// vertex shader
vertex VertexInOut homographyVertex(uint vid [[ vertex_id ]],
constant float4 *positions [[ buffer(0) ]],
constant float3 *texCoords [[ buffer(1) ]],
constant simd::float3x3 *homographies [[ buffer(2) ]])
{
VertexInOut v;
v.position = positions[vid];
// example homography
simd::float3x3 h = {
{1.03140473, 0.0778113901, 0.000169219566},
{0.0342947133, 1.06025684, 0.000459250761},
{-0.0364957005, -38.3375587, 0.818259298}
};
v.warp = h * texCoords[vid];
return v;
}
// fragment shader
fragment int4 homographyFragment(VertexInOut inFrag [[stage_in]],
texture2d<uint, access::sample> intensity [[ texture(1) ]])
{
constexpr sampler s(coord::pixel, filter::linear, address::clamp_to_zero);
float4 targetIntensity = intensityRight.sample(s, inFrag.warp.xy / inFrag.warp.z);
return targetIntensity;
}
原图:
更新 2:
与应该在片段着色器中完成透视分割的普遍看法相反,如果我在顶点着色器中分割(并且没有失真或三角形之间的接缝),但为什么呢?
更新 3:
如果出现以下情况,我会得到相同(错误)的结果:
- 我将透视划分移动到片段着色器
- 我只是从代码中删除了除法器
很奇怪,好像没有发生分裂。
好的,解决方案当然是一个非常小的细节:simd::float3
的 除法 表现得非常疯狂。事实上,如果我像这样在片段着色器中进行透视划分:
float4 targetIntensity = intensityRight.sample(s, inFrag.warp.xy * (1.0 / inFrag.warp.z));
有效!
这让我发现乘以预除浮点数与除以浮点数不同。其原因至今我不得而知,如果有人知道为什么我们可以解开这个谜。