Heya, Thanks for visiting!
edited

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.

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.

Setup and Usage:

  • Drag TextureTilingController.cs onto a Plane Primitive
  • Adjust the size by using the 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 proportianally 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 contrain 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: Overlaping 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) vertice limit for a single mesh.

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

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.

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 proportianally 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 contrain 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;
        // Proportianally 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();
    }
}