use bytemuck::{Pod, Zeroable}; use std::{borrow::Cow, f32::consts}; use wgpu::{util::DeviceExt, AstcBlock, AstcChannel}; const IMAGE_SIZE: u32 = 128; #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] struct Vertex { pos: [f32; 3], normal: [f32; 3], } struct Entity { vertex_count: u32, vertex_buf: wgpu::Buffer, } // Note: we use the Y=up coordinate space in this example. struct Camera { screen_size: (u32, u32), angle_y: f32, angle_xz: f32, dist: f32, } const MODEL_CENTER_Y: f32 = 2.0; impl Camera { fn to_uniform_data(&self) -> [f32; 16 * 3 + 4] { let aspect = self.screen_size.0 as f32 / self.screen_size.1 as f32; let proj = glam::Mat4::perspective_rh(consts::FRAC_PI_4, aspect, 1.0, 50.0); let cam_pos = glam::Vec3::new( self.angle_xz.cos() * self.angle_y.sin() * self.dist, self.angle_xz.sin() * self.dist + MODEL_CENTER_Y, self.angle_xz.cos() * self.angle_y.cos() * self.dist, ); let view = glam::Mat4::look_at_rh( cam_pos, glam::Vec3::new(0f32, MODEL_CENTER_Y, 0.0), glam::Vec3::Y, ); let proj_inv = proj.inverse(); let mut raw = [0f32; 16 * 3 + 4]; raw[..16].copy_from_slice(&AsRef::<[f32; 16]>::as_ref(&proj)[..]); raw[16..32].copy_from_slice(&AsRef::<[f32; 16]>::as_ref(&proj_inv)[..]); raw[32..48].copy_from_slice(&AsRef::<[f32; 16]>::as_ref(&view)[..]); raw[48..51].copy_from_slice(AsRef::<[f32; 3]>::as_ref(&cam_pos)); raw[51] = 1.0; raw } } pub struct Skybox { camera: Camera, sky_pipeline: wgpu::RenderPipeline, entity_pipeline: wgpu::RenderPipeline, bind_group: wgpu::BindGroup, uniform_buf: wgpu::Buffer, entities: Vec, depth_view: wgpu::TextureView, staging_belt: wgpu::util::StagingBelt, } impl Skybox { const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus; fn create_depth_texture( config: &wgpu::SurfaceConfiguration, device: &wgpu::Device, ) -> wgpu::TextureView { let depth_texture = device.create_texture(&wgpu::TextureDescriptor { size: wgpu::Extent3d { width: config.width, height: config.height, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: Self::DEPTH_FORMAT, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, label: None, view_formats: &[], }); depth_texture.create_view(&wgpu::TextureViewDescriptor::default()) } } impl wgpu_example::framework::Example for Skybox { fn optional_features() -> wgpu::Features { wgpu::Features::TEXTURE_COMPRESSION_ASTC | wgpu::Features::TEXTURE_COMPRESSION_ETC2 | wgpu::Features::TEXTURE_COMPRESSION_BC } fn init( config: &wgpu::SurfaceConfiguration, _adapter: &wgpu::Adapter, device: &wgpu::Device, queue: &wgpu::Queue, ) -> Self { let mut entities = Vec::new(); { let source = include_bytes!("../models/teslacyberv3.0.obj"); let data = obj::ObjData::load_buf(&source[..]).unwrap(); let mut vertices = Vec::new(); for object in data.objects { for group in object.groups { vertices.clear(); for poly in group.polys { for end_index in 2..poly.0.len() { for &index in &[0, end_index - 1, end_index] { let obj::IndexTuple(position_id, _texture_id, normal_id) = poly.0[index]; vertices.push(Vertex { pos: data.position[position_id], normal: data.normal[normal_id.unwrap()], }) } } } let vertex_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Vertex"), contents: bytemuck::cast_slice(&vertices), usage: wgpu::BufferUsages::VERTEX, }); entities.push(Entity { vertex_count: vertices.len() as u32, vertex_buf, }); } } } let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: None, entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, view_dimension: wgpu::TextureViewDimension::Cube, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }); // Create the render pipeline let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: None, source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))), }); let camera = Camera { screen_size: (config.width, config.height), angle_xz: 0.2, angle_y: 0.2, dist: 20.0, }; let raw_uniforms = camera.to_uniform_data(); let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Buffer"), contents: bytemuck::cast_slice(&raw_uniforms), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, bind_group_layouts: &[&bind_group_layout], push_constant_ranges: &[], }); // Create the render pipelines let sky_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Sky"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, entry_point: "vs_sky", buffers: &[], }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_sky", targets: &[Some(config.view_formats[0].into())], }), primitive: wgpu::PrimitiveState { front_face: wgpu::FrontFace::Cw, ..Default::default() }, depth_stencil: Some(wgpu::DepthStencilState { format: Self::DEPTH_FORMAT, depth_write_enabled: false, depth_compare: wgpu::CompareFunction::LessEqual, stencil: wgpu::StencilState::default(), bias: wgpu::DepthBiasState::default(), }), multisample: wgpu::MultisampleState::default(), multiview: None, }); let entity_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Entity"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, entry_point: "vs_entity", buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as wgpu::BufferAddress, step_mode: wgpu::VertexStepMode::Vertex, attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3], }], }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_entity", targets: &[Some(config.view_formats[0].into())], }), primitive: wgpu::PrimitiveState { front_face: wgpu::FrontFace::Cw, ..Default::default() }, depth_stencil: Some(wgpu::DepthStencilState { format: Self::DEPTH_FORMAT, depth_write_enabled: true, depth_compare: wgpu::CompareFunction::LessEqual, stencil: wgpu::StencilState::default(), bias: wgpu::DepthBiasState::default(), }), multisample: wgpu::MultisampleState::default(), multiview: None, }); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: None, address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Linear, ..Default::default() }); let device_features = device.features(); let skybox_format = if device_features.contains(wgpu::Features::TEXTURE_COMPRESSION_ASTC) { log::info!("Using ASTC"); wgpu::TextureFormat::Astc { block: AstcBlock::B4x4, channel: AstcChannel::UnormSrgb, } } else if device_features.contains(wgpu::Features::TEXTURE_COMPRESSION_ETC2) { log::info!("Using ETC2"); wgpu::TextureFormat::Etc2Rgb8UnormSrgb } else if device_features.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) { log::info!("Using BC"); wgpu::TextureFormat::Bc1RgbaUnormSrgb } else { log::info!("Using plain"); wgpu::TextureFormat::Bgra8UnormSrgb }; let size = wgpu::Extent3d { width: IMAGE_SIZE, height: IMAGE_SIZE, depth_or_array_layers: 6, }; let layer_size = wgpu::Extent3d { depth_or_array_layers: 1, ..size }; let max_mips = layer_size.max_mips(wgpu::TextureDimension::D2); log::debug!( "Copying {:?} skybox images of size {}, {}, 6 with {} mips to gpu", skybox_format, IMAGE_SIZE, IMAGE_SIZE, max_mips, ); let bytes = match skybox_format { wgpu::TextureFormat::Astc { block: AstcBlock::B4x4, channel: AstcChannel::UnormSrgb, } => &include_bytes!("../images/astc.dds")[..], wgpu::TextureFormat::Etc2Rgb8UnormSrgb => &include_bytes!("../images/etc2.dds")[..], wgpu::TextureFormat::Bc1RgbaUnormSrgb => &include_bytes!("../images/bc1.dds")[..], wgpu::TextureFormat::Bgra8UnormSrgb => &include_bytes!("../images/bgra.dds")[..], _ => unreachable!(), }; let image = ddsfile::Dds::read(&mut std::io::Cursor::new(&bytes)).unwrap(); let texture = device.create_texture_with_data( queue, &wgpu::TextureDescriptor { size, mip_level_count: max_mips, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: skybox_format, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, label: None, view_formats: &[], }, &image.data, ); let texture_view = texture.create_view(&wgpu::TextureViewDescriptor { label: None, dimension: Some(wgpu::TextureViewDimension::Cube), ..wgpu::TextureViewDescriptor::default() }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: uniform_buf.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&texture_view), }, wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&sampler), }, ], label: None, }); let depth_view = Self::create_depth_texture(config, device); Skybox { camera, sky_pipeline, entity_pipeline, bind_group, uniform_buf, entities, depth_view, staging_belt: wgpu::util::StagingBelt::new(0x100), } } #[allow(clippy::single_match)] fn update(&mut self, event: winit::event::WindowEvent) { match event { winit::event::WindowEvent::CursorMoved { position, .. } => { let norm_x = position.x as f32 / self.camera.screen_size.0 as f32 - 0.5; let norm_y = position.y as f32 / self.camera.screen_size.1 as f32 - 0.5; self.camera.angle_y = norm_x * 5.0; self.camera.angle_xz = norm_y; } _ => {} } } fn resize( &mut self, config: &wgpu::SurfaceConfiguration, device: &wgpu::Device, _queue: &wgpu::Queue, ) { self.depth_view = Self::create_depth_texture(config, device); self.camera.screen_size = (config.width, config.height); } fn render( &mut self, view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue, _spawner: &wgpu_example::framework::Spawner, ) { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); // update rotation let raw_uniforms = self.camera.to_uniform_data(); self.staging_belt .write_buffer( &mut encoder, &self.uniform_buf, 0, wgpu::BufferSize::new((raw_uniforms.len() * 4) as wgpu::BufferAddress).unwrap(), device, ) .copy_from_slice(bytemuck::cast_slice(&raw_uniforms)); self.staging_belt.finish(); { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[Some(wgpu::RenderPassColorAttachment { view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.1, g: 0.2, b: 0.3, a: 1.0, }), store: true, }, })], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { view: &self.depth_view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: false, }), stencil_ops: None, }), }); rpass.set_bind_group(0, &self.bind_group, &[]); rpass.set_pipeline(&self.entity_pipeline); for entity in self.entities.iter() { rpass.set_vertex_buffer(0, entity.vertex_buf.slice(..)); rpass.draw(0..entity.vertex_count, 0..1); } rpass.set_pipeline(&self.sky_pipeline); rpass.draw(0..3, 0..1); } queue.submit(std::iter::once(encoder.finish())); self.staging_belt.recall(); } } fn main() { wgpu_example::framework::run::("skybox"); } wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[test] #[wasm_bindgen_test::wasm_bindgen_test] fn skybox() { wgpu_example::framework::test::(wgpu_example::framework::FrameworkRefTest { image_path: "/examples/skybox/screenshot.png", width: 1024, height: 768, optional_features: wgpu::Features::default(), base_test_parameters: wgpu_test::TestParameters::default().specific_failure( Some(wgpu::Backends::GL), None, Some("ANGLE"), false, ), comparisons: &[wgpu_test::ComparisonType::Mean(0.015)], }); } #[test] #[wasm_bindgen_test::wasm_bindgen_test] fn skybox_bc1() { wgpu_example::framework::test::(wgpu_example::framework::FrameworkRefTest { image_path: "/examples/skybox/screenshot-bc1.png", width: 1024, height: 768, optional_features: wgpu::Features::TEXTURE_COMPRESSION_BC, base_test_parameters: wgpu_test::TestParameters::default(), // https://bugs.chromium.org/p/angleproject/issues/detail?id=7056 comparisons: &[wgpu_test::ComparisonType::Mean(0.02)], }); } #[test] #[wasm_bindgen_test::wasm_bindgen_test] fn skybox_etc2() { wgpu_example::framework::test::(wgpu_example::framework::FrameworkRefTest { image_path: "/examples/skybox/screenshot-etc2.png", width: 1024, height: 768, optional_features: wgpu::Features::TEXTURE_COMPRESSION_ETC2, base_test_parameters: wgpu_test::TestParameters::default(), // https://bugs.chromium.org/p/angleproject/issues/detail?id=7056 comparisons: &[wgpu_test::ComparisonType::Mean(0.015)], }); } #[test] #[wasm_bindgen_test::wasm_bindgen_test] fn skybox_astc() { wgpu_example::framework::test::(wgpu_example::framework::FrameworkRefTest { image_path: "/examples/skybox/screenshot-astc.png", width: 1024, height: 768, optional_features: wgpu::Features::TEXTURE_COMPRESSION_ASTC, base_test_parameters: wgpu_test::TestParameters::default(), // https://bugs.chromium.org/p/angleproject/issues/detail?id=7056 comparisons: &[wgpu_test::ComparisonType::Mean(0.016)], }); }