From 4c6c6c4dd5886746ace98f0a42c705aa9cd07aae Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 14 Jul 2024 22:14:08 -0400 Subject: [PATCH] render: PCF with poisson disc on directional lights --- Cargo.lock | 135 +++++++++++++++++++ examples/shadows/src/main.rs | 4 +- lyra-game/Cargo.toml | 1 + lyra-game/src/render/graph/passes/meshes.rs | 23 ++++ lyra-game/src/render/graph/passes/shadows.rs | 125 ++++++++++++----- lyra-game/src/render/light/mod.rs | 2 +- lyra-game/src/render/shaders/base.wgsl | 9 +- lyra-game/src/render/texture_atlas.rs | 6 +- 8 files changed, 261 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c988b5c..bdf2688 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-array" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c92d086290f52938013f6242ac62bf7d401fab8ad36798a609faa65c3fd2c" +dependencies = [ + "generic-array", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -122,6 +140,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "ash" version = "0.37.3+1.3.251" @@ -351,6 +378,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "backtrace" version = "0.3.69" @@ -786,6 +819,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "divrem" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69dde51e8fef5e12c1d65e0929b03d66e4c0c18282bc30ed2ca050ad6f44dd82" + [[package]] name = "dlib" version = "0.5.2" @@ -795,6 +834,12 @@ dependencies = [ "libloading 0.8.1", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "downcast-rs" version = "1.2.0" @@ -807,6 +852,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "elapsed" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f4e5af126dafd0741c2ad62d47f68b28602550102e5f0dd45c8a97fc8b49c29" + [[package]] name = "elua" version = "0.1.0" @@ -908,6 +959,18 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fast_poisson" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2472baa9796d2ee497bd61690e3093a26935390d8ce0dd0ddc2db9b47a65898f" +dependencies = [ + "kiddo", + "rand 0.8.5", + "rand_distr", + "rand_xoshiro", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -953,6 +1016,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fixed" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc715d38bea7b5bf487fcd79bcf8c209f0b58014f3018a7a19c2b855f472048" +dependencies = [ + "az", + "bytemuck", + "half", + "num-traits", + "typenum", +] + [[package]] name = "fixed-timestep-rotating-model" version = "0.1.0" @@ -1683,6 +1759,26 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "kiddo" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1c5ea778d68eacd5c33f29537ba0b7b6c2595e74ee013a69cedc20ab4d3177" +dependencies = [ + "aligned", + "aligned-array", + "az", + "divrem", + "doc-comment", + "elapsed", + "fixed", + "log", + "min-max-heap", + "num-traits", + "rand 0.8.5", + "rayon", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -1750,6 +1846,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libredox" version = "0.0.2" @@ -1868,6 +1970,7 @@ dependencies = [ "bind_match", "bytemuck", "cfg-if", + "fast_poisson", "gilrs-core", "glam", "image", @@ -2079,6 +2182,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "min-max-heap" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2687e6cf9c00f48e9284cf9fd15f2ef341d03cc7743abf9df4c5f07fdee50b18" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -2286,6 +2395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2727,6 +2837,25 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "range-alloc" version = "0.1.3" @@ -3226,6 +3355,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/examples/shadows/src/main.rs b/examples/shadows/src/main.rs index 5cef2a6..56a31c0 100644 --- a/examples/shadows/src/main.rs +++ b/examples/shadows/src/main.rs @@ -6,7 +6,7 @@ use lyra_engine::{ InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput, }, math::{self, Transform, Vec3}, - render::light::{directional::DirectionalLight, PointLight}, + render::light::directional::DirectionalLight, scene::{ CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, WorldTransform, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, @@ -161,7 +161,7 @@ fn setup_scene_plugin(game: &mut Game) { DirectionalLight { enabled: true, color: Vec3::new(1.0, 0.95, 0.9), - intensity: 0.5, + intensity: 0.9, }, light_tran, )); diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index 7c0d0d2..621a5f8 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -39,6 +39,7 @@ rustc-hash = "1.1.0" petgraph = { version = "0.6.5", features = ["matrix_graph"] } bind_match = "0.1.2" round_mult = "0.1.3" +fast_poisson = { version = "1.0.0", features = ["single_precision"] } [features] tracy = ["dep:tracing-tracy"] diff --git a/lyra-game/src/render/graph/passes/meshes.rs b/lyra-game/src/render/graph/passes/meshes.rs index 08aef81..99d127d 100644 --- a/lyra-game/src/render/graph/passes/meshes.rs +++ b/lyra-game/src/render/graph/passes/meshes.rs @@ -125,6 +125,11 @@ impl Node for MeshPass { .expect("missing ShadowMapsPassSlots::ShadowLightUniformsBuffer") .as_buffer() .unwrap(); + let pcf_poisson_disc = graph + .slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) + .expect("missing ShadowMapsPassSlots::PcfPoissonDiscBuffer") + .as_buffer() + .unwrap(); let atlas_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("bgl_shadows_atlas"), @@ -165,6 +170,16 @@ impl Node for MeshPass { }, count: None, }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, ], }); @@ -196,6 +211,14 @@ impl Node for MeshPass { size: None, }), }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: pcf_poisson_disc, + offset: 0, + size: None, + }), + }, ], }); diff --git a/lyra-game/src/render/graph/passes/shadows.rs b/lyra-game/src/render/graph/passes/shadows.rs index 220d890..b9b418f 100644 --- a/lyra-game/src/render/graph/passes/shadows.rs +++ b/lyra-game/src/render/graph/passes/shadows.rs @@ -1,10 +1,9 @@ use std::{ - collections::VecDeque, - mem, - rc::Rc, - sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, + collections::VecDeque, mem, rc::Rc, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard} }; +use fast_poisson::Poisson2D; +use itertools::Itertools; use lyra_ecs::{ query::{filter::Has, Entities}, AtomicRef, Component, Entity, ResourceData, @@ -16,12 +15,17 @@ use tracing::warn; use wgpu::util::DeviceExt; use crate::render::{ - graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, light::{directional::DirectionalLight, LightType, PointLight}, resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, transform_buffer_storage::TransformBuffers, vertex::Vertex, AtlasFrame, GpuSlotBuffer, TextureAtlas + graph::{Node, NodeDesc, NodeType, SlotAttribute, SlotValue}, + light::{directional::DirectionalLight, LightType, PointLight}, + resource::{FragmentState, RenderPipeline, RenderPipelineDescriptor, Shader, VertexState}, + transform_buffer_storage::TransformBuffers, + vertex::Vertex, + AtlasFrame, GpuSlotBuffer, TextureAtlas, }; use super::{MeshBufferStorage, RenderAssets, RenderMeshes}; -const PCF_SAMPLES_NUM: u32 = 4; +const PCF_SAMPLES_NUM: u32 = 6; const SHADOW_SIZE: glam::UVec2 = glam::uvec2(1024, 1024); #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] @@ -32,6 +36,7 @@ pub enum ShadowMapsPassSlots { ShadowAtlasSizeBuffer, ShadowLightUniformsBuffer, ShadowSettingsUniform, + PcfPoissonDiscBuffer, } #[derive(Debug, Clone, Hash, PartialEq, RenderGraphLabel)] @@ -174,8 +179,7 @@ impl ShadowMapsPass { let atlas_index = atlas .pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) .expect("failed to pack new shadow map into texture atlas"); - let atlas_frame = atlas.texture_frame(atlas_index) - .expect("Frame missing"); + let atlas_frame = atlas.texture_frame(atlas_index).expect("Frame missing"); let projection = glam::Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, NEAR_PLANE, FAR_PLANE); @@ -253,24 +257,12 @@ impl ShadowMapsPass { ), ]; - let atlas_idx_1 = - atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) - .unwrap(); - let atlas_idx_2 = - atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) - .unwrap(); - let atlas_idx_3 = - atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) - .unwrap(); - let atlas_idx_4 = - atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) - .unwrap(); - let atlas_idx_5 = - atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) - .unwrap(); - let atlas_idx_6 = - atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _) - .unwrap(); + let atlas_idx_1 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); + let atlas_idx_2 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); + let atlas_idx_3 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); + let atlas_idx_4 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); + let atlas_idx_5 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); + let atlas_idx_6 = atlas.pack(SHADOW_SIZE.x as _, SHADOW_SIZE.y as _).unwrap(); let frames = [ atlas.texture_frame(atlas_idx_1).unwrap(), @@ -325,6 +317,37 @@ impl ShadowMapsPass { fn mesh_buffers(&self) -> AtomicRef> { self.mesh_buffers.as_ref().unwrap().get() } + + /// Create the gpu buffer for a poisson disc + fn create_poisson_disc_buffer(&self, device: &wgpu::Device, label: &str, num_samples: u32) -> wgpu::Buffer { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size: mem::size_of::() as u64 * (num_samples.pow(2)) as u64, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }) + } + + /// Generate and write a Poisson disc to `buffer` with `num_pcf_samples.pow(2)` amount of points. + fn write_poisson_disc(&self, queue: &wgpu::Queue, buffer: &wgpu::Buffer, num_pcf_samples: u32) { + let num_points = num_pcf_samples.pow(2); + let num_floats = num_points * 2; // points are vec2f + let min_dist = (num_floats as f32).sqrt() / num_floats as f32; + let mut points = vec![]; + + // use a while loop to ensure that the correct number of floats is created + while points.len() < num_floats as usize { + let poisson = Poisson2D::new() + .with_dimensions([1.0, 1.0], min_dist) + .with_samples(num_pcf_samples); + + points = poisson.iter().flatten().collect_vec(); + + } + points.truncate(num_floats as _); + + queue.write_buffer(buffer, 0, bytemuck::cast_slice(points.as_slice())); + } } impl Node for ShadowMapsPass { @@ -368,7 +391,8 @@ impl Node for ShadowMapsPass { Some(SlotValue::Buffer(self.atlas_size_buffer.clone())), ); - let settings_buffer = graph.device().create_buffer(&wgpu::BufferDescriptor { + let device = graph.device(); + let settings_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("buffer_shadow_settings"), size: mem::size_of::() as _, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, @@ -380,6 +404,14 @@ impl Node for ShadowMapsPass { Some(SlotValue::Buffer(Arc::new(settings_buffer))), ); + node.add_buffer_slot( + ShadowMapsPassSlots::PcfPoissonDiscBuffer, + SlotAttribute::Output, + Some(SlotValue::Buffer(Arc::new( + self.create_poisson_disc_buffer(device, "buffer_poisson_disc_pcf", PCF_SAMPLES_NUM), + ))), + ); + node } @@ -390,9 +422,20 @@ impl Node for ShadowMapsPass { context: &mut crate::render::graph::RenderGraphContext, ) { { + // TODO: Update the poisson disc every time the PCF sampling point number changed + if !world.has_resource::() { + let buffer = graph.slot_value(ShadowMapsPassSlots::PcfPoissonDiscBuffer) + .unwrap().as_buffer().unwrap(); + self.write_poisson_disc(&context.queue, &buffer, ShadowSettings::default().pcf_samples_num); + } + // TODO: only write buffer on changes to resource let shadow_settings = world.get_resource_or_default::(); - context.queue_buffer_write_with(ShadowMapsPassSlots::ShadowSettingsUniform, 0, ShadowSettingsUniform::from(*shadow_settings)); + context.queue_buffer_write_with( + ShadowMapsPassSlots::ShadowSettingsUniform, + 0, + ShadowSettingsUniform::from(*shadow_settings), + ); } self.render_meshes = world.try_get_resource_data::(); @@ -425,8 +468,13 @@ impl Node for ShadowMapsPass { for (entity, pos, _) in world.view_iter::<(Entities, &Transform, Has)>() { if !self.depth_maps.contains_key(&entity) { // TODO: dont pack the textures as they're added - let atlas_index = - self.create_depth_map(&context.queue, LightType::Directional, entity, *pos, 45.0); + let atlas_index = self.create_depth_map( + &context.queue, + LightType::Directional, + entity, + *pos, + 45.0, + ); index_components_queue.push_back((entity, atlas_index)); } } @@ -553,12 +601,12 @@ impl Node for ShadowMapsPass { }); for light_depth_map in self.depth_maps.values() { - match light_depth_map.light_type { LightType::Directional => { pass.set_pipeline(&pipeline); - let frame = atlas.texture_frame(light_depth_map.atlas_index) + let frame = atlas + .texture_frame(light_depth_map.atlas_index) .expect("missing atlas frame for light"); light_shadow_pass_impl( @@ -570,12 +618,13 @@ impl Node for ShadowMapsPass { &frame, light_depth_map.uniform_index[0] as _, ); - }, + } LightType::Point => { pass.set_pipeline(&point_light_pipeline); for side in 0..6 { - let frame = atlas.texture_frame(light_depth_map.atlas_index + side) + let frame = atlas + .texture_frame(light_depth_map.atlas_index + side) .expect("missing atlas frame of light"); let ui = light_depth_map.uniform_index[side as usize]; @@ -589,7 +638,7 @@ impl Node for ShadowMapsPass { ui as _, ); } - }, + } LightType::Spotlight => todo!(), } } @@ -714,7 +763,9 @@ pub struct ShadowSettings { impl Default for ShadowSettings { fn default() -> Self { - Self { pcf_samples_num: PCF_SAMPLES_NUM } + Self { + pcf_samples_num: PCF_SAMPLES_NUM, + } } } @@ -731,4 +782,4 @@ impl From for ShadowSettingsUniform { pcf_samples_num: value.pcf_samples_num, } } -} \ No newline at end of file +} diff --git a/lyra-game/src/render/light/mod.rs b/lyra-game/src/render/light/mod.rs index b744a8a..545d4eb 100644 --- a/lyra-game/src/render/light/mod.rs +++ b/lyra-game/src/render/light/mod.rs @@ -211,7 +211,7 @@ impl LightUniformBuffers { lights.push(uniform); } - assert!(lights.len() < self.max_light_count as _); // ensure we dont overwrite the buffer + assert!(lights.len() < self.max_light_count as usize); // ensure we dont overwrite the buffer // write the amount of lights to the buffer, and right after that the list of lights. queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&[lights.len()])); diff --git a/lyra-game/src/render/shaders/base.wgsl b/lyra-game/src/render/shaders/base.wgsl index 4a61fa1..0e026f2 100755 --- a/lyra-game/src/render/shaders/base.wgsl +++ b/lyra-game/src/render/shaders/base.wgsl @@ -137,6 +137,8 @@ var s_shadow_maps_atlas: sampler_comparison; var u_shadow_settings: ShadowSettingsUniform; @group(5) @binding(3) var u_light_shadow: array; +@group(5) @binding(4) +var u_pcf_poisson_disc: array>; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { @@ -287,11 +289,16 @@ fn pcf_dir_light(tex_coords: vec2, test_depth: f32, shadow_u: LightShadowMa // Sample PCF var shadow = 0.0; + var i = 0; for (var x = -half_filter_size; x <= half_filter_size; x += 1.0) { for (var y = -half_filter_size; y <= half_filter_size; y += 1.0) { - let offset = tex_coords + vec2(x, y) * texel_size; + //let random = u_pcf_poisson_disc[i] * texel_size; + let offset = tex_coords + (u_pcf_poisson_disc[i] + vec2(x, y)) * texel_size; + let pcf_depth = textureSampleCompare(t_shadow_maps_atlas, s_shadow_maps_atlas, offset, test_depth); shadow += pcf_depth; + + i++; } } shadow /= pow(f32(u_shadow_settings.pcf_samples_num), 2.0); diff --git a/lyra-game/src/render/texture_atlas.rs b/lyra-game/src/render/texture_atlas.rs index 34955c9..3dec36e 100644 --- a/lyra-game/src/render/texture_atlas.rs +++ b/lyra-game/src/render/texture_atlas.rs @@ -198,7 +198,7 @@ impl SkylinePacker { /* if r.bottom() < min_height || (r.bottom() == min_height && self.skylines[i].width < min_width as usize) */ if y + height < min_height || - (y + height == min_height && self.skylines[i].width < min_width as _) + (y + height == min_height && self.skylines[i].width < min_width as usize) { min_height = y + height; min_width = self.skylines[i].width as _; @@ -224,8 +224,8 @@ impl SkylinePacker { width: frame.width as _ }; - assert!(skyline.right() <= self.size.x as _); - assert!(skyline.y <= self.size.y as _); + assert!(skyline.right() <= self.size.x as usize); + assert!(skyline.y <= self.size.y as usize); self.skylines.insert(i, skyline);