-
Notifications
You must be signed in to change notification settings - Fork 137
feat: advanced skin #2428
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
SkrubbySkrubInAShrub
merged 128 commits into
community-shaders:dev
from
jiayev:advanced-skin
Jun 1, 2026
Merged
feat: advanced skin #2428
Changes from all commits
Commits
Show all changes
128 commits
Select commit
Hold shift + click to select a range
ac8490d
feat: check feature compatibility (#1136)
alandtse 6cc1f85
ci: build cpp only when cpp changed (#1140)
alandtse 961dc21
build: remove pause from build script (#1142)
alandtse 32215bf
ci: create fallback for tj-actions/changed-files (#1146)
alandtse ca045b4
ci: add hlsl validation (#1145)
alandtse 5061c39
ci: treat skipped build or validation as success (#1148)
alandtse ea0c052
fix: detect core features properly (#1147)
alandtse 6b340e8
fix(extended materials): green channel detection (#1152)
doodlum 77ce8e2
ci: always run cpp-build (#1149)
alandtse c51ad1e
fix: support seasons swaps in PBR and TerrainHelper (#1099)
hakasapl c9ca0f8
fix: fix detection of deleted obsolete features (#1157)
alandtse f51d371
feat: add fast random float gen based on pcg (#1158)
sicsix b161382
feat(ui): organize features under subheadings (#1155)
davo0411 8c64f5d
feat: add raindrop ripples on water (#577)
TheRiverwoodModder 8a3b7f6
feat(ui): add Icon Support (#1107)
davo0411 fb52eae
refactor: move BRDFs to separate file (#1161)
jiayev 8363e96
squash
jiayev 37c6a2a
initial
jiayev 1e699f9
style: 🎨 apply clang-format changes
jiayev 6303182
get it work in setupmaterial
jiayev 8dfe692
style: 🎨 apply clang-format changes
jiayev 479378a
kinda work
jiayev 130764f
rework on skin wetness
jiayev 025bdf6
small work on wetness
jiayev af96381
fix crash on situations
jiayev c1500db
separate wetness map
jiayev 05b846e
fix crash and loading
jiayev b431d31
fix pbrglossiness for wet
jiayev 9c6cf49
small ao trick
jiayev e43ab29
wetness rework
jiayev 7ca88b1
keep working on sweat
jiayev 7e530da
mix instead of mult
jiayev 97d9f4d
keep working on skin wetness
jiayev 980bb01
smoother, fix indirect
jiayev a3ef96c
slightly better
jiayev 3114bbd
dynamic wetness
jiayev a004cfa
work for hair
jiayev c784bb7
add wetness to clothes
jiayev f0ac7df
making it more obvious
jiayev 548231d
more detailed wetness
jiayev cfa32a2
prevent sweat on metal
jiayev 633b3f2
not applying extra on other stuff
jiayev 6133c29
change to per geom
jiayev b0e0af2
add support for wet normal
jiayev 5f9e717
use pow to make it more like droplets
jiayev c305210
extra wetness for hair
jiayev a0d799f
smol change
jiayev 8ba2f6d
revert to the detail before
jiayev 2345d2b
fix conditions
jiayev 81af8d5
just use combined wet normal
jiayev 8bf33e2
set to 0 to disable
jiayev f36dcb6
apply dual lobe on indirect
jiayev 4aa6bbe
add info
jiayev 923b26b
use new BRDF
jiayev f14289b
fix typo
jiayev 32aac4f
refactor to avoid sampling when disabled
jiayev dc84cff
Merge branch 'dev' into advanced-skin
jiayev 1910bd4
had to apply skin on non deferred
jiayev 7aab8de
fix some nondeferred flickering
jiayev 3c61b61
disable hook when disable skin
jiayev f997ea1
Merge branch 'dev' into advanced-skin
jiayev 34fbeda
Merge branch 'dev' into advanced-skin
jiayev cd151a6
Merge branch 'dev' into advanced-skin
jiayev d534a2c
remove the detail ao because it looks bad
jiayev 68996d8
Merge branch 'dev' into advanced-skin
jiayev 3486a2a
Merge branch 'dev' into advanced-skin
jiayev b929b3f
fix hlsl error
jiayev 6973bf8
Merge branch 'dev' into advanced-skin
jiayev 610a9a5
Merge branch 'dev' into advanced-skin
jiayev f35c3a5
Merge branch 'dev' into advanced-skin
jiayev 58a4e37
Merge branch 'dev' into advanced-skin
jiayev ebaddec
fix compile error
jiayev e73623b
skin getsingleton fix
jiayev 13b3eb6
separate skin ao
jiayev b5cb19e
fix mask
jiayev d46caff
fix skin off
jiayev 8a9c3d4
change pi to PBRLightingCompensation
jiayev 4468881
fix skin decal
jiayev 6535aa4
temp disable wetness, fix tex loading
jiayev f25906a
revert special skin reflectance
jiayev 4fe7c7d
skip in another way
jiayev fb652e3
fix wetness logic
jiayev 2642d4a
really disable
jiayev 0119bf6
Merge branch 'dev' into advanced-skin
jiayev c809cfc
better handling extra texture
jiayev 6c05c15
fix compile error
jiayev 5828dc3
fix compile error
jiayev 8241fbd
reenable wetness
jiayev 626ca94
Merge branch 'dev' into advanced-skin
jiayev ffb0205
Merge branch 'dev' into advanced-skin
jiayev 56b025f
Merge branch 'dev' into advanced-skin
jiayev fef2275
saturate curvature to avoid negative value
jiayev a86541f
Merge branch 'dev' into advanced-skin
jiayev 83641bb
Merge branch 'dev' into advanced-skin
jiayev 0abc151
Merge branch 'dev' into advanced-skin
jiayev a7dcf2d
Merge branch 'dev' into advanced-skin
jiayev 5c26a31
adapt to dev
jiayev 3635ce7
Merge branch 'dev' into advanced-skin
jiayev 5036bb5
separate wet normal for specular
jiayev 40ccf3a
Merge branch 'dev' into advanced-skin
jiayev 133186f
fix broken detail
jiayev 7d254f0
Merge branch 'dev' into advanced-skin
jiayev b6e69f6
Merge branch 'dev' into advanced-skin
jiayev 2e2db8c
no more sweat at dead
jiayev fd01c8c
add dw api
jiayev ed07ade
fix code
jiayev 0f1f592
Merge branch 'dev' into advanced-skin
jiayev 541368c
Merge remote-tracking branch 'origin/dev' into advanced-skin
jiayev 0c2566d
reduce skin detail bias
jiayev c6a3459
Merge branch 'dev' into advanced-skin
jiayev 3aba25b
fix(Lighting): enhance soft lighting calculations in EvaluateLighting…
jiayev 7051ba8
Merge branch 'dev' into advanced-skin
jiayev 006d2ab
Merge branch 'dev' into advanced-skin
jiayev 8354492
fix shader
jiayev 2171ed2
Merge branch 'dev' into advanced-skin
jiayev 33ca1c9
Merge branch 'dev' into advanced-skin
jiayev e1fd330
fix(skin): install hooks in PostPostLoad for BSLightingShader
jiayev 7266bbe
fix: rename SetShaderResouces to SetShaderResources for consistency
jiayev 459a362
feat(skin): bone-anchored water wetness with temporal persistence
jiayev b18638e
fix(skin): improve tooltip descriptions for skin wetness settings
jiayev 4b8a9e2
fix(skin): decouple bone wetness from skinPerGeometry.y fade
jiayev e2100b3
Merge branch 'dev' into advanced-skin
jiayev 9ba5678
fix double to float
jiayev 32e69f3
Merge branch 'dev' into advanced-skin
jiayev 50039fc
revert per bone wetness
jiayev eacb63f
Merge branch 'dev' into advanced-skin
jiayev 27c674e
feat(skin): implement water height caching for improved wetness calcu…
jiayev 6662499
prevent 0 fade time
jiayev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| [Info] | ||
| Version = 1-0-0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,300 @@ | ||
| #ifndef __SKIN_HLSLI__ | ||
| #define __SKIN_HLSLI__ | ||
|
|
||
| #include "Common/BRDF.hlsli" | ||
| #include "Common/Color.hlsli" | ||
| #include "Common/LightingCommon.hlsli" | ||
| #include "Common/Math.hlsli" | ||
| #include "Common/Shading.hlsli" | ||
| #include "Common/SharedData.hlsli" | ||
|
|
||
| namespace Skin | ||
| { | ||
| float CalculateCurvature(float3 N) | ||
| { | ||
| const float3 dNdx = ddx(N); | ||
| const float3 dNdy = ddy(N); | ||
| return length(float2(dot(dNdx, dNdx), dot(dNdy, dNdy))); | ||
| } | ||
|
|
||
| #if defined(PSHADER) | ||
| cbuffer SkinPerGeometry : register(b7) | ||
| { | ||
| float4 skinPerGeometry; | ||
| }; | ||
| #endif | ||
| #if defined(SKIN) | ||
| Texture2D<float4> TexSkinDetailNormal : register(t72); | ||
|
|
||
| // [Jorge Jimenez, Diego Gutierrez 2015, "Separable Subsurface Scattering"] | ||
| // https://www.iryoku.com/separable-sss/ | ||
| float3 SSSSTransmittance(float translucency, float sssWidth, float3 worldNormal, float3 light, float d) | ||
| { | ||
| /** | ||
| * Calculate the scale of the effect. | ||
| */ | ||
| float scale = 8.25 * (1.0 - translucency) / sssWidth; | ||
|
|
||
| /** | ||
| * First we shrink the position inwards the surface to avoid artifacts: | ||
| * (Note that this can be done once for all the lights) | ||
| */ | ||
| // float4 shrinkedPos = float4(worldPosition - 0.005 * worldNormal, 1.0); | ||
|
|
||
| /** | ||
| * Now we calculate the thickness from the light point of view: | ||
| */ | ||
| // float4 shadowPosition = mul(shrinkedPos, lightViewProjection); | ||
| // float d1 = SSSSSampleShadowmap(shadowPosition.xy / shadowPosition.w).r; // 'd1' has a range of 0..1 | ||
| // float d2 = shadowPosition.z; // 'd2' has a range of 0..'lightFarPlane' | ||
| // d1 *= lightFarPlane; // So we scale 'd1' accordingly: | ||
| // float d = scale * abs(d1 - d2); | ||
| d = scale * abs(d); // Use the passed 'd' value instead of calculating it here. | ||
|
|
||
| /** | ||
| * Armed with the thickness, we can now calculate the color by means of the | ||
| * precalculated transmittance profile. | ||
| * (It can be precomputed into a texture, for maximum performance): | ||
| */ | ||
| float dd = -d * d; | ||
| float3 profile = float3(0.233, 0.455, 0.649) * exp(dd / 0.0064) + | ||
| float3(0.1, 0.336, 0.344) * exp(dd / 0.0484) + | ||
| float3(0.118, 0.198, 0.0) * exp(dd / 0.187) + | ||
| float3(0.113, 0.007, 0.007) * exp(dd / 0.567) + | ||
| float3(0.358, 0.004, 0.0) * exp(dd / 1.99) + | ||
| float3(0.078, 0.0, 0.0) * exp(dd / 7.41); | ||
|
|
||
| /** | ||
| * Using the profile, we finally approximate the transmitted lighting from | ||
| * the back of the object: | ||
| */ | ||
| return profile * saturate(0.3 + dot(light, -worldNormal)); | ||
| } | ||
|
|
||
| float3 DualSpecularGGX(float AverageRoughness, float Lobe0Roughness, float Lobe1Roughness, float LobeMix, float3 SpecularColor, float NdotL, float NdotV, float NdotH, float VdotH, out float3 F) | ||
| { | ||
| float D = lerp(BRDF::D_GGX(Lobe0Roughness, NdotH), BRDF::D_GGX(Lobe1Roughness, NdotH), LobeMix); | ||
| float G = BRDF::Vis_SmithJointApprox(AverageRoughness, NdotV, NdotL); | ||
| F = BRDF::F_Schlick(SpecularColor, VdotH); | ||
|
|
||
| return D * G * F; | ||
| } | ||
|
|
||
| // a contact shadow approximation, totally not physically correct; a riff on "Chan 2018, "Material Advances in Call of Duty: WWII" and "The Technical Art of Uncharted 4" http://advances.realtimerendering.com/other/2016/naughty_dog/NaughtyDog_TechArt_Final.pdf (microshadowing)" | ||
| float ApproximateDirectOcculusion(float aoVisibility, float NdotL) | ||
| { | ||
| float aperture = rsqrt(1.0000001 - aoVisibility); | ||
| NdotL += 0.1; // when using bent normals, avoids overshadowing - bent normals are just approximation anyhow | ||
| return saturate(NdotL * aperture); | ||
| } | ||
|
|
||
| void SkinDirectLightInput( | ||
| out DirectLightingOutput lightingOutput, | ||
| DirectContext context, | ||
| MaterialProperties material) | ||
| { | ||
| lightingOutput = (DirectLightingOutput)0; | ||
| context.lightColor *= Color::PBRLightingCompensation * context.detailedShadow; | ||
|
|
||
| const float3 N = context.worldNormal; | ||
| const float3 V = context.viewDir; | ||
| const float3 L = context.lightDir; | ||
| const float3 H = context.halfVector; | ||
|
|
||
| const float oNdotL = dot(N, L); | ||
| float NdotL = clamp(oNdotL, 1e-5, 1.0); | ||
| float NdotV = saturate(abs(dot(N, V)) + 1e-5); | ||
| float NdotH = saturate(dot(N, H)); | ||
| float VdotH = saturate(dot(V, H)); | ||
|
|
||
| context.lightColor *= ApproximateDirectOcculusion(material.AO, NdotL); | ||
|
|
||
| float averageRoughness = lerp(material.Roughness, material.RoughnessSecondary, material.SecondarySpecIntensity); | ||
|
|
||
| lightingOutput.diffuse += context.lightColor * NdotL * BRDF::Diffuse_Burley(averageRoughness, NdotV, NdotL, VdotH); | ||
| float3 F; | ||
| float3 F0 = material.F0 * saturate(1 - material.Curvature); | ||
|
|
||
| lightingOutput.specular += DualSpecularGGX(averageRoughness, material.Roughness, material.RoughnessSecondary, material.SecondarySpecIntensity, F0, NdotL, NdotV, NdotH, VdotH, F) * context.lightColor * NdotL; | ||
|
|
||
| float2 specularBRDF = BRDF::EnvBRDF(averageRoughness, NdotV); | ||
| lightingOutput.specular *= 1 + F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); | ||
| lightingOutput.diffuse *= 1 - F; | ||
|
|
||
| if (material.FuzzWeight > 0.0) { | ||
| float3 FuzzF0 = material.FuzzColor * saturate(1 - material.Curvature); | ||
| float fuzzD = BRDF::D_Charlie(material.FuzzRoughness, NdotH); | ||
| float fuzzG = BRDF::Vis_Neubelt(NdotV, NdotL); | ||
| float3 fuzzF = BRDF::F_Schlick(FuzzF0, VdotH); | ||
| float3 fuzzSpecular = fuzzD * fuzzG * fuzzF * context.lightColor * NdotL; | ||
| float2 fuzzSpecularBRDF = BRDF::EnvBRDFApproxLazarov(material.FuzzRoughness, NdotV); | ||
| fuzzSpecular *= 1 + material.FuzzColor * (1 / (fuzzSpecularBRDF.x + fuzzSpecularBRDF.y) - 1); | ||
|
|
||
| lightingOutput.specular += fuzzSpecular * material.FuzzWeight; | ||
| } | ||
|
|
||
| float3 sssTransmittance = SSSSTransmittance( | ||
| SharedData::skinData.sssParams.x, | ||
| SharedData::skinData.sssParams.y, | ||
| N, | ||
| L, | ||
| material.Thickness) * | ||
| SharedData::skinData.sssParams.w; | ||
| lightingOutput.transmission = min(sssTransmittance * context.lightColor * context.softShadow * material.BaseColor, context.lightColor); | ||
| } | ||
|
|
||
| void SkinIndirectLobeWeights( | ||
| out IndirectLobeWeights lobeWeights, | ||
| MaterialProperties material, | ||
| IndirectContext context) | ||
| { | ||
| lobeWeights = (IndirectLobeWeights)0; | ||
|
|
||
| const float3 N = context.worldNormal; | ||
| const float3 V = context.viewDir; | ||
| const float3 VN = context.vertexNormal; | ||
|
|
||
| float NdotV = saturate(dot(N, V)); | ||
|
|
||
| float averageRoughness = lerp(material.Roughness, material.RoughnessSecondary, material.SecondarySpecIntensity); | ||
|
|
||
| float2 specularBRDF = BRDF::EnvBRDF(averageRoughness, NdotV); | ||
|
|
||
| lobeWeights.specular = material.F0 * specularBRDF.x + specularBRDF.y; | ||
|
|
||
| lobeWeights.diffuse = material.BaseColor * (1.0 - lobeWeights.specular.x - lobeWeights.specular.y); | ||
| lobeWeights.specular *= 1 + material.F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); | ||
|
|
||
| float3 R = reflect(-V, N); | ||
| float horizon = min(1.0 + dot(R, VN), 1.0); | ||
| horizon *= horizon; | ||
| lobeWeights.specular *= horizon; | ||
|
|
||
| float3 diffuseAO = material.AO; | ||
| float3 specularAO = SpecularAOLagarde(NdotV, material.AO, averageRoughness); | ||
|
|
||
| diffuseAO = MultiBounceAO(material.BaseColor, diffuseAO.x).y; | ||
| specularAO = MultiBounceAO(material.F0, specularAO.x).y; | ||
|
|
||
| lobeWeights.diffuse *= diffuseAO; | ||
| lobeWeights.specular *= specularAO; | ||
|
|
||
| lobeWeights.specular *= saturate(1 - material.Curvature); | ||
| } | ||
|
|
||
| // https://blog.selfshadow.com/publications/blending-in-detail/ | ||
| // geometric normal s, a base normal t and a secondary (or detail) normal u | ||
| float3 ReorientNormal(float3 u, float3 t, float3 s) | ||
| { | ||
| // Build the shortest-arc quaternion | ||
| float4 q = float4(cross(s, t), dot(s, t) + 1) / sqrt(2 * (dot(s, t) + 1)); | ||
|
|
||
| // Rotate the normal | ||
| return u * (q.w * q.w - dot(q.xyz, q.xyz)) + 2 * q.xyz * dot(q.xyz, u) + 2 * q.w * cross(q.xyz, u); | ||
|
jiayev marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // for when s = (0,0,1) | ||
| float3 ReorientNormal(float3 n1, float3 n2) | ||
| { | ||
| n1 += float3(0, 0, 1); | ||
| n2 *= float3(-1, -1, 1); | ||
|
|
||
| return n1 * dot(n1, n2) / n1.z - n2; | ||
| } | ||
|
|
||
| float3x3 ReconstructTBN(float3 worldPos, float3 worldNormal, float2 uv) | ||
| { | ||
| float3 dFdx = ddx(worldPos); | ||
| float3 dFdy = ddy(worldPos); | ||
| float2 dUVdx = ddx(uv); | ||
| float2 dUVdy = ddy(uv); | ||
| float3 tangent = normalize(dFdx * dUVdy.y - dFdy * dUVdx.y); | ||
| float3 bitangent = normalize(dFdy * dUVdx.x - dFdx * dUVdy.x); | ||
| tangent = normalize(tangent - worldNormal * dot(worldNormal, tangent)); | ||
| bitangent = normalize(bitangent - worldNormal * dot(worldNormal, bitangent)); | ||
|
|
||
| return float3x3(tangent, bitangent, normalize(worldNormal)); | ||
| } | ||
|
|
||
| float3 CalculateNormalFromHeight(float height, float heightScale, float2 uv) | ||
| { | ||
| float dHdx = ddx(height); | ||
| float dHdy = ddy(height); | ||
| float2 dUVdx = ddx(uv); | ||
| float2 dUVdy = ddy(uv); | ||
|
|
||
| float det = dUVdx.x * dUVdy.y - dUVdx.y * dUVdy.x; | ||
| if (det == 0.0f) { | ||
| return float3(0, 0, 1); // Avoid division by zero | ||
| } | ||
|
|
||
| float dHdx_Tex = (dHdx * dUVdy.y - dHdy * dUVdx.y) / det; | ||
| float dHdy_Tex = (dHdy * dUVdx.x - dHdx * dUVdy.x) / det; | ||
| float3 normal = float3(-dHdx_Tex, -dHdy_Tex, 0); | ||
| return normal * heightScale + float3(0, 0, 1); | ||
| } | ||
|
|
||
| float FBM(float2 uv, float base_scale, int octaves, float lacunarity, float persistence, float z_offset_multiplier) | ||
| { | ||
| float total = 0.0; | ||
| float frequency = base_scale; | ||
| float amplitude = 1.0; | ||
| float max_amplitude = 0.0; | ||
| for (int i = 0; i < octaves; i++) { | ||
| total += amplitude * (Random::perlinNoise(float3(uv * frequency, (float)i * z_offset_multiplier)) + 1.0) * 0.5; | ||
|
|
||
| max_amplitude += amplitude; | ||
| amplitude *= persistence; | ||
| frequency *= lacunarity; | ||
| } | ||
| if (max_amplitude > 0.0) { | ||
| return total / max_amplitude; | ||
| } | ||
| return 0.0; | ||
| } | ||
|
|
||
| float PerlinNoise(float2 uv, float scale, float lacunarity, float persistence, float strength) | ||
| { | ||
| if (strength <= 0.001f) { | ||
| return 0.0f; | ||
| } | ||
| if (strength >= 0.999f) { | ||
| return 1.0f; | ||
| } | ||
| int octaves = 5; | ||
| float z_offset_multiplier = 7.375f; | ||
|
|
||
| float noise_value = FBM(uv, scale, octaves, lacunarity, persistence, z_offset_multiplier); | ||
|
|
||
| float dynamic_threshold = 1.0f - strength; | ||
|
|
||
| float sweat_intensity = saturate((noise_value - dynamic_threshold) / strength); | ||
|
|
||
| sweat_intensity = pow(sweat_intensity, 1.5f); | ||
|
|
||
| if (strength > 0.8f) { | ||
| sweat_intensity = sweat_intensity * saturate(0.99f - (strength - 0.8f) * 5.0f) + (strength - 0.8f) * 5.0f; | ||
| } | ||
| return pow(sweat_intensity, 0.1f); | ||
| } | ||
| #endif | ||
|
|
||
| float2 GetWetness(float z, float3 modelNormal) | ||
| { | ||
| if (skinPerGeometry.x == 0.f && skinPerGeometry.y == 0.f) | ||
| return 0.f; | ||
|
|
||
| float waterWet = 0.0f; | ||
| float waterLevel = skinPerGeometry.z + skinPerGeometry.w; | ||
|
|
||
| waterWet = skinPerGeometry.y * (1 - smoothstep(waterLevel - 2.5f, waterLevel + 2.5f, z)); | ||
|
|
||
| float sweatWet = skinPerGeometry.x; | ||
| #if !defined(SKIN) | ||
| sweatWet *= 1.0f - saturate(dot(modelNormal, float3(0, 0, 1))); | ||
| #endif | ||
| return float2(sweatWet, waterWet); | ||
| } | ||
| } | ||
|
|
||
| #endif // __SKIN_HLSLI__ | ||
Binary file not shown.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.