⬆️ ⬇️

Blending pointlight shadows in Unity

image



I still have not seen analogs of similar shadows for a point source of light (Pointlight with a blur effect at a distance, imitating arealight) in computer games. Everywhere - either fully baked shadows, or “light bulbs” without any shadows at all, the maximum is ordinary PCF filtering. Although PCSS-shadows have been used for directional sunlight for a long time (GTA5, for example). In Unreal, there is an interesting algorithm similar to raytracing, which draws beautiful arealight-shadows, but only for static geometry (additional volumes are required). In Unity, everything is very bad - only sunlight is gently filtered, and “spotlights” and “light bulbs” are in the span.



If at some “spotlight bilinear filtering” hangs on Unity, then on point “light bulbs” there is such a horror:

')





Is it really that adding ordinary PCF filtering is such a performance hit? Even if the blow, then why not make the option of including this filtering optional?



As it turned out, there is no impact on performance. In Unity 5, the technology deffered lighting (deferred lighting), and the more in the frame of light sources with dynamic shadow, the worse the performance. And what kind of filtering of these shadows is not so important. The same number of passes is performed, the same number of pixels are processed, this moment just requires scene optimization. And the number of samples from the cubic texture of the depth of the “light bulb” may affect the performance, but very little (in the area of ​​the FPS pair).



It should also be said that blurring the resulting shadow with the image effect will not work. There is no interlayer between sampling from the texture of depth and the imposition of light, such is the system in Unity. A pity, it would be possible to win something.



So, we change the algorithm



Creating special materials for the sake of soft shadows is painstaking, and making changes to existing shaders with a script (as was done in ShadowSoftener) is also somehow not very convenient. It is much faster and more practical to apply shadows at once to all shaders in the project (built-in, handwritten, downloaded) by changing just one file “UnityShadowLibrary.cginc”, which is located in the editor's directory: "... \ Unity5 \ Editor \ Data \ CGIncludes".



We find this piece, which is responsible for the shadow of a point source of light:



#if defined (SHADOWS_SOFT) float z = 1.0/128.0; float4 shadowVals; shadowVals.x = SampleCubeDistance (vec+float3( z, z, z)); shadowVals.y = SampleCubeDistance (vec+float3(-z,-z, z)); shadowVals.z = SampleCubeDistance (vec+float3(-z, z,-z)); shadowVals.w = SampleCubeDistance (vec+float3( z,-z,-z)); half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0f; return dot(shadows,0.25); #else 




In place of it, you can put any algorithm you like, but first we analyze the input parameters:



vec is a four-dimensional vector. x, y and z is the direction from the “light bulb” to the pixel. By sampling this value using SampleCubeDistance, we get the distance of the shadowed object. If the distance is greater than the length of vec, the shadow per pixel does not fall. (mydist is the same distance, but this is not an input parameter, it is calculated higher in the same file) In each sample, the coordinate is shifted by a fixed amount, creating the effect of a non-uniform border. Do not laugh. Professional programmers who develop Unity call it soft shadow.



_LightShadowData.r - this value of the slider adjusts the brightness of the shadow. You can use it, for example, to change the degree of blur of the shadow or to change some other parameter to debug the shader directly in the editor. Unfortunately, I have not figured out what other _LightShadowData components are affecting. Apparently, for a point source of light there are no more parameters.



We return the brightness from the function (0 to 1.0), i.e. a multiplier that acts as atten (attenuation) in all shaders, so the modified shadow will also work in all shaders that support shading.



To see the changes , find the folder of your project, in it - the folder «Library». Remove the “ShaderCache” directory from this folder and restart Unity. I also restarted Unity after each shader editing, it was unnerving and complicated debugging.



That's all.



I tried this option here, smearing the edges with an unsightly dithering:



  #if defined (SHADOWS_SOFT) //  ,    float downscale = 32.0f; //   const float3 rndseed = float3(12.9898,78.233,45.5432); float3 randomvec = float3( dot(vec,rndseed) , dot(vec.yzx,rndseed) , dot(vec.zxy,rndseed) ); randomvec = frac(sin(randomvec) * 43758.5453); //      float3 xvec = normalize(cross(vec,randomvec)); float3 yvec = normalize(cross(vec,xvec)); float3 vec1 = xvec / downscale; float3 vec2 = yvec / downscale; float4 shadowVals; //    shadowVals.x = SampleCubeDistance (vec+vec1); shadowVals.y = SampleCubeDistance (vec+vec2); shadowVals.z = SampleCubeDistance (vec-vec1); shadowVals.w = SampleCubeDistance (vec-vec2); //  half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0f; return dot(shadows,0.25); #else 




