/* * Tuxánci 2 - A first person shooter * Copyright (C) 2025-2026 Connor Thomson * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ struct light_uniforms { float3 direction; float shadow_texel_size; float shadow_minimum_bias; float shadow_maximum_bias; float shadow_strength; float padding; }; [[vk::binding(0, 3)]] ConstantBuffer light : register(b0, space3); [[vk::binding(0, 2)]] Texture2D shadow_map : register(t0, space2); [[vk::binding(1, 2)]] SamplerComparisonState shadow_sampler : register(s0, space2); struct FSInput { float3 normal : TEXCOORD0; float3 world_position : TEXCOORD1; float3 camera_position : TEXCOORD2; float4 light_space_position : TEXCOORD3; }; struct FSOutput { float4 out_color : SV_Target0; }; static const float ambient_strength = 0.22; static const float diffuse_strength = 0.78; static const float specular_strength = 0.6; static const float shininess = 32.0; static const int shadow_pcf_radius = 1; float shadow_visibility(float4 light_space_position, float normal_dot_light) { float3 projected = light_space_position.xyz / light_space_position.w; if (projected.z < 0.0 || projected.z > 1.0) { return 1.0; } float2 uv = float2(0.5 + 0.5 * projected.x, 0.5 - 0.5 * projected.y); if (any(uv < 0.0) || any(uv > 1.0)) { return 1.0; } float bias = max(light.shadow_maximum_bias * (1.0 - normal_dot_light), light.shadow_minimum_bias); float reference = projected.z - bias; float visibility = 0.0; float taps = 0.0; for (int y = -shadow_pcf_radius; y <= shadow_pcf_radius; y++) { for (int x = -shadow_pcf_radius; x <= shadow_pcf_radius; x++) { float2 offset = float2(x, y) * light.shadow_texel_size; visibility += shadow_map.SampleCmpLevelZero(shadow_sampler, uv + offset, reference); taps += 1.0; } } return visibility / taps; } FSOutput main(FSInput input) { float3 light_direction = normalize(light.direction); float3 normal = normalize(input.normal); float3 view_direction = normalize(input.camera_position - input.world_position); float3 halfway_direction = normalize(light_direction + view_direction); float normal_dot_light = dot(normal, light_direction); float diffuse = max(normal_dot_light, 0.0); float specular = diffuse > 0.0 ? pow(max(dot(normal, halfway_direction), 0.0), shininess) : 0.0; float visibility = shadow_visibility(input.light_space_position, normal_dot_light); visibility = lerp(1.0, visibility, light.shadow_strength); float shade = saturate(ambient_strength + visibility * (diffuse * diffuse_strength + specular * specular_strength)); FSOutput output; output.out_color = float4(shade, shade, shade, 1.0); return output; }