diff --git a/engine/graphics/src/graphics.rs b/engine/graphics/src/graphics.rs
index a7340df..cb7f3a7 100644
--- a/engine/graphics/src/graphics.rs
+++ b/engine/graphics/src/graphics.rs
@@ -5,7 +5,7 @@ use strafesnet_settings::settings;
 use strafesnet_session::session;
 use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
 use wgpu::{util::DeviceExt,AstcBlock,AstcChannel};
-use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
+use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex,DebugGraphicsVertex};
 
 pub fn required_limits()->wgpu::Limits{
 	wgpu::Limits::default()
@@ -36,12 +36,22 @@ struct GraphicsModel{
 	instance_count:u32,
 }
 
+struct DebugGraphicsMesh{
+	indices:Indices,
+	vertex_buf:wgpu::Buffer,
+}
+struct DebugGraphicsModel{
+	debug_mesh_id:u32,
+	bind_group:wgpu::BindGroup,
+}
+
 struct GraphicsSamplers{
 	repeat:wgpu::Sampler,
 }
 
 struct GraphicsBindGroupLayouts{
 	model:wgpu::BindGroupLayout,
+	debug_model:wgpu::BindGroupLayout,
 }
 
 struct GraphicsBindGroups{
@@ -52,6 +62,7 @@ struct GraphicsBindGroups{
 struct GraphicsPipelines{
 	skybox:wgpu::RenderPipeline,
 	model:wgpu::RenderPipeline,
+	debug:wgpu::RenderPipeline,
 }
 
 struct GraphicsCamera{
@@ -112,6 +123,8 @@ pub struct GraphicsState{
 	camera_buf:wgpu::Buffer,
 	temp_squid_texture_view:wgpu::TextureView,
 	models:Vec<GraphicsModel>,
+	debug_meshes:Vec<DebugGraphicsMesh>,
+	debug_models:Vec<DebugGraphicsModel>,
 	depth_view:wgpu::TextureView,
 	staging_belt:wgpu::util::StagingBelt,
 }
@@ -146,6 +159,76 @@ impl GraphicsState{
 		self.camera.fov=user_settings.calculate_fov(1.0,&self.camera.screen_size).as_vec2();
 	}
 	pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){
+		//generate debug meshes, each debug model refers to one
+		self.debug_meshes=map.meshes.iter().map(|mesh|{
+			let vertices:Vec<DebugGraphicsVertex>=mesh.unique_vertices.iter().map(|vertex|{
+				DebugGraphicsVertex{
+					pos:mesh.unique_pos[vertex.pos.get() as usize].to_array().map(Into::into),
+					normal:mesh.unique_normal[vertex.normal.get() as usize].to_array().map(Into::into),
+				}
+			}).collect();
+			let vertex_buf=device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
+				label:Some("Vertex"),
+				contents:bytemuck::cast_slice(&vertices),
+				usage:wgpu::BufferUsages::VERTEX,
+			});
+
+			let mut indices=Vec::new();
+			for physics_group in &mesh.physics_groups{
+				for polygon_group_id in &physics_group.groups{
+					for poly in mesh.polygon_groups[polygon_group_id.get() as usize].polys(){
+						// triangulate
+						let mut poly_vertices=poly.into_iter().copied();
+						if let (Some(a),Some(mut b))=(poly_vertices.next(),poly_vertices.next()){
+							for c in poly_vertices{
+								indices.extend([a,b,c]);
+								b=c;
+							}
+						}
+					}
+				}
+			}
+
+			DebugGraphicsMesh{
+				indices:if (u32::MAX as usize)<vertices.len(){
+					panic!("Model has too many vertices!")
+				}else if (u16::MAX as usize)<vertices.len(){
+					Indices::new(device,&indices.into_iter().map(|vertex_idx|vertex_idx.get() as u32).collect(),wgpu::IndexFormat::Uint32)
+				}else{
+					Indices::new(device,&indices.into_iter().map(|vertex_idx|vertex_idx.get() as u16).collect(),wgpu::IndexFormat::Uint16)
+				},
+				vertex_buf,
+			}
+		}).collect();
+
+		//generate debug models, only one will be rendered at a time
+		self.debug_models=map.models.iter().enumerate().map(|(model_id,model)|{
+			let model_uniforms=get_instance_buffer_data(&GraphicsModelOwned{
+				transform:model.transform.into(),
+				normal_transform:glam::Mat3::from_cols_array_2d(&model.transform.matrix3.to_array().map(|row|row.map(Into::into))).inverse().transpose(),
+				color:GraphicsModelColor4::new(glam::vec4(1.0,0.0,0.0,0.2)),
+			});
+			let model_buf=device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
+				label:Some(format!("Debug Model{} Buf",model_id).as_str()),
+				contents:bytemuck::cast_slice(&model_uniforms),
+				usage:wgpu::BufferUsages::UNIFORM|wgpu::BufferUsages::COPY_DST,
+			});
+			let bind_group=device.create_bind_group(&wgpu::BindGroupDescriptor{
+				layout:&self.bind_group_layouts.debug_model,
+				entries:&[
+					wgpu::BindGroupEntry{
+						binding:0,
+						resource:model_buf.as_entire_binding(),
+					},
+				],
+				label:Some(format!("Debug Model{} Bind Group",model_id).as_str()),
+			});
+			DebugGraphicsModel{
+				debug_mesh_id:model.mesh.get(),
+				bind_group,
+			}
+		}).collect();
+
 		//generate texture view per texture
 		let texture_views:HashMap<strafesnet_common::model::TextureId,wgpu::TextureView>=map.textures.iter().enumerate().filter_map(|(texture_id,texture_data)|{
 			let texture_id=model::TextureId::new(texture_id as u32);
@@ -588,6 +671,21 @@ impl GraphicsState{
 				},
 			],
 		});
