Skip to content

Instantly share code, notes, and snippets.

@majikayogames
Last active April 19, 2025 11:31
Show Gist options
  • Save majikayogames/94ac6c76650a609e4db09febb82ab197 to your computer and use it in GitHub Desktop.
Save majikayogames/94ac6c76650a609e4db09febb82ab197 to your computer and use it in GitHub Desktop.
A weapon view model clipping prevention shader and function to apply the shader to your models in Godot.
## CC0 license use this function wherever you want no need to credit.
## Call this function on any node to apply the weapon_clip_and_fov_shader.gdshader to all meshes within it.
func apply_clip_and_fov_shader_to_view_model(node3d : Node3D, fov_or_negative_for_unchanged = -1.0):
var all_mesh_instances = node3d.find_children("*", "MeshInstance3D")
if node3d is MeshInstance3D:
all_mesh_instances.push_back(node3d)
for mesh_instance in all_mesh_instances:
var mesh = mesh_instance.mesh
# Important to turn shadow casting off for view model or will cause issues with both
# view model, casting shadows on itself once unclipped, & also will look weird casting on world.
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
for surface_idx in mesh.get_surface_count():
var base_mat = mesh.surface_get_material(surface_idx)
if not base_mat is BaseMaterial3D: continue
var weapon_shader_material := ShaderMaterial.new()
weapon_shader_material.shader = preload("res://FPSController/weapon_manager/weapon_clip_and_fov_shader.gdshader")
weapon_shader_material.set_shader_parameter("texture_albedo", base_mat.albedo_texture)
weapon_shader_material.set_shader_parameter("texture_metallic", base_mat.metallic_texture)
weapon_shader_material.set_shader_parameter("texture_roughness", base_mat.roughness_texture)
weapon_shader_material.set_shader_parameter("texture_normal", base_mat.normal_texture)
weapon_shader_material.set_shader_parameter("albedo", base_mat.albedo_color)
weapon_shader_material.set_shader_parameter("metallic", base_mat.metallic)
weapon_shader_material.set_shader_parameter("specular", base_mat.metallic_specular)
weapon_shader_material.set_shader_parameter("roughness", base_mat.roughness)
weapon_shader_material.set_shader_parameter("viewmodel_fov", fov_or_negative_for_unchanged)
var tex_channels = { 0: Vector4(1., 0., 0., 0.), 1: Vector4(0., 1., 0., 0.), 2: Vector4(0., 0., 1., 0.), 3: Vector4(1., 0., 0., 1.), 4: Vector4() }
weapon_shader_material.set_shader_parameter("metallic_texture_channel", tex_channels[base_mat.metallic_texture_channel])
mesh.surface_set_material(surface_idx, weapon_shader_material)
// NOTE: Shader automatically converted from Godot Engine 4.2.1.stable's StandardMaterial3D.
shader_type spatial;
render_mode blend_mix,depth_draw_opaque,cull_disabled,diffuse_burley,specular_schlick_ggx;
uniform vec4 albedo : source_color = vec4(1., 1., 1., 1.);
uniform sampler2D texture_albedo : source_color,filter_linear_mipmap,repeat_enable;
uniform float point_size : hint_range(0,128) = 1.0;
uniform float roughness : hint_range(0,1) = 1.0;
uniform sampler2D texture_metallic : hint_default_white,filter_linear_mipmap,repeat_enable;
uniform vec4 metallic_texture_channel = vec4(0., 0., 0., 0.);
uniform sampler2D texture_roughness : hint_roughness_g,filter_linear_mipmap,repeat_enable;
uniform float specular = 0.5;
uniform float metallic = 1.0;
uniform sampler2D texture_normal : hint_roughness_normal,filter_linear_mipmap,repeat_enable;
uniform float normal_scale : hint_range(-16,16) = 1.0;
uniform vec3 uv1_scale = vec3(1., 1., 1.);
uniform vec3 uv1_offset = vec3(0., 0., 0.);
uniform vec3 uv2_scale = vec3(1., 1., 1.);
uniform vec3 uv2_offset = vec3(0., 0., 0.);
uniform float viewmodel_fov = 75.0f;
void vertex() {
UV=UV*uv1_scale.xy+uv1_offset.xy;
/* VIEW MODEL Z CLIP FIX CODE */
if (viewmodel_fov > 0.0) {
float onetanfov = 1.0f / tan(0.5f * (viewmodel_fov * PI / 180.0f));
float aspect = VIEWPORT_SIZE.x / VIEWPORT_SIZE.y;
// Optional: Modify projection matrix to change the FOV
PROJECTION_MATRIX[1][1] = -onetanfov;
PROJECTION_MATRIX[0][0] = onetanfov / aspect;
}
POSITION = PROJECTION_MATRIX * MODELVIEW_MATRIX * vec4(VERTEX.xyz, 1.0);
// Mix vertex Z super close to clip plane, still maintaining some distance
//POSITION.z = mix(POSITION.z, 0.0, 0.999); // Old version. Mix towards 0.0 (near z plane)
// Above doesn't work for Godot 4.3 Z was reversed: https://godotengine.org/article/introducing-reverse-z/
// Now we need to mix towards 1.0, but in clip space division by POSITION.w still hasn't happened.
// So we mix z towards POSITION.w so that after perspective divsion, it will come out to 1.0 (near buffer since Z is reversed)
POSITION.z = mix(POSITION.z, POSITION.w, 0.9); // Could be higher mix val like 0.999 but I found 0.9 prevented 99% clipping w/ less artifacts than 0.999.
/* END VIEW MODEL Z CLIP FIX CODE */
}
// POSITION.xyz = POSITION.xyz / POSITION.w;
void fragment() {
// https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/spatial_shader.html#fragment-built-ins
// in vec3 VERTEX
// Vertex that comes from vertex function (default, in view space).
// You can also do the depth mix in fragment shader, but I found it to cause more artifacts.
//vec4 clipSpace = PROJECTION_MATRIX * vec4(VERTEX, 1.0);
//vec3 ndc = clipSpace.xyz / clipSpace.w;
//DEPTH = mix((ndc.z + 1.0) / 2.0, 1.0, 0.9);
vec2 base_uv = UV;
vec4 albedo_tex = texture(texture_albedo,base_uv);
ALBEDO = albedo.rgb * albedo_tex.rgb;
float metallic_tex = dot(texture(texture_metallic,base_uv),metallic_texture_channel);
METALLIC = metallic_tex * metallic;
vec4 roughness_texture_channel = vec4(0.0,1.0,0.0,0.0);
float roughness_tex = dot(texture(texture_roughness,base_uv),roughness_texture_channel);
ROUGHNESS = roughness_tex * roughness;
SPECULAR = specular;
NORMAL_MAP = texture(texture_normal,base_uv).rgb;
NORMAL_MAP_DEPTH = normal_scale;
}
@sivert-io
Copy link

