From 79ed26d26d271077cfb499f21fcd505ac5b95bce Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Fri, 13 Jun 2025 14:30:29 +1000 Subject: [PATCH] rust: Implement lesson08 --- Cargo.toml | 4 + build.rs | 1 + src/rust/lesson7.rs | 5 +- src/rust/lesson8.rs | 407 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 415 insertions(+), 2 deletions(-) create mode 100644 src/rust/lesson8.rs diff --git a/Cargo.toml b/Cargo.toml index 65074a4..a608ba0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,3 +36,7 @@ path = "src/rust/lesson6.rs" [[bin]] name = "lesson7" path = "src/rust/lesson7.rs" + +[[bin]] +name = "lesson8" +path = "src/rust/lesson8.rs" diff --git a/build.rs b/build.rs index 4ea0492..d573852 100644 --- a/build.rs +++ b/build.rs @@ -42,6 +42,7 @@ pub fn main() &[ "NeHe.bmp", "Crate.bmp", + "Glass.bmp", ]); copy_resources(&src_dir.join("shaders"), &dst_dir.join("Shaders"), &[ diff --git a/src/rust/lesson7.rs b/src/rust/lesson7.rs index 8f8d867..b1981af 100644 --- a/src/rust/lesson7.rs +++ b/src/rust/lesson7.rs @@ -239,9 +239,10 @@ impl AppImplementation for Lesson7 } // Create texture samplers - let create_sampler = |filter: SDL_GPUFilter, enable_mip: bool| -> Result<&mut SDL_GPUSampler, NeHeError> + let create_sampler = |filter: SDL_GPUFilter, enable_mip: bool| + -> Result<&mut SDL_GPUSampler, NeHeError> { - let mut sampler_info: SDL_GPUSamplerCreateInfo = unsafe { std::mem::zeroed() }; + let mut sampler_info = SDL_GPUSamplerCreateInfo::default(); sampler_info.min_filter = filter; sampler_info.mag_filter = filter; sampler_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST; diff --git a/src/rust/lesson8.rs b/src/rust/lesson8.rs new file mode 100644 index 0000000..0539d4c --- /dev/null +++ b/src/rust/lesson8.rs @@ -0,0 +1,407 @@ +/* + * 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_B, 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 Lesson8 +{ + pso_unlit: *mut SDL_GPUGraphicsPipeline, + pso_light: *mut SDL_GPUGraphicsPipeline, + pso_blend_unlit: *mut SDL_GPUGraphicsPipeline, + pso_blend_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, + blending: bool, + light: Light, + filter: usize, + + rot: (f32, f32), + speed: (f32, f32), + z: f32, +} + +impl Default for Lesson8 +{ + fn default() -> Self + { + Self + { + pso_unlit: null_mut(), + pso_light: null_mut(), + pso_blend_unlit: null_mut(), + pso_blend_light: null_mut(), + vtx_buffer: null_mut(), + idx_buffer: null_mut(), + samplers: [null_mut(); 3], + texture: null_mut(), + projection: Mtx::IDENTITY, + + lighting: false, + blending: 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 Lesson8 +{ + const TITLE: &'static str = "Tom Stanis & NeHe's Blending 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; + + // Common pipeline depth & colour target options + + let mut color_target = [ SDL_GPUColorTargetDescription::default() ]; + color_target[0].format = unsafe { SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window) }; + info.target_info.color_target_descriptions = color_target.as_ptr(); + info.target_info.num_color_targets = 1; + info.target_info.depth_stencil_format = Self::CREATE_DEPTH_BUFFER; + info.target_info.has_depth_stencil_target = true; + info.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL; + + // Setup depth/stencil & colour pipeline state for no blending + info.depth_stencil_state.enable_depth_test = true; + info.depth_stencil_state.enable_depth_write = true; + color_target[0].blend_state = unsafe { std::mem::zeroed() }; + + // Create unlit pipeline + info.vertex_shader = vertex_shader_unlit; + info.fragment_shader = fragment_shader_unlit; + self.pso_unlit = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info).as_mut() } + .ok_or(NeHeError::sdl("SDL_CreateGPUGraphicsPipeline"))?; + + // Create lit pipeline + info.vertex_shader = vertex_shader_light; + info.fragment_shader = fragment_shader_light; + self.pso_light = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info).as_mut() } + .ok_or(NeHeError::sdl("SDL_CreateGPUGraphicsPipeline"))?; + + // Setup depth/stencil & colour pipeline state for blending + info.depth_stencil_state.enable_depth_test = false; + info.depth_stencil_state.enable_depth_write = false; + color_target[0].blend_state.enable_blend = true; + color_target[0].blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; + color_target[0].blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; + color_target[0].blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; + color_target[0].blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE; + color_target[0].blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; + color_target[0].blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE; + + // Create unlit blended pipeline + info.vertex_shader = vertex_shader_unlit; + info.fragment_shader = fragment_shader_unlit; + self.pso_blend_unlit = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info).as_mut() } + .ok_or(NeHeError::sdl("SDL_CreateGPUGraphicsPipeline"))?; + + // Create lit blended pipeline + info.vertex_shader = vertex_shader_light; + info.fragment_shader = fragment_shader_light; + self.pso_blend_light = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info).as_mut() } + .ok_or(NeHeError::sdl("SDL_CreateGPUGraphicsPipeline"))?; + + // Free shaders + unsafe + { + SDL_ReleaseGPUShader(ctx.device, fragment_shader_light); + SDL_ReleaseGPUShader(ctx.device, vertex_shader_light); + SDL_ReleaseGPUShader(ctx.device, fragment_shader_unlit); + SDL_ReleaseGPUShader(ctx.device, vertex_shader_unlit); + } + + // Create texture samplers + let create_sampler = |filter: SDL_GPUFilter, enable_mip: bool| + -> Result<&mut SDL_GPUSampler, NeHeError> + { + let mut sampler_info = SDL_GPUSamplerCreateInfo::default(); + 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/Glass.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_blend_light); + SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso_blend_unlit); + 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, match (self.blending, self.lighting) + { + (false, false) => self.pso_unlit, + (false, true) => self.pso_light, + (true, false) => self.pso_blend_unlit, + (true, true) => self.pso_blend_light, + }); + + // 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 color = [1.0, 1.0, 1.0, 0.5]; // 50% translucency + let u = Uniforms { model_view_proj: self.projection * model, color }; + 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_B if down => self.blending = !self.blending, + SDLK_F if down => self.filter = (self.filter + 1) % self.samplers.len(), + _ => () + } + } +} + +pub fn main() -> Result> +{ + run::()?; + Ok(ExitCode::SUCCESS) +}