+		let debug_model_bind_group_layout=device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor{
+			label:Some("Debug Model Bind Group Layout"),
+			entries:&[
+				wgpu::BindGroupLayoutEntry{
+					binding:0,
+					visibility:wgpu::ShaderStages::VERTEX_FRAGMENT,
+					ty:wgpu::BindingType::Buffer{
+						ty:wgpu::BufferBindingType::Uniform,
+						has_dynamic_offset:false,
+						min_binding_size:None,
+					},
+					count:None,
+				},
+			],
+		});
 
 		let clamp_sampler=device.create_sampler(&wgpu::SamplerDescriptor{
 			label:Some("Clamp Sampler"),
@@ -736,6 +834,14 @@ impl GraphicsState{
 			],
 			push_constant_ranges:&[],
 		});
+		let debug_model_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{
+				label:None,
+				bind_group_layouts:&[
+					&camera_bind_group_layout,
+					&debug_model_bind_group_layout,
+				],
+				push_constant_ranges:&[],
+			});
 		let sky_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{
 			label:None,
 			bind_group_layouts:&[
@@ -811,6 +917,45 @@ impl GraphicsState{
 			multiview:None,
 			cache:None,
 		});
+		let debug_model_pipeline=device.create_render_pipeline(&wgpu::RenderPipelineDescriptor{
+			label:Some("Debug Model Pipeline"),
+			layout:Some(&debug_model_pipeline_layout),
+			vertex:wgpu::VertexState{
+				module:&shader,
+				entry_point:Some("vs_debug"),
+				buffers:&[wgpu::VertexBufferLayout{
+					array_stride:std::mem::size_of::<DebugGraphicsVertex>() as wgpu::BufferAddress,
+					step_mode:wgpu::VertexStepMode::Vertex,
+					attributes:&wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x3],
+				}],
+				compilation_options:wgpu::PipelineCompilationOptions::default(),
+			},
+			fragment:Some(wgpu::FragmentState{
+				module:&shader,
+				entry_point:Some("fs_debug"),
+				targets:&[Some(wgpu::ColorTargetState{
+					format:config.view_formats[0],
+					blend:Some(wgpu::BlendState::ALPHA_BLENDING),
+					write_mask:wgpu::ColorWrites::default(),
+				})],
+				compilation_options:wgpu::PipelineCompilationOptions::default(),
+			}),
+			primitive:wgpu::PrimitiveState{
+				front_face:wgpu::FrontFace::Cw,
+				cull_mode:Some(wgpu::Face::Front),
+				..Default::default()
+			},
+			depth_stencil:Some(wgpu::DepthStencilState{
+				format:Self::DEPTH_FORMAT,
+				depth_write_enabled:true,
+				depth_compare:wgpu::CompareFunction::Always,
+				stencil:wgpu::StencilState::default(),
+				bias:wgpu::DepthBiasState::default(),
+			}),
+			multisample:wgpu::MultisampleState::default(),
+			multiview:None,
+			cache:None,
+		});
 
 		let camera=GraphicsCamera::default();
 		let camera_uniforms=camera.to_uniform_data(glam::Vec3::ZERO,glam::Vec2::ZERO);