There are as many samples as in the eerie canonical implementation, but at least the pixels do not make my eyes callous:







Without hesitation (but for a long time debugging), I made the following PCF4x4 implementation with “manual” bilinear filtering (of course, DirectX11 commands can be inserted into Unity shaders, among which there is bilinear filtering of the cubic shadows, but I went selections for “manual” smoothing, it turned out not 16 samples, but 25).



image



Or even like this:







This is not an honest PCSS, but a faster and simpler implementation. The maximum blur is the same PCF4x4. A clear shadow near the shader is achieved by increasing the sharpness. By the way, the shapen effect is also a lot of where it applies. Here we have a symbiosis, with which you can achieve a beautiful effect.



The above-mentioned slider adjusts the transition angle of a soft shadow to a sharp one. This parameter can be selected in different ways for different "light bulbs", based on the size of the premises in which they are located.



I put the code of the last implementation under cat. It is a bit cumbersome and untidy, because I did not use a two-dimensional cycle of samples, but I wrote them down manually to reduce the number of commands and the number of variables used, and at the same time apply operations with vectors where you can calculate several variables at once. Plus, I basically did not use conditional transitions, replacing them with mathematics. Therefore, look at your own risk.



Necronomicon
 inline half UnitySampleShadowmap (float3 vec) { float mydist = length(vec) * _LightPositionRange.w; mydist *= 0.97; // bias const float downscale = 128.0f; const float sat_mult = 312.0f; #if defined (SHADOWS_SOFT) #define shadow_close_scalefactor (_LightShadowData.r + 0.001f) //        #define xvec main_axis.zxy #define yvec main_axis.yzx #define VIRTUAL_COORD(x,y) (ceilvec + xvec*x + yvec*y) //   ,       // (     ) half3 main_axis = abs(vec)*1666.666f; main_axis = normalize(clamp(main_axis.xyz - main_axis.yzx,0.0f,1.0f)*clamp(main_axis.xyz - main_axis.zxy,0.0f,1.0f)); //     vec /= abs(dot(vec,main_axis)); //    ,       fixed3 ceilvec = ceil(vec*downscale) / downscale; //     -     4-  fixed4 lerp_delta; vec = (ceilvec - vec) * downscale; lerp_delta.x = dot(vec * xvec,1.0f); lerp_delta.y = dot(vec * yvec,1.0f); lerp_delta.z = 1.0f - lerp_delta.x; lerp_delta.w = 1.0f - lerp_delta.y; //    main_axis /= downscale; ceilvec -= (xvec + yvec)*0.5f; //     float4 shadowVals, distance_sums, distancesides; fixed4 shadowsides, shadow_sums, distancesides_nums, distance_nums; #define DISTANCE_COMPARE_X4(sum,distance_sum,distance_num) shadowVals = mydist.xxxx - shadowVals; sum = dot(clamp(shadowVals *sat_mult,0.0f,1.0f),1.0f); distance_sum = dot(clamp(shadowVals, 0.0f,100.0f),1.0f); distance_num = dot(clamp(shadowVals *sat_mult,0.0f,1.0f),1.0f) #define DISTANCE_COMPARE_X3(sum,distance_sum,distance_num) shadowVals = mydist.xxxx - shadowVals; sum = dot(clamp(shadowVals.xyz*sat_mult,0.0f,1.0f),1.0f); distance_sum = dot(clamp(shadowVals.xyz,0.0f,100.0f),1.0f); distance_num = dot(clamp(shadowVals.xyz*sat_mult,0.0f,1.0f),1.0f) #define DISTANCE_COMPARE_X1(sum,distance_sum,distance_num) shadowVals.x = mydist - shadowVals.x; sum = clamp(shadowVals.x *sat_mult,0.0f,1.0f); distance_sum = clamp(shadowVals.x, 0.0f,100.0f); distance_num = clamp(shadowVals.x *sat_mult,0.0f,1.0f) //      -     //  4  shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-1.0f,-1.0f)); shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(0.0f,-1.0f)); shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(1.0f,-1.0f)); shadowVals.w = SampleCubeDistance (VIRTUAL_COORD(1.0f,0.0f)); DISTANCE_COMPARE_X4(shadowsides.x,distancesides.x,distancesides_nums.x); //  4  shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-1.0f,-0.0f)); shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(-1.0f,1.0f)); shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(0.0f,1.0f)); shadowVals.w = SampleCubeDistance (VIRTUAL_COORD(1.0f,1.0f)); DISTANCE_COMPARE_X4(shadowsides.y,distancesides.y,distancesides_nums.y); //   shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(0.0f,0.0f)); DISTANCE_COMPARE_X1(shadowsides.z,distancesides.z,distancesides_nums.z); //    shadow_sums = dot(shadowsides.xyz,1.0f).xxxx; distance_sums = dot(distancesides.xyz,1.0f).xxxx; distance_nums = dot(distancesides_nums.xyz,1.0f).xxxx + fixed4(0.01f,0.01f,0.01f,0.01f); //      //  shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-2.0f,-1.0f)); shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(-2.0f,0.0f)); shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(-2.0f,1.0f)); DISTANCE_COMPARE_X3(shadowsides.x,distancesides.x,distancesides_nums.x); //  shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-1.0f,-2.0f)); shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(0.0f,-2.0f)); shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(1.0f,-2.0f)); DISTANCE_COMPARE_X3(shadowsides.y,distancesides.y,distancesides_nums.y); //  shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(2.0f,-1.0f)); shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(2.0f,0.0f)); shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(2.0f,1.0f)); DISTANCE_COMPARE_X3(shadowsides.z,distancesides.z,distancesides_nums.z); //  shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-1.0f,2.0f)); shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(0.0f,2.0f)); shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(1.0f,2.0f)); DISTANCE_COMPARE_X3(shadowsides.w,distancesides.w,distancesides_nums.w); //      shadow_sums += (shadowsides.xzxz + shadowsides.yyww); distance_sums += (distancesides.xzxz + distancesides.yyww); distance_nums += distancesides_nums.xzxz + distancesides_nums.yyww; //   shadowVals.x = SampleCubeDistance (VIRTUAL_COORD(-2.0f,-2.0f)); shadowVals.y = SampleCubeDistance (VIRTUAL_COORD(2.0f,-2.0f)); shadowVals.z = SampleCubeDistance (VIRTUAL_COORD(-2.0f,2.0f)); shadowVals.w = SampleCubeDistance (VIRTUAL_COORD(2.0f,2.0f)); //  ,        shadowVals = mydist.xxxx - shadowVals; shadow_sums += clamp(shadowVals*sat_mult,0.0f,1.0f); distance_sums += clamp(shadowVals,0.0f,1.0f); distance_nums += clamp(shadowVals*sat_mult,0.0f,1.0f); //     shadow_sums.x = dot(shadow_sums * lerp_delta.xzxz * lerp_delta.yyww, 1.0f) / 16.0f; distance_sums.x = dot(clamp((distance_sums/distance_nums),0.0f,1.0f) * lerp_delta.xzxz * lerp_delta.yyww, 1.0f); //      fixed contrastfactor = 1.0f - clamp(distance_sums.x/shadow_close_scalefactor,0.0f,1.0f); shadow_sums.x = clamp((shadow_sums.x - 0.5f) * (1.0f + contrastfactor*4.0f)+0.5f,0.0f,1.0f); return 1.0f - shadow_sums.x; #else //     4     vec = normalize(vec) * 0.5f; //    const float3 rndseed = float3(12.9898,78.233,45.5432); float3 randomvec = float3( dot(vec,rndseed) , dot(vec.yzx,rndseed) , dot(vec.zxy,rndseed) ); randomvec = frac(sin(randomvec) * 43758.5453); float3 vec1 = normalize(cross(vec,randomvec)) / downscale; float3 vec2 = normalize(cross(vec,vec1)) / downscale; float4 shadowVals; shadowVals.x = SampleCubeDistance (vec+vec1); shadowVals.y = SampleCubeDistance (vec+vec2); shadowVals.z = SampleCubeDistance (vec-vec1); shadowVals.w = SampleCubeDistance (vec-vec2); shadowVals = mydist.xxxx - shadowVals; fixed4 shadows = clamp(shadowVals*sat_mult,0.0f,1.0f); return dot(shadows,0.25); #endif } 






If you are not exactly satisfied with this approach, implement more suitable algorithms within Unity. I would only be glad of such activity, because I really am depressed by the stagnation in Unity in terms of realtime shadows, which has been dragging on for many years.



PS: Most of all I would like to hear the opinions of those who work directly with Unity and know what is possible and what is not, although there will most likely be extensive reflections on algorithms that cannot be inserted into the engine because of its closed architecture. But I also will join the lengthy reflections with joy and explain some points.

Source: https://habr.com/ru/post/307582/



All Articles