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

纵饮孤独 提交于 2020-05-09 06:25:08

问题


I am learning WebGL and I've drawn a full screen quad with colors for each vertex. No lighting or normals or perspective matrix or depth buffer; I'm just drawing a gradient background. This is what I get:

It looks good but I cannot help noticing the diagonal smear from the bottom right to the top left. I feel this is an artifact of linear interpolating the far opposite vertices. I'm drawing two triangles: the bottom left, and the top right. I think I would get similar results using OpenGL instead of WebGL.

Given the same four colors and the same size rectangle, is there a way to render this so the edge between the two triangles isn't so apparent? Maybe more vertices, or a different blending function? I'm not sure exactly what the colors should be at each pixel; I just want to know how to get rid of the diagonal smear.


回答1:


The issue is the top right triangle has no knowledge of the bottom left corner so the top right triangle is not including any of the blue from the bottom left (and visa versa)

A couple of ways to fix that.

One is to use a 2x2 texture with linear sampling. You have to do some extra math to get the interpolation correct because a texture only interpolates between pixels

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

Above is a 4 pixel texture stretched to 14 by 6. Sampling happens between pixels so only this center area will get the gradient. Outside that area would be sampled with pixels outside the texture so using CLAMP_TO_EDGE or on the opposite side of the texture using 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>

Note: to see what I mean about the extra math needed for the texture coordinates here is the same example without the extra math

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>

Also of course, rather than do the math in the fragment shader we could fix the texture coordinates in 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>

Another way is to do the interpolation yourself based on those corners (which is effectively doing what the texture sampler is doing in the previous example, bi-linear interpolation of the 4 colors).

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>



回答2:


Number of dimensions

You should be passing the 2D coordinate space of the quad to the fragment shader rather than the one dimensional (per channel) color space.

Then in the fragment shader you can do the color interpolation in 2D space removing the color artifact due to the diagonal line interpolating in 1D.

The shader snippet for linear color interpolation, where coord2D is the 2D coordinate space

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

Improved color interpolation

When interpolating colors by their RGB values the results can visually darken between opposing hues.

An easy fix is to use a closer approximation of the sRGB color model by interpolating between the squares of the color channel values. The final output is the square root of the interpolated values.

The interpolation snippet.

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

Note that the color channel values in the uniform colors are in logarithmic space. [R^2, G^2, B^2] and thus range from 0 to 65025.

Example

In the example click the canvas to switch between interpolation methods.

You will note that when using approx ~sRGB that the brightness in the center of the canvas out towards the center edges does not dip as much below the perceivable brightness at the corners.

Also note that the balance of the transition from the bottom blue and redish to the top orange and white moves down closer the the center. This is because interpolating the RGB model will darken colors that have strong components from 2 or more channels, Reds, greens, blues and blacks will dominate over yellows, cyans, magentas and whites making the interpolation seem to shift and stretch out the RGB primaries.

    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>


来源:https://stackoverflow.com/questions/60212615/how-to-properly-blend-colors-across-two-triangles-and-remove-diagonal-smear

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!