pebble/wgpu/cubemap.rs
1use crate::{
2 assets::upload::Asset,
3 ecs::system::Res,
4 wgpu::{
5 backend::WGPUBackend,
6 gpu_context::GpuContext,
7 mipmap::MipmapGenerator,
8 textures::{bytes_per_pixel, decode_file, write_texture_level0},
9 },
10};
11
12/// Source data for [`GPUCubemap`]. Prefer the
13/// [`from_files`](Self::from_files)/[`from_faces`](Self::from_faces)/
14/// [`empty`](Self::empty) constructors over setting fields by hand.
15pub struct CubemapDescriptor {
16 /// Edge length in pixels — cubemap faces are always square.
17 pub size: u32,
18 /// GPU pixel format to upload as. Defaults to `Rgba8UnormSrgb`.
19 pub format: wgpu::TextureFormat,
20 /// `Some` uploads 6 faces of pixel data up front (wgpu's expected
21 /// order: +X, -X, +Y, -Y, +Z, -Z). `None` allocates an empty cubemap
22 /// meant to be filled later by rendering into per-face views — e.g. an
23 /// environment-map capture pass — in which case [`wgpu_descriptor`](Self::wgpu_descriptor)
24 /// adds `RENDER_ATTACHMENT` usage instead of requiring upload data.
25 pub faces: Option<[Vec<u8>; 6]>,
26 /// File paths for each of the 6 faces (same order as `faces`), decoded
27 /// through the same loader used by `GPUTexture`/`GPUTextureArray`.
28 pub face_files: Option<[&'static str; 6]>,
29 /// Whether to generate a full mip chain (via [`MipmapGenerator`]). Only
30 /// applies when uploading pixel data (`faces`/`face_files` set) —
31 /// meaningless for an [`empty`](Self::empty) render-target cubemap.
32 pub generate_mips: bool,
33}
34
35impl CubemapDescriptor {
36 /// Load 6 faces from files (+X, -X, +Y, -Y, +Z, -Z). Size is inferred from the first face.
37 pub fn from_files(size: u32, files: [&'static str; 6]) -> Self {
38 Self {
39 size,
40 format: wgpu::TextureFormat::Rgba8UnormSrgb,
41 faces: None,
42 face_files: Some(files),
43 generate_mips: false,
44 }
45 }
46
47 /// Supply raw pixel bytes for each face (+X, -X, +Y, -Y, +Z, -Z).
48 pub fn from_faces(size: u32, format: wgpu::TextureFormat, faces: [Vec<u8>; 6]) -> Self {
49 Self {
50 size,
51 format,
52 faces: Some(faces),
53 face_files: None,
54 generate_mips: false,
55 }
56 }
57
58 /// Allocate an empty cubemap for use as a render target (e.g. environment capture).
59 pub fn empty(size: u32, format: wgpu::TextureFormat) -> Self {
60 Self {
61 size,
62 format,
63 faces: None,
64 face_files: None,
65 generate_mips: false,
66 }
67 }
68
69 /// Override the format set by whichever constructor was used (all
70 /// three default to or take `format` directly — this exists for the
71 /// builder-chain case, e.g. `CubemapDescriptor::empty(size, format).with_mips()`
72 /// followed later by a format change, without re-specifying `size`).
73 pub fn with_format(mut self, format: wgpu::TextureFormat) -> Self {
74 self.format = format;
75 self
76 }
77
78 /// Enable full mip chain generation.
79 pub fn with_mips(mut self) -> Self {
80 self.generate_mips = true;
81 self
82 }
83
84 /// `render_target` is set for an empty capture-target cubemap (see
85 /// [`empty`](Self::empty)), rendered into directly. Separately from
86 /// that, `mip_count > 1` also needs `RENDER_ATTACHMENT` — mips beyond
87 /// level 0 are rendered into by [`MipmapGenerator::generate_mips`](super::mipmap::MipmapGenerator::generate_mips)
88 /// regardless of whether the base texture is a capture target or one
89 /// uploaded from real face data, so the two conditions are OR'd rather
90 /// than `render_target` alone deciding the usage.
91 fn wgpu_descriptor(&self, mip_count: u32, render_target: bool) -> wgpu::TextureDescriptor<'_> {
92 let mut usage = super::mipmap::texture_usage(mip_count);
93 if render_target {
94 usage |= wgpu::TextureUsages::RENDER_ATTACHMENT;
95 }
96
97 wgpu::TextureDescriptor {
98 label: None,
99 size: wgpu::Extent3d {
100 width: self.size,
101 height: self.size,
102 depth_or_array_layers: 6,
103 },
104 mip_level_count: mip_count,
105 sample_count: 1,
106 dimension: wgpu::TextureDimension::D2,
107 format: self.format,
108 usage,
109 view_formats: &[],
110 }
111 }
112}
113
114/// A cubemap uploaded to the GPU, ready to bind (e.g. via
115/// [`BindingInstanceEntry::Cubemap`](super::instance::BindingInstanceEntry::Cubemap)).
116/// Opaque — bind it via
117/// [`BindGroupBuilder::texture_cubemap`](super::buffers::BindGroupBuilder::texture_cubemap).
118///
119/// [`empty`](CubemapDescriptor::empty)'s documented use case — rendering
120/// into per-face views for environment capture — needs raw per-face
121/// `wgpu::TextureView` access that isn't available yet: the render-pass
122/// recording API itself is still unwrapped (see the crate's `wgpu`
123/// module-level docs), so there's currently no way to hand a capture pass a
124/// face of this texture to render into. Tracked as a follow-up once pass
125/// recording is wrapped.
126pub struct GPUCubemap {
127 texture: wgpu::Texture,
128 view: wgpu::TextureView,
129 size: u32,
130 format: wgpu::TextureFormat,
131 ctx: GpuContext,
132}
133
134impl GPUCubemap {
135 /// Overwrites one face's level-0 pixel data (+X, -X, +Y, -Y, +Z, -Z is
136 /// `face` 0..=5, matching [`CubemapDescriptor::from_faces`]'s order).
137 /// See [`GPUTexture::write`](super::textures::GPUTexture::write) for the
138 /// same caveat about mip levels not being regenerated.
139 pub fn write_face(&self, face: u32, pixels: &[u8]) {
140 write_texture_level0(self.ctx.queue(), &self.texture, face, self.format, self.size, self.size, pixels);
141 }
142
143 /// Edge length in pixels.
144 pub fn size(&self) -> u32 {
145 self.size
146 }
147
148 pub(crate) fn view(&self) -> &wgpu::TextureView {
149 &self.view
150 }
151}
152
153impl Asset<WGPUBackend> for GPUCubemap {
154 type Source = CubemapDescriptor;
155 type Deps<'a> = Res<'a, MipmapGenerator>;
156
157 fn upload<'a>(
158 source: &CubemapDescriptor,
159 backend: &WGPUBackend,
160 mipmap_generator: &Res<'a, MipmapGenerator>,
161 ) -> Option<Self> {
162 let faces: Option<[Vec<u8>; 6]> = if let Some(files) = &source.face_files {
163 let mut out: [Vec<u8>; 6] = Default::default();
164 for (i, path) in files.iter().enumerate() {
165 let (w, h, data) = decode_file(path, source.format)?;
166 if w != source.size || h != source.size {
167 tracing::error!(
168 "CubemapSpec: face {i} ('{path}') is {w}x{h}, expected {0}x{0}",
169 source.size
170 );
171 return None;
172 }
173 out[i] = data;
174 }
175 Some(out)
176 } else {
177 source.faces.clone()
178 };
179
180 let mip_count = super::mipmap::mip_count(source.size, source.generate_mips);
181
182 let texture = backend
183 .device
184 .create_texture(&source.wgpu_descriptor(mip_count, faces.is_none()));
185
186 if let Some(faces) = &faces {
187 for (face, data) in faces.iter().enumerate() {
188 backend.queue.write_texture(
189 wgpu::TexelCopyTextureInfo {
190 texture: &texture,
191 mip_level: 0,
192 origin: wgpu::Origin3d {
193 x: 0,
194 y: 0,
195 z: face as u32,
196 },
197 aspect: wgpu::TextureAspect::All,
198 },
199 data,
200 wgpu::TexelCopyBufferLayout {
201 offset: 0,
202 bytes_per_row: Some(bytes_per_pixel(source.format) * source.size),
203 rows_per_image: Some(source.size),
204 },
205 wgpu::Extent3d {
206 width: source.size,
207 height: source.size,
208 depth_or_array_layers: 1,
209 },
210 );
211 }
212
213 if mip_count > 1 {
214 mipmap_generator.generate_mips(
215 &backend.device,
216 &backend.queue,
217 &texture,
218 source.format,
219 mip_count,
220 6,
221 );
222 }
223 }
224
225 let view = texture.create_view(&wgpu::TextureViewDescriptor {
226 dimension: Some(wgpu::TextureViewDimension::Cube),
227 ..Default::default()
228 });
229 Some(Self {
230 texture,
231 view,
232 size: source.size,
233 format: source.format,
234 ctx: GpuContext::from_backend(backend),
235 })
236 }
237}
238
239crate::wgpu::plugin_macros::mipmap_asset_plugin! {
240 /// Registers the [`GPUCubemap`] asset pipeline (`Assets<CubemapDescriptor>`
241 /// → `ProcessedAssets<GPUCubemap>`), plus the [`MipmapGenerator`] it
242 /// depends on for `generate_mips`. Included by
243 /// [`WGPUPlugin`](super::backend::WGPUPlugin); add directly only if you're
244 /// assembling the `wgpu` module's plugins by hand.
245 CubemapPlugin, GPUCubemap
246}