카테고리 없음

쉐이더 심화학습 04-Vertex Shader (~변환 행렬, 단위 행렬)

mazayong 2024. 3. 6. 23:59

 

 

4. 변환 행렬

5. 변환 행렬 애니메이션

6. 단위 행렬

 

 

 

 

 

 

 

4. 변환 행렬

= 대부분의 게임에서 크기 변환, 이동 변환, 회전 변환을 합쳐서 처리하는 방법

 

 

4.1. 행렬

= 사각형의 형태로 정렬된 숫자의 배열.

= 이동 회전, 크기의 조합을 저장하기 위해 사용되는 데이터 구조

= 곱하기를 통해 변환 행렬을 합칠 수 있다. 결과는 각 행렬에 들어 있는 연산을 합친 행렬이다.

= 벡터에 변환 행렬을 곱하면 행렬의 이동, 회전, 크기 연산을 벡터에 적용할 수 있다.

 

 

 

4.2. GLM 클래스를 사용한 행렬 만들기

GLM 클래스를 이용한 예시는 아래와 같다.

using namespace glm;
mat4 translation=translate(vec3(0.5, 0.0, 0.0));
mat4 rotation=rotate((float)PI*0.5f, vec3(0.0, 0.0, 1.0));
mat4 scaler=scale(vec3(0.5, 0.25, 1.0));

 

= 이동 연산 : translate()

= 회전 연산 : rotate()

= 크기 연산 : scale()

 

4.2.1. rotate()

= 축/각도(axis/angle) 스타일의 함수로 회전을 만든다.

= vec3로 회전의 기준이 되는 축을, 축을 중심으로 얼만큼 회전할지 radian으로 전달한다.

(위의 코드는 Z축, 화면을 바라보는 방향으로 회전한다. vertex shader에서 사용한 회전과 일치한다.)

 

 

 

4.3 행렬을 사용한 mesh 변환 방법

4.3.1. 단일 연산만 시행할 경우

= 단일 행렬만 shader로 전달하기

 

4.3.2. 2개 이상의 연산을 시행할 경우

= 행렬을 서로 곱해 변환 행렬로 만들어서 시행한다.

 

예시로 위 translation, rotation, scaler을 곱해본다. 여기서 곱하는 순서에 주목하자.

mat4 transformA = translation * rotation * scaler;
mat4 transformB = scaler * rotation * translation;

 

++ 행렬을 곱하는 순서가 복잡한 문제인 이유?

= 수학 라이브러리가 메모리에서 행렬을 선형 배열 형식으로 저장하기 때문.

 

4.3.3. 선형 배열(linear array)의 값을 매핑하는 법

행렬의 메모리 레이아웃 방식에 따라 지칭하는 방법이 다르다.

 

* 행우선(raw-major) 행렬

= 데이터 값을 행에 따라 저장.

= 사후곱셈(post-multiply) 방식으로, 뒤쪽부터 원하는 연산을 작성한다.

 

* 열우선(column-major) 행렬

= 데이터 값을 열에 따라 저장.

= 앞쪽부터 원하는 연산 작성.

 

둘의 차이는 아래와 같다.

 

1       2                        1          3

3       4                       2         4

행우선 행렬                열우선 행렬

 

 

4.3.4 변환 행렬 생성

GLM 클래스를 사용해 변환 행렬을 생성하는 예시를 나타내면 아래와 같다.

(GLM은 열 우선 행렬이다.)

glm::mat4 buildMatrix(glm::vec3 trans, float rot, glm::vec3 scale)
{
	using glm::mat4;
    mat4 translation = glm::translate(trans);
    mat4 scaler = glm::scale(scale);
    return translation * rotation * scaler;
}

 

 

 

 

4.4. 이전과 같은 모습으로 보이도록 코드 적용하기

행렬의 곱셈을 적용해서 이전과 같이 보이도록 수정할 것이다.

 

4.4.1. 변환 행렬 생성하기

4.3.4에서 작성한 코드를 ofApp.cpp에 추가한다.

 

그리고 draw()에 행렬을 생성하고 적용하는 코드를 추가한다.

void ofApp::draw() {

    using namespace glm;

	//생략

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

    mat4 transformA = buildMatrix(vec3(-0.55, 0.0, 0.0), 0.0f, vec3(1.5, 1, 1));
	mat4 transformB = buildMatrix(vec3(0.4, 0.2, 0.0), 1.0f, vec3(1, 1, 1));

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

	cloudShader.setUniformMatrix4f("transform", transformA);
	cloudMesh.draw();

	cloudShader.setUniformMatrix4f("transform", transformB);
	cloudMesh.draw();

	cloudShader.end();

}

 

 

 

