TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역] Unity - PBR과 PBR+NPR 캐릭터 렌더링 연구

jplee 2025. 10. 25. 13:26

저자: shader仔

1. NPR 렌더링

NPR은 실제로 PBR의 변형이며, NPR의 조명 모델은 여전히 최종 색상 = 직접광 난반사 + 직접광 정반사 + 환경광 난반사 + 환경광 정반사 공식을 따릅니다. 이 중 환경광 난반사와 환경광 정반사는 약화되어 있으며, 붕괴: 스타레일은 URP의 기본 구면 조화 함수와 반사 프로브만 사용합니다. 이들의 역할은 어두운 곳에서 약간의 밝기를 제공하여 어두운 영역이 완전히 검게 되는 것을 방지하는 것뿐입니다. 따라서 렌더링의 전체 효과는 직접광에 의해 형성됩니다.

원신과 붕괴 시리즈에서 난반사 부분은 NdotL을 기반으로 LightMap의 스타일라이즈된 그림자를 사용하여 NdotL을 리맵핑한 후, 이 NdotL로 Ramp 텍스처를 샘플링합니다. 이 작업은 실제로 사전 통합 SSS의 작업과 매우 유사합니다. 이 문서에서는 (NdotL, Curvature)로 SSS Lut를 샘플링하고, 샘플링 결과인 SSS가 DirectDiffuseColor의 NdotL 부분을 대체하는 것을 언급했습니다. 다음은 샘플링 후 SSS의 효과입니다:

SSS

Ramp

SSS가 실제로 Ramp 텍스처의 효과와 매우 유사하며, 두 가지 모두 수식상으로도 매우 비슷함을 알 수 있습니다.

PBR Lambert 난반사 = Albedo NdotL mainLight.color * mainLight.Shadow

SSS Lambert 난반사 = Albedo SSS mainLight.color * mainLight.Shadow

NPR 난반사 = Albedo Ramp mainLight.color

따라서 Ramp 텍스처와 SSS Lut는 본질적으로 같은 것이며, SSS Lut에서도 가로축 부분을 잘라내어 가로축(즉, NdotL)에서만 1차원 샘플링을 수행하여 샘플링 부담을 줄이는 방법이 있는데, 이는 Ramp의 샘플링 방식과 동일합니다.

Ramp

SSS Lut

미술적인 관점에서도 Ramp를 이해할 수 있습니다. 스타일라이즈된 NdotL만 출력하면 모델의 명암 변화만 나타나는 것을 확인할 수 있습니다. NdotL은 명암처럼 작용하고, Albedo는 고유색과 같습니다. 물체가 빛을 받는 영향 요인은 많지만, 그 중 가장 핵심적인 것은 명암 관계입니다. 사진을 흑백으로 변환해도 여전히 사진 속 물체의 디테일을 알아볼 수 있듯이, 화면을 분석할 때 가장 먼저 착수하는 것은 명암 관계입니다.

또한 Ramp 텍스처는 색상의 정리라고 할 수 있습니다. Ramp 텍스처를 통해 특정 회색도에서 특정 색상을 입힐 수 있으며, 이는 그림의 역과정과 비슷합니다. 예를 들어 직접 그림을 그릴 때 명암 경계선에 색이 있는 전환을 그리거나, 어두운 면의 특정 부분에 밝은 색으로 하이라이트를 주는 것처럼 말입니다. 각 부위의 NdotL(가로축)과 Color(샘플링 결과)를 Ramp 텍스처로 만들 수 있습니다. NdotL은 면과 조명 방향의 위치 관계를 나타내는 것으로 이해할 수 있으며, 위치가 같으면 조명 결과도 같아야 합니다(난반사에만 해당). Ramp 텍스처가 있으면 반대로 NdotL만 알면 원래의 색상을 역추적할 수 있습니다. 즉, 렌더링할 때 피부 산란, 환경광 반사 등 재질과 외부의 상호작용을 고려할 필요가 없습니다. 최종 색상 = Function(NdotL, Ramp)로, NdotL과 Ramp만으로 최종 화면을 결정할 수 있습니다. 실제 프로젝트에서는 매개변수를 설정하고 NdotL을 리맵핑하여 이진화된 그림자를 구현하거나, 그림자 텍스처를 그려 NdotL을 리맵핑할 수 있습니다. Ramp의 경우 어두운 면을 약간 밝게 하여 피부 차표면 산란을 모방할 수 있습니다.

