픽셀 셰이더를 사용하여 Photoshop 스타일의 베벨, 광채 효과를 만듭니다.

이쪽은, Siv3D Advent Calendar 2015 의 17일째의 기사입니다.

게임 제작 등으로 Photoshop에 신세를지고 있다고 하는 분도 많다고 생각합니다만, Photoshop에는 레이어 스타일이라고 하는 화상에 여러가지 이펙트를 붙일 수 있는 기능이 있습니다.


이 이펙트를 프로그램 안에서 취급할 수 있으면 재미 있다고 생각했으므로 일부분만 실장해 보았습니다.

만든 것



베벨





베벨은 각 픽셀에서 가장 가까운 투명 픽셀까지의 거리를 계산한 후 그 거리에 따라 높이를 설정하고 높이와 마우스 위치에서 조명을 계산했습니다. 그리고 뭔가 돌 같은 질감이 되고 있습니다만, 이것은 별로 그러한 것은 아니고 투명 픽셀까지의 거리 계산의 정밀도가 나쁜 것뿐입니다.

광채(내부)





베벨과 거의 같습니다. 투명 픽셀로부터의 거리에 따라 색을 그대로 가산했습니다.

조합하면 이런 느낌





덧붙여서 자신의 노트북이라면 너무 늦어서 거의 움직이지 않았기 때문에, 게임에 짜넣기 위해서는 아직 여러가지 개선이 필요할 것 같습니다.

구현



각 픽셀로부터 투명 픽셀까지의 거리를 알면 9할 완성한 것 같은 것이므로, 이 계산이 제일 중요합니다.

가장 간단한 방법은 각 픽셀로부터 1개씩 인접 픽셀을 봐 가는 방법입니다만, 이것이라고 아마 느리기 때문에 이번 밉맵을 사용한 구현을 시도해 보았습니다.

큰 밉 레벨의 1 픽셀의 색은, 작은 밉 레벨의 각각의 픽셀의 색의 평균이 되어 있으므로, 큰 밉 레벨의 알파치를 조사하면 그 범위 내에 반투명 픽셀이 존재하는지가 판정 수 있습니다. (아마)

따라서 각 픽셀에서 최대 밉맵 레벨에서 하나씩 낮추면서 가까운 픽셀의 알파 값이 1인지 여부를 보면 효율적으로 가장 가까운 반투명 픽셀에 도달할 수 있을까 생각했습니다.

Main.cpp
# include <Siv3D.hpp>

class EffectTexture
{
public:

    struct CB
    {
        Float2 lightDir;
        float lightAltitude;
        float intensity;
    };

    EffectTexture(const Image& image)
        :m_texture(image, TextureDesc::Mipped)
        , m_shader(L"EffectTexture.hlsl")
    {
        m_cb->lightAltitude = 200.0;
        m_cb->intensity = 0.0;
    }

    EffectTexture(const FilePath& filepath)
        : EffectTexture(Image(filepath))
    {}

    EffectTexture()
        : EffectTexture(Image())
    {}

    void update()
    {
        const float speed = Input::KeyShift.pressed ? 0.5 : 0.03;
        if (Input::KeyZ.pressed)
        {
            m_cb->lightAltitude += speed;
        }
        if (Input::KeyX.pressed)
        {
            m_cb->lightAltitude -= speed;
        }

        if (Input::KeyUp.pressed)
        {
            m_cb->intensity += speed;
        }
        if (Input::KeyDown.pressed)
        {
            m_cb->intensity -= speed;
        }

        m_cb->intensity = Clamp(m_cb->intensity, 0.0f, 1.0f);
    }

    void draw(const Vec2& pos = { 0, 0 })
    {
        Graphics2D::BeginShader(m_shader);
        m_cb->lightDir = Mouse::Pos();
        Graphics2D::SetConstant(ShaderStage::Pixel, 1, m_cb);

        m_texture.draw(pos);

        Graphics2D::EndShader();
    }

    void set(const Image& image)
    {
        m_texture = Texture(image, TextureDesc::Mipped);
    }

private:

