Как правильно смешать цвета на двух треугольниках и удалить диагональное смазывание

Я изучаю WebGL и нарисовал полноэкранный четырехугольник с цветами для каждой вершины. Нет освещения, нормалей, матрицы перспективы или буфера глубины; Я просто рисую градиентный фон. Вот что я получаю:

полноэкранный градиент

Выглядит хорошо, но я не могу не заметить диагональный мазок от правого нижнего угла к левому верхнему. Я чувствую, что это артефакт линейной интерполяции противоположных вершин. Я рисую два треугольника: нижний левый и верхний правый. Я думаю, что получил бы аналогичные результаты, используя OpenGL вместо WebGL.

Имея те же четыре цвета и прямоугольник одинакового размера, есть ли способ отобразить это так, чтобы край между двумя треугольниками не был таким очевидным? Может быть, больше вершин или другая функция смешивания? Я не уверен точно, какие цвета должны быть в каждом пикселе; Я просто хочу знать, как избавиться от диагонального мазка.


person whiterook6    schedule 13.02.2020    source источник
comment
stackoverflow.com/a/5362303   -  person genpfault    schedule 13.02.2020
comment
Да, это делает это. Если бы вы хотели написать что-то о билинейной интерполяции в качестве ответа с этой ссылкой, я бы отметил это как правильный ответ. Спасибо!   -  person whiterook6    schedule 13.02.2020
comment
Я хотел бы, чтобы этот вопрос был вновь открыт, поскольку повторяющемуся вопросу уже 9 лет, он не применим к текущему WebGL, а ответы довольно низкого качества, поскольку все они не используют правильную интерполяцию.   -  person Blindman67    schedule 13.02.2020


Ответы (2)


Проблема в том, что верхний правый треугольник ничего не знает о нижнем левом углу, поэтому верхний правый треугольник не включает ни одного синего из нижнего левого угла (и наоборот).

Пара способов исправить это.

Один из них — использовать текстуру 2x2 с линейной выборкой. Вы должны сделать некоторые дополнительные математические вычисления, чтобы получить правильную интерполяцию, потому что текстура интерполируется только между пикселями.

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

Выше показана 4-пиксельная текстура, растянутая до 14 на 6. Выборка происходит между пикселями, поэтому только эта центральная область получит градиент. За пределами этой области будут выбраны пиксели вне текстуры, поэтому используйте 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>

person gman    schedule 14.02.2020

Количество измерений

Вы должны передавать 2D-координатное пространство четырехугольника фрагментному шейдеру, а не одномерное (на канал) цветовое пространство.

Затем во фрагментном шейдере вы можете выполнить интерполяцию цвета в 2D-пространстве, удалив цветовой артефакт из-за интерполяции диагональной линии в 1D.

Фрагмент шейдера для линейной интерполяции цветов, где coord2D — двумерное координатное пространство.

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 находятся в логарифмическом пространстве. [R^2, G^2, B^2] и, таким образом, находится в диапазоне от 0 до 65025.

Пример

В примере щелкните холст, чтобы переключиться между методами интерполяции.

Вы заметите, что при использовании приблизительно ~sRGB яркость в центре холста по направлению к центральным краям не падает настолько ниже воспринимаемой яркости в углах.

Также обратите внимание, что баланс перехода от нижнего синего и красноватого к верхнему оранжевому и белому смещается вниз ближе к центру. Это связано с тем, что интерполяция модели 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>

person Blindman67    schedule 15.02.2020