실제 Ramp 텍스처에는 많은 세로 열이 있으며, 다른 행은 낮과 밤의 서로 다른 의상과 피부를 나타냅니다. 동시에 HDRP에는 SSS Profile 설정도 있습니다. 개인적으로는 서로 다른 재질 표면의 차표면 산란 효과가 다르고, 광원의 색상과 강도도 산란 결과에 영향을 미치므로 낮과 밤, 그리고 서로 다른 재질을 구분해야 한다고 생각합니다.

정리하자면, NPR 난반사 부분의 핵심은 NdotL과 Ramp이며, 많은 다양한 스타일의 NPR 렌더링은 본질적으로 NdotL과 Ramp를 활용하는 것입니다.

두 번째 부분은 직접 하이라이트입니다. 간단히 말해 Blinn-Phong으로 하이라이트를 만들고, Mask에서 금속 부분과 하이라이트 강도를 지정하며, 비금속 부분에도 약한 하이라이트를 추가합니다.

추가적인 부분으로는 외곽선 효과와 외곽선 색상, 윤곽광, SDF 얼굴 그림자, 앞머리 그림자, 머리카락을 통해 보이는 눈이나 눈썹이 있습니다.

외곽선: 원신의 방식은 법선 외부 확장이며, 정점 색상의 알파 채널에 확장 강도를 저장하고, 외곽선 색상은 Ramp 텍스처를 샘플링합니다.

윤곽광: 프레넬을 사용하거나 스크린 공간 가장자리 광을 사용합니다.

SDF 얼굴 그림자: 관련 문서가 많이 있습니다.

머리카락을 통해 보이는 눈이나 눈썹: 스텐실 테스트를 사용했으며, 관련 문서가 많이 있습니다.

앞머리 그림자: 머리카락 깊이를 기록하고, 해당 깊이 맵을 오프셋 샘플링하여 얻은 깊이와 얼굴의 깊이를 비교합니다.

2. PBR+NPR 렌더링

실제 렌더링은 다음 문서를 참고하여 진행하였으며, 그 과정을 기록하고 정리해보겠습니다.

// Property prepare
half emission                 = 1 - mainTex.a;
half metallic                 = lerp(0, _Metallic, pbrMask.r);
half smoothness               = lerp(0, _Smoothness, pbrMask.g);
half occlusion                = lerp(1 - _Occlusion, 1, pbrMask.b);
half directOcclusion          = lerp(1 - _DirectOcclusion, 1, pbrMask.b);
half3 albedo = mainTex.rgb * _BaseColor.rgb;
// NPR diffuse
float shadowArea = sigmoid(1 - halfLambert, _ShadowOffset, _ShadowSmooth * 10) * _ShadowStrength;
half3 shadowRamp = lerp(1, _ShadowColor.rgb, shadowArea);
//Remap NdotL for PBR Spec
half NdotLRemap = 1 - shadowArea;
#if _SHADOW_RAMP
    shadowRamp = SampleDirectShadowRamp(TEXTURE2D_ARGS(_ShadowRampTex, sampler_ShadowRampTex), NdotLRemap);
#endif
    
// NdotV modify fresnel

// ilmShadow
shadowRamp.rgb = lerp(_SecShadowColor.rgb, shadowRamp.rgb, ilmAO);

이 부분의 역할은 다음과 같습니다:

  • PBR Mask의 각 채널 값을 조정합니다
  • sigmoid로 리맵핑된 Half NdotL을 계산하고, 결과를 사용하여 Ramp를 샘플링하여 shadowRamp를 얻습니다
  • ilmAO가 존재하면 lerp를 사용하여 ilmAO를 혼합하며, AO 색상은 사용자 정의할 수 있습니다

개인적인 의견:

  • ilmAO는 shadowArea에 곱하는 방식이 더 적합하다고 생각합니다. lerp로 혼합하는 방식은 그다지 올바르지 않은 것 같습니다. 부위마다 AO 색상이 다를 수 있으며, AO는 단순히 검은색이어서는 안 됩니다. _SecShadowColor를 사용하면 AO 색상이 하나만 사용될 수 있으므로, AO 색상은 Ramp가 담당해야 하며 ilmAO는 shadowArea에 혼합되어야 합니다
  • ilm에 스타일라이즈된 그림자가 있는 경우, sigmoid를 사용할 때 스타일라이즈된 그림자도 포함해야 합니다

