Transmit grass across individual unique planes/meshes

Godot Version

4.7.1

Question

Grass system based off this: Grass Rendering Series Part 4: Level-Of-Detail Tricks for Infinite Plains of Grass in Godot | hexaquo

I found a leftover script in the repo that goes unmentioned in the article, that being grass_grid.gd. You point the preload towards a packed scene of the finished grass system and attach it to an empty Node3D, and it will transmit your grass chunks dynamically with LODs across a square grid generated by the grass_grid.gd.

It looks amazing, but, for my use case I intend on placing my grass onto terrain such as below.

A couple of problems:

  • If I place individual 5x5 and 10x10 grass chunks manually, they won’t hug the terrain properly and the grass/impostors will appear too angular and look like ramps, ruining the affect.
  • Can’t use the grass_grid.gd unless it’s on completely flat ground otherwise it will clip into terrain as the code generates a uniform square grid.

I already know that to get my grass to “hug” the terrain and look like it’s actually physically attached to it I need to get it onto a mesh that fits the shape of areas of the terrain, which I can do by exporting the entire terrain as a mesh into Blender and then shaping/making the separate meshes for my grass to sit on. The result is grass that sits far more naturally than it would look otherwise.

The image below has grass on a simplified and not even crazy curvy mesh and even then the difference is staggering

Problem with this however is that due to how the MultiMesh grass system functions, it’s just duplications of that mesh.

This means that currently I’d have to make a huge amount of unique individual grass chunks all based off of the varying areas of the terrain. This approach here just doesn’t seem viable at all from a performance standpoint.

Expert artistry below to try and depict this.

This leads me to what I think the solution may be: projecting the flat 5x5 grass_chunks that I do have onto separate larger (but simplified) meshes that follow the shape of the terrain using the grass_grid.gd script.

I have no idea if this is a thing though, but I’d love to find out if it is.

Any help would be much appreciated.

Your grass is MultiMesh-based right? You don’t need the plane at the bottom, you could use a tool script to place the MultiMesh transforms against the terrain itself. How does your terrain work? If it has collision you could use raycasts, it may be slow but such a tool only has to run once.

Yes it is MultiMesh-based, it has two steps, one detailed with normal grass LOD and one simple for low LOD, setup below.

If possible I would like to keep the planes/meshes the grass sit on as they’re needed for the impostor’s that appear at distance to work.

I’d also like to keep the grass separate from my terrain systemically if possible for future modularity and general flexibility. Having the grass sit on a mesh separate from the terrain (but conforming to the terrains shape) is preferable for my use case.

That’s what leads me to the idea of using the grass_grid.gd to “transmit” the instanced grass over the shape of a mesh that covers larger areas rather than a generated flat square grid.

A thought did just occur: if the instanced grass chunk, which is a flat 5x5 plane, is “transmitted” over a mesh that isn’t also a flat plane, such as a curvy hill shaped mesh, would the grass chunks that transmit onto this mesh using grass_grid.gd not be conforming to the mesh shape as if the MultiMesh were generated on it? Even if you were to get the script to recognize the mesh shape, would the MultiMesh instances conform properly?

I’m imagining a scenario like this

MultiMesh transforms don’t “Transmit” over meshes, they don’t do a lot on their own, you are doing something to get those transforms placed. They conform to what ever script you are using.

I don’t think you need the imposter layer if you have already textured terrain.

Terrains are typically generated from heightmaps. If you have a heightmap, you can just sample it from the grass vertex shader and displace the vertices accordingly. This will automatically conform grass to the terrain without requiring any other setup.

The impostor layer has noise that mimics grass swaying in the wind visible at a distance, which the terrain does not. I could get rid of it but it would be a huge loss as it’s a crazy cheap but super effective visual.

@normalized I can get the heightmaps for my terrain. With the grass vertex shader displacing the vertices onto the terrain, would I have any control over the shape of the area the grass occupies? I intend on iterating the shapes of these large patches of grass over time as I continue to iterate on my terrain.

