Основы WebGL

Грегг Таварес
Gregg Tavares

Основы WebGL

WebGL позволяет отображать потрясающую 3D-графику в реальном времени в вашем браузере, но многие люди не знают, что WebGL на самом деле является 2D-API, а не 3D-API. Позволь мне объяснить.

WebGL заботится только о двух вещах. Координаты пространства отсечения в 2D и цветах. Ваша задача как программиста, использующего WebGL, — обеспечить WebGL этими двумя вещами. Для этого вы предоставляете 2 «шейдера». Вершинный шейдер, предоставляющий координаты пространства отсечения, и фрагментный шейдер, предоставляющий цвет. Координаты пространства отсечения всегда варьируются от -1 до +1 независимо от размера холста. Вот простой пример WebGL, который показывает WebGL в его простейшей форме.

// Get A WebGL context
var canvas = document.getElementById("canvas");
var gl = canvas.getContext("experimental-webgl");

// setup a GLSL program
var vertexShader = createShaderFromScriptElement(gl, "2d-vertex-shader");
var fragmentShader = createShaderFromScriptElement(gl, "2d-fragment-shader");
var program = createProgram(gl, [vertexShader, fragmentShader]);
gl.useProgram(program);

// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");

// Create a buffer and put a single clipspace rectangle in
// it (2 triangles)
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        -1.0, -1.0,
         1.0, -1.0,
        -1.0,  1.0,
        -1.0,  1.0,
         1.0, -1.0,
         1.0,  1.0]),
    gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

// draw
gl.drawArrays(gl.TRIANGLES, 0, 6);

Вот 2 шейдера

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

void main() {
  gl_Position = vec4(a_position, 0, 1);
}
</script>

<script id="2d-fragment-shader" type="x-shader/x-fragment">
void main() {
  gl_FragColor = vec4(0,1,0,1);  // green
}
</script>

Опять же, координаты пространства отсечения всегда изменяются от -1 до +1 независимо от размера холста. В приведенном выше случае вы можете видеть, что мы ничего не делаем, а просто передаем данные о нашей позиции напрямую. Поскольку данные о положении уже находятся в пространстве отсечения, никаких действий делать не нужно. Если вам нужно 3D, вам следует предоставить шейдеры, преобразующие 3D в 2D, потому что WebGL — ЭТО 2D API! Для 2D-материалов вы, вероятно, предпочтете работать в пикселях, а не в пространстве отсечения, поэтому давайте изменим шейдер, чтобы мы могли предоставлять прямоугольники в пикселях и конвертировать их в пространство отсечения. Вот новый вершинный шейдер

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;

void main() {
   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = a_position / u_resolution;

   // convert from 0->1 to 0->2
   vec2 zeroToTwo = zeroToOne * 2.0;

   // convert from 0->2 to -1->+1 (clipspace)
   vec2 clipSpace = zeroToTwo - 1.0;

   gl_Position = vec4(clipSpace, 0, 1);
}
</script>

Теперь мы можем изменить наши данные из пространства отсечения в пиксели.

// set the resolution
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);

// setup a rectangle from 10,20 to 80,30 in pixels
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    10, 20,
    80, 20,
    10, 30,
    10, 30,
    80, 20,
    80, 30]), gl.STATIC_DRAW);

Вы можете заметить, что прямоугольник находится в нижней части этой области. WebGL считает нижний левый угол равным 0,0. Чтобы сделать его более традиционным верхним левым углом, используемым для API 2D-графики, мы просто переворачиваем координату y.

gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

Давайте превратим код, определяющий прямоугольник, в функцию, чтобы мы могли вызывать ее для прямоугольников разного размера. Пока мы этим занимаемся, мы сделаем цвет настраиваемым. Сначала мы заставляем фрагментный шейдер принимать входные данные с однородным цветом.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

uniform vec4 u_color;

void main() {
   gl_FragColor = u_color;
}
</script>

А вот новый код, который рисует 50 прямоугольников в случайных местах и ​​случайных цветов.

...

  var colorLocation = gl.getUniformLocation(program, "u_color");
  ...
  // Create a buffer
  var buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.enableVertexAttribArray(positionLocation);
  gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

  // draw 50 random rectangles in random colors
  for (var ii = 0; ii < 50; ++ii) {
    // Setup a random rectangle
    setRectangle(
        gl, randomInt(300), randomInt(300), randomInt(300), randomInt(300));

    // Set a random color.
    gl.uniform4f(colorLocation, Math.random(), Math.random(), Math.random(), 1);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }
}

