WebGL-Grundlagen

Gregg Tavares
Gregg Tavares

WebGL-Grundlagen

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

Für WebGL sind nur zwei Dinge wichtig. Clipspace-Koordinaten in 2D und Farben. Als Programmierer, der WebGL verwendet, müssen Sie WebGL diese beiden Dinge bereitstellen. Dazu stellst du zwei „Shader“ bereit. Vertex-Shader, der die Clipspace-Koordinaten und einen Fragment-Shader bereitstellt, der die Farbe bereitstellt. Die Clipspace-Koordinaten gehen unabhängig von der Größe Ihres Canvas immer von -1 bis +1. 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 2 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>

Auch hier gehen die Clipspace-Koordinaten unabhängig von der Größe des Canvas immer von -1 bis +1 über. Wie Sie im Beispiel oben sehen, geben wir lediglich die Positionsdaten weiter. Da sich die Positionsdaten bereits im Clipspace befinden, ist nichts weiter zu tun. Wenn Sie 3D verwenden möchten, müssen Sie Shader bereitstellen, die von 3D in 2D konvertiert werden, da WebGL eine 2D API ist. Bei 2D-Dingen würden Sie wahrscheinlich eher in Pixeln als mit Clipspace arbeiten. Ändern wir also den Shader, damit wir Rechtecke in Pixeln bereitstellen und ihn für uns in Clipspace konvertieren können. 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 ändern.

// 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 des Bereichs. WebGL behandelt die linke untere Ecke als 0,0. Um es zur traditionelleren oberen linken Ecke zu machen, die für 2D-Grafik-APIs verwendet wird, drehen Sie einfach die y-Koordinate.

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

Verwandeln wir den Code, der ein Rechteck definiert, in eine Funktion, damit wir es für Rechtecke unterschiedlicher Größe aufrufen können. Und schon legen wir die Farbe fest. Zuerst sorgen wir dafür, dass der Fragment-Shader eine Farbeinheitseingabe übernimmt.

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

uniform vec4 u_color;

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

Hier ist der neue Code, mit dem 50 Rechtecke an zufällig gewählten Stellen und 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 sehen, dass WebGL eigentlich eine ziemlich einfache API ist. Es kann zwar etwas komplizierter werden, 3D-Effekte zu nutzen, doch Sie als Programmierer fügen diese Komplikationen in Form komplexerer Shader hinzu. Die WebGL API selbst ist in 2D dargestellt und recht 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 angeben oder type="javascript" oder type="text/javascript" eingeben. Der Browser interpretiert dann den Inhalt als JavaScript. Wenn Sie etwas anderes eingeben, ignoriert der Browser den Inhalt des Skript-Tags.

Wir können diese Funktion verwenden, um Shader in Skript-Tags zu speichern. Noch besser können wir 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 Skript mit der angegebenen id und prüft dann den type, um zu entscheiden, 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 bei WebGL beim Rendern von Pixeln erwartet WebGL Texturkoordinaten beim Lesen einer Textur. Texturkoordinaten reichen unabhängig von den Abmessungen der Textur von 0,0 bis 1,0. Da wir nur ein einzelnes Rechteck (also zwei Dreiecke) zeichnen, muss WebGL mitgeteilt werden, welcher Stelle in der Textur jeder Punkt des Rechtecks entspricht. Wir übergeben diese Informationen vom Vertex-Shader an den Fragment-Shader mit einer speziellen Art von Variablen, die als "Variieren" bezeichnet wird. Sie wird als variierend bezeichnet, weil sie variiert. WebGL interpoliert die Werte, die wir im Vertex-Shader angeben, während jedes Pixel mit dem Fragment-Shader gezeichnet wird. Mit dem Vertex-Shader vom Ende des vorherigen Abschnitts müssen wir ein Attribut hinzufügen, um Texturkoordinaten zu übergeben und diese dann an den Fragment-Shader weiterzugeben.

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

Anschließend 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>

Schließlich müssen wir ein Bild laden, eine Textur erstellen und das Bild in die Textur kopieren. Da die Bilder im Browser asynchron geladen werden, müssen wir unseren Code ein wenig neu anordnen, um auf das Laden der Textur zu warten. Sobald es geladen ist, zeichnen wir es.

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

Es ist nicht zu aufregend, also lass uns das Bild bearbeiten. Wie wäre es, einfach nur Rot und Blau zu vertauschen?

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

Wie gehen wir vor, wenn eine Bildverarbeitung erfolgen soll, bei der auch andere Pixel berücksichtigt werden? Da WebGL auf Texturen in Texturkoordinaten verweist, die von 0,0 bis 1,0 reichen, können wir mit der einfachen Berechnung onePixel = 1.0 / textureSize berechnen, wie viel sich um ein Pixel bewegen soll. Hier ist ein Fragment-Shader, mit dem der Durchschnitt der linken und rechten Pixel jedes Pixels in der Textur ermittelt wird.

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

