WebGL 기초

Gregg Tavares
Gregg Tavares

WebGL 기초

WebGL을 사용하면 브라우저에서 멋진 실시간 3D 그래픽을 표시할 수 있지만 WebGL이 실제로는 3D API가 아니라 2D API라는 사실은 많은 사람이 알지 못합니다. 제가 설명해 드릴게요.

WebGL은 두 가지에만 관심이 있습니다. 2D 및 색상의 Clipspace 좌표 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> 태그에는 기본적으로 자바스크립트가 포함됩니다. 유형을 지정하지 않거나 type="javascript" 또는 type="text/javascript"를 입력할 수 있으며 브라우저가 콘텐츠를 자바스크립트로 해석합니다. 다른 값을 입력하면 브라우저가 스크립트 태그의 내용을 무시합니다.

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

이 경우 createShaderFromScriptElement 함수는 지정된 id가 있는 스크립트를 찾은 다음 type를 확인하여 생성할 셰이더 유형을 결정합니다.

WebGL 이미지 처리

WebGL에서는 이미지를 쉽게 처리할 수 있습니다. 얼마나 쉬운가요? 아래를 참고하세요.

WebGL에서 이미지를 그리려면 텍스처를 사용해야 합니다. WebGL은 렌더링 시 픽셀 대신 클립스페이스 좌표를 예상하는 것과 마찬가지로 텍스처를 읽을 때 텍스처 좌표를 예상합니다. 텍스처 좌표는 텍스처 크기에 관계없이 0.0에서 1.0으로 변경됩니다. 직사각형 (두 개의 삼각형)만 그려야 하므로 텍스처에서 직사각형의 각 지점에 해당하는 위치가 어디인지 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에서 Framebuffer는 실제로 잘못된 이름입니다. WebGL/OpenGL Framebuffer는 사실상 상태 모음일 뿐이며 실제로 어떤 종류의 버퍼도 아닙니다. 하지만 프레임 버퍼에 텍스처를 연결하여 그 텍스처로 렌더링할 수 있습니다. 먼저 이전의 텍스처 생성 코드를 함수로 변환해 보겠습니다.

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

몇 가지를 살펴볼게요.

nullgl.bindFramebuffer를 호출하면 WebGL이 프레임 버퍼 중 하나가 아닌 캔버스에 렌더링하려고 한다는 것을 알립니다. WebGL은 클립스페이스에서 픽셀로 다시 변환해야 합니다. 이는 gl.viewport의 설정을 기반으로 합니다. gl.viewport 설정은 WebGL을 초기화할 때 기본적으로 캔버스 크기로 설정됩니다. 렌더링하는 프레임 버퍼의 크기가 다르므로 캔버스는 표시 영역을 적절하게 설정해야 합니다. 마지막으로 WebGL 기본 예에서는 렌더링할 때 Y 좌표를 뒤집었습니다. WebGL이 0,0을 사용하여 캔버스를 표시하므로 기존 2D 왼쪽 상단 대신 왼쪽 하단이 됩니다. 프레임 버퍼로 렌더링할 때는 필요하지 않습니다. 프레임 버퍼는 표시되지 않으므로 상단과 하단은 관련이 없습니다. 중요한 것은 프레임 버퍼의 픽셀 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은 브라우저와 웹페이지로 합성되며 기본값은 투명도 및 2d를 가진 .png <img> 태그와 동일한 사전 곱셈 알파를 사용하는 것입니다. WebGL에는 이를 OpenGL처럼 만드는 여러 방법이 있습니다.

#1) WebGL에 사전 곱셈되지 않은 알파와 합성되기를 원한다고 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이 합성되는 방식을 최적화할 수 있기 때문입니다. 물론 이는 백버퍼에 알파가 없다는 뜻이기도 합니다. 따라서 어떤 목적으로 백버퍼에 알파를 사용하고 있다면 효과가 없을 수도 있습니다. 백버퍼에서 알파를 사용하는 앱은 거의 없습니다. 아마도 이것이 기본값으로 설정되었어야 한다고 생각합니다.

#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 파일을 텍스처로 로드하는 경우, 기본적으로 알파가 미리 곱해진다는 것은 대부분의 게임에서 일반적으로 하는 방식은 아닙니다. 이러한 동작을 방지하려면

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

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