Subsurface Profile 셰이딩 모델은 Subsurface 와 그 방식이 비슷하지만, Screen Space에서 Scattering 처리를 수행한다는 점이 다르다. Screen Space 렌더링은 BackScattering은 귀 같은 부분에서만 나타나고, 전체적으로 미묘한 Scattering을 보이는 피부 같은 질감을 더 효과적으로 표현할 수 있는 방법이다.
대략적인 과정은 아래와 같다.
Specular 와 같은 view-dependent 한 라이팅 계산을 제외한 Diffuse 연산 결과물을 렌더타겟이 저장한다.
결과물을 퍼포먼스를 위해 DownSample 하고, 두 개의 post-process pass 로 필터링한다. 이때 사용되는 kernel은 G-buffer 에 저장된 Subsurface Profile 정보에 따라 달라지는데, 여기에는 색상이 있는 가중치들과 Sample position 상세가 들어 있으며, Profile 에 정의된 Scale 정보에 따라 스케일링된다. 한 장면당 총 255 개의 subsurface profile 을 사용할 수 있다.
필터링 된 결과를 full-resolution 결과물에 블렌딩한다.
View dependent 한 라이팅 결과물과 non-view dependent 라이팅 결과를 분리할 때, Scene Color 의 알파 채널을 사용하기 때문에 64-bit 렌더러를 사용해야 한다. (이것은 r.SceneColorFormat으로 확인할 수 있다)
non-SSS 머터리얼이 SSS Material를 가릴 때, 주변에 회색 그래디언트가 생긴다.
여기까지가 Unreal Docs에 적힌 내용을 일부 요약해 본 것이다. 아래부터는 직접 작업하고 검색하면서 알게 된 내용들을 기록해 둔다.
Subsurface Profile
Subsurface Profile 은 Subsurface Profile 셰이딩 모델에서 사용하는, 표면의 scattering 에 관한 정보의 모음이다. 이 정보를 토대로 GBuffer에 각 Profile 마다 SubsurfaceProfileTexture를 저장해 두었다가. 이후 Screen Space 처리에서 사용하게 된다.
아티스트가 직접 작성하는 파라미터라고 되어 있지만, 실제로 에셋을 열어봤을 때 나오는 용어들에 대한 지식이 없는 사람이라면 그냥 눈으로 보면서 제일 좋아보이는 임의의 값을 넣게 된다. 그런데 이렇게 하면 특정 라이팅 환경에서 어색해 보일 수 있다. Unreal Docs에도 속시원한 설명이 없어서 직접 알아보았다.
Surface Albedo : 여기에서는 ‘표면 반사율’ 을 의미한다. 이 단어의 의미를 찾아보면 아래와 같다. Albedo (/ælˈbiːdoʊ/al-BEE-doh; from Latin albedo 'whiteness') is the fraction of sunlight that is diffusely reflected by a body. It is measured on a scale from 0 (corresponding to a black body that absorbs all incident radiation) to 1 (corresponding to a body that reflects all incident radiation). Surface albedo is defined as the ratio of radiosity Je to the irradiance Ee (flux per unit area) received by a surface 주로 태양에서 온 빛 에너지를 지구 표면에서 얼마나 부드럽게(diffusely), 여러 방향으로 반사하였는지 측정하는 데 사용되는 용어인 듯 하다. 입사한 빛을 표면에서 얼마나 반사하였는지에 대한 비율로, 받아들인 모든 빛을 흡수하는 흑체(BlackBody) 의 경우 0.0, 모든 빛을 반사하는 물체가 1.0 인것과 대응된다.
기체의 경우 압력이 더 높아질수록 짧아진다고 하는데, 이 부분은 우리가 하려는 렌더링과는 무관해 보인다.
이것을 알고 다시 Unreal Docs를 보면, 파라미터의 의미를 더 쉽게 이해할 수 있다.
Mean Free Path Distance : 위에서 살펴본 Mean Free Path 의 개념에 가까운 파라미터이다. 표면 아래로 빛이 나아갈 수 있는 거리이다.
Mean Free Path Color : 표면 아래로 빛 입자가 침투한 이후, 각 R, G, B 색상 요소가 나아갈 수 있는 거리의 비율이다. 이것은 위의 Mean Free Path Distance에 의해 스케일링된다. 만약 R 값이 G, B 값보다 더 높다면, 표면 안에서 붉은 계열 빛 입자가 다른 노란, 푸른 계열보다 더 멀리 이동할 수 있다는 뜻이고, 이것은 scattering이 일어난 이후의 빛의 색상과 관련되어 있음을 짐작할 수 있다.
Monte-Carlo Simulation? SubSurface Scattering을 계산하는 가장 보편적인 방식이다. 오브젝트를 Volume ㅇ리가ㅗ가정하고, brute-force Monte Carlo Simulation을 수행한다. 그러나, 이것은 매우 느리다 - 특히 복잡한 씬에서 더 느리다. Donner et al[2009] 는 반 무한대의 볼륨의 평평한 표면에서의 입자 움직임을 Monte-Carlo 를 사용해서 추적해 보았다. 이것을 토대로 빛의 입사각, 잔존하는 빛의 상대적인 위치, 그리고 volume albedo, mean free path length 등의 물리적인 요소들과 빛이 표면을 떠날 때의 분포를 표로 만들었다. 이 데이터를 계산하는 데 수 개월이 걸렸고, 총 데이터는 250MB 정도가 되었다.
이 커브들을 근사하기 위한 여러 시도들이 있었다. Burley는 이들을 두 개의 exponential(지수) 들의 합을 Distance 로 나눈 것으로 보았다. 그리고 Reflectance R을 아래의 식으로 표한하였다.
이 결과에 Surface Albedo A 가 곱해진다.
d 라는 파라미터가 커브의 너비와 높이를 조절하는 역할을 한다.. 그렇다면 d는 뭘까?
→ 여기서는 아티스트가 조절할 수 있는, 표면의 부드러운 정도(softness) 를 나타낸다.
표면에서 빛이 Scattering 되는 거리는 주로 앞에서 살펴본 Mean Free Path 또는 Diffuse Mean Free Path로 표현된다. (Mean Free Path 는 Volume에서, Diffuse Mean Free Path는 표면에서의 산란 거리를 말한다)
RenderMan 이 아닌 다른 렌더러들에서 셰이딩 모델을 살펴봐도, 같은 파라미터를 발견할 수 있다.
모바일에서의 Subsurface Profile 셰이딩 모델 동작에 관한 내용을 구글링해서도 얻을 수 없었기 때문에, 이 글을 작성하게 되었다.
Subsurface Profile 셰이딩 모델의 BRDF Code 살펴보면, 아래와 같다.
in ShadingModels.ush
FDirectLighting SubsurfaceProfileBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow ){
BxDFContext Context;
#if SHADING_PATH_MOBILEInitMobile(Context, N, V, L, NoL);
#elseInit( Context, N, V, L );
#endifSphereMaxNoH( Context, AreaLight.SphereSinAlpha, true );
Context.NoV = saturate( abs( Context.NoV ) + 1e-5 );
uint SubsurfaceProfileId = ExtractSubsurfaceProfileInt(GBuffer);
half Opacity = GBuffer.CustomData.a;
half Roughness = GBuffer.Roughness;
half Lobe0Roughness = 0;
half Lobe1Roughness = 0;
half LobeMix = 0;
GetProfileDualSpecular(SubsurfaceProfileId, Roughness, Opacity, Lobe0Roughness, Lobe1Roughness, LobeMix);
half AverageRoughness = lerp(Lobe0Roughness, Lobe1Roughness, LobeMix);
// Take the average roughness instead of compute a separate energy term for each roughnessconst FBxDFEnergyTerms EnergyTerms = ComputeGGXSpecEnergyTerms(AverageRoughness, Context.NoV, GBuffer.SpecularColor);
FDirectLighting Lighting;
#if SHADING_PATH_MOBILE
half Curvature = GBuffer.Curvature;
half UnClampedNoL = dot(GBuffer.WorldNormal, L);
half ShadowFactor = 1.0f - sqrt(Shadow.SurfaceShadow);
// Rotate the world normal based on the shadow value, it's just a experimental value
half UnClampedRotatedNoL = max(UnClampedNoL - max(2.0f * UnClampedNoL, 0.4f) * ShadowFactor, -1.0f);
half4 BurleyDiffuse = GetSSProfilePreIntegratedValue(SubsurfaceProfileId, UnClampedRotatedNoL, Curvature);
// asset specific color
half3 Tint = GetSubsurfaceProfileTexture(SSSS_TINT_SCALE_OFFSET, SubsurfaceProfileId).rgb;
// Needs to apply shadow at here, since the preintegrated value has already counted it, and we need to skip applying shadow outside.
Lighting.Diffuse = lerp(AreaLight.FalloffColor * (Falloff * NoL) * Shadow.SurfaceShadow, BurleyDiffuse.rgb, Tint) * Diffuse_Lambert(GBuffer.DiffuseColor);
#else#if MATERIAL_ROUGHDIFFUSE// Use Chan's diffuse model. It reduces flatness look and has better match, e.g., at the edge of human face skin when compared to GT.const float3 DiffuseReflection = Diffuse_Chan(GBuffer.DiffuseColor, Pow4(GBuffer.Roughness), Context.NoV, NoL, Context.VoH, Context.NoH, GetAreaLightDiffuseMicroReflWeight(AreaLight));
#elseconst float3 DiffuseReflection = Diffuse_Burley(GBuffer.DiffuseColor, GBuffer.Roughness, Context.NoV, NoL, Context.VoH);
#endif
Lighting.Diffuse = AreaLight.FalloffColor * (Falloff * NoL) * DiffuseReflection;
#endifif (IsRectLight(AreaLight))
{
float3 Lobe0Specular = RectGGXApproxLTC(Lobe0Roughness, GBuffer.SpecularColor, N, V, AreaLight.Rect, AreaLight.Texture);
float3 Lobe1Specular = RectGGXApproxLTC(Lobe1Roughness, GBuffer.SpecularColor, N, V, AreaLight.Rect, AreaLight.Texture);
Lighting.Specular = lerp(Lobe0Specular, Lobe1Specular, LobeMix);
}
else
{
Lighting.Specular = AreaLight.FalloffColor * (Falloff * NoL) * DualSpecularGGX(AverageRoughness, Lobe0Roughness, Lobe1Roughness, LobeMix, GBuffer.SpecularColor, Context, NoL, AreaLight);
}
Lighting.Diffuse *= ComputeEnergyPreservation(EnergyTerms);
Lighting.Specular *= ComputeEnergyConservation(EnergyTerms);
#if USE_TRANSMISSIONconst uint ProfileId = ExtractSubsurfaceProfileInt(GBuffer);
FTransmissionProfileParams TransmissionParams = GetTransmissionProfileParams(ProfileId);
float Thickness = Shadow.TransmissionThickness;
Thickness = DecodeThickness(Thickness);
Thickness *= SSSS_MAX_TRANSMISSION_PROFILE_DISTANCE;
float3 Profile = GetTransmissionProfile(ProfileId, Thickness).rgb;
float3 RefracV = refract(V, -N, TransmissionParams.OneOverIOR);
float PhaseFunction = ApproximateHG( dot(-L, RefracV), TransmissionParams.ScatteringDistribution );
Lighting.Transmission = AreaLight.FalloffColor * Profile * (Falloff * PhaseFunction); // TODO: This probably should also include cosine term (NoL)#else// USE_TRANSMISSION
Lighting.Transmission = 0;
#endif// USE_TRANSMISSIONreturn Lighting;
}
이 중 SHADING_PATH_MOBILE 부분이 모바일 렌더링 시 Diffuse 계산에 관련된 부분이다.
half Curvature = GBuffer.Curvature;
half UnClampedNoL = dot(GBuffer.WorldNormal, L);
half ShadowFactor = 1.0f - sqrt(Shadow.SurfaceShadow);
// Rotate the world normal based on the shadow value, it's just a experimental value
half UnClampedRotatedNoL = max(UnClampedNoL - max(2.0f * UnClampedNoL, 0.4f) * ShadowFactor, -1.0f);
half4 BurleyDiffuse = GetSSProfilePreIntegratedValue(SubsurfaceProfileId, UnClampedRotatedNoL, Curvature);
// asset specific color
half3 Tint = GetSubsurfaceProfileTexture(SSSS_TINT_SCALE_OFFSET, SubsurfaceProfileId).rgb;
// Needs to apply shadow at here, since the preintegrated value has already counted it, and we need to skip applying shadow outside.
Lighting.Diffuse = lerp(AreaLight.FalloffColor * (Falloff * NoL) * Shadow.SurfaceShadow, BurleyDiffuse.rgb, Tint) * Diffuse_Lambert(GBuffer.DiffuseColor);
이 부분은 2022년경에 Cloth 나 Eye 와 같은 다른 셰이딩 모델을 모바일 환경에서도 지원하려는 의도로 추가된 것으로 보인다.
Forward Rendering일 때는 맨 앞에서 살펴본 Screen Space Filtering 등을 사용할 수 없다.
2. NdotL 값을 ShadowFactor 를 토대로 Rotate 해서, UnClampedRotatedNoL 값을 얻는다.
3.1과 2 의 값으로 Texture에 기록된 Scattering값을 얻어온다. 텍스처에 저장된 값은 아래와 같다.
(텍스처가 아래에서 위로 올라갈수록 변하는 모습을 봤을 때, 1/r = Curvature가 1에 가까울수록 Scattering이 많이 일어날 것이라고 예상할 수 있다)
현재 사용중인 Subsurface Profile 파라미터들 - Surface Albedo, Mean Free Path, Mean Free Path Distance 등으부터 계산한, Sphere에서 모든 방향으로부터 Scattering 될 수 있는 값을 모두 저장해 둔 것이다.
텍스처를 만드는 데 사용되는 함수들은 SSProfilePreIntegratedMobile.usf 와BurleyNormalizedSSSCommon.ush 에 모여 있다. 실제 UE에서 만들어진 Texture는 아래와 같은 모양이다.
PF_A2B10G10R10 포맷이다. 별다른 설정이 없었다면 64x64 크기이다. 이 텍스처가 각 Subsurface Profile 1종마다 1개씩 생성되어, Texture Array 에 저장되고 사용된다. index 에 8-bit 가 사용되기 때문에, 한 장면에 최대 256종의 Subsurface Profile 만 사용할 수 있는 것이다.
Surface Albedo는 아래 식을 통해 Curve의 Scaling Factor S로 변환된다. 주석에 따르면, Monte Carlo Reference와 비교했을 때 평균 3.9% 정도의 오차를 가진다. in BurleyNormalizedSSSCommon.ush
float3 GetDiffuseSurfaceScalingFactor3D(float3 SurfaceAlbedo){
float3 Value = SurfaceAlbedo - 0.8;
return1.9 - SurfaceAlbedo + 3.5 * Value * Value;
}
SSProfilePreintegratedMobile.usf를 살펴보면, 핵심 함수인 GetIrredianceFromBurleyDiffusionOnRIng 에 위에서 살펴본 Burley Diffusion 에 관련된 내용을 확인할 수 있다.
3-1. 세로 축은 Curvature, 가로 축 값은 UnClampedRotatedNdotL 값을 사용해서 해당 부분의 색상을 샘플링해 온다. (in ShadingModels.ush)
여기에서 AreaLight.FalloffColor * (Falloff * NoL) 이라는 부분은 Default Lit 같은 다른 셰이딩 모델에서도 사용되는 Diffuse 항의 기본형이다.
Tint 값이 0에 가까울수록 Diffuse 항 계산 결과를, 1.0에 가까울수록 앞서 계산한 BurleyDiffuse 값을 반환한다.
Tint 파라미터가 0에 가까울수록 Scattering 이 없는 것 같이 렌더링 되는것을 볼 수 있다. Tint가 float3이기 때문에, 섞이는 강도를 R, G ,B 각 색상 요소에 따라 다르게 할 수 있다. (아마도? 의도은 그렇게 보이는데 lerp 가 실제로 이렇게 동작하는지는 정확하지 않음..)
4. 마지막으로 여기에 DiffuseColor 색상이 곱해지면, 이 결과가 Mobile Path의 최종 Diffuse 계산 결과가 된다. 표면이 특별히 Metalic 하지 않다면, 여기에서는 DiffuseColor = BaseColor 이다. 참고 : GBuffer.DiffuseColor = GBuffer.BaseColor - GBuffer.BaseColor * GBuffer.Metallic;
Mobile에서 Subsurface Profile 셰이딩 모델 사용하기
위 내용을 알면, 이제 Subsurface Profile 을 모바일에서 사용할 때 필요한 것들을 예상할 수 있다.
1. Material 에서 적절한 Curvature 텍스처를 생성해서 넣어준다. 여기에는 모델이 가진 큰 굴곡에 대한 데이터만 포함되어야 한다.
(주름, 모공 등의 디테일은 노멀에서 표현할 것이므로 없어도 된다)
이 텍스처가 메시 각 부분의 Scattering 강도를 결정할 것이다.
아래는 MetaHuman 모델링에서 추출한 Curvature 데이터이다.
(Subsurface Painter에 내장된 baker를 사용했다. Low-Poly Mesh를 High-Poly Mesh 로 사용하였다)
실제로는 이 텍스처를 그대로 사용하는 것이 아니라, Curve를 통해 필요한 영역의 데이터만 사용하도록 적절히 조정해 주었다.
2. 되도록이면 물리적인 값을 참조한 Subsurface Profile 파라미터를 사용한다.
Pixar 의 테이블, 또는 MetaHuman 의 Subsurface Profile 값을 참고하는 것이 좋다.
아래는 파라미터를 조절해 보면서 든 생각이다.
Surface Albedo 값은 (툴팁에 쓰여있는 것 처럼..) Albedo 텍스처의 색상 값과 비슷하게 넣어 주는 것이 낫다.
Tint 값은 되도록 1.0 에 가까운 값을 사용하는 것이 낫다. 이 파라미터의 R, G, B 값 중 어느 하나가 지나치게 높거나 낮을수록 실사 셰이딩과는 점점 거리가 먼 결과가 된다.
3. 기타 알게 된 내용들
Opacity 는 0.1 이하이거나 이상인 값만 유효하다. (이것은 SubsurfaceProfileCommon.ush 의 #define SSSS_OPACITY_THRESHOLD_EPS 가 0.10 으로 정의되어 있기 때문이다) PC에서는 Scattering 되는 강도로 Opacity를 사용하지만, 모바일에서는 0.1 이하이면 Scattering 하지 않음, 0.1 이상이면 Scattering 함 으로 보면 된다. 0.1 이하인 부분에서는 Default Lit 셰이딩 모델과 똑같이 동작한다. 메시의 각 부분별로 Scattering 되는 값을 조정하고 싶으면, 위의 Curvature 텍스처에서 해당 부분을 0으로 주면 된다.
MakeMaterialAttributes 노드를 사용할 경우, 핀 이름이 ClearCoat 가 되는 것에 주의한다.
이 핀에 들어가는 내용이 Curvature 이다.
Mobile 과 PC 간의 Specular 연산이 다르고, Screen-space에서의 Blur 효과도 모바일에서는 없기 때문에, Normal Intensity나 Specular Intensity 같은 값에 Mobile 전용 Multiplier 파라미터를 추가해서 강도를 조절해 주는 것이 좋아 보인다. (MetaHuman 샘플 참조)