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}