From 251b611fef77533082165bab0b88eefa05b49fbe Mon Sep 17 00:00:00 2001 From: Connor Thomson Date: Sat, 12 Sep 2026 14:16:55 -0700 Subject: Add simple shadows --- include/files.h | 8 ++ include/model.h | 2 + include/screen.h | 1 + include/screens/main_menu.h | 1 + include/shadow.h | 41 +++++++++ include/ta_math.h | 7 +- include/teapot.h | 1 + shaders/model.frag.slang | 65 ++++++++++++-- shaders/model.vert.slang | 25 +++--- shaders/model_wireframe.frag.slang | 2 - shaders/shadow.frag.slang | 20 +++++ shaders/shadow.vert.slang | 38 ++++++++ src/gpu.c | 7 ++ src/main.c | 3 + src/model.c | 179 ++++++++++++++++++++++++++----------- src/screen.c | 10 +++ src/screens/main_menu.c | 4 + src/shadow.c | 152 +++++++++++++++++++++++++++++++ src/ta_math.c | 18 ++++ src/teapot.c | 5 ++ 20 files changed, 516 insertions(+), 73 deletions(-) create mode 100644 include/shadow.h create mode 100644 shaders/shadow.frag.slang create mode 100644 shaders/shadow.vert.slang create mode 100644 src/shadow.c diff --git a/include/files.h b/include/files.h index 8c68d38..ebd57b4 100644 --- a/include/files.h +++ b/include/files.h @@ -55,4 +55,12 @@ extern unsigned char *file_model_wireframe_frag_slang_spv_start; extern unsigned char *file_model_wireframe_frag_slang_spv_end; extern unsigned int file_model_wireframe_frag_slang_spv_size; +extern unsigned char *file_shadow_vert_slang_spv_start; +extern unsigned char *file_shadow_vert_slang_spv_end; +extern unsigned int file_shadow_vert_slang_spv_size; + +extern unsigned char *file_shadow_frag_slang_spv_start; +extern unsigned char *file_shadow_frag_slang_spv_end; +extern unsigned int file_shadow_frag_slang_spv_size; + #endif // TA_FILES_H \ No newline at end of file diff --git a/include/model.h b/include/model.h index 47364ba..56a8c75 100644 --- a/include/model.h +++ b/include/model.h @@ -38,6 +38,7 @@ typedef struct ta_model_mesh { typedef struct ta_model { SDL_GPUGraphicsPipeline *pipeline; SDL_GPUGraphicsPipeline *wireframe_pipeline; + SDL_GPUGraphicsPipeline *shadow_pipeline; SDL_GPUBuffer *vertex_buffer; ta_model_mesh mesh; Uint32 vertex_count; @@ -51,6 +52,7 @@ void ta_model_free(ta_model_mesh *mesh); bool ta_model_init(ta_model *model, SDL_GPUDevice *device, const float positions[][3], size_t position_count, const unsigned int faces[][TA_MODEL_DIMENSION], size_t face_count, SDL_GPUTextureFormat color_format, SDL_GPUTextureFormat depth_format, SDL_GPUSampleCount sample_count); void ta_model_upload(ta_model *model, SDL_GPUCommandBuffer *command_buffer, SDL_GPUDevice *device); void ta_model_render(const ta_model *model, SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass, int window_width, int window_height); +void ta_model_render_shadow(const ta_model *model, SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass); void ta_model_destroy(ta_model *model, SDL_GPUDevice *device); #endif // TA_MODEL_H \ No newline at end of file diff --git a/include/screen.h b/include/screen.h index dffbf95..2d61a78 100644 --- a/include/screen.h +++ b/include/screen.h @@ -31,5 +31,6 @@ extern ta_screens ta_screen_current; void ta_screen_update(void); void ta_screen_upload(SDL_GPUCommandBuffer *command_buffer); void ta_screen_render(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass); +void ta_screen_render_shadow(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass); #endif // TA_SCREEN_H \ No newline at end of file diff --git a/include/screens/main_menu.h b/include/screens/main_menu.h index a49e8b0..7a449d7 100644 --- a/include/screens/main_menu.h +++ b/include/screens/main_menu.h @@ -24,5 +24,6 @@ void ta_screens_main_menu_update(void); void ta_screens_main_menu_upload(SDL_GPUCommandBuffer *command_buffer); void ta_screens_main_menu_render(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass); +void ta_screens_main_menu_render_shadow(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass); #endif // TA_SCREENS_MAIN_MENU_H \ No newline at end of file diff --git a/include/shadow.h b/include/shadow.h new file mode 100644 index 0000000..0087ef4 --- /dev/null +++ b/include/shadow.h @@ -0,0 +1,41 @@ +/* + * 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 . + */ + +#ifndef TA_SHADOW_H +#define TA_SHADOW_H + +#include "ta_sdl.h" + +#define TA_SHADOW_DIMENSION 3 +#define TA_SHADOW_MAP_SIZE 2048u +#define TA_SHADOW_FORMAT SDL_GPU_TEXTUREFORMAT_D32_FLOAT + +extern SDL_GPUTexture *ta_shadow_texture; +extern SDL_GPUSampler *ta_shadow_sampler; +extern float ta_shadow_light_direction[TA_SHADOW_DIMENSION]; +extern float ta_shadow_normal_offset; +extern float ta_shadow_minimum_bias; +extern float ta_shadow_maximum_bias; +extern float ta_shadow_strength; + +void ta_shadow_init(void); +void ta_shadow_get_light_view_projection(float *matrix); +SDL_GPURenderPass *ta_shadow_begin_render_pass(SDL_GPUCommandBuffer *command_buffer); +void ta_shadow_shutdown(void); + +#endif // TA_SHADOW_H diff --git a/include/ta_math.h b/include/ta_math.h index 4852fc2..58751ea 100644 --- a/include/ta_math.h +++ b/include/ta_math.h @@ -21,9 +21,12 @@ #include -#define PI 3.14159265358979323846 -#define CIRCLE_DEG 360.0 +#define PI 3.14159265358979323846 +#define CIRCLE_DEG 360.0 +#define TA_MATRIX_ELEMENTS 16 double ta_rad(double degrees); +void ta_matrix_identity(float *matrix); +void ta_matrix_multiply(float *result, const float *left, const float *right); #endif // TA_MATH_H \ No newline at end of file diff --git a/include/teapot.h b/include/teapot.h index 99be7a6..a1af76e 100644 --- a/include/teapot.h +++ b/include/teapot.h @@ -24,6 +24,7 @@ void ta_teapot_init(void); void ta_teapot_upload(SDL_GPUCommandBuffer *command_buffer); void ta_teapot_render(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass); +void ta_teapot_render_shadow(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass); void ta_teapot_shutdown(void); #endif // TA_TEAPOT_H \ No newline at end of file diff --git a/shaders/model.frag.slang b/shaders/model.frag.slang index 4684db0..ac12d0e 100644 --- a/shaders/model.frag.slang +++ b/shaders/model.frag.slang @@ -16,10 +16,29 @@ * 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; + float3 normal : TEXCOORD0; + float3 world_position : TEXCOORD1; + float3 camera_position : TEXCOORD2; + float4 light_space_position : TEXCOORD3; }; struct FSOutput { @@ -30,17 +49,45 @@ 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(float3(-0.45, 0.75, 0.55)); + 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 diffuse = max(dot(normal, light_direction), 0.0); - float specular = diffuse > 0.0 ? pow(max(dot(normal, halfway_direction), 0.0), shininess) : 0.0; - - float shade = saturate(ambient_strength + diffuse * diffuse_strength + specular * specular_strength); + 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; diff --git a/shaders/model.vert.slang b/shaders/model.vert.slang index 380ff37..3ddb39d 100644 --- a/shaders/model.vert.slang +++ b/shaders/model.vert.slang @@ -19,8 +19,9 @@ struct model_uniforms { float4x4 mvp; float4x4 model; + float4x4 light_view_projection; float3 camera_position; - float padding; + float shadow_normal_offset; }; [[vk::binding(0, 1)]] @@ -32,18 +33,22 @@ struct VSInput { }; struct VSOutput { - float4 position : SV_Position; - float3 normal : TEXCOORD0; - float3 world_position : TEXCOORD1; - float3 camera_position : TEXCOORD2; + float4 position : SV_Position; + float3 normal : TEXCOORD0; + float3 world_position : TEXCOORD1; + float3 camera_position : TEXCOORD2; + float4 light_space_position : TEXCOORD3; }; VSOutput main(VSInput input) { VSOutput output; - float4 world_position = mul(uniforms.model, float4(input.position, 1.0)); - output.position = mul(uniforms.mvp, float4(input.position, 1.0)); - output.normal = normalize(mul((float3x3)uniforms.model, input.normal)); - output.world_position = world_position.xyz; - output.camera_position = uniforms.camera_position; + float4 world_position = mul(uniforms.model, float4(input.position, 1.0)); + float3 world_normal = normalize(mul((float3x3)uniforms.model, input.normal)); + output.position = mul(uniforms.mvp, float4(input.position, 1.0)); + output.normal = world_normal; + output.world_position = world_position.xyz; + output.camera_position = uniforms.camera_position; + float3 offset_position = world_position.xyz + world_normal * uniforms.shadow_normal_offset; + output.light_space_position = mul(uniforms.light_view_projection, float4(offset_position, 1.0)); return output; } \ No newline at end of file diff --git a/shaders/model_wireframe.frag.slang b/shaders/model_wireframe.frag.slang index bac0966..10c2a52 100644 --- a/shaders/model_wireframe.frag.slang +++ b/shaders/model_wireframe.frag.slang @@ -20,8 +20,6 @@ struct FSOutput { float4 out_color : SV_Target0; }; -// Ignores geometry normals/lighting entirely; used with the LINE fill mode -// pipeline so only the mesh edges are drawn, in pure white. FSOutput main() { FSOutput output; output.out_color = float4(1.0, 1.0, 1.0, 1.0); diff --git a/shaders/shadow.frag.slang b/shaders/shadow.frag.slang new file mode 100644 index 0000000..4f895f1 --- /dev/null +++ b/shaders/shadow.frag.slang @@ -0,0 +1,20 @@ +/* + * 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 . + */ + +void main() { +} \ No newline at end of file diff --git a/shaders/shadow.vert.slang b/shaders/shadow.vert.slang new file mode 100644 index 0000000..1e56f46 --- /dev/null +++ b/shaders/shadow.vert.slang @@ -0,0 +1,38 @@ +/* + * 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 shadow_uniforms { + float4x4 light_model_view_projection; +}; + +[[vk::binding(0, 1)]] +ConstantBuffer uniforms : register(b0, space1); + +struct VSInput { + float3 position : POSITION0; +}; + +struct VSOutput { + float4 position : SV_Position; +}; + +VSOutput main(VSInput input) { + VSOutput output; + output.position = mul(uniforms.light_model_view_projection, float4(input.position, 1.0)); + return output; +} \ No newline at end of file diff --git a/src/gpu.c b/src/gpu.c index b8c0c7d..e6f83ce 100644 --- a/src/gpu.c +++ b/src/gpu.c @@ -25,6 +25,7 @@ #include "text.h" #include "logging.h" #include "screen.h" +#include "shadow.h" #define SPIRV_ENTRY_POINT "main" @@ -361,6 +362,12 @@ void ta_gpu_end_frame(void) { ta_gpu_upload_text(ta_gpu_command_buffer); ta_screen_upload(ta_gpu_command_buffer); + SDL_GPURenderPass *shadow_pass = ta_shadow_begin_render_pass(ta_gpu_command_buffer); + if (shadow_pass) { + ta_screen_render_shadow(ta_gpu_command_buffer, shadow_pass); + SDL_EndGPURenderPass(shadow_pass); + } + SDL_GPUColorTargetInfo color_target = { .texture = ta_gpu_msaa_texture, .load_op = SDL_GPU_LOADOP_CLEAR, diff --git a/src/main.c b/src/main.c index 4edac13..adbc5cc 100644 --- a/src/main.c +++ b/src/main.c @@ -35,6 +35,7 @@ #include "teapot.h" #include "mouse.h" #include "camera.h" +#include "shadow.h" static void ta_main_init(void) { ta_color_init(); @@ -44,6 +45,7 @@ static void ta_main_init(void) { ta_sdl_ttf_init(); ta_text_init(); ta_gpu_init(); + ta_shadow_init(); ta_teapot_init(); #ifdef ENABLE_AUDIO ta_audio_init(); @@ -71,6 +73,7 @@ void ta_main_exit(int code) { #endif // ENABLE_AUDIO ta_text_shutdown(); ta_teapot_shutdown(); + ta_shadow_shutdown(); ta_window_shutdown(); ta_sdl_ttf_shutdown(); ta_sdl_shutdown(); diff --git a/src/model.c b/src/model.c index 4171c21..2c78850 100644 --- a/src/model.c +++ b/src/model.c @@ -23,6 +23,7 @@ #include "camera.h" #include "files.h" #include "gpu.h" +#include "shadow.h" #include "ta_math.h" static bool ta_model_push_vertex(ta_model_mesh *mesh, size_t *capacity, const ta_model_vertex *vertex) { @@ -131,41 +132,39 @@ void ta_model_free(ta_model_mesh *mesh) { } typedef struct ta_model_uniforms { - float mvp[16]; // TODO: Use dynamic buffer - float model[16]; // TODO: Use dynamic buffer + float mvp[TA_MATRIX_ELEMENTS]; + float model[TA_MATRIX_ELEMENTS]; + float light_view_projection[TA_MATRIX_ELEMENTS]; float camera_position[TA_MODEL_DIMENSION]; - float padding; + float shadow_normal_offset; } ta_model_uniforms; -static SDL_GPUShader *ta_model_shader(SDL_GPUDevice *device, const void *code, size_t size, SDL_GPUShaderStage stage) { +typedef struct ta_model_light_uniforms { + float direction[TA_MODEL_DIMENSION]; + float shadow_texel_size; + float shadow_minimum_bias; + float shadow_maximum_bias; + float shadow_strength; + float padding; +} ta_model_light_uniforms; + +typedef struct ta_model_shadow_uniforms { + float light_model_view_projection[TA_MATRIX_ELEMENTS]; +} ta_model_shadow_uniforms; + +static SDL_GPUShader *ta_model_shader(SDL_GPUDevice *device, const void *code, size_t size, SDL_GPUShaderStage stage, Uint32 num_samplers, Uint32 num_uniform_buffers) { SDL_GPUShaderCreateInfo info = { .code = code, .code_size = size, .entrypoint = "main", .format = SDL_GPU_SHADERFORMAT_SPIRV, .stage = stage, - .num_uniform_buffers = stage == SDL_GPU_SHADERSTAGE_VERTEX ? 1 : 0, + .num_samplers = num_samplers, + .num_uniform_buffers = num_uniform_buffers, }; return ta_sdl_create_gpu_shader(device, &info); } -static void ta_model_identity(float *matrix) { - for (int index = 0; index < 16; index++) { - matrix[index] = index % 5 == 0 ? 1.0f : 0.0f; - } -} - -static void ta_model_multiply(float *result, const float *left, const float *right) { - float product[16]; - for (int column = 0; column < 4; column++) for (int row = 0; row < 4; row++) { - product[column * 4 + row] = 0.0f; - for (int inner = 0; inner < 4; inner++) { - product[column * 4 + row] += left[inner * 4 + row] * right[column * 4 + inner]; - } - } - memcpy(result, product, sizeof(product)); -} - bool ta_model_init(ta_model *model, SDL_GPUDevice *device, const float positions[][TA_MODEL_DIMENSION], size_t position_count, const unsigned int faces[][TA_MODEL_DIMENSION], size_t face_count, SDL_GPUTextureFormat color_format, SDL_GPUTextureFormat depth_format, SDL_GPUSampleCount sample_count) { memset(model, 0, sizeof(*model)); if (!ta_model_build(positions, position_count, faces, face_count, &model->mesh) || model->mesh.vertex_count == 0) { @@ -182,8 +181,8 @@ bool ta_model_init(ta_model *model, SDL_GPUDevice *device, const float positions return false; } model->vertex_count = (Uint32)model->mesh.vertex_count; - SDL_GPUShader *vertex_shader = ta_model_shader(device, file_model_vert_slang_spv_start, file_model_vert_slang_spv_size, SDL_GPU_SHADERSTAGE_VERTEX); - SDL_GPUShader *fragment_shader = ta_model_shader(device, file_model_frag_slang_spv_start, file_model_frag_slang_spv_size, SDL_GPU_SHADERSTAGE_FRAGMENT); + SDL_GPUShader *vertex_shader = ta_model_shader(device, file_model_vert_slang_spv_start, file_model_vert_slang_spv_size, SDL_GPU_SHADERSTAGE_VERTEX, 0, 1); + SDL_GPUShader *fragment_shader = ta_model_shader(device, file_model_frag_slang_spv_start, file_model_frag_slang_spv_size, SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); SDL_GPUVertexBufferDescription vertex_buffer = {.slot = 0, .pitch = sizeof(ta_model_vertex), .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX}; SDL_GPUVertexAttribute attributes[2] = { { @@ -232,15 +231,48 @@ bool ta_model_init(ta_model *model, SDL_GPUDevice *device, const float positions }; model->pipeline = ta_sdl_create_gpu_graphics_pipeline(device, &pipeline_info); - SDL_GPUShader *wireframe_fragment_shader = ta_model_shader(device, file_model_wireframe_frag_slang_spv_start, file_model_wireframe_frag_slang_spv_size, SDL_GPU_SHADERSTAGE_FRAGMENT); + SDL_GPUShader *wireframe_fragment_shader = ta_model_shader(device, file_model_wireframe_frag_slang_spv_start, file_model_wireframe_frag_slang_spv_size, SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 0); pipeline_info.fragment_shader = wireframe_fragment_shader; pipeline_info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_LINE; model->wireframe_pipeline = ta_sdl_create_gpu_graphics_pipeline(device, &pipeline_info); + SDL_GPUShader *shadow_vertex_shader = ta_model_shader(device, file_shadow_vert_slang_spv_start, file_shadow_vert_slang_spv_size, SDL_GPU_SHADERSTAGE_VERTEX, 0, 1); + SDL_GPUShader *shadow_fragment_shader = ta_model_shader(device, file_shadow_frag_slang_spv_start, file_shadow_frag_slang_spv_size, SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 0); + SDL_GPUGraphicsPipelineCreateInfo shadow_pipeline_info = { + .vertex_shader = shadow_vertex_shader, + .fragment_shader = shadow_fragment_shader, + .vertex_input_state = { + .vertex_buffer_descriptions = &vertex_buffer, + .num_vertex_buffers = 1, + .vertex_attributes = attributes, + .num_vertex_attributes = 1 + }, + .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + .rasterizer_state = { + .cull_mode = SDL_GPU_CULLMODE_NONE + }, + .depth_stencil_state = { + .compare_op = SDL_GPU_COMPAREOP_LESS, + .enable_depth_test = true, + .enable_depth_write = true + }, + .multisample_state = { + .sample_count = SDL_GPU_SAMPLECOUNT_1 + }, + .target_info = { + .num_color_targets = 0, + .depth_stencil_format = TA_SHADOW_FORMAT, + .has_depth_stencil_target = true + }, + }; + model->shadow_pipeline = ta_sdl_create_gpu_graphics_pipeline(device, &shadow_pipeline_info); + SDL_ReleaseGPUShader(device, vertex_shader); SDL_ReleaseGPUShader(device, fragment_shader); SDL_ReleaseGPUShader(device, wireframe_fragment_shader); - return model->pipeline != NULL && model->wireframe_pipeline != NULL; + SDL_ReleaseGPUShader(device, shadow_vertex_shader); + SDL_ReleaseGPUShader(device, shadow_fragment_shader); + return model->pipeline != NULL && model->wireframe_pipeline != NULL && model->shadow_pipeline != NULL; } void ta_model_upload(ta_model *model, SDL_GPUCommandBuffer *command_buffer, SDL_GPUDevice *device) { @@ -271,44 +303,48 @@ void ta_model_upload(ta_model *model, SDL_GPUCommandBuffer *command_buffer, SDL_ model->uploaded = true; } -void ta_model_render(const ta_model *model, SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass, int window_width, int window_height) { - if (!model->pipeline || !model->vertex_buffer || !model->uploaded) { - return; - } - ta_model_uniforms uniforms; - float view[16]; - float projection[16] = {0}; - float view_model[16]; - float rotation_x[16]; - float rotation_y[16]; - float rotation_z[16]; - float rotation_xy[16]; +static void ta_model_get_model_matrix(const ta_model *model, float *matrix) { + float rotation_x[TA_MATRIX_ELEMENTS]; + float rotation_y[TA_MATRIX_ELEMENTS]; + float rotation_z[TA_MATRIX_ELEMENTS]; + float rotation_xy[TA_MATRIX_ELEMENTS]; float cosine = cosf(model->rotation[0]); float sine = sinf(model->rotation[0]); - ta_model_identity(rotation_x); + ta_matrix_identity(rotation_x); rotation_x[5] = cosine; rotation_x[6] = sine; rotation_x[9] = -sine; rotation_x[10] = cosine; cosine = cosf(model->rotation[1]); sine = sinf(model->rotation[1]); - ta_model_identity(rotation_y); + ta_matrix_identity(rotation_y); rotation_y[0] = cosine; rotation_y[2] = -sine; rotation_y[8] = sine; rotation_y[10] = cosine; cosine = cosf(model->rotation[2]); sine = sinf(model->rotation[2]); - ta_model_identity(rotation_z); + ta_matrix_identity(rotation_z); rotation_z[0] = cosine; rotation_z[1] = sine; rotation_z[4] = -sine; rotation_z[5] = cosine; - ta_model_multiply(rotation_xy, rotation_y, rotation_x); - ta_model_multiply(uniforms.model, rotation_z, rotation_xy); - uniforms.model[12] = model->position[0]; - uniforms.model[13] = model->position[1]; - uniforms.model[14] = model->position[2]; + ta_matrix_multiply(rotation_xy, rotation_y, rotation_x); + ta_matrix_multiply(matrix, rotation_z, rotation_xy); + matrix[12] = model->position[0]; + matrix[13] = model->position[1]; + matrix[14] = model->position[2]; +} + +void ta_model_render(const ta_model *model, SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass, int window_width, int window_height) { + if (!model->pipeline || !model->vertex_buffer || !model->uploaded) { + return; + } + ta_model_uniforms uniforms; + float view[TA_MATRIX_ELEMENTS]; + float projection[TA_MATRIX_ELEMENTS] = {0}; + float view_model[TA_MATRIX_ELEMENTS]; + ta_model_get_model_matrix(model, uniforms.model); float aspect = (float)window_width / (float)window_height; float focal_length = 1.0f / tanf(ta_rad(ta_camera_current.fov * 0.5f)); float near_plane = 0.1f; @@ -319,15 +355,57 @@ void ta_model_render(const ta_model *model, SDL_GPUCommandBuffer *command_buffer projection[10] = far_plane / (near_plane - far_plane); projection[11] = -1.0f; projection[14] = near_plane * far_plane / (near_plane - far_plane); - ta_model_multiply(view_model, view, uniforms.model); - ta_model_multiply(uniforms.mvp, projection, view_model); + ta_matrix_multiply(view_model, view, uniforms.model); + ta_matrix_multiply(uniforms.mvp, projection, view_model); + ta_shadow_get_light_view_projection(uniforms.light_view_projection); uniforms.camera_position[0] = ta_camera_current.x; uniforms.camera_position[1] = ta_camera_current.y; uniforms.camera_position[2] = ta_camera_current.z; - uniforms.padding = 0.0f; - SDL_GPUGraphicsPipeline *pipeline = ta_gpu_enable_wireframe && model->wireframe_pipeline ? model->wireframe_pipeline : model->pipeline; + uniforms.shadow_normal_offset = ta_shadow_normal_offset; + bool wireframe = ta_gpu_enable_wireframe && model->wireframe_pipeline; + SDL_GPUGraphicsPipeline *pipeline = wireframe ? model->wireframe_pipeline : model->pipeline; SDL_PushGPUVertexUniformData(command_buffer, 0, &uniforms, sizeof(uniforms)); SDL_BindGPUGraphicsPipeline(render_pass, pipeline); + + if (!wireframe) { + ta_model_light_uniforms light = { + .direction = { + ta_shadow_light_direction[0], + ta_shadow_light_direction[1], + ta_shadow_light_direction[2] + }, + .shadow_texel_size = 1.0f / (float)TA_SHADOW_MAP_SIZE, + .shadow_minimum_bias = ta_shadow_minimum_bias, + .shadow_maximum_bias = ta_shadow_maximum_bias, + .shadow_strength = ta_shadow_strength, + }; + SDL_GPUTextureSamplerBinding shadow_binding = { + .texture = ta_shadow_texture, + .sampler = ta_shadow_sampler + }; + SDL_PushGPUFragmentUniformData(command_buffer, 0, &light, sizeof(light)); + SDL_BindGPUFragmentSamplers(render_pass, 0, &shadow_binding, 1); + } + + SDL_GPUBufferBinding binding = { + .buffer = model->vertex_buffer + }; + SDL_BindGPUVertexBuffers(render_pass, 0, &binding, 1); + SDL_DrawGPUPrimitives(render_pass, model->vertex_count, 1, 0, 0); +} + +void ta_model_render_shadow(const ta_model *model, SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass) { + if (!model->shadow_pipeline || !model->vertex_buffer || !model->uploaded) { + return; + } + ta_model_shadow_uniforms uniforms; + float model_matrix[TA_MATRIX_ELEMENTS]; + float light_view_projection[TA_MATRIX_ELEMENTS]; + ta_model_get_model_matrix(model, model_matrix); + ta_shadow_get_light_view_projection(light_view_projection); + ta_matrix_multiply(uniforms.light_model_view_projection, light_view_projection, model_matrix); + SDL_PushGPUVertexUniformData(command_buffer, 0, &uniforms, sizeof(uniforms)); + SDL_BindGPUGraphicsPipeline(render_pass, model->shadow_pipeline); SDL_GPUBufferBinding binding = { .buffer = model->vertex_buffer }; @@ -338,6 +416,7 @@ void ta_model_render(const ta_model *model, SDL_GPUCommandBuffer *command_buffer void ta_model_destroy(ta_model *model, SDL_GPUDevice *device) { SDL_ReleaseGPUGraphicsPipeline(device, model->pipeline); SDL_ReleaseGPUGraphicsPipeline(device, model->wireframe_pipeline); + SDL_ReleaseGPUGraphicsPipeline(device, model->shadow_pipeline); SDL_ReleaseGPUBuffer(device, model->vertex_buffer); ta_model_free(&model->mesh); memset(model, 0, sizeof(*model)); diff --git a/src/screen.c b/src/screen.c index 599e79e..ed76094 100644 --- a/src/screen.c +++ b/src/screen.c @@ -51,4 +51,14 @@ void ta_screen_render(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *r case NONE: break; } +} + +void ta_screen_render_shadow(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass) { + switch (ta_screen_current) { + case MAIN_MENU: + ta_screens_main_menu_render_shadow(command_buffer, render_pass); + break; + case NONE: + break; + } } \ No newline at end of file diff --git a/src/screens/main_menu.c b/src/screens/main_menu.c index 8ac0f43..18b9bff 100644 --- a/src/screens/main_menu.c +++ b/src/screens/main_menu.c @@ -29,4 +29,8 @@ void ta_screens_main_menu_upload(SDL_GPUCommandBuffer *command_buffer) { void ta_screens_main_menu_render(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass) { ta_teapot_render(command_buffer, render_pass); +} + +void ta_screens_main_menu_render_shadow(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass) { + ta_teapot_render_shadow(command_buffer, render_pass); } \ No newline at end of file diff --git a/src/shadow.c b/src/shadow.c new file mode 100644 index 0000000..2967674 --- /dev/null +++ b/src/shadow.c @@ -0,0 +1,152 @@ +/* + * 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 . + */ + +#include +#include "shadow.h" +#include "gpu.h" +#include "ta_math.h" + +#define TA_SHADOW_EXTENT 16.0f +#define TA_SHADOW_DISTANCE 30.0f +#define TA_SHADOW_NEAR 1.0f +#define TA_SHADOW_FAR 60.0f + +SDL_GPUTexture *ta_shadow_texture; +SDL_GPUSampler *ta_shadow_sampler; + +float ta_shadow_light_direction[TA_SHADOW_DIMENSION] = { -0.45f, 0.75f, 0.55f }; + +float ta_shadow_normal_offset = 0.03f; +float ta_shadow_minimum_bias = 0.0005f; +float ta_shadow_maximum_bias = 0.003f; +float ta_shadow_strength = 1.0f; + +static float ta_shadow_dot(const float *left, const float *right) { + return left[0] * right[0] + left[1] * right[1] + left[2] * right[2]; +} + +static void ta_shadow_cross(float *result, const float *left, const float *right) { + result[0] = left[1] * right[2] - left[2] * right[1]; + result[1] = left[2] * right[0] - left[0] * right[2]; + result[2] = left[0] * right[1] - left[1] * right[0]; +} + +static void ta_shadow_normalize(float *vector) { + float length = sqrtf(ta_shadow_dot(vector, vector)); + if (length > 0.0f) { + vector[0] /= length; + vector[1] /= length; + vector[2] /= length; + } +} + +void ta_shadow_init(void) { + SDL_GPUTextureCreateInfo texture_info = { + .type = SDL_GPU_TEXTURETYPE_2D, + .format = TA_SHADOW_FORMAT, + .usage = SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER, + .width = TA_SHADOW_MAP_SIZE, + .height = TA_SHADOW_MAP_SIZE, + .layer_count_or_depth = 1, + .num_levels = 1, + .sample_count = SDL_GPU_SAMPLECOUNT_1, + }; + ta_shadow_texture = ta_sdl_create_gpu_texture(ta_gpu_device, &texture_info); + + SDL_GPUSamplerCreateInfo sampler_info = { + .min_filter = SDL_GPU_FILTER_LINEAR, + .mag_filter = SDL_GPU_FILTER_LINEAR, + .mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST, + .address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE, + .address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE, + .address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE, + .compare_op = SDL_GPU_COMPAREOP_LESS, + .enable_compare = true, + }; + ta_shadow_sampler = ta_sdl_create_gpu_sampler(ta_gpu_device, &sampler_info); +} + +void ta_shadow_get_light_view_projection(float *matrix) { + static const float world_up[TA_SHADOW_DIMENSION] = { 0.0f, 1.0f, 0.0f }; + static const float world_forward[TA_SHADOW_DIMENSION] = { 0.0f, 0.0f, 1.0f }; + + float direction[TA_SHADOW_DIMENSION]; + float forward[TA_SHADOW_DIMENSION]; + float right[TA_SHADOW_DIMENSION]; + float up[TA_SHADOW_DIMENSION]; + float eye[TA_SHADOW_DIMENSION]; + float view[TA_MATRIX_ELEMENTS]; + float projection[TA_MATRIX_ELEMENTS] = { 0 }; + + memcpy(direction, ta_shadow_light_direction, sizeof(direction)); + ta_shadow_normalize(direction); + + for (int index = 0; index < TA_SHADOW_DIMENSION; index++) { + eye[index] = direction[index] * TA_SHADOW_DISTANCE; + forward[index] = -direction[index]; + } + + ta_shadow_cross(right, forward, fabsf(direction[1]) > 0.999f ? world_forward : world_up); + ta_shadow_normalize(right); + ta_shadow_cross(up, right, forward); + + ta_matrix_identity(view); + view[0] = right[0]; + view[4] = right[1]; + view[8] = right[2]; + view[12] = -ta_shadow_dot(right, eye); + view[1] = up[0]; + view[5] = up[1]; + view[9] = up[2]; + view[13] = -ta_shadow_dot(up, eye); + view[2] = -forward[0]; + view[6] = -forward[1]; + view[10] = -forward[2]; + view[14] = ta_shadow_dot(forward, eye); + + projection[0] = 1.0f / TA_SHADOW_EXTENT; + projection[5] = 1.0f / TA_SHADOW_EXTENT; + projection[10] = -1.0f / (TA_SHADOW_FAR - TA_SHADOW_NEAR); + projection[14] = -TA_SHADOW_NEAR / (TA_SHADOW_FAR - TA_SHADOW_NEAR); + projection[15] = 1.0f; + + ta_matrix_multiply(matrix, projection, view); +} + +SDL_GPURenderPass *ta_shadow_begin_render_pass(SDL_GPUCommandBuffer *command_buffer) { + if (!ta_shadow_texture) { + return NULL; + } + + SDL_GPUDepthStencilTargetInfo depth_target = { + .texture = ta_shadow_texture, + .clear_depth = 1.0f, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE, + .stencil_load_op = SDL_GPU_LOADOP_DONT_CARE, + .stencil_store_op = SDL_GPU_STOREOP_DONT_CARE, + }; + return ta_sdl_begin_gpu_render_pass(command_buffer, NULL, 0, &depth_target); +} + +void ta_shadow_shutdown(void) { + SDL_ReleaseGPUSampler(ta_gpu_device, ta_shadow_sampler); + SDL_ReleaseGPUTexture(ta_gpu_device, ta_shadow_texture); + ta_shadow_sampler = NULL; + ta_shadow_texture = NULL; +} \ No newline at end of file diff --git a/src/ta_math.c b/src/ta_math.c index 53375a5..5b976fc 100644 --- a/src/ta_math.c +++ b/src/ta_math.c @@ -16,8 +16,26 @@ * along with this program. If not, see . */ +#include #include "ta_math.h" double ta_rad(double degrees) { return degrees * (PI / (CIRCLE_DEG / 2)); +} + +void ta_matrix_identity(float *matrix) { + for (int index = 0; index < TA_MATRIX_ELEMENTS; index++) { + matrix[index] = index % 5 == 0 ? 1.0f : 0.0f; + } +} + +void ta_matrix_multiply(float *result, const float *left, const float *right) { + float product[TA_MATRIX_ELEMENTS]; + for (int column = 0; column < 4; column++) for (int row = 0; row < 4; row++) { + product[column * 4 + row] = 0.0f; + for (int inner = 0; inner < 4; inner++) { + product[column * 4 + row] += left[inner * 4 + row] * right[column * 4 + inner]; + } + } + memcpy(result, product, sizeof(product)); } \ No newline at end of file diff --git a/src/teapot.c b/src/teapot.c index 3d9cb6d..a8889c9 100644 --- a/src/teapot.c +++ b/src/teapot.c @@ -62,6 +62,11 @@ void ta_teapot_render(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *r ta_model_render(&ta_floor_model, command_buffer, render_pass, ta_window_width, ta_window_height); } +void ta_teapot_render_shadow(SDL_GPUCommandBuffer *command_buffer, SDL_GPURenderPass *render_pass) { + ta_model_render_shadow(&ta_teapot_model, command_buffer, render_pass); + ta_model_render_shadow(&ta_floor_model, command_buffer, render_pass); +} + void ta_teapot_shutdown(void) { ta_model_destroy(&ta_teapot_model, ta_gpu_device); ta_model_destroy(&ta_floor_model, ta_gpu_device); -- cgit v1.2.3