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

Unpack Tangent space normal texture

Discussion in 'Shaders' started by Southridge1985, Feb 16, 2016.

  1. Southridge1985

    Southridge1985

    Joined:
    Jul 17, 2012
    Posts:
    17
    Hi everyone, I'm new to shader scripting and I have attemped to create a shader that will unpack my tangent space normal map correctly but what i have done does not work, This is for mobile use.
    Code (CSharp):
    1. Shader "Custom/SRCarMaterial" {
    2.     Properties {
    3.         _Color ("Color", Color) = (1,1,1,1)
    4.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    5.         _Glossiness ("Smoothness", Range(0,1)) = 0.5
    6.         _Metallic ("Metallic", Range(0,1)) = 0.0
    7.         _BumpMap ("Bumpmap",2D) = "bump"{}
    8.     }
    9.     SubShader {
    10.         Tags { "RenderType"="Opaque" }
    11.         LOD 200
    12.    
    13.         CGPROGRAM
    14.         // Physically based Standard lighting model, and enable shadows on all light types
    15.         #pragma surface surf Standard fullforwardshadows
    16.         // Use shader model 3.0 target, to get nicer looking lighting
    17.         #pragma target 3.0
    18.         sampler2D _MainTex;
    19.         sampler2D _BumpMap;
    20.         struct Input {
    21.             float2 uv_MainTex;
    22.             float2 uv_BumpMap;
    23.         };
    24.         half _Glossiness;
    25.         half _Metallic;
    26.         fixed4 _Color;
    27.         void surf (Input IN, inout SurfaceOutputStandard o) {
    28.             // Albedo comes from a texture tinted by color
    29.             fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    30.             o.Albedo = c.rgb;
    31.             // Metallic and smoothness come from slider variables
    32.             o.Metallic = _Metallic;
    33.             o.Smoothness = _Glossiness;
    34.             o.Alpha = c.a;
    35.             /// my crapy addition
    36.             half4 sample = tex2D(_BumpMap, IN.uv_BumpMap.xy);
    37.             half4 packedNormal = half4(0, sample.y, 0, sample.x);
    38.             o.Normal = UnpackNormal(packedNormal);
    39.         }
    40.         ENDCG
    41.     }
    42.     FallBack "Diffuse"
    43. }
     
    Last edited: Feb 16, 2016
  2. AcidArrow

    AcidArrow

    Joined:
    May 20, 2010
    Posts:
    10,936
    Why are you doing this? :

    half4 packedNormal = half4(0, sample.y, 0, sample.x);
     
  3. Southridge1985

    Southridge1985

    Joined:
    Jul 17, 2012
    Posts:
    17
    I was sent
    To keep a long answer short, I have a tangent space texture that i am importing at runtime, I cant get the same results as when i import into the editor and set type to normal map. I first tried to tackle this via runtime C# scripts. Then was istructed to attempt a fix through the shader instead. Check ouot my original post to get more detail. Thanks!
    http://forum.unity3d.com/threads/runtime-normal-map-import-please-help.384888/
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,209
    So there are two issues. One is that normal maps are linear textures and by default Texture2D creates an sRGB texture. The second is the one you're trying to work around by using packedNormal thing.

    The first is solved by creating the initial Texture2D using:
    new Texture2D(2048,2048,TextureFormat.RGBA32, false, true);

    The second issue can be solved by not using UnpackNormal. Instead just use:
    o.Normal = normalize(tex2D(_BumpMap, IN.uv_BumpMap.xy).xyz - 0.5);
     
  5. Southridge1985

    Southridge1985

    Joined:
    Jul 17, 2012
    Posts:
    17
    Thank you for the advise on the shader normal. The intital Texture2D still doesnt look right. When i use the same texture that was imported into the editor set as a normal map i get a perfect result, see below for comparison.
    Here are the results

    Shader code snippet implemented and bump convert script excecuted on imported normal map


    Shader code snippet + tangent space texture un altered as is


    The result i want but cant make happen during run time



    Any help to get this figured out would be really appreciated. Thanks!
     
  6. Owers

    Owers

    Joined:
    Jul 7, 2012
    Posts:
    39
    Something you should know, Unity does some weird things to your texture when you set the texture type to 'Normal map'.

    For PCs and consoles: Unity takes the Red channel from your texture and moves it to the Alpha channel. The Blue channel from your texture gets thrown away completely. This process occurs when you compile the game, not during runtime. In the shader, Unity will move the Alpha channel back into the Red channel, and generate a new Blue channel using a dot product function, then it remaps the values (normalmap * 2 - 1). This process is expensive, but can provide high quality normal maps even when they're compressed.

    For mobile: Unity will leave your texture as it is and simply remaps the values in the shader (normalmap * 2 - 1). This is much faster to process, but will decrease the visual quality of your normal map a bit.

    Now with that in mind, since you're targeting mobile, all you need to do is import the normal map like a diffuse texture and in your shader unpack your normals like this:

    Code (CSharp):
    1. o.Normal = tex2D(_BumpMap, IN.uv_BumpMap.xy) * 2 - 1;
    That's it. This should emulate exactly how Unity unpacks normals for mobile. Unity may complain about your texture not being marked as a normal map but you can ignore it, or use a variable name that isn't _BumpMap

    However, if you wish to use high quality normal maps for mobile you'll need to move the Red channel of your normal map to the Alpha channel yourself and unpack it with this:
    Code (CSharp):
    1. fixed3 normal;
    2. normal.xy = tex2D(_BumpMap, IN.uv_BumpMap.xy).wy * 2 - 1;
    3. normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
    4. o.Normal = normal;
    Just be aware that this may impact performance.

    I hope this has helped.

    Also if this is for mobile you may want to consider using fixed instead of half or float in your pixel shader. It's less precise but is better for performance.
     
    Last edited: Feb 17, 2016
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,209
    Also, with that code (or mine, but @Owers is faster) textures you import as normal maps in the editor should actually stop looking correct! If they're still working then the shader isn't.

    You can change all of your imported normal maps to type Advanced, and set "Bypass sRGB" on. This should replicate the way they're being imported with LoadImage if that's working correctly.

    The other option you might look into is using Resource.Load or asset packages instead of LoadImage so you can load an imported and Unity processed normal map asset directly instead of loading it from a PNG or JPG.
     
  8. Southridge1985

    Southridge1985

    Joined:
    Jul 17, 2012
    Posts:
    17
    Thanks you for the great advise peeps, this really helps me understand what is going on.
    Just to confirm.

    To test the result of my normal maps
    Import a normal texture map into the unity editor (Bypass sRGB turned on) for comparison of a www texture at runtime
    Use the single line of code in the shader to handle the normal texture.
    Code (CSharp):
    1. o.Normal = tex2D(_BumpMap, IN.uv_BumpMap.xy) * 2 - 1;
    Press play and the result is what i will get on my mobile device when i make a build.?

    My projects asset pipeline is all online.
    I use a runtime OBJ importer for the vehicle mesh.
    Then download via www texture's (Diffuse and Normals).
    Then i add the textures to the materials and scripts to GO for the vehicle control.

    That's why its imperative that i figure this out or like you say ill need to upgrade to pro to utilize Resource.Load for Asset packages.
    ( I don't want to use this method because i'd rather more freedom to make updates to individual files without having to re package it from unity3d each time.)

    Thanks again to all for the help, without it i'd be stuffed.
     
  9. Owers

    Owers

    Joined:
    Jul 7, 2012
    Posts:
    39
    That should do the trick. Bypass sRGB is definitely recommended, I forgot to mention that part, but if your project is using gamma colour space and not linear it might not be necessary. Since Unity currently doesn't support linear colour space for mobile you might be able to get away with skipping that step, but it's good practice to bypass sRGB in your normal map when it gets pulled from the web.
     
    Last edited: Feb 18, 2016
  10. Southridge1985

    Southridge1985

    Joined:
    Jul 17, 2012
    Posts:
    17
    Hey just wanted to confirm and sign off on this, despite all the good advise i still couldnt get the result i was after. So reluctantly i have adapted my build to now work with asset bundles and I am happy with the results.
    Not the best screen caps from an android device but you get the idea. Thanks to all for your help, I'm now able to move onto the multiplayer aspect of teh game... yay!!

     
  11. mcurtiss

    mcurtiss

    Joined:
    Nov 29, 2012
    Posts:
    26
    Why do you need to fill up the alpha channel instead of just using the red channel to start? I'm getting the same results if I just read from the red channel with:

    Code (CSharp):
    1. normal.xy = tex2D(_BumpMap, IN.uv_BumpMap.xy).xy * 2 - 1;
     
  12. grizzly

    grizzly

    Joined:
    Dec 5, 2012
    Posts:
    355
    It's due to the way normal maps are compressed. For certain platforms, Unity uses DXT5nm where the red and blue channels have a lower bit depth, while the green and alpha channels hold higher bit depths. Moving from the lower to higher precision channels reduces artifacts in your normal map.
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,209
    The red and blue colors of a DXT are stored with 5 bits of precision, and green with 6 bits, though the final texture when rendered is interpolated to 8 bits per color, like an uncompressed texture would use. It can also only use 4 colors for every 4x4 block of pixels and two of those colors have to be mixes of the other two. The simple version is you can't have a 4x4 block of pixels with pure red, green, and blue pixels. Instead you'll likely get red, green, and two shades brown. The alpha channel is stored with 8bits per alpha value, so the full 256 values, and 6 unique values for every 4x4 block of pixels. For most textures the compressed color is fine and it does a reasonable job looking like the original. For normal maps using DXT1 results in obvious blocky shapes and a loss of small details. Using DXT5 as a "DXTnm" with only the green and alpha channels used means the color values just need to be 4 green values and the alpha is compressed completely separately. For some normal maps the results can be almost exactly the same as the uncompressed original.
     
  14. mcurtiss

    mcurtiss

    Joined:
    Nov 29, 2012
    Posts:
    26
    So to clarify, setting import type to "Normal map" simply changes the default compression to DXT5, and then the red channel gets copied to the alpha channel , and Unity then uses the red and green channels to recalc an 'uncompressed' blue channel. Is this true for mobile too, Or is there some preprocessor directive to tell shaders to use a different unpack normal function using the texture's compressed .rgb values? Basically I want to know know if the normal map's blue channel is safe to store some info in.
     
  15. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,209
    Mobile does not do this. There's a cost to reconstructing the blue channel, one that desktops and consoles can usually ignore, but mobile platforms are far less powerful so instead the textures are just stored as their original RGB layout. If the texture compression artifacts are too obvious then using a "16 bit" or uncompressed "Truecolor" texture is usually the path taken for mobile instead of trying to repack into different texture channels, especially since many mobile GPUs done support DXT and the common texture formats either don't have an alpha component or don't compress the alpha separately removing the benefit of the DXT5 method.