use lightmap crate for the actual lightmap generation

- the interface to lightmaps in the engine remains the same
- only the "math" is used from the `lightmap` crate
This commit is contained in:
Dmitry Stepanov
2024-03-07 22:03:00 +03:00
parent b1bc6d2ee8
commit db4aa8c4f4
2 changed files with 74 additions and 476 deletions
+1
View File
@@ -67,6 +67,7 @@ half = "2.2.1"
fast_image_resize = "3.0.4"
base64 = "0.21.0"
uvgen = "0.1.0"
lightmap = { git = "https://github.com/mrDIMAS/lightmap" }
[features]
enable_profiler = ["fyrox-core/enable_profiler"]
+73 -476
View File
@@ -9,9 +9,8 @@
use crate::{
asset::manager::{ResourceManager, ResourceRegistrationError},
core::{
algebra::{Matrix3, Matrix4, Point3, Vector2, Vector3, Vector4},
arrayvec::ArrayVec,
math::{self, ray::Ray, Matrix4Ext, Rect, TriangleDefinition, Vector2Ext},
algebra::{Matrix3, Matrix4, Point3, Vector2, Vector3},
math::{Matrix4Ext, TriangleDefinition},
pool::Handle,
reflect::prelude::*,
sstorage::ImmutableString,
@@ -36,7 +35,9 @@ use crate::{
utils::{uvgen, uvgen::SurfaceDataPatch},
};
use fxhash::FxHashMap;
use fyrox_core::math::octree::{Octree, OctreeNode};
use lightmap::light::{
DirectionalLightDefinition, LightDefinition, PointLightDefinition, SpotLightDefinition,
};
use rayon::prelude::*;
use std::{
fmt::{Display, Formatter},
@@ -151,32 +152,13 @@ pub struct Lightmap {
pub patches: FxHashMap<u64, SurfaceDataPatchWrapper>,
}
struct WorldVertex {
world_normal: Vector3<f32>,
world_position: Vector3<f32>,
second_tex_coord: Vector2<f32>,
}
struct InstanceData {
/// World-space vertices.
vertices: Vec<WorldVertex>,
triangles: Vec<TriangleDefinition>,
octree: Octree,
}
struct Instance {
owner: Handle<Node>,
source_data: SurfaceSharedData,
data: Option<InstanceData>,
data: Option<lightmap::input::Mesh>,
transform: Matrix4<f32>,
}
impl Instance {
pub fn data(&self) -> &InstanceData {
self.data.as_ref().unwrap()
}
}
/// Small helper that allows you stop lightmap generation in any time.
#[derive(Clone, Default)]
pub struct CancellationToken(pub Arc<AtomicBool>);
@@ -334,7 +316,7 @@ impl From<VertexFetchError> for LightmapGenerationError {
pub struct LightmapInputData {
data_set: FxHashMap<u64, SurfaceSharedData>,
instances: Vec<Instance>,
lights: Vec<LightDefinition>,
lights: FxHashMap<Handle<Node>, LightDefinition>,
}
impl LightmapInputData {
@@ -364,7 +346,7 @@ impl LightmapInputData {
progress_indicator.set_stage(ProgressStage::LightsCaching, light_count);
let mut lights = Vec::with_capacity(light_count as usize);
let mut lights = FxHashMap::default();
for (handle, node) in scene.graph.pair_iter() {
if !filter(handle, node) {
@@ -380,43 +362,50 @@ impl LightmapInputData {
}
if let Some(point) = node.cast::<PointLight>() {
lights.push(LightDefinition::Point(PointLightDefinition {
lights.insert(
handle,
intensity: point.base_light_ref().intensity(),
position: node.global_position(),
color: point.base_light_ref().color().srgb_to_linear().as_frgb(),
radius: point.radius(),
sqr_radius: point.radius() * point.radius(),
}))
LightDefinition::Point(PointLightDefinition {
intensity: point.base_light_ref().intensity(),
position: node.global_position(),
color: point.base_light_ref().color().srgb_to_linear().as_frgb(),
radius: point.radius(),
sqr_radius: point.radius() * point.radius(),
}),
)
} else if let Some(spot) = node.cast::<SpotLight>() {
lights.push(LightDefinition::Spot(SpotLightDefinition {
lights.insert(
handle,
intensity: spot.base_light_ref().intensity(),
edge0: ((spot.hotspot_cone_angle() + spot.falloff_angle_delta()) * 0.5).cos(),
edge1: (spot.hotspot_cone_angle() * 0.5).cos(),
color: spot.base_light_ref().color().srgb_to_linear().as_frgb(),
direction: node
.up_vector()
.try_normalize(std::f32::EPSILON)
.unwrap_or_else(Vector3::y),
position: node.global_position(),
distance: spot.distance(),
sqr_distance: spot.distance() * spot.distance(),
}))
LightDefinition::Spot(SpotLightDefinition {
intensity: spot.base_light_ref().intensity(),
edge0: ((spot.hotspot_cone_angle() + spot.falloff_angle_delta()) * 0.5)
.cos(),
edge1: (spot.hotspot_cone_angle() * 0.5).cos(),
color: spot.base_light_ref().color().srgb_to_linear().as_frgb(),
direction: node
.up_vector()
.try_normalize(f32::EPSILON)
.unwrap_or_else(Vector3::y),
position: node.global_position(),
distance: spot.distance(),
sqr_distance: spot.distance() * spot.distance(),
}),
)
} else if let Some(directional) = node.cast::<DirectionalLight>() {
lights.push(LightDefinition::Directional(DirectionalLightDefinition {
lights.insert(
handle,
intensity: directional.base_light_ref().intensity(),
direction: node
.up_vector()
.try_normalize(std::f32::EPSILON)
.unwrap_or_else(Vector3::y),
color: directional
.base_light_ref()
.color()
.srgb_to_linear()
.as_frgb(),
}))
LightDefinition::Directional(DirectionalLightDefinition {
intensity: directional.base_light_ref().intensity(),
direction: node
.up_vector()
.try_normalize(f32::EPSILON)
.unwrap_or_else(Vector3::y),
color: directional
.base_light_ref()
.color()
.srgb_to_linear()
.as_frgb(),
}),
)
} else {
continue;
};
@@ -579,7 +568,7 @@ impl Lightmap {
* view.read_3_f32(VertexAttributeUsage::Normal).unwrap())
.try_normalize(f32::EPSILON)
.unwrap_or_default();
WorldVertex {
lightmap::input::WorldVertex {
world_normal,
world_position,
second_tex_coord: view
@@ -589,23 +578,17 @@ impl Lightmap {
})
.collect::<Vec<_>>();
let world_triangles = data
.geometry_buffer
.iter()
.map(|tri| {
[
world_vertices[tri[0] as usize].world_position,
world_vertices[tri[1] as usize].world_position,
world_vertices[tri[2] as usize].world_position,
]
})
.collect::<Vec<_>>();
instance.data = Some(InstanceData {
vertices: world_vertices,
triangles: data.geometry_buffer.triangles_ref().to_vec(),
octree: Octree::new(&world_triangles, 64),
});
instance.data = Some(
lightmap::input::Mesh::new(
world_vertices,
data.geometry_buffer
.triangles_ref()
.iter()
.map(|t| t.0)
.collect(),
)
.unwrap(),
);
progress_indicator.advance_progress();
@@ -617,15 +600,20 @@ impl Lightmap {
progress_indicator.set_stage(ProgressStage::CalculatingLight, instances.len() as u32);
let mut map: FxHashMap<Handle<Node>, Vec<LightmapEntry>> = FxHashMap::default();
for instance in instances.iter() {
let meshes = instances
.iter_mut()
.filter_map(|i| i.data.take())
.collect::<Vec<_>>();
let light_definitions = lights.values().map(|d| d).cloned().collect::<Vec<_>>();
for (mesh, instance) in meshes.iter().zip(instances.iter()) {
if cancellation_token.is_cancelled() {
return Err(LightmapGenerationError::Cancelled);
}
let lightmap = generate_lightmap(instance, &instances, &lights, texels_per_unit);
let lightmap = generate_lightmap(mesh, &meshes, &light_definitions, texels_per_unit);
map.entry(instance.owner).or_default().push(LightmapEntry {
texture: Some(TextureResource::new_ok(Default::default(), lightmap)),
lights: lights.iter().map(|light| light.handle()).collect(),
lights: lights.keys().cloned().collect(),
});
progress_indicator.advance_progress();
@@ -661,212 +649,6 @@ impl Lightmap {
}
}
/// Directional light is a light source with parallel rays. Example: Sun.
pub struct DirectionalLightDefinition {
/// A handle of light in the scene.
pub handle: Handle<Node>,
/// Intensity is how bright light is. Default is 1.0.
pub intensity: f32,
/// Direction of light rays.
pub direction: Vector3<f32>,
/// Color of light.
pub color: Vector3<f32>,
}
/// Spot light is a cone light source. Example: flashlight.
pub struct SpotLightDefinition {
/// A handle of light in the scene.
pub handle: Handle<Node>,
/// Intensity is how bright light is. Default is 1.0.
pub intensity: f32,
/// Color of light.
pub color: Vector3<f32>,
/// Direction vector of light.
pub direction: Vector3<f32>,
/// Position of light in world coordinates.
pub position: Vector3<f32>,
/// Distance at which light intensity decays to zero.
pub distance: f32,
/// Square of distance.
pub sqr_distance: f32,
/// Smoothstep left bound. It is ((hotspot_cone_angle + falloff_angle_delta) * 0.5).cos()
pub edge0: f32,
/// Smoothstep right bound. It is (hotspot_cone_angle * 0.5).cos()
pub edge1: f32,
}
/// Point light is a spherical light source. Example: light bulb.
pub struct PointLightDefinition {
/// A handle of light in the scene.
pub handle: Handle<Node>,
/// Intensity is how bright light is. Default is 1.0.
pub intensity: f32,
/// Position of light in world coordinates.
pub position: Vector3<f32>,
/// Color of light.
pub color: Vector3<f32>,
/// Radius of sphere at which light intensity decays to zero.
pub radius: f32,
/// Square of radius.
pub sqr_radius: f32,
}
/// Light definition for lightmap rendering.
pub enum LightDefinition {
/// See docs of [DirectionalLightDefinition](struct.PointLightDefinition.html)
Directional(DirectionalLightDefinition),
/// See docs of [SpotLightDefinition](struct.SpotLightDefinition.html)
Spot(SpotLightDefinition),
/// See docs of [PointLightDefinition](struct.PointLightDefinition.html)
Point(PointLightDefinition),
}
impl LightDefinition {
fn handle(&self) -> Handle<Node> {
match self {
LightDefinition::Directional(v) => v.handle,
LightDefinition::Spot(v) => v.handle,
LightDefinition::Point(v) => v.handle,
}
}
}
/// Computes total area of triangles in surface data and returns size of square
/// in which triangles can fit.
fn estimate_size(data: &InstanceData, texels_per_unit: u32) -> u32 {
let mut area = 0.0;
for triangle in data.triangles.iter() {
let a = data.vertices[triangle[0] as usize].world_position;
let b = data.vertices[triangle[1] as usize].world_position;
let c = data.vertices[triangle[2] as usize].world_position;
area += math::triangle_area(a, b, c);
}
area.sqrt().ceil() as u32 * texels_per_unit
}
/// Calculates distance attenuation for a point using given distance to the point and
/// radius of a light.
fn distance_attenuation(distance: f32, sqr_radius: f32) -> f32 {
let attenuation = (1.0 - distance * distance / sqr_radius).clamp(0.0, 1.0);
attenuation * attenuation
}
/// Calculates properties of pixel (world position, normal) at given position.
fn pick(
uv: Vector2<f32>,
grid: &Grid,
data: &InstanceData,
scale: f32,
) -> Option<(Vector3<f32>, Vector3<f32>)> {
if let Some(cell) = grid.pick(uv) {
for triangle in cell.triangles.iter().map(|&ti| &data.triangles[ti]) {
let ia = triangle[0] as usize;
let ib = triangle[1] as usize;
let ic = triangle[2] as usize;
let uv_a = data.vertices[ia].second_tex_coord;
let uv_b = data.vertices[ib].second_tex_coord;
let uv_c = data.vertices[ic].second_tex_coord;
let center = (uv_a + uv_b + uv_c).scale(1.0 / 3.0);
let to_center = (center - uv)
.try_normalize(std::f32::EPSILON)
.unwrap_or_default()
.scale(scale * 0.3333333);
let mut current_uv = uv;
for _ in 0..3 {
let barycentric = math::get_barycentric_coords_2d(current_uv, uv_a, uv_b, uv_c);
if math::barycentric_is_inside(barycentric) {
let a = data.vertices[ia].world_position;
let b = data.vertices[ib].world_position;
let c = data.vertices[ic].world_position;
let na = data.vertices[ia].world_normal;
let nb = data.vertices[ib].world_normal;
let nc = data.vertices[ic].world_normal;
return Some((
math::barycentric_to_world(barycentric, a, b, c),
math::barycentric_to_world(barycentric, na, nb, nc),
));
}
// Offset uv to center for conservative rasterization.
current_uv += to_center;
}
}
}
None
}
struct GridCell {
// List of triangle indices.
triangles: Vec<usize>,
}
struct Grid {
cells: Vec<GridCell>,
size: usize,
fsize: f32,
}
impl Grid {
/// Creates uniform grid where each cell contains list of triangles
/// whose second texture coordinates intersects with it.
fn new(data: &InstanceData, size: usize) -> Self {
let mut cells = Vec::with_capacity(size);
let fsize = size as f32;
for y in 0..size {
for x in 0..size {
let bounds =
Rect::new(x as f32 / fsize, y as f32 / fsize, 1.0 / fsize, 1.0 / fsize);
let mut triangles = Vec::new();
for (triangle_index, triangle) in data.triangles.iter().enumerate() {
let uv_a = data.vertices[triangle[0] as usize].second_tex_coord;
let uv_b = data.vertices[triangle[1] as usize].second_tex_coord;
let uv_c = data.vertices[triangle[2] as usize].second_tex_coord;
let uv_min = uv_a.per_component_min(&uv_b).per_component_min(&uv_c);
let uv_max = uv_a.per_component_max(&uv_b).per_component_max(&uv_c);
let triangle_bounds =
Rect::new(uv_min.x, uv_min.y, uv_max.x - uv_min.x, uv_max.y - uv_min.y);
if triangle_bounds.intersects(bounds) {
triangles.push(triangle_index);
}
}
cells.push(GridCell { triangles })
}
}
Self {
cells,
size,
fsize: size as f32,
}
}
fn pick(&self, v: Vector2<f32>) -> Option<&GridCell> {
let ix = (v.x * self.fsize) as usize;
let iy = (v.y * self.fsize) as usize;
self.cells.get(iy * self.size + ix)
}
}
/// https://en.wikipedia.org/wiki/Lambert%27s_cosine_law
fn lambertian(light_vec: Vector3<f32>, normal: Vector3<f32>) -> f32 {
normal.dot(&light_vec).max(0.0)
}
/// https://en.wikipedia.org/wiki/Smoothstep
fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
let k = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
k * k * (3.0 - 2.0 * k)
}
/// Generates lightmap for given surface data with specified transform.
///
/// # Performance
@@ -875,205 +657,20 @@ fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
/// time it will take. Required time increases drastically if you enable shadows and
/// global illumination (TODO), because in this case your data will be raytraced.
fn generate_lightmap(
instance: &Instance,
other_instances: &[Instance],
mesh: &lightmap::input::Mesh,
other_meshes: &[lightmap::input::Mesh],
lights: &[LightDefinition],
texels_per_unit: u32,
) -> Texture {
// We have to re-generate new set of world-space vertices because UV generator
// may add new vertices on seams.
let atlas_size = estimate_size(instance.data(), texels_per_unit);
let scale = 1.0 / atlas_size as f32;
let grid = Grid::new(instance.data(), (atlas_size / 32).max(4) as usize);
let mut pixels: Vec<Vector4<u8>> =
vec![Vector4::new(0, 0, 0, 0); (atlas_size * atlas_size) as usize];
let half_pixel = scale * 0.5;
pixels
.par_iter_mut()
.enumerate()
.for_each(|(i, pixel): (usize, &mut Vector4<u8>)| {
let x = i as u32 % atlas_size;
let y = i as u32 / atlas_size;
let uv = Vector2::new(x as f32 * scale + half_pixel, y as f32 * scale + half_pixel);
if let Some((world_position, world_normal)) = pick(uv, &grid, instance.data(), scale) {
let mut pixel_color = Vector3::default();
for light in lights {
let (light_color, mut attenuation, light_position) = match light {
LightDefinition::Directional(directional) => {
let attenuation = directional.intensity
* lambertian(directional.direction, world_normal);
(directional.color, attenuation, Vector3::default())
}
LightDefinition::Spot(spot) => {
let d = spot.position - world_position;
let distance = d.norm();
let light_vec = d.scale(1.0 / distance);
let spot_angle_cos = light_vec.dot(&spot.direction);
let cone_factor = smoothstep(spot.edge0, spot.edge1, spot_angle_cos);
let attenuation = cone_factor
* spot.intensity
* lambertian(light_vec, world_normal)
* distance_attenuation(distance, spot.sqr_distance);
(spot.color, attenuation, spot.position)
}
LightDefinition::Point(point) => {
let d = point.position - world_position;
let distance = d.norm();
let light_vec = d.scale(1.0 / distance);
let attenuation = point.intensity
* lambertian(light_vec, world_normal)
* distance_attenuation(distance, point.sqr_radius);
(point.color, attenuation, point.position)
}
};
// Shadows
if attenuation >= 0.01 {
let mut query_buffer = ArrayVec::<usize, 64>::new();
let shadow_bias = 0.01;
let ray = Ray::from_two_points(light_position, world_position);
'outer_loop: for other_instance in other_instances {
other_instance
.data()
.octree
.ray_query_static(&ray, &mut query_buffer);
for &node in query_buffer.iter() {
match other_instance.data().octree.node(node) {
OctreeNode::Leaf { indices, .. } => {
let other_data = other_instance.data();
for &triangle_index in indices {
let triangle =
&other_data.triangles[triangle_index as usize];
let va = other_data.vertices[triangle[0] as usize]
.world_position;
let vb = other_data.vertices[triangle[1] as usize]
.world_position;
let vc = other_data.vertices[triangle[2] as usize]
.world_position;
if let Some(pt) =
ray.triangle_intersection_point(&[va, vb, vc])
{
if ray.origin.metric_distance(&pt) + shadow_bias
< ray.dir.norm()
{
attenuation = 0.0;
break 'outer_loop;
}
}
}
}
OctreeNode::Branch { .. } => unreachable!(),
}
}
}
}
pixel_color += light_color.scale(attenuation);
}
*pixel = Vector4::new(
(pixel_color.x.clamp(0.0, 1.0) * 255.0) as u8,
(pixel_color.y.clamp(0.0, 1.0) * 255.0) as u8,
(pixel_color.z.clamp(0.0, 1.0) * 255.0) as u8,
255, // Indicates that this pixel was "filled"
);
}
});
// Prepare light map for bilinear filtration. This step is mandatory to prevent bleeding.
let mut rgb_pixels: Vec<Vector3<u8>> = Vec::with_capacity((atlas_size * atlas_size) as usize);
for y in 0..(atlas_size as i32) {
for x in 0..(atlas_size as i32) {
let fetch = |dx: i32, dy: i32| -> Option<Vector3<u8>> {
pixels
.get(((y + dy) * (atlas_size as i32) + x + dx) as usize)
.and_then(|p| {
if p.w != 0 {
Some(Vector3::new(p.x, p.y, p.z))
} else {
None
}
})
};
let src_pixel = pixels[(y * (atlas_size as i32) + x) as usize];
if src_pixel.w == 0 {
// Check neighbour pixels marked as "filled" and use it as value.
if let Some(west) = fetch(-1, 0) {
rgb_pixels.push(west);
} else if let Some(east) = fetch(1, 0) {
rgb_pixels.push(east);
} else if let Some(north) = fetch(0, -1) {
rgb_pixels.push(north);
} else if let Some(south) = fetch(0, 1) {
rgb_pixels.push(south);
} else if let Some(north_west) = fetch(-1, -1) {
rgb_pixels.push(north_west);
} else if let Some(north_east) = fetch(1, -1) {
rgb_pixels.push(north_east);
} else if let Some(south_east) = fetch(1, 1) {
rgb_pixels.push(south_east);
} else if let Some(south_west) = fetch(-1, 1) {
rgb_pixels.push(south_west);
} else {
rgb_pixels.push(Vector3::new(0, 0, 0));
}
} else {
rgb_pixels.push(Vector3::new(src_pixel.x, src_pixel.y, src_pixel.z))
}
}
}
// Blur lightmap using simplest box filter.
let mut bytes = Vec::with_capacity((atlas_size * atlas_size * 3) as usize);
for y in 0..(atlas_size as i32) {
for x in 0..(atlas_size as i32) {
if x < 1 || y < 1 || x + 1 == atlas_size as i32 || y + 1 == atlas_size as i32 {
bytes.extend_from_slice(
rgb_pixels[(y * (atlas_size as i32) + x) as usize].as_slice(),
);
} else {
let fetch = |dx: i32, dy: i32| -> Vector3<i16> {
let u8_pixel = rgb_pixels[((y + dy) * (atlas_size as i32) + x + dx) as usize];
Vector3::new(u8_pixel.x as i16, u8_pixel.y as i16, u8_pixel.z as i16)
};
let north_west = fetch(-1, -1);
let north = fetch(0, -1);
let north_east = fetch(1, -1);
let west = fetch(-1, 0);
let center = fetch(0, 0);
let east = fetch(1, 0);
let south_west = fetch(-1, 1);
let south = fetch(0, 1);
let south_east = fetch(-1, 1);
let sum = north_west
+ north
+ north_east
+ west
+ center
+ east
+ south_west
+ south
+ south_east;
bytes.push((sum.x / 9).clamp(0, 255) as u8);
bytes.push((sum.y / 9).clamp(0, 255) as u8);
bytes.push((sum.z / 9).clamp(0, 255) as u8);
}
}
}
let map = lightmap::LightMap::new(mesh, other_meshes, lights, texels_per_unit as usize);
Texture::from_bytes(
TextureKind::Rectangle {
width: atlas_size,
height: atlas_size,
width: map.width as u32,
height: map.height as u32,
},
TexturePixelKind::RGB8,
bytes,
map.pixels,
)
.unwrap()
}