// Direct
float3 directDiffColor = albedo.rgb;
float perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(smoothness);
float roughness           = max(PerceptualRoughnessToRoughness(perceptualRoughness), HALF_MIN_SQRT);
float roughnessSquare     = max(roughness * roughness, HALF_MIN);
float3 F0 = lerp(0.04, albedo, metallic);
float NDF = DistributionGGX(NdotH, roughnessSquare);
float G = GeometrySmith(NdotLRemap, NdotV, pow(roughness + 1.0, 2.0) / 8.0);
float3 F = fresnelSchlick(HdotV, F0);

// GGX specArea remap
NDF = NDF * ilmSpecMask;
float3 kSpec = F;
// LightUpDiff: (1.0 - F) => (1.0 - F) * 0.5 + 0.5
float3 kDiff = ((1.0 - F) * 0.5 + 0.5) * (1.0 - metallic);
float3 nom = NDF * G * F;
float3 denom = 4.0 * NdotV * NdotLRemap + 0.0001;
float3 BRDFSpec = nom / denom;
directDiffColor = kDiff * albedo;
float3 directSpecColor = BRDFSpec * PI;
#if _SHADOW_RAMP
    float specRange= saturate(NDF * G / denom.x);
    half4 specRampCol = SampleDirectSpecularRamp(TEXTURE2D_ARGS(_ShadowRampTex, sampler_ShadowRampTex), specRange);
    directSpecColor = clamp(specRampCol.rgb * 3 + BRDFSpec * PI / F, 0, 10) * F * shadowRamp;
#endif
    // Compose direct lighting
    float3 directLightResult = (directDiffColor * shadowRamp + directSpecColor * NdotLRemap)
                              * mainLight.color * mainLight.shadowAttenuation * directOcclusion;
  • albedo를 directDiffColor로 사용합니다
  • directDiffColor 부분은 KDiff albedo shadowRamp mainLight.color mainLight.shadowAttenuation * directOcclusion입니다

정리하자면, 특별한 부분은 directOcclusion일 것입니다. 여기서 directOcclusion은 PBR Mask의 occlusion을 의미하는 것으로 보입니다. 개인적으로 완전히 PBR을 따른다면 occlusion은 이 위치에 곱해서는 안 되며, AO는 환경광 부분에 영향을 주어야 하고 직접광에는 영향을 주지 않아야 합니다. 여기에 AO를 곱하면 주광원으로 비출 수 없는 부분이 반드시 존재하게 됩니다. NPR 관점에서 보면(NPR 렌더링 참조, ilm에는 스타일라이즈된 그림자와 ilmAO가 포함되어 있으며, 이 두 값은 smoothStep에 참여하여 Half NdotL에 영향을 줌) AO가 직접광에 작용하는 것은 문제가 없지만, 여기에 occlusion을 곱하는 것도 NPR에 완전히 부합하지는 않습니다. 일반적인 NPR은 명암 관계를 계산해야 하며, AO를 추가하려면 명암 관계에 AO를 혼합해야 하고, 색상 적용은 Ramp가 담당해야 합니다. AO를 외부에 곱하는 것은 색상을 적용한 후 다시 색상을 어둡게 하는 것과 같습니다. 개인적으로는 이것이 워크플로우에 도움이 되지 않는다고 생각하며, AO 부분의 색상 적용은 Ramp의 어두운 부분과 PBR Mask에 동시에 의존하게 됩니다. 따라서 저는 여기에 directOcclusion을 곱하지 않았습니다.

  • 먼저 D, G, F를 계산합니다
  • 정상적으로 BRDFSpec을 계산하고, 동시에 NDF에 ilmSpecMask를 곱합니다(있는 경우, 하이라이트 마스크를 의미)
  • saturate(NDF * G / denom.x)로 SpecRamp를 샘플링하고, 샘플링 결과를 원래 하이라이트 항과 혼합하며 clamp로 제한합니다. BRDFSpec에서 F로 나눴기 때문에 외부에서 다시 F를 곱하고, UNITY_PI를 곱하여 에너지 보상을 합니다. ShadowRamp와 NdotLRemap을 곱합니다

NdotLRemap

ShadowRamp

