WinCNT

URP로 Toon Shader 만들기 -Rim Light 2- 본문

Unity/URP or Shader 관련

URP로 Toon Shader 만들기 -Rim Light 2-

WinCNT_SSS 2023. 5. 26. 13:05

URP에 대해서 공부하면서 알게 된 것을 정리하는 페이지입니다


서론

저번에 이어서, 이번에는 Rim Light의 실제 구현에 대해서 정리하고자 한다


림 라이트의 경계선

카툰 렌더링의 림 라이트는 스펙큘러와 마찬가지로, 림 라이트의 경계선이 뚜렷한가 아닌가의 2가지 종류가 존재한다

쓰임새는 경우에 따라 다르겠지만 일단 경계선이 뚜렷하지 않은 쪽을 디폴트로 구현해봤다

(근처에 계신 애니메이터 분의 의견으로는 왼쪽이 좀 더 잘 쓸 것 같다고 하시긴 함)


Lim Light 구현하기

사실 카툰 렌더링에서도 림 라이트의 구현 원리는 비슷하다

(경계선이 뚜렷하지 않는 경우면 거의 똑같다)

즉, 시선 벡터와 노멀 벡터를 이용해서 오브젝트의 가장자리를 검출하는 건 똑같다!

 

이번에 구현해본 Lim Light의 프로퍼티는 다음과 같다

Properties
{   
    [Space(10)][Header(Rim Light)]
    _RimColor ("Rim Color", Color) = (1,1,1,1)
    _RimPower ("Rim Power", Range(0, 1.0)) = 0.1
    _RimThreshold ("Rim Threshold", Range(0.0001, 1)) = 0.0001
    
    _RimHorizonOffset ("Rim Horizon Offset", Range(-1, 1)) = 0
    _RimVerticalOffset ("Rim Vertical Offset", Range(-1, 1)) = 0
}

 

이번에 구현해본 Lim Light의 프래그먼트 셰이더의 기본 골자는 다음과 같다

(버텍스 셰이더는 생략했음)

half4 frag(Varyings IN) : SV_Target
{
  // 기타 등등의 프로퍼티(예제라서 일단 프래그먼트 셰이더에서 선언)
	// Rim Light를 사용할지에 대한 여부
  const bool _UseRim = true;
	// Rim Color에 Main LightColor를 반영할지에 대한 플래그
  const bool _UseMainLightColorForRim = false;
  // Rim Light의 마스크 맵(마스크 맵이 없으니 대충 설정)
  const float3  _RimMaskMap = { 1.0, 1.0, 1.0 };
  // Rim Light의 경계를 뚜렷히 하냐 마냐의 여부
  // 0 : 흐리, 1 : 뚜렷히
  const float _RimFeatherOff = 0.0f;

	// 시선 벡터
	float3 _RimViewFix = IN.viewDir;
              
	// Rim Color를 조정
  _RimColor.rgb = lerp(_RimColor.rgb, _RimColor.rgb * _MainLight.color.rgb, _UseMainLightColorForRim);
                
  // (1.0 - Normal와 View를 내적한 값)으로 윤곽선 부분을 추출
	float _RimDot = saturate(1.0 - dot(IN.normal, _RimViewFix));
	// Rim Light의 Power를 조정한다(매직 넘버 사용...)
	_RimPower = pow(abs(_RimDot), exp2(lerp(3.0, 0.0, _RimPower)));
	// Rim Light의 마스크 맵을 적용
	float _RimInsideMask = saturate(lerp( (0.0 + ( (_RimPower - _RimThreshold) * (1.0 - 0.0) ) / (1.0 - _RimThreshold)), step(_RimThreshold, _RimPower), _RimFeatherOff ));
	
	// Rim Light의 역치를 조정한다(RimThreshold가 1이면 림 라이트를 끈다)
	_RimInsideMask = lerp(_RimInsideMask, 0.0, step(1.0, _RimThreshold));

	// Rim Light가 Half-Lambert에 의해 마스크되도록 조정한다
  // (즉 음영이 강해질수록 림 라이트가 약해지게 조정)
	const half3 _LightDirMaskOnForRim = _RimColor.rgb * saturate(_RimInsideMask - ((1.0 - _HalfLambert)));
	
	// 림 라이트의 마스트 맵을 적용(그런데 없으므로 대충 하는 척)
	float3 _FinalRim = _RimMaskMap.g * _LightDirMaskOnForRim;
	
	// Rim Light를 반영
	_FinalColor.rgb = lerp(_FinalColor.rgb, (_FinalColor.rgb + _FinalRim.rgb), _UseRim);
}

