Search Unity

Vertex Lights: Culling vs Falloff mismatch?

Discussion in 'General Graphics' started by bluescrn, Oct 9, 2019.

  1. bluescrn

    bluescrn

    Joined:
    Feb 25, 2013
    Posts:
    642
    Hi,

    I'm trying to make use of vertex lights (rather than more expensive pixel lights), for VFX such as explosions. These lights are often intense but only last for a sort duration.

    But I'm seeing some unpleasant seams between meshes - it seems that with high-intensity lights, the light falloff can extend well outside of the light range, the range which is used to dermine which lights affect a mesh.

    In the image below, the two planes are identical, but the light 'range sphere' does not quite overlap the second plane, so the plane on the right isn't recieving any light from the point light.

    (Ignore the gap between the planes, that's just to show that it's 2 separate meshes. The falloff should extend well onto the 2nd plane)

    Image1.png

    Are there any workaround/fixes for this? - The project is using surface shaders, so I don't think I can easily mess with the vertex lighting calculations themselves? - or is there a way?

    Using less intense lights with higher range reduces the problem, but doesn't really give the visual effect that I'm after.

    (I'm using Unity 2018.4.2, Forward rendering, Gamma lighting, and Default-Material in this test scene - should be reproducable by just setting pixel light count to 0, and creating 2 planes and a very intense point light)
     
    hungrybelome likes this.
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,338
    Unity’s vertex lights approximate Unity’s built renderers’ falloff by using a fixed falloff rate and adjusting the brightness. Ironically, they use the more realistic inverse square falloff, but this means it has an infinite range.

    There is a way to convert the vertex lights to match Unity’s built in per pixel lights though, or at least decode the original range and clamp them. I figured out the conversion they were using to approximate the built in range here:
    https://forum.unity.com/threads/point-light-in-v-f-shader.499717/#post-3250460

    To make use of this in a Surface shader you’d have to do a bit of shader jujitsu. Mainly override the built in Shade4PointLights function with one that uses the alternative falloff calculations, but that's easier said than done ... mainly because HLSL doesn't allow you to redefine existing functions. So you'd have to disable vertex lights in the surface shader, calculate them yourself using the alternative attenuation calculation in a custom vertex function, pass them along as a custom Input variable, and re-apply them in the surf function as an emissive color (o.Emission += vertex lighting * albedo).

    All doable, but one drawback is the nature of per vertex lighting being, well, per vertex means that using the per pixel falloff looks way worse than the inverse square falloff as it actually helps hide the artifacts from doing the calculations per vertex.
     
    Last edited: Oct 10, 2019
  3. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,338
    Proof of concept version of the shader
    Code (CSharp):
    1. Shader "Custom/AlternativePerVertexLightFalloff" {
    2.     Properties {
    3.         _Color ("Color", Color) = (1,1,1,1)
    4.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    5.         _Glossiness ("Smoothness", Range(0,1)) = 0.5
    6.         [Gamma] _Metallic ("Metallic", Range(0,1)) = 0.0
    7.     }
    8.     SubShader {
    9.         Tags { "RenderType"="Opaque" }
    10.         LOD 200
    11.  
    12.         CGPROGRAM
    13.         // Physically based Standard lighting model, and enable shadows on all light types
    14.         #pragma surface surf Standard fullforwardshadows vertex:vert novertexlights
    15.  
    16.         // Use shader model 3.0 target, to get nicer looking lighting
    17.         #pragma target 3.0
    18.  
    19.         float3 Shade4PointLightsAlt (
    20.             float4 lightPosX, float4 lightPosY, float4 lightPosZ,
    21.             float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
    22.             float4 lightAttenSq,
    23.             float3 pos, float3 normal)
    24.         {
    25.             // to light vectors
    26.             float4 toLightX = lightPosX - pos.x;
    27.             float4 toLightY = lightPosY - pos.y;
    28.             float4 toLightZ = lightPosZ - pos.z;
    29.             // squared lengths
    30.             float4 lengthSq = 0;
    31.             lengthSq += toLightX * toLightX;
    32.             lengthSq += toLightY * toLightY;
    33.             lengthSq += toLightZ * toLightZ;
    34.             // don't produce NaNs if some vertex position overlaps with the light
    35.             lengthSq = max(lengthSq, 0.000001);
    36.  
    37.             // NdotL
    38.             float4 ndotl = 0;
    39.             ndotl += toLightX * normal.x;
    40.             ndotl += toLightY * normal.y;
    41.             ndotl += toLightZ * normal.z;
    42.             // correct NdotL
    43.             float4 corr = rsqrt(lengthSq);
    44.             ndotl = max (float4(0,0,0,0), ndotl * corr);
    45.  
    46.             // attenuation
    47.             float4 ranges = (0.005 * sqrt(1000000.0 - lightAttenSq)) / sqrt(lightAttenSq);  
    48.             float4 atten01 = lengthSq * corr / ranges;
    49.             float4 atten = saturate(1.0 / (1.0 + 25.0 * atten01 * atten01) * saturate((1.0 - atten01) * 5.0));
    50.             float4 diff = ndotl * atten;
    51.             // final color
    52.             float3 col = 0;
    53.             col += lightColor0 * diff.x;
    54.             col += lightColor1 * diff.y;
    55.             col += lightColor2 * diff.z;
    56.             col += lightColor3 * diff.w;
    57.             return col;
    58.         }
    59.  
    60.         sampler2D _MainTex;
    61.  
    62.         struct Input {
    63.             float2 uv_MainTex;
    64.             half3 vertexLighting;
    65.         };
    66.  
    67.         half _Glossiness;
    68.         half _Metallic;
    69.         fixed4 _Color;
    70.  
    71.         void vert(inout appdata_full v, out Input o)
    72.         {
    73.             UNITY_INITIALIZE_OUTPUT(Input,o);
    74.  
    75.             #ifdef UNITY_PASS_FORWARDBASE
    76.             o.vertexLighting = Shade4PointLightsAlt (
    77.                 unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
    78.                 unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
    79.                 unity_4LightAtten0, mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0)).xyz, UnityObjectToWorldNormal(v.normal));
    80.             #endif
    81.         }
    82.  
    83.         void surf (Input IN, inout SurfaceOutputStandard o) {
    84.             // Albedo comes from a texture tinted by color
    85.             fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    86.             o.Albedo = c.rgb;
    87.             // Metallic and smoothness come from slider variables
    88.             o.Metallic = _Metallic;
    89.             o.Smoothness = _Glossiness;
    90.             o.Alpha = c.a;
    91.  
    92.             o.Emission = o.Albedo * IN.vertexLighting;
    93.         }
    94.         ENDCG
    95.     }
    96.     FallBack "Diffuse"
    97. }
     
  4. bluescrn

    bluescrn

    Joined:
    Feb 25, 2013
    Posts:
    642
    hmmm, if you use 'novertexlights', does Unity actually set the light parameters up correctly for that mesh? Or do you get some left-over values that were set for a previous vertex-lit mesh?

    (The reason that I've never used Unity vertex lights until now is that, at least in the 4.x/5.x days, they simply wouldn't light anything that was lightmapped - and no amount of custom shader work could solve it - it simply didn't set up the lights for lightmapped meshes. The workaround was to implement our own global set of 4 vertex lights instead. But this time, there's no lightmaps involved)
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,338
    It just sets a shader keyword, doesn't seem to change anything for what it's passing to the shader, no.

    Also the documentation claims novertexlights disables ambient lighting, which isn't true.
     
  6. bluescrn

    bluescrn

    Joined:
    Feb 25, 2013
    Posts:
    642
    I may be wrong, but AFAIK, Unity sets vertex lights up per-mesh (e.g. closest 4 lights?), not globally. So they can be potentially be incorrect (set up for another mesh) or not set at all?

    May depend on the ambient mode. I was using a flat colour for ambient, and it did lose that.
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,338
    Correct, but setting of a keyword on the shader doesn't in this case seem to change the behavior of the engine. It still sets up the vertex light data for the mesh. Usually any behavioral changes happen based on pass tags or material queue rather than anything inside the CGPROGRAM block, though there are some controlled by shader variants (which this does affect). Easy enough to test with the example shader I posted above.

    Most curious. All ambient lighting modes bake into the spherical harmonic based ambient probes, so changing the ambient mode doesn't actually change anything for the shader.

    Here's a setup with 7 lights in the scene, 1 large white one shared between both, and 3 colored ones that overlap a bit into each mesh's bounds, but everything is working correctly. Ambient light mode is also set to solid color, and it works regardless of if I have the ambient set to realtime or baked, or if the shader is using normal maps or not (which potentially changes if the ambient lighting is done per vertex or per pixel). Adding noambient absolutely does disable ambient lighting though, as one would expect. I'm not doubting you that ambient lighting stops working for you, I'm just curious under what specific scenario it happens since I can't reproduce it.
    upload_2019-10-11_9-52-34.png
     
  8. InnerScript

    InnerScript

    Joined:
    Jan 29, 2017
    Posts:
    38
    Thank you for this! You've saved me so much headache.

    I was implementing a vertex lit shader, and Unity's default attenuation caused popping when the light was near, (but still out of range) and was using an incorrect attenuation from what I would expect. Yes, I expected the light radius to reflect the real world scale. Go figure.

    I think Unity should implement this in some fashion so developers can select to use "Correct Vertex Light Attenuation" in project settings, as the world is still very interested in using vertex lighting.