본문 바로가기

2.분석 및 설계

glTF 포멧

들어가기

우연하게 glTF을 알게되고 우연하게 괜찮은 자료를 알게 되어서 한번 간단하게 정리해보았습니다. 번역하고 정리하면 내용상에 틀린 부분도 있으니 혹시 있다면 저에게 알려주시면 수정하겠습니다. ^^;;;;

작성자: ospace114@empal.com, http://ospace.tistory.com/

glTF 구성

glTF은 Khronos Group에서 만든 JSON 형태의 3D 컨텐트를 네트워크에 전송하는 포멧이다. 아래는 파일에 최상위 층에 구성요소이다.

  • scenes, nodes: 장면에 기본 구조
  • cameras: 장면 시점 구성
  • meshes: 3D 객체 지오메트리
  • buffers, bufferView, accessors: 데이터 참조와 데이터 레이아웃 기술
  • material: 어떻게 객체를 렌더할지 정의
  • textures, images, samplers: 객체 표현 모습
  • skins: 정점 스키닝 정보
  • animations: 시간에 따른 속성 변화

각 구성요소에는 배열형태로 되어있고 객체 간에 참조는 배열 색인으로 구성된다. 모든 에셋은 단일 바이너리 glTF 파일에 저장 가능하다. 최상위 층의 구성요소간에 관계는 다음과 같다. (정확한 내용은 참조에)

출처:  https://www.khronos.org/files/gltf20-reference-guide.pdf

이진 데이터 참조

이미지와 버퍼는 외부 파일을 참조한다. 버퍼는 지오메트리와 에니메이션 데이터가 포함된 이진파일(.BIN)이다. 이미지는 모델에 대한 텍스처 데이터가 포함된 이미지파일(PNG, JPG,...)이다. 데이터는 URI로 참조되지만 MIME 타입 데이터 URI형태로 직접 포함될 수 있다.

  • data:application/gltf-buffer;base64,xxxxxxxxx
  • data:image/png;base64,xxxxxxxx

scenes, nodes

각 scene는 node 색인 배열이 있다. 각 node에는 chidren이라는 색인 배열이 있다. 이 children로 간단한 scene 트리구조를 만드다.
node은 로컬 변환을 가지고 있다. 컬럼위주 matrix나 translation, rotation, sacle 속성으로 되어있다. 쿼터니언형태로 제공된다.
로컬 변환 행렬은 M = T * R * S 형태로 계산된다.

scene: 0,
scenes: [
{ nodes: [0, 1, 2]}
],
nodes: [
{
  matrix: [ ... ],
  children: [3, 4],
  ...
},
{
  translation:[ 0,0,0 ],
  rotation: [0,0,0,1],
  scale: [1,1,1],
  ...
}
...
]

node의 글로벌 변환은 루트에서 각 노드에까지 경로상에 모든 로컬 변환 곱으로 제공된다. 각 node은 mesh나 camera를 참조한다. 레더링하는 동안 이들은 노드의 글로벌 변환으로 생성되고 변환된다. 이동, 회전, 스케일 속성은 애니메이션 대상이 된다. 노드들은 정점 스키닝에서도 사용된다. 노드 트리 구조는 움직이는 캐릭터의 스켈레톤을 정의하는데 사용된다. node은 mesh와 kim을 참조한다. skin은 mesh가 현재 스켈레톤 포즈을 기반으로 변형되는 방법에 대한 추가적인 정보를 가진다.

nodes: [
{
  mesh: 4,
  ...
},
{
  camera: 2,
  ...
}
]

meshes

meshes은 지오메트리 데이터를 참조하는 다중 mesh primitives을 가진다. 각 mesh primitive은 랜더링 mode을 가진다. 이는 POINTS, LINES, TRIANGLE을 알려준다.

meshes: [
{
  primitives: [
    {
      mode: 4,
      indices: 0,
      attributes: {
        POSITION: 1,
        NORMAL: 2
      },
      material: 2
    },
    {
      targets: [
      {
        POSITION: 11,
        NORMAL: 13
      },
      {
        POSITION: 21,
        NORMAL: 23
      }
      ]
    }
  ],
  weights: [0, 0.5]
}
]

primitive은 accessor의 색인을 사용한 정점 indices와 attributes을 참조한다. material 색인이 주어지고 렌더링을 위해서 사용된다.
각 attribute은 attribute 이름을 accessor 인덱스에 매핑하여 정의한다. 해당 accessor에 attribute data가 있다.
이 데이터는 mesh 렘더링할 때에 vertex attributes로 사용된다. mesh는 다중 morph target을 정의할 수 있다. 이런 morph target은 원본 mesh의 변형을 기술한다. morph target을 가진 mesh을 정의하기 위해서 각 mesh primitive은 targets 배열을 가진다. 이는 딕셔너리로서 attribute이름을 대상 지오메트리의 displacements가 있는 accessor의 색인으로 매핑된다. 다른 가중치를 가진 다중 morph target 조합은 다른 표정을 모델할 수 있게 한다. 가중치는 animation에 의해서 변경할수 있고 다른 지오메트리 상태 간에 보간된다.

