C++/Game

[게임 프레임워크 개발 일지] #18 Clipping Mask

Kareus 2024. 2. 19. 21:52

TextInput을 구현할 때 glScissor를 사용해서 Clipping을 구현했었고,

그 외에는 SFML의 RenderTexture에서 BlendMode 같은 걸 사용했었습니다.

 

그런데 TextInput에 transform을 적용해서 회전하거나 비율이 달라져도 glScissor에는 적용되지 않는다는 문제를 발견했습니다. 특히 회전하는 경우에는 직사각형 형태로 클리핑할 수가 없기 때문에, 새로운 방법이 필요했습니다.

 

사실 SFML에는 2.0 초창기부터 Clipping Mask에 대한 기능 요구가 있었습니다만,

다른 기능들과의 호환성 문제 때문인지 Clipping Mask가 구현된 fork까지 있음에도 불구하고 적용되지 않았습니다.

근데 이걸 이제야 3.0에서 구현하고 있더군요. 그래서 깃허브에서 clipping mask fork와 3.0 리포지토리를 참고하면서

Clipping Mask를 따로 구현해야겠다고 생각하게 되었습니다.

 

openGL 등에서 Clipping Mask를 위해 사용하는 것이 stencil buffer입니다.

stencil buffer에 특정한 값을 쓰고, stencil test로 그 위치의 fragment를 그릴지 말지를 판정하는 겁니다.

SFML 2에서 Window를 생성할 때는 기본적으로 stencil buffer 사용이 꺼져 있기 때문에,

ContextSettings에서 stencil buffer의 비트를 지정해줘야 합니다.

sf::ContextSettings settings(0, 8); //stencil buffer is 8 bits
sf::ContextSettings settings2; //default setting
settings2.stencilBits = 8; //you can also set stencil bits this way

sf::RenderWindow window(width, height, settings);

 

 

stencil bits를 8로 지정하는 경우, stencil buffer의 값으로 255까지 사용할 수 있습니다.

 

포스팅 시점 기준으로 SFML의 버전이 2.6이고, stencil test 관련 기능을 지원하지 않으므로

직접 openGL의 코드를 사용해서 stencil test를 실행해야 합니다. 기본적인 틀은 다음과 같습니다.

glEnable(GL_STENCIL_TEST);

//clear stencil buffer
glClearStencil(0);
glClear(GL_STENCIL_BUFFER_BIT);

//draw mask
glStencilMask(0xFF); //set stencil value mask
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); //set all color mask channels to false,
                                                     //so the mask is not drawn on screen
glStencilFunc(GL_ALWAYS, 1, 0xFF); //stencil buffer will be filled with the reference value (1),
                                   //always in test (GL_ALWAYS),
                                   //applying mask (0xFF) to the reference value/stencil buffer value.
glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE); //each operation occurs
                                                 //when stencil test fails /
                                                 //stencil test passes but depth test fails /
                                                 //stencil test and depth test passes
//draw the mask here!
target.draw(...);

//draw source on the stencil mask

////same initialization with mask
//glEnable(GL_STENCIL_TEST);
//glStencilMask(0xFF);

glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); //the source should be drawn on screen
glStencilFunc(GL_EQUAL, 1, 0xFF); //the source will be drawn on buffer which is equal to 1
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); //the source should not change the stencil buffer

//draw the source here!
target.draw(...);

//clean up stencil test
glDisable(GL_STENCIL_TEST);
//glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glStencilFunc(GL_NEVER, 1, 0xFF);
//glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);

 

요약하면 다음과 같습니다.

1. stencil buffer를 비운 후에,

2. glColorMask를 모두 false로 설정하여 그래픽을 그리지 않게 한 뒤

mask가 될 그래픽에 해당하는 버퍼를 조건과 상관없이 1로 채웁니다.

3. glColorMask를 다시 true로 설정하여 그래픽을 그리도록 한 뒤

