Rev. 2.73

네 번째 WebGL 레슨에 오신 것을 환영합니다. 이번에는 실제 3D 개체를 화면에 그려봅니다. 이 학습은 "NeHe OpenGL의 다섯 번째 튜토리얼"을 기반으로 하고 있습니다.

다음 동영상은 이번 학습에서 만들어 볼 WebGL 컨텐츠입니다. 물론, 브라우저에서 실행됩니다.

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

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

시작에 앞서...

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

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

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

변수 및 인자 추가/변경하기 - animate, drawScene

이 레슨과 이전 레슨의 차이는 animate, initBuffers 그리고 drawScene 이 3개의 함수에서만 이루어집니다. animate 함수가 위치한 곳으로 스크롤하면 그 첫 번째 아주 작은 변화를 발견할 수 있습니다. 두 객체의 현재 회전 상태를 기억하기 위한 변수인 rTri과 rSquare의 이름이 다음과 같이 변경되었습니다.

   rPyramid += (90 * elapsed) / 1000.0; 
   rCube -= (75 * elapsed) / 1000.0;

이것이 이 함수 변경의 전부입니다. 자, 다음으로 drawScene함수를 살펴 보도록합니다. 함수 직전에 다음과 같이 새로운 변수를 추가했습니다.

   var rPyramid = 0;
   var rCube = 0;

다음은 함수의 시작 부분에서, 피라미드의 위치를​​ 바꾸는 코드를 작성합니다. 여기서 한 번 그릴때 마다, 피라미드의 좌표는 Y축을 중심으로 회전합니다. 이것은 이전 단원에서 삼각형에 적용했던 것과 같습니다.

    mvRotate(rPyramid, [0, 1, 0]);

...이제 그려낼 수 있습니다. 이전 단원과의 차이는 컬러풀한(색조가 많은) 삼각형을 그대로 이용하여 화려한 피라미드를 나타내기 위해서는 더 많은 꼭지점들과 색상들을 처리하는 과정을 필요로하며, initBuffers함수에서 핸들하게 됩니다.(initBuffers함수는 조금 나중 봅시다). 즉, 이것의 의미하는 것은 지금까지 사용된 버퍼의 이름이 변경될 뿐 나머지 코드들은 동일하게 사용할 수 있습니다.

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

    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, pyramidVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLES, 0, pyramidVertexPositionBuffer.numItems);

예, 매우 간단합니다. 이제부터 큐브(Cube, 입방체)를 구성하는 코드를 살펴봅시다. 우선 회전시킵니다. 이번에는 X축 뿐만아니라 Z축(보는 사람의 관점)도 회전시킵니다.

    mvRotate(rCube, [1, 1, 1]);

이제, 큐브를 그립니다. 여기서 코드가 약간 복잡해지긴 하겠지만 아래와 같은 세가지 방법이 있습니다.

  • "트라이앵글 스트립(triangle strip)"을 사용하여 연결된 삼각형을 그리는 방법. 만약 큐브의 모든 면이 같은 색인 경우 이것이 가장 간단한 방법입니다. 이 방법은, 지금까지 사용했던 정점 좌표를 그대로 사용할 수 있습니다. 다음에 위치할 한 면을 그리기 위해서는 새로운 2개의 좌표만 추가하면 될 뿐입니다. 매우 효율적이죠. 하지만 모든 면이 다른 색상을 가지게 하고 싶기 때문에 이 방법은 부적절합니다. 왜냐하면, 큐브의 정점은 다른 세개의 삼각형이 공유하고 있으며, 색상을 변경하려면 매우 까다로운 방법을 사용할 필요가 있기 때문입니다. 설명하는 것 조차 어렵기에 이쯤에서 그만합시다.
  • 여섯개의 사각형 조각을 그리는 방법으로 속임수를 씁니다. 여섯개 사각형의 좌표를 부수적으로 관리하여 개별적으로 그리는 방식으로 색상을 입힐 수 있습니다. 첫 번째 레슨에서는 이 방법을 사용했습니다. 그리고 잘 작동했습니다. 하지만 그다지 좋은 WebGL 학습이 되지 않습니다. 나중에 추가적인 계산 비용이 매우 크게 소진될 것이 우려되었기에 그만두었었죠. 만약 계산 비용을 최소화하려고 했다면 drawArrays함수의 호출 횟수를 줄여야 합니다.
  • 마지막 옵션으로는 두개의 삼각형으로 조합된 여섯개의 사각형으로 큐브를 지정합니다. 그러나 특수한 명령을 WebGL에 사용하여 대량으로 렌더링하는 방법입니다. 이것은 트라이앵글 스트립을 사용하는 방법과 거의 유사하지만, 한점 한점 추가 해 나가는 것이 아니라, 12개의 삼각형을 한번에 정의해 버리는 점이 다릅니다. 또한 간단하게 각 면의 색상을 지정할 수 있으며, 코드도 비교적 깨끗하게 작성할 수 있습니다. 여기서 이를 위한 새로운 기능인 drawElements함수를 소개합니다. 결국, 이 방법이 선택된 것이죠. :-)

