Search Unity

Grass shader with real-time flattening

Discussion in 'Shaders' started by Nichotine, May 19, 2020.

  1. Nichotine

    Nichotine

    Joined:
    Jul 30, 2017
    Posts:
    18
    Hi,

    I've posted about learning grass shaders before and I've made some progress. Lighting is all working now but I've got some other issues I'm contemplating.

    The grass shader I'm working from is Roystan's https://roystan.net/articles/grass-shader.html

    The main problem is that I'm trying to find a way to flatten the grass in real time. I want the player to be able to place objects in the world, and grass within that object's collider flattened/discarded. I've found this tutorial which is also based on Roystan's https://jamesinvi.github.io/Elements-Website/pages/grass.html

    This shader uses a custom tesselation which I won't need. They are using a first person perspective whereas my game alternates between first person perspective and third person orthographic, so I'm assuming tesselation based on the camera could cause issues.

    The solution in this version seems to be to use a texture to determine where grass should be flattened, but it appears to use their tesselation code so I'm not sure how to fit it for my purpose. Also, I would want to modify grass in real-time by placing/removing objects, and I'm not sure how I would convert this world data to a texture.

    I'm wondering if there could also be a secondary version of this for swaying grass away from objects that don't flatten them, such as the player/NPCs. But this isn't essential to what I'm working on. I'm just assuming it could be a neat feature which might be implemented in a similar way using textures. I'm often going back to the catlikecoding tutorials but I still find working on shader code quite difficult.

    Any and all help appreciated.
     
  2. adehm

    adehm

    Joined:
    May 3, 2017
    Posts:
    369
    I would imagine you use a greyscale map to plot grass height based on rgb values. When someone or something flattens the grass I would use a compute shader to update the greyscale image and send it to the shader tessellating the grass. Camera distance I don't think will matter if you use fixed instead of edgelength.
     
  3. Nichotine

    Nichotine

    Joined:
    Jul 30, 2017
    Posts:
    18
    I guess the main issue I'm dealing with is how to write collider intersections with the grass mesh to that grayscale texture that the shader can use to determine grass height
     
  4. Nichotine

    Nichotine

    Joined:
    Jul 30, 2017
    Posts:
    18
    I think drawing specific collider outlines will be too difficult, so I'm going to try to limit it to custom draw squares/rectangles and circles. Now I just need to figure out how to draw to a texture so that the texture lines up with the real world positions on the mesh.

    EDIT: Perhaps RaycastHit.textureCoord ?
     
    Last edited: May 20, 2020
  5. adehm

    adehm

    Joined:
    May 3, 2017
    Posts:
    369
    I have never done it before but this is just my first thought on how I would attempt it.

    I would use a compute shader to update your greyscale then send that result to the shader. To find the position I believe you could translate the object xy to relative xy of the grass shader's position. Then draw a black circle fading from the center point of object over the top of previous grass grey scale texture with something like power = 1 - ((0-normalizedX) * (0-normalizedX) + (0-normalizedY) * (0-normalizedY)).
     
  6. Nichotine

    Nichotine

    Joined:
    Jul 30, 2017
    Posts:
    18
    Thanks for the help you're providing.

    Just to clarify before I start trying to put this together, you mean a compute shader writing the texture?

    The one thing I'm really not sure about is what method I used to translate the object world xy to the xy on the grass mesh
     
  7. adehm

    adehm

    Joined:
    May 3, 2017
    Posts:
    369
    Yeah the compute shader would write the new grey scale for grass tessellation by using the current iteration and drawing the new changes and then updating the shader with that texture.

    Let's say you have one rectangle that has a grass tessellation shader that is clamped. You know the player's position and you also know the rectangles position. So if the rectangle is 0,0 out of 1,1 and the player is 0.5,0.5 then you know the player is in the middle. You should be able to take an example like that and extrapolate to however you have it set up.
     
  8. Nichotine

    Nichotine

    Joined:
    Jul 30, 2017
    Posts:
    18
    Isn't it all relative to the grass mesh's UV though?
     
  9. adehm

    adehm

    Joined:
    May 3, 2017
    Posts:
    369
    Well Your vert shaders use normalized positioning so 0 to 1 and compute shaders use unormalized positioning so 1024x1024 for instance based on texture size. Then the actual world is separate from that. If you know where the object deforming the grass is relative to the object that holds the texture then you should be able to find the answer in theory.
     
  10. Nichotine

    Nichotine

    Joined:
    Jul 30, 2017
    Posts:
    18
    I see, thanks for the help. I'll try to make progress with this and update the thread with where I get
     
  11. Nichotine

    Nichotine

    Joined:
    Jul 30, 2017
    Posts:
    18
    I've been busy with other commitments but I've returned to this, unfortunately I've hit a wall I'm having trouble getting past.

    I found compute shaders a little intimidating at first but I got the hang of them when I figured out the code. Drawing shapes on the render texture is relatively trivial so I'm sure it will fit the use case I'm going for. I haven't fully implemented it yet, but that's mostly because the issue I'm having is between feeding that render texture to the grass shader in a meaningful way.

    My plan was to draw black shapes onto a red background and use that for 'obstructing' the grass. The jamesinvi grass variant I linked uses an if statement to discard the grass, although this is already built in to their grass sloping system which I don't use. I've heard putting if statements in shaders isn't efficient so I've opted to multiply the grass values by the red value of the selected pixel.

    So, by default it'll be a red texture, so they'll be multiplied by 1 and no difference. Whereas where I've drawn a black shape on the texture to mark obstruction, they'll multiply relevant values by 0 and make the grass invisible. If this is less efficient than I think it is, please point out to me how I could be doing better :)

    This effect seems to work... except I'm having a lot of difficulties getting the RenderTexture to be correctly sampled inside the shader. Currently, ALL grass is either 'open' or 'obstructed' depending on the lower left pixel of the RenderTexture. Here's the pipeline I've setup.

    1. A script (which as I mentioned, hasn't been fully implemented yet) manages all of the potential obstructions and feeds shape positions and values (circle radius, rectangle area, etc) into a compute shader.

    2. The compute shader calculates whether a given pixel should be red 'open' or black 'obstructed', draws the black shapes on a red background and writes to the render texture.

    3. The grass manager feeds the render texture into the shader using a MaterialPropertyBlock. Basically just propBlock.SetTexture() and rend.SetPropertyBlock(propBlock)

    4. The shader has a sampler2D variable which the propBlock writes to. I also have it as a property so I can access the texture from the shader menu.

    Here's where I think the issue starts

    5. I'm trying to take the x and z values of the grass blade's vertex position and use that to sample (tex2Dlod) the sampler2D to get that red value.

    float4 obTex = tex2Dlod(_ObstructionTexture, (x, z, 0, 0));

    As I've mentioned previously, this is basically a binary split, no matter how I change the code, the bottom left pixel is the only one that seems to matter. If it's black all the grass is obstructed, if it's red all the grass is open. I've tried to sample custom values and again, it never seems to matter what those values are. I'm going to continue to google this even though documentation is sparse. So if anyone has any insight into what I'm doing wrong here, I'd very much appreciate it.
     
  12. Nichotine

    Nichotine

    Joined:
    Jul 30, 2017
    Posts:
    18
    I feel very stupid... You can see my error in the line

    > float4 obTex = tex2Dlod(_ObstructionTexture, (x, z, 0, 0));

    Yeah... I needed to put a 'float4' before the sample coordinates. That fixed it :oops: