Merge branch 'zesterer/lod-objects' into 'master'

LoD Objects (inc. Trees)

See merge request veloren/veloren!3367
This commit is contained in:
Joshua Barretto 2022-05-11 14:56:12 +00:00
commit e02f8aee65
42 changed files with 1029 additions and 89 deletions

2
.gitattributes vendored
View File

@ -7,6 +7,8 @@
*.ogg filter=lfs diff=lfs merge=lfs -text
*.ico filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
assets/world/map/*.bin filter=lfs diff=lfs merge=lfs -text
*.ron gitlab-language=rust

View File

@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Waypoints saved between sessions and shared with group members.
- New rocks
- Weapon trails
@ -19,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- NPCs now have rudimentary personalities
- Added Belarusian translation
- Add FOV check for agents scanning for targets they are hostile to
- Implemented an LoD system for objects, making trees visible far beyond the view distance
### Changed

10
Cargo.lock generated
View File

@ -6560,6 +6560,7 @@ dependencies = [
"serde",
"tracing",
"walkdir 2.3.2",
"wavefront",
]
[[package]]
@ -7340,6 +7341,15 @@ dependencies = [
"wast",
]
[[package]]
name = "wavefront"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "249b7e6cd5bd1cc78a61d0475e5790c98bebabf2dc644a94a51ad58b39298652"
dependencies = [
"hashbrown 0.9.1",
]
[[package]]
name = "wayland-client"
version = "0.28.6"

View File

@ -59,6 +59,7 @@
"hud.settings.reset_gameplay": "Reset to Defaults",
"hud.settings.view_distance": "View Distance",
"hud.settings.lod_distance": "LoD Distance",
"hud.settings.sprites_view_distance": "Sprites View Distance",
"hud.settings.figures_view_distance": "Entities View Distance",
"hud.settings.maximum_fps": "Maximum FPS",

BIN
assets/voxygen/lod/giant_tree.obj (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/lod/house.obj (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/lod/oak.obj (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/lod/pine.obj (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -188,7 +188,7 @@ void main() {
}
vec3 reflect_color = get_sky_color(/*reflect_ray_dir*/ray_dir, time_of_day.x, f_pos, vec3(-100000), 0.125, true);
reflect_color = get_cloud_color(reflect_color, ray_dir, f_pos.xyz, time_of_day.x, 100000.0, 0.2);
reflect_color = get_cloud_color(reflect_color, ray_dir, f_pos.xyz, time_of_day.x, 100000.0, 0.1);
reflect_color *= f_light;
// Prevent the sky affecting light when underground

View File

