Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unity, modify standard shader to randomly flip texture?

Tags:

unity3d

shader

Imagine a scene with ten simple "cubes" all the same.

They each have the same material "M1" which has the simple standard shader, and, a simple PNG as the texture.

If you go to the material, and adjust the tiling,

enter image description here

enter image description here

You can conveniently flip the texture around for different looks.

Note that of course this will change all ten of the cubes all at once.

Is it possible to modify the standard shader, so that, simply,

  • on each object, which uses the material

  • the shader randomly changes the tiling (and, say, the offset)

  • again, I mean on a "per object" basis; in the example each of the ten would be randomly different.

(So: one cube would show tiling -1,1, one cube would show tiling -1,-1 and so on ... each cube different, although all using the same material, only one material exists.)

Note,

(1) it's totally trivial to generate a few different versions of the material, each with different tiling, and randomly select one of those for each cube. that's not the way to go

(2) note that if you vary a material, it of course makes more than one copy. you can't have thousands and thousands of materials.

The solution is to have (one) shader which knows to vary (say, randomly) the offset, inside the shader - on each object it is working on. (ie on a per specific object basis)

That's what I'm asking about here.

like image 869
Fattie Avatar asked Aug 29 '18 06:08

Fattie


People also ask

How do you rotate textures in shader?

To rotate a texture in a shader, you have to rotate its texture coordinates. This solution should work for both 2D and 3D. uv is the texture coordinate you're initially using. pivot is the point the uv will rotate about.


1 Answers

I noticed that you really really want to do this with nothing but a shader. You can't do this with a shader alone. The issue is not being able to generate a random number in a shader. The problem is being able to do it once. I haven't found a way to do so and don't think you can.

This is a problem that should be tackled with a code on the C# side.

(1) it's totally trivial to generate a few different versions of the material, each with different tiling, and randomly select one of those for each cube. that's not the way to go

(2) note that if you vary a material, it of course makes more than one copy. you can't have thousands and thousands of materials.

Not a problem at-all. This is what MaterialPropertyBlock is used for. It allows you to modify a shader property without creating new instance of that material.

"Use it in situations where you want to draw multiple objects with the same material, but slightly different properties." MaterialPropertyBlock

The code below would have cause many instances of the material to be created:

void Start()
{
    MeshRenderer meshRenderer = gameObject.GetComponent<MeshRenderer>();
    int tileX = (UnityEngine.Random.Range(1, 3) == 1) ? 1 : -1;
    int tileY = (UnityEngine.Random.Range(1, 3) == 1) ? 1 : -1;
    Vector2 tile = new Vector2(tileX, tileY);
    meshRenderer.material.SetTextureScale("_MainTex", tile);
}

With MaterialPropertyBlock, this issue is solved. Material copy is not made. Since you care about performance, you should also use Shader.PropertyToID:

void Start()
{
    int propertyID = Shader.PropertyToID("_MainTex_ST");
    meshRenderer = gameObject.GetComponent<MeshRenderer>();
    int tileX = (UnityEngine.Random.Range(1, 3) == 1) ? 1 : -1;
    int tileY = (UnityEngine.Random.Range(1, 3) == 1) ? 1 : -1;
    Vector2 tile = new Vector2(tileX, tileY);

    MaterialPropertyBlock matPropBlock = new MaterialPropertyBlock();
    //Get the current MaterialPropertyBlock settings
    meshRenderer.GetPropertyBlock(matPropBlock);
    //Assign the new tile value
    matPropBlock.SetVector(propertyID, tile);
    //matPropBlock.SetVector(Shader.PropertyToID("_MainTex_ST"), tile);
    //Apply the modified MaterialPropertyBlock back to the renderer
    meshRenderer.SetPropertyBlock(matPropBlock);
}

If this is done once in the game, it doesn't really make sense to attach the script to each GameObject. Just attach it t to one empty GameObject only, find all the objects by tag then run the code on each one.

void Start()
{
    GameObject[] objs = GameObject.FindGameObjectsWithTag("YourObjTag");
    int propertyID = Shader.PropertyToID("_MainTex_ST");
    for (int i = 0; i < objs.Length; i++)
    {
        MeshRenderer meshRenderer = objs[i].GetComponent<MeshRenderer>();
        int tileX = (UnityEngine.Random.Range(1, 3) == 1) ? 1 : -1;
        int tileY = (UnityEngine.Random.Range(1, 3) == 1) ? 1 : -1;
        Vector2 tile = new Vector2(tileX, tileY);

        MaterialPropertyBlock matPropBlock = new MaterialPropertyBlock();
        //Get the current MaterialPropertyBlock settings
        meshRenderer.GetPropertyBlock(matPropBlock);
        //Assign the new tile value
        matPropBlock.SetVector(propertyID, tile);
        //Apply the modified MaterialPropertyBlock back to the renderer
        meshRenderer.SetPropertyBlock(matPropBlock);
    }
}
like image 185
Programmer Avatar answered Oct 07 '22 15:10

Programmer