source가 될 그래픽에 해당하는 버퍼의 값이 1과 같다면 그 영역에만 source를 그리도록 합니다.

 

GL_LESS, GL_GEQUAL 같은 다른 부등식 조건을 쓰거나

reference value로 1 대신 0~255 사이의 다른 값을 사용할 수도 있겠죠.

저는 이걸 struct로 만들어뒀습니다.

struct Mask
{
    bool drawGraphics;
    GLenum cmp;
    GLint value;
    GLuint cmp_mask;
    GLuint stencil_mask;
    GLenum fail, zfail, zpass;

    Mask(bool drawGraphics = false, GLenum cmp = GL_ALWAYS, GLint value = 1, GLuint cmp_mask = 0xFF, GLuint stencil_mask = 0xFF, GLenum fail = GL_REPLACE, GLenum zfail = GL_REPLACE, GLenum zpass = GL_REPLACE);
};

void onMaskMode(Mask mask)
{
    onMaskMode(mask.drawGraphics, mask.cmp, mask.value, mask.cmp_mask, mask.stencil_mask, mask.fail, mask.zfail, mask.zpass);
}

void onMaskMode(bool drawGraphics, GLenum cmp, GLint value, GLuint cmp_mask, GLuint stencil_mask, GLenum fail, GLenum zfail, GLenum zpass)
{
    if (!glIsEnabled(GL_STENCIL_TEST)) glEnable(GL_STENCIL_TEST);

    glStencilMask(stencil_mask);
    GLboolean mask = drawGraphics;

    glColorMask(mask, mask, mask, mask);

    glStencilFunc(cmp, value, cmp_mask);
    glStencilOp(fail, zfail, zpass);
}

void off()
{
    if (glIsEnabled(GL_STENCIL_TEST)) glDisable(GL_STENCIL_TEST);

    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    glStencilFunc(GL_NEVER, 1, 0xFF);
    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
}

//example
Mask mask;
onMaskMode(mask);
target.draw(...);
off();

 

Stencil test를 이용하면 다음과 같이 직사각형이 아니더라도 Clipping Mask를 사용할 수 있습니다.

 

Circle Mask on Text

 

다만 Source를 먼저 그리고 Mask를 나중에 그리면 당연히 stencil test가 적용되지 않으며,

후술하겠지만, mask가 적용되는 buffer의 범위가 '보이는 것'과 다를 수 있습니다.

 

이즈음에서 떠오른 다른 문제가 있습니다. 마스크가 여러 개인 경우는 어떻게 처리하는가?

이건 두 가지 경우로 나뉘는데, 마스크가 중첩되는 경우와, 서로 다른 마스크를 교체하면서 그리는 경우로 나눌 수 있겠습니다.

 

마스크에 마스크를 중첩 적용하는 건, stencil test의 조건이 달라지게 되므로 사용자가 직접 바꾸는 수밖에 없습니다.

A와 B를 마스크로 사용해서 두 마스크가 겹치는 영역만 그리게 하고 싶다면 다음과 같은 방법을 써야 합니다.

 

A를 사용하는 stencil test에서 버퍼를 1로 설정했다면,

B를 사용하는 test에서는 값이 1과 같은 경우 (GL_EQUAL, 1)에만 2로 설정하도록 해서

(혹은 GL_REPLACE 대신 GL_INCR 같은 걸 사용해서 버퍼 값을 증가시켜도 될 것 같긴 합니다)

source를 2와 같은 경우에만 (GL_EQUAL, 2) 그리도록 한다든지

 

혹은 A와 B에 사용하는 reference mask의 비트를 다르게 해서 (A = 1, B = 1 << 1. 여기서도 B는 2긴 하네요) 

두 비트 모두 1인 영역에만 source를 그리도록 하면 되겠네요.

이 경우에는 코드가 이런 식으로 될 것 같습니다.

