다섯 번째 WebGL 레슨에 오신 것을 환영합니다. 이 학습은 "NeHe OpenGL의 여섯 번째 튜토리얼"을 바탕으로 하고 있습니다. 이번 레슨에서는 지금까지 만들었던 3D 오브젝트에 텍스처(Texture)를 입혀봅니다. - 별도로 준비된 이미지 파일로 물체의 표면을 덮는 것입니다. 이와 같은 작업을 통하여 믿을 수 없을 정도로 개체를 복잡해 보이게 할 수 있으며 3D장면을 더욱 디테일하게 만드는데 매우 유용하게 사용될 수 있습니다. 벽돌벽으로 이루어진 3차원 미로 게임을 만든다고 상상해 보세요. 벽돌 하나하나를 일일이 객체로 만들어 쌓고 싶지는 않으시겠죠? 당신은 분명히 밋밋한 객체를 만들고 벽돌 이미지를 붙여 대신할 것입니다.

다음 동영상은 WebGL을 지원하는 브라우저에서 이번 레슨의 결과물을 돌려본 결과입니다.

여기를 클릭하여 WebGL에서 작동하는 것을 확인해 보세요. 만약 WebGL이 갖춰지지 않은 환경이라면 레슨 0을 참고하세요.

이것이 어떻게 만들어졌는지 자세히 살펴봅시다.

시작에 앞서...

이 레슨은 충분한 자바스크립트 프로그래밍 지식이 있지만, 3D 그래픽 구현 경험이 없는 사람들을 대상으로 하고 있습니다. 그리고 가능한 한 빨리 자신만의 3D를 구사하여 결과물을 만들수 있도록 하는 것에 목적을 둡니다. 레슨에 사용된 예제에서 실재 구현된 코드를 분석하여 어떤 일이 일어나는지 알아보고 그것을 이해하여 스스로 응용할 수 있도록 하는 것입니다. 만약 이전의 레슨들을 학습하지 않았다면 먼저 학습하고 돌아오세요. 이전 레슨과의 차이점만을 소개하고 있으니까요.

이전과 동일하게 학습내용 중에는 버그 혹은 오류가 있을 수도 있습니다. 뭔가 잘못된 것을 발견하면 댓글로 알려주세요. 가능한 빨리 고치도록 하겠습니다.

여기에 사용된 예제의 소스코드를 얻는 방법은 두가지가 있습니다. 실제 예제가 작동하는 곳에서 "소스 보기"를 선택하거나, GitHub를 사용할 수 있다면 저장소에서 동일한 예제를 다운로드할 수 있습니다.(앞으로 학습할 예제들도 포함되어 있습니다.) 추천하는 방법은 두 번째로, 저장소에서 모두 가져온 다음 즐겨쓰는 텍스트 편집기에서 열어두고 학습하는 것입니다.

텍스쳐 로딩 - initTexture

텍스쳐를 입혀 질감을 표현하는 작동 원리는 3D개체의 점과 색상을 결정하는 특별한 방법이라고 인식하는 것입니다. 레슨 2를 떠올려 보면, 색상은 fr​​agment schader에 의해 설정됩니다. 따라서 우리가 해야 할 일은 이미지를 읽어들이고 fragment shader로 보내주는 것입니다. fragment shader를 처리하는 과정에서 fragment에 사용되는 이미지 조각이 어떻게 작동하는지 알아야 하고, 알아낸 정보를 다시 내보내야 합니다.