// Returns a random integer from 0 to range - 1.
function randomInt(range) {
  return Math.floor(Math.random() * range);
}

// Fills the buffer with the values that define a rectangle.
function setRectangle(gl, x, y, width, height) {
  var x1 = x;
  var x2 = x + width;
  var y1 = y;
  var y2 = y + height;
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
     x1, y1,
     x2, y1,
     x1, y2,
     x1, y2,
     x2, y1,
     x2, y2]), gl.STATIC_DRAW);
}

Надеюсь, вы понимаете, что WebGL на самом деле довольно простой API. Хотя создание 3D-изображений может оказаться более сложным, это усложнение добавляется вами, программистом, в виде более сложных шейдеров. Сам API WebGL является двухмерным и довольно простым.

Что означают type="x-shader/x-vertex" и type="x-shader/x-fragment"?

Теги <script> по умолчанию содержат JavaScript. Вы можете не указывать тип или указать type="javascript" или type="text/javascript" и браузер будет интерпретировать содержимое как JavaScript. Если вы поместите что-нибудь еще, браузер проигнорирует содержимое тега сценария.

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

В этом случае функция createShaderFromScriptElement ищет сценарий с указанным id , а затем смотрит на его type , чтобы решить, какой тип шейдера создать.

Обработка изображений WebGL

Обработка изображений в WebGL проста. Как легко? Читай ниже.

Чтобы рисовать изображения в WebGL, нам нужно использовать текстуры. Подобно тому, как WebGL ожидает при рендеринге координаты пространства отсечения вместо пикселей, WebGL ожидает координаты текстуры при чтении текстуры. Координаты текстуры варьируются от 0,0 до 1,0 независимо от размеров текстуры. Поскольку мы рисуем только один прямоугольник (ну, два треугольника), нам нужно сообщить WebGL, какому месту в текстуре соответствует каждая точка прямоугольника. Мы передадим эту информацию из вершинного шейдера во фрагментный шейдер, используя специальный тип переменной, называемый «переменной». Это называется варьирующимся, потому что оно варьируется. WebGL будет интерполировать значения, которые мы предоставляем в вершинном шейдере, при отрисовке каждого пикселя с помощью фрагментного шейдера. Используя вершинный шейдер из конца предыдущего раздела, нам нужно добавить атрибут для передачи координат текстуры, а затем передать их во фрагментный шейдер.

attribute vec2 a_texCoord;
...
varying vec2 v_texCoord;

void main() {
   ...
   // pass the texCoord to the fragment shader
   // The GPU will interpolate this value between points
   v_texCoord = a_texCoord;
}

Затем мы предоставляем фрагментный шейдер для поиска цветов из текстуры.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // Look up a color from the texture.
   gl_FragColor = texture2D(u_image, v_texCoord);
}
</script>

Наконец, нам нужно загрузить изображение, создать текстуру и скопировать изображение в текстуру. Поскольку мы находимся в браузере, изображения загружаются асинхронно, поэтому нам нужно немного перестроить наш код, чтобы дождаться загрузки текстуры. Как только он загрузится, мы его нарисуем.

function main() {
  var image = new Image();
  image.src = "http://someimage/on/our/server";  // MUST BE SAME DOMAIN!!!
  image.onload = function() {
    render(image);
  }
}

function render(image) {
  ...
  // all the code we had before.
  ...
  // look up where the texture coordinates need to go.
  var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");

  // provide texture coordinates for the rectangle.
  var texCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      0.0,  0.0,
      1.0,  0.0,
      0.0,  1.0,
      0.0,  1.0,
      1.0,  0.0,
      1.0,  1.0]), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(texCoordLocation);
  gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

  // Create a texture.
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set the parameters so we can render any size image.
  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);

  // Upload the image into the texture.
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  ...
}

Это не слишком интересно, поэтому давайте поработаем над этим изображением. Как насчет того, чтобы просто поменять местами красный и синий?

...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...

