Search Unity

Normal Map Artifacts

Discussion in 'General Graphics' started by DudoTheStampede, Jul 23, 2016.

  1. DudoTheStampede

    DudoTheStampede

    Joined:
    Mar 16, 2015
    Posts:
    81
    Hi everyone,

    we have encountered a very nasty problem involving normal maps in one of our project, we waited several days before opening this thread but every test we did was unsuccessful.

    First of all, here the screenshot with the problem:
    openExr_mayaVsUnity.png

    Left is in Maya.
    Right is in Unity.
    Same model (fbx) and same normal (baked with xNormal)

    A list of things we already checked (reading various threads):
    - imported as Truecolor (so, no compression issues) and Max resolution.
    - generated as openEXR (we tried .tga, .tiff, this one is .exr because it seems the best), so no compression of file.
    - baked at 8bit, 16bit and 32bit. The one in the screenshot is 32bit.
    - check model tangent (Imported/calculated etc) following various thread about tangent-space related issues.

    We read this as suggested by many, but you can see how in Maya everything works.
    We think this could be an import problem in Unity with 32 bit-depth textures but we find nothing to correct this behaviour!

    Anyone can help?

    Thanks!
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    The problem is Unity doesn't currently support anything but 8bpc (bits per channel) textures, and while it can read 16bpc and 32bpc textures on import they will crush them down to 8bpc with out dithering as the first step. HDR textures are special cased, but they're still just 8bpc textures once they get used by the engine (they're converted to RGBM format which stores the color in the RGB channels and an exponential brightness in the A).

    If you're importing height maps instead of normal maps into Unity, Unity's normal map generator is notoriously terrible. And, as mentioned before, will always be done on 8bpc textures as a 16bpc height map appears to get crushed down to 8bpc before its converted to a normal map leading to terrible banding.

    That link you pointed to does seem to have the best suggestion which is to generate your normals at 16bpc and convert them to 8bpc in Photoshop so that they get dithered nicely. This won't fully solve the problem though as what you want is for the mip maps to also be dithered, but Unity doesn't support the import of pre-made mip maps and it'll be mip mapping the 8bpc normal going right back to where you were.

    Depending on your application you might be able to get away with disabling mip maps on your normal maps, or you can generate each mip map individually and use a script in Unity to combine them into a single texture. Finally even if you do generate your own mip maps you might want to look at adjusting the texture's bias to around -0.25, which again will need to be done via scripting as this value isn't user facing and can't even be modified from the debug inspector.
     
  3. DudoTheStampede

    DudoTheStampede

    Joined:
    Mar 16, 2015
    Posts:
    81
    Thanks @bgolus for the great answer!
    We thought that could be the problem but we found nothing on the matter.

    Dithering is terrible for our normal maps... no one knows if Unity will ever support 32bpc textures for real? Or if is there a way to make it work in Unity as they should?
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Internally Unity has support for 16bpc and 32bpc textures with 1, 2 or 4 channels; 2 channel 16bpc is RGHalf, 4 channel 32bpc is RGBAFloat.

    These are uncompressed formats, which is likely fine for your purposes, but there's no way to directly import these types of textures. Instead you have to import them yourself using LoadRawTextureData. I know people have used it for getting 16bpc greyscale height maps into Unity, but I've not ever seen full example code for it.

    Ultimately I would wonder if for your use case of what I assume is a car you avoid normal maps altogether and just use a mesh detailed enough for your purposes. Normal maps for game engines tend to only use normal maps adding "texture" to surfaces or when having the full geometry for the wanted shape would be prohibitively high poly to replicate. A car body has neither of these issues.
     
    theANMATOR2b and JamesArndt like this.
  5. JamesArndt

    JamesArndt

    Joined:
    Dec 1, 2009
    Posts:
    2,932
    Having been a developer on multiple Need for Speed titles I can attest that normal maps are not used on car bodies generally. Interiors, and other mechanical parts yes, but the car body itself is almost always modeled detail for seams, etc.
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Unity's roadmap has been updated to include features in development / already implemented in Unity 5.5. One of the features is actual support for 16bpc textures, both compressed with BC6H or uncompressed! The feature is listed as being for better support for HDRI (yay no more RGBM!), but it also means native editor support for 16 bpc normal maps might be possible.

    https://unity3d.com/unity/roadmap

    Curiously no mention of BC5 for normal maps, which would also greatly increase the quality of existing compressed 8 bpc normals... the format the rest of the industry has been using for nearly a decade to replace the DXT5 / BC3 normal maps Unity uses. :(
     
  7. DudoTheStampede

    DudoTheStampede

    Joined:
    Mar 16, 2015
    Posts:
    81
    Thanks @JamesArndt, we'll do the same for the future. For now that's a demo project and we got that normal map to start with. After seeing that problem I wanted to try and solve it before deciding to model the details! ^_^

    @bgolus thanks to your suggestion of using LoadRawTextureData, I spent a morning researching and trying and now we have a shiny 32bpc normal map on our meshes! :D
    For this demo purposes it works really well (the quality is the same as the maya screenshot) but obviously it's not easy and fast as importing the map directly in the editor!
    Let's hope Unity 5.5 will include it natively!
     
  8. torvald-mgt

    torvald-mgt

    Joined:
    Jun 14, 2015
    Posts:
    8
    Hey DudoTheStampede. Could you tell us how you solved this problem? Or a link to a resource that will help in this?
     
  9. DudoTheStampede

    DudoTheStampede

    Joined:
    Mar 16, 2015
    Posts:
    81
    As I said I used LoadRawTextureData using the bytes of the raw normal in 32bpc.
    Here is an extract of my code! ^_^

    Code (CSharp):
    1. Texture2D tex = new Texture2D( 2048, 2048, TextureFormat.RGBAFloat, false );
    2. tex.LoadRawTextureData( rawTextureBytes );
    3. tex.Apply();
    4. material.SetTexture("_normalMap", tex );
    5.  
    Get "rawTextureBytes" as you want and assign it to your "material".


    Hope someone else will make good use of it! ;)
     
    torvald-mgt likes this.
  10. Kamil_Reich

    Kamil_Reich

    Joined:
    Aug 14, 2014
    Posts:
    195
    Hi @DudoTheStampede May you give more code? I have got similar problem and cannot get it to work :(
     
  11. ViperTechnologies

    ViperTechnologies

    Joined:
    Apr 28, 2017
    Posts:
    42
    @DudoTheStampede and Whoever successfully solved this problem, As far as i understand in that code you tried to get the texture data load it in a new byte array and then read that data and insert it into the current GPU processed texture and bypass the whole compression and unity3d stuff that applied on the original un-capped texture right ?
    i tried some approaches :

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class JaggyNorm : MonoBehaviour {
    6.  
    7. // Use this for initialization
    8. public void Start () {
    9.         Texture2D tex = new Texture2D(2048, 2048, TextureFormat.RGBAFloat, false);
    10.         byte[] rawTextureBytes = tex.GetRawTextureData();
    11.         tex.LoadRawTextureData(tex.rawTextureBytes);
    12.         tex.Apply();
    13.         GetComponent<Renderer>().material.mainTexture = tex;
    14. }
    15.  
    16. }
    which is i think nonsense,
    and another one is similar to your work :
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class JaggyNorm : MonoBehaviour {
    6.  
    7. // Use this for initialization
    8. void Start () {
    9.         Texture2D tex = new Texture2D(2048, 2048, TextureFormat.RGBAFloat, false);
    10.         byte[] rawTextureBytes = new byte[]
    11.         {
    12.  
    13.         };
    14.         tex.LoadRawTextureData(tex.GetRawTextureData());
    15.         tex.Apply();
    16.         GetComponent<Renderer>().material.mainTexture = tex;
    17. }
    18.  
    19. }
    this is what you meant, but how am i going to get the arrays of bytes ? can you please guide me through it ?
    cause its been a great amount of time i searched and found nothing but more and more trouble.
    i am requesting any who have had successfully overcome this matter please kindly share your knowledge.
    Screen Shot 2018-08-17 at 15.54.42.png


    Thanks.
     
    Last edited: Aug 17, 2018
  12. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    If the image file on disk is 16 bit per channel, just go to the texture's per platform overrides (the icon tabs for the compression settings) and choose RGBHalf.
     
    ViperTechnologies likes this.
  13. DudoTheStampede

    DudoTheStampede

    Joined:
    Mar 16, 2015
    Posts:
    81
    Hi Viper!

    You can get the bytes in different ways! I can show you one for example:

    Code (CSharp):
    1. public TextAsset textureRAW;
    2.  
    3. void GenerateTexture {
    4.     Texture2D tex = new Texture2D( 2048, 2048, TextureFormat.RGBAFloat, false );
    5.     tex.LoadRawTextureData( textureRAW.bytes );
    6.     tex.Apply();
    7.     material.SetTexture("_normalMap", tex );
    8. }
    To easily use your Normal map as TextAsset just change it's extension to .bytes and then drag it from the editor and drop it in the inspector! ^_^

    I hope to have helped you! Let me know if you can make it work!
     
    ViperTechnologies likes this.
  14. ViperTechnologies

    ViperTechnologies

    Joined:
    Apr 28, 2017
    Posts:
    42
    Thank you for the answer @bgolus .
    it is a psd file i am using and i converted it to rgb 8 bit inside photoshop.
    the Normal Map itself is generated with XNormal. the unity version i am using is the latest one 2018.2.3.
    as in the attachment, there is no options as RGBHalf. :(

    @DudoTheStampede Hi ! :)
    thanks for the reply and assistance, am a bit lost, correct me if am wrong:
    so i attach this code to the object with the material, and then rename my NormalMap material extension from .psd to .bytes and then magic will happen right ? :)
    let me give it a try ...
    thanks so much.
     

    Attached Files:

  15. ViperTechnologies

    ViperTechnologies

    Joined:
    Apr 28, 2017
    Posts:
    42
    Okay So here is what i've done so far :


    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class JaggyNorm : MonoBehaviour
    6. {
    7.     public TextAsset textureRAW;
    8.     Renderer m_Renderer;
    9.  
    10.  
    11.     void GenerateTexture()
    12.     {
    13.         Texture2D tex = new Texture2D(2048, 2048, TextureFormat.RGBAFloat, false);
    14.         tex.LoadRawTextureData(textureRAW.bytes);
    15.         tex.Apply();
    16.         m_Renderer.material.SetTexture("_normalMap", tex);
    17.     }
    18.  
    19. }
    and then renamed my normalMap's .psd extension to .bytes extension.
    then attach the the script to the game object and assign my renamed .bytes file to it and hit play --> NOTHING Different.
    what i am missing :(
    P.S: here is the picture with the comments
     

    Attached Files:

  16. DudoTheStampede

    DudoTheStampede

    Joined:
    Mar 16, 2015
    Posts:
    81
    Ok well, two things to adjust:

    First of all, I can't do that with a PSD file. Export it in .exr format from photoshop then change the extension from .exr to .bytes.
    Second, change "GenerateTexture" in the script with "Start". Otherwise that function won't be called ever! ^_^
     
  17. ViperTechnologies

    ViperTechnologies

    Joined:
    Apr 28, 2017
    Posts:
    42
    Ok,
    first of all i am very glad and grateful of your effort and kind help. ^_^ :)
    i did the steps you mentioned :
    saved the psd to .exr with uncompressed settings. and then renamed it to .bytes.

    and then reorganize the code like this :

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class JaggyNorm : MonoBehaviour
    6. {
    7.     public TextAsset textureRAW;
    8.     Renderer m_Renderer;
    9.    
    10.     void Start()
    11.     {
    12.  
    13.         Texture2D tex = new Texture2D(2048, 2048, TextureFormat.RGBAFloat, false);
    14.         tex.LoadRawTextureData(textureRAW.bytes);
    15.         tex.Apply();
    16.         m_Renderer.material.SetTexture("_normalMap", tex);
    17.     }
    18.  
    19. }
    this time when i hit play unity gives me :
    Code (CSharp):
    1. UnityException: LoadRawTextureData: not enough data provided (will result in overread).
    2. UnityEngine.Texture2D.LoadRawTextureData (System.Byte[] data) (at /Users/builduser/buildslave/unity/build/Runtime/Export/Texture.cs:405)
    3. JaggyNorm.Start () (at Assets/0Mits/JaggyNorm.cs:23)
    4.  
    P.S = in the screenshot below i tried both giving the normal the previous normalmap.psd and without it ... no difference.
    Screen Shot 2018-08-17 at 19.27.16.png
     
  18. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Why? This step alone, assuming the original out of XNormal is a 16 bit tiff, means nothing you do in Unity is going to help. Converting to 8 bit means you've lost the normal information. Just use the original file that XNormal outputs. Everything else you're trying to do with using a custom import script is pointless if your file is already 8 bits. There's no extra data to get out of the file.

    Unity won't offer an option for RGBAHalf when you've set the texture format to Normal ... for reasons I don't understand. But it's easy to solve. Set the Texture Type to Default, disable sRGB, and as long as the file is 16 bpc you can choose RGBAHalf as the format.

    16 bit normal.png

    This is using a 16 bpc .tiff straight out of xNormal. Unity might complain that it's not a "Normal Map" when used in some shaders, but it will work properly.

    The only reason to use a custom import script is if you want to use an RGHalf format instead of the RGBAHalf to save some memory, but even then you could just do the conversion in the editor from the imported asset to save yourself some headache of dealing with raw file loading.
     
  19. ViperTechnologies

    ViperTechnologies

    Joined:
    Apr 28, 2017
    Posts:
    42
    OOOps,
    Ok then i will regenerate the NormalMap using XNormal with the output of OpenExr (.exr)
    is there any recommended settings for the XNormal to achieve the optimal result?
    i will give this a try as well now. :) thanks so much
     
  20. ViperTechnologies

    ViperTechnologies

    Joined:
    Apr 28, 2017
    Posts:
    42
    @bgolus i did what you mentioned, it looks way better now !!!
    Screen Shot 2018-08-17 at 20.29.18.png
    but for the custom script to access the normal data as you and @DudoTheStampede mentioned, it still doesn't work for me or better say i couldn't put it to work unfortunately ... which i really want to see the result of it cause now its better not fixed yet :( the edges and sharp cuts are gone with this method ...
    BUT, the size of the normalmap ... is an OMG ! ~ 42 MB :D

    @DudoTheStampede, i would like to kindly receive more detail for the procedure.
    i still get the same error :
    Code (CSharp):
    1. UnityException: LoadRawTextureData: not enough data provided (will result in overread).
    2. UnityEngine.Texture2D.LoadRawTextureData (System.Byte[] data) (at /Users/builduser/buildslave/unity/build/Runtime/Export/Texture.cs:405)
    3. JaggyNorm.Start () (at Assets/0Mits/JaggyNorm.cs:14)
    i still have no idea if i have to fill the NormalMap box in the material or not i gave both bytes and exr to it like below :

    Screen Shot 2018-08-17 at 20.37.38.png
     
  21. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    One additional note of caution, this only works in Linear color space rendering. If you're using Gamma color space for your project, 16 bit textures appear to always get sRGB correction applied on import.
     
  22. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    That's the nature of using an uncompressed 64 bit (16 bpc) texture. It takes a lot of memory! If you end up getting the option @DudoTheStampede is suggesting using an 32bpc EXR file and the RGBAFloat format, the resulting image will be 84 MB.

    The best case scenario is using an RGHalf format, which should bring it down to 21 MB (the same as the default uncompressed option, which uses a 32 bit (8bpc) RGBA texture). That would require creating a new texture and copying the values from the raw imported texture, or the RGBAHalf texture asset, to the other using GetPixels() / SetPixels(). Either way, you're not going to get back to the ~5 MB of a BC5 or DXT5 normal maps if you need this level of quality.
     
    ViperTechnologies likes this.