WebGL을 사용하면 브라우저에서 놀라운 실시간 3D 그래픽을 표시할 수 있지만, 많은 사람들이 WebGL이 실제로 3D API가 아닌 2D API라는 사실을 모릅니다. 설명해 드리겠습니다.
WebGL은 두 가지만 신경 씁니다. 2D 및 색상의 클립 공간 좌표입니다. WebGL을 사용하는 프로그래머의 역할은 WebGL에 이 두 가지를 제공하는 것입니다. 이를 위해 2개의 '셰이더'를 제공합니다. 클립스페이스 좌표를 제공하는 버텍스 셰이더와 색상을 제공하는 프래그먼트 셰이더입니다. 클립스페이스 좌표는 캔버스 크기와 관계없이 항상 -1에서 +1까지입니다. 다음은 가장 간단한 형태의 WebGL을 보여주는 간단한 WebGL 예입니다.
// 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);
셰이더 2개
<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>
다시 말하지만, 클립 공간 좌표는 캔버스의 크기와 관계없이 항상 -1에서 +1까지입니다. 위의 경우 위치 데이터를 직접 전달하는 것 외에는 아무것도 하지 않는 것을 볼 수 있습니다. 위치 데이터가 이미 클립 공간에 있으므로 할 일이 없습니다. 3D를 원한다면 3D에서 2D로 변환하는 셰이더를 제공해야 합니다. WebGL은 2D API이기 때문입니다. 2D의 경우 클립 공간보다는 픽셀로 작업하는 것이 더 좋으므로, 픽셀 단위로 사각형을 제공하고 이를 클립 공간으로 변환할 수 있도록 셰이더를 변경해 보겠습니다. 새 꼭짓점 셰이더는 다음과 같습니다.
<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>
이제 데이터를 클립스페이스에서 픽셀로 변경할 수 있습니다.
// 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);
직사각형이 해당 영역의 하단 근처에 있는 것을 확인할 수 있습니다. WebGL은 왼쪽 하단 모서리를 0,0으로 간주합니다. 2D 그래픽 API에 사용되는 보다 전통적인 왼쪽 상단 모서리로 가져오려면 y 좌표를 뒤집으면 됩니다.
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
직사각형을 정의하는 코드를 함수로 만들어 다양한 크기의 직사각형에 대해 호출할 수 있도록 하겠습니다. 이 작업을 하는 동안 색상을 설정할 수 있도록 하겠습니다. 먼저 프래그먼트 셰이더가 색상 유니폼 입력을 받도록 합니다.
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
</script>
그리고 여기 임의의 위치에 임의의 색상으로 50개의 직사각형을 그리는 새로운 코드가 있습니다.
...
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);
}
WebGL이 실제로 매우 간단한 API라는 것을 알 수 있습니다. 3D를 구현하는 것이 더 복잡해질 수 있지만, 이러한 복잡성은 프로그래머가 더 복잡한 셰이더의 형태로 추가하는 것입니다. WebGL API 자체는 2D이며 상당히 간단합니다.
<p>type="x-shader/x-vertex" 및 type="x-shader/x-fragment"는 무엇을 의미하나요?</p>
<script> 태그에는 기본적으로 JavaScript가 포함되어 있습니다. 유형을 입력하지 않거나 type="javascript" 또는 type="text/javascript"를 입력하면 브라우저가 콘텐츠를 JavaScript로 해석합니다. 다른 항목을 입력하면 브라우저에서 스크립트 태그의 콘텐츠를 무시합니다.
이 기능을 사용하여 스크립트 태그에 셰이더를 저장할 수 있습니다. 더 좋은 점은 자체 유형을 만들고 JavaScript에서 이를 찾아 셰이더를 꼭짓점 셰이더로 컴파일할지 프래그먼트 셰이더로 컴파일할지 결정할 수 있다는 것입니다.
이 경우 createShaderFromScriptElement 함수는 지정된 id가 있는 스크립트를 찾은 다음 type를 확인하여 생성할 셰이더 유형을 결정합니다.
WebGL 이미지 처리
WebGL에서는 이미지 처리가 쉽습니다. 얼마나 쉬운가요? 아래 내용을 참고하세요.
WebGL에서 이미지를 그리려면 텍스처를 사용해야 합니다. WebGL이 렌더링할 때 픽셀 대신 클립 공간 좌표를 예상하는 것과 마찬가지로, WebGL은 텍스처를 읽을 때 텍스처 좌표를 예상합니다. 텍스처 좌표는 텍스처의 크기와 관계없이 0.0에서 1.0까지입니다. 단일 직사각형 (삼각형 2개)만 그리고 있으므로 직사각형의 각 점이 텍스처의 어느 위치에 해당하는지 WebGL에 알려야 합니다. 'varying'이라는 특수한 유형의 변수를 사용하여 이 정보를 정점 셰이더에서 프래그먼트 셰이더로 전달합니다. 변화하기 때문에 변화라고 합니다. WebGL은 프래그먼트 셰이더를 사용하여 각 픽셀을 그릴 때 정점 셰이더에 제공하는 값을 보간합니다. 이전 섹션 끝부분의 정점 셰이더를 사용하여 텍스처 좌표를 전달하는 속성을 추가한 다음 이를 프래그먼트 셰이더에 전달해야 합니다.
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;
}
그런 다음 텍스처에서 색상을 조회하는 프래그먼트 셰이더를 제공합니다.
<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>
마지막으로 이미지를 로드하고 텍스처를 만든 다음 이미지를 텍스처에 복사해야 합니다. 브라우저에서는 이미지가 비동기식으로 로드되므로 텍스처가 로드될 때까지 기다리도록 코드를 약간 재정렬해야 합니다. 로드되면 그립니다.
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);
...
}
별로 흥미롭지 않으니 이미지를 조작해 보겠습니다. 빨간색과 파란색을 바꾸는 건 어떨까요?
...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...
다른 픽셀을 실제로 살펴보는 이미지 처리를 수행하려면 어떻게 해야 할까요? WebGL은 0.0에서 1.0까지의 텍스처 좌표에서 텍스처를 참조하므로 간단한 수학 onePixel = 1.0 / textureSize을 사용하여 1픽셀을 이동할 양을 계산할 수 있습니다.
다음은 텍스처에 있는 각 픽셀의 왼쪽 및 오른쪽 픽셀을 평균화하는 프래그먼트 셰이더입니다.
<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>
그런 다음 JavaScript에서 텍스처 크기를 전달해야 합니다.
...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height);
...
다른 픽셀을 참조하는 방법을 알았으니 이제 컨볼루션 커널을 사용하여 일반적인 이미지 처리를 수행해 보겠습니다. 이 경우 3x3 커널을 사용합니다. 컨볼루션 커널은 3x3 행렬로, 행렬의 각 항목은 렌더링하는 픽셀 주변의 8개 픽셀을 곱할 값을 나타냅니다. 그런 다음 결과를 커널의 가중치 (커널의 모든 값의 합계) 또는 1.0 중 더 큰 값으로 나눕니다. 이 도움말에서 자세한 내용을 확인하세요. C++로 직접 작성하는 경우 실제 코드를 보여주는 또 다른 도움말입니다. 이 경우 셰이더에서 해당 작업을 수행하므로 새 프래그먼트 셰이더가 있습니다.
<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>
JavaScript에서는 컨볼루션 커널을 제공해야 합니다.
...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
...
var edgeDetectKernel = [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
...
이 글을 통해 WebGL의 이미지 처리가 매우 간단하다는 것을 알게 되셨기를 바랍니다. 다음으로 이미지에 두 개 이상의 효과를 적용하는 방법을 살펴보겠습니다.
GLSL에서 변수 앞에 붙는 a, u, v_ 프리픽스는 무엇인가요?
이는 이름 지정 규칙일 뿐입니다. 버퍼에서 제공하는 데이터인 속성의 경우 a_ u_는 셰이더의 입력인 유니폼에 사용되고 v_는 꼭짓점 셰이더에서 프래그먼트 셰이더로 전달되고 그려진 각 픽셀의 꼭짓점 간에 보간 (또는 변경)되는 값인 가변에 사용됩니다.
여러 효과 적용하기
이미지 처리에서 다음으로 가장 명확한 질문은 여러 효과를 적용하는 방법입니다.
셰이더를 즉석에서 생성해 볼 수 있습니다. 사용자가 사용할 효과를 선택할 수 있는 UI를 제공한 다음 모든 효과를 처리하는 셰이더를 생성합니다. 이 기법은 실시간 그래픽 효과를 만드는 데 자주 사용되지만 항상 가능한 것은 아닙니다. 더 유연한 방법은 텍스처를 2개 더 사용하고 각 텍스처에 차례로 렌더링하여 앞뒤로 핑퐁하고 매번 다음 효과를 적용하는 것입니다.
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
이렇게 하려면 프레임 버퍼를 만들어야 합니다. WebGL 및 OpenGL에서 프레임 버퍼는 사실 적절한 이름이 아닙니다. WebGL/OpenGL 프레임 버퍼는 실제로는 상태의 모음일 뿐이며 어떤 종류의 버퍼도 아닙니다. 하지만 텍스처를 프레임 버퍼에 연결하면 해당 텍스처로 렌더링할 수 있습니다. 먼저 이전 텍스처 생성 코드를 함수로 변환해 보겠습니다.
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);
이제 이 함수를 사용하여 텍스처를 2개 더 만들고 프레임 버퍼 2개에 연결해 보겠습니다.
// 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);
}
이제 커널 집합을 만든 다음 적용할 커널 목록을 만듭니다.
// 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"
];
마지막으로 각 텍스처를 적용하고 렌더링할 텍스처를 번갈아 가며 적용합니다.
// 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);
}
검토해야 할 사항이 있습니다.
null를 사용하여 gl.bindFramebuffer를 호출하면 프레임 버퍼 중 하나가 아닌 캔버스에 렌더링하려는 WebGL에 알립니다.
WebGL은 클립스페이스에서 픽셀로 다시 변환해야 합니다. gl.viewport의 설정을 기반으로 이 작업을 수행합니다. gl.viewport의 설정은 WebGL을 초기화할 때 캔버스의 크기로 기본 설정됩니다. 렌더링하는 프레임 버퍼는 캔버스와 크기가 다르므로 표시 영역을 적절하게 설정해야 합니다.
마지막으로 WebGL 기본사항 예시에서는 렌더링할 때 Y 좌표를 뒤집었습니다. WebGL은 2D에서 더 일반적인 왼쪽 상단이 아닌 왼쪽 하단이 0,0인 캔버스를 표시하기 때문입니다. 프레임 버퍼에 렌더링할 때는 필요하지 않습니다. 프레임 버퍼는 표시되지 않으므로 상단과 하단이 어느 부분인지는 중요하지 않습니다. 중요한 것은 프레임 버퍼의 픽셀 0,0이 계산의 0,0에 해당한다는 것입니다. 이 문제를 해결하기 위해 셰이더에 입력을 하나 더 추가하여 뒤집을지 여부를 설정할 수 있도록 했습니다.
<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>
그런 다음 렌더링할 때 설정할 수 있습니다.
...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);
이 예시에서는 여러 효과를 얻을 수 있는 단일 GLSL 프로그램을 사용하여 간단하게 유지했습니다. 전체 이미지 처리를 수행하려면 많은 GLSL 프로그램이 필요할 수 있습니다. 색조, 채도, 휘도 조정 프로그램입니다. 밝기 및 대비를 위한 또 다른 레이어 하나는 반전용, 다른 하나는 레벨 조정용 등입니다. GLSL 프로그램을 전환하고 특정 프로그램의 매개변수를 업데이트하려면 코드를 변경해야 합니다. 이 예시를 작성하는 것을 고려했지만, 각 GLSL 프로그램마다 고유한 매개변수가 필요하므로 이 예시를 작성하면 모든 것이 엉망이 되지 않도록 대대적인 리팩터링이 필요할 수 있으므로 독자에게 맡기는 것이 가장 좋습니다. 이 예와 이전 예가 WebGL을 조금 더 쉽게 접근할 수 있도록 만들고 2D로 시작하면 WebGL을 조금 더 쉽게 이해할 수 있기를 바랍니다. 시간이 나면 3D를 수행하는 방법과 WebGL이 실제로 내부에서 수행하는 작업에 대한 자세한 내용을 다루는 몇 가지 기사를 더 작성해 보겠습니다.
WebGL 및 알파
일부 OpenGL 개발자가 WebGL이 백버퍼 (즉, 캔버스)에서 알파를 처리하는 방식에 문제가 있는 것을 발견했으므로 알파와 관련된 WebGL과 OpenGL의 차이점을 살펴보는 것이 좋겠다고 생각했습니다.
OpenGL과 WebGL의 가장 큰 차이점은 OpenGL은 아무것도 합성되지 않은 백버퍼에 렌더링하거나 OS의 창 관리자에 의해 아무것도 합성되지 않은 백버퍼에 효과적으로 렌더링하므로 알파가 무엇인지는 중요하지 않다는 것입니다.
WebGL은 브라우저에서 웹페이지와 합성되며 기본값은 투명도가 있는 .png <img> 태그 및 2d 캔버스 태그와 동일한 미리 곱한 알파를 사용하는 것입니다.
WebGL에는 이를 OpenGL과 더 유사하게 만드는 몇 가지 방법이 있습니다.
#1. WebGL에 미리 곱하지 않은 알파와 합성하고 싶다고 알립니다.
gl = canvas.getContext("experimental-webgl", {premultipliedAlpha: false});
기본값은 true입니다. 물론 결과는 캔버스 아래에 있는 배경 색상 (캔버스의 배경 색상, 캔버스의 컨테이너 배경 색상, 페이지의 배경 색상, 캔버스의 z-index가 0보다 큰 경우 캔버스 뒤에 있는 요소 등)과 함께 페이지 위에 합성됩니다. 즉, CSS가 웹페이지의 해당 영역에 정의하는 색상입니다. 알파 문제가 있는지 확인하는 가장 좋은 방법은 캔버스의 배경을 빨간색과 같은 밝은 색상으로 설정하는 것입니다. 무슨 일이 일어나고 있는지 즉시 확인할 수 있습니다.
<canvas style="background: red;"></canvas>
검은색으로 설정하여 알파 문제를 숨길 수도 있습니다.
#2. WebGL에 백버퍼에 알파를 원하지 않는다고 알리기
gl = canvas.getContext("experimental-webgl", {alpha: false});
이렇게 하면 백 버퍼에 RGB만 있으므로 OpenGL과 더 유사하게 작동합니다. 좋은 브라우저는 알파가 없음을 확인하고 실제로 WebGL이 합성되는 방식을 최적화할 수 있으므로 이 옵션이 가장 좋습니다. 물론 이는 백 버퍼에 알파가 없다는 의미이기도 하므로, 어떤 목적으로 백 버퍼에서 알파를 사용하는 경우 작동하지 않을 수 있습니다. 백버퍼에서 알파를 사용하는 앱은 거의 없습니다. 이것이 기본값이어야 한다고 생각합니다.
#3. 렌더링 끝에서 알파 지우기
..
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);
대부분의 하드웨어에는 특수한 사례가 있으므로 지우기는 일반적으로 매우 빠릅니다. 대부분의 데모에서 이 작업을 수행했습니다. 현명한 사람이라면 위의 두 번째 방법으로 전환할 것입니다. 이거 게시하고 바로 해봐야겠어요. 대부분의 WebGL 라이브러리는 이 메서드를 기본값으로 사용해야 할 것 같습니다. 합성 효과에 알파를 실제로 사용하는 소수의 개발자는 알파를 요청할 수 있습니다. 나머지 사용자는 최상의 성능을 경험하고 예상치 못한 문제를 최소화할 수 있습니다.
#4. 알파를 한 번 지운 후 더 이상 렌더링하지 않음
// 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);
물론 자체 프레임 버퍼로 렌더링하는 경우 알파로 렌더링을 다시 사용 설정한 다음 캔버스로 렌더링으로 전환할 때 다시 사용 중지해야 할 수 있습니다.
#5. 이미지 처리
또한 알파가 있는 PNG 파일을 텍스처에 로드하는 경우 기본적으로 알파가 미리 곱해지는데, 이는 일반적으로 대부분의 게임에서 사용하는 방식이 아닙니다. 이 동작을 방지하려면 WebGL에 다음을 알려야 합니다.
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
#6. 미리 곱해진 알파와 함께 작동하는 혼합 방정식 사용
제가 작성하거나 작업한 거의 모든 OpenGL 앱은
gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);
이는 사전 곱셈이 적용되지 않은 알파 텍스처에 적용됩니다. 미리 곱해진 알파 텍스처를 사용하려면
gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);
제가 알고 있는 방법은 여기까지입니다. 더 알고 계신다면 아래에 게시해 주세요.