Renamed tether renderer to rope, fixed tether lengths

This commit is contained in:
Joshua Barretto 2023-10-20 13:06:21 +01:00
parent 879a28fbb6
commit 7589774967
12 changed files with 93 additions and 65 deletions

View File

@ -24,7 +24,7 @@ layout (std140, set = 2, binding = 0)
uniform u_locals { uniform u_locals {
vec4 pos_a; vec4 pos_a;
vec4 pos_b; vec4 pos_b;
float tether_length; float rope_length;
}; };
layout(location = 0) out vec3 f_pos; layout(location = 0) out vec3 f_pos;
@ -43,7 +43,7 @@ void main() {
wind_wave(pos.y * 1.5, 1.9, wind_vel.x, wind_vel.y), 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) 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(tether_length - dist, 0.0); 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); pos += vec3(ideal_wind_sway * min(pow(dip, 2), 0.005), -0.5 * dip);
f_pos = pos + focus_pos.xyz; f_pos = pos + focus_pos.xyz;

View File

@ -19,7 +19,7 @@ impl<'a> System<'a> for Sys {
Entities<'a>, Entities<'a>,
Read<'a, DeltaTime>, Read<'a, DeltaTime>,
ReadStorage<'a, Is<Follower>>, ReadStorage<'a, Is<Follower>>,
WriteStorage<'a, Pos>, ReadStorage<'a, Pos>,
WriteStorage<'a, Vel>, WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>, WriteStorage<'a, Ori>,
ReadStorage<'a, Body>, ReadStorage<'a, Body>,

View File

@ -41,7 +41,7 @@ use common::{
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
generation::{EntityConfig, EntityInfo}, generation::{EntityConfig, EntityInfo},
link::Is, link::Is,
mounting::Rider, mounting::{Rider, Volume, VolumeRider},
npc::{self, get_npc_name}, npc::{self, get_npc_name},
outcome::Outcome, outcome::Outcome,
parse_cmd_args, parse_cmd_args,
@ -1817,16 +1817,33 @@ fn handle_spawn_ship(
.state .state
.read_component_cloned::<Is<Rider>>(target) .read_component_cloned::<Is<Rider>>(target)
.map(|is_rider| is_rider.mount) .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)); .or_else(|| server.state.ecs().uid_from_entity(target));
let tether_follower = server.state.ecs().uid_from_entity(new_entity); let tether_follower = server.state.ecs().uid_from_entity(new_entity);
if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) { 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().z * 2.0 + 0.5)
.unwrap_or(6.0);
server server
.state .state
.link(Tethered { .link(Tethered {
leader, leader,
follower, follower,
tether_length: 6.0, tether_length,
}) })
.map_err(|_| "Failed to tether entities")?; .map_err(|_| "Failed to tether entities")?;
} else { } else {

View File

@ -9,11 +9,11 @@ pub mod lod_terrain;
pub mod particle; pub mod particle;
pub mod postprocess; pub mod postprocess;
pub mod rain_occlusion; pub mod rain_occlusion;
pub mod rope;
pub mod shadow; pub mod shadow;
pub mod skybox; pub mod skybox;
pub mod sprite; pub mod sprite;
pub mod terrain; pub mod terrain;
pub mod tether;
pub mod trail; pub mod trail;
pub mod ui; pub mod ui;

View File

@ -8,15 +8,15 @@ use vek::*;
pub struct Locals { pub struct Locals {
pos_a: [f32; 4], pos_a: [f32; 4],
pos_b: [f32; 4], pos_b: [f32; 4],
tether_length: f32, rope_length: f32,
} }
impl Locals { impl Locals {
pub fn new(pos_a: Vec3<f32>, pos_b: Vec3<f32>, tether_length: f32) -> Self { pub fn new(pos_a: Vec3<f32>, pos_b: Vec3<f32>, rope_length: f32) -> Self {
Self { Self {
pos_a: pos_a.with_w(0.0).into_array(), pos_a: pos_a.with_w(0.0).into_array(),
pos_b: pos_b.with_w(0.0).into_array(), pos_b: pos_b.with_w(0.0).into_array(),
tether_length, rope_length,
} }
} }
} }
@ -58,11 +58,11 @@ impl VertexTrait for Vertex {
const STRIDE: wgpu::BufferAddress = mem::size_of::<Self>() as wgpu::BufferAddress; const STRIDE: wgpu::BufferAddress = mem::size_of::<Self>() as wgpu::BufferAddress;
} }
pub struct TetherLayout { pub struct RopeLayout {
pub locals: wgpu::BindGroupLayout, pub locals: wgpu::BindGroupLayout,
} }
impl TetherLayout { impl RopeLayout {
pub fn new(device: &wgpu::Device) -> Self { pub fn new(device: &wgpu::Device) -> Self {
Self { Self {
locals: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { locals: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
@ -101,23 +101,23 @@ impl TetherLayout {
} }
} }
pub struct TetherPipeline { pub struct RopePipeline {
pub pipeline: wgpu::RenderPipeline, pub pipeline: wgpu::RenderPipeline,
} }
impl TetherPipeline { impl RopePipeline {
pub fn new( pub fn new(
device: &wgpu::Device, device: &wgpu::Device,
vs_module: &wgpu::ShaderModule, vs_module: &wgpu::ShaderModule,
fs_module: &wgpu::ShaderModule, fs_module: &wgpu::ShaderModule,
global_layout: &GlobalsLayouts, global_layout: &GlobalsLayouts,
layout: &TetherLayout, layout: &RopeLayout,
aa_mode: AaMode, aa_mode: AaMode,
) -> Self { ) -> Self {
common_base::span!(_guard, "TetherPipeline::new"); common_base::span!(_guard, "RopePipeline::new");
let render_pipeline_layout = let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Tether pipeline layout"), label: Some("Rope pipeline layout"),
push_constant_ranges: &[], push_constant_ranges: &[],
bind_group_layouts: &[ bind_group_layouts: &[
&global_layout.globals, &global_layout.globals,
@ -129,7 +129,7 @@ impl TetherPipeline {
let samples = aa_mode.samples(); let samples = aa_mode.samples();
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Tether pipeline"), label: Some("Rope pipeline"),
layout: Some(&render_pipeline_layout), layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState { vertex: wgpu::VertexState {
module: vs_module, module: vs_module,

View File

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

View File

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

View File

@ -6,8 +6,8 @@ use super::{
instances::Instances, instances::Instances,
model::{DynamicModel, Model, SubModel}, model::{DynamicModel, Model, SubModel},
pipelines::{ pipelines::{
blit, bloom, clouds, debug, figure, fluid, lod_object, lod_terrain, particle, shadow, blit, bloom, clouds, debug, figure, fluid, lod_object, lod_terrain, particle, rope,
skybox, sprite, terrain, tether, trail, ui, AtlasTextures, FigureSpriteAtlasData, shadow, skybox, sprite, terrain, trail, ui, AtlasTextures, FigureSpriteAtlasData,
GlobalsBindGroup, TerrainAtlasData, GlobalsBindGroup, TerrainAtlasData,
}, },
AltIndices, CullingMode, AltIndices, CullingMode,
@ -966,13 +966,13 @@ impl<'pass> FirstPassDrawer<'pass> {
ParticleDrawer { render_pass } ParticleDrawer { render_pass }
} }
pub fn draw_tethers(&mut self) -> TetherDrawer<'_, 'pass> { pub fn draw_ropes(&mut self) -> RopeDrawer<'_, 'pass> {
let mut render_pass = self.render_pass.scope("tethers", self.borrow.device); let mut render_pass = self.render_pass.scope("ropes", self.borrow.device);
render_pass.set_pipeline(&self.pipelines.tether.pipeline); render_pass.set_pipeline(&self.pipelines.rope.pipeline);
set_quad_index_buffer::<tether::Vertex>(&mut render_pass, self.borrow); set_quad_index_buffer::<rope::Vertex>(&mut render_pass, self.borrow);
TetherDrawer { render_pass } RopeDrawer { render_pass }
} }
pub fn draw_sprites<'data: 'pass>( pub fn draw_sprites<'data: 'pass>(
@ -1121,17 +1121,17 @@ impl<'pass_ref, 'pass: 'pass_ref> ParticleDrawer<'pass_ref, 'pass> {
} }
#[must_use] #[must_use]
pub struct TetherDrawer<'pass_ref, 'pass: 'pass_ref> { pub struct RopeDrawer<'pass_ref, 'pass: 'pass_ref> {
render_pass: Scope<'pass_ref, wgpu::RenderPass<'pass>>, render_pass: Scope<'pass_ref, wgpu::RenderPass<'pass>>,
} }
impl<'pass_ref, 'pass: 'pass_ref> TetherDrawer<'pass_ref, '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 // Note: if we ever need to draw less than the whole model, these APIs can be
// changed // changed
pub fn draw<'data: 'pass>( pub fn draw<'data: 'pass>(
&mut self, &mut self,
model: &'data Model<tether::Vertex>, model: &'data Model<rope::Vertex>,
locals: &'data tether::BoundLocals, locals: &'data rope::BoundLocals,
) { ) {
self.render_pass.set_vertex_buffer(0, model.buf().slice(..)); self.render_pass.set_vertex_buffer(0, model.buf().slice(..));
self.render_pass.set_bind_group(2, &locals.bind_group, &[]); self.render_pass.set_bind_group(2, &locals.bind_group, &[]);

View File

@ -4,7 +4,7 @@ use super::{
super::{ super::{
pipelines::{ pipelines::{
blit, bloom, clouds, debug, figure, fluid, lod_object, lod_terrain, particle, blit, bloom, clouds, debug, figure, fluid, lod_object, lod_terrain, particle,
postprocess, shadow, skybox, sprite, terrain, tether, trail, ui, postprocess, rope, shadow, skybox, sprite, terrain, trail, ui,
}, },
AaMode, BloomMode, CloudMode, FluidMode, LightingMode, PipelineModes, ReflectionMode, AaMode, BloomMode, CloudMode, FluidMode, LightingMode, PipelineModes, ReflectionMode,
RenderError, ShadowMode, RenderError, ShadowMode,
@ -23,7 +23,7 @@ pub struct Pipelines {
pub fluid: fluid::FluidPipeline, pub fluid: fluid::FluidPipeline,
pub lod_terrain: lod_terrain::LodTerrainPipeline, pub lod_terrain: lod_terrain::LodTerrainPipeline,
pub particle: particle::ParticlePipeline, pub particle: particle::ParticlePipeline,
pub tether: tether::TetherPipeline, pub rope: rope::RopePipeline,
pub trail: trail::TrailPipeline, pub trail: trail::TrailPipeline,
pub clouds: clouds::CloudsPipeline, pub clouds: clouds::CloudsPipeline,
pub bloom: Option<bloom::BloomPipelines>, pub bloom: Option<bloom::BloomPipelines>,
@ -47,7 +47,7 @@ pub struct IngamePipelines {
fluid: fluid::FluidPipeline, fluid: fluid::FluidPipeline,
lod_terrain: lod_terrain::LodTerrainPipeline, lod_terrain: lod_terrain::LodTerrainPipeline,
particle: particle::ParticlePipeline, particle: particle::ParticlePipeline,
tether: tether::TetherPipeline, rope: rope::RopePipeline,
trail: trail::TrailPipeline, trail: trail::TrailPipeline,
clouds: clouds::CloudsPipeline, clouds: clouds::CloudsPipeline,
pub bloom: Option<bloom::BloomPipelines>, pub bloom: Option<bloom::BloomPipelines>,
@ -95,7 +95,7 @@ impl Pipelines {
fluid: ingame.fluid, fluid: ingame.fluid,
lod_terrain: ingame.lod_terrain, lod_terrain: ingame.lod_terrain,
particle: ingame.particle, particle: ingame.particle,
tether: ingame.tether, rope: ingame.rope,
trail: ingame.trail, trail: ingame.trail,
clouds: ingame.clouds, clouds: ingame.clouds,
bloom: ingame.bloom, bloom: ingame.bloom,
@ -130,8 +130,8 @@ struct ShaderModules {
lod_object_frag: wgpu::ShaderModule, lod_object_frag: wgpu::ShaderModule,
particle_vert: wgpu::ShaderModule, particle_vert: wgpu::ShaderModule,
particle_frag: wgpu::ShaderModule, particle_frag: wgpu::ShaderModule,
tether_vert: wgpu::ShaderModule, rope_vert: wgpu::ShaderModule,
tether_frag: wgpu::ShaderModule, rope_frag: wgpu::ShaderModule,
trail_vert: wgpu::ShaderModule, trail_vert: wgpu::ShaderModule,
trail_frag: wgpu::ShaderModule, trail_frag: wgpu::ShaderModule,
ui_vert: wgpu::ShaderModule, ui_vert: wgpu::ShaderModule,
@ -344,8 +344,8 @@ impl ShaderModules {
lod_object_frag: create_shader("lod-object-frag", ShaderKind::Fragment)?, lod_object_frag: create_shader("lod-object-frag", ShaderKind::Fragment)?,
particle_vert: create_shader("particle-vert", ShaderKind::Vertex)?, particle_vert: create_shader("particle-vert", ShaderKind::Vertex)?,
particle_frag: create_shader("particle-frag", ShaderKind::Fragment)?, particle_frag: create_shader("particle-frag", ShaderKind::Fragment)?,
tether_vert: create_shader("tether-vert", ShaderKind::Vertex)?, rope_vert: create_shader("rope-vert", ShaderKind::Vertex)?,
tether_frag: create_shader("tether-frag", ShaderKind::Fragment)?, rope_frag: create_shader("rope-frag", ShaderKind::Fragment)?,
trail_vert: create_shader("trail-vert", ShaderKind::Vertex)?, trail_vert: create_shader("trail-vert", ShaderKind::Vertex)?,
trail_frag: create_shader("trail-frag", ShaderKind::Fragment)?, trail_frag: create_shader("trail-frag", ShaderKind::Fragment)?,
ui_vert: create_shader("ui-vert", ShaderKind::Vertex)?, ui_vert: create_shader("ui-vert", ShaderKind::Vertex)?,
@ -528,7 +528,7 @@ fn create_ingame_and_shadow_pipelines(
sprite_task, sprite_task,
lod_object_task, lod_object_task,
particle_task, particle_task,
tether_task, rope_task,
trail_task, trail_task,
lod_terrain_task, lod_terrain_task,
clouds_task, clouds_task,
@ -673,20 +673,20 @@ fn create_ingame_and_shadow_pipelines(
"particle pipeline creation", "particle pipeline creation",
) )
}; };
// Pipeline for rendering tethers // Pipeline for rendering ropes
let create_tether = || { let create_rope = || {
tether_task.run( rope_task.run(
|| { || {
tether::TetherPipeline::new( rope::RopePipeline::new(
device, device,
&shaders.tether_vert, &shaders.rope_vert,
&shaders.tether_frag, &shaders.rope_frag,
&layouts.global, &layouts.global,
&layouts.tether, &layouts.rope,
pipeline_modes.aa, pipeline_modes.aa,
) )
}, },
"tether pipeline creation", "rope pipeline creation",
) )
}; };
// Pipeline for rendering weapon trails // Pipeline for rendering weapon trails
@ -912,7 +912,7 @@ fn create_ingame_and_shadow_pipelines(
) )
}) })
}; };
let j8 = create_tether; let j8 = create_rope;
// Ignore this // Ignore this
let ( let (
@ -925,10 +925,7 @@ fn create_ingame_and_shadow_pipelines(
(postprocess, point_shadow), (postprocess, point_shadow),
(terrain_directed_shadow, (figure_directed_shadow, debug_directed_shadow)), (terrain_directed_shadow, (figure_directed_shadow, debug_directed_shadow)),
), ),
( ((lod_object, (terrain_directed_rain_occlusion, figure_directed_rain_occlusion)), rope),
(lod_object, (terrain_directed_rain_occlusion, figure_directed_rain_occlusion)),
tether,
),
), ),
) = pool.join( ) = pool.join(
|| pool.join(|| pool.join(j1, j2), || pool.join(j3, j4)), || pool.join(|| pool.join(j1, j2), || pool.join(j3, j4)),
@ -942,7 +939,7 @@ fn create_ingame_and_shadow_pipelines(
fluid, fluid,
lod_terrain, lod_terrain,
particle, particle,
tether, rope,
trail, trail,
clouds, clouds,
bloom, bloom,

View File

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

View File

@ -1,5 +1,5 @@
use crate::render::{ use crate::render::{
pipelines::tether::{BoundLocals, Locals, Vertex}, pipelines::rope::{BoundLocals, Locals, Vertex},
FirstPassDrawer, Mesh, Model, Quad, Renderer, FirstPassDrawer, Mesh, Model, Quad, Renderer,
}; };
use client::Client; use client::Client;
@ -15,6 +15,20 @@ use vek::*;
pub struct TetherMgr { pub struct TetherMgr {
model: Model<Vertex>, 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 algoruthms too).
stale_flag: bool, stale_flag: bool,
tethers: HashMap<(Uid, Uid), (BoundLocals, bool)>, tethers: HashMap<(Uid, Uid), (BoundLocals, bool)>,
} }
@ -61,7 +75,7 @@ impl TetherMgr {
.entry((is_follower.leader, is_follower.follower)) .entry((is_follower.leader, is_follower.follower))
.or_insert_with(|| { .or_insert_with(|| {
( (
renderer.create_tether_bound_locals(&[Locals::default()]), renderer.create_rope_bound_locals(&[Locals::default()]),
self.stale_flag, self.stale_flag,
) )
}); });
@ -81,9 +95,9 @@ impl TetherMgr {
} }
pub fn render<'a>(&'a self, drawer: &mut FirstPassDrawer<'a>) { pub fn render<'a>(&'a self, drawer: &mut FirstPassDrawer<'a>) {
let mut tether_drawer = drawer.draw_tethers(); let mut rope_drawer = drawer.draw_ropes();
for (locals, _) in self.tethers.values() { for (locals, _) in self.tethers.values() {
tether_drawer.draw(&self.model, locals); rope_drawer.draw(&self.model, locals);
} }
} }
} }