Skip to main content

pebble/wgpu/
cubemap.rs

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