Unable to save generated/rendered Texture2D to use for billboard

So I am attempting to finalize a billboard asset maker, which unity sorely does not have, or explain in ANY amount, but it seems while I can generate the texture properly and apply it to the material of the billboard material, I attempt to save it to a png file, using a method I’ve seen explained time and time again, but it doesn’t seem to save it correctly. I will paste the script below at the bottom. It’s a wizard to create billboard assets of complicated objects. Basically, to test, put a complex object at 0,0 of a scene, then put an empty object at 0,0 and put a camera as a child of it on the local +x axis from it.
Here is what seems to get saved in the file,

But here is what the material looks like (you will need to make a material using the speed tree billboard shader to give to the wizard)
151506-materialhaspropersaved.png

As you can see, the texture on the material is set properly, but when I press cntrl-s to save the scene, the instance of it seems to be removed back to a pure white texture, so it’s not permanent.

This is really baffling, the texture itself is properly in the data but attempting to save it just throws it out?

Here is the script, I added some tooltips and comments to help explain what each thing does
(The little archive function was an attempt I found, but it doesn’t work either);

using UnityEditor;
using UnityEngine;

#if UNITY_EDITOR
public class GenerateBillboard : ScriptableWizard
{
    [Header("")]
    [Tooltip("This should be a Nature/Speedtree billboard material")]
    public Material m_material;
    [Tooltip("How much to pinch the mesh at a certain point, to cut off extra pixels off the mesh that won't be needed.")]
    [Range(0, 1)]
    public float topWidth = 1;
    [Tooltip("How much to pinch the mesh at a certain point, to cut off extra pixels off the mesh that won't be needed.")]
    [Range(0, 1)]
    public float midWidth = 1;
    [Tooltip("How much to pinch the mesh at a certain point, to cut off extra pixels off the mesh that won't be needed.")]
    [Range(0, 1)]
    public float botWidth = 1;

    [Tooltip("Units in height of the object, roughly, this can be fine-tuned later on the final asset")]
    public float objectHeight = 0;
    [Tooltip("Units in width of the object, roughly, this can be fine-tuned later on the final asset")]
    public float objectWidth = 0;
    [Tooltip("Usually negative and small, to make it sit in the ground slightly, can be modifed on final asset")]
    public float bottomOffset = 0;

    [Tooltip("The amount of rows in the texture atlas")]
    [Min(1)]
    public int atlasRowImageCount = 3;
    [Tooltip("The amount of columns in the texture atlas")]
    [Min(1)]
    public int atlasColumnImageCount = 3;
    [Tooltip("The total number of images to bake, ALSO decides how many angles to view from")]
    [Min(1)]
    public int totalImageCount = 8;

    [Header("-")]
    [Tooltip("This dictates the rotational center of the render for the billboard, and what is rotated to get different angles")]
    public GameObject toRotateCamera;
    [Tooltip("This should be child of toRotateCamera, and on the local +x axis from it, facing center with a complete view of the object")]
    public Camera renderCamera;

    [Header("Dimensios of atlas")]
    public int atlasPixelWidth = 1024;
    public int atlasPixelHeight = 1024;

    [Header("Optional renderer to set in")]
    public BillboardRenderer optionalBillboardRenderer;

    void OnWizardUpdate()
    {
        string helpString = "";
        bool isValid = (m_material != null && objectHeight != 0 && objectWidth != 0 && renderCamera != null && toRotateCamera != null);

        if (toRotateCamera != null)
        {
            Camera cam = toRotateCamera.GetComponentInChildren<Camera>();
            if (cam != null) { renderCamera = cam; }
        }
    }



