use image::DynamicImage;
use crate::engine::Engine;
const SKYBOX_SHADER_SOURCE: &str = include_str!("../assets/shaders/skybox.wgsl");
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct SkyboxUniform {
view_proj: [[f32; 4]; 4],
}
pub struct CubemapTexture {
pub(crate) view: wgpu::TextureView,
pub(crate) sampler: wgpu::Sampler,
}
impl CubemapTexture {
pub fn from_faces(
device: &wgpu::Device,
queue: &wgpu::Queue,
faces: [&DynamicImage; 6],
) -> Self {
let (width, height) = faces[0].to_rgba8().dimensions();
for face in &faces[1..] {
let (w, h) = face.to_rgba8().dimensions();
assert_eq!(
(width, height),
(w, h),
"All cubemap faces must have the same dimensions"
);
}
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 6, };
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Cubemap Texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
for (i, face) in faces.iter().enumerate() {
let rgba = face.to_rgba8();
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: 0,
y: 0,
z: i as u32,
},
aspect: wgpu::TextureAspect::All,
},
&rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
}
let view = texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("Cubemap View"),
format: Some(wgpu::TextureFormat::Rgba8UnormSrgb),
dimension: Some(wgpu::TextureViewDimension::Cube),
usage: None,
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: Some(6),
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Cubemap Sampler"),
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()
});
Self { view, sampler }
}
#[allow(clippy::too_many_arguments)]
pub fn from_face_bytes(
device: &wgpu::Device,
queue: &wgpu::Queue,
right: &[u8],
left: &[u8],
top: &[u8],
bottom: &[u8],
front: &[u8],
back: &[u8],
) -> Result<Self, image::ImageError> {
let faces = [
image::load_from_memory(right)?,
image::load_from_memory(left)?,
image::load_from_memory(top)?,
image::load_from_memory(bottom)?,
image::load_from_memory(front)?,
image::load_from_memory(back)?,
];
Ok(Self::from_faces(
device,
queue,
[
&faces[0], &faces[1], &faces[2], &faces[3], &faces[4], &faces[5],
],
))
}
pub fn solid_color(device: &wgpu::Device, queue: &wgpu::Queue, color: [u8; 4]) -> Self {
let pixel_data = vec![color[0], color[1], color[2], color[3]];
let img = DynamicImage::ImageRgba8(image::RgbaImage::from_raw(1, 1, pixel_data).unwrap());
Self::from_faces(device, queue, [&img, &img, &img, &img, &img, &img])
}
}
pub struct Skybox {
pub(crate) cubemap: CubemapTexture,
}
impl Skybox {
pub fn from_faces(g: &Engine, faces: [&DynamicImage; 6]) -> Self {
let cubemap = CubemapTexture::from_faces(
&g.render.backend_state.device,
&g.render.backend_state.queue,
faces,
);
Self { cubemap }
}
pub fn from_face_bytes(
g: &Engine,
right: &[u8],
left: &[u8],
top: &[u8],
bottom: &[u8],
front: &[u8],
back: &[u8],
) -> Result<Self, image::ImageError> {
let cubemap = CubemapTexture::from_face_bytes(
&g.render.backend_state.device,
&g.render.backend_state.queue,
right,
left,
top,
bottom,
front,
back,
)?;
Ok(Self { cubemap })
}
pub fn solid_color(g: &Engine, r: u8, g_color: u8, b: u8) -> Self {
let cubemap = CubemapTexture::solid_color(
&g.render.backend_state.device,
&g.render.backend_state.queue,
[r, g_color, b, 255],
);
Self { cubemap }
}
}
pub struct SkyboxShader {
pipeline: wgpu::RenderPipeline,
uniform_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
texture_bind_group_layout: wgpu::BindGroupLayout,
}
impl SkyboxShader {
pub fn new(g: &Engine) -> Self {
let device = &g.render.backend_state.device;
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Skybox Shader"),
source: wgpu::ShaderSource::Wgsl(SKYBOX_SHADER_SOURCE.into()),
});
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Skybox Uniform Buffer"),
size: std::mem::size_of::<SkyboxUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Skybox Uniform Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Skybox Uniform Bind Group"),
layout: &uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Skybox Texture Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::Cube,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Skybox Pipeline Layout"),
bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Skybox Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader_module,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[], },
fragment: Some(wgpu::FragmentState {
module: &shader_module,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: g.render.backend_state.surface_view_format,
blend: None, write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None, unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: false, depth_compare: wgpu::CompareFunction::LessEqual, stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
Self {
pipeline,
uniform_buffer,
uniform_bind_group,
texture_bind_group_layout,
}
}
pub fn record(
&self,
g: &Engine,
render_target: &wgpu::Texture,
depth_view: &wgpu::TextureView,
skybox: &Skybox,
encoder: &mut wgpu::CommandEncoder,
should_clear: bool,
) {
let device = &g.render.backend_state.device;
let queue = &g.render.backend_state.queue;
let size = render_target.size();
let aspect = if size.height == 0 {
1.0
} else {
size.width as f32 / size.height as f32
};
let camera = g.camera3d();
let projection = camera.projection_matrix(aspect);
let mut view = camera.view_matrix();
view.w_axis.x = 0.0;
view.w_axis.y = 0.0;
view.w_axis.z = 0.0;
let view_proj = projection * view;
let uniform = SkyboxUniform {
view_proj: view_proj.to_cols_array_2d(),
};
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniform]));
let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Skybox Texture Bind Group"),
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&skybox.cubemap.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&skybox.cubemap.sampler),
},
],
});
let view = render_target.create_view(&wgpu::TextureViewDescriptor::default());
let (color_load, depth_load) = if should_clear {
(
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
wgpu::LoadOp::Clear(0.0), )
} else {
(wgpu::LoadOp::Load, wgpu::LoadOp::Load)
};
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Skybox Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: color_load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: depth_load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_bind_group(1, &texture_bind_group, &[]);
render_pass.draw(0..36, 0..1);
}
}