buffers, bufferViews, accessors

buffer은 데이터를 가지고 이는 3D 모델 지오메트리, 애니메이션, 스키닝에 사용된다. bufferViews은 이런 데이터에 대한 구조적인 정보를 추가한다. accessors은 정확한 타입과 데이터 레이아웃을 정의한다.

buffers: [
{
  byteLength: 35, // 원본 데이터의 한 블럭의 소스 길이
  uri: 'buffer01.bin' // 이진 데이터 파일 참조
}
],
bufferViews: [
{
  buffer: 0, // buffers에 색인 0에 해당
  byteOffset: 4, // buffer에서 위치
  byteLength: 28, // buffer에서 범위
  byteStride: 12, // 여러개 데이터 간격
  target: 34963 // OpenGL 버퍼 target(옵션)
}
],
accessors: [
{
  bufferView:0, // bufferViews에서 색인 0에 해당
  byteOffset: 4, // bufferView에서 시작 위치
  type: 'VEC2', // 포함된 데이터 형식
  componentType: 5126, // 데이터형 GL_FLOAT
  count: 2,
  min: [0.1, 0.2], // 포함된 값의 범위
  max: [0.9, 0.8]  // 포함된 값의 범위
}
]

Sparse accessors

accessor의 일부 요소가 기본값과 차이가 있다(morph target에서 종종있음). 이 경우 sparse 데이터 형태를 사용하면 매우 작게 압축할 수 있다.

accessors: [
{
  type: 'SCALAR',
  componentType: 5126, // 데이터 형
  count: 10, //  총 구성요소 개수
  sparse: {  // sparse 데이터 블럭
    count: 4, // sparse 데이터 요소 개수
    values: { // sparse data가 있는 bufferView에 참조
      bufferView: 2,
    },
    indices: { // sparse data에 대한 target 색인
      bufferView: 1, // 참조할 bufferView 색인
      componentType: 5123 // 데이터 형
    }
  }
}
]

values에 2,4,6,8이고 indices에 1,4,5,7이라는 데이터가 있다면 10개 요소가 있는 실제 데이터는 다음과 같게 된다.

[0, 2, 0, 0, 4, 5, 0, 8, 0, 0]

materials

각 mesh primitive은 한 material을 참조한다. 이는 glTF asset에 포함되었다. material은 객체가 물리적 속성에 기반으로 어떻게 렌더될지 기술한다. 이는 Physically Based Rednering(PBR) 기술이 적용된다. 이는 렌더된 객체의 외형이 모든 렌더러 간에 일관성을을 가진다. 기본 material 모델은 Metallic-Roughness-Model이다. 0.0~1.0 사이 값으로 metal에 비슷한지와 표면 거친 특성을 표현한다. 이는 전체 객체에 동일 단일 값이 주어지거나 텍스처에서 읽어온다.

materials: [
{
  phrMetallicRoughness: { // Metallic-Roughness-Model에 대한 머터리얼 정의
    baseColorTexture: { // 개체에 적용될 주 텍스처
      index: 1,
      texCoord: 1
    },
    baseColorFactor: [1.0, 0.75, 0.35, 1.0], // 색상 정보 스케일 인자
    metallicRoughnexxTexture: { // blue채널에 metal 정도, green채널에 거친 정도
      index: 5,
      texCoord: 1
    },
    metallicFactor: 1.0, // metal 정도에 곱해지는 값
    roughnessFactor: 0.0, // 거친 정도에 곱해지는 값
  },
  normalTexture: { // 탄젠트 공간 법선 정보가 포함된 텍스처
    scale: 0.8, // 법선에 적용되는 스케일 인수
    index: 2,
    texCoord: 1
  },
  occlusionTexture: { // 빛이 차단되 어둡게 랜더링되는 표면을 정의. 텍스처의 red채널에 포함됨.
    strength: 0.9, // 적용되는 스케일 인수
    index: 4,
    texCoord: 1
  },
  emissiveTexture: { // 객체 표면에서 빛이 방출되는 부분에 대한 텍스처
    index: 3,
    texCoord: 1
  },
  emissiveFactor: [0.4, 0.8, 0.6] // 텍스처의 red, green, blue 요소에 대한 스케일 인수
}
]

Material properties in texture

material에서 텍스처 참조는 항상 텍스처 색인이 있다. 이는 또한 texCoord 색인도 있다.
이는 렌더된 mesh primitive의 TEXCOORD_ 속성으로 텍스처에 대한 텍스처 좌표를 갖는다. 기본은 0이다.

