Merge branch 'zesterer/tethering' into 'master'

Allow entities to be tethered to one-another

See merge request veloren/veloren!3953
This commit is contained in:
Joshua Barretto 2023-10-21 14:35:15 +00:00
commit dfd63bf23b
31 changed files with 1198 additions and 44 deletions

View File

@ -233,4 +233,40 @@
19: Air(Lantern, 4),
},
),
Cart: (
bone0: (
offset: (-3.5, -3.5, 0.5),
central: ("cart.structure"),
),
bone1: (
offset: (-1.0, -2.5, -1.0),
central: ("cart.axle"),
),
bone2: (
offset: (-1.0, -2.5, -1.0),
central: ("cart.axle"),
),
bone3: (
offset: (0.0, 0.0, 0.0),
central: ("empty"),
),
custom_indices: {
1: Air(ChairSingle, 4),
2: Air(Helm, 0),
3: Air(ChairSingle, 2),
4: Air(ChairSingle, 6),
9: Air(CraftingBench, 0),
10: Air(Window1, 0),
11: Air(RepairBench, 0),
12: Air(DismantlingBench, 4),
13: Air(Window1, 2),
14: Air(Crate, 0),
15: Air(Cauldron, 2),
16: Air(ChairSingle, 0),
17: Air(CookingPot, 0),
18: Air(WallLampSmall, 4),
19: Air(Lantern, 4),
},
),
})

BIN
assets/common/voxel/cart/axle.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/common/voxel/cart/structure.vox (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -804,4 +804,17 @@ vec3 simple_lighting(vec3 pos, vec3 col, float shade) {
return col * clamp(2.5 / dot(d, d), shade * (get_sun_brightness() + 0.01), 1);
}
float wind_wave(float off, float scaling, float speed, float strength) {
float aspeed = abs(speed);
// TODO: Right now, the wind model is pretty simplistic. This means that there is frequently no wind at all, which
// looks bad. For now, we add a lower bound on the wind speed to keep things looking nice.
strength = max(strength, 6.0);
aspeed = max(aspeed, 5.0);
return (sin(tick_loop(2.0 * PI, 0.35 * scaling * floor(aspeed), off)) * (1.0 - fract(aspeed))
+ sin(tick_loop(2.0 * PI, 0.35 * scaling * ceil(aspeed), off)) * fract(aspeed)) * abs(strength) * 0.25;
//return sin(tick.x * 1.5 * scaling + off) + sin(tick.x * 0.35 * scaling + off);
}
#endif

View File

@ -0,0 +1,237 @@
#version 420 core
#define FIGURE_SHADER
#include <constants.glsl>
#define LIGHTING_TYPE LIGHTING_TYPE_REFLECTION
#define LIGHTING_REFLECTION_KIND LIGHTING_REFLECTION_KIND_GLOSSY
#if (FLUID_MODE == FLUID_MODE_LOW)
#define LIGHTING_TRANSPORT_MODE LIGHTING_TRANSPORT_MODE_IMPORTANCE
#elif (FLUID_MODE >= FLUID_MODE_MEDIUM)
#define LIGHTING_TRANSPORT_MODE LIGHTING_TRANSPORT_MODE_RADIANCE
#endif
#define LIGHTING_DISTRIBUTION_SCHEME LIGHTING_DISTRIBUTION_SCHEME_MICROFACET
#define LIGHTING_DISTRIBUTION LIGHTING_DISTRIBUTION_BECKMANN
#define HAS_SHADOW_MAPS
#include <globals.glsl>
#include <light.glsl>
#include <cloud.glsl>
#include <lod.glsl>
layout(location = 0) in vec3 f_pos;
// in float dummy;
// in vec3 f_col;
// in float f_ao;
// flat in uint f_pos_norm;
layout(location = 1) in vec3 f_norm;
layout(location = 2) in vec3 m_pos;
// in float f_alt;
// in vec4 f_shadow;
// in vec3 light_pos[2];
// #if (SHADOW_MODE == SHADOW_MODE_MAP)
// in vec4 sun_pos;
// #elif (SHADOW_MODE == SHADOW_MODE_CHEAP || SHADOW_MODE == SHADOW_MODE_NONE)
// const vec4 sun_pos = vec4(0.0);
// #endif
//struct ShadowLocals {
// mat4 shadowMatrices;
// mat4 texture_mat;
//};
//
//layout (std140)
//uniform u_light_shadows {
// ShadowLocals shadowMats[/*MAX_LAYER_FACES*/192];
//};
layout (std140, set = 2, binding = 0)
uniform u_locals {
vec4 pos_a;
vec4 pos_b;
};
layout(location = 0) out vec4 tgt_color;
layout(location = 1) out uvec4 tgt_mat;
void main() {
// vec2 texSize = textureSize(t_col_light, 0);
// vec4 col_light = texture(t_col_light, (f_uv_pos + 0.5) / texSize);
// vec3 f_col = col_light.rgb;
// float f_ao = col_light.a;
// vec4 f_col_light = texture(t_col_light, (f_uv_pos + 0.5) / textureSize(t_col_light, 0));
// vec3 f_col = f_col_light.rgb;
// float f_ao = f_col_light.a;
float f_ao = 1.0;
vec3 f_col = mix(
vec3(0.05, 0.03, 0.01),
vec3(0.1, 0.07, 0.05),
floor(abs(fract(m_pos.z * 10.0 + atan(m_pos.x, m_pos.y) * 0.159) - 0.5) * 6.0) / 3.0
);
#ifdef EXPERIMENTAL_BAREMINIMUM
tgt_color = vec4(simple_lighting(f_pos.xyz, f_col, f_ao), 1);
return;
#endif
// float /*f_light*/f_ao = textureProj(t_col_light, vec3(f_uv_pos, texSize)).a;//1.0;//f_col_light.a * 4.0;// f_light = float(v_col_light & 0x3Fu) / 64.0;
// vec3 my_chunk_pos = (vec3((uvec3(f_pos_norm) >> uvec3(0, 9, 18)) & uvec3(0x1FFu)) - 256.0) / 2.0;
// tgt_color = vec4(hash(floor(vec4(my_chunk_pos.x, 0, 0, 0))), hash(floor(vec4(0, my_chunk_pos.y, 0, 1))), hash(floor(vec4(0, 0, my_chunk_pos.z, 2))), 1.0);
// float f_ao = 0;
// tgt_color = vec4(vec3(f_ao), 1.0);
// tgt_color = vec4(f_col, 1.0);
// return;
// vec3 du = dFdx(f_pos);
// vec3 dv = dFdy(f_pos);
// vec3 f_norm = normalize(cross(du, dv));
// vec4 light_pos[2];
//#if (SHADOW_MODE == SHADOW_MODE_MAP)
// // for (uint i = 0u; i < light_shadow_count.z; ++i) {
// // light_pos[i] = /*vec3(*/shadowMats[i].texture_mat * vec4(f_pos, 1.0)/*)*/;
// // }
// vec4 sun_pos = /*vec3(*/shadowMats[0].texture_mat * vec4(f_pos, 1.0)/*)*/;
//#elif (SHADOW_MODE == SHADOW_MODE_CHEAP || SHADOW_MODE == SHADOW_MODE_NONE)
// vec4 sun_pos = vec4(0.0);
//#endif
vec3 cam_to_frag = normalize(f_pos - cam_pos.xyz);
// vec4 vert_pos4 = view_mat * vec4(f_pos, 1.0);
// vec3 view_dir = normalize(-vec3(vert_pos4)/* / vert_pos4.w*/);
vec3 view_dir = -cam_to_frag;
/* vec3 sun_dir = get_sun_dir(time_of_day.x);
vec3 moon_dir = get_moon_dir(time_of_day.x); */
// float sun_light = get_sun_brightness(sun_dir);
// float moon_light = get_moon_brightness(moon_dir);
/* float sun_shade_frac = horizon_at(f_pos, sun_dir);
float moon_shade_frac = horizon_at(f_pos, moon_dir); */
#if (SHADOW_MODE == SHADOW_MODE_CHEAP || SHADOW_MODE == SHADOW_MODE_MAP || FLUID_MODE >= FLUID_MODE_MEDIUM)
float f_alt = alt_at(f_pos.xy);
#elif (SHADOW_MODE == SHADOW_MODE_NONE || FLUID_MODE == FLUID_MODE_LOW)
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;//horizon_at2(f_shadow, f_alt, f_pos, sun_dir);
#endif
float moon_shade_frac = 1.0;// horizon_at2(f_shadow, f_alt, f_pos, moon_dir);
// Globbal illumination "estimate" used to light the faces of voxels which are parallel to the sun or moon (which is a very common occurrence).
// Will be attenuated by k_d, which is assumed to carry any additional ambient occlusion information (e.g. about shadowing).
// float ambient_sides = clamp(mix(0.5, 0.0, abs(dot(-f_norm, sun_dir)) * 10000.0), 0.0, 0.5);
// NOTE: current assumption is that moon and sun shouldn't be out at the sae time.
// This assumption is (or can at least easily be) wrong, but if we pretend it's true we avoids having to explicitly pass in a separate shadow
// for the sun and moon (since they have different brightnesses / colors so the shadows shouldn't attenuate equally).
// float shade_frac = /*1.0;*/sun_shade_frac + moon_shade_frac;
// DirectionalLight sun_info = get_sun_info(sun_dir, sun_shade_frac, light_pos);
DirectionalLight sun_info = get_sun_info(sun_dir, sun_shade_frac, /*sun_pos*/f_pos);
DirectionalLight moon_info = get_moon_info(moon_dir, moon_shade_frac/*, light_pos*/);
vec3 surf_color = f_col;
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 emitted_light, reflected_light;
// vec3 light_frac = /*vec3(1.0);*//*vec3(max(dot(f_norm, -sun_dir) * 0.5 + 0.5, 0.0));*/light_reflection_factor(f_norm, view_dir, vec3(0, 0, -1.0), vec3(1.0), vec3(R_s), alpha);
// vec3 point_light = light_at(f_pos, f_norm);
// vec3 light, diffuse_light, ambient_light;
//get_sun_diffuse(f_norm, time_of_day.x, view_dir, k_a * point_shadow * (shade_frac * 0.5 + light_frac * 0.5), k_d * point_shadow * shade_frac, k_s * point_shadow * shade_frac, alpha, emitted_light, reflected_light);
float max_light = 0.0;
// reflected_light *= point_shadow * shade_frac;
// emitted_light *= point_shadow * max(shade_frac, MIN_SHADOW);
// max_light *= point_shadow * shade_frac;
// reflected_light *= point_shadow;
// emitted_light *= point_shadow;
// max_light *= point_shadow;
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);
#if (FLUID_MODE >= FLUID_MODE_MEDIUM)
cam_attenuation =
medium.x == MEDIUM_WATER ? compute_attenuation_point(cam_pos.xyz, view_dir, mu, fluid_alt, /*cam_pos.z <= fluid_alt ? cam_pos.xyz : f_pos*/f_pos)
: compute_attenuation_point(f_pos, -view_dir, mu, fluid_alt, /*cam_pos.z <= fluid_alt ? cam_pos.xyz : f_pos*/cam_pos.xyz);
#endif
// Prevent the sky affecting light when underground
float not_underground = clamp((f_pos.z - f_alt) / 128.0 + 1.0, 0.0, 1.0);
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);
// TODO: Hack to add a small amount of underground ambient light to the scene
reflected_light += vec3(0.01, 0.02, 0.03) * (1.0 - not_underground);
// Apply baked AO
float ao = f_ao * sqrt(f_ao);//0.25 + f_ao * 0.75; ///*pow(f_ao, 0.5)*/f_ao * 0.85 + 0.15;
reflected_light *= ao;
emitted_light *= ao;
// Apply point light AO
float point_shadow = shadow_at(f_pos, f_norm);
reflected_light *= point_shadow;
emitted_light *= point_shadow;
/* reflected_light *= cloud_shadow(f_pos); */
/* vec3 point_light = light_at(f_pos, f_norm);
emitted_light += point_light;
reflected_light += point_light; */
// get_sun_diffuse(f_norm, time_of_day.x, cam_to_frag, surf_color * f_light * point_shadow, 0.5 * surf_color * f_light * point_shadow, 0.5 * surf_color * f_light * point_shadow, 2.0, emitted_light, reflected_light);
// get_sun_diffuse(f_norm, time_of_day.x, light, diffuse_light, ambient_light, 1.0);
// diffuse_light *= point_shadow;
// ambient_light *= point_shadow;
// vec3 point_light = light_at(f_pos, f_norm);
// light += point_light;
// diffuse_light += point_light;
// reflected_light += point_light;
// vec3 surf_color = illuminate(srgb_to_linear(highlight_col.rgb * f_col), light, diffuse_light, ambient_light);
float reflectance = 0.0;
// TODO: Do reflectance properly like this later
vec3 reflect_color = vec3(0);
surf_color = illuminate(max_light, view_dir, mix(surf_color * emitted_light, reflect_color, reflectance), mix(surf_color * reflected_light, reflect_color, reflectance));
// if ((flags & 1) == 1 && int(cam_mode) == 1) {
// float distance = distance(vec3(cam_pos), focus_pos.xyz) - 2;
// float opacity = clamp(distance / distance_divider, 0, 1);
// // if(threshold_matrix[int(gl_FragCoord.x) % 4][int(gl_FragCoord.y) % 4] > opacity) {
// // discard;
// // return;
// // }
// }
tgt_color = vec4(surf_color, 1.0);
tgt_mat = uvec4(uvec3((f_norm + 1.0) * 127.0), MAT_FIGURE);
}

