Search Unity

Gradient Mapped Shader: Ideas?

Discussion in 'Shaders' started by paracosm, Apr 15, 2013.

  1. paracosm

    paracosm

    Joined:
    Apr 2, 2013
    Posts:
    44
    I am attempting to make a shader in unity that will behave similarly to the one by Andrew Maximov in UDK:

    http://www.youtube.com/watch?v=a4TcnfX5c3Y&feature=player_embedded

    For now, I want to start small. I don't intend to reproduce all of the functionality he has here all at once.

    I'd like to start by being able to convert a heightmap into something more like a heatmap. Meaning I'd like to be able to single out a certain range of grayscale values, normalize them back up to one through zero, and then multiply them by one of three colors such that the heighmap now has red hues where it was white, green where it was darker, and blue where it was darker than that, for example.

    Does anyone know how to approach this problem? The thing blocking me now is that I just don't know how to ask an image to only consider pixels within a certain range of values for the next operation. For instance, I know I can multiply a color by just the red channel, but how might I multiply a color by just a certain value range of an image or channel?

    Looking at it a different way, I know I can clamp the values in an image or channel. But this would leave all values outside my desired range as values that are within my range and not provide a useful mask.

    Is there a way to perform an operation similar to a clamp wherein the outlying values are set to 0?
     
  2. 747823

    747823

    Joined:
    Apr 10, 2013
    Posts:
    43
    An easier way to make a gradient mapping shader based on height would be to just sample a gradient texture created externally, and you decide where to sample based on the heightmap's brightness in your case. pseudo-code example:

    texture height_in;
    texture gradient_in;
    otherparameter = 0; // this could be something else if you want to sample it in 2d, but not necessary for your purposes

    height_out = sampler2d( height_in, uvcoords );
    gradient_out = sampler2d( gradient_in, float2( height_out.r, otherparameter ) );

    Then you would blend it however you want with other textures. This method is very simple as you can see, but you have to make the texture externally so if it is important to you that you specify the colors inside of unity then maybe this is not helpful.
     
    Last edited: Apr 16, 2013
  3. paracosm

    paracosm

    Joined:
    Apr 2, 2013
    Posts:
    44
    Yes the purpose of specifying the colors in the unity material is to eliminate a diffuse texture all together. This would allow me to create a Diffuse Bump Spec material with just one texture if Unity didn't hijack normal maps and nuke any data you store in the relatively useless blue and alpha channels. However, even without being able to stuff a height map and spec map into the normal map, I am able to create shaders which provide masks for reflection, specular power, and emissivity with just 2 textures (a normal and a mix map) if I can use a gradient plotted over a height map to cover the diffuse. The result is quite effective.

    I have managed to figure out the obvious solution for a 2 color gradient just by writing out what I wanted in plain language. It suddenly became obvious I was just talking about a lerp. So now I have 2 colors plotted across a height map. Now I have to figure out how to combine lerps and clamps in the right way to get "n" colors plotted across the height map, though I find there isn't much use for more than 4 colors.

    For reference my albedo input is:

    Where gmap.r is the texture information for my mix map in which the r channel represents my heightmap.
     
  4. 747823

    747823

    Joined:
    Apr 10, 2013
    Posts:
    43
    You could try this:
    Code (csharp):
    1.  
    2.  
    3. //So say you have like 4 points (on the gradient):
    4. point0 = 0, point1 = 0.1, point2 = 0.7, point3 = 0.9;
    5.  
    6. //And 4 colors:
    7. color0 = (0, 0, 0);
    8. color1 = (1, 1, 1);
    9. color2 = (0, 1, 0);
    10. color3 = (0, 0, 1);
    11.  
    12.  
    13. float3 gradientmap()
    14. {
    15.     float3 color_out = color3;
    16.  
    17.     if ( height.r < point0 )
    18.     {
    19.         color_out = color0;
    20.     }
    21.     else if (height.r <= point1 )
    22.     {
    23.         t = clamp( height.r - point0, 0, 1);
    24.         t /= 1 - (point1+point0);
    25.         color_out = lerp( color0, color1, t );
    26.     }
    27.  
    28.     else if ( height.r <= point2 )
    29.     {
    30.         t = clamp( height.r - point1, 0, 1);
    31.         t /= 1 - (point2+point1);
    32.         color_out = lerp( color1, color2, t );
    33.     }
    34.  
    35.     else if ( height.r <= point3 )
    36.     {
    37.         t = clamp( height.r - point2, 0, 1);
    38.         t /= 1 - (point2+point3);
    39.         color_out = lerp( color1, color2, t );
    40.     }
    41.     /*
    42.     else
    43.     {
    44.         color_out = color3; //Don't need this if you set it to color3 by default
    45.     }
    46.     */
    47.     return color_out;
    48. }
    49.  
    50. //You could also use nonlinear interpolation, it might look better. Simple example:
    51. float3 cosinterp( float3 a, float3 b, float t )
    52. {
    53.     t = 1 - ( cos(t*pi) + 1 ) / 2;
    54.     return lerp( a, b, t );
    55. }
    56.  
    One downside of this is that you only have a fixed number of inputs. I suppose you could allow the number of inputs to scale by putting them all in an array and using a loop to do check which two points you are currently interpolating without knowing how many points there are. But you would probably still have to create separate shaders so that these values would be available in unity. Perhaps just begin with more than you'll ever need need (like 10) and if you don't need them all, make the "point" of the extras 1.0 so that they are never visible.
     
    Last edited: Apr 17, 2013
  5. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    There's nothing to stop you ignoring Unity's normal map compression and just rolling your own unpack.

    Just don't DXT5nm compress it and Unity will leave it as-is.
     
  6. 747823

    747823

    Joined:
    Apr 10, 2013
    Posts:
    43
    When I use PSD files as normal maps it still "nuked" two of the channels for me.

    From the testing I did it seems like unpacking (when file is a psd) still goes like this:

    float4 normal = tex2d(_normalmap, IN.uv)*2-1;
    normal.g = normal.a;
    normal.a = 1.0; // or whatever
    normal = normalize(normal);

    I was experimenting with this just last night. Perhaps unity automatically makes them into dxt5nm? Maybe if I save it as tga it will not?
     
    Last edited: Apr 17, 2013
  7. Noisecrime

    Noisecrime

    Joined:
    Apr 7, 2010
    Posts:
    2,054
    While you can completely ignore Unity's DXT5nm normal mapping, it would be unwise since the format is designed to enable compression with minimal loss/artefacts by using the green and alpha components. Of course you could ignore this if you were happy with uncompressed normal maps, but they can suck up a huge amount of memory.

    Alternatively you can keep Unity's DXT5nm format that swizzles the red component to alpha (green remains the same and blue or rather the z axis is derived in the shader), but simply utilise the other channels as well and add you own unpacking code to any shader. Though this will usually result in lower quality again due to the nature of dxt compressors, which may check all channels to determine compression levels.


    Unity will only convert to DXT5nm if you tell it to treat the image as a normalmap, though you can change the format afterwards to avoid compressing it, but it remains in the _Y_X component style.

    I had assumed like Farfarer that using a different compression scheme would leave it alone, but if you set it to a normalmap type this does not appear to be the case. For example importing a standard normalmap as a 'texture' in Unity and applying it to a cube, gives you the familiar blue tint image. Whilst setting the texture to normal map, even without compression causes the texture to appear grey.

    You don't have to set the texture to be a normalmap, though I don't remember off-hand how Unity will then treat it. I.e. does it treat it like a normal rgb normalmap or still a _Y_X normalmap? Obviously it will constantly complain/warn you that its not set to a normalmap when used though.

    You'll need to do some testing with the shaders and see if Unity expects _Y_X normalmaps when extracting normal data. If it does then presumably in photoshop all you need to do is swap the channels (e.g. XYZ_ to _Y_X) and then add your heightmap or other data into either or both of the red/blue channels.

    Let us know the results, as it will save others who have to go down this route. I always thought it was a bit wasteful not using the red/blue channels, but that had to be balanced against the benefits of having minimal artefact compression, which can save huge amounts of memory. Whilst it may be beneficial to put additional data into red/blue from a 'reducing the number of texture inputs' point of view, the potential for more artefacts due to using these channels may outweight the benefits.
     
    Last edited: Apr 17, 2013
  8. paracosm

    paracosm

    Joined:
    Apr 2, 2013
    Posts:
    44
    Yeah I thought about this recently. I'll just have to worry about other artists on the team hitting "fix now" because they like to follow orders from machines. lol. Thank you.

    What will probably happen is that I will only be able to use blue and alpha channels in my own shaders in which I do not use the UnpackNormal (tex2D (...)) call and instead just read in the texture's r and g and give a white blue (if I am using blue for something else).
     
    Last edited: Apr 26, 2013
  9. paracosm

    paracosm

    Joined:
    Apr 2, 2013
    Posts:
    44
    Here is the solution for a gradient mapped shader in which the thresholds between the four allowed colors are plotted evenly across the provided heightmap, if anyone is interested. I have various versions of the shader using normal info, having specular properties, having different amounts of colors (2 to 4) etc. I would like to modify the shader later such that the threshold can be edited via range sliders. For now, this achieves what I want. I don't know about the performance, but I am able to keep the bump and spec versions in shader 2.0. For the version with both bump and spec I had to go to shader 3.0.

    Code (csharp):
    1. o.Albedo = (_Color.rgb * max(0.33 - abs(hmap.r - 1), 0) * 3) + (_Color2.rgb * max(0.33 - abs(hmap.r - 0.67), 0) * 3) + (_Color3.rgb * max(0.33 - abs(hmap.r - 0.33), 0) * 3) + (_Color4.rgb * max(0.33 - hmap.r, 0) * 3);