meshes: [
{
  primitives: [
  {
    material: 2, // material의 3번째 참조
    attributes: {
      NORMAL: 3,
      POSITION: 1,
      TEXCOORD_0: 2,
      TEXCOORD_1: 5,
    }
  }
  ]
}
]
materials: [
...
{ // 3rd matrial
  name: 'brushed gold',
  pbrMetalicRoughness: {
    baseColorFactor: [1,1,1,1],
    baseColorTexture: {
      index: 1, // texture의 2qjsWo ckawh
      texCoord: 1 // primitives의 TEXCOORD_1을 참조
    },
  metallicFactor: 1.0,
  roughnessFactor: 1.0
}
}
]
textures: [
...
{ // 2nd texture
  source: 4,
  sampler: 2
}
]

cameras

각 노드는 하나의 camera를 참조한다. 이는 glTF 에셋에 정의된다.

cameras: [
{
  type: 'perspective',
  perspective: {
    aspectRatio: 1.0,
    yfov: 0.65,
    zfar: 100, // 카메라에서 clipping plane 떨어진 거리로 선택적이다. 없으면 무한이다.
    znear: 0.01
  }
},
{
  type: 'orthographic',
  orthographic: {
    xmag: 1.0,
    ymax: 1.0,
    zfar: 100,
    znear: 0.01
  }
}
]

두가지 형태의 카메라가 있고 각각 투영 행렬을 정의한다. node가 camera를 참조하면 camera가 생성된다. camera 행렬은 노드의 글로벌 변형 행렬에 의해서 주어진다.

texture, images, samplers

texture은 렌더될 객체에 적용할 텍스처에 대한 정보를 갖고 있다. material에 의해서 참조된다.

textures: [
{
  source: 4, // 텍스처 소스로 5번째 이미지 참조
  sampler: 2 // 3번째 sampler 참조
}
]
images: [ // 텍스처에 대한 이미지 정의
...
{ // 5rd image
  uri: 'file01.png' // URI에 의해서 이미지 위치 지정
},
{
  bufferView: 3, // URI대신 MIME type으로 bufferView 참조
  mimeType: 'image/jpeg'
}
]
samplers: [ // 텍스처 래핑과 스케일 지정(OpenGL 상수값에 해당)
...
{ // 3rd sampler
  magFilter: 9729,
  minFilter: 9987,
  wrapS: 10497,
  wrapT: 10497
}
]

skins

glTF 에셋은 정점 스키닝을 위해 필요한 정보를 가지고 있다.
현재 포즈에서 스케레톤의 본에 영향을 받는 mesh의 정점을 지정한다.

nodes: [
{
  name: 'Skinned mesh node',
  mesh: 0, // 1번째 mesh 참조
  skin: 0 // 1번째 skin 참조
},
...
{
  name: 'Torso',
  children: [2, 3, 4, 5, 6], // 7번째 노드 참조
  rotation: [...],
  scale: [...],
  translation: [...]
},
...
{ // 7th node
  name: 'LegL',
  children: [7], // 8번째 노드 참조
  ...
},
{ // 8th node
  name: 'FootL',
}
]
skins: [ // joins 배열이 포함
{ // 1st skin
  inverseBindMatrices: 12, // 각 join에 해당하는 행렬이 포함된 accessor 참조
  joints: [1, 2, 3, ...] // 스켈레톤 트리구조를 정의한 노드 색인들
}
]
meshes: [
{
  primitives: [
  {
    attributes: {
      POSITION: 0,
      JOINTS_0: 1,
      WEIGHTS_0: 2
    }
  }
]
}
]

각 joint 노드는 로컬 변환과 children 배열을 가지고 스켈레톤의 본은 암시적으로 joint들 같에 연결로서 주어진다. mesh에서 스키닝을 위한 특별한 속성인 JOINTS_0과 WEIGHT_0이 있고 각각 accessor을 참조한다. JOINTS_0은 정점에 영향을 주는 joint 색인이 포함된 데이터이다. WEIGHTS_0은 얼마나 강하게 join가 정점에 영향을 주는지 가리킨다.

Computing the skinning matrix

스키닝 행렬은 mesh의 정점이 스켈레톤의 현재 포즈을 기반으로 어떻게 변환되는지 기술한다. 스키닝 행렬은 joint 행렬들 가중치 조합이다. skin은 inverseBindMatrices에 대한 참조이다. 이는 accessor로서 각 joint에 대해 한개 역 바인드 행렬이 있다. 이 행렬든 mesh을 jont의 로컬 영역으로 변환한다. skin의 joint에서 색인을 가진 노드에 대해 글로벌 변환 행렬을 계산할 수 있다. mesh을 joint의 현재 전역 변환을 기반으로 joint의 로컬 영역에서 변환된다. 이를 globalJointTransform이라고 한다. 이 행렬에서 jointMatrix은 각 joint에 대해 계산된다.

