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

쉐이더 심화-3. 투명도와 깊이(~스프라이트 애니메이션)

by mazayong 2024. 2. 20.

 

이번에 구름을 만들면서 alpha blending, alpha texture의 차이를 알아보고 태양을 통해 addictive blending, 스프라이트 애니메이션 생성 과정을 알아볼 것이다.

 

 

 

4. Alpha Blending

5. Addictive Blending

6. Sprite Sheet Animation

 

 

 

 

 

 

 

4. Alpha Blending

4.1. Alpha Testing과의 불투명도 표현 차이

= Alpha Testing : 보이는 fragment / 보이지 않는 fragment로만 구분 가능.

= Alpha Blending : 투명도 수치를 조절 가능.

이 둘의 직관적인 차이를 보기 위해 alpha texture, alpha blending으로 각각 다르게 구름을 렌더링해 차이를 볼 것이다.

구름의 크기는 화면 너비의 1/4, 화면 높이의 1/6이고, 구름의 위치는 (-0.55, 0, 0)에 배치한다.

(++실습 파일에 사용되는 구름 이미지가 있지만, 필자는 이미지가 잘 적용되지 않아 다른 png를 찾아서 적용했다.)

 

 

4.2. alpha testing shader를 이용한 구름 렌더링

ofImage로 새로운 구름 이미지를 가져오고, ofMesh로 mesh를 선언한 후 이전에 작성한 alphaTestShader를 적용하면 아래와 같은 이미지가 렌더링된다.

 

 

4.3. alpha blending shader를 이용한 구름 렌더링

이제 alpha blending을 적용한 구름을 만들어보자. 

이 때 구름의 형태는 유지하기 위해 texture에 있는 alpha 정보는 유지해야 한다.

(fragment alpha <= 0.8 *&& 0.8보다 작은 alpha값은 유지)

=> fragment shader에 min() 사용.

 

 

cloud.frag

#version 410
uniform sampler2D tex;
in vec2 fragUV;

out vec4 outCol;

void main()
{
    outCol=texture(tex, fragUV);
    outCol.a=min(outCol.a, 0.8);
}

 

조건문을 사용할 수도 있지만, 더 깔끔하게 처리되는 min()을 사용했다.

 

 

ofApp.cpp

void ofApp::setup(){
    //생략
    cloudShader.load("passthrough.vert", "cloud.frag");
}


void ofApp::draw(){
   //생략
	alphaTestShader.end();

    cloudShader.begin();
    cloudShader.setUniformTexture("tex",cloudImg,0);
    cloudMesh.draw();
    cloudShader.end();
}

= shader 2개가 필요하므로 draw()에서 alphaTestShader를 종료시키고, cloudShader를 시작한다.

= setup()에서 ofDisableAlphaBlending이 켜져있지 않도록 유의한다.

 

 

 

위의 코드를 적용하면 결과는 아래와 같다.

 

++ min(), max()

= 두 함수는 각각 2개의 인자를 받아서 작거나 큰 인자를 반환한다.

= 인자를 정수, 실수, 벡터 등으로 받아올 수 있다.

 

 

 

 

 

4.4. alpha blending의 원리

= 화면에 최종적으로 그려지는 픽셀의 색상을 구하기 위해서는 여러 fragment의 색상을 어떻게 blending할지 GPU가 알고 있어야 함.

(shader는 씬의 다른 대상의 정보를 갖고 있지 않지만, 결과적으로 화면에 이미지가 그려졌기 때문.)

= fragment shader에서 alpha blending + depth buffer 계산 과정 실행.

(ex: 구름이 차지하는 화면의 모든 픽셀 = 복수의 fragment 생성. (구름, 배경)

두 fragment를 합쳐서 최종적으로 화면에 그려질 픽셀의 fragment 생성.)

 

 

4.4.1. 구름이 불투명할 경우

= GPU가 깊이 계산을 통해 구름이 배경보다 앞에 있다고 결정 가능.

= 그 영역에 해당하는 배경의 fragment 버리고, 구름 fragment를 화면에 그리면 됨.

 

 

4.4.2. 구름이 반투명할 경우

= 화면에 비치는 픽셀의 최종색 : 그 위치에 해당하는 두 fragment를 섞은 값.

= 섞을 때 사용하는 수식 : blending equation 사용.

(반투명해서 fragment를 버릴 수 없기 때문.)

 

 

4.4.3. Alpha Blend Equation(블렌딩 공식)

vec4 finalColor = src * src.a + (1.0 - src.a) * dst;