그럼 텍스쳐를 로드하는 부분부터 시작해 봅시다. 이 처리는 페이지 하단부에 위치한 자바스크립트 프로세스가 시작되는 지점인 webGLStart 함수에서 호출됩니다.(새로운 코드 부근에는 주석이 있습니다.)

  function webGLStart() {
    var canvas = document.getElementById("lesson05-canvas");
    initGL(canvas);
    initShaders();
    initTexture(); // added

    gl.clearColor(0.0, 0.0, 0.0, 1.0);

initTexture 함수를 살펴봅시다. - 맨 위에서 1/3정도에 위치에 있습니다. 이것은 모두 새로운 코드입니다.

  var neheTexture;
  function initTexture() {
    neheTexture = gl.createTexture();
    neheTexture.image = new Image();
    neheTexture.image.onload = function() {
      handleLoadedTexture(neheTexture)
    }

    neheTexture.image.src = "nehe.gif";
  }

텍스쳐를 유지하기 위해 전역 변수를 만듭니다. 당연히, 단 하나의 텍스쳐만을 이용하고 전역 변수를 사용해야 하는 것은 아닙니다. 여기에서는 이해를 돕기위해 이같이 사용했을 뿐입니다. gl.createTexture을 통해 텍스쳐에 대한 참조를 만들어 앞서 작성한 전역 변수에 할당한 다음 텍스쳐에 첨부되는 자바스크립트 이미지 개체를 만들어 새로운 속성에 저장합니다. 되집어 보자면, 특정 개체에 필드를 자유룝게 설정할 수 있다는 자바스크립트의 장점을 이용한 것입니다. 이 텍스쳐 객체는 기본적으로 이미지 필드가 없지만 이런식으로 추가해서 나중에 편리하게 사용할 수 있습니다.

텍스쳐 가공 - handleLoadedTexture

다음 과정에서는 이미지 개체에 저장될 실제 이미지를 로드하는 것입니다. 사전 준비 과정으로 콜백 함수를 이미지 개체에 추가합니다. 이 함수는 이미지가 완전히 로드가 완료 되는 시점에 호출됩니다. 따라서 먼저 선언해 두는 것이 안전합니다. 이제 이미지 개체의 src속성을 지정하는 것으로 마무리합니다. 이미지의 로딩은 비동기적으로 수행됩니다. - 그렇기 때문에 src속성에 대한 설정이 즉시 완료됩니다. 그리고 브라우저는 별도의 스레드로 웹서버로 부터 이미지를 로드합니다. 로드가 완료되면 등록한 콜백 함수가 호출되고, 이에 의해서 handleLoadedTexture가 호출되는 것입니다.

  function handleLoadedTexture(texture) {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.bindTexture(gl.TEXTURE_2D, null);
  }

가장 먼저 할 일은 WebGL의 텍스쳐가 "current"텍스쳐라고 알려주는 것입니다. WebGL의 텍스쳐 함수는 인수로 대상을 지정하는 것이 아니라, 모두 "current"텍스쳐에서 수행됩니다. bindTexture가 current를 지정하는 함수입니다. 이것은 이미 살펴본 바 있는 gl.bindBuffer 때와 똑같네요.

이제 WebGL의 텍스쳐로 로드된 이미지를 세로 방향으로 회전하라고 지시합니다. 이것을 하는 이유는 좌표계에 차이가 있기 때문입니다. 이번 텍스쳐 좌표계는 일반적인 수학에서 사용하는 것과 마찬가지로, 세로는 위쪽 방향으로 증가합니다. 이것은 정점 좌표로 사용하는 X, Y, Z 좌표와 일치합니다. 이와는 반대로, 대부분의 그래픽 시스템 - 예를 들어 텍스쳐 이미지에 GIF 형식의 이미지를 사용하면 세로축의 아래 방향이 증가합니다. 가로축은 모두 동일한 좌표계입니다.

이 세로축의 차이는 WebGL의 관점에서 텍스쳐에 사용되는 GIF 이미지는 이미 세로 방향으로 전환되고 있기 때문에 "unflip"을 지정할 필요가 있다는 것을 의미합니다.(Ilmari Heikkinen씨의 댓글 덕분에 명확하게 되었습니다)

다음 단계에는 방금 로드된 이미지를 texImage2D 함수에서 그래픽 카드의 텍스쳐 공간에 업로드하는 것입니다. 인수를 사용하여 어떤 종류의 이미지를 사용하거나, 디테일 수준(LOD, level of detai - 다음 레슨에서 설명합니다), 어떠한 형식으로 그래픽 카드에 저장할 것인지(다시 말하지만 다음 레슨에서 설명할께요.) 또한 이미지의 각 "channel" 크기(빨강, 녹색, 파랑을 저장하는데 사용하는 데이터 타입) 그리고 이미지 자체을 지정합니다.

이후로 계속되는 두 라인에서 텍스쳐에 특별한 스케일링 파라미터를 설정합니다. 첫 번째로 텍스쳐 이미지의 크기로 화면에 얼마나 크게 표시하는 지를 지정하여 WebGL에 전달합니다. 다시 말하자면, 어떻게 스케일 할지에 대한 힌트를 주는 것입니다. 다음 라인 역시 이와 비슷한 것으로, 축소하는 경우에 대한 것입니다. NEAREST는 기존의 이미지를 그대로 사용하기 때문에 가장 매력없는 스케일링 방법입니다. 가까이에서 보면 아주 뭉툭해(blocky) 보이죠. 그러나 이 방법은 느린 시스템에서도 빠르게 작동한다는 장점이 있습니다. 다음 레슨에서는 다른 스케일링 방법들을 알아보겠습니다. 각각의 성능과 외형의 품질 비교할 수 있을 것입니다.

여기까지 완료되면 current texture를 null로 설정합니다. 반드시 필요한 일은 아니지만, 작업에 사용한 후 스스로 정리한다는 의미에서 좋은 습관입니다.

텍스쳐 버퍼 추가 - initBuffers

이제 텍스쳐 로드와 관련된 모든 코드의 작성은 끝났습니다. 다음은 initBuffers를 살펴 보도록합시다. 여기에는 레슨 4에 존재했던 피라미드에 관한 부분을 모두 제거했습니다. 더 흥미로운 사실​​은 큐브 vertex colour buffer가 새로운 것으로 대체되는 것입니다 - texture coordinate buffer가 등장하죠.

    cubeVertexTextureCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
    var textureCoords = [
      // Front face
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,

      // Back face
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,

      // Top face
      0.0, 1.0,
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,

      // Bottom face
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,
      1.0, 0.0,

      // Right face
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,

      // Left face
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
    cubeVertexTextureCoordBuffer.itemSize = 2;
    cubeVertexTextureCoordBuffer.numItems = 24;

이렇게 손보았더니 이 코드를 읽기가 참 편안해 졌군요. 이것은 정점마다 각 속성을 버퍼에 설정하고 있다는 것을 알 수 있습니다. 정점마다 2개의 값을 가진다고 말할수 있죠. 이 텍스쳐 좌표가 의미하는 것은 데카르트(Cartesian) 좌표(x, y)로 텍스쳐 상의 어느 지점에 정점이 위치하는지 지정하는 방법입니다. 텍스쳐의 크기는 표준화되기 때문에 크기는 높이 1, 너비 1이고 (0,0)이 왼쪽 (1,1)이 오른쪽 상단에 해당합니다.

initBuffers의 변화는 이것 뿐입니​​다. 이제는 drawScene의 변화를 살펴볼 차례입니다. 우리가 이 레슨에서 가장 흥미를 느껴야하는 변화는 텍스쳐를 사용하도록 변경하는 부분입니다. 이 함수는 피라미드와 관련한 코드를 삭제한 것과 이에 따른 큐브의 회전 방법이 변경된 것에 대한 몇 가지 간략한 변동사항은 여러분이 직접 살펴 보도록 하세요. 이 함수의 변경된 부분에 대해서는 자세히 다루지 않도록 하겠습니다.

  var xRot = 0; // changed
  var yRot = 0; // changed
  var zRot = 0; // changed
  function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

    mat4.identity(mvMatrix);

    mat4.translate(mvMatrix, [0.0, 0.0, -5.0]); // changed

    mat4.rotate(mvMatrix, degToRad(xRot), [1, 0, 0]); // changed
    mat4.rotate(mvMatrix, degToRad(yRot), [0, 1, 0]); // changed
    mat4.rotate(mvMatrix, degToRad(zRot), [0, 0, 1]); // changed

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

animete함수에도 xRot, yRot, zRot를 갱신하는 프로세스에 대응하는 변화가 있지만 설명하지 않습니다.

자 이제부터 본론입니다. 텍스쳐에 관한 부분을 살펴봅시다. initBuffers함수에서 텍스쳐 좌표를 포함하는 버퍼를 설정했기 때문에 쉐이더에서 이것을 이용할 수 있도록 다음과 같이 적합한 속성을 바인드해 줄 필요가 있습니다.

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

...이제 WebGL은 각 버텍스가 텍스쳐의 어느 부분을 사용할지를 전달했습니다. 좀 전에 가져온 텍스쳐를 이용하는 것을 전달하고 큐브를 그립니다.

    gl.activeTexture(gl.TEXTURE0); // added
    gl.bindTexture(gl.TEXTURE_2D, neheTexture); // added
    gl.uniform1i(shaderProgram.samplerUniform, 0); // added

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    setMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

여기서 일어난 일은 다소 복잡합니다. WebGL은 gl.DrawElements와 같은 기능의 함수 호출에 대하여 최대 32개까지 텍스쳐를 사용할 수 있고 이들은 TEXTURE0에서 TEXTURE31로 넘버링이 되어 있습니다. 처음 두 줄은 좀 전에 로드된 텍스쳐를 TEXTURE0으로 사용할 것으로 선언하고, 세 번째 줄에 쉐이더의 uniform 변수에 0이라는 번호를 전달합니다.(uniform 변수는 다른 메트릭스를 위해 이용하는 경우와 마찬가지로 initShader의 쉐이더에서 보관하고 있습니다) 여기에는 텍스쳐 0을 사용하여 쉐이더에 전하고 있는 것입니다. 이 값이 어떻게 사용되는지에 대한것은 나중에 알아보겠습니다.

어쨌든, 이 세 줄이 실행되면 계속할 준비가 된 것입니다. 큐브를 형성하기 위한 삼각형을 그리는 일은 이전과 동일한 코드를 실행합니다.

텍스쳐 쉐이더

이제, 남은 설명이 필요한 새로운 코드는 쉐이더입니다. 먼저 vertex shader를 살펴 보도록합니다.

  attribute vec3 aVertexPosition;
  attribute vec2 aTextureCoord;  // added

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;

  varying vec2 vTextureCoord; // added

  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    vTextureCoord = aTextureCoord;  // added
  }

이것은 레슨 2에서 색상지정과 관련하여 작업을 수행했던 적이 있는 vertex shader와 아주 비슷합니다. 여기에서 행해지고 있는 것은 색상 대신 텍스쳐 좌표를 정점마다 속성으로부터 받고 varying변수에 출력하고 있을 뿐입니다.

모든 버텍스에 대하여 처리되도록 호출되면, WebGL은 fragment(기본적으로 모든 픽셀을 일일이 처리하죠)를 위해 정점 사이의 출력값을 선형으로 보간(linear interpolation)합니다. - 레슨 2에서 색상을 다룬던 그대로 말이죠. 따라서 (1,0)의 텍스쳐 좌표 (0,0)텍스쳐 좌표를 가진 정점 가운데 fragment는 텍스쳐 좌표는 (0.5,0)를 얻게 되는 것입니다. (0,0)과 (1,1)사이에서 (0.5,0.5)를 얻을 것입니다. 다음은 fragment shader입니다.

  #ifdef GL_ES
  precision highp float;
  #endif

  varying vec2 vTextureCoord; //added

  uniform sampler2D uSampler; //added

  void main(void) {
    gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)); //added
  }

