From 1f10698811a57b11d343dee62a47ec1b5610b626 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Mon, 5 Oct 2020 15:45:10 -0700 Subject: [PATCH] more toon shader control + dithered shading --- Effects/Deferred_ToonEffect.cs | 4 ++ Effects/FXB/Deferred_ToonEffect.fxb | 4 +- Effects/HLSL/Conversion.fxh | 34 +++++++++++ Effects/HLSL/Deferred_ToonEffect.fx | 68 +++++++++++++++------- Effects/HLSL/Dither.fxh | 89 +++++++++++++++++++++++++++++ Effects/HLSL/Shadow.fxh | 35 ++++++++++++ Geometry/Model.cs | 31 +++++++++- Renderer.cs | 1 + 8 files changed, 240 insertions(+), 26 deletions(-) create mode 100644 Effects/HLSL/Conversion.fxh create mode 100644 Effects/HLSL/Dither.fxh diff --git a/Effects/Deferred_ToonEffect.cs b/Effects/Deferred_ToonEffect.cs index f5d2bfe..1b1deea 100644 --- a/Effects/Deferred_ToonEffect.cs +++ b/Effects/Deferred_ToonEffect.cs @@ -8,6 +8,7 @@ namespace Kav EffectParameter gPositionParam; EffectParameter gAlbedoParam; EffectParameter gNormalParam; + EffectParameter gMetallicRoughnessParam; EffectParameter shadowMapOneParam; EffectParameter shadowMapTwoParam; @@ -31,6 +32,7 @@ namespace Kav public Texture2D GPosition { get; set; } public Texture2D GAlbedo { get; set; } public Texture2D GNormal { get; set; } + public Texture2D GMetallicRoughness { get; set; } public Texture2D ShadowMapOne { get; set; } public Texture2D ShadowMapTwo { get; set; } @@ -62,6 +64,7 @@ namespace Kav gPositionParam.SetValue(GPosition); gAlbedoParam.SetValue(GAlbedo); gNormalParam.SetValue(GNormal); + gMetallicRoughnessParam.SetValue(GMetallicRoughness); shadowMapOneParam.SetValue(ShadowMapOne); shadowMapTwoParam.SetValue(ShadowMapTwo); @@ -88,6 +91,7 @@ namespace Kav gPositionParam = Parameters["gPosition"]; gAlbedoParam = Parameters["gAlbedo"]; gNormalParam = Parameters["gNormal"]; + gMetallicRoughnessParam = Parameters["gMetallicRoughness"]; shadowMapOneParam = Parameters["shadowMapOne"]; shadowMapTwoParam = Parameters["shadowMapTwo"]; diff --git a/Effects/FXB/Deferred_ToonEffect.fxb b/Effects/FXB/Deferred_ToonEffect.fxb index bcd420f..685bdea 100644 --- a/Effects/FXB/Deferred_ToonEffect.fxb +++ b/Effects/FXB/Deferred_ToonEffect.fxb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:725d85ea8bb4ad3016dadae76f6da73792b3f46f99facdd5fe91a5666cc3a13a -size 21072 +oid sha256:1d94e2b3520ef29a3de94389bb42b949f07af8e19cbac1fdd170e6ae116f4142 +size 7344 diff --git a/Effects/HLSL/Conversion.fxh b/Effects/HLSL/Conversion.fxh new file mode 100644 index 0000000..3d828bc --- /dev/null +++ b/Effects/HLSL/Conversion.fxh @@ -0,0 +1,34 @@ +float3 HUEtoRGB(in float H) +{ + float R = abs(H * 6 - 3) - 1; + float G = 2 - abs(H * 6 - 2); + float B = 2 - abs(H * 6 - 4); + return saturate(float3(R,G,B)); +} + +float Epsilon = 1e-10; + +float3 RGBtoHCV(in float3 RGB) +{ + // Based on work by Sam Hocevar and Emil Persson + float4 P = (RGB.g < RGB.b) ? float4(RGB.bg, -1.0, 2.0/3.0) : float4(RGB.gb, 0.0, -1.0/3.0); + float4 Q = (RGB.r < P.x) ? float4(P.xyw, RGB.r) : float4(RGB.r, P.yzx); + float C = Q.x - min(Q.w, Q.y); + float H = abs((Q.w - Q.y) / (6 * C + Epsilon) + Q.z); + return float3(H, C, Q.x); +} + +float3 RGBtoHSL(float3 RGB) +{ + float3 HCV = RGBtoHCV(RGB); + float L = HCV.z - HCV.y * 0.5; + float S = HCV.y / (1 - abs(L * 2 - 1) + Epsilon); + return float3(HCV.x, S, L); +} + +float3 HSLtoRGB(float3 HSL) +{ + float3 RGB = HUEtoRGB(HSL.x); + float C = (1 - abs(2 * HSL.z - 1)) * HSL.y; + return (RGB - 0.5) * C + HSL.z; +} diff --git a/Effects/HLSL/Deferred_ToonEffect.fx b/Effects/HLSL/Deferred_ToonEffect.fx index 6806710..3d8ac3f 100644 --- a/Effects/HLSL/Deferred_ToonEffect.fx +++ b/Effects/HLSL/Deferred_ToonEffect.fx @@ -1,11 +1,13 @@ #include "Macros.fxh" #include "Shadow.fxh" +#include "Dither.fxh" static const int NUM_SHADOW_CASCADES = 4; DECLARE_TEXTURE(gPosition, 0); DECLARE_TEXTURE(gAlbedo, 1); DECLARE_TEXTURE(gNormal, 2); +DECLARE_TEXTURE(gMetallicRoughness, 3); DECLARE_TEXTURE(shadowMapOne, 4); DECLARE_TEXTURE(shadowMapTwo, 5); DECLARE_TEXTURE(shadowMapThree, 6); @@ -88,11 +90,9 @@ float ComputeShadow(float3 positionWorldSpace, float3 N, float3 L) lightSpaceMatrix = LightSpaceMatrixFour; } - // PCF + Poisson soft shadows - if (shadowCascadeIndex == 0) { - return PoissonShadow( + return HardShadow( positionWorldSpace, N, L, @@ -103,7 +103,7 @@ float ComputeShadow(float3 positionWorldSpace, float3 N, float3 L) } else if (shadowCascadeIndex == 1) { - return PoissonShadow( + return HardShadow( positionWorldSpace, N, L, @@ -114,7 +114,7 @@ float ComputeShadow(float3 positionWorldSpace, float3 N, float3 L) } else if (shadowCascadeIndex == 2) { - return PoissonShadow( + return HardShadow( positionWorldSpace, N, L, @@ -125,7 +125,7 @@ float ComputeShadow(float3 positionWorldSpace, float3 N, float3 L) } else { - return PoissonShadow( + return HardShadow( positionWorldSpace, N, L, @@ -138,30 +138,43 @@ float ComputeShadow(float3 positionWorldSpace, float3 N, float3 L) float IntensityBanding(float NdotL) { - if (NdotL > 0.5) + // if (NdotL > 0.5) + // { + // return 1.0; + // } + // else if (NdotL > 0.25) + // { + // return 0.5; + // } + // else if (NdotL > 0.0) + // { + // return 0.25; + // } + // else + // { + // return 0.0; + // } + if (NdotL > 0) { return 1.0; } - else if (NdotL > 0.25) - { - return 0.5; - } - else if (NdotL > 0.0) - { - return 0.25; - } else { - return 0.0; + return 0.25; } } // FIXME: organize this float4 main_ps(PixelInput input) : SV_TARGET0 { + float2 screenPosition = input.Position.xy; float3 worldPosition = SAMPLE_TEXTURE(gPosition, input.TexCoord).rgb; float3 normal = SAMPLE_TEXTURE(gNormal, input.TexCoord).xyz; float3 albedo = SAMPLE_TEXTURE(gAlbedo, input.TexCoord).rgb; + float2 metallicRoughness = SAMPLE_TEXTURE(gMetallicRoughness, input.TexCoord).rg; + + // the lower the glossiness, the sharper the specular highlight + float glossiness = lerp(64, 16, 1.0 - metallicRoughness.r); float3 V = normalize(EyePosition - worldPosition); float3 L = normalize(DirectionalLightDirection); @@ -172,23 +185,36 @@ float4 main_ps(PixelInput input) : SV_TARGET0 float NdotH = max(dot(N, H), 0.0); float lightIntensity = IntensityBanding(NdotL); - float3 light = lightIntensity * DirectionalLightColor; + //float3 light = lightIntensity * DirectionalLightColor; + float3 light = DirectionalLightColor; - float specularIntensity = pow(NdotH * lightIntensity, 32 * 32); + if (lightIntensity < 1) + { + light *= dither(lightIntensity, screenPosition); + } + + float specularIntensity = pow(NdotH * lightIntensity, glossiness * glossiness); float specularSmooth = smoothstep(0.005, 0.01, specularIntensity); float3 specular = specularSmooth * float3(1.0, 1.0, 1.0); - float rimColor = float3(1.0, 1.0, 1.0); + if (metallicRoughness.r == 0.0) { specular = float3(0.0, 0.0, 0.0); } + + float3 rimColor = float3(1.0, 1.0, 1.0); float rimThreshold = 0.1; - float rimAmount = 0.76; + float rimAmount = 1 - metallicRoughness.g; float rimDot = 1 - dot(V, N); float rimIntensity = rimDot * pow(max(NdotL, 0.0), rimThreshold); rimIntensity = smoothstep(rimAmount - 0.01, rimAmount + 0.01, rimIntensity); float3 rim = rimIntensity * rimColor; float shadow = ComputeShadow(worldPosition, N, L); - float3 color = albedo * (light + specular + rim) * shadow; + float3 color = albedo * (light + specular + rim); // * shadow; + + if (shadow < 1) + { + color *= dither(shadow, screenPosition); + } return float4(color, 1.0); } diff --git a/Effects/HLSL/Dither.fxh b/Effects/HLSL/Dither.fxh new file mode 100644 index 0000000..e0cf119 --- /dev/null +++ b/Effects/HLSL/Dither.fxh @@ -0,0 +1,89 @@ +#include "Conversion.fxh" + +// technique from http://alex-charlton.com/posts/Dithering_on_the_GPU/ + +uniform float3 palette[8]; +static const int paletteSize = 8; + +static const int indexMatrix4x4[16] = +{ + 0, 8, 2, 10, + 12, 4, 14, 6, + 3, 11, 1, 9, + 15, 7, 13, 5 +}; + +float indexValue(float2 screenCoords) +{ + int x = int(screenCoords.x % 4); + int y = int(screenCoords.y % 4); + + return indexMatrix4x4[(x + y * 4)] / 16.0; +} + +float hueDistance(float h1, float h2) +{ + float diff = abs(h1 - h2); + return min(abs(1.0 - diff), diff); +} + +void closestColors(float hue, out float3 ret[2]) +{ + float3 closest = float3(-2, 0, 0); + float3 secondClosest = float3(-2, 0, 0); + float3 temp; + + for (int i = 0; i < paletteSize; i++) + { + temp = palette[i]; + float tempDistance = hueDistance(temp.x, hue); + if (tempDistance < hueDistance(closest.x, hue)) + { + secondClosest = closest; + closest = temp; + } + else + { + if (tempDistance < hueDistance(secondClosest.x, hue)) + { + secondClosest = temp; + } + } + } + + ret[0] = closest; + ret[1] = secondClosest; +} + +float3 dither(float3 color, float2 screenCoords) +{ + float3 colors[2]; + float3 hsl = RGBtoHSL(color); + closestColors(hsl.x, colors); + float3 closestColor = colors[0]; + float3 secondClosestColor = colors[1]; + float d = indexValue(screenCoords); + float hueDiff = hueDistance(hsl.x, closestColor.x) / hueDistance(secondClosestColor.x, secondClosestColor.x); + return HSLtoRGB(hueDiff < d ? closestColor : secondClosestColor); +} + +// brightColor refers to undithered max color +// float3 dither(float3 color, float3 brightColor, float2 screenCoords) +// { +// float brightHue = RGBtoHSL(brightColor.x); +// float colorHue = RGBtoHSL(color.x); +// float halfDistance = hueDistance(0.0, brightHue) / 2.0; +// float3 closestColor = (colorHue < halfDistance) ? float3(0.0, 0.0, 0.0) : brightColor; +// float3 secondClosestColor = brightColor - closestColor; +// float d = indexValue(screenCoords); +// float distance = abs(closestColor - color); +// return (distance < d) ? closestColor : secondClosestColor; +// } + +float3 dither(float color, float2 screenCoords) { + float closestColor = (color < 0.5) ? 0 : 1; + float secondClosestColor = 1 - closestColor; + float d = indexValue(screenCoords); + float distance = abs(closestColor - color); + return (distance < d) ? closestColor : secondClosestColor; +} diff --git a/Effects/HLSL/Shadow.fxh b/Effects/HLSL/Shadow.fxh index 38efc3e..47af839 100644 --- a/Effects/HLSL/Shadow.fxh +++ b/Effects/HLSL/Shadow.fxh @@ -85,3 +85,38 @@ float PoissonShadow( return visibility; } + +float HardShadow( + float3 positionWorldSpace, + float3 N, + float3 L, + float4x4 lightSpaceMatrix, + sampler shadowMap, + int shadowMapSize +) { + // float bias = 0.005 * tan(acos(dot(N, L))); + // bias = clamp(bias, 0, 0.01); + + float bias = max(0.05 * (1.0 - dot(N, L)), 0.005); + + float4 positionLightSpace = mul(float4(positionWorldSpace, 1.0), lightSpaceMatrix); + + // maps to [-1, 1] + float3 projectionCoords = positionLightSpace.xyz / positionLightSpace.w; + + // maps to [0, 1] + // NOTE: In XNA, clip space z is [0, 1] range + projectionCoords.x = (projectionCoords.x * 0.5) + 0.5; + projectionCoords.y = (projectionCoords.y * 0.5) + 0.5; + projectionCoords.y *= -1; + + if (projectionCoords.z > 1.0) + { + return 1.0; + } + + float closestDepth = tex2D(shadowMap, projectionCoords.xy); + float currentDepth = projectionCoords.z; + + return (currentDepth - bias > closestDepth ? 0.25 : 1.0); +} diff --git a/Geometry/Model.cs b/Geometry/Model.cs index e9ad690..9ab1dc0 100644 --- a/Geometry/Model.cs +++ b/Geometry/Model.cs @@ -6,11 +6,8 @@ namespace Kav { public Mesh[] Meshes { get; } - private Color albedoValue; - public Color Albedo { - get { return albedoValue; } set { foreach (var mesh in Meshes) @@ -23,6 +20,34 @@ namespace Kav } } + public float Metallic + { + set + { + foreach (var mesh in Meshes) + { + foreach (var meshPart in mesh.MeshParts) + { + meshPart.Metallic = value; + } + } + } + } + + public float Roughness + { + set + { + foreach (var mesh in Meshes) + { + foreach (var meshPart in mesh.MeshParts) + { + meshPart.Roughness = value; + } + } + } + } + public Model(Mesh[] meshes) { Meshes = meshes; diff --git a/Renderer.cs b/Renderer.cs index e8b2bf8..747d88b 100644 --- a/Renderer.cs +++ b/Renderer.cs @@ -346,6 +346,7 @@ namespace Kav Deferred_ToonEffect.GPosition = gPosition; Deferred_ToonEffect.GAlbedo = gAlbedo; Deferred_ToonEffect.GNormal = gNormal; + Deferred_ToonEffect.GMetallicRoughness = gMetallicRoughness; Deferred_ToonEffect.EyePosition = camera.Position; Deferred_ToonEffect.DirectionalLightDirection = directionalLight.Direction;