- 셰이딩 모델의 추가하는 가장 간단한 방법 -> 엔진 코드 수정
- 중국 사이트 Zhihu에는 Shading Model의 코드를 확장하는 방법이 아주 많이 나옴
- 실제로 코드로 적용해보자!
- 그리고 효과, 그리고 그 원리를 간단하게 알아보자
- 아래 참고자료를 보고 번역했습니다. (구글 번역, DeepL 짱!)
셰이딩 모델은 최종적으로 머티리얼에 표현되어야 하며 다음 단계가 필요합니다.
- 상위 재질에 사용하려는 셰이딩 모델을 선택
- PixelMaterialInputs선택 후, 셰이딩 모델의 값은 PS에서 이 매개변수를 얻을 수 있도록 머트리얼의 입력 매개변수로 전달
- 디퍼드 렌더링이기 때문에 동시에 셰이딩 모델의 값도 SetGBufferForShadingModel함수를 통해 GBuffer에 전달
- 매개변수 입력을 처리한 후 IntegrateBxDF조명 계산 함수에서 전달된 Shading Model 값에 따라 최종적으로 해당 BxDF 함수를 선택하여 조명 계산을 수행
- 마지막 단계는 맞춤형 BxDF 조명 계산을 실행하는 것
위의 단계를 통해 Shading Model에서 정의한 조명 계산을 최종적으로 머트리얼에서 호출하여 랜더링 할 수 있습니다.
ps. 샘플 기준이 SM6 디퍼드 랜더링 기준으로 작업이 되어 있습니다. 모바일은 내용에 없습니다.
저도 따라해본게 다라.... 깊게 설명하는 부분은 패스합니다. 참고자료를 봐주세요.
언리얼 5.1 버젼 기준으로 작성되었습니다.
- enum EMaterialShadingModel 에 Toon 추가
//EngineTypes.h
UENUM()
enum EMaterialShadingModel : int
{
MSM_Unlit UMETA(DisplayName="Unlit"),
MSM_DefaultLit UMETA(DisplayName="Default Lit"),
MSM_Subsurface UMETA(DisplayName="Subsurface"),
MSM_PreintegratedSkin UMETA(DisplayName="Preintegrated Skin"),
MSM_ClearCoat UMETA(DisplayName="Clear Coat"),
MSM_SubsurfaceProfile UMETA(DisplayName="Subsurface Profile"),
MSM_TwoSidedFoliage UMETA(DisplayName="Two Sided Foliage"),
MSM_Hair UMETA(DisplayName="Hair"),
MSM_Cloth UMETA(DisplayName="Cloth"),
MSM_Eye UMETA(DisplayName="Eye"),
MSM_SingleLayerWater UMETA(DisplayName="SingleLayerWater"),
MSM_ThinTranslucent UMETA(DisplayName="Thin Translucent"),
MSM_Strata UMETA(DisplayName="Strata", Hidden),
MSM_Toon UMETA(DisplayName="Toon"), //Toon 추가
/** Number of unique shading models. */
MSM_NUM UMETA(Hidden),
/** Shading model will be determined by the Material Expression Graph,
by utilizing the 'Shading Model' MaterialAttribute output pin. */
MSM_FromMaterialExpression UMETA(DisplayName="From Material Expression"),
MSM_MAX
};
- GetShadingModelString 에 이름 추가
//MaterialShader.cpp
FString GetShadingModelString(EMaterialShadingModel ShadingModel)
{
FString ShadingModelName;
switch(ShadingModel)
{
case MSM_Unlit: ShadingModelName = TEXT("MSM_Unlit"); break;
case MSM_DefaultLit: ShadingModelName = TEXT("MSM_DefaultLit"); break;
case MSM_Subsurface: ShadingModelName = TEXT("MSM_Subsurface"); break;
case MSM_PreintegratedSkin: ShadingModelName = TEXT("MSM_PreintegratedSkin"); break;
case MSM_ClearCoat: ShadingModelName = TEXT("MSM_ClearCoat"); break;
case MSM_SubsurfaceProfile: ShadingModelName = TEXT("MSM_SubsurfaceProfile"); break;
case MSM_TwoSidedFoliage: ShadingModelName = TEXT("MSM_TwoSidedFoliage"); break;
case MSM_Hair: ShadingModelName = TEXT("MSM_Hair"); break;
case MSM_Cloth: ShadingModelName = TEXT("MSM_Cloth"); break;
case MSM_Eye: ShadingModelName = TEXT("MSM_Eye"); break;
case MSM_SingleLayerWater: ShadingModelName = TEXT("MSM_SingleLayerWater"); break;
case MSM_ThinTranslucent: ShadingModelName = TEXT("MSM_ThinTranslucent"); break;
case MSM_Toon: ShadingModelName = TEXT("MSM_Toon"); break; //추가
default: ShadingModelName = TEXT("Unknown"); break;
}
return ShadingModelName;
}
- 쉐이더 매크로 정의 추가
//HLSLMaterialTranslator.cpp
if (ShadingModels.HasShadingModel(MSM_SingleLayerWater))
{
OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_SINGLELAYERWATER"), TEXT("1"));
NumSetMaterials++;
}
if (ShadingModels.HasShadingModel(MSM_ThinTranslucent))
{
OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT"), TEXT("1"));
NumSetMaterials++;
bMaterialRequestsDualSourceBlending = true;
}
//추가
if (ShadingModels.HasShadingModel(MSM_Toon))
{
OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_TOON"), TEXT("1"));
NumSetMaterials++;
}
if (ShadingModels.HasShadingModel(MSM_SingleLayerWater) && FDataDrivenShaderPlatformInfo::GetRequiresDisableForwardLocalLights(Platform))
{
OutEnvironment.SetDefine(TEXT("DISABLE_FORWARD_LOCAL_LIGHTS"), TEXT("1"));
}
//ShaderMaterial.h
struct FShaderMaterialPropertyDefines
{
//DECLARE_TYPE_LAYOUT(FShaderMaterialPropertyDefines, NonVirtual);
//void ModifyEnvironment(FShaderCompilerEnvironment& OutEnvironment) const;
//void WriteFrozenVertexFactoryParameters(FMemoryImageWriter& Writer, const TMemoryImagePtr<FShaderMaterialPropertyDefines>& InPropDefines) const;
uint8 MATERIAL_ENABLE_TRANSLUCENCY_FOGGING : 1;
uint8 MATERIALBLENDING_ANY_TRANSLUCENT : 1;
uint8 MATERIAL_USES_SCENE_COLOR_COPY : 1;
uint8 MATERIALBLENDING_MASKED_USING_COVERAGE : 1;
uint8 MATERIAL_COMPUTE_FOG_PER_PIXEL : 1;
uint8 MATERIAL_SHADINGMODEL_UNLIT : 1;
uint8 MATERIAL_SHADINGMODEL_DEFAULT_LIT : 1;
uint8 MATERIAL_SHADINGMODEL_SUBSURFACE : 1;
uint8 MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN : 1;
uint8 MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE : 1;
uint8 MATERIAL_SHADINGMODEL_CLEAR_COAT : 1;
uint8 MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE : 1;
uint8 MATERIAL_SHADINGMODEL_HAIR : 1;
uint8 MATERIAL_SHADINGMODEL_CLOTH : 1;
uint8 MATERIAL_SHADINGMODEL_EYE : 1;
uint8 MATERIAL_SHADINGMODEL_SINGLELAYERWATER : 1;
uint8 SINGLE_LAYER_WATER_SEPARATED_MAIN_LIGHT : 1;
uint8 MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT : 1;
uint8 MATERIAL_SHADINGMODEL_TOON : 1; //추가
- 쉐이더 컴파일 코드 생성
//ShaderGenerationUtil.cpp
void FShaderCompileUtilities::ApplyFetchEnvironment(FShaderMaterialPropertyDefines& SrcDefines, FShaderCompilerEnvironment& OutEnvironment)
{
FETCH_COMPILE_BOOL(MATERIAL_ENABLE_TRANSLUCENCY_FOGGING);
FETCH_COMPILE_BOOL(MATERIALBLENDING_ANY_TRANSLUCENT);
FETCH_COMPILE_BOOL(MATERIAL_USES_SCENE_COLOR_COPY);
FETCH_COMPILE_BOOL(MATERIALBLENDING_MASKED_USING_COVERAGE);
FETCH_COMPILE_BOOL(MATERIAL_COMPUTE_FOG_PER_PIXEL);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_UNLIT);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_DEFAULT_LIT);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_SUBSURFACE);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_CLEAR_COAT);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_HAIR);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_CLOTH);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_EYE);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_SINGLELAYERWATER);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT);
FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_TOON); //추가
FETCH_COMPILE_BOOL(SINGLE_LAYER_WATER_SEPARATED_MAIN_LIGHT);
FETCH_COMPILE_BOOL(MATERIAL_FULLY_ROUGH);
FETCH_COMPILE_BOOL(USES_EMISSIVE_COLOR);
- 쉐이더 모델이 추가 데이터 사용하는 경우 CustomData를 통해 매개변수를 전달 할수 있다.
- 머트리얼의 노드의 핀을 수정할수도 있다.
- 쉐이딩 모델 아래의 Custom 핀을 활성화 한다.
//Material.cpp
case MP_Normal:
Active = (ShadingModels.IsLit() && (!bIsTranslucentBlendMode || !bIsNonDirectionalTranslucencyLightingMode)) || bUsesDistortion;
break;
case MP_Tangent:
Active = ShadingModels.HasAnyShadingModel({ MSM_DefaultLit, MSM_ClearCoat }) && (!bIsTranslucentBlendMode || !bIsVolumetricTranslucencyLightingMode);
break;
case MP_SubsurfaceColor:
Active = ShadingModels.HasAnyShadingModel({ MSM_Subsurface, MSM_PreintegratedSkin, MSM_TwoSidedFoliage, MSM_Cloth });
break;
case MP_CustomData0: //추가
Active = ShadingModels.HasAnyShadingModel({ MSM_ClearCoat, MSM_Hair, MSM_Cloth, MSM_Eye, MSM_SubsurfaceProfile, MSM_Toon });
break;
case MP_CustomData1: //추가
Active = ShadingModels.HasAnyShadingModel({ MSM_ClearCoat, MSM_Eye, MSM_Toon });
break;
- 핀에 대해 알아두면 좋은 점
- 핀 이름(예: customdata0 )을 더 나은 이름으로 바꿔야 하는 경우 해당 이름은 MaterialShared.cpp 파일 과 FMaterialAttributeDefinitionMap::GetAttributeOverrideForMaterial() 함수 내에 있습니다 .
- 새 핀 추가 이 코드는 원래 MaterialShared.cpp 파일 아래에 배치되었지만 주요 개발 브랜치의 최신 버전에서는 이를 MaterialAttributeDefinitionMap.cpp 아래로 이동했습니다
//MaterialShared.cpp/MaterialAttributeDefinitionMap.cpp
case MP_CustomData0:
CustomPinNames.Add({ MSM_ClearCoat, "Clear Coat" });
CustomPinNames.Add({MSM_Hair, "Backlit"});
CustomPinNames.Add({MSM_Cloth, "Cloth"});
CustomPinNames.Add({MSM_Eye, "Iris Mask"});
CustomPinNames.Add({MSM_SubsurfaceProfile, "Curvature" });
//쉐이딩모델을 Toon으로 바꾸었을때 CustomData0 이름출력이 Specular Range으로 변경
CustomPinNames.Add({ MSM_Toon, "Specular Range" }); //추가
return FText::FromString(GetPinNameFromShadingModelField(Material->GetShadingModels(), CustomPinNames, "Custom Data 0"));
case MP_CustomData1:
CustomPinNames.Add({ MSM_ClearCoat, "Clear Coat Roughness" });
CustomPinNames.Add({MSM_Eye, "Iris Distance"});
//쉐이딩모델을 Toon으로 바꾸었을때 CustomData1 이름출력이 Offset으로 변경
CustomPinNames.Add({ MSM_Toon, "Offset" }); //추가
return FText::FromString(GetPinNameFromShadingModelField(Material->GetShadingModels(), CustomPinNames, "Custom Data 1"));
- GBuffer에 CustomData 쓰기
//ShaderGenerationUtil.cpp
if (Mat.MATERIAL_SHADINGMODEL_EYE)
{
SetStandardGBufferSlots(Slots, bWriteEmissive, bHasTangent, bHasVelocity, bHasStaticLighting, bIsStrataMaterial);
Slots[GBS_CustomData] = bUseCustomData;
}
if (Mat.MATERIAL_SHADINGMODEL_SINGLELAYERWATER)
{
// single layer water uses standard slots
SetStandardGBufferSlots(Slots, bWriteEmissive, bHasTangent, bHasVelocity, bHasStaticLighting, bIsStrataMaterial);
if (Mat.SINGLE_LAYER_WATER_SEPARATED_MAIN_LIGHT)
{
Slots[GBS_SeparatedMainDirLight] = true;
}
}
// doesn't write to GBuffer
if (Mat.MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT)
{
}
if (Mat.MATERIAL_SHADINGMODEL_TOON) //추가
{
SetStandardGBufferSlots(Slots, bWriteEmissive, bHasTangent, bHasVelocity, bHasStaticLighting, bIsStrataMaterial);
Slots[GBS_CustomData] = bUseCustomData;
}
//ShaderMaterialDerivedHelpers.cpp
Dst.NEEDS_LIGHTMAP = (Dst.NEEDS_LIGHTMAP_COORDINATE) && !Lightmap.PRECOMPUTED_IRRADIANCE_VOLUME_LIGHTING;
Dst.USES_GBUFFER = (FEATURE_LEVEL >= ERHIFeatureLevel::SM4_REMOVED && (Mat.MATERIALBLENDING_SOLID || Mat.MATERIALBLENDING_MASKED) && !SrcGlobal.FORWARD_SHADING);
// Only some shader models actually need custom data. //추가
Dst.WRITES_CUSTOMDATA_TO_GBUFFER = (Dst.USES_GBUFFER && (Mat.MATERIAL_SHADINGMODEL_SUBSURFACE || Mat.MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN || Mat.MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE || Mat.MATERIAL_SHADINGMODEL_CLEAR_COAT || Mat.MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE || Mat.MATERIAL_SHADINGMODEL_HAIR || Mat.MATERIAL_SHADINGMODEL_CLOTH || Mat.MATERIAL_SHADINGMODEL_EYE || Mat.MATERIAL_SHADINGMODEL_TOON));
- C++ 부분에 대한 변경은 기본적으로 끝났습니다.
- 결과를 보면 머티리얼 에디터에 Shading Model 옵션을 추가했습니다.
- 동시에 이 Shading Model을 선택하면 두 개의 사용자 정의 핀이 나타납니다.
- 이 두 핀은 계산에 사용할 수 있도록 CustomData를 통해 셰이더에 매개변수를 전달합니다.
- 다음은 셰이더 부분입니다. 우선 동일한 작업을 수행할 수 있습니다.
- 다른 셰이딩 모델의 이름을 전역적으로 검색하고 먼저 몇 가지 정의를 추가할 수 있습니다.
//Definitions.usf
#if STRATA_NORMAL_QUALITY==0
#define STRATA_TOP_LAYER_TYPE uint
#elif STRATA_NORMAL_QUALITY==1
#define STRATA_TOP_LAYER_TYPE uint2
#else
#error Uknown STRATA_NORMAL_QUALITY
#endif
#ifndef MATERIAL_SHADINGMODEL_DEFAULT_LIT
#define MATERIAL_SHADINGMODEL_DEFAULT_LIT 0
#endif
//추가
#ifndef MATERIAL_SHADINGMODEL_TOON
#define MATERIAL_SHADINGMODEL_TOON 0
#endif
#ifndef MATERIAL_SHADINGMODEL_SUBSURFACE
#define MATERIAL_SHADINGMODEL_SUBSURFACE 0
#endif
#ifndef MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN
#define MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN 0
#endif
- Shading Model ID를 추가하고 View에서 Debug의 색상을 정의합니다.
//ShadingCommon.ush
// SHADINGMODELID_* occupy the 4 low bits of an 8bit channel and SKIP_* occupy the 4 high bits
#define SHADINGMODELID_UNLIT 0
#define SHADINGMODELID_DEFAULT_LIT 1
#define SHADINGMODELID_SUBSURFACE 2
#define SHADINGMODELID_PREINTEGRATED_SKIN 3
#define SHADINGMODELID_CLEAR_COAT 4
#define SHADINGMODELID_SUBSURFACE_PROFILE 5
#define SHADINGMODELID_TWOSIDED_FOLIAGE 6
#define SHADINGMODELID_HAIR 7
#define SHADINGMODELID_CLOTH 8
#define SHADINGMODELID_EYE 9
#define SHADINGMODELID_SINGLELAYERWATER 10
#define SHADINGMODELID_THIN_TRANSLUCENT 11
#define SHADINGMODELID_STRATA 12 // Temporary while we convert everything to Strata
#define SHADINGMODELID_TOON 13 // 추가
#define SHADINGMODELID_NUM 14
#define SHADINGMODELID_MASK 0xF // 4 bits reserved for ShadingModelID
// The flags are defined so that 0 value has no effect!
// These occupy the 4 high bits in the same channel as the SHADINGMODELID_*
#define HAS_ANISOTROPY_MASK (1 << 4)
#define SKIP_PRECSHADOW_MASK (1 << 5)
#define ZERO_PRECSHADOW_MASK (1 << 6)
#define SKIP_VELOCITY_MASK (1 << 7)
#define SSS_PROFILE_ID_INVALID 256
#define SSS_PROFILE_ID_PERPIXEL 512
// for debugging and to visualize
float3 GetShadingModelColor(uint ShadingModelID)
{
// TODO: PS4 doesn't optimize out correctly the switch(), so it thinks it needs all the Samplers even if they get compiled out
// This will get fixed after launch per Sony...
#if PS4_PROFILE
if (ShadingModelID == SHADINGMODELID_UNLIT) return float3(0.1f, 0.1f, 0.2f); // Dark Blue
else if (ShadingModelID == SHADINGMODELID_DEFAULT_LIT) return float3(0.1f, 1.0f, 0.1f); // Green
else if (ShadingModelID == SHADINGMODELID_SUBSURFACE) return float3(1.0f, 0.1f, 0.1f); // Red
else if (ShadingModelID == SHADINGMODELID_PREINTEGRATED_SKIN) return float3(0.6f, 0.4f, 0.1f); // Brown
else if (ShadingModelID == SHADINGMODELID_CLEAR_COAT) return float3(0.1f, 0.4f, 0.4f);
else if (ShadingModelID == SHADINGMODELID_SUBSURFACE_PROFILE) return float3(0.2f, 0.6f, 0.5f); // Cyan
else if (ShadingModelID == SHADINGMODELID_TWOSIDED_FOLIAGE) return float3(0.2f, 0.2f, 0.8f); // Blue
else if (ShadingModelID == SHADINGMODELID_HAIR) return float3(0.6f, 0.1f, 0.5f);
else if (ShadingModelID == SHADINGMODELID_CLOTH) return float3(0.7f, 1.0f, 1.0f);
else if (ShadingModelID == SHADINGMODELID_EYE) return float3(0.3f, 1.0f, 1.0f);
else if (ShadingModelID == SHADINGMODELID_SINGLELAYERWATER) return float3(0.5f, 0.5f, 1.0f);
else if (ShadingModelID == SHADINGMODELID_THIN_TRANSLUCENT) return float3(1.0f, 0.8f, 0.3f);
else if (ShadingModelID == SHADINGMODELID_STRATA) return float3(1.0f, 1.0f, 0.0f);
else if (ShadingModelID == SHADINGMODELID_TOON) return float3(0.75f, 0.1f, 0.1f); //추가
else return float3(1.0f, 1.0f, 1.0f); // White
#else
switch(ShadingModelID)
{
case SHADINGMODELID_UNLIT: return float3(0.1f, 0.1f, 0.2f); // Dark Blue
case SHADINGMODELID_DEFAULT_LIT: return float3(0.1f, 1.0f, 0.1f); // Green
case SHADINGMODELID_SUBSURFACE: return float3(1.0f, 0.1f, 0.1f); // Red
case SHADINGMODELID_PREINTEGRATED_SKIN: return float3(0.6f, 0.4f, 0.1f); // Brown
case SHADINGMODELID_CLEAR_COAT: return float3(0.1f, 0.4f, 0.4f); // Brown
case SHADINGMODELID_SUBSURFACE_PROFILE: return float3(0.2f, 0.6f, 0.5f); // Cyan
case SHADINGMODELID_TWOSIDED_FOLIAGE: return float3(0.2f, 0.2f, 0.8f); // Cyan
case SHADINGMODELID_HAIR: return float3(0.6f, 0.1f, 0.5f);
case SHADINGMODELID_CLOTH: return float3(0.7f, 1.0f, 1.0f);
case SHADINGMODELID_EYE: return float3(0.3f, 1.0f, 1.0f);
case SHADINGMODELID_SINGLELAYERWATER: return float3(0.5f, 0.5f, 1.0f);
case SHADINGMODELID_THIN_TRANSLUCENT: return float3(1.0f, 0.8f, 0.3f);
case SHADINGMODELID_STRATA: return float3(1.0f, 1.0f, 0.0f);
case SHADINGMODELID_TOON: return float3(0.75f, 0.75f, 0.1f); //추가
default: return float3(1.0f, 1.0f, 1.0f); // White
}
#endif
}
- CustomData가 작성되었는지 확인
//DeferredShadingCommon.ush
bool HasCustomGBufferData(int ShadingModelID)
{
return ShadingModelID == SHADINGMODELID_SUBSURFACE
|| ShadingModelID == SHADINGMODELID_PREINTEGRATED_SKIN
|| ShadingModelID == SHADINGMODELID_CLEAR_COAT
|| ShadingModelID == SHADINGMODELID_SUBSURFACE_PROFILE
|| ShadingModelID == SHADINGMODELID_TWOSIDED_FOLIAGE
|| ShadingModelID == SHADINGMODELID_HAIR
|| ShadingModelID == SHADINGMODELID_CLOTH
|| ShadingModelID == SHADINGMODELID_EYE
|| ShadingModelID == SHADINGMODELID_TOON; //추가
}
- CustomData를 GBuffer에 쓰기 위한 쓰기 정의
//BasePassCommon.ush
#define NEEDS_LIGHTMAP (NEEDS_LIGHTMAP_COORDINATE)
#define USES_GBUFFER (FEATURE_LEVEL >= FEATURE_LEVEL_SM4 && (MATERIALBLENDING_SOLID || MATERIALBLENDING_MASKED) && !FORWARD_SHADING)
// Only some shader models actually need custom data. //추가
#define WRITES_CUSTOMDATA_TO_GBUFFER (USES_GBUFFER && (MATERIAL_SHADINGMODEL_SUBSURFACE || MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN || MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE || MATERIAL_SHADINGMODEL_CLEAR_COAT || MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE || MATERIAL_SHADINGMODEL_HAIR || MATERIAL_SHADINGMODEL_CLOTH || MATERIAL_SHADINGMODEL_EYE || MATERIAL_SHADINGMODEL_TOON))
- GBuffer에 CustomData 쓰기
//ShadingModelsMaterial.ush
#if MATERIAL_SHADINGMODEL_EYE
else if (ShadingModel == SHADINGMODELID_EYE)
{
const float IrisMask = saturate(GetMaterialCustomData0(MaterialParameters));
const float IrisDistance = saturate(GetMaterialCustomData1(MaterialParameters));
GBuffer.CustomData.x = EncodeSubsurfaceProfile(SubsurfaceProfile).x;
GBuffer.CustomData.w = 1.0f - IrisMask; // Opacity
#if IRIS_NORMAL
float2 WorldNormalOct = UnitVectorToOctahedron( GBuffer.WorldNormal );
// CausticNormal stored as octahedron
#if NUM_MATERIAL_OUTPUTS_GETTANGENTOUTPUT > 0
// Blend in the negative intersection normal to create some concavity
// Not great as it ties the concavity to the convexity of the cornea surface
// No good justification for that. On the other hand, if we're just looking to
// introduce some concavity, this does the job.
float3 PlaneNormal = normalize( GetTangentOutput0(MaterialParameters) );
float3 CausticNormal = normalize( lerp( PlaneNormal, -GBuffer.WorldNormal, IrisMask*IrisDistance ) );
float2 CausticNormalOct = UnitVectorToOctahedron( CausticNormal );
float2 CausticNormalDelta = ( CausticNormalOct - WorldNormalOct ) * 0.5 + (128.0/255.0);
GBuffer.Metallic = CausticNormalDelta.x;
GBuffer.Specular = CausticNormalDelta.y;
#else
float3 PlaneNormal = GBuffer.WorldNormal;
GBuffer.Metallic = 128.0/255.0;
GBuffer.Specular = 128.0/255.0;
#endif
// IrisNormal CustomData.yz
#if NUM_MATERIAL_OUTPUTS_CLEARCOATBOTTOMNORMAL > 0
float3 IrisNormal = normalize( ClearCoatBottomNormal0(MaterialParameters) );
#if MATERIAL_TANGENTSPACENORMAL
IrisNormal = normalize( TransformTangentVectorToWorld( MaterialParameters.TangentToWorld, IrisNormal ) );
#endif
#else
float3 IrisNormal = PlaneNormal;
#endif
float2 IrisNormalOct = UnitVectorToOctahedron( IrisNormal );
float2 IrisNormalDelta = ( IrisNormalOct - WorldNormalOct ) * 0.5 + (128.0/255.0);
GBuffer.CustomData.yz = IrisNormalDelta;
#else
GBuffer.Metallic = IrisDistance;
#if NUM_MATERIAL_OUTPUTS_GETTANGENTOUTPUT > 0
float3 Tangent = GetTangentOutput0(MaterialParameters);
GBuffer.CustomData.yz = UnitVectorToOctahedron( normalize(Tangent) ) * 0.5 + 0.5;
#endif
#endif
#if SHADING_PATH_MOBILE
#if MATERIAL_SHADINGMODEL_EYE_USE_CURVATURE
GBuffer.Curvature = Metallic;
#else
GBuffer.Curvature = CalculateCurvature(GBuffer.WorldNormal, MaterialParameters.WorldPosition_CamRelative);
#endif
GBuffer.Curvature = clamp(GBuffer.Curvature, 0.001f, 1.0f);
#endif
}
#endif
//추가
#if MATERIAL_SHADINGMODEL_TOON
else if (ShadingModel == SHADINGMODELID_TOON)
{
GBuffer.CustomData.x = saturate( GetMaterialCustomData0(MaterialParameters) );
GBuffer.CustomData.y = saturate( GetMaterialCustomData1(MaterialParameters) );
}
#endif
- 위 작업을 완료한 후 다음 단계는 실제로 라이트 계산 및 라이트 모델을 수정해야 한다
The deferred light pass
- 함수의 시작점은 DeferredLightPixelShaders.usf 파일에 있는 DeferredLightPixelMain() 함수입니다.
- 이 함수 안에서 라이트는 모두 GetDynamicLighting() 함수에 의해 계산되며,
- 이 함수는 GetDynamicLightingSplit() 을 호출하고,
- 이 함수는 다시 DeferredLightingCommon.ush 파일에서 AccumulateDynamicLighting() 을 호출합니다:
// DeferredLightPixelShaders.usf
void DeferredLightPixelMain(
#if LIGHT_SOURCE_SHAPE > 0
float4 InScreenPosition : TEXCOORD0,
#else
float2 ScreenUV : TEXCOORD0,
float3 ScreenVector : TEXCOORD1,
#endif
// [...]
)
{
const float2 PixelPos = SVPos.xy;
OutColor = 0;
// [...]
float SurfaceShadow = 1.0f;
float4 LightAttenuation = GetLightAttenuationFromShadow(InputParams, SceneDepth);
float4 Radiance = GetDynamicLighting(DerivedParams.TranslatedWorldPosition, DerivedParams.CameraVector, ScreenSpaceData.GBuffer, ScreenSpaceData.AmbientOcclusion, ScreenSpaceData.GBuffer.ShadingModelID, LightData, LightAttenuation, Dither, uint2(InputParams.PixelPos), SurfaceShadow);
OutColor += Radiance;
// [...]
}
// DeferredLightingCommon.ush
float4 GetDynamicLighting(
float3 TranslatedWorldPosition, float3 CameraVector, FGBufferData GBuffer, float AmbientOcclusion, uint ShadingModelID,
FDeferredLightData LightData, float4 LightAttenuation, float Dither, uint2 SVPos,
inout float SurfaceShadow)
{
FDeferredLightingSplit SplitLighting = GetDynamicLightingSplit(
TranslatedWorldPosition, CameraVector, GBuffer, AmbientOcclusion, ShadingModelID,
LightData, LightAttenuation, Dither, SVPos,
SurfaceShadow);
return SplitLighting.SpecularLighting + SplitLighting.DiffuseLighting;
}
/** Calculates lighting for a given position, normal, etc with a fully featured lighting model designed for quality. */
FDeferredLightingSplit GetDynamicLightingSplit(
float3 TranslatedWorldPosition, float3 CameraVector, FGBufferData GBuffer, float AmbientOcclusion, uint ShadingModelID,
FDeferredLightData LightData, float4 LightAttenuation, float Dither, uint2 SVPos,
inout float SurfaceShadow)
{
FLightAccumulator LightAccumulator = AccumulateDynamicLighting(TranslatedWorldPosition, CameraVector, GBuffer, AmbientOcclusion, ShadingModelID, LightData, LightAttenuation, Dither, SVPos, SurfaceShadow);
return LightAccumulator_GetResultSplit(LightAccumulator);
}
- AccumulateDynamicLighting() 함수는 각 조명과 픽셀에 대한 빛과 그림자를 계산하는 데 사용되는 함수입니다.
- 꽤 길기 때문에 이해에 도움이 되는 중요한 부분만 설명하겠습니다.
- 한 픽셀의 경우 먼저 빛 감쇠를 얻습니다.
- 픽셀이 라이트 마스크 밖에 있으면 나머지 계산은 쓸모가 없으므로 버립니다.
float3 L = LightData.Direction; // Already normalized
float3 ToLight = L;
float3 MaskedLightColor = LightData.Color;
float LightMask = 1;
if (LightData.bRadialLight)
{
LightMask = GetLocalLightAttenuation( TranslatedWorldPosition, LightData, ToLight, L );
MaskedLightColor *= LightMask;
}
LightAccumulator.EstimatedCost += 0.3f; // running the PixelShader at all has a cost
BRANCH
if( LightMask > 0 )
- 픽셀이 마스크 안에 있으면 그림자 항을 얻습니다.
- 픽셀 위치가 다른 오브젝트(SurfaceShadow 멤버)가 드리운 그림자 안에 있는 경우와 같은 그림자 계산입니다:
FShadowTerms Shadow;
Shadow.SurfaceShadow = AmbientOcclusion;
Shadow.TransmissionShadow = 1;
Shadow.TransmissionThickness = 1;
Shadow.HairTransmittance.OpaqueVisibility = 1;
const float ContactShadowOpacity = GBuffer.CustomData.a;
GetShadowTerms(GBuffer.Depth, GBuffer.PrecomputedShadowFactors, GBuffer.ShadingModelID, ContactShadowOpacity, LightData, TranslatedWorldPosition, L, LightAttenuation, Dither, Shadow);
SurfaceShadow = Shadow.SurfaceShadow;
- 그런 다음 중요한 함수인 IntegrateBxDF().
- 이 함수에서는 셰이딩 모델에 따라 다양한 셰이딩 메서드가 실행됩니다.
- 이 함수는 디퓨즈, 스페큘러 및 투과 색상(서브서피스)을 계산합니다.
- 여기서 셀 셰이딩 계산을 수행합니다
- IntegrateBxDF 이 함수안에 새로운 Toon 모델 추가
if (LightData.bRectLight)
{
FRect Rect = GetRect( ToLight, LightData );
const FRectTexture SourceTexture = InitRectTexture(LightData);
#if REFERENCE_QUALITY
Lighting = IntegrateBxDF( GBuffer, N, V, Rect, Shadow, SourceTexture, SVPos );
#else
Lighting = IntegrateBxDF( GBuffer, N, V, Rect, Shadow, SourceTexture);
#endif
}
else
{
FCapsuleLight Capsule = GetCapsule( ToLight, LightData );
#if REFERENCE_QUALITY
Lighting = IntegrateBxDF( GBuffer, N, V, Capsule, Shadow, SVPos );
#else
Lighting = IntegrateBxDF( GBuffer, N, V, Capsule, Shadow, LightData.bInverseSquared );
#endif
}
- 마지막으로 LightAccumulator_AddSplit() 함수는 조명 결과를 누적합니다(표면 그림자 및 투과 그림자도 적용).
- 이 함수는 여러 번 호출할 수 있습니다:
Lighting.Specular *= LightData.SpecularScale;
LightAccumulator_AddSplit( LightAccumulator, Lighting.Diffuse, Lighting.Specular, Lighting.Diffuse, LightData.Color * SurfaceShadowMultiplier, bNeedsSeparateSubsurfaceLightAccumulation );
LightAccumulator_AddSplit( LightAccumulator, Lighting.Transmission, 0.0f, Lighting.Transmission, MaskedLightColor * Shadow.TransmissionShadow, bNeedsSeparateSubsurfaceLightAccumulation );
- 이제 라이트 패스가 어떻게 작동하는지 이해했습니다.
- 셀셰이딩 계산을 해보겠습니다.
- ShadingModelID를 기반으로 BxDF 선택
//ShadingModels.ush
FDirectLighting IntegrateBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
switch( GBuffer.ShadingModelID )
{
case SHADINGMODELID_DEFAULT_LIT:
case SHADINGMODELID_SINGLELAYERWATER:
case SHADINGMODELID_THIN_TRANSLUCENT:
return DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_SUBSURFACE:
return SubsurfaceBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_PREINTEGRATED_SKIN:
return PreintegratedSkinBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_CLEAR_COAT:
return ClearCoatBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_SUBSURFACE_PROFILE:
return SubsurfaceProfileBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_TWOSIDED_FOLIAGE:
return TwoSidedBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_HAIR:
return HairBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_CLOTH:
return ClothBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_EYE:
return EyeBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_TOON: //추가
return ToonBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
default:
return (FDirectLighting)0;
}
}
- ToonBxDF 정의
- 중국 사이트 Zhihu 에서 검색하시면 다양한 Toon 관련 라이트 계산 코드들이 많이 나옵니다.
FDirectLighting ToonBxDF(FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow)
{
#if GBUFFER_HAS_TANGENT
half3 X = GBuffer.WorldTangent;
half3 Y = normalize(cross(N, X));
#else
half3 X = 0;
half3 Y = 0;
#endif
BxDFContext Context;
Init(Context, N, X, Y, V, L);
SphereMaxNoH(Context, AreaLight.SphereSinAlpha, true);
Context.NoV = saturate(abs(Context.NoV) + 1e-5);
float SpecularOffset = 0.5;
float SpecularRange = GBuffer.CustomData.x;
float3 ShadowColor = 0;
ShadowColor = GBuffer.DiffuseColor * ShadowColor;
float offset = GBuffer.CustomData.y;
float SoftScatterStrength = 0;
offset = offset * 2 - 1;
half3 H = normalize(V + L);
float NoH = saturate(dot(N, H));
NoL = (dot(N, L) + 1) / 2; // overwrite NoL to get more range out of it
half NoLOffset = saturate(NoL + offset);
FDirectLighting Lighting;
Lighting.Diffuse = AreaLight.FalloffColor * (smoothstep(0, 1, NoLOffset) * Falloff) * Diffuse_Lambert(GBuffer.DiffuseColor) * 2.2;
float InScatter = pow(saturate(dot(L, -V)), 12) * lerp(3, .1f, 1);
float NormalContribution = saturate(dot(N, H));
float BackScatter = GBuffer.GBufferAO * NormalContribution / (PI * 2);
Lighting.Specular = ToonStep(SpecularRange, (saturate(D_GGX(SpecularOffset, NoH)))) * (AreaLight.FalloffColor * GBuffer.SpecularColor * Falloff * 8);
float3 TransmissionSoft = AreaLight.FalloffColor * (Falloff * lerp(BackScatter, 1, InScatter)) * ShadowColor * SoftScatterStrength;
float3 ShadowLightener = (saturate(smoothstep(0, 1, saturate(1 - NoLOffset))) * ShadowColor * 0.1);
Lighting.Transmission = (ShadowLightener + TransmissionSoft) * Falloff;
return Lighting;
}
- 주로 카툰 렌더링을 위한 그라데이션 감쇠 조명을 달성하기 위해 조명 계산을 변경합니다
//DeferredLightingCommon.ush
BRANCH
if( LightMask > 0 )
{
FShadowTerms Shadow;
Shadow.SurfaceShadow = AmbientOcclusion;
Shadow.TransmissionShadow = 1;
Shadow.TransmissionThickness = 1;
Shadow.HairTransmittance.OpaqueVisibility = 1;
const float ContactShadowOpacity = GBuffer.CustomData.a;
GetShadowTerms(GBuffer.Depth, GBuffer.PrecomputedShadowFactors, GBuffer.ShadingModelID, ContactShadowOpacity,
LightData, TranslatedWorldPosition, L, LightAttenuation, Dither, Shadow);
SurfaceShadow = Shadow.SurfaceShadow;
LightAccumulator.EstimatedCost += 0.3f; // add the cost of getting the shadow terms
//여기 부분 추가
float3 Attenuation = 1;
BRANCH
if (GBuffer.ShadingModelID == SHADINGMODELID_TOON)
{
float offset = GBuffer.CustomData.y;
float TerminatorRange = saturate(GBuffer.Roughness - 0.5);
offset = offset * 2 - 1;
BRANCH
if (offset >= 1)
{
Attenuation = 1;
}
else
{
float NoL = (dot(N, L) + 1) / 2;
float NoLOffset = saturate(NoL + offset);
float LightAttenuationOffset = saturate( Shadow.SurfaceShadow + offset);
float ToonSurfaceShadow = smoothstep(0.5 - TerminatorRange, 0.5 + TerminatorRange, LightAttenuationOffset);
Attenuation = smoothstep(0.5 - TerminatorRange, 0.5 + TerminatorRange, NoLOffset) * ToonSurfaceShadow;
}
}
#if SHADING_PATH_MOBILE
const bool bNeedsSeparateSubsurfaceLightAccumulation = UseSubsurfaceProfile(GBuffer.ShadingModelID);
FDirectLighting Lighting = (FDirectLighting)0;
half NoL = max(0, dot(GBuffer.WorldNormal, L));
#if TRANSLUCENCY_NON_DIRECTIONAL
NoL = 1.0f;
#endif
Lighting = EvaluateBxDF(GBuffer, N, V, L, NoL, Shadow);
Lighting.Specular *= LightData.SpecularScale;
//LightAccumulator_AddSplit 이부분에서 위에 계산한 Attenuation를 곱해준다
LightAccumulator_AddSplit( LightAccumulator, Lighting.Diffuse, Lighting.Specular, Lighting.Diffuse, MaskedLightColor * Shadow.SurfaceShadow * Attenuation, bNeedsSeparateSubsurfaceLightAccumulation );
LightAccumulator_AddSplit( LightAccumulator, Lighting.Transmission, 0.0f, Lighting.Transmission, MaskedLightColor * Shadow.TransmissionShadow, bNeedsSeparateSubsurfaceLightAccumulation );
#else // SHADING_PATH_MOBILE
BRANCH
if( Shadow.SurfaceShadow + Shadow.TransmissionShadow > 0 )
{
const bool bNeedsSeparateSubsurfaceLightAccumulation = UseSubsurfaceProfile(GBuffer.ShadingModelID);
#if NON_DIRECTIONAL_DIRECT_LIGHTING
float Lighting;
if( LightData.bRectLight )
{
FRect Rect = GetRect( ToLight, LightData );
Lighting = IntegrateLight( Rect );
}
else
{
FCapsuleLight Capsule = GetCapsule( ToLight, LightData );
Lighting = IntegrateLight( Capsule, LightData.bInverseSquared );
}
float3 LightingDiffuse = Diffuse_Lambert( GBuffer.DiffuseColor ) * Lighting;
//추가
LightAccumulator_AddSplit(LightAccumulator, LightingDiffuse, 0.0f, 0, MaskedLightColor * Shadow.SurfaceShadow * Attenuation, bNeedsSeparateSubsurfaceLightAccumulation);
#else
FDirectLighting Lighting;
if (LightData.bRectLight)
{
FRect Rect = GetRect( ToLight, LightData );
const FRectTexture SourceTexture = ConvertToRectTexture(LightData);
#if REFERENCE_QUALITY
Lighting = IntegrateBxDF( GBuffer, N, V, Rect, Shadow, SourceTexture, SVPos );
#else
Lighting = IntegrateBxDF( GBuffer, N, V, Rect, Shadow, SourceTexture);
#endif
}
else
{
FCapsuleLight Capsule = GetCapsule( ToLight, LightData );
#if REFERENCE_QUALITY
Lighting = IntegrateBxDF( GBuffer, N, V, Capsule, Shadow, SVPos );
#else
Lighting = IntegrateBxDF( GBuffer, N, V, Capsule, Shadow, LightData.bInverseSquared );
#endif
}
Lighting.Specular *= LightData.SpecularScale;
//추가
LightAccumulator_AddSplit( LightAccumulator, Lighting.Diffuse, Lighting.Specular, Lighting.Diffuse, MaskedLightColor * Shadow.SurfaceShadow * Attenuation, bNeedsSeparateSubsurfaceLightAccumulation );
LightAccumulator_AddSplit( LightAccumulator, Lighting.Transmission, 0.0f, Lighting.Transmission, MaskedLightColor * Shadow.TransmissionShadow, bNeedsSeparateSubsurfaceLightAccumulation );
LightAccumulator.EstimatedCost += 0.4f; // add the cost of the lighting computations (should sum up to 1 form one light)
#endif
}
#endif // SHADING_PATH_MOBILE
}
- ShadingModel과 관련된 사용자 정의 기능을 저장하기 위해 새 파일이 생성됩니다.
//ToonShadingCommon.ush
float3 ToonStep(float feather, float halfLambert, float threshold = 0.5f)
{
return smoothstep(threshold - feather, threshold + feather, halfLambert);
}
- 머트리얼에서 선을 연결하면 기본 카툰 렌더링 효과를 볼 수 있다.
- 원하는 카툰 쉐이딩 효과를 만들려면 쉐이딩 코드를 수정해야 합니다.
- 라이트 계산 코드 역시 Zhihu의 많은 글을 참고하고 있다. Shading Model이라는 키워드로 검색하시면 많이 나옵니다.
- 참고자료
- 아래 자료를 참고하여 구글 번역 및 정리하였습니다.
https://eukky.github.io/GameDev/UnrealEngine/CustomShadingModel.html
UE5中的卡通渲染——自定义Shading Model | META TECH
eukky.github.io
New shading models and changing the GBuffer | Community tutorial
Implementing a Celshading model directly into UE5.1 source. This celshading use a linear color curve atlas to drive all the values. Learn how to set you...
dev.epicgames.com
https://zhuanlan.zhihu.com/p/404857208
UE5自定义着色模型 Unreal Engine 5 custom Shading Model
UE5相信大家已经迫不及待地用起来了,而custom Shading Model又是很常见的需求,也是认识UE5渲染管线非常好的途径。在这之前我尚未接触UE源码,网上暂时也没有什么针对性教程,所以今天就写一篇
zhuanlan.zhihu.com
https://github.com/mljuw/UEToonStylized
GitHub - mljuw/UEToonStylized
Contribute to mljuw/UEToonStylized development by creating an account on GitHub.
github.com
https://zhuanlan.zhihu.com/p/542384881
Yivanlee's UE5 Toon Rendering Technical Documents
随着卡通渲染的需求越来越多,我们需要一套高品质的UE5卡通渲染技术方案,为此我对虚幻5进行了管线定制来实现高品质的卡通渲染需求,适用于游戏,影视,虚拟偶像等多种场合的需求。 推
zhuanlan.zhihu.com