    void OnWizardCreate()
    {
        //function to execute on submit

        BillboardAsset billboard = new BillboardAsset();

        billboard.material = m_material;
        Vector4[] texCoords = new Vector4[totalImageCount];

        ushort[] indices = new ushort[12];
        Vector2[] vertices = new Vector2[6];

        //make texture to save at end
        var texture = new Texture2D(atlasPixelWidth, atlasPixelHeight, TextureFormat.ARGB32, false);
        //make render texture to copy to texture and assign it to camera
        //renderCamera.targetTexture = RenderTexture.GetTemporary(atlasPixelWidth / atlasColumnImageCount, atlasPixelHeight / atlasRowImageCount, 16);
        renderCamera.targetTexture = RenderTexture.GetTemporary(atlasPixelWidth, atlasPixelHeight, 16);
        var renderTex = renderCamera.targetTexture;
        renderCamera.targetTexture = renderTex;

        //reset rotation, but camera should be on local +x axis from rotating object
        toRotateCamera.transform.eulerAngles = Vector3.zero;
        int imageAt = 0;
        for (int j = 0; j < atlasRowImageCount; j++)
        {
            for (int i = 0; i < atlasColumnImageCount; i++)
            {
                //i is x, j is y
                if (imageAt < totalImageCount)
                {
                    //atla them left-right, top-bottom, 0,0 is bottom left
                    float xRatio = (float)i / atlasColumnImageCount;
                    float yRatio = (float)(atlasRowImageCount - j - 1) / atlasRowImageCount;

                    //starts at viewing from +x, and rotates camera clockwise around object, uses amount of vertices set (later down) to tell how many angles to view from
                    texCoords[imageAt].Set(xRatio, yRatio, (float)1 / atlasColumnImageCount, (float)1 / atlasRowImageCount);
                    imageAt++;

                    //set rect of where to render texture to
                    renderCamera.rect = new Rect(xRatio, yRatio, (float)1 / atlasColumnImageCount, (float)1 / atlasRowImageCount);
                    renderCamera.Render();

                    //read pixels on rec
                    Rect rec = new Rect(xRatio * atlasPixelWidth, yRatio * atlasPixelHeight, (float)1 / atlasColumnImageCount * atlasPixelWidth, (float)1 / atlasRowImageCount * atlasPixelHeight);
                    texture.ReadPixels(rec, i / atlasColumnImageCount * atlasPixelWidth, (atlasRowImageCount - j - 1) / atlasRowImageCount * atlasPixelHeight);

                    toRotateCamera.transform.eulerAngles -= Vector3.up * (360 / totalImageCount);
                }
            }
        }
        toRotateCamera.transform.eulerAngles = Vector3.zero;
        renderCamera.rect = new Rect(0, 0, 1, 1);

        Graphics.CopyTexture(renderTex, texture);

        //texCoords[0].Set(0.230981f, 0.33333302f, 0.230981f, -0.33333302f);
        //texCoords[1].Set(0.230981f, 0.66666603f, 0.230981f, -0.33333302f);
        //texCoords[2].Set(0.33333302f, 0.0f, 0.33333302f, 0.23098099f);
        //texCoords[3].Set(0.564314f, 0.23098099f, 0.23098099f, -0.33333302f);
        //texCoords[4].Set(0.564314f, 0.564314f, 0.23098099f, -0.33333403f);
        //texCoords[5].Set(0.66666603f, 0.0f, 0.33333302f, 0.23098099f);
        //texCoords[6].Set(0.89764804f, 0.23098099f, 0.230982f, -0.33333302f);
        //texCoords[7].Set(0.89764804f, 0.564314f, 0.230982f, -0.33333403f);

        //make basic box out of four trinagles, to be able to pinch the top/bottom/middle to cut extra transparent pixels
        //still not sure how this works but it connects vertices to make the mesh
        indices[0] = 4;
        indices[1] = 3;
        indices[2] = 0;
        indices[3] = 1;
        indices[4] = 4;
        indices[5] = 0;
        indices[6] = 5;
        indices[7] = 4;
        indices[8] = 1;
        indices[9] = 2;
        indices[10] = 5;
        indices[11] = 1;

        //set vertices positions on mesh
        vertices[0].Set(-botWidth / 2 + 0.5f, 0);
        vertices[1].Set(-midWidth / 2 + 0.5f, 0.5f);
        vertices[2].Set(-topWidth / 2 + 0.5f, 1);
        vertices[3].Set(botWidth / 2 + 0.5f, 0);
        vertices[4].Set(midWidth / 2 + 0.5f, 0.5f);
        vertices[5].Set(topWidth / 2 + 0.5f, 1);

        //assign data
        billboard.SetImageTexCoords(texCoords);
        billboard.SetIndices(indices);
        billboard.SetVertices(vertices);

        billboard.width = objectWidth;
        billboard.height = objectHeight;
        billboard.bottom = bottomOffset;

        //save assets
        string path;
        int nameLength = AssetDatabase.GetAssetPath(m_material).Length;
        //take out ".mat" prefix
        path = AssetDatabase.GetAssetPath(m_material).Substring(0, nameLength - 4) + ".asset";
        AssetDatabase.CreateAsset(billboard, path);
        path = AssetDatabase.GetAssetPath(m_material).Substring(0, nameLength - 4) + ".png";
        byte[] byteArray = texture.EncodeToPNG();
        System.IO.File.WriteAllBytes(path, byteArray);
        Debug.Log("File saved to " + path + ", if pressing save breaks billboard, manually assign texture to material");
        //will save texture at bottom of function

        if (optionalBillboardRenderer != null)
        {
            optionalBillboardRenderer.billboard = billboard;
        }

        //cleanup / qol things
        RenderTexture.ReleaseTemporary(renderTex);
        renderCamera.targetTexture = null;
        m_material.SetTexture("_MainTex", texture);

        AssetDatabase.Refresh();
    }

