Rev. 2.73

세 번째 WebGL 레슨에 오신 것을 환영합니다. 레슨 2에 이어 이번 레슨에서는 객체가 회전 운동을 할 수 있도록 합니다. 이번 학습은 NeHe OpenGL의 네 번째 튜토리얼을 바탕으로 합니다.

다음 동영상은 이번 레슨에서 얻어지는 결과물입니다.

WebGL을 지원하는 브라우저를 사용중이라면 여기를 클릭하여 실재로 작동하는 WebGL 버전을 확인할 수 있습니다. 만약 지원하지 않는 브라우저를 사용중이라면 레슨 0을 참고하여 설치하세요.

지금부터 자세한 내용을 설명하도록 합니다.

시작에 앞서...

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

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

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

시작하기 전에 확실히 집고 넘어가야 하는 것이 있습니다. WebGL에서 3D장면 연출을 위해 애니메이션을 부여하는 것은 아주 간단합니다. 다른 장면을 연속해서 그릴려낼 뿐이니까요. 이 사실을 알고 난 후 저는 조금 허무했습니다. 당연하다고 말할 수 있을지도 모르지만, 제가 혼란스러워하는 이유는 이 보다 한층 더 높은 수준의 추상화를 사용하는 것이라고 생각했기 때문입니다. 예를 들자면, X지점에 그려진 사각형이 있습니다. 사각형을 이동하기 위하여 3D시스템에 Y지점까지 이동하라고 명령하면 자동으로 애니메이션을 제공해 줄 것이라고 생각했습니다. 그러나 실제로는 그렇지 않았고, X지점에 있는 사각형을 Y지점으로 이동하는 동작을 그려낸 후 Z지점까지 이동하는 동작을 매 프레임마다 계속해서 3D시스템에 주문하여 애니메이션을 구현해야 했습니다.

초기화 함수 - webGLStart

어쨋든, 이것이 뜻하는 바는 drawScene이라 불리우는 함수를 반복적으로 호출할 수 밖에 없다는 것입니다. 애니메이션하기 위해서는 뭔가 약간씩 달라지도록 그리는 처리를 직접해야 하는 것입니다. 자, 이제 예제 코드의 하단으로 이동하여 초기화 함수인 webGLStart의 변화를 살펴봅시다.

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

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

    tick(); // changed
  }

유일한 변화는 함수의 마지막에 drawScene 대신에 tick이라는 함수를 호출한 것입니다. 이 함수는 화면 갱신을 위해 정해진 시간에 의해 정기적으로 뭔가를 그려내는 일을 합니다. 예를 들면, 81도로 회전한 삼각형을 82도로 회전하는 과정을 수행합니다. 위로 조금 이동하면 다음과 같은 함수를 발견할 수 있습니다.

재귀함수 - tick

function tick() {
    requestAnimFrame(tick);

이 라인은 다음에 그려질 화면을 호출하는 과정입니다. requestAnimFrame 함수는 구글에서 제공하는 유틸리티 라이브러리인데 페이지의 상단에 위치한 <script> 태그에서 webgl-utils.js파일을 로드하여 사용할 수 있습니다. 이 라이브러리는 브라우저마다 독립적으로 구현된 기능(WebGL 장면을 다시 그려내는 시기를 브라우저에게 묻는 기능)을 하나로 통합하여 사용할 수 있게 합니다. 파이어폭스는 mozRequestAnimationFrame을 호출하고 크롬과 사파리는 webkitRequestAnimationFrame을 호출해야 합니다. 나중에는 requestAnimationFrame으로 통합될 것으로 예상되지만, 그 이전에는 이 라이브러리의 도움을 받기로 합니다.

requestAnimFrame은 drawScene 함수가 정기적으로 호출되는 효과를 가집니다. 자바스크립트 내장함수인 setInterval을 사용할 수도 있었지만(예전에는 실제로도 많이 사용되었음), 사용자가 WebGL로 구현된 페이지를 보고있지 않아도(혹은 다른 탭을 조회 중) 지속적으로 작동하기 때문에 브라우저의 전반적인 성능이 떨어지는 문제가 있어 이와 같은 자체 함수를 제공하기 시작한 것입니다. 즉, 방문객이 WebGL로 구현된 화면을 조회하고 있을 때에만 발동하는 것입니다. tick 함수의 나머지를 보면,

    drawScene();
    animate();
  }

tick함수는 자기자신을 다음번에 다시 호출해 줄 것을 requestAnimFrame에 예약해 두고 drawScene과 animate 두개의 함수를 실행합니다.