View File

@ -0,0 +1,58 @@
#version 420 core
#include <constants.glsl>
#define FIGURE_SHADER
#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 <sky.glsl>
layout(location = 0) in vec3 v_pos;
layout(location = 1) in vec3 v_norm;
layout (std140, set = 2, binding = 0)
uniform u_locals {
vec4 pos_a;
vec4 pos_b;
float rope_length;
};
layout(location = 0) out vec3 f_pos;
layout(location = 1) out vec3 f_norm;
layout(location = 2) out vec3 m_pos;
void main() {
m_pos = v_pos;
vec3 rz = normalize(pos_b.xyz - pos_a.xyz);
vec3 rx = normalize(cross(vec3(0, 0, 1), rz));
vec3 ry = normalize(cross(rz, rx));
float dist = distance(pos_a.xyz, pos_b.xyz);
vec3 pos = pos_a.xyz + (rx * v_pos.x + ry * v_pos.y) * 0.1 + rz * v_pos.z * dist;
vec2 ideal_wind_sway = wind_vel * vec2(
wind_wave(pos.y * 1.5, 1.9, wind_vel.x, wind_vel.y),
wind_wave(pos.x * 1.5, 2.1, wind_vel.y, wind_vel.x)
);
float dip = (1 - pow(abs(v_pos.z - 0.5) * 2.0, 2)) * max(rope_length - dist, 0.0);
pos += vec3(ideal_wind_sway * min(pow(dip, 2), 0.005), -0.5 * dip);
f_pos = pos + focus_pos.xyz;
#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 = rx * v_norm.x + ry * v_norm.y + rz * v_norm.z;
gl_Position = all_mat * vec4(f_pos, 1);
}

View File

