diff --git a/Cargo.lock b/Cargo.lock index fefb089..a0491a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1877,6 +1877,21 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "survivor_game" +version = "0.1.0" +dependencies = [ + "bytemuck", + "env_logger", + "log", + "pollster", + "voltex_math", + "voltex_platform", + "voltex_renderer", + "wgpu", + "winit", +] + [[package]] name = "syn" version = "2.0.117" diff --git a/Cargo.toml b/Cargo.toml index 4b455c8..8dc72a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ "crates/voltex_script", "crates/voltex_editor", "examples/editor_demo", + "examples/survivor_game", ] [workspace.dependencies] diff --git a/examples/survivor_game/Cargo.toml b/examples/survivor_game/Cargo.toml new file mode 100644 index 0000000..363c90f --- /dev/null +++ b/examples/survivor_game/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "survivor_game" +version = "0.1.0" +edition = "2021" + +[dependencies] +voltex_math.workspace = true +voltex_platform.workspace = true +voltex_renderer.workspace = true +wgpu.workspace = true +winit.workspace = true +bytemuck.workspace = true +pollster.workspace = true +env_logger.workspace = true +log.workspace = true diff --git a/examples/survivor_game/src/arena.rs b/examples/survivor_game/src/arena.rs new file mode 100644 index 0000000..345f91c --- /dev/null +++ b/examples/survivor_game/src/arena.rs @@ -0,0 +1,186 @@ +use voltex_renderer::vertex::MeshVertex; + +/// Generate a box mesh with the given half-extents centered at the origin. +/// Returns (vertices, indices) with u32 indices (matching Mesh::new). +pub fn generate_box(width: f32, height: f32, depth: f32) -> (Vec, Vec) { + let hw = width * 0.5; + let hh = height * 0.5; + let hd = depth * 0.5; + + // 6 faces, 4 vertices each = 24 vertices + // Each face has outward-pointing normal, UV from 0..1, and tangent. + let mut vertices = Vec::with_capacity(24); + let mut indices = Vec::with_capacity(36); + + // Helper to push a quad (4 verts + 6 indices) + let mut push_face = |positions: [[f32; 3]; 4], + normal: [f32; 3], + tangent: [f32; 4], + uvs: [[f32; 2]; 4]| { + let base = vertices.len() as u32; + for i in 0..4 { + vertices.push(MeshVertex { + position: positions[i], + normal, + uv: uvs[i], + tangent, + }); + } + // Two triangles: 0-1-2, 0-2-3 + indices.push(base); + indices.push(base + 1); + indices.push(base + 2); + indices.push(base); + indices.push(base + 2); + indices.push(base + 3); + }; + + // +Y face (top) + push_face( + [ + [-hw, hh, -hd], + [-hw, hh, hd], + [hw, hh, hd], + [hw, hh, -hd], + ], + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0, 1.0], + [[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]], + ); + + // -Y face (bottom) + push_face( + [ + [-hw, -hh, hd], + [-hw, -hh, -hd], + [hw, -hh, -hd], + [hw, -hh, hd], + ], + [0.0, -1.0, 0.0], + [1.0, 0.0, 0.0, 1.0], + [[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]], + ); + + // +Z face (front) + push_face( + [ + [-hw, -hh, hd], + [hw, -hh, hd], + [hw, hh, hd], + [-hw, hh, hd], + ], + [0.0, 0.0, 1.0], + [1.0, 0.0, 0.0, 1.0], + [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], + ); + + // -Z face (back) + push_face( + [ + [hw, -hh, -hd], + [-hw, -hh, -hd], + [-hw, hh, -hd], + [hw, hh, -hd], + ], + [0.0, 0.0, -1.0], + [-1.0, 0.0, 0.0, 1.0], + [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], + ); + + // +X face (right) + push_face( + [ + [hw, -hh, hd], + [hw, -hh, -hd], + [hw, hh, -hd], + [hw, hh, hd], + ], + [1.0, 0.0, 0.0], + [0.0, 0.0, -1.0, 1.0], + [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], + ); + + // -X face (left) + push_face( + [ + [-hw, -hh, -hd], + [-hw, -hh, hd], + [-hw, hh, hd], + [-hw, hh, -hd], + ], + [-1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 1.0], + [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], + ); + + (vertices, indices) +} + +/// An arena entity: a mesh with a model transform and material parameters. +pub struct ArenaEntity { + pub vertices: Vec, + pub indices: Vec, + pub base_color: [f32; 4], + pub metallic: f32, + pub roughness: f32, +} + +/// Generate the arena: 1 floor + 4 obstacles. +pub fn generate_arena() -> Vec { + let mut entities = Vec::new(); + + // Floor: 20x0.2x20 box (flat ground) + let (floor_v, floor_i) = generate_box(20.0, 0.2, 20.0); + entities.push(ArenaEntity { + vertices: floor_v, + indices: floor_i, + base_color: [0.5, 0.5, 0.5, 1.0], // gray + metallic: 0.0, + roughness: 0.8, + }); + + // 4 obstacle boxes: 2x1x2 each at predefined positions + let obstacle_positions: [(f32, f32, f32); 4] = [ + (-5.0, 0.6, -3.0), + (4.0, 0.6, 2.0), + (-3.0, 0.6, 5.0), + (6.0, 0.6, -4.0), + ]; + + for _pos in &obstacle_positions { + let (obs_v, obs_i) = generate_box(2.0, 1.0, 2.0); + entities.push(ArenaEntity { + vertices: obs_v, + indices: obs_i, + base_color: [0.3, 0.3, 0.35, 1.0], // dark gray + metallic: 0.1, + roughness: 0.6, + }); + } + + entities +} + +/// Model matrices for the 5 entities (floor + 4 obstacles). +pub fn arena_model_matrices() -> Vec { + use voltex_math::Mat4; + + let mut models = Vec::new(); + + // Floor centered at origin, y=0 (the box is 0.2 tall, so top is at y=0.1) + models.push(Mat4::translation(0.0, 0.0, 0.0)); + + // Obstacles + let obstacle_positions: [(f32, f32, f32); 4] = [ + (-5.0, 0.6, -3.0), + (4.0, 0.6, 2.0), + (-3.0, 0.6, 5.0), + (6.0, 0.6, -4.0), + ]; + + for (x, y, z) in &obstacle_positions { + models.push(Mat4::translation(*x, *y, *z)); + } + + models +} diff --git a/examples/survivor_game/src/camera.rs b/examples/survivor_game/src/camera.rs new file mode 100644 index 0000000..15eee25 --- /dev/null +++ b/examples/survivor_game/src/camera.rs @@ -0,0 +1,39 @@ +use voltex_math::{Vec3, Mat4}; + +/// Fixed quarter-view (isometric-like) camera for the survivor game. +pub struct QuarterViewCamera { + /// Offset from the target (camera position = target + offset). + pub offset: Vec3, + /// The point the camera is looking at (player position later). + pub target: Vec3, +} + +impl QuarterViewCamera { + pub fn new() -> Self { + Self { + offset: Vec3::new(0.0, 15.0, 10.0), + target: Vec3::ZERO, + } + } + + /// Compute the view matrix for the current camera state. + pub fn view_matrix(&self) -> Mat4 { + let eye = self.target + self.offset; + Mat4::look_at(eye, self.target, Vec3::Y) + } + + /// Compute the perspective projection matrix. + pub fn projection_matrix(&self, aspect: f32) -> Mat4 { + Mat4::perspective(45.0_f32.to_radians(), aspect, 0.1, 100.0) + } + + /// Compute combined view-projection matrix. + pub fn view_projection(&self, aspect: f32) -> Mat4 { + self.projection_matrix(aspect) * self.view_matrix() + } + + /// Get the eye position in world space. + pub fn eye_position(&self) -> Vec3 { + self.target + self.offset + } +} diff --git a/examples/survivor_game/src/main.rs b/examples/survivor_game/src/main.rs new file mode 100644 index 0000000..affb936 --- /dev/null +++ b/examples/survivor_game/src/main.rs @@ -0,0 +1,614 @@ +mod arena; +mod camera; + +use winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, EventLoop}, + keyboard::{KeyCode, PhysicalKey}, + window::WindowId, +}; +use voltex_math::{Vec3, Mat4}; +use voltex_platform::{VoltexWindow, WindowConfig, InputState, GameTimer}; +use voltex_renderer::{ + GpuContext, CameraUniform, LightsUniform, LightData, + Mesh, GpuTexture, MaterialUniform, create_pbr_pipeline, + ShadowMap, ShadowUniform, ShadowPassUniform, SHADOW_MAP_SIZE, + create_shadow_pipeline, shadow_pass_bind_group_layout, + IblResources, pbr_texture_bind_group_layout, create_pbr_texture_bind_group, +}; +use wgpu::util::DeviceExt; + +use arena::{generate_arena, arena_model_matrices}; +use camera::QuarterViewCamera; + +const NUM_ENTITIES: usize = 5; // 1 floor + 4 obstacles + +struct SurvivorApp { + state: Option, +} + +struct RenderEntity { + mesh: Mesh, +} + +struct AppState { + window: VoltexWindow, + gpu: GpuContext, + pbr_pipeline: wgpu::RenderPipeline, + shadow_pipeline: wgpu::RenderPipeline, + entities: Vec, + camera: QuarterViewCamera, + // Color pass resources + camera_buffer: wgpu::Buffer, + light_buffer: wgpu::Buffer, + material_buffer: wgpu::Buffer, + camera_light_bind_group: wgpu::BindGroup, + _albedo_tex: GpuTexture, + _normal_tex: (wgpu::Texture, wgpu::TextureView, wgpu::Sampler), + pbr_texture_bind_group: wgpu::BindGroup, + material_bind_group: wgpu::BindGroup, + // Shadow resources + shadow_map: ShadowMap, + shadow_uniform_buffer: wgpu::Buffer, + shadow_bind_group: wgpu::BindGroup, + shadow_pass_buffer: wgpu::Buffer, + shadow_pass_bind_group: wgpu::BindGroup, + _ibl: IblResources, + // Misc + input: InputState, + timer: GameTimer, + cam_aligned_size: u32, + mat_aligned_size: u32, + shadow_pass_aligned_size: u32, + // Arena data + models: Vec, + materials: Vec<([f32; 4], f32, f32)>, +} + +fn camera_light_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Camera+Light Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: wgpu::BufferSize::new( + std::mem::size_of::() as u64, + ), + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }) +} + +fn align_up(size: u32, alignment: u32) -> u32 { + ((size + alignment - 1) / alignment) * alignment +} + +impl ApplicationHandler for SurvivorApp { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let config = WindowConfig { + title: "Voltex Survivor".to_string(), + width: 1280, + height: 720, + ..Default::default() + }; + let window = VoltexWindow::new(event_loop, &config); + let gpu = GpuContext::new(window.handle.clone()); + + // Dynamic uniform buffer alignment + let alignment = gpu.device.limits().min_uniform_buffer_offset_alignment; + let cam_aligned_size = align_up(std::mem::size_of::() as u32, alignment); + let mat_aligned_size = align_up(std::mem::size_of::() as u32, alignment); + let shadow_pass_aligned_size = + align_up(std::mem::size_of::() as u32, alignment); + + // Generate arena geometry + let arena_entities = generate_arena(); + let models = arena_model_matrices(); + + // Collect materials and create GPU meshes + let mut render_entities = Vec::new(); + let mut materials = Vec::new(); + + for entity in &arena_entities { + let mesh = Mesh::new(&gpu.device, &entity.vertices, &entity.indices); + render_entities.push(RenderEntity { mesh }); + materials.push((entity.base_color, entity.metallic, entity.roughness)); + } + + // Quarter-view camera + let camera = QuarterViewCamera::new(); + + // Light: directional from upper-left + let mut lights_uniform = LightsUniform::new(); + lights_uniform.ambient_color = [0.08, 0.08, 0.08]; + lights_uniform.add_light(LightData::directional( + [-1.0, -2.0, -1.0], + [1.0, 0.98, 0.95], + 2.0, + )); + + // ---- Color pass buffers ---- + let camera_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Camera Dynamic UBO"), + size: (cam_aligned_size as usize * NUM_ENTITIES) as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let light_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Light UBO"), + contents: bytemuck::cast_slice(&[lights_uniform]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let material_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Material Dynamic UBO"), + size: (mat_aligned_size as usize * NUM_ENTITIES) as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + // Bind group layouts + let cl_layout = camera_light_bind_group_layout(&gpu.device); + let pbr_tex_layout = pbr_texture_bind_group_layout(&gpu.device); + let mat_layout = MaterialUniform::bind_group_layout(&gpu.device); + + // Camera+Light bind group + let camera_light_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Camera+Light BG"), + layout: &cl_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &camera_buffer, + offset: 0, + size: wgpu::BufferSize::new( + std::mem::size_of::() as u64, + ), + }), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: light_buffer.as_entire_binding(), + }, + ], + }); + + // PBR texture bind group (white albedo + flat normal) + let old_tex_layout = GpuTexture::bind_group_layout(&gpu.device); + let albedo_tex = GpuTexture::white_1x1(&gpu.device, &gpu.queue, &old_tex_layout); + let normal_tex = GpuTexture::flat_normal_1x1(&gpu.device, &gpu.queue); + let pbr_texture_bind_group = create_pbr_texture_bind_group( + &gpu.device, + &pbr_tex_layout, + &albedo_tex.view, + &albedo_tex.sampler, + &normal_tex.1, + &normal_tex.2, + ); + + // IBL resources + let ibl = IblResources::new(&gpu.device, &gpu.queue); + + // Material bind group + let material_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Material BG"), + layout: &mat_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &material_buffer, + offset: 0, + size: wgpu::BufferSize::new( + std::mem::size_of::() as u64, + ), + }), + }], + }); + + // ---- Shadow resources ---- + let shadow_map = ShadowMap::new(&gpu.device); + let shadow_layout = ShadowMap::bind_group_layout(&gpu.device); + + let shadow_uniform = ShadowUniform { + light_view_proj: Mat4::IDENTITY.cols, + shadow_map_size: SHADOW_MAP_SIZE as f32, + shadow_bias: 0.005, + _padding: [0.0; 2], + }; + let shadow_uniform_buffer = + gpu.device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Shadow Uniform Buffer"), + contents: bytemuck::cast_slice(&[shadow_uniform]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let shadow_bind_group = shadow_map.create_bind_group( + &gpu.device, + &shadow_layout, + &shadow_uniform_buffer, + &ibl.brdf_lut_view, + &ibl.brdf_lut_sampler, + ); + + // Shadow pass dynamic UBO + let sp_layout = shadow_pass_bind_group_layout(&gpu.device); + let shadow_pass_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Shadow Pass Dynamic UBO"), + size: (shadow_pass_aligned_size as usize * NUM_ENTITIES) as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let shadow_pass_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Shadow Pass BG"), + layout: &sp_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &shadow_pass_buffer, + offset: 0, + size: wgpu::BufferSize::new( + std::mem::size_of::() as u64, + ), + }), + }], + }); + + // ---- Pipelines ---- + let shadow_pipeline = create_shadow_pipeline(&gpu.device, &sp_layout); + let pbr_pipeline = create_pbr_pipeline( + &gpu.device, + gpu.surface_format, + &cl_layout, + &pbr_tex_layout, + &mat_layout, + &shadow_layout, + ); + + self.state = Some(AppState { + window, + gpu, + pbr_pipeline, + shadow_pipeline, + entities: render_entities, + camera, + camera_buffer, + light_buffer, + material_buffer, + camera_light_bind_group, + _albedo_tex: albedo_tex, + _normal_tex: normal_tex, + pbr_texture_bind_group, + material_bind_group, + shadow_map, + shadow_uniform_buffer, + shadow_bind_group, + shadow_pass_buffer, + shadow_pass_bind_group, + _ibl: ibl, + input: InputState::new(), + timer: GameTimer::new(60), + cam_aligned_size, + mat_aligned_size, + shadow_pass_aligned_size, + models, + materials, + }); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + let state = match &mut self.state { + Some(s) => s, + None => return, + }; + + match event { + WindowEvent::CloseRequested => event_loop.exit(), + + WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + physical_key: PhysicalKey::Code(key_code), + state: key_state, + .. + }, + .. + } => { + let pressed = key_state == winit::event::ElementState::Pressed; + state.input.process_key(key_code, pressed); + if key_code == KeyCode::Escape && pressed { + event_loop.exit(); + } + } + + WindowEvent::Resized(size) => { + state.gpu.resize(size.width, size.height); + } + + WindowEvent::CursorMoved { position, .. } => { + state.input.process_mouse_move(position.x, position.y); + } + + WindowEvent::MouseInput { + state: btn_state, + button, + .. + } => { + let pressed = btn_state == winit::event::ElementState::Pressed; + state.input.process_mouse_button(button, pressed); + } + + WindowEvent::MouseWheel { delta, .. } => { + let y = match delta { + winit::event::MouseScrollDelta::LineDelta(_, y) => y, + winit::event::MouseScrollDelta::PixelDelta(pos) => pos.y as f32, + }; + state.input.process_scroll(y); + } + + WindowEvent::RedrawRequested => { + state.timer.tick(); + state.input.begin_frame(); + + let aspect = state.gpu.config.width as f32 / state.gpu.config.height as f32; + + // ----- Compute light VP for shadows ----- + let light_dir = Vec3::new(-1.0, -2.0, -1.0).normalize(); + let light_pos = Vec3::ZERO - light_dir * 25.0; + let light_view = Mat4::look_at(light_pos, Vec3::ZERO, Vec3::Y); + let light_proj = Mat4::orthographic(-15.0, 15.0, -15.0, 15.0, 0.1, 60.0); + let light_vp = light_proj * light_view; + + let cam_aligned = state.cam_aligned_size as usize; + let mat_aligned = state.mat_aligned_size as usize; + let sp_aligned = state.shadow_pass_aligned_size as usize; + + // ----- Build shadow pass staging data ----- + let sp_total = sp_aligned * NUM_ENTITIES; + let mut sp_staging = vec![0u8; sp_total]; + for i in 0..NUM_ENTITIES { + let sp_uniform = ShadowPassUniform { + light_vp_model: (light_vp * state.models[i]).cols, + }; + let bytes = bytemuck::bytes_of(&sp_uniform); + let offset = i * sp_aligned; + sp_staging[offset..offset + bytes.len()].copy_from_slice(bytes); + } + state + .gpu + .queue + .write_buffer(&state.shadow_pass_buffer, 0, &sp_staging); + + // ----- Build color pass staging data ----- + let view_proj = state.camera.view_projection(aspect); + let eye = state.camera.eye_position(); + let cam_pos = [eye.x, eye.y, eye.z]; + + let cam_total = cam_aligned * NUM_ENTITIES; + let mat_total = mat_aligned * NUM_ENTITIES; + let mut cam_staging = vec![0u8; cam_total]; + let mut mat_staging = vec![0u8; mat_total]; + + for i in 0..NUM_ENTITIES { + let cam_uniform = CameraUniform { + view_proj: view_proj.cols, + model: state.models[i].cols, + camera_pos: cam_pos, + _padding: 0.0, + }; + let bytes = bytemuck::bytes_of(&cam_uniform); + let offset = i * cam_aligned; + cam_staging[offset..offset + bytes.len()].copy_from_slice(bytes); + + let (color, metallic, roughness) = state.materials[i]; + let mat_uniform = + MaterialUniform::with_params(color, metallic, roughness); + let bytes = bytemuck::bytes_of(&mat_uniform); + let offset = i * mat_aligned; + mat_staging[offset..offset + bytes.len()].copy_from_slice(bytes); + } + + state + .gpu + .queue + .write_buffer(&state.camera_buffer, 0, &cam_staging); + state + .gpu + .queue + .write_buffer(&state.material_buffer, 0, &mat_staging); + + // Update shadow uniform with light VP + let shadow_uniform = ShadowUniform { + light_view_proj: light_vp.cols, + shadow_map_size: SHADOW_MAP_SIZE as f32, + shadow_bias: 0.005, + _padding: [0.0; 2], + }; + state.gpu.queue.write_buffer( + &state.shadow_uniform_buffer, + 0, + bytemuck::cast_slice(&[shadow_uniform]), + ); + + // Write light uniform + let mut lights_uniform = LightsUniform::new(); + lights_uniform.ambient_color = [0.08, 0.08, 0.08]; + lights_uniform.add_light(LightData::directional( + [-1.0, -2.0, -1.0], + [1.0, 0.98, 0.95], + 2.0, + )); + state.gpu.queue.write_buffer( + &state.light_buffer, + 0, + bytemuck::cast_slice(&[lights_uniform]), + ); + + // ----- Render ----- + let output = match state.gpu.surface.get_current_texture() { + Ok(t) => t, + Err(wgpu::SurfaceError::Lost) => { + let (w, h) = state.window.inner_size(); + state.gpu.resize(w, h); + return; + } + Err(wgpu::SurfaceError::OutOfMemory) => { + event_loop.exit(); + return; + } + Err(_) => return, + }; + + let color_view = + output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = state.gpu.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("Survivor Encoder"), + }, + ); + + // ===== Pass 1: Shadow ===== + { + let mut shadow_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Shadow Pass"), + color_attachments: &[], + depth_stencil_attachment: Some( + wgpu::RenderPassDepthStencilAttachment { + view: &state.shadow_map.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }, + ), + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + shadow_pass.set_pipeline(&state.shadow_pipeline); + + for (i, entity) in state.entities.iter().enumerate() { + let offset = (i as u32) * state.shadow_pass_aligned_size; + shadow_pass + .set_bind_group(0, &state.shadow_pass_bind_group, &[offset]); + shadow_pass + .set_vertex_buffer(0, entity.mesh.vertex_buffer.slice(..)); + shadow_pass.set_index_buffer( + entity.mesh.index_buffer.slice(..), + wgpu::IndexFormat::Uint32, + ); + shadow_pass.draw_indexed(0..entity.mesh.num_indices, 0, 0..1); + } + } + + // ===== Pass 2: Color (PBR) ===== + { + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Color Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &color_view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.15, + g: 0.15, + b: 0.2, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some( + wgpu::RenderPassDepthStencilAttachment { + view: &state.gpu.depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }, + ), + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + render_pass.set_pipeline(&state.pbr_pipeline); + render_pass + .set_bind_group(1, &state.pbr_texture_bind_group, &[]); + render_pass.set_bind_group(3, &state.shadow_bind_group, &[]); + + for (i, entity) in state.entities.iter().enumerate() { + let cam_offset = (i as u32) * state.cam_aligned_size; + let mat_offset = (i as u32) * state.mat_aligned_size; + render_pass.set_bind_group( + 0, + &state.camera_light_bind_group, + &[cam_offset], + ); + render_pass.set_bind_group( + 2, + &state.material_bind_group, + &[mat_offset], + ); + render_pass + .set_vertex_buffer(0, entity.mesh.vertex_buffer.slice(..)); + render_pass.set_index_buffer( + entity.mesh.index_buffer.slice(..), + wgpu::IndexFormat::Uint32, + ); + render_pass.draw_indexed(0..entity.mesh.num_indices, 0, 0..1); + } + } + + state.gpu.queue.submit(std::iter::once(encoder.finish())); + output.present(); + } + + _ => {} + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + if let Some(state) = &self.state { + state.window.request_redraw(); + } + } +} + +fn main() { + env_logger::init(); + let event_loop = EventLoop::new().unwrap(); + let mut app = SurvivorApp { state: None }; + event_loop.run_app(&mut app).unwrap(); +}