WebGL-Grundlagen

Gregg Tavares
Gregg Tavares

WebGL-Grundlagen

Mit WebGL können Sie beeindruckende 3D-Grafiken in Echtzeit in Ihrem Browser anzeigen. Viele Nutzer wissen jedoch nicht, dass WebGL eigentlich eine 2D-API und keine 3D-API ist. Lassen Sie mich das erklären.

Für WebGL sind nur zwei Dinge wichtig. Clipspace-Koordinaten in 2D und Farben. Ihre Aufgabe als WebGL-Programmierer besteht darin, WebGL diese beiden Dinge zur Verfügung zu stellen. Dazu stellen Sie zwei „Shader“ bereit. Ein Vertex-Shader, der die Clipspace-Koordinaten und ein Fragment-Shader, der die Farbe bereitstellt. Clipspace-Koordinaten reichen immer von -1 bis +1, unabhängig von der Größe des Canvas. Hier ist ein einfaches WebGL-Beispiel, das WebGL in seiner einfachsten Form zeigt.

// 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);

Hier sind die beiden Shader

<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>

Die Clipspace-Koordinaten reichen unabhängig von der Größe des Canvas immer von -1 bis +1. Im obigen Beispiel geben wir unsere Standortdaten einfach weiter. Da sich die Positionierungsdaten bereits im Clipspace befinden, ist keine weitere Bearbeitung erforderlich. Wenn Sie 3D-Objekte benötigen, müssen Sie Shader bereitstellen, die von 3D zu 2D konvertieren, da WebGL EINE 2D-API IST! Bei 2D-Elementen arbeiten Sie wahrscheinlich lieber in Pixeln als im Clipspace. Ändern wir also den Shader, damit wir Rechtecke in Pixeln angeben und in Clipspace umwandeln lassen. Hier ist der neue Vertex-Shader

<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>

Jetzt können wir unsere Daten von Clipspace in Pixel umwandeln.

// 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);

Möglicherweise fällt Ihnen auf, dass sich das Rechteck unten in diesem Bereich befindet. In WebGL ist die linke untere Ecke 0,0. Damit es der traditionellere linke obere Eckpunkt wird, der für 2D-Grafik-APIs verwendet wird, kehren wir einfach die y-Koordinate um.

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

Wir machen den Code, der ein Rechteck definiert, zu einer Funktion, damit wir ihn für Rechtecke unterschiedlicher Größe aufrufen können. Und wir machen die Farbe gleich auch einstellbar. Zuerst sorgen wir dafür, dass der Fragment-Shader eine einheitlich farbige Eingabe erhält.

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

uniform vec4 u_color;

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

Und hier ist der neue Code, mit dem 50 Rechtecke an zufälligen Stellen und in zufälligen Farben gezeichnet werden.

...

  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);
}

Ich hoffe, Sie können sehen, dass WebGL eigentlich eine ziemlich einfache API ist. Die 3D-Grafik kann zwar komplizierter werden, aber diese Komplexität wird von Ihnen, dem Programmierer, in Form von komplexeren Shadern hinzugefügt. Die WebGL API selbst ist 2D und ziemlich einfach.

Was bedeuten „type="x-shader/x-vertex"“ und „type="x-shader/x-fragment"“?

<script>-Tags enthalten standardmäßig JavaScript-Code. Sie können keinen Typ angeben oder type="javascript" oder type="text/javascript" eingeben. Der Browser interpretiert den Inhalt dann als JavaScript. Andernfalls wird der Inhalt des Script-Tags vom Browser ignoriert.

Mit dieser Funktion können wir Shader in Script-Tags speichern. Noch besser: Wir können einen eigenen Typ erstellen und in unserem JavaScript danach suchen, um zu entscheiden, ob der Shader als Vertex- oder als Fragment-Shader kompiliert werden soll.

In diesem Fall sucht die Funktion createShaderFromScriptElement nach einem Script mit der angegebenen id und prüft dann anhand der type, welche Art von Shader erstellt werden soll.

WebGL-Bildverarbeitung

Die Bildverarbeitung ist in WebGL ganz einfach. Wie einfach? Weitere Informationen finden Sie unten.

