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    pub fn with_format(mut self, format: wgpu::TextureFormat) -> Self {
28        self.format = format;
29        self
30    }
31
32    fn wgpu_descriptor(&self, mip_count: u32, render_target: bool) -> wgpu::TextureDescriptor<'_> {
33        wgpu::TextureDescriptor {
34            label: None,
35            size: wgpu::Extent3d {
36                width: self.size,
37                height: self.size,
38                depth_or_array_layers: 6,
39            },
40            mip_level_count: mip_count,
41            sample_count: 1,
42            dimension: wgpu::TextureDimension::D2,
43            format: self.format,
44            usage: if render_target {
45                wgpu::TextureUsages::TEXTURE_BINDING
46                    | wgpu::TextureUsages::COPY_DST
47                    | wgpu::TextureUsages::RENDER_ATTACHMENT
48            } else {
49                wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST
50            },
51            view_formats: &[],
52        }
53    }
54}
55
56pub struct GPUCubemap {
57    pub texture: wgpu::Texture,
58    pub view: wgpu::TextureView,
59}
60
61impl Asset<WGPUBackend> for GPUCubemap {
62    type Source = CubemapDescriptor;
63    type Deps<'a> = Res<'a, MipmapGenerator>;
64
65    fn upload<'a>(
66        source: &CubemapDescriptor,
67        backend: &WGPUBackend,
68        mipmap_generator: &Res<'a, MipmapGenerator>,
69    ) -> Option<Self> {
70        let faces: Option<[Vec<u8>; 6]> = if let Some(files) = &source.face_files {
71            let mut out: [Vec<u8>; 6] = Default::default();
72            for (i, path) in files.iter().enumerate() {
73                let (w, h, data) = decode_file(path, source.format);
74                if w != source.size || h != source.size {
75                    tracing::error!(
76                        "CubemapSpec: face {i} ('{path}') is {w}x{h}, expected {0}x{0}",
77                        source.size
78                    );
79                    return None;
80                }
81                out[i] = data;
82            }
83            Some(out)
84        } else {
85            source.faces.clone()
86        };
87
88        let mip_count = if source.generate_mips {
89            (source.size as f32).log2().floor() as u32 + 1
90        } else {
91            1
92        };
93
94        let texture = backend
95            .device
96            .create_texture(&source.wgpu_descriptor(mip_count, faces.is_none()));
97
98        if let Some(faces) = &faces {
99            for (face, data) in faces.iter().enumerate() {
100                backend.queue.write_texture(
101                    wgpu::TexelCopyTextureInfo {
102                        texture: &texture,
103                        mip_level: 0,
104                        origin: wgpu::Origin3d {
105                            x: 0,
106                            y: 0,
107                            z: face as u32,
108                        },
109                        aspect: wgpu::TextureAspect::All,
110                    },
111                    data,
112                    wgpu::TexelCopyBufferLayout {
113                        offset: 0,
114                        bytes_per_row: Some(bytes_per_pixel(source.format) * source.size),
115                        rows_per_image: Some(source.size),
116                    },
117                    wgpu::Extent3d {
118                        width: source.size,
119                        height: source.size,
120                        depth_or_array_layers: 1,
121                    },
122                );
123            }
124
125            if mip_count > 1 {
126                mipmap_generator.generate_mips(
127                    &backend.device,
128                    &backend.queue,
129                    &texture,
130                    source.format,
131                    mip_count,
132                    6,
133                );
134            }
135        }
136
137        let view = texture.create_view(&wgpu::TextureViewDescriptor {
138            dimension: Some(wgpu::TextureViewDimension::Cube),
139            ..Default::default()
140        });
141        Some(Self { texture, view })
142    }
143}
144
145pub struct CubemapPlugin;
146impl Plugin for CubemapPlugin {
147    fn build(&self, app: &mut crate::prelude::App) {
148        app.add_plugin(LazyResourcePlugin::<WGPUBackend, MipmapGenerator>::new());
149        app.add_plugin(AssetPlugin::<WGPUBackend, GPUCubemap>::new());
150    }
151}