news 2026/6/10 18:41:38

3D SDF 多光源 阴影 的不同尝试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
3D SDF 多光源 阴影 的不同尝试

一、选择对角色光照贡献最大的一盏灯作为主灯,用于主基调的亮光面和阴影面,其他灯光对亮光面和被光面进行补光(增亮)

如果对角色模型,在所有光源里面进行遍历,找到对角色光照贡献最强的一盏光作为主光,那么当角色移动,或者有一盏灯亮度逐渐增大时,取代之前的主光,容易产生阴影闪烁的问题

例如:

主灯会硬切
如果当前主灯 A 强度是 10,副灯 B 慢慢变到 10.1,材质里如果直接选最大值,主灯会瞬间从 A 切到 B。

那么现在问题就是如何平和过渡这个主光的切换

二、根据每盏灯对角色脸部光照贡献程度,取一个带权重的方向作为主光,且只有一个主光没有副光

做法就是每个方向都进行带权加到一个方向向量上,比如说:我这里离角色近,且灯光亮度高,那么我就对最后的主方向给的贡献就更大,这样带权累加过后进行归一化处理

这样的好处:
我就不需要再考虑什么主光源之间的切换问题,不会有单体上的主光源的概念,只会灯光根据对角色脸部的光照贡献对最终方向进行加权

  1. 多个方向会生成不存在的阴影形状

比如左前方一盏灯、右前方一盏灯。
如果你把两个方向加权平均,得到的是“正前方光”。但真实卡渲里这不一定等价,因为 SDF 图集里“正前方阴影”是单独画出来的美术结果,不是左右两张图数学平均出来的。

三、我选择一个灯光作为主光并一直保持不变(例如选一盏方向光)

做法就是选一个灯光作为主光,不管其他灯光对角色脸部亮度的贡献是否大于选择的这盏主光,都不会代替人为选择的这个主光,其他的灯光对脸部进行补光

好处:不会有不正常的阴影或者主光发生变化产生的阴影突变的问题

坏处:加入我突然进屋了,那么按照道理这个方向光对角色的贡献已经失效了,我应该选一个新主光,那么这个时候,假如选择了新主光,那么阴影很容易造成阴影突变的问题,这其实本质又是主光发生突变导致的阴影突变的问题

四、我根据光照对脸部的光照贡献选最强光为主光,其余光如果能够照到主光的阴影区,则增亮阴影区,若光照贡献相同,则脸部两盏灯照的是一样亮的

特别注意一点,就是照亮阴影不能只照亮一部分,那样会特别脏,如果其余光照亮了主光阴影的全部范围,这样才对阴影提亮进行贡献,否则不贡献

如果只是这样去限制,会产生一个情况,就是我现在有一个主光和多个副光,我任一一个副光对脸部的光照着色不超过主光,但是我可能多个副光对主光阴影的贡献会超过主光这个情况,那我该如何去限制?
还有一个问题,我们刚才只讨论了标题上面是根据一盏灯在角色左前方,一盏灯在右前方,互相照亮对方的阴影,提亮彼此的阴影的情况

我们还有一个情况没有考虑到,那就是两盏灯同侧(同为角色向前左前方,同为角色向前右前方)但是角度不同,我就不应该存在完全照亮对方阴影的可能,所以这个时候我尝试用smooth union平滑融合角度比较小的阴影,使得当同一侧的A主光被同一侧的B副光替代为新主光的时候,有一个很平滑的阴影融合效果

性能优化方面,我希望对脸部光照贡献最强的和第二强的才采样SDF贴图,其余直接根据光照贡献对脸部进行增亮即可,不用采样
还有一个就是当对脸部光照贡献小于某个值的时候,我将剩下的逻辑直接跳过continue不进行下一步更复杂的计算

代码如下:

