목차

WebGL2Fundamentals.org

Fix, Fork, Contribute

WebGL2 Attributes

이 글은 WebGL에서 attribute 상태(state)가 어떻게 구성되는지에 대한 개념의 이해를 돕기 위해 작성되었습니다. 텍스처 유닛프레임 버퍼에 대해서도 유사한 글이 있으니 참고 하세요.

이 내용을 이해하기 위해서는 먼저 WebGL 작동 원리WebGL 셰이더와 GLSL을 먼저 읽으셔야 합니다.

Attributes

WebGL의 attribute는 버퍼로부터 데이터를 읽어와서 정점 셰이더의 입력으로 주어지는 데이터를 말합니다. WebGL에서는 gl.drawArrays 또는 gl.drawElements가 호출되면 사용자가 작성한 정점 셰이더를 N번 실행합니다. 각 iteration마다 attribute는 바인딩 되어있는 버퍼로부터 어떻게 데이터를 가져올지를 정의하고 그 데이터를 정점 셰이더 내의 attribute로 넘겨줍니다.

이러한 동작이 만약 자바스크립트로 구현되었다면 아마 아래와 같이 쓰여졌을 겁니다.

// pseudo code
const gl = {
  arrayBuffer: null,
  vertexArray: {
    attributes: [
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 },
    ],
    elementArrayBuffer: null,
  },
}

위에서 볼 수 있듯이 16개의 attribute가 있습니다.

gl.enableVertexAttribArray(location) 또는 gl.disableVertexAttribArray를 호출하는 것은 아래와 같은 상황으로 생각하시면 됩니다.

// pseudo code
gl.enableVertexAttribArray = function(location) {
  const attrib = gl.vertexArray.attributes[location];
  attrib.enable = true;
};

gl.disableVertexAttribArray = function(location) {
  const attrib = gl.vertexArray.attributes[location];
  attrib.enable = false;
};

다시말해, location은 attribute의 인덱스를 직접 참조하는 값입니다.

비슷하게, gl.vertexAttribPointer는 attribute의 나머지 값들을 세팅하기 위해 사용됩니다. 아래와 같은 방식으로 구현될 수 있을겁니다.

// pseudo code
gl.vertexAttribPointer = function(location, size, type, normalize, stride, offset) {
  const attrib = gl.vertexArray.attributes[location];
  attrib.size = size;
  attrib.type = type;
  attrib.normalize = normalize;
  attrib.stride = stride ? stride : sizeof(type) * size;
  attrib.offset = offset;
  attrib.buffer = gl.arrayBuffer;  // !!!! <-----
};

gl.vertexAttribPointer를 호출하면 attrib.buffer는 현재 활성화 상태인 gl.arrayBuffer로 설정되게 됩니다. 위 의사 코드(pseudo code)에서 gl.arrayBuffergl.bindBuffer(gl.ARRAY_BUFFER, someBuffer)를 호출함으로써 설정됩니다.

