Edge/outline 从片段着色器中的纹理检测
Edge/outline detection from texture in fragment shader
我正在尝试在 WebGL 中显示纹理的清晰轮廓。
我将纹理传递给我的片段着色器,然后我使用局部导数来显示 contours/outline,但是,它并不像我期望的那样平滑。
只打印纹理而不进行处理按预期工作:
vec2 texc = vec2(((vProjectedCoords.x / vProjectedCoords.w) + 1.0 ) / 2.0,
((vProjectedCoords.y / vProjectedCoords.w) + 1.0 ) / 2.0 );
vec4 color = texture2D(uTextureFilled, texc);
gl_FragColor = color;
对于局部导数,它会遗漏一些边:
vec2 texc = vec2(((vProjectedCoords.x / vProjectedCoords.w) + 1.0 ) / 2.0,
((vProjectedCoords.y / vProjectedCoords.w) + 1.0 ) / 2.0 );
vec4 color = texture2D(uTextureFilled, texc);
float maxColor = length(color.rgb);
gl_FragColor.r = abs(dFdx(maxColor));
gl_FragColor.g = abs(dFdy(maxColor));
gl_FragColor.a = 1.;
理论上,你的代码是正确的。
但实际上大多数 GPU 都是在 2x2 像素块上计算导数。
因此,对于此类块的所有 4 个像素,dFdX 和 dFdY 值将相同。
(详细解释here)
这会导致某种混叠,并且您会随机错过形状轮廓的一些像素(当从黑色到形状颜色的过渡发生在 2x2 块的边界时,就会发生这种情况)。
要解决这个问题,并获得真正的每像素导数,您可以自己计算,这看起来像这样:
// get tex coordinates
vec2 texc = vec2(((vProjectedCoords.x / vProjectedCoords.w) + 1.0 ) / 2.0,
((vProjectedCoords.y / vProjectedCoords.w) + 1.0 ) / 2.0 );
// compute the U & V step needed to read neighbor pixels
// for that you need to pass the texture dimensions to the shader,
// so let's say those are texWidth and texHeight
float step_u = 1.0 / texWidth;
float step_v = 1.0 / texHeight;
// read current pixel
vec4 centerPixel = texture2D(uTextureFilled, texc);
// read nearest right pixel & nearest bottom pixel
vec4 rightPixel = texture2D(uTextureFilled, texc + vec2(step_u, 0.0));
vec4 bottomPixel = texture2D(uTextureFilled, texc + vec2(0.0, step_v));
// now manually compute the derivatives
float _dFdX = length(rightPixel - centerPixel) / step_u;
float _dFdY = length(bottomPixel - centerPixel) / step_v;
// display
gl_FragColor.r = _dFdX;
gl_FragColor.g = _dFdY;
gl_FragColor.a = 1.;
一些重要的事情:
- 纹理不应使用 mipmaps
- 纹理最小和放大过滤应设置为 GL_NEAREST
- 纹理夹紧模式应设置为夹紧(不重复)
这是一个 ShaderToy 示例,演示了这一点:
我正在尝试在 WebGL 中显示纹理的清晰轮廓。 我将纹理传递给我的片段着色器,然后我使用局部导数来显示 contours/outline,但是,它并不像我期望的那样平滑。
只打印纹理而不进行处理按预期工作:
vec2 texc = vec2(((vProjectedCoords.x / vProjectedCoords.w) + 1.0 ) / 2.0,
((vProjectedCoords.y / vProjectedCoords.w) + 1.0 ) / 2.0 );
vec4 color = texture2D(uTextureFilled, texc);
gl_FragColor = color;
对于局部导数,它会遗漏一些边:
vec2 texc = vec2(((vProjectedCoords.x / vProjectedCoords.w) + 1.0 ) / 2.0,
((vProjectedCoords.y / vProjectedCoords.w) + 1.0 ) / 2.0 );
vec4 color = texture2D(uTextureFilled, texc);
float maxColor = length(color.rgb);
gl_FragColor.r = abs(dFdx(maxColor));
gl_FragColor.g = abs(dFdy(maxColor));
gl_FragColor.a = 1.;
理论上,你的代码是正确的。
但实际上大多数 GPU 都是在 2x2 像素块上计算导数。 因此,对于此类块的所有 4 个像素,dFdX 和 dFdY 值将相同。 (详细解释here)
这会导致某种混叠,并且您会随机错过形状轮廓的一些像素(当从黑色到形状颜色的过渡发生在 2x2 块的边界时,就会发生这种情况)。
要解决这个问题,并获得真正的每像素导数,您可以自己计算,这看起来像这样:
// get tex coordinates
vec2 texc = vec2(((vProjectedCoords.x / vProjectedCoords.w) + 1.0 ) / 2.0,
((vProjectedCoords.y / vProjectedCoords.w) + 1.0 ) / 2.0 );
// compute the U & V step needed to read neighbor pixels
// for that you need to pass the texture dimensions to the shader,
// so let's say those are texWidth and texHeight
float step_u = 1.0 / texWidth;
float step_v = 1.0 / texHeight;
// read current pixel
vec4 centerPixel = texture2D(uTextureFilled, texc);
// read nearest right pixel & nearest bottom pixel
vec4 rightPixel = texture2D(uTextureFilled, texc + vec2(step_u, 0.0));
vec4 bottomPixel = texture2D(uTextureFilled, texc + vec2(0.0, step_v));
// now manually compute the derivatives
float _dFdX = length(rightPixel - centerPixel) / step_u;
float _dFdY = length(bottomPixel - centerPixel) / step_v;
// display
gl_FragColor.r = _dFdX;
gl_FragColor.g = _dFdY;
gl_FragColor.a = 1.;
一些重要的事情:
- 纹理不应使用 mipmaps
- 纹理最小和放大过滤应设置为 GL_NEAREST
- 纹理夹紧模式应设置为夹紧(不重复)
这是一个 ShaderToy 示例,演示了这一点: