Search Unity

How do I get my additive particle effects to look consistent between light and dark backgrounds?

Discussion in 'General Graphics' started by AssembledVS, Jan 30, 2020.

  1. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    I’ve been using the Particles/Standard Unlit/Additive Multiply shader for particle effects that should look luminous and glow-y. The issue is that, while this shader looks great over most backgrounds, very light backgrounds make the effects invisible.

    Here is what it looks like (and should look like over all backgrounds):


    The particular effect I’m working on now uses texture sheet animation to randomly choose between a number of sprites:




    I have a very limited understanding of shaders, but what I’ve read seems to indicate that I should be using an Alpha Blended Premultiply shader. However, when I use this shader (Legacy Shaders/Particles/Alpha Blended Premultiply or whatever similar ones I can find online), I’m just getting a box for my particle, as if the shape and alpha of the sprite is not being considered. I also don’t know what to use for the Particle Texture field, as my effect is composed of multiple sprites. I also don’t know if I’m supposed to be putting a sprite there. What’s more (and I’m not sure if this is because I’m setting the material up improperly) is that the effect over white seems to be the same as using the Particles/Standard Unlit/Additive Multiply shader.

    Here are the settings and what the effect looks like:




    A hacky solution is making two particle systems that are superimposed, identical except for their materials: one uses Particles/Standard Unlit/Additive Multiply and the other uses Legacy Shaders/Particles/Multiply. Disabling random seed and using a value of 0 for both makes them line up exactly at every frame. In this setup, I get the results that I’m looking for: everything looks similar no matter the background color, including pure white or black, and the effect is luminous. But this can’t be the way to go.

    All images:
    https://imgur.com/a/2MF8eI6
     
    matthiaskruis likes this.
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Any purely additive particle effect is going to have this issue of looking terrible on bright backgrounds. Honestly it's often a problem for alpha blended effects too. But the solution you came up with is actually a good one, as insane as that sounds. A slightly easier to work with revision of that solution would be to write a custom shader with two passes that combines both of those shaders so you don't need two separate particle systems.
     
    richardkettlewell likes this.
  3. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    Thanks for the reply.

    I think that the custom shader with two passes would be much better than having two particle systems. Many particles should look additive/luminous, so I can't imagine doing this for so many effects, etc. Unfortunately, I've never written shaders, but I have used Shader Forge and other programs. Would this be possible to do with Shader Graph or Amplify Shader Editor?

    So you're saying that Alpha Blended Premultiply is not the right solution here?
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Amplify, as far as I know, yes.

    Shader Graph (and SRPs in general AFAIK), no. SRPs dont support multi-pass shaders very well, and in this case it's two unlit passes which SRPs can't do at all. You could hack around it by setting one unlit pass as being a lit pass, and just don't do lighting. But none of this is an option in Shader Graph since it'll only ever use a single pass even if you have multiple Master nodes.

    I have no idea what Amplify does when you have multiple passes and you're using one of Unity's SRP.

    It's fine. The main difference between the different particle shaders is how it handles the blend mode and output color value. Traditional alpha blending is
    Blend SrcAlpha OneMinusSrcAlpha
    , which works as most people expect transparency to work with the alpha controlling the fade from the "background" to the output color. Additive blending is
    Blend SrcAlpha One
    and just does what it says, adds the output color with the background, though it also multiplies the out color by the out alpha before adding it. Alternatively you could use
    Blend One One
    which ignores the output alpha entirely, which is how it works more traditionally, but often confuses people.

    The premultiplied and additive multiply shaders both use a premultiplied blend mode,
    Blend One OneMinusSrcAlpha
    , which usually means you'd need to have the color value of your texture assets pre-multiplied by the alpha value, but it lets you have both additive and alpha blended elements in the same material... but both shaders modify the outputs so this isn't how they work. These are fine to use if they get you the look you want, but I generally avoid them (or write my own shaders that actually do premultiplied alpha properly).

    The problem is any additive shaders will eventually go to white (assuming they're not using a fully saturated color), and if the background is already white then there's "no where to go" and it gets lost. Alpha blended shaders, including premultiplied alpha ones, will always darken or replacing what's been rendered before, so they don't exhibit the nice looking color shift to white that additive shaders have.

    Using two passes means you can darken the background using a multiply or alpha blending shader in one pass, and then in the second pass add over the darkened or colored area. This gives you the benefits of both passes, but it's only possible if done as two passes where the darkening happens first for all particles before the second. Your hacky setup with two particle systems worked this way, and rendering using a two pass shader will as well. Technically I think you can also assign multiple materials to a single particle system and it'll render both, but I don't remember if the interface lets you do that (mesh renderers do).
     
  5. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    I thought that Shader Graph was just Unity's version of a visual shader-authoring system. Luckily I own Amplify Shader so I may take a look at that.

    I wish that I understood most of the
    Blend One OneMinusSrcAlpha
    and other pieces of shader code. I have messed around a little within shader files but never wrote anything. Maybe if I compare the various shaders I can figure out a way to combine them.

    Just looked up the multiple materials to a single shader. You can't do it directly in the Inspector by default but I think there's some workaround: https://answers.unity.com/questions/725590/multiple-materials-on-particle-system.html If this works, this would be way easier than multiple particle systems (and way less heavy, too). How would I make sure that the additive gets rendered on top of the multiplied?
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    I mean, it is, but it only works with Unity's own SRPs (LWRP, URP, or HDRP), and is pretty far behind Amplify still in features they expose.

    The
    Blend
    part is honestly the easiest part to understand once you wrap your head around it. It controls the blend function the GPU uses; ie: what math it applies when adding the shader output (source, or Src) to what's previously been rendered (destination, or Dst). The
    Blend
    has two elements which control what it multiplies the source and the destination by.
    Blend "sourceMultiplier" "destiationMultiplier"


    The full blend function looks like this:
    finalColor = (source * sourceMultiplier) + (destination * destinationMultiplier);


    So lets go over a few blend modes.
    Blend One One
    - Basic additive blending.
    One
    means literally just that; 1.
    finalColor = (source * 1.0) + (destination * 1.0);

    or
    finalColor = source + destination;

    Hence, additive. It's literally just adding the color values together. For something like a particle, you'd want the texture or particle color to fade to black where you don't want the particle to show up as the alpha doesn't have any affect.

    Unity uses
    Blend SrcAlpha One
    instead for it's additive shader, which means it's multiplying the shader's output by it's alpha value, That looks like this:
    finalColor = (source * source.a) + destination;

    The affect of that is the alpha value fades out the shader, which is very useful for particle effects where you can just set a single color and just animate the alpha, or reuse textures original designed for alpha blending without them showing as a solid square.

    Blend SrcAlpha OneMinusSrcAlpha
    - Traditional alpha blending. We've already seen
    ScrAlpha
    , but now we have
    OneMinusSrcAlpha
    . This, again, means exactly what it says; 1.0 - the shader's out alpha value.
    finalColor = (source * source.a) + (destination * (1.0 - source.a));

    Math wise, this means anywhere the alpha is 1.0 the source is unaffected and the destination is black, and the two are added together, and vise versa. Visually it just means the color value is blending from one to the other. This is visually the same as how layer transparency works in Photoshop or Gimp layers, it's how transparency works in most 3D modelling programs when you fade out an object's opacity, etc. It's also the exact same math as a
    lerp
    you might see in a shader or c#.

    Blend One OneMinusSrcAlpha
    - Premultiplied alpha blending. So this is a combination of both additive blending and traditional alpha blending. Alpha darkens the destination but does not affect the source. Why would you want this? Well, it means you can do both additive and alpha blending in the same shader by careful manipulation of the color & alpha values! A solid color + black alpha is additive, + a white alpha is alpha blended. For textures you'd just need to multiply the color value by the alpha prior to using it if you want to replicate traditional alpha blending. In the past it also potentially saved one math operation (multiplying the source by the alpha) which could speed things up, so it was way more common to see being used in the early days of computer graphics, and is still very common for video compositing. It's been "reinvented" multiple times in real time rendering as people discover how useful it is.

    Unity's own premultipled particle shader used to work just like this. But at some point mid Unity 5 it was modified to multiply the color value by the particle's alpha value in the shader. This was probably because the default particle material used the premultiplied shader and it confused people that adjusting the particle's alpha didn't make the particle disappear, but instead get brighter (because it became additive). Instead of changing the default particle material to use the alpha blended material, they modified the premultiplied shader to not longer be a premultiplied shader, and now it acts more like a traditional alpha blended shader, but still requires a premultiplied texture. Unfortunately this subtly messed up and the premultiplied shader ends up more transparent than it should when using a texture with alpha.

    If you use a single shader with two passes, the order is (generally) determined by the order the passes exist in the shader. There are some bugs when it comes to lighting passes where this can break, but for an unlit shader it should be correct every time. If you have two particle systems both using the same material using that two pass material it may or may not render the first pass for both particle systems then the second pass for both particle systems. It may also render both passes for one system, then both passes for another. I don't remember which it'll do these days. So be mindful of that.

    If you want to go with the multi material option, then the only way to ensure the order is by modifying the materials' queues. The lower queue will render first. In this case for sure when you have multiple particle systems using these materials all of the particles will render the first material first, then all render the second.
     
  7. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    Thanks for the detailed reply, @bgolus.

    I think I did it - I combined LegacyShaders/Particles/Additive and LegacyShaders/Particles/Multiply into one shader and it seems to create the effect that I'm looking for, which is an additive-like, luminous effect no matter the background color. The reason I worked with the legacy shaders and not the current variants (current: Particles/Standard Unlit [Additive + Multiply] and Particles/Standard Unlit [Modulate + Multiply]) is because the actual shader code for each effect is separated within the legacy shaders. Since I really don't know what I'm doing, I found it easier to read and work with. It took a few tries moving different parts around until the shader worked as expected. Here is the final code:

    Code (CSharp):
    1. /* Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
    2. * Modified shader combining LegacyShaders/Particles/Additive and LegacyShaders/Particles/Multiply */
    3.  
    4. Shader "Test/AdditiveMultiplyTwoPass"
    5. {
    6.     Properties
    7.     {
    8.         _TintColor ("Tint Color", Color) = (0.5,0.5,0.5,0.5)
    9.         _MainTex ("Particle Texture", 2D) = "white" {}
    10.         _InvFade ("Soft Particles Factor", Range(0.01,3.0)) = 1.0
    11.     }
    12.  
    13.     Category
    14.     {
    15.         Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" }
    16.         Cull Off Lighting Off ZWrite Off
    17.  
    18.         SubShader
    19.         {
    20.             Pass
    21.             {
    22.                 Blend Zero SrcColor
    23.  
    24.                 CGPROGRAM
    25.                 #pragma vertex vert
    26.                 #pragma fragment frag
    27.                 #pragma target 2.0
    28.                 #pragma multi_compile_particles
    29.                 #pragma multi_compile_fog
    30.  
    31.                 #include "UnityCG.cginc"
    32.  
    33.                 sampler2D _MainTex;
    34.                 fixed4 _TintColor;
    35.  
    36.                 struct appdata_t {
    37.                     float4 vertex : POSITION;
    38.                     fixed4 color : COLOR;
    39.                     float2 texcoord : TEXCOORD0;
    40.                     UNITY_VERTEX_INPUT_INSTANCE_ID
    41.                 };
    42.  
    43.                 struct v2f {
    44.                     float4 vertex : SV_POSITION;
    45.                     fixed4 color : COLOR;
    46.                     float2 texcoord : TEXCOORD0;
    47.                     UNITY_FOG_COORDS(1)
    48.                     #ifdef SOFTPARTICLES_ON
    49.                     float4 projPos : TEXCOORD2;
    50.                     #endif
    51.                     UNITY_VERTEX_OUTPUT_STEREO
    52.                 };
    53.  
    54.                 float4 _MainTex_ST;
    55.  
    56.                 v2f vert (appdata_t v)
    57.                 {
    58.                     v2f o;
    59.                     UNITY_SETUP_INSTANCE_ID(v);
    60.                     UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
    61.                     o.vertex = UnityObjectToClipPos(v.vertex);
    62.                     #ifdef SOFTPARTICLES_ON
    63.                     o.projPos = ComputeScreenPos (o.vertex);
    64.                     COMPUTE_EYEDEPTH(o.projPos.z);
    65.                     #endif
    66.                     o.color = v.color;
    67.                     o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex);
    68.                     UNITY_TRANSFER_FOG(o,o.vertex);
    69.                     return o;
    70.                 }
    71.  
    72.                 UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
    73.                 float _InvFade;
    74.  
    75.                 fixed4 frag (v2f i) : SV_Target
    76.                 {
    77.                     #ifdef SOFTPARTICLES_ON
    78.                     float sceneZ = LinearEyeDepth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
    79.                     float partZ = i.projPos.z;
    80.                     float fade = saturate (_InvFade * (sceneZ-partZ));
    81.                     i.color.a *= fade;
    82.                     #endif
    83.  
    84.                     half4 prev = i.color * tex2D(_MainTex, i.texcoord);
    85.                     fixed4 col = lerp(half4(1,1,1,1), prev, prev.a);
    86.                     UNITY_APPLY_FOG_COLOR(i.fogCoord, col, fixed4(1,1,1,1)); // fog towards white due to our blend mode
    87.                     return col;
    88.                 }
    89.                 ENDCG
    90.             }
    91.  
    92.             Pass
    93.             {
    94.                 Blend SrcAlpha One
    95.                 ColorMask RGB
    96.        
    97.                 CGPROGRAM
    98.                 #pragma vertex vert
    99.                 #pragma fragment frag
    100.                 #pragma target 2.0
    101.                 #pragma multi_compile_particles
    102.                 #pragma multi_compile_fog
    103.  
    104.                 #include "UnityCG.cginc"
    105.  
    106.                 sampler2D _MainTex;
    107.                 fixed4 _TintColor;
    108.  
    109.                 struct appdata_t {
    110.                     float4 vertex : POSITION;
    111.                     fixed4 color : COLOR;
    112.                     float2 texcoord : TEXCOORD0;
    113.                     UNITY_VERTEX_INPUT_INSTANCE_ID
    114.                 };
    115.  
    116.                 struct v2f {
    117.                     float4 vertex : SV_POSITION;
    118.                     fixed4 color : COLOR;
    119.                     float2 texcoord : TEXCOORD0;
    120.                     UNITY_FOG_COORDS(1)
    121.                     #ifdef SOFTPARTICLES_ON
    122.                     float4 projPos : TEXCOORD2;
    123.                     #endif
    124.                     UNITY_VERTEX_OUTPUT_STEREO
    125.                 };
    126.  
    127.                 float4 _MainTex_ST;
    128.  
    129.                 v2f vert (appdata_t v)
    130.                 {
    131.                     v2f o;
    132.                     UNITY_SETUP_INSTANCE_ID(v);
    133.                     UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
    134.                     o.vertex = UnityObjectToClipPos(v.vertex);
    135.                     #ifdef SOFTPARTICLES_ON
    136.                     o.projPos = ComputeScreenPos (o.vertex);
    137.                     COMPUTE_EYEDEPTH(o.projPos.z);
    138.                     #endif
    139.                     o.color = v.color;
    140.                     o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex);
    141.                     UNITY_TRANSFER_FOG(o,o.vertex);
    142.                     return o;
    143.                 }
    144.  
    145.                 UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
    146.                 float _InvFade;
    147.  
    148.                 fixed4 frag (v2f i) : SV_Target
    149.                 {
    150.                     #ifdef SOFTPARTICLES_ON
    151.                     float sceneZ = LinearEyeDepth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
    152.                     float partZ = i.projPos.z;
    153.                     float fade = saturate (_InvFade * (sceneZ-partZ));
    154.                     i.color.a *= fade;
    155.                     #endif
    156.  
    157.                     fixed4 col = 2.0f * i.color * _TintColor * tex2D(_MainTex, i.texcoord);
    158.                     col.a = saturate(col.a); // alpha should not have double-brightness applied to it, but we can't fix that legacy behavior without breaking everyone's effects, so instead clamp the output to get sensible HDR behavior (case 967476)
    159.  
    160.                     UNITY_APPLY_FOG_COLOR(i.fogCoord, col, fixed4(0,0,0,0)); // fog towards black due to our blend mode
    161.                     return col;
    162.                 }
    163.                 ENDCG
    164.             }
    165.         }
    166.     }
    167. }
    Here is what the effect looks like over black and white backgrounds (it would have been invisible over white previously):



    Here are the material settings:



    Now, I'm not quite sure if I messed up somewhere and set myself up for issues later, but this seems to work. I do get the following warning on the shader file, but so do the original Unity legacy shaders: "Material property is found in another cbuffer than "UnityPerMaterial" (_MainTex_ST)":



    I take it as the additive renders over the multiplied because the additive is the second pass?

    I have saved your post for future reference. Thanks for the help!
     
    Last edited: Feb 1, 2020
    akasabutski, jiraphatK, xjjon and 8 others like this.