C++/Game

[게임 프레임워크 개발 일지] #9 SFML의 Alpha Mask와 Invert

Kareus 2022. 8. 15. 02:15

TextInput을 구현하려면 여러 가지 기능을 구현해야 합니다.

텍스트 입력을 처리하다 보니 커서도 만들어줘야 되고,

한글 입력을 해보니 조합이 완성되어야 글자가 나타나고,

텍스트 선택을 구현하다보니 렌더링 효과를 만들어줘야 하더군요.

 

파란 배경에 흰색 글자

오늘 다룰 것은 바로 이 렌더링 효과입니다. 이건 TextInput과 상관 없어도 다룰 필요가 있었거든요.

이것 관련해서 할 얘기가 좀 있긴 한데, 이건 다음에 얘기합시다. 골치 아프니까요.

 

선택한 텍스트 영역은 주로 파란 배경에 흰색 텍스트로 표시됩니다.

Visual Studio 등에서는 텍스트 색이 그대로기도 합니다만, 텍스트 색이 배경색과 같은 경우의 참사를 막기 위해서

여기서는 파란 배경에 흰색 텍스트 표시를 목표로 하겠습니다.

 

파란 배경이 될 사각형 영역은 글자의 위치를 계산해서 만들어줄 겁니다.

문제는 흰색 텍스트가 되겠네요. 텍스트 전부가 아니라, 일부만 흰색으로 표시해야 합니다.

 

일부의 색이 다른 텍스트는 RichText로 구현해야 했고, Text 여러 개로 표현하기 때문에 커서 및 영역 선택을 구현하기가 어렵습니다. 하필 선택한 영역에 Text 객체가 두 개 이상 있으면, 각각을 쪼개서 흰색으로 표시할 곳을 찾아줘야 되고

아무튼 더럽기 때문에 sf::Text로 구현해야 했습니다. 아무튼 그런 상황입니다.

 

뭐 그래서, Text의 색을 직접 바꿔서 구현하기는 어렵습니다.

결국 저 부분만 다시 그려줘야 된다는 이야기인데 방법은 두 가지 정도 생각해볼 수 있습니다.


하나는 원본 텍스트 위에 파란 사각형을 덮어씌워 그리고, 부분 텍스트만을 새로 sf::Text 객체로 만들어서 그 위치에 그려주는 겁니다.

window.clear();

sf::Text text("Text test", font);
text.setFillColor(sf::Color(100, 100, 100));

sf::RectangleShape bg(sf::Vector2f(text.findCharacterPos(9).x - text.findCharacterPos(5).x, text.getCharacterSize()));
bg.setFillColor(sf::Color(51, 125, 255));
bg.setPosition(text.findCharacterPos(5));

sf::Text selected("test", font);
selected.setPosition(text.findCharacterPos(5));

window.draw(text);
window.draw(bg);
window.draw(selected);
window.display();

test 1


다른 하나는 alpha mask로서 사각형 영역 위에 Text 객체를 다시 한 번 더 그리는 겁니다.

처음에는 Text 객체를 하나 더 만들 필요가 없으니 이 방법을 써야겠다고 생각하고 구현하려 했습니다.

BlendMode에 교차 영역만 그리는 기능은 흔히 있었으니까요.

 

문제는, 윈도우의 배경이 불투명색이라서 원하는 소스만 골라낼 수가 없다는 점이었습니다.

그럼 어떻게 해야하느냐? 투명한 렌더 소스에 그려줘야 됩니다. RenderTexture가 있겠네요.

//init
sf::RectangleShape mask(sf::Vector2f(150, 50));
mask.setFillColor(sf::Color(51, 153, 255, 255)); //light blue
mask.setPosition(0, 50);

sf::RectangleShape src(sf::Vector2f(100, 100));
src.setFillColor(sf::Color::White);
src.setPosition(25, 25);

window.clear();

sf::RenderTexture texture;
texture.create(600, 400);

sf::RenderStates states = sf::RenderStates::Default;


//draw to texture
texture.draw(mask);

states.blendMode = sf::BlendMode(sf::BlendMode::DstAlpha, sf::BlendMode::Zero, sf::BlendMode::Add);
texture.draw(src, states);


//render to window
sf::Sprite sprite;
auto t = texture.getTexture();
sprite.setTexture(t);

int width = t.getSize().x, height = t.getSize().y;
sprite.setTextureRect(sf::IntRect(0, height, width, -height));

window.draw(sprite);

test 2