Что, если мы хотим выполнить обработку изображений, которая на самом деле рассматривает другие пиксели? Поскольку WebGL ссылается на текстуры в координатах текстуры от 0,0 до 1,0, мы можем вычислить, сколько нужно переместить на 1 пиксель, с помощью простой математики onePixel = 1.0 / textureSize . Вот фрагментный шейдер, который усредняет левый и правый пиксели каждого пикселя текстуры.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // compute 1 pixel in texture coordinates.
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;

   // average the left, middle, and right pixels.
   gl_FragColor = (
       texture2D(u_image, v_texCoord) +
       texture2D(u_image, v_texCoord + vec2(onePixel.x, 0.0)) +
       texture2D(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
}
</script>

Затем нам нужно передать размер текстуры из JavaScript.

...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height);
...

Теперь, когда мы знаем, как ссылаться на другие пиксели, давайте воспользуемся ядром свертки для выполнения ряда обычных операций обработки изображений. В данном случае мы будем использовать ядро ​​3x3. Ядро свертки — это просто матрица 3x3, где каждая запись в матрице представляет, на сколько нужно умножить 8 пикселей вокруг пикселя, который мы визуализируем. Затем мы делим результат на вес ядра (сумму всех значений в ядре) или на 1,0, в зависимости от того, какое значение больше. Вот довольно хорошая статья об этом . А вот еще одна статья, показывающая реальный код, если бы вы написали его вручную на C++ . В нашем случае мы собираемся выполнить эту работу в шейдере, поэтому вот новый фрагментный шейдер.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
   vec4 colorSum =
     texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  0)) * u_kernel[3] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  0)) * u_kernel[4] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  0)) * u_kernel[5] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  1)) * u_kernel[6] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  1)) * u_kernel[7] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  1)) * u_kernel[8] ;
   float kernelWeight =
     u_kernel[0] +
     u_kernel[1] +
     u_kernel[2] +
     u_kernel[3] +
     u_kernel[4] +
     u_kernel[5] +
     u_kernel[6] +
     u_kernel[7] +
     u_kernel[8] ;

   if (kernelWeight <= 0.0) {
     kernelWeight = 1.0;
   }

   // Divide the sum by the weight but just use rgb
   // we'll set alpha to 1.0
   gl_FragColor = vec4((colorSum / kernelWeight).rgb, 1.0);
}
</script>

В JavaScript нам нужно предоставить ядро ​​свертки.

...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
...
var edgeDetectKernel = [
    -1, -1, -1,
    -1,  8, -1,
    -1, -1, -1
];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
...

Надеюсь, это убедило вас, что обработка изображений в WebGL довольно проста. Далее я расскажу, как применить к изображению более одного эффекта.

Что с префиксами a , u и v_ в переменных в GLSL?

Это всего лишь соглашение об именах. a_ для атрибутов, которые представляют собой данные, предоставляемые буферами. u_ для униформ, которые являются входными данными для шейдеров, v_ для переменных, которые представляют собой значения, передаваемые из вершинного шейдера во фрагментный шейдер и интерполируемые (или изменяющиеся) между вершинами для каждого нарисованного пикселя.

Применение нескольких эффектов

Следующий наиболее очевидный вопрос при обработке изображений: как применить несколько эффектов?

Ну, вы можете попробовать генерировать шейдеры на лету. Предоставьте пользовательский интерфейс, который позволит пользователю выбирать эффекты, которые он хочет использовать, а затем создавать шейдер, который выполняет все эффекты. Это не всегда возможно, хотя этот метод часто используется для создания эффектов для графики реального времени . Более гибкий способ — использовать еще две текстуры и рендерить каждую текстуру по очереди, перемещаясь вперед и назад и каждый раз применяя следующий эффект.

Original Image -> [Blur]        -> Texture 1
Texture 1      -> [Sharpen]     -> Texture 2
Texture 2      -> [Edge Detect] -> Texture 1
Texture 1      -> [Blur]        -> Texture 2
Texture 2      -> [Normal]      -> Canvas

Для этого нам нужно создать фреймбуферы. В WebGL и OpenGL фреймбуфер на самом деле является неудачным названием. Фреймбуфер WebGL/OpenGL на самом деле представляет собой просто набор состояний, а не какой-либо буфер. Но, прикрепив текстуру к фреймбуферу, мы можем выполнить рендеринг в эту текстуру. Сначала давайте превратим старый код создания текстуры в функцию.

function createAndSetupTexture(gl) {
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set up texture so we can render any size image and so we are
  // working with pixels.
  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);

  return texture;
}

// Create a texture and put the image in it.
var originalImageTexture = createAndSetupTexture(gl);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