GLuint A = 1, B = 1 << 1;

//mask A
glStencilMask(A);
glStencilFunc(GL_ALWAYS, 0xFF, 0xFF);
glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);

//mask B
glStencilMask(B);
glStencilFunc(GL_ALWAYS, 0xFF, 0xFF);
glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);

 

사실 이 코드는 대충 구글링해서 정리만 해놨고, 따로 테스트하진 않았습니다. 귀찮네요

 

두번째로 여러 개의 마스크를 바꿔가면서 적용한다는 건, 마스크 A를 쓰다가 마스크 B로 교체하고 다시 마스크 A로 돌아가야 하는 등의 경우를 말합니다.

 

코드로 예를 들면

onMaskMode(A);
//draw
onMaskMode(B);
//draw
onMaskMode(A);
//draw

같은 느낌이 되겠지만, 항상 무슨 마스크인지 다 알고 있는 상태에서 동작하는 건 아니기 때문에

stack을 만들어서 mask setting을 저장해줘야 했습니다. openGL에서 matrix state를 다룰 때의 그 느낌이군요.

 

std::stack<Mask> stack_mask;

void pushMask(Mask mask)
{
    stack_mask.push(mask);
    update();
}

void popMask()
{
    if (stack_mask.empty()) return;
    stack_mask.pop();
    update();
}

void update()
{
    if (stack_mask.empty())
    {
        off();
        return;
    }
    
    auto& mask = stack_mask.top();
    onMaskMode(mask);
}

 

여기서는 모두 Mask인 걸로 가정하고 간단하게 작성했습니다.

Source까지 다 적용하려면 Mask, Source를 모두 포함하는 struct를 정의하고,

mode 같은 변수를 추가해서 각 mode에 따라 onMaskMode를 할지, onSourceMode를 할지 결정해야겠죠.

 

이러고보니 문제가 생겼습니다.

stencil mask가 중첩으로도 작동하게끔 설계한 건 좋은데, 결국 buffer 값은 영향을 받는다는 점이었습니다.

예를 들어 처음 문제로 되돌아가서,

TextInput에 clipping rectangle이 있고 여기 자체에 stencil mask를 적용하고 싶다면

TextInput은 내부 clipping에 stencil test를, clipping이 완료된 Text에 stencil test를 적용해야 합니다.

그런데 이미 clipping에서 stencil buffer를 수정했다면, 이후 stencil test에 영향을 줄 수 밖에 없는 거죠.

그러니 임시 stencil buffer를 새로 만들 필요가 있었습니다.

 

SFML에서는 RenderTexture를 사용하면 새로운 RenderTarget을 만들 수 있습니다.

그러니 과정이 조금 번거롭더라도, 이걸 쓰면 쉽게 해결할 수 있겠죠.

void TextInput::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
    states.transform *= getTransform(); //apply transform to render states
    
    auto size = target.getSize();
    sf::RenderTexture texture;
    texture.create(size.x, size.y, sf::ContextSettings(0, 8));
    texture.setSmooth(true);
    
    //draw clipping mask
    Mask::onMaskMode(Mask());
    sf::RectangleShape rect(sf::Vector2f(clipWidth, clipHeight));
    texture.draw(rect);
    
    /draw text with stencil clipping
    Mask::onSourceMode(Source());
    texture.draw(text);
    
    Mask::update();
    sf::Sprite sprite(texture.getTexture());
    auto textureRect = sprite.getTextureRect(); //get texture rect
    
    //texture is y-flipped, so flip it
    textureRect.y = textureRect.height;
    textureRect.height = -textureRect.height;
    sprite.setTextureRect(textureRect);
    
    target.draw(sprite, states);
}

 

texture에서 setSmooth를 사용해야 계단 현상이 발생하지 않습니다.

저는 이걸 몰라서 굉장한 삽질을 해야했습니다.

 

