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)
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
Answer by BarShiftGames · Jan 22, 2020 at 02:24 AM
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.\nThis 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 + ",\n 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
Awesome! Glad you were able to fix up the rendertexture method. It really is best to have two working separate ways of accomplishing this in case limitations occur down the line. I learned a lot from this exercise so thanks a ton for writing the initial idea and getting the motivation to see it through :)
I tested out your new version and it works perfectly! Here are the results from it on my custom tree: https://i.imgur.com/hXOtsO0.png
And you can compare it to the results from my version when I finely tune the snapshot position: https://i.imgur.com/QcpqgtR.png
They're pretty identical! However yours is definitely slightly easier to set up since I don't have to adjust the position manually. I suppose that's a result of the ReadPixels() behaving weird with showing the Editor UI as opposed to the rendertexture trick. I still have no clue how you were able to figure that out--if I understand correctly you are rendering the camera with a custom output rect to fill each corner, and all of that gets buffered to the rendertexture. Which you then copy to the final texture. It seems almost like a hacky solution but it really does work pretty well.
Finally, the reason I had to use clearcolor to paste alpha in was because it seems like ReadPixels() has a limitation of not carrying over alpha even with Depth Only/Don't Clear. Or with clear color Alpha at 0. I guess RenderTexture doesn't have that issue which is why you're able to do it here.
Anyways thanks again :) I'll definitely be using this in the future for all my billboard needs and eventually I'll share my findings with the forum users so that there's more info on BillboardAsset and how to make one online. I'll make sure to link back here so you get credit for figuring all this out. Good luck with your future projects!
Edit: One last thing... using the resulting billboard with a tree LOD is perfect. It even supports SpeedTree LOD crossfade animation. However when you plug it into a terrain it seems to mess with the billboard a bit. It appears to ignore the scale of the object. So make sure you set the billboard height/width properly ins$$anonymous$$d of scaling the object itself so it'll show up on Terrains. Best to do this by previewing with scale 1 in the prefab and just adjusting the BillboardAsset until it looks right. Best of luck!
Answer by SomeGuy22 · Jan 21, 2020 at 08:48 PM
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 :)
Hello, thank you for getting back to me with your solutions! But I'm having trouble with some of your code changes from the other answer, why do we need to adjust based on editor screen, and which screen? The scene view or the game view? I've tried porting over your changes but it doesn't seem to be working..?
@BarShiftGames Unrelated but first make sure that your camera is set to either "Don't Clear" or "Depth Only" because it matches the pure black color and trims that out as alpha. To answer your question: take a look at what happens when my game view is really small and I don't set the aspect ratio to free aspect: https://i.imgur.com/LRkT1Cn.png
See how I can actually see the UI in the resulting texture? This is because ReadPixels() apparently captures the entire Editor render, including the game view UI background. As I mentioned there might be a way to auto detect where this happens but for now you basically have to fiddle with the camera shift x and y to align the snapshot into the actual game view. And yes, it captures from the game view camera - essentially the "viewport" at which you see the game.
So to solve this I set the camera shift X to 190 ins$$anonymous$$d of the default and I get this: https://i.imgur.com/3fDnFUF.png
Notice how the snapshot location has moved over slightly. But in this example I actually got an out of bounds error and one of the snapshots didn't take. So I guess you really need the gameview window to be large enough for the rect to fit inside, perhaps another area to improve. Regardless, the goal here is to use those 3 params to align the snapshot until the object is centered in the picture.
Here's the results when I size up the game view, set it to "free aspect" ratio, and shift X to 200: https://i.imgur.com/C7SkFaJ.png So it really does matter how big your game view is when you perform the action. Hopefully that made sense, let me know if you have any more trouble :) I'd also recommend makings sure you don't miss any steps I took in my code, remember that I changed both the rec variable and the camera view rectangle.
Wooo!!, thanks to you pushing me, I was able to finally have an epiphany, the reason it comes out good in editor until I press save was due to the fact that I was using Graphics.CopyTexture which didn't sync with CPU, so I needed to simply stop reading pixels until the end of the for loop, then read it all in at once without Graphics.CopyTexture, which by doing this stops the error messages anyway, and this doesn't require any packages to be installed, since it works without pausing! I was also able to clean up my code thanks to your second eyes. The FINAL key was texture.Apply(). Thank you, I will credit you in the final code which I will mark as an answer to this question. It doesn't require any custom stuff, and it's independent of your current aspect ratio and such. Oh, and before I forget, I simply set my camera's clear flags to solid color, and make the background color have alpha zero, let me know if that works better/the same as your solution of manually setting all pixels to invisible? I noticed you had to do that with your previous method.
Answer by Remjie · May 13, 2020 at 08:33 PM
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.
Your answer
Follow this Question
Related Questions
Better way to edit RawImage texture data?,Better way to edit a RawImage texture? 0 Answers
How would I go about color correcting a Raw Image? 0 Answers
How to create texture at runtime (Without using TextureImporter)? 1 Answer
Convert Texture to Texture2d takes a lot of time in android device. 0 Answers