Also would this same code be applicable for the impostor grass shader so the impostor plane conforms to the terrain?

Yes you would still have to place the grass, the shader would only have to displace the grass on the Y axis

If your impostor plane was subdivided enough yes, it could use Y-axis vertex displacement just like the grass does, though you will only see the terrain or the impostor, it’s a little silly to keep both.

I’m having a hard time understanding how this would work. I’m trying to put a simple vertex displacement VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0); inside the grass.gdshader but it’s not displacing the vertices, it’s just stretching the grass upwards uniformly and not in line with the math below.

The grass is sitting inside of a MultiMesh instance saved as .tres, which to my limited knowledge would mean that any vertex displacement in the grass.gdshader wouldn’t work in this case right?

It probably is working the way the shader is describing, but this vertex addition happens for every blade of grass. you are adding the X and Z to the Y, you should try only adding to the Y based on your heightmap texture and the instance’s position relative to that texture. Just to start see how adding sin(TIME) * 8 to the vertex.y looks?

Didn’t change anything visually for the grass, although now all of my grass is now uniformly floating up and down.

Yeah that’s the idea. Visually the grass is floating up and down with time.

Hopefully that makes a little more sense what the VERTEX.y += is doing; next you have to load the heightmap into this shader with a

uniform sampler2D my_heightmap;

What to do with that heightmap gets a lot tricker, you need to take the world position of this grass and translate it to a 0 to 1 coordnate on that heightmap image. Using render_mode world_vertex_coords; may help too, I’d imagine with this on you only need to scale down the VERTEX.xz to match the heightmap.

render_mode world_vertex_coords;

uniform sampler2D my_heightmap;
const float HEIGHTMAP_SCALE;

void vertex {
    vec2 world_uv = VERTEX.xz / HEIGHTMAP_SCALE;
    float height = texture(my_heightmap, world_uv);
    VERTEX.y += height;
}

something along these lines

Have my heightmap ready but I’m running into this

As the error say - you’re supposed to initialize constants with some value, otherwise it wouldn’t make sense to declare them as constants. So const float HEIGHTMAP_SCALE = 1.0;

Got it working and was able to plug in the heightmap, although it seems to break the grass.

You’d want to scale the height.

Also, the code @gertkeno posted is just a demonstration of the principle. Copypasting it directly probably won’t work out of the box for your specific case. You’d need to match the heightmap scale and horizontal translation to the actual terrain.

How did you create the terrain mesh?

Ah I see, still learning.

I think I can see what it’s doing when I move around the grass, it does seem to somewhat follow the terrain, although still scuffed. When you say I need to match the heightmap scale, am I giving it the number for the highest point of terrain unit wise so it knows what height range to act in? I assume this would be handled by the heightmap no? (I’m most likely misunderstanding this)

Terrain was created with Terrain3D.

Exported the heightmap as .r16 into Krita, exported from Krita as .png, resolution matches terrain size which is 8km on a side.

You need to match how your shader interprets the heightmap to the way Terrain3D interprets it.

Heightmaps don’t encode absolute height. They are just grayscale images. The height is thus stored as a value between 0 and 1. Ditto for the planar area the heightmap covers. It’s just an uv space between 0 and 1.

The code that interprets the heightmap needs to scale its unitless data to some real world size. Terrain3D does this through the parameters you give it. Your shader should do the same so that the mesh generated by Terrain3D and your grass displacement match.

https://terrain3d.readthedocs.io/en/latest/docs/heightmaps.html#scaling

I think I understand? If the unitless data is a value between 0 and 1, would the scale remain 1.0 and I’d be multiplying this number by the size of my terrain in meters, specifically height? Would that mean I’d have to find the tallest point of my terrain and multiply the scale to reach that number?

Currently hitting a wall, grass seems to only want to come from the world origin, which I assume is because I have no horizontal translation.

How do I define the real world size for the unitless data? I understand the concept here and that I can declare the size of the map as a var, but the unitless data conversion to what would be in this case meters, and then how to tell it to read that, with the addition of horizontal translation is lost on me.