4.4.2. vertex shader 수정

이전에 vertex shader에 적용되던 부분이 cpp에 작성되었으므로 이전과 같이 수정한다.

 

cloud.vert

#version 410

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

uniform mat4 transform;
out vec2 fragUV;

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

 

 

결과는 아래와 같다.

 

= GPU는 행렬 곱셈을 삼각함수보다 더 빠르게 처리하므로 런타임 성능에서 효율적이다.

=> 크기/회전/이동 등 연산을 할 때는 행렬 곱셈을 사용하자!

 

 

 

 

 

 

 

 

 

 

5. 변환 행렬 애니메이션

행렬을 사용해서 구름을 회전하게 만들 것이다.

= 매 frame 회전 각도를 바꾸고, 각도를 사용해 mesh에 적용할 행렬을 frame단위로 갱신한다.

 

 

5.1. frame 회전각 변경을 통한 frame 갱신을 통해 애니메이션 만들기

5.1.1. 매 frame 회전 행렬 갱신하기

= 회전 행렬에 있던 고정된 회전 값을 매 프레임 갱신하는 값으로 대체했다.

 

ofApp.cpp

void ofApp::draw(){
//생략
	static float rotation = 0.0f;
    rotation += 0.1f;

    mat4 transformA = buildMatrix(vec3(-0.55, 0.0, 0.0), 0.0f, vec3(1.5, 1, 1));
 //생략
}

 

 

 

5.2. 곱셈 순서에 따른 결과 비교

= resultA : 원래의 크기, 회전, 이동 연산에 앞서 새로운 회전을 처리한다.

= resultB : 크기, 회전, 이동 연산 이후 회전을 추가한다.

 

ofApp.cpp

void ofApp::draw(){
//생략
    mat4 transformA = buildMatrix(vec3(-0.55, 0.0, 0.0), 0.0f, vec3(1.5, 1, 1));
	mat4 ourRotation = rotate(rotation, vec3(0.0, 0.0, 1.0));
    mat4 resultA = transformA * ourRotation;
    mat4 resultB = ourRotation * transformA;
//생략
    }

 

resultA는 왼쪽 구름, resultB는 오른쪽 구름의 형태이다.

resultA

= 메쉬의 크기 이상하게 변형 : 크기 연산 전에 회전을 처리했기 때문.

resultB

= 화면 원점을 중심으로 구름 회전 : 이동 연산 다음에 회전을 처리했기 때문.

 

= 모든 회전 연산은 mesh에 적용되는 크기, 이동 연산 사이에 있는 것이 가장 이상적.

 

 

 

5.3. 이상적인 회전 연산 적용법?

그렇다면, 원하는 회전을 넣으려면 아래와 같이 적용해야 할 것이다.

 

크기-회전-원하는회전-이동

(그러나 기술적으로 불가능)

 

기술적으로 해당 원리를 적용해서 회전을 적용하면 아래와 같다.

 

크기-회전-이동-정반대 이동-원하는 회전-이동

 

 

 

5.4. 5.3을 반영하여 원하는 회전 적용시키기

 

5.4.1. 회전이 없는 상태의 변환 행렬 설정하기

아래와 같이 회전이 없는 상태의 변환 행렬을 설정한다.

(++ 크기-이동 순서로 곱해야 한다는 사실을 명심한다.)

void ofApp::draw(){
//생략
	static float rotation = 1.0f;
    rotation += 1.0f * ofGetLastFrameTime();

    mat4 translationA = translate(vec3(-0.55, 0.0, 0.0));
    mat4 scaleA = scale(vec3(1.5, 1, 1));
    mat4 transformA = transformA = translationA * scaleA;
 //생략
}

 

 

5.4.2. 회전, 이동 행렬 합치기

= 구름 mesh에 적용하려는 회전과 변환 행렬이 이미 갖고 있는 이동을 미리 합친다.

= frame 단위로 갱신되는 회전 행렬에 원래 이동 행렬을 곱한다.

(연산을 수행하려는 순서와 반대 순서로 행렬을 곱한다.)

void ofApp::draw(){
//생략
    mat4 ourRotation = rotate(rotation, vec3(0.0, 0.0, 1.0));
    mat4 newMatrix = translationA * ourRotation * inverse(translationA);
//생략
}

 

 

5.4.3. 원래 이동의 정반대 이동, 회전, 원래 이동 순서로 처리하기

= inverse를 사용해 원래 이동의 역행렬을 만든다. (정반대 이동 처리)

= 원래 행렬을 newMatrix로 곱해 제자리에서 구름이 회전하는 결과를 도출한다.