작동은 잘... 합니다만 이걸 위해서 Texture 객체를 만들고 Sprite를 만들어줘야 됩니다. 되려 비효율적이네요.

Texture는 또 y축이 flip되어 있어서 Sprite로 만들 때 별도로 TextureRect를 바꿔줘야 됩니다.

 


Original

그럼 파란 사각형을 alpha mask로 사용하되, 파란 영역은 렌더링되지 않게 하려면 어떻게 해야 할까요?

지워줘야 됩니다. 그럼 먼저 지우는 것부터 구현해보죠.

 

remove

sf::RenderStates states = sf::RenderStates::Default;
states.blendMode = sf::BlendMode(sf::BlendMode::DstAlpha, sf::BlendMode::SrcAlpha, sf::BlendMode::Equation::Subtract);

texture.draw(src);
texture.draw(mask, states);

remove

 

mask

remove의 여집합입니다. 원본에서 subtract를 한 번 더 해주면 되겠네요.

sf::RenderStates states = sf::RenderStates::Default;

texture.draw(src);

states.blendMode = sf::BlendMode(sf::BlendMode::DstAlpha, sf::BlendMode::SrcAlpha, sf::BlendMode::Equation::Subtract);
texture.draw(mask, states);

states.blendMode = sf::BlendMode(sf::BlendMode::SrcAlpha, sf::BlendMode::DstAlpha, sf::BlendMode::Equation::Subtract);
texture.draw(src, states);

mask

 

alpha mask이기 때문에 알파 값이 255가 아니라면 결과 이미지 또한 mask의 알파 값을 따라갑니다.

알파 값을 특정 값으로 정하고 싶다면 shader 코드를 작성해야 할 것 같네요.

이미 다음 내용부터 질리도록 쓰고 있으니, 그나마 간단한 여기서는 쓰지 않겠습니다.


텍스트 커서를 보면 배경색에 따라 반전된 색깔을 띠고 있습니다.

흰 배경에는 검은색, 검은 배경에는 흰색 이런 느낌이죠.

blend mode에서 invert에 해당하는 효과입니다.

 

2022.08.19 수정)

invert는 1 - dst_color를 적용해야 합니다.

코드로 작성하면 다음과 같습니다.

window.draw(src);

sf::RenderStates states = sf::RenderStates::Default;
states.blendMode = sf::BlendMode(sf::BlendMode::OneMinusDstColor, sf::BlendMode::Zero, sf::BlendMode::Add);
window.draw(mask, states);

 

결과를 알아보기 쉽게, src은 녹색으로 바꿨습니다.

 

invert original

source color가 영향을 끼치므로, 온전하게 destination color를 invert하려면 mask는 흰색이어야 합니다.

우리가 직접 mask의 color를 흰색으로 지정하고 그리게 할 수도 있겠지만, 이러한 선처리 과정이 귀찮을 수 있으니

shader를 이용해서 흰색으로 덮어씌워 고정시켜버릴 수도 있습니다.

 

그리고 배경이 검은색이라 그렇지, 배경도 invert가 적용되어서 흰색으로 렌더링되었습니다.

 

const char* vertShader = R"(

	void main() {
		gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; 
		gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0; 
		gl_FrontColor = gl_Color; 
	}
)";

const char* fragShader = R"(
	uniform sampler2D texture;

	void main()
	{
		vec4 color = texture2D(texture, gl_TexCoord[0].xy);
		if (color.a > 0) color = vec4(1,1,1,color.a);
		gl_FragColor = vec4(1, 1, 1, gl_Color.a) * color;
	}
	)";
    
sf::Shader shader;
shader.loadFromMemory(vertShader, fragShader);
shader.setUniform("texture", shader.CurrentTexture);
window.draw(src);

sf::RenderStates states = sf::RenderStates::Default;
states.blendMode = sf::BlendMode(sf::BlendMode::OneMinusDstColor, sf::BlendMode::Zero, sf::BlendMode::Add);
states.shader = &shader;

window.draw(mask, states);

GLSL이 버전에 따라 문법이 판이하게 다르기 때문에, 최신 버전으로 코드 작성이 어렵더군요.

그냥 예제 코드 긁어와서 대충 수정했습니다.

 

whiten sprite

uniform sampler2D texture는 sprite 처럼 texture를 사용하는 그래픽을 위해 선언해줬습니다.

 

