본문 바로가기
카테고리 없음

쉐이더 심화학습 04-Vertex Shader (~이동, 회전, 크기 조절)

by mazayong 2024. 3. 2.

이번에는 vertex shader의 기능을 중심으로 공부한다.

 

 

 

1. Mesh 이동 처리

2. 크기 변환

3. 회전

 

 

 

 

 

 

 

 

1. Mesh 이동 처리

1.1. Mesh Vertex 갱신

대상을 x축 방향으로 5유닛만큼 이동시킨다고 가정하겠다.

ofApp.cpp의 KeyPressed에 아래 내용을 추가하여 움직일 수도 있다.

int numVerts=charMEsh.GetNumVertices();

for(int i=0; i<numVerts; ++i)
{
	charMesh.setVertex(i,charMesh.getVertex(i)+glm::vec3(0.2f, 0.0f, 0.0f));
}

 

이 방법의 단점을 아래와 같다.

* mesh의 Vertex를 매 프레임마다 갱신할 경우, 연산량 증가로 인해 비효율적이다.

* 게임 속 오브젝트들이 자신만의 mesh instance를 가지므로, mesh 데이터 저장용으로 메모리가 낭비된다.

(게임 속 오브젝트들이 메모리에 있는 mesh를 공유하는 방식 불가)

 

 

 

 

1.2. 화면에 그려지는 vertex 위치 수정

= 화면에 그려지는 vertex 위치를 수정.(mesh 데이터 직접 수정 x, 그래픽스 파이프라인으로는 동일한 mesh 데이터 전송.)

 

spritesheet.vert 

void main()
{
	gl_Position = vec4(pos, 1.0);
    gl_Position=vec4(pos+translation,1.0);
	fragUV = vec2(uv.x, 1.0-uv.y) * size + (offset*size);
}

= mesh vertex에 벡터를 더해준다.(NDC: -1~1)

= sprite sheet의 UV를 기존 fragment shader로 전송하는 역할은 동일.

 

 

 

 

1.3. Uniform 벡터를 사용한 이동

이동 벡터를 Uniform 벡터에 담아서 cpp에서 이동을 제어할 수 있게 처리한다.

예시는 키를 누르고 있는 동안은 캐릭터가 부드럽게 앞으로 걸어가고, 키에서 손을 떼면 멈춰 서게 만들 것이다.

 

 

1.3.1. 필요한 벡터 생성

캐릭터 이동을 담당할 vec3 변수, 키보드 입력 여부 저장할 bool변수를 추가한다.

ofApp.h

bool walkRight;
glm::vec3 charPos;

 

 

1.3.2. 키 입력 상태 파악

keyPressed(), keyReleased()에 키 입력 상태를 파악하는 코드를 작성한다.

ofApp.cpp

void ofApp::keyPressed(int key){
    if(key==ofKey::OF_KEY_RIGHT)
    {
        walkRight=true;
    }
}

//--------------------------------------------------------------
void ofApp::keyReleased(int key){
    if(key==ofKey::OF_KEY_RIGHT)
    {
        walkRight=false;
    }

}

 

 

 

1.3.3. 캐릭터 위치 벡터 갱신

update()에서 매 프레임마다 상태를 점검한다.

(draw()는 렌더링과 관련된 로직이 들어간다. 드로우콜과 직접적으로 관련되지 않는 로직은 update()에 담는 것이 일반적이다.)

 

ofApp.cpp

void ofApp::update(){
    if(walkRight)
    {
        float speed=0.4*ofGetLastFrameTime();
        charPos+=glm::vec3(speed,0,0);
    }

}

= ofGetLastFrameTime()

-> 앞 프레임 시작-현재 프레임까지 경과한 시간을 구해 곱해줌으로써 컴퓨터 성능과 관계 없이 일정한 속도로 보이도록 해준다.

(DeltaTime이라고 불린다.)

 

 

 

1.3.4. charPos값 shader에 전송

vec3 타입이어서 setUniform3f()를 사용해 전송한다.

