Search Unity

Getting output "textures" from a surface shader

Discussion in 'General Graphics' started by plmx, Jun 6, 2019.

  1. plmx

    plmx

    Joined:
    Sep 10, 2015
    Posts:
    308
    Hi,

    is there a way to render a certain mesh with a certain material, namely a non-standard complex surface shader such that I get, as a result, one texture for each of the SurfaceOutputStandard fields (Albedo, Metallic, Smoothness, Alpha, and Normal), i.e. with the correct UV coordinates of the mesh? Such that I can plug the resulting textures into the Standard shader?

    I realize that this will only create a snapshot of the looks created by the shader. That is fine.

    Philip
     
  2. ParadoxSolutions

    ParadoxSolutions

    Joined:
    Jul 27, 2015
    Posts:
    325
    Usually I would say use Graphics.Blit but that wont give reliable results with a surface shader, what you will need to do is instantiate a forward facing quad with the surface shader applied, then instantiate a camera in orthographic mode with its bounds focused on the quad so all the camera sees is the quad with your shader applied. Then for each texture you want to capture remove all textures but that map and use camera.Render() what the camera sees to a render texture that you can then write to a texture2d and save to disk. This will get the textures out of the surface shader.

    Here is a good starting point, this was adapted from an editor-only version I wrote a while ago and you can throw it on a monobehaviour during runtime. However I know I fixed a bug in this at some point but I'm pretty sure this isn't the fixed version; also it needs culling layers in case stuff is in front of your quad but you could just use it in a blank scene. To get each of your maps you will have to run this for on a material that has just one of the maps or it will get them all.
    Code (CSharp):
    1.  public Texture2D GetTextureFromSurfaceShader(Material mat, int width, int height)
    2.     {
    3.         //Create render texture:
    4.         RenderTexture temp = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
    5.  
    6.         //Create a Quad:
    7.         GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
    8.         MeshRenderer rend = quad.GetComponent<MeshRenderer>();
    9.         rend.material = mat;
    10.         Vector3 quadScale = quad.transform.localScale / (float)((Screen.height / 2.0) / Camera.main.orthographicSize);
    11.         quad.transform.position = Vector3.forward;
    12.  
    13.         //Setup camera:
    14.         GameObject camGO = new GameObject("CaptureCam");
    15.         Camera cam = camGO.AddComponent<Camera>();
    16.         cam.renderingPath = RenderingPath.Forward;
    17.         cam.orthographic = true;
    18.         cam.clearFlags = CameraClearFlags.SolidColor;
    19.         cam.backgroundColor = new Color(1, 1, 1, 0);
    20.         if (cam.rect.width < 1 || cam.rect.height < 1)
    21.         {
    22.             cam.rect = new Rect(cam.rect.x, cam.rect.y, 1, 1);
    23.         }
    24.         cam.orthographicSize = 0.5f;
    25.         cam.rect = new Rect(0, 0, quadScale.x, quadScale.y);
    26.         cam.aspect = quadScale.x / quadScale.y;
    27.         cam.targetTexture = temp;
    28.         cam.allowHDR = false;
    29.  
    30.  
    31.         //Capture image and write to the render texture:
    32.         cam.Render();
    33.         temp = cam.targetTexture;
    34.  
    35.         //Apply changes:
    36.         Texture2D newTex = new Texture2D(temp.width, temp.height, TextureFormat.ARGB32, true, true);
    37.         RenderTexture.active = temp;
    38.         newTex.ReadPixels(new Rect(0, 0, temp.width, temp.height), 0, 0);
    39.         newTex.Apply();
    40.  
    41.         //Clean up:
    42.         RenderTexture.active = null;
    43.         temp.Release();
    44.         Destroy(quad);
    45.         Destroy(camGO);
    46.  
    47.         return newTex;
    48.     }
    And here is to save as a .png (or whatever you want to encode to):

    Code (CSharp):
    1.     public static void SaveTexture(Texture2D tex, string name)
    2.     {
    3.         var png = tex.EncodeToPNG();
    4.         Debug.Log(Application.dataPath);
    5.         File.WriteAllBytes(Application.dataPath + "/" + name + ".png", png);
    6.     }
     
    Gamba04 and plmx like this.
  3. plmx

    plmx

    Joined:
    Sep 10, 2015
    Posts:
    308
    Hi, and thank you very much for your insights and the code. I am currently attempting to get it to work. One thing that happened initially was that the result was squashed into the lower left corner of the generated texture. I've adjusted line 10 like so to get rid of that issue (I think)

    Code (CSharp):
    1. Vector3 quadScale = quad.transform.localScale;
    I can get some results out which do seem resemble the original looks of the input shader which is great.The textures seem to be complete, i.e. top and bottom / left and right are correct. However, there is still information lacking in the center, and I am struggling with the correct channels.

    First, regarding the output channels (Albedo, Metallic, ...). You said "remove all textures but that map", am I correct in assuming that you mean change the shader code to output only the specific channel I want (i.e. remove, for example, o.Metallic= , leave o.Albedo=, etc.?)

    Furthermore, I have some trouble getting complete textures, for some reason, some parts of the resulting texture seem to be missing information; they look "stretched". I've looked at the models and the shader and it seems there are two sets of UV coordinates in use (input mentiones uv_texcoord and uv4_texcoord4). I have tried using a custom Blender-made quad with 4 identical UV maps, but that didn't change anything. Do you have any insights regarding this issue?

    Thanks,

    Philip
     
    Last edited: Jun 11, 2019
  4. ParadoxSolutions

    ParadoxSolutions

    Joined:
    Jul 27, 2015
    Posts:
    325
    Yeah that might have been the bug I fixed in a later version =/

    Yes, I think some legacy shaders worked this way i.e. if there was only a normal map and no other texture input your model would actually show the purple normal color; you will have to make sure your shader outputs in this way.

    Not sure what you mean without seeing pictures, but you will need to disable any parallax/tessellation that might make the mesh vertices appear larger and result in stretching or at least make sure that those effects displace toward the camera and not away in a noticeable direction. You may also just be able to adjust scale and tiling to remedy this.

    You can also convert your surface shader to a vertex fragment shader and just use Graphics.Blit which is meant for this sort of thing, I think I just ended up doing that to get textures out.

    Edit: Here is the code I use to get textures from a material that uses a non-surface shader (You can try it on a surface shader but I don't remember what happens).
    Code (CSharp):
    1.  /// <summary>
    2.         /// Passes the source texure to a <see cref="RenderTexture"/> and applies a material's shader properties.
    3.         /// This will retain the size of the source texture.
    4.         /// </summary>
    5.         /// <param name="tex">The source texture.</param>
    6.         /// <param name="mat">The material that contains the shader we will apply to the texture.</param>
    7.         /// <param name="channel">The material's channel.</param>
    8.         /// <returns></returns>
    9.         public static Texture2D ApplyMaterialToTexture(Texture2D tex, Material mat, int channel = -1)
    10.         {
    11.             //Create new texture and render texture:
    12.             Texture2D newTex = new Texture2D(tex.width, tex.height, TextureFormat.ARGB32, false, true);
    13.             RenderTexture rendTex = new RenderTexture(tex.width, tex.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
    14.             rendTex.DiscardContents();
    15.  
    16.             //Set the materials channel and blit the material to the source texture:
    17.             if (channel >= 0)
    18.             {
    19.                 mat.SetInt("_Channel", channel);
    20.             }
    21.             GL.sRGBWrite = (QualitySettings.activeColorSpace == ColorSpace.Linear);
    22.             //GL.Clear(true, true, Color.black);
    23.             Graphics.Blit(tex, rendTex, mat, channel);
    24.             GL.sRGBWrite = false;
    25.  
    26.             //Read out the render texure's pixels back to the source texture and release the render texture from memory:
    27.             RenderTexture.active = rendTex;
    28.             newTex.ReadPixels(new Rect(0, 0, rendTex.width, rendTex.height), 0, 0, false);
    29.             newTex.Apply();
    30.             RenderTexture.active = null;
    31.             rendTex.Release();
    32.             return newTex;
    33.         }
     
    Last edited: Jun 11, 2019
    Newtori and plmx like this.
  5. plmx

    plmx

    Joined:
    Sep 10, 2015
    Posts:
    308
    Hi,

    sorry for the late reply, and thanks very much for the code. I've now debugged into the problem and, sure enough, as you said there is indeed some UV displacement going on in the original shader, which, I suspect, is the reason I am not seeing the output I wanted.

    For shaders which do not do this, your code seems to be working perfectly, so I guess there is nothing I can do about this. Too bad.

    Anyway, your posts helped me understand the problem properly, in particular, the code was really helpful. So, again, thanks very much!!

    Philip
     
    ParadoxSolutions likes this.
  6. ParadoxSolutions

    ParadoxSolutions

    Joined:
    Jul 27, 2015
    Posts:
    325
    @plmx

    I know this thread is from a year ago but I found myself coming back here after googling this problem (had a good laugh finding my own answer saying you can't do this)

    BUT

    you can try:

    Code (CSharp):
    1.  
    2. //Texture background:
    3. GL.Clear(true, true, Color.black);
    4.  
    5. //Init matrix:
    6. GL.PushMatrix();
    7.  
    8. //Create an orthographic camera:
    9. GL.LoadOrtho();
    10.  
    11. //Load the mesh you want to draw:
    12. Graphics.DrawMeshNow(meshToDraw, Matrix4x4.identity);
    13.  
    14. //Blit texture:
    15. Graphics.Blit( blablabla look at my above posts)
    16.  
    17. GL.PopMatrix();

    Not working code but what you want is Graphics.DrawMeshNow and then in the blit function you pass a version of the surface shader you want to render with its vertex position output set to its uv coordinates (unwrapped).

    This method can work for some effects (unfortunatly for me, baking triplanar maps isn't one of them) maybe with some tweaks. Hope this is a good starting point for someone else.