I had a problem getting the FOV change to work, so I updated the FOV logic from this:

if (viewmodel_fov > 0.0) {
    float onetanfov = 1.0f / tan(0.5f * (viewmodel_fov * PI / 180.0f));
    float aspect = VIEWPORT_SIZE.x / VIEWPORT_SIZE.y;
    PROJECTION_MATRIX[1][1] = -onetanfov;
    PROJECTION_MATRIX[0][0] = onetanfov / aspect;
}
POSITION = PROJECTION_MATRIX * MODELVIEW_MATRIX * vec4(VERTEX.xyz, 1.0);

to this:

float scale = 1.0 / tan(FOV * 0.5 * PI / -180.0);
PROJECTION_MATRIX[0][0] = scale / (-VIEWPORT_SIZE.x / VIEWPORT_SIZE.y);
PROJECTION_MATRIX[1][1] = scale;

POSITION = PROJECTION_MATRIX * MODELVIEW_MATRIX * vec4(VERTEX.xyz, 1.0);

To be honest I am not sure why my method works. Here's the entire script for the lazy ones:

shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_disabled, diffuse_burley, specular_schlick_ggx;

uniform vec4 albedo : source_color = vec4(1., 1., 1., 1.);
uniform sampler2D texture_albedo : source_color, filter_linear_mipmap, repeat_enable;
uniform float point_size : hint_range(0, 128) = 1.0;
uniform float roughness : hint_range(0, 1) = 1.0;
uniform sampler2D texture_metallic : hint_default_white, filter_linear_mipmap, repeat_enable;
uniform vec4 metallic_texture_channel = vec4(0., 0., 0., 0.);
uniform sampler2D texture_roughness : hint_roughness_g, filter_linear_mipmap, repeat_enable;
uniform float specular = 0.5;
uniform float metallic = 1.0;
uniform sampler2D texture_normal : hint_roughness_normal, filter_linear_mipmap, repeat_enable;
uniform float normal_scale : hint_range(-16, 16) = 1.0;

uniform vec3 uv1_scale = vec3(1., 1., 1.);
uniform vec3 uv1_offset = vec3(0., 0., 0.);
uniform vec3 uv2_scale = vec3(1., 1., 1.);
uniform vec3 uv2_offset = vec3(0., 0., 0.);

uniform float FOV : hint_range(20, 120) = 68; // FOV handling from the first shader

void vertex() {
    // Apply UV scaling and offset
    UV = UV * uv1_scale.xy + uv1_offset.xy;

    // Compute the projection matrix based on the FOV
    float scale = 1.0 / tan(FOV * 0.5 * PI / -180.0);
    PROJECTION_MATRIX[0][0] = scale / (-VIEWPORT_SIZE.x / VIEWPORT_SIZE.y);
    PROJECTION_MATRIX[1][1] = scale;

    // Apply the standard transformation
    POSITION = PROJECTION_MATRIX * MODELVIEW_MATRIX * vec4(VERTEX.xyz, 1.0);
}

void fragment() {
    vec2 base_uv = UV;

    // Base albedo and texture
    vec4 albedo_tex = texture(texture_albedo, base_uv);
    ALBEDO = albedo.rgb * albedo_tex.rgb;

    // Metallic and texture
    float metallic_tex = dot(texture(texture_metallic, base_uv), metallic_texture_channel);
    METALLIC = metallic_tex * metallic;

    // Roughness and texture
    vec4 roughness_texture_channel = vec4(0.0, 1.0, 0.0, 0.0);
    float roughness_tex = dot(texture(texture_roughness, base_uv), roughness_texture_channel);
    ROUGHNESS = roughness_tex * roughness;

    // Specular
    SPECULAR = specular;

    // Normal mapping
    NORMAL_MAP = texture(texture_normal, base_uv).rgb;
    NORMAL_MAP_DEPTH = normal_scale;
}

@SaverioMartiradonna
Copy link

SaverioMartiradonna commented Apr 19, 2025

void vertex() {
    // Apply UV scaling and offset
    UV = UV * uv1_scale.xy + uv1_offset.xy;

    // Compute the projection matrix based on the FOV
    float scale = 1.0 / tan(FOV * 0.5 * PI / -180.0);
    PROJECTION_MATRIX[0][0] = scale / (-VIEWPORT_SIZE.x / VIEWPORT_SIZE.y);
    PROJECTION_MATRIX[1][1] = scale;

    // Apply the standard transformation
    POSITION = PROJECTION_MATRIX * MODELVIEW_MATRIX * vec4(VERTEX.xyz, 1.0);
}

viewmodel clips. does not work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment