WebGL 기초

Gregg Tavares
Gregg Tavares

WebGL 기초

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를 원한다면 WebGL이 2D API이므로 3D에서 2D로 변환하는 셰이더를 제공해야 합니다. 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이고 매우 간단합니다.

type="x-shader/x-vertex" 및 type="x-shader/x-fragment"는 무슨 뜻인가요?

<script> 태그는 기본적으로 JavaScript가 포함됩니다. 유형을 지정하지 않거나 type="javascript" 또는 type="text/javascript"를 지정하면 브라우저에서 콘텐츠를 JavaScript로 해석합니다. 다른 값을 입력하면 브라우저에서 스크립트 태그의 콘텐츠를 무시합니다.

이 기능을 사용하여 셰이더를 스크립트 태그에 저장할 수 있습니다. 더 좋은 점은 자체 유형을 만들 수 있으며 JavaScript에서 이 유형을 찾아 셰이더를 꼭짓점 셰이더로 컴파일할지 프래그먼트 셰이더로 컴파일할지 결정합니다.

이 경우 createShaderFromScriptElement 함수는 지정된 id가 있는 스크립트를 찾은 다음 type를 보고 만들 셰이더 유형을 결정합니다.

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개를 더 만들고 framebuffer 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 canva와 동일하게 사전 곱셈된 알파를 사용하는 것입니다. WebGL에는 OpenGL과 더 유사하게 만드는 여러 가지 방법이 있습니다.

#1) WebGL에 사전 곱셈되지 않은 알파로 합성되기를 바란다고 알립니다.

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

기본값은 true입니다. 물론 결과는 여전히 캔버스 아래에 있는 배경 색상 (캔버스의 배경 색상, 캔버스의 컨테이너 배경 색상, 페이지의 배경 색상, 캔버스에 Z-색인이 0보다 큰 경우 캔버스 뒤의 내용 등)으로 페이지 위에 합성됩니다. 즉, CSS가 웹페이지의 해당 영역을 정의합니다. 알파 문제가 있는지 확인하는 좋은 방법은 캔버스의 배경을 빨간색과 같은 밝은 색상으로 설정하는 것입니다. 어떤 일이 일어나고 있는지 즉시 확인할 수 있습니다.

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

검은색으로 설정하면 알파 문제가 숨겨집니다.

#2) 백버퍼에 알파를 사용하지 않겠다고 WebGL에 알립니다.

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

이렇게 하면 백버퍼에는 RGB만 있으므로 OpenGL처럼 작동합니다. 좋은 브라우저에서는 알파가 없음을 확인하고 실제로 WebGL이 합성되는 방식을 최적화할 수 있으므로 이 방법이 가장 좋은 옵션일 것입니다. 물론 backbuffer에 실제로 알파가 없으므로 어떤 목적으로든 backbuffer에서 알파를 사용하고 있다면 제대로 작동하지 않을 수 있습니다. 백버퍼에서 알파를 사용하는 앱은 거의 없습니다. 사실 이 기능이 기본으로 제공되어야 한다고 생각합니다.

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

대부분의 하드웨어에는 이를 위한 특수 사례가 있으므로 일반적으로 매우 빠르게 지워집니다. 대부분의 데모에서 이 작업을 수행했습니다. 현명하다면 위의 방법 2로 전환할 것입니다. 이 글을 게시한 직후에 해야 할 것 같습니다. 대부분의 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);

제가 알고 있는 방법은 이 정도입니다. 더 알고 있는 경우 아래에 게시해 주세요.