이러고 보니, 사실 stencil test는 필요하지 않다는 걸 알게 되었습니다.

어차피 transform을 나중에 적용할 거라면, 여기서는 그냥 scissor로 clipping을 해도 되는 것이며

더 나아가서 그냥 texture의 size를 clipping size에 맞게 잘라버리면 되는 거 아닌가???

그렇게 이 과정의 의미도 잃은 채로 다시 코드를 작성했습니다.

 

void TextInput::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
    states.transform *= getTransform(); //apply transform to render states

    sf::RenderTexture texture;
    texture.create(clipWidth, clipHeight);
    texture.setSmooth(true);

    Mask::off(); //turn off the stencil mask to draw original text
    texture.draw(text);
    
    Mask::update();
    sf::Sprite sprite(texture.getTexture());
    auto textureRect = sprite.getTextureRect(); //get texture rect
    
    //texture is y-flipped, so flip it
    textureRect.y = textureRect.height;
    textureRect.height = -textureRect.height;
    sprite.setTextureRect(textureRect);
    
    target.draw(sprite, states);
}

 

이 상태에서 테스트하다보니 또!!! 다른 문제를 발견했습니다.

이렇게 그린 Sprite는 clipping rect와 같은 크기의 Texture를 그린 것이기 때문에,

이를 Mask로 사용하게 되면 투명한 영역도 포함한 사각형 영역에 대해 stencil buffer를 작성하는 것이었습니다.

text가 아니라 texture의 rect에 마스크가 적용

사실, 굳이 이러한 Texture가 아니더라도, Text는 그리는 glpyh를 texture로 사용하기 때문에 눈에 보이는 글자대로가 아니라, 그 글자가 포함된 사각형을 mask로 사용합니다.

마찬가지로 일반적인 Sprite도 그리는 Texture의 사각형대로 마스크를 적용하겠죠.

 

이런 탓에 어디까지 갔느냐? openGL 차원에서 Framebuffer를 새로 만드는 방법까지 갔다 왔습니다.

결론만 말하자면, 결국 Texture를 사용해서 그리는 건 똑같기 때문에 이 문제를 해결하려면 추가적으로 다른 조치가 필요합니다.

 

그래도 아까우니 Framebuffer를 사용해서 그리는 과정을 다뤄봅시다.

일단 SFML에서 openGL을 사용하고 있기 때문에, 함부로 openGL의 API를 호출하면 코드가 꼬이게 됩니다.

따라서 resetGLStates 등을 이용해서 내부적으로 꼬이지 않도록 세팅해줘야 됩니다.

 

또한 Framebuffer 생성은 SFML에서 제공하는 OpenGL.hpp의 함수들만으로는 불가능합니다.

SFML의 github에서는 extension으로 glad를 불러와 사용하기 때문에, 그리고 glad는 환경에 따라 달리 생성해서 쓰는 라이브러리이기 때문에 저는 SFML github의 glad를 그대로 include해서 쓰기로 했습니다.

 

Framebuffer에 그래픽을 그리기 위해서는, Framebuffer와 그에 연결해 사용할 Texture, Renderbuffer를 생성해 연결해야 합니다. 그래서 이걸 처리해줄 클래스 및 함수를 구현했습니다.

 

//GLBufferEXT.hpp

class GLBufferEXT
{
    unsigned int fb;
    unsigned int rb;
    unsigned int texture;
    int prevBuffer, prevTexture, prevRender;
    unsigned int width, height;

public:
    GLBufferEXT(unsigned int width, unsigned int height);
    ~GLBufferEXT();

    unsigned int getFBO() const;
    unsigned int getRBO() const;
    unsigned int getTexImage2D() const;

};

//GLBufferEXT.cpp

#include "GLBufferEXT.h"
#include <glad/gl.h>