    void Archive(Texture2D tex)
    {
        var path = EditorUtility.SaveFilePanel("Save textures to directory", "", "", ".png");
        if (path.Length != 0)
        {
            // for( var texture : Texture2D in textures) {
            // Convert the texture to a format compatible with EncodeToPNG
            if (tex.format != TextureFormat.DXT1 && tex.format != TextureFormat.RGB24)
            {
                var newTexture = new Texture2D(tex.width, tex.height);
                newTexture.SetPixels(tex.GetPixels(0), 0);
                tex = newTexture;
            }
            var pngData = tex.EncodeToPNG();
            if (pngData != null)
                System.IO.File.WriteAllBytes(path, pngData);
            else
                Debug.Log("Could not convert " + tex.name + " to png. Skipping saving texture");
        }

        AssetDatabase.Refresh();

    }

    [MenuItem("GameObject/Generate Billboard of Object")]
    static void MakeBillboard()
    {
        ScriptableWizard.DisplayWizard<GenerateBillboard>(
            "Make Billboard from object", "Create");
    }
}
#endif

Hey, you should see my comment on your other answer here: https://answers.unity.com/questions/1538195/unity-lod-billboard-asset-example.html?childToView=1692072#answer-1692072

The modified version of your code that I wrote completely changes the approach so I’m not sure if you really want to use it over your rendertexture method. However, I am able to answer why the texture “disappears” which I also explained a bit in my comment. I think there are differences between your code here and what you posted a few days ago in that other answer, so I’ll answer for that version specifically. You were setting the material’s texture to be the temporary one–the texture2D stored in memory. The PNG file you saved was created out of the byte data from the encode, however what you linked was not that PNG, rather that temporary texture. It exists nowhere on the hard drive, so I’d imagine it gets dumped when you save or make a change to the Editor.

This line is the issue:

m_material.SetTexture("_MainTex", texture);

For my version I just set it manually and didn’t worry about setting it through code. However if you do want to do this, you will probably have to read the created asset and somehow assign that instead.

When I tested your original code I also realized differences between the saved PNG texture and what was applied. I believe that happens because you never called texture.Apply(). Or perhaps it’s a problem with the rendertexture copy trick. Or maybe it had to do with the destination of your ReadPixels() since I think there was an issue with integer division after my debug tests. This is my modified version of the destination:

float xPos = xRatio * atlasPixelWidth;
float yPos = yRatio * atlasPixelWidth;

texture.ReadPixels(rec, Mathf.FloorToInt(xPos), Mathf.FloorToInt(yPos));

Again I don’t really have time to proofread your original intention, but this idea should be what you want. xRatio is the current index / total x amount in float form. Then you multiply by the desired width in pixels to get the ratio in pixel measurement. Now you have a pixel position of the x position. ReadPixels() takes ints, so I floor the final xPos. This may end up with imprecision in the final atlas. However in my version I set all pixels to alpha 0 beforehand so it doesn’t matter if the snapshot doesn’t sit perfectly in the designated spot.

I hope that helps! Thanks again for sharing your code and let me know if you have any other questions, I’ll be happy to help if you want to tweak it further or expand upon your original rendertexture idea :slight_smile:

