Rev. 2.73

Felix Woitzel씨가 만든 놀라운 WebGL 데모입니다. 그는 쉐이더를 이용하여 l-시스템, 프랙탈 끌어당기기와 같은 데모들을 만들어 내고 있습니다. 아래의 데모역시 그가 최근 작업한 것이며, 그래픽 카드에서 실행되는 WebGL 픽셀 쉐이더 언어인 GLSL을 이용하여 만들어진 것입니다. 이방법은 화면에 출력되는 비트맵의 모든 픽셀을 매우 빠르게 처리할 수 있게 합니다. 그는 Fine-tuned 8Bit Reaction-Diffusion 시스템에 순회하는 물결들과 서브픽셀 붕괴(subpixel decay)효과를 추가하고, 광원의 변위 매핑을 위해서 그라디언트로 생성한 텍스처를 사용했다고 합니다. 참고로, 그는 독일인이며 윈엠프의 쉐이더기반 플러그인인 MilkDrop를 만든 경험이 있습니다.

fps

x-shader/x-fragment :

#ifdef GL_ES
precision highp float;
#endif
 
  uniform sampler2D sampler_prev;
  uniform sampler2D sampler_prev_n;
  uniform sampler2D sampler_blur;
  uniform sampler2D sampler_noise;
  uniform sampler2D sampler_noise_n;
 
  varying vec2 pixel;
  uniform vec2 pixelSize;
  uniform vec2 aspect;
  uniform vec4 rnd;
  uniform vec2 mouse;
  uniform float time;
 
void main(void) {
 
  vec2 lightSize=vec2(4.);
 
  // grabbing the blurred gradients
  vec2 d = pixelSize*2.;
  vec4 dx = (texture2D(sampler_blur, pixel + vec2(1,0)*d) - texture2D(sampler_blur, pixel - vec2(1,0)*d))*0.5;
  vec4 dy = (texture2D(sampler_blur, pixel + vec2(0,1)*d) - texture2D(sampler_blur, pixel - vec2(0,1)*d))*0.5;
 
  // adding the pixel gradients
  d = pixelSize*1.;
  dx += texture2D(sampler_prev, pixel + vec2(1,0)*d) - texture2D(sampler_prev, pixel - vec2(1,0)*d);
  dy += texture2D(sampler_prev, pixel + vec2(0,1)*d) - texture2D(sampler_prev, pixel - vec2(0,1)*d);
 
  vec2 displacement = vec2(dx.x,dy.x)*lightSize; // using only the red gradient as displacement vector
  float light = pow(max(1.-distance(0.5+(pixel-0.5)*aspect*lightSize + displacement,0.5+(mouse-0.5)*aspect*lightSize),0.),4.);
 
  // recoloring the lit up red channel
  vec4 rd = vec4(texture2D(sampler_prev,pixel+vec2(dx.x,dy.x)*pixelSize*8.).x)*vec4(0.7,1.5,2.0,1.0)-vec4(0.3,1.0,1.0,1.0);
  gl_FragColor = mix(rd,vec4(8.0,6.,2.,1.), light*0.75*vec4(1.-texture2D(sampler_prev,pixel+vec2(dx.x,dy.x)*pixelSize*8.).x));
  
  //gl_FragColor = texture2D(sampler_prev, pixel); // bypass
  gl_FragColor.a = 1.;
}

Comments

다섯 번째 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

네 번째 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