// M_qita_SDF_3D Custom HLSL // Key light controls main 3D SDF shadow. // Top1/Top2 relation drives stable transition. // Same-side close Top2 smooth-unions with key shadow. // Opposite-side close Top2 fades key shadow to lit. // Other fills only brighten, they do not reshape the main shadow. // Exposure is allowed to output values above 1.0. #ifndef GM_BASEPASS_FORWARD_LIGHT_ACCESS #define GM_BASEPASS_FORWARD_LIGHT_ACCESS 0 #endif #define FACE_FORWARD_AXIS float3(0.0, 1.0, 0.0) #define FACE_RIGHT_AXIS float3(1.0, 0.0, 0.0) #define FACE_UP_AXIS float3(0.0, 0.0, 1.0) #define FACE_CENTER_OFFSET_LOCAL float3(0.0, 0.0, 145.0) #define SDF_ATLAS_SIZE 9.0 #define SDF_ATLAS_MAX_CELL 8.0 #define SDF_CELL_UV_PADDING 0.008 #define SDF_EDGE_WIDTH 0.08 #define SDF_SHADOW_MIN 0.22 #define SDF_LIGHT_POWER 1.0 #define SDF_NO_SHADOW_VALUE 1.0 #define LOCAL_LIGHT_VISUAL_REFERENCE_INTENSITY 80.0 #define DIRECTIONAL_LIGHT_VISUAL_REFERENCE_INTENSITY 1.0 #define QITA_DIRECTIONAL_VISUAL_INTENSITY_FIX 3.14159265 #define SDF_LOCAL_VISUAL_RESPONSE_SCALE 0.005 #define SDF_LOCAL_KEY_WEIGHT 1.0 #define SDF_DIRECTIONAL_KEY_WEIGHT 1.0 #define SDF_FILL_SHADOW_STRENGTH 1.0 #define SDF_FILL_RATIO_MAX 1.0 #define SDF_FILL_VISUAL_SCALE 1.0 #define SDF_FILL_FACE_GATE_START -0.20 #define SDF_FILL_FACE_GATE_END 0.30 #define SDF_FILL_SHADOW_SIDE_START 0.15 #define SDF_FILL_SHADOW_SIDE_END 0.80 #define SDF_SAME_SIDE_RAW_START -0.08 #define SDF_SAME_SIDE_RAW_END 0.08 #define SDF_SAME_SIDE_CONTRIB_START 0.70 #define SDF_SAME_SIDE_CONTRIB_END 0.95 #define SDF_SAME_SIDE_SMOOTH_UNION_K 0.30 #define SDF_CROSS_SIDE_FADE_START 0.70 #define SDF_CROSS_SIDE_FADE_END 1.0 #define SDF_VISUAL_COLOR_SCALE 0.18 #define SDF_VISUAL_COLOR_MAX 0.65 #define SDF_VISUAL_LIT_START 0.35 #define SDF_VISUAL_LIT_END 1.0 #define SDF_EXPOSURE_SCALE 0.035 #define SDF_EXPOSURE_LIT_START 0.62 #define SDF_EXPOSURE_LIT_END 0.98 #define SDF_EXPOSURE_LIT_POWER 1.40 #define SDF_VERTICAL_BLEND_START 0.04 #define SDF_VERTICAL_BLEND_END 0.30 #define SDF_FRONT_BACK_BLEND_WIDTH 0.10 #define SDF_LR_BLEND_WIDTH 0.04 #define SDF_LR_SIGN_FLIP 1.0 #define QITA_SAFE_NORM3(V) ((V) * rsqrt(max(dot((V), (V)), 0.0001))) #define QITA_SAMPLE_SDF3D(UV_VALUE) Texture2DSample(SDF3D, SDF3DSampler, UV_VALUE).r #define QITA_TO_SDF_MASK(SIGNED_SDF_VALUE) pow(saturate(smoothstep(-SDF_EDGE_WIDTH, SDF_EDGE_WIDTH, SIGNED_SDF_VALUE)), SDF_LIGHT_POWER) #define QITA_SMOOTH_MAX_SIGNED(A_VALUE, B_VALUE, K_VALUE, OUT_VALUE) \ { \ float _SmoothA = (A_VALUE); \ float _SmoothB = (B_VALUE); \ float _SmoothK = max((K_VALUE), 0.0001); \ float _SmoothH = saturate(0.5 + 0.5 * (_SmoothA - _SmoothB) / _SmoothK); \ OUT_VALUE = clamp(lerp(_SmoothB, _SmoothA, _SmoothH) + _SmoothK * _SmoothH * (1.0 - _SmoothH), -1.0, 1.0); \ } float4x4 LocalToWorld = DFHackToFloat(GetPrimitiveData(Parameters).LocalToWorld); float3 ObjectTranslatedWorld = GetObjectTranslatedWorldPosition(Parameters); float3 FaceForwardWS = QITA_SAFE_NORM3(mul(float4(FACE_FORWARD_AXIS, 0.0), LocalToWorld).xyz); float3 FaceRightWS = QITA_SAFE_NORM3(mul(float4(FACE_RIGHT_AXIS, 0.0), LocalToWorld).xyz); float3 FaceUpWS = QITA_SAFE_NORM3(mul(float4(FACE_UP_AXIS, 0.0), LocalToWorld).xyz); float3 FaceCenterWS = ObjectTranslatedWorld + mul(float4(FACE_CENTER_OFFSET_LOCAL, 0.0), LocalToWorld).xyz; float2 TexCoord = UV0; float3 BaseColor = Texture2DSample(ColorTexture, ColorTextureSampler, UV0).rgb; float KeyScore = 0.0; float3 KeyLightDir = FaceForwardWS; float KeyVisualEnergy = 0.0; float KeyIsDirectional = 0.0; uint KeyLocalLightIndex = 0; float SecondScore = 0.0; float3 SecondLightDir = FaceForwardWS; float SecondVisualEnergy = 0.0; float SecondIsDirectional = 0.0; uint SecondLocalLightIndex = 0; float KeyMask = 0.0; float OriginalKeyMask = 0.0; float FusedKeySignedSDF = SDF_NO_SHADOW_VALUE; float FillShadowAccum = 0.0; float CrossSideFadeAccum = 0.0; float PixelVisualEnergy = 0.0; #define QITA_PUSH_TOP2(CAND_SCORE, CAND_DIR, CAND_VISUAL, CAND_IS_DIRECTIONAL, CAND_INDEX) \ { \ float _CandidateScore = (CAND_SCORE); \ if (_CandidateScore > KeyScore) \ { \ SecondScore = KeyScore; \ SecondLightDir = KeyLightDir; \ SecondVisualEnergy = KeyVisualEnergy; \ SecondIsDirectional = KeyIsDirectional; \ SecondLocalLightIndex = KeyLocalLightIndex; \ KeyScore = _CandidateScore; \ KeyLightDir = (CAND_DIR); \ KeyVisualEnergy = (CAND_VISUAL); \ KeyIsDirectional = (CAND_IS_DIRECTIONAL); \ KeyLocalLightIndex = (CAND_INDEX); \ } \ else if (_CandidateScore > SecondScore) \ { \ SecondScore = _CandidateScore; \ SecondLightDir = (CAND_DIR); \ SecondVisualEnergy = (CAND_VISUAL); \ SecondIsDirectional = (CAND_IS_DIRECTIONAL); \ SecondLocalLightIndex = (CAND_INDEX); \ } \ } #define QITA_SAMPLE_ATLAS_BY_FACE_DOTS(DOT_FORWARD_VALUE, SIDE_SIGN_VALUE, DOT_UP_VALUE, OUT_SIGNED_SDF) \ { \ float _MirrorWeight = smoothstep(-SDF_LR_BLEND_WIDTH, SDF_LR_BLEND_WIDTH, (SIDE_SIGN_VALUE) * SDF_LR_SIGN_FLIP); \ float2 _RawCellUV = saturate(TexCoord); \ float _CellU_A = lerp(SDF_CELL_UV_PADDING, 1.0 - SDF_CELL_UV_PADDING, _RawCellUV.x); \ float _CellU_B = lerp(SDF_CELL_UV_PADDING, 1.0 - SDF_CELL_UV_PADDING, 1.0 - _RawCellUV.x); \ float _CellV = lerp(SDF_CELL_UV_PADDING, 1.0 - SDF_CELL_UV_PADDING, _RawCellUV.y); \ float2 _CellUV_A = float2(_CellU_A, _CellV); \ float2 _CellUV_B = float2(_CellU_B, _CellV); \ float2 _Angle01 = 1.0 - acos(clamp(float2(DOT_FORWARD_VALUE, DOT_UP_VALUE), -1.0, 1.0)) * 0.31830988618; \ float2 _Grid = min(floor(_Angle01 * SDF_ATLAS_MAX_CELL), float2(SDF_ATLAS_MAX_CELL, SDF_ATLAS_MAX_CELL)); \ float2 _Frac = frac(_Angle01 * SDF_ATLAS_MAX_CELL); \ float2 _G00 = _Grid; \ float2 _G10 = min(_Grid + float2(1.0, 0.0), float2(SDF_ATLAS_MAX_CELL, SDF_ATLAS_MAX_CELL)); \ float2 _G01 = min(_Grid + float2(0.0, 1.0), float2(SDF_ATLAS_MAX_CELL, SDF_ATLAS_MAX_CELL)); \ float2 _G11 = min(_Grid + float2(1.0, 1.0), float2(SDF_ATLAS_MAX_CELL, SDF_ATLAS_MAX_CELL)); \ float _S00A = QITA_SAMPLE_SDF3D((_G00 + _CellUV_A) / SDF_ATLAS_SIZE); \ float _S10A = QITA_SAMPLE_SDF3D((_G10 + _CellUV_A) / SDF_ATLAS_SIZE); \ float _S01A = QITA_SAMPLE_SDF3D((_G01 + _CellUV_A) / SDF_ATLAS_SIZE); \ float _S11A = QITA_SAMPLE_SDF3D((_G11 + _CellUV_A) / SDF_ATLAS_SIZE); \ float _SampleA = lerp(lerp(_S00A, _S10A, _Frac.x), lerp(_S01A, _S11A, _Frac.x), _Frac.y); \ float _S00B = QITA_SAMPLE_SDF3D((_G00 + _CellUV_B) / SDF_ATLAS_SIZE); \ float _S10B = QITA_SAMPLE_SDF3D((_G10 + _CellUV_B) / SDF_ATLAS_SIZE); \ float _S01B = QITA_SAMPLE_SDF3D((_G01 + _CellUV_B) / SDF_ATLAS_SIZE); \ float _S11B = QITA_SAMPLE_SDF3D((_G11 + _CellUV_B) / SDF_ATLAS_SIZE); \ float _SampleB = lerp(lerp(_S00B, _S10B, _Frac.x), lerp(_S01B, _S11B, _Frac.x), _Frac.y); \ float _SDFSample = lerp(_SampleA, _SampleB, _MirrorWeight); \ OUT_SIGNED_SDF = clamp((0.5 - _SDFSample) * 2.0, -1.0, 1.0); \ } #define QITA_SAMPLE_3D_FACE_SDF_SIGNED(LIGHT_DIR_VALUE, OUT_SIGNED_SDF) \ { \ float3 _LightDir = QITA_SAFE_NORM3(LIGHT_DIR_VALUE); \ float _UpRaw = dot(FaceUpWS, _LightDir); \ float3 _Planar = _LightDir - FaceUpWS * _UpRaw; \ float _HorizontalLen = length(_Planar); \ float3 _PlanarDir = _Planar * rsqrt(max(dot(_Planar, _Planar), 0.0001)); \ float _NoHorizontal = 1.0 - step(0.0001, _HorizontalLen); \ _PlanarDir = lerp(_PlanarDir, FaceForwardWS, _NoHorizontal); \ float _DotForward = dot(FaceForwardWS, _PlanarDir); \ float _SideSign = dot(FaceRightWS, _PlanarDir); \ float _DotUp = -_UpRaw; \ float _NormalSigned = SDF_NO_SHADOW_VALUE; \ QITA_SAMPLE_ATLAS_BY_FACE_DOTS(_DotForward, _SideSign, _DotUp, _NormalSigned); \ float _FrontSign = _DotForward >= 0.0 ? 1.0 : -1.0; \ float _BeforeDotForward = abs(_DotForward); \ float _BeforeSideSign = _SideSign * _FrontSign; \ float _AfterDotForward = -_BeforeDotForward; \ float _AfterSideSign = -_BeforeSideSign; \ float _Before90Signed = SDF_NO_SHADOW_VALUE; \ float _After90Signed = SDF_NO_SHADOW_VALUE; \ QITA_SAMPLE_ATLAS_BY_FACE_DOTS(_BeforeDotForward, _BeforeSideSign, _DotUp, _Before90Signed); \ QITA_SAMPLE_ATLAS_BY_FACE_DOTS(_AfterDotForward, _AfterSideSign, _DotUp, _After90Signed); \ float _Forward3D = dot(FaceForwardWS, _LightDir); \ float _Before90Weight = smoothstep(-SDF_FRONT_BACK_BLEND_WIDTH, SDF_FRONT_BACK_BLEND_WIDTH, _Forward3D); \ float _DualSigned = lerp(_After90Signed, _Before90Signed, _Before90Weight); \ float _VerticalBlend = 1.0 - smoothstep(SDF_VERTICAL_BLEND_START, SDF_VERTICAL_BLEND_END, _HorizontalLen); \ OUT_SIGNED_SDF = lerp(_NormalSigned, _DualSigned, _VerticalBlend); \ } #if GM_BASEPASS_FORWARD_LIGHT_ACCESS && FEATURE_LEVEL >= FEATURE_LEVEL_SM5 const uint EyeIndex = GM_GetBasePassForwardEyeIndex(Parameters); const uint GridIndex = GM_GetForwardLightGridIndex(Parameters); const FCulledLightsGridHeader Header = GetCulledLightsGridHeader(GridIndex); const uint NumLights = min(Header.NumLights, GetMaxLightsPerCell()); const FDirectionalLightData DirectionalLightData = GetDirectionalLightData(); float3 DirectionalLightDirStored = FaceForwardWS; float DirectionalVisualEnergyStored = 0.0; float DirectionalScoreStored = 0.0; if (DirectionalLightData.HasDirectionalLight != 0) { DirectionalLightDirStored = QITA_SAFE_NORM3(DirectionalLightData.DirectionalLightDirection); float DirectionalRawIntensity = dot(DirectionalLightData.DirectionalLightColor, float3(0.299, 0.587, 0.114)); DirectionalVisualEnergyStored = max((DirectionalRawIntensity * QITA_DIRECTIONAL_VISUAL_INTENSITY_FIX) / max(DIRECTIONAL_LIGHT_VISUAL_REFERENCE_INTENSITY, 0.001), 0.0); DirectionalScoreStored = DirectionalVisualEnergyStored * SDF_DIRECTIONAL_KEY_WEIGHT; QITA_PUSH_TOP2(DirectionalScoreStored, DirectionalLightDirStored, DirectionalVisualEnergyStored, 1.0, 0); } LOOP for (uint LightIndex = 0; LightIndex < NumLights; ++LightIndex) { const FLocalLightData LocalLight = GetLocalLightDataFromGrid(Header.DataStartIndex + LightIndex, EyeIndex); float3 LightPos = UnpackLightTranslatedWorldPosition(LocalLight); float3 FaceToLight = LightPos - FaceCenterWS; float FaceDistSqr = dot(FaceToLight, FaceToLight); float3 LightDir = QITA_SAFE_NORM3(FaceToLight); float InvRadius = UnpackLightInvRadius(LocalLight); float Dist01Sqr = FaceDistSqr * InvRadius * InvRadius; float Fade = saturate(1.0 - Dist01Sqr * Dist01Sqr); float Atten = Fade * Fade * Fade; float3 LightColor = UnpackLightColor(LocalLight); float RawIntensity = dot(LightColor, float3(0.299, 0.587, 0.114)); float VisualEnergy = Atten * max(RawIntensity / max(LOCAL_LIGHT_VISUAL_REFERENCE_INTENSITY, 0.001), 0.0) * SDF_LOCAL_VISUAL_RESPONSE_SCALE; float LocalScore = VisualEnergy * SDF_LOCAL_KEY_WEIGHT; QITA_PUSH_TOP2(LocalScore, LightDir, VisualEnergy, 0.0, LightIndex); } if (KeyScore > 0.000001) { float KeySignedSDF = SDF_NO_SHADOW_VALUE; QITA_SAMPLE_3D_FACE_SDF_SIGNED(KeyLightDir, KeySignedSDF); OriginalKeyMask = QITA_TO_SDF_MASK(KeySignedSDF); FusedKeySignedSDF = KeySignedSDF; if (SecondScore > 0.000001) { float3 SecondDirN = QITA_SAFE_NORM3(SecondLightDir); float3 KeyDirN = QITA_SAFE_NORM3(KeyLightDir); float SecondRatio = min(SecondScore / max(KeyScore, 0.0001), SDF_FILL_RATIO_MAX); float SameSideRaw = dot(FaceRightWS, SecondDirN) * dot(FaceRightWS, KeyDirN); float SameSideGate = smoothstep(SDF_SAME_SIDE_RAW_START, SDF_SAME_SIDE_RAW_END, SameSideRaw); float CrossSideGate = 1.0 - SameSideGate; float SameSideContribGate = smoothstep(SDF_SAME_SIDE_CONTRIB_START, SDF_SAME_SIDE_CONTRIB_END, SecondRatio); float SameSideFuseWeight = saturate(SameSideGate * SameSideContribGate); if (SameSideFuseWeight > 0.0001) { float SecondSignedSDF = SDF_NO_SHADOW_VALUE; QITA_SAMPLE_3D_FACE_SDF_SIGNED(SecondDirN, SecondSignedSDF); float WeightedSecondSignedSDF = lerp(-SDF_NO_SHADOW_VALUE, SecondSignedSDF, SameSideFuseWeight); QITA_SMOOTH_MAX_SIGNED(FusedKeySignedSDF, WeightedSecondSignedSDF, SDF_SAME_SIDE_SMOOTH_UNION_K, FusedKeySignedSDF); } float CrossFade = CrossSideGate * smoothstep(SDF_CROSS_SIDE_FADE_START, SDF_CROSS_SIDE_FADE_END, SecondRatio); CrossSideFadeAccum = max(CrossSideFadeAccum, CrossFade); float SecondFaceGate = smoothstep(SDF_FILL_FACE_GATE_START, SDF_FILL_FACE_GATE_END, dot(FaceForwardWS, SecondDirN)); float SecondShadowSide = 1.0 - smoothstep(SDF_FILL_SHADOW_SIDE_START, SDF_FILL_SHADOW_SIDE_END, dot(SecondDirN, KeyDirN)); float SecondGlobalMask = saturate(SecondFaceGate * SecondShadowSide * CrossSideGate); FillShadowAccum += SecondGlobalMask * SecondRatio * SDF_FILL_SHADOW_STRENGTH; float SecondVisualMask = saturate(OriginalKeyMask * SecondFaceGate + (1.0 - OriginalKeyMask) * max(SecondGlobalMask, SameSideFuseWeight * SecondFaceGate + CrossFade)); PixelVisualEnergy += SecondVisualEnergy * SecondVisualMask * SDF_FILL_VISUAL_SCALE; } if (DirectionalLightData.HasDirectionalLight != 0 && KeyIsDirectional < 0.5 && SecondIsDirectional < 0.5) { float3 FillDirN = QITA_SAFE_NORM3(DirectionalLightDirStored); float3 KeyDirN = QITA_SAFE_NORM3(KeyLightDir); float FillRatio = min(DirectionalScoreStored / max(KeyScore, 0.0001), SDF_FILL_RATIO_MAX); float SameSideRaw = dot(FaceRightWS, FillDirN) * dot(FaceRightWS, KeyDirN); float SameSideGate = smoothstep(SDF_SAME_SIDE_RAW_START, SDF_SAME_SIDE_RAW_END, SameSideRaw); float CrossSideGate = 1.0 - SameSideGate; float FillFaceGate = smoothstep(SDF_FILL_FACE_GATE_START, SDF_FILL_FACE_GATE_END, dot(FaceForwardWS, FillDirN)); float FillShadowSide = 1.0 - smoothstep(SDF_FILL_SHADOW_SIDE_START, SDF_FILL_SHADOW_SIDE_END, dot(FillDirN, KeyDirN)); float FillGlobalMask = saturate(FillFaceGate * FillShadowSide * CrossSideGate); FillShadowAccum += FillGlobalMask * FillRatio * SDF_FILL_SHADOW_STRENGTH; float FillVisualMask = saturate(OriginalKeyMask * FillFaceGate + (1.0 - OriginalKeyMask) * FillGlobalMask); PixelVisualEnergy += DirectionalVisualEnergyStored * FillVisualMask * SDF_FILL_VISUAL_SCALE; } LOOP for (uint LightIndex = 0; LightIndex < NumLights; ++LightIndex) { bool UsedAsKey = (KeyIsDirectional < 0.5 && LightIndex == KeyLocalLightIndex); bool UsedAsSecond = (SecondScore > 0.000001 && SecondIsDirectional < 0.5 && LightIndex == SecondLocalLightIndex); if (!UsedAsKey && !UsedAsSecond) { const FLocalLightData LocalLight = GetLocalLightDataFromGrid(Header.DataStartIndex + LightIndex, EyeIndex); float3 LightPos = UnpackLightTranslatedWorldPosition(LocalLight); float3 FaceToLight = LightPos - FaceCenterWS; float FaceDistSqr = dot(FaceToLight, FaceToLight); float3 LightDir = QITA_SAFE_NORM3(FaceToLight); float InvRadius = UnpackLightInvRadius(LocalLight); float Dist01Sqr = FaceDistSqr * InvRadius * InvRadius; float Fade = saturate(1.0 - Dist01Sqr * Dist01Sqr); float Atten = Fade * Fade * Fade; float3 LightColor = UnpackLightColor(LocalLight); float RawIntensity = dot(LightColor, float3(0.299, 0.587, 0.114)); float VisualEnergy = Atten * max(RawIntensity / max(LOCAL_LIGHT_VISUAL_REFERENCE_INTENSITY, 0.001), 0.0) * SDF_LOCAL_VISUAL_RESPONSE_SCALE; float LocalScore = VisualEnergy * SDF_LOCAL_KEY_WEIGHT; float3 FillDirN = QITA_SAFE_NORM3(LightDir); float3 KeyDirN = QITA_SAFE_NORM3(KeyLightDir); float FillRatio = min(LocalScore / max(KeyScore, 0.0001), SDF_FILL_RATIO_MAX); float SameSideRaw = dot(FaceRightWS, FillDirN) * dot(FaceRightWS, KeyDirN); float SameSideGate = smoothstep(SDF_SAME_SIDE_RAW_START, SDF_SAME_SIDE_RAW_END, SameSideRaw); float CrossSideGate = 1.0 - SameSideGate; float FillFaceGate = smoothstep(SDF_FILL_FACE_GATE_START, SDF_FILL_FACE_GATE_END, dot(FaceForwardWS, FillDirN)); float FillShadowSide = 1.0 - smoothstep(SDF_FILL_SHADOW_SIDE_START, SDF_FILL_SHADOW_SIDE_END, dot(FillDirN, KeyDirN)); float FillGlobalMask = saturate(FillFaceGate * FillShadowSide * CrossSideGate); FillShadowAccum += FillGlobalMask * FillRatio * SDF_FILL_SHADOW_STRENGTH; float FillVisualMask = saturate(OriginalKeyMask * FillFaceGate + (1.0 - OriginalKeyMask) * FillGlobalMask); PixelVisualEnergy += VisualEnergy * FillVisualMask * SDF_FILL_VISUAL_SCALE; } } KeyMask = QITA_TO_SDF_MASK(FusedKeySignedSDF); KeyMask = lerp(KeyMask, 1.0, saturate(CrossSideFadeAccum)); PixelVisualEnergy += KeyVisualEnergy * KeyMask; } #endif float FillShadowMask = saturate(FillShadowAccum); float SDFMask = saturate(KeyMask + (1.0 - KeyMask) * FillShadowMask); float FinalMask = lerp(SDF_SHADOW_MIN, 1.0, SDFMask); float VisualLitMask = smoothstep(SDF_VISUAL_LIT_START, SDF_VISUAL_LIT_END, SDFMask); float VisualBoost = min(max(PixelVisualEnergy * SDF_VISUAL_COLOR_SCALE, 0.0), SDF_VISUAL_COLOR_MAX); FinalMask = lerp(FinalMask, 1.0, VisualBoost * VisualLitMask); float3 FinalTint = lerp(ShadowColor, NormalColor, FinalMask); float3 ResultColor = FinalTint * BaseColor; float ExposureMask = smoothstep(SDF_EXPOSURE_LIT_START, SDF_EXPOSURE_LIT_END, SDFMask); ExposureMask = pow(saturate(ExposureMask), SDF_EXPOSURE_LIT_POWER); float ExposureBoost = max(PixelVisualEnergy * SDF_EXPOSURE_SCALE, 0.0); ResultColor += (NormalColor * BaseColor) * ExposureBoost * ExposureMask; #undef QITA_PUSH_TOP2 #undef QITA_SMOOTH_MAX_SIGNED #undef QITA_TO_SDF_MASK #undef QITA_SAMPLE_3D_FACE_SDF_SIGNED #undef QITA_SAMPLE_ATLAS_BY_FACE_DOTS #undef QITA_SAMPLE_SDF3D #undef QITA_SAFE_NORM3 #undef FACE_FORWARD_AXIS #undef FACE_RIGHT_AXIS #undef FACE_UP_AXIS #undef FACE_CENTER_OFFSET_LOCAL #undef SDF_ATLAS_SIZE #undef SDF_ATLAS_MAX_CELL #undef SDF_CELL_UV_PADDING #undef SDF_EDGE_WIDTH #undef SDF_SHADOW_MIN #undef SDF_LIGHT_POWER #undef SDF_NO_SHADOW_VALUE #undef LOCAL_LIGHT_VISUAL_REFERENCE_INTENSITY #undef DIRECTIONAL_LIGHT_VISUAL_REFERENCE_INTENSITY #undef QITA_DIRECTIONAL_VISUAL_INTENSITY_FIX #undef SDF_LOCAL_VISUAL_RESPONSE_SCALE #undef SDF_LOCAL_KEY_WEIGHT #undef SDF_DIRECTIONAL_KEY_WEIGHT #undef SDF_FILL_SHADOW_STRENGTH #undef SDF_FILL_RATIO_MAX #undef SDF_FILL_VISUAL_SCALE #undef SDF_FILL_FACE_GATE_START #undef SDF_FILL_FACE_GATE_END #undef SDF_FILL_SHADOW_SIDE_START #undef SDF_FILL_SHADOW_SIDE_END #undef SDF_SAME_SIDE_RAW_START #undef SDF_SAME_SIDE_RAW_END #undef SDF_SAME_SIDE_CONTRIB_START #undef SDF_SAME_SIDE_CONTRIB_END #undef SDF_SAME_SIDE_SMOOTH_UNION_K #undef SDF_CROSS_SIDE_FADE_START #undef SDF_CROSS_SIDE_FADE_END #undef SDF_VISUAL_COLOR_SCALE #undef SDF_VISUAL_COLOR_MAX #undef SDF_VISUAL_LIT_START #undef SDF_VISUAL_LIT_END #undef SDF_EXPOSURE_SCALE #undef SDF_EXPOSURE_LIT_START #undef SDF_EXPOSURE_LIT_END #undef SDF_EXPOSURE_LIT_POWER #undef SDF_VERTICAL_BLEND_START #undef SDF_VERTICAL_BLEND_END #undef SDF_FRONT_BACK_BLEND_WIDTH #undef SDF_LR_BLEND_WIDTH #undef SDF_LR_SIGN_FLIP return ResultColor;
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 18:35:52