// pseudo code
gl.bindBuffer = function(target, buffer) {
  switch (target) {
    case ARRAY_BUFFER:
      gl.arrayBuffer = buffer;
      break;
    case ELEMENT_ARRAY_BUFFER;
      gl.vertexArray.elementArrayBuffer = buffer;
      break;
  ...
};

다음으로, 정점 셰이더를 봅시다. 정점 셰이더에서는 attribute를 선언할 수 있습니다. 예를들어:

#version 300 es
in vec4 position;
in vec2 texcoord;
in vec3 normal;

...

void main() {
  ...
}

gl.linkProgram(someProgram)를 호출해서 정점 셰이더와 프래그먼트 셰이더를 링크하면 WebGL (드라이버/GPU/브라우저)은 어떤 인덱스/location을 각 attibute에 대해 사용할지를 결정합니다. 추후에 보여드릴 코드처럼 직접 location을 할당해 주지 않는 한, WebGL에서 각 attribute에 대해 어떤 인덱스를 고를지 알 수 없습니다. 그러니 position, texcoord, normal에 대해 어떤 attibute를 사용하기로 결정했는지 물어봐야겠죠? gl.getAttribLocation를 호출해서 물어볼 수 있습니다.

const positionLoc = gl.getAttribLocation(program, 'position');
const texcoordLoc = gl.getAttribLocation(program, 'texcoord');
const normalLoc = gl.getAttribLocation(program, 'normal');

positionLoc = 5라고 가정해봅시다. 그 말은 정점 셰이더가 실행될 때(여러분이 gl.drawArrays 또는 gl.drawElements를 호출했을 때) 정점 셰이더는 여러분이 attribute 5에 대해 올바른 type, size, offset, stride, buffer 등등을 설정했다고 가정한다는 것입니다.

프로그램을 링크하기 전에 gl.bindAttribLocation(program, location, nameOfAttribute)를 호출해서 location을 직접 선택할 수 있습니다. 예를들어:

// `gl.linkProgram`에게 `position`에 대해 attribute 7번을 사용하도록 명시
gl.bindAttribLocation(program, 7, 'position');

GLSL ES 3.00 셰이더를 사용한다면 셰이더 내에서 직접적으로 어떤 location을 사용할지 아래와 같이 명시해 줄 수도 있습니다.

layout(location = 0) in vec4 position;
layout(location = 1) in vec2 texcoord;
layout(location = 2) in vec3 normal;

...

bindAttribLocation을 사용하는 것이 훨씬 D.R.Y.한 것 같긴 하지만, 여러분이 원하는대로 하시면 됩니다.

전체 Attribute State

위의 설명 중 빠진 것 중 하나는, 각 attribute는 기본값이 있다는 것입니다. 위에서 설명하지 않은 이유는 이를 활용하는 경우가 흔하지 않기 때문입니다.

attributeValues: [
  [0, 0, 0, 1],
  [0, 0, 0, 1],
  ...
],
vertexArray: {
  attributes: [
   { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?,
    divisor: 0, },
   { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?,
    divisor: 0, },
   ...

gl.vertexAttribXXX들을 사용해 각 attibute의 값을 설정해 줄 수 있습니다. 이 값들은 enable가 false일 때 사용됩니다. enable이 true라면 attribute의 데이터는 할당된 버퍼로부터 가져옵니다.

Vertex Array Objects (VAO)s

const vao = gl.createVertexArray();

이 코드는 위쪽 의사 코드gl.vertexArray에 할당될 객체를 만듭니다. gl.bindVertexArray(vao)를 호출하면 생성된 vertex array object를 현재 vertex array로 만듭니다.

// pseudo code
gl.bindVertexArray = function(vao) {
  gl.vertexArray = vao ? vao : defaultVAO;
};

이 코드는 현재 VAO에 있는 모든 attribute와 ELEMENT_ARRAY_BUFFER를 설정해서 여러분이 특정 형상을 화면에 그리고 싶을 때 gl.bindVertexArray만을 호출해서 모든 attibute를 설정할 수 있게 합니다. 그렇지 않으면 각 attibute마다 gl.bindBuffer, gl.vertexAttribPointer (추가적으로 gl.enableVertexAttribArray까지)를 호출해야 합니다.

보시다시피 vertex array object를 사용하는 것이 훨씬 좋습니다. 하지만 그러려면 코드가 좀 더 구조화가 필요합니다. 예를 들어 한 셰이더를 통해 gl.TRIANGLES로 정육면체를 그리고 다른 셰이더로 gl.LINES로 한번 더 그리고 싶다고 해봅시다. 삼각형들을 그릴 때는 조명 효과를 위해 법선을 사용하기 때문에 셰이더에서 attribute를 아래와 같이 선언했다고 해 봅시다:

#version 300 es
// 조명 효과 셰이더
// 정육면체를 삼각형 기반으로 그릴 때 사용하는 셰이더

in vec4 a_position;
in vec3 a_normal;

이 위치와 법선값들을 조명 효과 관련한 첫 글에서처럼 사용한다고 합시다.

라인으로 그릴 때는 조명 효과가 필요 없고, 단일 색상으로 그려서 첫 글에서 사용한 셰이더와 유사한 효과를 얻고 싶다고 해 봅시다. 색상을 위한 uniform을 선언하고, 정점 셰이더에서는 위치값만 사용합니다.

#version 300 es
// 단일 색상 셰이더
// 정육면체를 라인 기반으로 그릴 때 사용하는 셰이더

in vec4 a_position;

각 셰이더에 대해 attribute의 location이 어떻게 결정될지 알 수 없습니다. 예를 들어 조명 효과 셰이더에서는 location이 아래와 같이 되었다고 해 봅시다.

a_position location = 1
a_normal location = 0

그리고 단일 색상 셰이더에 대해서는 아래와 같다고 합시다.

a_position location = 0

셰이더를 바꾸면 attribute 설정이 달라져야 하는것이 자명합니다. 한 셰이더에서는 a_position의 데이터가 attribute 0에 들어와야 하고, 다른 셰이더에서는 attribute 1에 들어와야 합니다.

attribute를 다시 설정하는 추가적인 작업이 필요합니다. vertex array object를 사용하는 이유는 그 작업을 하지 않기 위해서였는데도 말입니다. 이 문제를 해결하기 위해서 셰이더 프로그램을 링크하기 전에 location을 바인딩해 줄겁니다.

WebGL에 아래와 같이 알려줍니다.

gl.bindAttribLocation(solidProgram, 0, 'a_position');
gl.bindAttribLocation(lightingProgram, 0, 'a_position');
gl.bindAttribLocation(lightingProgram, 1, 'a_normal');

gl.linkProgram을 호출하기 전에요. 이렇게 하면 WebGL에게 셰이더를 링크하기 전에 어떤 location을 할당할지를 알려줄 수 있습니다. 이렇게 하면 두 셰이더에 대해서 동일한 VAO를 사용할 수 있습니다.

Attribute 상한

WebGL2 명세에서는 최소 16개의 attibute를 지원하도록 되어 있지만 특정 컴퓨터/브라우저/구현/드라이버는 더 많은 attribute를 지원할 수 있습니다. 얼마나 많은 attribute를 지원하는지 아래 코드를 호출하여 확인할 수 있습니다.

const maxAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);

16개 이상의 attribute를 사용하고자 하면 실제 지원하는 것이 몇개인지를 확인하는 것이 좋고, 사용자의 환경에서 그 만큼을 지원하지 않는다면 이를 사용자에게 알려주거나 다른 더 간단한 셰이더를 사용하도록 해야 합니다.

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