qbsp/mesh/lightmap/
mod.rs

1//! Module for computing lightmap atlases with various techniques for GPU rendering.
2
3use std::collections::HashMap;
4
5use glam::{uvec2, UVec2, Vec2};
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8use smallvec::{smallvec, SmallVec};
9use thiserror::Error;
10
11mod packer;
12
13pub use packer::{DefaultLightmapPacker, LightmapPacker, LightmapPackerFaceView, PerSlotLightmapPacker, PerStyleLightmapPacker};
14
15use crate::{
16	data::{lighting::LightmapStyle, texture::BspTexFlags},
17	mesh::FaceExtents,
18	BspData, BspParseError,
19};
20
21#[derive(Debug, Clone, Copy)]
22#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23pub struct ComputeLightmapSettings {
24	/// The of a pixel is no lightmaps are stored there.
25	pub default_color: [u8; 3],
26	/// A single pixel of a lightmap atlas is reserved for faces which don't have a lightmap or `special` flag, this is the color of that pixel.
27	pub no_lighting_color: [u8; 3],
28	/// A single pixel of a lightmap atlas is reserved for faces which don't have a lightmap, but do have the `special` flag, this is the color of that pixel.
29	pub special_lighting_color: [u8; 3],
30	pub max_width: u32,
31	pub max_height: u32,
32	/// Number of pixels to pad around each island, stretches the sides of textures.
33	pub extrusion: u32,
34}
35impl Default for ComputeLightmapSettings {
36	fn default() -> Self {
37		Self {
38			default_color: [0; 3],
39			no_lighting_color: [0; 3],
40			special_lighting_color: [255; 3],
41			max_width: 2048,
42			max_height: u32::MAX,
43			extrusion: 0,
44		}
45	}
46}
47
48#[derive(Error, Debug, Clone)]
49pub enum ComputeLightmapAtlasError {
50	#[error(
51		"Failed to pack lightmap of size {lightmap_size}, {images_packed} lightmaps have already been packed. Max atlas size: {max_lightmap_size}"
52	)]
53	PackFailure {
54		lightmap_size: UVec2,
55		images_packed: usize,
56		max_lightmap_size: UVec2,
57	},
58	#[error("No lightmaps")]
59	NoLightmaps,
60	#[error("DECOUPLED_LM BSPX lump is present, but failed to parse: {0}")]
61	InvalidDecoupledLM(BspParseError),
62}
63
64struct ReservedLightmapPixel {
65	position: Option<UVec2>,
66	color: [u8; 3],
67}
68impl ReservedLightmapPixel {
69	pub fn new(color: [u8; 3]) -> Self {
70		Self { position: None, color }
71	}
72
73	pub fn get_uvs<P: LightmapPacker>(
74		&mut self,
75		lightmap_packer: &mut P,
76		view: LightmapPackerFaceView,
77	) -> Result<FaceUvs, ComputeLightmapAtlasError> {
78		let position = match self.position {
79			Some(v) => v,
80			None => {
81				// TODO: Is this handled by `texture_packer`?
82				let rect = lightmap_packer.pack(
83					view,
84					P::create_single_color_input(UVec2::ONE + lightmap_packer.settings().extrusion * 2, self.color),
85				)?;
86				self.position = Some(rect.min);
87				rect.min
88			}
89		};
90
91		Ok(smallvec![position.as_vec2() + Vec2::splat(0.5); view.face.num_edges.0 as usize])
92	}
93}
94
95impl BspData {
96	/// Packs every face's lightmap together onto a single atlas for GPU rendering.
97	pub fn compute_lightmap_atlas<P: LightmapPacker>(&self, mut packer: P) -> Result<LightmapAtlasOutput<P>, ComputeLightmapAtlasError> {
98		let Some(lighting) = &self.lighting else { return Err(ComputeLightmapAtlasError::NoLightmaps) };
99		let mut decoupled_lm = match self.bspx.parse_decoupled_lm(&self.parse_ctx) {
100			Some(x) => Some(x.map_err(ComputeLightmapAtlasError::InvalidDecoupledLM)?),
101			None => None,
102		};
103
104		// This is done in FTE quake's source code, each with a comment saying "sigh" after, not sure why.
105		if let Some(lm_infos) = &mut decoupled_lm {
106			for lm_info in lm_infos {
107				lm_info.projection.u_offset += 0.5;
108				lm_info.projection.v_offset += 0.5;
109			}
110		}
111
112		let settings = packer.settings();
113
114		let mut lightmap_uvs: HashMap<u32, FaceUvs> = HashMap::new();
115
116		let mut empty_reserved_pixel = ReservedLightmapPixel::new(settings.no_lighting_color);
117		let mut special_reserved_pixel = ReservedLightmapPixel::new(settings.special_lighting_color);
118
119		for (face_idx, face) in self.faces.iter().enumerate() {
120			let tex_info = &self.tex_info[face.texture_info_idx.0 as usize];
121
122			let decoupled_lightmap = decoupled_lm.as_ref().map(|lm_infos| lm_infos[face_idx]);
123
124			let lm_info = match &decoupled_lightmap {
125				Some(lm_info) => {
126					let uvs: FaceUvs = face.vertices(self).map(|pos| lm_info.projection.project(pos)).collect();
127					let extents = FaceExtents::new_decoupled(uvs.iter().copied(), lm_info);
128
129					LightmapInfo {
130						uvs,
131						extents,
132						lightmap_offset: lm_info.offset,
133					}
134				}
135				None => {
136					let uvs: FaceUvs = face.vertices(self).map(|pos| tex_info.projection.project(pos)).collect();
137					let extents = FaceExtents::new(uvs.iter().copied());
138
139					LightmapInfo {
140						uvs,
141						extents,
142						lightmap_offset: face.lightmap_offset.pixels,
143					}
144				}
145			};
146
147			let view = LightmapPackerFaceView {
148				lm_info: &lm_info,
149
150				bsp: self,
151
152				face_idx,
153				face,
154				tex_info,
155				lighting,
156			};
157
158			if lm_info.lightmap_offset.is_negative() || lm_info.extents.lightmap_size() == UVec2::ZERO {
159				lightmap_uvs.insert(
160					face_idx as u32,
161					if tex_info.flags.texture_flags.unwrap_or_default() == BspTexFlags::Normal {
162						// TODO: For BSP3x (GoldSrc/Quake 2), we should look at the texture name
163						// to figure out the texture flags.
164						empty_reserved_pixel.get_uvs(&mut packer, view)?
165					} else {
166						special_reserved_pixel.get_uvs(&mut packer, view)?
167					},
168				);
169				continue;
170			}
171
172			let input = packer.read_from_face(view);
173
174			let frame = packer.pack(view, input)?;
175
176			lightmap_uvs.insert(
177				face_idx as u32,
178				lm_info
179					.extents
180					.compute_lightmap_uvs(lm_info.uvs, (frame.min + settings.extrusion).as_vec2())
181					.collect(),
182			);
183		}
184
185		let atlas = packer.export();
186
187		// Normalize lightmap UVs from texture space
188		for uvs in lightmap_uvs.values_mut() {
189			for uv in uvs {
190				*uv /= atlas.size().as_vec2();
191			}
192		}
193
194		Ok(LightmapAtlasOutput {
195			uvs: lightmap_uvs,
196			data: atlas,
197		})
198	}
199}
200
201/// Computed information about the specifics of how a lightmap applies to a face.
202#[derive(Debug, Clone)]
203pub struct LightmapInfo {
204	/// The vertices of the face projected onto it's texture or decoupled lightmap.
205	pub uvs: FaceUvs,
206	pub extents: FaceExtents,
207	/// The offset into the lightmap lump in bytes to read the lightmap data or -1. Will need to be multiplied by 3 for colored lighting.
208	pub lightmap_offset: i32,
209}
210impl LightmapInfo {
211	/// Computes the index into [`BspLighting`](crate::data::lighting::BspLighting) for the specific face specified. Assumes [`lightmap_offset`](Self::lightmap_offset) is positive.
212	#[inline]
213	pub fn compute_lighting_index(&self, light_style_idx: usize, x: u32, y: u32) -> usize {
214		self.lightmap_offset as usize
215			+ (self.extents.lightmap_pixels() as usize * light_style_idx)
216			+ (y * self.extents.lightmap_size().x + x) as usize
217	}
218}
219
220/// Trait for a resulting lightmap atlas from a [`LightmapPacker`].
221pub trait LightmapAtlas {
222	fn size(&self) -> UVec2;
223}
224
225pub struct PerSlotLightmapData {
226	pub slots: [image::RgbImage; 4],
227	pub styles: image::RgbaImage,
228}
229impl LightmapAtlas for PerSlotLightmapData {
230	fn size(&self) -> UVec2 {
231		self.styles.dimensions().into()
232	}
233}
234
235/// Container for mapping lightmap styles to lightmap images (either atlas' or standalone) to later composite together to achieve animated lightmaps.
236///
237/// This is just a wrapper for a HashMap that ensures that all containing images are the same size.
238#[derive(Debug, Clone)]
239pub struct PerStyleLightmapData {
240	size: UVec2,
241	inner: HashMap<LightmapStyle, image::RgbImage>,
242}
243impl PerStyleLightmapData {
244	#[inline]
245	pub fn new(size: impl Into<UVec2>) -> Self {
246		Self {
247			size: size.into(),
248			inner: HashMap::new(),
249		}
250	}
251
252	#[inline]
253	pub fn inner(&self) -> &HashMap<LightmapStyle, image::RgbImage> {
254		&self.inner
255	}
256
257	#[inline]
258	pub fn into_inner(self) -> HashMap<LightmapStyle, image::RgbImage> {
259		self.inner
260	}
261
262	/// Modifies the internal map, checking to ensure all images are the same size after.
263	pub fn modify_inner<O, F: FnOnce(&mut HashMap<LightmapStyle, image::RgbImage>) -> O>(
264		&mut self,
265		modifier: F,
266	) -> Result<O, LightmapsInvalidSizeError> {
267		let out = modifier(&mut self.inner);
268
269		for (style, image) in &self.inner {
270			let image_size = uvec2(image.width(), image.height());
271			if self.size != image_size {
272				return Err(LightmapsInvalidSizeError {
273					style: *style,
274					image_size,
275					expected_size: self.size,
276				});
277			}
278		}
279
280		Ok(out)
281	}
282
283	/// Inserts a new image into the collection. Returns `Err` if the atlas' size doesn't match the collection's expected size.
284	pub fn insert(&mut self, style: LightmapStyle, image: image::RgbImage) -> Result<Option<image::RgbImage>, LightmapsInvalidSizeError> {
285		let image_size = uvec2(image.width(), image.height());
286		if self.size != image_size {
287			return Err(LightmapsInvalidSizeError {
288				style,
289				image_size,
290				expected_size: self.size,
291			});
292		}
293
294		Ok(self.inner.insert(style, image))
295	}
296}
297impl LightmapAtlas for PerStyleLightmapData {
298	fn size(&self) -> UVec2 {
299		self.size
300	}
301}
302
303#[derive(Debug, Error)]
304#[error("Lightmap image of style {style} is size {image_size}, when the lightmap collection's expected size is {expected_size}")]
305pub struct LightmapsInvalidSizeError {
306	pub style: LightmapStyle,
307	pub image_size: UVec2,
308	pub expected_size: UVec2,
309}
310
311/// Contains a lightmap packers' output, and the UVs into said atlas' for each face.
312pub struct LightmapAtlasOutput<P: LightmapPacker> {
313	/// Map of face indexes to normalized UV coordinates into the atlas.
314	pub uvs: LightmapUvMap,
315	pub data: P::Output,
316}
317
318/// Maps face indexes to normalized UV coordinates into a lightmap atlas.
319pub type LightmapUvMap = HashMap<u32, FaceUvs>;
320
321/// The vast majority of faces have 5 or less vertices, so this is a pretty easy optimization.
322pub type FaceUvs = SmallVec<[Vec2; 5]>;