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, decode_file, write_texture_level0},
11    },
12};
13
14/// Source data for [`GPUCubemap`]. Fields are private — build one via the
15/// [`from_files`](Self::from_files)/[`from_faces`](Self::from_faces)/
16/// [`empty`](Self::empty) constructors rather than as a struct literal.
17pub struct Cubemap {
18    /// Edge length in pixels — cubemap faces are always square.
19    size: u32,
20    /// GPU pixel format to upload as. Defaults to `Rgba8UnormSrgb`.
21    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    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    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    generate_mips: bool,
35}
36
37impl Cubemap {
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. `Cubemap::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    /// Logs a WARN for `with_mips()` called on an [`empty`](Self::empty)
87    /// cubemap — per `generate_mips`'s own doc, mip generation only applies
88    /// when uploading real face data, so this combination is a no-op.
89    fn validate(&self) {
90        if self.generate_mips && self.faces.is_none() && self.face_files.is_none() {
91            tracing::warn!(
92                "Cubemap: with_mips() has no effect on an empty() render-target cubemap — mip \
93                 generation only applies when faces/face_files supply pixel data"
94            );
95        }
96    }
97
98    /// Consume the builder and return the finished [`Cubemap`] value.
99    pub fn build(self) -> Self {
100        self.validate();
101        self
102    }
103
104    /// Consume the builder, insert into `assets` under `name`, and return
105    /// the resulting [`Handle<Cubemap>`].
106    pub fn build_asset(self, name: &str, assets: &mut Assets<Self>) -> Handle<Self> {
107        self.validate();
108        assets.insert(name, self)
109    }
110
111    /// `render_target` is set for an empty capture-target cubemap (see
112    /// [`empty`](Self::empty)), rendered into directly. Separately from
113    /// that, `mip_count > 1` also needs `RENDER_ATTACHMENT` — mips beyond
114    /// level 0 are rendered into by [`MipmapGenerator::generate_mips`](super::mipmap::MipmapGenerator::generate_mips)
115    /// regardless of whether the base texture is a capture target or one
116    /// uploaded from real face data, so the two conditions are OR'd rather
117    /// than `render_target` alone deciding the usage.
118    fn wgpu_descriptor(&self, mip_count: u32, render_target: bool) -> wgpu::TextureDescriptor<'_> {
119        let mut usage = super::mipmap::texture_usage(mip_count);
120        if render_target {
121            usage |= TextureUsages::RENDER_ATTACHMENT.into();
122        }
123
124        wgpu::TextureDescriptor {
125            label: None,
126            size: wgpu::Extent3d {
127                width: self.size,
128                height: self.size,
129                depth_or_array_layers: 6,
130            },
131            mip_level_count: mip_count,
132            sample_count: 1,
133            dimension: wgpu::TextureDimension::D2,
134            format: self.format.into(),
135            usage,
136            view_formats: &[],
137        }
138    }
139}
140
141/// A cubemap uploaded to the GPU, ready to bind (e.g. via
142/// [`BindingInstanceEntry::Cubemap`](super::instance::BindingInstanceEntry::Cubemap)).
143/// Opaque — bind it via
144/// [`BindGroupBuilder::texture_cubemap`](super::buffers::BindGroupBuilder::texture_cubemap).
145///
146/// [`empty`](Cubemap::empty)'s documented use case — rendering
147/// into per-face views for environment capture (a skybox capture, an
148/// irradiance/specular IBL prefilter pass, a reflection probe, ...) — is
149/// [`face_attachment`](Self::face_attachment).
150pub struct GPUCubemap {
151    texture: wgpu::Texture,
152    view: wgpu::TextureView,
153    size: u32,
154    format: TextureFormat,
155    ctx: GpuContext,
156}
157
158impl GPUCubemap {
159    /// Overwrites one face's level-0 pixel data (+X, -X, +Y, -Y, +Z, -Z is
160    /// `face` 0..=5, matching [`Cubemap::from_faces`]'s order).
161    /// See [`GPUTexture::write`](super::textures::GPUTexture::write) for the
162    /// same caveat about mip levels not being regenerated.
163    pub fn write_face(&self, face: u32, pixels: &[u8]) {
164        write_texture_level0(self.ctx.queue(), &self.texture, face, self.format.into(), self.size, self.size, pixels);
165    }
166
167    /// A render-target view onto one face at one mip level, for rendering
168    /// into directly — an environment-map capture, a specular IBL prefilter
169    /// pass writing successive mip levels, a reflection probe. `face` is
170    /// `0..=5` in the same order as [`Cubemap::from_faces`]'s
171    /// array (+X, -X, +Y, -Y, +Z, -Z); `mip_level` is `0` unless this
172    /// cubemap was built [`with_mips`](Cubemap::with_mips), in
173    /// which case a prefilter pass typically writes one mip level per
174    /// roughness step.
175    ///
176    /// Only meaningful for a cubemap allocated with `RENDER_ATTACHMENT`
177    /// usage, which [`Cubemap::empty`] sets automatically.
178    /// Panics if `face` is out of range.
179    pub fn face_attachment(&self, face: u32, mip_level: u32) -> super::texture_view::TextureView {
180        assert!(face < 6, "GPUCubemap::face_attachment: face {face} out of range (0..=5)");
181        let view = self.texture.create_view(&wgpu::TextureViewDescriptor {
182            dimension: Some(wgpu::TextureViewDimension::D2),
183            base_mip_level: mip_level,
184            mip_level_count: Some(1),
185            base_array_layer: face,
186            array_layer_count: Some(1),
187            ..Default::default()
188        });
189        super::texture_view::TextureView::from_raw(view, self.texture.clone())
190    }
191
192    /// Edge length in pixels.
193    pub fn size(&self) -> u32 {
194        self.size
195    }
196
197    pub(crate) fn view(&self) -> &wgpu::TextureView {
198        &self.view
199    }
200}
201
202impl Asset<WGPUBackend> for GPUCubemap {
203    type Source = Cubemap;
204    type Deps<'a> = Res<'a, MipmapGenerator>;
205
206    fn upload<'a>(
207        source: &Cubemap,
208        backend: &WGPUBackend,
209        mipmap_generator: &Res<'a, MipmapGenerator>,
210    ) -> Option<Self> {
211        let faces: Option<[Vec<u8>; 6]> = if let Some(files) = &source.face_files {
212            let mut out: [Vec<u8>; 6] = Default::default();
213            for (i, path) in files.iter().enumerate() {
214                let (w, h, data) = decode_file(path, source.format.into())?;
215                if w != source.size || h != source.size {
216                    tracing::error!(
217                        "CubemapSpec: face {i} ('{path}') is {w}x{h}, expected {0}x{0}",
218                        source.size
219                    );
220                    return None;
221                }
222                out[i] = data;
223            }
224            Some(out)
225        } else {
226            source.faces.clone()
227        };
228
229        let mip_count = super::mipmap::mip_count(source.size, source.generate_mips);
230
231        let texture = backend
232            .device
233            .create_texture(&source.wgpu_descriptor(mip_count, faces.is_none()));
234
235        if let Some(faces) = &faces {
236            for (face, data) in faces.iter().enumerate() {
237                backend.queue.write_texture(
238                    wgpu::TexelCopyTextureInfo {
239                        texture: &texture,
240                        mip_level: 0,
241                        origin: wgpu::Origin3d {
242                            x: 0,
243                            y: 0,
244                            z: face as u32,
245                        },
246                        aspect: wgpu::TextureAspect::All,
247                    },
248                    data,
249                    wgpu::TexelCopyBufferLayout {
250                        offset: 0,
251                        bytes_per_row: Some(bytes_per_pixel(source.format.into()) * source.size),
252                        rows_per_image: Some(source.size),
253                    },
254                    wgpu::Extent3d {
255                        width: source.size,
256                        height: source.size,
257                        depth_or_array_layers: 1,
258                    },
259                );
260            }
261
262            if mip_count > 1 {
263                mipmap_generator.generate_mips(
264                    &backend.device,
265                    &backend.queue,
266                    &texture,
267                    source.format.into(),
268                    mip_count,
269                    6,
270                );
271            }
272        }
273
274        let view = texture.create_view(&wgpu::TextureViewDescriptor {
275            dimension: Some(wgpu::TextureViewDimension::Cube),
276            ..Default::default()
277        });
278        Some(Self {
279            texture,
280            view,
281            size: source.size,
282            format: source.format,
283            ctx: GpuContext::from_backend(backend),
284        })
285    }
286}
287
288crate::wgpu::plugin_macros::mipmap_asset_plugin! {
289    /// Registers the [`GPUCubemap`] asset pipeline (`Assets<Cubemap>`
290    /// → `ProcessedAssets<GPUCubemap>`), plus the [`MipmapGenerator`] it
291    /// depends on for `generate_mips`. Included by
292    /// [`WGPUPlugin`](super::backend::WGPUPlugin); add directly only if you're
293    /// assembling the `wgpu` module's plugins by hand.
294    CubemapPlugin, GPUCubemap
295}