GLBufferEXT::GLBufferEXT(unsigned int width, unsigned int height) : width(width), height(height)
{
    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevBuffer);
    glGetIntegerv(GL_TEXTURE_BINDING_2D, &prevTexture);
    glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRender);

    glGenFramebuffers(1, &fb);
    glBindFramebuffer(GL_FRAMEBUFFER, fb);

    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

    glGenRenderbuffers(1, &rb);
    glBindRenderbuffer(GL_RENDERBUFFER, rb);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb);
}

GLBufferEXT::~GLBufferEXT()
{
    glBindFramebuffer(GL_FRAMEBUFFER, prevBuffer);
    glBindTexture(GL_TEXTURE_2D, prevTexture);
    glBindRenderbuffer(GL_RENDERBUFFER, prevRender);

    glDeleteRenderbuffers(1, &rb);
    glDeleteFramebuffers(1, &fb);
    glDeleteTextures(1, &texture);
}

GLuint GLBufferEXT::getFBO() const
{
    return fb;
}

GLuint GLBufferEXT::getRBO() const
{
    return rb;
}

GLuint GLBufferEXT::getTexImage2D() const
{
    return texture;
}

 

openGL의 API는 중복으로 불러오면 에러가 발생하기 때문에,

헤더에서 불러오다가 꼬이지 않게 cpp 파일에서 불러오게끔 작성했습니다.

내용은 간단히 요약해서 이전에 연결된 Framebuffer나 Texture 등의 ID를 저장하고 새로운 버퍼를 생성해서 연결합니다.

새로운 버퍼 생성 후에 이전의 버퍼에 다시 연결하는 작업은 수행하지 않게 했습니다.

 

이걸 구현하면서 실험해본 버퍼에 그리는 방법은 두 가지입니다.

첫째는 윈도우 사이즈와 같은 크기의 버퍼를 만들고, 그 위에 transform을 적용한 채로 그린 다음

그대로 픽셀을 복사해오는 겁니다.

둘째는 RenderTexture처럼 clipping rect와 같은 크기로 만들고, 그 위에 original 그대로 그린 다음

transform을 적용해서 그리는 방법입니다.

 

첫 번째 방법이 간편하고 쉬운 만큼, 퍼포먼스가 굉장히 안 좋습니다.

문제는 두 번째 방법을 구현하는 게 굉장히 어려웠다는 점이겠군요.

 

첫 번째 방법은 다음과 같습니다.

auto size = target.getSize();

target.resetGLStates();

GLint previousBuffer;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &previousBuffer);

GLBufferEXT buffer(size.x, size.y);

if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
    Debug::warn("Failed to create the Framebuffer.");
    Mask::update();

    glBindFramebuffer(GL_FRAMEBUFFER, previousBuffer);
    target.draw(text, states);
}
else
{
    Utils::Mask::onMaskMode(); //temporal masks. did not push in stack.
    sf::RectangleShape shape(sf::Vector2f(clipWidth, clipHeight));
    target.draw(shape, states);

    Utils::Mask::onSourceMode();
    target.draw(text, states);

    GLubyte* pixels = new GLubyte[size.x * size.y * 4];
    glReadPixels(0, 0, size.x, size.y, GL_RGBA, GL_UNSIGNED_BYTE, pixels);

    glBindFramebuffer(GL_FRAMEBUFFER, previousBuffer);

    Mask::update();

    glDrawPixels(size.x, size.y, GL_RGBA, GL_UNSIGNED_BYTE, pixels);

    delete[] pixels;
}

 

중간의 if문에서 보이듯이, Framebuffer 생성에 실패할 수도 있기 때문에 이에 관해서 확인해줄 필요가 있습니다.

 

두 번째 방법은 다음과 같습니다.

auto size = target.getDefaultView().getSize(); //original window size
auto size2 = target.getSize(); //current window size (scaled window size)
sf::Vector2f renderSize = sf::Vector2f(clipWidth, clipHeight);

target.resetGLStates();

