subtileFeatured

Dev Diary #1 – SubTile Meshes

I’m currently working on another update for Look Out Below!, focusing on performance and user experience as we prepare to launch it for free with ads. One of the places that suffered performance issue the most was over the cities, which I suspect is due to the high number of draw calls in this area. In this page on the Unity manual they describe about the CPU cost or drawing an object on screen and how reducing the number of draw calls can increase performance. In Look Out Below! we use a lot of tiled textures which are great because they take up very little memory but still provide plenty of texture resolution, normally a building with have the same texture tiled across it 20 times or so, quick example below.tiledTextureExampleBut a big part or reducing draw calls is atlasing, as each tiled texture requires a different material and therefore a different draw call, even if its applied to the same mesh (on a submesh), where as with atlasing many textures are combined together into one big texture, and the meshes UVs are moved to the correct part of the texture (this is what Unity’s lightmaps are, large texture atlases that contain and combine many small textures generated for each mesh), for example out trees use a single texture. atlasedExample Unfortunately for us, tiled textures and texture atlases don’t mix well, tiling relies on continuous UVs that go beyond the normal 0,1 UV range, and atlasing relies on each mesh having its UVs confined to a small area of the UV range.

Our solution is to tile within a smaller UV range using a shader. The shader uses a faces vertex colors to specify a range to tile in. In our 3D package we have multiple materials assigned to a mesh that have a name like “WindowMat_6_6″, on import this tells the importer that faces with this material applied need to have vertex colors. In this example the importer will assign (0.83333, 0, 0.1666,0.1666) as the vertex colors, which in the shader will equate to the last sixth of the texture (i.e. texture 6 of 6 assuming the textures are laid out horizontally). Here it it in action; subtileExample Now the building which was previously 6 tiled textures and 6 draw calls can be rendered in one draw call, providing a performance boost without actually sacrificing any quality. One thing I should mention is while we still use tiling on the Y axis it’s not so much tiling as mirroring in the x axis, this is so that the difference in UV sample position isn’t so large as to cause extreme mip-mapping, for us this isn’t an issue as our textures are mostly symmetrical on the x axis anyway but it does limit it’s usefulness in other situations.

Heres the simplest form of the shader we’re using, it tiles in Y and mirrors in x based on vertex colors;


Shader "Custom/SubTileX" {
Properties {
_Color ("_Color", Color) = (1, 1, 1, 1)
_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM
#pragma surface surf Lambert

sampler2D _MainTex;
fixed4 _Color;

struct Input {
float2 uv_MainTex;
float4 color : COLOR;
};

void surf (Input IN, inout SurfaceOutput o) {
float2 t = frac(IN.uv_MainTex*0.5)*2.0;
float2 length = {1.0,1.0};
float2 mirrorTexCoords = length-abs(t-length);
float2 uv = frac(mirrorTexCoords) * IN.color.ba + IN.color.rg;
half4 c = tex2D (_MainTex, float2(uv.x, IN.uv_MainTex.y));
o.Albedo = c.rgb * _Color.rgb;
}
ENDCG
}
FallBack "Diffuse"
}

I’ve mentioned in a previous post about MeshBatcher.cs, a class that will combine multiple meshes, that share the same material and lightmap, into a single draw call and while using this class on the cities would certainly would also have reduced our draw calls when applied to multiple buildings sharing the same material, it adds to the memory as the mesh data for a building is duplicated for each batch and we wanted to keep the download size as low as possible, which means sharing as much mesh data as possible. So that’s about it for my first Dev Diary, we’re also still working on Kindred and other smaller projects, be sure to support us by buying Look Out Below! in the meantime!

UPDATE:

I’ve cleaned up our importer a bit (though its still pretty filthy) so I can show the importer code that goes with that shader. Theres a few steps to go through in you 3D package I’ll go through as well, check out the image below;

subtileThis church model has 3 materials assigned, the materials are named to match up to the position of the tile in the texture. It also has an extra attribute called “Unity” with the value “subtile”. Unity allows us to detect these extra attributes be extending the AssetPostprocessor class and using the OnPostprocessGameObjectWithUserProperties method. In our importer we go through the objects with the unity attribute, parse the value and if its “subtile” we start subtiling the mesh.

There are somethings to bear in mind with this implementation, firstly the models material import settings must be “From Models Material” to get correct material names, and secondly all materials must be part of a subtile.

Here’s our subtile importer (it’s actually just a part of our much larger importer which I’ll cover some of the features of in the future)

//----------------------------------------------
// SubTileImporter
// Mark Hogan
// www.popupasylum.co.uk
//----------------------------------------------
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System;
using System.Text.RegularExpressions;
using System.IO;

/// <summary>
/// Sub tile importer.
/// 
/// Add an extra string attribute to your 3D called "unity" set ts value to "subtile"
/// You must set the material naming convention to "From Models Material"
/// 
/// </summary>
public class SubTileImporter : AssetPostprocessor {

	void OnPostprocessGameObjectWithUserProperties (GameObject go, string[] names, object[] values)
	{
		for (int i = 0; i < names.Length; i++) {
			if (names [i].ToString ().ToLower () == "unity") {
				ProcessTag (go, (string)values [i]);
			}
		}
	}

	void ProcessTag (GameObject go, string tagString)
	{		
		tagString = tagString.ToLower ();
		
		if (tagString.Contains ("subtile")) {
			CreateSubTileMesh(go);
		}
	}

