Search Unity

Cel Shader from scratch fix for more lights

Discussion in 'Shaders' started by TenBitNet, Jan 16, 2019.

  1. TenBitNet

    TenBitNet

    Joined:
    Nov 9, 2017
    Posts:
    29
    I need my shader to support multiple lights. What I want to happen is for the secondary light change the vertex color to be bright like the first light. So it will increase the lit area.

    1 light

    2 lights


    Code (CSharp):
    1. Shader "Custom/Cartoon"
    2. {
    3.     Properties
    4.     {
    5.         [HideInInspector][PerRenderer][HDR]_Color ("Color", Color) = (1,1,1,1)
    6.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    7.         _LitArea("Lit Area", Range(0,1)) = 0
    8.     }
    9.     SubShader
    10.     {
    11.         Tags { "RenderType"="Opaque"}
    12.         AlphaToMask On
    13.  
    14.  
    15.         LOD 200
    16.  
    17.         CGPROGRAM
    18.         //Custom light
    19.         #pragma surface surf Cartoon
    20.  
    21.         // Use shader model 3.0 target, to get nicer looking lighting
    22.         #pragma target 3.0
    23.  
    24.         sampler2D _MainTex;
    25.  
    26.         struct Input
    27.         {
    28.             float2 uv_MainTex;
    29.         };
    30.  
    31.         fixed4 _Color;
    32.  
    33.         half _CellAmount;
    34.         half _LitArea;
    35.  
    36.         void surf (Input IN, inout SurfaceOutput o)
    37.         {
    38.             // Albedo comes from a texture tinted by color
    39.             o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgba;
    40.         }
    41.  
    42.         half4 LightingCartoon (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
    43.         {
    44.             //Direction of the light
    45.             half NdotL = (dot(s.Normal, normalize(lightDir)) + 1)/2;
    46.             //Store Color
    47.             half4 c;
    48.             //Determine if it is in a lit area then smoothly transition to dark
    49.             half Cell = smoothstep(_LitArea-.01, _LitArea, NdotL);
    50.             //Calculate the light on the tip of the object to the middle
    51.             half rim = 1- saturate(dot(normalize(viewDir), s.Normal));
    52.             //Add lighting to the rim of lit areas
    53.             half3 _rimLighting = pow(rim, 5) * smoothstep(_LitArea,1,NdotL)*_LightColor0.rgb;
    54.             //Calculate the color of the vert
    55.             half3 _shadingOfObject = s.Albedo * _Color * (Cell+.3) -.3;
    56.             //Put them together
    57.             c.rgb = _shadingOfObject + _rimLighting;
    58.             c.a = s.Alpha;
    59.             return c;
    60.         }
    61.        
    62.  
    63.         ENDCG
    64.     }
    65.     FallBack "Diffuse"
    66. }
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    Not possible with a surface shader.

    Unity's lighting system is a multi pass additive system. Each light draws the entire object again and adds the results on top of the previous pass. What you want is to have knowledge of all lights in one pass so you can take the min light value of all lights affecting the mesh rather then adding them together. But this just isn't how Unity's lighting system works, and Surface Shaders are confined to this system.

    The "easiest" way to do this might be too take the generated shader code from a surface shader and change the blend mode of the ForwardAdd from the default:
    Blend One One

    to:
    BlendOp Min
    BlendOp Max

    The other way I'd go about it is to write a custom vertex fragment shader with only the ForwardBase pass and uses the data intended to be used for vertex lights to do all lighting. That limits you to one directional light and four point lights per object.
     
    Last edited: Jan 17, 2019
    Subliminum and TenBitNet like this.
  3. TenBitNet

    TenBitNet

    Joined:
    Nov 9, 2017
    Posts:
    29
    So there is no way to get the previous color before hand even through vertex fragment shaders?

    Then subtract it from the new one then take the gray scale to determine if it is white or gray/black?

    Do you think adding textures would completely break this?

    Is it possible to just add textures on one final pass?

    Thanks for the reply I'm pretty new to shaders.
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    There's Unity's GrabPass stuff, but that's really slow, and there's no way to have Unity do this before every light's ForwardAdd pass. You're really just limited to whatever Blend operations are available on the GPU.
    https://docs.unity3d.com/Manual/SL-Blend.html

    Well, technically if you're only targeting Apple devices you can read the current framebuffer directly in the shader, but it really literally is only iOS devices that have this.

    You should need to do any subtraction. Pretty much any elementary arithmetic you do isn't going to get you what you want.

    Shouldn't.

    Sure, you could have a final pass that multiplies the final lighting results using Blend DstColor Zero, but this shouldn't be necessary.
     
  5. TenBitNet

    TenBitNet

    Joined:
    Nov 9, 2017
    Posts:
    29
    If Im correct I can have a blend mode for each pass if so would it be possible for me to save the passes into a texture this way I have something that tells me what is lit and what isn't then use that to tell me what is lit and what isn't (0 being the dark areas and 1 being the lit areas )
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    Yes!

    Yes and no. That requires a fairly significant change to how rendering systems work that's more than just modifying a shader file. You'd need to set the current render target to be said texture, render out your object's lighting too that texture, then change the render target again and sample the previous texture to get the lighting data. It's similar in concept to a light pre-pass rendering pipeline. That requires a good deal of c# scripting and basically not using any of Unity's existing rendering paths. It is also completely unnecessary since the math you need can already be handled by the per-pass blend modes.
     
  7. TenBitNet

    TenBitNet

    Joined:
    Nov 9, 2017
    Posts:
    29
    Except that I want rim lighting to know the light direction. For the rim lighting to be white. What I was thinking I would do is basically make the white area 1 and the dark 0. This way when I multiply it with forward add that it would only have the lit area then I would need to multiply it by the color/texture. I want after that I need to add in rim lighting so the rim lighting is in it. which is ok but in order to give it the look I want I need to add to that pass so that the dark part isn't just black which would go before color and rim lighting. Not to mention I wouldn't get the gradient effect through NdotL.

    Basically if I could somehow interact with the previous passes it probably would look a lot better.

    Also if I wanted to in the future I couldn't have the ability to (if I wanted) light it with a gradient of the color of the light to white
     
  8. TenBitNet

    TenBitNet

    Joined:
    Nov 9, 2017
    Posts:
    29
    I found graphics.blit but I don't understand how I'm going to get my render texture to reciere the calculated ndotl could you point me in the direction where I can figure out how to make a ndotl render texture
     
  9. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    Simple proof of concept using nothing but two shader passes:
    upload_2019-1-19_13-16-8.png
    Code (CSharp):
    1. Shader "Toon BlendOp Max"
    2. {
    3.     Properties {
    4.         _Color ("Color", Color) = (1,1,1,1)
    5.         [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
    6.     }
    7.  
    8.     SubShader {
    9.  
    10.         Pass {
    11.             Tags { "LightMode" = "ForwardBase" }
    12.             CGPROGRAM
    13.             #include "UnityCG.cginc"
    14.             #pragma multi_compile_fwdbase
    15.  
    16.             #pragma vertex vert
    17.             #pragma fragment frag
    18.  
    19.             float4 _Color;
    20.             sampler2D _MainTex;
    21.             float4 _LightColor0;
    22.  
    23.             struct v2f {
    24.                 float4 pos : SV_POSITION;
    25.                 float2 uv : TEXCOORD0;
    26.                 float3 normal : TEXCOORD1;
    27.             };
    28.  
    29.             void vert(appdata_full v, out v2f o)
    30.             {
    31.                 o.uv = v.texcoord;
    32.                 o.normal = UnityObjectToWorldNormal(v.normal);
    33.                 o.pos = UnityObjectToClipPos(v.vertex);
    34.             }
    35.  
    36.             float4 frag (v2f i) : SV_Target
    37.             {
    38.                 float4 tex = tex2D(_MainTex, i.uv);
    39.                 float ndotl = dot(i.normal, _WorldSpaceLightPos0.xyz);
    40.  
    41.                 float toon = saturate(ndotl / fwidth(ndotl) + 0.5);
    42.  
    43.                 float ambient = ShadeSH9(float4(i.normal, 1));
    44.  
    45.                 float3 lighting = lerp(ambient, _LightColor0.rgb, toon);
    46.  
    47.                 return float4(tex.rgb * _Color.rgb * lighting, 1.0);
    48.             }
    49.  
    50.             ENDCG
    51.         }
    52.  
    53.         Pass {
    54.             Tags { "LightMode" = "ForwardAdd" }
    55.             BlendOp Max
    56.  
    57.             CGPROGRAM
    58.             #include "UnityCG.cginc"
    59.             #pragma multi_compile_fwdadd
    60.  
    61.             #pragma vertex vert
    62.             #pragma fragment frag
    63.  
    64.             float4 _Color;
    65.             sampler2D _MainTex;
    66.             float4 _LightColor0;
    67.  
    68.             struct v2f {
    69.                 float4 pos : SV_POSITION;
    70.                 float2 uv : TEXCOORD0;
    71.                 float3 normal : TEXCOORD1;
    72.                 float3 worldPos : TEXCOORD2;
    73.             };
    74.  
    75.             void vert(appdata_full v, out v2f o)
    76.             {
    77.                 o.uv = v.texcoord;
    78.                 o.normal = UnityObjectToWorldNormal(v.normal);
    79.                 o.worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0)).xyz;
    80.                 o.pos = UnityObjectToClipPos(v.vertex);
    81.             }
    82.  
    83.             float4 frag (v2f i) : SV_Target
    84.             {
    85.                 float4 tex = tex2D(_MainTex, i.uv);
    86.                 float3 lightDir = _WorldSpaceLightPos0.w == 0 ? _WorldSpaceLightPos0.xyz : normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
    87.                 float ndotl = dot(i.normal, lightDir);
    88.  
    89.                 float toon = saturate(ndotl / fwidth(ndotl) + 0.5);
    90.  
    91.                 float3 lighting = toon * _LightColor0.rgb;
    92.  
    93.                 return float4(tex.rgb * _Color.rgb * lighting, 1.0);
    94.             }
    95.  
    96.             ENDCG
    97.         }
    98.     }
    99. }
     
    unity_EDiggw010YBkRA likes this.
  10. TenBitNet

    TenBitNet

    Joined:
    Nov 9, 2017
    Posts:
    29
    I understand that would work with the most intense lighting that is available. Although I want it to handle multiple lights if I could just make it a texture. Not only could I get the light color I could do everything I need to do. Im sorry for wasting your time have a nice day.
     
  11. TenBitNet

    TenBitNet

    Joined:
    Nov 9, 2017
    Posts:
    29
    I realize now my only choice is grabpass thanks for the help
     
  12. daxiongmao

    daxiongmao

    Joined:
    Feb 2, 2016
    Posts:
    412
    The main issue is that you can’t read and write to the same texture at the same time.

    So you have to get around that by making multiple textures and switching between them.

    Bgolus gave a few ways around it.

    The other would be you could gather information about nearby lights manually and add them to shader variables. Then use them in your surface shader.

    But doing grab passes and other multi texture stuff is going to hurt your performance. It could be worth it for a hero type entity but doing a whole scene would probably not be the best idea when other options exists.
     
  13. TenBitNet

    TenBitNet

    Joined:
    Nov 9, 2017
    Posts:
    29
    Yes but I only need one grab pass and it can store all the objects on screen using the same passes