Alright, with help from “SomeGuy22” here I was able to finish this spectacular piece of open-source software. Please feel free to use it, and feel free to read the comments on it in case you have questions, it should explain a lot of how to use it in tooltips as well. I decided to leave it in Window > Rendering > Generate Billboard of Object

Here it is, no special packages needed, just paste it into a file with the same class name and set up a camera around the object on an arm to rotate it, and make it so the camera can only see the objects you want to render, as well as the camera having clearflags set to solid color and make the alpha of the background 0.

using UnityEditor;
using UnityEngine;


//Credits:
//Original hard-coded solution by NathanJSmith: https://answers.unity.com/questions/1538195/unity-lod-billboard-asset-example.html?childToView=1692072#answer-1692072
//Cutomization to script by BarShiftGames on above link, and below link
//Inspiration/Feedback/More coding optimization by SomeGuy22 : https://answers.unity.com/questions/1692296/unable-to-save-generatedrendered-texture2d-to-use.html

#if UNITY_EDITOR
public class GenerateBillboard : ScriptableWizard
{
    [Header("")]
    [Tooltip("This should be a Nature/Speedtree billboard material")]
    public Material m_material;
    [Tooltip("How much to pinch the mesh at a certain point, to cut off extra pixels off the mesh that won't be needed.")]
    [Range(0, 1)]
    public float topWidth = 1;
    [Tooltip("How much to pinch the mesh at a certain point, to cut off extra pixels off the mesh that won't be needed.")]
    [Range(0, 1)]
    public float midWidth = 1;
    [Tooltip("How much to pinch the mesh at a certain point, to cut off extra pixels off the mesh that won't be needed.")]
    [Range(0, 1)]
    public float botWidth = 1;

    [Tooltip("Units in height of the object, roughly, this can be fine-tuned later on the final asset")]
    public float objectHeight = 0;
    [Tooltip("Units in width of the object, roughly, this can be fine-tuned later on the final asset")]
    public float objectWidth = 0;
    [Tooltip("Usually negative and small, to make it sit in the ground slightly, can be modifed on final asset")]
    public float bottomOffset = 0;

    [Tooltip("The amount of rows in the texture atlas")]
    [Min(1)]
    public int atlasRowImageCount = 3;
    [Tooltip("The amount of columns in the texture atlas")]
    [Min(1)]
    public int atlasColumnImageCount = 3;
    [Tooltip("The total number of images to bake, ALSO decides how many angles to view from")]
    [Min(1)]
    public int totalImageCount = 8;