그냥 (1,1,1,1)을 집어넣으면 shape는 잘 돌아가는데 sprite가 texture 여부에 관계 없이 화면 전체를 흰색으로 덮어버립니다. texture에 따라 알파 값이 있는 구역만 흰색으로 칠하게 해야해서 이렇게 작성했습니다.

 

gl_Color -> vec4(1, 1, 1, gl_Color.a) : shape에서는 gl_Color만 사용하므로 흰색으로 바꾸고 알파 값은 유지했습니다.

color는 if문을 사용해서 값을 흰색으로 바꿨습니다.

 

 

사실 포스팅을 다시 수정하기 전에는 mask가 흰색이어야 정상 작동한다는 것을 모르고 파란색 mask로 헛짓했습니다.

그러다가 openGL을 직접 건드리는 코드를 작성했었는데, 이 코드도 일단 첨부하겠습니다.

위와 사실상 같은 코드이고, 같은 결과입니다. SFML 차원에서 기능으로 제공하는 거 굳이 openGL로 직접 건드린 거라고 생각하시면 됩니다.

 

window.draw(src);

sf::RenderStates states = sf::RenderStates::Default;
sf::Shader shader;
shader.loadFromMemory(vertShader, fragShader);
states.shader = &shader;

window.pushGLStates();
glEnable(GL_BLEND);
glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO);
window.draw(mask, states);
window.popGLStates();

 

example code 찾기가 어려워서 여기다가 끼워넣은 것이기도 합니다.

 

alpha mask까지 적용하려면, 다시 말해서 특정 source만 invert하고 싶다면 결국 render texture를 경유해야 합니다.

window는 불투명색의 background가 있으니 여기까지 영향을 받을 수 밖에 없습니다.

 

그런데 문제는, render texture에서는 invert 효과가 적용이 안 된다는 점입니다.

 

failure in texture

이유는 1 - dst.color가 정확히는 (1 - r, 1 - g, 1 - b, 1 - a) 이기 때문입니다.

window에서는 alpha 값이 변하지 않아서 괜찮은데, render texture는 alpha channel이 있는 이미지라서 그런지

alpha 값이 0이 되어버려서 저렇게 구멍이 생깁니다.

 

이런 탓에, blendmode로는 제대로 된 결과가 나오지 않습니다.

결국 color 값을 건드려야되니 shader를 작성해야 되는데...

destination의 color를 가져올 방법이 없습니다.

 

shader에 destination의 texture를 넘겨주는 방법을 생각해봤는데, 

texture를 사용하는 그래픽이 아니라면 적용이 안 됩니다.

shape에 texture를 적용할 수 있기는 한데, texture 객체를 생성해줘야 되고 y축이 반대라서 뒤집어줘야 됩니다.

 

대충 이런 식입니다.

invert for textures

const char* vertShader = R"(

	void main() {
		gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; 
		gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0; 
		gl_FrontColor = gl_Color; 
	}
)";

const char* fragShader = R"(

    uniform sampler2D texture;

	void main()
	{
		vec4 pixel = texture2D(texture, gl_TexCoord[0].xy);
		gl_FragColor = vec4(1 - pixel.r, 1 - pixel.g, 1 - pixel.b, pixel.a);
	}
	)";
    
sf::RenderTexture texture;
texture.create(600, 400);

sf::RenderStates states = sf::RenderStates::Default;
texture.draw(src);


sf::Shader shader;
shader.loadFromMemory(vertShader, fragShader);
shader.setUniform("texture", shader.CurrentTexture);

auto tex = texture.getTexture();
states.texture = &tex;
states.shader = &shader;

mask.setTexture(&tex);
mask.setTextureRect(sf::IntRect(0, 400 - 50, 150, -50)); //height is 400
texture.draw(mask, states);

invert 1

gl_FragColor = gl_Color * vec4(1 - pixel.r, ...); 를 하면 fill Color와 multiply된 색상이 나옵니다.

 

texture를 사용하는 Sprite 등은 이렇게 해야 작동합니다.

Shape는 texture를 지정해주면 작동은 합니다만 좀 지저분합니다.

위 코드 같은 경우에는 texture를 별도로 생성해줘야 됩니다.

그래서 다른 방법을 생각해봤는데, source를 아예 invert된 버전으로 mask의 alpha를 blend mode로 해서 그려버리는 겁니다.

 

이렇게 하려면 mask를 먼저 그냥 그리고, mask의 alpha를 source factor로 해서 invert로 그린 뒤에

나머지 부분을 그냥 그리면 됩니다. invert로 이미 그린 부분은 alpha가 1, 나머지 부분의 destination alpha는 0일테니

