Merge branch 'train-tracks' into 'master'

Train tracks

See merge request veloren/veloren!3679
This commit is contained in:
Samuel Keiffer 2022-11-05 22:56:07 +00:00
commit 72bffe0df8
20 changed files with 566 additions and 64 deletions

View File

@ -1,9 +1,18 @@
#version 420 core
#include <constants.glsl>
#include <globals.glsl>
#include <srgb.glsl>
#include <sky.glsl>
#include <light.glsl>
#include <lod.glsl>
layout (location = 0)
in vec4 f_color;
layout (location = 1)
in vec3 f_pos;
layout (location = 2)
in vec3 f_norm;
layout (std140, set = 1, binding = 0)
uniform u_locals {
@ -11,9 +20,60 @@ uniform u_locals {
vec4 w_color;
};
layout(set = 2, binding = 0)
uniform texture2D t_col_light;
layout(set = 2, binding = 1)
uniform sampler s_col_light;
layout (location = 0)
out vec4 tgt_color;
void main() {
tgt_color = f_color;
vec3 cam_to_frag = normalize(f_pos - cam_pos.xyz);
vec3 view_dir = -cam_to_frag;
float point_shadow = shadow_at(f_pos, f_norm);
#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, point_shadow * sun_shade_frac, f_pos);
DirectionalLight moon_info = get_moon_info(moon_dir, point_shadow * moon_shade_frac);
vec3 surf_color = f_color.xyz;
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(0.8);
vec3 k_s = vec3(R_s);
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);
vec3 emitted_light = vec3(1);
vec3 reflected_light = vec3(1);
max_light += get_sun_diffuse2(sun_info, moon_info, f_norm, view_dir, f_pos, mu, cam_attenuation, fluid_alt, k_a, k_d, k_s, alpha, f_norm, 1.0, emitted_light, reflected_light);
max_light += lights_at(f_pos, f_norm, view_dir, mu, cam_attenuation, fluid_alt, k_a, k_d, k_s, alpha, f_norm, 1.0, emitted_light, reflected_light);
surf_color = illuminate(max_light, view_dir, surf_color * emitted_light, surf_color * reflected_light * 1.0);
tgt_color = vec4(surf_color, 1.0);
//tgt_color = vec4(f_norm, 1.0);
}

View File

