notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }