diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml
index 84ab2e61a5..0428dfb1e4 100644
--- a/voxygen/Cargo.toml
+++ b/voxygen/Cargo.toml
@@ -45,8 +45,8 @@ i18n = {package = "veloren-i18n", path = "i18n"}
 
 # Graphics
 winit = {version = "0.24.0", features = ["serde"]}
-wgpu = {git="https://github.com/gfx-rs/wgpu-rs.git"}
-zerocopy = "0.3"
+wgpu = { git="https://github.com/gfx-rs/wgpu-rs.git", rev= "ab8b0e3766558d541206da2790dfd63f15b13bc4" }
+bytemuck = { version="1.4", features=["derive"] }
 shaderc = "0.6.2"
 
 # Ui
diff --git a/voxygen/src/render/buffer.rs b/voxygen/src/render/buffer.rs
index d90d27c0ae..4bf1209784 100644
--- a/voxygen/src/render/buffer.rs
+++ b/voxygen/src/render/buffer.rs
@@ -1,15 +1,15 @@
+use bytemuck::Pod;
 use wgpu::util::DeviceExt;
-use zerocopy::AsBytes;
 
 #[derive(Clone)]
-pub struct Buffer<T: Copy + AsBytes> {
+pub struct Buffer<T: Copy + Pod> {
     pub buf: wgpu::Buffer,
     // bytes
     count: usize,
     phantom_data: std::marker::PhantomData<T>,
 }
 
-impl<T: Copy + AsBytes> Buffer<T> {
+impl<T: Copy + Pod> Buffer<T> {
     pub fn new(device: &mut wgpu::Device, cap: usize, usage: wgpu::BufferUsage) -> Self {
         Self {
             buf: device.create_buffer(&wgpu::BufferDescriptor {
diff --git a/voxygen/src/render/consts.rs b/voxygen/src/render/consts.rs
index 72930b2998..d851bdaeba 100644
--- a/voxygen/src/render/consts.rs
+++ b/voxygen/src/render/consts.rs
@@ -1,15 +1,15 @@
-use super::{buffer::Buffer, RenderError};
-use zerocopy::AsBytes;
+use super::buffer::Buffer;
+use bytemuck::Pod;
 
 /// A handle to a series of constants sitting on the GPU. This is used to hold
 /// information used in the rendering process that does not change throughout a
 /// single render pass.
 #[derive(Clone)]
-pub struct Consts<T: Copy + AsBytes> {
+pub struct Consts<T: Copy + Pod> {
     buf: Buffer<T>,
 }
 
-impl<T: Copy + AsBytes> Consts<T> {
+impl<T: Copy + Pod> Consts<T> {
     /// Create a new `Const<T>`.
     pub fn new(device: &mut wgpu::Device, len: usize) -> Self {
         Self {
@@ -24,7 +24,7 @@ impl<T: Copy + AsBytes> Consts<T> {
         queue: &wgpu::Queue,
         vals: &[T],
         offset: usize,
-    ) -> Result<(), RenderError> {
+    ) {
         self.buf.update(device, queue, vals, offset)
     }
 
diff --git a/voxygen/src/render/instances.rs b/voxygen/src/render/instances.rs
index 681dbbcaa6..02b0792e8b 100644
--- a/voxygen/src/render/instances.rs
+++ b/voxygen/src/render/instances.rs
@@ -1,13 +1,13 @@
 use super::{buffer::Buffer, RenderError};
-use zerocopy::AsBytes;
+use bytemuck::Pod;
 
 /// Represents a mesh that has been sent to the GPU.
 #[derive(Clone)]
-pub struct Instances<T: Copy + AsBytes> {
+pub struct Instances<T: Copy + Pod> {
     buf: Buffer<T>,
 }
 
-impl<T: Copy + AsBytes> Instances<T> {
+impl<T: Copy + Pod> Instances<T> {
     pub fn new(device: &mut wgpu::Device, len: usize) -> Self {
         Self {
             buf: Buffer::new(device, len, wgpu::BufferUsage::VERTEX),
diff --git a/voxygen/src/render/mod.rs b/voxygen/src/render/mod.rs
index 11067697ae..a8fed89da6 100644
--- a/voxygen/src/render/mod.rs
+++ b/voxygen/src/render/mod.rs
@@ -22,17 +22,18 @@ pub use self::{
             BoneData as FigureBoneData, BoneMeshes, FigureLayout, FigureModel,
             Locals as FigureLocals,
         },
-        lod_terrain::LodData,
-        particle::Instance as ParticleInstance,
-        postprocess::create_mesh as create_pp_mesh,
+        fluid::Vertex as FluidVertex,
+        lod_terrain::{LodData, Vertex as LodTerrainVertex},
+        particle::{Instance as ParticleInstance, Vertex as ParticleVertex},
+        postprocess::{create_mesh as create_pp_mesh, Vertex as PostProcessVertex},
         shadow::Locals as ShadowLocals,
-        skybox::create_mesh as create_skybox_mesh,
-        sprite::{Instance as SpriteInstance, Locals as SpriteLocals},
+        skybox::{create_mesh as create_skybox_mesh, Vertex as SkyboxVertex},
+        sprite::{Instance as SpriteInstance, Locals as SpriteLocals, Vertex as SpriteVertex},
         terrain::{Locals as TerrainLocals, TerrainLayout, Vertex as TerrainVertex},
         ui::{
             create_quad as create_ui_quad,
             create_quad_vert_gradient as create_ui_quad_vert_gradient, create_tri as create_ui_tri,
-            Locals as UiLocals, Mode as UiMode,
+            Locals as UiLocals, Mode as UiMode, Vertex as UiVertex,
         },
         GlobalModel, Globals, GlobalsLayouts, Light, Shadow,
     },
@@ -41,7 +42,7 @@ pub use self::{
 };
 pub use wgpu::{AddressMode, FilterMode};
 
-trait Vertex = Clone + zerocopy::AsBytes;
+pub trait Vertex = Clone + bytemuck::Pod;
 
 use serde::{Deserialize, Serialize};
 /// Anti-aliasing modes
diff --git a/voxygen/src/render/pipelines/figure.rs b/voxygen/src/render/pipelines/figure.rs
index 5369f04841..7f77235597 100644
--- a/voxygen/src/render/pipelines/figure.rs
+++ b/voxygen/src/render/pipelines/figure.rs
@@ -3,11 +3,11 @@ use super::{
     terrain::Vertex,
 };
 use crate::mesh::greedy::GreedyMesh;
+use bytemuck::Pod;
 use vek::*;
-use zerocopy::AsBytes;
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Locals {
     model_mat: [[f32; 4]; 4],
     highlight_col: [f32; 4],
@@ -19,7 +19,7 @@ pub struct Locals {
 }
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct BoneData {
     bone_mat: [[f32; 4]; 4],
     normals_mat: [[f32; 4]; 4],
diff --git a/voxygen/src/render/pipelines/fluid.rs b/voxygen/src/render/pipelines/fluid.rs
index 78b528dc38..a7356d3af7 100644
--- a/voxygen/src/render/pipelines/fluid.rs
+++ b/voxygen/src/render/pipelines/fluid.rs
@@ -1,9 +1,9 @@
 use super::super::{AaMode, GlobalsLayouts};
+use bytemuck::Pod;
 use vek::*;
-use zerocopy::AsBytes;
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Vertex {
     pos_norm: u32,
 }
diff --git a/voxygen/src/render/pipelines/lod_terrain.rs b/voxygen/src/render/pipelines/lod_terrain.rs
index b4ed18e57b..501e89afc6 100644
--- a/voxygen/src/render/pipelines/lod_terrain.rs
+++ b/voxygen/src/render/pipelines/lod_terrain.rs
@@ -1,9 +1,9 @@
 use super::super::{AaMode, GlobalsLayouts, Renderer, Texture};
+use bytemuck::Pod;
 use vek::*;
-use zerocopy::AsBytes;
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Vertex {
     pos: [f32; 2],
 }
diff --git a/voxygen/src/render/pipelines/mod.rs b/voxygen/src/render/pipelines/mod.rs
index 40b60b8803..226ab3ec50 100644
--- a/voxygen/src/render/pipelines/mod.rs
+++ b/voxygen/src/render/pipelines/mod.rs
@@ -12,16 +12,16 @@ pub mod ui;
 
 use super::Consts;
 use crate::scene::camera::CameraMode;
+use bytemuck::Pod;
 use common::terrain::BlockKind;
 use vek::*;
-use zerocopy::AsBytes;
 
 pub const MAX_POINT_LIGHT_COUNT: usize = 31;
 pub const MAX_FIGURE_SHADOW_COUNT: usize = 24;
 pub const MAX_DIRECTED_LIGHT_COUNT: usize = 6;
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Globals {
     view_mat: [[f32; 4]; 4],
     proj_mat: [[f32; 4]; 4],
@@ -55,14 +55,14 @@ pub struct Globals {
 }
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Light {
     pos: [f32; 4],
     col: [f32; 4],
 }
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Shadow {
     pos_radius: [f32; 4],
 }
diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs
index 11e9e8771c..bfe05d554f 100644
--- a/voxygen/src/render/pipelines/particle.rs
+++ b/voxygen/src/render/pipelines/particle.rs
@@ -1,9 +1,9 @@
 use super::super::{AaMode, GlobalsLayouts};
+use bytemuck::Pod;
 use vek::*;
-use zerocopy::AsBytes;
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Vertex {
     pos: [f32; 3],
     // ____BBBBBBBBGGGGGGGGRRRRRRRR
@@ -79,7 +79,7 @@ impl ParticleMode {
 }
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Instance {
     // created_at time, so we can calculate time relativity, needed for relative animation.
     // can save 32 bits per instance, for particles that are not relatively animated.
diff --git a/voxygen/src/render/pipelines/postprocess.rs b/voxygen/src/render/pipelines/postprocess.rs
index 4dd9df9474..15130f2417 100644
--- a/voxygen/src/render/pipelines/postprocess.rs
+++ b/voxygen/src/render/pipelines/postprocess.rs
@@ -1,9 +1,9 @@
 use super::super::{AaMode, GlobalsLayouts, Mesh, Tri};
+use bytemuck::Pod;
 use vek::*;
-use zerocopy::AsBytes;
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Locals {
     proj_mat_inv: [[f32; 4]; 4],
     view_mat_inv: [[f32; 4]; 4],
@@ -23,7 +23,7 @@ impl Locals {
 }
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Vertex {
     pub pos: [f32; 2],
 }
diff --git a/voxygen/src/render/pipelines/shadow.rs b/voxygen/src/render/pipelines/shadow.rs
index babaebf4d8..3af3760840 100644
--- a/voxygen/src/render/pipelines/shadow.rs
+++ b/voxygen/src/render/pipelines/shadow.rs
@@ -2,11 +2,11 @@ use super::super::{
     AaMode, ColLightInfo, FigureLayout, GlobalsLayouts, Renderer, TerrainLayout, TerrainVertex,
     Texture,
 };
+use bytemuck::Pod;
 use vek::*;
-use zerocopy::AsBytes;
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Locals {
     shadow_matrices: [[f32; 4]; 4],
     texture_mats: [[f32; 4]; 4],
diff --git a/voxygen/src/render/pipelines/skybox.rs b/voxygen/src/render/pipelines/skybox.rs
index 1799939f85..342e2a4fad 100644
--- a/voxygen/src/render/pipelines/skybox.rs
+++ b/voxygen/src/render/pipelines/skybox.rs
@@ -1,8 +1,8 @@
 use super::super::{AaMode, GlobalsLayouts, Mesh, Quad};
-use zerocopy::AsBytes;
+use bytemuck::Pod;
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Vertex {
     pub pos: [f32; 3],
 }
diff --git a/voxygen/src/render/pipelines/sprite.rs b/voxygen/src/render/pipelines/sprite.rs
index 0c07d46590..fdfeb01d7e 100644
--- a/voxygen/src/render/pipelines/sprite.rs
+++ b/voxygen/src/render/pipelines/sprite.rs
@@ -1,10 +1,10 @@
 use super::super::{AaMode, GlobalsLayouts, TerrainLayout};
+use bytemuck::Pod;
 use core::fmt;
 use vek::*;
-use zerocopy::AsBytes;
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Vertex {
     pos: [f32; 3],
     // Because we try to restrict terrain sprite data to a 128×128 block
@@ -70,7 +70,7 @@ impl Vertex {
 }
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Instance {
     pos_ori: u32,
     inst_mat0: [f32; 4],
@@ -122,7 +122,7 @@ impl Default for Instance {
 }
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Locals {
     // Each matrix performs rotatation, translation, and scaling, relative to the sprite
     // origin, for all sprite instances.  The matrix will be in an array indexed by the
diff --git a/voxygen/src/render/pipelines/terrain.rs b/voxygen/src/render/pipelines/terrain.rs
index d63d7d5746..13957a75a1 100644
--- a/voxygen/src/render/pipelines/terrain.rs
+++ b/voxygen/src/render/pipelines/terrain.rs
@@ -1,9 +1,9 @@
 use super::super::{AaMode, GlobalsLayouts};
+use bytemuck::Pod;
 use vek::*;
-use zerocopy::AsBytes;
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Vertex {
     pos_norm: u32,
     atlas_pos: u32,
@@ -129,7 +129,7 @@ impl Vertex {
 }
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Locals {
     model_offs: [f32; 3],
     load_time: f32,
diff --git a/voxygen/src/render/pipelines/ui.rs b/voxygen/src/render/pipelines/ui.rs
index 3749f5fd12..4be2660cff 100644
--- a/voxygen/src/render/pipelines/ui.rs
+++ b/voxygen/src/render/pipelines/ui.rs
@@ -1,9 +1,9 @@
 use super::super::{AaMode, GlobalsLayouts, Quad, Tri};
+use bytemuck::Pod;
 use vek::*;
-use zerocopy::AsBytes;
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Vertex {
     pos: [f32; 2],
     uv: [f32; 2],
@@ -24,7 +24,7 @@ impl Vertex {
 }
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, AsBytes)]
+#[derive(Copy, Clone, Debug, Pod)]
 pub struct Locals {
     pos: [f32; 4],
 }
diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs
index e8189e3131..339fa49e04 100644
--- a/voxygen/src/render/renderer.rs
+++ b/voxygen/src/render/renderer.rs
@@ -944,7 +944,7 @@ impl Renderer {
     }
 
     /// Create a new set of constants with the provided values.
-    pub fn create_consts<T: Copy + zerocopy::AsBytes>(
+    pub fn create_consts<T: Copy + bytemuck::Pod>(
         &mut self,
         vals: &[T],
     ) -> Result<Consts<T>, RenderError> {
@@ -954,16 +954,12 @@ impl Renderer {
     }
 
     /// Update a set of constants with the provided values.
-    pub fn update_consts<T: Copy + zerocopy::AsBytes>(
-        &mut self,
-        consts: &mut Consts<T>,
-        vals: &[T],
-    ) -> Result<(), RenderError> {
+    pub fn update_consts<T: Copy + bytemuck::Pod>(&mut self, consts: &mut Consts<T>, vals: &[T]) {
         consts.update(&mut self.encoder, vals, 0)
     }
 
     /// Create a new set of instances with the provided values.
-    pub fn create_instances<T: Copy + zerocopy::AsBytes>(
+    pub fn create_instances<T: Copy + bytemuck::Pod>(
         &mut self,
         vals: &[T],
     ) -> Result<Instances<T>, RenderError> {
diff --git a/voxygen/src/scene/figure/cache.rs b/voxygen/src/scene/figure/cache.rs
index e74ba048a2..c5ea7f98c4 100644
--- a/voxygen/src/scene/figure/cache.rs
+++ b/voxygen/src/scene/figure/cache.rs
@@ -1,9 +1,7 @@
 use super::{load::BodySpec, FigureModelEntry};
 use crate::{
     mesh::{greedy::GreedyMesh, Meshable},
-    render::{
-        BoneMeshes, ColLightInfo, FigureModel, FigurePipeline, Mesh, Renderer, TerrainPipeline,
-    },
+    render::{BoneMeshes, ColLightInfo, FigureModel, Mesh, Renderer, TerrainVertex},
     scene::camera::CameraMode,
 };
 use anim::Skeleton;
@@ -411,7 +409,7 @@ where
 
                     // Then, set up meshing context.
                     let mut greedy = FigureModel::make_greedy();
-                    let mut opaque = Mesh::<TerrainPipeline>::new();
+                    let mut opaque = Mesh::<TerrainVertex>::new();
                     // Choose the most conservative bounds for any LOD model.
                     let mut figure_bounds = anim::vek::Aabb {
                         min: anim::vek::Vec3::zero(),
@@ -473,13 +471,13 @@ where
 
                     fn generate_mesh<'a>(
                         greedy: &mut GreedyMesh<'a>,
-                        opaque_mesh: &mut Mesh<TerrainPipeline>,
+                        opaque_mesh: &mut Mesh<TerrainVertex>,
                         segment: &'a Segment,
                         offset: Vec3<f32>,
                         bone_idx: u8,
                     ) -> BoneMeshes {
                         let (opaque, _, _, bounds) =
-                            Meshable::<FigurePipeline, &mut GreedyMesh>::generate_mesh(
+                            Meshable::<TerrainVertex, &mut GreedyMesh>::generate_mesh(
                                 segment,
                                 (greedy, opaque_mesh, offset, Vec3::one(), bone_idx),
                             );
@@ -488,14 +486,14 @@ where
 
                     fn generate_mesh_lod_mid<'a>(
                         greedy: &mut GreedyMesh<'a>,
-                        opaque_mesh: &mut Mesh<TerrainPipeline>,
+                        opaque_mesh: &mut Mesh<TerrainVertex>,
                         segment: &'a Segment,
                         offset: Vec3<f32>,
                         bone_idx: u8,
                     ) -> BoneMeshes {
                         let lod_scale = 0.6;
                         let (opaque, _, _, bounds) =
-                            Meshable::<FigurePipeline, &mut GreedyMesh>::generate_mesh(
+                            Meshable::<TerrainVertex, &mut GreedyMesh>::generate_mesh(
                                 segment.scaled_by(Vec3::broadcast(lod_scale)),
                                 (
                                     greedy,
@@ -510,14 +508,14 @@ where
 
                     fn generate_mesh_lod_low<'a>(
                         greedy: &mut GreedyMesh<'a>,
-                        opaque_mesh: &mut Mesh<TerrainPipeline>,
+                        opaque_mesh: &mut Mesh<TerrainVertex>,
                         segment: &'a Segment,
                         offset: Vec3<f32>,
                         bone_idx: u8,
                     ) -> BoneMeshes {
                         let lod_scale = 0.3;
                         let (opaque, _, _, bounds) =
-                            Meshable::<FigurePipeline, &mut GreedyMesh>::generate_mesh(
+                            Meshable::<TerrainVertex, &mut GreedyMesh>::generate_mesh(
                                 segment.scaled_by(Vec3::broadcast(lod_scale)),
                                 (
                                     greedy,
diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs
index 0f5537c113..912bfb2bae 100644
--- a/voxygen/src/scene/figure/mod.rs
+++ b/voxygen/src/scene/figure/mod.rs
@@ -7,8 +7,8 @@ pub use load::load_mesh; // TODO: Don't make this public.
 use crate::{
     ecs::comp::Interpolated,
     render::{
-        ColLightFmt, ColLightInfo, Consts, FigureBoneData, FigureLocals, FigureModel, GlobalModel,
-        Mesh, RenderError, Renderer, ShadowPipeline, TerrainPipeline, Texture,
+        pipelines, ColLightInfo, Consts, FigureBoneData, FigureLocals, FigureModel, GlobalModel,
+        Mesh, RenderError, Renderer, TerrainVertex, Texture,
     },
     scene::{
         camera::{Camera, CameraMode, Dependents},
@@ -64,7 +64,7 @@ pub type FigureModelRef<'a> = (
     &'a Consts<FigureLocals>,
     &'a Consts<FigureBoneData>,
     &'a FigureModel,
-    &'a Texture<ColLightFmt>,
+    &'a Texture, /* <ColLightFmt> */
 );
 
 /// An entry holding enough information to draw or destroy a figure in a
@@ -80,7 +80,7 @@ pub struct FigureModelEntry<const N: usize> {
     /// Texture used to store color/light information for this figure entry.
     /* TODO: Consider using mipmaps instead of storing multiple texture atlases for different
      * LOD levels. */
-    col_lights: Texture<ColLightFmt>,
+    col_lights: Texture, /* <ColLightFmt> */
     /// Models stored in this figure entry; there may be several for one figure,
     /// because of LOD models.
     pub models: [FigureModel; N],
@@ -5208,10 +5208,8 @@ impl FigureColLights {
     }
 
     /// Find the correct texture for this model entry.
-    pub fn texture<'a, const N: usize>(
-        &'a self,
-        model: &'a FigureModelEntry<N>,
-    ) -> &'a Texture<ColLightFmt> {
+    pub fn texture<'a, const N: usize>(&'a self, model: &'a FigureModelEntry<N>) -> &'a Texture /* <ColLightFmt> */
+    {
         /* &self.col_lights */
         &model.col_lights
     }
@@ -5226,7 +5224,7 @@ impl FigureColLights {
         &mut self,
         renderer: &mut Renderer,
         (tex, tex_size): ColLightInfo,
-        (opaque, bounds): (Mesh<TerrainPipeline>, math::Aabb<f32>),
+        (opaque, bounds): (Mesh<TerrainVertex>, math::Aabb<f32>),
         vertex_range: [Range<u32>; N],
     ) -> Result<FigureModelEntry<N>, RenderError> {
         span!(_guard, "create_figure", "FigureColLights::create_figure");
@@ -5237,7 +5235,7 @@ impl FigureColLights {
                 i32::from(tex_size.y),
             ))
             .expect("Not yet implemented: allocate new atlas on allocation failure.");
-        let col_lights = ShadowPipeline::create_col_lights(renderer, &(tex, tex_size))?;
+        let col_lights = pipelines::shadow::create_col_lights(renderer, &(tex, tex_size))?;
         let model_len = u32::try_from(opaque.vertices().len())
             .expect("The model size for this figure does not fit in a u32!");
         let model = renderer.create_model(&opaque)?;
@@ -5507,5 +5505,5 @@ impl<S: Skeleton> FigureState<S> {
 fn figure_bone_data_from_anim(
     mats: &[anim::FigureBoneData; anim::MAX_BONE_COUNT],
 ) -> &[FigureBoneData] {
-    gfx::memory::cast_slice(mats)
+    bytemuck::cast_slice(mats)
 }
diff --git a/voxygen/src/scene/lod.rs b/voxygen/src/scene/lod.rs
index 76dab1b870..926559f199 100644
--- a/voxygen/src/scene/lod.rs
+++ b/voxygen/src/scene/lod.rs
@@ -1,7 +1,7 @@
 use crate::{
     render::{
-        pipelines::lod_terrain::{Locals, LodData, Vertex},
-        Consts, GlobalModel, LodTerrainPipeline, Mesh, Model, Quad, Renderer,
+        pipelines::lod_terrain::{LodData, Vertex},
+        GlobalModel, LodTerrainVertex, Mesh, Model, Quad, Renderer,
     },
     settings::Settings,
 };
@@ -10,8 +10,7 @@ use common::{spiral::Spiral2d, util::srgba_to_linear};
 use vek::*;
 
 pub struct Lod {
-    model: Option<(u32, Model<LodTerrainPipeline>)>,
-    locals: Consts<Locals>,
+    model: Option<(u32, Model<LodTerrainVertex>)>,
     data: LodData,
 }
 
@@ -25,7 +24,6 @@ impl Lod {
     pub fn new(renderer: &mut Renderer, client: &Client, settings: &Settings) -> Self {
         Self {
             model: None,
-            locals: renderer.create_consts(&[Locals::default()]).unwrap(),
             data: LodData::new(
                 renderer,
                 client.world_data().chunk_size(),
@@ -33,7 +31,7 @@ impl Lod {
                 client.world_data().lod_alt.raw(),
                 client.world_data().lod_horizon.raw(),
                 settings.graphics.lod_detail.max(100).min(2500),
-                water_color().into_array().into(),
+                // water_color().into_array().into(),
             ),
         }
     }
@@ -68,7 +66,7 @@ impl Lod {
     }
 }
 
-fn create_lod_terrain_mesh(detail: u32) -> Mesh<LodTerrainPipeline> {
+fn create_lod_terrain_mesh(detail: u32) -> Mesh<LodTerrainVertex> {
     // detail is even, so we choose odd detail (detail + 1) to create two even
     // halves with an empty hole.
     let detail = detail + 1;
diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs
index daff82a064..6f496f5386 100644
--- a/voxygen/src/scene/mod.rs
+++ b/voxygen/src/scene/mod.rs
@@ -17,8 +17,8 @@ use crate::{
     audio::{ambient::AmbientMgr, music::MusicMgr, sfx::SfxMgr, AudioFrontend},
     render::{
         create_clouds_mesh, create_pp_mesh, create_skybox_mesh, CloudsLocals, CloudsPipeline,
-        Consts, GlobalModel, Globals, Light, LodData, Model, PostProcessLocals,
-        PostProcessPipeline, Renderer, Shadow, ShadowLocals, SkyboxLocals, SkyboxPipeline,
+        Consts, GlobalModel, Globals, Light, LodData, Model, PostProcessLocals, Renderer, Shadow,
+        ShadowLocals, SkyboxLocals,
     },
     settings::Settings,
     window::{AnalogGameInput, Event},
@@ -67,8 +67,7 @@ struct EventLight {
 }
 
 struct Skybox {
-    model: Model<SkyboxPipeline>,
-    locals: Consts<SkyboxLocals>,
+    model: Model<SkyboxVertex>,
 }
 
 struct Clouds {
@@ -77,8 +76,7 @@ struct Clouds {
 }
 
 struct PostProcess {
-    model: Model<PostProcessPipeline>,
-    locals: Consts<PostProcessLocals>,
+    model: Model<PostProcessVertex>,
 }
 
 pub struct Scene {
@@ -295,7 +293,6 @@ impl Scene {
 
             skybox: Skybox {
                 model: renderer.create_model(&create_skybox_mesh()).unwrap(),
-                locals: renderer.create_consts(&[SkyboxLocals::default()]).unwrap(),
             },
             clouds: Clouds {
                 model: renderer.create_model(&create_clouds_mesh()).unwrap(),
@@ -303,9 +300,6 @@ impl Scene {
             },
             postprocess: PostProcess {
                 model: renderer.create_model(&create_pp_mesh()).unwrap(),
-                locals: renderer
-                    .create_consts(&[PostProcessLocals::default()])
-                    .unwrap(),
             },
             terrain: Terrain::new(renderer, sprite_render_context),
             lod: Lod::new(renderer, client, settings),
diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs
index 0cc0e5f1b6..a33f88a926 100644
--- a/voxygen/src/scene/particle.rs
+++ b/voxygen/src/scene/particle.rs
@@ -3,7 +3,7 @@ use crate::{
     mesh::{greedy::GreedyMesh, Meshable},
     render::{
         pipelines::particle::ParticleMode, GlobalModel, Instances, Light, LodData, Model,
-        ParticleInstance, ParticlePipeline, Renderer,
+        ParticleInstance, ParticleVertex, Renderer,
     },
 };
 use common::{
@@ -38,7 +38,7 @@ pub struct ParticleMgr {
     instances: Instances<ParticleInstance>,
 
     /// GPU Vertex Buffers
-    model_cache: HashMap<&'static str, Model<ParticlePipeline>>,
+    model_cache: HashMap<&'static str, Model<ParticleVertex>>,
 }
 
 impl ParticleMgr {
@@ -1211,7 +1211,7 @@ fn default_instances(renderer: &mut Renderer) -> Instances<ParticleInstance> {
 
 const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle";
 
-fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model<ParticlePipeline>> {
+fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model<ParticleVertex>> {
     let mut model_cache = HashMap::new();
 
     model_cache.entry(DEFAULT_MODEL_KEY).or_insert_with(|| {
@@ -1227,7 +1227,7 @@ fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model<Particl
         let segment = Segment::from(&vox.read().0);
         let segment_size = segment.size();
         let mut mesh =
-            Meshable::<ParticlePipeline, &mut GreedyMesh>::generate_mesh(segment, &mut greedy).0;
+            Meshable::<ParticleVertex, &mut GreedyMesh>::generate_mesh(segment, &mut greedy).0;
         // Center particle vertices around origin
         for vert in mesh.vertices_mut() {
             vert.pos[0] -= segment_size.x as f32 / 2.0;
diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs
index 2b0b0135a9..cc367d81a0 100644
--- a/voxygen/src/scene/simple.rs
+++ b/voxygen/src/scene/simple.rs
@@ -1,10 +1,10 @@
 use crate::{
     mesh::{greedy::GreedyMesh, Meshable},
     render::{
-        create_clouds_mesh, create_pp_mesh, create_skybox_mesh, BoneMeshes, CloudsLocals,
-        CloudsPipeline, Consts, FigureModel, FigurePipeline, GlobalModel, Globals, Light, Mesh,
-        Model, PostProcessLocals, PostProcessPipeline, Renderer, Shadow, ShadowLocals,
-        SkyboxLocals, SkyboxPipeline, TerrainPipeline,
+        create_clouds_mesh, create_pp_mesh, create_skybox_mesh, BoneMeshes, CloudsLocals, Consts,
+        FigureModel, GlobalModel, Globals, Light, Mesh, Model, PostProcessLocals,
+        PostProcessVertex, Renderer, Shadow, ShadowLocals, SkyboxLocals, SkyboxVertex,
+        TerrainVertex,
     },
     scene::{
         camera::{self, Camera, CameraMode},
@@ -45,13 +45,13 @@ impl ReadVol for VoidVol {
 
 fn generate_mesh<'a>(
     greedy: &mut GreedyMesh<'a>,
-    mesh: &mut Mesh<TerrainPipeline>,
+    mesh: &mut Mesh<TerrainVertex>,
     segment: Segment,
     offset: Vec3<f32>,
     bone_idx: u8,
 ) -> BoneMeshes {
     let (opaque, _, /* shadow */ _, bounds) =
-        Meshable::<FigurePipeline, &mut GreedyMesh>::generate_mesh(
+        Meshable::<TerrainVertex, &mut GreedyMesh>::generate_mesh(
             segment,
             (greedy, mesh, offset, Vec3::one(), bone_idx),
         );
@@ -59,13 +59,11 @@ fn generate_mesh<'a>(
 }
 
 struct Skybox {
-    model: Model<SkyboxPipeline>,
-    locals: Consts<SkyboxLocals>,
+    model: Model<SkyboxVertex>,
 }
 
 struct PostProcess {
-    model: Model<PostProcessPipeline>,
-    locals: Consts<PostProcessLocals>,
+    model: Model<PostProcessVertex>,
 }
 
 struct Clouds {
@@ -139,7 +137,6 @@ impl Scene {
 
             skybox: Skybox {
                 model: renderer.create_model(&create_skybox_mesh()).unwrap(),
-                locals: renderer.create_consts(&[SkyboxLocals::default()]).unwrap(),
             },
             clouds: Clouds {
                 model: renderer.create_model(&create_clouds_mesh()).unwrap(),
@@ -147,9 +144,6 @@ impl Scene {
             },
             postprocess: PostProcess {
                 model: renderer.create_model(&create_pp_mesh()).unwrap(),
-                locals: renderer
-                    .create_consts(&[PostProcessLocals::default()])
-                    .unwrap(),
             },
             lod: LodData::new(
                 renderer,
diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs
index 65e34bde73..137c807464 100644
--- a/voxygen/src/scene/terrain.rs
+++ b/voxygen/src/scene/terrain.rs
@@ -4,9 +4,9 @@ pub use self::watcher::{BlocksOfInterest, Interaction};
 use crate::{
     mesh::{greedy::GreedyMesh, terrain::SUNLIGHT, Meshable},
     render::{
-        ColLightFmt, ColLightInfo, Consts, FluidPipeline, GlobalModel, Instances, Mesh, Model,
-        RenderError, Renderer, ShadowPipeline, SpriteInstance, SpriteLocals, SpritePipeline,
-        TerrainLocals, TerrainPipeline, Texture,
+        pipelines, ColLightInfo, Consts, FluidVertex, GlobalModel, Instances, Mesh, Model,
+        RenderError, Renderer, SpriteInstance, SpriteLocals, SpriteVertex, TerrainLocals,
+        TerrainVertex, Texture,
     },
 };
 
@@ -61,8 +61,8 @@ type LightMapFn = Arc<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>;
 pub struct TerrainChunkData {
     // GPU data
     load_time: f32,
-    opaque_model: Model<TerrainPipeline>,
-    fluid_model: Option<Model<FluidPipeline>>,
+    opaque_model: Model<TerrainVertex>,
+    fluid_model: Option<Model<FluidVertex>>,
     /// If this is `None`, this texture is not allocated in the current atlas,
     /// and therefore there is no need to free its allocation.
     col_lights: Option<guillotiere::AllocId>,
@@ -72,7 +72,7 @@ pub struct TerrainChunkData {
     /// shadow chunks will still keep it alive; we could deal with this by
     /// making this an `Option`, but it probably isn't worth it since they
     /// shouldn't be that much more nonlocal than regular chunks).
-    texture: Texture<ColLightFmt>,
+    texture: Arc<Texture>, // TODO: make this actually work with a bind group
     light_map: LightMapFn,
     glow_map: LightMapFn,
     sprite_instances: HashMap<(SpriteKind, usize), Instances<SpriteInstance>>,
@@ -98,8 +98,8 @@ struct ChunkMeshState {
 /// Just the mesh part of a mesh worker response.
 pub struct MeshWorkerResponseMesh {
     z_bounds: (f32, f32),
-    opaque_mesh: Mesh<TerrainPipeline>,
-    fluid_mesh: Mesh<FluidPipeline>,
+    opaque_mesh: Mesh<TerrainVertex>,
+    fluid_mesh: Mesh<FluidVertex>,
     col_lights_info: ColLightInfo,
     light_map: LightMapFn,
     glow_map: LightMapFn,
@@ -264,7 +264,7 @@ fn mesh_worker<V: BaseVol<Vox = Block> + RectRasterableVol + ReadVol + Debug + '
 struct SpriteData {
     /* mat: Mat4<f32>, */
     locals: Consts<SpriteLocals>,
-    model: Model<SpritePipeline>,
+    model: Model<SpriteVertex>,
     /* scale: Vec3<f32>, */
     offset: Vec3<f32>,
 }
@@ -317,12 +317,12 @@ pub struct Terrain<V: RectRasterableVol = TerrainChunk> {
 
     // GPU data
     sprite_data: Arc<HashMap<(SpriteKind, usize), Vec<SpriteData>>>,
-    sprite_col_lights: Texture<ColLightFmt>,
+    sprite_col_lights: Texture, /* <ColLightFmt> */
     /// As stated previously, this is always the very latest texture into which
     /// we allocate.  Code cannot assume that this is the assigned texture
     /// for any particular chunk; look at the `texture` field in
     /// `TerrainChunkData` for that.
-    col_lights: Texture<ColLightFmt>,
+    col_lights: Texture, /* <ColLightFmt> */
     waves: Texture,
 
     phantom: PhantomData<V>,
@@ -336,7 +336,7 @@ impl TerrainChunkData {
 pub struct SpriteRenderContext {
     sprite_config: Arc<SpriteSpec>,
     sprite_data: Arc<HashMap<(SpriteKind, usize), Vec<SpriteData>>>,
-    sprite_col_lights: Texture<ColLightFmt>,
+    sprite_col_lights: Texture, /* <ColLightFmt> */
 }
 
 pub type SpriteRenderContextLazy = Box<dyn FnMut(&mut Renderer) -> SpriteRenderContext>;
@@ -348,7 +348,7 @@ impl SpriteRenderContext {
 
         struct SpriteDataResponse {
             locals: [SpriteLocals; 8],
-            model: Mesh<SpritePipeline>,
+            model: Mesh<SpriteVertex>,
             offset: Vec3<f32>,
         }
 
@@ -369,7 +369,6 @@ impl SpriteRenderContext {
             let mut locals_buffer = [SpriteLocals::default(); 8];
             let sprite_config_ = &sprite_config;
             // NOTE: Tracks the start vertex of the next model to be meshed.
-
             let sprite_data: HashMap<(SpriteKind, usize), _> = SpriteKind::into_enum_iter()
                 .filter_map(|kind| Some((kind, kind.elim_case_pure(&sprite_config_.0).as_ref()?)))
                 .flat_map(|(kind, sprite_config)| {
@@ -429,7 +428,7 @@ impl SpriteRenderContext {
                                             // has no
                                             // interesting return value, but updates the mesh.
                                             let mut opaque_mesh = Mesh::new();
-                                            Meshable::<SpritePipeline, &mut GreedyMesh>::generate_mesh(
+                                            Meshable::<SpriteVertex, &mut GreedyMesh>::generate_mesh(
                                                 Segment::from(&model.read().0).scaled_by(lod_scale),
                                                 (greedy, &mut opaque_mesh, false),
                                             );
@@ -522,8 +521,9 @@ impl SpriteRenderContext {
                     )
                 })
                 .collect();
-            let sprite_col_lights = ShadowPipeline::create_col_lights(renderer, &sprite_col_lights)
-                .expect("Failed to upload sprite color and light data to the GPU!");
+            let sprite_col_lights =
+                pipelines::shadow::create_col_lights(renderer, &sprite_col_lights)
+                    .expect("Failed to upload sprite color and light data to the GPU!");
 
             Self {
                 sprite_config: Arc::clone(&sprite_config),
@@ -559,9 +559,8 @@ impl<V: RectRasterableVol> Terrain<V> {
             waves: renderer
                 .create_texture(
                     &assets::Image::load_expect("voxygen.texture.waves").read().0,
-                    Some(gfx::texture::FilterMethod::Trilinear),
-                    Some(gfx::texture::WrapMode::Tile),
-                    None,
+                    Some(wgpu::FilterMode::Linear),
+                    Some(wgpu::AddressMode::Repeat),
                 )
                 .expect("Failed to create wave texture"),
             col_lights,
@@ -571,7 +570,7 @@ impl<V: RectRasterableVol> Terrain<V> {
 
     fn make_atlas(
         renderer: &mut Renderer,
-    ) -> Result<(AtlasAllocator, Texture<ColLightFmt>), RenderError> {
+    ) -> Result<(AtlasAllocator, Texture /* <ColLightFmt> */), RenderError> {
         span!(_guard, "make_atlas", "Terrain::make_atlas");
         let max_texture_size = renderer.max_texture_size();
         let atlas_size =
@@ -583,20 +582,29 @@ impl<V: RectRasterableVol> Terrain<V> {
             ..guillotiere::AllocatorOptions::default()
         });
         let texture = renderer.create_texture_raw(
-            gfx::texture::Kind::D2(
-                max_texture_size,
-                max_texture_size,
-                gfx::texture::AaMode::Single,
-            ),
-            1_u8,
-            gfx::memory::Bind::SHADER_RESOURCE,
-            gfx::memory::Usage::Dynamic,
-            (0, 0),
-            gfx::format::Swizzle::new(),
-            gfx::texture::SamplerInfo::new(
-                gfx::texture::FilterMethod::Bilinear,
-                gfx::texture::WrapMode::Clamp,
-            ),
+            wgpu::TextureDescriptor {
+                label: Some("Atlas texture"),
+                size: wgpu::Extent3d {
+                    width: max_texture_size,
+                    height: max_texture_size,
+                    depth: 1,
+                },
+                mip_level_count: 1,
+                sample_count: 1,
+                dimension: wgpu::TextureDimension::D1,
+                format: wgpu::TextureFormat::Rgba8UnormSrgb,
+                usage: wgpu::TextureUsage::COPY_DST | wgpu::TextureUsage::SAMPLED,
+            },
+            wgpu::SamplerDescriptor {
+                label: Some("Atlas sampler"),
+                address_mode_u: wgpu::AddressMode::ClampToEdge,
+                address_mode_v: wgpu::AddressMode::ClampToEdge,
+                address_mode_w: wgpu::AddressMode::ClampToEdge,
+                mag_filter: wgpu::FilterMode::Linear,
+                min_filter: wgpu::FilterMode::Linear,
+                mipmap_filter: wgpu::FilterMode::Nearest,
+                ..Default::default()
+            },
         )?;
         Ok((atlas, texture))
     }
diff --git a/voxygen/src/ui/cache.rs b/voxygen/src/ui/cache.rs
index fe2eaa13f5..ab057ffedc 100644
--- a/voxygen/src/ui/cache.rs
+++ b/voxygen/src/ui/cache.rs
@@ -1,6 +1,6 @@
 use super::graphic::{Graphic, GraphicCache, Id as GraphicId};
 use crate::{
-    render::{Mesh, Renderer, Texture, UiPipeline},
+    render::{Mesh, Renderer, Texture, UIVertex},
     Error,
 };
 use conrod_core::{text::GlyphCache, widget::Id};
@@ -13,7 +13,7 @@ const GLYPH_CACHE_SIZE: u16 = 1;
 const SCALE_TOLERANCE: f32 = 0.5;
 const POSITION_TOLERANCE: f32 = 0.5;
 
-type TextCache = HashMap<Id, Mesh<UiPipeline>>;
+type TextCache = HashMap<Id, Mesh<UIVertex>>;
 
 pub struct Cache {
     // Map from text ids to their positioned glyphs.
diff --git a/voxygen/src/ui/graphic/mod.rs b/voxygen/src/ui/graphic/mod.rs
index 241e14dbbc..a044502bb3 100644
--- a/voxygen/src/ui/graphic/mod.rs
+++ b/voxygen/src/ui/graphic/mod.rs
@@ -4,7 +4,7 @@ mod renderer;
 pub use renderer::{SampleStrat, Transform};
 
 use crate::{
-    render::{RenderError, Renderer, Texture},
+    render::{Renderer, Texture},
     ui::KeyedJobs,
 };
 use common::{figure::Segment, slowjob::SlowJobPool};
@@ -474,21 +474,17 @@ fn upload_image(renderer: &mut Renderer, aabr: Aabr<u16>, tex: &Texture, image:
         size,
         // NOTE: Rgba texture, so each pixel is 4 bytes, ergo this cannot fail.
         // We make the cast parameters explicit for clarity.
-        gfx::memory::cast_slice::<u8, [u8; 4]>(&image),
+        bytemuck::cast_slice::<u8, [u8; 4]>(&image),
     ) {
         warn!(?e, "Failed to update texture");
     }
 }
 
-fn create_image(
-    renderer: &mut Renderer,
-    image: RgbaImage,
-    border_color: Rgba<f32>,
-) -> Result<Texture, RenderError> {
+fn create_image(renderer: &mut Renderer, image: RgbaImage, border_color: Rgba<f32>) {
     renderer.create_texture(
         &DynamicImage::ImageRgba8(image),
         None,
-        Some(gfx::texture::WrapMode::Border),
+        Some(wgpu::AddressMode::ClampToBorder),
         Some(border_color.into_array().into()),
     )
 }
diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs
index 9a4ea66dc2..05a300feef 100644
--- a/voxygen/src/ui/mod.rs
+++ b/voxygen/src/ui/mod.rs
@@ -27,8 +27,8 @@ pub use widgets::{
 
 use crate::{
     render::{
-        create_ui_quad, create_ui_tri, Consts, DynamicModel, Globals, Mesh, RenderError, Renderer,
-        UiLocals, UiMode, UiPipeline,
+        create_ui_quad, create_ui_tri, Consts, Globals, Mesh, Model, RenderError, Renderer,
+        UIVertex, UiLocals, UiMode,
     },
     window::Window,
     Error,
@@ -109,9 +109,9 @@ pub struct Ui {
     draw_commands: Vec<DrawCommand>,
     // Mesh buffer for UI vertices; we reuse its allocation in order to limit vector reallocations
     // during redrawing.
-    mesh: Mesh<UiPipeline>,
+    mesh: Mesh<UIVertex>,
     // Model for drawing the ui
-    model: DynamicModel<UiPipeline>,
+    model: Model<UIVertex>,
     // Consts for default ui drawing position (ie the interface)
     interface_locals: Consts<UiLocals>,
     default_globals: Consts<Globals>,
@@ -577,7 +577,13 @@ impl Ui {
                         .map(|x| [255, 255, 255, *x])
                         .collect::<Vec<[u8; 4]>>();
 
-                    if let Err(err) = renderer.update_texture(cache_tex, offset, size, &new_data) {
+                    if let Err(err) = renderer.update_texture(
+                        cache_tex,
+                        offset,
+                        size,
+                        &new_data,
+                        rect.width() * 4,
+                    ) {
                         warn!("Failed to update texture: {:?}", err);
                     }
                 }) {