1 - dst.alpha로 그리면 됩니다.

 

이 과정까지 하면 mask가 적용되지 않는 곳은 mask를 그대로 그릴 수 있습니다.

 

invert with drawing mask

//shader string
const char* vertShader = R"(

	void main() {
		gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; 
		gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0; 
		gl_FrontColor = gl_Color; 
	}
)";

const char* fragShader = R"(

	void main()
	{
		gl_FragColor = vec4(1 - gl_Color.r, 1 - gl_Color.g, 1 - gl_Color.b, gl_Color.a);
	}
	)";
    
    
//init
sf::RectangleShape mask(sf::Vector2f(150, 50));
mask.setFillColor(sf::Color(51, 153, 255, 255)); //light blue
mask.setPosition(0, 50);

sf::RectangleShape src(sf::Vector2f(100, 100));
src.setFillColor(sf::Color::Green);
src.setPosition(25, 25);


window.clear();

sf::RenderTexture texture;
texture.create(600, 400);

sf::RenderStates states = sf::RenderStates::Default;

texture.draw(mask);

sf::Shader shader;
shader.loadFromMemory(vertShader, fragShader);

states.blendMode = sf::BlendMode(sf::BlendMode::DstAlpha, sf::BlendMode::Zero, sf::BlendMode::Add);
states.shader = &shader;

texture.draw(src, states);

states.blendMode = sf::BlendMode(sf::BlendMode::OneMinusDstAlpha, sf::BlendMode::One, sf::BlendMode::Add);
states.shader = NULL;
texture.draw(src, states);

invert 2

앞서 이야기했듯이, texture를 사용하는 그래픽은 uniform sampler2D texture 등을 선언해서

렌더링할 texture를 가져오고 픽셀을 매핑해줘야 합니다.

여기서 쓴 shader 코드는 단순히 gl_Color만 사용하고 있기 때문에, 이것만을 사용하는 단색 shape 이외에는 작동하지 않습니다.

마찬가지로, 단색 shape는 gl_Color만 사용하고 texture는 사용하지 않기 때문에 texture에서 값을 가져와봤자 (0,0,0,1)입니다.

texture 사용 여부에 상관 없이 쓰려면 조건문이 필요해서, 이건 글 말미에 다루겠습니다.

 

굳이 window를 놔두고 render texture에 그린 이유는 mask의 나머지 부분을 지우기 위해서였습니다.

mask 부분을 지워봅시다. invert한 source를 그리기 전에 alpha mask에서 했던 것처럼 mask를 처리해주면 됩니다.

 

invert intersect only

sf::RenderStates states = sf::RenderStates::Default;

texture.draw(mask);

//
states.blendMode = sf::BlendMode(sf::BlendMode::DstAlpha, sf::BlendMode::SrcAlpha, sf::BlendMode::Subtract);
texture.draw(src, states);

states.blendMode = sf::BlendMode(sf::BlendMode::SrcAlpha, sf::BlendMode::DstAlpha, sf::BlendMode::Subtract);
texture.draw(mask, states);
//

sf::Shader shader;
shader.loadFromMemory(vertShader, fragShader);

states.blendMode = sf::BlendMode(sf::BlendMode::DstAlpha, sf::BlendMode::Zero, sf::BlendMode::Add);
states.shader = &shader;

texture.draw(src, states);

states.blendMode = sf::BlendMode(sf::BlendMode::OneMinusDstAlpha, sf::BlendMode::One, sf::BlendMode::Add);
states.shader = NULL;
texture.draw(src, states);

invert 3

사실 invert 1과 같은 사진입니다. 뭐 어차피 같은 결과니까요.

 

정리하자면 invert를 구현하는 방법에는 두 개 있습니다.

하나는 BlendMode를 OneMinusDstAlpha로 지정해서 흰색 mask를 source 위에 그리는 것.

이를 위해서 mask의 색에 관계없이 무조건 흰색으로 그리게 하는 shader를 작성했습니다.

 

다른 하나는 source 자체의 색을 반전시켜서 그리고 mask 부분만 보이도록 지우고 원본을 다시 그리는 것.

여기서는 색을 반전시키는 shader를 작성했습니다.


결론

 

결국, 구현하려고 했던 효과는 substring으로 Text를 하나 더 만드는 게 편하고 효율적인 것 같습니다.

텍스트 커서의 경우에는 invert 효과를 구현하는 의미가 있긴 했네요.

