summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/files.h8
-rw-r--r--include/model.h2
-rw-r--r--include/screen.h1
-rw-r--r--include/screens/main_menu.h1
-rw-r--r--include/shadow.h41
-rw-r--r--include/ta_math.h7
-rw-r--r--include/teapot.h1
-rw-r--r--shaders/model.frag.slang65
-rw-r--r--shaders/model.vert.slang25
-rw-r--r--shaders/model_wireframe.frag.slang2
-rw-r--r--shaders/shadow.frag.slang20
-rw-r--r--shaders/shadow.vert.slang38
-rw-r--r--src/gpu.c7
-rw-r--r--src/main.c3
-rw-r--r--src/model.c179
-rw-r--r--src/screen.c10
-rw-r--r--src/screens/main_menu.c4
-rw-r--r--src/shadow.c152
-rw-r--r--src/ta_math.c18
-rw-r--r--src/teapot.c5
20 files changed, 516 insertions, 73 deletions
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 <https://www.gnu.org/licenses/>.
+ */
+
+#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 <math.h>
-#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 <https://www.gnu.org/licenses/>.
*/
+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_uniforms> light : register(b0, space3);
+
+[[vk::binding(0, 2)]]
+Texture2D<float> 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 <https://www.gnu.org/licenses/>.
+ */
+
+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 <https://www.gnu.org/licenses/>.
+ */
+
+struct shadow_uniforms {
+ float4x4 light_model_view_projection;
+};
+
+[[vk::binding(0, 1)]]
+ConstantBuffer<shadow_uniforms> 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#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 <https://www.gnu.org/licenses/>.
*/
+#include <string.h>
#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);