Search Unity

UV Tiled Variation Offset

Discussion in 'Shaders' started by SweetBro, Nov 17, 2019.

  1. SweetBro

    SweetBro

    Joined:
    Jan 14, 2013
    Posts:
    69
    My goal is to emulate the "variation tile" effect that's standard in basically every 2D tilemapping tool on a 3D plane. Basically the input would be a 64x64 texture, consisting of 4, 32x32 tiled variations. When the texture would be tiled, rather than displaying the full 64x64 texture, I would like to instead display one of the 32x32 variations, chosen randomly.

    Now my understanding is that this would be most easily achieved with a vertex/fragment shader. That would rescale the UV map, and then randomly offset it using the tile height/width of the texture map as limits.

    I'm however not amazing at shader-fu, and have been struggling to find relevant documentation to this since the language used has a lot of other meanings in shaderland and are constantly leading me to completely unrelated searches.

    If someone could help me out here, I would greatly appreciate it. I feel like I've spent days on what should be no more than a dozen or two lines of code.
     
  2. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    Which bit are you struggling with? It sounds like you've already figured out the solution.
     
  3. SweetBro

    SweetBro

    Joined:
    Jan 14, 2013
    Posts:
    69
    Well I understand the theory of it more so than the actual practical solution. For one I'm not sure how to generate a random float on a per vertex basis. I'm also looking for an in-shader way for this to automatically tile based on the size of the surface, since right now the tiling effect only seems achievable from the Albedo 2D input property for the texture. Finally I would like to be able to run in this in conjunction with the standard surface shader which is problematic since unless I'm wrong there's no way for me to simply extend it.
     
  4. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    You cannot generate random numbers on GPUs - their performance relies on the fact they have no randomness.

    The standard practice is to either include a texture anytime you want random data (and sample it), or embed a complex function that - over time - gives numbers that jump around a lot, and sort-of-maybe-perhaps feel kind of random (although they're not).

    Tiling based on size of the surface isn't possible, by definition, unless you start writing geometry shaders: shaders don't have access to the mesh.

    Without writing GS's, the normal thing to do is encode extra data into the surface as vertex-attributes (which Unity annoyingly and wrongly calls "UV channels"), by pre-baking the information into the mesh, and read those back inside your shader. e.g. when generating the mesh (or afterwards, if you write a script to post-process your meshes) you store in each vertex "%age distance across, %age distance down", so that the shader can figure out tiling later on.

    Or ... just add two parameters to your Shader that the user has to provide on a case-by-case basis. Much much easier!

    You can easily extend the Standard Shader, I recommend @Jasper-Flick's tutorials: https://catlikecoding.com/unity/tutorials/rendering/part-2/ - over the course of 20 or so tutorials you gradually build-up the full source of the Standard shader, but in ways that you can easily modularise it. However, that'll take you days merely to read through. If you're brave you could jump to a late tutorial, download the source (which contains a highly modularised re-creation of the Standard shader) and try to only alter the bits you care about.

    (a lot of the Standard Shader is actually 1-line function calls that contain all the Unity stuff, and you can simply change the parameters and get your special effect. But you need to know the names of them all, and when to use them, and how they fit together, which is what Jasper's detailed series are useful fo)
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Mostly accurate, but somewhat misleading. When most people say random, pretty much no one but those in cryptography mean true random. They mean some form of pseudo random number. And it's not just GPUs, no computer can generate true random numbers. So all "random" values when it comes to computer code is some form of pseudo random number generator. True random numbers for cryptography are generated with the help of specialized hardware or bespoke setups, like hardware that generates numbers from cosmic radio waves, thermal decay of a resistor, or a camera pointed at a bunch of lava lamps.

    Pseudo random functions that shaders usually make use of take one or more input values and return a seemingly random, but consistent value. These are more formally known as hash functions. The return value is always the same for a given input (on the same hardware), so it’s not “over time”, just “over input range”. Time can certainly be one of the inputs, but doesn’t have to be.

    A noise texture is often used instead of a hash function just because you can use higher quality noise functions to produce more pleasing "randomness" than a cheap hash function can. And higher quality in this context could mean something that's smoother, like Perlin noise, or just feels more random, what I call "natural random", where the values are guaranteed to be different from one pixel to the next. Blue noise is a good example as that can take several seconds to produce a larger texture worth of blue noise making it implausible to generate in real-time.

    It’s pretty common to do something like use a quantized UV value as input into the hash function to produce random looking stable values. A common use case is exactly the goal you’re after of a “randomly tiled” texture. The second interior mapping shader I posted here is doing something almost exactly along the lines of what you're looking for in that regard, along with a bunch of other stuff:
    https://forum.unity.com/threads/interior-mapping.424676/#post-2751518

    That's the flat side of the default cube mesh randomly picking between "rooms" using a very dumb hash function.

    Also mostly accurate, but misleading. Technically geometry shaders don't have access to the mesh either, just a single triangle at a time, where as other stages only have access to a single vertex or a single pixel at a time. Often times just scaling the UVs based on the object’s scale can work well enough. Or triplanar UV mapping can provide a solution here. Technically geometry shaders would also fail in actually providing useful scaled UVs on most meshes since they would be scaling by each triangle without knowledge of the rest of the mesh’s UVs. This means seams would appear between each triangle since they may no longer align after scaling. This wouldn’t be a problem for a simple plane, but in that case scaling by the object’s scale would be just as effective and cheaper.

    I think you're mixing some terminology here when it comes to Unity shaders.

    The "Standard" shader is the built in shader that materials default to, and is a pain to modify, as @a436t4ataf alludes to. The code for most of it isn't super complex, but it's spread across multiple files effectively obfuscating much of the code. And it is really is not meant to be modified by users without making a copy of most of the relevant files in the long chain of .cginc files that individually reference each other.

    A "Surface Shader" is a type of shader file that is intended to allow you to write your own shaders more easily making use of the built in shading models Unity provides, like the one the Standard shader uses, without needing to go crazy. Surface shaders are more accurately shader generators, as they take the code you write, often limited to a single struct definition for things like UVs you want to use, and a surf function that defines basic surface values, and injects that custom surf function into the multiple generated vertex fragment shader passes needed by Unity's lighting system. This is the path you want to look at.
     
  6. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    The point I was making was that Do-it-yourself pRNG's in a shader tend to be a very poor and crappy experience compared to the quality of pRNG we get today from your typical CPU-side platform library / OS library. And that tends to show-up more often in graphics, in my experience (where small imbalances in the RNG's pdf appear as noticeable blotches on your tiled texture, for instance).

    TL;DR: you can use a function, but I'd always use a texture if possible.

    PS: I love the room-cube :).

    Oops. I've never written GS's. I thought they had access to higher-level info e.g. primitive ID (that isn't valid by the time you get to vertex/frag shaders), that enabled them to operate on a mesh as a whole, not just per-triangle.

    Do you mean manually, outside the shader? I assumed OP was looking for automatic, fully in-shader, adaptive scaling (hence my focus on what info exists at the vertex and fragment stages).
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Here's a simple example of randomized UV tiling. By default it scales up the UVs so one tile covers the 0.0 to 1.0 range of the initial UVs, so you might want to set the texture scale on the Albedo Atlas to 10, 10 or something when you create the material.
    Code (CSharp):
    1. Shader "Custom/RandomTiled"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Albedo Atlas (RGB)", 2D) = "white" {}
    6.         _AtlasInfo ("Atlas Rows & Columns (XY)", Vector) = (4,4,0,0)
    7.         _Glossiness ("Smoothness", Range(0,1)) = 0.5
    8.         [Gamma] _Metallic ("Metallic", Range(0,1)) = 0.0
    9.     }
    10.     SubShader
    11.     {
    12.         Tags { "RenderType"="Opaque" }
    13.         LOD 200
    14.  
    15.         CGPROGRAM
    16.         // Physically based Standard lighting model, and enable shadows on all light types
    17.         #pragma surface surf Standard fullforwardshadows
    18.  
    19.         // Use shader model 3.0 target, to get nicer looking lighting
    20.         #pragma target 3.0
    21.  
    22.         sampler2D _MainTex;
    23.  
    24.         struct Input
    25.         {
    26.             float2 uv_MainTex;
    27.         };
    28.  
    29.         half _Glossiness;
    30.         half _Metallic;
    31.         float2 _AtlasInfo;
    32.  
    33.         // Hash without Sine 2 (WebGL 2)
    34.         // https://www.shadertoy.com/view/XdGfRR
    35.         #define UI0 1597334673U
    36.         #define UI1 3812015801U
    37.         #define UI2 uint2(UI0, UI1)
    38.         #define UIF (1.0 / float(0xffffffffU))
    39.  
    40.         float2 hash22(float2 p)
    41.         {
    42.             uint2 q = uint2(int2(p))*UI2;
    43.             q = (q.x ^ q.y) * UI2;
    44.             return float2(q) * UIF;
    45.         }
    46.  
    47.         void surf (Input IN, inout SurfaceOutputStandard o)
    48.         {
    49.             // scale the UVs so that each 0.0 to 1.0 range of the original UVs are one tile
    50.             float2 scaledUV = IN.uv_MainTex / _AtlasInfo;
    51.  
    52.             // calculate the screen space derivatives for use later to prevent mip map artifacts
    53.             float2 uvdx = ddx(scaledUV);
    54.             float2 uvdy = ddy(scaledUV);
    55.  
    56.             // calculate the per tile offsets based on the original UVs
    57.             float2 indexOffset = floor(hash22(IN.uv_MainTex) * _AtlasInfo) / _AtlasInfo;
    58.  
    59.             // apply offsets to UVs
    60.             float2 randomizedUV = frac(scaledUV + indexOffset);
    61.  
    62.             // sample texture with randomized UVs, using derivatives from un-randomized UVs
    63.             fixed4 c = tex2Dgrad (_MainTex, randomizedUV, uvdx, uvdy);
    64.  
    65.             o.Albedo = c.rgb;
    66.             o.Metallic = _Metallic;
    67.             o.Smoothness = _Glossiness;
    68.         }
    69.         ENDCG
    70.     }
    71.     FallBack "Diffuse"
    72. }
    Note: this is not doing any object size based scaling.
     
    Last edited: Nov 19, 2019
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    No, I mean fully in the shader. For something like a flat plane or mostly flat wall or floor section, you can extract the scale from the unity_ObjectToWorld matrix and apply that to the UVs and get usable results.