Mutating Shader Parameters with Procedural Algorithms

Godot Version

v4.7.dev2.official [778cf54da]

Question (1st time)

How would one pass procedurally-generated, instance-specific parameters to any one shader of the node’s Sprite2D layers?

In other words,

How do I make procedurally-defined shaders?

Context

Suppose one had a scene for a planet that is procedurally generated at runtime or during play, that may or may not have user-inputs taken into account.

The scene then makes use of separate Sprite2D nodes, that when layered, give the impression that the planet is composed of many distinct and separate systems (oceans, land, vegetation, city-lights, atmosphere, etc.).

The base textures for each layer are generated using various noisemaps that are made at or during runtime. This is fine, as it is NOT shader-based.

The textures are all 2:1 flat maps, that are then projected onto gently spinning spheres using various shaders that are all fundamentally congruent, but tuned differently (e.g. atmosphere spins slightly slower or faster than the land one; citycover fades out at the terminator rather than fading in).

Here is one of these shaders, as an example:

shader_type canvas_item;

varying bool is_paused;

instance uniform float rotation_speed = 0.3;
instance uniform float tilt;
instance uniform float light_multiplier = 2.0;
instance uniform bool surface_glow = true;
instance uniform vec3 glow_colour : source_color; //= vec3(0.2, 0.4, 1.0);

instance uniform vec3 liquid_mask_colour : source_color; //= vec3(0.2, 0.4, 1.0);
instance uniform float liquid_albedo;

instance uniform vec2 lightspot_pos;
instance uniform float lightspot_strength;
instance uniform float lightspot_colour;
instance uniform float lightspot_scale;

instance uniform vec3 light_dir = vec3(1.0, 0.0, 0.0);
instance uniform float offset;

varying float stored_time;

void fragment() {
	
	is_paused = false;
	
	if ( !is_paused ) {
			stored_time = TIME;
		}
	
	vec2 p = UV * 2.0 - 1.0;

	// aspect correction
	p.x *= TEXTURE_PIXEL_SIZE.y / TEXTURE_PIXEL_SIZE.x;

	float r2 = dot(p, p);

	if (r2 > 1.0) {
		COLOR = vec4(0.0);
	} else {
		float z = sqrt(1.0 - r2);

		vec3 n = vec3(p.x, p.y, z);

		// axial tilt
		float ct = cos(tilt);
		float st = sin(tilt);

		n = vec3(
			n.x,
			n.y * ct - n.z * st,
			n.y * st + n.z * ct
		);

		float longitude = atan(n.x, n.z);
		float latitude  = asin(n.y);

		vec2 sphere_uv;
		sphere_uv.x = longitude / (2.0 * PI) + 0.5;
		sphere_uv.y = latitude  / PI + 0.5;

		// base rotation
		sphere_uv.x = fract(sphere_uv.x - TIME * 0.1 * rotation_speed);

		vec4 col = texture(TEXTURE, sphere_uv);

		// lighting + liquid albedo
		float light = dot(n, normalize(light_dir)) * light_multiplier;
		light = clamp(light * 0.5 + 0.5, 0.0, 1.0);
		
		float albedo_light = 0.0;
		
		if (distance(col.rgb, liquid_mask_colour) < 0.1) {
			albedo_light = distance(UV.x + offset, clamp(lightspot_pos.x, 0.0, 1.0)) * liquid_albedo;
		}
		
		col.rgb *= light + albedo_light * albedo_light;

		// surface_glow
		if ( surface_glow == true ) {
			float fresnel = pow(1.0 - dot(n, vec3(0.0, 0.0, 1.0)), 3.0);
			col.rgb += glow_colour * fresnel * 0.3;
		};

		COLOR = col;
		
		if (!is_paused) {
			stored_time = TIME;
		}
	}
}

The shaders aren’t fully done yet, but this is a rough idea.

As of now, I have the working shaders that I can tune, and the planets work. But they cannot be generated at runtime. So far, I can only handcraft them myself before runtime.

The reason they’d be tuned differently, is that the colour of the atmosphere or land or vegetation should be able to change, and this will have real-game impacts on what resources (or chemical compounds) are found there. The way it’s generated might mess with its axial tilt, or its atmospheric windspeeds, or if it has an atmosphere at all. Stuff like that! It’ll be important for the game’s mechanics. Ideally, players would be able to venture out and generate these themselves (with or without user input).

Question (2nd time)

How would one pass procedurally-generated, instance-specific parameters to any one shader of the node’s Sprite2D layers?

In other words,

How do I make procedurally-defined shaders?

In trying to use get_shader_uniform_list(), I noticed that the instance uniform variables don’t get listed, and when I try mutating them from an outside .gd file, or even its parent .gd file, it seems I can’t.

However, if I switched to standard uniform variables, then I’d be able to mutate them, but it would mutate the shader for all objects using it. And I wouldn’t want that either.

Am I missing something?

The docs don’t seem to help much. I’m also relatively new to shaders, so please forgive me if I missed something obvious.

  1. Add a ShaderMaterial to a MeshInstance3D.
  2. Apply the Shader to the ShaderMaterial.
  3. Get a reference to the ShaderMaterial.
  4. Use set_shader_parameter()
extends MeshInstance3D


func _ready():
 	var material: ShaderMaterial = get_surface_override_material(0)
	material.set_shader_parameter("rotation_speed ", 0.5)
1 Like

If you set your shader material “Local to Scene” then you can mutate shader uniforms for each sprite, you likely do not want to use instance uniforms in this case.

2 Likes

I tried doing this and I’m not sure how to make it work with the rest of my game.

My game is entirely in 2D, with the shader being the only technicality. I also have almost no experience at all with 3D game development on Godot, but I can definitely look into it.

Would this solution work for a fully 2D game?

Yes, but how does one do this through code?

After reading around a bit, the checkable “Local to scene” setting in the Resource tab seems to be what I need, and I think it’s what you mentioned. I didn’t realize it could automatically be done at runtime without having to manually right-click them and make them local. I’ll see if this works.

Technically, you can’t change uniforms per shader. You can only change them eihter per material or per instance.

For the first approach you need to duplicate the material for the each instance. If your instance is inside a scene then you can tell Godot to this automatically by enabling material’s instance_local_to_scene flag.

WIth the second approach you set the instance uniforms by calling set_instance_shader_parameter() on the instance itself and no material duplication is needed.

3 Likes

Yep! Thanks to both of you.

Ticking the resource property “Local to scene” is what really worked!

Using instance uniforms, my shader parameters wouldn’t show up when using the get_shader_uniform_list() method for some reason. Being able to make them regular uniforms by making the shaders local was what did it, because now the shader parameters show up, and each planet here is its own instance (visualized by the randomized scale of each planet).

I assume them being listed means I will have very few issues mutating them now.

Screenshot was taken during runtime. Note the debug window bottom left. The returned array was empty when I had them set as instance uniforms.

Thanks everyone!

You can get instance shader parameters from the node:

func get_instance_shader_parameters(node):
	return node.get_property_list().filter(func(p): return p.name.contains("instance_shader_parameter") )
1 Like

Missed the 2D bit. But yes, the solution still works with some tweaking.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.