-
Notifications
You must be signed in to change notification settings - Fork 4
CH07 Primitive Assembly and Rasterization
이 장에선 OpenGL ES 에서 지원하는 Primitive
와 Geometric objects
의 유형에 대해 설명하고, 이를 어떻게 그리는지에 대해 이야기합니다.
그 후, Vertex Shader 에서 Primitive 의 Vertices 를 처리한 뒤 발생하는 Primitive Assembly
단계에 대해 설명합니다.
Primitive Assembly
단계에선, stage, clipping, perspective, viewport transformation 작업이 수행됩니다.
이 장에서는 Rasterization stage
의 설명을 기점으로 마무리합니다.
Rasterization
은 Primitive
를 Fragment shader
에 의해 처리되는 primitives
를 2-demensional fragments
로 변환하는 과정입니다.
이 이차원 fragments
는 화면에 그려질 픽셀을 의미합니다.
primitives
는 OpenGL ES 의
glDrawArrays
,glDrawElements
,glDrawRangeElements
,glDrawArraysInstanced
,glDrawElementsInstanced
등을 이용해 그려질 수 있는 기하학적 오브젝트입니다.
primitives
는 Vertex 위치를 나타내는 집합입니다.
또한, 색상, 텍스쳐 좌표, 법선벡터를 나타내는 정보도 담길 수 있습니다.
primitives
는 OpenGL ES 3.0 에서
- 삼각형
- 선
- 점
으로 그려질 수 있습니다.
삼각형은 3D 애플리케이션에서 랜더링된 geometry object
를 묘사하는데 가장 일반적으로 사용되는 방법입니다.
OpenGL ES 3.0 에서 지원하는 triangle primitives 는
GL_TRIANGLES
, GL_TRIANGLES_STRIP
, GLTRIANGLE_FAN
세 가지 입니다.
GL_TRIANGLES
는 각각의 개별적인 삼각형을 그립니다.
위의 예시에서는 (v_0, v_1, v_2) 와, (v_3, v_4, v_5) 로 지정된 두개의 삼각형이 그려집니다.
총 n/3 개의 삼각형이 그려지는데, 여기서 n 은 glDraw~~~
API 에서 개수 로 지정된 index 의 길이입니다.
GL_TRIANGLES_STRIP
은 일련의 연결된 삼각형을 그립니다.
예시에서는 (v_0, v_1, v_2), (v_2, v_1, v_3), (v_2, v_3, v_4) 로 주어진 세 개의 삼각형이 그려집니다.
총 (n - 2) 개의 삼각형이 그려지며, 여기서 n 은 glDraw~~~
API 에서 개수 로 지정된 index 의 길이입니다.
GL_TRIANGLES_FAN
도 마찬가지로 연결된 삼각형을 그린다.
OpenGL ES 에서 지원하는 line primitives 는 GL_LINES
, GL_LINE_STRIP
, GL_LINE_LOOP
입니다.
GL_LINES
는 연결되지 않은 선분들을 그립니다.
위의 예제에선 (v_0, v_1), (v_2, v_3), (v_4, v_5) 새 개의 개별 선이 그려집니다.
총 n/2
개의 세그먼트가 그려지며 여기서 n
은 glDraw~~~
API 에서 개수로 지정된 index 의 길이입니다.
GL_LINE_STRIP
은 일련의 연결된 선분을 그립니다.
위의 예제에선 (v_0, v_1), (v_1, v_2), (v_2, v_3) 으로 연결된 세그먼트가 그려지며,
총 n -1 개의 선분이 그려집니다.
GL_LINE_LOOP
는 최종 선분 (v_n-1, v_0)이 그려짐을 제외하면 GL_LINE_STRIP
과 비슷하게 동작합니다.
총 n 개의 선분이 그려집니다.
선의 굵기 는 glLineWidth
API 호출을 통해 지정할 수 있습니다.
void glLineWidth(GLfloat width)
--------------------------------------
width: 선의 굵기를 픽셀단위로 지정 할 수 있습니다. 기본 굵기는 1.0 입니다.
OpenGL ES 에서 지원하는 포인트 스프라이트는 GL_POINTS
입니다.
- Point Sprites 예제
지정된 각 Vertex
에 대해서 포인트 스프라이트가 그려집니다.
포인트 스프라이트는 일반적으로 파티클 효과를 quad(사변형) 대신 점으로 그려 랜더링의 효율을 높이는데 사용됩니다.
포인트 스프라이트는 position
과 radius
가 지정된 화면을 바라보는 (screen-aligned) 사변형입니다. **
위치는 사변형의 중심을 나타내고, 반경은 포인트 스프라이트를 표시하는 사변형의 네 좌표를 계산하는데 사용됩니다.
gl_PointSize
는 Vertex Shader 에서 포인트 크기를 출력하는데 사용할 수 있는 내장 변수입니다.
포인트 프리미티브와 연결된 Vertex Shader 가 gl_PointSize 를 출력하는것이 중요하며 그렇지 않으면 포인트 크기 값이 정의되지 않은 것으로 간주되어 드로잉 오류가 발생할 가능성이 높습니다.
정점 셰이더의 gl_PointSize
값 출력은 OpenGL ES 3.0 구현에서 지원하는 앨리어싱된 포인트 크기 범위로 고정됩니다.
이범위는 다음의 명령을 통해 쿼리가능합니다.
GLfloat pointSizeRange[2];
glGetFloatv ( GL_ALIASED_POINT_SIZE_RANGE, pointSizeRange );
##주의점
기본적으로 OpenGL ES 3.0은 창 원점(0, 0)을 (left, bottom) 영역으로 설명합니다. but 포인트 스프라이트의 경우 포인트 좌표 원점은 (left, top)입니다.
gl_PointCoord
는 렌더링되는 기본 요소가 포인트 스프라이트인 경우 프래그먼트 셰이더 내부에서만 사용할 수 있는 내장 변수입니다.
이는 mediumup
정밀도 한정자를 사용하여 vec2
변수로 선언됩니다.
gl_PointCoord
에 할당된 값은 그림 7-3에 설명된 것처럼 left to right로 또는 top to bottom으로 이동할 때 0.0에서 1.0으로 이동합니다.
다음 프래그먼트 셰이더 코드는 gl_PointCoord
를 텍스처 좌표로 사용하여 텍스처 포인트 스프라이트를 그리는 방법을 보여줍니다.
#version 300 es
precision mediump float;
uniform sampler2D s_texSprite;
layout(location = 0) out vec4 outColor;
void main() {
outColor = texture(s_texSprite, gl_PointCoord);
}
OpenGL ES에는 primitives를 그리는 5개의 API가 있습니다. glDrawArrays, glDrawElements, glDrawRangeElements, glDrawArraysInstanced, glDrawElementsInstanced. Instance draw가 아닌 처음 3개 API를 먼저 보고 2개의 instanced call들은 다음 섹션에서 보겠습니다.
glDrawArrays는 mode인자에 명시된 primitive를 그립니다. 그리고 그때 vertex는 [first, first+count-1] 의 index들로 구성이 됩니다. 예컨데, glDrawArrays(GL_TRIANGLES, 0, 6)은 2개의 삼각형을 그립니다: (0, 1, 2)구성 인덱스(element indices)로 그려지는 삼각형, (3,4, 5) 구성 인덱스로 그려지는 삼각형으로 이루어집니다. 비슷하게, glDrawArrays(GL_TRIANGLE_STRIP, 0, 5) 함수호출은 3개의 삼각형을 그립니다. (0,1,2), (2,1,3), (2,3,4)
void glDrawArrays(GLenum mode, GLint first, GLsizei count)
mode: specifies the primitive to render; valid values are
GL_POINTS
GL_LINES
GL_LINE_STRIP
GL_LINE_LOOP
GL_TRIANGLES
GL_TRIANGLE_STRIP
GL_TRIANGLE_FAN
first: specifies the starting vertex index in the enabled vertex arrays
count: specifies the number of vertices to be drawn
void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices)
void glDrawRangeElements(GLenum mode, GLuint start,GLuint end, GLsizei count,
GLenum type, const GLvoid *indices)
mode: specifies the primitive to render; valid values are
GL_POINTS
GL_LINES
GL_LINE_STRIP
GL_LINE_LOOP
GL_TRIANGLES
GL_TRIANGLE_STRIP
GL_TRIANGLE_FAN
start: specifies the minimum array index in indices
(glDrawRangeElements only)
end: specifies the maximum array index in indices
(glDrawRangeElements only)
count: specifies the number of indices to be drawn
type: specifies the type of element indices stored in indices;
valid values are
GL_UNSIGNED_BYTE
GL_UNSIGNED_SHORT
GL_UNSIGNED_INT
indices: specifies a pointer to location where element indices are stored
glDrawArrays는 당신이 구성하는 점들(element indices)이 순서대로 놓여있을때, 그리고 기하체의 점들이 공유되어지지 않을때 유용합니다. 하지만 게임이나 3D application에서 사용되는 일반적인 물체들은 순서대로 정의되어 있지 않은 triangle meshes로 정의되며, 보통 점들 역시 공유되어집니다.
7-4 Figure의 정육면체를 고려해 봅시다. 만약 우리가 glDrawArrays 함수를 통해 이 정육면체를 그린다면, 코드는 다음과 같을것입니다.
#define VERTEX_POS_INDX 0
#define NUM_FACES 6
GLfloat vertices[] = {…};
// (x, y, z) per vertex
glEnableVertexAttribArray(VERTEX_POS_INDX);
glVertexAttribPointer(VERTEX_POS_INDX, 3, GL_FLOAT,
GL_FALSE, 0, vertices);
for (int i = 0; i < NUM_FACES; i++)
{
glDrawArrays(GL_TRIANGLE_FAN, i *4, 4);
}
Or
glDrawArrays(GL_TRIANGLES, 0, 36);
glDrawArrays를 통해 정육면체를 그린다면, glDrawArrays를 각 면마다 호출해야 할것입니다.
공유되어지는 점들은 복제되어야 합니다. 즉, 8개의 점 대신에, 24개의 점들을 할당해야 합니다(만약 각 면을 GL_TRIANGLE_FAN으로 그릴때). 또는 36개의 점들을 통해 그려질 수 있습니다(GL_TRIANGLE을 사용할때). 이것은 효율적인 접근방식이 아닙니다.
다음은 동일한 정육면체를 glDrawElements를 통해 그리는 코드입니다:
#define VERTEX_POS_INDX 0
GLfloat vertices[] = {…};
// (x, y, z) per vertex
GLubyte indices[36] =
{ 0, 1, 2, 0, 2, 3,
0, 3, 4, 0, 4, 5,
0, 5, 6, 0, 6, 1,
7, 1, 6, 7, 2, 1,
7, 5, 4, 7, 6, 5,
7, 3, 2, 7, 4, 3
};
glEnableVertexAttribArray(VERTEX_POS_INDX);
glVertexAttribPointer(VERTEX_POS_INDX, 3, GL_FLOAT,
GL_FALSE, 0, vertices);
glDrawElements(GL_TRIANGLES,
sizeof(indices) / sizeof(GLubyte),
GL_UNSIGNED_BYTE, indices);
비록 우리가 triangles를 glDrawElements와 triangle fan을 glDrawArrays와 glDrawElements를 통해 그렸지만, 우리의 application은 glDrawArrays보다 gpu상에서 더욱 빠르게 실행될 몇가지 이유가 있습니다. 예를들어 vertex attribute data의 사이즈는 glDrawElements
를 사용하면 더욱 작을것입니다. 왜냐하면 점들이 공유되어지기 때문입니다(뒤의 챕터에서 GPU post-transform vertex cache를 다룰것입니다). 이것은 또한 적은 메모리를 사용하게 할 것입니다.
primitive restart기법을 사용한다면, 당신은 여러개의 끊어진 primitive들을(예컨대 triangle fan들이나 strip들) 하나의 draw함수 호출로 그릴 수 있습니다. 이것은 draw API 호출을 줄임으로써 부하를 방지하는 장점이 있습니다. 조금 우아하지 않은 방법으로는, degenerate triangles를 만들어 내는 방법이 있습니다(이때 몇가지 주의사항이 있습니다). 이 방법은 조금 뒤의 section에서 살펴보겠습니다.
primitive restart기법을 사용하면, 당신은 primitive를 인덱스 방식의 draw API호출을 통해(예컨대 glDrawElements, glDrawElementsInstanced, glDrawRangeElements) 재시작(restart) 할 수 있습니다. 이것은 indices list안에 특수 index를 집어넣음으로써 이루어집니다. 특수 index는 index type의 최대값을 입니다(예컨대 GL_UNSIGNED_BYTE:255, GL_UNSIGNED_SHORT:65535).
예컨대, 2개의 triangle strip들이 다음 indices로 각각 이루어져 있다고 쳐봅시다(0, 1, 2, 3), (8, 9, 10, 11). Primitive restart기법을 이용해서 1번의 glDrawElemts* 함수호출을 통해 모든 strip들을 그리려면, 그때 element index list는 (0, 1, 2, 3, 255, 8, 9, 10, 11)이 됩니다. 이때 index type은 GL_UNSIGNED_BYTE입니다.
당신은 primitive restart기능을 다음 함수들을 통해 on/off할 수 있습니다.
glEnable ( GL_PRIMITIVE_RESTART_FIXED_INDEX );
// Draw primitives
glDisable ( GL_PRIMITIVE_RESTART_FIXED_INDEX )
지시자(qualifiers) 없이, vertex shader의 output values는 primitive사이에 선형보간(linearly interpolated across the primitive)됩니다. 하지만, flat shading을 사용한다면(Chapter 5의 Interpolation Qualifiers섹션에서 설명됨), 어떠한 보간도 이루어지지 않습니다. 보간이 이루어 지지 않음으로 오직 하나의 vertex value가 fragment shader에서 사용될 수 있습니다. 주어진 primitive instance에 대해, provoking vertex는 vertex shader의 어떤 vertices output이 사용될 지 결정합니다-오직 하나밖에 사용될 수 밖에 없으므로. 7-1의 table은 provoking vertex 결정 규칙을 나타냅니다.
Type of Primitive i | Provoking vertex |
---|---|
GL_POINTS | i |
GL_LINES | 2i |
GL_LINE_LOOP | i+1, if i<n |
1, if i==n | |
GL_LINE_STRIP | i+1 |
GL_TRIANGLES | 3i |
GL_TRIANGLE_STRIP | i+2 |
GL_TRIANGLE_FAN | i+2 |
Geometry instancing은 하나의 object를, 여러개의 서로다른 attributes를 통해 여러번 그리는것을 효율적으로 그리도록 해주는 기능입니다(예컨대, 서로다른 transformation matrix, color, size 등). 이때 한번의 API 호출이 이루어 집니다. 이 기능은 군중 랜더링(crowd rendering)과 같이 서로 비슷한 object들을 많은 수로 랜더링 할때 유용합니다. Geometry instancing은 API 호출 수를 낮춰줌으로써 CPU 프로새싱 오버해드를 낮춥니다. instanced draw call을 통해 렌더링 하기위해서는 다음 명령들을 사용합니다:
void glDrawArraysInstanced(GLenum mode, GLint first,
GLsizei count, GLsizei instanceCount)
void glDrawElementsInstanced (GLenum mode, GLsizei count,
GLenum type, const GLvoid *indices,
GLsizei instanceCount)
mode: specifies the primitive to render; valid values are
GL_POINTS
GL_LINES
GL_LINE_STRIP
GL_LINE_LOOP
GL_TRIANGLES
GL_TRIANGLE_STRIP
GL_TRIANGLE_FAN
first: specifies the starting vertex index in the enabled vertex arrays
(glDrawArraysInstanced only)
count: specifies the number of indices to be drawn
type: specifies the type of element indices stored in indices
(glDrawElementsInstanced only);
valid values are
GL_UNSIGNED_BYTE
GL_UNSIGNED_SHORT
GL_UNSIGNED_INT
indices: specifies a pointer to the location where element indices are stored
(glDrawElementsInstanced only)
instanceCount: specifies the number of instances of the primitive to be
drawn
per-instance data를 접근하는데에는 2가지 방식이 있다. 첫번째 방식은 vertex attribute를 통해 이루어 지는 방식으로, OpenGL ES가 vertex attributes를 instance마다 한번 읽을것인지, 아니면 여러번 읽을것인지 지시해주는 함수를 사용해서 per-instance data 접근을 조절한다.
void glVertexAttribDivisor(GLuint index, GLuint divisor)
index: specifies the index of the generic vertex attribute
divisor: specifies the number of instances that will pass between
updates of the generic attribute at slot index
Default 행동양식은 만약 glVertexAttribDivisor가 명시되지 않거나 divisor가 0이라면, vertex attributes는 vertex마다 한번씩 읽힐 것이다. 만약 divisor가 1과 같다면, vertex attributes는 primitive instance마다 한번씩 읽힐 것이다.
💡 여기서 primitive instance는 삼각형, point 들과 같은 primitive를 지칭하는 것이 아니라 glDrawArraysInstanced, glDrawElementsInstanced 함수를 통해 그리고자 하는 object( 예컨대 정육면체)를 의미한다.두번째 방식은 gl_InstanceID built-in input variable을 vertex shader안에 있는 buffer의 index로 사용하는 방식이다. glDrawArraysInstanced, glDrawElementsInstanced함수를 사용할때, gl_InstanceID는 현재 처리되고 있는 primitive instance의 index를 가지고 있다. 만약 이 두 함수가 아닌, non-instanced draw call들이 사용되었다면 gl_InstancedID는 0을 return할 것이다.
다음 두개의 code fragment들은 하나의 instanced draw콜을 통해 여러개의 geometry(i.e., 큐브)를 그리는 코드입니다. 큐브들은 각자 고유한 색상으로 표현됩니다. 완전한 코드는 Chapter_7/Instancing 예시에서 제공되고 있습니다.
// Random color for each instance
{
GLubyte colors[NUM_INSTANCES][4];
int instance;
srandom(0);
for (instance = 0; instance < NUM_INSTANCES; instance++)
{
colors[instance][0] = random() % 255;
colors[instance][1] = random() % 255;
colors[instance][2] = random() % 255;
colors[instance][3] = 0;
}
glGenBuffers(1, &userData->colorVBO);
glBindBuffer(GL_ARRAY_BUFFER, userData->colorVBO);
glBufferData(GL_ARRAY_BUFFER, NUM_INSTANCES *4, colors,
GL_STATIC_DRAW);
}
color buffer가 만들어지고 채워졌다면, color buffer를 geometry의 하나의 vertex attributes로써 (셰이더에) 바인딩 시킬 수 있습니다. 그리고 나서, vertex attribute divisor를 1로 셋팅후, 각 primitive instance마다 하나의 컬러값을 읽어들일 수 있습니다. 마지막으로, cube들을 하나의 draw call을 통해 그릴 수 있습니다.
// Load the instance color buffer
glBindBuffer(GL_ARRAY_BUFFER, userData->colorVBO);
glVertexAttribPointer(COLOR_LOC, 4, GL_UNSIGNED_BYTE,
GL_TRUE, 4* sizeof(GLubyte),
(const void *) NULL);
glEnableVertexAttribArray(COLOR_LOC);
// Set one color per instance
glVertexAttribDivisor(COLOR_LOC, 1);
// code skipped ...
// Bind the index buffer
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, userData->indicesIBO);
// Draw the cubes
glDrawElementsInstanced(GL_TRIANGLES, userData->numIndices,
GL_UNSIGNED_INT,
(const void *) NULL, NUM_INSTANCES);
Application은 glDrawElements와 glDrawElementsInstanced 함수들이 최대한 큰 primitive size로 호출되도록 해야합니다. 이것은 GL_TRIANGLES을 그릴때는 쉽습니다. 하지만 우리가 triangle strips나 fans로 이루어진 meshes들을 그릴때면, 각 strip/fan mesh마다 glDrawElements*함수들을 호출하지 않고, primitive restart기법을 사용해서 이 mesh들을 이을 수 있습니다(이전 섹션에서 이것을 다루었습니다).
당신이 primitive restart 매커니즘을 사용하지 못한다면(예전버전의 OpenGL ES와의 호환성등을 위하여), denegerate triangles를 만드는 element indices를 추가하는 방식이 있습니다. 단 이때 약간의 indices들을 더 사용해야 하며(primitive restart와 대비하여), 그리고 여기에서 언급할 몇가지 주의사항을 지켜줘야합니다. degenerate triangle이란, 2개 혹은 그 이상의 점들이 일치하는 삼각형을 의미합니다. GPU는 이러한 것을 감지할 수 있고 degenerate triangle을 거부할 수 있습니다. 따라서 이것은 GPU에 의해 큰 primitive를 그릴 시 좋은 성능향상을 가져다 줍니다.
우리가 distinct mesh들을 이어주기 위해 추가해야할 element indices의 갯수는 1. mesh가 triangle fan인지, triangle strip인지에 달려있고, 2.각 strip의 indices 갯수에 따라 다릅니다. mesh안에 존재하는 indices 갯수는 중요한데, 그 이유는 서로 구분되는 mesh들에 존재하는 삼각형들의 마지막 삼각형, 첫번째 삼각형의 winding order를 보존해야 하기 때문입니다.
💡 winding order: 시계방향, 반시계 방향을 의미 ![Untitled 3](https://github.com/daemyung/learn-opengl-es/assets/99255566/7deb5b5b-a724-47b0-b9a6-945a06857285)구분된 triangle strips를 연결할 때에는, 첫번째 strip의 마지막 삼각형, 두번째 strip의 첫번째 삼각형들의 점들의 순서를 고려해야 합니다. 7-5 Figure에서보이듯이, 짝수개의 triangles로 이루어진 strip의 점들의 순서는 홀수개의 triangles로 이루어진 strip과 그 순서가 다릅니다.
두가지 경우가 처리되어야 합니다.
- 홀수개의 triangles로 이루어진 첫번째 triangle strip이 두번째 triangle strip에 붙을때
- 짝수개의 triangles로 이루어진 첫번째 triangle strip이 두번째 triangle strip에 붙을때
다음 7-5 Figure는 각 케이스들을 나타냅니다.
7-5의 opposite vertex order를 가지고 있는 triangle strips들의 element indices들은 각각 (0,1,2,3), (8,9,10,11)입니다. glDrawElements*호출을 통해서 하나의 함수로 2개의 strip들을 이어지게 그리려면 (0, 1, 2, 3, 3, 8, 8, 9, 10, 11)이 될 것입니다. 이 새로운 element index는 다음 삼각형들을 만들어지게 할 것입니다: (0, 1, 2), (2, 1, 3), (2, 3, 3),(3, 3, 8), (3, 8, 8), (8, 8, 9), (8, 9, 10), (10, 9, 11). boldface 삼각형들이 degenerate triangles입니다. element index안의 boldface는 합쳐진 element index list를 위해 새롭게 추가된 indices들 입니다.
7-5의 same vertex order를 가지고 있는 triangle strips들의 element indices들은 각각 (0,1,2,3,4), (8,9,10,11)입니다. glDrawElements*호출을 통해서 하나의 함수로 2개의 strip들을 이어지게 그리려면 (0, 1, 2, 3, 4, 4, 8, 8, 9, 10, 11)이 될 것입니다. 이 새로운 element index는 다음 삼각형들을 만들어지게 할 것입니다: (0, 1, 2), (2, 1, 3),(2, 3, 4), (4, 3, 4), (4, 4, 4), (4, 4, 8), (4, 8, 8), (8, 8, 9), (8, 9, 10),(10, 9, 11). boldface 삼각형들이 degenerate triangles입니다. element index안의 boldface는 합쳐진 element index list를 위해 새롭게 추가된 indices들 입니다.
추가적으로 필요한 element indices의 갯수와 그로 인해 생성된 degenerate triangle들의 갯수는 첫번째 strip의 vertices 갯수에 의해 변한다는 것을 명심하세요. 이것은 다음 strip의 window order를 유지하기 위해서 그렇습니다.
post-transform vertex cache를 고려하는 방법도 있다:—>????
Figure 7-6은 primitive assembly단계를 보여줍니다.
glDraw***
를 통해 제공되는 정점은 정점 셰이더에 의해 실행됩니다.
정점 셰이더에 의해 변환된 각 정점에는 정점의 (x, y, z, w) 값을 설명하는 정점 위치가 포함됩니다.
기본 유형과 정점 인덱스는 렌더링될 개별 기본 요소를 결정합니다. 각 개별 primitive(삼각형, 선 및 점)와 해당 정점에 대해 primitive assembly 단계는 그림 7-6에 표시된 작업을 수행합니다.
OpenGL ES에서 primitives 가 래스터화되는 방법을 논의하기 전에 OpenGL ES 3.0에서 사용되는 다양한 좌표계를 이해해야 합니다. 이는 OpenGL ES 3.0 파이프라인의 다양한 단계를 거치면서 정점 좌표에 어떤 일이 발생하는지 잘 이해하는 데 필요합니다.
Figure 7-7은 정점이 정점 셰이더와 primitive assembly 단계를 거치는 동안의 좌표계를 보여줍니다. 정점은 객체 또는 로컬 좌표 공간에서 OpenGL ES에 입력됩니다. 이는 객체가 모델링되고 저장될 가능성이 가장 높은 좌표 공간입니다. 정점 셰이더가 실행된 후 정점 위치는 클립 좌표 공간에 있는 것으로 간주됩니다. 정점 위치를 로컬 좌표계(즉, 개체 좌표)에서 클립 좌표로 변환하는 작업은 정점 셰이더에 정의된 적절한 유니폼에서 이 변환을 수행하는 적절한 행렬을 로드하여 수행됩니다.
Chapter 8, “Vertex Shaders,”에서는 정점 위치를 객체에서 클립 좌표로 변환하는 방법과 정점 셰이더에 적절한 행렬을 로드하여 이 변환을 수행하는 방법을 설명합니다.
Clipping
cf) clipping 사전적의미로는 ‘가위로 자르다 베다’ 라는 뜻이 있습니다.
(Direcr3D 왼손 좌표계 opengl, opengles는 오른손 좌표계를 사용하는 것으로 알고있음) 그림은 왼손좌표계인것으로 보이는데 제가 틀린건지 더블체크 부탁드립니다:D
보이는 공간 이외의 연산을 방지하기위해 primitives는 클립 공간에 clip 됩니다.
정점 셰이더가 실행된 후의 정점 위치는 클립 좌표 공간에 있습니다.
클립 좌표는 (xc, yc, zc, wc)로 주어진 homogeneous coordinate입니다. 클립 공간(xc, yc, zc, wc)에 정의된 정점 좌표는 보기 볼륨(클립 볼륨이라고도 함)에 대해 잘립니다.
-
homogeneous coordinate란
https://darkpgmr.tistory.com/78
시간나면 우리의 언어로 다시 설명을 달면 좋을 것 같습니다😀
즉 사용하는 이유는 평면에 투영시키기 때문이다(랜더링을 위해서다)라고 이해하시면 좋습니다.
Figure 7-8에 표시된 대로 클립 볼륨은 근거리 및 원거리 클립 평면, 왼쪽 및 오른쪽 클립 평면, 상단 및 하단 클립 평면이라고 하는 6개의 클립 평면으로 정의됩니다. 클립 좌표에서 클립 볼륨은 다음과 같이 지정됩니다. -wc <= xc <= wc -wc <= yc <= wc -wc <= zc <= wc 위의 6개 checks는 primitive를 클리핑해야 하는 평면 목록을 결정하는 데 도움이 됩니다.
클리핑 단계는 Figure 7-8에 표시된 클립 볼륨에 대해 각 primitive를 클리핑합니다. 여기서 "primitive"이란 GL_TRIANGLES
를 사용하여 그린 개별 삼각형 목록의 각 삼각형, 삼각형 스트립 또는 팬의 삼각형, GL_LINES
를 사용하여 그린 개별 선 목록의 선 또는 선의 선을 의미합니다. 스트립이나 라인 루프, 또는 포인트 스프라이트 목록의 특정 포인트. 각 기본 유형에 대해 다음 작업이 수행됩니다.
• 클리핑 삼각형 - 삼각형이 보기 볼륨 내부에 완전히 포함된 경우 클리핑이 수행되지 않습니다. 삼각형이 보기 영역에서 완전히 벗어나면 해당 삼각형은 삭제됩니다. 삼각형이 부분적으로 보기 볼륨 내부에 있는 경우 삼각형은 해당 평면에 대해 잘립니다. 클리핑 작업은 삼각형 팬으로 배열된 평면에 클리핑되는 새로운 정점을 생성합니다.
• 선 자르기 - 선이 보기 볼륨 내부에 완전히 포함된 경우에는 자르기가 수행되지 않습니다. 라인이 보기 볼륨 외부에 완전히 있으면 라인이 삭제됩니다. 선이 부분적으로 보기 볼륨 내부에 있으면 선이 잘리고 적절한 새 정점이 생성됩니다.
• 클리핑 포인트 스프라이트 - 클리핑 단계에서 포인트가 삭제됩니다.
포인트 위치가 근거리 또는 원거리 클립 평면 외부에 있거나 포인트 스프라이트를 나타내는 쿼드가 클립 볼륨 외부에 있는 경우 스프라이트입니다. 그렇지 않으면 변경되지 않은 채 전달되고 포인트 스프라이트는 클립 볼륨 내부에서 외부로 또는 그 반대로 이동할 때 잘립니다.
6개의 클리핑 평면에 대해 기본 요소가 클리핑된 후 정점 좌표는 원근 분할을 거쳐 정규화된 장치 좌표가 됩니다. 정규화된 장치 좌표의 범위는 –1.0에서 +1.0입니다.
Note
클리핑 작업(특히 선과 삼각형의 경우)을 하드웨어에서 수행하려면 비용이 많이 들 수 있습니다. 기본 요소는 표시된 것처럼 보기 볼륨의 6개 클립 평면에 대해 잘려야 합니다. Figure 7-8에서. 근거리 평면과 원거리 평면 외부에 부분적으로 있는 프리미티브는 클리핑 작업을 거칩니다. 그러나 부분적으로 x 및 y 평면 외부에 있는 기본 요소는 반드시 잘라낼 필요가 없습니다. glViewport
로 지정된 뷰포트의 크기보다 큰 뷰포트로 렌더링하면 x 및 y 평면의 클리핑이 가위 작업이 됩니다. 가위질은 GPU에 의해 매우 효율적으로 구현됩니다. 이 더 큰 뷰포트 영역을 보호 대역 영역이라고 합니다. OpenGL ES는 애플리케이션이 보호 대역 영역을 지정하는 것을 허용하지 않지만 대부분(전부는 아니지만) OpenGL ES 구현은 보호 대역을 구현합니다.
cf)orthogonal projection
원근 분할은 클립 좌표(xc, yc, zc, wc)로 지정된 점을 가져와 화면이나 뷰포트에 투영합니다. 이 투영은 (xc, yc, zc) 좌표를 wc로 나누어 수행됩니다. (xc /wc), (yc /wc) 및 (zc /wc)를 수행한 후 정규화된 장치 좌표(xd, yd, zd)를 얻습니다. 이는 [-1.0 ... 1.0] 범위에 있으므로 **normalized device coordinates(NDC)**라고 합니다. 정규화된(xd, yd) 좌표는 뷰포트 크기에 따라 실제 화면(또는 창) 좌표로 변환됩니다. 정규화된(zd) 좌표는 glDepthRangef
에 지정된 근거리 및 원거리 깊이 값을 사용하여 화면 z 값으로 변환됩니다. 이러한 변환은 뷰포트 변환 단계에서 수행됩니다.
뷰포트는 모든 OpenGL ES 렌더링 작업이 최종적으로 표시되는 2D 직사각형 창 영역입니다. 뷰포트 변환은 다음 API 호출을 사용하여 설정할 수 있습니다. (화면에 보여지는것)
void glViewport(GLint x, GLint y, GLsizei w, GLsizei h)
x, y specifies the window coordinates of the viewport’s lower-left corner in pixels
w, h specifies the width and height of viewport in pixels; these values must be greater than 0
**f,n*은 far plane과 near plane (max depth, min depth)
NDC(xd, yd, zd)에서 window coordinate(xw, yw, zw)로의 변환은 다음 변환을 통해 제공됩니다.
변환 ox = x + w/2 및 oy = y + h/2에서 n과 f는 원하는 깊이 범위를 나타냅니다.
(x, y는 viewport의 시작점을 의미합니다. glViewport에서 정한)
깊이 범위 값 n과 f는 다음 API 호출을 사용하여 설정할 수 있습니다.
void glDepthRangef(GLclampf n, GLclampf f)
*n, f* : specify the desired depth range. Default values for n and f are 0.0 and
1.0, respectively. The values are clamped to lie within (0.0, 1.0).
glDepthRangef
및 glViewport
에 의해 지정된 값은 정규화된 장치 좌표의 정점 위치를 창(화면) 좌표로 변환하는 데 사용됩니다.
초기(또는 기본) 뷰포트 상태는 OpenGL ES가 렌더링을 수행하는 응용 프로그램에서 생성된 창의 w=너비, h = 높이로 설정됩니다. 이 창은 eglCreateWindowSurface
에 지정된 EGLNativeWindowType win
인수에 의해 제공됩니다.
사진 7-9
사진 7-9 는 레스터화 파이프라인을 보여줍니다. 정점들이 변형되고 primitive 가 클리핑되고 난 후, rasterization 파이프라인은 삼각형, 라인 세그먼트, 점 그리고 생성된 적절한 프레그먼트 와 같은 개별 primitive 들을 가집니다. 각각의 프레그먼트는 스크린공간의 integer 픽셀 위치로 인식됩니다. 프레그먼트는 스크린공간에서 지정된 픽셀위치와 색상을 만들어내기 위해 셰이더에서 처리할 프레그먼트 데이터를 나타냅니다. 이러한 연산들은 챕터 9 “Texturing”, 챕터 10 “Fragment Shaders” 에서 더 자세하게 설명하겠습니다. 이번 섹션에서 우리는 앱이 삼각현, 스트라이프, 팬의 래스터라제이션을 컨트롤하는 다양한 옵션에 대해 배웁니다.
cf. Culling(도태) 은 최적화를 위해 Rendering 과정에서 굳이 렌더링하지 않아도 된다고 판단된 오브젝트들을 제외하는 것을 말합니다. Culling 과정을 거치면 cpu, gpu의 부담을 덜어줄 수 있기에 퍼포먼스적인 이점을 얻을 수 있겠죠.
삼각형이 레지스터되기 전에 우리는 앞면을 볼 건지, 뒷면을 볼 것인지 결정해야 할 필요가 있습니다. Culling 연산은 보는 쪽으로부터 등지는 삼각형을 삭제합니다. 삼각형이 앞면인지 뒷면인지를 결정하는 것은 삼각형의 방향을 알 필요가 있습니다.
삼각형의 방향은 첫 번째 정점에서 시작하여 두 번째와 세 번째 정점을 지나 첫 번째 정점에서 끝나는 경로의 감김 순서를 지정합니다. 사진 7-10은 시계방향으로 감긴 삼각형과 반시계방향으로 감긴 삼각형의 예시를 보여줍니다.
사진 7-10
삼각형의 방향은 윈도우 좌표계에서 삼각형이 표시된 영역을 연산함으로써 결정됩니다. 우리는 이제 시계방향 또는 반시계방향으로 연산된 삼각형 영역의 신호를 알아낼 필요가 있습니다. 위 신호로부터 이 매핑은 다음과 같은 API 콜을 통해 구체화할 수 있습니다.
void glFrontFace(GLenum dir)
dir: `GL_CW` or `GL_CCW`
삼각형의 방향을 어떻게 계산하는지 배워보겠습니다. 삼각형이 culled 될 필요가 있는지 결정하기 위해서는 우리는 삼각혀의 앞뒷면을 알아야합니다. 이는 glCullFace
호출을 통해 알 수 있습니다.
void glCullFace(GLenum mode)
mode: `GL_FRONT`, `GL_BACK`(defalut), `GL_FRONT_AND_BACK`
마지막으로, 우리는 culling 연산이 수행되어야하는지 아닌지 알아야합ㄴ니다. Culling 연산은 GL_CULL_FACE
상태가 가능해야 수행됩니다. GL_CULL_FACE
는 glEnabe
또는 glDisable
호출을 통해 알 수 있습니다.
void glEnable(GLenum cap)
void glDisable(GLenum cap)
cap: GL_CULL_FACE 로 설정, 초기엔 culling이 비활성화
요약하자면, 적절한 삼각형을 컬링하려면 OpenGL ES 애플리케이션은 먼저 glEnable(GL_CULL_FACE)
을 사용하여 컬링을 활성화하고, glCullFace
를 사용하여 적절한 컬링 면을 설정하고, glFrontFace
를 사용하여 전면 삼각형의 방향을 설정해야 합니다.
참고: 보이지 않는 삼각형을 래스터화하는데 GPU가 시간을 낭비하지 않도록 하려면 컬링을 항상 활성화해야 합니다. 컬링을 활성화하면 OpenGL ES 애플리케이션의 전반적인 성능이 향상됩니다.
서로 겹치는 두 개의 다각형을 그리는 경우를 생각해 보면, Figure 7-11에 표시된 것처럼 아티팩트가 나타날 가능성이 높습니다. Z-파이팅 아티팩트라고 하는 이러한 아티팩트는 삼각형 래스터화의 제한된 정밀도로 인해 발생합니다. 이는 perfragment로 생성된 깊이 값의 정밀도에 영향을 미쳐 아티팩트를 초래할 수 있습니다. 삼각형 래스터화에 사용되는 매개변수의 제한된 정밀도와 perfragment로 생성된 깊이 값은 점점 더 좋아질 것이지만 완전히 해결되지는 않습니다.
(이론적으로는 겹쳤으면 깔끔하게 나와야하지만, 작은 삼각형들의 집합이므로 그림에서 보이는 것과 같이 artifact가 형성될 수 있다.) polygon offset은 이를 없애기 위한 방법론이다.
Figure 7-11은 그려지는 두 개의 동일 평면상의 다각형을 보여줍니다. 다각형 오프셋 없이 두 개의 동일 평면상의 다각형을 그리는 코드는 다음과 같습니다.
glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
// load vertex shader
// set the appropriate transformation matrices
// set the vertex attribute state
// draw the SMALLER quad
glDrawArrays ( GL_TRIANGLE_FAN, 0, 4 );
// set the depth func to <= as polygons are coplanar
glDepthFunc ( GL_LEQUAL );
// set the vertex attribute state
// draw the LARGER quad
glDrawArrays ( GL_TRIANGLE_FAN, 0, 4 );
이러한 현상을 피하기 위해서는 깊이 테스트를 수행하기 전과 깊이 값이 깊이 버퍼에 기록되기 전에 계산된 깊이 값에 delta를 추가해야 합니다.
**깊이 테스트를 통과하면 원래 깊이 값 + delta가 아닌 원래 깊이 값이 깊이 버퍼에 저장됩니다.
void glPolygonOffset(GLfloat factor, GLfloat units)
깊이 오프셋은 다음과 같이 계산됩니다.
depth offset = m * factor + *r * units
m 은 또한 ** max {|∂z/∂x|, |∂z/∂y|} 로 계산 가능합니다.
기울기인 ∂z/∂x, ∂z/∂y 은 opengles에서 레스터라이제이션 단계에서 구할수 있습니다.
r은 구현에 정의된 상수이며 깊이 값의 보장된 차이를 생성할 수 있는 가장 작은 값을 나타냅니다.
glEnable(GL_POLYGON_OFFSET_FILL)
및 glDisable(GL_POLYGON_OFFSET_FILL)
을 사용하여 다각형 오프셋을 활성화하거나 비활성화할 수 있습니다.
다각형 오프셋이 활성화된 경우 Figure 7-11에서 렌더링된 삼각형의 코드는 다음과 같습니다.
//추가된것
const float polygonOffsetFactor = –l.Of;
const float polygonOffsetUnits = –2.Of;
glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
// load vertex shader
// set the appropriate transformation matrices
// set the vertex attribute state
// draw the SMALLER quad
glDrawArrays ( GL_TRIANGLE_FAN, 0, 4 );
// set the depth func to <= as polygons are coplanar
glDepthFunc ( GL_LEQUAL );
//추가된것
glEnable ( GL_POLYGON_OFFSET_FILL );
glPolygonOffset ( polygonOffsetFactor, polygonOffsetUnits );
// set the vertex attribute state
// draw the LARGER quad
glDrawArrays ( GL_TRIANGLE_FAN, 0, 4 );
cf. 필요 없는 Geometry(지형, 물체 등)를 그리지 않도록 불필요한 연산을 제거하는 기술입니다. 보통 모니터에 보이는 화면에 벗어난 물체 또는 다른 물체에 의해서 가려지는 물체(Occlusion) 등에 대한 연산을 수행하지 않게 하여서 Rendering 성능을 향상하는 방법을 의미합니다. Occlusion Culling은 크게 “Occlusion Query”와 “Early-Z” 방법을 사용하여서 필요 없는 Rendering 연산을 제거합니다.
Occlusion Queries 는 쿼리객체를 사용해서 어떠한 프레그먼트들 또는 심도있는 테스트를 통과한 샘플들을 추적하는데 쓰입니다. 이런 접근은 렌즈 플레어 효과에 대한 가시성 결정뿐만 아니라 경계 볼륨이 가려진 가려진 객체에 대한 지오메트리 처리 수행을 방지하기 위한 최적화와 같은 다양한 기술에 사용될 수 있습니다.
Occlusion Queries 는 (각각 GL_ANY_SAMPLES_PASSED
or GL_ANY_SAMPLES_PASSED_CONSERVATIVE
target을 사용한) glBeginQuery
와 glEndQuery
를 사용하여 시작되고 끝낼 수 있습니다.
void glBeginQuery(GLenum target, GLuint id)
void glEndQuery(GLenum target)
target: `GL_ANY_SAMPLES_PASSED`, `GL_ANY_SAMPLES_PASSED_CONSERVATIVE`, `GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN`
id: 쿼리객체의 id
GL_ANY_SAMPLES_PASSED
를 사용하면 샘플이 심도있게 테스트를 통과했는지 bool 상태로 여부를 나타냅니다. GL_ANY_SAMPLES_PASSED_CONSERVATIVE
를 사용하면 더 나은 성능을 제공할 수 있겠지만, 다소 덜 정확한 답을 제공할 수 있습니다.
GL_ANY_SAMPLES_PASSED_CONSERVATIVE
를 사용하면 일부 구현에서는 심도있게 테스트를 통과한 샘플이 없더라도 GL_TRUE 를 반환할 수 있습니다.
id 는 glGenQueries
를 사용하여 생성하고 glDeleteQueries
를 사용해 삭제합니다.
void glGenQueries(GLsizei n, GLuint *ids)
n:
ids:
void glDeleteQuries(GLsizei n, const GLuint *ids)
n:
ids:
위 메서드를 사용하여 쿼리객체의 범위를 구체화한 후, glGetQueryObjectuiv
를 사용해 쿼리객체의 결과를 검색할 수 있습니다.
void glGetQueryObjectuiv()
참고: 더 나은 성능을 위해서는, glGetQueryObjectuiv
콜을 수행하기 전에, GPU에서 사용가능할 때까지 몇 프레임 정도 기다리는 것이 좋습니다.
아래는 어떻게 occlusion 쿼리객체를 설정하고 결과를 사용하는지에 대한 예시입니다.
glBeginQuery ( GL_ANY_SAMPLES_PASSED, queryObject );
// draw primitives here
...
glEndQuery ( GL_ANY_SAMPLES_PASSED );
...
// after several frames have elapsed, query the number of
// samples that passed the depth test
glGetQueryObjectuiv( queryObject, GL_QUERY_RESULT,
&numSamples );
이번 챕터에서는 OpenGL ES에서 제공하는 primitives 타입을 배웠고 그것들이 인스턴스된 것과 안된 드로우 콜을 사용하여 어떻게 그리는지에 대해 봤습니다.
또한 정점에서 좌표 변환이 수행되는 방법에 대해서도 배웠습니다. 게다가 기본 요소가 화면에 그려질 수 있는 픽셀을 나타내는 조각으로 변환되는 래스터화 단계에 대해 배웠습니다. 이제 정점 데이터를 사용하여 기본 요소를 그리는 방법을 배웠으므로 다음 장에서는 기본 요소의 정점을 처리하기 위해 정점 셰이더를 작성하는 방법을 설명하겠습니다.
OpenGL_ES 3.0을 기반으로 하고있습니다 🍡
OpenGLES3.0 Programming Guide 책을 해설 및 번역 작업과 예제 코드 작성을 진행중입니다.
챕터 | 완료여부 |
---|---|
chapter 1 | |
chapter 2 | ✅ |
chapter 3 | ✅ |
chapter 4 | ✅ |
chapter 5 | ✅ |
chapter 6 | ✅ |
chapter 7 | ✅ |
chapter 8 | ✅ |
chapter 9 |