Da wir nun wissen, wie auf andere Pixel verwiesen wird, können wir einen Faltungskernel verwenden, um eine Reihe gängiger Bildverarbeitungen durchzuführen. In diesem Fall verwenden wir einen 3x3-Kernel. Ein Faltungskernel ist nur eine 3x3-Matrix, wobei jeder Eintrag in der Matrix angibt, wie viel die 8 Pixel um das gerenderte Pixel 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 größer der Wert ist. Hier finden Sie einen ziemlich guten Artikel dazu. Hier ist ein weiterer Artikel, der echten Code zeigt, wenn Sie dies von Hand in C++ schreiben würden. In unserem Fall arbeiten wir im Shader, also 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 Faltungskernel bereitstellen.

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

Hoffentlich hat dies Sie davon überzeugt, dass die Bildverarbeitung in WebGL ziemlich einfach ist. Als Nächstes zeige ich Ihnen, wie Sie mehrere Effekte auf ein Bild anwenden können.

Was bedeutet die Präfixe a, u und v_ von Variablen in GLSL?

Das ist nur eine Namenskonvention. a_ für Attribute, bei denen es sich um die von Puffern bereitgestellten Daten handelt. u_ für Uniformen, die Eingaben an die Shader sind, v_ für Variationen, bei denen es sich um Werte handelt, die von einem Vertex-Shader an einen Fragment-Shader übergeben und zwischen den Eckpunkten für jedes gezeichnete Pixel interpoliert (oder variieren) werden.

Mehrere Effekte anwenden

Die nächste offensichtliche Frage bei der Bildverarbeitung ist: Wie lassen sich mehrere Effekte anwenden?

Nun, Sie könnten versuchen, Shader direkt zu generieren. Stellen Sie eine Benutzeroberfläche bereit, auf der Nutzende die gewünschten Effekte auswählen und dann einen Shader generieren können, der alle Effekte übernimmt. Das ist nicht immer möglich. Diese Technik wird jedoch häufig verwendet, um Effekte für Echtzeitgrafiken zu erzeugen. Flexibler ist die Verwendung von zwei weiteren Texturen, die der Reihe nach in jede Textur gerendert werden. Pingponging und jedes Mal der nächste Effekt.

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. Bei WebGL und OpenGL ist ein Framebuffer eigentlich ein schlechter Name. Ein WebGL/OpenGL-Framebuffer ist im Grunde nur eine Sammlung von Status und kein Puffer. Wenn wir jedoch eine Textur an einen Framebuffer anhängen, können wir diese Textur rendern. Wandeln wir zunächst 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);

Jetzt erstellen wir mit dieser Funktion zwei weitere Texturen und hängen sie an zwei Framebuffer an.

// 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 wir nun eine Reihe von Kernels und dann eine Liste der Kernel, die angewendet werden sollen.

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

Zum Schluss wenden wir die einzelnen Elemente an und legen fest, welche Textur wir auch rendern.

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

Auf einige Punkte sollte ich eingehen.

Wenn Sie gl.bindFramebuffer mit null aufrufen, teilen Sie WebGL mit, dass Sie im Canvas und nicht in einem Ihrer Framebuffer rendern möchten. WebGL muss von Zwischenabständen zurück in Pixel konvertieren. Dies geschieht auf Grundlage der Einstellungen von gl.viewport. Die Einstellungen von gl.viewport werden beim Initialisieren von WebGL standardmäßig auf die Größe des Canvas festgelegt. Da die Framebuffer, in die wir rendern, eine andere Größe haben als das Canvas, müssen wir den Darstellungsbereich entsprechend festlegen. In den Beispielen für die WebGL-Grundlagen haben wir schließlich die Y-Koordinate beim Rendern umgedreht, da WebGL das Canvas so anzeigt, dass 0,0 für die untere linke Ecke steht und nicht die traditionellere bei 2D oben links. Dies 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 Framepuffer in unseren Berechnungen 0,0 entspricht. Um dieses Problem zu lösen, habe ich festgelegt, ob umgedreht werden soll oder nicht, indem ich dem Shader eine weitere Eingabe hinzugefügt habe.

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

Diese können wir dann festlegen,