void ofApp::draw(){
//생략
    spritesheetShader.begin();
    spritesheetShader.setUniform2f("size", spriteSize);
    spritesheetShader.setUniform2f("offset", spriteFrame);
    spritesheetShader.setUniformTexture("tex", alienImg, 0);
    spritesheetShader.setUniform3f("translation", charPos);
    charMesh.draw();
//생략
}

 

그러면 결과는 아래와 같다.

 

 

 

 

 

 

 

 

2. 크기 변환

2.1. 배경 지식-변환(transformation)

= 3D 씬에 존재하는 오브젝트가 갖는 속성들 중 하나 이상을 바꾸는 처리.

= 오브젝트의 속성(이동, 회전, 크기)

= 속성들을 통해 3D 공간에서 오브젝트가 갖는 모든 위치와 방향을 설명할 수 있다.

 

 

2.2. Mesh 크기 변환

= 크기를 조절하기 위해 scale 벡터를 생성 후, 해당 벡터를 자신과 곱한다.

(크기를 그대로 유지하려면 크기 벡터를 단위 벡터로 설정한다.

1.0을 기준으로 이상/이하 값을 곱하면 컴포넌트 차원에서 mesh 크기를 조절한다.)

해당 기능을 보여주기 위해 아래와 같이 코드를 수정한다.

 

passthrough.vert

void main()
{
    vec3 scale=vec3(0.5, 0.75, 1.0);
    gl_Position=vec4((pos*scale), 1.0);
    fragUV=vec2(uv.x, 1.0-uv.y);
}

 

 

결과는 아래와 같다.

= vertex 위치를 NDC에서 정의(배경 mesh 크기를 바꿨지만 화면 중앙에 존재.)

-> vertex 위치가 정의된 좌표계의 원점이 화면 중앙.

= mesh의 오브젝트 공간이 NDC와 일치하는 상황.

(오브젝트 공간(object space) : mesh의 vertex가 정의된 좌표계)

= 위 사진의 경우, 구름 quad는 화면 중심으로부터 먼 곳에 있지만, 오브젝트 공간의 원점은 화면 중앙에 있다.

 

=> 크기를 조절하기 위해서는 기준이 되는 좌표계의 종류와 상관없이, mesh의 중심은 (0, 0, 0) 또는 원점에 존재해야 한다.

 

 

 

2.3 오브젝트 공간 이동 후 크기 조절

 

2.3.1. setup()에서 위치 조건 제거

= 위치 offset은 shader에서 처리하도록 수정한다.

 

ofApp.cpp

void ofApp::setup()
{
//생략
buildMesh(cloudMesh, 0.25, 0.15, glm::vec3(0.0, 0.0, 0.0));
//생략
cloudShader.load("cloud.vert", "cloud.frag");
}

 

 

2.3.2. vertex shader 생성

= vertex shader에서 크기 벡터를 먼저 곱하고, 이동 벡터를 더한다.

(크기를 먼저 조정한 후 이동해야 한다.

크기를 바꾸는 동안 모든 vertex가 원점을 중심으로 위치하기 때문)

 

cloud.vert

#version 410

layout (location = 0) in vec3 pos;
layout (location = 3) in vec2 uv;

out vec2 fragUV;

void main()
{
	vec3 translation = vec3(-0.55, 0.0, 0.0);
	vec3 scale = vec3(0.5, 0.75, 1.0);
	gl_Position = vec4( (pos * scale) + translation, 1.0);
	fragUV = vec2(uv.x, 1.0-uv.y);
}

 

= GPU에서 빠른 처리가 가능한 MAD 연산(곱한 다음 더함)의 장점 적용 가능.

 

위의 결과는 아래와 같다.

(아래 사진은 구름 크기의 비교 확인을 직관적으로 하기 위해 passthrough.vert를 다시 원래 사이즈로 수정하고, 태양 이미지를 지웠다.) 

 

 

 

 

 

 

 

 

 

 

 

 

 

3. 회전

2D Quad에서는 삼각함수를 이용해 회전을 3D보다 간단하게 구현할 수 있다.

 

3.1. vertex shader 수정을 통한 오브젝트 회전

구름 오브젝트를 회전시키기 위해 cloud.vert를 아래와 같이 수정한다.

 

cloud.vert

#version 410

layout (location = 0) in vec3 pos;
layout (location = 3) in vec2 uv;

