Heya, Thanks for visiting!

Texture Tiling based on object Size/Scale in Unity

  • unity-3d
  • textures

Say you want to create a ground plane that can be scaled to any size and maintain the same texture size on the surface without having to change the material tiling every time. Since this is not default functionality or some setting you can turn on, how can this be accomplished? I had this exact same question about a year ago when messing with Unity. DaveA answered a bit vaguely and re-reading his answer now, I implemented exactly what he suggested. You can see how to create both ideas he suggested in this article.

Various plane surfaces in Unity at different sizes but the same texture scale

There are two methods to achieve texture tiling based on size. The first way is adjusting actual texture tiling property whenever a change is detected in scale. The second option is to procedurally generate a plane mesh with many segments all with overlapping UVs.

Method #1: Texture Tiling

I recommend this approach because it doesn't waste memory and resources on a bunch of vertices like method #2 requires.

We will be monitoring Transform.lossyScale for changes because it is the world scale, and we only care about the plane's actual size which includes any parents scaling. Once we detect a change, update Material.mainTextureScale to the appropriate tiling for that new size.

Another point of interest is the [ExecuteInEditMode] attribute which will make the script run in the editor even without playing.

Note: Unity's default Plane primitive seems to have its UVs upside down. You can model your own 10mx10m, 10x10 segment plane in a 3D program(Blender, C4D, 3ds max). Or you can use this procedural plane script which is available in my project Radius, on GitHub.

Showing how the actual texture appears upside down in Unity because the UVs are upside down in relation to the Z axis

Setup and Usage:

  • Drag TextureTilingController.cs onto a Plane Primitive
    • Adjust the "Texture to Mesh Z" option to set the texture scale in the game world
  • Now you can adjust the plane size however big or small you want (use Transform scale properties for x and z).
    • Whenever the object changes size, it will adjust the texture tiling automatically.
    • You can also manually update the tiling using the "Update Tiling" option in the context menu.

Showing off the TextureTilingController script component on a plane.

TextureTilingController.cs

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class TextureTilingController : MonoBehaviour {

	// Give us the texture so that we can scale proportionally the width according to the height variable below
	// We will grab it from the meshRenderer
	public Texture texture;
	public float textureToMeshZ = 2f; // Use this to constrain texture to a certain size

	Vector3 prevScale = Vector3.one;
	float prevTextureToMeshZ = -1f;

	// Use this for initialization
	void Start () {
		this.prevScale = gameObject.transform.lossyScale;
		this.prevTextureToMeshZ = this.textureToMeshZ;

		this.UpdateTiling();
	}

	// Update is called once per frame
	void Update () {
		// If something has changed
		if(gameObject.transform.lossyScale != prevScale || !Mathf.Approximately(this.textureToMeshZ, prevTextureToMeshZ))
			this.UpdateTiling();

		// Maintain previous state variables
		this.prevScale = gameObject.transform.lossyScale;
		this.prevTextureToMeshZ = this.textureToMeshZ;
	}

	[ContextMenu("UpdateTiling")]
	void UpdateTiling()
	{
		// A Unity plane is 10 units x 10 units
		float planeSizeX = 10f;
		float planeSizeZ = 10f;

		// Figure out texture-to-mesh width based on user set texture-to-mesh height
		float textureToMeshX = ((float)this.texture.width/this.texture.height)*this.textureToMeshZ;

		gameObject.renderer.material.mainTextureScale = new Vector2(planeSizeX*gameObject.transform.lossyScale.x/textureToMeshX, planeSizeZ*gameObject.transform.lossyScale.z/textureToMeshZ);
	}
}

Method #2: Overlapping UVs Method

In order to have your texture tile seamlessly across the plane using the overlapping UV technique, there are two things to take into account:

  • Make a procedural plane mesh that adapts the number segments depending on the proportion of the texture
  • Make each pair of triangles that make up one rectangle overlap in the UV map.

This means you will have 6 vertices for every iteration of the tiling. I do not recommend this for large ground planes.
Note: Unity has a 65k(65,534) vertex limit for a single mesh.

Showing off the the difference between overlapping UVs vs non-overlapping UVs. The bottom-left corning is (0, 0) and the top-right corner is (1, 1)

I use this same technique for making any sized territory in Radius.

Screenshot from Radius of the transparent circle territories useful for king of hill or land grab type game types

Setup and Usage:

  • Drag ProceduralPlane.cs and GroundController.cs onto an empty gameobject.
  • To adjust the plane size, edit the Width and Height properties of GroundController.cs.
  • Fill in the references for Procedural Plane and Texture. Drag the object with script in the hierarchy tab onto the Procedural Plane field. Drag the texture you are going to tile from the Project tab onto the Texture field.

Showing off the ProceduralPlane script component on an empty gameobject

ProceduralPlane.cs

You can get the procedural plane script in my project Radius, on GitHub.

GroundController.cs

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class GroundController : MonoBehaviour {

	public ProceduralPlane proceduralPlane;

	public float Width = 10f;
	public float Height = 10f;

	// Give us the texture so that we can scale proportionally the width according to the height variable below
	// We will grab it from the meshRenderer
	public Texture texture;
	public float textureToMeshZ = 2f; // Use this to constrain texture to a certain size


	float prevWidth = 10f;
	float prevHeight = 10f;
	float prevTextureToMeshZ = 2f;

	// Use this for initialization
	void Start () {
		this.prevWidth = this.Width;
		this.prevHeight = this.Height;
		this.prevTextureToMeshZ = this.textureToMeshZ;

		// Do calculations and Generate the mesh
		this.UpdatePlaneSize();
	}

	// Update is called once per frame
	void Update () {
		// If something has changed
		if(this.Width != this.prevWidth || this.Height != this.prevHeight || this.textureToMeshZ != this.prevTextureToMeshZ)
			this.UpdatePlaneSize();


		// Maintain previous state variables
		this.prevWidth = this.Width;
		this.prevHeight = this.Height;
		this.prevTextureToMeshZ = this.textureToMeshZ;
	}

	[ContextMenu("UpdatePlaneSize")]
	void UpdatePlaneSize()
	{
		//Debug.Log("updating ground plane");

		// We will pack as many height segments in collider height
		this.proceduralPlane.SegmentsZ = (int)Mathf.Floor((float)this.Height/this.textureToMeshZ);
		// Multiply amount of height segments by the texture-to-mesh height
		// This will not be the same as the collider height.
		this.proceduralPlane.Height = this.proceduralPlane.SegmentsZ * this.textureToMeshZ;

		// Figure out texture-to-mesh width based on user set texture-to-mesh height
		float textureToMeshX = ((float)this.texture.width/this.texture.height)*this.textureToMeshZ;
		// Proportionally pack in the width segments
		this.proceduralPlane.SegmentsX = (int)Mathf.Floor((float)this.Width/textureToMeshX);
		// Multiply amount of width segments by the texture-to-mesh width
		this.proceduralPlane.Width = this.proceduralPlane.SegmentsX * textureToMeshX;

		// Generate mesh
		this.proceduralPlane.RecalculateMesh();
	}
}