    [Header("-")]
    [Tooltip("This dictates the rotational center of the render for the billboard, and what is rotated to get different angles.

This also checks once for an object with named "BillboardCameraArm"")]
public GameObject toRotateCamera;
[Tooltip(“This should be child of toRotateCamera, and on the local +x axis from it, facing center with a complete view of the object”)]
public Camera renderCamera;

    [Header("Dimensios of atlas")]
    public int atlasPixelWidth = 1024;
    public int atlasPixelHeight = 1024;

    [Header("Optional renderer to set in")]
    public BillboardRenderer optionalBillboardRenderer;

    private bool doOnce = true;
    private bool checkArmOnce = true;

    void OnWizardUpdate()
    {
        string helpString = "";
        bool isValid = (m_material != null && objectHeight != 0 && objectWidth != 0 && renderCamera != null && toRotateCamera != null);

        if (doOnce)
        {
            //this will get activated once
            doOnce = false;
            toRotateCamera = GameObject.Find("BillboardCameraArm");

        }

        if (toRotateCamera != null && checkArmOnce)
        {
            //this will check for a camera under toRotateCamera once
            checkArmOnce = false;
            Camera cam = toRotateCamera.GetComponentInChildren<Camera>();
            if (cam != null) { renderCamera = cam; }
        }
    }



    void OnWizardCreate()
    {
        //function to execute on submit

        BillboardAsset billboard = new BillboardAsset();

        billboard.material = m_material;
        Vector4[] texCoords = new Vector4[totalImageCount];

        ushort[] indices = new ushort[12];
        Vector2[] vertices = new Vector2[6];

        //make texture to save at end
        var texture = new Texture2D(atlasPixelWidth, atlasPixelHeight, TextureFormat.ARGB32, false);
        //make render texture to copy to texture and assign it to camera
        //renderCamera.targetTexture = RenderTexture.GetTemporary(atlasPixelWidth / atlasColumnImageCount, atlasPixelHeight / atlasRowImageCount, 16);
        renderCamera.targetTexture = RenderTexture.GetTemporary(atlasPixelWidth, atlasPixelHeight, 16);
        var renderTex = renderCamera.targetTexture;
        renderCamera.targetTexture = renderTex;

        //reset rotation, but camera should be on local +x axis from rotating object
        toRotateCamera.transform.eulerAngles = Vector3.zero;
        int imageAt = 0;
        for (int j = 0; j < atlasRowImageCount; j++)
        {
            for (int i = 0; i < atlasColumnImageCount; i++)
            {
                //i is x, j is y
                if (imageAt < totalImageCount)
                {
                    //atla them left-right, top-bottom, 0,0 is bottom left
                    float xRatio = (float)i / atlasColumnImageCount;
                    float yRatio = (float)(atlasRowImageCount - j - 1) / atlasRowImageCount;

                    //starts at viewing from +x, and rotates camera clockwise around object, uses amount of vertices set (later down) to tell how many angles to view from
                    texCoords[imageAt].Set(xRatio, yRatio, 1f / atlasColumnImageCount, 1f / atlasRowImageCount);
                    imageAt++;

                    //set rect of where to render texture to
                    renderCamera.rect = new Rect(xRatio, yRatio, 1f / atlasColumnImageCount, 1f / atlasRowImageCount);
                    renderCamera.Render();

                    //read pixels on rec
                    //Rect rec = new Rect(xRatio * atlasPixelWidth, yRatio * atlasPixelHeight, (float)1 / atlasColumnImageCount * atlasPixelWidth, (float)1 / atlasRowImageCount * atlasPixelHeight);
                    //texture.ReadPixels(rec, i / atlasColumnImageCount * atlasPixelWidth, (atlasRowImageCount - j - 1) / atlasRowImageCount * atlasPixelHeight);

                    toRotateCamera.transform.eulerAngles -= Vector3.up * (360 / totalImageCount);
                }
            }
        }
        toRotateCamera.transform.eulerAngles = Vector3.zero;
        renderCamera.rect = new Rect(0, 0, 1, 1);

        RenderTexture pastActive = RenderTexture.active;
        RenderTexture.active = renderTex;
        texture.ReadPixels(new Rect(0, 0, atlasPixelWidth, atlasPixelHeight), 0, 0);
        RenderTexture.active = pastActive;
        texture.Apply();

        //texCoords[0].Set(0.230981f, 0.33333302f, 0.230981f, -0.33333302f);
        //texCoords[1].Set(0.230981f, 0.66666603f, 0.230981f, -0.33333302f);
        //texCoords[2].Set(0.33333302f, 0.0f, 0.33333302f, 0.23098099f);
        //texCoords[3].Set(0.564314f, 0.23098099f, 0.23098099f, -0.33333302f);
        //texCoords[4].Set(0.564314f, 0.564314f, 0.23098099f, -0.33333403f);
        //texCoords[5].Set(0.66666603f, 0.0f, 0.33333302f, 0.23098099f);
        //texCoords[6].Set(0.89764804f, 0.23098099f, 0.230982f, -0.33333302f);
        //texCoords[7].Set(0.89764804f, 0.564314f, 0.230982f, -0.33333403f);

        //make basic box out of four trinagles, to be able to pinch the top/bottom/middle to cut extra transparent pixels
        //still not sure how this works but it connects vertices to make the mesh
        indices[0] = 4;
        indices[1] = 3;
        indices[2] = 0;
        indices[3] = 1;
        indices[4] = 4;
        indices[5] = 0;
        indices[6] = 5;
        indices[7] = 4;
        indices[8] = 1;
        indices[9] = 2;
        indices[10] = 5;
        indices[11] = 1;

        //set vertices positions on mesh
        vertices[0].Set(-botWidth / 2 + 0.5f, 0);
        vertices[1].Set(-midWidth / 2 + 0.5f, 0.5f);
        vertices[2].Set(-topWidth / 2 + 0.5f, 1);
        vertices[3].Set(botWidth / 2 + 0.5f, 0);
        vertices[4].Set(midWidth / 2 + 0.5f, 0.5f);
        vertices[5].Set(topWidth / 2 + 0.5f, 1);

        //assign data
        billboard.SetImageTexCoords(texCoords);
        billboard.SetIndices(indices);
        billboard.SetVertices(vertices);

        billboard.width = objectWidth;
        billboard.height = objectHeight;
        billboard.bottom = bottomOffset;

        //save assets
        string path;
        int nameLength = AssetDatabase.GetAssetPath(m_material).Length;
        //take out ".mat" prefix
        path = AssetDatabase.GetAssetPath(m_material).Substring(0, nameLength - 4) + ".asset";
        AssetDatabase.CreateAsset(billboard, path);
        path = AssetDatabase.GetAssetPath(m_material).Substring(0, nameLength - 4) + ".png";
        byte[] byteArray = texture.EncodeToPNG();
        System.IO.File.WriteAllBytes(path, byteArray);
        Debug.Log("BILLBOARD ASSET COMPLETED: File saved to " + path + ",

if pressing save in editor breaks billboard, manually assign texture to material");

        if (optionalBillboardRenderer != null)
        {
            optionalBillboardRenderer.billboard = billboard;
        }

        //cleanup / qol things
        RenderTexture.ReleaseTemporary(renderTex);
        renderCamera.targetTexture = null;
        m_material.SetTexture("_MainTex", texture);

        AssetDatabase.Refresh();
    }

    [MenuItem("Window/Rendering/Generate Billboard of Object")]
    static void MakeBillboard()
    {
        ScriptableWizard.DisplayWizard<GenerateBillboard>(
            "Make Billboard from object", "Create");
    }
}
#endif

Really cool script.
Thank you guys.

I made a little modification too, to automatize a little the process.

Select a gameobject you want to billboard, go to “Window > Rendering > Generate Billboard of Object” as you set up initialy, change atlas size and number of view you expected then profit.

Here is the modifications :

using System.IO;
using UnityEditor;
using UnityEngine;

//Credits:
//Original hard-coded solution by NathanJSmith: https://answers.unity.com/questions/1538195/unity-lod-billboard-asset-example.html?childToView=1692072#answer-1692072
//Cutomization to script by BarShiftGames on above link, and below link
//Inspiration/Feedback/More coding optimization by SomeGuy22 : https://answers.unity.com/questions/1692296/unable-to-save-generatedrendered-texture2d-to-use.html
//Automatization for the asset creation by Remjie.

#if UNITY_EDITOR
public class GenerateBillboard : ScriptableWizard
{
    [Header("Dimensions of atlas")]
    public int atlasPixelWidth = 1024;

    [Tooltip("The total number of images to bake, ALSO decides how many angles to view from")]
    [Min(1)]
    public int totalImageCount = 8;

    [Tooltip("How much to pinch the mesh at a certain point, to cut off extra pixels off the mesh that won't be needed.")]
    [Range(0, 1)]
    public float topWidth = 1;
    [Tooltip("How much to pinch the mesh at a certain point, to cut off extra pixels off the mesh that won't be needed.")]
    [Range(0, 1)]
    public float midWidth = 1;
    [Tooltip("How much to pinch the mesh at a certain point, to cut off extra pixels off the mesh that won't be needed.")]
    [Range(0, 1)]
    public float botWidth = 1;
    
    [Header("Optional renderer to set in")]
    public BillboardRenderer optionalBillboardRenderer;

    // The amount of rows in the texture atlas
    private int atlasRowImageCount = 3;
    //The amount of columns in the texture atlas
    private int atlasColumnImageCount = 3;

    //This dictates the rotational center of the render for the billboard, and what is rotated to get different angles
    private GameObject toRotateCamera;
    // This should be child of toRotateCamera, and on the local +x axis from it, facing center with a complete view of the object
    private Camera renderCamera;

    // Units in height of the object, roughly, this can be fine-tuned later on the final asset
    private float objectHeight = 0;
    //Units in width of the object, roughly, this can be fine-tuned later on the final asset
    private float objectWidth = 0;
    //Usually negative and small, to make it sit in the ground slightly, can be modifed on final asset
    private float bottomOffset = 0;

    //This should be a Nature/Speedtree billboard material
    private Material m_material;

    GameObject go;

    private const string DefFolder = "/Billboards/";

    void OnWizardUpdate()
    {
        // not used anymore
    }

    void OnWizardCreate()
    {
        // early exit if no go
        if (Selection.activeTransform == null)
        {
            Debug.Log("No GameObject selected");
            return;
        }

        // Gameobject bounds
        Transform t = Selection.activeTransform;

        Renderer rend = t.GetComponent<Renderer>();

        if(rend == null)
        {
            Debug.Log("No renderer on this Gameobject, abord the mission");
            return;
        }

        Vector3 center = rend.bounds.center;
        float radius = Mathf.Round(rend.bounds.extents.magnitude);

        // setup material
        m_material = new Material(Shader.Find("Nature/SpeedTree Billboard"));

        // create camera
        GameObject AxisCenter = new GameObject("CameraRotation");
        GameObject toRotateCamera = Instantiate(AxisCenter, t);
        // recenter camera to keep GO in center of image
        toRotateCamera.transform.position = new Vector3(
            toRotateCamera.transform.position.x,
            toRotateCamera.transform.position.y + radius/2,
            toRotateCamera.transform.position.z);

        toRotateCamera.AddComponent<Camera>();
        renderCamera = toRotateCamera.GetComponent<Camera>();
        
        // setup camera
        renderCamera.clearFlags = CameraClearFlags.Depth;
        renderCamera.orthographic = true;
        renderCamera.orthographicSize = radius;
        renderCamera.nearClipPlane = -radius;
        renderCamera.farClipPlane = radius;

        // set billboard property
        objectHeight = radius * 2;
        objectWidth = radius * 2;
        bottomOffset = -radius / 2;

        //function to execute on submit

        BillboardAsset billboard = new BillboardAsset();

        billboard.material = m_material;
        Vector4[] texCoords = new Vector4[totalImageCount];

        ushort[] indices = new ushort[12];
        Vector2[] vertices = new Vector2[6];

        //make texture to save at end
        var texture = new Texture2D(atlasPixelWidth, atlasPixelWidth, TextureFormat.ARGB32, false);
        //make render texture to copy to texture and assign it to camera
        //renderCamera.targetTexture = RenderTexture.GetTemporary(atlasPixelWidth / atlasColumnImageCount, atlasPixelHeight / atlasRowImageCount, 16);
        renderCamera.targetTexture = RenderTexture.GetTemporary(atlasPixelWidth, atlasPixelWidth, 16);
        var renderTex = renderCamera.targetTexture;
        renderCamera.targetTexture = renderTex;

        // calculate atlas row and column
        atlasRowImageCount = atlasColumnImageCount = Mathf.RoundToInt(Mathf.Sqrt(totalImageCount));
        Debug.Log(Mathf.Sqrt(totalImageCount));

        //correct camera rotation
        toRotateCamera.transform.eulerAngles = new Vector3(
            toRotateCamera.transform.eulerAngles.x,
            toRotateCamera.transform.eulerAngles.y - 90,
            toRotateCamera.transform.eulerAngles.z);

        int imageAt = 0;
        for (int j = 0; j < atlasRowImageCount; j++)
        {
            for (int i = 0; i < atlasColumnImageCount; i++)
            {
                //i is x, j is y
                if (imageAt < totalImageCount)
                {
                    //atla them left-right, top-bottom, 0,0 is bottom left
                    float xRatio = (float)i / atlasColumnImageCount;
                    float yRatio = (float)(atlasRowImageCount - j - 1) / atlasRowImageCount;

                    //starts at viewing from +x, and rotates camera clockwise around object, uses amount of vertices set (later down) to tell how many angles to view from
                    texCoords[imageAt].Set(xRatio, yRatio, 1f / atlasColumnImageCount, 1f / atlasRowImageCount);
                    imageAt++;

                    //set rect of where to render texture to
                    renderCamera.rect = new Rect(xRatio, yRatio, 1f / atlasColumnImageCount, 1f / atlasRowImageCount);
                    renderCamera.Render();

                    //read pixels on rec
                    //Rect rec = new Rect(xRatio * atlasPixelWidth, yRatio * atlasPixelHeight, (float)1 / atlasColumnImageCount * atlasPixelWidth, (float)1 / atlasRowImageCount * atlasPixelHeight);
                    //texture.ReadPixels(rec, i / atlasColumnImageCount * atlasPixelWidth, (atlasRowImageCount - j - 1) / atlasRowImageCount * atlasPixelHeight);

                    toRotateCamera.transform.eulerAngles -= Vector3.up * (360 / totalImageCount);
                }
            }
        }
        toRotateCamera.transform.eulerAngles = Vector3.zero;
        renderCamera.rect = new Rect(0, 0, 1, 1);

        RenderTexture pastActive = RenderTexture.active;
        RenderTexture.active = renderTex;
        texture.ReadPixels(new Rect(0, 0, atlasPixelWidth, atlasPixelWidth), 0, 0);
        RenderTexture.active = pastActive;
        texture.Apply();

        //make basic box out of four trinagles, to be able to pinch the top/bottom/middle to cut extra transparent pixels
        //still not sure how this works but it connects vertices to make the mesh
        indices[0] = 4;
        indices[1] = 3;
        indices[2] = 0;
        indices[3] = 1;
        indices[4] = 4;
        indices[5] = 0;
        indices[6] = 5;
        indices[7] = 4;
        indices[8] = 1;
        indices[9] = 2;
        indices[10] = 5;
        indices[11] = 1;

        //set vertices positions on mesh
        vertices[0].Set(-botWidth / 2 + 0.5f, 0);
        vertices[1].Set(-midWidth / 2 + 0.5f, 0.5f);
        vertices[2].Set(-topWidth / 2 + 0.5f, 1);
        vertices[3].Set(botWidth / 2 + 0.5f, 0);
        vertices[4].Set(midWidth / 2 + 0.5f, 0.5f);
        vertices[5].Set(topWidth / 2 + 0.5f, 1);

        //assign data
        billboard.SetImageTexCoords(texCoords);
        billboard.SetIndices(indices);
        billboard.SetVertices(vertices);

        billboard.width = objectWidth;
        billboard.height = objectHeight;
        billboard.bottom = bottomOffset;

        //save assets
        string path = "Assets" + DefFolder + t.name + "/";

        if (!Directory.Exists(path))
            Directory.CreateDirectory(path);

        Debug.Log(path + t.name + ".mat");
        // material
        AssetDatabase.CreateAsset(m_material, path + t.name + ".mat");
        // billboard
        AssetDatabase.CreateAsset(billboard, path + t.name + ".asset");
        // texture
        byte[] byteArray = texture.EncodeToPNG();
        File.WriteAllBytes(path + t.name + ".png", byteArray);
        
        // is it really usefull to keep?
        if (optionalBillboardRenderer != null)
        {
            optionalBillboardRenderer.billboard = billboard;
        }

        //cleanup / qol things
        RenderTexture.ReleaseTemporary(renderTex);
        renderCamera.targetTexture = null;
        m_material.SetTexture("_MainTex", texture);

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();

        // avoid texture deleted when pressing play or restart Editor
        Texture s = AssetDatabase.LoadAssetAtPath<Texture>(path + t.name + ".png");
        Material m = AssetDatabase.LoadAssetAtPath<Material>(path + t.name + ".mat");
        m.mainTexture = s;
        
        // create billboard in scene
        GameObject go = new GameObject(t.name + " Billboard");
        go.AddComponent<BillboardRenderer>();
        go.GetComponent<BillboardRenderer>().billboard = billboard;

        PrefabUtility.SaveAsPrefabAsset(go, path + t.name + ".prefab");
        
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();

        // delete useless things
        DestroyImmediate(AxisCenter);
        DestroyImmediate(toRotateCamera);
     

        Debug.Log("BILLBOARD ASSET COMPLETED: File saved to " + path + ".");
    }

    [MenuItem("Window/Rendering/Generate Billboard of Object")]
    static void MakeBillboard()
    {
        ScriptableWizard.DisplayWizard<GenerateBillboard>(
            "Make Billboard from object", "Create");
    }
}
#endif

cheers.