Skip to main content

pebble/graphics/pipeline/
cubemap.rs

1use crate::{
2    assets::{handle::Handle, storage::Assets, upload::{Asset, AssetSource}},
3    ecs::resources::Read,
4    graphics::{
5        pipeline::{
6            mipmap::{MipLevels, MipmapGenerator},
7            texture_view::TextureView,
8            textures::{bytes_per_pixel, check_texture_dimensions, decode_file, write_texture_mip},
9        },
10        render::{Backend, gpu_context::GpuContext},
11        types::TextureFormat,
12    },
13};
14
15/// A cubemap texture asset — six square faces of equal size, same
16/// construction pattern as [`Texture`](super::textures::Texture). Face
17/// order follows wgpu's convention: `+X, -X, +Y, -Y, +Z, -Z`.
18pub struct Cubemap {
19    size: u32,
20    format: TextureFormat,
21    faces: Option<[Vec<u8>; 6]>,
22    face_files: Option<[&'static str; 6]>,
23    mip_levels: MipLevels,
24}
25
26impl Cubemap {
27    pub fn from_files(size: u32, files: [&'static str; 6]) -> Self {
28        Self { size, format: TextureFormat::Rgba8UnormSrgb, faces: None, face_files: Some(files), mip_levels: MipLevels::None }
29    }
30
31    pub fn from_data(size: u32, format: TextureFormat, faces: [Vec<u8>; 6]) -> Self {
32        Self { size, format, faces: Some(faces), face_files: None, mip_levels: MipLevels::None }
33    }
34
35    /// No source data — a render target (e.g. for baking an environment map), or something you'll [`write_face`](GPUCubemap::write_face) yourself.
36    pub fn empty(size: u32, format: TextureFormat) -> Self {
37        Self { size, format, faces: None, face_files: None, mip_levels: MipLevels::None }
38    }
39
40    pub fn with_format(mut self, format: TextureFormat) -> Self {
41        self.format = format;
42        self
43    }
44
45    pub fn with_mips(mut self) -> Self {
46        self.mip_levels = MipLevels::Full;
47        self
48    }
49
50    pub fn with_mip_count(mut self, count: u32) -> Self {
51        self.mip_levels = MipLevels::Fixed(count);
52        self
53    }
54
55    fn validate(&self) {
56        if self.size == 0 && (self.faces.is_some() || self.face_files.is_some()) {
57            tracing::warn!(
58                "Cubemap::from_data()/from_files(): size is 0 — did you forget to pass the real size?"
59            );
60        }
61    }
62
63    pub fn build_asset(self, name: &str, assets: &mut Assets<Cubemap>) -> Handle<Cubemap> {
64        self.validate();
65        assets.insert(name, self)
66    }
67
68    /// CPU-side faces — only ever `Some` for a `from_data()` cubemap. See
69    /// [`Texture::data`](super::textures::Texture::data).
70    pub fn faces(&self) -> Option<&[Vec<u8>; 6]> {
71        self.faces.as_ref()
72    }
73
74    /// Frees the CPU-side copy. See
75    /// [`Texture::release_cpu_data`](super::textures::Texture::release_cpu_data).
76    pub fn release_cpu_data(&mut self) {
77        self.faces = None;
78    }
79
80    fn wgpu_descriptor(&self, mip_count: u32, render_target: bool) -> wgpu::TextureDescriptor<'_> {
81        let mut usage = crate::graphics::pipeline::mipmap::texture_usage(mip_count);
82        if render_target {
83            usage |= wgpu::TextureUsages::RENDER_ATTACHMENT;
84        }
85
86        wgpu::TextureDescriptor {
87            label: None,
88            size: wgpu::Extent3d { width: self.size, height: self.size, depth_or_array_layers: 6 },
89            mip_level_count: mip_count,
90            sample_count: 1,
91            dimension: wgpu::TextureDimension::D2,
92            format: self.format.into(),
93            usage,
94            view_formats: &[],
95        }
96    }
97}
98
99/// The GPU-resident cubemap an uploaded [`Cubemap`] produces.
100pub struct GPUCubemap {
101    texture: wgpu::Texture,
102    view: wgpu::TextureView,
103    size: u32,
104    format: TextureFormat,
105    ctx: GpuContext,
106}
107
108impl GPUCubemap {
109    /// Overwrites one mip level of one face with new pixel data.
110    pub fn write_face(&self, face: u32, mip_level: u32, pixels: &[u8]) {
111        write_texture_mip(self.ctx.queue(), &self.texture, face, mip_level, self.format.into(), self.size, self.size, pixels);
112    }
113
114    /// A view into a single face and mip level.
115    pub fn get_view(&self, face: u32, mip_level: u32) -> TextureView {
116        assert!(face < 6, "GPUCubemap::get_view: face {face} out of range (0..=5)");
117        let view = self.texture.create_view(&wgpu::TextureViewDescriptor {
118            dimension: Some(wgpu::TextureViewDimension::D2),
119            base_mip_level: mip_level,
120            mip_level_count: Some(1),
121            base_array_layer: face,
122            array_layer_count: Some(1),
123            ..Default::default()
124        });
125        TextureView::from_raw(view, self.texture.clone())
126    }
127
128    pub fn size(&self) -> u32 {
129        self.size
130    }
131
132    pub(crate) fn view(&self) -> &wgpu::TextureView {
133        &self.view
134    }
135}
136
137impl AssetSource for Cubemap {
138    type Processed = GPUCubemap;
139}
140
141impl Asset<Backend> for Cubemap {
142    type Deps<'a> = Read<'a, MipmapGenerator>;
143
144    fn upload<'a>(&self, backend: &Backend, mipmap_generator: &Read<'a, MipmapGenerator>) -> Option<GPUCubemap> {
145        let faces: Option<[Vec<u8>; 6]> = if let Some(files) = &self.face_files {
146            let mut out: [Vec<u8>; 6] = Default::default();
147            for (i, path) in files.iter().enumerate() {
148                let (w, h, data) = decode_file(path, self.format.into())?;
149                if w != self.size || h != self.size {
150                    tracing::error!(
151                        "CubemapSpec: face {i} ('{path}') is {w}x{h}, expected {0}x{0}",
152                        self.size
153                    );
154                    return None;
155                }
156                out[i] = data;
157            }
158            Some(out)
159        } else {
160            self.faces.clone()
161        };
162
163        check_texture_dimensions(&backend.device, "GPUCubemap", self.size, self.size);
164
165        let mip_count = crate::graphics::pipeline::mipmap::mip_count(self.size, self.mip_levels);
166
167        let texture = backend.device.create_texture(&self.wgpu_descriptor(mip_count, faces.is_none()));
168
169        if let Some(faces) = &faces {
170            for (face, data) in faces.iter().enumerate() {
171                backend.queue.write_texture(
172                    wgpu::TexelCopyTextureInfo {
173                        texture: &texture,
174                        mip_level: 0,
175                        origin: wgpu::Origin3d { x: 0, y: 0, z: face as u32 },
176                        aspect: wgpu::TextureAspect::All,
177                    },
178                    data,
179                    wgpu::TexelCopyBufferLayout {
180                        offset: 0,
181                        bytes_per_row: Some(bytes_per_pixel(self.format.into()) * self.size),
182                        rows_per_image: Some(self.size),
183                    },
184                    wgpu::Extent3d { width: self.size, height: self.size, depth_or_array_layers: 1 },
185                );
186            }
187
188            if mip_count > 1 {
189                mipmap_generator.generate_mips(backend, &texture, self.format.into(), mip_count, 6);
190            }
191        }
192
193        let view = texture.create_view(&wgpu::TextureViewDescriptor {
194            dimension: Some(wgpu::TextureViewDimension::Cube),
195            ..Default::default()
196        });
197        Some(GPUCubemap { texture, view, size: self.size, format: self.format, ctx: GpuContext::from_backend(backend) })
198    }
199}