우선, 큐브의 정점 좌표와 각 면의 색상이 들어간 버퍼 배열을 만듭니다. 여기서 버퍼 배열은 피라미드를 만들때 했던것과 같이 initBuffers함수에 정의하고 있습니다.

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

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

다음 과정은 앞서 정의한 정점으로 삼각형을 그려냅니다. 여기서 작은 문제가 발생합니다. 큐브의 정면을 나타낸다고 생각해 봅시다. 4개의 정점에 대한 위치를 가지며, 각각의 정점에는 색상이 지정됩니다. 그러나 이는 곧 삼각형 2개를 이용해서 그리는 것이기 때문에 총 6개의 정점을 가진다는 의미입니다. 하지만 앞서 정의한 정점 배열은 4개의 위치정보만이 들어있을 뿐입니다.

그렇다면 어떻게 해야할까요? 해결방법을 조금 알기쉽게 말하자면 "배열 버퍼의 처음부터 3번째 까지의 정점으로 1개의 삼각형을 그린다. 그런 다음 1번째와 3번째와 4번째 정점으로 삼각형을 그리는" 것입니다. 이렇게 함으로써 하나의 면을 그릴 수 있습니다. 나머지 면도 마찬가지로 그려갑니다. 이것이 핵심입니다.

이렇게 사용하는 버퍼를 "element array buffer"라 칭하며, drawElements함수에서 호출됩니다. 지금까지 사용했던 정점 배열과 같이, "element array buffer"도 initBuffers에서 초기화합니다. 이 새로운 배열은 정점에 대한 좌표와 색상 정보를 포함합니다. 나중에 그 속을 들여다 봅시다.

이것을 사용하기 위해서는 큐브의 정점 배열을 element array buffer로 등록해 줍니다.(WebGL은 서로 다른 current array와 element array을 저장할 수 있으므로, 명시적으로 어떤 것이 gl.bindbuffer에서 호출될 것인지를 결정해야 합니다.) 그리고 모델-뷰 행렬(model-view matrix)과 투영 행렬(projection matrix)들을 그래픽 카드로 보내는 코드를 drawElements에 작성합니다.

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

버퍼의 입체화 - initBuffers

이제 drawScene함수가 준비되었습니다. 남아있는 initBuffers함수가 가진 코드는 매우 간단합니다. 여기에는 새로운 종류의 개체를 처리하기 위해 또다른 새로운 이름을 가진 버퍼를 정의합니다. 물론, 큐브의 정점 배열도 여기에 추가합니다.

  var pyramidVertexPositionBuffer;
  var pyramidVertexColorBuffer;
  var cubeVertexPositionBuffer;
  var cubeVertexColorBuffer;
  var cubeVertexIndexBuffer;

