Vertex program Multiply shader

Hi. I’m trying to write a vertex program version of the built-in Mobile/Particles/Multiply shader. It works with rgb textures, but it doesn’t work if the texture has alpha. What I’m doing wrong?

Shader "Unlit/Multiply"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags {"RenderType" = "Transparent" "IgnoreProjector" = "True" "Queue" = "Transparent"}
        Blend Zero SrcColor // Blend DstColor Zero
        Cull Off Lighting Off ZWrite Off
       

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
           
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
           
            fixed4 frag (v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }
    }
}
float4 result = tex2D(_MainTex, i.uv);
result = lerp(1.0, result, result.a);
return result;

Thanks. It worked. But I didn’t get this lerp. How can it return a valid value between a single float (1.0) and a float4 (result)?

If the alpha is 1, it returns the sampled texture. But if the alpha is 0, it returns 1? Does it mean (1,1,1,1)?

Depends on the shader compiler (which depends on what platform you’re working on and building for). Desktop, especially Windows PCs with an Nvidia GPU, are quite lenient about this kind of thing and will make a guess as to what the author wanted. That line will likely result in a compiler error if building for Android or iOS, and maybe even OSX. On PCs with AMD GPUs I’ve seen stuff like that compile, but then just not do what you might expect.

Let’s dissect that line a little.
result = lerp(1.0, result, result.a);

The variable result is a float4, so that’s a lerp between a float and a float4 which you correctly noticed doesn’t make any sense. So there’s three ways to handle that. One is to throw an error and say “don’t do that”, which is what you expected.

The other two are to treat that as being either:
result = lerp(float4(1.0, 1.0, 1.0, 1.0), result, result.a);
or:
result = lerp(1.0, result.r, result.a);

The first of those two is what is what it is likely choosing. In the case of the lerp function that’s probably a safe bet since the value you’re setting is also a float4, and otherwise if that value was a float the outcome would still kind of “work” even if it chose to expand the 1.0 to a float4 as it would then just take the first component of that result and the shader compiler’s optimizer would just skip the other 3 components ever being calculated. I’ve seen some compilers actually choose the second option though, probably because the first variable in the lerp is a float, and float4 = float is completely valid, even in more strict compilers.

So, that’s a long way of saying yes, it shouldn’t work, but it does because desktop shader compilers try to be smart.

As for why desktop is “smart” and mobile is “dumb”, really it comes down to the early days of PC gaming. A lot of devs were writing software that didn’t work, and they’d go to the GPU makers and API writers and ask “why doesn’t this work.” Early on you wouldn’t get any kind of error, usually something just didn’t render, or rendered wrong, or outright crashed the computer! Sometimes it’d be a legitimate bug with something other than the game dev’s code, but likely most of the time it was a silly typo or mistake like this. Someone probably decided it’d be easier to just “make it work” than spend time debugging and helping devs understand why terrible code didn’t work. Over a short time it became a marketing point; “our GPU can run game X perfectly and vendor B can’t!” Ever wonder why whenever a new big PC game comes out there are new drivers for your GPU with special “optimizations” for games? Part of that is because the GPU makers are building in new fixes to work around bad code from devs, and sometimes outright replacing bits of the game’s code with faster / working code! It’s become a bit of an arms race between Intel, AMD and Nvidia, and one that Nvidia has been winning. This is especially true since having devs continue to write badly authored graphics code that can run with out a noticeable issue on an Nvidia GPU (which a lot of devs are probably using themselves) but might have graphical glitches or outright fail on AMD and Intel only helps Nvidia’s image. Personally I use Nvidia for dev because I have found Photoshop has annoying issues like the selection marquee being off by one pixel when using AMD.

For mobile it seems early on it was decided to just make the APIs more strict, and make sure there were tools that would give an error instead of just failing to render or crashing the hardware (the later of which is what happened for early PC devs most of the time). Plus you can’t expect a phone to get updated as often as a gaming PC. Mobile devs probably have some fun stories about having to work around issues on specific devices because of this.

Thus concludes the bgolus unrequested history hour… tune back in next week…

I normally expand my vectors, but for lerp things are not what they seem. As far as I know, there is no actual lerp instruction. It’s just a shortcut like so many other instructions. So then:

lerp(x, y, s)

is just a shortcut for:

x*(1-s) + y*s

So in this case, making y a scalar instead of a 4 component vector, actually reduces computation (depending on the hardware.) And it is perfectly fine to add a scalar to a vector as far as I know. (There is no risk it’s interpreted as add this scalar only to the first component.)

I think I’ve also read that in a shader optimization guide somewhere. (Together with some even better tricks.)

1 Like

Depends on the implementation on the compiler side. For some GPUs it might implement it as a bunch functions with matched vector pairs, in which case it’ll complain. Or it might be using:

lerp(x, y, s) { return a + s * (y - x); }

In which case the float 1 might not be safe, even though float4 - float is usually fine, float - float4 definitely isn’t.

True, it does depend on the compiler implementation. But, one part of that optimization guide pointed out that you should always go MAD :wink: That is one of the easiest optimizations that the compiler won’t just do for you. (Combining MUL and ADD into a MAD instruction.)

Considering that, the logical implementation would be:
ys + (-xs + x)
or
-xs + (ys + x)

(Another note from that optimization guide is that - on inputs is free on desktop hardware.) So this way it comes down to two (MAD) instructions to do the lerp.

I have learned to not expect the logical implementation from mobile shader compilers. :wink:

Well, I have to agree with that. In earlier days the desktop compilers could also work fairly surprising. And as said, a negate is typically free on inputs for desktop hardware, but that says nothing about mobile hardware.