@ -4,6 +4,10 @@
layout (location = 0)
in vec3 v_pos;
layout (location = 1)
in vec4 v_color;
layout (location = 2)
in vec3 v_norm;
layout (std140, set = 1, binding = 0)
uniform u_locals {
@ -14,9 +18,13 @@ uniform u_locals {
layout (location = 0)
out vec4 f_color;
layout (location = 1)
out vec3 f_pos;
layout (location = 2)
out vec3 f_norm;
void main() {
f_color = w_color;
f_color = w_color * v_color;
// Build rotation matrix
// https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Rotation_matrices
@ -41,5 +49,7 @@ void main() {
float r22 = 1 - 2 * (pow(q1, 2) + pow(q2, 2));
rotation_matrix[2] = vec3(r20, r21, r22);
gl_Position = all_mat * vec4((v_pos * rotation_matrix + w_pos.xyz) - focus_off.xyz, 1);
f_pos = (v_pos * rotation_matrix + w_pos.xyz) - focus_off.xyz;
f_norm = normalize(v_norm);
gl_Position = all_mat * vec4(f_pos, 1);
}

View File

@ -530,4 +530,5 @@ void main() {
surf_color += f_select * (surf_color + 0.1) * vec3(0.5, 0.5, 0.5);
tgt_color = vec4(surf_color, f_alpha);
//tgt_color = vec4(f_norm, f_alpha);
}

View File

@ -15,4 +15,5 @@
wildlife_density: 1.0,
peak_naming: true,
biome_naming: true,
train_tracks: false, // TODO: train stations, train entities
)

View File

@ -252,6 +252,7 @@ pub enum ServerChatCommand {
BuildAreaRemove,
Campfire,
DebugColumn,
DebugWays,
DisconnectAllPlayers,
DropAll,
Dummy,
@ -411,6 +412,11 @@ impl ServerChatCommand {
"Prints some debug information about a column",
Some(Moderator),
),
ServerChatCommand::DebugWays => cmd(
vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
"Prints some debug information about a column's ways",
Some(Moderator),
),
ServerChatCommand::DisconnectAllPlayers => cmd(
vec![Any("confirm", Required)],
"Disconnects all players from the server",
@ -738,6 +744,7 @@ impl ServerChatCommand {
ServerChatCommand::BuildAreaRemove => "build_area_remove",
ServerChatCommand::Campfire => "campfire",
ServerChatCommand::DebugColumn => "debug_column",
ServerChatCommand::DebugWays => "debug_ways",
ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
ServerChatCommand::DropAll => "dropall",
ServerChatCommand::Dummy => "dummy",

View File

@ -57,6 +57,8 @@ impl<V, S: RectVolSize, M: Clone> Chonk<V, S, M> {
pub fn meta(&self) -> &M { &self.meta }
pub fn meta_mut(&mut self) -> &mut M { &mut self.meta }
#[inline]
pub fn get_min_z(&self) -> i32 { self.z_offset }

View File

@ -90,6 +90,9 @@ pub struct TerrainChunkMeta {
temp: f32,
humidity: f32,
site: Option<SiteKindMeta>,
tracks: Vec<CubicBezier3<f32>>,
debug_points: Vec<Vec3<f32>>,
debug_lines: Vec<LineSegment3<f32>>,
}
impl TerrainChunkMeta {
@ -116,6 +119,9 @@ impl TerrainChunkMeta {
temp,
humidity,
site,
tracks: Vec::new(),
debug_points: Vec::new(),
debug_lines: Vec::new(),
}
}
@ -131,6 +137,9 @@ impl TerrainChunkMeta {
temp: 0.0,
humidity: 0.0,
site: None,
tracks: Vec::new(),
debug_points: Vec::new(),
debug_lines: Vec::new(),
}
}
@ -153,6 +162,24 @@ impl TerrainChunkMeta {
pub fn temp(&self) -> f32 { self.temp }
pub fn humidity(&self) -> f32 { self.humidity }
pub fn tracks(&self) -> &[CubicBezier3<f32>] { &self.tracks }
pub fn add_track(&mut self, bezier: CubicBezier3<f32>) {
self.tracks.push(bezier);
}
pub fn debug_points(&self) -> &[Vec3<f32>] { &self.debug_points }
pub fn add_debug_point(&mut self, point: Vec3<f32>) {
self.debug_points.push(point);
}
pub fn debug_lines(&self) -> &[LineSegment3<f32>] { &self.debug_lines }
pub fn add_debug_line(&mut self, line: LineSegment3<f32>) {
self.debug_lines.push(line);
}
}
// Terrain type aliases

View File

@ -61,10 +61,10 @@ use rand::{thread_rng, Rng};
use specs::{
saveload::MarkerAllocator, storage::StorageEntry, Builder, Entity as EcsEntity, Join, WorldExt,
};
use std::{str::FromStr, sync::Arc};
use std::{fmt::Write, str::FromStr, sync::Arc};
use vek::*;
use wiring::{Circuit, Wire, WireNode, WiringAction, WiringActionEffect, WiringElement};
use world::util::Sampler;
use world::util::{Sampler, LOCALITY};
use common::comp::Alignment;
use tracing::{error, info, warn};
@ -136,6 +136,7 @@ fn do_command(
ServerChatCommand::BuildAreaRemove => handle_build_area_remove,
ServerChatCommand::Campfire => handle_spawn_campfire,
ServerChatCommand::DebugColumn => handle_debug_column,
ServerChatCommand::DebugWays => handle_debug_ways,
ServerChatCommand::DisconnectAllPlayers => handle_disconnect_all_players,
ServerChatCommand::DropAll => handle_drop_all,
ServerChatCommand::Dummy => handle_spawn_training_dummy,
@ -2791,6 +2792,7 @@ fn handle_debug_column(
let downhill = chunk.downhill;
let river = &chunk.river;
let flux = chunk.flux;
let path = chunk.path;
Some(format!(
r#"wpos: {:?}
@ -2806,7 +2808,8 @@ temp {:?}
humidity {:?}
rockiness {:?}
tree_density {:?}
spawn_rate {:?} "#,
spawn_rate {:?}
path {:?} "#,
wpos,
alt,
col.alt,
@ -2822,7 +2825,8 @@ spawn_rate {:?} "#,
humidity,
rockiness,
tree_density,
spawn_rate
spawn_rate,
path,
))
};
if let Some(s) = msg_generator(&calendar) {
@ -2833,6 +2837,51 @@ spawn_rate {:?} "#,
}
}
#[cfg(not(feature = "worldgen"))]
fn handle_debug_ways(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
_args: Vec<String>,
_action: &ServerChatCommand,
) -> CmdResult<()> {
Err("Unsupported without worldgen enabled".into())
}
#[cfg(feature = "worldgen")]
fn handle_debug_ways(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
args: Vec<String>,
_action: &ServerChatCommand,
) -> CmdResult<()> {
let sim = server.world.sim();
let wpos = if let (Some(x), Some(y)) = parse_cmd_args!(args, i32, i32) {
Vec2::new(x, y)
} else {
let pos = position(server, target, "target")?;
// FIXME: Deal with overflow, if needed.
pos.0.xy().map(|x| x as i32)
};
let msg_generator = || {
let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32);
let mut ret = String::new();
for delta in LOCALITY {
let pos = chunk_pos + delta;
let chunk = sim.get(pos)?;
writeln!(ret, "{:?}: {:?}", pos, chunk.path).ok()?;
}
Some(ret)
};
if let Some(s) = msg_generator() {
server.notify_client(client, ServerGeneral::server_msg(ChatType::CommandInfo, s));
Ok(())
} else {
Err("Not a pre-generated chunk.".into())
}
}
fn handle_disconnect_all_players(
server: &mut Server,
client: EcsEntity,

View File

@ -222,6 +222,13 @@ impl<'a> Widget for Chat<'a> {
// Maintain scrolling //
if !self.new_messages.is_empty() {
for message in self.new_messages.iter() {
// Log the output of commands since the ingame terminal doesn't support copying
// the output to the clipboard
if let ChatType::CommandInfo = message.chat_type {
tracing::info!("Chat command info: {}", message.message);
}
}
//new messages - update chat w/ them & scroll down if at bottom of chat
state.update(|s| s.messages.extend(self.new_messages.drain(..)));
// Prevent automatic scroll upon new messages if not already scrolled to bottom

View File

@ -7,18 +7,18 @@ use vek::*;
#[derive(Copy, Clone, Debug, Zeroable, Pod)]
pub struct Vertex {
pub pos: [f32; 3],
pub color: [f32; 4],
pub normal: [f32; 3],
}
impl Vertex {
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRIBUTES: [wgpu::VertexAttribute; 3] =
wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x4, 2 => Float32x3];
wgpu::VertexBufferLayout {
array_stride: Self::STRIDE,
step_mode: wgpu::InputStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}],
attributes: &ATTRIBUTES,
}
}
}
@ -46,6 +46,18 @@ impl From<Vec3<f32>> for Vertex {
fn from(pos: Vec3<f32>) -> Vertex {
Vertex {
pos: [pos.x, pos.y, pos.z],
color: [1.0; 4],
normal: [0.0, 0.0, 1.0],
}
}
}
impl From<(Vec3<f32>, [f32; 4], Vec3<f32>)> for Vertex {
fn from((pos, color, normal): (Vec3<f32>, [f32; 4], Vec3<f32>)) -> Vertex {
Vertex {
pos: [pos.x, pos.y, pos.z],
color,
normal: [normal.x, normal.y, normal.z],
}
}
}

View File

@ -19,6 +19,68 @@ pub enum DebugShape {
radius: f32,
height: f32,
},
TrainTrack {
path: CubicBezier3<f32>,
rail_width: f32,
rail_sep: f32,
plank_width: f32,
plank_height: f32,
plank_sep: f32,
},
}
/// If (q, r) is the given `line`, append the following mesh to `mesh`, where
/// the distance between a-b is `width` and b-d is `height`:
/// e-----f
/// /| /|
/// / | r/ |
/// / | / |
/// / g-/-- h
/// / / / /
/// a-----b /
/// | / | /
/// | /q | /
/// |/ |/
/// c-----d
fn box_along_line(
line: LineSegment3<f32>,
width: f32,
height: f32,
color: [f32; 4],
mesh: &mut Mesh<DebugVertex>,
) {
// dx is along b-a
// dz is along b-d
let dx = -Vec3::unit_z().cross(line.end - line.start).normalized();
let dz = dx.cross(line.end - line.start).normalized();
let w = width / 2.0;
let h = height / 2.0;
let LineSegment3 { start: q, end: r } = line;
let a = q - w * dx + h * dz;
let b = q + w * dx + h * dz;
let c = q - w * dx - h * dz;
let d = q + w * dx - h * dz;
let e = r - w * dx + h * dz;
let f = r + w * dx + h * dz;
let g = r - w * dx - h * dz;
let h = r + w * dx - h * dz;
let quad = |x: Vec3<f32>, y: Vec3<f32>, z: Vec3<f32>, w: Vec3<f32>| {
let normal = (y - x).cross(z - y).normalized();
Quad::<DebugVertex>::new(
(x, color, normal).into(),
(y, color, normal).into(),
(z, color, normal).into(),
(w, color, normal).into(),
)
};
mesh.push_quad(quad(a, c, d, b));
mesh.push_quad(quad(a, b, f, e));
mesh.push_quad(quad(a, e, g, c));
mesh.push_quad(quad(b, d, h, f));
mesh.push_quad(quad(e, f, h, g));
mesh.push_quad(quad(d, c, g, h));
}
impl DebugShape {
@ -34,8 +96,15 @@ impl DebugShape {
match self {
DebugShape::Line([a, b]) => {
let h = Vec3::new(0.0, 1.0, 0.0);
mesh.push_quad(quad(*a, a + h, b + h, *b));
//let h = Vec3::new(0.0, 1.0, 0.0);
//mesh.push_quad(quad(*a, a + h, b + h, *b));
box_along_line(
LineSegment3 { start: *a, end: *b },
0.1,
0.1,
[1.0; 4],
&mut mesh,
);
},
DebugShape::Cylinder { radius, height } => {
const SUBDIVISIONS: u8 = 16;
@ -140,6 +209,47 @@ impl DebugShape {
// 3) Draw second half-cylinder
draw_cylinder_sector(&mut mesh, p1, HALF_SECTORS, TOTAL);
},
DebugShape::TrainTrack {
path,
rail_width,
rail_sep,
plank_width,
plank_height,
plank_sep,
} => {
const STEEL_COLOR: [f32; 4] = [0.6, 0.6, 0.6, 1.0];
const WOOD_COLOR: [f32; 4] = [0.6, 0.2, 0.0, 1.0];
const SUBPLANK_LENGTH: usize = 5;
let length = path.length_by_discretization(100);
let num_planks = (length / (plank_sep + plank_width)).ceil() as usize;
let step_size = 1.0 / (SUBPLANK_LENGTH * num_planks) as f32;
for i in 0..(SUBPLANK_LENGTH * num_planks) {
let start = path.evaluate(i as f32 * step_size);
let end = path.evaluate((i + 1) as f32 * step_size);
let center = LineSegment3 { start, end };
let dx =
*rail_sep * -Vec3::unit_z().cross(center.end - center.start).normalized();
let dz = dx.cross(center.end - center.start).normalized();
let left = LineSegment3 {
start: center.start + dx,
end: center.end + dx,
};
let right = LineSegment3 {
start: center.start - dx,
end: center.end - dx,
};
box_along_line(left, *rail_width, *rail_width, STEEL_COLOR, &mut mesh);
box_along_line(right, *rail_width, *rail_width, STEEL_COLOR, &mut mesh);
//box_along_line(center, 0.1, 0.1, [1.0, 0.0, 0.0, 1.0], &mut mesh);
if i % SUBPLANK_LENGTH == 0 {
let across = LineSegment3 {
start: center.start - 1.5 * dx - *rail_width * dz,
end: center.start + 1.5 * dx - *rail_width * dz,
};
box_along_line(across, *plank_width, *plank_height, WOOD_COLOR, &mut mesh);
}
}
},
}
mesh
}

View File

@ -34,7 +34,7 @@ use common::{
comp,
outcome::Outcome,
resources::DeltaTime,
terrain::{BlockKind, TerrainChunk},
terrain::{BlockKind, TerrainChunk, TerrainGrid},
vol::ReadVol,
};
use common_base::{prof_span, span};
@ -1301,8 +1301,63 @@ impl Scene {
client: &Client,
settings: &Settings,
hitboxes: &mut HashMap<specs::Entity, DebugShapeId>,
tracks: &mut HashMap<Vec2<i32>, Vec<DebugShapeId>>,
) {
let ecs = client.state().ecs();
{
let mut current_chunks = hashbrown::HashSet::new();
let terrain_grid = ecs.read_resource::<TerrainGrid>();
for (key, chunk) in terrain_grid.iter() {
current_chunks.insert(key);
tracks.entry(key).or_insert_with(|| {
let mut ret = Vec::new();
for bezier in chunk.meta().tracks().iter() {
let shape_id = self.debug.add_shape(DebugShape::TrainTrack {
path: *bezier,
rail_width: 0.25,
rail_sep: 1.0,
plank_width: 0.5,
plank_height: 0.125,
plank_sep: 2.0,
});
ret.push(shape_id);
self.debug
.set_context(shape_id, [0.0; 4], [1.0; 4], [0.0, 0.0, 0.0, 1.0]);
}
for point in chunk.meta().debug_points().iter() {
let shape_id = self.debug.add_shape(DebugShape::Cylinder {
radius: 0.1,
height: 0.1,
});
ret.push(shape_id);
self.debug.set_context(
shape_id,
point.with_w(0.0).into_array(),
[1.0; 4],
[0.0, 0.0, 0.0, 1.0],
);
}
for line in chunk.meta().debug_lines().iter() {
let shape_id = self
.debug
.add_shape(DebugShape::Line([line.start, line.end]));
ret.push(shape_id);
self.debug
.set_context(shape_id, [0.0; 4], [1.0; 4], [0.0, 0.0, 0.0, 1.0]);
}
ret
});
}
tracks.retain(|k, v| {
let keep = current_chunks.contains(k);
if !keep {
for shape in v.iter() {
self.debug.remove_shape(*shape);
}
}
keep
});
}
let mut current_entities = hashbrown::HashSet::new();
if settings.interface.toggle_hitboxes {
let positions = ecs.read_component::<comp::Pos>();
@ -1343,6 +1398,7 @@ impl Scene {
} else {
[0.0, 1.0, 0.0, 0.5]
};
//let color = [1.0, 1.0, 1.0, 1.0];
let ori = ori.to_quat();
let hb_ori = [ori.x, ori.y, ori.z, ori.w];
self.debug.set_context(*shape_id, hb_pos, color, hb_ori);

View File

@ -91,6 +91,7 @@ pub struct SessionState {
#[cfg(not(target_os = "macos"))]
mumble_link: SharedLink,
hitboxes: HashMap<specs::Entity, DebugShapeId>,
tracks: HashMap<Vec2<i32>, Vec<DebugShapeId>>,
}
/// Represents an active game session (i.e., the one being played).
@ -157,6 +158,7 @@ impl SessionState {
mumble_link,
hitboxes: HashMap::new(),
metadata,
tracks: HashMap::new(),
}
}
@ -184,8 +186,12 @@ impl SessionState {
span!(_guard, "tick", "Session::tick");
let mut client = self.client.borrow_mut();
self.scene
.maintain_debug_hitboxes(&client, &global_state.settings, &mut self.hitboxes);
self.scene.maintain_debug_hitboxes(
&client,
&global_state.settings,
&mut self.hitboxes,
&mut self.tracks,
);
// All this camera code is just to determine if it's underwater for the sfx
// filter

View File

@ -93,6 +93,7 @@ pub struct Features {
pub wildlife_density: f32,
pub peak_naming: bool,
pub biome_naming: bool,
pub train_tracks: bool,
}
impl assets::Asset for Features {

View File

@ -38,4 +38,11 @@ impl<'a> Land<'a> {
pub fn get_chunk_wpos(&self, wpos: Vec2<i32>) -> Option<&sim::SimChunk> {
self.sim.and_then(|sim| sim.get_wpos(wpos))
}
pub fn get_nearest_path(
&self,
wpos: Vec2<i32>,
) -> Option<(f32, Vec2<f32>, sim::Path, Vec2<f32>)> {
self.sim.and_then(|sim| sim.get_nearest_path(wpos))
}
}

View File

@ -14,8 +14,9 @@ pub use self::{
use crate::{
column::ColumnSample,
config::CONFIG,
sim,
util::{FastNoise, RandomField, RandomPerm, Sampler},
Canvas, IndexRef,
Canvas, CanvasInfo, IndexRef,
};
use common::{
assets::AssetExt,
@ -48,6 +49,52 @@ pub struct Colors {
const EMPTY_AIR: Block = Block::air(SpriteKind::Empty);
pub struct PathLocals {
pub riverless_alt: f32,
pub alt: f32,
pub water_dist: f32,
pub bridge_offset: f32,
pub depth: i32,
}
impl PathLocals {
pub fn new(info: &CanvasInfo, col: &ColumnSample, path_nearest: Vec2<f32>) -> PathLocals {
// Try to use the column at the centre of the path for sampling to make them
// flatter
let col_pos = -info.wpos().map(|e| e as f32) + path_nearest;
let col00 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 0));
let col10 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 0));
let col01 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 1));
let col11 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 1));
let col_attr = |col: &ColumnSample| {
Vec3::new(col.riverless_alt, col.alt, col.water_dist.unwrap_or(1000.0))
};
let [riverless_alt, alt, water_dist] = match (col00, col10, col01, col11) {
(Some(col00), Some(col10), Some(col01), Some(col11)) => Lerp::lerp(
Lerp::lerp(col_attr(col00), col_attr(col10), path_nearest.x.fract()),
Lerp::lerp(col_attr(col01), col_attr(col11), path_nearest.x.fract()),
path_nearest.y.fract(),
),
_ => col_attr(col),
}
.into_array();
let (bridge_offset, depth) = (
((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) * 5.0,
((1.0 - ((water_dist + 2.0) * 0.3).min(0.0).cos().abs())
* (riverless_alt + 5.0 - alt).max(0.0)
* 1.75
+ 3.0) as i32,
);
PathLocals {
riverless_alt,
alt,
water_dist,
bridge_offset,
depth,
}
}
}
pub fn apply_paths_to(canvas: &mut Canvas) {
let info = canvas.info();
canvas.foreach_col(|canvas, wpos2d, col| {
@ -67,32 +114,13 @@ pub fn apply_paths_to(canvas: &mut Canvas) {
{
let inset = 0;
// Try to use the column at the centre of the path for sampling to make them
// flatter
let col_pos = -info.wpos().map(|e| e as f32) + path_nearest;
let col00 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 0));
let col10 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 0));
let col01 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 1));
let col11 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 1));
let col_attr = |col: &ColumnSample| {
Vec3::new(col.riverless_alt, col.alt, col.water_dist.unwrap_or(1000.0))
};
let [riverless_alt, alt, water_dist] = match (col00, col10, col01, col11) {
(Some(col00), Some(col10), Some(col01), Some(col11)) => Lerp::lerp(
Lerp::lerp(col_attr(col00), col_attr(col10), path_nearest.x.fract()),
Lerp::lerp(col_attr(col01), col_attr(col11), path_nearest.x.fract()),
path_nearest.y.fract(),
),
_ => col_attr(col),
}
.into_array();
let (bridge_offset, depth) = (
((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) * 5.0,
((1.0 - ((water_dist + 2.0) * 0.3).min(0.0).cos().abs())
* (riverless_alt + 5.0 - alt).max(0.0)
* 1.75
+ 3.0) as i32,
);
let PathLocals {
riverless_alt,
alt: _,
water_dist: _,
bridge_offset,
depth,
} = PathLocals::new(&canvas.info(), col, path_nearest);
let surface_z = (riverless_alt + bridge_offset).floor() as i32;
for z in inset - depth..inset {
@ -121,6 +149,70 @@ pub fn apply_paths_to(canvas: &mut Canvas) {
});
}
pub fn apply_trains_to(
canvas: &mut Canvas,
sim: &sim::WorldSim,
sim_chunk: &sim::SimChunk,
chunk_center_wpos2d: Vec2<i32>,
) {
let mut splines = Vec::new();
let g = |v: Vec2<f32>| -> Vec3<f32> {
let path_nearest = sim
.get_nearest_path(v.as_::<i32>())
.map(|x| x.1)
.unwrap_or(v.as_::<f32>());
let alt = if let Some(c) = canvas.col_or_gen(v.as_::<i32>()) {
let pl = PathLocals::new(canvas, &c, path_nearest);
pl.riverless_alt + pl.bridge_offset + 0.75
} else {
sim_chunk.alt
};
v.with_z(alt)
};
fn hermite_to_bezier(
p0: Vec3<f32>,
m0: Vec3<f32>,
p3: Vec3<f32>,
m3: Vec3<f32>,
) -> CubicBezier3<f32> {
let hermite = Vec4::new(p0, p3, m0, m3);
let hermite = hermite.map(|v| v.with_w(0.0));
let hermite: [[f32; 4]; 4] = hermite.map(|v: Vec4<f32>| v.into_array()).into_array();
// https://courses.engr.illinois.edu/cs418/sp2009/notes/12-MoreSplines.pdf
let mut m = Mat4::from_row_arrays([
[1.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
[-3.0, 3.0, 0.0, 0.0],
[0.0, 0.0, -3.0, 3.0],
]);
m.invert();
let bezier = m * Mat4::from_row_arrays(hermite);
let bezier: Vec4<Vec4<f32>> =
Vec4::<[f32; 4]>::from(bezier.into_row_arrays()).map(Vec4::from);
let bezier = bezier.map(Vec3::from);
CubicBezier3::from(bezier)
}
for sim::NearestWaysData { bezier: bez, .. } in
sim.get_nearest_ways(chunk_center_wpos2d, &|chunk| Some(chunk.path))
{
if bez.length_by_discretization(16) < 0.125 {
continue;
}
let a = 0.0;
let b = 1.0;
for bez in bez.split((a + b) / 2.0) {
let p0 = g(bez.evaluate(a));
let p1 = g(bez.evaluate(a + (b - a) / 3.0));
let p2 = g(bez.evaluate(a + 2.0 * (b - a) / 3.0));
let p3 = g(bez.evaluate(b));
splines.push(hermite_to_bezier(p0, 3.0 * (p1 - p0), p3, 3.0 * (p3 - p2)));
}
}
for spline in splines.into_iter() {
canvas.chunk.meta_mut().add_track(spline);
}
}
pub fn apply_caves_to(canvas: &mut Canvas, rng: &mut impl Rng) {
let info = canvas.info();
canvas.foreach_col(|canvas, wpos2d, col| {

View File

@ -30,6 +30,7 @@ pub use crate::{
canvas::{Canvas, CanvasInfo},
config::{Features, CONFIG},
land::Land,
layer::PathLocals,
};
pub use block::BlockGen;
pub use column::ColumnSample;
@ -358,6 +359,10 @@ impl World {
entities: Vec::new(),
};
if index.features.train_tracks {
layer::apply_trains_to(&mut canvas, &self.sim, sim_chunk, chunk_center_wpos2d);
}
if index.features.caverns {
layer::apply_caverns_to(&mut canvas, &mut dynamic_rng);
}

View File

@ -2133,14 +2133,11 @@ impl WorldSim {
Some(z0 + z1 + z2 + z3)
}
/// Return the distance to the nearest way in blocks, along with the
/// closest point on the way, the way metadata, and the tangent vector
/// of that way.
pub fn get_nearest_way<M: Clone + Lerp<Output = M>>(
&self,
pub fn get_nearest_ways<'a, M: Clone + Lerp<Output = M>>(
&'a self,
wpos: Vec2<i32>,
get_way: impl Fn(&SimChunk) -> Option<(Way, M)>,
) -> Option<(f32, Vec2<f32>, M, Vec2<f32>)> {
get_way: &'a impl Fn(&SimChunk) -> Option<(Way, M)>,
) -> impl Iterator<Item = NearestWaysData<M, impl FnOnce() -> Vec2<f32>>> + 'a {
let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| {
e.div_euclid(sz as i32)
});
@ -2150,10 +2147,9 @@ impl WorldSim {
})
};
let get_way = &get_way;
LOCALITY
.iter()
.filter_map(|ctrl| {
.filter_map(move |ctrl| {
let (way, meta) = get_way(self.get(chunk_pos + *ctrl)?)?;
let ctrl_pos = get_chunk_centre(chunk_pos + *ctrl).map(|e| e as f32)
+ way.offset.map(|e| e as f32);
@ -2163,7 +2159,7 @@ impl WorldSim {
return None;
}
let (start_pos, _start_idx, start_meta) = if chunk_connections != 2 {
let (start_pos, start_idx, start_meta) = if chunk_connections != 2 {
(ctrl_pos, None, meta.clone())
} else {
let (start_idx, start_rpos) = NEIGHBORS
@ -2186,8 +2182,10 @@ impl WorldSim {
NEIGHBORS
.iter()
.enumerate()
.filter(move |(i, _)| way.neighbors & (1 << *i as u8) != 0)
.filter_map(move |(_, end_rpos)| {
.filter(move |(i, _)| {
way.neighbors & (1 << *i as u8) != 0 && Some(*i) != start_idx
})
.filter_map(move |(i, end_rpos)| {
let end_pos_chunk = chunk_pos + *ctrl + end_rpos;
let (end_way, end_meta) = get_way(self.get(end_pos_chunk)?)?;
let end_pos = get_chunk_centre(end_pos_chunk).map(|e| e as f32)
@ -2209,15 +2207,42 @@ impl WorldSim {
} else {
Lerp::lerp(meta.clone(), end_meta, nearest_interval - 0.5)
};
Some((dist_sqrd, pos, meta, move || {
bez.evaluate_derivative(nearest_interval).normalized()
}))
Some(NearestWaysData {
i,
dist_sqrd,
pos,
meta,
bezier: bez,
calc_tangent: move || {
bez.evaluate_derivative(nearest_interval).normalized()
},
})
}),
)
})
.flatten()
.min_by_key(|(dist_sqrd, _, _, _)| (dist_sqrd * 1024.0) as i32)
.map(|(dist, pos, meta, calc_tangent)| (dist.sqrt(), pos, meta, calc_tangent()))
}
/// Return the distance to the nearest way in blocks, along with the
/// closest point on the way, the way metadata, and the tangent vector
/// of that way.
pub fn get_nearest_way<M: Clone + Lerp<Output = M>>(
&self,
wpos: Vec2<i32>,
get_way: impl Fn(&SimChunk) -> Option<(Way, M)>,
) -> Option<(f32, Vec2<f32>, M, Vec2<f32>)> {
let get_way = &get_way;
self.get_nearest_ways(wpos, get_way)
.min_by_key(|NearestWaysData { dist_sqrd, .. }| (dist_sqrd * 1024.0) as i32)
.map(
|NearestWaysData {
dist_sqrd,
pos,
meta,
calc_tangent,
..
}| (dist_sqrd.sqrt(), pos, meta, calc_tangent()),
)
}
pub fn get_nearest_path(&self, wpos: Vec2<i32>) -> Option<(f32, Vec2<f32>, Path, Vec2<f32>)> {
@ -2334,6 +2359,15 @@ pub struct RegionInfo {
pub seed: u32,
}
pub struct NearestWaysData<M, F: FnOnce() -> Vec2<f32>> {
pub i: usize,
pub dist_sqrd: f32,
pub pos: Vec2<f32>,
pub meta: M,
pub bezier: QuadraticBezier2<f32>,
pub calc_tangent: F,
}
impl SimChunk {
fn generate(map_size_lg: MapSizeLg, posi: usize, gen_ctx: &GenCtx, gen_cdf: &GenCdf) -> Self {
let pos = uniform_idx_as_vec2(map_size_lg, posi);

View File

@ -10,6 +10,7 @@ pub use self::{
util::Dir,
};
use crate::{
sim::Path,
site::{namegen::NameGen, SpawnRules},
util::{attempt, DHashSet, Grid, CARDINALS, SQUARE_4, SQUARE_9},
Canvas, Land,
@ -323,9 +324,10 @@ impl Site {
Spiral2d::new()
.take((SEARCH_RADIUS * 2 + 1).pow(2) as usize)
.for_each(|tile| {
let wpos = self.tile_center_wpos(tile);
if let Some(kind) = Spiral2d::new()
.take(9)
.find_map(|rpos| wpos_is_hazard(land, self.tile_center_wpos(tile) + rpos))
.find_map(|rpos| wpos_is_hazard(land, wpos + rpos))
{
for &rpos in &SQUARE_4 {
// `get_mut` doesn't increase generation bounds
@ -334,6 +336,13 @@ impl Site {
.map(|tile| tile.kind = TileKind::Hazard(kind));
}
}
if let Some((dist, _, Path { width }, _)) = land.get_nearest_path(wpos) {
if dist < 2.0 * width {
self.tiles
.get_mut(tile)
.map(|tile| tile.kind = TileKind::Path);
}
}
});
}
@ -1004,7 +1013,7 @@ impl Site {
#[allow(clippy::single_match)]
match &tile.kind {
TileKind::Plaza => {
TileKind::Plaza | TileKind::Path => {
let near_roads = CARDINALS.iter().filter_map(|rpos| {
if self.tiles.get(tpos + rpos) == tile {
Some(Aabr {

View File

@ -181,6 +181,7 @@ pub enum TileKind {
Field,
Plaza,
Road { a: u16, b: u16, w: u16 },
Path,
Building,
Castle,
Wall(Dir),
@ -219,7 +220,12 @@ impl Tile {
pub fn is_natural(&self) -> bool { matches!(self.kind, TileKind::Empty | TileKind::Hazard(_)) }
pub fn is_road(&self) -> bool { matches!(self.kind, TileKind::Plaza | TileKind::Road { .. }) }
pub fn is_road(&self) -> bool {
matches!(
self.kind,
TileKind::Plaza | TileKind::Road { .. } | TileKind::Path
)
}
pub fn is_obstacle(&self) -> bool {
matches!(self.kind, TileKind::Hazard(_)) || self.is_building()