#ifndef CAPSULE_SHADOW_INCLUDED #define CAPSULE_SHADOW_INCLUDED #pragma multi_compile_fragment _ _MultiCapsule_Shadow_AO_ON #pragma multi_compile_fragment _ _MultiCapsule_AO_ON #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct Capsule { half3 a, b; half radius; }; struct Character { half3 position; half radius; int capsuleStartID, capsuleEndID; uint capsuleAOSettingMask; }; struct CapsuleShadowArea { half AmbientIntensity; half ConeAngle; half ShadowIntensity; half ShadowSharpness; int lightStartID, lightEndID; }; StructuredBuffer _CapsuleData; uint _CapsulesCount; StructuredBuffer _CharacterData; uint _CharactersCount; StructuredBuffer _CapsuleShadowData; uint _CapsuleShadowDataCount; half4 _CapsuleLightsDir[32]; uint _CapsuleLightsCount; float CalcCapsuleShadowByIndex(float3 ro, float3 rd, uint s, uint e, in float k, float intensity, float intensity1) { float shadow = 1.0; for (uint i = s; i < e; ++i) { Capsule c = _CapsuleData[i]; float3 a = c.a; float3 b = c.b; float r = c.radius; float3 ba = b - a; float3 oa = ro - a; float oad = dot(oa, rd); float dba = dot(rd, ba); float baba = dot(ba, ba); float oaba = dot(oa, ba); float2 th = float2(-oad * baba + dba * oaba, oaba - oad * dba) / (baba - dba * dba); th.x = max(th.x, 0.0001); th.y = saturate(th.y); //线段ab上离射线最近的点p float3 p = a + ba * th.y; //射线rord 找到距离线段 ab 最近的点q float3 q = ro + rd * th.x; //沿着光的方向发射射线, 射线离胶囊体最短的距离 float d = length(p - q) - r; float s = saturate(k * d / th.x + 0.3); s = s * s * (3.0 - 2.0 * s); shadow *= s; } shadow = saturate(lerp(1, shadow, intensity)); return saturate(lerp(1, shadow, intensity1)); } float CalcCapsuleOcclusionByIndex(float3 p, float3 n, uint s, uint e, float intensity, float intensity1) { float ao = 1.0; for (uint i = s; i < e; ++i) { Capsule capsule = _CapsuleData[i]; float3 a = capsule.a; float3 b = capsule.b; float r = capsule.radius; float3 ba = b - a; float3 pa = p - a; float h = saturate(dot(pa, ba) / dot(ba, ba)); // 地面点 p 到 ab 最近点 x, 向量 xp = d float3 d = pa - h * ba; float l = length(d); float nl = dot(-d, n); float o = 1.0 - max(0.0, nl) * r * r / (l * l * l); // multiplier o *= 1.0 + r*(l-r)/(l*l); o = sqrt(o * o * o); ao *= o; } ao = saturate(lerp(1, ao, intensity)); return clamp(lerp(1, ao, intensity1), 0.6, 1); } #define sq(x) (x * x) float acosFast(float x) { // Lagarde 2014, "Inverse trigonometric functions GPU optimization for AMD GCN architecture" // This is the approximation of degree 1, with a max absolute error of 9.0x10^-3 float y = abs(x); float p = -0.1565827 * y + 1.570796; p *= sqrt(1.0 - y); return x >= 0.0 ? p : PI - p; } float acosFastPositive(float x) { // Lagarde 2014, "Inverse trigonometric functions GPU optimization for AMD GCN architecture" float p = -0.1565827 * x + 1.570796; return p * sqrt(1.0 - x); } /* ref: https://developer.amd.com/wordpress/media/2012/10/Oat-AmbientApetureLighting.pdf Approximate the area of intersection of two spherical caps. With some modifcations proposed by the shadertoy implementation */ float SphericalCapsIntersectionAreaFast(float cosCap1, float cosCap2, float cap2, float cosDistance) { // Precompute constants float radius1 = acosFastPositive(cosCap1); // First caps radius (arc length in radians) float radius2 = cap2; // Second caps radius (in radians) float dist = acosFast(cosDistance); // Distance between caps (radians between centers of caps) // Conditional expressions instead of if-else float check1 = min(radius1, radius2) <= max(radius1, radius2) - dist; float check2 = radius1 + radius2 <= dist; // Ternary operator to replace if-else float result = check1 ? (1.0 - max(cosCap1, cosCap2)) : (check2 ? 0.0 : 1.0 - max(cosCap1, cosCap2)); float delta = abs(radius1 - radius2); float x = 1.0 - saturate((dist - delta) / max(radius1 + radius2 - delta, FLT_EPS)); // simplified smoothstep() float area = sq(x) * (-2.0 * x + 3.0); // Multiply by (1.0 - max(cosCap1, cosCap2)) only once return area * result; } float DirectionalOcclusionSphere(float3 rayPosition, float3 spherePosition, float sphereRadius, float4 coneProperties) { float3 occluderPosition = spherePosition.xyz - rayPosition; float occluderLength2 = dot(occluderPosition, occluderPosition); float3 occluderDir = occluderPosition * rsqrt(occluderLength2); float cosPhi = dot(occluderDir, coneProperties.xyz); // sq(sphere.w) should be a uniform --> capsuleRadius^2 float cosTheta = sqrt(occluderLength2 / (sq(sphereRadius) + occluderLength2)); float cosCone = cos(coneProperties.w); return 1.0 - SphericalCapsIntersectionAreaFast(cosTheta, cosCone, coneProperties.w, cosPhi) / (1.0 - cosCone); } float DirectionalOcclusionCapsule(float3 rayPosition, float3 capsuleA, float3 capsuleB, float capsuleRadius, float4 coneProperties) { float3 Ld = capsuleB - capsuleA; float3 L0 = capsuleA - rayPosition; float a = dot(coneProperties.xyz, Ld); float t = saturate(dot(L0, a * coneProperties.xyz - Ld) / (dot(Ld, Ld) - a * a)); float3 positionToRay = capsuleA + t * Ld; return DirectionalOcclusionSphere(rayPosition, positionToRay, capsuleRadius, coneProperties); } float CalcCapsuleShadowByIndexV2(float3 ro, float4 cone, uint s, uint e, float intensity, float intensity1) { float shadow = 1.0; for (uint i = s; i < e; ++i) { Capsule c = _CapsuleData[i]; shadow*= DirectionalOcclusionCapsule(ro, c.a, c.b, c.radius, cone); } shadow = saturate(lerp(1, shadow, intensity)); return saturate(lerp(1, shadow, intensity1)); } float CalcSphereOcclusion2(in float3 pos, in float3 nor, in float4 sph) { float3 di = sph.xyz - pos; float l = length(di); float rad = sph.w; float nl = max(0.0, dot(nor, di / l)); return (1.0 - nl * rad * rad / (l * l )); // float3 di = (sph.xyz - pos) / sph.w; // float sqL = dot(di, di); // return clamp(1.0 - dot(nor, di) * rsqrt(sqL * sqL * sqL), 0.1, 1); } float CalcSphereOcclusion2_( in float3 pos, in float3 nor, in float4 sph ) { float3 di = sph.xyz - pos; float l = length(di); float nl = dot(nor,di/l) * _CapsuleShadowData[0].ConeAngle; float h = l/sph.w; float h2 = h*h; float k2 = 1.0 - h2*nl*nl; // above/below horizon // EXACT: Quilez - https://iquilezles.org/articles/sphereao float res = max(0.0,nl)/h2; // intersecting horizon if( k2 > 0.001 ) { #if 0 // EXACT : Lagarde/de Rousiers - https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf res = nl*acos(-nl*sqrt( (h2-1.0)/(1.0-nl*nl) )) - sqrt(k2*(h2-1.0)); res = res/h2 + atan( sqrt(k2/(h2-1.0))); res /= 3.141593; #else // APPROXIMATED : Quilez - https://iquilezles.org/articles/sphereao res = (nl*h+1.0)/h2; res = 0.33*res*res; #endif } return 1 -res; } float CalcCapsuleOcclusionByIndexV2(float3 p, float3 n, uint s, uint e, float intensity, float intensity1) { float ao = 1.0; for (uint i = s; i < e; ++i) { Capsule capsule = _CapsuleData[i]; float3 ba = capsule.b - capsule.a; float3 pa = p - capsule.a; float l = dot(ba, ba); // p 在 ba 上投影长度与 ba 长度比值 float t = /* abs(l) < 1e-8f ? 0.0 : */ saturate(dot(pa, ba) / l); float3 positionToRay = capsule.a + t * ba; ao *= CalcSphereOcclusion2_(p, n, float4(positionToRay, capsule.radius)); } ao = saturate(lerp(1, ao, intensity)); return saturate(lerp(1, ao, intensity1)); } bool IsInBounds(float3 p, float3 c, float b, out float intensity) { float3 diff = abs(p - c); if (diff.x < b && diff.y < b && diff.z < b) { intensity = saturate(dot(b, b) / dot(diff.xz, diff.xz)); return true; } intensity = 0; return false; } float4 GetConeProperties(float3 lightDir, float coneAngle) { float4 cone = float4(lightDir.xyz, radians(coneAngle) * 0.5); return cone; } void CalcCapsuleShadow(float3 worldPos, float3 worldNormal, out float shadow, out float occlusion) { shadow = 1.0; occlusion = 1.0; #if _MultiCapsule_Shadow_AO_ON || _MultiCapsule_AO_ON for (uint i = 0; i < _CharactersCount; ++i) { float intensity = 1; Character c = _CharacterData[i]; if (IsInBounds(worldPos, c.position, c.radius * 3, intensity)) { #ifdef _MultiCapsule_Shadow_AO_ON uint mask = c.capsuleAOSettingMask; uint areaCnt = countbits(mask); for (uint a = 0; a < areaCnt; ++a) { uint index = firstbitlow(mask); mask ^= 1 << index; CapsuleShadowArea area = _CapsuleShadowData[index]; for (uint b = area.lightStartID; b < area.lightEndID; ++b) { float4 lightDir = _CapsuleLightsDir[b]; //float4 cone = GetConeProperties(lightDir.xyz, area.ConeAngle); float4 cone = GetConeProperties(normalize(lightDir.xyz - worldPos), area.ConeAngle); float tempIntensity = intensity / smoothstep(0.1, 2, lightDir.w); float tempShadow = CalcCapsuleShadowByIndex(worldPos, cone.xyz, c.capsuleStartID, c.capsuleEndID, area.ShadowSharpness, tempIntensity, area.ShadowIntensity); // float tempShadow = CalcCapsuleShadowByIndexV2(worldPos, cone, c.capsuleStartID, c.capsuleEndID, tempIntensity, area.ShadowIntensity); shadow = min(shadow, tempShadow); } } // occlusion *= CalcCapsuleOcclusionByIndex(worldPos, worldNormal, c.capsuleStartID, c.capsuleEndID, intensity, _CapsuleShadowData[0].AmbientIntensity); occlusion *= CalcCapsuleOcclusionByIndexV2(worldPos, worldNormal, c.capsuleStartID, c.capsuleEndID, intensity, _CapsuleShadowData[0].AmbientIntensity); #elif defined(_MultiCapsule_AO_ON) occlusion *= CalcCapsuleOcclusionByIndex(worldPos, worldNormal, c.capsuleStartID, c.capsuleEndID, intensity, _CapsuleShadowData[0].AmbientIntensity); #endif } } #endif } #endif