lightmap/lib.rs
1//! Light map generator.
2//!
3//! ## Example
4//!
5//! ```rust
6//! use lightmap::{
7//! input::{Mesh, WorldVertex},
8//! light::{LightDefinition, PointLightDefinition},
9//! LightMap,
10//! };
11//! use nalgebra::{Vector2, Vector3};
12//!
13//! #[derive(Copy, Clone, Debug, Default, PartialEq)]
14//! pub struct Vertex {
15//! pub position: Vector3<f32>,
16//! pub tex_coord: Vector2<f32>,
17//! }
18//!
19//! impl Vertex {
20//! fn new(x: f32, y: f32, z: f32) -> Self {
21//! Self {
22//! position: Vector3::new(x, y, z),
23//! tex_coord: Default::default(),
24//! }
25//! }
26//! }
27//!
28//! // Create cube geometry.
29//! let mut vertices = vec![
30//! Vertex::new(-0.5, -0.5, 0.5),
31//! Vertex::new(-0.5, 0.5, 0.5),
32//! Vertex::new(0.5, 0.5, 0.5),
33//! Vertex::new(0.5, -0.5, 0.5),
34//! Vertex::new(-0.5, -0.5, -0.5),
35//! Vertex::new(-0.5, 0.5, -0.5),
36//! Vertex::new(0.5, 0.5, -0.5),
37//! Vertex::new(0.5, -0.5, -0.5),
38//! ];
39//! let mut triangles = vec![
40//! // Front
41//! [2, 1, 0],
42//! [3, 2, 0],
43//! // Back
44//! [4, 5, 6],
45//! [4, 6, 7],
46//! // Right
47//! [7, 6, 2],
48//! [2, 3, 7],
49//! // Left
50//! [0, 1, 5],
51//! [0, 5, 4],
52//! // Top
53//! [5, 1, 2],
54//! [5, 2, 6],
55//! // Bottom
56//! [3, 0, 4],
57//! [7, 3, 4],
58//! ];
59//!
60//! // Step 1. Generate second texture coordinates first.
61//! let patch = uvgen::generate_uvs(
62//! vertices.iter().map(|v| v.position),
63//! triangles.iter().cloned(),
64//! 0.005,
65//! )
66//! .unwrap();
67//!
68//! // Step 2. Clone vertices on seams.
69//! for &vertex_index in &patch.additional_vertices {
70//! let vertex = vertices[vertex_index as usize];
71//! vertices.push(vertex);
72//! }
73//!
74//! // Step 3. Assign generated second texture coordinates.
75//! for (vertex, tex_coord) in vertices.iter_mut().zip(&patch.second_tex_coords) {
76//! vertex.tex_coord = *tex_coord;
77//! }
78//!
79//! // Step 4. Replace topology of the mesh using new one, that was produced when generating UVs.
80//! triangles = patch.triangles;
81//!
82//! // Step 5. Generate light map.
83//! let lights = [LightDefinition::Point(PointLightDefinition {
84//! intensity: 1.0,
85//! color: Vector3::new(1.0, 1.0, 1.0),
86//! radius: 2.0,
87//! position: Vector3::new(0.0, 2.0, 0.0),
88//! sqr_radius: 4.0,
89//! })];
90//!
91//! let mut mesh = Mesh::new(
92//! vertices
93//! .iter()
94//! .map(|v| WorldVertex {
95//! world_normal: Default::default(),
96//! world_position: v.position,
97//! second_tex_coord: v.tex_coord,
98//! })
99//! .collect(),
100//! triangles,
101//! )
102//! .unwrap();
103//!
104//! // Calculate normals.
105//! for triangle in mesh.triangles.iter() {
106//! let pos_a = mesh.vertices[triangle[0] as usize].world_position;
107//! let pos_b = mesh.vertices[triangle[1] as usize].world_position;
108//! let pos_c = mesh.vertices[triangle[2] as usize].world_position;
109//!
110//! let normal = (pos_c - pos_a)
111//! .cross(&(pos_c - pos_b))
112//! .try_normalize(f32::EPSILON)
113//! .unwrap_or_default();
114//!
115//! mesh.vertices[triangle[0] as usize].world_normal = normal;
116//! mesh.vertices[triangle[1] as usize].world_normal = normal;
117//! mesh.vertices[triangle[2] as usize].world_normal = normal;
118//! }
119//!
120//! let meshes = vec![mesh];
121//!
122//! let light_map = LightMap::new(&meshes[0], &meshes, &lights, 64);
123//!
124//! dbg!(light_map);
125//! ```
126
127#![forbid(unsafe_code)]
128
129pub use fyrox_math as math;
130pub use uvgen;
131
132pub mod error;
133pub mod input;
134pub mod light;
135
136use crate::{input::Mesh, light::LightDefinition};
137use arrayvec::ArrayVec;
138use math::{octree::OctreeNode, ray::Ray, Rect};
139use nalgebra::{Vector2, Vector3, Vector4};
140use rayon::prelude::*;
141
142// Computes total area of triangles in surface data and returns size of square in which triangles
143// can fit.
144fn estimate_size(data: &Mesh, texels_per_unit: usize) -> usize {
145 let mut area = 0.0;
146 for triangle in data.triangles.iter() {
147 let a = data.vertices[triangle[0] as usize].world_position;
148 let b = data.vertices[triangle[1] as usize].world_position;
149 let c = data.vertices[triangle[2] as usize].world_position;
150 area += math::triangle_area(a, b, c);
151 }
152 area.sqrt().ceil() as usize * texels_per_unit
153}
154
155// Calculates distance attenuation for a point using given distance to the point and radius of a
156// light.
157fn distance_attenuation(distance: f32, sqr_radius: f32) -> f32 {
158 let attenuation = (1.0 - distance * distance / sqr_radius).clamp(0.0, 1.0);
159 attenuation * attenuation
160}
161
162// Calculates properties of pixel (world position, normal) at given position.
163fn pick(
164 uv: Vector2<f32>,
165 grid: &Grid,
166 data: &Mesh,
167 scale: f32,
168) -> Option<(Vector3<f32>, Vector3<f32>)> {
169 if let Some(cell) = grid.pick(uv) {
170 for triangle in cell.triangles.iter().map(|&ti| &data.triangles[ti]) {
171 let ia = triangle[0] as usize;
172 let ib = triangle[1] as usize;
173 let ic = triangle[2] as usize;
174
175 let uv_a = data.vertices[ia].second_tex_coord;
176 let uv_b = data.vertices[ib].second_tex_coord;
177 let uv_c = data.vertices[ic].second_tex_coord;
178
179 let center = (uv_a + uv_b + uv_c).scale(1.0 / 3.0);
180 let to_center = (center - uv)
181 .try_normalize(std::f32::EPSILON)
182 .unwrap_or_default()
183 .scale(scale * 0.3333333);
184
185 let mut current_uv = uv;
186 for _ in 0..3 {
187 let barycentric = math::get_barycentric_coords_2d(current_uv, uv_a, uv_b, uv_c);
188
189 if math::barycentric_is_inside(barycentric) {
190 let a = data.vertices[ia].world_position;
191 let b = data.vertices[ib].world_position;
192 let c = data.vertices[ic].world_position;
193
194 let na = data.vertices[ia].world_normal;
195 let nb = data.vertices[ib].world_normal;
196 let nc = data.vertices[ic].world_normal;
197
198 return Some((
199 math::barycentric_to_world(barycentric, a, b, c),
200 math::barycentric_to_world(barycentric, na, nb, nc),
201 ));
202 }
203
204 // Offset uv to center for conservative rasterization.
205 current_uv += to_center;
206 }
207 }
208 }
209 None
210}
211
212struct GridCell {
213 // List of triangle indices.
214 triangles: Vec<usize>,
215}
216
217struct Grid {
218 cells: Vec<GridCell>,
219 size: usize,
220 fsize: f32,
221}
222
223impl Grid {
224 // Creates uniform grid where each cell contains list of triangles whose second texture
225 // coordinates intersects with it.
226 fn new(data: &Mesh, size: usize) -> Self {
227 let mut cells = Vec::with_capacity(size);
228 let fsize = size as f32;
229 for y in 0..size {
230 for x in 0..size {
231 let bounds =
232 Rect::new(x as f32 / fsize, y as f32 / fsize, 1.0 / fsize, 1.0 / fsize);
233
234 let mut triangles = Vec::new();
235
236 for (triangle_index, triangle) in data.triangles.iter().enumerate() {
237 let uv_a = data.vertices[triangle[0] as usize].second_tex_coord;
238 let uv_b = data.vertices[triangle[1] as usize].second_tex_coord;
239 let uv_c = data.vertices[triangle[2] as usize].second_tex_coord;
240 let uv_min = uv_a.inf(&uv_b).inf(&uv_c);
241 let uv_max = uv_a.sup(&uv_b).sup(&uv_c);
242 let triangle_bounds =
243 Rect::new(uv_min.x, uv_min.y, uv_max.x - uv_min.x, uv_max.y - uv_min.y);
244 if triangle_bounds.intersects(bounds) {
245 triangles.push(triangle_index);
246 }
247 }
248
249 cells.push(GridCell { triangles })
250 }
251 }
252
253 Self {
254 cells,
255 size,
256 fsize: size as f32,
257 }
258 }
259
260 fn pick(&self, v: Vector2<f32>) -> Option<&GridCell> {
261 let ix = (v.x * self.fsize) as usize;
262 let iy = (v.y * self.fsize) as usize;
263 self.cells.get(iy * self.size + ix)
264 }
265}
266
267// https://en.wikipedia.org/wiki/Lambert%27s_cosine_law
268fn lambertian(light_vec: Vector3<f32>, normal: Vector3<f32>) -> f32 {
269 normal.dot(&light_vec).max(0.0)
270}
271
272// https://en.wikipedia.org/wiki/Smoothstep
273fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
274 let k = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
275 k * k * (3.0 - 2.0 * k)
276}
277
278#[derive(Clone, Debug)]
279pub struct LightMap {
280 pub pixels: Vec<u8>,
281 pub width: usize,
282 pub height: usize,
283}
284
285impl LightMap {
286 pub fn new(
287 mesh: &Mesh,
288 other_meshes: &[Mesh],
289 lights: &[LightDefinition],
290 texels_per_unit: usize,
291 ) -> Self {
292 // We have to re-generate new set of world-space vertices because UV generator
293 // may add new vertices on seams.
294 let atlas_size = estimate_size(mesh, texels_per_unit);
295 let scale = 1.0 / atlas_size as f32;
296 let grid = Grid::new(mesh, (atlas_size / 32).max(4));
297
298 let mut pixels: Vec<Vector4<u8>> = vec![Vector4::new(0, 0, 0, 0); atlas_size * atlas_size];
299
300 let half_pixel = scale * 0.5;
301 pixels
302 .par_iter_mut()
303 .enumerate()
304 .for_each(|(i, pixel): (usize, &mut Vector4<u8>)| {
305 let x = i % atlas_size;
306 let y = i / atlas_size;
307
308 let uv = Vector2::new(x as f32 * scale + half_pixel, y as f32 * scale + half_pixel);
309
310 if let Some((world_position, world_normal)) = pick(uv, &grid, mesh, scale) {
311 let mut pixel_color = Vector3::default();
312 for light in lights {
313 let (light_color, mut attenuation, light_position) = match light {
314 LightDefinition::Directional(directional) => {
315 let attenuation = directional.intensity
316 * lambertian(directional.direction, world_normal);
317 (directional.color, attenuation, Vector3::default())
318 }
319 LightDefinition::Spot(spot) => {
320 let d = spot.position - world_position;
321 let distance = d.norm();
322 let light_vec = d.scale(1.0 / distance);
323 let spot_angle_cos = light_vec.dot(&spot.direction);
324 let cone_factor =
325 smoothstep(spot.edge0, spot.edge1, spot_angle_cos);
326 let attenuation = cone_factor
327 * spot.intensity
328 * lambertian(light_vec, world_normal)
329 * distance_attenuation(distance, spot.sqr_distance);
330 (spot.color, attenuation, spot.position)
331 }
332 LightDefinition::Point(point) => {
333 let d = point.position - world_position;
334 let distance = d.norm();
335 let light_vec = d.scale(1.0 / distance);
336 let attenuation = point.intensity
337 * lambertian(light_vec, world_normal)
338 * distance_attenuation(distance, point.sqr_radius);
339 (point.color, attenuation, point.position)
340 }
341 };
342 // Shadows
343 if attenuation >= 0.01 {
344 let mut query_buffer = ArrayVec::<usize, 64>::new();
345 let shadow_bias = 0.01;
346 let ray = Ray::from_two_points(light_position, world_position);
347 'outer_loop: for other_instance in other_meshes {
348 other_instance
349 .octree
350 .ray_query_static(&ray, &mut query_buffer);
351 for &node in query_buffer.iter() {
352 match other_instance.octree.node(node) {
353 OctreeNode::Leaf { indices, .. } => {
354 let other_data = other_instance;
355 for &triangle_index in indices {
356 let triangle =
357 &other_data.triangles[triangle_index as usize];
358 let va = other_data.vertices[triangle[0] as usize]
359 .world_position;
360 let vb = other_data.vertices[triangle[1] as usize]
361 .world_position;
362 let vc = other_data.vertices[triangle[2] as usize]
363 .world_position;
364 if let Some(pt) =
365 ray.triangle_intersection_point(&[va, vb, vc])
366 {
367 if ray.origin.metric_distance(&pt) + shadow_bias
368 < ray.dir.norm()
369 {
370 attenuation = 0.0;
371 break 'outer_loop;
372 }
373 }
374 }
375 }
376 OctreeNode::Branch { .. } => unreachable!(),
377 }
378 }
379 }
380 }
381 pixel_color += light_color.scale(attenuation);
382 }
383
384 *pixel = Vector4::new(
385 (pixel_color.x.clamp(0.0, 1.0) * 255.0) as u8,
386 (pixel_color.y.clamp(0.0, 1.0) * 255.0) as u8,
387 (pixel_color.z.clamp(0.0, 1.0) * 255.0) as u8,
388 255, // Indicates that this pixel was "filled"
389 );
390 }
391 });
392
393 // Prepare light map for bilinear filtration. This step is mandatory to prevent bleeding.
394 let mut rgb_pixels: Vec<Vector3<u8>> =
395 Vec::with_capacity((atlas_size * atlas_size) as usize);
396 for y in 0..(atlas_size as i32) {
397 for x in 0..(atlas_size as i32) {
398 let fetch = |dx: i32, dy: i32| -> Option<Vector3<u8>> {
399 pixels
400 .get(((y + dy) * (atlas_size as i32) + x + dx) as usize)
401 .and_then(|p| {
402 if p.w != 0 {
403 Some(Vector3::new(p.x, p.y, p.z))
404 } else {
405 None
406 }
407 })
408 };
409
410 let src_pixel = pixels[(y * (atlas_size as i32) + x) as usize];
411 if src_pixel.w == 0 {
412 // Check neighbour pixels marked as "filled" and use it as value.
413 if let Some(west) = fetch(-1, 0) {
414 rgb_pixels.push(west);
415 } else if let Some(east) = fetch(1, 0) {
416 rgb_pixels.push(east);
417 } else if let Some(north) = fetch(0, -1) {
418 rgb_pixels.push(north);
419 } else if let Some(south) = fetch(0, 1) {
420 rgb_pixels.push(south);
421 } else if let Some(north_west) = fetch(-1, -1) {
422 rgb_pixels.push(north_west);
423 } else if let Some(north_east) = fetch(1, -1) {
424 rgb_pixels.push(north_east);
425 } else if let Some(south_east) = fetch(1, 1) {
426 rgb_pixels.push(south_east);
427 } else if let Some(south_west) = fetch(-1, 1) {
428 rgb_pixels.push(south_west);
429 } else {
430 rgb_pixels.push(Vector3::new(0, 0, 0));
431 }
432 } else {
433 rgb_pixels.push(Vector3::new(src_pixel.x, src_pixel.y, src_pixel.z))
434 }
435 }
436 }
437
438 // Blur lightmap using simplest box filter.
439 let mut bytes = Vec::with_capacity((atlas_size * atlas_size * 3) as usize);
440 for y in 0..(atlas_size as i32) {
441 for x in 0..(atlas_size as i32) {
442 if x < 1 || y < 1 || x + 1 == atlas_size as i32 || y + 1 == atlas_size as i32 {
443 bytes.extend_from_slice(
444 rgb_pixels[(y * (atlas_size as i32) + x) as usize].as_slice(),
445 );
446 } else {
447 let fetch = |dx: i32, dy: i32| -> Vector3<i16> {
448 let u8_pixel =
449 rgb_pixels[((y + dy) * (atlas_size as i32) + x + dx) as usize];
450 Vector3::new(u8_pixel.x as i16, u8_pixel.y as i16, u8_pixel.z as i16)
451 };
452
453 let north_west = fetch(-1, -1);
454 let north = fetch(0, -1);
455 let north_east = fetch(1, -1);
456 let west = fetch(-1, 0);
457 let center = fetch(0, 0);
458 let east = fetch(1, 0);
459 let south_west = fetch(-1, 1);
460 let south = fetch(0, 1);
461 let south_east = fetch(-1, 1);
462
463 let sum = north_west
464 + north
465 + north_east
466 + west
467 + center
468 + east
469 + south_west
470 + south
471 + south_east;
472
473 bytes.push((sum.x / 9).clamp(0, 255) as u8);
474 bytes.push((sum.y / 9).clamp(0, 255) as u8);
475 bytes.push((sum.z / 9).clamp(0, 255) as u8);
476 }
477 }
478 }
479
480 Self {
481 pixels: bytes,
482 width: atlas_size,
483 height: atlas_size,
484 }
485 }
486}