	void CreateSubTileMesh (GameObject go){
		
		//Store the Mesh to create subtile information from
		Mesh mesh;
		
		//Find the mesh, if no mesh is found then return;
		MeshFilter subTileFilter = go.GetComponent<MeshFilter> ();
		if (subTileFilter && subTileFilter.sharedMesh && go.renderer) {
			mesh = subTileFilter.sharedMesh;
		} else {
			return;
		}
		
		//store the name of the objects first material for parsing
		string materialName = go.renderer.sharedMaterial.name;
		
		//Set a default number of tiles to 1
		int totalNumberOfSubTiles = 1;
		
		//Parse the name of the material, the expected format is "MaterialName_#1_#2" where #1 is the index of the tile (tile index) and #2 is the total number of tiles (numbers indexed from 1)
		if (int.TryParse (materialName.Substring (materialName.LastIndexOf ("_") + 1, 1), out totalNumberOfSubTiles)) {
			//If the parse was successful store the value (adding 1 due to indexing from 1)
			totalNumberOfSubTiles += 1;
		}
		
		//Store some values to help with maths
		
		//padding wastes texture space but helps with mipmapping
		float paddingPercentage = 0.1f;
		//texture decimal is the fraction of UVs each texture takes (i.e if there were 2 tiles it would be 0.5)
		float textureDecimal = 1f / (totalNumberOfSubTiles - 1);
		//convert the padding as a percentage to a percentage of the decimal
		float padding = textureDecimal * paddingPercentage;
		//work out the final fraction of the texture that will be used including padding
		float textureDecimalMinusPadding = textureDecimal - padding - padding;
		
		//Create an array of colors to set values for (if the model doesnt have vertex colors create a new array)
		Color[] cols = mesh.colors;
		if (cols.Length == 0){
			cols = new Color[mesh.vertexCount];
		}
		
		//Create a list to store offsets
		List<int> tileOffset = new List<int> ();
		//Create a list of materials
		List<Material> materials = new List<Material>(go.renderer.sharedMaterials);
		//Sort the materials based on their tile index
		materials.Sort((x,y) => int.Parse(x.name.Substring(x.name.LastIndexOf("_")-1, 1)).CompareTo(int.Parse(y.name.Substring(y.name.LastIndexOf("_")-1, 1))));
		
		//Store the tile index of each material (this needed because for out project a single mesh may only use tiles 1,3,5, is every object had every material assigned this could be skipped)
		foreach (Material mat in materials) {
			int getOffset = 0;
			if (int.TryParse(mat.name.Substring(mat.name.LastIndexOf("_")-1, 1), out getOffset)){
				tileOffset.Add(getOffset-1);
			}
		}
		
		//Store the Uvs to assign to the new mesh
		Vector2[] uvs = mesh.uv;
		
		//For each sub mesh in the model
		for (int i = 0; i < mesh.subMeshCount; i++) {
			
			//Get its tile offset based on its material
			int tile = tileOffset [materials.IndexOf (go.renderer.sharedMaterials [i])];
			
			//Check if the scale isnt Vector3.one (I havent added support for tiling using the TRANSFORM_TEX yet)
			Vector2 tiling = go.renderer.sharedMaterials [i].mainTextureScale;
			if (tiling != Vector2.one) {
				Debug.LogWarning ("Tiled textures cannot be subtiled at this time, please scale your UVs");
			}
			
			//Get the triangles for this submesh
			int[] tris = mesh.GetTriangles (i);
			
			//Calculate and apply a color for each vertex on each triangle of the submesh
			foreach (int vertex in tris) {
				cols [vertex] = new Color ((tile * textureDecimal) + padding, 0, textureDecimalMinusPadding, 1);
			}
		}
		
		//Create a new mesh (could probably just assign this to the original mesh but hey ho
		Mesh newMesh = new Mesh ();
		
		//Create a list to combine triangle indicies from each submesh (this may be unneeded)
		List<int> completeTriangles = new List<int> ();		
		for (int i = 0; i < mesh.subMeshCount; i++) {
			completeTriangles.AddRange (mesh.GetTriangles (i));
		}
		
		//Assign all the data to the new mesh
		newMesh.vertices = mesh.vertices;
		newMesh.colors = cols;
		newMesh.triangles = completeTriangles.ToArray ();
		newMesh.uv = uvs;
		newMesh.uv2 = mesh.uv2;
		newMesh.normals = mesh.normals;
		newMesh.tangents = mesh.tangents;
		newMesh.RecalculateBounds ();
		
		//Save the new mesh as an asset
		
		//Get/Create a file path with the name of the asset (this currently is hard coded to FBX so watch out here if your 3d is different)
		string newFolderName = assetPath.Substring(assetPath.LastIndexOf ("/") + 1).Replace(".fbx", "_fbx");
		string newPath = assetPath.Replace (".fbx", "_fbx");

		if (AssetDatabase.AssetPathToGUID(newPath) == "" || !Directory.Exists(Application.dataPath.Replace("Assets","") + newPath)) {
			newPath = AssetDatabase.GUIDToAssetPath(AssetDatabase.CreateFolder (assetPath, newFolderName));
			AssetDatabase.Refresh();
		}
		newPath += "/" + go.name + "_subTile.asset";
		
		//Save the asset
		AssetDatabase.CreateAsset (newMesh, newPath);
		AssetDatabase.SaveAssets ();
		
		//Destroy the old mesh
		GameObject.DestroyImmediate (go.GetComponent<MeshFilter> ().sharedMesh);
		
		//Assign the new mesh
		go.GetComponent<MeshFilter> ().sharedMesh = AssetDatabase.LoadAssetAtPath (newPath, typeof(Mesh)) as Mesh;
		go.renderer.sharedMaterials = new Material[]{go.renderer.sharedMaterial};
	}
}

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Current ye@r *