@ -54,7 +54,16 @@ void main() {
// f_pos.z -= 250.0 * (1.0 - min(1.0001 - 0.02 / pow(tick.x - load_time, 10.0), 1.0));
// f_pos.z -= min(32.0, 25.0 * pow(distance(focus_pos.xy, f_pos.xy) / view_distance.x, 20.0));
f_pos.z -= 250.0 * (1.0 - min(1.0001 - 0.02 / pow(tick.x - load_time, 10.0), 1.0));
// Terrain 'pop-in' effect
#ifndef EXPERIMENTAL_BAREMINIMUM
#ifndef EXPERIMENTAL_NOTERRAINPOP
f_pos.z -= 250.0 * (1.0 - min(1.0001 - 0.02 / pow(tick.x - load_time, 10.0), 1.0));
// f_pos.z -= min(32.0, 25.0 * pow(distance(focus_pos.xy, f_pos.xy) / view_distance.x, 20.0));
#endif
#endif
float pull_down = pow(distance(focus_pos.xy, f_pos.xy) / (view_distance.x * 0.95), 20.0) * 0.7;
f_pos.z -= pull_down;
#ifdef EXPERIMENTAL_CURVEDWORLD
f_pos.z -= pow(distance(f_pos.xy + focus_off.xy, focus_pos.xy + focus_off.xy) * 0.05, 2);

View File

@ -324,7 +324,7 @@ vec3 lod_norm(vec2 f_pos/*vec3 pos*/) {
norm.xy += vec2(
textureLod(sampler2D(t_noise, s_noise), wpos / 100, 0).x - 0.5,
textureLod(sampler2D(t_noise, s_noise), wpos / 100 + 0.5, 0).x - 0.5
) * 0.15 / pow(norm.z + 0.1, 3);
) * 0.25 / pow(norm.z + 0.1, 3);
norm = normalize(norm);
#endif
@ -373,6 +373,7 @@ vec3 lod_col(vec2 pos) {
vec3 col = textureBicubic(t_map, s_map, pos_to_tex(pos)).rgb;
/*
#ifdef EXPERIMENTAL_PROCEDURALLODDETAIL
col *= pow(vec3(
textureLod(sampler2D(t_noise, s_noise), wpos / 40, 0).x - 0.5,
@ -380,6 +381,7 @@ vec3 lod_col(vec2 pos) {
textureLod(sampler2D(t_noise, s_noise), wpos / 45 + 0.75, 0).x - 0.5
) + 1.0, vec3(0.5));
#endif
*/
return col;
}

View File

@ -0,0 +1,126 @@
#version 420 core
#include <constants.glsl>
#define LIGHTING_TYPE LIGHTING_TYPE_REFLECTION
#define LIGHTING_REFLECTION_KIND LIGHTING_REFLECTION_KIND_GLOSSY
#if (FLUID_MODE == FLUID_MODE_CHEAP)
#define LIGHTING_TRANSPORT_MODE LIGHTING_TRANSPORT_MODE_IMPORTANCE
#elif (FLUID_MODE == FLUID_MODE_SHINY)
#define LIGHTING_TRANSPORT_MODE LIGHTING_TRANSPORT_MODE_RADIANCE
#endif
#define LIGHTING_DISTRIBUTION_SCHEME LIGHTING_DISTRIBUTION_SCHEME_MICROFACET
#define LIGHTING_DISTRIBUTION LIGHTING_DISTRIBUTION_BECKMANN
#include <globals.glsl>
layout(location = 0) in vec3 f_pos;
layout(location = 1) in vec3 f_norm;
layout(location = 2) in vec4 f_col;
layout(location = 3) in vec3 model_pos;
layout(location = 4) in float snow_cover;
layout(location = 0) out vec4 tgt_color;
#include <sky.glsl>
#include <light.glsl>
#include <lod.glsl>
const float FADE_DIST = 32.0;
void main() {
#ifdef EXPERIMENTAL_BAREMINIMUM
tgt_color = vec4(simple_lighting(f_pos.xyz, f_col.rgb, 1.0), 1);
return;
#endif
vec3 cam_to_frag = normalize(f_pos - cam_pos.xyz);
vec3 view_dir = -cam_to_frag;
#if (SHADOW_MODE == SHADOW_MODE_CHEAP || SHADOW_MODE == SHADOW_MODE_MAP || FLUID_MODE == FLUID_MODE_SHINY)
float f_alt = alt_at(f_pos.xy);
#elif (SHADOW_MODE == SHADOW_MODE_NONE || FLUID_MODE == FLUID_MODE_CHEAP)
float f_alt = f_pos.z;
#endif
#if (SHADOW_MODE == SHADOW_MODE_CHEAP || SHADOW_MODE == SHADOW_MODE_MAP)
vec4 f_shadow = textureBicubic(t_horizon, s_horizon, pos_to_tex(f_pos.xy));
float sun_shade_frac = horizon_at2(f_shadow, f_alt, f_pos, sun_dir);
#elif (SHADOW_MODE == SHADOW_MODE_NONE)
float sun_shade_frac = 1.0;
#endif
float moon_shade_frac = 1.0;
DirectionalLight sun_info = get_sun_info(sun_dir, sun_shade_frac, f_pos);
DirectionalLight moon_info = get_moon_info(moon_dir, moon_shade_frac);
vec3 surf_color = f_col.rgb;
float alpha = 1.0;
const float n2 = 1.5;
const float R_s2s0 = pow((1.0 - n2) / (1.0 + n2), 2);
const float R_s1s0 = pow((1.3325 - n2) / (1.3325 + n2), 2);
const float R_s2s1 = pow((1.0 - 1.3325) / (1.0 + 1.3325), 2);
const float R_s1s2 = pow((1.3325 - 1.0) / (1.3325 + 1.0), 2);
float R_s = (f_pos.z < f_alt) ? mix(R_s2s1 * R_s1s0, R_s1s0, medium.x) : mix(R_s2s0, R_s1s2 * R_s2s0, medium.x);
vec3 k_a = vec3(1.0);
vec3 k_d = vec3(1.0);
vec3 k_s = vec3(R_s);
vec3 my_norm = vec3(f_norm.xy, abs(f_norm.z));
vec3 voxel_norm;
float my_alt = f_pos.z + focus_off.z;
float f_ao = 1.0;
const float VOXELIZE_DIST = 2000;
float voxelize_factor = clamp(1.0 - (distance(focus_pos.xy, f_pos.xy) - view_distance.x) / VOXELIZE_DIST, 0, 0.65);
vec3 cam_dir = normalize(cam_pos.xyz - f_pos.xyz);
vec3 side_norm = normalize(vec3(my_norm.xy, 0));
vec3 top_norm = vec3(0, 0, 1);
#ifdef EXPERIMENTAL_NOLODVOXELS
f_ao = 1.0;
voxel_norm = normalize(mix(side_norm, top_norm, cam_dir.z));
#else
float side_factor = 1.0 - my_norm.z;
// min(dot(vec3(0, -sign(cam_dir.y), 0), -cam_dir), dot(vec3(-sign(cam_dir.x), 0, 0), -cam_dir))
if (max(abs(my_norm.x), abs(my_norm.y)) < 0.01 || fract(my_alt) * clamp(dot(normalize(vec3(cam_dir.xy, 0)), side_norm), 0, 1) < cam_dir.z / my_norm.z) {
f_ao *= mix(1.0, clamp(fract(my_alt) / length(my_norm.xy) + clamp(dot(side_norm, -cam_dir), 0, 1), 0, 1), voxelize_factor);
voxel_norm = top_norm;
} else {
f_ao *= mix(1.0, clamp(pow(fract(my_alt), 0.5), 0, 1), voxelize_factor);
if (fract(f_pos.x) * abs(my_norm.y / cam_dir.x) < fract(f_pos.y) * abs(my_norm.x / cam_dir.y)) {
voxel_norm = vec3(sign(cam_dir.x), 0, 0);
} else {
voxel_norm = vec3(0, sign(cam_dir.y), 0);
}
}
f_ao = min(f_ao, max(f_norm.z * 0.5 + 0.5, 0.0));
voxel_norm = mix(my_norm, voxel_norm == vec3(0.0) ? f_norm : voxel_norm, voxelize_factor);
#endif
vec3 emitted_light, reflected_light;
// To account for prior saturation.
float max_light = 0.0;
vec3 cam_attenuation = vec3(1);
float fluid_alt = max(f_pos.z + 1, floor(f_alt + 1));
vec3 mu = medium.x == MEDIUM_WATER ? MU_WATER : vec3(0.0);
max_light += get_sun_diffuse2(sun_info, moon_info, voxel_norm, view_dir, f_pos, mu, cam_attenuation, fluid_alt, k_a, k_d, k_s, alpha, voxel_norm, 1.0, emitted_light, reflected_light);
emitted_light *= f_ao;
reflected_light *= f_ao;
vec3 side_color = mix(surf_color, vec3(0.5, 0.6, 1.0), snow_cover);
vec3 top_color = mix(surf_color, surf_color * 0.3, 0.5 + snow_cover * 0.5);
surf_color = mix(side_color, top_color, pow(fract(model_pos.z * 0.1), 2.0));
surf_color = illuminate(max_light, view_dir, surf_color * emitted_light, surf_color * reflected_light);
tgt_color = vec4(surf_color, 1.0);
}

View File

@ -0,0 +1,63 @@
#version 420 core
#include <constants.glsl>
#define LIGHTING_TYPE LIGHTING_TYPE_REFLECTION
#define LIGHTING_REFLECTION_KIND LIGHTING_REFLECTION_KIND_GLOSSY
#define LIGHTING_TRANSPORT_MODE LIGHTING_TRANSPORT_MODE_IMPORTANCE
#define LIGHTING_DISTRIBUTION_SCHEME LIGHTING_DISTRIBUTION_SCHEME_MICROFACET
#define LIGHTING_DISTRIBUTION LIGHTING_DISTRIBUTION_BECKMANN
#include <globals.glsl>
#include <srgb.glsl>
#include <random.glsl>
#include <lod.glsl>
layout(location = 0) in vec3 v_pos;
layout(location = 1) in vec3 v_norm;
layout(location = 2) in vec3 v_col;
layout(location = 3) in vec3 inst_pos;
layout(location = 4) in uvec3 inst_col;
layout(location = 5) in uint inst_flags;
const uint FLAG_SNOW_COVERED = 1;
layout(location = 0) out vec3 f_pos;
layout(location = 1) out vec3 f_norm;
layout(location = 2) out vec4 f_col;
layout(location = 3) out vec3 model_pos;
layout(location = 4) out float snow_cover;
void main() {
vec3 obj_pos = inst_pos - focus_off.xyz;
f_pos = obj_pos + v_pos;
model_pos = v_pos;
float pull_down = 1.0 / pow(distance(focus_pos.xy, obj_pos.xy) / (view_distance.x * 0.95), 150.0);
#ifndef EXPERIMENTAL_NOTERRAINPOP
f_pos.z -= pull_down;
#else
f_pos.z -= step(0.1, pull_down) * 10000.0;
#endif
#ifdef EXPERIMENTAL_CURVEDWORLD
f_pos.z -= pow(distance(f_pos.xy + focus_off.xy, focus_pos.xy + focus_off.xy) * 0.05, 2);
#endif
f_norm = v_norm;
f_col = vec4(vec3(inst_col) * (1.0 / 255.0) * v_col * (hash(inst_pos.xyxy) * 0.35 + 0.65), 1.0);
if ((inst_flags & FLAG_SNOW_COVERED) > 0u) {
snow_cover = 1.0;
} else {
snow_cover = 0.0;
}
gl_Position =
all_mat *
vec4(f_pos, 1);
}

View File

@ -333,26 +333,30 @@ void main() {
hit_xy ? vec3(0.0, 0.0, sides.z) : vec3(0.0, 0.0, 0.0);
*/
vec3 voxel_norm;
const float VOXELIZE_DIST = 2000;
float voxelize_factor = clamp(1.0 - (distance(focus_pos.xy, f_pos.xy) - view_distance.x) / VOXELIZE_DIST, 0, 1);
vec3 cam_dir = normalize(cam_pos.xyz - f_pos.xyz);
vec3 side_norm = normalize(vec3(my_norm.xy, 0));
vec3 top_norm = vec3(0, 0, 1);
float side_factor = 1.0 - my_norm.z;
// min(dot(vec3(0, -sign(cam_dir.y), 0), -cam_dir), dot(vec3(-sign(cam_dir.x), 0, 0), -cam_dir))
if (max(abs(my_norm.x), abs(my_norm.y)) < 0.01 || fract(my_alt) * clamp(dot(normalize(vec3(cam_dir.xy, 0)), side_norm), 0, 1) < cam_dir.z / my_norm.z) {
//f_ao *= mix(1.0, clamp(fract(my_alt) / length(my_norm.xy) + clamp(dot(side_norm, -cam_dir), 0, 1), 0, 1), voxelize_factor);
voxel_norm = top_norm;
} else {
f_ao *= mix(1.0, clamp(pow(fract(my_alt), 0.5), 0, 1), voxelize_factor);
if (fract(f_pos.x) * abs(my_norm.y / cam_dir.x) < fract(f_pos.y) * abs(my_norm.x / cam_dir.y)) {
voxel_norm = vec3(sign(cam_dir.x), 0, 0);
#ifdef EXPERIMENTAL_NOLODVOXELS
f_ao = 1.0;
voxel_norm = normalize(mix(side_norm, top_norm, max(cam_dir.z, 0.0)));
#else
float side_factor = 1.0 - my_norm.z;
// min(dot(vec3(0, -sign(cam_dir.y), 0), -cam_dir), dot(vec3(-sign(cam_dir.x), 0, 0), -cam_dir))
if (max(abs(my_norm.x), abs(my_norm.y)) < 0.01 || fract(my_alt) * clamp(dot(normalize(vec3(cam_dir.xy, 0)), side_norm), 0, 1) < cam_dir.z / my_norm.z) {
//f_ao *= mix(1.0, clamp(fract(my_alt) / length(my_norm.xy) + clamp(dot(side_norm, -cam_dir), 0, 1), 0, 1), voxelize_factor);
voxel_norm = top_norm;
} else {
voxel_norm = vec3(0, sign(cam_dir.y), 0);
f_ao *= mix(1.0, clamp(pow(fract(my_alt), 0.5), 0, 1), voxelize_factor);
if (fract(f_pos.x) * abs(my_norm.y / cam_dir.x) < fract(f_pos.y) * abs(my_norm.x / cam_dir.y)) {
voxel_norm = vec3(sign(cam_dir.x), 0, 0);
} else {
voxel_norm = vec3(0, sign(cam_dir.y), 0);
}
}
}
#endif
// vec3 f_ao_view = max(vec3(dot(f_orig_view_dir.yz, sides.yz), dot(f_orig_view_dir.xz, sides.xz), dot(f_orig_view_dir.xy, sides.xy)), 0.0);
// delta_sides *= sqrt(1.0 - f_ao_view * f_ao_view);

View File

@ -81,8 +81,10 @@ void main() {
// Terrain 'pop-in' effect
#ifndef EXPERIMENTAL_BAREMINIMUM
v_pos.z -= 250.0 * (1.0 - min(1.0001 - 0.02 / pow(tick.x - load_time, 10.0), 1.0));
// f_pos.z -= min(32.0, 25.0 * pow(distance(focus_pos.xy, f_pos.xy) / view_distance.x, 20.0));
#ifndef EXPERIMENTAL_NOTERRAINPOP
v_pos.z -= 250.0 * (1.0 - min(1.0001 - 0.02 / pow(tick.x - load_time, 10.0), 1.0));
// f_pos.z -= min(32.0, 25.0 * pow(distance(focus_pos.xy, f_pos.xy) / view_distance.x, 20.0));
#endif
#endif
#ifdef EXPERIMENTAL_CURVEDWORLD

View File

@ -35,10 +35,12 @@ use common::{
event::{EventBus, LocalEvent},
grid::Grid,
link::Is,
lod,
mounting::Rider,
outcome::Outcome,
recipe::RecipeBook,
resources::{PlayerEntity, TimeOfDay},
spiral::Spiral2d,
terrain::{
block::Block, map::MapConfig, neighbors, BiomeKind, SitesKind, SpriteKind, TerrainChunk,
TerrainChunkSize,
@ -171,10 +173,12 @@ pub struct Client {
pub chat_mode: ChatMode,
recipe_book: RecipeBook,
available_recipes: HashMap<String, Option<SpriteKind>>,
lod_zones: HashMap<Vec2<i32>, lod::Zone>,
lod_last_requested: Option<Instant>,
max_group_size: u32,
// Client has received an invite (inviter uid, time out instant)
invite: Option<(Uid, std::time::Instant, std::time::Duration, InviteKind)>,
invite: Option<(Uid, Instant, Duration, InviteKind)>,
group_leader: Option<Uid>,
// Note: potentially representable as a client only component
group_members: HashMap<Uid, group::Role>,
@ -202,6 +206,7 @@ pub struct Client {
state: State,
view_distance: Option<u32>,
lod_distance: f32,
// TODO: move into voxygen
loaded_distance: f32,
@ -624,6 +629,9 @@ impl Client {
available_recipes: HashMap::default(),
chat_mode: ChatMode::default(),
lod_zones: HashMap::new(),
lod_last_requested: None,
max_group_size,
invite: None,
group_leader: None,
@ -650,6 +658,7 @@ impl Client {
tick: 0,
state,
view_distance: None,
lod_distance: 4.0,
loaded_distance: 0.0,
pending_chunks: HashMap::new(),
@ -769,7 +778,8 @@ impl Client {
&mut self.in_game_stream
},
//Only in game, terrain
ClientGeneral::TerrainChunkRequest { .. } => {
ClientGeneral::TerrainChunkRequest { .. }
| ClientGeneral::LodZoneRequest { .. } => {
#[cfg(feature = "tracy")]
{
terrain = 1.0;
@ -880,6 +890,11 @@ impl Client {
self.send_msg(ClientGeneral::SetViewDistance(view_distance));
}
pub fn set_lod_distance(&mut self, lod_distance: u32) {
let lod_distance = lod_distance.max(0).min(1000) as f32 / lod::ZONE_SIZE as f32;
self.lod_distance = lod_distance;
}
pub fn use_slot(&mut self, slot: Slot) {
self.control_action(ControlAction::InventoryAction(InventoryAction::Use(slot)))
}
@ -994,6 +1009,8 @@ impl Client {
&self.available_recipes
}
pub fn lod_zones(&self) -> &HashMap<Vec2<i32>, lod::Zone> { &self.lod_zones }
/// Returns whether the specified recipe can be crafted and the sprite, if
/// any, that is required to do so.
pub fn can_craft_recipe(&self, recipe: &str) -> (bool, Option<SpriteKind>) {
@ -1709,6 +1726,34 @@ impl Client {
let now = Instant::now();
self.pending_chunks
.retain(|_, created| now.duration_since(*created) < Duration::from_secs(3));
// Manage LoD zones
let lod_zone = pos.0.xy().map(|e| lod::from_wpos(e as i32));
// Request LoD zones that are in range
if self
.lod_last_requested
.map_or(true, |i| i.elapsed() > Duration::from_secs(5))
{
if let Some(rpos) = Spiral2d::new()
.take((1 + self.lod_distance.ceil() as i32 * 2).pow(2) as usize)
.filter(|rpos| !self.lod_zones.contains_key(&(lod_zone + *rpos)))
.min_by_key(|rpos| rpos.magnitude_squared())
.filter(|rpos| {
rpos.map(|e| e as f32).magnitude() < (self.lod_distance - 0.5).max(0.0)
})
{
self.send_msg_err(ClientGeneral::LodZoneRequest {
key: lod_zone + rpos,
})?;
self.lod_last_requested = Some(Instant::now());
}
}
// Cull LoD zones out of range
self.lod_zones.retain(|p, _| {
(*p - lod_zone).map(|e| e as f32).magnitude_squared() < self.lod_distance.powi(2)
});
}
Ok(())
@ -2084,6 +2129,10 @@ impl Client {
}
self.pending_chunks.remove(&key);
},
ServerGeneral::LodZoneUpdate { key, zone } => {
self.lod_zones.insert(key, zone);
self.lod_last_requested = None;
},
ServerGeneral::TerrainBlockUpdates(blocks) => {
if let Some(mut blocks) = blocks.decompress() {
blocks.drain().for_each(|(pos, block)| {

View File

@ -10,6 +10,7 @@ lazy_static = "1.4.0"
assets_manager = {version = "0.7", features = ["bincode", "ron", "json"]}
ron = { version = "0.7", default-features = false }
dot_vox = "4.0"
wavefront = "0.2" # TODO: Use vertex-colors branch when we have models that have them
image = { version = "0.23.12", default-features = false, features = ["png"] }
tracing = "0.1"

View File

@ -191,6 +191,22 @@ impl Asset for DotVoxAsset {
const EXTENSION: &'static str = "vox";
}
pub struct ObjAsset(pub wavefront::Obj);
impl Asset for ObjAsset {
type Loader = ObjAssetLoader;
const EXTENSION: &'static str = "obj";
}
pub struct ObjAssetLoader;
impl Loader<ObjAsset> for ObjAssetLoader {
fn load(content: std::borrow::Cow<[u8]>, _: &str) -> Result<ObjAsset, BoxedError> {
let data = wavefront::Obj::from_reader(&*content)?;
Ok(ObjAsset(data))
}
}
/// Return path to repository root by searching 10 directories back
pub fn find_root() -> Option<PathBuf> {
std::env::current_dir().map_or(None, |path| {

View File

@ -83,6 +83,9 @@ pub enum ClientGeneral {
TerrainChunkRequest {
key: Vec2<i32>,
},
LodZoneRequest {
key: Vec2<i32>,
},
//Always possible
ChatMsg(String),
Command(String, Vec<String>),
@ -128,6 +131,7 @@ impl ClientMsg {
| ClientGeneral::ExitInGame
| ClientGeneral::PlayerPhysics { .. }
| ClientGeneral::TerrainChunkRequest { .. }
| ClientGeneral::LodZoneRequest { .. }
| ClientGeneral::UnlockSkill(_)
| ClientGeneral::RequestSiteInfo(_)
| ClientGeneral::UnlockSkillGroup(_)

View File

@ -7,6 +7,7 @@ use common::{
calendar::Calendar,
character::{self, CharacterItem},
comp::{self, invite::InviteKind, item::MaterialStatManifest},
lod,
outcome::Outcome,
recipe::RecipeBook,
resources::TimeOfDay,
@ -169,6 +170,10 @@ pub enum ServerGeneral {
key: Vec2<i32>,
chunk: Result<SerializedTerrainChunk, ()>,
},
LodZoneUpdate {
key: Vec2<i32>,
zone: lod::Zone,
},
TerrainBlockUpdates(CompressedData<HashMap<Vec3<i32>, Block>>),
// Always possible
PlayerListUpdate(PlayerListUpdate),
@ -293,6 +298,7 @@ impl ServerMsg {
| ServerGeneral::ExitInGameSuccess
| ServerGeneral::InventoryUpdate(_, _)
| ServerGeneral::TerrainChunkUpdate { .. }
| ServerGeneral::LodZoneUpdate { .. }
| ServerGeneral::TerrainBlockUpdates(_)
| ServerGeneral::SetViewDistance(_)
| ServerGeneral::Outcomes(_)

View File

@ -49,6 +49,7 @@ pub mod figure;
pub mod generation;
#[cfg(not(target_arch = "wasm32"))] pub mod grid;
#[cfg(not(target_arch = "wasm32"))] pub mod link;
#[cfg(not(target_arch = "wasm32"))] pub mod lod;
#[cfg(not(target_arch = "wasm32"))]
pub mod lottery;
#[cfg(not(target_arch = "wasm32"))]

41
common/src/lod.rs Normal file
View File

@ -0,0 +1,41 @@
use crate::{terrain::TerrainChunkSize, vol::RectVolSize};
use serde::{Deserialize, Serialize};
use strum::EnumIter;
use vek::*;
// In chunks
pub const ZONE_SIZE: u32 = 32;
bitflags::bitflags! {
#[derive(Serialize, Deserialize)]
pub struct Flags: u8 {
const SNOW_COVERED = 0b00000001;
}
}
#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize, EnumIter)]
#[repr(u16)]
pub enum ObjectKind {
Oak,
Pine,
House,
GiantTree,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Object {
pub kind: ObjectKind,
pub pos: Vec3<i16>,
pub flags: Flags,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Zone {
pub objects: Vec<Object>,
}
pub fn to_wpos(wpos: i32) -> i32 { wpos * (TerrainChunkSize::RECT_SIZE.x * ZONE_SIZE) as i32 }
pub fn from_wpos(zone_pos: i32) -> i32 {
zone_pos.div_euclid((TerrainChunkSize::RECT_SIZE.x * ZONE_SIZE) as i32)
}

View File

@ -118,6 +118,7 @@ impl Client {
},
//Ingame related, terrain
ServerGeneral::TerrainChunkUpdate { .. }
| ServerGeneral::LodZoneUpdate { .. }
| ServerGeneral::TerrainBlockUpdates(_) => {
self.terrain_stream.lock().unwrap().send(g)
},
@ -191,6 +192,7 @@ impl Client {
},
//Ingame related, terrain
ServerGeneral::TerrainChunkUpdate { .. }
| ServerGeneral::LodZoneUpdate { .. }
| ServerGeneral::TerrainBlockUpdates(_) => {
PreparedMsg::new(5, &g, &self.terrain_stream_params)
},

View File

@ -24,6 +24,7 @@ pub mod error;
pub mod events;
pub mod input;
pub mod location;
pub mod lod;
pub mod login_provider;
pub mod metrics;
pub mod persistence;
@ -449,6 +450,9 @@ impl Server {
// Insert the world into the ECS (todo: Maybe not an Arc?)
let world = Arc::new(world);
state.ecs_mut().insert(Arc::clone(&world));
state
.ecs_mut()
.insert(lod::Lod::from_world(&world, index.as_index_ref()));
state.ecs_mut().insert(index.clone());
// Set starting time for the server.

33
server/src/lod.rs Normal file
View File

@ -0,0 +1,33 @@
use common::lod;
use hashbrown::HashMap;
use vek::*;
use world::World;
static EMPTY_ZONE: lod::Zone = lod::Zone {
objects: Vec::new(),
};
pub struct Lod {
pub zones: HashMap<Vec2<i32>, lod::Zone>,
}
impl Lod {
pub fn from_world(world: &World, index: world::IndexRef) -> Self {
let mut zones = HashMap::new();
let zone_sz = (world.sim().get_size() + lod::ZONE_SIZE - 1) / lod::ZONE_SIZE;
for i in 0..zone_sz.x {
for j in 0..zone_sz.y {
let zone_pos = Vec2::new(i, j).map(|e| e as i32);
zones.insert(zone_pos, world.get_lod_zone(zone_pos, index));
}
}
Self { zones }
}
pub fn zone(&self, zone_pos: Vec2<i32>) -> &lod::Zone {
self.zones.get(&zone_pos).unwrap_or(&EMPTY_ZONE)
}
}

View File

@ -295,6 +295,7 @@ impl Sys {
| ClientGeneral::Character(_)
| ClientGeneral::Spectate
| ClientGeneral::TerrainChunkRequest { .. }
| ClientGeneral::LodZoneRequest { .. }
| ClientGeneral::ChatMsg(_)
| ClientGeneral::Command(..)
| ClientGeneral::Terminate => {

View File

@ -1,4 +1,6 @@
use crate::{client::Client, metrics::NetworkRequestMetrics, presence::Presence, ChunkRequest};
use crate::{
client::Client, lod::Lod, metrics::NetworkRequestMetrics, presence::Presence, ChunkRequest,
};
use common::{
comp::Pos,
event::{EventBus, ServerEvent},
@ -20,6 +22,7 @@ impl<'a> System<'a> for Sys {
Entities<'a>,
Read<'a, EventBus<ServerEvent>>,
ReadExpect<'a, TerrainGrid>,
ReadExpect<'a, Lod>,
ReadExpect<'a, NetworkRequestMetrics>,
Write<'a, Vec<ChunkRequest>>,
ReadStorage<'a, Pos>,
@ -37,6 +40,7 @@ impl<'a> System<'a> for Sys {
entities,
server_event_bus,
terrain,
lod,
network_metrics,
mut chunk_requests,
positions,
@ -101,6 +105,12 @@ impl<'a> System<'a> for Sys {
network_metrics.chunks_request_dropped.inc();
}
},
ClientGeneral::LodZoneRequest { key } => {
client.send(ServerGeneral::LodZoneUpdate {
key,
zone: lod.zone(key).clone(),
})?;
},
_ => {
debug!(
"Kicking possibly misbehaving client due to invalud terrain \

View File

@ -39,6 +39,9 @@ widget_ids! {
vd_slider,
vd_text,
vd_value,
ld_slider,
ld_text,
ld_value,
lod_detail_slider,
lod_detail_text,
lod_detail_value,
@ -280,8 +283,6 @@ impl<'a> Widget for Video<'a> {
if let Some(new_val) = ImageSlider::discrete(
self.global_state.settings.graphics.view_distance,
1,
// FIXME: Move back to 64 once we support multiple texture atlases, or figure out a
// way to increase the size of the terrain atlas.
65,
self.imgs.slider_indicator,
self.imgs.slider,
@ -306,9 +307,44 @@ impl<'a> Widget for Video<'a> {
.color(TEXT_COLOR)
.set(state.ids.vd_value, ui);
// LoD Distance
Text::new(self.localized_strings.get("hud.settings.lod_distance"))
.down_from(state.ids.vd_slider, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.ld_text, ui);
if let Some(new_val) = ImageSlider::discrete(
self.global_state.settings.graphics.lod_distance,
0,
500,
self.imgs.slider_indicator,
self.imgs.slider,
)
.w_h(104.0, 22.0)
.down_from(state.ids.ld_text, 8.0)
.track_breadth(12.0)
.slider_length(10.0)
.pad_track((5.0, 5.0))
.set(state.ids.ld_slider, ui)
{
events.push(GraphicsChange::AdjustLodDistance(new_val));
}
Text::new(&format!(
"{}",
self.global_state.settings.graphics.lod_distance
))
.right_from(state.ids.ld_slider, 8.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.ld_value, ui);
// Max FPS
Text::new(self.localized_strings.get("hud.settings.maximum_fps"))
.down_from(state.ids.vd_slider, 10.0)
.down_from(state.ids.ld_slider, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
@ -343,7 +379,7 @@ impl<'a> Widget for Video<'a> {
// Max Background FPS
Text::new(self.localized_strings.get("hud.settings.background_fps"))
.down_from(state.ids.vd_slider, 10.0)
.down_from(state.ids.ld_slider, 10.0)
.right_from(state.ids.max_fps_value, 30.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
@ -391,7 +427,7 @@ impl<'a> Widget for Video<'a> {
// Present Mode
Text::new(self.localized_strings.get("hud.settings.present_mode"))
.down_from(state.ids.vd_slider, 10.0)
.down_from(state.ids.ld_slider, 10.0)
.right_from(state.ids.max_background_fps_value, 30.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)

View File

@ -135,8 +135,9 @@ impl PlayState for CharSelectionState {
{
let mut c = self.client.borrow_mut();
c.request_character(character_id);
//Send our ViewDistance
//Send our ViewDistance and LoD distance
c.set_view_distance(global_state.settings.graphics.view_distance);
c.set_lod_distance(global_state.settings.graphics.lod_distance);
}
return PlayStateResult::Switch(Box::new(SessionState::new(
global_state,

View File

@ -26,6 +26,7 @@ pub use self::{
Locals as FigureLocals,
},
fluid::Vertex as FluidVertex,
lod_object::{Instance as LodObjectInstance, Vertex as LodObjectVertex},
lod_terrain::{LodData, Vertex as LodTerrainVertex},
particle::{Instance as ParticleInstance, Vertex as ParticleVertex},
postprocess::Locals as PostProcessLocals,
@ -459,4 +460,8 @@ pub enum ExperimentalShader {
BareMinimum,
/// Lowers strength of the glow effect for lights near the camera.
LowGlowNearCamera,
/// Disable the fake voxel effect on LoD features.
NoLodVoxels,
// Disable the 'pop-in' effect when loading terrain.
NoTerrainPop,
}

View File

@ -0,0 +1,157 @@
use super::super::{AaMode, GlobalsLayouts, Vertex as VertexTrait};
use bytemuck::{Pod, Zeroable};
use std::mem;
use vek::*;
#[repr(C)]
#[derive(Copy, Clone, Debug, Zeroable, Pod)]
pub struct Vertex {
pos: [f32; 3],
norm: [f32; 3],
col: [f32; 3],
}
impl Vertex {
pub fn new(pos: Vec3<f32>, norm: Vec3<f32>, col: Rgb<f32>) -> Self {
Self {
pos: pos.into_array(),
norm: norm.into_array(),
col: col.into_array(),
}
}
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRIBUTES: [wgpu::VertexAttribute; 3] =
wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x3];
wgpu::VertexBufferLayout {
array_stride: Self::STRIDE,
step_mode: wgpu::InputStepMode::Vertex,
attributes: &ATTRIBUTES,
}
}
}
impl VertexTrait for Vertex {
const QUADS_INDEX: Option<wgpu::IndexFormat> = None;
const STRIDE: wgpu::BufferAddress = mem::size_of::<Self>() as wgpu::BufferAddress;
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Zeroable, Pod)]
pub struct Instance {
inst_pos: [f32; 3],
inst_col: [u8; 4],
flags: u32,
}
impl Instance {
pub fn new(inst_pos: Vec3<f32>, col: Rgb<u8>, flags: common::lod::Flags) -> Self {
Self {
inst_pos: inst_pos.into_array(),
inst_col: Rgba::new(col.r, col.g, col.b, 255).into_array(),
flags: flags.bits() as u32,
}
}
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRIBUTES: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
3 => Float32x3,
4 => Uint8x4,
5 => Uint32,
];
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::InputStepMode::Instance,
attributes: &ATTRIBUTES,
}
}
}
// impl Default for Instance {
// fn default() -> Self { Self::new(Mat4::identity(), 0.0, 0.0,
// Vec3::zero(), 0, 1.0, 0.0, 0) } }
// TODO: ColLightsWrapper instead?
pub struct Locals;
pub struct LodObjectPipeline {
pub pipeline: wgpu::RenderPipeline,
}
impl LodObjectPipeline {
pub fn new(
device: &wgpu::Device,
vs_module: &wgpu::ShaderModule,
fs_module: &wgpu::ShaderModule,
global_layout: &GlobalsLayouts,
aa_mode: AaMode,
) -> Self {
common_base::span!(_guard, "LodObjectPipeline::new");
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("LoD object pipeline layout"),
push_constant_ranges: &[],
bind_group_layouts: &[&global_layout.globals, &global_layout.shadow_textures],
});
let samples = match aa_mode {
AaMode::None | AaMode::Fxaa => 1,
AaMode::MsaaX4 => 4,
AaMode::MsaaX8 => 8,
AaMode::MsaaX16 => 16,
};
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("LoD object pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: vs_module,
entry_point: "main",
buffers: &[Vertex::desc(), Instance::desc()],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
clamp_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::GreaterEqual,
stencil: wgpu::StencilState {
front: wgpu::StencilFaceState::IGNORE,
back: wgpu::StencilFaceState::IGNORE,
read_mask: !0,
write_mask: !0,
},
bias: wgpu::DepthBiasState {
constant: 0,
slope_scale: 0.0,
clamp: 0.0,
},
}),
multisample: wgpu::MultisampleState {
count: samples,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: fs_module,
entry_point: "main",
targets: &[wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba16Float,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrite::ALL,
}],
}),
});
Self {
pipeline: render_pipeline,
}
}
}

View File

@ -4,6 +4,7 @@ pub mod clouds;
pub mod debug;
pub mod figure;
pub mod fluid;
pub mod lod_object;
pub mod lod_terrain;
pub mod particle;
pub mod postprocess;

View File

@ -4,8 +4,9 @@ use super::{
instances::Instances,
model::{DynamicModel, Model, SubModel},
pipelines::{
blit, bloom, clouds, debug, figure, fluid, lod_terrain, particle, shadow, skybox,
sprite, terrain, trail, ui, ColLights, GlobalsBindGroup, ShadowTexturesBindGroup,
blit, bloom, clouds, debug, figure, fluid, lod_object, lod_terrain, particle, shadow,
skybox, sprite, terrain, trail, ui, ColLights, GlobalsBindGroup,
ShadowTexturesBindGroup,
},
},
Renderer, ShadowMap, ShadowMapRenderer,
@ -764,6 +765,15 @@ impl<'pass> FirstPassDrawer<'pass> {
}
}
pub fn draw_lod_objects(&mut self) -> LodObjectDrawer<'_, 'pass> {
let mut render_pass = self.render_pass.scope("lod objects", self.borrow.device);
render_pass.set_pipeline(&self.pipelines.lod_object.pipeline);
set_quad_index_buffer::<lod_object::Vertex>(&mut render_pass, self.borrow);
LodObjectDrawer { render_pass }
}
pub fn draw_fluid(&mut self) -> FluidDrawer<'_, 'pass> {
let mut render_pass = self.render_pass.scope("fluid", self.borrow.device);
@ -909,6 +919,25 @@ impl<'pass_ref, 'pass: 'pass_ref> Drop for SpriteDrawer<'pass_ref, 'pass> {
}
}
#[must_use]
pub struct LodObjectDrawer<'pass_ref, 'pass: 'pass_ref> {
render_pass: Scope<'pass_ref, wgpu::RenderPass<'pass>>,
}
impl<'pass_ref, 'pass: 'pass_ref> LodObjectDrawer<'pass_ref, 'pass> {
pub fn draw<'data: 'pass>(
&mut self,
model: &'data Model<lod_object::Vertex>,
instances: &'data Instances<lod_object::Instance>,
) {
self.render_pass.set_vertex_buffer(0, model.buf().slice(..));
self.render_pass
.set_vertex_buffer(1, instances.buf().slice(..));
self.render_pass
.draw(0..model.len() as u32, 0..instances.count() as u32);
}
}
#[must_use]
pub struct FluidDrawer<'pass_ref, 'pass: 'pass_ref> {
render_pass: Scope<'pass_ref, wgpu::RenderPass<'pass>>,

View File

@ -1,8 +1,8 @@
use super::{
super::{
pipelines::{
blit, bloom, clouds, debug, figure, fluid, lod_terrain, particle, postprocess, shadow,
skybox, sprite, terrain, trail, ui,
blit, bloom, clouds, debug, figure, fluid, lod_object, lod_terrain, particle,
postprocess, shadow, skybox, sprite, terrain, trail, ui,
},
AaMode, BloomMode, CloudMode, FluidMode, LightingMode, PipelineModes, RenderError,
ShadowMode,
@ -28,6 +28,7 @@ pub struct Pipelines {
// player_shadow: figure::FigurePipeline,
pub skybox: skybox::SkyboxPipeline,
pub sprite: sprite::SpritePipeline,
pub lod_object: lod_object::LodObjectPipeline,
pub terrain: terrain::TerrainPipeline,
pub ui: ui::UiPipeline,
pub blit: blit::BlitPipeline,
@ -49,6 +50,7 @@ pub struct IngamePipelines {
// player_shadow: figure::FigurePipeline,
skybox: skybox::SkyboxPipeline,
sprite: sprite::SpritePipeline,
lod_object: lod_object::LodObjectPipeline,
terrain: terrain::TerrainPipeline,
}
@ -85,6 +87,7 @@ impl Pipelines {
//player_shadow: ingame.player_shadow,
skybox: ingame.skybox,
sprite: ingame.sprite,
lod_object: ingame.lod_object,
terrain: ingame.terrain,
ui: interface.ui,
blit: interface.blit,
@ -106,6 +109,8 @@ struct ShaderModules {
fluid_frag: wgpu::ShaderModule,
sprite_vert: wgpu::ShaderModule,
sprite_frag: wgpu::ShaderModule,
lod_object_vert: wgpu::ShaderModule,
lod_object_frag: wgpu::ShaderModule,
particle_vert: wgpu::ShaderModule,
particle_frag: wgpu::ShaderModule,
trail_vert: wgpu::ShaderModule,
@ -293,6 +298,8 @@ impl ShaderModules {
fluid_frag: create_shader(&selected_fluid_shader, ShaderKind::Fragment)?,
sprite_vert: create_shader("sprite-vert", ShaderKind::Vertex)?,
sprite_frag: create_shader("sprite-frag", ShaderKind::Fragment)?,
lod_object_vert: create_shader("lod-object-vert", ShaderKind::Vertex)?,
lod_object_frag: create_shader("lod-object-frag", ShaderKind::Fragment)?,
particle_vert: create_shader("particle-vert", ShaderKind::Vertex)?,
particle_frag: create_shader("particle-frag", ShaderKind::Fragment)?,
trail_vert: create_shader("trail-vert", ShaderKind::Vertex)?,
@ -415,7 +422,7 @@ fn create_ingame_and_shadow_pipelines(
needs: PipelineNeeds,
pool: &rayon::ThreadPool,
// TODO: Reduce the boilerplate in this file
tasks: [Task; 15],
tasks: [Task; 16],
) -> IngameAndShadowPipelines {
prof_span!(_guard, "create_ingame_and_shadow_pipelines");
@ -434,6 +441,7 @@ fn create_ingame_and_shadow_pipelines(
terrain_task,
fluid_task,
sprite_task,
lod_object_task,
particle_task,
trail_task,
lod_terrain_task,
@ -546,6 +554,21 @@ fn create_ingame_and_shadow_pipelines(
"sprite pipeline creation",
)
};
// Pipeline for rendering lod objects
let create_lod_object = || {
lod_object_task.run(
|| {
lod_object::LodObjectPipeline::new(
device,
&shaders.lod_object_vert,
&shaders.lod_object_frag,
&layouts.global,
pipeline_modes.aa,
)
},
"lod object pipeline creation",
)
};
// Pipeline for rendering particles
let create_particle = || {
particle_task.run(
@ -732,6 +755,7 @@ fn create_ingame_and_shadow_pipelines(
create_figure_directed_shadow,
)
};
let j7 = create_lod_object;
// Ignore this
let (
@ -739,10 +763,13 @@ fn create_ingame_and_shadow_pipelines(
((debug, (skybox, figure)), (terrain, (fluid, bloom))),
((sprite, particle), (lod_terrain, (clouds, trail))),
),
((postprocess, point_shadow), (terrain_directed_shadow, figure_directed_shadow)),
(
((postprocess, point_shadow), (terrain_directed_shadow, figure_directed_shadow)),
lod_object,
),
) = pool.join(
|| pool.join(|| pool.join(j1, j2), || pool.join(j3, j4)),
|| pool.join(j5, j6),
|| pool.join(|| pool.join(j5, j6), j7),
);
IngameAndShadowPipelines {
@ -758,6 +785,7 @@ fn create_ingame_and_shadow_pipelines(
postprocess,
skybox,
sprite,
lod_object,
terrain,
// player_shadow_pipeline,
},

View File

@ -61,6 +61,8 @@ impl assets::Compound for Shaders {
"fluid-frag.shiny",
"sprite-vert",
"sprite-frag",
"lod-object-vert",
"lod-object-frag",
"particle-vert",
"particle-frag",
"trail-vert",

View File

@ -1,17 +1,41 @@
use crate::{
render::{
pipelines::lod_terrain::{LodData, Vertex},
FirstPassDrawer, LodTerrainVertex, Mesh, Model, Quad, Renderer,
FirstPassDrawer, Instances, LodObjectInstance, LodObjectVertex, LodTerrainVertex, Mesh,
Model, Quad, Renderer, Tri,
},
scene::{camera, Camera},
settings::Settings,
};
use client::Client;
use common::{spiral::Spiral2d, util::srgba_to_linear};
use common::{
assets::{AssetExt, ObjAsset},
lod,
spiral::Spiral2d,
util::srgba_to_linear,
};
use hashbrown::HashMap;
use std::ops::Range;
use treeculler::{BVol, Frustum, AABB};
use vek::*;
// For culling
const MAX_OBJECT_RADIUS: i32 = 64;
struct ObjectGroup {
instances: Instances<LodObjectInstance>,
// None implies no instances
z_range: Option<Range<i32>>,
frustum_last_plane_index: u8,
visible: bool,
}
pub struct Lod {
model: Option<(u32, Model<LodTerrainVertex>)>,
data: LodData,
zone_objects: HashMap<Vec2<i32>, HashMap<lod::ObjectKind, ObjectGroup>>,
object_data: HashMap<lod::ObjectKind, Model<LodObjectVertex>>,
}
// TODO: Make constant when possible.
@ -22,18 +46,31 @@ pub fn water_color() -> Rgba<f32> {
impl Lod {
pub fn new(renderer: &mut Renderer, client: &Client, settings: &Settings) -> Self {
let data = LodData::new(
renderer,
client.world_data().chunk_size().as_(),
client.world_data().lod_base.raw(),
client.world_data().lod_alt.raw(),
client.world_data().lod_horizon.raw(),
settings.graphics.lod_detail.max(100).min(2500),
/* TODO: figure out how we want to do this without color borders?
* water_color().into_array().into(), */
);
Self {
model: None,
data: LodData::new(
renderer,
client.world_data().chunk_size().as_(),
client.world_data().lod_base.raw(),
client.world_data().lod_alt.raw(),
client.world_data().lod_horizon.raw(),
settings.graphics.lod_detail.max(100).min(2500),
/* TODO: figure out how we want to do this without color borders?
* water_color().into_array().into(), */
),
data,
zone_objects: HashMap::new(),
object_data: [
(lod::ObjectKind::Oak, make_lod_object("oak", renderer)),
(lod::ObjectKind::Pine, make_lod_object("pine", renderer)),
(lod::ObjectKind::House, make_lod_object("house", renderer)),
(
lod::ObjectKind::GiantTree,
make_lod_object("giant_tree", renderer),
),
]
.into_iter()
.collect(),
}
}
@ -44,7 +81,14 @@ impl Lod {
self.data.tgt_detail = (detail - detail % 2).max(100).min(2500);
}
pub fn maintain(&mut self, renderer: &mut Renderer) {
pub fn maintain(
&mut self,
renderer: &mut Renderer,
client: &Client,
focus_pos: Vec3<f32>,
camera: &Camera,
) {
// Update LoD terrain mesh according to detail
if self
.model
.as_ref()
@ -58,12 +102,100 @@ impl Lod {
.unwrap(),
));
}
// Create new LoD groups when a new zone has loaded
for (p, zone) in client.lod_zones() {
self.zone_objects.entry(*p).or_insert_with(|| {
let mut objects = HashMap::<_, Vec<_>>::new();
let mut z_range = None;
for object in zone.objects.iter() {
let pos = p.map(|e| lod::to_wpos(e) as f32).with_z(0.0)
+ object.pos.map(|e| e as f32)
+ Vec2::broadcast(0.5).with_z(0.0);
z_range = Some(z_range.map_or(
pos.z as i32..pos.z as i32,
|z_range: Range<i32>| {
z_range.start.min(pos.z as i32)..z_range.end.max(pos.z as i32)
},
));
// TODO: Put this somewhere more easily configurable, like a manifest
let color = match object.kind {
lod::ObjectKind::Pine => Rgb::new(0, 25, 12),
lod::ObjectKind::Oak => Rgb::new(10, 50, 5),
lod::ObjectKind::House => Rgb::new(20, 15, 0),
lod::ObjectKind::GiantTree => Rgb::new(8, 35, 5),
};
objects
.entry(object.kind)
.or_default()
.push(LodObjectInstance::new(pos, color, object.flags));
}
objects
.into_iter()
.map(|(kind, instances)| {
(kind, ObjectGroup {
instances: renderer
.create_instances(&instances)
.expect("Renderer error?!"),
z_range: z_range.clone(),
frustum_last_plane_index: 0,
visible: false,
})
})
.collect()
});
}
// Remove zones that are unloaded
self.zone_objects
.retain(|p, _| client.lod_zones().contains_key(p));
// Determine visiblity of zones based on view frustum
let camera::Dependents {
view_mat,
proj_mat_treeculler,
..
} = camera.dependents();
let focus_off = focus_pos.map(|e| e.trunc());
let frustum = Frustum::from_modelview_projection(
(proj_mat_treeculler * view_mat * Mat4::translation_3d(-focus_off)).into_col_arrays(),
);
for (pos, groups) in &mut self.zone_objects {
for group in groups.values_mut() {
if let Some(z_range) = &group.z_range {
let group_min = (pos.map(lod::to_wpos).with_z(z_range.start)
- MAX_OBJECT_RADIUS)
.map(|e| e as f32);
let group_max = ((pos + 1).map(lod::to_wpos).with_z(z_range.end)
+ MAX_OBJECT_RADIUS)
.map(|e| e as f32);
let (in_frustum, last_plane_index) =
AABB::new(group_min.into_array(), group_max.into_array())
.coherent_test_against_frustum(
&frustum,
group.frustum_last_plane_index,
);
group.visible = in_frustum;
group.frustum_last_plane_index = last_plane_index;
}
}
}
}
pub fn render<'a>(&'a self, drawer: &mut FirstPassDrawer<'a>) {
if let Some((_, model)) = self.model.as_ref() {
drawer.draw_lod_terrain(model);
}
// Draw LoD objects
let mut drawer = drawer.draw_lod_objects();
for groups in self.zone_objects.values() {
for (kind, group) in groups.iter().filter(|(_, g)| g.visible) {
if let Some(model) = self.object_data.get(kind) {
drawer.draw(model, &group.instances);
}
}
}
}
}
@ -94,3 +226,24 @@ fn create_lod_terrain_mesh(detail: u32) -> Mesh<LodTerrainVertex> {
})
.collect()
}
fn make_lod_object(name: &str, renderer: &mut Renderer) -> Model<LodObjectVertex> {
let model = ObjAsset::load_expect(&format!("voxygen.lod.{}", name));
let mesh = model
.read()
.0
.triangles()
.map(|vs| {
let [a, b, c] = vs.map(|v| {
LodObjectVertex::new(
v.position().into(),
v.normal().unwrap_or([0.0, 0.0, 1.0]).into(),
Rgb::broadcast(1.0),
//v.color().unwrap_or([1.0; 3]).into(),
)
});
Tri::new(a, b, c)
})
.collect();
renderer.create_model(&mesh).expect("Mesh was empty!")
}

View File

@ -681,7 +681,7 @@ impl Scene {
renderer.update_postprocess_locals(PostProcessLocals::new(proj_mat_inv, view_mat_inv));
// Maintain LoD.
self.lod.maintain(renderer);
self.lod.maintain(renderer, client, focus_pos, &self.camera);
// Maintain debug shapes
self.debug.maintain(renderer);

View File

@ -69,6 +69,7 @@ pub enum Gameplay {
#[derive(Clone)]
pub enum Graphics {
AdjustViewDistance(u32),
AdjustLodDistance(u32),
AdjustLodDetail(u32),
AdjustSpriteRenderDistance(u32),
AdjustFigureLoDRenderDistance(u32),
@ -344,6 +345,14 @@ impl SettingsChange {
settings.graphics.view_distance = view_distance;
},
Graphics::AdjustLodDistance(lod_distance) => {
session_state
.client
.borrow_mut()
.set_lod_distance(lod_distance);
settings.graphics.lod_distance = lod_distance;
},
Graphics::AdjustLodDetail(lod_detail) => {
session_state.scene.lod.set_detail(lod_detail);

View File

@ -30,6 +30,7 @@ impl fmt::Display for Fps {
#[serde(default)]
pub struct GraphicsSettings {
pub view_distance: u32,
pub lod_distance: u32,
pub sprite_render_distance: u32,
pub particles_enabled: bool,
pub lossy_terrain_compression: bool,
@ -51,6 +52,7 @@ impl Default for GraphicsSettings {
fn default() -> Self {
Self {
view_distance: 10,
lod_distance: 200,
sprite_render_distance: 100,
particles_enabled: true,
lossy_terrain_compression: false,

View File

@ -3,7 +3,7 @@ use crate::{
block::block_from_structure,
column::ColumnGen,
util::{gen_cache::StructureGenCache, RandomPerm, Sampler, UnitChooser},
Canvas,
Canvas, ColumnSample,
};
use common::{
assets::AssetHandle,
@ -33,6 +33,23 @@ static MODEL_RAND: RandomPerm = RandomPerm::new(0xDB21C052);
static UNIT_CHOOSER: UnitChooser = UnitChooser::new(0x700F4EC7);
static QUIRKY_RAND: RandomPerm = RandomPerm::new(0xA634460F);
// Ensure that it's valid to place a tree here
pub fn tree_valid_at(col: &ColumnSample, seed: u32) -> bool {
if col.alt < col.water_level
|| col.spawn_rate < 0.9
|| col.water_dist.map(|d| d < 8.0).unwrap_or(false)
|| col.path.map(|(d, _, _, _)| d < 12.0).unwrap_or(false)
{
return false;
}
if ((seed.wrapping_mul(13)) & 0xFF) as f32 / 256.0 > col.tree_density {
return false;
}
true
}
pub fn apply_trees_to(
canvas: &mut Canvas,
dynamic_rng: &mut impl Rng,
@ -68,17 +85,7 @@ pub fn apply_trees_to(
let col = ColumnGen::new(info.chunks()).get((wpos, info.index(), calendar))?;
// Ensure that it's valid to place a *thing* here
if col.alt < col.water_level
|| col.spawn_rate < 0.9
|| col.water_dist.map(|d| d < 8.0).unwrap_or(false)
|| col.path.map(|(d, _, _, _)| d < 12.0).unwrap_or(false)
{
return None;
}
// Ensure that it's valid to place a tree here
if ((seed.wrapping_mul(13)) & 0xFF) as f32 / 256.0 > col.tree_density {
if !tree_valid_at(&col, seed) {
return None;
}

View File

@ -51,6 +51,7 @@ use common::{
assets,
calendar::Calendar,
generation::{ChunkSupplement, EntityInfo},
lod,
resources::TimeOfDay,
terrain::{
Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize, TerrainGrid,
@ -60,6 +61,7 @@ use common::{
use common_net::msg::{world_msg, WorldMapMsg};
use rand::{prelude::*, Rng};
use rand_chacha::ChaCha8Rng;
use rayon::iter::ParallelIterator;
use serde::Deserialize;
use std::time::Duration;
use vek::*;
@ -463,4 +465,107 @@ impl World {
Ok((chunk, supplement))
}
// Zone coordinates
pub fn get_lod_zone(&self, pos: Vec2<i32>, index: IndexRef) -> lod::Zone {
let min_wpos = pos.map(lod::to_wpos);
let max_wpos = (pos + 1).map(lod::to_wpos);
let mut objects = Vec::new();
// Add trees
objects.append(
&mut self
.sim()
.get_area_trees(min_wpos, max_wpos)
.filter_map(|attr| {
ColumnGen::new(self.sim())
.get((attr.pos, index, self.sim().calendar.as_ref()))
.filter(|col| layer::tree::tree_valid_at(col, attr.seed))
.zip(Some(attr))
})
.filter_map(|(col, tree)| {
Some(lod::Object {
kind: match tree.forest_kind {
all::ForestKind::Oak => lod::ObjectKind::Oak,
all::ForestKind::Pine | all::ForestKind::Frostpine => {
lod::ObjectKind::Pine
},
_ => lod::ObjectKind::Oak,
},
pos: {
let rpos = tree.pos - min_wpos;
if rpos.is_any_negative() {
return None;
} else {
rpos.map(|e| e as i16).with_z(
self.sim().get_alt_approx(tree.pos).unwrap_or(0.0) as i16,
)
}
},
flags: lod::Flags::empty()
| if col.snow_cover {
lod::Flags::SNOW_COVERED
} else {
lod::Flags::empty()
},
})
})
.collect(),
);
// Add buildings
objects.extend(
index
.sites
.iter()
.filter(|(_, site)| {
site.get_origin()
.map2(min_wpos.zip(max_wpos), |e, (min, max)| e >= min && e < max)
.reduce_and()
})
.filter_map(|(_, site)| match &site.kind {
SiteKind::Refactor(site) => {
Some(site.plots().filter_map(|plot| match &plot.kind {
site2::plot::PlotKind::House(_) => Some(site.tile_wpos(plot.root_tile)),
_ => None,
}))
},
_ => None,
})
.flatten()
.map(|wpos2d| lod::Object {
kind: lod::ObjectKind::House,
pos: (wpos2d - min_wpos)
.map(|e| e as i16)
.with_z(self.sim().get_alt_approx(wpos2d).unwrap_or(0.0) as i16),
flags: lod::Flags::empty(),
}),
);
// Add giant trees
objects.extend(
index
.sites
.iter()
.filter(|(_, site)| {
site.get_origin()
.map2(min_wpos.zip(max_wpos), |e, (min, max)| e >= min && e < max)
.reduce_and()
})
.filter(|(_, site)| matches!(&site.kind, SiteKind::GiantTree(_)))
.map(|(_, site)| lod::Object {
kind: lod::ObjectKind::GiantTree,
pos: {
let wpos2d = site.get_origin();
(wpos2d - min_wpos)
.map(|e| e as i16)
.with_z(self.sim().get_alt_approx(wpos2d).unwrap_or(0.0) as i16)
},
flags: lod::Flags::empty(),
}),
);
lod::Zone { objects }
}
}

View File

@ -2134,36 +2134,40 @@ impl WorldSim {
/// them spawning).
pub fn get_near_trees(&self, wpos: Vec2<i32>) -> impl Iterator<Item = TreeAttr> + '_ {
// Deterministic based on wpos
let normal_trees =
self.gen_ctx
.structure_gen
.get(wpos)
.into_iter()
.filter_map(move |(wpos, seed)| {
let lottery = self.make_forest_lottery(wpos);
Some(TreeAttr {
pos: wpos,
seed,
scale: 1.0,
forest_kind: *lottery.choose_seeded(seed).as_ref()?,
inhabited: false,
})
});
self.gen_ctx
.structure_gen
.get(wpos)
.into_iter()
.filter_map(move |(wpos, seed)| {
let lottery = self.make_forest_lottery(wpos);
Some(TreeAttr {
pos: wpos,
seed,
scale: 1.0,
forest_kind: *lottery.choose_seeded(seed).as_ref()?,
inhabited: false,
})
})
}
// // For testing
// let giant_trees =
// std::array::IntoIter::new(self.gen_ctx.big_structure_gen.get(wpos))
// // Don't even consider trees if we aren't close
// .filter(move |(pos, _)| pos.distance_squared(wpos) < 512i32.pow(2))
// .map(move |(pos, seed)| TreeAttr {
// pos,
// seed,
// scale: 5.0,
// forest_kind: ForestKind::Giant,
// inhabited: (seed / 13) % 2 == 0,
// });
normal_trees //.chain(giant_trees)
pub fn get_area_trees(
&self,
wpos_min: Vec2<i32>,
wpos_max: Vec2<i32>,
) -> impl ParallelIterator<Item = TreeAttr> + '_ {
self.gen_ctx
.structure_gen
.par_iter(wpos_min, wpos_max)
.filter_map(move |(wpos, seed)| {
let lottery = self.make_forest_lottery(wpos);
Some(TreeAttr {
pos: wpos,
seed,
scale: 1.0,
forest_kind: *lottery.choose_seeded(seed).as_ref()?,
inhabited: false,
})
})
}
}