피라미드의 정점 배열 좌표를 입력합니다. numItems 값이 변경되고 있는데에 주목하세요.

    pyramidVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
    var vertices = [
        // Front face
         0.0,  1.0,  0.0,
        -1.0, -1.0,  1.0,
         1.0, -1.0,  1.0,
        // Right face
         0.0,  1.0,  0.0,
         1.0, -1.0,  1.0,
         1.0, -1.0, -1.0,
        // Back face
         0.0,  1.0,  0.0,
         1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0,
        // Left face
         0.0,  1.0,  0.0,
        -1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    pyramidVertexPositionBuffer.itemSize = 3;
    pyramidVertexPositionBuffer.numItems = 12;

...마찬가지로 피라미드의 버텍스에 색상 버퍼를 변경합니다.

    pyramidVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
    var colors = [
        // Front face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        // Right face
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        // Back face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        // Left face
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    pyramidVertexColorBuffer.itemSize = 4;
    pyramidVertexColorBuffer.numItems = 12;

...그리고 큐브의 버텍스 위치 배열입니다. 여기에는 정점 좌표가 들어 있습니다.

    cubeVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
    vertices = [
      // Front face
      -1.0, -1.0,  1.0,
       1.0, -1.0,  1.0,
       1.0,  1.0,  1.0,
      -1.0,  1.0,  1.0,

      // Back face
      -1.0, -1.0, -1.0,
      -1.0,  1.0, -1.0,
       1.0,  1.0, -1.0,
       1.0, -1.0, -1.0,

      // Top face
      -1.0,  1.0, -1.0,
      -1.0,  1.0,  1.0,
       1.0,  1.0,  1.0,
       1.0,  1.0, -1.0,

      // Bottom face
      -1.0, -1.0, -1.0,
       1.0, -1.0, -1.0,
       1.0, -1.0,  1.0,
      -1.0, -1.0,  1.0,

      // Right face
       1.0, -1.0, -1.0,
       1.0,  1.0, -1.0,
       1.0,  1.0,  1.0,
       1.0, -1.0,  1.0,

      // Left face
      -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.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    cubeVertexPositionBuffer.itemSize = 3;
    cubeVertexPositionBuffer.numItems = 24;

큐브의 정점 색상 배열은 조금 복잡합니다. 정점 색상 목록을 만들기 위해 반복문(roop, 루프)을 사용하고 있으며, 각 색상을 4번씩 정의할 필요는 없습니다.

    cubeVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer);
    var colors = [
      [1.0, 0.0, 0.0, 1.0],     // Front face
      [1.0, 1.0, 0.0, 1.0],     // Back face
      [0.0, 1.0, 0.0, 1.0],     // Top face
      [1.0, 0.5, 0.5, 1.0],     // Bottom face
      [1.0, 0.0, 1.0, 1.0],     // Right face
      [0.0, 0.0, 1.0, 1.0],     // Left face
    ];
    var unpackedColors = []
    for (var i in colors) {
      var color = colors[i];
      for (var j=0; j < 4; j++) {
        unpackedColors = unpackedColors.concat(color);
      }
    }
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpackedColors), gl.STATIC_DRAW);
    cubeVertexColorBuffer.itemSize = 4;
    cubeVertexColorBuffer.numItems = 24;

마지막으로, element array buffer를 하나 더 정의합니다.(gl.bindBuffer와 gl.bufferData의 첫 인수는 다르다는 것에 다시 한번 주의하세요.)

    cubeVertexIndexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    var cubeVertexIndices = [
      0, 1, 2,      0, 2, 3,    // Front face
      4, 5, 6,      4, 6, 7,    // Back face
      8, 9, 10,     8, 10, 11,  // Top face
      12, 13, 14,   12, 14, 15, // Bottom face
      16, 17, 18,   16, 18, 19, // Right face
      20, 21, 22,   20, 22, 23  // Left face
    ]
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);
    cubeVertexIndexBuffer.itemSize = 1;
    cubeVertexIndexBuffer.numItems = 36;

기억하세요. 이것은 "어떤 정점을 나타내는데, 정점의 위치 배열과 색상 배열 중의 특정 인덱스를 사용 해라"라는 배열입니다. 첫 번째는 drawScene에서 명령이 발생할 때에 0번째와 1번째와 2번째 정점으로 삼각형을 사용하고, 0번째와 2번째와 3번째 정점으로 삼각형을 사용한다는 뜻이 됩니다. 이 때 2개의 삼각형은 같은 색으로 이어져 있기 때문에 결과적으로 정점 0과 1과 2와 3을 사용하여 사각형을 사용한다는 것입니다. 이것을 나머지 모든 면에서 반복하면 완료됩니다.

이제 WebGL 장면에서 어떻게 3D개체를 사용하고 만드는가, 그리고 어떻게 element array buffer와 drawElement 로 정점을 다시 사용하는가에 대해서 배운 것입니다. 만약 질문이나 내용의 정정이 필요하다면 댓글로 남겨주세요. 다음 레슨에서는 텍스쳐 매핑(texture mapping)에 대하여 학습합니다.

이 문서의 원본은 WebGL Lesson 4 – some real 3D objects입니다.

Comments

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

Your Reaction Time!

captcha

avatar