Grass interaction with player position instead of object

Godot Version

4.7.1

Question

Following this grass tutorial: Grass Rendering Series Part 3: Animating and Interacting With Grass in Godot | hexaquo

Thanks to a forum member the object interaction with grass is now working. However, I’d like the shader to read the player position and use that for grass interaction instead. I’ve spent the past couple of hours trying to get it to work and referenced other solutions and no dice.

Currently the grass is a MultiMeshInstance3D with a MeshInstance3D Sphere as a child to the MultiMesh which updates the Object Position value, giving the grass the pressed down look.

I intend on making multiple MultiMesh grass patches and would like the player to be able to interact with these separate patches without each patch needing it’s own child sphere mesh, I assume this would be unnecessarily more costly compared to updating the shader Object Position parameter with the player position.

For reference, the sphere on the right is what’s currently implemented, I’d like to transfer this over to the player controller on the left.

Code for the grass shader:

shader_type spatial;
render_mode cull_disabled;

uniform float size_small = 0.2;
uniform float size_large = 0.6;

uniform float blade_bend = 0.5;

uniform vec3 color_small: source_color = vec3(0.3, 0.6, 0.1);
uniform vec3 color_large: source_color = vec3(0.9, 0.9, 0.2);

uniform sampler2D patch_noise;
uniform float patch_scale = 5.0;
varying float patch_factor;

uniform sampler2D wind_noise;
uniform float wind_strength = 0.1;
uniform vec2 wind_direction = vec2(1.0, 0.0);
uniform float wind_bend_strength = 2.0;
uniform float wind_ao_affect = 1.5;

uniform float object_radius = 1.0;
uniform vec3 object_position;

varying float bottom_to_top;
varying float current_wind_bend;
varying float cut_height;

void vertex() {
	cut_height = 1.0;

	// Cutting all grass on the right
	if (NODE_POSITION_WORLD.x > 0.0) {
		cut_height = 0.5;

		VERTEX.y = min(VERTEX.y, cut_height);
		UV.y = max(UV.y, 1.0 - cut_height);
	}

	bottom_to_top = 1.0 - UV.y;

	// Wind logic
	vec2 wind_position = NODE_POSITION_WORLD.xz / 10.0;
	wind_position -= (TIME + 8.0) * wind_direction * wind_strength;

	current_wind_bend = texture(wind_noise, wind_position).x;

	current_wind_bend *= wind_strength;
	current_wind_bend *= bottom_to_top * 2.0;

	mat4 inv_model = inverse(MODEL_MATRIX);
	vec2 local_direction = (inv_model * vec4(wind_direction.x, 0.0, wind_direction.y, 0.0)).xz;

	VERTEX.xz += current_wind_bend * local_direction * wind_bend_strength;

	// Bend away from the object
	float object_distance = distance(object_position, NODE_POSITION_WORLD);
	float bend_away_strength = max(object_radius - object_distance, 0.0) / object_radius;
	vec2 bend_direction = normalize(object_position.xz - NODE_POSITION_WORLD.xz);

	VERTEX.xz -= (inv_model * vec4(bend_direction.x, 0.0, bend_direction.y, 0.0)).xz
			* bend_away_strength * bottom_to_top;
	VERTEX.y -= bend_away_strength * bottom_to_top * 0.5;

	// General appearance
	VERTEX.z += blade_bend * pow(bottom_to_top, 2.0);

	patch_factor = texture(patch_noise, NODE_POSITION_WORLD.xz / patch_scale).r;
	VERTEX *= mix(size_small, size_large, patch_factor);

	NORMAL = mix(NORMAL, vec3(0.0, 1.0, 0.0), bottom_to_top);
}

void fragment() {
	if (!FRONT_FACING) NORMAL = -NORMAL;
	AO = bottom_to_top - current_wind_bend * wind_ao_affect;
	AO_LIGHT_AFFECT = cut_height;

	ALBEDO = mix(color_small, color_large, patch_factor);
	BACKLIGHT = vec3(0.2);

	ROUGHNESS = 0.4;
	SPECULAR = 0.2;
}

Script hooked into the MultiMesh:

@tool
extends MultiMeshInstance3D