...
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 einziges GLSL-Programm verwendet habe, mit dem mehrere Effekte erzielt werden können. Wenn Sie die Bildverarbeitung vollständig durchführen möchten, benötigen Sie wahrscheinlich viele GLSL-Programme. Ein Programm zur Anpassung von Farbton, Sättigung und Helligkeit. einen für Helligkeit und Kontrast. eine zum Umkehren, eine zum Anpassen von Stufen usw. Du müsstest den Code ändern, um die GLSL-Programme zu wechseln und die Parameter für dieses spezielle Programm zu aktualisieren. Ich hätte überlegt, dieses Beispiel zu schreiben, aber es sollte am besten dem Leser überlassen werden, da mehrere GLSL-Programme mit jeweils eigenen Parameteranforderungen wahrscheinlich eine umfassende Refaktorierung erfordern, um zu verhindern, dass alles zu einem großen Durcheinander für Spaghetti wird. Ich hoffe, dass WebGL durch diese und die vorherigen Beispiele ein wenig verständlicher geworden ist, und hoffe, dass der Einstieg in 2D dazu beiträgt, WebGL etwas leichter verständlich zu machen. Wenn ich Zeit habe, würde ich gerne ein paar weitere Artikel dazu schreiben, wie 3D funktioniert und was WebGL eigentlich alles im Hintergrund tut.

WebGL und Alpha

Ich habe festgestellt, dass einige OpenGL-Entwickler Probleme damit haben, wie WebGL die Alphaversion im Backbuffer (d. h. im Canvas) handhabt. Daher dachte ich, es wäre hilfreich, 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 rendert, der nicht so oder vom Fenstermanager des Betriebssystems mit etwas zusammengesetzt ist. Es spielt also keine Rolle, was die Alphaversion ist. WebGL wird vom Browser mit der Webseite zusammengesetzt. Standardmäßig werden vorab multiplizierte Alpha-Tags wie .png-<img>-Tags mit Transparenz und .2d-Tags verwendet. WebGL bietet mehrere Möglichkeiten, dies ähnlich wie OpenGL zu gestalten.

1. Angeben, dass WebGL mit nicht vorab multipliziertem Alpha kombiniert werden soll

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

Die Standardeinstellung ist "true". Natürlich wird das Ergebnis weiterhin über die Seite zusammengesetzt und zwar mit der Hintergrundfarbe, die schließlich unter dem Canvas liegt (die Hintergrundfarbe des Canvas, die Containerhintergrundfarbe des Canvas, die Hintergrundfarbe der Seite, der Inhalt hinter dem Canvas, falls der Canvas einen Z-Index > 0 hat usw.). Mit anderen Worten, die CSS-Farbe definiert für diesen Bereich der Webseite. Ich kann sehr gut herausfinden, ob Sie Alphaprobleme haben, indem Sie den Hintergrund des Canvas auf eine leuchtende Farbe wie Rot einstellen. Du siehst sofort, was passiert.

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

Du kannst sie auch auf Schwarz setzen. Dadurch werden alle Alphaprobleme ausgeblendet.

2. Angeben, dass WebGL im Backbuffer keine Alpha-Funktion erhalten soll

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

Sie verhält sich dann ähnlich wie OpenGL, da der Backbuffer nur RGB verwendet. Dies ist wahrscheinlich die beste Option, da ein guter Browser erkennen könnte, dass Sie keine Alphaversion haben, und die Art der Zusammensetzung von WebGL optimieren. Das bedeutet natürlich auch, dass Alpha im Backpuffer nicht vorhanden ist. Wenn Sie also Alpha im Backbuffer für einen Zweck verwenden, der für Sie möglicherweise nicht funktioniert. Wenige Apps, von denen ich weiß, dass sie im Backbuffer Alpha verwenden. Ich denke, das hätte die Standardeinstellung sein sollen.

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 erfolgt in der Regel sehr schnell, da es bei den meisten Hardware dafür einen Sonderfall gibt. Das habe ich bei den meisten meiner Demos gemacht. Wenn ich schlau wäre, würde ich zur Methode Nr. 2 oben wechseln. Vielleicht mache ich das, nachdem ich dies gepostet habe. Anscheinend wird diese Methode für die meisten WebGL-Bibliotheken standardmäßig verwendet. Die wenigen Entwickler, die tatsächlich Alpha zum Komponieren von Effekten verwenden, können danach fragen. Der Rest liefert nur die beste Leistung und die geringsten Überraschungen.

4) Alpha einmal löschen und dann nicht mehr 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 mit Ihren eigenen Framebuffern rendern, müssen Sie natürlich zuerst den Alphamodus aktivieren und dann wieder deaktivieren, wenn Sie zum Canvas-Rendering wechseln.

5. Umgang mit Bildern

Wenn Sie PNG-Dateien mit Alpha in Texturen laden, wird der Alphawert standardmäßig vorab multipliziert, was in der Regel NICHT so ist, wie die meisten Spiele vorgehen. Um dies zu verhindern, müssen Sie WebGL mit

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6) Verwendung einer Mischgleichung, die mit vorab multipliziertem Alpha funktioniert

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

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Das funktioniert bei nicht vorab multiplizierten Alpha-Texturen. Wenn Sie tatsächlich mit vorab multiplizierten Alpha-Texturen arbeiten möchten,

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Diese Methoden sind mir bekannt. Sollten Sie weitere Fragen haben, können Sie sie unten posten.