There seems to be new feature for generating texture atlasses, but I don’t understand how would I proceed with this. Where do I combine meshes and assign the material with packed texture? Can this method be used to dynamically reduce batching?
All PackTextures does is actually pack multiple textures into one large texture. It gives you the new large texture back and the locations where the input textures are in this texture.
This can be used for combining meshes or for some other things (e.g. terrain engine uses it internally to pack textures of detail objects). The mesh combining code you’d have to do yourself though, probably starting from CombineChildren script in standard assets.
OK… Now I get it. Just make new empty Texture2D and pack the other ones there. How do I set UV rects for the atlas materials? Would it be feasible to alter meshes UVs somehow?
I’m trying to make combining script that would also be able to reduce ammount of materials. That would be a huge time saver for our artist not needing to hand optimize for better batching.
The packing idea is that you’ll end up with a single material (otherwise, if meshes will still use different materials, there’s not much point in packing).
So the process would be roughly like this:
find all meshes that you can pack (that use the same shader, won’t move, and don’t use texture tiling)
pack all their textures into a single one
create material that uses this single texture and the same shader that original meshes used
create one big mesh that combines all the original meshes, similar to what CombineChildren script does. Additionally, for each input mesh, modify UVs so that they use sub-rectangle of the new big texture.
In the end you’ll have one big mesh, one big texture and one material for everything that is combined.
I think Jon Czeck (aarku) has done something like this… Jon, want to share it on the wiki? :roll:
Yeah, something like that. Except that your script just assigns the same texture to all objects, which will bring almost zero performance gains. If you want to actually combine the objects, you have to combine their meshes into a single mesh.
Of course, but this is only for test of packed texture on mesh with recalculated UVs. Now I just could use the Combine Children script to achieve a single mesh. I just posted the script because maybe someone else is interested in this one (maybe we could do one helluva mesh combiner for wiki).
I added the combine children script and made it handle submeshes. Now it combines Materials and packs textures per shader basis. It dosen’t take other material attributes in count. Atleast I’m going to add Lightmap packing. I’m open for ideas how to make this script better.
UPDATED Now also packs lightmaps. Excludes materials with tiling or offsets. Lightmaps will pack only if second uv set exists. Here it comes:
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
public class TexturePacker : MonoBehaviour {
public bool generateTriangleStrips = true;
private Dictionary<Shader,List<Material>> shaderToMaterial = new Dictionary<Shader,List<Material>>();
private Dictionary<Shader,Material> generatedMaterials = new Dictionary<Shader,Material>();
private Dictionary<Material, Rect> generatedUVs = new Dictionary<Material, Rect>();
private Dictionary<Material, Rect> generatedUV2s = new Dictionary<Material, Rect>();
// Use this for initialization
void Start () {
Component[] filters = GetComponentsInChildren(typeof(MeshFilter));
// Find all unique shaders in hierarchy.
for (int i=0;i < filters.Length;i++) {
Renderer curRenderer = filters[i].renderer;
if (curRenderer != null curRenderer.enabled curRenderer.material != null) {
Material[] materials = curRenderer.sharedMaterials;
if (materials != null) {
foreach (Material mat in materials) {
if (((mat.HasProperty("_LightMap") !(((MeshFilter)filters[i]).mesh.uv2.Length == 0) mat.GetTexture("_LightMap") != null) || !(mat.HasProperty("_LightMap")))
(mat.mainTextureScale==new Vector2(1.0f,1.0f)
(mat.mainTextureOffset==Vector2.zero))
) {
if (mat.shader != null mat.mainTexture != null) {
if (shaderToMaterial.ContainsKey(mat.shader)) {
shaderToMaterial[mat.shader].Add(mat);
}
else {
shaderToMaterial[mat.shader]=new List<Material>();
shaderToMaterial[mat.shader].Add(mat);
}
}
}
}
}
}
}
// Pack textures per shader basis and generate UV rect and material dictinaries.
foreach (Shader key in shaderToMaterial.Keys) {
Texture2D packedTexture=new Texture2D(1024,1024);
Texture2D[] texs = new Texture2D[shaderToMaterial[key].Count];
generatedMaterials[key] = new Material(key);
for (int i=0;i < texs.Length; i++) {
texs[i] = shaderToMaterial[key][i].mainTexture as Texture2D;
}
Rect[] uvs = packedTexture.PackTextures(texs,0,2048);
generatedMaterials[key].CopyPropertiesFromMaterial(shaderToMaterial[key][0]);
generatedMaterials[key].mainTexture=packedTexture;
for (int i=0;i < texs.Length; i++) {
if (shaderToMaterial[key][i].HasProperty("_LightMap")) {
texs[i] = shaderToMaterial[key][i].GetTexture("_LightMap") as Texture2D;
}
}
packedTexture=new Texture2D(1024,1024);
Rect[] uvs2 = packedTexture.PackTextures(texs,0,2048);
if (generatedMaterials[key].HasProperty("_LightMap")) {
generatedMaterials[key].SetTexture("_LightMap", packedTexture);
}
for (int i=0;i < texs.Length; i++) {
generatedUVs[shaderToMaterial[key][i]] = uvs[i];
generatedUV2s[shaderToMaterial[key][i]] = uvs2[i];
}
}
Vector2[] uv,uv2;
// Calculate new UVs for all submeshes and assign generated materials.
for (int i=0;i < filters.Length;i++) {
int subMeshCount = ((MeshFilter)filters[i]).mesh.subMeshCount;
Material[] mats = filters[i].gameObject.renderer.sharedMaterials;
uv = (Vector2[])(((MeshFilter)filters[i]).mesh.uv);
uv2 = (Vector2[])(((MeshFilter)filters[i]).mesh.uv2);
for (int j=0; j < subMeshCount; j++) {
if ( generatedUVs.ContainsKey(mats[j])) {
Rect uvs = generatedUVs[mats[j]];
Rect uvs2 = generatedUV2s[mats[j]];
int[] subMeshVertices = DeleteDuplicates(((MeshFilter)filters[i]).mesh.GetTriangles(j)) as int[];
mats[j]=generatedMaterials[filters[i].gameObject.renderer.sharedMaterials[j].shader];
foreach (int vert in subMeshVertices) {
uv[vert]=new Vector2((uv[vert].x*uvs.width)+uvs.x, (uv[vert].y*uvs.height)+uvs.y);
if (uv2!=null !(uv2.Length==0)) {
uv2[vert]=new Vector2((uv2[vert].x*uvs2.width)+uvs2.x, (uv2[vert].y*uvs2.height)+uvs2.y);
}
}
}
}
filters[i].gameObject.renderer.sharedMaterials=mats;
((MeshFilter)filters[i]).mesh.uv=uv;
if (uv2!=null !(uv2.Length==0)) {
((MeshFilter)filters[i]).mesh.uv2=uv2;
}
}
// Combine Meshes
CombineMeshes();
}
// Combine Children script to be called after Material and Texture packing.
private void CombineMeshes() {
Component[] filters = GetComponentsInChildren(typeof(MeshFilter));
Matrix4x4 myTransform = transform.worldToLocalMatrix;
Hashtable materialToMesh= new Hashtable();
for (int i=0;i<filters.Length;i++) {
MeshFilter filter = (MeshFilter)filters[i];
Renderer curRenderer = filters[i].renderer;
MeshCombineUtility.MeshInstance instance = new MeshCombineUtility.MeshInstance ();
instance.mesh = filter.sharedMesh;
if (curRenderer != null curRenderer.enabled instance.mesh != null) {
instance.transform = myTransform * filter.transform.localToWorldMatrix;
Material[] materials = curRenderer.sharedMaterials;
for (int m=0;m<materials.Length;m++) {
instance.subMeshIndex = System.Math.Min(m, instance.mesh.subMeshCount - 1);
ArrayList objects = (ArrayList)materialToMesh[materials[m]];
if (objects != null) {
objects.Add(instance);
}
else
{
objects = new ArrayList ();
objects.Add(instance);
materialToMesh.Add(materials[m], objects);
}
}
curRenderer.enabled = false;
}
}
foreach (DictionaryEntry de in materialToMesh) {
ArrayList elements = (ArrayList)de.Value;
MeshCombineUtility.MeshInstance[] instances = (MeshCombineUtility.MeshInstance[])elements.ToArray(typeof(MeshCombineUtility.MeshInstance));
// We have a maximum of one material, so just attach the mesh to our own game object
if (materialToMesh.Count == 1)
{
// Make sure we have a mesh filter renderer
if (GetComponent(typeof(MeshFilter)) == null)
gameObject.AddComponent(typeof(MeshFilter));
if (!GetComponent("MeshRenderer"))
gameObject.AddComponent("MeshRenderer");
MeshFilter filter = (MeshFilter)GetComponent(typeof(MeshFilter));
filter.mesh = MeshCombineUtility.Combine(instances, generateTriangleStrips);
renderer.material = (Material)de.Key;
renderer.enabled = true;
}
// We have multiple materials to take care of, build one mesh / gameobject for each material
// and parent it to this object
else
{
GameObject go = new GameObject("Combined mesh");
go.transform.parent = transform;
go.transform.localScale = Vector3.one;
go.transform.localRotation = Quaternion.identity;
go.transform.localPosition = Vector3.zero;
go.AddComponent(typeof(MeshFilter));
go.AddComponent("MeshRenderer");
go.renderer.material = (Material)de.Key;
MeshFilter filter = (MeshFilter)go.GetComponent(typeof(MeshFilter));
filter.mesh = MeshCombineUtility.Combine(instances, generateTriangleStrips);
}
}
}
public static Array DeleteDuplicates(Array arr)
{
// this procedure works only with vectors
if (arr.Rank != 1 )
throw new ArgumentException("Multiple-dimension arrays are not supported");
// we use a hashtable to track duplicates
// make the hash table large enough to avoid memory re-allocations
Hashtable ht = new Hashtable(arr.Length * 2);
// we will store unique elements in this ArrayList
ArrayList elements = new ArrayList();
foreach (object Value in arr)
{
if ( !ht.Contains(Value) )
{
// we've found a non duplicate
elements.Add(Value);
// remember it for later
ht.Add(Value, null);
}
}
// return an array of same type as the original array
return elements.ToArray(arr.GetType().GetElementType());
}
}
Very nice! What are the odds of having a version of this working for 1.6.2? Unfortunately the project I am working on fails ( bug submitted several times ) to import into Unity 2.0 and these features would be SUPER helpful.
Odds getting this working on 1.6.2 are very close to zero. My script uses generics which is .net 2.0 feature and it uses Unity 2.0 api.
I have been thinking some new features tho. I think it would be very useful to generate color lookup texture for non-textured materials so that those could be combined too. I’ll propably also add exclude lists per object and per shader basis so that you can hand pick materials and objects you don’t want to combine. Maybe layer choosing functionality for generated meshes would be useful too.
There’s one big problem when packing textures. What if they won’t fit? Script needs to group textures for packing in optimal way. The big problem is that I have to guestimate how good job PackTextures() does. Best solution would be if Unity provided method for packing on multiple atlasses.
Aras or anyone at Unity Technologies can you provide information how PackTextures() does the actual packing so that I can group textures in a way that optimal packing is achieved?
If textures don’t fit, they are decreased in size until they fit. Currently when packing fails, all textures are decreased twice in each dimension and packing is attempted again. No texture is made smaller than 4 pixels though. If ultimately packing still fails, PackTextures returns a null rectangles array.
Current packing algorithm is very similar to this one: Packing Lightmaps and performs quite well in general. Basically, largest textures will end up in top-left area of the texture.
I’ve added texture generation for non-textured objects(Generate Color Textures toggle). exclusion of objects. Now second uv set is generated if lightmapped without second uvs(Generate Light map UVs toggle). Anything with advanced shaders like normal maps is no go. Color for generated materials is selected from generated material color attribute.
This one still needs testing so I would really appreciate if you report any bugs you find to me.
I’m afraid I don’t quite understand the usefulness of the PackTextures function. It seems like, after you generate the textures, you then need to manually edit the UVs of every mesh in your game so they are lined up with the atlas. Wouldn’t it be simpler to design the texture atlas first, in photoshop or whatever, and design your meshes based around that?
Or is there some hypothetical script that would remap the UVs one by one for you, based on the rectangle array?
I suspect this is a more robust solution than I think it is, and I’m just not understanding the description correctly…