inline void InitializeBRDFDataDirect ( half3 albedo, half3 diffuse, half3 specular, half reflectivity, half oneMinusReflectivity, half smoothness, inout half alpha, out BRDFData outBRDFData)
{
outBRDFData = (BRDFData) 0 ;
outBRDFData.albedo = albedo;
outBRDFData.diffuse = diffuse;
outBRDFData.specular = specular;
outBRDFData.reflectivity = reflectivity;
outBRDFData.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness (smoothness);
outBRDFData.roughness = max ( PerceptualRoughnessToRoughness (outBRDFData.perceptualRoughness), HALF_MIN_SQRT);
outBRDFData.roughness2 = max (outBRDFData.roughness * outBRDFData.roughness, HALF_MIN);
outBRDFData.grazingTerm = saturate (smoothness + reflectivity);
outBRDFData.normalizationTerm = outBRDFData.roughness * half ( 4.0 ) + half ( 2.0 );
outBRDFData.roughness2MinusOne = outBRDFData.roughness2 - half ( 1.0 );
// Input is expected to be non-alpha-premultiplied while ROP is set to pre-multiplied blend.
// We use input color for specular, but (pre-)multiply the diffuse with alpha to complete the standard alpha blend equation.
// In shader: Cs' = Cs * As, in ROP: Cs' + Cd(1-As);
// i.e. we only alpha blend the diffuse part to background (transmittance).
#if defined ( _ALPHAPREMULTIPLY_ON )
// TODO: would be clearer to multiply this once to accumulated diffuse lighting at end instead of the surface property.
outBRDFData.diffuse *= alpha;
#endif
}
여기서부터 봅니다. BRDF.hlsl 파일을 봐야 해요 . 이전시간에 얘길 안했네.
BRDFData 구조체를 채우는 곳인데요. outBRDFData.albedo = albedo; 알베도는 뭐 알베도고 outBRDFData.diffuse = diffuse; 디퓨즈네요. 디퓨즈는 이전 시간에 뭐였죠? albedo * oneMinusReflectivity 이거였어요. 알베도에 1- 스페큘러 반사율을 곱한거.outBRDFData.specular 그럼 스페큘러는? lerp(kDielectricSpec.rgb, albedo, metallic) 이거죠. 비금속일 경우 0.4의 색이 들어가고, 금속일 경우 알베도가 100% 들어가요. floa3의 색이죠outBRDFData.reflectivity 반사율은? half(1.0) - oneMinusReflectivity 이죠? 이거 뭐.. 스페큘러 반사율이예요. 비메탈이면 0.96, 메탈이면 0 이면 나오는게 oneMinusReflectivity 니까, 이건 반대로 비메탈이면 0.04, 메탈이면 1이 나오는.. 즉 '정반사율' 인거죠. 이건 float 이예요. 하 설명하기 복잡하게도 해놨네
outBRDFData.perceptualRoughness 다음은 이거. 퍼셉츄얼러프니스. 직역하면 '지각 거칠기' 이거 타고 들어가면 PerceptualSmoothnessToPerceptualRoughness(smoothness) 이고 이건 (1.0 - perceptualSmoothness) 이거든요. 즉 스무스니스 값을 받아서 1- 해준값이예요. 러프니스랑 스무스니스는 반대니까..
그리고 이제 러프니스 값은 max(PerceptualRoughnessToRoughness(outBRDFData.perceptualRoughness), HALF_MIN_SQRT); 하하하하 지각 거칠기 값을 그냥 거칠기값으로 변환시킨다네요 PerceptualRoughnessToRoughness 는 그냥 perceptualRoughness * perceptualRoughness 예요. 근데 이건 전처리가 붙어 있죠 #ifndef BUILTIN_TARGET_API 이건 즉 빌트인 상태일때만이라는 뜻일까나? 그땐 감마 모드일테니 이렇게 하라는 것일지도.. 여하간 이건 뭐 심각하게 중요한건 아니고. HALF_MIN_SQRT 는 float 연산에서 최소 값 이상의 정밀도를 보장하기 위해 사용됩니다. 언더플로우 방지용 숫자라고 볼 수 있겠네요
은 러프니스 2제곱이예요.
그레이징텀. 이거 그 노말과 조명벡터가 거의 90도가 되는 면을 의미해요 saturate(smoothness + reflectivity) 란 공식이 들어 있으니 smoothness + 비메탈이면 0.04, 메탈이면 1 이네요.
outBRDFData.normalizationTerm
노말라이제이션 텀. outBRDFData.roughness * half(4.0) + half(2.0); 러프니스값에 4를 곱하고 2를 더했네요 PBR 기반에서 수치 보정용. 나중에 써요
outBRDFData.roughness2MinusOne
이것도 마찬가지. outBRDFData.roughness2 - half(1.0) 이렇게 되어 있네요 러프니스 2제곱에서 1빼기. 나중에 계산식이 간편하게 되기 위해서 미리 구해놓은거예요
그 아래는 #if defined(_ALPHAPREMULTIPLY_ON) 이니까 알파 프리멀티플라이 할때 알파를 곱하는거라 뭐.. 안 중요하니까 패스.
휴.. PBR 연산하느라 기본 설정하는게 너무 많아서 헷갈리는군요. 여기까지가 다 전처리라는게 참 거시기함.
자 다시 연산을 보죠.
half3 LightingPhysicallyBased (BRDFData brdfData, BRDFData brdfDataClearCoat,
half3 lightColor, half3 lightDirectionWS, float lightAttenuation,
half3 normalWS, half3 viewDirectionWS,
half clearCoatMask, bool specularHighlightsOff)
{
half NdotL = saturate ( dot (normalWS, lightDirectionWS));
half3 radiance = lightColor * (lightAttenuation * NdotL);
half3 brdf = brdfData.diffuse;
brdf += brdfData.specular * DirectBRDFSpecular (brdfData, normalWS, lightDirectionWS, viewDirectionWS);
return brdf * radiance;
}
이게 직광 라이트였잖아요? 클리어코트는 신경 안써도 되고..
half NdotL = saturate ( dot (normalWS, lightDirectionWS));
half3 radiance = lightColor * (lightAttenuation * NdotL);
그럼 일단 radiance (발광)은 NdotL 에 라이트칼라죠 사실. 그리고
half3 brdf = brdfData.diffuse;
니까 현재 brdf는 albedo * oneMinusReflectivity 예요. 이거 뭐 칼라.. 말하는거지 금속일 경우는 0가 되겠고 비금속일 경우는 albedo * 0.96 이 되겠죠 그 다음은
brdf += brdfData.specular * DirectBRDFSpecular (brdfData, normalWS, lightDirectionWS, viewDirectionWS);
인데 이거 스페큘러가 좀 중요하죠. brdfData.specular 는 일단 lerp(kDielectricSpec.rgb, albedo, metallic) 니까. 무슨 색이 나오냐 얘기예요. 금속일때와 비금속일때의 결과가 다르죠. 그리고 여기
DirectBRDFSpecular (brdfData, normalWS, lightDirectionWS, viewDirectionWS);
가 진짜 핵심 부분인 스페큘러예요.
휴우.. 자 그럼 볼까..
Specular
// Computes the scalar specular term for Minimalist CookTorrance BRDF
// NOTE: needs to be multiplied with reflectance f0, i.e. specular color to complete
half DirectBRDFSpecular (BRDFData brdfData, half3 normalWS, half3 lightDirectionWS, half3 viewDirectionWS)
{
float3 lightDirectionWSFloat3 = float3 (lightDirectionWS);
float3 halfDir = SafeNormalize (lightDirectionWSFloat3 + float3 (viewDirectionWS));
float NoH = saturate ( dot ( float3 (normalWS), halfDir));
half LoH = half ( saturate ( dot (lightDirectionWSFloat3, halfDir)));
// GGX Distribution multiplied by combined approximation of Visibility and Fresnel
// BRDFspec = (D * V * F) / 4.0
// D = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2
// V * F = 1.0 / ( LoH^2 * (roughness + 0.5) )
// See "Optimizing PBR for Mobile" from Siggraph 2015 moving mobile graphics course
// Final BRDFspec = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2 * (LoH^2 * (roughness + 0.5) * 4.0)
// We further optimize a few light invariant terms
// brdfData.normalizationTerm = (roughness + 0.5) * 4.0 rewritten as roughness * 4.0 + 2.0 to a fit a MAD.
float d = NoH * NoH * brdfData.roughness2MinusOne + 1. 00001f;
half LoH2 = LoH * LoH;
half specularTerm = brdfData.roughness2 / ((d * d) * max ( 0. 1h, LoH2) * brdfData.normalizationTerm);
// On platforms where half actually means something, the denominator has a risk of overflow
// clamp below was added specifically to "fix" that, but dx compiler (we convert bytecode to metal/gles)
// sees that specularTerm have only non-negative terms, so it skips max(0,..) in clamp (leaving only min(100,...))
#if REAL_IS_HALF
specularTerm = specularTerm - HALF_MIN;
// Update: Conservative bump from 100.0 to 1000.0 to better match the full float specular look.
// Roughly 65504.0 / 32*2 == 1023.5,
// or HALF_MAX / ((mobile) MAX_VISIBLE_LIGHTS * 2),
// to reserve half of the per light range for specular and half for diffuse + indirect + emissive.
specularTerm = clamp (specularTerm, 0.0 , 1000.0 ); // Prevent FP16 overflow on mobiles
#endif
return specularTerm;
}
키엑.
보통 쿡 토렌스 공식을 쓰는데요. 유니티는 이걸 See "Optimizing PBR for Mobile" from Siggraph 2015 moving mobile graphics course https://community.arm.com/events/1155
Moving Mobile Graphics - SIGGRAPH 2015 Course - Calendar - Arm Community - Arm Community
community.arm.com
이 식으로 모바일용 최적화를 해놨어요.
쿡 토런스 공식을 그냥 써도 되겠죠. 여하간 이게 물리 기반에서 스페큘러를 구하는 공식 축약판이예요.
즉 이렇게 되면
마지막에 return brdf * radiance;
radiance를 곱해주죠. 즉 NdotL*라이트칼라 를 곱해주고 끝이예요. 보통 파이로 나눠서 광량을 표시하곤 하는데 유니티 라이트는 걍 1 이라는 비물리적 수치라서 그냥 둔 모양.
이렇게 다이렉트 라이트가 완성되었구요. LightingPhysicallyBased
이게 다이렉트 라이트였으니 이제 인다이렉트 라이트도 동일하게 해줘야죠
inDirectLight
인다이렉트 라이트는
half4 UniversalFragmentPBR (InputData inputData, SurfaceData surfaceData)
{
BRDFData brdfData;
// NOTE: can modify "surfaceData"...
InitializeBRDFData (surfaceData, brdfData);
// Clear-coat calculation...
BRDFData brdfDataClearCoat = CreateClearCoatBRDFData (surfaceData, brdfData);
half4 shadowMask = CalculateShadowMask (inputData);
AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor (inputData, surfaceData);
Light mainLight = GetMainLight (inputData, shadowMask, aoFactor);
InitializeBRDFData
// NOTE: We don't apply AO to the GI here because it's done in the lighting calculation below...
MixRealtimeAndBakedGI (mainLight, inputData.normalWS, inputData.bakedGI);
LightingData lightingData = CreateLightingData (inputData, surfaceData);
lightingData.giColor = GlobalIllumination (brdfData, brdfDataClearCoat, surfaceData.clearCoatMask,
inputData.bakedGI, aoFactor.indirectAmbientOcclusion, inputData.positionWS,
inputData.normalWS, inputData.viewDirectionWS, inputData.normalizedScreenSpaceUV);
{
lightingData.mainLightColor = LightingPhysicallyBased (brdfData, brdfDataClearCoat,
mainLight,
inputData.normalWS, inputData.viewDirectionWS,
surfaceData.clearCoatMask, specularHighlightsOff);
}
return CalculateFinalColor (lightingData, surfaceData.alpha);
}
여기 안에 있는
lightingData.giColor = GlobalIllumination (brdfData, brdfDataClearCoat, surfaceData.clearCoatMask,
inputData.bakedGI, aoFactor.indirectAmbientOcclusion, inputData.positionWS,
inputData.normalWS, inputData.viewDirectionWS, inputData.normalizedScreenSpaceUV);
이 부분이 인다이렉트 라이트입니다.
뭐 얘도 동일하죠.
좋아요 그럼 GlobalIllumination 을 가봐야겠지.. GlobalIllumination.hlsl 에 있는 녀석을 가져옵니다.
half3 GlobalIllumination (BRDFData brdfData, BRDFData brdfDataClearCoat, float clearCoatMask,
half3 bakedGI, half occlusion, float3 positionWS,
half3 normalWS, half3 viewDirectionWS, float2 normalizedScreenSpaceUV)
{
half3 reflectVector = reflect (-viewDirectionWS, normalWS);
half NoV = saturate ( dot (normalWS, viewDirectionWS));
half fresnelTerm = Pow4 ( 1.0 - NoV);
half3 indirectDiffuse = bakedGI;
half3 indirectSpecular = GlossyEnvironmentReflection (reflectVector, positionWS, brdfData.perceptualRoughness, 1. 0h, normalizedScreenSpaceUV);
half3 color = EnvironmentBRDF (brdfData, indirectDiffuse, indirectSpecular, fresnelTerm);
if ( IsOnlyAOLightingFeatureEnabled ())
{
color = half3 ( 1 , 1 , 1 ); // "Base white" for AO debug lighting mode
}
#if defined ( _CLEARCOAT ) || defined ( _CLEARCOATMAP )
half3 coatIndirectSpecular = GlossyEnvironmentReflection (reflectVector, positionWS, brdfDataClearCoat.perceptualRoughness, 1. 0h, normalizedScreenSpaceUV);
// TODO: "grazing term" causes problems on full roughness
half3 coatColor = EnvironmentBRDFClearCoat (brdfDataClearCoat, clearCoatMask, coatIndirectSpecular, fresnelTerm);
// Blend with base layer using khronos glTF recommended way using NoV
// Smooth surface & "ambiguous" lighting
// NOTE: fresnelTerm (above) is pow4 instead of pow5, but should be ok as blend weight.
half coatFresnel = kDielectricSpec .x + kDielectricSpec .a * fresnelTerm;
return (color * ( 1.0 - coatFresnel * clearCoatMask) + coatColor) * occlusion;
#else
return color * occlusion;
#endif
}
역시 필요없는걸 날려보죠
half3 GlobalIllumination (BRDFData brdfData, BRDFData brdfDataClearCoat, float clearCoatMask,
half3 bakedGI, half occlusion, float3 positionWS,
half3 normalWS, half3 viewDirectionWS, float2 normalizedScreenSpaceUV)
{
half3 reflectVector = reflect (-viewDirectionWS, normalWS);
half NoV = saturate ( dot (normalWS, viewDirectionWS));
half fresnelTerm = Pow4 ( 1.0 - NoV);
half3 indirectDiffuse = bakedGI;
half3 indirectSpecular = GlossyEnvironmentReflection (reflectVector, positionWS, brdfData.perceptualRoughness, 1. 0h, normalizedScreenSpaceUV);
half3 color = EnvironmentBRDF (brdfData, indirectDiffuse, indirectSpecular, fresnelTerm);
return color * occlusion;
}
음 좀 단순해졌다..
자 이것도 디퓨즈랑 스페큘러로 나눠져 있겠죠?
half3 indirectDiffuse = bakedGI;
하핫 indirectDiffuse는 쉽네요. bakedGI 를 씁니다. 이건 라이트 프로브 + 라이트맵이라고 볼 수 있어요. 거기에 알베도가 곱해진... 그래요 뭐 이미 구워진 놈들이죠 . 틀리지 않지
심플하네. 그냥 다 되어 있어요 디퓨즈는 끝났네?
이젠 스페큘러를 봅시다.
half3 indirectSpecular = GlossyEnvironmentReflection (reflectVector, positionWS, brdfData.perceptualRoughness, 1. 0h, normalizedScreenSpaceUV);
헤에 너는 좀 복잡하구나?
half3 GlossyEnvironmentReflection ( half3 reflectVector, half perceptualRoughness, half occlusion)
{
#if ! defined ( _ENVIRONMENTREFLECTIONS_OFF )
half3 irradiance;
half mip = PerceptualRoughnessToMipmapLevel (perceptualRoughness);
half4 encodedIrradiance = half4 ( SAMPLE_TEXTURECUBE_LOD (unity_SpecCube0, samplerunity_SpecCube0, reflectVector, mip));
irradiance = DecodeHDREnvironment (encodedIrradiance, unity_SpecCube0_HDR);
return irradiance * occlusion;
#else
return _GlossyEnvironmentColor .rgb * occlusion;
#endif // _ENVIRONMENTREFLECTIONS_OFF
}
역시 정리해보면 별 거 없어요
half3 GlossyEnvironmentReflection ( half3 reflectVector, half perceptualRoughness, half occlusion)
{
half3 irradiance;
half mip = PerceptualRoughnessToMipmapLevel (perceptualRoughness);
half4 encodedIrradiance = half4 ( SAMPLE_TEXTURECUBE_LOD (unity_SpecCube0, samplerunity_SpecCube0, reflectVector, mip));
irradiance = DecodeHDREnvironment (encodedIrradiance, unity_SpecCube0_HDR);
return irradiance * occlusion;
}
댑따 쉽죠. 일단 mip 레벨을 구해요. 리플렉션 프로브가 반사잖아요. 얘가 스페큘러 텍스쳐예요. 근데 예를 스무스니스에 따라 반응하게 하려면
// The *approximated* version of the non-linear remapping. It works by
// approximating the cone of the specular lobe, and then computing the MIP map level
// which (approximately) covers the footprint of the lobe with a single texel.
// Improves the perceptual roughness distribution.
real PerceptualRoughnessToMipmapLevel (real perceptualRoughness, uint maxMipLevel)
{
perceptualRoughness = perceptualRoughness * ( 1.7 - 0.7 * perceptualRoughness);
return perceptualRoughness * maxMipLevel;
}
이걸로 밉 레벨이 적당히 반응하게 만들어요 그리고
half4 encodedIrradiance = half4 ( SAMPLE_TEXTURECUBE_LOD (unity_SpecCube0, samplerunity_SpecCube0, reflectVector, mip));
이걸로 그냥 큐브맵 받아와서 밉 레벨 적용하는거예요. 유니티 리플렉션 프로브요
irradiance = DecodeHDREnvironment (encodedIrradiance, unity_SpecCube0_HDR);
그리고 다음줄에서 HDR로 들어오는 이녀석을 디코드 시키죠. 여기에 오클루젼 곱해주면 끝. return irradiance * occlusion;
와 설명할 것도 없네?
자 다시. GlobalIllumination 함수에서
half3 indirectDiffuse = bakedGI;
half3 indirectSpecular = GlossyEnvironmentReflection (reflectVector, positionWS, brdfData.perceptualRoughness, 1. 0h, normalizedScreenSpaceUV);
half3 color = EnvironmentBRDF (brdfData, indirectDiffuse, indirectSpecular, fresnelTerm);
이 부분에서 위에 두 개가 구해졌으므로 (라이트 프로브와 리플렉션 프로브) 마지막에 EnvironmentBRDF 함수에서 금속 비금속 어쩌구 해주면 될듯.
half3 EnvironmentBRDF (BRDFData brdfData, half3 indirectDiffuse, half3 indirectSpecular, half fresnelTerm)
{
half3 c = indirectDiffuse * brdfData.diffuse;
c += indirectSpecular * EnvironmentBRDFSpecular (brdfData, fresnelTerm);
return c;
}
역시 텍스쳐 곱해주는 곳으로 연결되는군요.
indirectDiffuse 는 brdfData.diffuse 랑 곱해줘서 금속 비금속 텍스쳐 작동하게 만들죠 indirectSpecular 는 EnvironmentBRDFSpecular로 가는데,
half3 EnvironmentBRDFSpecular (BRDFData brdfData, half fresnelTerm)
{
float surfaceReduction = 1.0 / (brdfData.roughness2 + 1.0 );
return half3 (surfaceReduction * lerp (brdfData.specular, brdfData.grazingTerm, fresnelTerm));
}
이거예요. 이 공식으로 스페큘러쪽 처리를 해주고 더해주면 GI도 끝.
댓글