Billboard Grass with Wind

For presets and textures check the grass out here.

Features

  • Option to use a 2×2 texture atlas for variation.
  • Wind based on noise texture
  • Option to have grass blades be billboards.
  • Options for smooth, dithering and cut alphas for more control.
  • Wind based on a noise texture giving a wavy pattern
  • Colors based on a gradient textures.
  • Option to randomly pick parts of grass to have different color for highlights.

Usage

This grass shader uses MultiMeshInstance3D as its base.

  • If your scene doesn’t already have a ground mesh: Create a MeshInstance3D with your chosen mesh for the ground 
  • Add another MeshInstance3D with a QuadMesh. This will act as a base for the grass blades.
    • Increase the subdivisions to improve the quality of bending in the wind. Change the size to change the size.
    • You can change the subdivisions and size later as well. It’s also suggested to set the Center Offset Y axis to half your size (This way the pivot of the grass is at the bottom of the blades)
  • Add a MultiMeshInstance3D node, and at the top of your viewport select MultiMesh > Populate Surface

  • Set your ground mesh as the target, Source mesh as the quad mesh and play with the settings to fit your needs.

  • Finally add the shader to the Material Override slot on the MultiMeshInstance3D

Shader code
shader_type spatial;
render_mode blend_mix, depth_prepass_alpha;

uniform bool billboard = false;

uniform sampler2D shape_texture;
uniform sampler2D shape_atlas;
uniform bool use_atlas = false;

group_uniforms Colors;
uniform sampler2D noise_texture;
uniform sampler2D color_gradient;
uniform float random_variation : hint_range(0.0, 1.0, 0.001) = 0.002;

group_uniforms Wind;
uniform sampler2D wind_texture;
uniform vec2 wind_velocity;

group_uniforms Transparency;
uniform float alpha_cut_start : hint_range(0.0, 1.0, 0.05) = 0.1;
uniform float alpha_cut_end : hint_range(0.0, 1.0, 0.05) = 0.9;
uniform int alpha_mode : hint_enum("Smooth", "Dithered", "Cut") = 0;

varying flat int id;
varying vec3 world_pos;

#include "util/dither.gdshaderinc"

vec2 atlas_uv(vec2 uv, int index, int size){
	int tile_count = size * size;
	int i = index % tile_count;
	float x = float(i % size);
	float y = float(i / size);
	vec2 tile_size = 1.0 / vec2(float(size));
	return uv* tile_size + (vec2(x, y) * tile_size);
}

float randomf(int index, int seed){
	float value = sin(float(index + seed)) * 0.5 + 0.5;
	return value;
}

float wind_noise(){
	float value = texture(wind_texture, (world_pos.xz*0.03) - (vec2(TIME * 0.01) * wind_velocity)).r;
	return value;
}

vec3 wind(vec2 uv){
	float wind_noise = wind_noise();
	float wind_affect = pow(1.0 - uv.y, 2.0);
	vec3 value = vec3(
		wind_noise * (wind_velocity.x),
		0.0,
		wind_noise * (wind_velocity.y)) * 0.25;
	value *= wind_affect;
	return value;
}

void vertex() {
	world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
	id = INSTANCE_ID;

	if(billboard){
		mat4 billboard_matrix = mat4(
				MAIN_CAM_INV_VIEW_MATRIX[0],
				MAIN_CAM_INV_VIEW_MATRIX[1],
				MAIN_CAM_INV_VIEW_MATRIX[2],
				MODEL_MATRIX[3]);
		MODELVIEW_MATRIX = VIEW_MATRIX * billboard_matrix;

		VERTEX += (VIEW_MATRIX * vec4(wind(UV), 0.0)).xyz;
	} else {
		VERTEX += wind(UV);
	}
	NORMAL = vec3(0.0,1.0,0.0);
}

void fragment() {
	float shape_value = texture(shape_texture, UV).r;
	if(use_atlas){
		vec2 atlas_uv = atlas_uv(UV, id, 2);
		shape_value = texture(shape_atlas, atlas_uv).r;
	}

	float noise_value = texture(noise_texture, world_pos.xz * 0.1).r;
	if(randomf(id,1) < random_variation) noise_value += 0.4;
	vec4 color = texture(color_gradient, vec2(noise_value, 0.0));

	ALBEDO = color.rgb;

	float alpha = shape_value;
	if(alpha_mode == 0){
		ALPHA = alpha;
	} else {
		alpha = clamp((shape_value - alpha_cut_start) / (alpha_cut_end - alpha_cut_start), 0.0, 1.0);
		if(alpha_mode == 1){
			alpha = step(bayer4(FRAGCOORD.xy) + 0.01, alpha);
		}
		if(alpha < 0.1) discard;
	}

}

void light(){
	float ndotl = dot(LIGHT, NORMAL) * ATTENUATION;
	DIFFUSE_LIGHT += ndotl;
}
Live Preview
Tags
3d, grass, pixel-art
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from binbun

Related shaders

guest

7 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
sweetbabyalaska
26 days ago

Looks amazing! It appears to be missing the shader include “util/dither.gdshaderinc”

breadpack
19 days ago

Please, add source_color for color textures to adjust color space, because the colors are displayed wrong.

Last edited 19 days ago by breadpack
breadpack
10 days ago
Reply to  binbun

You can add source_color for texture. It’s used for sRGB conversions, not just for fancy vec representation.

Last edited 10 days ago by breadpack
LJ
LJ
13 days ago

I experienced issues with very obvious seams at the edges of the wind noise texture, particularly visible with fast wind speeds. This likely happens because the default behavior for UV coordinates exceeding 1.0 is simply to wrap them back around to 0, or a modulus operation.

Proposed solution is feeding the wind texture UV map through a periodic tent map function, like so:

// Maps given UV coordinates to a periodic sawtooth pattern,
// with inflections every 1.0 units, allowing for endless and
// seamless tiling by symmetrically flipping.
vec2 tent_map(vec2 uv) {
    uv *= vec2(0.5, 0.5);
    return vec2(2.0,2.0)*min(mod(uv,1), vec2(1,1)-mod(uv,1));
}

float wind_noise() {
  vec2 looped_uv = tent_map((world_pos.xz*0.03) – (vec2(TIME * 0.01) * wind_velocity));
  float value = texture(wind_texture, looped_uv).r;
  return value;
}

Last edited 13 days ago by LJ
LJ
LJ
13 days ago
Reply to  LJ

Oh. There is also the option to make noise textures seamless. Missed that the first time around.