void ofApp::draw(){
//생략
    mat4 finalMatrixA = newMatrix * transformA;
    mat4 transformB = buildMatrix(vec3(0.4, 0.2, 0.0), 1.0f, vec3(1, 1, 1));
//생략
}

 

 

전체적인 코드를 합치면 아래와 같다.

    static float rotation = 0.0f;
    rotation += 0.1f;

    mat4 translationA = translate(vec3(-0.55, 0.0, 0.0));
    mat4 scaleA = scale(vec3(1.5, 1, 1));
    mat4 transformA = transformA = translationA * scaleA;

    mat4 ourRotation = rotate(rotation, vec3(0.0, 0.0, 1.0));
    mat4 newMatrix = translationA * ourRotation * inverse(translationA);
    mat4 finalMatrixA = newMatrix * transformA;
    mat4 transformB = buildMatrix(vec3(0.4, 0.2, 0.0), 1.0f, vec3(1, 1, 1));

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

	cloudShader.setUniformMatrix4f("transform", finalMatrixA);
	cloudMesh.draw();

    cloudShader.setUniformMatrix4f("transform", transformB);
	cloudMesh.draw();

	cloudShader.end();

 

 

 

그러면 finalMatrixA가 적용된 왼쪽 구름은 제자리에서 회전하고 있고, transformB가 적용된 오른쪽 구름은 회전된 채로 유지되는 그림을 볼 수 있다.

대부분의 게임은 오브젝트 회전을 위해 반대 이동-회전-다시 이동의 행렬을 사용하지 않는다.

(게임에서는 각 연산을 벡터에 저장하고, 매 frame 또는 어떤 벡터가 변할 때마다 새로운 변환 행렬 만드는 방법을 많이 사용.)

 

 

 

 

 

 

 

6. 단위 행렬

= 일관된 방법으로 오브젝트를 다뤄서, 효율적인 방법으로 shader를 재사용하기 위해 사용.

= 이동 연산이 없으면서 다른 곳에 사용하는 것과 동일한 shader로 렌더링 가능.

 

6.1. 단위 행렬 만들기

= buildMax()에 이동이 모두 0, 회전 0, 크기 모두 1인 벡터를 대입한다.

= GLM mat4 타입의 기본 값이 단위 행렬이다.

 

아래와 같은 방법으로 GLM을 사용해 단위 행렬을 만든다.

mat4 identity = glm::mat4();

 

 

 

 

6.2. 프로젝트 수정

= 변환 행렬을 사용하는 하나의 vertex shader를 공유하도록 프로젝트를 수정할 것이다.

 

 

6.2.1. vertex shader 수정

= sprite sheet shader : 외계인에 사용.

= passthrough shader : 다른 오브젝트에 사용.

 

6.2.1.1. passthrough vertex shader 수정

= 변환 행렬 대입해서 위치 설정할 수 있도록 코드 수정.

 

#version 410

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

uniform mat4 transform;
out vec2 fragUV;

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

 

 

6.2.1.2. sprite sheet shader 수정

= 위치값을 해당 변환 행렬에 곱한 값으로 대치해 해당 행렬값을 받아와서 작업하도록 한다.

 

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

 

 

6.2.2. 변환 행렬 적용하기

ofApp.cpp에서 행렬을 uniform으로 받아와서 연산 가능하도록 수정한다.

 

//생략
void ofApp::setup(){
//생략
	buildMesh(charMesh, 0.1, 0.2, glm::vec3(0.0, 0.0, 0.0));
	charPos = glm::vec3(0.0, -0.2, 0.0);
 //생략
 }
 
 
 void ofApp:draw(){
 //생략
	mat4 identity = glm::mat4();

	ofDisableBlendMode();
	ofEnableDepthTest();

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

	spritesheetShader.end();

	alphaTestShader.begin();
	alphaTestShader.setUniformTexture("tex", backgroundImg, 0);
	alphaTestShader.setUniformMatrix4f("transform", translate(vec3(0.0,0.0,0.5)));
	backgroundMesh.draw();
	alphaTestShader.end();
 //생략
 }

 

= 단위행렬로 적용하기 위해 외계인이 원점에서 이동한 거리를 charPos로 따로 빼서 계산한다.

= sprite sheet shader에 해당 벡터만큼 이동 연산을 적용한 변환 행렬을 만들기 위해 translate()를 사용한다.

= 배경 텍스트도 같은 방법으로 변환 행렬을 적용한다.

 

 

결과는 아래와 같이 출력된다.

 

 

 

이렇게 vertex shader를 수정함으로써 행렬을 통해 오브젝트의 크기, 이동, 회전 연산을 적용할 수 있고, 단위 행렬에 대해 이해할 수 있었다.