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.
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.
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.
TextureTilingController.cs
onto a Plane Primitive
Transform
scale properties for x
and z
).
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);
}
}
In order to have your texture tile seamlessly across the plane using the overlapping UV technique, there are two things to take into account:
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.
I use this same technique for making any sized territory in Radius.
ProceduralPlane.cs
and GroundController.cs
onto an empty gameobject.Width
and Height
properties of GroundController.cs
.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.ProceduralPlane.cs
You can get the procedural plane script in my project Radius, on GitHub.
MeshUtils.cs
also available in Radius.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();
}
}