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

Save a 3D Render Texture to file

Discussion in 'Scripting' started by Frostbite23, Nov 28, 2021.

  1. Frostbite23

    Frostbite23

    Joined:
    Mar 8, 2013
    Posts:
    458
    The topic name is the question, google shows up references on how to save a Render Texture that is 2D, but I don't see anything that pertains to saving a 3D render texture. So how do I do that?

    In the posts about saving 2D Render Textures you set the render texture as the active render texture, and then create a rect that is the same size as the render texture dimensions. However if I did that exact thing with a 3D render texture I'd only be grabbing a single slice of the 3D texture. So how do you grab the different slices of a 3D Render Texture?
     
    Last edited: Nov 28, 2021
  2. Frostbite23

    Frostbite23

    Joined:
    Mar 8, 2013
    Posts:
    458
    Because no one here seems to know how to do what I'm asking, I managed through long googling sessions to find a post that basically helps answer my question. So for others who need to solve the same issue I have, your solution lies in here - https://answers.unity.com/questions/840983/how-do-i-copy-a-3d-rendertexture-isvolume-true-to.html

    "There is a way to do this. The procedure is to slice the 3D RenderTexture into an array of 2D RenderTextures using a compute shader. After that you have to transform that 2D RenderTextures into Texture2D and then fill a Texture3D with the content of the slices."
     
    Last edited: Jan 13, 2022
    RendergonPolygons and Bunny83 like this.
  3. Frostbite23

    Frostbite23

    Joined:
    Mar 8, 2013
    Posts:
    458
    UPDATE: So I improved the script provided in the Unity Answers Link from Nesvi, and made it so it's more readable and actually adjustable to different volume resolutions (Since in Nesvi's answer, he assumed that your volume resolution would be consistent for every axis). I figured I'd share this here because according to my long googling sessions no one knows how to do it... so here it is.

    CSharp Script
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Rendering;
    3. using UnityEditor;
    4.  
    5. public class SaveRenderTextures : MonoBehaviour
    6. {
    7.     //define each resolution axis of the volume
    8.     public int voxelSizeWidth;
    9.     public int voxelSizeHeight;
    10.     public int voxelSizeDepth;
    11.  
    12.     //name of the asset to be saved
    13.     public string assetName;
    14.  
    15.     //the slicer compute shader
    16.     public ComputeShader computeShader;
    17.  
    18.     //render texture format of the volume
    19.     private static RenderTextureFormat rtFormat = RenderTextureFormat.ARGBHalf;
    20.  
    21.     //texture3d format of the final asset
    22.     private static TextureFormat volumeFormat = TextureFormat.RGBAHalf;
    23.  
    24.     /// <summary>
    25.     /// Captures a single slice of the volume we are capturing.
    26.     /// </summary>
    27.     /// <param name="source"></param>
    28.     /// <param name="layer"></param>
    29.     /// <returns></returns>
    30.     RenderTexture Copy3DSliceToRenderTexture(RenderTexture source, int layer)
    31.     {
    32.         //create a SLICE of the render texture
    33.         RenderTexture render = new RenderTexture(voxelSizeWidth, voxelSizeHeight, 0, rtFormat);
    34.  
    35.         //set our options for the render texture SLICE
    36.         render.dimension = TextureDimension.Tex2D;
    37.         render.enableRandomWrite = true;
    38.         render.wrapMode = TextureWrapMode.Clamp;
    39.         render.Create();
    40.  
    41.         //find the main function in the slicer shader and start displaying each slice
    42.         int kernelIndex = computeShader.FindKernel("CSMain");
    43.         computeShader.SetTexture(kernelIndex, "voxels", source);
    44.         computeShader.SetInt("layer", layer);
    45.         computeShader.SetTexture(kernelIndex, "Result", render);
    46.         computeShader.Dispatch(kernelIndex, voxelSizeWidth, voxelSizeHeight, 1);
    47.  
    48.         return render;
    49.     }
    50.  
    51.     /// <summary>
    52.     /// Converts a 2D render texture to a Texture2D object.
    53.     /// </summary>
    54.     /// <param name="rt"></param>
    55.     /// <returns></returns>
    56.     Texture2D ConvertFromRenderTexture(RenderTexture rt)
    57.     {
    58.         //create our texture2D object to store the slice
    59.         Texture2D output = new Texture2D(voxelSizeWidth, voxelSizeHeight, volumeFormat, false);
    60.  
    61.         //make sure the render texture slice is active so we can read from it
    62.         RenderTexture.active = rt;
    63.  
    64.         //read the texture and store the data in the texture2D object
    65.         output.ReadPixels(new Rect(0, 0, voxelSizeWidth, voxelSizeHeight), 0, 0);
    66.         output.Apply();
    67.  
    68.         return output;
    69.     }
    70.  
    71.     /// <summary>
    72.     /// Saves a 3D Render Texture
    73.     /// </summary>
    74.     /// <param name="rt"></param>
    75.     public void Save3D(RenderTexture rt)
    76.     {
    77.         RenderTexture[] layers = new RenderTexture[voxelSizeDepth]; //create an array that matches in length the "depth" of the volume
    78.         Texture2D[] finalSlices = new Texture2D[voxelSizeDepth]; //create another array to store the texture2D versions of the layers array
    79.  
    80.         //copy each slice of the volume into a single render texture
    81.         for (int i = 0; i < voxelSizeDepth; i++)
    82.         {
    83.             layers[i] = Copy3DSliceToRenderTexture(rt, i);
    84.         }
    85.  
    86.         //convert each single render texture slice into a texture2D
    87.         for (int i = 0; i < voxelSizeDepth; i++)
    88.         {
    89.             finalSlices[i] = ConvertFromRenderTexture(layers[i]);
    90.         }
    91.  
    92.         //create our final texture3D object
    93.         Texture3D output = new Texture3D(voxelSizeWidth, voxelSizeHeight, voxelSizeDepth, volumeFormat, false);
    94.         output.filterMode = FilterMode.Trilinear;
    95.  
    96.         //iterate for each slice
    97.         for (int z = 0; z < voxelSizeDepth; z++)
    98.         {
    99.             //get the texture2D slice
    100.             Texture2D slice = finalSlices[z];
    101.  
    102.             //iterate for the x axis
    103.             for (int x = 0; x < voxelSizeWidth; x++)
    104.             {
    105.                 //iterate for the y axis
    106.                 for (int y = 0; y < voxelSizeHeight; y++)
    107.                 {
    108.                     //get the color corresponding to the x and y resolution
    109.                     Color singleColor = slice.GetPixel(x, y);
    110.  
    111.                     //apply the color corresponding to the slice we are on, and the x and y pixel of that slice.
    112.                     output.SetPixel(x, y, z, singleColor);
    113.                 }
    114.             }
    115.         }
    116.  
    117.         //apply our changes to the 3D texture
    118.         output.Apply();
    119.  
    120.         //save the 3D texture asset to the disk
    121.         AssetDatabase.CreateAsset(output, "Assets/" + assetName + ".asset");
    122.     }
    123. }
    124.  
    Slicer Compute Shader (Same as Nesvi's)
    Code (CSharp):
    1. #pragma kernel CSMain
    2.  
    3. Texture3D<float4> voxels;
    4. RWTexture2D<float4> Result;
    5. int layer;
    6.  
    7. [numthreads(32, 32, 1)]
    8. void CSMain(uint3 id : SV_DispatchThreadID)
    9. {
    10.     uint3 pos = uint3(id.x, id.y, layer);
    11.     Result[id.xy] = voxels[pos];
    12. }
     
  4. JollyTheory

    JollyTheory

    Joined:
    Dec 30, 2018
    Posts:
    134
    More robust and much faster:
    Code (CSharp):
    1. void SaveRT3DToTexture3DAsset(RenderTexture rt3D, string pathWithoutAssetsAndExtension)
    2. {
    3.     int width = rt3D.width, height = rt3D.height, depth = rt3D.volumeDepth;
    4.     var a = new NativeArray<byte>(width * height * depth, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); //change if format is not 8 bits (i was using R8_UNorm) (create a struct with 4 bytes etc)
    5.     AsyncGPUReadback.RequestIntoNativeArray(ref a, rt3D, 0, (_) =>
    6.     {
    7.         Texture3D output = new Texture3D(width, height, depth, rt3D.graphicsFormat, TextureCreationFlags.None);
    8.         output.SetPixelData(a, 0);
    9.         output.Apply(updateMipmaps: false, makeNoLongerReadable: true);
    10.         AssetDatabase.CreateAsset(output, $"Assets/{pathWithoutAssetsAndExtension}.asset");
    11.         AssetDatabase.SaveAssetIfDirty(output);
    12.         a.Dispose();
    13.         rt3D.Release();
    14.     });
    15. }
     
    Frostbite23 likes this.