장면 그리기 - drawScene

drawScene함수는 index.html 소스의 2/3정도로 스크롤하면 발견할 수 있습니다. 먼저 눈에 들어온는 것은 함수 앞에 새롭게 정의된 두개의 전역 변수입니다.

  var rTri = 0;
  var rSquare = 0;

이들은 각 삼각형과 사각형의 회전상태를 보존할 것입니다. 모두 0도에서 시작하여 매 시간마다 조금씩 회전수치가 증가합니다.(여담 입니다만, 이런식의 전역 변수를 사용하는 것은 실제로 좋지 않은 방법입니다. 레슨 9에서 더 우아한 구조를 보여드리겠습니다.)

drawScene 함수의 변경사항은 삼각형을 그리는 부분입니다. 그 부분의 코드를 모두 올립니다, 새롭게 추가되거나 변경된 부분은 주석으로 표시했습니다.

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

    mat4.identity(mvMatrix);

    mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);

    mvPushMatrix(); // added
    mvRotate(rTri, [0, 1, 0]); // added

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

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

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

    mvPopMatrix();  // added

여기서 무슨 일이 발생했는지 설명하기 위하여 레슨 1로 돌아가 봅시다. 다음과 같이 말했습니다.

OpenGL은 장면을 그릴 때 대상 물체마다 현재(Current)의 위치와 회전값을 전달합니다. - 예를 들어, "20 만큼 전진하고 32도 만큼 회전하여 로봇을 그려"라는 식으로 말이죠. 실제로 복잡한 부분은 "요만큼 움직였고 이만큼 회전하여 그렸다"라는 지침이 있습니다. 이것은 "로봇을 그려라"라는 코드 한개의 함수로 캡슐화 할 수 있으므로 매우 편리합니다. 그리고 함수 호출 전에 이동 및 회전값을 변경함으로써 마음대로 로봇을 이동시킬 수 있습니다.

그렇습니다. model-view matrix에는 현재의 상태값이 보존되어 있습니다. 다음 코드를 봅시다.

    mat4.rotate(mvMatrix, degToRad(rTri), [0, 1, 0]);

아주 명백하지요. model-view matrix에 보존되어 있는 현재 회전수치를 수직 축 주위의 rTri에 표시된 각도만큼 회전하도록 변경합니다.(축이 두 번째 인수에 배열로 지정됩니다.) 이것은 삼각형을 그릴 때, rTri도 만큼 회전하고 있다는 것입니다. mat4.rotate에 의해 앵글의 각도가 변경됩니다. 여기에 사용된 degToRad 함수는 각도로 변환해 주는 간단한 일을 처리합니다.

그렇다면, mvPushMatrix과 mvPopMatrix는 무엇일까요? 이름에서 알 수 있을지도 모르지만, 이것들도 model-view matrix와 관련이 있습니다. 로봇을 그리는 비유로 돌아가 보면, A지점으로 이동하는 로봇을 그리려고 합니다. 최고 수준의 추상화로써 A지점으로 이동하는 구간의 일부분(옵셋) 만큼 이동하고 주전자를 그립니다. 로봇을 그리는 코드는 model-view matrix에서 변경됩니다. 이것은 로봇의 몸체에 적용되어 시작되며, 아래에는 다리, 위에는 머리, 그리고 팔이 붙어있습니다. 여기서 문제는 이처럼 A지점에서 옵셋만큼 이동 시키려고했을 때 발생합니다. A지점이 아니라 마지막으로 그린​​ 것에 대해 옵셋만큼 이동하기 때문에 로봇의 팔에 들려있는 주전자는 뜬 상태로 렌더링됩니다. 이것은 의도대로 되지 않아요.(?)

요구되는 분명한 것은 로봇을 그리기 전에 model-view matrix를 보존할 방법과 다시 되돌릴 방법 이 되겠군요. 예 그렇습니다, mvPushMatrix과 mvPopMatrix에서 이것을 수행합니다. mvPushMatrix는 매트릭스를 스택(Stack)에 쌓습니다. mvPopMatrix는 스택에서 매트릭스를 꺼내 현재의 매트릭스를 바꿉니다. 스택을 이용한다는 것은 여러 단계의 중첩 그리기 코드의 비트를 이용할 수 있다는 것입니다. 각각 그리기 코드 안에서 model-view matrix를 조작하여 그려낸 후 복구합니다. 따라서 회전하는 삼각형의 그리기가 완료되면 mvPopMatrix을 통해 model-view matrix를 리턴합니다.(?) 다음 코드를 봅시다.

    mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);