실제 구현에서 필요한 부분만 가져온 거라 어딘가에서 에러가 발생할 수는 있지만, 기본 골자는 변함이 없다

정리하자면 일반적인 림 라이트와 똑같이 계산하고 그 다음을 Feather Off 플래그나 Rim Threshold로 아티스트의 의도대로 카툰 렌더링의 느낌을 내는 느낌이다

 

예를 들어, 림 라이트와 램버트의 관계에 대해서 필자는 크게 신경 쓰지 않았지만, 아티스트에게는 중요한 부분이라 여겨지는 것 같았다

// Rim Light가 Half-Lambert에 의해 마스크되도록 조정한다
// (즉 음영이 강해질수록 림 라이트가 약해지게 조정)
const half3 _LightDirMaskOnForRim = _RimColor.rgb * saturate(_RimInsideMask - ((1.0 - _HalfLambert)));

시선 벡터의 Offset

카툰 렌더링이 물리적 정합성보다 아트적 정합성(?)을 추구하기도 하고, Lim Light도 어느 정도는 그러한 경향이 없진 않기 때문에, 경우에 따라서는 프로퍼티로 림 라이트를 컨트롤할 수 있게 하는 것도 필요하다

 

다음의 참고 사이트를 이용해서 추가해봤다

LC_ilmlp博客-LC_ilmlp专栏文章-文集-哔哩哔哩视频

 

LC_ilmlp博客-LC_ilmlp专栏文章-文集-哔哩哔哩视频

 

space.bilibili.com

 

아래는 추가한 소스 코드

half4 frag(Varyings IN) : SV_Target
{
	// 생략...

	// 시선 벡터
	float3 _RimViewFix = IN.viewDir;
	// 림 라이트의 각도 조정
	float3 _HorizonBias = UNITY_MATRIX_V[0].xyz;
	float3 _VerticalBias = UNITY_MATRIX_V[1].xyz;
	_RimViewFix = -_RimHorizonOffset  * _HorizonBias  + (1 - abs(_RimHorizonOffset))  * _RimViewFix;
	_RimViewFix = -_RimVerticalOffset * _VerticalBias + (1 - abs(_RimVerticalOffset)) * _RimViewFix;

  // 생략...
}

다음은 결과물이다

 


결과

이것으로 Lim Light도 구현해보았다

만세!


마무리

이것으로 카툰 렌더링의 3요소인 Three(Two) Tone Shading, Specular, Rim Light를 모두 구현해보았다

사실 구현한지는 조금 오래되었는데, 바빠서 글로 정리하는게 늦어졌다…반성해야지

 

이제와서 생각해보니 URP와는 큰 상관은 없었지만, 아무튼 URP 환경에서 구현했고 SRP Batcher도 잘 작동하니 제목은 그대로 가려고 한다

 

그래도 앞으로는 좀 더 URP와 연관된 글을 작성하도록 노력하고자 한다

아무튼 카툰 렌더링의 기본 특징에 대한 정리는 이제 끝!


참고 사이트

LC_ilmlp博客-LC_ilmlp专栏文章-文集-哔哩哔哩视频

 

LC_ilmlp博客-LC_ilmlp专栏文章-文集-哔哩哔哩视频

 

space.bilibili.com