pebble/wgpu/cubemap.rs
1use crate::{
2 assets::upload::Asset,
3 ecs::system::Res,
4 wgpu::{
5 backend::WGPUBackend,
6 flags::TextureUsages,
7 gpu_context::GpuContext,
8 mipmap::MipmapGenerator,
9 texture_format::TextureFormat,
10 textures::{bytes_per_pixel, decode_file, write_texture_level0},
11 },
12};
13
14/// Source data for [`GPUCubemap`]. Prefer the
15/// [`from_files`](Self::from_files)/[`from_faces`](Self::from_faces)/
16/// [`empty`](Self::empty) constructors over setting fields by hand.
17pub struct CubemapDescriptor {
18 /// Edge length in pixels — cubemap faces are always square.
19 pub size: u32,
20 /// GPU pixel format to upload as. Defaults to `Rgba8UnormSrgb`.
21 pub format: TextureFormat,
22 /// `Some` uploads 6 faces of pixel data up front (wgpu's expected
23 /// order: +X, -X, +Y, -Y, +Z, -Z). `None` allocates an empty cubemap
24 /// meant to be filled later by rendering into per-face views — e.g. an
25 /// environment-map capture pass — in which case [`wgpu_descriptor`](Self::wgpu_descriptor)
26 /// adds `RENDER_ATTACHMENT` usage instead of requiring upload data.
27 pub faces: Option<[Vec<u8>; 6]>,
28 /// File paths for each of the 6 faces (same order as `faces`), decoded
29 /// through the same loader used by `GPUTexture`/`GPUTextureArray`.
30 pub face_files: Option<[&'static str; 6]>,
31 /// Whether to generate a full mip chain (via [`MipmapGenerator`]). Only
32 /// applies when uploading pixel data (`faces`/`face_files` set) —
33 /// meaningless for an [`empty`](Self::empty) render-target cubemap.
34 pub generate_mips: bool,
35}
36
37impl CubemapDescriptor {
38 /// Load 6 faces from files (+X, -X, +Y, -Y, +Z, -Z). Size is inferred from the first face.
39 pub fn from_files(size: u32, files: [&'static str; 6]) -> Self {
40 Self {
41 size,
42 format: TextureFormat::Rgba8UnormSrgb,
43 faces: None,
44 face_files: Some(files),
45 generate_mips: false,
46 }
47 }
48
49 /// Supply raw pixel bytes for each face (+X, -X, +Y, -Y, +Z, -Z).
50 pub fn from_faces(size: u32, format: TextureFormat, faces: [Vec<u8>; 6]) -> Self {
51 Self {
52 size,
53 format,
54 faces: Some(faces),
55 face_files: None,
56 generate_mips: false,
57 }
58 }
59
60 /// Allocate an empty cubemap for use as a render target (e.g. environment capture).
61 pub fn empty(size: u32, format: TextureFormat) -> Self {
62 Self {
63 size,
64 format,
65 faces: None,
66 face_files: None,
67 generate_mips: false,
68 }
69 }
70
71 /// Override the format set by whichever constructor was used (all
72 /// three default to or take `format` directly — this exists for the
73 /// builder-chain case, e.g. `CubemapDescriptor::empty(size, format).with_mips()`
74 /// followed later by a format change, without re-specifying `size`).
75 pub fn with_format(mut self, format: TextureFormat) -> Self {
76 self.format = format;
77 self
78 }
79
80 /// Enable full mip chain generation.
81 pub fn with_mips(mut self) -> Self {
82 self.generate_mips = true;
83 self
84 }
85
86 /// `render_target` is set for an empty capture-target cubemap (see
87 /// [`empty`](Self::empty)), rendered into directly. Separately from
88 /// that, `mip_count > 1` also needs `RENDER_ATTACHMENT` — mips beyond
89 /// level 0 are rendered into by [`MipmapGenerator::generate_mips`](super::mipmap::MipmapGenerator::generate_mips)
90 /// regardless of whether the base texture is a capture target or one
91 /// uploaded from real face data, so the two conditions are OR'd rather
92 /// than `render_target` alone deciding the usage.
93 fn wgpu_descriptor(&self, mip_count: u32, render_target: bool) -> wgpu::TextureDescriptor<'_> {
94 let mut usage = super::mipmap::texture_usage(mip_count);
95 if render_target {
96 usage |= TextureUsages::RENDER_ATTACHMENT.into();
97 }
98
99 wgpu::TextureDescriptor {
100 label: None,
101 size: wgpu::Extent3d {
102 width: self.size,
103 height: self.size,
104 depth_or_array_layers: 6,
105 },
106 mip_level_count: mip_count,
107 sample_count: 1,
108 dimension: wgpu::TextureDimension::D2,
109 format: self.format.into(),
110 usage,
111 view_formats: &[],
112 }
113 }
114}
115
116/// A cubemap uploaded to the GPU, ready to bind (e.g. via
117/// [`BindingInstanceEntry::Cubemap`](super::instance::BindingInstanceEntry::Cubemap)).
118/// Opaque — bind it via
119/// [`BindGroupBuilder::texture_cubemap`](super::buffers::BindGroupBuilder::texture_cubemap).
120///
121/// [`empty`](CubemapDescriptor::empty)'s documented use case — rendering
122/// into per-face views for environment capture (a skybox capture, an
123/// irradiance/specular IBL prefilter pass, a reflection probe, ...) — is
124/// [`face_attachment`](Self::face_attachment).
125pub struct GPUCubemap {
126 texture: wgpu::Texture,
127 view: wgpu::TextureView,
128 size: u32,
129 format: TextureFormat,
130 ctx: GpuContext,
131}
132
133impl GPUCubemap {
134 /// Overwrites one face's level-0 pixel data (+X, -X, +Y, -Y, +Z, -Z is
135 /// `face` 0..=5, matching [`CubemapDescriptor::from_faces`]'s order).
136 /// See [`GPUTexture::write`](super::textures::GPUTexture::write) for the
137 /// same caveat about mip levels not being regenerated.
138 pub fn write_face(&self, face: u32, pixels: &[u8]) {
139 write_texture_level0(self.ctx.queue(), &self.texture, face, self.format.into(), self.size, self.size, pixels);
140 }
141
142 /// A render-target view onto one face at one mip level, for rendering
143 /// into directly — an environment-map capture, a specular IBL prefilter
144 /// pass writing successive mip levels, a reflection probe. `face` is
145 /// `0..=5` in the same order as [`CubemapDescriptor::from_faces`]'s
146 /// array (+X, -X, +Y, -Y, +Z, -Z); `mip_level` is `0` unless this
147 /// cubemap was built [`with_mips`](CubemapDescriptor::with_mips), in
148 /// which case a prefilter pass typically writes one mip level per
149 /// roughness step.
150 ///
151 /// Only meaningful for a cubemap allocated with `RENDER_ATTACHMENT`
152 /// usage, which [`CubemapDescriptor::empty`] sets automatically.
153 /// Panics if `face` is out of range.
154 pub fn face_attachment(&self, face: u32, mip_level: u32) -> super::texture_view::TextureView {
155 assert!(face < 6, "GPUCubemap::face_attachment: face {face} out of range (0..=5)");
156 let view = self.texture.create_view(&wgpu::TextureViewDescriptor {
157 dimension: Some(wgpu::TextureViewDimension::D2),
158 base_mip_level: mip_level,
159 mip_level_count: Some(1),
160 base_array_layer: face,
161 array_layer_count: Some(1),
162 ..Default::default()
163 });
164 super::texture_view::TextureView::from_raw(view, self.texture.clone())
165 }
166
167 /// Edge length in pixels.
168 pub fn size(&self) -> u32 {
169 self.size
170 }
171
172 pub(crate) fn view(&self) -> &wgpu::TextureView {
173 &self.view
174 }
175}
176
177impl Asset<WGPUBackend> for GPUCubemap {
178 type Source = CubemapDescriptor;
179 type Deps<'a> = Res<'a, MipmapGenerator>;
180
181 fn upload<'a>(
182 source: &CubemapDescriptor,
183 backend: &WGPUBackend,
184 mipmap_generator: &Res<'a, MipmapGenerator>,
185 ) -> Option<Self> {
186 let faces: Option<[Vec<u8>; 6]> = if let Some(files) = &source.face_files {
187 let mut out: [Vec<u8>; 6] = Default::default();
188 for (i, path) in files.iter().enumerate() {
189 let (w, h, data) = decode_file(path, source.format.into())?;
190 if w != source.size || h != source.size {
191 tracing::error!(
192 "CubemapSpec: face {i} ('{path}') is {w}x{h}, expected {0}x{0}",
193 source.size
194 );
195 return None;
196 }
197 out[i] = data;
198 }
199 Some(out)
200 } else {
201 source.faces.clone()
202 };
203
204 let mip_count = super::mipmap::mip_count(source.size, source.generate_mips);
205
206 let texture = backend
207 .device
208 .create_texture(&source.wgpu_descriptor(mip_count, faces.is_none()));
209
210 if let Some(faces) = &faces {
211 for (face, data) in faces.iter().enumerate() {
212 backend.queue.write_texture(
213 wgpu::TexelCopyTextureInfo {
214 texture: &texture,
215 mip_level: 0,
216 origin: wgpu::Origin3d {
217 x: 0,
218 y: 0,
219 z: face as u32,
220 },
221 aspect: wgpu::TextureAspect::All,
222 },
223 data,
224 wgpu::TexelCopyBufferLayout {
225 offset: 0,
226 bytes_per_row: Some(bytes_per_pixel(source.format.into()) * source.size),
227 rows_per_image: Some(source.size),
228 },
229 wgpu::Extent3d {
230 width: source.size,
231 height: source.size,
232 depth_or_array_layers: 1,
233 },
234 );
235 }
236
237 if mip_count > 1 {
238 mipmap_generator.generate_mips(
239 &backend.device,
240 &backend.queue,
241 &texture,
242 source.format.into(),
243 mip_count,
244 6,
245 );
246 }
247 }
248
249 let view = texture.create_view(&wgpu::TextureViewDescriptor {
250 dimension: Some(wgpu::TextureViewDimension::Cube),
251 ..Default::default()
252 });
253 Some(Self {
254 texture,
255 view,
256 size: source.size,
257 format: source.format,
258 ctx: GpuContext::from_backend(backend),
259 })
260 }
261}
262
263crate::wgpu::plugin_macros::mipmap_asset_plugin! {
264 /// Registers the [`GPUCubemap`] asset pipeline (`Assets<CubemapDescriptor>`
265 /// → `ProcessedAssets<GPUCubemap>`), plus the [`MipmapGenerator`] it
266 /// depends on for `generate_mips`. Included by
267 /// [`WGPUPlugin`](super::backend::WGPUPlugin); add directly only if you're
268 /// assembling the `wgpu` module's plugins by hand.
269 CubemapPlugin, GPUCubemap
270}