Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Creating a shader with colour channels?

Discussion in 'Shaders' started by Nanako, Mar 8, 2015.

  1. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    Hi all x i'd like to create a custom material/shader which supports a few colour channels, the idea being that i could change these at runtime to get a lot of different appearances out of a single material.

    I'm completely new to writing custom shaders, but i seem to have a specific need that can't be met otherwise, so i'm here to learn .I'd also appreciate links to tutorials and stuff on the subject matter

    Maybe oyu guys can help with my specific use case.
    The way i envision it working is something like this


    Base Texture:


    Alpha Map describing the area of the colour channel:



    Colour Applied to the channel



    Final result:



    so yea, there's a rough idea. I'm not 100% clear on how they should mix together. I made this mockup in photoshop using the overlay blend mode, but i noticed some odd behaviour from it the closer i got to a primary colour.

    The general gist of what i want though, is to have a greyscale base texture with shadows and highlighting, and to "tint" that by adding scripted colour, while maintaining the shadows and highlighting.

    how would i go about doing something like this?
     
  2. Whippets

    Whippets

    Joined:
    Feb 28, 2013
    Posts:
    1,775
    I would create an image (of the eye) with the parts you want colouring in in each of the 4 channels RGBA, and then write a shader that took 4 colour parameters and the image you created. The shader would swap out the image's colours and replace them with the given 4 colours.
     
    Nanako likes this.
  3. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
    This is quite a simple shader to write, so search for surface shaders in the documents, take the basic one that takes a basic diffuse texture, then duplicate the texture bits for a second texture, then in the surf function you do your texture look up as with the other texture, but this time you need to multiply you mask output by your color variable then by the texture then add the texture again, have a play see how you get on, if you get stuck I recon it would take me 10 minutes to knock this up for you, so feel free to ask.

    Here is the link, save you a search:

    http://docs.unity3d.com/Manual/SL-SurfaceShaderExamples.html
     
    Nanako likes this.
  4. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    :eek: thank you. i'll give this a go if i can't figure it out myself, i'll be back here requesting your help in, at most, 48 hours from now x
     
    Jonny-Roy likes this.
  5. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047

    Okay, and i'm sort of stuck.
    I'm actually kind of proud of how far i got:




    The black area is obviously undesireable. And also this only supports one mask/colour pair.

    Aside from getting it working, i want to have a total of four colour channels in the shader, and i'd like to also make it run on unity 5's new lighting model, rather than lambert. Her'es my code so far anyways. I need halp


    Code (CSharp):
    1. Shader "Test/Diffuse Texture"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.         _Color1 ("Channel 1 Colour", Color) = (1,1,1,1)
    7.         _Channel1 ("Texture", 2D) = "white" {}
    8.     }
    9.     SubShader
    10.     {
    11.         Tags
    12.         {
    13.             "RenderType" = "Opaque"
    14.         }
    15.         CGPROGRAM
    16.             #pragma surface surf Lambert
    17.             struct Input
    18.             {
    19.                 float2 uv_MainTex;
    20.                 float2 uv_Channel1;
    21.             };
    22.             sampler2D _MainTex;
    23.             sampler2D _Channel1;
    24.             fixed4 _Color1;
    25.  
    26.             void surf (Input IN, inout SurfaceOutput o)
    27.             {
    28.                 fixed4 c = tex2D(_Channel1, IN.uv_Channel1) * _Color1 * tex2D (_MainTex, IN.uv_MainTex);
    29.                 o.Albedo = c.rgb;
    30.                 o.Alpha = c.a;
    31.             }
    32.             /*
    33.             void surf (Input IN, inout SurfaceOutput o)
    34.             {
    35.                 o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
    36.             }*/
    37.         ENDCG
    38.     }
    39. Fallback "Diffuse"
    40. }
    @Jonny Roy come help me
     
  6. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
    Ooo, so close good work, shaders are very tricky for most people so you've done very good to get that far so easily and not just end up with a pink blob! So here is the correct shader, I've commented where I changed your code from above.


    Code (CSharp):
    1. Shader "Test/Diffuse Texture"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.         _Color1 ("Channel 1 Colour", Color) = (1,1,1,1)
    7.         _Channel1 ("Texture", 2D) = "white" {}
    8.     }
    9.     SubShader
    10.     {
    11.         Tags
    12.         {
    13.             "RenderType" = "Opaque"
    14.         }
    15.         CGPROGRAM
    16.             #pragma surface surf Lambert
    17.             struct Input
    18.             {
    19.                 //We only need uv_MainTex as the Channel1 texture will use the same UV set, this makes it more efficient!
    20.                 //If you used uv2 to give different coords in your model for the mask you would need it back!
    21.                 float2 uv_MainTex;
    22.             };
    23.             sampler2D _MainTex;
    24.             sampler2D _Channel1;
    25.             fixed4 _Color1;
    26.             void surf (Input IN, inout SurfaceOutput o)
    27.             {
    28.                 //Sample the mask
    29.                 fixed4 mask=tex2D (_Channel1, IN.uv_MainTex);
    30.                 //Sample the texture
    31.                 fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
    32.                 //Use lerp to make the texture tint only when the mask r channel is set
    33.                 fixed4 c = lerp(tex, tex * _Color1, mask.r);
    34.                 o.Albedo = c.rgb;
    35.                 o.Alpha = c.a;
    36.             }
    37.         ENDCG
    38.     }
    39. Fallback "Diffuse"
    40. }
    Now to get the Unity 5 lighting model we just tweak it a little more:

    Change:

    #pragma surface surf Lambert

    To:

    #pragma surface surf Standard

    Basically Lambert is the old style non-specular lighting, Standard is the new model.

    Then you also need to change:

    void surf (Input IN, inout SurfaceOutput o)

    To:

    void surf (Input IN, inout SurfaceOutputStandard o)

    Which give you the new properties which for your reference are:

    fixed3 Albedo; // base (diffuse or specular) color
    fixed3 Normal; // tangent space normal, if written
    half3 Emission;
    half Metallic; // 0=non-metal, 1=metal
    half Smoothness; // 0=rough, 1=smooth
    half Occlusion; // occlusion (default 1)
    fixed Alpha; // alpha for transparencies

    Not so bad huh! So for you color sliders it's pretty easy to add the extras as you created the first, for the math I'd use each channel in the mask to lerp each color slider, although it depends on the effect you want in the end. Have a try at finishing it up, I'm here if you need me. (I'm on UK time though, but will probably be online tonight)

    Jon

    PS for the next channel assign it as:

    c = lerp(c, c * _Color2, mask.g);

    and so forth!
     
    Nanako likes this.
  7. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    well that is all very confusin. I don't quite get it, let me see...

    Code (CSharp):
    1.  //Use lerp to make the texture tint only when the mask r channel is set
    2.                 fixed4 c = lerp(tex, tex * _Color1, mask.r);
    I dont understand what you've done here. Lerp finds a value between a higher and lower bound, right?
    What's with the use of mask.r ? why would you only use the red channel, and how does this not break it. How does this code cut away the black areas?

    I think i really need a thorough explanation of what this shader is doing, to any given pixel.

    ill poke around with the U5 version too, although you forgot to put it in code tags so i think the formatting is messed up ;-;
     
  8. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
    Okay this is easier than you think, so lerp basically does this (I'm assuming you math is pretty good :S)

    lerp(a, b, c) = a * c + b * (1 - c)

    Where c is clamped between 0 and 1, in other words, if mask.r is 0 then output tex if it's 1 output tex * _Color1, if it was 0.5 we'd mix the two. I only used the red channel, as I thought you'd want to use each of the other channel for your other masks and save on texture lookups!

    Does that make sense?
     
    Nanako likes this.
  9. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    now i realise that. It wasn't the original plan, but it seems like the best idea.

    I've made an attempt at implementing it, see bottom of post.

    The RGB channels work fine, but the alpha channel does not. (incidentally, this is the image i'm using to test, i exported this as a 32-bit tga)




    Anyways the alpha channel doesn't wor right. Rather than tinting an oblong area at the corner as shown there, channel 4 tints the whole image. I guess i need some different method for alpha?

    Here's m,y code:

    Code (CSharp):
    1.     Shader "Test/Diffuse Texture"
    2.     {
    3.         Properties
    4.         {
    5.             _MainTex ("Texture", 2D) = "white" {}
    6.              _Mask ("Texture", 2D) = "white" {}
    7.             _Color1 ("Channel 1 Colour", Color) = (1,1,1,1)
    8.             _Color2 ("Channel 2 Colour", Color) = (1,1,1,1)
    9.             _Color3 ("Channel 3 Colour", Color) = (1,1,1,1)
    10.             _Color4 ("Channel 4 Colour", Color) = (1,1,1,1)
    11.  
    12.         }
    13.         SubShader
    14.         {
    15.             Tags
    16.             {
    17.                 "RenderType" = "Opaque"
    18.             }
    19.             CGPROGRAM
    20.                 #pragma surface surf Standard
    21.                 struct Input
    22.                 {
    23.                     //We only need uv_MainTex as the Channel1 texture will use the same UV set, this makes it more efficient!
    24.                     //If you used uv2 to give different coords in your model for the mask you would need it back!
    25.                     float2 uv_MainTex;
    26.                 };
    27.                 sampler2D _MainTex;
    28.                 sampler2D _Mask;
    29.                 fixed4 _Color1;
    30.                 fixed4 _Color2;
    31.                 fixed4 _Color3;
    32.                 fixed4 _Color4;
    33.                 fixed3 Albedo; // base (diffuse or specular) color
    34.                 fixed3 Normal; // tangent space normal, if written
    35.                 half3 Emission;
    36.                 half Metallic; // 0=non-metal, 1=metal
    37.                 half Smoothness; // 0=rough, 1=smooth
    38.                 half Occlusion; // occlusion (default 1)
    39.                 fixed Alpha; // alpha for transparencies
    40.                 void surf (Input IN, inout SurfaceOutputStandard o)
    41.                 {
    42.                     //Sample the mask
    43.                     fixed4 mask=tex2D (_Mask, IN.uv_MainTex);
    44.                     //Sample the texture
    45.                     fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
    46.                     //Use lerp to make the texture tint only when the mask r channel is set
    47.                     fixed4 c = lerp(tex, tex * _Color1, mask.r);
    48.                     c = lerp(c,c * _Color2, mask.g);
    49.                     c = lerp(c, c * _Color3, mask.b);
    50.                     c = lerp(c, c * _Color4, mask.a);
    51.                     o.Albedo = c.rgb;
    52.                     o.Alpha = c.a;
    53.                 }
    54.             ENDCG
    55.         }
    56.     Fallback "Diffuse"
    57.     }
    58.  
     
  10. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
    I think your code looks okay, check in Unity on the texture import settings and make sure it has Alpha is transparency ticked, it maybe that, but the preview image at the bottom should show the transparency if it's worked correctly!
     
  11. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    Edit: i screwed up. Now got the alpha channel imported correctly, see next post in a minute...
     
    Last edited: Mar 10, 2015
  12. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    Okay here's what i've ended up with now:



    The alpha is working correctly, but it seems its also cutting out any pixels that are masked out. I don't understand this.

    surely the color information is still there, even for pixels with 0 alpha? But it seems reading them isnt working
     
  13. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
    Ah, I've had this before, try it as a tif, photoshop does this thing with some formats where 0 alpha= 0 for everything else or something. I think I solved it with a tif.
     
  14. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    tif doesnt seem to help
     
  15. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    any other ideas? i guess i'll poke arouind with other formats and see what i can do, this seems rather troublesome :/
     
  16. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
    Do you have it as a psd? PM it if you can I'll take a quick look.
     
  17. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
    Actually, I have a working sample, let me send it to you.
     
  18. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    The issue's probably the very last line there;
    Code (csharp):
    1. o.Alpha= c.a;
    You're assigning the transparency to be a blend of the stuff that's come before - that's stored in the alpha of the c variable.
    I assume you want;
    Code (csharp):
    1. o.Alpha= tex.a;
    That will retain the alpha channel of the original texture. That said, you don't have any transparency set up in that shader so you might as well use.
    Code (csharp):
    1. o.Alpha = 1.0;
    You're also progressively multiplying the result by the colours each time you do the lerp... I suspect you might want something closer to...

    Code (csharp):
    1. //Sample the texture
    2. fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
    3. //Sample the mask
    4. fixed4 mask=tex2D (_Mask, IN.uv_MainTex);
    5. //Use lerp to make the texture tint only when the mask r channel is set
    6. fixed3 c = tex.rgb;
    7. c = lerp(c, tex.rgb * _Color1.rgb, mask.r);
    8. c = lerp(c, tex.rgb * _Color2.rgb, mask.g);
    9. c = lerp(c, tex.rgb * _Color3.rgb, mask.b);
    10. c = lerp(c, tex.rgb * _Color4.rgb, mask.a);
    11. o.Albedo = c.rgb;
    12. o.Alpha = tex.a;
    Alternatively, you could normalize the mask values so that the mask values always add up to 1.0, that way you're not getting a darker result than you should do.
    Code (csharp):
    1. //Sample the texture
    2. fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
    3. //Sample the mask
    4. fixed4 mask = normalize(tex2D (_Mask, IN.uv_MainTex));
    5. //Use lerp to make the texture tint only when the mask r channel is set
    6. fixed3 c = tex.rgb;
    7. c = lerp(c, c * _Color1.rgb, mask.r);
    8. c = lerp(c, c * _Color2.rgb, mask.g);
    9. c = lerp(c, c * _Color3.rgb, mask.b);
    10. c = lerp(c, c * _Color4.rgb, mask.a);
    11. o.Albedo = c.rgb;
    12. o.Alpha = tex.a;
     
    Nanako likes this.
  19. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
     
    Nanako likes this.
  20. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
    For anyone looking Farfarer blending options are quite valid solutions, the first will prioritise different colors, the second works to an extent but will error if there are zeros across the whole mask, a blend option to cover the zero situation is as follows:


    Code (CSharp):
    1.                 void surf (Input IN, inout SurfaceOutputStandard o)
    2.                 {
    3.                     //Sample the mask
    4.                     float4 mask=tex2D (_Mask, IN.uv_MainTex);
    5.                     //Sample the texture
    6.                     fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
    7.                    
    8.                     float sum=mask.r+mask.g+mask.b+mask.a;                  
    9.                     fixed4 c=lerp(tex,tex * (mask.r* _Color1 + mask.g* _Color2 +  mask.b* _Color3 + mask.a* _Color4) / max(1.0,sum),saturate(sum));
    10.                    
    11.                     o.Albedo = c.rgb;
    12.                     o.Alpha = c.a;
    13.                 }
    Try each and see which works best for your case :)
     
    Farfarer likes this.
  21. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    Nicely fixed :)
     
    Jonny-Roy likes this.
  22. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
    Thanks Farfarer...I should add for everyone else the alpha issue is caused by how photoshop exports instructions to get it to work are:
    If so, here is the process:

    1) fill the background layer in Photoshop black.
    2) Draw you colours over the black, so it should have no transparency at all once you are done.
    3) Swap to channels, click the "Create new Channel" button (the one that looks like a page with the corner folded over)
    4) I rename it to Alpha, but I don't think you have to, click on it so it's highlighted, all you colours should disappear so it looks black.
    5) Draw your alpha channel using White and black!
    then I normally save as a Tif

    Then use these settings in Unity to import:

    Texture Type: Texture
    Both Alpha boxes unticked
    Wrap repeat
    Bilinear
    Ansio 1
    format Compressed

    Nothing is as easy as it should be :D
     
    Nanako likes this.
  23. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    We got it working. The solution was to use a .tif image for the mask, with an alpha channel built in. And then in unity, to NOT tick the "Alpha is Transparency" box on the texture import settings. Once i unchecked that everything worked nicely.

    Next up is to test the blending and stuff. Could someone possibly elaborate about the normalising/darkening issue? i dont fully understand what's going on there
     
    Jonny-Roy likes this.
  24. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
    The normalizing issue is because Normalize does this:

    float total=(r+g+b+a);
    float4 result=float4(r/total,g/total,b/total,a/total);

    So if nothing is set you get a division by 0 error, in most GPU's it will exit the shader at that point filling the pixel black!
     
  25. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    Yeah, normalize effectively ensures that all of the components of the variable, when added together, equals 1.

    Usually this is mostly important when you're dealing with vectors that need to be unit length (which is shorthand for saying the sum of it's components add up to 1).

    But in this case, say a pixel of your mask image has 1.0 or white in each of the rgb channels... when you blend it all together, you're getting the texture multiplied fully by each of your blend colours.
    Say your blend colours were pure red, pure green and pure blue... the end result would be totally black (because all of the components wind up being multiplied by 0 at some stage).
    Normalizing the mask ensures that you're only multiplying by a third of red, a third of blue and then a third of green.

    Depends on the end result you're after but I wouldn't think you want multiple tints to the same pixel to wind up with a black - or at least very dark - result.
     
  26. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    what's this saturate function?
     
  27. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    I'm using this, and it seems to be working s treat. But i'm still trying to figure out why

    if colours aren't normalised, wouldn't that be likely to make the total greater than 1, and thus to be lighter, not darker? i'd think the problem would be a loss of dynamic range and lots of things clamping at white.

    In any case, a very small new problem has cropped up. It seems i have a tiny, 1 pixel wide dark line at the edge of color zones, where they meet the base texture . My eye noticed this from afar, so i'm reasonably sure it's new.

    It wont be a big deal anyways, but i'm curious as to what caused it.


    My code now reads thusly:

    Code (CSharp):
    1. Shader "Test/Diffuse Texture"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.             _Mask ("Texture", 2D) = "white" {}
    7.         _Color1 ("Channel 1 Colour", Color) = (1,1,1,1)
    8.         _Color2 ("Channel 2 Colour", Color) = (1,1,1,1)
    9.         _Color3 ("Channel 3 Colour", Color) = (1,1,1,1)
    10.         _Color4 ("Channel 4 Colour", Color) = (1,1,1,1)
    11.  
    12.     }
    13.     SubShader
    14.     {
    15.         Tags
    16.         {
    17.             "RenderType" = "Opaque"
    18.         }
    19.         CGPROGRAM
    20.             #pragma surface surf Standard
    21.             struct Input
    22.             {
    23.                 //We only need uv_MainTex as the Channel1 texture will use the same UV set, this makes it more efficient!
    24.                 //If you used uv2 to give different coords in your model for the mask you would need it back!
    25.                 float2 uv_MainTex;
    26.             };
    27.             sampler2D _MainTex;
    28.             sampler2D _Mask;
    29.             fixed4 _Color1;
    30.             fixed4 _Color2;
    31.             fixed4 _Color3;
    32.             fixed4 _Color4;
    33.             fixed3 Albedo; // base (diffuse or specular) color
    34.             fixed3 Normal; // tangent space normal, if written
    35.             half3 Emission;
    36.             half Metallic; // 0=non-metal, 1=metal
    37.             half Smoothness; // 0=rough, 1=smooth
    38.             half Occlusion; // occlusion (default 1)
    39.             fixed Alpha; // alpha for transparencies
    40.             void surf (Input IN, inout SurfaceOutputStandard o)
    41.             {
    42.                 //Sample the mask
    43.                 fixed4 mask=tex2D (_Mask, IN.uv_MainTex);
    44.                 //Sample the texture
    45.                 fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
    46.                 //Use lerp to make the texture tint only when the mask r channel is set
    47.                 float sum = mask.r+mask.g+mask.b+mask.a;
    48.                 fixed4 c = lerp(tex,(tex * (mask.r* _Color1 + mask.g* _Color2 +  mask.b* _Color3 + mask.a* _Color4) / max(1.0,sum)),saturate(sum));
    49.          
    50.                 o.Albedo = c.rgb;
    51.                 o.Alpha = tex.a;
    52.             }
    53.         ENDCG
    54.     }
    55. Fallback "Diffuse"
    56. }
    57.  
     
  28. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666
  29. Jonny-Roy

    Jonny-Roy

    Joined:
    May 29, 2013
    Posts:
    666

    If colours aren't normalised, wouldn't that be likely to make the total greater than 1, and thus to be lighter, not darker? i'd think the problem would be a loss of dynamic range and lots of things clamping at white.


    The little sum gets the total of each color, then the max(1.0,sum) ensures you don't get a division by zero. I think the tiny line you are referring to is where you are getting texture interpolation, we could either tweak the formula to try and remove it or at least make it more of a blend than a difference, but I'd guess if you turned Mipmaps off on the mask texture and maybe turned filtering to point it might get rid of it.
     
  30. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    blending it would be nice, but its not really an issue, i'm probably going to not even end up having any visible base texture areas in the final application
     
    Jonny-Roy likes this.