А теперь давайте воспользуемся этой функцией, чтобы создать еще две текстуры и прикрепить их к двум фреймбуферам.

// create 2 textures and attach them to framebuffers.
var textures = [];
var framebuffers = [];
for (var ii = 0; ii < 2; ++ii) {
  var texture = createAndSetupTexture(gl);
  textures.push(texture);

  // make the texture the same size as the image
  gl.texImage2D(
      gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
      gl.RGBA, gl.UNSIGNED_BYTE, null);

  // Create a framebuffer
  var fbo = gl.createFramebuffer();
  framebuffers.push(fbo);
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Attach a texture to it.
  gl.framebufferTexture2D(
      gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}

Теперь давайте создадим набор ядер, а затем список их для применения.

// Define several convolution kernels
var kernels = {
  normal: [
    0, 0, 0,
    0, 1, 0,
    0, 0, 0
  ],
  gaussianBlur: [
    0.045, 0.122, 0.045,
    0.122, 0.332, 0.122,
    0.045, 0.122, 0.045
  ],
  unsharpen: [
    -1, -1, -1,
    -1,  9, -1,
    -1, -1, -1
  ],
  emboss: [
     -2, -1,  0,
     -1,  1,  1,
      0,  1,  2
  ]
};

// List of effects to apply.
var effectsToApply = [
  "gaussianBlur",
  "emboss",
  "gaussianBlur",
  "unsharpen"
];

И, наконец, давайте применим каждую из них, проверяя, какую текстуру мы рендерим.

// start with the original image
gl.bindTexture(gl.TEXTURE_2D, originalImageTexture);

// don't y flip images while drawing to the textures
gl.uniform1f(flipYLocation, 1);

// loop through each effect we want to apply.
for (var ii = 0; ii < effectsToApply.length; ++ii) {
  // Setup to draw into one of the framebuffers.
  setFramebuffer(framebuffers[ii % 2], image.width, image.height);

  drawWithKernel(effectsToApply[ii]);

  // for the next draw, use the texture we just rendered to.
  gl.bindTexture(gl.TEXTURE_2D, textures[ii % 2]);
}

// finally draw the result to the canvas.
gl.uniform1f(flipYLocation, -1);  // need to y flip for canvas
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");

function setFramebuffer(fbo, width, height) {
  // make this the framebuffer we are rendering to.
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Tell the shader the resolution of the framebuffer.
  gl.uniform2f(resolutionLocation, width, height);

  // Tell webgl the viewport setting needed for framebuffer.
  gl.viewport(0, 0, width, height);
}

function drawWithKernel(name) {
  // set the kernel
  gl.uniform1fv(kernelLocation, kernels[name]);

  // Draw the rectangle.
  gl.drawArrays(gl.TRIANGLES, 0, 6);
}

Некоторые вещи мне следует обсудить.

Вызов gl.bindFramebuffer с null сообщает WebGL, что вы хотите выполнить рендеринг на холст, а не на один из ваших фреймбуферов. WebGL должен конвертировать пространство отсечения обратно в пиксели. Это делается на основе настроек gl.viewport . Настройки gl.viewport по умолчанию соответствуют размеру холста при инициализации WebGL. Поскольку фреймбуферы, в которые мы рендерим, имеют разный размер, нам необходимо соответствующим образом установить область просмотра. Наконец, в примерах по основам WebGL мы перевернули координату Y при рендеринге, поскольку WebGL отображает холст с 0,0, обозначающим нижний левый угол, вместо более традиционного для 2D верхнего левого угла. Это не требуется при рендеринге во фреймбуфер. Поскольку фреймбуфер никогда не отображается, не имеет значения, какая часть находится сверху, а какая снизу. Важно лишь то, что пиксель 0,0 во фреймбуфере соответствует 0,0 в наших расчетах. Чтобы справиться с этим, я позволил задать, следует ли переворачивать или нет, добавив еще один вход в шейдер.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
...
uniform float u_flipY;
...

void main() {
   ...
   gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);
   ...
}
</script>

И затем мы можем установить его при рендеринге с помощью

...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);

