Maratonec
Maratonec

Reputation: 73

Godot shader swap materials by world position on 3d mesh

I am trying to replicate something similar to this from Unity in Godot Engine with shaders, however, I am not able to find a solution. Calculating the position of the effect is the problem. How can I get the position in Godot, where I don't have access to the worlPos variable used in the video? A full code snippet of the shader would be really appreciated. Output

Currently, my shader code looks like this. ob_position is the position passed from the node.

shader_type spatial;
uniform vec2 ob_position = vec2(1, 0.68);
uniform float ob_radius = 0.01;


float circle(vec2 position, float radius, float feather)
{
    return smoothstep(radius, radius + feather, length(position - vec2(0.5)));
}

void fragment() {
    ALBEDO.rgb = vec3(circle(UV * (ob_position), ob_radius, 0.001) );
}

Upvotes: 0

Views: 1073

Answers (2)

Maratonec
Maratonec

Reputation: 73

The answer from Theraot was a lifesaver for me however, I also needed support for multiple positions, using arrays, uniform vec3 sphere_position[]; So I came up with this:

shader_type spatial;
uniform uint ob_position_size;
uniform vec3 sphere_position[2];
uniform sampler2D noise_texture;
uniform sampler2D tex1;
uniform float radius;
uniform float edge;
void fragment()
{
    vec3 pixel_world_pos = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;

    float noise_value = texture(noise_texture, pixel_world_pos.xy + vec2(TIME)).r;
    ALBEDO = texture(SCREEN_TEXTURE, SCREEN_UV).rgb;
    for(int i = 0; i < sphere_position.length(); i++) {
    float dist = distance(sphere_position[i], pixel_world_pos) + noise_value;
    float threshold = step(radius, dist);
    ALBEDO.rgb = mix(texture(tex1, UV).rgb, ALBEDO.rgb, threshold);
    //EMISSION = vec3(step(dist, edge + radius) * step(radius, dist));
    }
}

Upvotes: 0

Theraot
Theraot

Reputation: 40315

The video says:

  1. Send the sphere position to the shader in script.

    We can do that. First define an uniform:

    uniform vec3 sphere_position;
    

    And we can set it from code:

    material.set_shader_param("sphere_position", global_transform.origin)
    

    Since you need to set this every time the sphere moves, you can use NOTIFICATION_TRANSFORM_CHANGED which you enable by calling set_notify_local_transform(true).

  2. Get the distance between the sphere and World Position.

    To do that we need to figure out the world position of the fragment. Let us start by looking at the Fragment Build-ins. We find that:

    1. VERTEX is the position of the fragment in view space.
    2. CAMERA_MATRIX is the transform from view space to world space.

    Yes, the naming is confusing.

    So we can do this (in fragment):

    vec3 pixel_world_pos = (CAMERA_MATRIX * vec4(VERTEX, 1.0)).xyz;
    

    You can use this to debug: ALBEDO.rgb = pixel_world_pos;. In general, output whatever variable you want to visualize for debugging to ALBEDO.

    And now the distance is:

    float dist = distance(sphere_position, pixel_world_pos);
    
  3. Control the size by dividing by radius.

    While we don't have direct translation for the code in the video… sure, we can divide by radius (dist / radius). Where radius would be a uniform float.

  4. Create a cutoff with Step.

    That would be something like this: step(0.5, dist / radius).

    Honestly, I would rather do this: step(radius, dist).

    Your mileage may vary.

  5. Lerp two different textures over the cutoff.

    For that we can use mix. But first, define your textures as uniform sampler2D. Then you can something like this:

    float threshold = step(radius, dist);
    ALBEDO.rgb = mix(texture(tex1, UV).rgb, texture(tex2, UV).rgb, threshold);
    
  6. Moving worldspace noise.

    Add one more uniform sampler2D and set a NoiseTexture (make sure to set its noise and make seamless to true), and then we can query it with the world coordinates we already have.

    float noise_value = texture(noise_texture, pixel_world_pos.xy + vec2(TIME)).r;
    
  7. Add worldspace to noise.

    I'm not sure what they mean. But from the visual, they use the noise to distort the cutoff. I'm not sure if this yields the same result, but it looks good to me:

    vec3 pixel_world_pos = (CAMERA_MATRIX * vec4(VERTEX, 1.0)).xyz;
    
    float noise_value = texture(noise_texture, pixel_world_pos.xy + vec2(TIME)).r;
    
    float dist = distance(sphere_position, pixel_world_pos) + noise_value;
    float threshold = step(radius, dist);
    ALBEDO.rgb = mix(texture(tex1, UV).rgb, texture(tex2, UV).rgb, threshold);
    
  8. Add a line to Emission (glow).

    I don't understand what they did originally, so I came up with my own solution:

    EMISSION = vec3(step(dist, edge + radius) * step(radius, dist));
    

    What is going on here is that we will have a white EMISSION when dist < edge + radius and radius < dist. To reiterate, we will have white EMISSION when the distance is greater than the radius (radius < dist) and lesser than the radius plus some edge (dist < edge + radius). The comparisons become step functions, which return 0.0 or 1.0, and the AND operation is a multiplication.

  9. Reveal object by clipping instead of adding a second texture.

    I suppose that means there is another version of the shader that either uses discard or ALPHA and it is used for other objects.


This is the shader I wrote to test this:

shader_type spatial;

uniform vec3 sphere_position;
uniform sampler2D noise_texture;
uniform sampler2D tex1;
uniform sampler2D tex2;
uniform float radius;
uniform float edge;

void fragment()
{
    vec3 pixel_world_pos = (CAMERA_MATRIX * vec4(VERTEX, 1.0)).xyz;

    float noise_value = texture(noise_texture, pixel_world_pos.xy + vec2(TIME)).r;

    float dist = distance(sphere_position, pixel_world_pos) + noise_value;
    float threshold = step(radius, dist);
    ALBEDO.rgb = mix(texture(tex1, UV).rgb, texture(tex2, UV).rgb, threshold);
    EMISSION = vec3(step(dist, edge + radius) * step(radius, dist));
}

Upvotes: 1

Related Questions