    Texture m_texture;
    PixelShader m_shader;
    ConstantBuffer<CB> m_cb;
};

void Main()
{
    Graphics::SetBackground(Palette::Black);

    EffectTexture texture(L"Example/siv3D-kun.png");

    /*Font font(128, Typeface::Black);
    Image image(640, 480, Alpha(0));
    font.overwrite(image, L"Siv3D", 50, 50, Palette::Orange);
    texture.set(image);*/

    while (System::Update())
    {
        if (Input::KeyZ.clicked)
        {
            ScreenCapture::BeginGIF();
        }
        if (Input::KeyX.clicked)
        {
            ScreenCapture::EndGIF();
        }

        texture.update();
        texture.draw();

        Circle(Mouse::Pos(), 5).draw();
    }
}

EffectTexture.hlsl
Texture2D texture0 : register( t0 );
SamplerState sampler0 : register( s0 );

struct VS_OUTPUT
{
    float4 position : SV_POSITION;
    float4 color : COLOR0;
    float2 tex : TEXCOORD0;
};

cbuffer CB : register(b1)
{
    float2 lightDir;
    float lightAltitude;
    float intensity;
};

#define BUFFER_SIZE 5

//http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html#sec-SourceCode
float radicalInverse_VdC(uint bits)
{
    bits = (bits << 16u) | (bits >> 16u);
    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
    return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}

float2 hammersley2d(uint i, uint N)
{
    return float2((float(i) + 0.5) / float(N), radicalInverse_VdC(i));
}

void sortNearBuffer(inout float2 nears[BUFFER_SIZE], in float2 pos)
{
    for (int i = 0; i < BUFFER_SIZE - 1; ++i)
    {
        for (int j = BUFFER_SIZE - 1; j > i; --j)
        {
            const float2 previous = nears[j - 1];
            const float2 current = nears[j];
            const float dp2 = dot(previous - pos, previous - pos);
            const float dc2 = dot(current - pos, current - pos);
            if (dc2 < dp2)
            {
                nears[j] = previous;
                nears[j - 1] = current;
            }
        }
    }
}

void tryInsert(inout float2 nears[BUFFER_SIZE], in float2 pos, in float2 newElement)
{
    int left = 0;
    int right = BUFFER_SIZE;
    int mid;

    const float lengthSq = dot(newElement - pos, newElement - pos);

    while (left <= right)
    {
        mid = ((left + right) >> 1);
        const float midLengthSq = dot(nears[mid] - pos, nears[mid] - pos);
        if (midLengthSq < lengthSq)
        {
            left = mid + 1;
        }
        else
        {
            right = mid - 1;
        }
    }

    for (int i = BUFFER_SIZE - 1; mid + 1 <= i; --i)
    {
        nears[i] = nears[i - 1];
    }
    if (mid <= BUFFER_SIZE - 1)
    {
        nears[mid] = newElement;
    }
}