여기에서 보완된 텍스쳐 좌표를 꺼냅니다. 또한 쉐이더에서 텍스쳐를 나타내는 sampler 유형의 값을 가지고 있습니다. drawScene에서 텍스쳐는 gl.TEXTURE0에 바인드 되었고 uniform변수인 uSampler는 0으로 설정되어 있습니다. 이 sampler는 텍스쳐를 나타냅니다. 쉐이더를 만드는 것은 texture2D 함수를 사용하여 원하는 색상을 텍스쳐 좌표를 사용하여 텍스쳐에서 추출하는 것입니다.

fragment를 위한 색상을 얻어내면 완료된 것입니다. 화면에 텍스쳐가 입혀진 개체가 나타나고 있습니다.

이번 레슨은 여기까지입니다. 이 레슨에서는 WebGL에서 3D 개체에 텍스쳐를 입히는 방법으로 이미지를 로드한 다음 WebGL에 텍스쳐를 사용한다는 사실을 알리고, 개체에 텍스쳐 좌표를 만들어 주고, 쉐이더에서 텍스쳐 좌표를 사용하는 것에 대하여 배웠습니다. 만약 질문이나 내용의 정정이 필요하다면 댓글로 남겨주세요.

다음 레슨에서는 Web 페이지를 보고있는 사람이 직접 조작할 수 있도록 키보드 입력을 통해 3D화면을 이동하는 방법에 대하여 다룹니다. 레슨 6을 학습하면 사용자가 직접 큐브를 회전하거나 확대, 축소 등을 할 수 있도록 할 수 있습니다. 더불어 WebGL에 텍스쳐의 스케일링을 위해 WebGL에게 던져주는 힌트를 조정해 봅니다.

이 문서의 원본은 WebGL Lesson 5 – introducing textures입니다. 최근들어 이곳저곳 신경쓸 일들이 급증해서 통 블로그에 시간을 할애하지 못하고 있네요. 역시, 관리직은 저랑 잘 안맞는단 말예요.

Comments

Got something to add? You can just leave a comment.

Your Reaction Time!

avatar

captcha