12
22

외곽선은 명암이나 색조가 확 달라지는 픽셀을 찾은 결과이다.

현재 픽셀을 기준으로 상하 좌우 텍셀을 체크하면서 외곽선인지 판단하면 된다.

이때 컨벌루션(Convolution)이란 것을 사용한다.

컨벌루션 : 블러나 샤프닝 등 효과에 쓰임,
현재 픽셀 기준, 주위 펙셀마다 가중치를 곱해서 결과를
모두 더한 값으로 현재 픽셀의 값을 변경하는 연산이다.

소벨 연산자(sobel operator)의 커널을 이용해 컨벌루션을 해 외곽선을 찾을 수 있다.

소벨 연산자의 3x3 커널

Kx는 좌우연산, Ky는 상하연산을 한다. 결과값은 Lx, Ly이라고 하자.

현재의 픽셀이 0.9라고 가정을 하자.

주변 픽셀들도 0.9로 모두 같은 모습이다.
이러면 외곽선이 생길 일이 없겠죠?
저 소벨 연산자의 커널을 곱해서 다 더한 값은 0이다.
-0.9 + 0.9 + -1.8 + 1.8 + -0.9 + 0.9 = 0

명암차가 클 수록 Lx의 값도 커진다.
또한 왼쪽이 오른쪽보다 크면 값이 작아지고 (음수)
오른쪽이 더 크다면 값이 커진다 (양수).
결국 0이 아니면 외곽선이 있다는 것.

기존 흑백 셰이더에서
이어서 해서 버텍스 셰이더는 생략하겠다.

PS 픽셀 셰이더

픽셀 셰이더 전체 코드


앞에서 이야기한 커널 3x3 행렬을 만들고
for문을 돌리며 주변 픽셀들 마다 Kx, Ky를 곱한다.

2중 for문을 돌리며 Kx,Ky를 곱해서 더한 결과를 Lx Ly에 저장함


픽셀 하나 하나 불러와야하는데 옆 픽셀과 현재 픽셀의 UV좌표 차이를 알아야한다.
UV좌표는 0~1이고, U는 1/텍스쳐 width, V는 1/텍스쳐 height의 차이가 난다.

이건 gPixelOffset 전역변수에

뷰포트 디멘션 인벌스를 시멘틱으로 나누기를 생략함


이렇게 해서 텍스쳐를 불러온다.

주변 픽셀들에 커널을 곱해서 가중치를 구했다면 피타고라스 정리처럼 계산해야한다.
hlsl 함수인 sqrt() 루트 연산 함수를 쓴다. (pow()는 제곱)

Lx와 Ly를 합치려면 피타고라스 정리를 사용함


책에서 그냥 더해도 된다는데 빗변의 길이를 구하는 피타고라스 정리를 사용했다.

luminance는 우리가 흑백셰이더 만들었을때 그 위키피디아에서 인간 색영역에 맞춘 공식이다.

그 결과를 리턴하면 된다.

결과


양각효과

는 위 내용과 비슷하다. 커널을 좀 수정한다.

왼쪽 위 픽셀이 오른쪽 아래 픽셀보다 밝다면 그림자를 드리움


컨벌루션의 결과를 누적하는 res를 리턴한다.

결과

이렇게 외곽선과 엠보스 효과를 적용해 보았다.
책에서는 색상보정, HDR효과, 비네트효과, Dof효과, SSAO 효과, 잔상효과 등
여러 영상처리기법을 소개했다.

책을 보며 따라 구현하며 빛, 그림자, 영상처리기법을
완벽히는 아니여도 흐름을 이해하는 기회도 있었지만,
무엇보다 셰이더와 친해질 수 있는 기회가 되어 좋았다.

좋은 책을 써주신 김포프 교수님께 존경을 표한다.

더보기

//vs

struct VS_INPUT
{
float4 mPosition : POSITION;
float2 mUV : TEXCOORD0;
};

struct VS_OUTPUT
{
float4 mPosition : POSITION;
float2 mUV : TEXCOORD0;
};

VS_OUTPUT vs_main( VS_INPUT Input )
{
VS_OUTPUT Output;

Output.mPosition = Input.mPosition;
Output.mUV= Input.mUV;

return( Output );

}
//ps

struct PS_INPUT
{
float2 mUV : TEXCOORD0;
};

sampler2D SceneSampler;
float2 gPixelOffset;

float3x3 Kx = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
float3x3 Ky = { 1, 2, 1,
0, 0, 0,
-1,-2,-1};

float4 ps_main(PS_INPUT Input) : COLOR
{
float Lx = 0;
float Ly = 0;

for(int y = -1; y<= 1; y++)
{
for(int x = -1; x<=1; x++)
{
float2 offset = float2(x,y)*gPixelOffset;
float3 tex = tex2D(SceneSampler, Input.mUV+offset).rgb;
float luminance = dot(tex, float3(0.3f, 0.59f, 0.11f));

Lx+= luminance * Kx[y+1][x+1];
Ly+= luminance * Ky[y+1][x+1];

}
}

float L = sqrt((Lx*Lx)+(Ly*Ly));
return float4(L.xxx, 1);
}
//vs

struct VS_INPUT
{
float4 mPosition : POSITION;
float2 mUV : TEXCOORD0;
};

struct VS_OUTPUT
{
float4 mPosition : POSITION;
float2 mUV : TEXCOORD0;
};

VS_OUTPUT vs_main( VS_INPUT Input )
{
VS_OUTPUT Output;

Output.mPosition = Input.mPosition;
Output.mUV= Input.mUV;

return( Output );

}
//ps
struct PS_INPUT
{
float2 mUV : TEXCOORD0;
};

sampler2D SceneSampler;
float2 gPixelOffset;

float3x3 K = {-2,-1, 1,
-1, 0, 1,
0, 1, 2};

float4 ps_main(PS_INPUT Input) : COLOR
{
float res = 0;

for(int y = -1; y<= 1; y++)
{
for(int x = -1; x<=1; x++)
{
float2 offset = float2(x,y)*gPixelOffset;
float3 tex = tex2D(SceneSampler, Input.mUV+offset).rgb;
float luminance = dot(tex, float3(0.3f, 0.59f, 0.11f));

res+= luminance * K[y+1][x+1];

}
}

res+=0.1f;
return float4(res.xxx, 1);
}

COMMENT