Skip to main content

pebble/wgpu/
cubemap.rs

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