Shader improvement feedback

Godot Version

4.7.1.stable

Question

Is there anything wrong/suboptimal with the following shader? It works like I want for my purposes/doesn't error, but was asking just in case if there is anything to improve. The object space mapping is intentional.

shader_type spatial;

uniform sampler2DArray texture_albedo : source_color, repeat_enable, filter_linear_mipmap;
uniform sampler2DArray texture_normal : hint_normal, repeat_enable, filter_linear_mipmap;
uniform sampler2DArray texture_orm    : hint_default_white, repeat_enable, filter_linear_mipmap;

uniform sampler2D material_tint_lut : source_color, filter_nearest;
uniform sampler2D material_base_id_lut : filter_nearest;

uniform float lut_size = 512.0;

uniform float triplanar_scale = 1.0;
uniform float triplanar_sharpness = 4.0;

uniform bool use_override_tint = false;
uniform vec3 override_tint : source_color = vec3(1.0);

uniform bool use_override_base_id = false;
uniform float override_base_id = 0.0;

varying vec3 v_vertex;
varying vec3 v_normal;

varying flat uint v_material_id;

void vertex() {
vec3 world_scale = vec3(
length(MODEL_MATRIX[0].xyz),
length(MODEL_MATRIX[1].xyz),
length(MODEL_MATRIX[2].xyz)
);

v_vertex = VERTEX * world_scale;
v_normal = normalize(NORMAL);

uint r = uint(round(COLOR.r * 255.0));
uint g = uint(round(COLOR.g * 255.0));
uint b = uint(round(COLOR.b * 255.0));

v_material_id = r | (g << uint(8)) | (b << uint(16));

}

vec3 blend_rnm(vec3 base_normal, vec3 detail_normal) {
vec3 t = base_normal + vec3(0.0, 0.0, 1.0);
vec3 u = detail_normal * vec3(-1.0, -1.0, 1.0);
return normalize(t * dot(t, u) - u * t.z);
}

void fragment() {
float id_f = float(v_material_id);
float pixel_x = mod(id_f, lut_size);
float pixel_y = floor(id_f / lut_size);

vec2 lut_uv = vec2(pixel_x + 0.5, pixel_y + 0.5) / lut_size;

vec3 tint = texture(material_tint_lut, lut_uv).rgb;
vec2 base_id_data = texture(material_base_id_lut, lut_uv).rg;

float base_mat_id = round(base_id_data.r * 255.0) + (round(base_id_data.g * 255.0) * 256.0);

if (use_override_tint) {
    tint = override_tint;
}
if (use_override_base_id) {
    base_mat_id = override_base_id;
}

vec3 triplanar_normal = normalize(v_normal);
vec3 axis_sign = sign(triplanar_normal);

vec3 blend = abs(triplanar_normal);
blend = pow(blend, vec3(triplanar_sharpness));
blend /= dot(blend, vec3(1.0)); 

vec2 uv_x = vec2(-v_vertex.z * axis_sign.x, v_vertex.y) * triplanar_scale;
vec2 uv_y = vec2(-v_vertex.x * axis_sign.y, v_vertex.z) * triplanar_scale;
vec2 uv_z = vec2( v_vertex.x * axis_sign.z, v_vertex.y) * triplanar_scale;

vec3 albedo_x = texture(texture_albedo, vec3(uv_x, base_mat_id)).rgb;
vec3 albedo_y = texture(texture_albedo, vec3(uv_y, base_mat_id)).rgb;
vec3 albedo_z = texture(texture_albedo, vec3(uv_z, base_mat_id)).rgb;
vec3 albedo_final = (albedo_x * blend.x + albedo_y * blend.y + albedo_z * blend.z) * tint;

vec3 orm_x = texture(texture_orm, vec3(uv_x, base_mat_id)).rgb;
vec3 orm_y = texture(texture_orm, vec3(uv_y, base_mat_id)).rgb;
vec3 orm_z = texture(texture_orm, vec3(uv_z, base_mat_id)).rgb;
vec3 orm_final = orm_x * blend.x + orm_y * blend.y + orm_z * blend.z;

vec3 normal_x = normalize(texture(texture_normal, vec3(uv_x, base_mat_id)).rgb * 2.0 - 1.0);
vec3 normal_y = normalize(texture(texture_normal, vec3(uv_y, base_mat_id)).rgb * 2.0 - 1.0);
vec3 normal_z = normalize(texture(texture_normal, vec3(uv_z, base_mat_id)).rgb * 2.0 - 1.0);

vec3 base_normal_x = vec3(
    -triplanar_normal.z * axis_sign.x,
    triplanar_normal.y,
    triplanar_normal.x * axis_sign.x
);
vec3 base_normal_y = vec3(
    -triplanar_normal.x * axis_sign.y,
    triplanar_normal.z,
    triplanar_normal.y * axis_sign.y
);
vec3 base_normal_z = vec3(
    triplanar_normal.x * axis_sign.z,
    triplanar_normal.y,
    triplanar_normal.z * axis_sign.z
);

normal_x = blend_rnm(base_normal_x, normal_x);
normal_y = blend_rnm(base_normal_y, normal_y);
normal_z = blend_rnm(base_normal_z, normal_z);

vec3 normal_x_ws = vec3(
    normal_x.z * axis_sign.x,
    normal_x.y,
    -normal_x.x * axis_sign.x
);
vec3 normal_y_ws = vec3(
    -normal_y.x * axis_sign.y,
    normal_y.z * axis_sign.y,
    normal_y.y
);
vec3 normal_z_ws = vec3(
    normal_z.x * axis_sign.z,
    normal_z.y,
    normal_z.z * axis_sign.z
);

vec3 normal_final = normalize(normal_x_ws * blend.x + normal_y_ws * blend.y + normal_z_ws * blend.z);

mat3 model_rotation = mat3(
    normalize(MODEL_MATRIX[0].xyz),
    normalize(MODEL_MATRIX[1].xyz),
    normalize(MODEL_MATRIX[2].xyz)
);
vec3 normal_world = model_rotation * normal_final;
vec3 normal_view = normalize((VIEW_MATRIX * vec4(normal_world, 0.0)).xyz);

ALBEDO = albedo_final;
NORMAL = normal_view;
AO = orm_final.r;
ROUGHNESS = orm_final.g;
METALLIC = orm_final.b;

}

It’s suboptimaly indented :smiley:

Too many instructions and texture lookups can slow down your shader code. The fragment shader is generally executed many more times than the vertex shader … similar to a parallel version of :

for each triangle

--for each vertex

--for each fragment

I don’t think there are too many instructions. It’s basically standard triplanar mapping. You need 3 texture lookup per texture and with 3 standard textures (albedo, normal and orm) there’s no way around 9 lookups.

Some micro-optimizations could probably be made but I wouldn’t worry about it unless there’s a solid proof that the shader doesn’t perform acceptably on the target hardware.

The indentations didnt transfer over the copy paste for some reason, it is indented properly in the actual file.

You could implement biplanar mapping to cut down texture reads by 33%, but it increases ALU (computation) usage and has a slight negative impact on quality. It’s often faster (especially on bandwidth-constrained GPUs like mobile and integrated graphics), but you should benchmark it regardless.