GLBufferEXT buffer(renderSize.x, renderSize.y);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
    ...
}
else
{
    float yfactor = (size2.y - renderSize.y) * (size.y / size2.y); //y flip space value

    //set temporary view for default drawing
    sf::View temp = target.getView();
    sf::RenderStates tt(sf::RenderStates::Default);
    target.setView(target.getDefaultView());
    tt.transform.translate(0, yfactor);
    tt.transform.scale(sf::Vector2f(size.x / size2.x, size.y / size2.y));

    //turn off stencils
    Mask::off();

    target.draw(text_to_render, tt);
    target.setView(temp); //revert the view

    glBindFramebuffer(GL_FRAMEBUFFER, previousBuffer);

    //set projection matrix
    glMatrixMode(GL_PROJECTION);
    glLoadMatrixf(target.getView().getTransform().getMatrix());

    //set texture
    glBindTexture(GL_TEXTURE_2D, buffer.getTexImage2D());
    glMatrixMode(GL_TEXTURE);

    glLoadIdentity();

    //set transform
    glMatrixMode(GL_MODELVIEW);
    glLoadMatrixf(states.transform.getMatrix());

    float vertices[] = {
        renderSize.x / 2, renderSize.y / 2,
        0, 0,
        renderSize.x, 0,
        renderSize.x, renderSize.y,
        0, renderSize.y,
        0, 0,
    };

    float colors[] = {
        1, 1, 1, 1,
        1, 1, 1, 1,
        1, 1, 1, 1,
        1, 1, 1, 1,
        1, 1, 1, 1,
        1, 1, 1, 1
    };

    float texCoords[] = {
        0.5, 0.5,
        0, 1,
        1, 1,
        1, 0,
        0, 0,
        0, 1
    };

    //restore settings
    Mask::update();

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glVertexPointer(2, GL_FLOAT, 0, vertices);
    glColorPointer(4, GL_FLOAT, 0, colors);
    glTexCoordPointer(2, GL_FLOAT, 0, texCoords);

    glDrawArrays(GL_TRIANGLE_FAN, 0, 6);
}

 

굉장히... 복잡합니다.

resetGLStates로 openGL에 관련된 걸 대부분 초기화했기 때문에,

openGL의 API를 사용할 때는 일일이 다시 지정해줘야 됩니다.

더군다나 draw 자체는 SFML을 경유해서 호출해야 하기 때문에 좌표계도 y축 기준으로 반대이고, view 또한 그에 맞춰서 보정해줘야 됩니다. 그러니 그냥 이 방법은 안 쓰는 게 낫습니다.

 

그래도 삽질을 했습니다. 결과물이 안 깔끔한 게 열받으니까요.

SFML에서 draw하는 그래픽의 좌표계와 openGL의 Framebuffer의 좌표계가 달랐기 때문에 보정이 필요했습니다.

그림으로 표현하면 다음과 같은 상황이었습니다.

대충 설명하는 이미지

녹색 영역이 실제 윈도우의 스크린입니다. original window size에 scale이 임의로 적용되어 있고,

이러한 actual screen size는 target.getSize()로 구할 수 있습니다.

original window size는 target.getDefaultView().getSize()로 구할 수 있습니다.

 

Framebuffer의 빨간색 영역과 SFML의 하얀색 영역의 크기는 Texture의 크기로 지정한 clipping rect (=renderSize)가 될 겁니다.

 

문제는 SFML 기준의 coordinate와 Framebuffer의 coordinate 기준이 다르다는 건데,

Framebuffer는 스크린 기준 좌하단에서 시작해서 위/오른쪽으로 좌표가 증가하지만

SFML의 렌더링은 좌상단에서 시작해서 아래/오른쪽으로 좌표가 증가합니다.

 

그래서 Framebuffer의 texture size가 screen size와 같다면 영역이 완전히 겹쳐서 문제가 없어 보이는데,