Sprite 등을 위해서도 alpha mask나 invert 효과는 구현해둘 필요가 있었기 때문에 이렇게 포스팅으로 정리했습니다.

 

 

invert에 대해서 좀 더 이야기해봅시다.

shader 내에서 보면 texture를 사용하지 않는 그래픽은 gl_Color를,

texture를 사용하는 그래픽은 gl_Color * texture2D(texture, gl_TexCoord[0].xy)를 사용합니다.

 

편의상 전자를 shape, 후자를 sprite라고 하겠습니다.

여기서 texture는 shader.CurrentTexture로 직접 지정해줘야 됩니다.

 

반전해줘야할 색은 shape에선 gl_Color이고, sprite에서는 texture2D(texture, gl_TexCoord[0].xy)입니다.

그러니 texture를 쓰냐에 따라 사용할 변수부터가 다릅니다.

const char* vertShader = R"(

	void main() {
		gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; 
		gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0; 
		gl_FrontColor = gl_Color; 
	}
)";

const char* fragShader = R"(
	uniform sampler2D texture;

	void main()
	{
		vec4 color = gl_Color * texture2D(texture, gl_TexCoord[0].xy);
		gl_FragColor = vec4(1 - color.r, 1 - color.g, 1 - color.b, color.a);
	}
	)";


sf::Shader shader;
shader.loadFromMemory(vertShader, fragShader);
shader.setUniform("texture", shader.CurrentTexture);

sprite에서 색을 반전시키려면 이런 shader 코드를 사용해야 합니다.

 

이렇게 되니 shape와 sprite를 동시에 쓸 수가 없습니다.

SFML에서든 GLSL에서든 이 둘을 구분할 수 있는 근거가 없어서 관련 변수를 만들고 직접 지정해줘야 됩니다.

const char* vertShader = R"(

	void main() {
		gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; 
		gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0; 
		gl_FrontColor = gl_Color; 
	}
)";

const char* fragShader = R"(
	uniform sampler2D texture;
	uniform int useTexture = 1;

	void main()
	{
		vec4 color = gl_Color * (texture2D(texture, gl_TexCoord[0].xy) * useTexture + vec4(1, 1, 1, 1) * (1 - useTexture));
		gl_FragColor = vec4(1 - color.r, 1 - color.g, 1 - color.b, color.a);
	}
	)";
    

sf::RenderTexture texture;
texture.create(600, 400);

sf::RenderStates states = sf::RenderStates::Default;

texture.draw(mask);

states.blendMode = sf::BlendMode(sf::BlendMode::DstAlpha, sf::BlendMode::SrcAlpha, sf::BlendMode::Subtract);
texture.draw(src, states);

states.blendMode = sf::BlendMode(sf::BlendMode::SrcAlpha, sf::BlendMode::DstAlpha, sf::BlendMode::Subtract);
texture.draw(mask, states);

sf::Shader shader;
shader.loadFromMemory(vertShader, fragShader);

states.blendMode = sf::BlendMode(sf::BlendMode::DstAlpha, sf::BlendMode::Zero, sf::BlendMode::Add);
shader.setUniform("texture", shader.CurrentTexture);
states.shader = &shader;

shader.setUniform("useTexture", 0);
texture.draw(src, states);

states.blendMode = sf::BlendMode(sf::BlendMode::OneMinusDstAlpha, sf::BlendMode::One, sf::BlendMode::Add);
states.shader = NULL;
texture.draw(src, states);

sf::Sprite sprite;
auto t = texture.getTexture();
sprite.setTexture(t);

int width = t.getSize().x, height = t.getSize().y;
sprite.setTextureRect(sf::IntRect(0, height, width, -height));

states = sf::RenderStates::Default;
shader.setUniform("useTexture", 1);
states.shader = &shader;
window.draw(sprite, states);

invert의 invert

useTexture가 1이면 texture2D(...)를, 0이면 vec4(1,1,1,1)를 가져와 gl_Color에 곱합니다.

sprite는 gl_Color * texture2D(...), shape는 gl_Color가 되겠네요.

이렇게 계산한 값을 반전시켜주면 됩니다.

 

결과 이미지는 방금 얻은 texture 이미지를 사용하는 sprite를 다시 invert해서 window에 그렸습니다.

 

글을 여러번 수정하니까 이게 코드가 맞는 건지 시간상 맞는 건지도 헷갈리네요.

이거 하려고 삽질을 얼마나 했담