...이 회전하지 않는 기준 좌표계에서의 장면을 가로질러 (model-view matrix를) 이동하도록 합니다. (만약 아직도 이 의미를 잘 모르겠다면, 코드를 복사하여 push/pop을 삭제한 후 실행하여 무슨 일이 일어나는가를 관찰하는 것이 좋습니다. 직접 보면 금세 알아차릴 수 있을 것입니다.)

이 세가지 변화는 삼각형이 사각형에 영향을 주지 않고 중심에서 수직축을 중심으로 회전합니다. 또한 사각형의 수평축을 중심으로 주위를 회​​전하는 유사한 세가지를 처리해 줍니다.

    mvPushMatrix(); // added
    mat4.rotate(mvMatrix, degToRad(rSquare), [1, 0, 0]); // added

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

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

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);

    mvPopMatrix(); // added
  }

...그리고 이것으로 drawScene 코드의 모든 변경은 끝났습니다.

살아 움직이도록 - animate

장면을 애니메이션하기 위해 필요한 또 다른 것은 각각의 렌더링에서 조금씩 다른 장면을 그릴 수 있도록 rTri과 rSquare의 값이 시간의 흐름에 따라 변경되도록 하는 것입니다. 이 과정은 drawScene과 마찬가지로 주기적으로 호출되는 animate라는 새롭게 추가된 함수에서 수행합니다. 다음 코드를 보시죠.

  var lastTime = 0;
  function animate() {
    var timeNow = new Date().getTime();
    if (lastTime != 0) {
      var elapsed = timeNow - lastTime;

      rTri += (90 * elapsed) / 1000.0;
      rSquare += (75 * elapsed) / 1000.0;
    }
    lastTime = timeNow;
  }

장면을 애니메이션하는 간단한 방법은 animate 함수가 호출될 때 마다 일정한 값을 단순히 더하는 것입니다.(이 레슨의 기반이 되는 원래 OpenGL 레슨도 그렇게하고 있습니다) 그러나 여기에는 좀 더 좋은 방법이라고 생각되는 방법을 선택했습니다. 개체의 회전량은 이전 함수 호출에서 얼마나 경과했는지에 따라 결정됩니다. 좀더 정확히 말하면, 삼각형은 1초에 90도 사각형은 1초 동안 75도 회전합니다. 이 방법의 좋은 점은, 컴퓨터의 연산 처리능력에 상관없이 같은 속도로 움직이는 장면을 볼 수 있게 되는 것입니다. 느린 컴퓨터를 사용하는 사람은 단순히 움직임이 끊겨 보이게 될 뿐이죠. 이 것은 이번 레슨과 같은 간단한 데모에서는 문제없이 사용할 수 있지만, 게임과 같은 경우에는 분명히 문제가 됩니다.

나머지 추가한 코드들

이것으로, 주기적으로 호출되는 animate 함수와 drawScene 함수에 대하여 알아 보았습니다. 이제 추가적인 코드인 mvPushMatrix와 mvPopMatrix를 살펴봅시다.

  var mvMatrix = mat4.create();
  var mvMatrixStack = []; // added
  var pMatrix = mat4.create();

  // added
  function mvPushMatrix() {
    var copy = mat4.create();
    mat4.set(mvMatrix, copy);
    mvMatrixStack.push(copy);
  }

  function mvPopMatrix() {
    if (mvMatrixStack.length == 0) {
      throw "Invalid popMatrix!";
    }
    mvMatrix = mvMatrixStack.pop();
  }

놀랄만한 것은 아무것도 없네요. 매트릭스를 유지하기 위한 스택를 가지고 적절하게 push와 pop이 되도록 정의하고 있습니다. 마지막으로 압서 언급한 degToRad 함수를 살펴 보도록 하겠습니다. 학교에서 가르쳐주는 수학을 기억한다면 그리 놀랄일은 없습니다.

    function degToRad(degrees) {
        return degrees * Math.PI / 180;
    }

이것으로 끝입니다. 더 이상의 변경사항은 없습니다. 이제 당신은 WebGL을 이용하여 간단한 애니메이션을 장면에 담는 방법을 알게되었습니다. 의견이나 수정할 곳이 있으면 아래에 댓글로 남겨주세요. 그리고, 다음 레슨에서는 2D개체를 3D월드에 존재하는 진짜 3D개체로 만듭니다. 다음 레슨으로 이동하시려면 여기를 클릭하세요.

이 문서의 원본은 WebGL Lesson 3 – a bit of movement입니다.

Comments

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

Your Reaction Time!

captcha

avatar