NdotLRemap은 원래 PBR의 NdotL을 대체하는 것으로 보입니다. 원래 PBR에서 하이라이트 부분에 NdotL을 곱하는 목적은 에너지 감쇠를 하기 위한 것으로 보입니다(확실하지 않음). 그리고 하이라이트는 밝은 면에만 나타나므로 NdotLRemap과 ShadowRamp 어두운 면의 차이는 무시할 수 있습니다. shadowRamp를 다시 곱하는 목적은 하이라이트에 물체의 색상을 혼합하고 싶어서라고 생각할 수밖에 없습니다.

정리하자면, 첫째로 개인적으로는 실제로 NdotLRemap을 곱할 필요가 없다고 생각하며, ShadowRamp만 곱하면 됩니다. 둘째로 개인적으로는 ShadowRamp도 곱해서는 안 된다고 생각합니다. ShadowRamp는 난반사의 Ramp이며, 이미 하이라이트의 Ramp가 있으므로 난반사의 Ramp를 곱할 필요가 없습니다. 그리고 F 항을 계산할 때 이미 물체의 albedo를 혼합했으므로 ShadowRamp를 곱하는 의미도 크지 않습니다.

float3 F0 = lerp(0.04, albedo, metallic);
float3 F = F_FrenelSchlick(HdotV, F0);

다중 광원 부분은 공식 Ramp 텍스처를 참조하였습니다.

포인트 라이트는 난반사와 정반사 부분을 모두 가지고 있지만, 성능을 절약하기 위해 포인트 라이트의 정반사 부분은 생략할 수 있습니다. 구체적인 방법은 직접광과 동일하게 계산하면 되며, 계산 결과를 최종적으로 FinalLight에 직접 더하면 됩니다.


간접광 부분은 원문에서도 구면 조화 함수와 반사 프로브를 사용하며, SH Color도 색상 혼합을 했고, 반사 프로브는 CubeMap을 혼합했으며 난반사와 정반사의 강도를 조절했습니다. 실제로 작업하는 과정에서 원래 PBR을 직접 사용하면 환경광 효과가 너무 명확하다는 것을 발견했습니다.

조정 전

조정 후

이때서야 CubeMap과 SH Color를 혼합하는 목적을 이해했습니다 — 환경의 영향을 줄이고, 약간 어두운 CubeMap을 사용하는 것입니다. 다음은 Ramp 텍스처 두 번째 행의 문제입니다. 인터넷에서 Ramp 텍스처의 두 번째 행을 어떻게 사용하는지 찾을 수 없었지만, 원문에서는 두 번째 행이 환경광에 사용되며 NdotH to ramp라고만 언급했습니다. 모든 Ramp의 두 번째 행이 동일하므로, 두 번째 행은 일종의 가중치 매개변수로 사용되어야 하며, Ramp의 계조 변화가 매우 명확하므로 샘플링 결과는 반드시 이진화되어야 합니다. 구체적인 방법은 프레임 캡처 방법으로만 알 수 있을 것 같습니다.

NdotH

NdotH로 Ramp 샘플링


나머지 부분은 특수 부위의 렌더링입니다: 얼굴 SDF, 앞머리 그림자, 머리카락을 통해 보이는 눈썹, 머리카락 동적 하이라이트, 다중 광원 외곽선, 다층 눈, 깊이 가장자리 광입니다.


정리하자면, 텍스처 측면에서 소녀전선2는 PBR Mask와 Normal Map만 사용했으며, 다른 문서에서는 Punishing: Gray Raven이 ILM Map도 사용했다고 언급했습니다. 하지만 프로젝트마다 ILM의 용도가 다르며, 목적은 여전히 특정 부분을 스타일라이즈하는 것입니다. 예를 들어 스타일라이즈된 그림자와 하이라이트 마스크 등입니다.

NPR+PBR은 주로 PBR 공식을 따르며, 색상과 관련된 부분에서는 여전히 Ramp로 색상을 제어하고, PBR로 계산된 결과(리맵핑된 NdotL, F 항이 없는 BRDFSpec 등)를 활용하여 Ramp를 샘플링합니다. 이 F 항이 없는 BRDFSpec에 대한 저의 이해는, NdotL처럼 명암 관계를 표현할 수 있다는 것입니다.

F 항이 없는 BRDFSpec

NPR과 마찬가지로 NPR+PBR도 환경광을 약간 약화시키며, Custom CubeMap과 Custom SH Color를 사용하여 환경광의 색상을 제어하고, 또한 두 가지의 강도를 낮췄습니다.

정리