src, dst : blending할 두 fragment 색상값.

dst : 색을 섞어야 할 시점에 back buffer에 이미 저장된 값.

src : back buffer에 저장된 값과 섞어야 하는 새 fragment 색상값.

= src alpha 값이 클수록, dst에 비해 더 많이 반영됨.

 

= 그래픽 파이프라인 거친 렌더링의 결과를 백 버퍼(back buffer)에 저장.

= 이후 특정 프레임의 렌더링이 끝나면 back buffer를 화면에 보여줌.

(그래픽 파이프라인은 mesh를 개별적으로 처리하기 때문.)

 

 

 

ex) 예시-나무에 반투명한 구름 렌더링

1) 배경 이미지를 화면에 그린다. 

= 불투명한 녹색 fragment를 back buffer에 저장.

(back buffer = empty)

 

2) 구름을 그린다. (하얗고 반투명한 fragment)

= fragment shader이 같은 픽셀 위한 fragment 생성.

 

3) GPU가 back buffer에 픽셀에 해당하는 값이 이미 저장되어 있음을 확인.

= 블렌딩 공식에서 구름 fragment를 src에, back buffer에서 픽셀에 저장된 현재 값을 dst에 저장.

 

4) 블렌딩 공식에 따라 계산하고, 결과를 back buffer에서 픽셀이 해당하는 위치에 저장.

 

 

 

 

 

 

 

 

 

5. Addictive Blending(가산 블렌딩)

= 뒤에 있는 것을 더 잘 보이게 만드는 블렌딩 기법 중 하나.

 

 

5.1. Addictive Blending?

vec4 finalColor = src + dst;

= 두 fragment 더함. (alpha를 사용하지 않고 각각의 100% 더함)

= 대상을 밝게 만드는데 최적화. 

=> 조금만 밝아지는 효과 : 낮은 alpha가 아닌 어두운 색상 이용.

 

 

 

 

5.2 Addictive Blending을 적용한 태양 렌더링

5.2.1. 태양 mesh 생성

우선 mesh 생성을 위한 buidMesh를 아래와 같이 호출한다.

buildMesh(sunMesh, 1.0, 1.0, glm::vec3(0.0, 0.0, 0.4));

 

 

5.2.2. shader 설정

구름과 같은 alphaTest Shader를 이용한다.

= 태양의 alpha가 매우 작아서 구름에서의 alpha 최대치 고정 로직이 fragment에 적용되지 않기 때문.

 

 

5.2.3. 블렌딩 공식 설정

addictive blending을 사용하기 위해 아래와 같이 코드를 수정한다.

void ofApp::draw(){
    ofDisableBlendMode();

	alphaTestShader.begin();
	alphaTestShader.setUniformTexture("tex", alienImg, 0);
	charMesh.draw();

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

	ofEnableBlendMode(ofBlendMode::OF_BLENDMODE_ALPHA);

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

	ofEnableBlendMode(ofBlendMode::OF_BLENDMODE_ADD);

	cloudShader.setUniformTexture("tex", sunImg, 0);
	sunMesh.draw();
	cloudShader.end();
}

 

ofEnableBlendMode(ofBlendMode::OF_BLENDMODE_ADD);

= GPU에게 다음에 보낼 드로우콜(sunMesh.draw())가 addictive blending 사용할 것 명시.

 

ofEnableBlendMode(ofBlendMode::OF_BLENDMODE_ALPHA);

= 구름의 블렌딩 모드를 alpha blending으로 설정. 

= 복수의 블렌딩 모드를 사용하기 때문에 구름을 그리기 전에 블렌딩 모드 명확하게 설정. (일단 모드를 정하면 바꾸기 전까지 GPU가 해당 블렌딩 모드 유지)

 

ofDisableBlendMode();

= draw()가 addictive blending이 활성화된 상태로 끝나기 때문에 새로운 프레임 렌더링 전 블렌딩 비활성화가 필요하기 때문.

= 캐릭터, 배경의 블렌딩 비활성화.

(++ 블렌딩은 상대적으로 성능 비용이 커서 꼭 필요할 때만 활성화하는 것 필요.)

(++ 불투명/투명 fragment는 alpha blending이 렌더링에 영향 미치지 않음.)

 

 

위와 같이 코드를 작성하면 결과는 아래와 같다.

 

= 구름 quad 전체의 깊이 정보를 GPU로 보내기 때문.