COM3D2女仆调校器:实时修改游戏角色属性的终极解决方案

COM3D2女仆调校器&#xff1a;实时修改游戏角色属性的终极解决方案 【免费下载链接】COM3D2.MaidFiddler Maid Fiddler for COM3D2 -- a real-time value editor for COM3D2 项目地址: https://gitcode.com/gh_mirrors/co/COM3D2.MaidFiddler COM3D2.MaidFiddler是专为C…

作者头像 李华
网站建设 2026/6/10 18:29:09

轮播图,河北软件职业技术学院介绍

代码如下&#xff1a;Entry Component struct Tab{private bannerList:Resource[] [$r(app.media.first),$r(app.media.second),$r(app.media.third)]build() {Tabs(){TabContent(){Column({space:40}) {Text(欢迎来到河北软件职业技术学院).fontSize(24)Column() {Swiper() {…

作者头像 李华
网站建设 2026/6/10 18:29:06

i.MX RT1060X引脚与BGA封装设计实战:从数据手册到PCB布局

1. 项目概述&#xff1a;从数据手册到电路板&#xff0c;如何驾驭i.MX RT1060X的引脚迷宫搞嵌入式硬件设计&#xff0c;尤其是用上像NXP i.MX RT1060X这类高性能跨界处理器&#xff0c;拿到芯片数据手册的第一眼&#xff0c;很多人都会有点发怵。几百页的文档&#xff0c;最让人…

作者头像 李华
网站建设 2026/6/10 18:27:13

i.MX RT1160接口时序与引脚配置实战:从数据手册到稳定电路设计

1. 从数据手册到电路板&#xff1a;i.MX RT1160接口时序的实战解读如果你是一位嵌入式硬件工程师&#xff0c;拿到一颗像i.MX RT1160这样的高性能跨界处理器&#xff0c;第一感觉可能是兴奋&#xff0c;紧接着就是压力。数据手册动辄上千页&#xff0c;其中最让人头疼又无法回避…

作者头像 李华
网站建设 2026/6/10 18:25:45

开源Qobuz无损音乐下载工具:构建您的个人高解析度音乐库

开源Qobuz无损音乐下载工具&#xff1a;构建您的个人高解析度音乐库 【免费下载链接】qobuz-dl A complete Lossless and Hi-Res music downloader for Qobuz 项目地址: https://gitcode.com/gh_mirrors/qo/qobuz-dl 您是否曾梦想拥有一个私人的无损音乐宝库&#xff1f…

作者头像 李华