목차

WebGL2Fundamentals.org

Fix, Fork, Contribute

WebGL2 두개 이상의 텍스처 사용하기

이 글은 WebGL 이미지 처리에서 이어지는 글입니다. 아직 읽지 않으셨다면 먼저 이 글부터 읽으시길 추천 드립니다.

이제 이 질문에 대답할 때가 된 것 같습니다. "두개 이상의 텍스처를 사용하려면 어떻게 해야하죠?"

꽤 간단합니다. 이미지 하나를 그리는 첫 번째 셰이더로 돌아가서, 두 개의 이미지를 사용하도록 수정해 보겠습니다.

우선 해야 할 것은 코드를 수정해서 이미지 두개를 로딩하도록 하는 것입니다. WebGL코드는 아니고 HTML5 자바스크립트 코드지만 이 부분도 처리해 주어야 합니다. 이미지는 비동기적으로 로드되는데, 웹 프로그래밍의 초보자이시라면 익숙해 지셔야 할 겁니다.

처리하는 방법에는 두가지가 있습니다. 텍스처가 없는 상태로 실행되다가 텍스처가 로드되면 업데이트하도록 구성할 수 있습니다. 이 방법은 뒤쪽 글에서 다루어보도록 하겠습니다.

여기서는 무언가를 그리기 전에 모든 이미지 로딩이 완료되길 기다리도록 할 겁니다.

먼저 이미지를 로딩하는 코드를 함수로 만들겠습니다. 간단합니다. 이 함수는 새로운 Image 객체를 만들고, 로딩할 URL을 설정하고, 로딩이 완료되면 호출할 콜백을 설정합니다.

function loadImage (url, callback) {
  var image = new Image();
  image.src = url;
  image.onload = callback;
  return image;
}

이제 URL 배열을 가지고 이미지들을 로딩하고 이미지 배열을 생성하는 코드를 만들어 봅시다. imagesToLoad를 우리가 로딩할 이미지의 개수로 설정합니다. 그리고 loadImage에 전달하는 콜백마다 imagesToLoad를 감소하도록 합니다. imagesToLoad가 0이 되면 모든 이미지가 로딩된 것이고 이미지 배열을 콜백에 전달하도록 합니다.

function loadImages(urls, callback) {
  var images = [];
  var imagesToLoad = urls.length;

  // 이미지 로딩이 완료될 때마다 호출됩니다.
  var onImageLoad = function() {
    --imagesToLoad;
    // 모든 이미지 로딩이 완료되면 콜백을 호출합니다.
    if (imagesToLoad === 0) {
      callback(images);
    }
  };

  for (var ii = 0; ii < imagesToLoad; ++ii) {
    var image = loadImage(urls[ii], onImageLoad);
    images.push(image);
  }
}

이제 loadImages를 아래와 같이 호출합니다.

function main() {
  loadImages([
    "resources/leaves.jpg",
    "resources/star.jpg",
  ], render);
}

다음으로 셰이더가 2개의 텍스처를 사용하도록 수정합니다. 예제에서는 하나의 텍스처를 다른 텍스처에 곱하도록 할 겁니다.

#version 300 es
precision highp float;

// 텍스처
*uniform sampler2D u_image0;
*uniform sampler2D u_image1;

// 정점 셰이더에서 넘어온 텍스처 좌표.
in vec2 v_texCoord;

// 프래그먼트 셰이더는 출력을 선언해 주어야 합니다.
out vec2 outColor;

void main() {
*   vec4 color0 = texture2D(u_image0, v_texCoord);
*   vec4 color1 = texture2D(u_image1, v_texCoord);
*   outColor = color0 * color1;
}

두 개의 WebGL 텍스처를 만들어야 합니다.

  // 텍스처 두 개를 만듭니다.
  var textures = [];
  for (var ii = 0; ii < 2; ++ii) {
    var texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);

    // 밉맵을 사용하지 않는 파라메터를 설정합니다.
    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,
                  images[ii]);

    // 텍스처를 텍스처 배열에 추가합니다.
    textures.push(texture);
  }

WebGL에는 "텍스처 유닛"이라는 것이 있습니다. 텍스처에 대한 참조의 배열이라고 생각하시면 됩니다. 각 샘플러에 대해 어떤 텍스처 유닛을 사용할지를 알려주어야 합니다.

  // 샘플러 위치른 찾습니다.
  var u_image0Location = gl.getUniformLocation(program, "u_image0");
  var u_image1Location = gl.getUniformLocation(program, "u_image1");

  ...

  // 렌더링에 어떤 텍스처 유닛을 사용할지른 설정합니다.
  gl.uniform1i(u_image0Location, 0);  // 텍스처 유닛 0
  gl.uniform1i(u_image1Location, 1);  // 텍스처 유닛 1

