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