@@ -850,7 +995,8 @@ impl GraphicsState{
 		Self{
 			pipelines:GraphicsPipelines{
 				skybox:sky_pipeline,
-				model:model_pipeline
+				model:model_pipeline,
+				debug:debug_model_pipeline,
 			},
 			bind_groups:GraphicsBindGroups{
 				camera:camera_bind_group,
@@ -859,9 +1005,14 @@ impl GraphicsState{
 			camera,
 			camera_buf,
 			models:Vec::new(),
+			debug_meshes:Vec::new(),
+			debug_models:Vec::new(),
 			depth_view,
 			staging_belt:wgpu::util::StagingBelt::new(0x100),
-			bind_group_layouts:GraphicsBindGroupLayouts{model:model_bind_group_layout},
+			bind_group_layouts:GraphicsBindGroupLayouts{
+				model:model_bind_group_layout,
+				debug_model:debug_model_bind_group_layout,
+			},
 			samplers:GraphicsSamplers{repeat:repeat_sampler},
 			temp_squid_texture_view:squid_texture_view,
 		}
@@ -949,6 +1100,7 @@ impl GraphicsState{
 			rpass.set_bind_group(0,&self.bind_groups.camera,&[]);
 			rpass.set_bind_group(1,&self.bind_groups.skybox_texture,&[]);
 
+			// Draw all models.
 			rpass.set_pipeline(&self.pipelines.model);
 			for model in &self.models{
 				rpass.set_bind_group(2,&model.bind_group,&[]);
@@ -960,6 +1112,19 @@ impl GraphicsState{
 
 			rpass.set_pipeline(&self.pipelines.skybox);
 			rpass.draw(0..3,0..1);
+
+			// render a single debug_model in red
+			if let Some(model_id)=frame_state.debug_model{
+				if let Some(model)=self.debug_models.get(model_id.get() as usize){
+					let mesh=&self.debug_meshes[model.debug_mesh_id as usize];
+					rpass.set_pipeline(&self.pipelines.debug);
+					rpass.set_bind_group(1,&model.bind_group,&[]);
+					rpass.set_vertex_buffer(0,mesh.vertex_buf.slice(..));
+					rpass.set_index_buffer(mesh.indices.buf.slice(..),mesh.indices.format);
+					//TODO: loop over triangle strips
+					rpass.draw_indexed(0..mesh.indices.count,0,0..1);
+				}
+			}
 		}
 
 		queue.submit(std::iter::once(encoder.finish()));
@@ -968,21 +1133,23 @@ impl GraphicsState{
 	}
 }
 const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
-const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*4;
+const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*core::mem::size_of::<f32>();
+fn get_instance_buffer_data(instance:&GraphicsModelOwned)->[f32;MODEL_BUFFER_SIZE]{
+	let mut out=[0.0;MODEL_BUFFER_SIZE];
+	out[0..16].copy_from_slice(instance.transform.as_ref());
+	out[16..19].copy_from_slice(instance.normal_transform.x_axis.as_ref());
+	// out[20]=0.0;
+	out[20..23].copy_from_slice(instance.normal_transform.y_axis.as_ref());
+	// out[24]=0.0;
+	out[24..27].copy_from_slice(instance.normal_transform.z_axis.as_ref());
+	// out[28]=0.0;
+	out[28..32].copy_from_slice(instance.color.get().as_ref());
+	out
+}
 fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
 	let mut raw=Vec::with_capacity(MODEL_BUFFER_SIZE*instances.len());
 	for mi in instances{
-		//model transform
-		raw.extend_from_slice(&AsRef::<[f32; 4*4]>::as_ref(&mi.transform)[..]);
-		//normal transform
-		raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.x_axis));
-		raw.extend_from_slice(&[0.0]);
-		raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.y_axis));
-		raw.extend_from_slice(&[0.0]);
-		raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
-		raw.extend_from_slice(&[0.0]);
-		//color
-		raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
+		raw.extend_from_slice(&get_instance_buffer_data(mi));
 	}
 	raw
 }
diff --git a/engine/graphics/src/model.rs b/engine/graphics/src/model.rs
index 2468cda..775b7c6 100644
--- a/engine/graphics/src/model.rs
+++ b/engine/graphics/src/model.rs
@@ -8,6 +8,12 @@ pub struct GraphicsVertex{
 	pub normal:[f32;3],
 	pub color:[f32;4],
 }