Zum Zeichnen von Bildern in WebGL müssen wir Texturen verwenden. Ähnlich wie beim Rendering erwartet WebGL beim Lesen einer Textur Texturkoordinaten anstelle von Pixeln. Texturkoordinaten reichen unabhängig von den Abmessungen der Textur von 0,0 bis 1,0. Da wir nur ein einziges Rechteck (eigentlich zwei Dreiecke) zeichnen, müssen wir WebGL mitteilen, welchem Punkt in der Textur jeder Punkt im Rechteck entspricht. Wir übergeben diese Informationen vom Vertex-Shader an den Fragment-Shader mithilfe einer speziellen Variablen namens „varying“. Sie wird als Variable bezeichnet, weil sie variiert. WebGL interpoliert die Werte, die wir im Vertex-Shader angeben, wenn es jedes Pixel mit dem Fragment-Shader zeichnet. Für den Vertex-Shader aus dem vorherigen Abschnitt müssen wir ein Attribut hinzufügen, um Texturkoordinaten einzugeben und dann an den Fragment-Shader weiterzuleiten.

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;
}

Dann stellen wir einen Fragment-Shader bereit, um Farben aus der Textur abzurufen.

<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>

Zum Schluss müssen wir ein Bild laden, eine Textur erstellen und das Bild in die Textur kopieren. Da wir uns in einem Browser befinden, werden Bilder asynchron geladen. Daher müssen wir unseren Code ein wenig umstrukturieren, um zu warten, bis die Textur geladen ist. Sobald es geladen ist, wird es gezeichnet.

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);
  ...
}

Nicht sehr aufregend. Lassen Sie uns das Bild also bearbeiten. Wie wäre es, wenn Sie einfach Rot und Blau tauschen?

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

Was ist, wenn wir eine Bildverarbeitung durchführen möchten, bei der tatsächlich andere Pixel berücksichtigt werden? Da WebGL Texturen in Texturenkoordinaten referenziert, die von 0,0 bis 1,0 gehen, können wir mit der einfachen Mathematik onePixel = 1.0 / textureSize berechnen, wie viel sich ein Pixel bewegen muss. Hier ist ein Fragment-Shader, der die linken und rechten Pixel jedes Pixels in der Textur mittelt.

<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>

Anschließend müssen wir die Größe der Textur aus JavaScript übergeben.

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

Nachdem wir nun wissen, wie wir auf andere Pixel verweisen, verwenden wir einen Convolutionskern, um einige gängige Bildverarbeitungsschritte auszuführen. In diesem Fall verwenden wir einen 3 × 3-Kernel. Ein Convolutionskern ist nur eine 3 × 3-Matrix, bei der jeder Eintrag in der Matrix angibt, um wie viel die 8 Pixel um das Pixel, das wir rendern, multipliziert werden sollen. Das Ergebnis wird dann durch das Gewicht des Kerns (die Summe aller Werte im Kern) oder 1,0 geteilt, je nachdem, was größer ist. Hier ist ein guter Artikel dazu. In diesem Artikel finden Sie einen Code, den Sie manuell in C++ schreiben könnten. In unserem Fall erledigen wir diese Arbeit im Shader. Hier ist der neue Fragment-Shader.

<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>

In JavaScript müssen wir einen Convolutionskern angeben.

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

Ich hoffe, ich konnte Sie davon überzeugen, dass die Bildverarbeitung in WebGL ziemlich einfach ist. Als Nächstes zeige ich Ihnen, wie Sie mehrere Effekte auf das Bild anwenden.

Was bedeuten die Präfixe „a“, „u“ und „v_“ vor Variablen in GLSL?

Das ist nur eine Namenskonvention. a_ für Attribute, also die von Puffern bereitgestellten Daten. u_ für Uniforms, die Eingaben für die Shader sind, v_ für Varyings, die Werte, die von einem Vertex-Shader an einen Fragment-Shader übergeben und für jedes gezeichnete Pixel zwischen den Vertexen interpoliert (oder variiert) werden.

Mehrere Effekte anwenden

Die nächste naheliegende Frage bei der Bildverarbeitung ist, wie mehrere Effekte angewendet werden.

Sie könnten versuchen, Shader direkt zu generieren. Bieten Sie eine Benutzeroberfläche, mit der der Nutzer die gewünschten Effekte auswählen und dann einen Shader generieren kann, der alle Effekte ausführt. Das ist jedoch nicht immer möglich. Diese Technik wird jedoch häufig verwendet, um Effekte für Echtzeitgrafiken zu erstellen. Eine flexiblere Methode besteht darin, zwei weitere Texturen zu verwenden und nacheinander auf jede Textur zu rendern, hin und her zu pingpongen und jedes Mal den nächsten Effekt anzuwenden.

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