그리고 각각의 텍스처 유닛에 텍스처를 바인딩해 주어야 합니다.

  // 텍스처 유닛에 사용할 텍스처를 설정합니다.
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, textures[0]);
  gl.activeTexture(gl.TEXTURE1);
  gl.bindTexture(gl.TEXTURE_2D, textures[1]);

우리가 로딩할 두개의 이미지는 아래와 같습니다.

아래는 WebGL에서 두 개의 이미지를 곱한 결과입니다.

몇 가지 설명드려야 할 것이 있습니다.

텍스처 유닛에 대해 쉽게 생각해 보자면 다음과 같습니다: 모든 텍스처 관련 함수는 "활성화된(active) 텍스처 유닛"에 적용됩니다. "활성화된 텍스처 유닛"이란 단순히 여러분이 작업하고자 하는 텍스처 유닛의 인덱스인 전역 변수입니다. WebGL의 텍스처 유닛은 네 개의 타겟이 있습니다. TEXTURE_2D, TEXTURE_3D, TEXTURE_2D_ARRAY, TEXTURE_CUBE_MAP 입니다. 각 텍스처 함수는 현재 활성화된 텍스처 유닛의 명시된 타겟에 대해 동작합니다. 만일 WebGL을 자바스크립트를 활용해 구현한다면 아래와 같을겁니다.

var getContext = function() {
  var textureUnits = [
    { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
    { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
    { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
    { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
    { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
    { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
    { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
    { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
  ];
  var activeTextureUnit = 0;

  var activeTexture = function(unit) {
    // 유닛 열거자를 인덱스로 변환합니다.
    var index = unit - gl.TEXTURE0;
    // 활성화된 텍스처 유닛을 설정합니다.
    activeTextureUnit = index;
  };

  var bindTexture = function(target, texture) {
    // 활성화된 텍스처 유닛의 타겟에 텍스처를 설정합니다.
    textureUnits[activeTextureUnit][target] = texture;
  };

  var texImage2D = function(target, ...args) {
    // 현재 텍스처 유닛의 활성화된 텍스처에 texImage2D를 호출합니다.
    var texture = textureUnits[activeTextureUnit][target];
    texture.image2D(...args);
  };

  var texImage3D = function(target, ...args) {
    // 현재 텍스처 유닛의 활성화된 텍스처에 texImage3D를 호출합니다.
    var texture = textureUnits[activeTextureUnit][target];
    texture.image3D(...args);
  };

  // WebGL API를 반환합니다.
  return {
    activeTexture: activeTexture,
    bindTexture: bindTexture,
    texImage2D: texImage2D,
    texImage3D: texImage3D,
  };
};

셰이더는 텍스처 유닛의 인덱스를 받습니다. 아래 두 줄의 코드의 의미가 명확해지셨길 바랍니다.

  gl.uniform1i(u_image0Location, 0);  // 텍스처 유닛 0
  gl.uniform1i(u_image1Location, 1);  // 텍스처 유닛 1

하나 주의하셔야 할 것은, 유니폼을 설정할 때에는 텍스처 유닛의 인덱스를 사용하지만 gl.activeTexture를 호출할 때에는 gl.TEXTURE0, gl.TEXTURE1과 같은 특수한 상수를 사용해야 한다는 것입니다. 다행히 상수들은 연속된 숫자이므로 아래와 같이 하는 대신,

  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, textures[0]);
  gl.activeTexture(gl.TEXTURE1);
  gl.bindTexture(gl.TEXTURE_2D, textures[1]);

이렇게 할 수 있습니다.

  gl.activeTexture(gl.TEXTURE0 + 0);
  gl.bindTexture(gl.TEXTURE_2D, textures[0]);
  gl.activeTexture(gl.TEXTURE0 + 1);
  gl.bindTexture(gl.TEXTURE_2D, textures[1]);

아니면 이렇게도 가능합니다.

  for (var ii = 0; ii < 2; ++ii) {
    gl.activeTexture(gl.TEXTURE0 + ii);
    gl.bindTexture(gl.TEXTURE_2D, textures[ii]);
  }

이 짧은 설명으로, 한 번의 WebGL 호출로 여러 텍스처를 그리는데에 도움이 되었기를 바랍니다.

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