일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- URP로 변경
- 개인 바이트
- 가상 바이트
- Rim Light
- C언어
- Toon Shader
- 게임 수학
- VR
- Cell Shader
- ColorGradingLutPass
- Cell Look
- Private Bytes
- 3d
- 벡터
- ASW(Application SpaceWarp)
- Specular
- working set
- 메모리 누수
- Cartoon Rendering
- Three(Two) Tone Shading
- OculusMotionVectorPass
- 작업 집합
- URP
- AppSW
- Windows Build
- Virtual Byte
- 프로그래밍 기초
- Today
- Total
WinCNT
URP에서 Outline 셰이더 만들어보기! 본문
서론
퍼포먼스 문제로 미뤄왔던 Outline…
그래도 일단 넣어보고 안 되면 빼자는 의견에 설득당해 일단 만들기로 했다
참고로 이번에 만들 건 카툰 렌더링 표현을 위한 Outline이 아니다
이번에 작업하면서 알게 되었는데 매우 귀찮게도 Outline의 표현은 2가지가 있으면서 명칭은 하나밖에 없다는 것이었다
최소한 필자는 명확히 구별되는 명칭을 찾지 못 했다
하나는 유니티의 씬 뷰에서 오브젝트 등을 클릭하면 나오는 Outline이다
오브젝트의 내부에는 Outline를 안 그리는 방식이다
인게임에서는 상호작용할 대상 오브젝트나 상태 이상(중독 등)을 나타낼 때도 사용되기도 한다
다른 방식은 툰 셰이딩, 셀 셰이딩 등 NPR의 특징 중 하나인 Outline이다
첫번째 방식과의 차이점은 오브젝트의 내부에도 Outline을 그린다는 점이다
이쪽은 아트 스타일을 위한 방식에 가깝다
기본적으로 인게임의 캐릭터 등에 계속 적용되고 상호 작용 등으로 나오거나 사라지지는 않는다
(연출 등으로 두꺼워지거나 할 수는 있을지도)
이번에 구현할 방식은 바로 첫번째!
오브젝트의 외곽에만 나오는 Outline이다
Outline의 구현 방법
Outline의 구현은 필자가 보기에 크게 다음의 2종류가 존재한다
- 오브젝트의 노멀으로 구현하는 방법
- 포스트 프로세싱으로 구현하는 방법
【Unity】【シェーダ】4種のアウトライン描画方法とその特徴 - LIGHT11
참고 사이트에는 4가지를 소개하고 있는데, 잘 보면 오브젝트의 노멀이냐 포스트 프로세싱이냐의 2종류이다
물론 그 외의 방식도 존재하겠지만 필자에겐 이 정도로 충분했으므로 생략
현 프로젝트는 포스트 프로세싱 사용을 극도로 피하고 있다보니 이번에 구현한 방법은 당연히 오브젝트의 노멀을 이용하는 방법이다
어찌 되었든 오브젝트를 2번 그리는 것이라 무거워지지만 어쩔 수 없지…
우선은 Outline 패스의 코드의 샘플부터
일단은 샘플 코드부터
가장 마지막에 소개할 Stencil을 이용한 Outline 패스의 코드이다
// Outline
Pass
{
Name "Outline"
Tags
{
"RenderType" = "Opaque"
"LightMode" = "Outline"
}
Stencil
{
Ref 1
Comp NotEqual
}
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _OUTLINE_NML _OUTLINE_POS
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
TEXTURE2D(_BumpMap);
SAMPLER(sampler_BumpMap);
TEXTURE2D(_EmissionMask);
CBUFFER_START(UnityPerMaterial)
half _Cutoff;
float4 _BaseMap_ST;
half4 _BaseColor;
float4 _EmissionMask_ST;
half4 _EmissionColor;
// Outline
float _OutlineWidth;
half4 _OutlineColor;
CBUFFER_END
struct Attributes
{
float3 pos : POSITION;
float4 tex_coord: TEXCOORD0;
half3 normal: NORMAL;
half4 tangent: TANGENT;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 pos : SV_POSITION;
float2 tex_coord: TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
Varyings vert(Attributes IN)
{
Varyings OUT;
ZERO_INITIALIZE(Varyings, OUT);
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
float3 normalOS = 0;
#ifdef _OUTLINE_NML
normalOS = IN.normal;
#elif _OUTLINE_POS
normalOS = normalize(IN.pos.xyz); // For Hard Edge
#endif
VertexNormalInputs normalInput = GetVertexNormalInputs(normalOS, IN.tangent);
float3 normal = 0;
// inverse transpose View Matrix
normal = mul(normalInput.normalWS, (float3x3)GetViewToWorldMatrix());
float4 offset = TransformWViewToHClip(normal);
// Outline Offset
OUT.pos = TransformObjectToHClip(IN.pos);
OUT.pos.xy += offset.xy * _OutlineWidth * 0.01;
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(IN);
half4 outColor = half4(0.0, 0.0, 0.0, 0.5);
outColor.rgb = _OutlineColor.rgb;
return outColor;
}
ENDHLSL
}
물론 Stencil 이외의 방식에도 비슷한 코드를 사용했기 때문에 위의 코드를 기준으로 설명하고자 한다
로직은 버텍스의 위치를 노멀 방향으로 옮겨주는 방법이다
여러 공간에서 실험해봤는데 HClip 공간에서 해주는 게 제일 괜찮게 나오는 것 같았다
그리고 잊으면 안 되는 것이 바로 LightMode이다
이건 나중에 URP Asset의 Universal Renderer Data에 LightMode Tag에 설정해야 한다
(샘플 코드에서는 알기 쉽게 Outline로 했다)
이 부분이 이상하면 SRP Batcher가 깨져버린다…
https://wincnt-shim.tistory.com/374
settings…
다음은 아웃라인을 나오게 하기 위한 세팅이다
우선은 태그 설정!
그리고 URP Asset의 Universal Renderer Data에 Render Objects를 추가!
이러면 무려 Outline이 나온다!!…원래의 모델을 덮는 방식으로…!
이건 당연히 버텍스를 노멀 방향으로 이동시킨 다음 한 번 더그리는 것이기 때문이다
샘플 코드를 Cull Front를 해주면 드디어 Outline이 나온다!!!…두번째 방식으로…
(그리고 비록 Hard Edge는 이상할 순 있어도)
Z-Offset
아쉽게도 이번에 필자가 원하는 건 내부에는 아웃라인이 없는 첫번째 방식…
내부에 아웃라인을 없애기 위해 처음에는 Z축으로도 Offset를 줘봤다(UTS에서 아이디어를 얻었음)
// Cull Front로 설정
// Outline Offset
OUT.pos = TransformObjectToHClip(IN.pos);
OUT.pos.xy += offset.xy * _OutlineWidth * 0.01;
// Outline Z-Offset
OUT.pos.z += _OutlineZOffset;
얼핏 보기에는 성공한 것 같았다!
단점
그렇다…얼핏보기에 성공했다는 것은 자세히 보면 이상한 부분이 있다는 것…
단순히 Z축으로 Offset를 주는 것이기 때문에 정면에선 괜찮지만 발과 같은 부분은 Z축의 조정만으로 Outline의 버텍스가 원래의 모델에 가려지지 않기 때문에 여전히 내부에도 Outline이 나온다
어떻게 카메라의 시점 벡터와 공간을 잘 조정하면 될 거 같았는데 잘 안 되서 관뒀다
Render Objects의 Depth Override
다음으로 실험한 것은 Outline 대상의 오브젝트와 Outline을 따로 그리는 방식이었다
우선은 디폴트의 렌더링에서 Outline용 태그를 설정한 오브젝트들을 제외한다
그리고 Outline 패스를 그릴 Render Objects와 Outline 대상 오브젝트를 그릴 Render Objects를 추가한다
아웃라인을 먼저 그리고 오브젝트들을 다음에 그리도록 Event를 설정했다
그리고 중요한 것은 아웃라인의 Render Objects에서 Depth를 체크하고 Write Depth를 체크하지 않는 것!
이렇게 하면 아웃라인은 그려질 때 뎁스 버퍼를 갱신하지 않게 되므로, 그 뒤에 그려질 (아웃라인 대상이 된)오브젝트들에 의해 덮어쓰여지게 된다
최적화 여부는 일단 잊자!!!
프레임 디버거로 순서대로 살펴보자
우선 아웃라인과 관련 없는 오브젝트들을 그린다(Skybox까지)
그 다음 아웃라인을 그린다
이 때 뎁스 버퍼가 갱신되지 않는다
그 다음 오브젝트들을 그린다
아웃라인들은 뎁스 값을 가지지 않으니 오브젝트에 의해 덮어쓰여진다
짜잔~!
아웃라인은 오브젝트에 의해 덮어쓰여지니 당연히 오브젝트 내부에 아웃라인이 나오지 않게 된다!
물론 이 방법도 아쉬운 점은 있다
일단 오브젝트가 겹쳐서 보이게 되면 그 내부에는 아웃라인이 안 그려진다
여러 오브젝트들이 동시에 아웃라인이 생기거나 색이 다르면 의외로 위화감이 느껴졌다…
다행히 이번에는 이 정도의 위화감은 그냥 넘어가기로 했다
Render Objects의 Stencil Override
상술했던 Depth을 Override해서 2번 그리는 방식에는 좀 낭비가 많다
아웃라인의 역할을 하는 오브젝트 위에 실제의 오브젝트를 한 번 더 그리는 방식이기 때문에 Overdraw되는 부분이 발생한다
게다가 아웃라인의 특성상 그리 두꺼운 경우는 많지 않기 때문에 Overdraw되는 부분이 더욱 낭비처럼 느껴진다…
그래서 이번에 소개할 것이 바로 Stencil을 이용한 방식!!
(실제로 오브젝트 외부에만 아웃라인을 그리고 싶을 경우에는 Stencil을 쓰는 것이 정공법인 것 같음)
컴퓨터 그래픽스에서 Stencil이란 간략하게 말해 미술에서의 Stencil처럼 특정 부분에만 렌더링을 하게 해주거나 안 할 수 있게 해주는 기법을 의미한다
원리에 대해서도 간략하게 설명하자면 화면 크기의 Stencil Buffer라는 것을 준비하고, 특정 패스나 오브젝트가 그려질 때 특정 값으로 갱신한다
(일반적으로 디폴트가 0이며 1~255 사이의 값으로 갱신함)
그리고 그 다음 특정 패스나 오브젝트가 그려질 때, Stencil Buffer의 값을 확인하고 그릴지 안 그릴지를 판단(Stencil Test)하는 방식이다
Stencil은 거울이나 매직 카드, 포털 등의 렌더링에 자주 사용된다
필자도 이번 구현에 앞서 Stencil로 매직 카드를 간단히 구현해보기도 했다
https://wincnt-shim.tistory.com/404
아무튼 이번에는 Stencil로 오브젝트의 내부에는 아웃라인을 그리지 않고 외부인 경우에만 그리도록 하는 데 사용해봤다
로직을 대략적으로 설명하자면, 우선 아웃라인의 대상이 되는 오브젝트를 그릴 때 Stencil Buffer를 1로 갱신한다
그 후, 아웃라인을 그릴 때 Stencil Buffer를 1이 아닌 곳에만 그린다
이미지로 나타내면 다음과 같다
검정색 원은 아웃라인의 대상이 되는 오브젝트이며, 빨간색 원은 아웃라인이다
이러한 방식이면 아웃라인의 대상이 되는 오브젝트에는 아웃라인이 그려지지 않고 아웃라인을 그려지므로 성능상으로 더 이점이 있을 것이다!
이론적인 이야기는 여기까지 하고 실제로 Render Objects의 설정을 어떻게 했는지에 대해서 알아보자
대부분은 Depth 때와 비슷하지만 Stencil을 체크하고 값을 설정한 부분이 다르다
순서로는 아웃라인이 위쪽에 있지만, 유니티에서는 불투명 오브젝트가 스카이박스 전에 그려지기 때문에, 위와 같이 설정해도 아웃라인의 대상이 되는 오브젝트 → 아웃라인 순으로 렌더링된다
참고로 샘플 코드처럼 Stencil 설정을 셰이더에 직접 작성할 수도 있다(사실 이게 원래 방법)
// Outline
Stencil
{
Ref 1
Comp NotEqual
}
위와 같이 설정하고 실제로 렌더링이 어떻게 되는지 살펴보자
먼저 스카이박스가 그려지기 전에 아웃라인의 대상이 되는 오브젝트가 그려진다
그 뒤에 스카이박스가 그려지고, 아웃라인이 렌더링되는 것을 볼 수 있다!
물론 Depth의 방식과 같이 내부에 아웃라인은 그려지지 않았다
사족) 유니티의 씬 뷰의 아웃라인은 어떤 방식일까?
결론부터 말하자면 RenderDoc으로 확인해본 결과 포스트 프로세싱의 방식을 쓰는 것 같았다
셰이더도 어셈블리어로는 볼 수 있지만 지금은 딱히 필요 없으니 생략!
마무리
생각 외로 여러가지로 고생한 작업이었다
역시 포스트 프로세싱을 마음껏 쓰고 싶다…제한이 너무 많아 흑흑
참고 사이트
【Unity】【シェーダ】4種のアウトライン描画方法とその特徴 - LIGHT11
The Quest for Very Wide Outlines