You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WebGL纹理坐标需添加0.5的原因及失效示例咨询

WebGL纹理坐标偏移0.5的疑问与验证

背景

在WebGL中,访问纹理像素常使用如下公式:

texture2D(oneTexture, vec2( (i + 0.5) / width , (j + 0.5 ) / height ) )  

其中i、j是代表纹理元素位置的整数。但实际测试发现,无论是否添加0.5偏移,代码都能正常运行,因此提出以下问题:

  1. 为何理论上要为纹理坐标添加0.5?
  2. 请提供一个不加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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 06:17:35