如何正确混合两个三角形的颜色并去除对角线涂抹

How to properly blend colors across two triangles and remove diagonal smear

我正在学习 WebGL,我已经绘制了一个全屏四边形,每个顶点都有颜色。没有照明或法线或透视矩阵或深度缓冲区;我只是在绘制渐变背景。这是我得到的:

看起来不错,但我不禁注意到从右下角到左上角的对角线涂抹。我觉得这是对远相反顶点进行线性插值的产物。我正在画两个三角形:左下角和右上角。我想我会使用 OpenGL 而不是 WebGL 得到类似的结果。

给定相同的四种颜色和相同大小的矩形,有没有办法渲染它以使两个三角形之间的边缘不那么明显?也许更多的顶点,或者不同的混合函数?我不确定每个像素的确切颜色应该是什么;我只想知道如何去除对角线拖影。

问题是右上角的三角形不知道左下角,所以右上角的三角形不包括左下角的任何蓝色(反之亦然)

有几种方法可以解决这个问题。

一种是使用带有线性采样的 2x2 纹理。您必须做一些额外的数学运算才能使插值正确,因为纹理仅在像素之间插值

+-------+-------+
|       |       |
|   +-------+   |
|   |   |   |   |
+---|---+---|---+
|   |   |   |   |
|   +-------+   |
|       |       |
+-------+-------+

上面是一个 4 像素的纹理,由 6 倍拉伸到 14 倍。采样发生在像素之间,因此只有这个中心区域会获得渐变。在该区域之外将使用纹理外部的像素进行采样,因此使用 CLAMP_TO_EDGE 或在纹理的另一侧使用 REPEAT.

const gl = document.querySelector('canvas').getContext('webgl');

const tl = [254, 217, 138];
const tr = [252, 252, 252];
const bl = [18, 139, 184];
const br = [203, 79, 121];

const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.texImage2D(
    gl.TEXTURE_2D,
    0, // mip level
    gl.RGB,  // internal format
    2,  // width,
    2,  // height,
    0,  // border
    gl.RGB, // format
    gl.UNSIGNED_BYTE, // type
    new Uint8Array([...bl, ...br, ...tl, ...tr]));
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.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
  gl_Position = position;
  v_texcoord = texcoord;
}
`;

const fs = `
precision mediump float;
varying vec2 v_texcoord;
const vec2 texSize = vec2(2, 2);  // could pass this in
uniform sampler2D tex;
void main() {
  gl_FragColor = texture2D(tex, 
     (v_texcoord * (texSize - 1.0) + 0.5) / texSize);
}
`;

const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const texcoordLoc = gl.getAttribLocation(program, 'texcoord');

function createBufferAndSetupAttribute(loc, data) {
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(loc);
  gl.vertexAttribPointer(
      loc,
      2,  // 2 elements per iteration
      gl.FLOAT,  // type of data in buffer
      false,  // normalize
      0,  // stride
      0,  // offset
  );
}

createBufferAndSetupAttribute(positionLoc, [
  -1, -1,
   1, -1,
  -1,  1,
  -1,  1,
   1, -1,
   1,  1,
]);
createBufferAndSetupAttribute(texcoordLoc, [
   0,  0,
   1,  0,
   0,  1,
   0,  1,
   1,  0,
   1,  1,
]);

gl.useProgram(program);
// note: no need to set sampler uniform as it defaults
// to 0 which is what we'd set it to anyway.
gl.drawArrays(gl.TRIANGLES, 0, 6);
canvas { border: 1px solid black; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>

注意:要了解我的意思,这里的纹理坐标所需的额外数学是同一个例子,没有额外的数学

const gl = document.querySelector('canvas').getContext('webgl');

const tl = [254, 217, 138];
const tr = [252, 252, 252];
const bl = [18, 139, 184];
const br = [203, 79, 121];

const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.texImage2D(
    gl.TEXTURE_2D,
    0, // mip level
    gl.RGB,  // internal format
    2,  // width,
    2,  // height,
    0,  // border
    gl.RGB, // format
    gl.UNSIGNED_BYTE, // type
    new Uint8Array([...bl, ...br, ...tl, ...tr]));
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.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
  gl_Position = position;
  v_texcoord = texcoord;
}
`;

