diff --git a/lib/rbx_loader/src/primitives.rs b/lib/rbx_loader/src/primitives.rs
index c2d3319d8..2ce21ae9b 100644
--- a/lib/rbx_loader/src/primitives.rs
+++ b/lib/rbx_loader/src/primitives.rs
@@ -1,6 +1,6 @@
 use crate::rbx::{RobloxPartDescription,RobloxWedgeDescription,RobloxCornerWedgeDescription};
-use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
-use strafesnet_common::integer::{vec3,Planar64Vec3};
+use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,MeshBuilder,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
+use strafesnet_common::integer::{vec3,Planar64,Planar64Vec3};
 
 #[derive(Debug)]
 pub enum Primitives{
@@ -480,3 +480,132 @@ pub fn generate_partial_unit_cornerwedge(CornerWedgeFaceDescription(face_descrip
 		physics_groups:vec![physics_group],
 	}
 }
+
+pub fn generate_partial_cylinder(face_descriptions:CubeFaceDescription)->Mesh{
+	// cylinder is oriented about the x axis
+	// roblox cylinders use projected grid coordinates
+	/// how many grid coordinates to use (positive and negative)
+	const GON:i32=3;
+	/// grid perimeter
+	const POINTS:[[i32;2];4*2*GON as usize]=const{
+		let mut points=[[0;2];{4*2*GON as usize}];
+		let mut i=-GON;
+		while i<GON{
+			points[(i+GON) as usize]=[i,GON];
+			points[(i+GON+1*2*GON) as usize]=[GON,-i];
+			points[(i+GON+2*2*GON) as usize]=[-i,-GON];
+			points[(i+GON+3*2*GON) as usize]=[-GON,i];
+			i+=1;
+		}
+		points
+	};
+
+	let mut mb=MeshBuilder::new();
+	let mut polygon_groups=Vec::with_capacity(CubeFaceDescription::FACES);
+	let mut graphics_groups=Vec::with_capacity(CubeFaceDescription::FACES);
+	let mut physics_group=IndexedPhysicsGroup{groups:Vec::with_capacity(CubeFaceDescription::FACES)};
+	let CubeFaceDescription([right,top,back,left,bottom,front])=face_descriptions;
+
+	macro_rules! end_face{
+		($face_description:expr,$end:expr,$iter:expr)=>{
+			let normal=mb.acquire_normal_id($end);
+			let color=mb.acquire_color_id($face_description.color);
+
+			// single polygon for physics
+			let polygon:Vec<_>=$iter.map(|[x,y]|{
+				let tex=mb.acquire_tex_id(
+					$face_description.transform.transform_point2(
+						(glam::vec2(-x as f32,y as f32).normalize()+1.0)/2.0
+					)
+				);
+				let pos=mb.acquire_pos_id($end+vec3::int(0,-x,y).with_length(Planar64::ONE).divide().wrap_1());
+				mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color})
+			}).collect();
+
+			// fanned polygons for graphics
+			let pos=mb.acquire_pos_id($end);
+			let tex=mb.acquire_tex_id($face_description.transform.transform_point2(glam::Vec2::ONE/2.0));
+			let center=mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color});
+			let polygon_list=(0..POINTS.len()).map(|i|
+				vec![center,polygon[i],polygon[(i+1)%POINTS.len()]]
+			).collect();
+
+			// end face graphics
+			let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
+			polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
+			graphics_groups.push(IndexedGraphicsGroup{
+				render:$face_description.render,
+				groups:vec![group_id],
+			});
+
+			// end face physics
+			let polygon_list=vec![polygon];
+
+			let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
+			polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
+			physics_group.groups.push(group_id);
+		}
+	}
+	macro_rules! tex{
+		($face_description:expr,$tex:expr)=>{{
+			let [x,y]=$tex;
+			$face_description.transform.transform_point2(
+				glam::vec2((x+GON) as f32,(y+GON) as f32)/(2*GON) as f32
+			)
+		}};
+	}
+	macro_rules! barrel_face{
+		($face_description:expr,$loop:ident,$lo_dir:expr,$hi_dir:expr,$tex_0:expr,$tex_1:expr,$tex_2:expr,$tex_3:expr)=>{
+			let mut polygon_list=Vec::with_capacity(CubeFaceDescription::FACES);
+			for $loop in -GON..GON{
+				// lo Z
+				let lz_dir=$lo_dir.with_length(Planar64::ONE).divide().wrap_1();
+				// hi Z
+				let hz_dir=$hi_dir.with_length(Planar64::ONE).divide().wrap_1();
+
+				// pos
+				let lx_lz_pos=mb.acquire_pos_id(vec3::NEG_X+lz_dir);
+				let lx_hz_pos=mb.acquire_pos_id(vec3::NEG_X+hz_dir);
+				let hx_hz_pos=mb.acquire_pos_id(vec3::X+hz_dir);
+				let hx_lz_pos=mb.acquire_pos_id(vec3::X+lz_dir);
+				// tex
+				let lx_lz_tex=mb.acquire_tex_id(tex!($face_description,$tex_0));
+				let lx_hz_tex=mb.acquire_tex_id(tex!($face_description,$tex_1));
+				let hx_hz_tex=mb.acquire_tex_id(tex!($face_description,$tex_2));
+				let hx_lz_tex=mb.acquire_tex_id(tex!($face_description,$tex_3));
+				// norm
+				let lz_norm=mb.acquire_normal_id(lz_dir);
+				let hz_norm=mb.acquire_normal_id(hz_dir);
+				// color
+				let color=mb.acquire_color_id($face_description.color);
+
+				polygon_list.push(vec![
+					mb.acquire_vertex_id(IndexedVertex{pos:lx_lz_pos,tex:lx_lz_tex,normal:lz_norm,color}),
+					mb.acquire_vertex_id(IndexedVertex{pos:lx_hz_pos,tex:lx_hz_tex,normal:hz_norm,color}),
+					mb.acquire_vertex_id(IndexedVertex{pos:hx_hz_pos,tex:hx_hz_tex,normal:hz_norm,color}),
+					mb.acquire_vertex_id(IndexedVertex{pos:hx_lz_pos,tex:hx_lz_tex,normal:lz_norm,color}),
+				]);
+			}
+
+			// push face
+			let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
+			polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
+			graphics_groups.push(IndexedGraphicsGroup{
+				render:$face_description.render,
+				groups:vec![group_id],
+			});
+			physics_group.groups.push(group_id);
+		};
+	}
+
+	end_face!(right, vec3::X,POINTS.into_iter());
+	barrel_face!(top, z,vec3::int(0,GON,z),vec3::int(0,GON,z+1), [GON,z],[GON,z+1],[-GON,z+1],[-GON,z]);
+	barrel_face!(back, y,vec3::int(0,y+1,GON),vec3::int(0,y,GON), [GON,y+1],[GON,y],[-GON,y],[-GON,y+1]);
+	end_face!(left, vec3::NEG_X,POINTS.into_iter().rev());
+	barrel_face!(bottom, z,vec3::int(0,-GON,z+1),vec3::int(0,-GON,z), [-GON,z+1],[-GON,z],[GON,z],[GON,z+1]);
+	barrel_face!(front, y,vec3::int(0,y,-GON),vec3::int(0,y+1,-GON), [-GON,y],[-GON,y+1],[GON,y+1],[GON,y]);
+
+	let physics_groups=vec![physics_group];
+
+	mb.build(polygon_groups,graphics_groups,physics_groups)
+}
diff --git a/lib/rbx_loader/src/rbx.rs b/lib/rbx_loader/src/rbx.rs
index 8f2a14b06..11d497b7f 100644
--- a/lib/rbx_loader/src/rbx.rs
+++ b/lib/rbx_loader/src/rbx.rs
@@ -611,8 +611,8 @@ pub fn convert<'a>(
 					let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
 					mesh_id_from_description.insert(basepart_description.clone(),mesh_id);//borrow checker going crazy
 					let mesh=match basepart_description{
+						RobloxBasePartDescription::Cylinder(part_texture_description)=>primitives::generate_partial_cylinder(CubeFaceDescription::new(part_texture_description,textureless_render_group)),
 						RobloxBasePartDescription::Sphere(part_texture_description)
-						|RobloxBasePartDescription::Cylinder(part_texture_description)
 						|RobloxBasePartDescription::Part(part_texture_description)=>primitives::generate_partial_unit_cube(CubeFaceDescription::new(part_texture_description,textureless_render_group)),
 						RobloxBasePartDescription::Wedge(wedge_texture_description)=>primitives::generate_partial_unit_wedge(WedgeFaceDescription::new(wedge_texture_description,textureless_render_group)),
 						RobloxBasePartDescription::CornerWedge(cornerwedge_texture_description)=>primitives::generate_partial_unit_cornerwedge(CornerWedgeFaceDescription::new(cornerwedge_texture_description,textureless_render_group)),