@ -76,19 +76,6 @@ vec4 nearest_entity(in vec3 sprite_pos, const float entity_radius_factor) {
return closest;
}
float wind_wave(float off, float scaling, float speed, float strength) {
float aspeed = abs(speed);
// TODO: Right now, the wind model is pretty simplistic. This means that there is frequently no wind at all, which
// looks bad. For now, we add a lower bound on the wind speed to keep things looking nice.
strength = max(strength, 6.0);
aspeed = max(aspeed, 5.0);
return (sin(tick_loop(2.0 * PI, 0.35 * scaling * floor(aspeed), off)) * (1.0 - fract(aspeed))
+ sin(tick_loop(2.0 * PI, 0.35 * scaling * ceil(aspeed), off)) * fract(aspeed)) * abs(strength) * 0.25;
//return sin(tick.x * 1.5 * scaling + off) + sin(tick.x * 0.35 * scaling + off);
}
void main() {
// Matrix to transform this sprite instance from model space to chunk space
mat4 inst_mat;

View File

@ -35,6 +35,8 @@ macro_rules! synced_components {
is_mount: IsMount,
is_rider: IsRider,
is_volume_rider: IsVolumeRider,
is_leader: IsLeader,
is_follower: IsFollower,
mass: Mass,
density: Density,
collider: Collider,
@ -74,7 +76,11 @@ macro_rules! reexport_comps {
mod inner {
pub use common::comp::*;
use common::link::Is;
use common::mounting::{Mount, Rider, VolumeRider};
use common::{
mounting::{Mount, Rider, VolumeRider},
tether::{Leader, Follower},
};
// We alias these because the identifier used for the
// component's type is reused as an enum variant name
// in the macro's that we pass to `synced_components!`.
@ -84,6 +90,8 @@ macro_rules! reexport_comps {
pub type IsMount = Is<Mount>;
pub type IsRider = Is<Rider>;
pub type IsVolumeRider = Is<VolumeRider>;
pub type IsLeader = Is<Leader>;
pub type IsFollower = Is<Follower>;
}
// Re-export all the component types. So that uses of `synced_components!` outside this
@ -182,6 +190,14 @@ impl NetSync for IsVolumeRider {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for IsLeader {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for IsFollower {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Mass {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}

View File

@ -688,6 +688,11 @@ impl ServerChatCommand {
.collect(),
Optional,
),
Boolean(
"Whether the ship should be tethered to the target (or its mount)",
"false".to_string(),
Optional,
),
Float("destination_degrees_ccw_of_east", 90.0, Optional),
],
"Spawns a ship",
@ -723,6 +728,7 @@ impl ServerChatCommand {
Integer("amount", 1, Optional),
Boolean("ai", "true".to_string(), Optional),
Float("scale", 1.0, Optional),
Boolean("tethered", "false".to_string(), Optional),
],
"Spawn a test entity",
Some(Admin),

View File

@ -1198,6 +1198,7 @@ impl Body {
ship::Body::Skiff => [1.0, -2.0, 2.0],
ship::Body::Submarine => [1.0, -2.0, 2.0],
ship::Body::Carriage => [1.0, -2.0, 2.0],
ship::Body::Cart => [1.0, -2.0, 2.0],
ship::Body::Volume => [0.0, 0.0, 0.0],
},
_ => [0.0, 0.0, 0.0],
@ -1214,6 +1215,14 @@ impl Body {
.into()
}
pub fn tether_offset_leader(&self) -> Vec3<f32> {
Vec3::new(0.0, self.dimensions().y * -0.4, self.dimensions().z * 0.7)
}
pub fn tether_offset_follower(&self) -> Vec3<f32> {
Vec3::new(0.0, self.dimensions().y * 0.6, self.dimensions().z * 0.7)
}
pub fn localize(&self) -> Content {
match self {
Self::BipedLarge(biped_large) => biped_large.localize(),

View File

@ -19,12 +19,13 @@ pub const ALL_BODIES: [Body; 6] = [
];
pub const ALL_AIRSHIPS: [Body; 2] = [Body::DefaultAirship, Body::AirBalloon];
pub const ALL_SHIPS: [Body; 5] = [
pub const ALL_SHIPS: [Body; 6] = [
Body::SailBoat,
Body::Galleon,
Body::Skiff,
Body::Submarine,
Body::Carriage,
Body::Cart,
];
make_case_elim!(
@ -40,6 +41,7 @@ make_case_elim!(
Skiff = 5,
Submarine = 6,
Carriage = 7,
Cart = 8,
}
);
@ -72,6 +74,7 @@ impl Body {
Body::Skiff => Some("skiff.structure"),
Body::Submarine => Some("submarine.structure"),
Body::Carriage => Some("carriage.structure"),
Body::Cart => Some("cart.structure"),
Body::Volume => None,
}
}
@ -84,7 +87,8 @@ impl Body {
Body::Galleon => Vec3::new(14.0, 48.0, 10.0),
Body::Skiff => Vec3::new(7.0, 15.0, 10.0),
Body::Submarine => Vec3::new(2.0, 15.0, 8.0),
Body::Carriage => Vec3::new(6.0, 12.0, 8.0),
Body::Carriage => Vec3::new(5.0, 12.0, 2.0),
Body::Cart => Vec3::new(3.0, 6.0, 1.0),
}
}
@ -119,12 +123,20 @@ impl Body {
Body::DefaultAirship | Body::AirBalloon | Body::Volume => Density(AIR_DENSITY),
Body::Submarine => Density(WATER_DENSITY), // Neutrally buoyant
Body::Carriage => Density(WATER_DENSITY * 0.5),
Body::Cart => Density(500.0 / self.dimensions().product()), /* Carts get a constant */
// mass
_ => Density(AIR_DENSITY * 0.95 + WATER_DENSITY * 0.05), /* Most boats should be very
* buoyant */
}
}
pub fn mass(&self) -> Mass { Mass((self.hull_vol() + self.balloon_vol()) * self.density().0) }
pub fn mass(&self) -> Mass {
if self.can_fly() {
Mass((self.hull_vol() + self.balloon_vol()) * self.density().0)
} else {
Mass(self.density().0 * self.dimensions().product())
}
}
pub fn can_fly(&self) -> bool {
matches!(self, Body::DefaultAirship | Body::AirBalloon | Body::Volume)
@ -136,7 +148,7 @@ impl Body {
matches!(self, Body::SailBoat | Body::Galleon | Body::Skiff)
}
pub fn has_wheels(&self) -> bool { matches!(self, Body::Carriage) }
pub fn has_wheels(&self) -> bool { matches!(self, Body::Carriage | Body::Cart) }
pub fn make_collider(&self) -> Collider {
match self.manifest_entry() {

View File

@ -57,6 +57,7 @@ pub mod spiral;
pub mod states;
pub mod store;
pub mod terrain;
pub mod tether;
pub mod time;
pub mod trade;
pub mod util;

View File

@ -2,6 +2,7 @@ use crate::{
comp::{self, pet::is_mountable, ship::figuredata::VOXEL_COLLIDER_MANIFEST},
link::{Is, Link, LinkHandle, Role},
terrain::{Block, TerrainGrid},
tether,
uid::{IdMaps, Uid},
vol::ReadVol,
};
@ -45,6 +46,7 @@ impl Link for Mounting {
WriteStorage<'a, Is<Mount>>,
WriteStorage<'a, Is<Rider>>,
ReadStorage<'a, Is<VolumeRider>>,
ReadStorage<'a, Is<tether::Follower>>,
);
type DeleteData<'a> = (
Read<'a, IdMaps>,
@ -67,7 +69,7 @@ impl Link for Mounting {
fn create(
this: &LinkHandle<Self>,
(id_maps, is_mounts, is_riders, is_volume_rider): &mut Self::CreateData<'_>,
(id_maps, is_mounts, is_riders, is_volume_rider, is_followers): &mut Self::CreateData<'_>,
) -> Result<(), Self::Error> {
let entity = |uid: Uid| id_maps.uid_entity(uid);
@ -79,7 +81,7 @@ impl Link for Mounting {
// relationship
if !is_mounts.contains(mount)
&& !is_riders.contains(rider)
&& !is_riders.contains(rider)
&& !is_followers.contains(rider)
// TODO: Does this definitely prevent mount cycles?
&& (!is_mounts.contains(rider) || !is_riders.contains(mount))
&& !is_volume_rider.contains(rider)

View File

@ -150,7 +150,7 @@ impl Body {
quadruped_low::Species::Driggle => 120.0,
quadruped_low::Species::HermitAlligator => 120.0,
},
Body::Ship(ship::Body::Carriage) => 200.0,
Body::Ship(ship::Body::Carriage) => 40.0,
Body::Ship(_) => 0.0,
Body::Arthropod(arthropod) => match arthropod.species {
arthropod::Species::Tarantula => 135.0,

123
common/src/tether.rs Normal file
View File

@ -0,0 +1,123 @@
use crate::{
comp,
link::{Is, Link, LinkHandle, Role},
mounting::{Rider, VolumeRider},
uid::{IdMaps, Uid},
};
use serde::{Deserialize, Serialize};
use specs::{Entities, Read, ReadStorage, WriteStorage};
use vek::*;
#[derive(Serialize, Deserialize, Debug)]
pub struct Leader;
impl Role for Leader {
type Link = Tethered;
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Follower;
impl Role for Follower {
type Link = Tethered;
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Tethered {
pub leader: Uid,
pub follower: Uid,
pub tether_length: f32,
}
#[derive(Debug)]
pub enum TetherError {
NoSuchEntity,
NotTetherable,
}
impl Link for Tethered {
type CreateData<'a> = (
Read<'a, IdMaps>,
WriteStorage<'a, Is<Leader>>,
WriteStorage<'a, Is<Follower>>,
ReadStorage<'a, Is<Rider>>,
ReadStorage<'a, Is<VolumeRider>>,
);
type DeleteData<'a> = (
Read<'a, IdMaps>,
WriteStorage<'a, Is<Leader>>,
WriteStorage<'a, Is<Follower>>,
);
type Error = TetherError;
type PersistData<'a> = (
Read<'a, IdMaps>,
Entities<'a>,
ReadStorage<'a, comp::Health>,
ReadStorage<'a, Is<Leader>>,
ReadStorage<'a, Is<Follower>>,
);
fn create(
this: &LinkHandle<Self>,
(id_maps, is_leaders, is_followers, is_riders, is_volume_rider): &mut Self::CreateData<'_>,
) -> Result<(), Self::Error> {
let entity = |uid: Uid| id_maps.uid_entity(uid);
if this.leader == this.follower {
// Forbid self-tethering
Err(TetherError::NotTetherable)
} else if let Some((leader, follower)) = entity(this.leader).zip(entity(this.follower)) {
// Ensure that neither leader or follower are already part of a conflicting
// relationship
if !is_riders.contains(follower)
&& !is_volume_rider.contains(follower)
&& !is_followers.contains(follower)
// TODO: Does this definitely prevent tether cycles?
&& (!is_leaders.contains(follower) || !is_followers.contains(leader))
{
let _ = is_leaders.insert(leader, this.make_role());
let _ = is_followers.insert(follower, this.make_role());
Ok(())
} else {
Err(TetherError::NotTetherable)
}
} else {
Err(TetherError::NoSuchEntity)
}
}
fn persist(
this: &LinkHandle<Self>,
(id_maps, entities, healths, is_leaders, is_followers): &mut Self::PersistData<'_>,
) -> bool {
let entity = |uid: Uid| id_maps.uid_entity(uid);
if let Some((leader, follower)) = entity(this.leader).zip(entity(this.follower)) {
let is_alive = |entity| {
entities.is_alive(entity) && healths.get(entity).map_or(true, |h| !h.is_dead)
};
// Ensure that both entities are alive and that they continue to be linked
is_alive(leader)
&& is_alive(follower)
&& is_leaders.get(leader).is_some()
&& is_followers.get(follower).is_some()
} else {
false
}
}
fn delete(
this: &LinkHandle<Self>,
(id_maps, is_leaders, is_followers): &mut Self::DeleteData<'_>,
) {
let entity = |uid: Uid| id_maps.uid_entity(uid);
let leader = entity(this.leader);
let follower = entity(this.follower);
// Delete link components
leader.map(|leader| is_leaders.remove(leader));
follower.map(|follower| is_followers.remove(follower));
}
}

View File

@ -19,6 +19,7 @@ use common::{
shared_server_config::ServerConstants,
slowjob::SlowJobPool,
terrain::{Block, MapSizeLg, TerrainChunk, TerrainGrid},
tether,
time::DayPeriod,
trade::Trades,
vol::{ReadVol, WriteVol},
@ -202,6 +203,8 @@ impl State {
ecs.register::<Is<Mount>>();
ecs.register::<Is<Rider>>();
ecs.register::<Is<VolumeRider>>();
ecs.register::<Is<tether::Leader>>();
ecs.register::<Is<tether::Follower>>();
ecs.register::<comp::Mass>();
ecs.register::<comp::Density>();
ecs.register::<comp::Collider>();

View File

@ -13,6 +13,7 @@ pub mod phys;
pub mod projectile;
mod shockwave;
mod stats;
mod tether;
// External
use common_ecs::{dispatch, System};
@ -21,6 +22,7 @@ use specs::DispatcherBuilder;
pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) {
//TODO: don't run interpolation on server
dispatch::<interpolation::Sys>(dispatch_builder, &[]);
dispatch::<tether::Sys>(dispatch_builder, &[]);
dispatch::<mount::Sys>(dispatch_builder, &[]);
dispatch::<controller::Sys>(dispatch_builder, &[&mount::Sys::sys_name()]);
dispatch::<character_behavior::Sys>(dispatch_builder, &[&controller::Sys::sys_name()]);

View File

@ -863,6 +863,18 @@ impl<'a> PhysicsData<'a> {
let climbing =
character_state.map_or(false, |cs| matches!(cs, CharacterState::Climb(_)));
let friction_factor = |vel: Vec3<f32>| {
if let Some(Body::Ship(ship)) = body && ship.has_wheels() {
vel
.try_normalized()
.and_then(|dir| Some(orientations.get(entity)?.right().dot(dir).abs()))
.unwrap_or(1.0)
.max(0.2)
} else {
1.0
}
};
match &collider {
Collider::Voxel { .. } | Collider::Volume(_) => {
// For now, treat entities with voxel colliders
@ -894,6 +906,7 @@ impl<'a> PhysicsData<'a> {
},
read,
&ori,
friction_factor,
);
tgt_pos = cpos.0;
},
@ -928,6 +941,7 @@ impl<'a> PhysicsData<'a> {
},
read,
&ori,
friction_factor,
);
// Sticky things shouldn't move when on a surface
@ -1214,6 +1228,7 @@ impl<'a> PhysicsData<'a> {
},
read,
&ori,
|vel| friction_factor(previous_cache_other.ori * vel),
);
// Transform entity attributes back into world space now
@ -1433,6 +1448,8 @@ fn box_voxel_collision<T: BaseVol<Vox = Block> + ReadVol>(
mut land_on_ground: impl FnMut(Entity, Vel, Vec3<f32>),
read: &PhysicsRead,
ori: &Ori,
// Get the proportion of surface friction that should be applied based on the current velocity
friction_factor: impl Fn(Vec3<f32>) -> f32,
) {
// We cap out scale at 10.0 to prevent an enormous amount of lag
let scale = read.scales.get(entity).map_or(1.0, |s| s.0.min(10.0));
@ -1905,7 +1922,8 @@ fn box_voxel_collision<T: BaseVol<Vox = Block> + ReadVol>(
} * physics_state
.on_ground
.map(|b| b.get_friction())
.unwrap_or(0.0);
.unwrap_or(0.0)
* friction_factor(vel.0);
let wall_fric = if physics_state.on_wall.is_some() && climbing {
FRIC_GROUND
} else {

View File

@ -0,0 +1,136 @@
use common::{
comp::{Body, Mass, Ori, Pos, Scale, Vel},
link::Is,
resources::DeltaTime,
tether::Follower,
uid::IdMaps,
util::Dir,
};
use common_ecs::{Job, Origin, Phase, System};
use specs::{Entities, Join, LendJoin, Read, ReadStorage, WriteStorage};
use vek::*;
/// This system is responsible for controlling mounts
#[derive(Default)]
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Read<'a, IdMaps>,
Entities<'a>,
Read<'a, DeltaTime>,
ReadStorage<'a, Is<Follower>>,
ReadStorage<'a, Pos>,
WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>,
ReadStorage<'a, Body>,
ReadStorage<'a, Scale>,
ReadStorage<'a, Mass>,
);
const NAME: &'static str = "tether";
const ORIGIN: Origin = Origin::Common;
const PHASE: Phase = Phase::Create;
fn run(
_job: &mut Job<Self>,
(
id_maps,
entities,
dt,
is_followers,
positions,
mut velocities,
mut orientations,
bodies,
scales,
masses,
): Self::SystemData,
) {
for (follower, is_follower, follower_body, follower_scale) in
(&entities, &is_followers, bodies.maybe(), scales.maybe()).join()
{
let Some(leader) = id_maps.uid_entity(is_follower.leader) else {
continue;
};
let (Some(leader_pos), Some(follower_pos)) = (
positions.get(leader).copied(),
positions.get(follower).copied(),
) else {
continue;
};
let (Some(leader_mass), Some(follower_mass)) =
(masses.get(leader).copied(), masses.get(follower).copied())
else {
continue;
};
if velocities.contains(follower) && velocities.contains(leader) {
let attach_offset = orientations
.get(leader)
.map(|ori| {
ori.to_quat()
* bodies
.get(leader)
.map(|b| {
b.tether_offset_leader()
* scales.get(leader).copied().unwrap_or(Scale(1.0)).0
})
.unwrap_or_default()
})
.unwrap_or_default();
let attach_pos = leader_pos.0 + attach_offset;
let tether_offset = orientations
.get(follower)
.map(|ori| {
ori.to_quat()
* follower_body
.map(|b| {
b.tether_offset_follower()
* follower_scale.copied().unwrap_or(Scale(1.0)).0
})
.unwrap_or_default()
})
.unwrap_or_default();
let tether_pos = follower_pos.0 + tether_offset;
let pull_factor =
(attach_pos.distance(tether_pos) - is_follower.tether_length).max(0.0);
let strength = pull_factor * 50000.0;
let pull_dir = (leader_pos.0 - follower_pos.0)
.try_normalized()
.unwrap_or(Vec3::unit_y());
let impulse = pull_dir * strength * dt.0;
// Can't fail
velocities.get_mut(follower).unwrap().0 += impulse / follower_mass.0;
velocities.get_mut(leader).unwrap().0 -= impulse / leader_mass.0;
if let Some(follower_ori) = orientations.get_mut(follower) {
let turn_strength = pull_factor
* (tether_offset.magnitude() * tether_pos.distance(attach_pos)
- tether_offset.dot(attach_pos - tether_pos).abs())
// TODO: proper moment of inertia
* 500.0
/ follower_mass.0;
// TODO: Should consider the offset
let target_ori = follower_ori.yawed_towards(Dir::new(pull_dir));
*follower_ori = follower_ori.slerped_towards(target_ori, turn_strength * dt.0);
}
if let Some(leader_ori) = orientations.get_mut(leader) {
let turn_strength = pull_factor
* (attach_offset.magnitude() * tether_pos.distance(attach_pos)
- attach_offset.dot(tether_pos - attach_pos).abs())
// TODO: proper moment of inertia
* 500.0
/ leader_mass.0;
// TODO: Should consider the offset
let target_ori = leader_ori.yawed_towards(Dir::new(pull_dir));
*leader_ori = leader_ori.slerped_towards(target_ori, turn_strength * dt.0);
}
}
}
}
}

View File

@ -40,12 +40,15 @@ use common::{
effect::Effect,
event::{EventBus, ServerEvent},
generation::{EntityConfig, EntityInfo},
link::Is,
mounting::{Rider, Volume, VolumeRider},
npc::{self, get_npc_name},
outcome::Outcome,
parse_cmd_args,
resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay, TimeScale},
rtsim::{Actor, Role},
terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize},
tether::Tethered,
uid::Uid,
vol::ReadVol,
weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect,
@ -1558,8 +1561,15 @@ fn handle_spawn(
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
match parse_cmd_args!(args, String, npc::NpcBody, u32, bool, f32) {
(Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai, opt_scale) => {
match parse_cmd_args!(args, String, npc::NpcBody, u32, bool, f32, bool) {
(
Some(opt_align),
Some(npc::NpcBody(id, mut body)),
opt_amount,
opt_ai,
opt_scale,
opt_tethered,
) => {
let uid = uid(server, target, "target")?;
let alignment = parse_alignment(uid, &opt_align)?;
let amount = opt_amount.filter(|x| *x > 0).unwrap_or(1).min(50);
@ -1608,6 +1618,28 @@ fn handle_spawn(
let new_entity = entity_base.build();
if opt_tethered == Some(true) {
let tether_leader = server
.state
.read_component_cloned::<Is<Rider>>(target)
.map(|is_rider| is_rider.mount)
.or_else(|| server.state.ecs().uid_from_entity(target));
let tether_follower = server.state.ecs().uid_from_entity(new_entity);
if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) {
server
.state
.link(Tethered {
leader,
follower,
tether_length: 4.0,
})
.map_err(|_| "Failed to tether entities")?;
} else {
return Err("Tether members don't have Uids.".into());
}
}
// Add to group system if a pet
if matches!(alignment, comp::Alignment::Owned { .. }) {
let server_eventbus =
@ -1748,7 +1780,7 @@ fn handle_spawn_ship(
args: Vec<String>,
_action: &ServerChatCommand,
) -> CmdResult<()> {
let (body_name, angle) = parse_cmd_args!(args, String, f32);
let (body_name, tethered, angle) = parse_cmd_args!(args, String, bool, f32);
let mut pos = position(server, target, "target")?;
pos.0.z += 2.0;
const DESTINATION_RADIUS: f32 = 2000.0;
@ -1767,6 +1799,7 @@ fn handle_spawn_ship(
let mut builder = server
.state
.create_ship(pos, ori, ship, |ship| ship.make_collider());
if let Some(pos) = destination {
let (kp, ki, kd) =
comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0));
@ -1776,7 +1809,47 @@ fn handle_spawn_ship(
.with_position_pid_controller(comp::PidController::new(kp, ki, kd, pos, 0.0, pure_z));
builder = builder.with(agent);
}
builder.build();
let new_entity = builder.build();
if tethered == Some(true) {
let tether_leader = server
.state
.read_component_cloned::<Is<Rider>>(target)
.map(|is_rider| is_rider.mount)
.or_else(|| {
server
.state
.read_component_cloned::<Is<VolumeRider>>(target)
.and_then(|is_volume_rider| {
if let Volume::Entity(uid) = is_volume_rider.pos.kind {
Some(uid)
} else {
None
}
})
})
.or_else(|| server.state.ecs().uid_from_entity(target));
let tether_follower = server.state.ecs().uid_from_entity(new_entity);
if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) {
let tether_length = tether_leader
.and_then(|uid| server.state.ecs().entity_from_uid(uid))
.and_then(|e| server.state.read_component_cloned::<comp::Body>(e))
.map(|b| b.dimensions().y * 1.5 + 1.0)
.unwrap_or(6.0);
server
.state
.link(Tethered {
leader,
follower,
tether_length,
})
.map_err(|_| "Failed to tether entities")?;
} else {
return Err("Tether members don't have Uids.".into());
}
}
server.notify_client(
client,

View File

@ -116,6 +116,7 @@ impl<'a> From<&'a Body> for SkeletonAttr {
Skiff => (0.0, 0.0, 0.0),
Submarine => (0.0, 0.0, 0.0),
Carriage => (0.0, 0.0, 0.0),
Cart => (0.0, 0.0, 0.0),
Volume => (0.0, 0.0, 0.0),
},
bone1: match body {
@ -126,6 +127,7 @@ impl<'a> From<&'a Body> for SkeletonAttr {
Skiff => (0.0, 0.0, 0.0),
Submarine => (0.0, -15.0, 3.5),
Carriage => (0.0, 3.0, 2.0),
Cart => (0.0, 1.0, 1.0),
Volume => (0.0, 0.0, 0.0),
},
bone2: match body {
@ -136,6 +138,7 @@ impl<'a> From<&'a Body> for SkeletonAttr {
Skiff => (0.0, 0.0, 0.0),
Submarine => (0.0, 0.0, 0.0),
Carriage => (0.0, -3.0, 2.0),
Cart => (0.0, -2.5, 1.0),
Volume => (0.0, 0.0, 0.0),
},
bone3: match body {
@ -146,18 +149,20 @@ impl<'a> From<&'a Body> for SkeletonAttr {
Skiff => (0.0, 0.0, 0.0),
Submarine => (0.0, -18.0, 3.5),
Carriage => (0.0, 0.0, 0.0),
Cart => (0.0, 0.0, 0.0),
Volume => (0.0, 0.0, 0.0),
},
bone1_ori: match body {
Carriage => std::f32::consts::PI * 0.5,
Carriage | Cart => std::f32::consts::PI * 0.5,
_ => 0.0,
},
bone2_ori: match body {
Carriage => std::f32::consts::PI * -0.5,
Carriage | Cart => std::f32::consts::PI * -0.5,
_ => 0.0,
},
bone_rotation_rate: match body {
Carriage => 0.25,
Cart => 0.4,
_ => 0.8,
},
bone1_prop_trail_offset: match body {

View File

@ -151,9 +151,9 @@ impl<V: Vertex> FromIterator<Quad<V>> for Mesh<V> {
/// Represents a triangle stored on the CPU.
pub struct Tri<V: Vertex> {
a: V,
b: V,
c: V,
pub a: V,
pub b: V,
pub c: V,
}
impl<V: Vertex> Tri<V> {
@ -162,10 +162,10 @@ impl<V: Vertex> Tri<V> {
/// Represents a quad stored on the CPU.
pub struct Quad<V: Vertex> {
a: V,
b: V,
c: V,
d: V,
pub a: V,
pub b: V,
pub c: V,
pub d: V,
}
impl<V: Vertex> Quad<V> {

View File

@ -9,6 +9,7 @@ pub mod lod_terrain;
pub mod particle;
pub mod postprocess;
pub mod rain_occlusion;
pub mod rope;
pub mod shadow;
pub mod skybox;
pub mod sprite;

View File

@ -0,0 +1,203 @@
use super::super::{AaMode, Bound, Consts, GlobalsLayouts, Vertex as VertexTrait};
use bytemuck::{Pod, Zeroable};
use std::mem;
use vek::*;
#[repr(C)]
#[derive(Copy, Clone, Debug, Zeroable, Pod)]
pub struct Locals {
pos_a: [f32; 4],
pos_b: [f32; 4],
rope_length: f32,
}
impl Locals {
pub fn new(pos_a: Vec3<f32>, pos_b: Vec3<f32>, rope_length: f32) -> Self {
Self {
pos_a: pos_a.with_w(0.0).into_array(),
pos_b: pos_b.with_w(0.0).into_array(),
rope_length,
}
}
}
impl Default for Locals {
fn default() -> Self { Self::new(Vec3::zero(), Vec3::zero(), 0.0) }
}
pub type BoundLocals = Bound<Consts<Locals>>;
#[repr(C)]
#[derive(Copy, Clone, Debug, Zeroable, Pod)]
pub struct Vertex {
pub pos: [f32; 3],
pub norm: [f32; 3],
}
impl Vertex {
pub fn new(pos: Vec3<f32>, norm: Vec3<f32>) -> Self {
Self {
pos: pos.into_array(),
norm: norm.into_array(),
}
}
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRIBUTES: [wgpu::VertexAttribute; 2] =
wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3];
wgpu::VertexBufferLayout {
array_stride: Self::STRIDE,
step_mode: wgpu::InputStepMode::Vertex,
attributes: &ATTRIBUTES,
}
}
}
impl VertexTrait for Vertex {
const QUADS_INDEX: Option<wgpu::IndexFormat> = Some(wgpu::IndexFormat::Uint16);
const STRIDE: wgpu::BufferAddress = mem::size_of::<Self>() as wgpu::BufferAddress;
}
pub struct RopeLayout {
pub locals: wgpu::BindGroupLayout,
}
impl RopeLayout {
pub fn new(device: &wgpu::Device) -> Self {
Self {
locals: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[
// locals
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::VERTEX | wgpu::ShaderStage::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
}),
}
}
pub fn bind_locals(&self, device: &wgpu::Device, locals: Consts<Locals>) -> BoundLocals {
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &self.locals,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: locals.buf().as_entire_binding(),
}],
});
BoundLocals {
bind_group,
with: locals,
}
}
}
pub struct RopePipeline {
pub pipeline: wgpu::RenderPipeline,
}
impl RopePipeline {
pub fn new(
device: &wgpu::Device,
vs_module: &wgpu::ShaderModule,
fs_module: &wgpu::ShaderModule,
global_layout: &GlobalsLayouts,
layout: &RopeLayout,
aa_mode: AaMode,
) -> Self {
common_base::span!(_guard, "RopePipeline::new");
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Rope pipeline layout"),
push_constant_ranges: &[],
bind_group_layouts: &[
&global_layout.globals,
&global_layout.shadow_textures,
&layout.locals,
],
});
let samples = aa_mode.samples();
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Rope pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: vs_module,
entry_point: "main",
buffers: &[Vertex::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 {
// TODO: use a constant and/or pass in this format on pipeline construction
format: wgpu::TextureFormat::Rgba16Float,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrite::ALL,
},
wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Uint,
blend: None,
write_mask: wgpu::ColorWrite::ALL,
},
],
}),
});
Self {
pipeline: render_pipeline,
}
}
}

View File

@ -24,8 +24,8 @@ use super::{
mesh::Mesh,
model::{DynamicModel, Model},
pipelines::{
blit, bloom, clouds, debug, figure, postprocess, rain_occlusion, shadow, sprite, terrain,
ui, GlobalsBindGroup, GlobalsLayouts, ShadowTexturesBindGroup,
blit, bloom, clouds, debug, figure, postprocess, rain_occlusion, rope, shadow, sprite,
terrain, ui, GlobalsBindGroup, GlobalsLayouts, ShadowTexturesBindGroup,
},
texture::Texture,
AddressMode, FilterMode, OtherModes, PipelineModes, RenderError, RenderMode, ShadowMapMode,
@ -54,6 +54,7 @@ struct ImmutableLayouts {
rain_occlusion: rain_occlusion::RainOcclusionLayout,
sprite: sprite::SpriteLayout,
terrain: terrain::TerrainLayout,
rope: rope::RopeLayout,
clouds: clouds::CloudsLayout,
bloom: bloom::BloomLayout,
ui: ui::UiLayout,
@ -383,6 +384,7 @@ impl Renderer {
let rain_occlusion = rain_occlusion::RainOcclusionLayout::new(&device);
let sprite = sprite::SpriteLayout::new(&device);
let terrain = terrain::TerrainLayout::new(&device);
let rope = rope::RopeLayout::new(&device);
let clouds = clouds::CloudsLayout::new(&device);
let bloom = bloom::BloomLayout::new(&device);
let postprocess = Arc::new(postprocess::PostProcessLayout::new(
@ -402,6 +404,7 @@ impl Renderer {
rain_occlusion,
sprite,
terrain,
rope,
clouds,
bloom,
ui,

View File

@ -3,7 +3,7 @@ use crate::render::pipelines::rain_occlusion;
use super::{
super::{
pipelines::{
debug, figure, lod_terrain, shadow, sprite, terrain, ui, AtlasTextures,
debug, figure, lod_terrain, rope, shadow, sprite, terrain, ui, AtlasTextures,
FigureSpriteAtlasData, GlobalModel, GlobalsBindGroup, TerrainAtlasData,
},
texture::Texture,
@ -67,6 +67,11 @@ impl Renderer {
.bind_locals(&self.device, locals, bone_data)
}
pub fn create_rope_bound_locals(&mut self, locals: &[rope::Locals]) -> rope::BoundLocals {
let locals = self.create_consts(locals);
self.layouts.rope.bind_locals(&self.device, locals)
}
pub fn create_terrain_bound_locals(
&mut self,
locals: &[terrain::Locals],

View File

@ -6,8 +6,8 @@ use super::{
instances::Instances,
model::{DynamicModel, Model, SubModel},
pipelines::{
blit, bloom, clouds, debug, figure, fluid, lod_object, lod_terrain, particle, shadow,
skybox, sprite, terrain, trail, ui, AtlasTextures, FigureSpriteAtlasData,
blit, bloom, clouds, debug, figure, fluid, lod_object, lod_terrain, particle, rope,
shadow, skybox, sprite, terrain, trail, ui, AtlasTextures, FigureSpriteAtlasData,
GlobalsBindGroup, TerrainAtlasData,
},
AltIndices, CullingMode,
@ -966,6 +966,15 @@ impl<'pass> FirstPassDrawer<'pass> {
ParticleDrawer { render_pass }
}
pub fn draw_ropes(&mut self) -> RopeDrawer<'_, 'pass> {
let mut render_pass = self.render_pass.scope("ropes", self.borrow.device);
render_pass.set_pipeline(&self.pipelines.rope.pipeline);
set_quad_index_buffer::<rope::Vertex>(&mut render_pass, self.borrow);
RopeDrawer { render_pass }
}
pub fn draw_sprites<'data: 'pass>(
&mut self,
globals: &'data sprite::SpriteGlobalsBindGroup,
@ -1111,6 +1120,28 @@ impl<'pass_ref, 'pass: 'pass_ref> ParticleDrawer<'pass_ref, 'pass> {
}
}
#[must_use]
pub struct RopeDrawer<'pass_ref, 'pass: 'pass_ref> {
render_pass: Scope<'pass_ref, wgpu::RenderPass<'pass>>,
}
impl<'pass_ref, 'pass: 'pass_ref> RopeDrawer<'pass_ref, 'pass> {
// Note: if we ever need to draw less than the whole model, these APIs can be
// changed
pub fn draw<'data: 'pass>(
&mut self,
model: &'data Model<rope::Vertex>,
locals: &'data rope::BoundLocals,
) {
self.render_pass.set_vertex_buffer(0, model.buf().slice(..));
self.render_pass.set_bind_group(2, &locals.bind_group, &[]);
// TODO: since we cast to u32 maybe this should returned by the len/count
// functions?
self.render_pass
.draw_indexed(0..model.len() as u32 / 4 * 6, 0, 0..1);
}
}
#[must_use]
pub struct SpriteDrawer<'pass_ref, 'pass: 'pass_ref> {
render_pass: Scope<'pass_ref, wgpu::RenderPass<'pass>>,

View File

@ -4,7 +4,7 @@ use super::{
super::{
pipelines::{
blit, bloom, clouds, debug, figure, fluid, lod_object, lod_terrain, particle,
postprocess, shadow, skybox, sprite, terrain, trail, ui,
postprocess, rope, shadow, skybox, sprite, terrain, trail, ui,
},
AaMode, BloomMode, CloudMode, FluidMode, LightingMode, PipelineModes, ReflectionMode,
RenderError, ShadowMode,
@ -23,6 +23,7 @@ pub struct Pipelines {
pub fluid: fluid::FluidPipeline,
pub lod_terrain: lod_terrain::LodTerrainPipeline,
pub particle: particle::ParticlePipeline,
pub rope: rope::RopePipeline,
pub trail: trail::TrailPipeline,
pub clouds: clouds::CloudsPipeline,
pub bloom: Option<bloom::BloomPipelines>,
@ -46,6 +47,7 @@ pub struct IngamePipelines {
fluid: fluid::FluidPipeline,
lod_terrain: lod_terrain::LodTerrainPipeline,
particle: particle::ParticlePipeline,
rope: rope::RopePipeline,
trail: trail::TrailPipeline,
clouds: clouds::CloudsPipeline,
pub bloom: Option<bloom::BloomPipelines>,
@ -93,6 +95,7 @@ impl Pipelines {
fluid: ingame.fluid,
lod_terrain: ingame.lod_terrain,
particle: ingame.particle,
rope: ingame.rope,
trail: ingame.trail,
clouds: ingame.clouds,
bloom: ingame.bloom,
@ -127,6 +130,8 @@ struct ShaderModules {
lod_object_frag: wgpu::ShaderModule,
particle_vert: wgpu::ShaderModule,
particle_frag: wgpu::ShaderModule,
rope_vert: wgpu::ShaderModule,
rope_frag: wgpu::ShaderModule,
trail_vert: wgpu::ShaderModule,
trail_frag: wgpu::ShaderModule,
ui_vert: wgpu::ShaderModule,
@ -339,6 +344,8 @@ impl ShaderModules {
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)?,
rope_vert: create_shader("rope-vert", ShaderKind::Vertex)?,
rope_frag: create_shader("rope-frag", ShaderKind::Fragment)?,
trail_vert: create_shader("trail-vert", ShaderKind::Vertex)?,
trail_frag: create_shader("trail-frag", ShaderKind::Fragment)?,
ui_vert: create_shader("ui-vert", ShaderKind::Vertex)?,
@ -500,7 +507,7 @@ fn create_ingame_and_shadow_pipelines(
needs: PipelineNeeds,
pool: &rayon::ThreadPool,
// TODO: Reduce the boilerplate in this file
tasks: [Task; 19],
tasks: [Task; 20],
) -> IngameAndShadowPipelines {
prof_span!(_guard, "create_ingame_and_shadow_pipelines");
@ -521,6 +528,7 @@ fn create_ingame_and_shadow_pipelines(
sprite_task,
lod_object_task,
particle_task,
rope_task,
trail_task,
lod_terrain_task,
clouds_task,
@ -665,6 +673,22 @@ fn create_ingame_and_shadow_pipelines(
"particle pipeline creation",
)
};
// Pipeline for rendering ropes
let create_rope = || {
rope_task.run(
|| {
rope::RopePipeline::new(
device,
&shaders.rope_vert,
&shaders.rope_frag,
&layouts.global,
&layouts.rope,
pipeline_modes.aa,
)
},
"rope pipeline creation",
)
};
// Pipeline for rendering weapon trails
let create_trail = || {
trail_task.run(
@ -888,6 +912,7 @@ fn create_ingame_and_shadow_pipelines(
)
})
};
let j8 = create_rope;
// Ignore this
let (
@ -900,11 +925,11 @@ fn create_ingame_and_shadow_pipelines(
(postprocess, point_shadow),
(terrain_directed_shadow, (figure_directed_shadow, debug_directed_shadow)),
),
(lod_object, (terrain_directed_rain_occlusion, figure_directed_rain_occlusion)),
((lod_object, (terrain_directed_rain_occlusion, figure_directed_rain_occlusion)), rope),
),
) = pool.join(
|| pool.join(|| pool.join(j1, j2), || pool.join(j3, j4)),
|| pool.join(|| pool.join(j5, j6), j7),
|| pool.join(|| pool.join(j5, j6), || pool.join(j7, j8)),
);
IngameAndShadowPipelines {
@ -914,6 +939,7 @@ fn create_ingame_and_shadow_pipelines(
fluid,
lod_terrain,
particle,
rope,
trail,
clouds,
bloom,

View File

@ -59,6 +59,8 @@ impl assets::Compound for Shaders {
"debug-vert",
"debug-frag",
"figure-frag",
"rope-vert",
"rope-frag",
"terrain-vert",
"terrain-frag",
"fluid-vert",

View File

@ -7,6 +7,7 @@ pub mod particle;
pub mod simple;
pub mod smoke_cycle;
pub mod terrain;
pub mod tether;
pub mod trail;
pub use self::{
@ -16,6 +17,7 @@ pub use self::{
lod::Lod,
particle::ParticleMgr,
terrain::{SpriteRenderContextLazy, Terrain},
tether::TetherMgr,
trail::TrailMgr,
};
use crate::{
@ -105,6 +107,7 @@ pub struct Scene {
particle_mgr: ParticleMgr,
trail_mgr: TrailMgr,
figure_mgr: FigureMgr,
tether_mgr: TetherMgr,
pub sfx_mgr: SfxMgr,
pub music_mgr: MusicMgr,
ambient_mgr: AmbientMgr,
@ -340,6 +343,7 @@ impl Scene {
particle_mgr: ParticleMgr::new(renderer),
trail_mgr: TrailMgr::default(),
figure_mgr: FigureMgr::new(renderer),
tether_mgr: TetherMgr::new(renderer),
sfx_mgr: SfxMgr::default(),
music_mgr: MusicMgr::new(&calendar),
ambient_mgr: AmbientMgr {
@ -880,6 +884,9 @@ impl Scene {
// Maintain LoD.
self.lod.maintain(renderer, client, focus_pos, &self.camera);
// Maintain tethers.
self.tether_mgr.maintain(renderer, client, focus_pos);
// Maintain debug shapes
self.debug.maintain(renderer);
@ -1439,6 +1446,9 @@ impl Scene {
);
drop(sprite_drawer);
// Render tethers.
self.tether_mgr.render(&mut first_pass);
// Draws translucent
self.terrain.render_translucent(&mut first_pass, focus_pos);

130
voxygen/src/scene/tether.rs Normal file
View File

@ -0,0 +1,130 @@
use crate::render::{
pipelines::rope::{BoundLocals, Locals, Vertex},
FirstPassDrawer, Mesh, Model, Quad, Renderer,
};
use client::Client;
use common::{
comp,
link::Is,
tether::Follower,
uid::{IdMaps, Uid},
};
use hashbrown::HashMap;
use specs::{Join, LendJoin, WorldExt};
use vek::*;
pub struct TetherMgr {
model: Model<Vertex>,
/// Used to garbage-collect tethers that no longer exist.
///
/// Because a tether is not an entity, but instead a relationship between
/// two entities, there is no single 'event' that we can listen to in
/// order to determine that a tether has been broken. Instead, every tick,
/// we go through the set of tethers that we observe in the world and
/// mark their entries in the `tethers` map below with a flag.
/// At the end of the tick, every unmarked tether in the `tethers` map below
/// can be deleted.
///
/// Every tick, the 'alive' state of the flag flips between `true` and
/// `false` to avoid the need to wastefully reset the flag of every
/// alive tether on each tick (this is a common optimisation in some garbage
/// collection algorithms too).
stale_flag: bool,
tethers: HashMap<(Uid, Uid), (BoundLocals, bool)>,
}
impl TetherMgr {
pub fn new(renderer: &mut Renderer) -> Self {
Self {
model: renderer.create_model(&create_tether_mesh()).unwrap(),
stale_flag: true,
tethers: HashMap::default(),
}
}
pub fn maintain(&mut self, renderer: &mut Renderer, client: &Client, focus_off: Vec3<f32>) {
let interpolated = client
.state()
.read_storage::<crate::ecs::comp::Interpolated>();
let scales = client.state().read_storage::<comp::Scale>();
let bodies = client.state().read_storage::<comp::Body>();
let id_maps = client.state().ecs().read_resource::<IdMaps>();
let is_followers = client.state().read_storage::<Is<Follower>>();
for (interp, is_follower, body, scale) in
(&interpolated, &is_followers, bodies.maybe(), scales.maybe()).join()
{
let Some(leader) = id_maps.uid_entity(is_follower.leader) else {
continue;
};
let pos_a = interpolated.get(leader).map_or(Vec3::zero(), |i| i.pos)
+ interpolated.get(leader).zip(bodies.get(leader)).map_or(
Vec3::zero(),
|(i, body)| {
i.ori.to_quat()
* body.tether_offset_leader()
* scales.get(leader).copied().unwrap_or(comp::Scale(1.0)).0
},
);
let pos_b = interp.pos
+ body.map_or(Vec3::zero(), |body| {
interp.ori.to_quat()
* body.tether_offset_follower()
* scale.copied().unwrap_or(comp::Scale(1.0)).0
});
let (locals, stale_flag) = self
.tethers
.entry((is_follower.leader, is_follower.follower))
.or_insert_with(|| {
(
renderer.create_rope_bound_locals(&[Locals::default()]),
self.stale_flag,
)
});
renderer.update_consts(locals, &[Locals::new(
pos_a - focus_off,
pos_b - focus_off,
is_follower.tether_length,
)]);
*stale_flag = self.stale_flag;
}
self.tethers.retain(|_, (_, flag)| *flag == self.stale_flag);
self.stale_flag ^= true;
}
pub fn render<'a>(&'a self, drawer: &mut FirstPassDrawer<'a>) {
let mut rope_drawer = drawer.draw_ropes();
for (locals, _) in self.tethers.values() {
rope_drawer.draw(&self.model, locals);
}
}
}
fn create_tether_mesh() -> Mesh<Vertex> {
const SEGMENTS: usize = 10;
const RADIAL_SEGMENTS: usize = 6;
(0..RADIAL_SEGMENTS)
.flat_map(|i| {
let at_angle = |x: f32| {
let theta = x / RADIAL_SEGMENTS as f32 * std::f32::consts::TAU;
Vec2::new(theta.sin(), theta.cos())
};
let start = at_angle(i as f32);
let end = at_angle(i as f32 + 1.0);
(0..SEGMENTS).map(move |s| {
let z = s as f32 / SEGMENTS as f32;
Quad {
a: Vertex::new(start.with_z(z), start.with_z(0.0)),
b: Vertex::new(start.with_z(z + 1.0 / SEGMENTS as f32), start.with_z(0.0)),
c: Vertex::new(end.with_z(z + 1.0 / SEGMENTS as f32), end.with_z(0.0)),
d: Vertex::new(end.with_z(z), end.with_z(0.0)),
}
})
})
.collect()
}