float4 PS(in VS_OUTPUT input) : SV_Target
{
    const float2 uv = input.tex;

    float2 size_;
    float textureNumOfLevels;
    texture0.GetDimensions(0, size_.x, size_.y, textureNumOfLevels);

    float4 srcColor = texture0.SampleLevel(sampler0, uv, 0);

    float2 maxLevelSize;
    texture0.GetDimensions(textureNumOfLevels, maxLevelSize.x, maxLevelSize.y, textureNumOfLevels);
    float4 maxLevelColor = texture0.Load(int3(int2(maxLevelSize*uv), textureNumOfLevels - 1));
    if (maxLevelColor.a == 1)
    {
        return float4(0, 0, 0, 1);
    }

    int currentMip = textureNumOfLevels - 2;

    const int numOfSumples = 10;

    float2 nullUV = float2(-10, -10);
    float2 nearTransperenntPixels[BUFFER_SIZE];
    nearTransperenntPixels[0] = uv;
    for (int j = 1; j < BUFFER_SIZE; ++j)
    {
        nearTransperenntPixels[j] = nullUV;
    }

    float2 posUV[4];
    int indices[4] = { 0, 1, 2, 3 };
    int iterations = 0;

    const float scl = 2.5;
    int d = -3;
    for (; 0 <= currentMip; --currentMip)
    {
        int2 currentSize;
        texture0.GetDimensions(currentMip, currentSize.x, currentSize.y, textureNumOfLevels);

        float2 uvPerPixel = float2(1.0, 1.0) / currentSize;

        for (int i = 0; i < BUFFER_SIZE; ++i)
        {
            if (nearTransperenntPixels[i].x == nullUV.x && nearTransperenntPixels[i].y == nullUV.y)
            {
                continue;
            }

            int2 pixelPos = (nearTransperenntPixels[i] + uvPerPixel*0.5) / uvPerPixel;
            float2 uvCenter = float2(pixelPos) / (currentSize);

            const float alphaCeenter = texture0.SampleLevel(sampler0, uvCenter, currentMip).a;
            if (alphaCeenter == 1.0)
            {
                nearTransperenntPixels[i] = nullUV;
                sortNearBuffer(nearTransperenntPixels, uv);
            }

            for (int j = 0; j < numOfSumples; ++j)
            {
                const float2 dx = (hammersley2d(j, numOfSumples) - float2(0.5, 0.5))*2.0;
                const float2 jitteredUV = uvCenter + dx*uvPerPixel*scl;
                if (texture0.SampleLevel(sampler0, jitteredUV, currentMip + d).a != 1.0)
                {
                    tryInsert(nearTransperenntPixels, uv, jitteredUV);
                }
            }
        }
    }

    sortNearBuffer(nearTransperenntPixels, uv);
    //失敗した時の動作
    if (nearTransperenntPixels[0].x == nullUV.x && nearTransperenntPixels[0].y == nullUV.y)
    {
        //return float4(1, 0, 1, 1);
        nearTransperenntPixels[0] = uv;
    }

    //透明ピクセルへの相対ベクトル
    const float2 rel = nearTransperenntPixels[0] - uv;

    //べベルの高さ
    float height = min(dot(rel, rel)*1000.0, 1.0)*500.0;

    //光彩の強さ
    float light = 1.0 - min(dot(rel, rel)*10000.0 * (1.0 - intensity), 1.0);

    float x = input.position.x;
    float y = input.position.y;

    const float3 tangent = normalize(ddx(float3(x, height, y)));
    const float3 binormal = normalize(ddy(float3(x, height, y)));

    const float3 normal = normalize(cross(tangent, binormal));

    float2 pixelToLight = lightDir - input.position.xy;
    const float3 lightDir = normalize(-float3(pixelToLight.x, lightAltitude, pixelToLight.y));
    const float3 eyeDir = float3(0, -1, 0);
    const float3 halfDir = normalize((lightDir + eyeDir)*0.5);
    const float diffuse = dot(normal, lightDir);

    const float df = 0.5;
    const float sf = 0.5;
    const float shinness = 6.0;

    const float diffuseSpecular = df*dot(normal, lightDir) + sf*(pow(dot(normal, halfDir), shinness));

    const float3 ambient = float3(1, 1, 1)*0.25;
    const float3 lightColor = float3(0, 0.5, 1)*light*intensity;

    return float4(saturate(lightColor + ambient + srcColor.rgb*diffuseSpecular), srcColor.a);
}


반성



・해상도가 2의 누승이 아닌 텍스처라면 밉맵의 해상도가 배로 늘어나가는 구조가 무너지므로 취급하기 어렵다.
・의사 난수를 사용해 어느 정도의 정밀도를 확보할 수 있으면 좋을까 생각했지만 전혀 정밀도가 오르지 않았기 때문에, 뭔가 착각하고 있는지 버그가 있을지도.
·나중에 늦다. numOfSumples와 BUFFER_SIZE를 낮추면 일단 빠르지는 않지만 정밀도가 더욱 희생된다.

내일은 @prince_0203 님의 기사입니다. 잘 부탁드립니다.

좋은 웹페이지 즐겨찾기