jointMatrix[j] = inverse(globalTransform) * globalJointTransform[j] * inverseBindMatrix[j];

mesh와 skin이 포함된 노드에서 어떤 글로벌 변환은 joint 행렬과 이 변환의 역행렬을 미리 곱해서 취소시킨다. 이 구현을 위해 jointMatrix 행렬은 정점 셰이더에 유니폼으로 전달된다.

join 행렬들을 결합하여 스키닝 행렬을 생성

스킨이 있는 mesh에 primitive은 POSITION, JOINT와 WEIGHT 속성이 포함되고 accessor을 참조한다. accessor은 각 정점에 대한 한개 요소를 가진다. 이 accessor 데이터는 정점 셰이더에 jointMatrix 행렬과 같이 속성으로 전달된다. 정점 셰이더에서 skinMatrix가 계산된다. 이는 JOINTS_0에 포함된 색인에 join 행렬들의 선형 결합으로 WEIGHTS_0 값으로 가중치가 부여된다.

uniform mat4 u_jointMatrix[12];
attribute vec4 a_position;
attribute vec4 a_joint;
attribute vec3 a_weight;

void main(void) {
  mat4 skinMatrix = 
    a_weight.x * u_jointMatrix[int(a_joint.x)] +
    a_weight.y * u_jointMatrix[int(a_joint.y)] +
    a_weight.z * u_jointMatrix[int(a_joint.z)] +
    a_weight.w * u_jointMatrix[int(a_joint.w)];
  gl_Position = modelViewProjection * skinMatrix * position;  
}

skinMatrix은 model-view-perspective 행렬로 변환전에 스켈레톤 포트을 기반으로 한 정점을 변형한다.

Animation

animation은 노드의 로컬 변환에 정의된 노드 속성이나 morph 대상에 가중치에 적용된다. animation에는 channels과 samplers 배열이 있다. 각 채널은 animation의 target을 정의한다. 이 target은 보통 색인으로 node와 path을 참조한다. 이는 애니메이션되는 속성 이름이다. 경로는 translation, rotation, sclae이 될 수 있고 이는 노드의 로컬 변환에 영향을 미치거나 weights은 node에 의해 참조되는 mesh의 morph 대상의 가중치에 영향을 미친다. 이 채널은 sampler을 참조하고 이는 실제 animation 데이터를 요약한다. smapler은 input과 output 데이터에 대해 참조하고 데이터를 제공하는 accessor의 색인을 사용한다. input은 스칼라 부동소수점 값을 가진 accessor을 참조한다. 이는 에니메이션 키 프레임의 시간이다. output은 accessor을 참조하고 각각의 키 프레임에서 에니메이션되는 속성에 값을 가진다. sampler은 에니메이션에 대해 interpolation모드를 정의하고 LINEAR, STEP, CUBICSPLINE이 될 수 있다.

animation samplers

애니메이션되는 동안 글로벌 애니메이션 시간(초단위)은 진행된다. 애니메이션 sampler의 input accessor의 데이터는 키 프레임 시간을 포함하고 있다. 에니메이션 sampler의 ouput accessor의 데이터는 애니메이션되는 속성에 대한 키 프레임 값을 포함하고 있다.
sampler은 input 데이터에서 현재 시간에 대한 키프레임을 조회한다. output 데이터의 해당 값을 읽고 sampler의 interpolation 모드를 기반으로 보간한다. 보간된 값은 애니메이션 채널 타겟으로 보내진다.

animation channel targets

애니메이션 샘플러에 의해 제공된 보간된 값은 다른 애니메이션 채널 대상으로 적용된다. 노드의 translation을 에니메이션하기.

translation=[2, 0, 0] --> translation=[3, 2, 0]

skin의 스켈리톤 노드의 rotation 애니메이션 하기

rotation = [0.0, 0.0, 0.0, 1.0] --> rotation = [0.0, 0.0, 0.38, 0.92]

node에 있는 mesh의 primiive에서 정의된 morph targets대한 weights 애니메이션하기.

원본 mesh primitive의 POSITION 속성     |             
morph target 0에서 POSITION에 대한 변경 | => 렌더링 => | weights=[0.5, 0.1]
morph target 1에서 POSITION에 대한 변경 |             | weights=[0.0, 0.5]

참조

[1] glTF 2.0 API Reference Guide, https://www.khronos.org/files/gltf20-reference-guide.pdf

반응형

'2.분석 및 설계' 카테고리의 다른 글

CAP 이론 소개  (1) 2023.11.20
의사코드(pseudocode) 사용하기  (0) 2023.10.19
[mybatis] Mybatis 내부동작 흐름  (0) 2022.01.26
YUV 포멧  (0) 2021.11.15
Mina로 본 네트웍 프레임워크  (0) 2012.07.27