최근 수행한 사실적인 캐릭터 작업을 결합하여 miHoYo의 NPR을 되돌아보고, 소녀전선2를 대표로 하는 NPR+PBR을 완성하면서, NPR이 실제로 PBR의 변형이며 본질적으로는 여전히 PBR 공식을 따르되 원래의 특정 항목을 대체한다는 것을 발견했습니다. NPR을 잘 구현하는 핵심은 여전히 PBR의 관점에서 출발해야 한다는 것입니다.


컴퓨터 그래픽스 전문 용어 레퍼런스

  • NPR (Non-Photorealistic Rendering): 비사실적 렌더링. 사진과 같은 현실성을 추구하는 PBR과 달리 예술적이고 스타일화된 표현을 목표로 하는 렌더링 기법입니다.
  • PBR (Physically Based Rendering): 물리 기반 렌더링. 물리 법칙을 기반으로 빛과 재질의 상호작용을 시뮬레이션하여 사실적인 이미지를 생성하는 렌더링 기법입니다.
  • 구면 조화 함수 (Spherical Harmonics, SH): 구면에서의 함수를 표현하는 수학적 기법으로, 게임에서는 환경광을 효율적으로 표현하는 데 사용됩니다.
  • 반사 프로브 (Reflection Probe): 주변 환경을 캡처하여 물체의 반사를 시뮬레이션하는 데 사용되는 기술입니다.
  • Ramp 텍스처: 특정 값(예: NdotL)을 입력으로 받아 색상을 반환하는 그라디언트 텍스처로, 스타일화된 음영 표현에 사용됩니다.
  • SSS (Subsurface Scattering): 차표면 산란. 빛이 물체 표면을 투과하여 내부에서 산란된 후 다시 나오는 현상으로, 피부나 밀랍 같은 반투명 재질 표현에 사용됩니다.
  • NdotL (Normal dot Light): 표면 법선 벡터와 빛 방향 벡터의 내적. 표면이 얼마나 빛을 직접 받는지를 나타냅니다.
  • NdotH (Normal dot Half): 표면 법선 벡터와 하프 벡터(빛 방향과 시선 방향의 중간 벡터)의 내적. 정반사 계산에 사용됩니다.
  • NdotV (Normal dot View): 표면 법선 벡터와 시선 방향 벡터의 내적. 프레넬 효과 계산 등에 사용됩니다.
  • HdotV (Half dot View): 하프 벡터와 시선 방향 벡터의 내적. 프레넬 근사 계산에 사용됩니다.
  • Lambert 난반사 (Lambert Diffuse): 램버트 반사 모델. 표면에서 모든 방향으로 균일하게 산란되는 빛을 모델링하는 가장 기본적인 난반사 모델입니다.
  • Blinn-Phong: 정반사를 계산하는 조명 모델. Phong 모델을 개선한 버전으로 계산이 더 효율적입니다.
  • 프레넬 효과 (Fresnel Effect): 시선 각도에 따라 반사율이 달라지는 현상. 얕은 각도에서 볼수록 반사가 강해집니다.
  • ILM Map (Illumination Map): 미리 계산된 조명 정보나 스타일화된 음영 정보를 저장하는 텍스처입니다.
  • PBR Mask: PBR 렌더링에 필요한 여러 속성(Metallic, Smoothness, Occlusion 등)을 채널별로 저장한 텍스처입니다.
  • AO (Ambient Occlusion): 주변 폐색. 주변 지오메트리로 인해 환경광이 차단되는 정도를 나타냅니다.
  • GGX: 마이크로패싯 기반의 정반사 분포 함수(NDF)의 일종으로, 현실적인 정반사를 표현하는 데 널리 사용됩니다.
  • BRDF (Bidirectional Reflectance Distribution Function): 양방향 반사율 분포 함수. 특정 방향에서 들어온 빛이 다른 특정 방향으로 반사되는 비율을 나타냅니다.
  • Albedo: 고유색. 재질이 빛을 받지 않았을 때의 본래 색상입니다.
  • Metallic: 금속성. 재질이 얼마나 금속적인지를 나타내는 값입니다.
  • Smoothness/Roughness: 매끄러움/거칠기. 표면이 얼마나 매끄러운지(또는 거친지)를 나타내는 값으로, 정반사의 선명도에 영향을 줍니다.
  • SDF (Signed Distance Field): 부호화된 거리장. 특정 지점에서 표면까지의 최단 거리를 저장하는 데이터 구조로, 얼굴 음영 등에 사용됩니다.

원문

https://zhuanlan.zhihu.com/p/1962657012576937432