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 bereitstellt, und einen 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>

Unabhängig von der Größe des Canvas gehen die Clipspace-Koordinaten immer von -1 bis +1. Im obigen Fall geben wir unsere Standortdaten einfach direkt 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! Für 2D-Elemente 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);

Das Rechteck befindet sich am unteren Rand dieses Bereichs. WebGL betrachtet die untere linke Ecke mit 0,0. Damit es die traditionellere obere linke Ecke wird, die für 2D-Grafik-APIs verwendet wird, drehen wir einfach die Y-Koordinate.

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. Es kann zwar komplizierter sein, 3D zu erstellen, aber du als Programmierer fügst diesen in Form komplexerer Shader hinzu. 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. Sie können keinen Typ oder type="javascript" oder type="text/javascript" einfügen. 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. Besser noch: Wir können einen eigenen Typ erstellen und in unserem JavaScript danach suchen, um zu entscheiden, ob der Shader als Vertex-Shader 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 type, um zu entscheiden, welche Art von Shader erstellt werden soll.

WebGL-Bildverarbeitung

Die Bildverarbeitung ist in WebGL ganz einfach. Wie einfach? Weitere Informationen dazu findest du unten.

Zum Zeichnen von Bildern in WebGL benötigen wir Texturen. Ä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 einzelnes 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 neu anordnen, 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 auf andere Pixel verwiesen wird, können wir einen Convolution Kernel verwenden, um eine Reihe von gängigen Bildverarbeitungen durchzufü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. Anschließend teilen wir das Ergebnis durch die Gewichtung des Kernels (die Summe aller Werte im Kernel) oder durch 1,0, je nachdem, welcher Wert größer ist. Hier finden Sie einen ziemlich guten 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 mehr als einen Effekt auf das Bild anwenden können.

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

Das ist nur eine Namenskonvention. a_ für Attribute. Dies sind die über Zwischenspeicher bereitgestellten Daten. u_ für Uniformen, die Eingaben für die Shader sind, v_ für verschiedene Werte. Dabei handelt es sich um Werte, die von einem Scheitel-Shader an einen Fragment-Shader übergeben und zwischen den Eckpunkten für jedes gezeichnete Pixel 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, über die 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 jede Textur der Reihe nach zu rendern. Dabei wird jedes Mal beim Tischtennis hin und her gespielt und der nächste Effekt wird angewendet.

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 in Wirklichkeit nur eine Sammlung von Statuswerten und kein Zwischenspeicher. Wenn wir jedoch eine Textur an einen Framebuffer anhängen, können wir sie in diese Textur rendern. Zunächst wandeln wir den alten Code zur Texturerstellung in eine Funktion um.

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

Ich möchte Ihnen einige Dinge erklären.

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 werden die Einstellungen von gl.viewport berücksichtigt. Die Einstellungen von gl.viewport entsprechen standardmäßig der Größe des Canvas, wenn wir WebGL initialisieren. Da die Framebuffer, in die gerendert werden, eine andere Größe haben, muss der Canvas entsprechend angepasst werden. In den Beispielen zu den WebGL-Grundlagen haben wir schließlich die Y-Koordinate beim Rendern umgedreht, da WebGL das Canvas mit 0,0 für die untere linke Ecke anstelle der traditionelleren 2D-Koordinate oben links anzeigt. 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 in unseren Berechnungen 0,0 entspricht. Um das zu beheben, 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 dem Leser am besten ü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

Mir ist aufgefallen, dass einige OpenGL-Entwickler Probleme mit der Behandlung von Alpha im Backbuffer (d. h. Canvas) von WebGL haben. Daher dachte ich, dass es hilfreich sein könnte, auf einige der Unterschiede zwischen WebGL und OpenGL im Zusammenhang mit Alpha einzugehen.

Der größte Unterschied zwischen OpenGL und WebGL besteht darin, dass OpenGL in einem Backbuffer gerendert wird, der nicht aus einem dieser Elemente besteht oder praktisch nicht mit etwas vom Fenstermanager des Betriebssystems zusammengesetzt ist. Es spielt also keine Rolle, was Ihr Alpha ist. WebGL wird vom Browser mit der Webseite zusammengesetzt. Standardmäßig werden vormultiplizierte Alpha-Tags wie .png-<img>-Tags mit Transparenz und Canvas verwendet. 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 es Alphaprobleme gibt, 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 setzen, 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 sehen könnte, dass Sie keinen Alpha-Test haben, und die Art und Weise optimieren könnte, wie WebGL zusammengesetzt ist. Das bedeutet natürlich auch, dass der Backbuffer kein Alpha enthält. Wenn Sie also Alpha im Backbuffer für einen bestimmten Zweck verwenden, funktioniert das möglicherweise nicht. Es gibt nur wenige Apps, von denen ich weiß, dass sie 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 bei den meisten meinen Demos gemacht. Wenn ich klug wäre, würde ich zu Methode 2 oben wechseln. Vielleicht mache ich das gleich, nachdem ich diesen Beitrag 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 Alpha in Texturen laden, werden diese standardmäßig vorab multipliziert. Das ist in der Regel NICHT die Vorgehensweise in den meisten Spielen. Wenn Sie dieses Verhalten verhindern möchten, müssen Sie WebGL mit

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6) Eine Vermischungsgleichung verwenden, die mit einem vorab multiplizierten Alphatest 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.