[GPU프로그래밍] 10. Using Geometry Shaders
Geometry
- primitive
- line, dot, polygon 등 그려지는 작은 단위
-> 이것을 추가하거나, 수정하거나, 제거할 수 있는 쉐이더가 geometry shader
◾ Geometry shader
- 각 primitive마다 한번 씩 실행
- ex) 삼각형 2개로 그리는 쿼드 -> geometry shader 2번, vertex shader 6번, fragmetn shader는 해상도에 따라 실행됨)
- Primitive마다 실행되기 때문에, primitive의 정보(vertex의 위치, 컬러, 텍스처좌표 등)을 사용할 수 있다
- Input
- Output
- zero: output이 없으면 원래 있던 primitive를 삭제하는 것
- one: 유지
- more: 추가
- input과 output의 primitive type은 다를 수 있다
- ex) input은 점인데 output은 polygon
- 하지만 output은 한가지 primitive type만 가능
Functionality
- 그려지지 않는 부분을 지워주는 culling
- geometry를 추가해서 모양 늘리기
- geometry를 바꾸진 않지만, 그 정보로 무언가 계산해내기 (ex-primitive에 속한 모든 vertex를 사용해 삼각형의 중심을 계산)
- input과 다른 형태의 geometry로 primitive 만들기
- 등등..
How to use
- Primitive를 생성하고 vertex를 보내기 위해 built-in function
EmitVertex
와 EndPrimitive
사용 (-> primitive 생성)
- EmitVertex: gs를 사용해서 생성하고자하는 primitive의 vertex 지정
- EndPrimitive: EmitVertex된 vertex들을 연결하여 primitive 생성
- EndPrimitive를 작성하지 않아도 gs가 끝날 때 자동으로 마무리되긴 한다 (그래도 명시적으로 사용하는게 좋음)
- EmitVertex를 아예 호출하지 않으면, input primitive는 삭제됨 (렌더링되지 않음)
👀 아래부터는 Geometry shader를 사용하는 예제 3가지
1. Point Sprites
- 쿼드를 사용해서 sprite 만들기
- 카메라를 바라보는 sprite -> texture가 항상 카메라 방향에 수직이 되도록 해야함
- 점을 그리고, gs에서 그 점을 기준으로 쿼드를 그린다
- 쿼드 하나 당 한 vertex(point)만 있으므로 GPU로 보내는 데이터 양이 훨씬 줄어듦
- OpenGL Application에서 점 만들기
GL_POINTS
를 사용해 원하는 vertex 위치에 점(single-point primitives)를 만듦
- 보통 픽셀 크기로 그려짐
- 회전이나 모양 변환이 어려워서 이것만드로 sprite를 그리는데 한계가 있음
- gs에서 점을 기준으로 쿼드 그리기
- input으로 point
P
를 받음
- camera coordinate 기준 -> 카메라 방향에 수직이 되는 쿼드를 계산하기 편함 (점 P와 A, B, C, D가 같은 평면)
- gs가 EmitVertex를 할 때 필요한 texture coordinate를 자동으로 계산해줌
-> 여러 스프라이트에 동일하게 적용해줄 수 있음
- 계산에 따라 쿼드가 아닌 다른 모양도 그릴 수 있고, 복잡한 모양인 경우 텍스쳐의 alpha값을 사용하여 배경을 투명하게 해주는 방법도 있다
📃 Code
OpenGL Application
- point들의 정보가 담긴 vertex buffer 필요
- normal, texture coord 정보는 필요 없음 (gs에서 계산함)
- vertex의 연결관계도 필요 없어서 index buffer같은 것도 필요X
Vertex shader
- 쿼드의 중심이 될 point들의 좌표를 input으로 받고, CC까지만 변환
Geometry shader
- CC기준 point를 받아 vertex 4개를 생성한다
- 각 vertex의 위치와 texture coordinate를 계산하고 EmitVertex (총 4번 진행)
- 이때 vertex의 위치는 projection까지 진행한 후 fragment로 넘겨준다
Fragment shader
- gs에서 계산된 texture coordinate에 맞춰 texture mapping
2. Drawing a Wireframe on Top of Shaded Mesh
- mesh를 쉐이딩하고, mesh마다 edge를 그려줌
- gs를 사용하지 않으면 모델을 두 번 렌더링하여 그려줘야하는데(depth test때문에 한 모델은 좀 더 크게 렌더링), gs를 사용하면 1pass로 그릴 수 있음
- primitive의 정보를 계산하기 위해 gs를 사용 (형태 변화x)
- 각 fragment가 edge랑 가까우면 edge color, 멀면 shading color를 적용하는 방식으로 적용
- 각 fragment의 edge까지의 거리를 알아야하고 이는 gs에서 계산할 수 있음
✔️ fragment에서 edge까지의 거리 구하기
- 삼각함수 식을 사용해 한 vertex에서 반대 edge까지의 거리를 구함
- 이 과정은 한 삼각형 안에서 얻는 값으로, 3개의 vertex가 필요하므로 gs에서 계산해야함
- EmitVertex를 할 때, edge-distance vector를 보냄
- fs에서 삼각형에 속하는 특정 fragment가 삼각형의 세 버텍스를 iterpolation한 값을 가지고, 특정 fragment에서의 각 edge까지의 거리를 알 수 있음
- 이때, 가장 가까운 edge와의 거리로 해당 fragment가 edge에 속하는지를 판별하여 컬러를 결정한다
- Edge 굵기가 같아보이게 하기 위해 screen space에서 계산
-> vertex 좌표를 screen space로 변환한 뒤 계산해야함
- fragment에서의 edge까지 가장 짧은 거리가 line width보다 작으면 line color와 섞음
- edge부분에선 자연스럽기 위해
smoothstep
기능으로 fade (antialiasing)
📃 Code
Vertex Shader
- gs에서 geometry가 바뀌는게 아니므로, 그냥 평소처럼 계산하여 fs로 넘겨준다 생각하면 됨
Geometry Shader
- screen space로 변환한 버텍스 위치값으로 edge까지의 거리를 구하고, edge-distance vector를 만들어 Emit
Fragment Shader
- gs에서 받은 interpolation된 edge-distance vector의 x, y, z 성분 중 가장 짧은 값을 찾음
- 그 값이 edge와 가까우면 line color와 mix해서 자연스럽게 그려줌 (smoothstep)
❗issues
- vertex shader에서 projection된 좌표를 geometry에서 사용할 때, w로 나누어서 3차원 좌표로 나타내는 과정에서 생길 수 있는 문제
- w가 아주 작으면(버텍스가 카메라에 아주 가깝다면), w로 나눈 값이 매우 커지거나 무한대로 변할 수 있다
- 3D상에서는 크지 않았는데 projection하며 무한히 커질 수 있음
- (절두체 view volume에 projection matrix를 곱하면 정사각형 형태의 view volume)
- 삼각형의 버텍스 중 하나라도 이러한 문제를 겪으면(삼각형이 커서), 삼각형 전체가 왜곡될 수 있음
3. Drawing Silhouette Lines
✔️ silhouette edge 찾고 그리기
- 서로 바라보는 방향이 다른 두 삼각형이 만나는 line이 실루엣 엣지
- 카메라 방향이 z축의 방향이므로, cc기준 normal이 z가 양수면 front-facing, 음수면 not front-facing
- normal의 z좌표만 계산하면 된다 (ABC는 버텍스, xy는 좌표)
- 실루엣 엣지에 카메라와 수직인 쿼드를 추가
- 삼각형의 바라보는 방향을 gs에서 계산하고 실루엣 엣지 판별을 하는데, 이 때 다른 primitive 정보도 알아야함 (주변 점)
Adjacency Modes
- 주변 primitive에 접근하여 추가적인 버텍스 정보 얻기
- 메시에 포함되지 않는 주변 버텍스도 받아올 수 있음
- GLLINES_ADJACENCY, GL_LINE_STRIP_ADJACENCY, GL_TRIANGLES_ADJACENCY, GL_TRIANGLE_STRIP_ADJACENCY 등
- GL_TRIANGLES_ADJACENCY mode를 사용해 한 삼각형 주변의 3개 버텍스 추가 정보를 알아온다 (한 Primitive에 총 6개의 버텍스 정보를 가져옴)
- 0, 2, 4는 실제 삼각형을 구성하는 버텍스
- 1, 3, 5는 주변 삼각형을 구성하는 버텍스
- 한 삼각형을 기준으로, 주변 삼각형의 normal등을 계산할 수 있다!
- 넘어온 6개의 버텍스에 자동으로 index가 붙는 것은 아니여서, opengl application에서 index array에 adjacency 정보도 추가해야함
📃 Code
OpenGL Application
- adjacency 정보가 추가되도록 mesh data(index array) 세팅
- 실루엣 엣지의 크기 및 컬러 설정
vertex shader
- 쉐이딩 및 노말 계산을 위한 cc기준 Vnormal과 Vposition
- 모델 렌더링을 위한 projection까지 마친 gl-Position
geometry shader
- cc기준 normal의 z좌표를 계산하여 facing 판별
isFrontFacing()
- 원래 삼각형이 front-facing일 때를 기준으로(back일 땐 엣지 그릴 필요도 없으니까), 주변 삼각형이 not front-facing인 경우 쿼드 생성!
- 쿼드를 생성할 때, uniform 변수로 넘어온 쿼드 길이 및 두께 정보를 사용해 4개의 버텍스를 emit해준다
emitEdgeQuad()
- 또한, counter clock wise 방향을 잘 지켜서 쿼드가 모델 바깥쪽으로 생길 수 있도록 한다
- 실루엣 쿼드인지 기존 삼각형인지 구분을 해줘야지 fs에서 color 결정을 할 수 있기 때문에 primitive 구별 정보
GisEdge
를 내보내준다
- edge그리는 함수에서 true가 되고, edge 다 내보낸 뒤 기존 삼각형을 내보내기 전에 false로 만들어준다
fragment shader
GisEdge
의 값에 따라 컬러 결정
❗issues
- edge위에 쿼드를 올릴 때, edge의 곡률이 클수록 많이 벌어지고 틈이 생긴다
- 곡률 정보를 반영하기는 복잡해서 보통 edge 길이를 늘려서
PctExtend
틈을 채우는데, 너무 늘리면 쿼드가 삐져나온다
- Depth testing으로 인해 실루엣 쿼드가 가려질 수 있다 -> 끊어지는 실루엣