= 구름 mesh는 태양 mesh 앞에 위치하는데, GPU는 이 깊이정보를 바탕으로 구름 quad 뒤의 태양 fragment 처리 생략.

=> 반투명한 mesh가 깊이 정보를 보내지 않게 설정해야 함.

 

 

 

5.2.4. 반투명한 mesh를 깊이 정보 저장에서 제외.

ofApp.cpp에서 깊이 테스팅을 비활성화하는 코드를 추가한다.

void ofApp::draw(){
    ofDisableBlendMode();
	ofEnableDepthTest();

	alphaTestShader.begin();
	alphaTestShader.setUniformTexture("tex", alienImg, 0);
	charMesh.draw();

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

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

	cloudShader.begin();
	cloudShader.setUniformTexture("tex", cloudImg, 0);
	cloudMesh.draw();
//생략
}

 

= 깊이 테스팅도 한 번 활성화/비활성화하면 GPU에 새 명령을 내리기 전까지 모든 드로우콜에서 설정을 유지하기 때문.

=> 반투명 mesh에 드로우콜 보내기 전에 깊이 테스팅을 비활성화하고, 불투명한 mesh 그리기 전에 다시 활성화한다.

 

위와 같이 코드를 추가하면 결과는 아래와 같다.

즉, 불투명한 mesh를 그릴 때는 깊이 테스팅을 활성화하고, 그 밖의 것은 깊이 테스팅을 비활성화해야 한다.

 

++ 일부 엔진의 경우, 깊이 쓰기(depth writing)과 깊이 테스팅을 구분할 수 있다.

-> GPU로 하여금 기존 깊이 버퍼를 이용해 특정 mesh가 다른 mesh에 가려지게 만들면서, mesh의 깊이 정보는 새로 쓰지 않게 만들 수 있다.

 

 

 

 

 

 

 

 

 

6. Sprite Sheet Animation

2D 애니메이션을 만들기 위해 sprite sheet(특정 동작의 단계를 담은 캐릭터의 여러 이미지)를 사용한다.

이것을 보통 애니메이션화하기 위해 quad mesh를 사용하고, 한 번에 1개의 애니메이션 프레임을 보여주기 위해 vertex shader에서 UV 좌표의 offset을 제어한다.

 

여기서 애니메이션 프레임은 shader의 uniform 변수가 선택하며, cpp가 이를 갱신하면서 보여진다.

예시로 캐릭터의 걷는 애니메이션을 제작해 보자.

 

 

6.1. vertex shader

UV좌표의 크기를 조절해 sprite sheet의 첫 번째 프레임만 선택하게 설정한다.

= texture 좌표 (0,0)을 이미지의 좌측 상단을 설정하도록 한다.(애니메이션 프레임도 좌측 상단에서 시작하기 때문.)

=> V좌표 방향으로 좌표를 반전시키기.(이전에는 좌측 하단 (0,0)에서 시작)

 

sprite sheet에서 1개 프레임만 선택하도록 설정

= UV좌표에 2D 벡터를 곱해 1개 프레임에 맞게 크기 조절.

 

원하는 프레임을 감쌀 수 있게 설정

= 다른 2D 벡터를 더해 좌표에 offset 설정.

 

위 방법을 코드로 작성하면 아래와 같다.

 

 

spritesheet.vert

#version 410

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

uniform vec2 size;
uniform vec2 offset;

out vec2 fragUV;

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

= UV 크기를 조절하는 2D 벡터와 원하는 프레임으로 이동하기 위한 2D 벡터 전달할 준비.

=> 두 값을 곱해 UV좌표에 적용할 이동 벡터 계산.

 

 

ofApp.cpp

void ofApp::setup(){
    //생략
    alienImg.load("walk_sheet.png");
  //생략
    spritesheetShader.load("spritesheet.vert", "alphaTest.frag");
   //생략
}

//생략

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);
    charMesh.draw();

    spritesheetShader.end();

	alphaTestShader.begin();
	alphaTestShader.setUniformTexture("tex", alienImg, 0);

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

	//생략
}

setup()

= 새로운 vertex shader, 기존 alphaTest fragment shader를 묶는 새로운 ofShader spritesheetShader 객체 생성.

= 외계인 이미지 교체

 

draw()

= 외계인 렌더링시 spritesheetShader로 드로우콜 전송

= vertex shader의 uniform 변수 값을 설정

= spritesheetShader 설정 후 외계인 메쉬 그리는 드로우콜 전송.

++ 위쪽은 애니메이션 프레임으 반복해서 재생하는 코드이다.