func _process(delta: float) → void:
if has_node(“MeshInstance3D”):
material_override.set_shader_parameter(“object_position”, $MeshInstance3D.global_position)

Any help would be much appreciated.

Just set object_position shader parameter to player’s global_position instead of sphere’s global_position. Not sure where the problem is. The shader only cares to get some position.

Would the proper naming be $CharacterBody3D.global_position? I’m not sure exactly how to reference the player’s global_position.

Depends on your scene tree. Is there a node named CharacterBody3D as a direct child of this script?

Maybe using an @export variable would be easier? This allows you to assign the node from the Inspector, instead of writing node paths in your code.

@tool
extends MultiMeshInstance3D

@export var tracked_node: Node3D

func _process(delta: float) -> void:
    if tracked_node != null:
        material_override.set_shader_parameter("object_position", tracked_node.global_position)

Post your scene structure.

“GrassCurve” is the MultiMeshInstance3D node I’m working on currently.

Here is the scene tree for the player controller if this is relevant

There is not, the tutorial setup just has the sphere mesh set up as a child to the MultiMesh. Would tracked_node: Node3D be tracked_node: PlayerCharacter in this example?

You can set the shader parameter from the character controller script.

If you want to do it from the script attached to GrassCurve then just fetch the player node using a relative path from GrassCurve to PlayerCharacter or just assign the player node to tracked_node in the inspector as per @gertkeno’s example. The declared type can stay Node3D.

What kind of values/code would I need to plug into the character controller script to set the shader parameter? That might make more sense in this use case for me as I don’t intend on anything but the player interacting with the grass, and it’d be nice to have it inherently built into the shader itself.

If I were to continue using the script attached to GrassCurve, how would you assign the player node to tracked_node in the inspector? This is still new to me.

$"../GrassContainer/GrassCurve".material_override.set_shader_parameter(“object_position”, global_position)

You need to select the GrassCurve node in the scene tree panel and the exported property with the “assign” button should appear in the inspector. Click the button and pick the node.

I see! I was too focused on the script itself to see it was adding a variable to GrassCurve, got it to work with that method, thank you!

Is the $ the beginning of the string? What part of the character controller script would that go into? Would it be a variable in the script or a function?

I understand what material_override.set_shader_parameter(“object_position”, global_position) is doing, and I can see it’s grabbing the GrassCurve path, I’m just confused what it’s supposed to be doing, I haven’t seen a string that starts with a $ yet.

That line would go into _process() function in the player script so the shader parameter is continuously updated every frame. Pretty much the same as you’re doing in GrassCurve script.

$ is the node path prefix. It’s basically a shorthand for calling get_node(). The string that follows it the relative node path from the script this code is in, to the node whose property you want to access, in this case it’s material_override property on GrassCurve node.

Again you were already using the same principle when doing $MeshInstance3D.global_position, just that the node path is simple as MeshInstance3D is a direct child of the node that runs the script.

Node paths use similar logic and syntax to directory paths in computer’s file system. Both are organized as a tree-like structure. Some node path examples here:

I know I’m definitely doing something wrong here, just trying to wrap my head around what.

Do I need to declare a variable in this script for this to work properly?

You’ve copied formatted text into your script, the quotes have been replaced with fancy quotes "this" has different quotes than “that”. I’d recommend re-typing the quotes

Ahaha that was it, did not know about fancy quotes vs default GDScript quotes. It works!

I also see that using this method does not update in editor as it doesn’t use a @tool at the start, and I’m assuming this means character controller scripts can’t utilize this as I just tried and it spat out so many errors (or maybe they can, something to learn another time). It’s not critical it updates in editor for my use case but the feedback of seeing it update in editor is nice, looks like I’ll go with the MultiMesh script solution anyways.

Thank you thank you thank you to both of you for helping, I learned a lot in both of you doing so.

You can do it from the character script. Make the script @tool and use Engine.is_editor_hint() in _process() to branch out parts of the code you don’t want to run in the editor.

@tool

func _process(delta):
	if not Engine.is_editor_hint(): 
		# your existing code (that should run only in game)
		
	# shader parameter update (runs in game and in editor)