out vec2 fragUV;

void main()
{
    float radians = 1.0f;
    vec3 rotatedPos = pos;
    rotatedPos.x = (cos(radians)*pos.x)-(sin(radians)*pos.y);
    rotatedPos.y = (cos(radians)*pos.y)-(sin(radians)*pos.x);

	gl_Position = vec4(rotatedPos, 1.0);
	fragUV = vec2(uv.x, 1.0-uv.y);
}

= 크기를 먼저 처리한 후, 회전을 처리해야 한다.

= 회전-이동 순서 : 제자리에서 회전 후 원하는 곳으로 이동.

= 이동-회전 순서 : 이동 후 좌표 공간의 원점을 중심으로 회전.(오브젝트는 회전한 방향으로 이동한 것처럼 보인다.)

 

 

 

위와 같이 코드를 적용한 결과는 아래와 같다.

 

 

3.2. 크기, 회전, 이동 과정 통합

= 대부분 vertex shader는 크기-회전-이동 순서로 처리한다.

 

위의 예시에서 다룬 내용들을 모두 합친 shader 코드는 아래와 같다.

cloud.vert

#version 410

layout (location = 0) in vec3 pos;
layout (location = 3) in vec2 uv;

uniform vec3 translation;
uniform float rotation;
uniform vec3 scale;

out vec2 fragUV;

void main()
{
	vec3 scaled = pos * scale; 

	vec3 rotated;
	rotated.x = (cos(rotation)*scaled.x)-(sin(rotation)*scaled.y);
	rotated.y = (cos(rotation)*scaled.y)+(sin(rotation)*scaled.x);

	gl_Position = vec4( rotated + translation , 1.0);

	fragUV = vec2(uv.x, 1.0-uv.y);
}

 

 

 

그리고 draw()를 수정해서 동일한 mesh를 대상으로 여러 개의 드로우콜을 작성한다.

uniform 변수에 다른 값을 넣어서 화면에서는 서로 다른 모습의 2개의 quad로 그려진다.

 

ofApp.cpp

//생략
void ofApp::draw() {

	static float frame = 0.0;
	frame = (frame > 10) ? 0.0 : frame += 0.2;
	glm::vec2 spriteSize = glm::vec2(0.28, 0.19);
	glm::vec2 spriteFrame = glm::vec2((int)frame % 3, (int)frame / 3);


	ofDisableBlendMode();
	ofEnableDepthTest();

	spritesheetShader.begin();
	spritesheetShader.setUniform2f("size", spriteSize);
	spritesheetShader.setUniform2f("offset", spriteFrame);
	spritesheetShader.setUniformTexture("tex", alienImg, 0);
	spritesheetShader.setUniform3f("translation", charPos);
	charMesh.draw();

	spritesheetShader.end();

	alphaTestShader.begin();
	alphaTestShader.setUniformTexture("tex", backgroundImg, 0);
	backgroundMesh.draw();
	alphaTestShader.end();

	ofDisableDepthTest();
	ofEnableBlendMode(ofBlendMode::OF_BLENDMODE_ALPHA);

	cloudShader.begin();
	cloudShader.setUniformTexture("tex", cloudImg, 0);
	cloudShader.setUniform1f("alpha", 1.0);

	cloudShader.setUniform3f("scale", glm::vec3(1.5, 1, 1));
	cloudShader.setUniform1f("rotation", 0.0f);
	cloudShader.setUniform3f("translation", glm::vec3(-0.55, 0, 0));
	cloudMesh.draw();

	cloudShader.setUniform3f("scale", glm::vec3(1, 1, 1));
	cloudShader.setUniform1f("rotation", 1.0f);
	cloudShader.setUniform3f("translation", glm::vec3(0.4, 0.2, 0));
	cloudMesh.draw();

}
//생략

 

= 게임에서는 mesh를 재사용하면서 변환 데이터를 사용해 각 mesh를 저장한다.

= 모든 mesh를 메모리에 저장하지 않고도 게임에 수많은 구름을 그릴 수 있다.

 

 

다음에는 위와 같은 대안을 실행하는 변환행렬에 대해 알아보도록 하겠다.