const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
  gl_FragColor = texture2D(tex, v_texcoord);
}
`;

const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const texcoordLoc = gl.getAttribLocation(program, 'texcoord');

function createBufferAndSetupAttribute(loc, data) {
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(loc);
  gl.vertexAttribPointer(
      loc,
      2,  // 2 elements per iteration
      gl.FLOAT,  // type of data in buffer
      false,  // normalize
      0,  // stride
      0,  // offset
  );
}

createBufferAndSetupAttribute(positionLoc, [
  -1, -1,
   1, -1,
  -1,  1,
  -1,  1,
   1, -1,
   1,  1,
]);
createBufferAndSetupAttribute(texcoordLoc, [
   0,  0,
   1,  0,
   0,  1,
   0,  1,
   1,  0,
   1,  1,
]);

gl.useProgram(program);
// note: no need to set sampler uniform as it defaults
// to 0 which is what we'd set it to anyway.
gl.drawArrays(gl.TRIANGLES, 0, 6);
canvas { border: 1px solid black; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>

当然,我们可以在 JavaScript

中固定纹理坐标,而不是在片段着色器中进行数学计算

const gl = document.querySelector('canvas').getContext('webgl');

const tl = [254, 217, 138];
const tr = [252, 252, 252];
const bl = [18, 139, 184];
const br = [203, 79, 121];

const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.texImage2D(
    gl.TEXTURE_2D,
    0, // mip level
    gl.RGB,  // internal format
    2,  // width,
    2,  // height,
    0,  // border
    gl.RGB, // format
    gl.UNSIGNED_BYTE, // type
    new Uint8Array([...bl, ...br, ...tl, ...tr]));
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.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
  gl_Position = position;
  v_texcoord = texcoord;
}
`;

const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
  gl_FragColor = texture2D(tex, v_texcoord);
}
`;

const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const texcoordLoc = gl.getAttribLocation(program, 'texcoord');

function createBufferAndSetupAttribute(loc, data) {
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(loc);
  gl.vertexAttribPointer(
      loc,
      2,  // 2 elements per iteration
      gl.FLOAT,  // type of data in buffer
      false,  // normalize
      0,  // stride
      0,  // offset
  );
}

createBufferAndSetupAttribute(positionLoc, [
  -1, -1,
   1, -1,
  -1,  1,
  -1,  1,
   1, -1,
   1,  1,
]);
createBufferAndSetupAttribute(texcoordLoc, [
   0.25,  0.25,
   0.75,  0.25,
   0.25,  0.75,
   0.25,  0.75,
   0.75,  0.25,
   0.75,  0.75,
]);

gl.useProgram(program);
// note: no need to set sampler uniform as it defaults
// to 0 which is what we'd set it to anyway.
gl.drawArrays(gl.TRIANGLES, 0, 6);
canvas { border: 1px solid black; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>

另一种方法是根据这些角自己进行插值(这实际上是在做纹理采样器在前面的示例中所做的事情,即 4 种颜色的双线性插值)。

const gl = document.querySelector('canvas').getContext('webgl');

const tl = [254/255, 217/255, 138/255];
const tr = [252/255, 252/255, 252/255];
const bl = [ 18/255, 139/255, 184/255];
const br = [203/255,  79/255, 121/255];

const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
  gl_Position = position;
  v_texcoord = texcoord;
}
`;

const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform vec3 tl;
uniform vec3 tr;
uniform vec3 bl;
uniform vec3 br;