Dazu müssen wir Framebuffer erstellen. In WebGL und OpenGL ist ein Framebuffer eigentlich ein schlechter Name. Ein WebGL-/OpenGL-Framebuffer ist eigentlich nur eine Sammlung von Status und kein Buffer. Wenn wir jedoch einem Framebuffer eine Textur zuweisen, können wir in diese Textur rendern. Ersetzen wir zuerst den alten Code zum Erstellen von Texturen durch eine Funktion.

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);

Verwenden wir diese Funktion jetzt, um zwei weitere Texturen zu erstellen und an zwei Framebuffer anzuhängen.

// 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);
}

Erstellen Sie nun eine Reihe von Kernen und dann eine Liste der anzuwendenden Kerne.

// 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"
];

Und schließlich wenden wir sie alle an und wechseln dabei die gerenderte Textur.

// 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);
}

Es gibt ein paar Dinge, die ich Ihnen erläutern sollte.

Wenn Sie gl.bindFramebuffer mit null aufrufen, teilen Sie WebGL mit, dass Sie das Rendering auf dem Canvas und nicht auf einem Ihrer Framebuffer ausführen möchten. WebGL muss aus dem Clipspace zurück in Pixel konvertieren. Dabei orientiert er sich an den Einstellungen von gl.viewport. Die Einstellungen von gl.viewport entsprechen standardmäßig der Größe des Canvas, wenn wir WebGL initialisieren. Da die Framebuffer, in die wir rendern, eine andere Größe als der Canvas haben, müssen wir den Darstellungsbereich entsprechend festlegen. In den Beispielen zu den WebGL-Grundlagen haben wir die Y-Koordinate beim Rendern umgedreht, da in WebGL der Canvas mit 0,0 als unterer linker Eckpunkt angezeigt wird, anstatt wie bei 2D-Grafiken üblich oben links. Das ist beim Rendern in einen Framebuffer nicht erforderlich. Da der Framebuffer nie angezeigt wird, ist es irrelevant, welcher Teil oben und welcher unten ist. Wichtig ist nur, dass Pixel 0,0 im Framebuffer 0,0 in unseren Berechnungen entspricht. Um das zu lösen, habe ich eine weitere Eingabe in den Shader eingefügt, mit der sich festlegen lässt, ob gedreht werden soll oder nicht.

<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>

Und dann können wir es beim Rendern mit

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

Ich habe dieses Beispiel einfach gehalten, indem ich ein einzelnes GLSL-Programm verwendet habe, mit dem mehrere Effekte erzielt werden können. Wenn Sie eine vollständige Bildverarbeitung durchführen möchten, benötigen Sie wahrscheinlich viele GLSL-Programme. Ein Programm zur Anpassung von Farbton, Sättigung und Leuchtkraft. Eine weitere für Helligkeit und Kontrast. Eines zum Invertieren, ein anderes zum Anpassen der Pegel usw. Sie müssten den Code ändern, um GLSL-Programme zu wechseln und die Parameter für das jeweilige Programm zu aktualisieren. Ich hatte überlegt, dieses Beispiel zu schreiben, aber es ist eine Übung, die am besten dem Leser überlassen wird, da mehrere GLSL-Programme mit jeweils eigenen Parameteranforderungen wahrscheinlich eine umfangreiche Refaktorisierung erfordern, um zu verhindern, dass das Ganze zu einem großen Spaghetti-Code wird. Ich hoffe, dass WebGL durch dieses und die vorangegangenen Beispiele etwas zugänglicher geworden ist. Außerdem hoffe ich, dass der Einstieg in 2D dazu beiträgt, WebGL ein wenig besser zu verstehen. Wenn ich die Zeit finde, werde ich versuchen, noch ein paar Artikel über 3D-Programmierung zu schreiben und mehr Details dazu zu liefern, was WebGL im Hintergrund wirklich tut.

WebGL und Alpha

Ich habe festgestellt, dass einige OpenGL-Entwickler Probleme mit der Verarbeitung von Alpha im Backbuffer (d. h. im Canvas) durch WebGL haben. Deshalb dachte ich, es wäre gut, einige der Unterschiede zwischen WebGL und OpenGL im Zusammenhang mit Alpha zu besprechen.

