Skip to main content

pebble/wgpu/
cubemap.rs

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