Skip to main content

pebble/wgpu/
cubemap.rs

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