Vertex Animation Texture (uses OpenVAT output)
Shader to read and display output from the OpenVAT plugin for Blender. (not affiliated.)
This is for single assets only and does not apply out of the box to mutli-instance meshes. Please reference OpenVAT for Godot for that use.
Referenced AntzGames’ OpenVAT, Michael Harmon’s visual shader, and Stoyan Dimitrov’s article to create this.
- Allows for Front, Back, and Double-sided faces to cover any use case you may have for your VAT meshes.
- Allows for reading UV1 or UV2 for the VAT texture depending on your output mesh.
- Minimized texture reads in the Vertex movement as much as I could.
- Fully PBR compliant.
- Reads a channel-packed ORM detail map to reduce texture samples further.
- Default code connects to Occlusion, Metallic, and Roughness.
- Select which channel your maps are in with the color selector. (This is for user flexibility. Please customize code to optimize this to your needs!)
- Frame Length of your OpenVAT texture is calculated automatically.
- The Min Bounce and Max Bounds must be input manually from the accompanying JSON file with every OpenVAT export. (automating these requires extra asset types / import scripts)
Shader code
shader_type spatial;
render_mode cull_disabled; // Prevents GPU hardware culling so shader can handle it
/*
For use with models exported from Blender using the OpenVAT plugin.
Expects:
Vertex Normals - Packed (Math gets too complex to include a toggle. Making a separate shader recommended if normals are not needed.)
Use Single Row - On
Export Model - On
Model Format - glTF Binary. (Unsure why the otherse do not work)
Ensure .exr file is set to Lossless compression and MipMaps - Off.
*/
// --- Uniforms ---
group_uniforms openVAT_inputs;
/** OpenVAT texture exported from Blender plugin. */
uniform sampler2D vertex_animation_texture : repeat_disable;
/** Min values from the .json file included with OpenVAT exports. */
uniform vec3 min_bounds;
/** Max values from the .json file included with OpenVAT exports. */
uniform vec3 max_bounds;
/** Plays animation automatically. Turn off to control manually via keyframes. */
uniform bool autoplay = true;
/**
Autoplay speed in frames per second. Defaults to 30.
[b]Blender default is 24. May require adjustment either in Blender or Here to match speed.[b]
*/
uniform float autoplay_speed = 30.0;
/** Frame to display if animating manually. */
uniform float frame = 1.0;
/** Enable this if the asset does not have 2 UV sets. */
uniform bool use_uv_1 = false;
/** Cull Mode, editable here for flexibility.
0: Backface Cull
1: Frontface Cull
2: Cull disabled (double sided rendering)*/
uniform int cull_mode : hint_enum("Back:0", "Front:1", "Disabled:2") = 0;
group_uniforms;
group_uniforms Albedo_map;
uniform sampler2D albedo_map : source_color, hint_default_white;
uniform vec4 albedo_tint : source_color = vec4(1.0);
group_uniforms;
group_uniforms ORM_inputs;
/**
[b]ORM texture.[/b] Standardized detail maps for PBR materials. Ignore, remap, or remove as needed.
[b]O[/b]cclusion (Ambient)
[b]R[/b]oughness
[b]M[/b]etallic
*/
uniform sampler2D orm_map : hint_default_white;
/** Select channel used by Occlusion map. R G [b]or[/b] B, combining will have strange results */
uniform vec4 occlusion_channel : source_color = vec4(1.0, 0.0, 0.0, 1.0);
/** Strength of Occlusion*/
uniform float occlusion : hint_range(0.0, 1.0) = 1.0;
/** Select channel used by Roughness map. R G [b]or[/b] B, combining will have strange results */
uniform vec4 roughness_channel : source_color = vec4(0.0, 1.0, 0.0, 1.0);
/** Strength of Roughness*/
uniform float roughness : hint_range(0.0, 1.0) = 1.0;
/** Select channel used by Metallic map. R G [b]or[/b] B, combining will have strange results */
uniform vec4 metallic_channel : source_color = vec4(0.0, 0.0, 1.0, 1.0);
/** Strength of Metallic*/
uniform float metallic : hint_range(0.0, 1.0) = 1.0;
/** Strength of Specular (No Map Included)*/
uniform float specular : hint_range(0.0, 1.0) = 0.5;
group_uniforms;
group_uniforms Normal_map;
uniform sampler2D normal_map : hint_roughness_normal;
uniform float normal_map_scale : hint_range(-10.0, 10.0) = 1.0;
float remapFloat(float value, float input_min, float input_max, float output_min, float output_max){
// Code to replicate the remap feature from Visual Shader.
float _input_range = input_max - input_min;
float _output_range = output_max - output_min;
return output_min + _output_range * ((value - input_min) / _input_range);
}
void vertex() {
// Animation Timing
// ------
// Divided by 2. since bottom half of texture is the normal data.
// Second argument (0) is MipMap level. These VATs use no mipmaps so always 0.
int total_frames = textureSize(vertex_animation_texture, 0).y / 2;
// Checks if Autoplaying is checked
float play_frame = autoplay ? (TIME * autoplay_speed) : frame;
// Loops the current frame counter so it stays between 0 and total_frames - 1
float current_frame = mod(play_frame, float(total_frames));
// Compute UV Coordinates
// ------
// samples pixel from the center, not the edge. Avoid unwanted bleeding into normal data pixels at the end of the loop.
float center_pixel = current_frame - 0.5;
float vat_y = center_pixel / float(total_frames);
vat_y = clamp(vat_y * 0.5, 0.0, 0.5);
vec2 vat_uv = vec2((use_uv_1 ? UV.x : UV2.x), vat_y);
// Vertex Position Displacement
// ------
vec4 vat_pos_raw = texture(vertex_animation_texture, vat_uv);
// Must go through some remapping to convert Blender to Godot coordinates
vec3 offset_values = vec3(
remapFloat(vat_pos_raw.r, 0.0, 1.0, min_bounds.x, max_bounds.x),
remapFloat(vat_pos_raw.b, 0.0, 1.0, min_bounds.z, max_bounds.z),
remapFloat(vat_pos_raw.g, 1.0, 0.0, min_bounds.y, max_bounds.y)
);
VERTEX += offset_values;
// Normals. 0.5 added to UV.y to shift it down by 1/2 the map - to the normals section.
vec4 vat_norm_raw = texture(vertex_animation_texture, vat_uv + vec2(0.0, 0.5));
vec3 decoded_normals = vec3(
remapFloat(vat_norm_raw.r, 0.0, 1.0, -1.0, 1.0),
remapFloat(vat_norm_raw.b, 0.0, 1.0, -1.0, 1.0),
remapFloat(vat_norm_raw.g, 1.0, 0.0, -1.0, 1.0)
);
NORMAL = normalize(decoded_normals);
// Calculate Tangent and Binormal from computed Normal
TANGENT = normalize(vec3(abs(NORMAL.y) + abs(NORMAL.z), 0.0, -abs(NORMAL.x)));
BINORMAL = normalize(vec3(0.0, abs(NORMAL.x) + abs(NORMAL.z), -abs(NORMAL.y)));
}
void fragment() {
// Cull Mode
if (cull_mode == 0 && !FRONT_FACING) {
discard; // Cull back faces
}
else if (cull_mode == 1 && FRONT_FACING) {
discard; // Cull front faces
}
// Albedo
ALBEDO = texture(albedo_map, UV).rgb * albedo_tint.rgb;
// ORM maps (Occlusion, Roughness, Metallic) Packing
vec4 orm_tex = texture(orm_map, UV);
AO = occlusion * dot(orm_tex.rgb, occlusion_channel.rgb);
ROUGHNESS = roughness * dot(orm_tex.rgb, roughness_channel.rgb);
METALLIC = metallic * dot(orm_tex.rgb, metallic_channel.rgb);
// Specular
SPECULAR = specular;
// Normal Map
NORMAL_MAP = texture(normal_map, UV).rgb;
NORMAL_MAP_DEPTH = normal_map_scale;
}



