WebGL纹理坐标需添加0.5的原因及失效示例咨询
背景
在WebGL中,访问纹理像素常使用如下公式:
texture2D(oneTexture, vec2( (i + 0.5) / width , (j + 0.5 ) / height ) )
其中i、j是代表纹理元素位置的整数。但实际测试发现,无论是否添加0.5偏移,代码都能正常运行,因此提出以下问题:
- 为何理论上要为纹理坐标添加
0.5? - 请提供一个不加
0.5会失效的示例。
测试代码
JavaScript
"use strict"; let vertex=` void main() { gl_Position = vec4(0,0,0,1); gl_PointSize=1.0; }`; let fragment=` precision highp float; uniform sampler2D image; void main() { //WHY 0.5 ? offset=0 does work float OFFSET = 0.5 ; gl_FragColor = texture2D( image, vec2( (3.0 + OFFSET ) / 5.0, (2.0 + OFFSET ) / 3.0 // (3,2) is the coordinates of darkgoldenrod )); }`; let canvas = document.querySelector("#canvas"); canvas.width=3; canvas.height=3; let gl = canvas.getContext("webgl"); let program = gl.createProgram(); const vertexShader = gl.createShader(gl.VERTEX_SHADER); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(vertexShader, vertex); gl.shaderSource(fragmentShader, fragment); gl.compileShader(vertexShader); gl.compileShader(fragmentShader); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); gl.useProgram(program); let texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); const blueviolet = [ 138, 43, 226 ,255]; const brown = [ 165, 42, 42,255]; const burlywood = [ 222, 184, 135,255]; const cadetblue = [ 95 ,158 ,160,255]; const chartreuse = [ 127 ,255 ,0,255]; const chocolate = [ 210, 105, 30,255]; const coral = [ 255 ,127, 80,255]; const cornflowerblue = [ 100, 149, 237,255]; const cornsilk = [ 255, 248, 220,255]; const crimson = [ 220 ,20, 60,255]; const cyan = [ 0 ,255, 255,255]; const darkblue = [ 0, 0, 139,255]; const darkcyan = [ 0 ,139, 139,255]; const darkgoldenrod = [ 184, 134, 11,255]; const darkgray = [ 169 ,169, 169,255]; gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, 5, 3, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([ // ----- 0 ---------- 1 -------------- 2 ---------------- 3 ---------------- 4 -------- ...blueviolet, ...brown, ...burlywood, ...cadetblue, ...chartreuse , // 0 ...chocolate, ...coral , ...cornflowerblue, ...cornsilk, ...crimson, // 1 ...cyan, ...darkblue, ...darkcyan, ...darkgoldenrod, ...darkgray // 2 // coordinate of darkgoldenrod is (3,2) ]) ); gl.viewport(0, 0, gl.canvas.width,gl.canvas.height); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.drawArrays( gl.POINTS, 0, 1);
CSS
canvas { width:300px; height: 300px; background-color:green; image-rendering: pixelated; }
HTML
<canvas id="canvas"></canvas>
问题解答
1. 为何必须添加0.5偏移?
WebGL的纹理坐标是归一化坐标,范围是[0, 1],对应纹理的左上角到右下角。而纹理的每个像素(texel)是一个矩形区域:比如宽度为width的纹理,第i个像素在水平方向的覆盖范围是[i/width, (i+1)/width],它的中心点坐标是(i + 0.5)/width。
当使用NEAREST过滤(最近邻采样)时,只要采样坐标落在某个像素的范围内,就会返回该像素的颜色——所以直接用i/width(像素左边缘)也能命中目标像素,这就是你测试中不加0.5也能运行的原因。
但如果使用LINEAR过滤(线性插值采样),GPU会对采样点周围的4个像素做加权平均。此时如果采样点落在像素边缘(比如i/width),就会同时取到相邻两个像素的颜色,导致结果出现模糊或错误的混合色。添加0.5偏移是为了确保采样点落在像素的中心位置,无论使用哪种过滤方式,都能准确获取目标像素的颜色,避免插值干扰。
另外,纹理坐标的映射还和WebGL的纹理对齐、坐标空间定义有关——GPU的纹理采样默认是基于像素中心的,所以添加0.5是符合GPU采样逻辑的标准做法。
2. 不加0.5会失效的示例
修改测试代码中的两个关键部分:
- 将纹理过滤模式改为
LINEAR - 把
OFFSET设为0
修改后的片段着色器:
precision highp float; uniform sampler2D image; void main() { float OFFSET = 0.0; gl_FragColor = texture2D( image, vec2( (3.0 + OFFSET ) / 5.0, (2.0 + OFFSET ) / 3.0 )); }
修改纹理参数的代码:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
此时运行代码,你会发现原本应该显示darkgoldenrod(暗金色)的点,变成了darkcyan(暗青色)和darkgoldenrod的混合色,甚至可能偏向相邻的darkgray——因为采样点落在了(3,2)像素的左边缘,触发了线性插值,混合了左侧(2,2)的darkcyan像素颜色。
如果把OFFSET改回0.5,采样点回到像素中心,就能准确获取darkgoldenrod的颜色,不会出现混合。
内容的提问来源于stack exchange,提问作者oceanGermanique

