- Home /
Unity LOD billboard asset example?
I see that the SpeedTree in Standard Asset has billboard LOD, but I can't extract the SpeedTree asset in Standard Asset as FBX to learn.
I just can see 2 things of this billboard asset:
The texture of the billboard contains the angles of the tree:
The model of the billboard is a simple plane:
Also according to the BillboardAsset manual, the billboard is like this:
I create this question to ask for a billboard asset example (as FBX file). Does anyone know a (free) billboard asset example that I can study?
Answer by NathanJSmith · Nov 29, 2018 at 08:10 AM
I got it, billboard asset can be created with C# script. I can open the billboard asset with Notepad, here is the content of a Billboard Asset file:
Billboard.asset
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!226 &22600000
BillboardAsset:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: Billboard_Original
serializedVersion: 2
width: 10.350581
bottom: -0.2622106
height: 7.172371
imageTexCoords:
- {x: 0.230981, y: 0.33333302, z: 0.230981, w: -0.33333302}
- {x: 0.230981, y: 0.66666603, z: 0.230981, w: -0.33333302}
- {x: 0.33333302, y: 0, z: 0.33333302, w: 0.23098099}
- {x: 0.564314, y: 0.23098099, z: 0.23098099, w: -0.33333302}
- {x: 0.564314, y: 0.564314, z: 0.23098099, w: -0.33333403}
- {x: 0.66666603, y: 0, z: 0.33333302, w: 0.23098099}
- {x: 0.89764804, y: 0.23098099, z: 0.230982, w: -0.33333302}
- {x: 0.89764804, y: 0.564314, z: 0.230982, w: -0.33333403}
vertices:
- {x: 0.47093, y: 0.020348798}
- {x: 0.037790697, y: 0.498547}
- {x: 0.037790697, y: 0.976744}
- {x: 0.52906996, y: 0.020348798}
- {x: 0.95930207, y: 0.498547}
- {x: 0.95930207, y: 0.976744}
indices: 040003000000010004000000050004000100020005000100
material: {fileID: 2100000, guid: 6e680dda9368db5418f19388474277a2, type: 2}
Here is the C# code that I use to generate the file above
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class BillboardBaker : MonoBehaviour
{
#if UNITY_EDITOR
public BillboardAsset m_outputFile;
public Material m_material;
[ContextMenu("Bake Billboard")]
void BakeBillboard()
{
BillboardAsset billboard = new BillboardAsset();
billboard.material = m_material;
Vector4[] texCoords = new Vector4[8];
ushort[] indices = new ushort[12];
Vector2[] vertices = new Vector2[6];
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);
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;
vertices[0].Set(0.47093f, 0.020348798f);
vertices[1].Set(0.037790697f, 0.498547f);
vertices[2].Set(0.037790697f, 0.976744f);
vertices[3].Set(0.52906996f, 0.020348798f);
vertices[4].Set(0.95930207f, 0.498547f);
vertices[5].Set(0.95930207f, 0.976744f);
billboard.SetImageTexCoords(texCoords);
billboard.SetIndices(indices);
billboard.SetVertices(vertices);
billboard.width = 10.35058f;
billboard.height = 7.172371f;
billboard.bottom = -0.2622106f;
if (m_outputFile != null)
{
EditorUtility.CopySerialized(billboard, m_outputFile);
}
else
{
string path;
path = AssetDatabase.GetAssetPath(m_material) + ".asset";
AssetDatabase.CreateAsset(billboard, path);
}
}
#endif
}
Note that the metrics I hard-code is the metrics of Palm tree Billboard from SpeedTree sample (that included in standard asset of Unity)
Some may ask: Why the billboard get cut off so perfectly? I don't see that cutoff information in the Billboard.asset file. Answer: the cutoff definition is in the image file (as Alpha channel), and Unity recognize the cutoff through shader code
This was really helpful. I also found information on how the texcoord values work in the SpeedTree documentation. This enabled me to map my own textures out. https://docs.speedtree.com/doku.php?id=accessing_billboard_geometry
Answer by BarShiftGames · Jan 18, 2020 at 06:50 AM
Using their code above, I have made a custom billboard asset maker, the script should be able to auto-render out a billboard asset and texture atlas, though I'm not sure about normals, how would one make a camera render normals?
The three slider values allow you to pinch the top, middle, and bottom width to cut off areas you won't normally render, like how they pinch the top of it for triangular trees, 1 is full width and 0 is it turns into a single point at the middle.
Basically, the "toRotateCamera" object (name it will try to auto-find is: BillboardCameraArm) is what rotates to take the pictures, and the camera is the camera that should be on the +x axis as a child of that rotator, kindof like an arm with a camera on the end that will spin to take pictures from all angles. The camera should only see the object/s you want to picture though, not everything in the scene, and have a background of transparent colors, with clear flags set to solid color. This was to allow complex objects to be easily baked into billboards, rather than only one mesh allowed, and scene lighting/post-processing is easy to customize.
The only special thing is you need to assign it a material from the nature/speedtree billboard shader thing, under the standard assets of unity, it saves everything in the same location as that material.
Access the wizard under Window / Rendering / Generate Billboard of Object. Huge thanks to SomeGuy22, and the original hard-coded example by NathanJSmith.
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
Okay, this was a huge coincidence since I've been searching for something exactly like what you wrote and realized you made this answer only a few days ago! So, I took a look at your code and was able to modify it using a different approach. $$anonymous$$y updated version of your code is now able to:
Render the snapshots with alpha background by overriding the clear color in post
$$anonymous$$anually adjust scaling/snapshot position
No more error messages about reading out of range
The biggest change is basically that I've simplified the approach somewhat. Ins$$anonymous$$d of rendering individual segments of camera react subspaces and copying the render texture using hack magic, I've ins$$anonymous$$d opted to just make a small rect to render to and use the ReadPixels() source destination to just output to a different position on the final atlas. However, in shifting to this approach I encountered new issues:
ReadPixels actually captures the Editor UI based on your aspect ratio. Solution: Set the aspect ratio to free aspect and manually align the snapshot position using the new cameraShift variables I included. You can also scale the camera rect to get a fuller/smaller view of the object you want to capture using cameraScale. There's probably a way to automatically detect this stuff but manual is fine for now.
ReadPixels doesn't get new info until the end of frame, so you only end up with one snapshot. Solution: Import the EditorCoroutines package. Now I can use the IEnumerator to yield during an Editor function and thus I can take snapshots in several steps.
Also a few small things:
I'm now calling AssetDatabase.Refresh() so that new assets always appear
You shouldn't set the material's texture to "texture" because it's temporary and not linked to the PNG asset which is created. That now has to be done manually.
I'm now calling texture.Apply() so that changes are properly saved
It's still a little messy, but it gets the job done. Here's my modified version: https://pastebin.com/0EHwuU0g
I think it gets pretty good results. On the left is the billboard render: https://i.imgur.com/zN$$anonymous$$bmk8.png
There's probably more QOL adjustments that can be made such as auto-detecting object position and saving assets in a more organized manner--overriding the billboard asset breaks links to renderers which sucks. But it works good enough for now and great for what I need to billboard my custom trees. Thanks a ton for your original version.
With your permission, would I be able to share this new code with the people down on the forums? There's practically no info on BillboardAsset online and I think this would be a great use case to take advantage of one of Unity's lesser known components. I'd also love to discuss it further if you have any more improvements. Thanks for sharing :)
EDIT: I should add that I spent 6 or 7+ hours on it so I hope it's useful for someone .-.
Man I definitely found this useful! Thanks for sharing really. I am going to implement top-down angle myself, seems pretty straightforward a syour code is actually quite readable. Great work dude thanks for your effort!
Answer by dan_wipf · Aug 04, 2018 at 10:33 AM
might this is one could be a example you’re looking for..
I have checked that asset out, it doesn't use the billboard asset but it use double-plane-mesh technique. (It doesn't have Billboard-render component)
well I just googled around and tried inside of Unity to figuerout some Information.
On Google, nobody figured it out how to make a Custom BillboardAsset and the $$anonymous$$anual dosent provide any useable Information. (might be real hard work to create a usable Script)
I think it's a Speedtree "only" thing. Because it creates the BillBoardAsset inside of the SpeedTree $$anonymous$$odeler and if you have a look to the Shader of the $$anonymous$$aterial the only which Works with BillboardRenderer is the SpeedtreeBillboard shader..
to sum up my Opinion. I'd looking for an alternative Billboard Solution or try to create a Script which makes RendereTextures which you combine to one and then compile it to a BillboardAsset.