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

Drawing a normal map to RenderTexture at runtime

Discussion in 'General Graphics' started by RogueCode, Oct 18, 2015.

  1. RogueCode

    RogueCode

    Joined:
    Apr 3, 2013
    Posts:
    230
    I'm attempting to do something that I had assumed would be pretty simple, but have been going in circles for a few hours now.

    I have a normal map - it is blue and marked as a normal map. Then I've got a script with a public `Texture` property (assigned in editor), and at runtime I create a new RenderTexture, write the normal map to it in various places (with `Graphics.DrawTexture`), then assign it to my material.

    This works 100% for a diffuse texture, but gives incorrect results with a normal map (the bump lighting is calculated incorrectly). I've tried my own shader, and the standard one with roughly the same effect.

    From what I've read, I understand that I will need to shift around the color values a bit - however everyone says to do this in the shader. For performance reasons I want the shader to be doing as little as possible, so would want to rather do whatever I need to do to the texture in code before assigning the texture.

    TL;DR: I need to write a normal map in my project to a RenderTexture, then assign that to a material that is using an unedited shader.

    Thanks!
     
  2. Zuntatos

    Zuntatos

    Joined:
    Nov 18, 2012
    Posts:
    612
    Well, for one thing, the default color for a normal map is a light purple ish blue - RGB of (0.5, 0.5, 1) / #8080FF. A texture marked as normalmap will be compressed into DXT5, with the original RGB0 turned into 0G0R, where 0 is an unused channel. So to properly copy it, your rendertexture will need an alpha channel. If pure runtime performance is your desire (or you already had an alpha channel), unpacking the 0G0R back into RGB0 will save some fps, as uncompressing the 0G0R in the shader involves a sqrt and things.
     
  3. RogueCode

    RogueCode

    Joined:
    Apr 3, 2013
    Posts:
    230
    I managed to get it *roughly* looking like what it should.
    One of the problems that threw me off was that in my shader I was increasing the bump by multiplying the R and G by something. However, I'm guessing that is actually incorrect now that this is just a regular texture, and not a normal one.

    Any idea which components I need to multiply to get extra bump now?
     
  4. Zuntatos

    Zuntatos

    Joined:
    Nov 18, 2012
    Posts:
    612
    @RogueCode You'd have to increase the R & G still, but you can't multiply them by a random scalar as that would mess up with the reconstruction of the B channel. Something like a sqrt() would do it, but performance :).
     
  5. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    8,896
  6. RogueCode

    RogueCode

    Joined:
    Apr 3, 2013
    Posts:
    230
    sqrt'ing R and G doesn't seem to increase it. Is does change the bump, but seems to just be shifting the direction of the light slightly.

    Assuming I can get the right math to increase it - I wonder if I could pre-increase/multiply/sqrt the RGB values in code when looping through the pixels?

    @mgear, thanks, got that :)
     
  7. Zuntatos

    Zuntatos

    Joined:
    Nov 18, 2012
    Posts:
    612
    @RogueCode oh, right, you'd have to sqrt the R & G while they're in -1,1 range as the neutral values get screwed otherwise. So something like
    Code (CSharp):
    1. float R = pixel.r * 2f - 1f
    2. pixel.r = 0.5f * sign(R) * sqrt(abs(R)) + 1f
    Should be perfectly fine to do this while looping through the pixels.
     
    RogueCode likes this.
  8. RogueCode

    RogueCode

    Joined:
    Apr 3, 2013
    Posts:
    230
    @Zuntatos - it appears to work! :D

    Ok, so last problem is this:


    Lights seems to calculate wrong still, although the amount and positioning of the actual bump is correct. In the image above, that point light causes a hard line as if it was a spotlight facing one direction.

    This is the code I'm using to go from the normal map 0G0R to RGBA (excluded your code above)

    Code (CSharp):
    1.  for (var x = 0; x < t.width; x++)
    2.     {
    3.       for (var y = 0; y < t.height; y++)
    4.       {
    5.         var pixel = t.GetPixel(x, y);
    6.  
    7.         var newPixel = new Color(pixel.a, pixel.g, pixel.b, pixel.r);
    8.         newTexture.SetPixel(x, y, newPixel);
    9.       }
    10.     }
    To be clear, the texture import settings are marked as normal map, then code converts it, then shader simply uses the texture given without unpacking or doing anything else.

    I'm assuming I'm putting something into the wrong channel, but can't figure out what.
     
  9. Zuntatos

    Zuntatos

    Joined:
    Nov 18, 2012
    Posts:
    612
    Code (CSharp):
    1.  
    2.             Color pixelStart; //get pixel from compressed normalmap
    3.             Color pixelNew; // goal pixel to write to texture
    4.  
    5.             // transform from 0,1 to -1,1
    6.             pixelNew.r = pixelStart.a * 2f - 1f;
    7.             pixelNew.g = pixelStart.g * 2f - 1f;
    8.  
    9.             // optional thing; makes normalmap more intense, hopefully.
    10.             // try other powers in Mathf.Pow(pixel, x) with x from 0 to 1 to make it intenser / weaker.
    11.             pixelNew.r = Mathf.Sign (pixelNew.r) * Mathf.Sqrt (Mathf.Abs (pixelNew.r));
    12.             pixelNew.g = Mathf.Sign (pixelNew.g) * Mathf.Sqrt (Mathf.Abs (pixelNew.g));
    13.  
    14.             // reconstruct z
    15.             pixelNew.b = Mathf.Sqrt (1f - Mathf.Clamp01 (pixelNew.r * pixelNew.r + pixelNew.g * pixelNew.g));
    16.  
    17.             // transform from -1,1 to 0,1
    18.             pixelNew.r = pixelNew.r * 0.5f + 0.5f;
    19.             pixelNew.g = pixelNew.g * 0.5f + 0.5f;
    20.             pixelNew.b = pixelNew.b * 0.5f + 0.5f;
    21.             return pixelNew;
    Not sure if this compiles or not, I didn't use unity :). This code should do it all, in intended order on proper channels.

    However! The shader will still expect a packed version of the normalmap for non-mobile platforms. You can circumvent this with a custom shader, by removing the UnpackNormal things unity puts into it. (normal = unpack(tex2d(tex, uv)) into normal = tex2d(tex, uv), its rather trivial)

    If you just want to increase the normalmap intensity, use this:

    Code (CSharp):
    1.  
    2.             Color pixel; //get pixel from compressed normalmap
    3.  
    4.             // transform from 0,1 to -1,1
    5.             pixel.a = pixel.a * 2f - 1f;
    6.             pixel.g = pixel.g * 2f - 1f;
    7.  
    8.             // optional thing; makes normalmap more intense, hopefully.
    9.             // try other powers in Mathf.Pow(pixel, x) with x from 0 to 1 to make it intenser / weaker.
    10.             pixel.a = Mathf.Sign (pixel.a) * Mathf.Sqrt (Mathf.Abs (pixel.a));
    11.             pixel.g = Mathf.Sign (pixel.g) * Mathf.Sqrt (Mathf.Abs (pixel.g));
    12.  
    13.             // transform from -1,1 to 0,1
    14.             pixel.a = pixel.a * 0.5f + 0.5f;
    15.             pixel.g = pixel.g * 0.5f + 0.5f;
    16.             return pixel;
    I'm not 100% sure on the decompression GetPixel will do when sampling a normalmap, but probably just standard dxt5 decompression, so it should be okay.
     
  10. RogueCode

    RogueCode

    Joined:
    Apr 3, 2013
    Posts:
    230
    @Zuntatos, thanks for the code :)
    Unfortunately this still is giving incorrect results. The first block with my own shader makes the quad receive no light at all. The second block with the Standard shader gives about what I had in the screenshot I posted (light with a hard line).
     
  11. Zuntatos

    Zuntatos

    Joined:
    Nov 18, 2012
    Posts:
    612
    @RogueCode well, tested it myself, and you're probably having a goal texture that is of formats like RGB24, that do not have an alpha channel. Packed normalmaps use the alpha channel and the green channel (for better DXT5 compression) so you'd lose half the data if you're in RGB24 instead of RGBA32, creating the black area.

    Code for a function going from packed normalmap -> more intense unpacked normalmap:

    Code (CSharp):
    1.  
    2.         Texture2D textureOut = new Texture2D (textureIn.width, textureIn.height, TextureFormat.ARGB32, true, true);
    3.         int width = textureIn.width;
    4.         int height = textureIn.height;
    5.         for (int x = 0; x < width; x++) {
    6.             for (int y = 0; y < height; y++) {
    7.                 Color pixel = textureIn.GetPixel (x, y);
    8.                 pixel.r = pixel.a * 2f - 1f;
    9.                 pixel.g = pixel.g * 2f - 1f;
    10.                 pixel.r = Mathf.Sign (pixel.r) * Mathf.Sqrt (Mathf.Abs (pixel.r));
    11.                 pixel.g = Mathf.Sign (pixel.g) * Mathf.Sqrt (Mathf.Abs (pixel.g));
    12.                 pixel.b = Mathf.Sqrt (1f - Mathf.Clamp01 (pixel.g * pixel.g + pixel.r * pixel.r));
    13.                 pixel.r = pixel.r * 0.5f + 0.5f;
    14.                 pixel.g = pixel.g * 0.5f + 0.5f;
    15.                 pixel.b = pixel.b * 0.5f + 0.5f;
    16.                 textureOut.SetPixel (x, y, pixel);
    17.             }
    18.         }
    19.         textureOut.Apply (true, true);
    20.         materialOut.SetTexture ("_BumpMap", textureOut);

    And code for a function going from packed normalmap -> more intense packed normalmap:

    Code (CSharp):
    1.  
    2.         Texture2D textureOut = new Texture2D (textureIn.width, textureIn.height, TextureFormat.ARGB32, true, true);
    3.         int width = textureIn.width;
    4.         int height = textureIn.height;
    5.         for (int x = 0; x < width; x++) {
    6.             for (int y = 0; y < height; y++) {
    7.                 Color pixel = textureIn.GetPixel (x, y);
    8.                 pixel.a = pixel.a * 2f - 1f;
    9.                 pixel.g = pixel.g * 2f - 1f;
    10.                 pixel.a = Mathf.Sign (pixel.a) * Mathf.Sqrt (Mathf.Abs (pixel.a));
    11.                 pixel.g = Mathf.Sign (pixel.g) * Mathf.Sqrt (Mathf.Abs (pixel.g));
    12.                 pixel.a = pixel.a * 0.5f + 0.5f;
    13.                 pixel.r = 0f;
    14.                 pixel.g = pixel.g * 0.5f + 0.5f;
    15.                 pixel.b = 0f;
    16.                 textureOut.SetPixel (x, y, pixel);
    17.             }
    18.         }
    19.         textureOut.Apply (true, true);
    20.         materialOut.SetTexture ("_BumpMap", textureOut);
     
    RogueCode likes this.
  12. RogueCode

    RogueCode

    Joined:
    Apr 3, 2013
    Posts:
    230
    It seems the root problem was because my RenderTexture was ARGB32. I set it to ARGBInt now and this is working properly.

    Thank you so much for the help!
     
  13. Zuntatos

    Zuntatos

    Joined:
    Nov 18, 2012
    Posts:
    612
    @RogueCode ...That fixes it? Very weird. Either you're writing big numbers or negative numbers then. Some core code that shows what's going on would help in helping. ARGBInts are 4x the size of ARGB32 and probably less performant as well.

    Btw, are you creating a tilemap/atlas in the rendertexture?