목차

WebGL2Fundamentals.org

Fix, Fork, Contribute
lazygyu님,
당신의 2 기여에 감사드립니다.

WebGL2 이미지 처리

WebGL에서 이미지 처리는 쉽습니다. 얼마나 쉽냐고요? 아래를 읽어 보면 됩니다.

이 글은 WebGL2 기초에서 이어지는 글입니다. 만약에 아직 읽어 보지 않았다면 먼저 읽어 보는 것이 좋습니다.

WebGL에서 이미지를 그리려면 텍스처를 사용 해야 합니다. 렌더링 할 때 픽셀 좌표 대신 클립 공간 좌표를 예상하는 것과 마찬가지로 일반적으로 텍스처를 읽을 때 텍스처 좌표를 예상합니다. 텍스쳐 좌표는 텍스처의 크기에 상관없이 0.0에서 1.0의 크기를 가집니다.

WebGL2에서는 픽셀 좌표를 사용하여 텍스처를 읽을수 있는 기능이 추가 되었습니다. 어떤 방법이 더 좋은지는 사람마다 다릅니다. 하지만 픽셀 좌표 보단 텍스처 좌표를 사용하는 것이 더 일반적이라고 생각 합니다.

지금은 하나의 사각형(2개의 삼각형)을 그리기 때문에 WebGL에 직사각형의 각 점이 텍스처의 어느 위치에 해당하는지 알려주어야 합니다. 이 정보를 정점 셰이더에서 프래그먼트 셰이더로 'varying’이라 불리는 특별한 변수를 사용하여 전달합니다. varying은 값이 변하기 때문에 varying이라고 불립니다. 프래그먼트 셰이더를 사용하여 각 픽셀을 그릴때 마다 WebGL은 정점 셰이더에서 제공 한 값을 보간합니다.

이전 글의 마지막 부분에서 작성한 정점 셰이더에서부터, 텍스처 좌표로 전달할 attribute를 추가 한 다음 이를 프래그먼트 셰이더로 전달 해야합니다.

  • ...
  •  
  • in vec2 a_texCoord;
  •  
  • ...
  •  
  • out vec2 v_texCoord;
  •  
  • void main() {
  • ...
  • // 프래그먼트 셰이더로 texCoord 전달
  • // GPU가 이 값들을 점 사이에서 보간 할 것입니다.
  • v_texCoord = a_texCoord;
  • }

그런 다음 텍스처에서 색상을 찾기 위한 프래그먼트 셰이더를 작성합니다.

  • #version 300 es
  • precision highp float;
  •  
  • // 사용할 텍스처
  • uniform sampler2D u_image;
  •  
  • // texCoord는 정점 셰이더에서 전달된 것입니다.
  • in vec2 v_texCoord;
  •  
  • // 프래그먼트 셰이더는 출력값을 선언해야합니다.
  • out vec4 outColor;
  •  
  • void main() {
  • // 텍스처에서 색상을 찾습니다.
  • outColor = texture(u_image, v_texCoord);
  • }

마지막으로 이미지를 로드하고 텍스처를 생성한 뒤, 이미지를 텍스처로 복사해야 합니다. 우리는 브라우저를 사용하고 있기 때문에 이미지가 비동기적으로 로딩되고, 이를 위해 텍스처가 로드 될 때까지를 기다리도록 코드를 약간 변경해야 합니다. 일단 로드가 완료되면 그 이후에 그릴 것입니다.

  • function main() {
  • var image = new Image();
  • image.src = "https://someimage/on/our/server"; // 반드시 같은 도메인 이어야 합니다!!!
  • image.onload = function() {
  • render(image);
  • }
  • }
  •  
  • function render(image) {
  • ...
  • // 정점 데이터가 가야할 location을 찾습니다.
  • var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
  • var texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");
  •  
  • // uniform 찾기
  • var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
  • var imageLocation = gl.getUniformLocation(program, "u_image");
  •  
  • ...
  •  
  • // 직사각형의 텍스처 좌표를 제공합니다.
  • 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(texCoordAttributeLocation);
  • var size = 2; // iteration마다 2개의 구성요소 사용
  • var type = gl.FLOAT; // 데이터는 32비트 부동소수점
  • var normalize = false; // 데이터를 정규화하지 않음
  • var stride = 0; // 0 = 반복할 때마다 다음 위치를 얻기 위해 size * sizeof(type)씩 앞으로 이동
  • var offset = 0; // 버퍼의 맨 앞부터 시작
  • gl.vertexAttribPointer(
  • texCoordAttributeLocation, size, type, normalize, stride, offset)
  • // 텍스처를 생성합니다.
  • var texture = gl.createTexture();
  • // 텍스처 유닛 0을 활성화합니다.
  • // (이후 호출하는 텍스처 명령들이 영향을 주게 될 텍스처 유닛)
  • gl.activeTexture(gl.TEXTURE0 + 0);
  • // 텍스처 유닛 0에 텍스처를 바인딩합니다.
  • gl.bindTexture(gl.TEXTURE_2D, texture);
  • // 매개 변수를 설정하여 우리는 밉맵이 필요 없으며
  • // 필터링 하지 않을 것이고 텍스처 반복(repeat)도 필요 없다고 알립니다.
  • 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);
  • // 텍스처로 이미지를 업로드
  • var mipLevel = 0; // 가장큰 밉맵
  • var internalFormat = gl.RGBA; // 텍스처로 사용하길 원하는 포맷
  • var srcFormat = gl.RGBA; // 우리가 제공하는 데이터의 포맷
  • var srcType = gl.UNSIGNED_BYTE // 우리가 제공하는 데이터의 타입
  • gl.texImage2D(gl.TEXTURE_2D,
  • mipLevel,
  • internalFormat,
  • srcFormat,
  • srcType,
  • image);
  •  
  • ...
  •  
  • // 프로그램(셰이더 쌍)을 사용하도록 합니다.
  • gl.useProgram(program);
  •  
  • // 셰이더에서 픽셀에서 클립공간으로 변환 할수 있도록
  • // 캔버스 해상도를 전달합니다.
  • gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
  •  
  • // 셰이더에 텍스처 유닛 0에서 텍스처를 가져오라고 알려줍니다.
  • gl.uniform1i(imageLocation, 0);
  •  
  • // position 버퍼를 바인딩하여 setRectangle에서 호출 될
  • // gl.bufferData가 position 버퍼에 데이터를 넣습니다.
  • gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  • // 사각형의 크기를 이미지와 같은 크기로 설정합니다.
  • setRectangle(gl, 0, 0, image.width, image.height);
  •  
  • }

