Simple bilboard impostor shader.

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By mrdunk

Hi, i’m new here.
I first tried Godot a month ago. I jumped in with V4.0. In retrospect, V3 might have been a better choice but V4.0 seems robust so far.
Ive no real previous game engine experience although i played with Babylon.js a few years ago.
Go easy on me.

So this started out as a question but i worked out the answer before i submitted.
There’s not much info online about the subject though so thought i’d post here in the hope that it helps someone else. (Or a future me when i forget what i’ve learned and go searching again.)

I want to draw trees. Many, many trees.
After a bit of online reading, it seems like i want some combination of multimesh, billboards and shaders is what i need to get the correct texture images showing from different view angles for all the trees in a forest.

This project https://github.com/wojtekpil/Godot-Octahedral-Impostors looks fantastic but not yet working on V4.0 and it’s likely overkill for my low-poly needs.

Reading online there’s a few people manipulating which texture gets drawn on the CPU but this seems like the wrong approach, particularly as i’ll need to get this working with MultiMeshInstance3D; The texture will need to be calculated per instance.

So here’s what i have so far that implements a working prototype.
Note that i still need to fix transparency, scaling, shadows, …etc…

shader_type spatial;

uniform sampler2D imported_texture;
uniform vec2 uv1_scale;

void vertex() {
// Bliiboard calculation. Make base geometry always face observer.
    mat4 modified_model_view = VIEW_MATRIX * mat4(
        INV_VIEW_MATRIX[0],
        INV_VIEW_MATRIX[1],
        INV_VIEW_MATRIX[2],
        MODEL_MATRIX[3]
    );
    MODELVIEW_MATRIX = modified_model_view;


    // Calculate view angle.
    vec3 node_view = NODE_POSITION_WORLD - CAMERA_POSITION_WORLD;
    node_view = normalize(node_view);

    // The dot product potput stored in angle_y is not linear.
    // If you wanted the real angle 
    // you'd want to do `real_angle = arcsing(angle_y)`.
    float angle_y = dot(node_view, vec3(0.0, 1.0, 0.0));

    node_view[1] = 0.0;
    // angle_xz not real angle. See comment for angle_y.
    float angle_xz = dot(node_view, vec3(0.0, 0.0, 1.0));

    if(node_view[0] < 0.0) {
        angle_xz = 2.0 - angle_xz;
    }

    if(angle_y < -0.707) {
        // When viewed from above,
        // the shows whole atlas texture. 
        // Just for demonstration purposes.
        UV = UV;
    } else if(angle_xz < 0.0) {
        // FIrst quadrent of atlas texture.
        UV = UV * uv1_scale + vec2(0, 0);
    } else if(angle_xz < 1.0) {
        UV = UV * uv1_scale + vec2(0.5, 0);
    } else if(angle_xz < 2.0) {
        UV = UV * uv1_scale + vec2(0, 0.5);
    } else {
        UV = UV * uv1_scale + vec2(0.5, 0.5);
    }
}

void fragment() {
    ALBEDO = texture(imported_texture, UV).xyz;
}

In this state, it draws my tree imposters from the right direction.
Next i need to fix transparency, scaling, etc.

There’s no reason this approach wouldn’t work for animated characters as well. You’d need to pass in a new uniform value for the animation frame.

Anyway, i hope this helps someone.

:bust_in_silhouette: Reply From: mrdunk

Here it is again with fixed billboard scaling and transparency working.

shader_type spatial;

uniform sampler2D imported_texture : source_color,filter_linear_mipmap,repeat_enable;
uniform vec2 uv1_scale;

void vertex() {
    // Bliiboard calculation.
    // Make base geometry always face observer.
    MODELVIEW_MATRIX = VIEW_MATRIX * mat4(
        INV_VIEW_MATRIX[0],
        INV_VIEW_MATRIX[1],
        INV_VIEW_MATRIX[2],
        MODEL_MATRIX[3]
    );
    // Scale the texture to fill the geometry. (Billboard scaling.)
    MODELVIEW_MATRIX = MODELVIEW_MATRIX * mat4(
        vec4(length(MODEL_MATRIX[0].xyz), 0.0, 0.0, 0.0),
        vec4(0.0, length(MODEL_MATRIX[1].xyz), 0.0, 0.0),
        vec4(0.0, 0.0, length(MODEL_MATRIX[2].xyz), 0.0),
        vec4(0.0, 0.0, 0.0, 1.0));
    MODELVIEW_NORMAL_MATRIX = mat3(MODELVIEW_MATRIX);

    // Calculate view angle.
    vec3 node_view = NODE_POSITION_WORLD - CAMERA_POSITION_WORLD;
    node_view = normalize(node_view);

    // The dot product potput stored in angle_y is not linear.
    // If you wanted the real angle you'd want to do `real_angle = arcsing(angle_y)`.
    float angle_y = dot(node_view, vec3(0.0, 1.0, 0.0));

    node_view[1] = 0.0;
    // angle_xz not real angle. See comment for angle_y.
    float angle_xz = dot(node_view, vec3(0.0, 0.0, 1.0));

    if(node_view[0] < 0.0) {
        angle_xz = 2.0 - angle_xz;
    }

    if(angle_y < -0.707) {
        // Above.
        UV = UV * uv1_scale + vec2(0, 0);
    } else if(angle_xz < -0.707) {
        // FIrst quadrent of atlas texture.
        UV = UV * uv1_scale + vec2(0.333, 0.5);
    } else if(angle_xz < 0.707) {
        UV = UV * uv1_scale + vec2(0.333, 0.5);
    } else if(angle_xz < 1.239) {
        UV = UV * uv1_scale + vec2(0.666, 0);
    } else if(angle_xz < 2.707) {
        UV = UV * uv1_scale + vec2(0333, 0.5);
    } else {
        UV = UV * uv1_scale + vec2(0.333, 0.5);
    }
}

void fragment() {
    ALBEDO = texture(imported_texture, UV).xyz;

    vec4 albedo_tex = texture(imported_texture, UV);
    ALPHA *= 255.0 * albedo_tex.a;
    ALPHA_SCISSOR_THRESHOLD = 0.5;
}

Use Godot 3.4 to bake the textures. And then,
Use this shader to use those textures in godot4…
I converted it for Godot 4.
Godot4 octahedral imposter shader

pixistone | 2023-05-02 20:11

Ha, good work. Thanks.
i’d considered doing this too but writing my own shader was a fun experiment.

mrdunk | 2023-05-02 22:09