Procedural Uniform UVs On a Plane

I have a script that procedurally generates a plane (rectangular) via width and height input. The plane is composed of a lattice across a grid of 1.0f spacing. For example, a plane of 4x5 has 20 quads, or 40 triangles.
I understand how UVs work but I’m having some trouble ‘building’ the UV Vector2’s procedurally. Basically, I want to have a UV mapping: 0,0 / 1,0 / 0,1 / 1,1 for each quad on the mesh. This is for an experiment in dynamic UV mapping so simply setting a UV for the whole plane and tiling the texture does not apply here. Many thanks in advance.

Here’s a modified version of the CreatePlane script (linked by Fattie in a comment above) that supports two-sided planes/quads. It has correctly reversed UV mapping and minimal vertices (4 certs, 4 tris for a two sided quad). Very useful for collision detection and thin transparent objects with transparency (or not).

Original source (now updated to support two-sided planes):
http://wiki.unity3d.com/index.php?title=CreatePlane

Usage: add script to an Editor folder in your project, then select Game Object → Create Other → Custom Plane…

using UnityEngine;
using UnityEditor;
using System.Collections;


public class CreatePlane : ScriptableWizard
{
	
	public enum Orientation
	{
		Horizontal,
		Vertical
	}
	
	public enum AnchorPoint
	{
		TopLeft,
		TopHalf,
		TopRight,
		RightHalf,
		BottomRight,
		BottomHalf,
		BottomLeft,
		LeftHalf,
		Center
	}
	
	public int widthSegments = 1;
	public int lengthSegments = 1;
	public float width = 1.0f;
	public float length = 1.0f;
	public Orientation orientation = Orientation.Horizontal;
	public AnchorPoint anchor = AnchorPoint.Center;
	public bool addCollider = false;
	public bool createAtOrigin = true;
	public bool twoSided = false;
	public string optionalName;
	