사이즈가 달라지면 겹치지 않는 영역이 생기게 되므로 의도치 않은 클리핑이 발생하는 등 이상하게 렌더링되는 겁니다.

 

위 그림 기준으로 Drawing Target이 (0, 0)을 기준으로 그려진다고 가정하면 Framebuffer의 영역에는 그려지는 게 아무 것도 없어서 Framebuffer의 texture를 가져와서 그리더라도 아무 것도 보이지 않습니다.

 

이를 위해 계산하는 보정 값이 yfactor입니다.

y값의 차이가 Actual screen height - frame buffer height이므로,

target.getSize().y - clipHeight (=> size2.y - renderSize.y)가 됩니다.

 

그리고 적용되는 transform이 다르다는 것도 문제입니다.

target의 view에 적용되는 transform이 framebuffer에는 적용되어 있지 않은데

그리는 건 target.draw(...)를 호출해서 억지로 그리려 하다보니 그렇습니다.

그래서 scale 보정으로 default view size scale / current window scale (=> size.y / size2.y) 을 곱해줬습니다.

 

여기서 rotation 등의 다른 transform이 적용되는 경우에도 더 달라지는지는... 테스트하지 않았습니다.

아니면 했는데 괜찮아서 까먹었거나... 애초에 그냥 이 삽질 한 게 아까워서 적는거지 추천하는 방법이 아니니까요.

 

하여튼 이렇게 보정하고 view도 default view로 바꿔서 framebuffer에 그리면 original transform으로 texture 위에 그릴 수 있습니다.

이제 이 framebuffer의 texture를 가져와서 screen에다 그려야겠군요.

 

view의 transform은 projection matrix에, 렌더링하는 오브젝트의 transform (states.transform)은 model view의 matrix에 로드해주면 됩니다. SFML이 그렇게 적용해서 그리더라구요.

 

vertices, colors, texCoords는 매핑해서 그릴 좌표, color 채널 값, 텍스트 좌표입니다.

TRIANGLE_FAN으로 그리기 때문에 사각형의 중심점을 0번 좌표로 시작해서 각 꼭짓점을 시계 방향으로 넣어주면 됩니다.

color 채널 [range: 0-1]은 그대로 그릴 것이니 (1,1,1,1)로 넣어주면 되고,

texCoords의 uv 좌표 [range: 0-1] 역시 vertices에 대응하도록 넣어주면 됩니다.

 

 

그래서 이거 안 쓸거면서 왜 이렇게 길게 설명했느냐?

아까 언급한 mask로 사용하는 영역 문제 때문입니다.

결국 원인은 투명한 곳이나 불투명한 곳이나 똑같은 mask로 사용된다는 것입니다.

그러니 이를 필터링할 수 있도록 Alpha test를 사용해주면 문제를 해결할 수 있습니다.

 

그런데 RenderTexture에 그릴 때는 이 alpha test가 작동하지 않습니다.

자세한 원인은 잘 모르겠지만... Texture 내부에서 뭔가 리셋하고 있어서가 아닐까 싶네요.

 

Framebuffer를 사용하는 방법에서는 alpha test가 작동합니다.

...
glEnable(GL_ALPHA_TEST);
glAlphaFunc(GL_GREATER, 0);

...
glDrawArrays(GL_TRIANGLE_FAN, 0, 6);
...

glDisable(GL_ALPHA_TEST);

 

Alpha Test를 이용해 알파 채널 값이 0보다 큰 영역만 그리게 할 수 있습니다...만

alpha test는 사용하기가 곤란합니다. 찾아보니 deprecated 더군요.

 

돌고돌아 RenderTexture로 돌아온 이유입니다.

이 방법이 복잡한 것도 있지만, alpha test를 바라보고 사용하기엔 이미 버려진 방법이었기 때문입니다.

퍼포먼스 비교를 해보니 별 차이도 없더군요! 차라리 성능이라도 더 좋았더라면...