Я сохранил этот пример простым, используя одну программу GLSL, которая может достигать нескольких эффектов. Если вы хотите полностью заняться обработкой изображений, вам, вероятно, понадобится множество программ GLSL. Программа для регулировки оттенка, насыщенности и яркости. Другой для яркости и контрастности. Один для инвертирования, другой для настройки уровней и т. д. Вам нужно будет изменить код, чтобы переключать программы GLSL и обновлять параметры для этой конкретной программы. Я подумывал о написании этого примера, но лучше оставить это упражнение читателю, потому что несколько программ GLSL, каждая со своими собственными параметрами, вероятно, означают серьезный рефакторинг, чтобы все это не превратилось в большую мешанину спагетти. Я надеюсь, что этот и предыдущие примеры сделали WebGL более доступным, и я надеюсь, что начало работы с 2D поможет сделать WebGL немного проще для понимания. Если я найду время, я постараюсь написать еще несколько статей о том, как создавать 3D, а также более подробную информацию о том, что на самом деле делает WebGL под капотом.

WebGL и Альфа

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

Самая большая разница между OpenGL и WebGL заключается в том, что OpenGL выполняет рендеринг в задний буфер, который ни с чем не скомпонован или фактически не скомпонован ни с чем с помощью оконного менеджера ОС, поэтому не имеет значения, какая у вас альфа-версия. WebGL объединяется браузером с веб-страницей, и по умолчанию используется предварительно умноженная альфа-код, так же, как теги .png <img> с прозрачностью и теги 2D-холста. В WebGL есть несколько способов сделать это более похожим на OpenGL.

#1) Сообщите WebGL, что вы хотите, чтобы он был составлен с использованием альфа без предварительного умножения.

gl = canvas.getContext("experimental-webgl", {premultipliedAlpha: false});

По умолчанию верно. Конечно, результат по-прежнему будет наложен на страницу с любым цветом фона, который окажется под холстом (цвет фона холста, цвет фона контейнера холста, цвет фона страницы, материал, стоящий за холстом, если холст имеет z-индекс). > 0 и т. д.), другими словами, цвет, который CSS определяет для этой области веб-страницы. Я действительно хороший способ узнать, есть ли у вас какие-либо проблемы с альфа-каналом, — это установить для фона холста яркий цвет, например красный. Вы сразу увидите, что происходит.

<canvas style="background: red;"></canvas>

Вы также можете установить его на черный цвет, что скроет любые проблемы с альфа-версией.

#2) Скажите WebGL, что вам не нужна альфа-версия в заднем буфере

gl = canvas.getContext("experimental-webgl", {alpha: false});

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

#3) Очистите альфу в конце рендеринга.

..
renderScene();
..
// Set the backbuffer's alpha to 1.0
gl.clearColor(1, 1, 1, 1);
gl.colorMask(false, false, false, true);
gl.clear(gl.COLOR_BUFFER_BIT);

Очистка обычно происходит очень быстро, поскольку в большинстве аппаратных средств для этого предусмотрен особый случай. Я делал это в большинстве своих демо. Если бы я был умным, я бы переключился на метод №2 выше. Возможно, я сделаю это сразу после того, как опубликую это. Похоже, что большинство библиотек WebGL должны использовать этот метод по умолчанию. Те немногие разработчики, которые на самом деле используют альфа для наложения эффектов, могут попросить об этом. Остальные просто получат лучшую производительность и минимум сюрпризов.

#4) Очистите альфу один раз и больше не визуализируйте ее.

// At init time. Clear the back buffer.
gl.clearColor(1,1,1,1);
gl.clear(gl.COLOR_BUFFER_BIT);

// Turn off rendering to alpha
gl.colorMask(true, true, true, false);

Конечно, если вы выполняете рендеринг в свои собственные кадровые буферы, вам может потребоваться снова включить альфа-рендеринг, а затем снова выключить его при переключении на рендеринг на холст.

№5) Обработка изображений

Кроме того, если вы загружаете PNG-файлы с альфой в текстуры, по умолчанию их альфа предварительно умножается, что обычно НЕ происходит в большинстве игр. Если вы хотите предотвратить такое поведение, вам нужно сообщить WebGL с помощью

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

#6) Использование уравнения смешивания, которое работает с предварительно умноженной альфа

Почти все приложения OpenGL, которые я писал или над которыми работал, используют

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Это работает для не предварительно умноженных альфа-текстур. Если вы действительно хотите работать с предварительно умноженными альфа-текстурами, вам, вероятно, понадобится

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Это методы, которые я знаю. Если вы знаете больше, пожалуйста, опубликуйте их ниже.