	static Camera cam;
	static Camera lastUsedCam;
	
	
	[MenuItem("GameObject/Create Other/Custom Plane...")]
	static void CreateWizard()
	{
		cam = Camera.current;
		// Hack because camera.current doesn't return editor camera if scene view doesn't have focus
		if (!cam)
			cam = lastUsedCam;
		else
			lastUsedCam = cam;
		ScriptableWizard.DisplayWizard("Create Plane",typeof(CreatePlane));
	}
	
	
	void OnWizardUpdate()
	{
		widthSegments = Mathf.Clamp(widthSegments, 1, 254);
		lengthSegments = Mathf.Clamp(lengthSegments, 1, 254);
	}
	
	
	void OnWizardCreate()
	{
		GameObject plane = new GameObject();
		
		if (!string.IsNullOrEmpty(optionalName))
			plane.name = optionalName;
		else
			plane.name = "Plane";
		
		if (!createAtOrigin && cam)
			plane.transform.position = cam.transform.position + cam.transform.forward*5.0f;
		else
			plane.transform.position = Vector3.zero;
		
		Vector2 anchorOffset;
		string anchorId;
		switch (anchor)
		{
		case AnchorPoint.TopLeft:
			anchorOffset = new Vector2(-width/2.0f,length/2.0f);
			anchorId = "TL";
			break;
		case AnchorPoint.TopHalf:
			anchorOffset = new Vector2(0.0f,length/2.0f);
			anchorId = "TH";
			break;
		case AnchorPoint.TopRight:
			anchorOffset = new Vector2(width/2.0f,length/2.0f);
			anchorId = "TR";
			break;
		case AnchorPoint.RightHalf:
			anchorOffset = new Vector2(width/2.0f,0.0f);
			anchorId = "RH";
			break;
		case AnchorPoint.BottomRight:
			anchorOffset = new Vector2(width/2.0f,-length/2.0f);
			anchorId = "BR";
			break;
		case AnchorPoint.BottomHalf:
			anchorOffset = new Vector2(0.0f,-length/2.0f);
			anchorId = "BH";
			break;
		case AnchorPoint.BottomLeft:
			anchorOffset = new Vector2(-width/2.0f,-length/2.0f);
			anchorId = "BL";
			break;			
		case AnchorPoint.LeftHalf:
			anchorOffset = new Vector2(-width/2.0f,0.0f);
			anchorId = "LH";
			break;			
		case AnchorPoint.Center:
		default:
			anchorOffset = Vector2.zero;
			anchorId = "C";
			break;
		}
		
		MeshFilter meshFilter = (MeshFilter)plane.AddComponent(typeof(MeshFilter));
		plane.AddComponent(typeof(MeshRenderer));
		
		string planeAssetName = plane.name + widthSegments + "x" + lengthSegments + "W" + width + "L" + length + (orientation == Orientation.Horizontal? "H" : "V") + anchorId + ".asset";
		Mesh m = (Mesh)AssetDatabase.LoadAssetAtPath("Assets/Editor/" + planeAssetName,typeof(Mesh));
		
		if (m == null)
		{
			m = new Mesh();
			m.name = plane.name;
			
			int hCount2 = widthSegments+1;
			int vCount2 = lengthSegments+1;
			int numTriangleVertices = widthSegments * lengthSegments * 6;
			if (twoSided) {
				numTriangleVertices *= 2;
			}
			int numVertices = hCount2 * vCount2;
			
			Vector3[] vertices = new Vector3[numVertices];
			Vector2[] uvs = new Vector2[numVertices];
			int[] triangleVertices = new int[numTriangleVertices];
			
			int index = 0;
			float uvFactorX = 1.0f/widthSegments;
			float uvFactorY = 1.0f/lengthSegments;
			float scaleX = width/widthSegments;
			float scaleY = length/lengthSegments;
			for (float y = 0.0f; y < vCount2; y++)
			{
				for (float x = 0.0f; x < hCount2; x++)
				{
					if (orientation == Orientation.Horizontal)
					{
						vertices[index] = new Vector3(x*scaleX - width/2f - anchorOffset.x, 0.0f, y*scaleY - length/2f - anchorOffset.y);
					}
					else
					{
						vertices[index] = new Vector3(x*scaleX - width/2f - anchorOffset.x, y*scaleY - length/2f - anchorOffset.y, 0.0f);
					}
					uvs[index++] = new Vector2(x*uvFactorX, y*uvFactorY);
				}
			}
			
			index = 0;
			for (int y = 0; y < lengthSegments; y++)
			{
				for (int x = 0; x < widthSegments; x++)
				{
					triangleVertices[index]   = (y     * hCount2) + x;
					triangleVertices[index+1] = ((y+1) * hCount2) + x;
					triangleVertices[index+2] = (y     * hCount2) + x + 1;
					
					triangleVertices[index+3] = ((y+1) * hCount2) + x;
					triangleVertices[index+4] = ((y+1) * hCount2) + x + 1;
					triangleVertices[index+5] = (y     * hCount2) + x + 1;
					index += 6;
				}
				if (twoSided) {
					// Same tri vertices, different order so normals are reversed
					for (int x = 0; x < widthSegments; x++)
					{
						triangleVertices[index]   = (y     * hCount2) + x;
						triangleVertices[index+1] = (y     * hCount2) + x + 1;
						triangleVertices[index+2] = ((y+1) * hCount2) + x;
						
						triangleVertices[index+3] = ((y+1) * hCount2) + x;
						triangleVertices[index+4] = (y     * hCount2) + x + 1;
						triangleVertices[index+5] = ((y+1) * hCount2) + x + 1;
						index += 6;
					}
				}
			}
			
			m.vertices = vertices;
			m.uv = uvs;
			m.triangles = triangleVertices;
			m.RecalculateNormals();
			
			AssetDatabase.CreateAsset(m, "Assets/Editor/" + planeAssetName);
			AssetDatabase.SaveAssets();
		}
		
		meshFilter.sharedMesh = m;
		m.RecalculateBounds();
		
		if (addCollider)
			plane.AddComponent(typeof(BoxCollider));
		
		Selection.activeObject = plane;
	}
}

You didn’t say what part you were having touble with, but…

“for each QUAD on the mesh”: the trick is the verts have UVs – faces and quads don’t, they guess them from surrounding verts. Two side-by-side quads normally share the middle two verts, so the bottom middle vert can’t have (1,0) for the left quad and also (0,0) for the right quad.

One solution is not to share verts. This is what a modelling program does whenever you make a seam (invisibly splits those verts.)

The other is to use the graphics-card’s built-in love of UV-tiling. Set x’s in the “two adjacent quads” example as 0,1,2. The two quads still share that x=1 middle vert. But quad #2 shows a full tile, from 1 to 2. This is what setting tiling secretly does.

But, if you wanted each quad to show 3/4ths of the image, it can’t be done w/o splitting verts (well, unless you want x=0, 0.75, 0, 0.75 for alternating reverse directions.)