/* * SPDX-FileCopyrightText: (C) 2025 a dinosaur * SPDX-License-Identifier: Zlib */ use nehe::application::config::AppImplementation; use nehe::application::run; use nehe::context::NeHeContext; use nehe::error::NeHeError; use nehe::matrix::Mtx; use sdl3_sys::gpu::*; use sdl3_sys::keyboard::SDL_GetKeyboardState; use sdl3_sys::keycode::{SDL_Keycode, SDLK_F, SDLK_L}; use sdl3_sys::pixels::SDL_FColor; use sdl3_sys::scancode::{SDL_SCANCODE_DOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_PAGEDOWN, SDL_SCANCODE_PAGEUP, SDL_SCANCODE_RIGHT, SDL_SCANCODE_UP}; use std::cmp::max; use std::ffi::c_void; use std::mem::offset_of; use std::process::ExitCode; use std::ptr::{addr_of, null_mut}; #[repr(C)] struct Vertex { x: f32, y: f32, z: f32, nx: f32, ny: f32, nz: f32, u: f32, v: f32, } const VERTICES: &'static [Vertex] = &[ // Front Face Vertex { x: -1.0, y: -1.0, z: 1.0, nx: 0.0, ny: 0.0, nz: 1.0, u: 0.0, v: 0.0 }, Vertex { x: 1.0, y: -1.0, z: 1.0, nx: 0.0, ny: 0.0, nz: 1.0, u: 1.0, v: 0.0 }, Vertex { x: 1.0, y: 1.0, z: 1.0, nx: 0.0, ny: 0.0, nz: 1.0, u: 1.0, v: 1.0 }, Vertex { x: -1.0, y: 1.0, z: 1.0, nx: 0.0, ny: 0.0, nz: 1.0, u: 0.0, v: 1.0 }, // Back Face Vertex { x: -1.0, y: -1.0, z: -1.0, nx: 0.0, ny: 0.0, nz: -1.0, u: 1.0, v: 0.0 }, Vertex { x: -1.0, y: 1.0, z: -1.0, nx: 0.0, ny: 0.0, nz: -1.0, u: 1.0, v: 1.0 }, Vertex { x: 1.0, y: 1.0, z: -1.0, nx: 0.0, ny: 0.0, nz: -1.0, u: 0.0, v: 1.0 }, Vertex { x: 1.0, y: -1.0, z: -1.0, nx: 0.0, ny: 0.0, nz: -1.0, u: 0.0, v: 0.0 }, // Top Face Vertex { x: -1.0, y: 1.0, z: -1.0, nx: 0.0, ny: 1.0, nz: 0.0, u: 0.0, v: 1.0 }, Vertex { x: -1.0, y: 1.0, z: 1.0, nx: 0.0, ny: 1.0, nz: 0.0, u: 0.0, v: 0.0 }, Vertex { x: 1.0, y: 1.0, z: 1.0, nx: 0.0, ny: 1.0, nz: 0.0, u: 1.0, v: 0.0 }, Vertex { x: 1.0, y: 1.0, z: -1.0, nx: 0.0, ny: 1.0, nz: 0.0, u: 1.0, v: 1.0 }, // Bottom Face Vertex { x: -1.0, y: -1.0, z: -1.0, nx: 0.0, ny: -1.0, nz: 0.0, u: 1.0, v: 1.0 }, Vertex { x: 1.0, y: -1.0, z: -1.0, nx: 0.0, ny: -1.0, nz: 0.0, u: 0.0, v: 1.0 }, Vertex { x: 1.0, y: -1.0, z: 1.0, nx: 0.0, ny: -1.0, nz: 0.0, u: 0.0, v: 0.0 }, Vertex { x: -1.0, y: -1.0, z: 1.0, nx: 0.0, ny: -1.0, nz: 0.0, u: 1.0, v: 0.0 }, // Right face Vertex { x: 1.0, y: -1.0, z: -1.0, nx: 1.0, ny: 0.0, nz: 0.0, u: 1.0, v: 0.0 }, Vertex { x: 1.0, y: 1.0, z: -1.0, nx: 1.0, ny: 0.0, nz: 0.0, u: 1.0, v: 1.0 }, Vertex { x: 1.0, y: 1.0, z: 1.0, nx: 1.0, ny: 0.0, nz: 0.0, u: 0.0, v: 1.0 }, Vertex { x: 1.0, y: -1.0, z: 1.0, nx: 1.0, ny: 0.0, nz: 0.0, u: 0.0, v: 0.0 }, // Left Face Vertex { x: -1.0, y: -1.0, z: -1.0, nx: -1.0, ny: 0.0, nz: 0.0, u: 0.0, v: 0.0 }, Vertex { x: -1.0, y: -1.0, z: 1.0, nx: -1.0, ny: 0.0, nz: 0.0, u: 1.0, v: 0.0 }, Vertex { x: -1.0, y: 1.0, z: 1.0, nx: -1.0, ny: 0.0, nz: 0.0, u: 1.0, v: 1.0 }, Vertex { x: -1.0, y: 1.0, z: -1.0, nx: -1.0, ny: 0.0, nz: 0.0, u: 0.0, v: 1.0 }, ]; const INDICES: &'static [u16] = &[ 0, 1, 2, 2, 3, 0, // Front 4, 5, 6, 6, 7, 4, // Back 8, 9, 10, 10, 11, 8, // Top 12, 13, 14, 14, 15, 12, // Bottom 16, 17, 18, 18, 19, 16, // Right 20, 21, 22, 22, 23, 20 // Left ]; #[allow(dead_code)] struct Light { ambient: (f32, f32, f32, f32), diffuse: (f32, f32, f32, f32), position: (f32, f32, f32, f32), } struct Lesson7 { pso_unlit: *mut SDL_GPUGraphicsPipeline, pso_light: *mut SDL_GPUGraphicsPipeline, vtx_buffer: *mut SDL_GPUBuffer, idx_buffer: *mut SDL_GPUBuffer, samplers: [*mut SDL_GPUSampler; 3], texture: *mut SDL_GPUTexture, projection: Mtx, lighting: bool, light: Light, filter: usize, rot: (f32, f32), speed: (f32, f32), z: f32, } impl Default for Lesson7 { fn default() -> Self { Self { pso_unlit: null_mut(), pso_light: null_mut(), vtx_buffer: null_mut(), idx_buffer: null_mut(), samplers: [null_mut(); 3], texture: null_mut(), projection: Mtx::IDENTITY, lighting: false, light: Light { ambient: (0.5, 0.5, 0.5, 1.0), diffuse: (1.0, 1.0, 1.0, 1.0), position: (0.0, 0.0, 2.0, 1.0), }, filter: 0, rot: (0.0, 0.0), speed: (0.0, 0.0), z: -5.0, } } } impl AppImplementation for Lesson7 { const TITLE: &'static str = "NeHe's Textures, Lighting & Keyboard Tutorial"; const WIDTH: i32 = 640; const HEIGHT: i32 = 480; const CREATE_DEPTH_BUFFER: SDL_GPUTextureFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM; fn init(&mut self, ctx: &NeHeContext) -> Result<(), NeHeError> { let (vertex_shader_unlit, fragment_shader_unlit) = ctx.load_shaders("lesson6", 1, 0, 1)?; let (vertex_shader_light, fragment_shader_light) = ctx.load_shaders("lesson7", 2, 0, 1)?; const VERTEX_DESCRIPTIONS: &'static [SDL_GPUVertexBufferDescription] = &[ SDL_GPUVertexBufferDescription { slot: 0, pitch: size_of::() as u32, input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX, instance_step_rate: 0, }, ]; const VERTEX_ATTRIBS: &'static [SDL_GPUVertexAttribute] = &[ SDL_GPUVertexAttribute { location: 0, buffer_slot: 0, format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, offset: offset_of!(Vertex, x) as u32, }, SDL_GPUVertexAttribute { location: 2, buffer_slot: 0, format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, offset: offset_of!(Vertex, nx) as u32, }, SDL_GPUVertexAttribute { location: 1, buffer_slot: 0, format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, offset: offset_of!(Vertex, u) as u32, }, ]; let mut info = SDL_GPUGraphicsPipelineCreateInfo::default(); info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; info.vertex_input_state = SDL_GPUVertexInputState { vertex_buffer_descriptions: VERTEX_DESCRIPTIONS.as_ptr(), num_vertex_buffers: VERTEX_DESCRIPTIONS.len() as u32, vertex_attributes: VERTEX_ATTRIBS.as_ptr(), num_vertex_attributes: VERTEX_ATTRIBS.len() as u32, }; info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; let colour_targets: &[SDL_GPUColorTargetDescription] = &[ SDL_GPUColorTargetDescription { format: unsafe { SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window) }, blend_state: SDL_GPUColorTargetBlendState::default(), } ]; info.target_info.color_target_descriptions = colour_targets.as_ptr(); info.target_info.num_color_targets = colour_targets.len() as u32; info.target_info.depth_stencil_format = SDL_GPU_TEXTUREFORMAT_D16_UNORM; info.target_info.has_depth_stencil_target = true; info.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL; info.depth_stencil_state.enable_depth_test = true; info.depth_stencil_state.enable_depth_write = true; // Create unlit pipeline info.vertex_shader = vertex_shader_unlit; info.fragment_shader = fragment_shader_unlit; self.pso_unlit = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info) }; unsafe { SDL_ReleaseGPUShader(ctx.device, fragment_shader_unlit); SDL_ReleaseGPUShader(ctx.device, vertex_shader_unlit); } if self.pso_unlit.is_null() { let err = NeHeError::sdl("SDL_CreateGPUGraphicsPipeline"); unsafe { SDL_ReleaseGPUShader(ctx.device, fragment_shader_light); SDL_ReleaseGPUShader(ctx.device, vertex_shader_light); } return Err(err); } // Create lit pipeline info.vertex_shader = vertex_shader_light; info.fragment_shader = fragment_shader_light; self.pso_light = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info) }; unsafe { SDL_ReleaseGPUShader(ctx.device, fragment_shader_light); SDL_ReleaseGPUShader(ctx.device, vertex_shader_light); } if self.pso_light.is_null() { return Err(NeHeError::sdl("SDL_CreateGPUGraphicsPipeline")); } // Create texture samplers let create_sampler = |filter: SDL_GPUFilter, enable_mip: bool| -> Result<&mut SDL_GPUSampler, NeHeError> { let mut sampler_info: SDL_GPUSamplerCreateInfo = unsafe { std::mem::zeroed() }; sampler_info.min_filter = filter; sampler_info.mag_filter = filter; sampler_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST; sampler_info.max_lod = if enable_mip { f32::MAX } else { 0.0 }; unsafe { SDL_CreateGPUSampler(ctx.device, &sampler_info).as_mut() } .ok_or(NeHeError::sdl("SDL_CreateGPUSampler")) }; self.samplers[0] = create_sampler(SDL_GPU_FILTER_NEAREST, false)?; self.samplers[1] = create_sampler(SDL_GPU_FILTER_LINEAR, false)?; self.samplers[2] = create_sampler(SDL_GPU_FILTER_LINEAR, true)?; ctx.copy_pass(|pass| { self.texture = pass.load_texture("Data/Crate.bmp", true, true)?; self.vtx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_VERTEX, VERTICES)?; self.idx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_INDEX, INDICES)?; Ok(()) }) } fn quit(&mut self, ctx: &NeHeContext) { unsafe { SDL_ReleaseGPUBuffer(ctx.device, self.idx_buffer); SDL_ReleaseGPUBuffer(ctx.device, self.vtx_buffer); SDL_ReleaseGPUTexture(ctx.device, self.texture); self.samplers.iter().rev().for_each(|sampler| SDL_ReleaseGPUSampler(ctx.device, *sampler)); SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso_light); SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso_unlit); } } fn resize(&mut self, _ctx: &NeHeContext, width: i32, height: i32) { let aspect = width as f32 / max(height, 1) as f32; self.projection = Mtx::perspective(45.0, aspect, 0.1, 100.0); } fn draw(&mut self, ctx: &NeHeContext, cmd: *mut SDL_GPUCommandBuffer, swapchain: *mut SDL_GPUTexture) { let mut color_info = SDL_GPUColorTargetInfo::default(); color_info.texture = swapchain; color_info.clear_color = SDL_FColor { r: 0.0, g: 0.0, b: 0.0, a: 0.5 }; color_info.load_op = SDL_GPU_LOADOP_CLEAR; color_info.store_op = SDL_GPU_STOREOP_STORE; let mut depth_info = SDL_GPUDepthStencilTargetInfo::default(); depth_info.texture = ctx.depth_texture; depth_info.clear_depth = 1.0; depth_info.load_op = SDL_GPU_LOADOP_CLEAR; depth_info.store_op = SDL_GPU_STOREOP_DONT_CARE; depth_info.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE; depth_info.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE; depth_info.cycle = true; unsafe { // Begin pass & bind pipeline state let pass = SDL_BeginGPURenderPass(cmd, &color_info, 1, &depth_info); SDL_BindGPUGraphicsPipeline(pass, if self.lighting { self.pso_light } else { self.pso_unlit }); // Bind texture let texture_binding = SDL_GPUTextureSamplerBinding { texture: self.texture, sampler: self.samplers[self.filter], }; SDL_BindGPUFragmentSamplers(pass, 0, &texture_binding, 1); // Bind vertex & index buffers let vtx_binding = SDL_GPUBufferBinding { buffer: self.vtx_buffer, offset: 0 }; let idx_binding = SDL_GPUBufferBinding { buffer: self.idx_buffer, offset: 0 }; SDL_BindGPUVertexBuffers(pass, 0, &vtx_binding, 1); SDL_BindGPUIndexBuffer(pass, &idx_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT); // Setup the cube's model matrix let mut model = Mtx::translation(0.0, 0.0, self.z); model.rotate(self.rot.0, 1.0, 0.0, 0.0); model.rotate(self.rot.1, 0.0, 1.0, 0.0); // Push shader uniforms if self.lighting { #[allow(dead_code)] struct Uniforms { model: Mtx, projection: Mtx } let u = Uniforms { model, projection: self.projection }; SDL_PushGPUVertexUniformData(cmd, 0, addr_of!(u) as *const c_void, size_of::() as u32); SDL_PushGPUVertexUniformData(cmd, 1, addr_of!(self.light) as *const c_void, size_of::() as u32); } else { #[allow(dead_code)] struct Uniforms { model_view_proj: Mtx, color: [f32; 4] } let u = Uniforms { model_view_proj: self.projection * model, color: [1.0; 4] }; SDL_PushGPUVertexUniformData(cmd, 0, addr_of!(u) as *const c_void, size_of::() as u32); } // Draw the textured cube SDL_DrawGPUIndexedPrimitives(pass, INDICES.len() as u32, 1, 0, 0, 0); SDL_EndGPURenderPass(pass); } let keys = unsafe { let mut numkeys: std::ffi::c_int = 0; let keys = SDL_GetKeyboardState(&mut numkeys); std::slice::from_raw_parts(keys, numkeys as usize) }; if keys[SDL_SCANCODE_PAGEUP.0 as usize] { self.z -= 0.02 } if keys[SDL_SCANCODE_PAGEDOWN.0 as usize] { self.z += 0.02; } if keys[SDL_SCANCODE_UP.0 as usize] { self.speed.0 -= 0.01; } if keys[SDL_SCANCODE_DOWN.0 as usize] { self.speed.0 += 0.01; } if keys[SDL_SCANCODE_RIGHT.0 as usize] { self.speed.1 += 0.1; } if keys[SDL_SCANCODE_LEFT.0 as usize] { self.speed.1 -= 0.1; } self.rot.0 += self.speed.0; self.rot.1 += self.speed.1; } fn key(&mut self, _ctx: &NeHeContext, key: SDL_Keycode, down: bool, _repeat: bool) { match key { SDLK_L if down => self.lighting = !self.lighting, SDLK_F if down => self.filter = (self.filter + 1) % self.samplers.len(), _ => () } } } pub fn main() -> Result> { run::()?; Ok(ExitCode::SUCCESS) }