+#[derive(Clone,Copy,Pod,Zeroable)]
+#[repr(C)]
+pub struct DebugGraphicsVertex{
+	pub pos:[f32;3],
+	pub normal:[f32;3],
+}
 #[derive(Clone,Copy,id::Id)]
 pub struct IndexedGraphicsMeshOwnedRenderConfigId(u32);
 pub struct IndexedGraphicsMeshOwnedRenderConfig{
diff --git a/engine/session/src/session.rs b/engine/session/src/session.rs
index 55bf47b..5b453c8 100644
--- a/engine/session/src/session.rs
+++ b/engine/session/src/session.rs
@@ -2,6 +2,7 @@ use std::collections::HashMap;
 
 use strafesnet_common::gameplay_modes::{ModeId,StageId};
 use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,InstructionFeedback,TimedInstruction};
+use strafesnet_common::model::ModelId;
 // session represents the non-hardware state of the client.
 // Ideally it is a deterministic state which is atomically updated by instructions, same as the simulation state.
 use strafesnet_common::physics::{
@@ -61,6 +62,7 @@ pub struct FrameState{
 	pub body:physics::Body,
 	pub camera:physics::PhysicsCamera,
 	pub time:PhysicsTime,
+	pub debug_model:Option<strafesnet_common::model::ModelId>,
 }
 
 pub struct Simulation{
@@ -77,11 +79,12 @@ impl Simulation{
 			physics,
 		}
 	}
-	pub fn get_frame_state(&self,time:SessionTime)->FrameState{
+	pub fn get_frame_state(&self,time:SessionTime,debug_model:Option<ModelId>)->FrameState{
 		FrameState{
 			body:self.physics.camera_body(),
 			camera:self.physics.camera(),
 			time:self.timer.time(time),
+			debug_model,
 		}
 	}
 }
@@ -190,9 +193,9 @@ impl Session{
 	}
 	pub fn get_frame_state(&self,time:SessionTime)->Option<FrameState>{
 		match &self.view_state{
-			ViewState::Play=>Some(self.simulation.get_frame_state(time)),
+			ViewState::Play=>Some(self.simulation.get_frame_state(time,self.last_ray_hit)),
 			ViewState::Replay(bot_id)=>self.replays.get(bot_id).map(|replay|
-				replay.simulation.get_frame_state(time)
+				replay.simulation.get_frame_state(time,None)
 			),
 		}
 	}
diff --git a/strafe-client/src/shader.wgsl b/strafe-client/src/shader.wgsl
index 4298a03..a9bb9b6 100644
--- a/strafe-client/src/shader.wgsl
+++ b/strafe-client/src/shader.wgsl
@@ -86,6 +86,29 @@ fn vs_entity_texture(
 	return result;
 }
 
+@group(1)
+@binding(0)
+var<uniform> model_instance: ModelInstance;
+
+struct DebugEntityOutput {
+	@builtin(position) position: vec4<f32>,
+	@location(1) normal: vec3<f32>,
+	@location(2) view: vec3<f32>,
+};
+
+@vertex
+fn vs_debug(
+	@location(0) pos: vec3<f32>,
+	@location(1) normal: vec3<f32>,
+) -> DebugEntityOutput {
+	var position: vec4<f32> = model_instance.transform * vec4<f32>(pos, 1.0);
+	var result: DebugEntityOutput;
+	result.normal = model_instance.normal_transform * normal;
+	result.view = position.xyz - camera.view_inv[3].xyz;//col(3)
+	result.position = camera.proj * camera.view * position;
+	return result;
+}
+
 //group 2 is the skybox texture
 @group(1)
 @binding(0)
@@ -110,3 +133,8 @@ fn fs_entity_texture(vertex: EntityOutputTexture) -> @location(0) vec4<f32> {
 	let reflected_color = textureSample(cube_texture, cube_sampler, reflected).rgb;
 	return mix(vec4<f32>(vec3<f32>(0.05) + 0.2 * reflected_color,1.0),mix(vertex.model_color,vec4<f32>(fragment_color.rgb,1.0),fragment_color.a),0.5+0.5*abs(d));
 }
+
+@fragment
+fn fs_debug(vertex: DebugEntityOutput) -> @location(0) vec4<f32> {
+	return model_instance.color;
+}