Skip to main content

pebble/wgpu/
cubemap.rs

1use crate::{
2    assets::{plugin::AssetPlugin, singleton_asset::LazyResourcePlugin, upload::Asset},
3    ecs::{plugin::Plugin, system::Res},
4    wgpu::{
5        backend::WGPUBackend,
6        mipmap::MipmapGenerator,
7        textures::{bytes_per_pixel, decode_file},
8    },
9};
10
11pub struct CubemapDescriptor {
12    pub size: u32, // cubemaps are square per face
13    pub format: wgpu::TextureFormat,
14    /// `Some` uploads 6 faces of pixel data up front (wgpu's expected
15    /// order: +X, -X, +Y, -Y, +Z, -Z). `None` allocates an empty cubemap
16    /// meant to be filled later by rendering into per-face views — e.g. an
17    /// environment-map capture pass — in which case [`wgpu_descriptor`](Self::wgpu_descriptor)
18    /// adds `RENDER_ATTACHMENT` usage instead of requiring upload data.
19    pub faces: Option<[Vec<u8>; 6]>,
20    /// File paths for each of the 6 faces (same order as `faces`), decoded
21    /// through the same loader used by `GPUTexture`/`GPUTextureArray`.
22    pub face_files: Option<[&'static str; 6]>,
23    pub generate_mips: bool,
24}
25
26impl CubemapDescriptor {
27    /// Load 6 faces from files (+X, -X, +Y, -Y, +Z, -Z). Size is inferred from the first face.
28    pub fn from_files(size: u32, files: [&'static str; 6]) -> Self {
29        Self {
30            size,
31            format: wgpu::TextureFormat::Rgba8UnormSrgb,
32            faces: None,
33            face_files: Some(files),
34            generate_mips: false,
35        }
36    }
37
38    /// Supply raw pixel bytes for each face (+X, -X, +Y, -Y, +Z, -Z).
39    pub fn from_faces(size: u32, format: wgpu::TextureFormat, faces: [Vec<u8>; 6]) -> Self {
40        Self {
41            size,
42            format,
43            faces: Some(faces),
44            face_files: None,
45            generate_mips: false,
46        }
47    }
48
49    /// Allocate an empty cubemap for use as a render target (e.g. environment capture).
50    pub fn empty(size: u32, format: wgpu::TextureFormat) -> Self {
51        Self {
52            size,
53            format,
54            faces: None,
55            face_files: None,
56            generate_mips: false,
57        }
58    }
59
60    pub fn with_format(mut self, format: wgpu::TextureFormat) -> Self {
61        self.format = format;
62        self
63    }
64
65    pub fn with_mips(mut self) -> Self {
66        self.generate_mips = true;
67        self
68    }
69
70    fn wgpu_descriptor(&self, mip_count: u32, render_target: bool) -> wgpu::TextureDescriptor<'_> {
71        wgpu::TextureDescriptor {
72            label: None,
73            size: wgpu::Extent3d {
74                width: self.size,
75                height: self.size,
76                depth_or_array_layers: 6,
77            },
78            mip_level_count: mip_count,
79            sample_count: 1,
80            dimension: wgpu::TextureDimension::D2,
81            format: self.format,
82            usage: if render_target {
83                wgpu::TextureUsages::TEXTURE_BINDING
84                    | wgpu::TextureUsages::COPY_DST
85                    | wgpu::TextureUsages::RENDER_ATTACHMENT
86            } else {
87                wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST
88            },
89            view_formats: &[],
90        }
91    }
92}
93
94pub struct GPUCubemap {
95    pub texture: wgpu::Texture,
96    pub view: wgpu::TextureView,
97}
98
99impl Asset<WGPUBackend> for GPUCubemap {
100    type Source = CubemapDescriptor;
101    type Deps<'a> = Res<'a, MipmapGenerator>;
102
103    fn upload<'a>(
104        source: &CubemapDescriptor,
105        backend: &WGPUBackend,
106        mipmap_generator: &Res<'a, MipmapGenerator>,
107    ) -> Option<Self> {
108        let faces: Option<[Vec<u8>; 6]> = if let Some(files) = &source.face_files {
109            let mut out: [Vec<u8>; 6] = Default::default();
110            for (i, path) in files.iter().enumerate() {
111                let (w, h, data) = decode_file(path, source.format);
112                if w != source.size || h != source.size {
113                    tracing::error!(
114                        "CubemapSpec: face {i} ('{path}') is {w}x{h}, expected {0}x{0}",
115                        source.size
116                    );
117                    return None;
118                }
119                out[i] = data;
120            }
121            Some(out)
122        } else {
123            source.faces.clone()
124        };
125
126        let mip_count = if source.generate_mips {
127            (source.size as f32).log2().floor() as u32 + 1
128        } else {
129            1
130        };
131
132        let texture = backend
133            .device
134            .create_texture(&source.wgpu_descriptor(mip_count, faces.is_none()));
135
136        if let Some(faces) = &faces {
137            for (face, data) in faces.iter().enumerate() {
138                backend.queue.write_texture(
139                    wgpu::TexelCopyTextureInfo {
140                        texture: &texture,
141                        mip_level: 0,
142                        origin: wgpu::Origin3d {
143                            x: 0,
144                            y: 0,
145                            z: face as u32,
146                        },
147                        aspect: wgpu::TextureAspect::All,
148                    },
149                    data,
150                    wgpu::TexelCopyBufferLayout {
151                        offset: 0,
152                        bytes_per_row: Some(bytes_per_pixel(source.format) * source.size),
153                        rows_per_image: Some(source.size),
154                    },
155                    wgpu::Extent3d {
156                        width: source.size,
157                        height: source.size,
158                        depth_or_array_layers: 1,
159                    },
160                );
161            }
162
163            if mip_count > 1 {
164                mipmap_generator.generate_mips(
165                    &backend.device,
166                    &backend.queue,
167                    &texture,
168                    source.format,
169                    mip_count,
170                    6,
171                );
172            }
173        }
174
175        let view = texture.create_view(&wgpu::TextureViewDescriptor {
176            dimension: Some(wgpu::TextureViewDimension::Cube),
177            ..Default::default()
178        });
179        Some(Self { texture, view })
180    }
181}
182
183#[derive(Default)]
184pub struct CubemapPlugin;
185impl CubemapPlugin {
186    pub fn new() -> Self {
187        Self
188    }
189}
190impl Plugin for CubemapPlugin {
191    fn build(&self, app: &mut crate::prelude::App) {
192        app.add_plugin(LazyResourcePlugin::<WGPUBackend, MipmapGenerator>::new());
193        app.add_plugin(AssetPlugin::<WGPUBackend, GPUCubemap>::new());
194    }
195}