lib.rs (42881B)
1 use glam::{Mat4, Vec2, Vec3, Vec4}; 2 3 use crate::material::make_material_gpudata; 4 use std::collections::HashMap; 5 use std::num::NonZeroU64; 6 7 mod camera; 8 mod ibl; 9 mod material; 10 mod model; 11 mod texture; 12 mod world; 13 14 #[cfg(feature = "egui")] 15 pub mod egui; 16 17 pub use camera::{ArcballController, Camera, FlyController, ThirdPersonController}; 18 pub use material::{MaterialGpu, MaterialUniform}; 19 pub use model::{Aabb, Mesh, Model, ModelData, ModelDraw, Vertex}; 20 pub use texture::upload_rgba8_texture_2d; 21 pub use world::{Node, NodeId, ObjectId, Transform, World}; 22 23 /// Active camera controller mode. 24 pub enum CameraMode { 25 Fly(camera::FlyController), 26 ThirdPerson(camera::ThirdPersonController), 27 } 28 29 #[repr(C)] 30 #[derive(Debug, Copy, Clone, bytemuck::NoUninit, bytemuck::Zeroable)] 31 struct ObjectUniform { 32 model: Mat4, 33 normal: Mat4, // inverse-transpose(model) 34 } 35 36 impl ObjectUniform { 37 fn from_model(model: Mat4) -> Self { 38 Self { 39 model, 40 normal: model.inverse().transpose(), 41 } 42 } 43 } 44 45 const MAX_SCENE_OBJECTS: usize = 256; 46 47 struct DynamicObjectBuffer { 48 buffer: wgpu::Buffer, 49 bindgroup: wgpu::BindGroup, 50 stride: u64, 51 } 52 53 #[repr(C)] 54 #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 55 struct Globals { 56 // 0..16 57 time: f32, 58 _pad0: f32, 59 resolution: Vec2, // 8 bytes, finishes first 16-byte slot 60 61 // 16..32 62 cam_pos: Vec3, // takes 12, but aligned to 16 63 _pad3: f32, // fills the last 4 bytes of this 16-byte slot nicely 64 65 // 32..48 66 light_dir: Vec3, 67 _pad1: f32, 68 69 // 48..64 70 light_color: Vec3, 71 _pad2: f32, 72 73 // 64..80 74 fill_light_dir: Vec3, 75 _pad4: f32, 76 77 // 80..96 78 fill_light_color: Vec3, 79 _pad5: f32, 80 81 // 96..160 82 view_proj: Mat4, 83 84 // 160..224 85 inv_view_proj: Mat4, 86 87 // 224..288 88 light_view_proj: Mat4, 89 } 90 91 impl Globals { 92 fn set_camera(&mut self, w: f32, h: f32, camera: &Camera) { 93 self.cam_pos = camera.eye; 94 self.view_proj = camera.view_proj(w, h); 95 self.inv_view_proj = self.view_proj.inverse(); 96 } 97 } 98 99 struct GpuData<R> { 100 data: R, 101 buffer: wgpu::Buffer, 102 bindgroup: wgpu::BindGroup, 103 } 104 105 const SHADOW_MAP_SIZE: u32 = 2048; 106 107 pub struct Renderer { 108 size: (u32, u32), 109 110 /// To propery resize we need a device. Provide a target size so 111 /// we can dynamically resize next time get one. 112 target_size: (u32, u32), 113 114 model_ids: u64, 115 116 depth_tex: wgpu::Texture, 117 depth_view: wgpu::TextureView, 118 pipeline: wgpu::RenderPipeline, 119 skybox_pipeline: wgpu::RenderPipeline, 120 grid_pipeline: wgpu::RenderPipeline, 121 shadow_pipeline: wgpu::RenderPipeline, 122 outline_pipeline: wgpu::RenderPipeline, 123 124 shadow_view: wgpu::TextureView, 125 shadow_globals_bg: wgpu::BindGroup, 126 127 world: World, 128 camera_mode: CameraMode, 129 130 globals: GpuData<Globals>, 131 object_buf: DynamicObjectBuffer, 132 material: GpuData<MaterialUniform>, 133 134 material_bgl: wgpu::BindGroupLayout, 135 136 ibl: ibl::IblData, 137 138 models: HashMap<Model, ModelData>, 139 140 start: std::time::Instant, 141 } 142 143 fn make_globals_bgl(device: &wgpu::Device) -> wgpu::BindGroupLayout { 144 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 145 label: Some("globals_bgl"), 146 entries: &[ 147 wgpu::BindGroupLayoutEntry { 148 binding: 0, 149 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 150 ty: wgpu::BindingType::Buffer { 151 ty: wgpu::BufferBindingType::Uniform, 152 has_dynamic_offset: false, 153 min_binding_size: None, 154 }, 155 count: None, 156 }, 157 wgpu::BindGroupLayoutEntry { 158 binding: 1, 159 visibility: wgpu::ShaderStages::FRAGMENT, 160 ty: wgpu::BindingType::Texture { 161 sample_type: wgpu::TextureSampleType::Depth, 162 view_dimension: wgpu::TextureViewDimension::D2, 163 multisampled: false, 164 }, 165 count: None, 166 }, 167 wgpu::BindGroupLayoutEntry { 168 binding: 2, 169 visibility: wgpu::ShaderStages::FRAGMENT, 170 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), 171 count: None, 172 }, 173 ], 174 }) 175 } 176 177 fn make_globals_bindgroup( 178 device: &wgpu::Device, 179 layout: &wgpu::BindGroupLayout, 180 globals_buf: &wgpu::Buffer, 181 shadow_view: &wgpu::TextureView, 182 shadow_sampler: &wgpu::Sampler, 183 ) -> wgpu::BindGroup { 184 device.create_bind_group(&wgpu::BindGroupDescriptor { 185 label: Some("globals_bg"), 186 layout, 187 entries: &[ 188 wgpu::BindGroupEntry { 189 binding: 0, 190 resource: globals_buf.as_entire_binding(), 191 }, 192 wgpu::BindGroupEntry { 193 binding: 1, 194 resource: wgpu::BindingResource::TextureView(shadow_view), 195 }, 196 wgpu::BindGroupEntry { 197 binding: 2, 198 resource: wgpu::BindingResource::Sampler(shadow_sampler), 199 }, 200 ], 201 }) 202 } 203 204 fn make_global_gpudata( 205 device: &wgpu::Device, 206 width: f32, 207 height: f32, 208 camera: &Camera, 209 globals_bgl: &wgpu::BindGroupLayout, 210 shadow_view: &wgpu::TextureView, 211 shadow_sampler: &wgpu::Sampler, 212 ) -> GpuData<Globals> { 213 let view_proj = camera.view_proj(width, height); 214 let globals = Globals { 215 time: 0.0, 216 _pad0: 0.0, 217 resolution: Vec2::new(width, height), 218 cam_pos: camera.eye, 219 _pad3: 0.0, 220 // Key light: warm, from upper right (direction of light rays) 221 light_dir: Vec3::new(-0.5, -0.7, -0.3), 222 _pad1: 0.0, 223 light_color: Vec3::new(1.0, 0.98, 0.92), 224 _pad2: 0.0, 225 // Fill light: cooler, from lower left (opposite side) 226 fill_light_dir: Vec3::new(-0.7, -0.3, -0.5), 227 _pad4: 0.0, 228 fill_light_color: Vec3::new(0.5, 0.55, 0.6), 229 _pad5: 0.0, 230 view_proj, 231 inv_view_proj: view_proj.inverse(), 232 light_view_proj: Mat4::IDENTITY, 233 }; 234 235 println!("Globals size = {}", std::mem::size_of::<Globals>()); 236 237 let globals_buf = device.create_buffer(&wgpu::BufferDescriptor { 238 label: Some("globals"), 239 size: std::mem::size_of::<Globals>() as u64, 240 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 241 mapped_at_creation: false, 242 }); 243 244 let globals_bg = make_globals_bindgroup( 245 device, 246 globals_bgl, 247 &globals_buf, 248 shadow_view, 249 shadow_sampler, 250 ); 251 252 GpuData::<Globals> { 253 data: globals, 254 buffer: globals_buf, 255 bindgroup: globals_bg, 256 } 257 } 258 259 fn make_dynamic_object_buffer( 260 device: &wgpu::Device, 261 ) -> (DynamicObjectBuffer, wgpu::BindGroupLayout) { 262 // Alignment for dynamic uniform buffer offsets (typically 256) 263 let align = device.limits().min_uniform_buffer_offset_alignment as u64; 264 let obj_size = std::mem::size_of::<ObjectUniform>() as u64; 265 let stride = obj_size.div_ceil(align) * align; 266 let total_size = stride * MAX_SCENE_OBJECTS as u64; 267 268 let buffer = device.create_buffer(&wgpu::BufferDescriptor { 269 label: Some("object_dynamic"), 270 size: total_size, 271 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 272 mapped_at_creation: false, 273 }); 274 275 let object_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 276 label: Some("object_bgl"), 277 entries: &[wgpu::BindGroupLayoutEntry { 278 binding: 0, 279 visibility: wgpu::ShaderStages::VERTEX, 280 ty: wgpu::BindingType::Buffer { 281 ty: wgpu::BufferBindingType::Uniform, 282 has_dynamic_offset: true, 283 min_binding_size: NonZeroU64::new(obj_size), 284 }, 285 count: None, 286 }], 287 }); 288 289 let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { 290 label: Some("object_dynamic_bg"), 291 layout: &object_bgl, 292 entries: &[wgpu::BindGroupEntry { 293 binding: 0, 294 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { 295 buffer: &buffer, 296 offset: 0, 297 size: NonZeroU64::new(obj_size), 298 }), 299 }], 300 }); 301 302 ( 303 DynamicObjectBuffer { 304 buffer, 305 bindgroup, 306 stride, 307 }, 308 object_bgl, 309 ) 310 } 311 312 /// Ray-AABB intersection using the slab method. 313 /// Transforms the ray into the object's local space via the inverse world matrix. 314 /// Returns the distance along the ray if there's a hit. 315 fn ray_aabb(origin: Vec3, dir: Vec3, aabb: &Aabb, world: &Mat4) -> Option<f32> { 316 let inv = world.inverse(); 317 let lo = (inv * origin.extend(1.0)).truncate(); 318 let ld = (inv * dir.extend(0.0)).truncate(); 319 let t1 = (aabb.min - lo) / ld; 320 let t2 = (aabb.max - lo) / ld; 321 let tmin = t1.min(t2); 322 let tmax = t1.max(t2); 323 let enter = tmin.x.max(tmin.y).max(tmin.z); 324 let exit = tmax.x.min(tmax.y).min(tmax.z); 325 if exit >= enter.max(0.0) { 326 Some(enter.max(0.0)) 327 } else { 328 None 329 } 330 } 331 332 impl Renderer { 333 pub fn new( 334 device: &wgpu::Device, 335 queue: &wgpu::Queue, 336 format: wgpu::TextureFormat, 337 size: (u32, u32), 338 ) -> Self { 339 let (width, height) = size; 340 341 let eye = Vec3::new(0.0, 16.0, 24.0); 342 let target = Vec3::new(0.0, 0.0, 0.0); 343 let camera = Camera::new(eye, target); 344 345 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 346 label: Some("shader"), 347 source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), 348 }); 349 350 let (_shadow_tex, shadow_view, shadow_sampler) = create_shadow_map(device); 351 let globals_bgl = make_globals_bgl(device); 352 let globals = make_global_gpudata( 353 device, 354 width as f32, 355 height as f32, 356 &camera, 357 &globals_bgl, 358 &shadow_view, 359 &shadow_sampler, 360 ); 361 let (object_buf, object_bgl) = make_dynamic_object_buffer(device); 362 let (material, material_bgl) = make_material_gpudata(device, queue); 363 364 let ibl_bgl = ibl::create_ibl_bind_group_layout(device); 365 let ibl = ibl::load_hdr_ibl_from_bytes( 366 device, 367 queue, 368 &ibl_bgl, 369 include_bytes!("../assets/venice_sunset_1k.hdr"), 370 ) 371 .expect("failed to load HDR environment map"); 372 373 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 374 label: Some("pipeline_layout"), 375 bind_group_layouts: &[&globals_bgl, &object_bgl, &material_bgl, &ibl_bgl], 376 push_constant_ranges: &[], 377 }); 378 379 /* 380 let pipeline_cache = unsafe { 381 device.create_pipeline_cache(&wgpu::PipelineCacheDescriptor { 382 label: Some("pipeline_cache"), 383 data: None, 384 fallback: true, 385 }) 386 }; 387 */ 388 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 389 label: Some("pipeline"), 390 //cache: Some(&pipeline_cache), 391 cache: None, 392 layout: Some(&pipeline_layout), 393 vertex: wgpu::VertexState { 394 module: &shader, 395 compilation_options: wgpu::PipelineCompilationOptions::default(), 396 entry_point: Some("vs_main"), 397 buffers: &[Vertex::desc()], 398 }, 399 fragment: Some(wgpu::FragmentState { 400 module: &shader, 401 compilation_options: wgpu::PipelineCompilationOptions::default(), 402 entry_point: Some("fs_main"), 403 targets: &[Some(wgpu::ColorTargetState { 404 format, 405 blend: Some(wgpu::BlendState::REPLACE), 406 write_mask: wgpu::ColorWrites::ALL, 407 })], 408 }), 409 primitive: wgpu::PrimitiveState { 410 topology: wgpu::PrimitiveTopology::TriangleList, 411 ..Default::default() 412 }, 413 depth_stencil: Some(wgpu::DepthStencilState { 414 format: wgpu::TextureFormat::Depth24Plus, 415 depth_write_enabled: true, 416 depth_compare: wgpu::CompareFunction::Less, 417 stencil: wgpu::StencilState::default(), 418 bias: wgpu::DepthBiasState::default(), 419 }), 420 multisample: wgpu::MultisampleState::default(), 421 multiview: None, 422 }); 423 424 // Skybox pipeline 425 let skybox_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 426 label: Some("skybox_shader"), 427 source: wgpu::ShaderSource::Wgsl(include_str!("skybox.wgsl").into()), 428 }); 429 430 let skybox_pipeline_layout = 431 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 432 label: Some("skybox_pipeline_layout"), 433 bind_group_layouts: &[&globals_bgl, &object_bgl, &material_bgl, &ibl_bgl], 434 push_constant_ranges: &[], 435 }); 436 437 let skybox_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 438 label: Some("skybox_pipeline"), 439 cache: None, 440 layout: Some(&skybox_pipeline_layout), 441 vertex: wgpu::VertexState { 442 module: &skybox_shader, 443 compilation_options: wgpu::PipelineCompilationOptions::default(), 444 entry_point: Some("vs_main"), 445 buffers: &[], // No vertex buffers - procedural fullscreen triangle 446 }, 447 fragment: Some(wgpu::FragmentState { 448 module: &skybox_shader, 449 compilation_options: wgpu::PipelineCompilationOptions::default(), 450 entry_point: Some("fs_main"), 451 targets: &[Some(wgpu::ColorTargetState { 452 format, 453 blend: Some(wgpu::BlendState::REPLACE), 454 write_mask: wgpu::ColorWrites::ALL, 455 })], 456 }), 457 primitive: wgpu::PrimitiveState { 458 topology: wgpu::PrimitiveTopology::TriangleList, 459 ..Default::default() 460 }, 461 depth_stencil: Some(wgpu::DepthStencilState { 462 format: wgpu::TextureFormat::Depth24Plus, 463 depth_write_enabled: true, 464 depth_compare: wgpu::CompareFunction::LessEqual, 465 stencil: wgpu::StencilState::default(), 466 bias: wgpu::DepthBiasState::default(), 467 }), 468 multisample: wgpu::MultisampleState::default(), 469 multiview: None, 470 }); 471 472 // Grid pipeline (infinite ground plane) 473 let grid_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 474 label: Some("grid_shader"), 475 source: wgpu::ShaderSource::Wgsl(include_str!("grid.wgsl").into()), 476 }); 477 478 let grid_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 479 label: Some("grid_pipeline_layout"), 480 bind_group_layouts: &[&globals_bgl, &object_bgl, &material_bgl, &ibl_bgl], 481 push_constant_ranges: &[], 482 }); 483 484 let grid_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 485 label: Some("grid_pipeline"), 486 cache: None, 487 layout: Some(&grid_pipeline_layout), 488 vertex: wgpu::VertexState { 489 module: &grid_shader, 490 compilation_options: wgpu::PipelineCompilationOptions::default(), 491 entry_point: Some("vs_main"), 492 buffers: &[], 493 }, 494 fragment: Some(wgpu::FragmentState { 495 module: &grid_shader, 496 compilation_options: wgpu::PipelineCompilationOptions::default(), 497 entry_point: Some("fs_main"), 498 targets: &[Some(wgpu::ColorTargetState { 499 format, 500 blend: Some(wgpu::BlendState::ALPHA_BLENDING), 501 write_mask: wgpu::ColorWrites::ALL, 502 })], 503 }), 504 primitive: wgpu::PrimitiveState { 505 topology: wgpu::PrimitiveTopology::TriangleList, 506 ..Default::default() 507 }, 508 depth_stencil: Some(wgpu::DepthStencilState { 509 format: wgpu::TextureFormat::Depth24Plus, 510 depth_write_enabled: true, 511 depth_compare: wgpu::CompareFunction::Less, 512 stencil: wgpu::StencilState::default(), 513 bias: wgpu::DepthBiasState::default(), 514 }), 515 multisample: wgpu::MultisampleState::default(), 516 multiview: None, 517 }); 518 519 // Shadow depth pipeline (depth-only, no fragment stage) 520 // Uses a separate globals BGL without the shadow texture to avoid 521 // the resource conflict (shadow tex as both attachment and binding). 522 let shadow_globals_bgl = 523 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 524 label: Some("shadow_globals_bgl"), 525 entries: &[wgpu::BindGroupLayoutEntry { 526 binding: 0, 527 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 528 ty: wgpu::BindingType::Buffer { 529 ty: wgpu::BufferBindingType::Uniform, 530 has_dynamic_offset: false, 531 min_binding_size: None, 532 }, 533 count: None, 534 }], 535 }); 536 537 let shadow_globals_bg = device.create_bind_group(&wgpu::BindGroupDescriptor { 538 label: Some("shadow_globals_bg"), 539 layout: &shadow_globals_bgl, 540 entries: &[wgpu::BindGroupEntry { 541 binding: 0, 542 resource: globals.buffer.as_entire_binding(), 543 }], 544 }); 545 546 let shadow_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 547 label: Some("shadow_shader"), 548 source: wgpu::ShaderSource::Wgsl(include_str!("shadow.wgsl").into()), 549 }); 550 551 let shadow_pipeline_layout = 552 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 553 label: Some("shadow_pipeline_layout"), 554 bind_group_layouts: &[&shadow_globals_bgl, &object_bgl], 555 push_constant_ranges: &[], 556 }); 557 558 let shadow_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 559 label: Some("shadow_pipeline"), 560 cache: None, 561 layout: Some(&shadow_pipeline_layout), 562 vertex: wgpu::VertexState { 563 module: &shadow_shader, 564 compilation_options: wgpu::PipelineCompilationOptions::default(), 565 entry_point: Some("vs_main"), 566 buffers: &[Vertex::desc()], 567 }, 568 fragment: None, // depth-only pass 569 primitive: wgpu::PrimitiveState { 570 topology: wgpu::PrimitiveTopology::TriangleList, 571 ..Default::default() 572 }, 573 depth_stencil: Some(wgpu::DepthStencilState { 574 format: wgpu::TextureFormat::Depth32Float, 575 depth_write_enabled: true, 576 depth_compare: wgpu::CompareFunction::Less, 577 stencil: wgpu::StencilState::default(), 578 bias: wgpu::DepthBiasState { 579 constant: 2, 580 slope_scale: 2.0, 581 clamp: 0.0, 582 }, 583 }), 584 multisample: wgpu::MultisampleState::default(), 585 multiview: None, 586 }); 587 588 // Outline pipeline (inverted hull, front-face culling) 589 let outline_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 590 label: Some("outline_shader"), 591 source: wgpu::ShaderSource::Wgsl(include_str!("outline.wgsl").into()), 592 }); 593 594 let outline_pipeline_layout = 595 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 596 label: Some("outline_pipeline_layout"), 597 bind_group_layouts: &[&shadow_globals_bgl, &object_bgl], 598 push_constant_ranges: &[], 599 }); 600 601 let outline_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 602 label: Some("outline_pipeline"), 603 cache: None, 604 layout: Some(&outline_pipeline_layout), 605 vertex: wgpu::VertexState { 606 module: &outline_shader, 607 compilation_options: wgpu::PipelineCompilationOptions::default(), 608 entry_point: Some("vs_main"), 609 buffers: &[Vertex::desc()], 610 }, 611 fragment: Some(wgpu::FragmentState { 612 module: &outline_shader, 613 compilation_options: wgpu::PipelineCompilationOptions::default(), 614 entry_point: Some("fs_main"), 615 targets: &[Some(wgpu::ColorTargetState { 616 format, 617 blend: Some(wgpu::BlendState::REPLACE), 618 write_mask: wgpu::ColorWrites::ALL, 619 })], 620 }), 621 primitive: wgpu::PrimitiveState { 622 topology: wgpu::PrimitiveTopology::TriangleList, 623 cull_mode: Some(wgpu::Face::Front), 624 ..Default::default() 625 }, 626 depth_stencil: Some(wgpu::DepthStencilState { 627 format: wgpu::TextureFormat::Depth24Plus, 628 depth_write_enabled: true, 629 depth_compare: wgpu::CompareFunction::Less, 630 stencil: wgpu::StencilState::default(), 631 bias: wgpu::DepthBiasState::default(), 632 }), 633 multisample: wgpu::MultisampleState::default(), 634 multiview: None, 635 }); 636 637 let (depth_tex, depth_view) = create_depth(device, width, height); 638 639 /* TODO: move to example 640 let model = load_gltf_model( 641 &device, 642 &queue, 643 &material_bgl, 644 "/home/jb55/var/models/ironwood/ironwood.glb", 645 ) 646 .unwrap(); 647 */ 648 649 let model_ids = 0; 650 651 let world = World::new(camera); 652 653 let camera_mode = CameraMode::Fly(camera::FlyController::from_camera(&world.camera)); 654 655 Self { 656 world, 657 camera_mode, 658 target_size: size, 659 model_ids, 660 size, 661 pipeline, 662 skybox_pipeline, 663 grid_pipeline, 664 shadow_pipeline, 665 outline_pipeline, 666 shadow_view, 667 shadow_globals_bg, 668 globals, 669 object_buf, 670 material, 671 material_bgl, 672 ibl, 673 models: HashMap::new(), 674 depth_tex, 675 depth_view, 676 start: std::time::Instant::now(), 677 } 678 } 679 680 pub fn size(&self) -> (u32, u32) { 681 self.size 682 } 683 684 fn globals_mut(&mut self) -> &mut Globals { 685 &mut self.globals.data 686 } 687 688 /// Load a glTF model from disk. Returns a handle that can be placed in 689 /// the scene with [`place_object`]. 690 pub fn load_gltf_model( 691 &mut self, 692 device: &wgpu::Device, 693 queue: &wgpu::Queue, 694 path: impl AsRef<std::path::Path>, 695 ) -> Result<Model, gltf::Error> { 696 let model_data = crate::model::load_gltf_model(device, queue, &self.material_bgl, path)?; 697 698 self.model_ids += 1; 699 let id = Model { id: self.model_ids }; 700 701 self.models.insert(id, model_data); 702 703 Ok(id) 704 } 705 706 /// Register a procedurally-generated model. Returns a handle that can 707 /// be placed in the scene with [`place_object`]. 708 pub fn insert_model(&mut self, model_data: ModelData) -> Model { 709 self.model_ids += 1; 710 let id = Model { id: self.model_ids }; 711 self.models.insert(id, model_data); 712 id 713 } 714 715 /// Create a PBR material from a base color texture view. 716 /// Used for procedural geometry (tilemaps, etc.). 717 pub fn create_material( 718 &self, 719 device: &wgpu::Device, 720 queue: &wgpu::Queue, 721 sampler: &wgpu::Sampler, 722 basecolor: &wgpu::TextureView, 723 uniform: MaterialUniform, 724 ) -> MaterialGpu { 725 let default_mr = texture::upload_rgba8_texture_2d( 726 device, 727 queue, 728 1, 729 1, 730 &[0, 255, 0, 255], 731 wgpu::TextureFormat::Rgba8Unorm, 732 "tilemap_mr", 733 ); 734 let default_normal = texture::upload_rgba8_texture_2d( 735 device, 736 queue, 737 1, 738 1, 739 &[128, 128, 255, 255], 740 wgpu::TextureFormat::Rgba8Unorm, 741 "tilemap_normal", 742 ); 743 model::make_material_gpu( 744 device, 745 queue, 746 &self.material_bgl, 747 sampler, 748 basecolor, 749 &default_mr, 750 &default_normal, 751 uniform, 752 ) 753 } 754 755 /// Place a loaded model in the scene with the given transform. 756 pub fn place_object(&mut self, model: Model, transform: Transform) -> ObjectId { 757 self.world.add_object(model, transform) 758 } 759 760 /// Place a loaded model as a child of an existing scene node. 761 /// The transform is local (relative to the parent). 762 pub fn place_object_with_parent( 763 &mut self, 764 model: Model, 765 transform: Transform, 766 parent: ObjectId, 767 ) -> ObjectId { 768 self.world.create_renderable(model, transform, Some(parent)) 769 } 770 771 /// Set or clear the parent of a scene object. 772 /// When parented, the object's transform becomes local to the parent. 773 pub fn set_parent(&mut self, id: ObjectId, parent: Option<ObjectId>) -> bool { 774 self.world.set_parent(id, parent) 775 } 776 777 /// Remove an object from the scene. 778 pub fn remove_object(&mut self, id: ObjectId) -> bool { 779 self.world.remove_object(id) 780 } 781 782 /// Update the transform of a placed object. 783 pub fn update_object_transform(&mut self, id: ObjectId, transform: Transform) -> bool { 784 self.world.update_transform(id, transform) 785 } 786 787 /// Perform a resize if the target size is not the same as size 788 pub fn set_target_size(&mut self, size: (u32, u32)) { 789 self.target_size = size; 790 } 791 792 pub fn resize(&mut self, device: &wgpu::Device) { 793 if self.target_size == self.size { 794 return; 795 } 796 797 let (width, height) = self.target_size; 798 let w = width as f32; 799 let h = height as f32; 800 801 self.size = self.target_size; 802 803 self.globals.data.resolution = Vec2::new(w, h); 804 self.globals.data.set_camera(w, h, &self.world.camera); 805 806 let (depth_tex, depth_view) = create_depth(device, width, height); 807 self.depth_tex = depth_tex; 808 self.depth_view = depth_view; 809 } 810 811 pub fn focus_model(&mut self, model: Model) { 812 let Some(md) = self.models.get(&model) else { 813 return; 814 }; 815 816 let (w, h) = self.size; 817 let w = w as f32; 818 let h = h as f32; 819 820 let aspect = w / h.max(1.0); 821 822 self.world.camera = Camera::fit_to_aabb( 823 md.bounds.min, 824 md.bounds.max, 825 aspect, 826 45_f32.to_radians(), 827 1.2, 828 ); 829 830 // Sync controller to new camera position 831 self.camera_mode = CameraMode::Fly(camera::FlyController::from_camera(&self.world.camera)); 832 833 self.globals.data.set_camera(w, h, &self.world.camera); 834 } 835 836 /// Set or clear which object shows a selection outline. 837 pub fn set_selected(&mut self, id: Option<ObjectId>) { 838 self.world.selected_object = id; 839 } 840 841 /// Get the axis-aligned bounding box for a loaded model. 842 pub fn model_bounds(&self, model: Model) -> Option<Aabb> { 843 self.models.get(&model).map(|md| md.bounds) 844 } 845 846 /// Get the cached world matrix for a scene object. 847 pub fn world_matrix(&self, id: ObjectId) -> Option<glam::Mat4> { 848 self.world.world_matrix(id) 849 } 850 851 /// Get the parent of a scene object, if it has one. 852 pub fn node_parent(&self, id: ObjectId) -> Option<ObjectId> { 853 self.world.node_parent(id) 854 } 855 856 /// Convert screen coordinates (relative to viewport) to a world-space ray. 857 /// Returns (origin, direction). 858 fn screen_to_ray(&self, screen_x: f32, screen_y: f32) -> (Vec3, Vec3) { 859 let (w, h) = self.target_size; 860 let ndc_x = (screen_x / w as f32) * 2.0 - 1.0; 861 let ndc_y = 1.0 - (screen_y / h as f32) * 2.0; 862 let vp = self.world.camera.view_proj(w as f32, h as f32); 863 let inv_vp = vp.inverse(); 864 let near4 = inv_vp * Vec4::new(ndc_x, ndc_y, 0.0, 1.0); 865 let far4 = inv_vp * Vec4::new(ndc_x, ndc_y, 1.0, 1.0); 866 let near = near4.truncate() / near4.w; 867 let far = far4.truncate() / far4.w; 868 (near, (far - near).normalize()) 869 } 870 871 /// Pick the closest scene object at the given screen coordinates. 872 /// Coordinates are relative to the viewport (0,0 = top-left). 873 pub fn pick(&self, screen_x: f32, screen_y: f32) -> Option<ObjectId> { 874 let (origin, dir) = self.screen_to_ray(screen_x, screen_y); 875 let mut closest: Option<(ObjectId, f32)> = None; 876 for &id in self.world.renderables() { 877 let model = match self.world.node_model(id) { 878 Some(m) => m, 879 None => continue, 880 }; 881 let aabb = match self.model_bounds(model) { 882 Some(a) => a, 883 None => continue, 884 }; 885 let world = match self.world.world_matrix(id) { 886 Some(w) => w, 887 None => continue, 888 }; 889 if let Some(t) = ray_aabb(origin, dir, &aabb, &world) 890 && closest.is_none_or(|(_, d)| t < d) 891 { 892 closest = Some((id, t)); 893 } 894 } 895 closest.map(|(id, _)| id) 896 } 897 898 /// Unproject screen coordinates to a point on a horizontal plane at the given Y height. 899 /// Useful for constraining object drag to the ground plane. 900 pub fn unproject_to_plane(&self, screen_x: f32, screen_y: f32, plane_y: f32) -> Option<Vec3> { 901 let (origin, dir) = self.screen_to_ray(screen_x, screen_y); 902 if dir.y.abs() < 1e-6 { 903 return None; 904 } 905 let t = (plane_y - origin.y) / dir.y; 906 if t < 0.0 { 907 return None; 908 } 909 Some(origin + dir * t) 910 } 911 912 /// Handle mouse drag for camera look/orbit. 913 pub fn on_mouse_drag(&mut self, delta_x: f32, delta_y: f32) { 914 match &mut self.camera_mode { 915 CameraMode::Fly(fly) => fly.on_mouse_look(delta_x, delta_y), 916 CameraMode::ThirdPerson(tp) => tp.on_mouse_look(delta_x, delta_y), 917 } 918 } 919 920 /// Handle scroll for camera speed/zoom. 921 pub fn on_scroll(&mut self, delta: f32) { 922 match &mut self.camera_mode { 923 CameraMode::Fly(fly) => fly.on_scroll(delta), 924 CameraMode::ThirdPerson(tp) => tp.on_scroll(delta), 925 } 926 } 927 928 /// Move the camera or avatar. forward/right/up are signed. 929 pub fn process_movement(&mut self, forward: f32, right: f32, up: f32, dt: f32) { 930 match &mut self.camera_mode { 931 CameraMode::Fly(fly) => fly.process_movement(forward, right, up, dt), 932 CameraMode::ThirdPerson(tp) => tp.process_movement(forward, right, up, dt), 933 } 934 } 935 936 /// Switch to third-person camera mode with avatar at the given position. 937 pub fn set_third_person_mode(&mut self, avatar_position: Vec3) { 938 let mut tp = camera::ThirdPersonController::from_camera(&self.world.camera); 939 tp.avatar_position = avatar_position; 940 self.camera_mode = CameraMode::ThirdPerson(tp); 941 } 942 943 /// Switch to fly camera mode. 944 pub fn set_fly_mode(&mut self) { 945 self.camera_mode = CameraMode::Fly(camera::FlyController::from_camera(&self.world.camera)); 946 } 947 948 /// Get the avatar position (None if not in third-person mode). 949 pub fn avatar_position(&self) -> Option<Vec3> { 950 match &self.camera_mode { 951 CameraMode::ThirdPerson(tp) => Some(tp.avatar_position), 952 _ => None, 953 } 954 } 955 956 /// Get the avatar yaw (None if not in third-person mode). 957 pub fn avatar_yaw(&self) -> Option<f32> { 958 match &self.camera_mode { 959 CameraMode::ThirdPerson(tp) => Some(tp.avatar_yaw), 960 _ => None, 961 } 962 } 963 964 pub fn update(&mut self) { 965 self.globals_mut().time = self.start.elapsed().as_secs_f32(); 966 967 // Update camera from active controller 968 match &self.camera_mode { 969 CameraMode::Fly(fly) => fly.update_camera(&mut self.world.camera), 970 CameraMode::ThirdPerson(tp) => tp.update_camera(&mut self.world.camera), 971 } 972 let (w, h) = self.size; 973 self.globals 974 .data 975 .set_camera(w as f32, h as f32, &self.world.camera); 976 977 //let t = self.globals_mut().time * 0.3; 978 //self.globals_mut().light_dir = Vec3::new(t_slow.cos() * 0.6, 0.7, t_slow.sin() * 0.6); 979 980 // Recompute dirty world transforms before rendering 981 self.world.update_world_transforms(); 982 983 // Compute light space matrix for shadow mapping 984 let light_dir = self.globals.data.light_dir.normalize(); 985 let light_pos = -light_dir * 30.0; // Position light 30m back along its direction 986 let light_view = Mat4::look_at_rh(light_pos, Vec3::ZERO, Vec3::Y); 987 let extent = 15.0; // 30m x 30m ortho frustum 988 let light_proj = Mat4::orthographic_rh(-extent, extent, -extent, extent, 0.1, 80.0); 989 self.globals.data.light_view_proj = light_proj * light_view; 990 } 991 992 pub fn prepare(&self, queue: &wgpu::Queue) { 993 write_gpu_data(queue, &self.globals); 994 995 // Write per-object transforms into the dynamic buffer 996 for (i, &node_id) in self.world.renderables().iter().enumerate() { 997 let node = self.world.get_node(node_id).unwrap(); 998 let obj_uniform = ObjectUniform::from_model(node.world_matrix()); 999 let offset = i as u64 * self.object_buf.stride; 1000 queue.write_buffer( 1001 &self.object_buf.buffer, 1002 offset, 1003 bytemuck::bytes_of(&obj_uniform), 1004 ); 1005 } 1006 } 1007 1008 /// Record the shadow depth pass onto the given command encoder. 1009 /// Must be called before the main render pass. 1010 pub fn render_shadow(&self, encoder: &mut wgpu::CommandEncoder) { 1011 let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 1012 label: Some("shadow_pass"), 1013 color_attachments: &[], 1014 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { 1015 view: &self.shadow_view, 1016 depth_ops: Some(wgpu::Operations { 1017 load: wgpu::LoadOp::Clear(1.0), 1018 store: wgpu::StoreOp::Store, 1019 }), 1020 stencil_ops: None, 1021 }), 1022 occlusion_query_set: None, 1023 timestamp_writes: None, 1024 }); 1025 1026 shadow_pass.set_pipeline(&self.shadow_pipeline); 1027 shadow_pass.set_bind_group(0, &self.shadow_globals_bg, &[]); 1028 1029 for (i, &node_id) in self.world.renderables().iter().enumerate() { 1030 let node = self.world.get_node(node_id).unwrap(); 1031 let model_handle = node.model.unwrap(); 1032 let Some(model_data) = self.models.get(&model_handle) else { 1033 continue; 1034 }; 1035 let dynamic_offset = (i as u64 * self.object_buf.stride) as u32; 1036 shadow_pass.set_bind_group(1, &self.object_buf.bindgroup, &[dynamic_offset]); 1037 1038 for d in &model_data.draws { 1039 shadow_pass.set_vertex_buffer(0, d.mesh.vert_buf.slice(..)); 1040 shadow_pass.set_index_buffer(d.mesh.ind_buf.slice(..), wgpu::IndexFormat::Uint32); 1041 shadow_pass.draw_indexed(0..d.mesh.num_indices, 0, 0..1); 1042 } 1043 } 1044 } 1045 1046 pub fn render(&self, frame: &wgpu::TextureView, encoder: &mut wgpu::CommandEncoder) { 1047 self.render_shadow(encoder); 1048 1049 // Main render pass 1050 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 1051 label: Some("rpass"), 1052 color_attachments: &[Some(wgpu::RenderPassColorAttachment { 1053 view: frame, 1054 resolve_target: None, 1055 ops: wgpu::Operations { 1056 load: wgpu::LoadOp::Clear(wgpu::Color { 1057 r: 0.00, 1058 g: 0.00, 1059 b: 0.00, 1060 a: 1.0, 1061 }), 1062 store: wgpu::StoreOp::Store, 1063 }, 1064 })], 1065 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { 1066 view: &self.depth_view, 1067 depth_ops: Some(wgpu::Operations { 1068 load: wgpu::LoadOp::Clear(1.0), 1069 store: wgpu::StoreOp::Store, 1070 }), 1071 stencil_ops: None, 1072 }), 1073 occlusion_query_set: None, 1074 timestamp_writes: None, 1075 }); 1076 1077 self.render_pass(&mut rpass); 1078 } 1079 1080 pub fn render_pass(&self, rpass: &mut wgpu::RenderPass<'_>) { 1081 // 1. Draw skybox first (writes depth=1.0) 1082 rpass.set_pipeline(&self.skybox_pipeline); 1083 rpass.set_bind_group(0, &self.globals.bindgroup, &[]); 1084 rpass.set_bind_group(1, &self.object_buf.bindgroup, &[0]); // dynamic offset 0 1085 rpass.set_bind_group(2, &self.material.bindgroup, &[]); // unused but required by layout 1086 rpass.set_bind_group(3, &self.ibl.bindgroup, &[]); 1087 rpass.draw(0..3, 0..1); 1088 1089 // 2. Draw ground grid (alpha-blended over skybox, writes depth) 1090 rpass.set_pipeline(&self.grid_pipeline); 1091 rpass.set_bind_group(0, &self.globals.bindgroup, &[]); 1092 rpass.set_bind_group(1, &self.object_buf.bindgroup, &[0]); 1093 rpass.set_bind_group(2, &self.material.bindgroup, &[]); 1094 rpass.set_bind_group(3, &self.ibl.bindgroup, &[]); 1095 rpass.draw(0..3, 0..1); 1096 1097 // 3. Draw all scene objects 1098 rpass.set_pipeline(&self.pipeline); 1099 rpass.set_bind_group(0, &self.globals.bindgroup, &[]); 1100 rpass.set_bind_group(3, &self.ibl.bindgroup, &[]); 1101 1102 for (i, &node_id) in self.world.renderables().iter().enumerate() { 1103 let node = self.world.get_node(node_id).unwrap(); 1104 let model_handle = node.model.unwrap(); 1105 let Some(model_data) = self.models.get(&model_handle) else { 1106 continue; 1107 }; 1108 1109 let dynamic_offset = (i as u64 * self.object_buf.stride) as u32; 1110 rpass.set_bind_group(1, &self.object_buf.bindgroup, &[dynamic_offset]); 1111 1112 for d in &model_data.draws { 1113 rpass.set_bind_group(2, &model_data.materials[d.material_index].bindgroup, &[]); 1114 rpass.set_vertex_buffer(0, d.mesh.vert_buf.slice(..)); 1115 rpass.set_index_buffer(d.mesh.ind_buf.slice(..), wgpu::IndexFormat::Uint32); 1116 rpass.draw_indexed(0..d.mesh.num_indices, 0, 0..1); 1117 } 1118 } 1119 1120 // 4. Draw selection outline for selected object 1121 if let Some(selected_id) = self.world.selected_object 1122 && let Some(sel_idx) = self 1123 .world 1124 .renderables() 1125 .iter() 1126 .position(|&id| id == selected_id) 1127 { 1128 let node = self.world.get_node(selected_id).unwrap(); 1129 let model_handle = node.model.unwrap(); 1130 if let Some(model_data) = self.models.get(&model_handle) { 1131 rpass.set_pipeline(&self.outline_pipeline); 1132 rpass.set_bind_group(0, &self.shadow_globals_bg, &[]); 1133 let dynamic_offset = (sel_idx as u64 * self.object_buf.stride) as u32; 1134 rpass.set_bind_group(1, &self.object_buf.bindgroup, &[dynamic_offset]); 1135 1136 for d in &model_data.draws { 1137 rpass.set_vertex_buffer(0, d.mesh.vert_buf.slice(..)); 1138 rpass.set_index_buffer(d.mesh.ind_buf.slice(..), wgpu::IndexFormat::Uint32); 1139 rpass.draw_indexed(0..d.mesh.num_indices, 0, 0..1); 1140 } 1141 } 1142 } 1143 } 1144 } 1145 1146 fn write_gpu_data<R: bytemuck::NoUninit>(queue: &wgpu::Queue, state: &GpuData<R>) { 1147 //state.staging.clear(); 1148 //let mut storage = encase::UniformBuffer::new(&mut state.staging); 1149 //storage.write(&state.data).unwrap(); 1150 queue.write_buffer(&state.buffer, 0, bytemuck::bytes_of(&state.data)); 1151 } 1152 1153 fn create_depth( 1154 device: &wgpu::Device, 1155 width: u32, 1156 height: u32, 1157 ) -> (wgpu::Texture, wgpu::TextureView) { 1158 assert!(width < 8192); 1159 assert!(height < 8192); 1160 let size = wgpu::Extent3d { 1161 width, 1162 height, 1163 depth_or_array_layers: 1, 1164 }; 1165 let tex = device.create_texture(&wgpu::TextureDescriptor { 1166 label: Some("depth"), 1167 size, 1168 mip_level_count: 1, 1169 sample_count: 1, 1170 dimension: wgpu::TextureDimension::D2, 1171 format: wgpu::TextureFormat::Depth24Plus, 1172 usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 1173 view_formats: &[], 1174 }); 1175 let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); 1176 (tex, view) 1177 } 1178 1179 fn create_shadow_map(device: &wgpu::Device) -> (wgpu::Texture, wgpu::TextureView, wgpu::Sampler) { 1180 let size = wgpu::Extent3d { 1181 width: SHADOW_MAP_SIZE, 1182 height: SHADOW_MAP_SIZE, 1183 depth_or_array_layers: 1, 1184 }; 1185 let tex = device.create_texture(&wgpu::TextureDescriptor { 1186 label: Some("shadow_map"), 1187 size, 1188 mip_level_count: 1, 1189 sample_count: 1, 1190 dimension: wgpu::TextureDimension::D2, 1191 format: wgpu::TextureFormat::Depth32Float, 1192 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, 1193 view_formats: &[], 1194 }); 1195 let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); 1196 let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 1197 label: Some("shadow_sampler"), 1198 address_mode_u: wgpu::AddressMode::ClampToEdge, 1199 address_mode_v: wgpu::AddressMode::ClampToEdge, 1200 mag_filter: wgpu::FilterMode::Linear, 1201 min_filter: wgpu::FilterMode::Linear, 1202 compare: Some(wgpu::CompareFunction::Less), 1203 ..Default::default() 1204 }); 1205 (tex, view, sampler) 1206 }