하여튼 alpha test를 사용하고 싶다면, fragment shader에서 discard를 활용해야 합니다.

 

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture1;

void main()
{             
    vec4 texColor = texture(texture1, TexCoords);
    if(texColor.a < 0.1)
        discard;
    FragColor = texColor;
}

 

해당 shader 코드는 Learn OpenGL - Blending에서 가져왔습니다.

 

이렇게 하면 alpha test는 대체할 수 있긴 한데, 원하는 곳에 편하게 끼워넣을 수가 없었습니다.

Shader object에 여러 개의 shader를 집어넣을 수가 없기 때문에

만약 TextInput에 사용자가 원하는 shader를 이미 지정한 상태라면 discard shader로 덮어씌우거나 discard shader를 무시하거나 둘 중 하나만 가능하지 두 개를 동시에 적용할 수가 없게 되는 겁니다.

 

discard shader를 함수로 만들어서 shader가 될 string 문자열에 어떻게 끼워넣을 수는 없을까 했는데

너무 지저분해질 것 같아서, '필요하면 알아서 shader를 그렇게 짜십쇼' 하는 게 최선일 것 같았습니다.

 

그래서 최종적으로는 RenderTexture를 사용한 코드를 다음과 같이 작성했습니다.

//member variable
sf::Text text;
mutable sf::RenderTexture clipTexture;
mutable sf::Sprite clipSprite;

//init texture and sprite when renderSize changes
void TextInput::setRenderSize(sf::Vector2f renderSize)
{
    this->renderSize = renderSize;
    if (renderSize.x >= 0 && renderSize.y >= 0)
    {
        clipTexture.create(renderSize.x, renderSize.y);
        clipTexture.setSmooth(true);
        clipSprite.setTexture(clipTexture.getTexture());
        auto rect = clipSprite.getTextureRect();
        rect.y = rect.height;
        rect.height = -rect.height; //flip y side
        clipSprite.setTextureRect(rect);
    }
}

void TextInput::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
    states.transform *= getTransform();

    if (renderSize.x >= 0 && renderSize.y >= 0)
    {
        //save test states and reset (stencil will be updated in Mask::update)
        GLboolean depth = glIsEnabled(GL_DEPTH_TEST);
        GLboolean scissor = glIsEnabled(GL_SCISSOR_TEST);
        int scissorBox[4];

        glGetIntegerv(GL_SCISSOR_BOX, scissorBox);

        //resetGLStates will reset depth, stencil tests but does not disable scissor test. (until 3.0 is released!)
        target.resetGLStates();
        glDisable(GL_SCISSOR_TEST);

        //turn off stencils
        Mask::offMaskSource();

        clipTexture.clear(sf::Color::Transparent);
        clipTexture.draw(text);

        //restore settings
        if (depth) glEnable(GL_DEPTH_TEST);
        if (scissor)
        {
            glEnable(GL_SCISSOR_TEST);
            glScissor(scissorBox[0], scissorBox[1], scissorBox[2], scissorBox[3]);
        }

        Mask::update();
        target.draw(clipSprite, states);
    }
    else
        target.draw(text, states);
}

 

기존 drawing target에서 depth test나 scissor test를 하고 있었을 수 있으므로 관련 데이터를 가져온 뒤에

target에 draw할 때는 복원시켰습니다.

Window의 view 차원에서 clipping 할 때는 scissor test를 사용하게 했어서 필요한 과정이었습니다.

 

draw 함수가 const이므로, draw 함수를 호출할 때마다 함수 내에서 texture와 sprite를 생성해서 그리게 하거나

혹은 member variable에 mutable 키워드를 사용해서 const 함수 내에서도 수정할 수 있게끔 해줘야 합니다.

저는 후자로 구현했고, 두 방법에 있어 성능의 유의미한 차이는 없는 것 같습니다.

 

 

이젠 진짜 게임 만들 수 있겠지...???