Podstawy WebGL
WebGL umożliwia wyświetlanie w przeglądarce niesamowitych grafik 3D w czasie rzeczywistym, ale wiele osób nie wie, że jest to interfejs API 2D, a nie 3D. Dlaczego tak się dzieje?
WebGL zwraca uwagę tylko na 2 rzeczy. współrzędne przestrzeni rzutowania w 2D i kolory. Twoim zadaniem jako programisty korzystającego z WebGL jest zapewnienie tych 2 elementów. W tym celu musisz przesłać 2 „shadery”. Shader wierzchołków, który dostarcza współrzędnych przestrzeni ujętej w ramki, oraz shader fragmentów, który dostarcza koloru. współrzędne przestrzeni ujętej w ramki zawsze mieszczą się w zakresie od -1 do +1, niezależnie od rozmiaru Twojego płótna. Oto prosty przykład WebGL, który pokazuje tę bibliotekę w jej najprostszej formie.
// 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);
Oto 2 shadery
<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>
Współrzędne przestrzeni klipu zawsze mieszczą się w zakresie od –1 do +1 niezależnie od rozmiaru płótna. Jak widać w powyższym przykładzie, nie robimy nic innego, tylko przekazujemy bezpośrednio dane o pozycji. Ponieważ dane o pozycji znajdują się już w przestrzeni klipu, nie trzeba niczego robić. Jeśli chcesz uzyskać obraz 3D, musisz dostarczyć shadery, które przekształcą obraz 3D w 2D, ponieważ WebGL to interfejs API 2D. W przypadku elementów 2D prawdopodobnie wolisz pracować w pikselach, a nie w przestrzeni clip. Zmieńmy więc shader, abyśmy mogli dostarczać prostokąty w pikselach, a on przekształcał je w przestrzeń clip. Oto nowy shader wierzchołkowy
<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>
Teraz możemy zmienić dane z clipspace na piksele
// 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);
Możesz zauważyć, że prostokąt znajduje się u dołu tego obszaru. W WebGL lewy dolny narożnik ma współrzędne 0,0. Aby uzyskać bardziej tradycyjny róg górny lewy używany w interfejsach API grafiki 2D, wystarczy odwrócić współrzędną y.
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
Zmieńmy kod definiujący prostokąt na funkcję, abyśmy mogli go wywoływać w przypadku prostokątów o różnych rozmiarach. Przy okazji umożliwimy też zmianę koloru. Najpierw spowodujemy, aby fragment shadera przyjmował kolor jednolity.
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
</script>
A tutaj jest nowy kod, który rysuje 50 prostokątów w losowych miejscach i losowych kolorach.
...
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);
}
Mam nadzieję, że widzisz, że WebGL jest w istocie dość prostym interfejsem API. Praca nad grafiką 3D może być bardziej skomplikowana, ale to Ty, programista, dodajesz te komplikacje w postaci bardziej złożonych shaderów. Interfejs API WebGL jest dwuwymiarowy i bardzo prosty.
Co oznaczają opcje type="x-shader/x-vertex" i type="x-shader/x-fragment"?
Tagi <script>
domyślnie zawierają kod JavaScript. Możesz użyć typu „none” (żaden) lub „type="javascript"
” (lub „type="text/javascript"
”), a przeglądarka zinterpretuje zawartość jako JavaScript. Jeśli wpiszesz coś innego, przeglądarka zignoruje zawartość tagu skryptu.
Możemy używać tej funkcji do przechowywania shaderów w tagach skryptu. Co więcej, możemy utworzyć własny typ i w naszym kodzie JavaScriptu sprawdzić, czy skompilować shader jako shader wierzchołkowy czy fragmentowy.
W tym przypadku funkcja createShaderFromScriptElement
wyszukuje skrypt o określonym id
, a potem sprawdza type
, aby zdecydować, jaki typ shadera utworzyć.
Przetwarzanie obrazu w WebGL
Przetwarzanie obrazu jest łatwe w WebGL. Jak łatwo? Więcej informacji znajdziesz poniżej.
Aby rysować obrazy w WebGL, musimy używać tekstur. Podobnie jak w przypadku renderowania, gdy zamiast pikseli WebGL oczekuje współrzędnych przestrzeni ujętej w ramki, tak samo podczas odczytu tekstury oczekuje współrzędnych tekstury. Współrzędne tekstury przyjmują wartości od 0,0 do 1,0 niezależnie od wymiarów tekstury. Rysujemy tylko jeden prostokąt (czyli 2 trójkąty), więc musimy podać WebGL, któremu miejscu w teksturze odpowiada każdy punkt prostokąta. Przekażemy te informacje z shadera wierzchołkowego do shadera fragmentowego, używając specjalnego typu zmiennej o nazwie „zmienna”. Nazywa się to zmienną, ponieważ się zmienia. WebGL będzie interpolować wartości podane w shaderze wierzchołka, gdy będzie rysować każdy piksel za pomocą shadera fragmentu. W shaderze wierzchołka z końca poprzedniej sekcji musimy dodać atrybut, aby przekazywać współrzędne tekstury, a potem przekazywać je do shadera fragmentu.
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;
}
Następnie dostarczamy fragment shadera, aby wyszukiwać kolory z tekstury.
<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>
Na koniec musimy załadować obraz, utworzyć teksturę i skopiować obraz do tekstury. Obrazy wczytują się asynchronicznie, więc musimy nieco zmienić kod, aby poczekać na załadowanie tekstury. Gdy się załaduje, narysujemy go.
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);
...
}
Nie jest to zbyt ekscytujące, więc zmodyfikujmy ten obraz. Może po prostu zamienić kolory na czerwony i niebieski?
...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...
Co zrobić, jeśli chcemy przetworzyć obraz, który uwzględnia inne piksele? Ponieważ WebGL odwołuje się do tekstur w układzie współrzędnych tekstur, który zawiera wartości od 0,0 do 1,0, możemy obliczyć, o ile przesunąć obraz o 1 piksel, wykonując proste obliczenia onePixel = 1.0 / textureSize
.
Oto fragment shadera, który oblicza średnią z lewych i prawych pikseli każdego piksela w teksturze.
<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>
Następnie musimy przekazać rozmiar tekstury z JavaScriptu.
...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height);
...
Teraz, gdy już wiemy, jak odwoływać się do innych pikseli, użyjemy jądra konwolucji do wykonania kilku typowych operacji przetwarzania obrazu. W tym przypadku użyjemy jądra 3 x 3. Kernel konwolucji to po prostu macierz 3 x 3, w której każdy element reprezentuje mnożnik dla 8 pikseli wokół piksela, który renderujemy. Następnie dzielimy wynik przez wagę jądra (sumę wszystkich wartości w jądrze) lub 1,0 – zależnie od tego, która wartość jest większa. Oto przydatny artykuł na ten temat Tutaj znajdziesz kolejny artykuł, który zawiera przykładowy kod C++ do samodzielnego napisania ręcznie. W naszym przypadku wykonamy to w shaderze. Oto nowy shader fragmentów.
<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>
W JavaScript musimy podać jądro konwolucji.
...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
...
var edgeDetectKernel = [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
...
Mam nadzieję, że udało mi się przekonać Cię, że przetwarzanie obrazu w WebGL jest dość proste. Teraz pokażę, jak zastosować do obrazu więcej niż 1 efekt.
Co oznaczają prefiksy a, u i v_ w przypadku zmiennych w GLSL?
To tylko konwencja nazewnictwa. a_
dla atrybutów, które są danymi dostarczanymi przez bufory. u_
dla uniformów, które są wejściami dla shaderów, v_
dla zmiennych, które są wartościami przekazywanymi z shadera wierzchołkowego do shadera fragmentowego i interpolowanych (lub zróżnicowanych) między wierzchołkami dla każdego rysowanego piksela.
Stosowanie wielu efektów
Kolejne oczywiste pytanie dotyczące przetwarzania obrazu brzmi: jak zastosować wiele efektów?
Możesz spróbować wygenerować shadery w locie. Udostępnij interfejs, który pozwoli użytkownikowi wybrać efekty, których chce użyć, a potem wygeneruj shader, który będzie stosować wszystkie efekty. Nie zawsze jest to możliwe, ale ta technika jest często używana do tworzenia efektów dla grafiki w czasie rzeczywistym. Bardziej elastycznym sposobem jest użycie 2 dodatkowych tekstur i renderowanie każdej z nich z kolei, przechodząc od jednej do drugiej i za każdym razem stosując kolejny efekt.
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
Aby to zrobić, musimy utworzyć framebuffery. W WebGL i OpenGL nazwa Framebuffer jest niezbyt trafna. Bufor framebuffera WebGL/OpenGL to tylko zbiór stanów, a nie bufor dowolnego typu. Jednak dołączając teksturę do framebuffera, możemy ją renderować. Najpierw przekształcimy stary kod do tworzenia tekstur w funkcję.
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);
Teraz użyjemy tej funkcji, aby utworzyć 2 tekstury i dołączyć je do 2 ramek.
// 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);
}
Teraz utwórzmy zestaw rdzeni, a potem listę, na której je zastosujemy.
// 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"
];
I na koniec zastosujmy każdą z nich, przełączając się między teksturami, które renderujemy
// 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);
}
Mam kilka spraw do omówienia.
Wywołanie funkcji gl.bindFramebuffer
z argumentem null
informuje WebGL, że chcesz renderować na płótnie zamiast na jednym z ramek.
WebGL musi przekształcić przestrzeń ujętą w ramki z powrotem w piksele. Robi to na podstawie ustawień gl.viewport
. Ustawienia gl.viewport
domyślnie przyjmują rozmiar rysunku podczas inicjowania WebGL. Ponieważ framebuffery, które renderujemy, mają inny rozmiar niż płótno, musimy odpowiednio ustawić widoczny obszar.
W przypadku przykładów podstawowych zagadnień dotyczących WebGL odwróciliśmy podczas renderowania współrzędną Y, ponieważ WebGL wyświetla płótno z wartością 0,0 w lewym dolnym rogu zamiast w lewym górnym rogu, jak to jest w przypadku tradycyjnych obrazów 2D. Nie jest to konieczne podczas renderowania do framebuffera. Ponieważ framebuffer nigdy nie jest wyświetlany, nie ma znaczenia, która część jest górą, a która – dołem. Ważne jest tylko to, aby piksel 0,0 w ramce framebuffera odpowiadał 0,0 w naszych obliczeniach. Aby rozwiązać ten problem, umożliwiłem ustawienie, czy ma być odwrócone, czy nie, przez dodanie jeszcze jednego wejścia do shadera.
<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>
Następnie możemy go ustawić podczas renderowania za pomocą
...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);
W tym przykładzie zachowałem prostotę, używając pojedynczego programu GLSL, który może wywołać wiele efektów. Jeśli chcesz przeprowadzić pełne przetwarzanie obrazu, prawdopodobnie będziesz potrzebować wielu programów GLSL. Program do regulacji odcienia, nasycenia i jasności. Kolejna opcja dotyczy jasności i kontrastu. Jeden do odwracania, drugi do dostosowywania poziomów itp. Musisz zmienić kod, aby przełączyć programy GLSL i zaktualizować parametry danego programu. Rozważałem napisanie tego przykładu, ale lepiej zostawić to ćwiczenie czytelnikowi, ponieważ wiele programów GLSL z własnymi parametrami wymagałoby prawdopodobnie poważnego refaktoryzacji, aby nie stało się to wielką plątaniną. Mam nadzieję, że ten i poprzednie przykłady sprawiły, że WebGL stał się nieco bardziej przystępny. Zacznij od 2D, aby ułatwić sobie zrozumienie WebGL. Jeśli znajdę czas, napiszę jeszcze kilka artykułów o tym, jak tworzyć grafikę 3D, oraz o tym, co tak naprawdę dzieje się w WebGL.
WebGL i alfa
Zauważyłem, że niektórzy deweloperzy OpenGL mają problemy z tym, jak WebGL traktuje kanał alfa w backbufferze (czyli na płótnie), więc pomyślałem, że warto omówić niektóre różnice między WebGL a OpenGL dotyczące kanału alfa.
Największa różnica między OpenGL a WebGL polega na tym, że OpenGL renderuje do bufora podrzędnego, który nie jest łączony z niczym przez menedżera okien systemu operacyjnego, więc nie ma znaczenia, jaka jest wartość alfa. WebGL jest łączony przez przeglądarkę z stroną internetową, a domyślnie używa premultiplikowanej wartości alfa, tak samo jak tagi <img>
w pliku .png z przezroczystością i tagi 2D canvas.
WebGL umożliwia na kilka sposobów upodobnienie tego do OpenGL.
#1) Powiedz WebGL, że chcesz go złożyć z niespremultiplikowaną wartością alfa.
gl = canvas.getContext("experimental-webgl", {premultipliedAlpha: false});
Wartość domyślna to prawda (true). Oczywiście wynik nadal będzie złożony na stronie z dowolnym kolorem tła, który znajduje się pod kanwą (kolor tła kanwy, kolor tła kontenera kanwy, kolor tła strony, elementy za kanwą, jeśli kanwa ma kolejność nakładania elementów > 0 itd.). Innymi słowy, kolor CSS jest definiowany dla danego obszaru strony internetowej. Dobrym sposobem na sprawdzenie, czy masz problemy z przezroczystością, jest ustawienie tła obrazu na jasny kolor, np. czerwony. Natychmiast zobaczysz, co się dzieje.
<canvas style="background: red;"></canvas>
Możesz też ustawić czarny kolor, który ukryje wszystkie problemy z alfa.
#2) Poinformuj WebGL, że nie chcesz alfa w buforze pomocniczym
gl = canvas.getContext("experimental-webgl", {alpha: false});
Dzięki temu będzie on działać bardziej jak OpenGL, ponieważ backbuffer będzie miał tylko RGB. Jest to prawdopodobnie najlepsza opcja, ponieważ dobra przeglądarka może wykryć, że nie masz kanału alfa, i optymalizować sposób łączenia WebGL. Oznacza to też, że w backbufferze nie będzie alfa kanału, więc jeśli używasz go w jakimś celu, może to nie działać. Z tego, co wiem, niewiele aplikacji używa przezroczystości w backbufferze. Myślę, że to powinno być ustawienie domyślne.
#3) Wyczyść kanał alfa na końcu renderowania.
..
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);
Czyszczenie jest zazwyczaj bardzo szybkie, ponieważ w większości urządzeń jest do tego przeznaczony specjalny przypadek. Wykorzystałam to w większości moich prezentacji. Jeśli byłbym mądry, wybrałbym metodę 2. Może zrobię to zaraz po wysłaniu tego posta. Wygląda na to, że większość bibliotek WebGL powinna domyślnie używać tej metody. Możesz poprosić o to tych nielicznych deweloperów, którzy faktycznie używają przezroczystości do tworzenia efektów. Pozostałe będą miały najlepszą wydajność i najmniej niespodzianek.
#4) Wyczyść alfa raz, a potem nie renderuj już do niego
// 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);
Oczywiście, jeśli renderujesz do własnych framebufferów, może być konieczne ponowne włączenie renderowania do alfa i jeszcze raz wyłączenie go, gdy przełączysz się na renderowanie do kanwy.
#5) Obsługa obrazów
Jeśli wczytujesz pliki PNG z kanałem alfa do tekstur, domyślnie ich kanał alfa jest wstępnie pomnożony, co nie jest stosowane w większości gier. Jeśli chcesz temu zapobiec, musisz użyć WebGL z pomocą
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
#6) Używanie równania mieszania, które działa z wielokrotnikiem alfa
Prawie wszystkie aplikacje OpenGL, które napisałem lub nad którymi pracowałem, używają
gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);
Działa to w przypadku tekstur z alfa-kanałem, które nie zostały zwielokrotnione. Jeśli chcesz pracować z teksturami alfa z wstępnie pomnożoną przezroczystością, prawdopodobnie
gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);
To są metody, o których wiem. Jeśli znasz więcej, podaj je poniżej.