void main() {
  vec3 l = mix(bl, tl, v_texcoord.t);
  vec3 r = mix(br, tr, v_texcoord.t);
  vec3 c = mix(l, r, v_texcoord.s);
  gl_FragColor = vec4(c, 1);
}
`;

const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const texcoordLoc = gl.getAttribLocation(program, 'texcoord');

const tlLoc = gl.getUniformLocation(program, 'tl');
const trLoc = gl.getUniformLocation(program, 'tr');
const blLoc = gl.getUniformLocation(program, 'bl');
const brLoc = gl.getUniformLocation(program, 'br');

function createBufferAndSetupAttribute(loc, data) {
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(loc);
  gl.vertexAttribPointer(
      loc,
      2,  // 2 elements per iteration
      gl.FLOAT,  // type of data in buffer
      false,  // normalize
      0,  // stride
      0,  // offset
  );
}

createBufferAndSetupAttribute(positionLoc, [
  -1, -1,
   1, -1,
  -1,  1,
  -1,  1,
   1, -1,
   1,  1,
]);
createBufferAndSetupAttribute(texcoordLoc, [
   0,  0,
   1,  0,
   0,  1,
   0,  1,
   1,  0,
   1,  1,
]);

gl.useProgram(program);
gl.uniform3fv(tlLoc, tl);
gl.uniform3fv(trLoc, tr);
gl.uniform3fv(blLoc, bl);
gl.uniform3fv(brLoc, br);
gl.drawArrays(gl.TRIANGLES, 0, 6);
canvas { border: 1px solid black; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>

维数

您应该将四边形的二维坐标 space 传递给片段着色器而不是一维(每个通道)颜色 space。

然后在片段着色器中,您可以在 2D 中进行颜色插值 space 消除由于在 1D 中对角线插值而导致的颜色伪影。

线性颜色插值的着色器片段,其中coord2D是二维坐标space

pixel = vec4(vec3(mix(
        mix(colors[0], colors[1], coord2D.x),
        mix(colors[2], colors[3], coord2D.x),
        coord2D.y
    )), 1);

改进的颜色插值

通过 RGB 值对颜色进行插值时,结果在相反的色调之间可能会在视觉上变暗。

一个简单的解决方法是通过在颜色通道值的平方之间进行插值来使用更接近 sRGB 颜色模型的近似值。最终输出是插值的平方根。

插值片段。

pixel = vec4(sqrt(vec3(mix(
        mix(colors[0], colors[1], coord2D.x),
        mix(colors[2], colors[3], coord2D.x),
        coord2D.y
    ))) / 255.0, 1);

请注意,统一 colors 中的颜色通道值是对数 space。 [R^2, G^2, B^2] 因此范围从 0 到 65025。

例子

在示例中单击 canvas 可在插值方法之间切换。

您会注意到,当使用大约 ~sRGB 时,canvas 中心的亮度朝向中心边缘的亮度不会低于角落处可感知的亮度。

另请注意,从底部的蓝色和红色到顶部的橙色和白色的过渡平衡向中心移动。这是因为插值 RGB 模型会使具有来自 2 个或更多通道的强分量的颜色变暗,红色、绿色、蓝色和黑色将主导黄色、青色、品红色和白色,使得插值似乎移动并拉伸了 RGB 原色。

    var program, colorsLoc, modelLoc, loc, text = " interpolation. Click for ", model = "RGB"; // or sRGB
const vertSrc = `#version 300 es
    in vec2 verts;
    out vec2 coord2D;
    void main() { 
        coord2D = verts * 0.5 + 0.5; // convert to quad space 0,0 <=> 1, 1
        gl_Position = vec4(verts, 1, 1); 
    }`;
const fragSrc = `#version 300 es
    #define channelMax 255.0
    // color location indexes 
    #define TR 3
    #define TL 2
    #define BR 1
    #define BL 0
    precision mediump float;
    uniform vec3 colors[4];
    uniform bool isRGB;
    in vec2 coord2D;
    out vec4 pixel;
    void main() {
        if (isRGB) {
            pixel = vec4(vec3(mix(
                    mix(colors[BL], colors[BR], coord2D.x),
                    mix(colors[TL], colors[TR], coord2D.x),
                    coord2D.y
                )) / channelMax, 1);
         } else {
            pixel = vec4(vec3(sqrt(mix(
                    mix(colors[BL], colors[BR], coord2D.x),
                    mix(colors[TL], colors[TR], coord2D.x),
                    coord2D.y
                ))) / channelMax, 1);
         }
    }`; 
const fArr = arr => new Float32Array(arr);
const colors = [64,140,190, 224,81,141, 247,223,140, 245,245,245];
const gl = canvas.getContext("webgl2", {premultipliedAlpha: false, antialias: false, alpha: false});
addEventListener("resize", draw);
addEventListener("click", draw);
setup();
draw();
function compileShader(src, type, shader = gl.createShader(type)) {
    gl.shaderSource(shader, src);
    gl.compileShader(shader);
    return shader;
}
function setup() {
    program = gl.createProgram();
    gl.attachShader(program, compileShader(vertSrc, gl.VERTEX_SHADER));
    gl.attachShader(program, compileShader(fragSrc, gl.FRAGMENT_SHADER));
    gl.linkProgram(program);   
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array([0,1,2,0,2,3]), gl.STATIC_DRAW);  
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER, fArr([-1,-1,1,-1,1,1,-1,1]), gl.STATIC_DRAW);   
    gl.enableVertexAttribArray(loc = gl.getAttribLocation(program, "verts"));
    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);      
    colorsLoc = gl.getUniformLocation(program, "colors");       
    modelLoc = gl.getUniformLocation(program, "isRGB");    
    gl.useProgram(program);
}
function draw() {
    [info.textContent, model] = model != "RGB"? [`RGB${text}~sRGB.`, "RGB"]: [`~sRGB${text}RGB.`, "~sRGB"];
    if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
        [canvas.width, canvas.height] = [innerWidth, innerHeight];
        gl.viewport(0, 0, canvas.width, canvas.height);
    }
    gl.uniform3fv(colorsLoc, fArr(colors.map(v => model=="RGB"? v: v*v)), 0, 12);         
    gl.uniform1i(modelLoc, model=="RGB");   
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);                         
}
body {
    padding: 0px;
    margin: 0px;
    font-family: arial;
    color: white;
}
canvas {
    position: absolute;
    top: 0px;
    left: 0px;
}
h2 {
    position: absolute;
    bottom: 0px;
    left: 0px;
    right: 0px;
    text-align: center;

}
<canvas id="canvas"></canvas>
<h2 id="info"></h2>