여기에 WebGL로 렌더링한 이미지가 있습니다.

아직 그렇게 신기하지는 않으니까 한번 이미지를 조작해봅시다. 빨간색이랑 파란색을 바꾸는 건 어떨까요?

  • ...
  • outColor = texture(u_image, v_texCoord).bgra;
  • ...

이제 빨간색과 파란색이 바뀌었습니다.

다른 픽셀들의 값을 사용하는 이미치 처리를 하고 싶다면 어떻게 해야 할까요? WebGL은 0.0에서 1.0까지인 텍스처 좌표에서 텍스처들을 참조하므로 1픽셀 거리가 텍스처 좌표로 얼만큼인지를 onePixel = 1.0 / textureSize를 사용해 계산할 수 있습니다.

아래는 각 픽셀의 왼쪽, 오른쪽 픽셀값을 사용해 평균을 내는 프래그먼트 셰이더 입니다.

  • #version 300 es
  •  
  • // 프래그먼트 셰이더는 정밀도 기본값이 없으므로 우리가 선택해야 합니다.
  • // highp가 대개 괜찮습니다. "높은 정밀도"를 의미합니다.
  • precision highp float;
  •  
  • // 텍스처
  • uniform sampler2D u_image;
  •  
  • // texCoord는 정점 셰이더에서 전달됩니다.
  • in vec2 v_texCoord;
  •  
  • // 프래그먼트 셰이더는 출력을 선언 해야합니다.
  • out vec4 outColor;
  •  
  • void main() {
  • vec2 onePixel = vec2(1) / vec2(textureSize(u_image, 0));
  • // 왼쪽, 중간, 오른쪽 픽셀의 평균을 계산합니다.
  • outColor = (
  • texture(u_image, v_texCoord) +
  • texture(u_image, v_texCoord + vec2( onePixel.x, 0.0)) +
  • texture(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
  • }

블러(blur)시키지 않은 위 예제와 비교해 보세요.

이제 다른 픽셀을 참조하는 방법을 알았으니 컨볼루션 커널(convolution kernel)을 사용하여 몇 가지 일반적인 이미지 처리를 해보겠습니다. 여기에서는 3x3 커널을 사용할 것입니다. 컨볼루션 커널은 단순히 3x3 행렬이며 여기서 행렬의 각 요소는 렌더링할 픽셀 주위에 있는 8개의 픽셀에 어떤 값을 곱할 것인지를 나타냅니다. 그 다음 그 결과를 커널 가중치(커널의 모든 값들의 합) 또는 1.0 중 큰 값으로 나눕니다. 여기 꽤 좋은 읽을거리가 있습니다. 그리고 C++로 직접 작성하려면 어떻게 해야하는지를 실제 코드를 통해 보여주는 다른 읽을거리가 있습니다.

우리의 경우 셰이더에서 이 작업을 수행할 것이므로 아래와 같이 새로운 프래그먼트 셰이더를 작성합니다.

  • #version 300 es
  •  
  • // 프래그먼트 셰이더는 정밀도 기본값이 없으므로 우리가 선택해야 합니다.
  • // highp가 대개 괜찮습니다. "높은 정밀도"를 의미합니다.
  • precision highp float;
  •  
  • // 텍스처
  • uniform sampler2D u_image;
  •  
  • // 컨볼루션 커널 데이터
  • uniform float u_kernel[9];
  • uniform float u_kernelWeight;
  •  
  • // texCoord는 정점 셰이더에서 전달됩니다.
  • in vec2 v_texCoord;
  •  
  • // 프래그먼트 셰이더는 출력을 선언 해야합니다.
  • out vec4 outColor;
  •  
  • void main() {
  • vec2 onePixel = vec2(1) / vec2(textureSize(u_image, 0));
  •  
  • vec4 colorSum =
  • texture(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
  • texture(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
  • texture(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
  • texture(u_image, v_texCoord + onePixel * vec2(-1, 0)) * u_kernel[3] +
  • texture(u_image, v_texCoord + onePixel * vec2( 0, 0)) * u_kernel[4] +
  • texture(u_image, v_texCoord + onePixel * vec2( 1, 0)) * u_kernel[5] +
  • texture(u_image, v_texCoord + onePixel * vec2(-1, 1)) * u_kernel[6] +
  • texture(u_image, v_texCoord + onePixel * vec2( 0, 1)) * u_kernel[7] +
  • texture(u_image, v_texCoord + onePixel * vec2( 1, 1)) * u_kernel[8] ;
  • outColor = vec4((colorSum / u_kernelWeight).rgb, 1);
  • }

자바스크립트에서 컨볼루션 커널과 커널의 가중치를 제공해야 합니다.

  • function computeKernelWeight(kernel) {
  • var weight = kernel.reduce(function(prev, curr) {
  • return prev + curr;
  • });
  • return weight <= 0 ? 1 : weight;
  • }
  •  
  • ...
  • var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
  • var kernelWeightLocation = gl.getUniformLocation(program, "u_kernelWeight");
  • ...
  • var edgeDetectKernel = [
  • -1, -1, -1,
  • -1, 8, -1,
  • -1, -1, -1
  • ];
  •  
  • // 커널과 커널의 가중치를 설정합니다.
  • gl.uniform1fv(kernelLocation, edgeDetectKernel);
  • gl.uniform1f(kernelWeightLocation, computeKernelWeight(edgeDetectKernel));
  • ...

그리고 짠… 드랍 다운 리스트를 사용해서 다른 커널을 선택해 보세요.

이 글을 통해 WebGL에서의 이미지 처리가 매우 간단하다는 것을 이해하셨길 바랍니다. 다음으로 이미지에 두가지 이상의 효과를 적용하는 방법을 살펴 보겠습니다.

텍스쳐 유닛(texture unit)이란 무엇 입니까?

gl.draw???를 호출할때 셰이더는 텍스처를 참조 할 수 있습니다. 텍스처들은 텍스처 유닛에 바인딩됩니다. 사용자의 디바이스에 따라 더 지원할 수도 있지만 기본적으로 WebGL2 스펙에서는 적어도 16개의 텍스처 유닛을 지원하도록 하고 있습니다. 각 샘플러 유닛이 참조하는 텍스쳐 유닛을 설정하기 위해서는 샘플러 uniform의 location을 찾고, 참조하길 원하는 텍스처 유닛의 인덱스를 설정하면 됩니다. 예를 들어:
  • var textureUnitIndex = 6; // 텍스쳐 유닛 6 사용
  • var u_imageLoc = gl.getUniformLocation(
  • program, "u_image");
  • gl.uniform1i(u_imageLoc, textureUnitIndex);

다른 텍스쳐들을 다른 유닛에 설정하기 위해 gl.activeTexture를 호출하고 원하는 유닛에 텍스처를 바인딩 하면 됩니다. 예를들어,

  • // 텍스처를 텍스쳐 유닛 6에 바인딩 합니다.
  • gl.activeTexture(gl.TEXTURE6);
  • gl.bindTexture(gl.TEXTURE_2D, someTexture);

아래와 같이 해도 됩니다.

  • var textureUnitIndex = 6; // 텍스쳐 유닛 6 사용.
  • // 텍스처를 텍스쳐 유닛 6에 바인드 합니다.
  • gl.activeTexture(gl.TEXTURE0 + textureUnitIndex);
  • gl.bindTexture(gl.TEXTURE_2D, someTexture);

GLSL 변수에서 a_, u_, v_ 접두어는 무엇입니까?

이는 단지 네이밍 컨벤션입니다. 꼭 필요하지는 않지만 어디서 값이 오는지 한눈에 보기 쉽게 해줍니다. a_는 버퍼에서 제공된 데이터를 나타내는 attribute에서 따왔습니다. u_는 셰이더의 입력으로 제공되는 uniform에서 따왔습니다. v_는 정점 셰이더에서 프래그먼트 셰이더로 전달되고, 각 픽셀이 그려지기 전에 정점 사이에서 보간 (또는 변화)되는 값을 나타내는 varying에서 따왔습니다. 더 자세한 내용은 작동 원리에서 확인하세요.

이슈나 버그가 있나요? 깃헙에서 이슈 만들기.