Fattie
Fattie

Reputation: 12621

Unity, modify standard shader to randomly flip texture?

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,

(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.

Upvotes: 11

Views: 4149

Answers (4)

Cafe UCafe
Cafe UCafe

Reputation: 41

If you want to flip use alpha = 0 for those objects, and for others alpha = 1.

In vertex data of your shader write something like this:

half flip = 1 -2*vertex.a;

So the flip would be -1 or 1.

From the comments:

enter image description here

Note however that each time you access .mesh , in Unity, you are making a whole new mesh:

public class TestFlipUV : MonoBehaviour
    {
        private void Awake()
        {

            Mesh mesh = GetComponent<MeshFilter>().mesh;
            // NOTE. IN UNITY, ACCESSING .mesh CREATES
            // A NEW INSTANCE OF THAT MESH EVERY TIME

            Vector2[] uv2 = mesh.uv;

            float rnd = Random.RandomRange(0.0f, 1.0f);
            if (rnd > 0.5f)
            {
                for (int i = 0; i < uv2.Length; i++)
                {
                    uv2[i].x *= -1;    
                }
            }

            rnd = Random.RandomRange(0.0f, 1.0f);
            if (rnd > 0.5f)
            {
                for (int i = 0; i < uv2.Length; i++)
                {
                    uv2[i].y *= -1;    
                }
            }
            mesh.uv2 = uv2;
        }
    }

... and then ...

Shader "Unlit/FlipUV"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float2 uv2 : TEXCOORD1;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _MainTex_TexelSize;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.uv *= v.uv2;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

Upvotes: 4

game development germ
game development germ

Reputation: 1334

Pseudorandomly flipped

Shader "RandomFlip"
{
Properties
{
    _MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
    Tags { "RenderType"="Opaque" "DisableBatching"="True" }
    LOD 100

    Pass
    {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #pragma multi_compile_fog

        #include "UnityCG.cginc"

        struct appdata
        {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
        };

        struct v2f
        {
            float2 uv : TEXCOORD0;
            UNITY_FOG_COORDS(1)
            float4 vertex : SV_POSITION;
        };

        sampler2D _MainTex;
        float4 _MainTex_ST;

        v2f vert (appdata v)
        {
            // position of pivot in world space
            float3 pivotWorldPos = float3( unity_ObjectToWorld[0].w, unity_ObjectToWorld[1].w, unity_ObjectToWorld[2].w );

            // randomness achieved by feeding trigonometry function with large numbers
            float flipHorizontally = sin( ( pivotWorldPos.x + pivotWorldPos.y + pivotWorldPos.z ) * 1000 ) > 0;
            float flipVertically = cos( ( pivotWorldPos.x + pivotWorldPos.y + pivotWorldPos.z ) * 1000 ) > 0;

            // randomly flipping uvs 
            float2 uv = lerp( v.uv, float2( 1.0 - v.uv.x, v.uv.y ), flipVertically );
            uv = lerp( uv, float2( uv.x, 1.0 - uv.y ), flipHorizontally ); 

            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uv = TRANSFORM_TEX(uv, _MainTex);
            UNITY_TRANSFER_FOG(o,o.vertex);
            return o;
        }

        fixed4 frag (v2f i) : SV_Target
        {
            // sample the texture
            fixed4 col = tex2D(_MainTex, i.uv);
            // apply fog
            UNITY_APPLY_FOG(i.fogCoord, col);
            return col;
        }
        ENDCG
    }
}
}

Though you better not moving these objects :D Because randomness here are coming from position of object in world space. Batching will also break the randomness.

Upvotes: 5

Programmer
Programmer

Reputation: 125275

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);
    }
}

Upvotes: 8

obywan
obywan

Reputation: 864

I will post this as answer, you can try it and see if it works for you. (If not, maybe someone else will find this solution helpful). Tiling can be changed with SetTextureScale

Attach for each cube.

public class MatTilingChange : MonoBehaviour {
    MeshRenderer mr;
    int[] vals = new int[] { -1, 1 };

    private void Awake()
    {
        mr = GetComponent<MeshRenderer>();
    }

    void Start () {
        mr.material.SetTextureScale("_MainTex", new Vector2(vals[Random.Range(0, 2)], 
vals[Random.Range(0, 2)]));
    }
}

Upvotes: 0

Related Questions