mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Mesh sprites in the background.
This makes the delay afetr selecting a character before logging into the game much shorter, in the common case. It still doesn't handle things perfectly (it blocks creating Terrain::new if it's not finished, and it could be faster due to working in the background), but it's still a lot better than it was before. To improve sprite meshing performance, we also apply the terrain flat_get optimizations to sprites. Though I didn't initially know how much of an impact it would have, it feels significantly faster to me, though being able to parallelize it would be ideal.
This commit is contained in:
parent
12c3ee440e
commit
1bdf3b13a8
@ -48,6 +48,7 @@ opt-level = 2
|
||||
[profile.dev.package."veloren-server-cli"]
|
||||
opt-level = 2
|
||||
[profile.dev.package."veloren-voxygen"]
|
||||
incremental = true
|
||||
opt-level = 2
|
||||
[profile.dev.package."veloren-world"]
|
||||
opt-level = 2
|
||||
|
@ -2,7 +2,14 @@
|
||||
#![allow(incomplete_features)]
|
||||
#![allow(clippy::option_map_unit_fn)]
|
||||
#![deny(clippy::clone_on_ref_ptr)]
|
||||
#![feature(array_map, bool_to_option, const_generics, drain_filter, or_patterns)]
|
||||
#![feature(
|
||||
array_map,
|
||||
bool_to_option,
|
||||
const_generics,
|
||||
drain_filter,
|
||||
once_cell,
|
||||
or_patterns
|
||||
)]
|
||||
#![recursion_limit = "2048"]
|
||||
|
||||
#[macro_use]
|
||||
@ -47,6 +54,7 @@ pub struct GlobalState {
|
||||
pub settings: Settings,
|
||||
pub profile: Profile,
|
||||
pub window: Window,
|
||||
pub lazy_init: scene::terrain::SpriteRenderContextLazy,
|
||||
pub audio: AudioFrontend,
|
||||
pub info_message: Option<String>,
|
||||
pub clock: Clock,
|
||||
|
@ -8,6 +8,7 @@ use veloren_voxygen::{
|
||||
i18n::{self, i18n_asset_key, Localization},
|
||||
profile::Profile,
|
||||
run,
|
||||
scene::terrain::SpriteRenderContext,
|
||||
settings::{get_fps, AudioOutput, Settings},
|
||||
window::Window,
|
||||
GlobalState,
|
||||
@ -175,14 +176,17 @@ fn main() {
|
||||
i18n.read().log_missing_entries();
|
||||
|
||||
// Create window
|
||||
let (window, event_loop) = Window::new(&settings).expect("Failed to create window!");
|
||||
let (mut window, event_loop) = Window::new(&settings).expect("Failed to create window!");
|
||||
|
||||
let clipboard = iced_winit::Clipboard::new(window.window());
|
||||
|
||||
let lazy_init = SpriteRenderContext::new(window.renderer_mut());
|
||||
|
||||
let global_state = GlobalState {
|
||||
audio,
|
||||
profile,
|
||||
window,
|
||||
lazy_init,
|
||||
clock: Clock::new(std::time::Duration::from_secs_f64(
|
||||
1.0 / get_fps(settings.graphics.max_fps) as f64,
|
||||
)),
|
||||
|
@ -13,6 +13,7 @@ use common::{
|
||||
figure::Cell,
|
||||
vol::{BaseVol, ReadVol, SizedVol, Vox},
|
||||
};
|
||||
use core::convert::TryFrom;
|
||||
use vek::*;
|
||||
|
||||
type SpriteVertex = <SpritePipeline as render::Pipeline>::Vertex;
|
||||
@ -79,10 +80,10 @@ where
|
||||
};
|
||||
let get_glow = |_vol: &mut V, _pos: Vec3<i32>| 0.0;
|
||||
let get_opacity =
|
||||
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true);
|
||||
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_empty());
|
||||
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, uv| {
|
||||
should_draw_greedy(pos, delta, uv, |vox| {
|
||||
vol.get(vox).map(|vox| *vox).unwrap_or(Vox::empty())
|
||||
vol.get(vox).map(|vox| *vox).unwrap_or(Cell::empty())
|
||||
})
|
||||
};
|
||||
let create_opaque = |atlas_pos, pos, norm| {
|
||||
@ -161,6 +162,14 @@ where
|
||||
&& lower_bound.y <= upper_bound.y
|
||||
&& lower_bound.z <= upper_bound.z
|
||||
);
|
||||
// Lower bound coordinates must fit in an i16 (which means upper bound
|
||||
// coordinates fit as integers in a f23).
|
||||
assert!(
|
||||
i16::try_from(lower_bound.x).is_ok()
|
||||
&& i16::try_from(lower_bound.y).is_ok()
|
||||
&& i16::try_from(lower_bound.z).is_ok(),
|
||||
"Sprite offsets should fit in i16",
|
||||
);
|
||||
let greedy_size = upper_bound - lower_bound + 1;
|
||||
// TODO: Should this be 16, 16, 64?
|
||||
assert!(
|
||||
@ -168,39 +177,69 @@ where
|
||||
"Sprite size out of bounds: {:?} ≤ (31, 31, 63)",
|
||||
greedy_size - 1
|
||||
);
|
||||
|
||||
let (flat, flat_get) = {
|
||||
let (w, h, d) = (greedy_size + 2).into_tuple();
|
||||
let flat = {
|
||||
let vol = self;
|
||||
|
||||
let mut flat = vec![Cell::empty(); (w * h * d) as usize];
|
||||
let mut i = 0;
|
||||
for x in -1..greedy_size.x + 1 {
|
||||
for y in -1..greedy_size.y + 1 {
|
||||
for z in -1..greedy_size.z + 1 {
|
||||
let wpos = lower_bound + Vec3::new(x, y, z);
|
||||
let block = vol.get(wpos).map(|b| *b).unwrap_or(Cell::empty());
|
||||
flat[i] = block;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
flat
|
||||
};
|
||||
|
||||
let flat_get = move |flat: &Vec<Cell>, Vec3 { x, y, z }| match flat
|
||||
.get((x * h * d + y * d + z) as usize)
|
||||
.copied()
|
||||
{
|
||||
Some(b) => b,
|
||||
None => panic!("x {} y {} z {} d {} h {}", x, y, z, d, h),
|
||||
};
|
||||
|
||||
(flat, flat_get)
|
||||
};
|
||||
|
||||
// NOTE: Cast to usize is safe because of previous check, since all values fit
|
||||
// into u16 which is safe to cast to usize.
|
||||
let greedy_size = greedy_size.as_::<usize>();
|
||||
|
||||
let greedy_size_cross = greedy_size;
|
||||
let draw_delta = lower_bound;
|
||||
let draw_delta = Vec3::new(1, 1, 1);
|
||||
|
||||
let get_light = |vol: &mut V, pos: Vec3<i32>| {
|
||||
if vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true) {
|
||||
let get_light = move |flat: &mut _, pos: Vec3<i32>| {
|
||||
if flat_get(flat, pos).is_empty() {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
let get_glow = |_vol: &mut V, _pos: Vec3<i32>| 0.0;
|
||||
let get_color = |vol: &mut V, pos: Vec3<i32>| {
|
||||
vol.get(pos)
|
||||
.ok()
|
||||
.and_then(|vox| vox.get_color())
|
||||
.unwrap_or(Rgb::zero())
|
||||
let get_glow = |_flat: &mut _, _pos: Vec3<i32>| 0.0;
|
||||
let get_color = move |flat: &mut _, pos: Vec3<i32>| {
|
||||
flat_get(flat, pos).get_color().unwrap_or(Rgb::zero())
|
||||
};
|
||||
let get_opacity =
|
||||
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true);
|
||||
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, uv| {
|
||||
should_draw_greedy_ao(vertical_stripes, pos, delta, uv, |vox| {
|
||||
vol.get(vox).map(|vox| *vox).unwrap_or(Vox::empty())
|
||||
})
|
||||
let get_opacity = move |flat: &mut _, pos: Vec3<i32>| flat_get(flat, pos).is_empty();
|
||||
let should_draw = move |flat: &mut _, pos: Vec3<i32>, delta: Vec3<i32>, uv| {
|
||||
should_draw_greedy_ao(vertical_stripes, pos, delta, uv, |vox| flat_get(flat, vox))
|
||||
};
|
||||
// NOTE: Fits in i16 (much lower actually) so f32 is no problem (and the final
|
||||
// position, pos + mesh_delta, is guaranteed to fit in an f32).
|
||||
let mesh_delta = lower_bound.as_::<f32>();
|
||||
let create_opaque = |atlas_pos, pos: Vec3<f32>, norm, _meta| {
|
||||
SpriteVertex::new(atlas_pos, pos + mesh_delta, norm)
|
||||
};
|
||||
let create_opaque =
|
||||
|atlas_pos, pos: Vec3<f32>, norm, _meta| SpriteVertex::new(atlas_pos, pos, norm);
|
||||
|
||||
greedy.push(GreedyConfig {
|
||||
data: self,
|
||||
data: flat,
|
||||
draw_delta,
|
||||
greedy_size,
|
||||
greedy_size_cross,
|
||||
@ -219,8 +258,8 @@ where
|
||||
|atlas_pos, pos, norm, &meta| create_opaque(atlas_pos, pos, norm, meta),
|
||||
));
|
||||
},
|
||||
make_face_texel: move |vol: &mut V, pos, light, glow| {
|
||||
TerrainVertex::make_col_light(light, glow, get_color(vol, pos))
|
||||
make_face_texel: move |flat: &mut _, pos, light, glow| {
|
||||
TerrainVertex::make_col_light(light, glow, get_color(flat, pos))
|
||||
},
|
||||
});
|
||||
|
||||
@ -288,10 +327,10 @@ where
|
||||
.unwrap_or(Rgb::zero())
|
||||
};
|
||||
let get_opacity =
|
||||
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true);
|
||||
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_empty());
|
||||
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, uv| {
|
||||
should_draw_greedy(pos, delta, uv, |vox| {
|
||||
vol.get(vox).map(|vox| *vox).unwrap_or(Vox::empty())
|
||||
vol.get(vox).map(|vox| *vox).unwrap_or(Cell::empty())
|
||||
})
|
||||
};
|
||||
let create_opaque = |_atlas_pos, pos: Vec3<f32>, norm| ParticleVertex::new(pos, norm);
|
||||
|
@ -67,7 +67,7 @@ pub struct ShadowPipeline;
|
||||
impl ShadowPipeline {
|
||||
pub fn create_col_lights(
|
||||
renderer: &mut Renderer,
|
||||
(col_lights, col_lights_size): ColLightInfo,
|
||||
(col_lights, col_lights_size): &ColLightInfo,
|
||||
) -> Result<Texture<ColLightFmt>, RenderError> {
|
||||
renderer.create_texture_immutable_raw(
|
||||
gfx::texture::Kind::D2(
|
||||
@ -76,7 +76,7 @@ impl ShadowPipeline {
|
||||
gfx::texture::AaMode::Single,
|
||||
),
|
||||
gfx::texture::Mipmap::Provided,
|
||||
&[&col_lights],
|
||||
&[col_lights],
|
||||
gfx::texture::SamplerInfo::new(
|
||||
gfx::texture::FilterMethod::Bilinear,
|
||||
gfx::texture::WrapMode::Clamp,
|
||||
|
@ -4904,7 +4904,7 @@ impl FigureColLights {
|
||||
i32::from(tex_size.y),
|
||||
))
|
||||
.expect("Not yet implemented: allocate new atlas on allocation failure.");
|
||||
let col_lights = ShadowPipeline::create_col_lights(renderer, (tex, tex_size))?;
|
||||
let col_lights = ShadowPipeline::create_col_lights(renderer, &(tex, tex_size))?;
|
||||
let model_len = u32::try_from(opaque.vertices().len())
|
||||
.expect("The model size for this figure does not fit in a u32!");
|
||||
let model = renderer.create_model(&opaque)?;
|
||||
|
@ -11,7 +11,7 @@ pub use self::{
|
||||
figure::FigureMgr,
|
||||
lod::Lod,
|
||||
particle::ParticleMgr,
|
||||
terrain::Terrain,
|
||||
terrain::{SpriteRenderContextLazy, Terrain},
|
||||
};
|
||||
use crate::{
|
||||
audio::{ambient::AmbientMgr, music::MusicMgr, sfx::SfxMgr, AudioFrontend},
|
||||
@ -267,8 +267,14 @@ fn compute_warping_parameter_perspective<F: Float + FloatConst>(
|
||||
|
||||
impl Scene {
|
||||
/// Create a new `Scene` with default parameters.
|
||||
pub fn new(renderer: &mut Renderer, client: &Client, settings: &Settings) -> Self {
|
||||
pub fn new(
|
||||
renderer: &mut Renderer,
|
||||
lazy_init: &mut SpriteRenderContextLazy,
|
||||
client: &Client,
|
||||
settings: &Settings,
|
||||
) -> Self {
|
||||
let resolution = renderer.get_resolution().map(|e| e as f32);
|
||||
let sprite_render_context = lazy_init(renderer);
|
||||
|
||||
Self {
|
||||
data: GlobalModel {
|
||||
@ -301,7 +307,7 @@ impl Scene {
|
||||
.create_consts(&[PostProcessLocals::default()])
|
||||
.unwrap(),
|
||||
},
|
||||
terrain: Terrain::new(renderer),
|
||||
terrain: Terrain::new(renderer, sprite_render_context),
|
||||
lod: Lod::new(renderer, client, settings),
|
||||
loaded_distance: 0.0,
|
||||
map_bounds: Vec2::new(
|
||||
|
@ -304,21 +304,37 @@ impl TerrainChunkData {
|
||||
pub fn can_shadow_sun(&self) -> bool { self.visible.is_visible() || self.can_shadow_sun }
|
||||
}
|
||||
|
||||
impl<V: RectRasterableVol> Terrain<V> {
|
||||
#[derive(Clone)]
|
||||
pub struct SpriteRenderContext {
|
||||
sprite_config: Arc<SpriteSpec>,
|
||||
sprite_data: Arc<HashMap<(SpriteKind, usize), Vec<SpriteData>>>,
|
||||
sprite_col_lights: Texture<ColLightFmt>,
|
||||
}
|
||||
|
||||
pub type SpriteRenderContextLazy = Box<dyn FnMut(&mut Renderer) -> SpriteRenderContext>;
|
||||
|
||||
impl SpriteRenderContext {
|
||||
#[allow(clippy::float_cmp)] // TODO: Pending review in #587
|
||||
pub fn new(renderer: &mut Renderer) -> Self {
|
||||
pub fn new(renderer: &mut Renderer) -> SpriteRenderContextLazy {
|
||||
let max_texture_size = renderer.max_texture_size();
|
||||
|
||||
struct SpriteDataResponse {
|
||||
locals: [SpriteLocals; 8],
|
||||
model: Mesh<SpritePipeline>,
|
||||
offset: Vec3<f32>,
|
||||
}
|
||||
|
||||
struct SpriteWorkerResponse {
|
||||
sprite_config: Arc<SpriteSpec>,
|
||||
sprite_data: HashMap<(SpriteKind, usize), Vec<SpriteDataResponse>>,
|
||||
sprite_col_lights: ColLightInfo,
|
||||
}
|
||||
|
||||
let join_handle = std::thread::spawn(move || {
|
||||
// Load all the sprite config data.
|
||||
let sprite_config =
|
||||
Arc::<SpriteSpec>::load_expect("voxygen.voxel.sprite_manifest").cloned();
|
||||
|
||||
// Create a new mpsc (Multiple Produced, Single Consumer) pair for communicating
|
||||
// with worker threads that are meshing chunks.
|
||||
let (send, recv) = channel::unbounded();
|
||||
|
||||
let (atlas, col_lights) =
|
||||
Self::make_atlas(renderer).expect("Failed to create atlas texture");
|
||||
|
||||
let max_texture_size = renderer.max_texture_size();
|
||||
let max_size =
|
||||
guillotiere::Size::new(i32::from(max_texture_size), i32::from(max_texture_size));
|
||||
let mut greedy = GreedyMesh::new(max_size);
|
||||
@ -367,7 +383,7 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
});
|
||||
let sprite_mat: Mat4<f32> =
|
||||
Mat4::translation_3d(offset).scaled_3d(SPRITE_SCALE);
|
||||
move |greedy: &mut GreedyMesh, renderer: &mut Renderer| {
|
||||
move |greedy: &mut GreedyMesh| {
|
||||
(
|
||||
(kind, variation),
|
||||
scaled
|
||||
@ -389,9 +405,6 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
Segment::from(&model.read().0).scaled_by(lod_scale),
|
||||
(greedy, &mut opaque_mesh, false),
|
||||
);
|
||||
let model = renderer.create_model(&opaque_mesh).expect(
|
||||
"Failed to upload sprite model data to the GPU!",
|
||||
);
|
||||
|
||||
let sprite_scale = Vec3::one() / lod_scale;
|
||||
let sprite_mat: Mat4<f32> =
|
||||
@ -409,12 +422,10 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
},
|
||||
);
|
||||
|
||||
SpriteData {
|
||||
/* vertex_range */ model,
|
||||
SpriteDataResponse {
|
||||
model: opaque_mesh,
|
||||
offset,
|
||||
locals: renderer.create_consts(&locals_buffer).expect(
|
||||
"Failed to upload sprite locals to the GPU!",
|
||||
),
|
||||
locals: locals_buffer,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@ -423,23 +434,99 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
},
|
||||
)
|
||||
})
|
||||
.map(|mut f| f(&mut greedy, renderer))
|
||||
.map(|mut f| f(&mut greedy))
|
||||
.collect();
|
||||
|
||||
let sprite_col_lights = ShadowPipeline::create_col_lights(renderer, greedy.finalize())
|
||||
let sprite_col_lights = greedy.finalize();
|
||||
|
||||
SpriteWorkerResponse {
|
||||
sprite_config,
|
||||
sprite_data,
|
||||
sprite_col_lights,
|
||||
}
|
||||
});
|
||||
|
||||
let init = core::lazy::OnceCell::new();
|
||||
let mut join_handle = Some(join_handle);
|
||||
let mut closure = move |renderer: &mut Renderer| {
|
||||
// The second unwrap can only fail if the sprite meshing thread panics, which
|
||||
// implies that our sprite assets either were not found or did not
|
||||
// satisfy the size requirements for meshing, both of which are
|
||||
// considered invariant violations.
|
||||
let SpriteWorkerResponse {
|
||||
sprite_config,
|
||||
sprite_data,
|
||||
sprite_col_lights,
|
||||
} = join_handle
|
||||
.take()
|
||||
.expect(
|
||||
"Closure should only be called once (in a `OnceCell::get_or_init`) in the \
|
||||
absence of caught panics!",
|
||||
)
|
||||
.join()
|
||||
.unwrap();
|
||||
|
||||
let sprite_data = sprite_data
|
||||
.into_iter()
|
||||
.map(|(key, models)| {
|
||||
(
|
||||
key,
|
||||
models
|
||||
.into_iter()
|
||||
.map(
|
||||
|SpriteDataResponse {
|
||||
locals,
|
||||
model,
|
||||
offset,
|
||||
}| {
|
||||
SpriteData {
|
||||
locals: renderer
|
||||
.create_consts(&locals)
|
||||
.expect("Failed to upload sprite locals to the GPU!"),
|
||||
model: renderer.create_model(&model).expect(
|
||||
"Failed to upload sprite model data to the GPU!",
|
||||
),
|
||||
offset,
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let sprite_col_lights = ShadowPipeline::create_col_lights(renderer, &sprite_col_lights)
|
||||
.expect("Failed to upload sprite color and light data to the GPU!");
|
||||
|
||||
Self {
|
||||
sprite_config: Arc::clone(&sprite_config),
|
||||
sprite_data: Arc::new(sprite_data),
|
||||
sprite_col_lights,
|
||||
}
|
||||
};
|
||||
Box::new(move |renderer| init.get_or_init(|| closure(renderer)).clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: RectRasterableVol> Terrain<V> {
|
||||
pub fn new(renderer: &mut Renderer, sprite_render_context: SpriteRenderContext) -> Self {
|
||||
// Create a new mpsc (Multiple Produced, Single Consumer) pair for communicating
|
||||
// with worker threads that are meshing chunks.
|
||||
let (send, recv) = channel::unbounded();
|
||||
|
||||
let (atlas, col_lights) =
|
||||
Self::make_atlas(renderer).expect("Failed to create atlas texture");
|
||||
|
||||
Self {
|
||||
atlas,
|
||||
sprite_config,
|
||||
sprite_config: sprite_render_context.sprite_config,
|
||||
chunks: HashMap::default(),
|
||||
shadow_chunks: Vec::default(),
|
||||
mesh_send_tmp: send,
|
||||
mesh_recv: recv,
|
||||
mesh_todo: HashMap::default(),
|
||||
mesh_todos_active: Arc::new(AtomicU64::new(0)),
|
||||
sprite_data: Arc::new(sprite_data),
|
||||
sprite_col_lights,
|
||||
sprite_data: sprite_render_context.sprite_data,
|
||||
sprite_col_lights: sprite_render_context.sprite_col_lights,
|
||||
waves: renderer
|
||||
.create_texture(
|
||||
&assets::Image::load_expect("voxygen.texture.waves").read().0,
|
||||
|
@ -84,6 +84,7 @@ impl SessionState {
|
||||
// game world.
|
||||
let mut scene = Scene::new(
|
||||
global_state.window.renderer_mut(),
|
||||
&mut global_state.lazy_init,
|
||||
&*client.borrow(),
|
||||
&global_state.settings,
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user