Der größte Unterschied zwischen OpenGL und WebGL besteht darin, dass OpenGL in einen Backbuffer gerendert wird, der nicht mit irgendetwas zusammengesetzt wird, oder zumindest nicht vom Fenstermanager des Betriebssystems zusammengesetzt wird. Daher spielt es keine Rolle, wie hoch der Alphawert ist. WebGL wird vom Browser mit der Webseite zusammengesetzt. Standardmäßig wird ein vormultiplizierter Alphawert verwendet, der dem von .png-<img>-Tags mit Transparenz und 2D-Canvas-Tags entspricht. Mit WebGL gibt es mehrere Möglichkeiten, dies OpenGL ähnlicher zu gestalten.

#1) WebGL mitteilen, dass die Komposition mit nicht vormultipliziertem Alpha erfolgen soll

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

Die Standardeinstellung ist "true". Natürlich wird das Ergebnis weiterhin über die Seite zusammengesetzt, wobei die Hintergrundfarbe verwendet wird, die sich unter dem Canvas befindet (die Hintergrundfarbe des Canvas, die Hintergrundfarbe des Canvas-Containers, die Hintergrundfarbe der Seite, die Elemente hinter dem Canvas, wenn der Canvas einen Z-Index von mehr als 0 hat usw.). Mit anderen Worten: die Farbe, die in CSS für diesen Bereich der Webseite definiert ist. Eine gute Möglichkeit, festzustellen, ob Sie Alphaprobleme haben, ist, den Hintergrund des Canvas in einer hellen Farbe wie Rot festzulegen. Sie sehen sofort, was passiert.

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

Sie können ihn auch auf Schwarz stellen, wodurch alle Alpha-Probleme ausgeblendet werden.

#2) WebGL mitteilen, dass keine Alphawerte im Backbuffer verwendet werden sollen

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

Dadurch verhält es sich eher wie OpenGL, da der Backbuffer nur RGB hat. Dies ist wahrscheinlich die beste Option, da ein guter Browser erkennen kann, dass Sie keine Alphawerte haben, und die WebGL-Komposition optimieren kann. Das bedeutet natürlich auch, dass der Backbuffer keine Alphawerte enthält. Wenn Sie also Alphawerte im Backbuffer verwenden, funktioniert das möglicherweise nicht. Mir sind nur wenige Apps bekannt, die Alpha im Backbuffer verwenden. Das sollte eigentlich die Standardeinstellung sein.

#3) Alpha am Ende des Renderings löschen

..
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);

Das Löschen ist in der Regel sehr schnell, da es bei den meisten Hardwarekomponenten einen speziellen Fall dafür gibt. Das habe ich in den meisten meiner Demos getan. Wenn ich schlau wäre, würde ich zu Methode 2 oben wechseln. Vielleicht mache ich das gleich, nachdem ich diese Nachricht gepostet habe. Die meisten WebGL-Bibliotheken sollten standardmäßig diese Methode verwenden. Die wenigen Entwickler, die Alpha tatsächlich für Compositing-Effekte verwenden, können sie anfordern. Der Rest erhält einfach die beste Leistung und die wenigsten Überraschungen.

#4) Alpha einmal löschen und dann nicht mehr darauf rendern

// 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);

Wenn Sie in eigene Framebuffer rendern, müssen Sie das Rendern in Alpha möglicherweise wieder aktivieren und dann wieder deaktivieren, wenn Sie zum Rendern in den Canvas wechseln.

#5) Umgang mit Bildern

Wenn Sie PNG-Dateien mit Alphakanal in Texturen laden, wird der Alphakanal standardmäßig vormultipliziert. Das ist in den meisten Spielen jedoch nicht der Fall. Wenn Sie dieses Verhalten verhindern möchten, müssen Sie WebGL mit

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

#6) Eine Mischungsgleichung verwenden, die mit vormultipliziertem Alpha funktioniert

Fast alle OpenGL-Apps, die ich geschrieben oder an denen ich gearbeitet habe, verwenden

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Das funktioniert für nicht vormultiplizierte Alpha-Texturen. Wenn Sie mit vormultiplizierten Alpha-Texturen arbeiten möchten, sollten Sie

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Das sind die Methoden, die mir bekannt sind. Wenn du weitere kennst, poste sie bitte unten.