Mit WebGL können Sie beeindruckende 3D-Grafiken in Echtzeit in Ihrem Browser anzeigen. Was viele jedoch nicht wissen: WebGL ist eigentlich eine 2D-API, keine 3D-API. Lassen Sie mich das erklären.
WebGL kümmert sich nur um zwei Dinge. Clipspace-Koordinaten in 2D und Farben. Ihre Aufgabe als Programmierer, der WebGL verwendet, besteht darin, WebGL diese beiden Dinge bereitzustellen. Dazu stellen Sie zwei „Shader“ bereit. Ein Vertex-Shader, der die Clipspace-Koordinaten bereitstellt, 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>
Nochmal: Clipspace-Koordinaten reichen immer von -1 bis +1, unabhängig von der Größe des Canvas. Im obigen Fall werden nur die Positionsdaten direkt weitergegeben. Da die Positionsdaten bereits im Clipspace vorliegen, ist nichts weiter zu tun. Wenn Sie 3D-Grafiken verwenden möchten, müssen Sie Shader bereitstellen, die von 3D in 2D konvertieren, da WebGL eine 2D-API ist. Für 2D-Inhalte arbeiten Sie wahrscheinlich lieber mit Pixeln als mit dem Clipspace. Ändern wir also den Shader, damit wir Rechtecke in Pixeln angeben können und diese für uns in den Clipspace konvertiert werden. 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 im unteren Bereich. In WebGL ist die linke untere Ecke 0,0. Um den Ursprung in die obere linke Ecke zu verschieben, wie es bei 2D-Grafik-APIs üblich ist, 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. Bei dieser Gelegenheit machen wir auch die Farbe einstellbar. Zuerst lassen wir den Fragment-Shader eine einheitliche Farbeingabe annehmen.
<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, der 50 Rechtecke an zufälligen Orten und in zufälligen Farben zeichnet.
...
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 haben gesehen, dass WebGL eigentlich eine ziemlich einfache API ist. 3D-Grafiken können zwar komplizierter sein, 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.
What do type="x-shader/x-vertex" and type="x-shader/x-fragment" mean?
<script>-Tags enthalten standardmäßig JavaScript. Sie können keinen Typ oder type="javascript" oder type="text/javascript" angeben. Der Browser interpretiert den Inhalt dann als JavaScript. Wenn Sie etwas anderes eingeben, ignoriert der Browser den Inhalt des Script-Tags.
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 Fragment-Shader kompiliert werden soll.
In diesem Fall sucht die Funktion createShaderFromScriptElement nach einem Script mit der angegebenen id und prüft dann den type, um zu entscheiden, welcher Shadertyp erstellt werden soll.
WebGL-Bildverarbeitung
Die Bildverarbeitung ist in WebGL einfach. Wie einfach? Weitere Informationen finden Sie unten.
Um Bilder in WebGL zu zeichnen, müssen wir Texturen verwenden. Ähnlich wie WebGL beim Rendern Clipspace-Koordinaten anstelle von Pixeln erwartet, erwartet WebGL beim Lesen einer Textur Texturkoordinaten. Die Texturkoordinaten reichen von 0,0 bis 1,0, unabhängig von den Abmessungen der Textur. Da wir nur ein einzelnes Rechteck (bzw. zwei Dreiecke) zeichnen, müssen wir WebGL mitteilen, welcher Stelle in der Textur jeder Punkt im Rechteck entspricht. Wir übergeben diese Informationen vom Vertex-Shader an den Fragment-Shader. Dazu verwenden wir eine spezielle Art von Variable, die als „varying“ bezeichnet wird. Sie wird als „varying“ bezeichnet, weil sie variiert. WebGL interpoliert die Werte, die wir im Vertex-Shader angeben, während es jedes Pixel mit dem Fragment-Shader zeichnet. Wir verwenden den Vertex-Shader aus dem vorherigen Abschnitt und fügen ein Attribut hinzu, um Texturkoordinaten zu übergeben und diese 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>
Schließlich 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. Wir müssen also unseren Code ein wenig anpassen, damit auf das Laden der Textur gewartet wird. 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);
...
}
Nicht besonders spannend, also bearbeiten wir das Bild. Wie wäre es, wenn wir einfach Rot und Blau vertauschen?
...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...
Was, wenn wir eine Bildverarbeitung durchführen möchten, bei der auch andere Pixel berücksichtigt werden? Da WebGL Texturen in Texturkoordinaten von 0,0 bis 1,0 referenziert, können wir mit der einfachen Formel onePixel = 1.0 / textureSize berechnen, wie weit wir uns für 1 Pixel bewegen müssen.
Hier ist ein Fragment-Shader, der den Durchschnitt der linken und rechten Pixel jedes Pixels in der Textur berechnet.
<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 Faltungskern, um eine Reihe gängiger Bildverarbeitungen durchzuführen. In diesem Fall verwenden wir einen 3x3-Kernel. Ein Faltungskernel ist eine 3x3-Matrix, in der jeder Eintrag angibt, wie stark die 8 Pixel um das zu rendernde Pixel multipliziert werden sollen. Anschließend dividieren wir das Ergebnis durch das Gewicht des Kernels (die Summe aller Werte im Kernel) oder 1,0, je nachdem, welcher Wert größer ist. Hier ist ein guter Artikel dazu. In diesem Artikel finden Sie den tatsächlichen Code, wenn Sie ihn manuell in C++ schreiben würden. 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 Faltungskernel 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 ein Bild anwenden können.
Was hat es mit den Präfixen a, u und v_ vor Variablen in GLSL auf sich?
Das ist nur eine Namenskonvention. a_ für Attribute, also die von Puffern bereitgestellten Daten. u_ für Uniforms, die als Eingaben für die Shader dienen, v_ für Varyings, die als Werte von einem Vertex-Shader an einen Fragment-Shader übergeben und zwischen den Vertices für jedes gezeichnete Pixel interpoliert (oder variiert) werden.
Mehrere Effekte anwenden
Die nächste Frage, die sich bei der Bildverarbeitung stellt, ist, wie mehrere Effekte angewendet werden können.
Sie könnten versuchen, Shader spontan zu generieren. Stellen Sie eine Benutzeroberfläche bereit, über die der Nutzer die gewünschten Effekte auswählen kann, und generieren Sie dann einen Shader, der alle Effekte ausführt. Das ist zwar nicht immer möglich, aber diese Technik wird häufig verwendet, um Effekte für Echtzeitgrafiken zu erstellen. Eine flexiblere Methode besteht darin, zwei weitere Texturen zu verwenden und nacheinander in jede Textur zu rendern, wobei die Texturen abwechselnd verwendet werden und jedes Mal der nächste Effekt angewendet wird.
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 „Framebuffer“ eigentlich ein schlechter Name. Ein WebGL-/OpenGL-Framebuffer ist eigentlich nur eine Sammlung von Zuständen und kein tatsächlicher Zwischenspeicher. Wenn wir jedoch eine Textur an einen Framebuffer anhängen, können wir in diese Textur rendern. Zuerst 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);
Und jetzt verwenden wir diese Funktion, um zwei weitere Texturen zu erstellen und sie 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);
}
Jetzt erstellen wir eine Reihe von Kernels und dann eine Liste, um sie anzuwenden.
// 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 jede Textur an und wechseln zwischen den Texturen, die wir 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);
}
Es gibt ein paar Dinge, die ich mit dir besprechen möchte.
Wenn Sie gl.bindFramebuffer mit null aufrufen, wird WebGL mitgeteilt, dass Sie auf dem Canvas statt auf einem Ihrer Framebuffer rendern möchten.
WebGL muss die Clipspace-Koordinaten wieder in Pixel umwandeln. 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 wir rendern, eine andere Größe haben als das Canvas, müssen wir den Darstellungsbereich entsprechend festlegen.
In den Beispielen zu den WebGL-Grundlagen haben wir die Y-Koordinate beim Rendern umgedreht, weil WebGL die Leinwand so darstellt, dass 0,0 die linke untere Ecke ist, anstatt der für 2D üblicheren linken oberen Ecke. 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 dieses Problem zu beheben, habe ich eine Möglichkeit geschaffen, festzulegen, ob die Textur 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>
Und dann können wir sie festlegen, wenn wir mit
...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);
Dieses Beispiel ist einfach gehalten, da ein einzelnes GLSL-Programm verwendet wird, 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 zum Anpassen von Farbton, Sättigung und Luminanz. Eine weitere für Helligkeit und Kontrast. Eines zum Invertieren, eines zum Anpassen der Pegel usw. Sie müssten den Code ändern, um zwischen GLSL-Programmen 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 größere Refaktorierung erfordern, um zu verhindern, dass alles zu einem großen Durcheinander wird. Ich hoffe, dass dieses und die vorherigen Beispiele WebGL etwas zugänglicher gemacht haben und dass der Einstieg mit 2D-Grafiken dazu beiträgt, WebGL etwas leichter verständlich zu machen. Wenn ich Zeit habe, werde ich versuchen, noch ein paar Artikel über die Erstellung von 3D-Inhalten zu schreiben und mehr Details darüber zu liefern, was WebGL wirklich im Hintergrund macht.
WebGL und Alpha
Ich habe festgestellt, dass einige OpenGL-Entwickler Probleme damit haben, wie WebGL Alpha im Backbuffer (also im Canvas) behandelt. Deshalb dachte ich, es wäre gut, einige der Unterschiede zwischen WebGL und OpenGL in Bezug auf Alpha zu erläutern.
Der größte Unterschied zwischen OpenGL und WebGL besteht darin, dass OpenGL in einen Backbuffer rendert, der nicht mit anderen Elementen zusammengesetzt wird, oder zumindest nicht vom Fenstermanager des Betriebssystems. Daher spielt es keine Rolle, welchen Alphawert Sie verwenden.
WebGL wird vom Browser mit der Webseite zusammengesetzt. Standardmäßig wird ein vormultiplizierter Alphawert verwendet, genau wie bei PNG-<img>-Tags mit Transparenz und 2D-Canvas-Tags.
WebGL bietet mehrere Möglichkeiten, dies OpenGL ähnlicher zu machen.
#1. WebGL mitteilen, dass es mit nicht vormultipliziertem Alpha-Wert zusammengesetzt werden soll
gl = canvas.getContext("experimental-webgl", {premultipliedAlpha: false});
Die Standardeinstellung ist "true". Natürlich wird das Ergebnis weiterhin über die Seite gelegt, wobei die Hintergrundfarbe unter dem Canvas (die Hintergrundfarbe des Canvas, die Hintergrundfarbe des Canvas-Containers, die Hintergrundfarbe der Seite, die Elemente hinter dem Canvas, wenn der Canvas einen Z-Index > 0 hat usw.) verwendet wird. Mit anderen Worten: die Farbe, die CSS für diesen Bereich der Webseite definiert. Eine gute Möglichkeit, um herauszufinden, ob es Probleme mit dem Alphakanal gibt, besteht darin, den Hintergrund des Canvas auf eine helle Farbe wie Rot festzulegen. Sie sehen sofort, was passiert.
<canvas style="background: red;"></canvas>
Sie können auch Schwarz festlegen, um alle Alpha-Probleme auszublenden.
#2. WebGL mitteilen, dass im Backbuffer kein Alpha-Wert gewünscht ist
gl = canvas.getContext("experimental-webgl", {alpha: false});
Dadurch verhält es sich eher wie OpenGL, da der Backbuffer nur RGB enthält. Das ist wahrscheinlich die beste Option, da ein guter Browser erkennen kann, dass kein Alphakanal vorhanden ist, und die Art und Weise, wie WebGL zusammengesetzt wird, optimieren kann. Das bedeutet natürlich auch, dass im Backbuffer kein Alpha vorhanden ist. Wenn Sie also Alpha im Backbuffer für einen bestimmten Zweck verwenden, funktioniert das möglicherweise nicht. Mir sind nur wenige Apps bekannt, die Alpha im Backbuffer verwenden. Meiner Meinung nach hätte das die Standardeinstellung sein sollen.
3. Alphakanal 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 geht in der Regel sehr schnell, da es bei den meisten Hardwarekomponenten einen Sonderfall dafür gibt. Das habe ich in den meisten meiner Demos gemacht. Wenn ich schlau wäre, würde ich zu Methode 2 wechseln. Vielleicht mache ich das gleich, nachdem ich das hier gepostet habe. Die meisten WebGL-Bibliotheken sollten diese Methode standardmäßig verwenden. Die wenigen Entwickler, die Alpha tatsächlich für Compositing-Effekte verwenden, können danach fragen. Alle anderen erhalten einfach die beste Leistung und die wenigsten Ü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 in Ihren eigenen Framebuffern rendern, müssen Sie das Rendern in Alpha möglicherweise wieder aktivieren und dann wieder deaktivieren, wenn Sie zum Rendern im Canvas wechseln.
5. Umgang mit Bildern
Wenn Sie PNG-Dateien mit Alpha in Texturen laden, wird das Alpha 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. Verwendung einer Mischgleichung, die mit vormultipliziertem Alpha funktioniert
Fast alle OpenGL-Anwendungen, die ich geschrieben oder an denen ich mitgearbeitet habe, verwenden
gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);
Das funktioniert bei nicht vormultiplizierten Alpha-Texturen. Wenn Sie tatsächlich mit vormultiplizierten Alpha-Texturen arbeiten möchten, sollten Sie wahrscheinlich
gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);
Das sind die Methoden, die mir bekannt sind. Wenn Sie weitere kennen, posten Sie sie bitte unten.