move engine components into modules
This commit is contained in:
14
engine/graphics/Cargo.toml
Normal file
14
engine/graphics/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "strafesnet_graphics"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1.13.1", features = ["derive"] }
|
||||
ddsfile = "0.5.1"
|
||||
glam = "0.29.0"
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
||||
strafesnet_session = { path = "../session", registry = "strafesnet" }
|
||||
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
|
||||
wgpu = "24.0.0"
|
||||
8
engine/graphics/LICENSE
Normal file
8
engine/graphics/LICENSE
Normal file
@@ -0,0 +1,8 @@
|
||||
/*******************************************************
|
||||
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
|
||||
*
|
||||
* This file is part of the StrafesNET bhop/surf client.
|
||||
*
|
||||
* StrafesNET can not be copied and/or distributed
|
||||
* without the express permission of Rhys Lloyd
|
||||
*******************************************************/
|
||||
988
engine/graphics/src/graphics.rs
Normal file
988
engine/graphics/src/graphics.rs
Normal file
@@ -0,0 +1,988 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashSet,HashMap};
|
||||
use strafesnet_common::map;
|
||||
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};
|
||||
|
||||
pub fn required_limits()->wgpu::Limits{
|
||||
wgpu::Limits::default()
|
||||
}
|
||||
|
||||
struct Indices{
|
||||
count:u32,
|
||||
buf:wgpu::Buffer,
|
||||
format:wgpu::IndexFormat,
|
||||
}
|
||||
impl Indices{
|
||||
fn new<T:bytemuck::Pod>(device:&wgpu::Device,indices:&Vec<T>,format:wgpu::IndexFormat)->Self{
|
||||
Self{
|
||||
buf:device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
|
||||
label:Some("Index"),
|
||||
contents:bytemuck::cast_slice(indices),
|
||||
usage:wgpu::BufferUsages::INDEX,
|
||||
}),
|
||||
count:indices.len() as u32,
|
||||
format,
|
||||
}
|
||||
}
|
||||
}
|
||||
struct GraphicsModel{
|
||||
indices:Indices,
|
||||
vertex_buf:wgpu::Buffer,
|
||||
bind_group:wgpu::BindGroup,
|
||||
instance_count:u32,
|
||||
}
|
||||
|
||||
struct GraphicsSamplers{
|
||||
repeat:wgpu::Sampler,
|
||||
}
|
||||
|
||||
struct GraphicsBindGroupLayouts{
|
||||
model:wgpu::BindGroupLayout,
|
||||
}
|
||||
|
||||
struct GraphicsBindGroups{
|
||||
camera:wgpu::BindGroup,
|
||||
skybox_texture:wgpu::BindGroup,
|
||||
}
|
||||
|
||||
struct GraphicsPipelines{
|
||||
skybox:wgpu::RenderPipeline,
|
||||
model:wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
struct GraphicsCamera{
|
||||
screen_size:glam::UVec2,
|
||||
fov:glam::Vec2,//slope
|
||||
//camera angles and such are extrapolated and passed in every time
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn perspective_rh(fov_x_slope:f32,fov_y_slope:f32,z_near:f32,z_far:f32)->glam::Mat4{
|
||||
//glam_assert!(z_near > 0.0 && z_far > 0.0);
|
||||
let r=z_far/(z_near-z_far);
|
||||
glam::Mat4::from_cols(
|
||||
glam::Vec4::new(1.0/fov_x_slope,0.0,0.0,0.0),
|
||||
glam::Vec4::new(0.0,1.0/fov_y_slope,0.0,0.0),
|
||||
glam::Vec4::new(0.0,0.0,r,-1.0),
|
||||
glam::Vec4::new(0.0,0.0,r*z_near,0.0),
|
||||
)
|
||||
}
|
||||
impl GraphicsCamera{
|
||||
pub fn proj(&self)->glam::Mat4{
|
||||
perspective_rh(self.fov.x,self.fov.y,0.4,4000.0)
|
||||
}
|
||||
pub fn world(&self,pos:glam::Vec3,angles:glam::Vec2)->glam::Mat4{
|
||||
//f32 good enough for view matrix
|
||||
glam::Mat4::from_translation(pos)*glam::Mat4::from_euler(glam::EulerRot::YXZ,angles.x,angles.y,0f32)
|
||||
}
|
||||
|
||||
pub fn to_uniform_data(&self,pos:glam::Vec3,angles:glam::Vec2)->[f32;16*4]{
|
||||
let proj=self.proj();
|
||||
let proj_inv=proj.inverse();
|
||||
let view_inv=self.world(pos,angles);
|
||||
let view=view_inv.inverse();
|
||||
|
||||
let mut raw=[0f32; 16 * 4];
|
||||
raw[..16].copy_from_slice(&AsRef::<[f32; 16]>::as_ref(&proj)[..]);
|
||||
raw[16..32].copy_from_slice(&AsRef::<[f32; 16]>::as_ref(&proj_inv)[..]);
|
||||
raw[32..48].copy_from_slice(&AsRef::<[f32; 16]>::as_ref(&view)[..]);
|
||||
raw[48..64].copy_from_slice(&AsRef::<[f32; 16]>::as_ref(&view_inv)[..]);
|
||||
raw
|
||||
}
|
||||
}
|
||||
impl std::default::Default for GraphicsCamera{
|
||||
fn default()->Self{
|
||||
Self{
|
||||
screen_size:glam::UVec2::ONE,
|
||||
fov:glam::Vec2::ONE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GraphicsState{
|
||||
pipelines:GraphicsPipelines,
|
||||
bind_groups:GraphicsBindGroups,
|
||||
bind_group_layouts:GraphicsBindGroupLayouts,
|
||||
samplers:GraphicsSamplers,
|
||||
camera:GraphicsCamera,
|
||||
camera_buf:wgpu::Buffer,
|
||||
temp_squid_texture_view:wgpu::TextureView,
|
||||
models:Vec<GraphicsModel>,
|
||||
depth_view:wgpu::TextureView,
|
||||
staging_belt:wgpu::util::StagingBelt,
|
||||
}
|
||||
|
||||
impl GraphicsState{
|
||||
const DEPTH_FORMAT:wgpu::TextureFormat=wgpu::TextureFormat::Depth24Plus;
|
||||
fn create_depth_texture(
|
||||
config:&wgpu::SurfaceConfiguration,
|
||||
device:&wgpu::Device,
|
||||
)->wgpu::TextureView{
|
||||
let depth_texture=device.create_texture(&wgpu::TextureDescriptor{
|
||||
size:wgpu::Extent3d{
|
||||
width:config.width,
|
||||
height:config.height,
|
||||
depth_or_array_layers:1,
|
||||
},
|
||||
mip_level_count:1,
|
||||
sample_count:1,
|
||||
dimension:wgpu::TextureDimension::D2,
|
||||
format:Self::DEPTH_FORMAT,
|
||||
usage:wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
label:None,
|
||||
view_formats:&[],
|
||||
});
|
||||
|
||||
depth_texture.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
}
|
||||
pub fn clear(&mut self){
|
||||
self.models.clear();
|
||||
}
|
||||
pub fn load_user_settings(&mut self,user_settings:&settings::UserSettings){
|
||||
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 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);
|
||||
let image=match ddsfile::Dds::read(std::io::Cursor::new(texture_data)){
|
||||
Ok(image)=>image,
|
||||
Err(e)=>{
|
||||
println!("Error loading texture: {e}");
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
let (mut width,mut height)=(image.get_width(),image.get_height());
|
||||
|
||||
let format=match image.header10.unwrap().dxgi_format{
|
||||
ddsfile::DxgiFormat::R8G8B8A8_UNorm_sRGB=>wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
ddsfile::DxgiFormat::BC7_UNorm_sRGB =>{
|
||||
//floor(w,4),should be ceil(w,4)
|
||||
width=width/4*4;
|
||||
height=height/4*4;
|
||||
wgpu::TextureFormat::Bc7RgbaUnormSrgb
|
||||
},
|
||||
other=>{
|
||||
println!("unsupported texture format{:?}",other);
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
let size=wgpu::Extent3d{
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers:1,
|
||||
};
|
||||
|
||||
let layer_size=wgpu::Extent3d{
|
||||
depth_or_array_layers:1,
|
||||
..size
|
||||
};
|
||||
let max_mips=layer_size.max_mips(wgpu::TextureDimension::D2);
|
||||
|
||||
let texture=device.create_texture_with_data(
|
||||
queue,
|
||||
&wgpu::TextureDescriptor{
|
||||
size,
|
||||
mip_level_count:max_mips,
|
||||
sample_count:1,
|
||||
dimension:wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage:wgpu::TextureUsages::TEXTURE_BINDING|wgpu::TextureUsages::COPY_DST,
|
||||
label:Some(format!("Texture{}",texture_id.get()).as_str()),
|
||||
view_formats:&[],
|
||||
},
|
||||
wgpu::util::TextureDataOrder::LayerMajor,
|
||||
&image.data,
|
||||
);
|
||||
Some((texture_id,texture.create_view(&wgpu::TextureViewDescriptor{
|
||||
label:Some(format!("Texture{} View",texture_id.get()).as_str()),
|
||||
dimension:Some(wgpu::TextureViewDimension::D2),
|
||||
..wgpu::TextureViewDescriptor::default()
|
||||
})))
|
||||
}).collect();
|
||||
let num_textures=texture_views.len();
|
||||
|
||||
//split groups with different textures into separate models
|
||||
//the models received here are supposed to be tightly packed,i.e. no code needs to check if two models are using the same groups.
|
||||
let indexed_models_len=map.models.len();
|
||||
//models split into graphics_group.RenderConfigId
|
||||
let mut owned_mesh_id_from_mesh_id_render_config_id:HashMap<model::MeshId,HashMap<RenderConfigId,IndexedGraphicsMeshOwnedRenderConfigId>>=HashMap::new();
|
||||
let mut unique_render_config_models:Vec<IndexedGraphicsMeshOwnedRenderConfig>=Vec::with_capacity(indexed_models_len);
|
||||
for model in &map.models{
|
||||
//wow
|
||||
let instance=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(model.color),
|
||||
};
|
||||
//get or create owned mesh map
|
||||
let owned_mesh_map=owned_mesh_id_from_mesh_id_render_config_id
|
||||
.entry(model.mesh).or_insert_with(||{
|
||||
let mut owned_mesh_map=HashMap::new();
|
||||
//add mesh if renderid never before seen for this model
|
||||
//add instance
|
||||
//convert Model into GraphicsModelOwned
|
||||
//check each group, if it's using a new render config then make a new clone of the model
|
||||
if let Some(mesh)=map.meshes.get(model.mesh.get() as usize){
|
||||
for graphics_group in mesh.graphics_groups.iter(){
|
||||
//get or create owned mesh
|
||||
let owned_mesh_id=owned_mesh_map
|
||||
.entry(graphics_group.render).or_insert_with(||{
|
||||
//create
|
||||
let owned_mesh_id=IndexedGraphicsMeshOwnedRenderConfigId::new(unique_render_config_models.len() as u32);
|
||||
unique_render_config_models.push(IndexedGraphicsMeshOwnedRenderConfig{
|
||||
unique_pos:mesh.unique_pos.iter().map(|v|v.to_array().map(Into::into)).collect(),
|
||||
unique_tex:mesh.unique_tex.iter().map(|v|*v.as_ref()).collect(),
|
||||
unique_normal:mesh.unique_normal.iter().map(|v|v.to_array().map(Into::into)).collect(),
|
||||
unique_color:mesh.unique_color.iter().map(|v|*v.as_ref()).collect(),
|
||||
unique_vertices:mesh.unique_vertices.clone(),
|
||||
render_config:graphics_group.render,
|
||||
polys:model::PolygonGroup::PolygonList(model::PolygonList::new(Vec::new())),
|
||||
instances:Vec::new(),
|
||||
});
|
||||
owned_mesh_id
|
||||
});
|
||||
let owned_mesh=unique_render_config_models.get_mut(owned_mesh_id.get() as usize).unwrap();
|
||||
match &mut owned_mesh.polys{
|
||||
model::PolygonGroup::PolygonList(polygon_list)=>polygon_list.extend(
|
||||
graphics_group.groups.iter().flat_map(|polygon_group_id|{
|
||||
mesh.polygon_groups[polygon_group_id.get() as usize].polys()
|
||||
})
|
||||
.map(|vertex_id_slice|
|
||||
vertex_id_slice.to_vec()
|
||||
)
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
owned_mesh_map
|
||||
});
|
||||
for owned_mesh_id in owned_mesh_map.values(){
|
||||
let owned_mesh=unique_render_config_models.get_mut(owned_mesh_id.get() as usize).unwrap();
|
||||
let render_config=&map.render_configs[owned_mesh.render_config.get() as usize];
|
||||
if model.color.w==0.0&&render_config.texture.is_none(){
|
||||
continue;
|
||||
}
|
||||
owned_mesh.instances.push(instance.clone());
|
||||
}
|
||||
}
|
||||
//check every model to see if it's using the same (texture,color) but has few instances,if it is combine it into one model
|
||||
//1. collect unique instances of texture and color,note model id
|
||||
//2. for each model id,check if removing it from the pool decreases both the model count and instance count by more than one
|
||||
//3. transpose all models that stay in the set
|
||||
|
||||
//best plan:benchmark set_bind_group,set_vertex_buffer,set_index_buffer and draw_indexed
|
||||
//check if the estimated render performance is better by transposing multiple model instances into one model instance
|
||||
|
||||
//for now:just deduplicate single models...
|
||||
let mut deduplicated_models=Vec::with_capacity(indexed_models_len);//use indexed_models_len because the list will likely get smaller instead of bigger
|
||||
let mut unique_texture_color=HashMap::new();//texture->color->vec![(model_id,instance_id)]
|
||||
for (model_id,model) in unique_render_config_models.iter().enumerate(){
|
||||
//for now:filter out models with more than one instance
|
||||
if 1<model.instances.len(){
|
||||
continue;
|
||||
}
|
||||
//populate hashmap
|
||||
let unique_color=unique_texture_color
|
||||
.entry(model.render_config)
|
||||
.or_insert_with(||HashMap::new());
|
||||
//separate instances by color
|
||||
for (instance_id,instance) in model.instances.iter().enumerate(){
|
||||
let model_instance_list=unique_color
|
||||
.entry(instance.color)
|
||||
.or_insert_with(||Vec::new());
|
||||
//add model instance to list
|
||||
model_instance_list.push((model_id,instance_id));
|
||||
}
|
||||
}
|
||||
//populate a hashset of models selected for transposition
|
||||
//construct transposed models
|
||||
let mut selected_model_instances=HashSet::new();
|
||||
for (render_config,unique_color) in unique_texture_color.into_iter(){
|
||||
for (color,model_instance_list) in unique_color.into_iter(){
|
||||
//world transforming one model does not meet the definition of deduplicaiton
|
||||
if 1<model_instance_list.len(){
|
||||
//create model
|
||||
let mut unique_pos=Vec::new();
|
||||
let mut pos_id_from=HashMap::new();
|
||||
let mut unique_tex=Vec::new();
|
||||
let mut tex_id_from=HashMap::new();
|
||||
let mut unique_normal=Vec::new();
|
||||
let mut normal_id_from=HashMap::new();
|
||||
let mut unique_color=Vec::new();
|
||||
let mut color_id_from=HashMap::new();
|
||||
let mut unique_vertices=Vec::new();
|
||||
let mut vertex_id_from=HashMap::new();
|
||||
|
||||
let mut polys=Vec::new();
|
||||
//transform instance vertices
|
||||
for (model_id,instance_id) in model_instance_list.into_iter(){
|
||||
//populate hashset to prevent these models from being copied
|
||||
selected_model_instances.insert(model_id);
|
||||
//there is only one instance per model
|
||||
let model=&unique_render_config_models[model_id];
|
||||
let instance=&model.instances[instance_id];
|
||||
//just hash word slices LOL
|
||||
let map_pos_id:Vec<PositionId>=model.unique_pos.iter().map(|untransformed_pos|{
|
||||
let pos=instance.transform.transform_point3(glam::Vec3::from_array(untransformed_pos.clone())).to_array();
|
||||
let h=bytemuck::cast::<[f32;3],[u32;3]>(pos);
|
||||
PositionId::new(*pos_id_from.entry(h).or_insert_with(||{
|
||||
let pos_id=unique_pos.len();
|
||||
unique_pos.push(pos);
|
||||
pos_id
|
||||
}) as u32)
|
||||
}).collect();
|
||||
let map_tex_id:Vec<TextureCoordinateId>=model.unique_tex.iter().map(|&tex|{
|
||||
let h=bytemuck::cast::<[f32;2],[u32;2]>(tex);
|
||||
TextureCoordinateId::new(*tex_id_from.entry(h).or_insert_with(||{
|
||||
let tex_id=unique_tex.len();
|
||||
unique_tex.push(tex);
|
||||
tex_id
|
||||
}) as u32)
|
||||
}).collect();
|
||||
let map_normal_id:Vec<NormalId>=model.unique_normal.iter().map(|untransformed_normal|{
|
||||
let normal=(instance.normal_transform*glam::Vec3::from_array(untransformed_normal.clone())).to_array();
|
||||
let h=bytemuck::cast::<[f32;3],[u32;3]>(normal);
|
||||
NormalId::new(*normal_id_from.entry(h).or_insert_with(||{
|
||||
let normal_id=unique_normal.len();
|
||||
unique_normal.push(normal);
|
||||
normal_id
|
||||
}) as u32)
|
||||
}).collect();
|
||||
let map_color_id:Vec<ColorId>=model.unique_color.iter().map(|&color|{
|
||||
let h=bytemuck::cast::<[f32;4],[u32;4]>(color);
|
||||
ColorId::new(*color_id_from.entry(h).or_insert_with(||{
|
||||
let color_id=unique_color.len();
|
||||
unique_color.push(color);
|
||||
color_id
|
||||
}) as u32)
|
||||
}).collect();
|
||||
//map the indexed vertices onto new indices
|
||||
//creating the vertex map is slightly different because the vertices are directly hashable
|
||||
let map_vertex_id:Vec<VertexId>=model.unique_vertices.iter().map(|unmapped_vertex|{
|
||||
let vertex=model::IndexedVertex{
|
||||
pos:map_pos_id[unmapped_vertex.pos.get() as usize],
|
||||
tex:map_tex_id[unmapped_vertex.tex.get() as usize],
|
||||
normal:map_normal_id[unmapped_vertex.normal.get() as usize],
|
||||
color:map_color_id[unmapped_vertex.color.get() as usize],
|
||||
};
|
||||
VertexId::new(*vertex_id_from.entry(vertex.clone()).or_insert_with(||{
|
||||
let vertex_id=unique_vertices.len();
|
||||
unique_vertices.push(vertex);
|
||||
vertex_id
|
||||
}) as u32)
|
||||
}).collect();
|
||||
polys.extend(model.polys.polys().map(|poly|
|
||||
poly.iter().map(|vertex_id|
|
||||
map_vertex_id[vertex_id.get() as usize]
|
||||
).collect()
|
||||
));
|
||||
}
|
||||
//push model into dedup
|
||||
deduplicated_models.push(IndexedGraphicsMeshOwnedRenderConfig{
|
||||
unique_pos,
|
||||
unique_tex,
|
||||
unique_normal,
|
||||
unique_color,
|
||||
unique_vertices,
|
||||
render_config,
|
||||
polys:model::PolygonGroup::PolygonList(model::PolygonList::new(polys)),
|
||||
instances:vec![GraphicsModelOwned{
|
||||
transform:glam::Mat4::IDENTITY,
|
||||
normal_transform:glam::Mat3::IDENTITY,
|
||||
color
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
//fill untouched models
|
||||
for (model_id,model) in unique_render_config_models.into_iter().enumerate(){
|
||||
if !selected_model_instances.contains(&model_id){
|
||||
deduplicated_models.push(model);
|
||||
}
|
||||
}
|
||||
|
||||
//de-index models
|
||||
let deduplicated_models_len=deduplicated_models.len();
|
||||
let models:Vec<GraphicsMeshOwnedRenderConfig>=deduplicated_models.into_iter().map(|model|{
|
||||
let mut vertices=Vec::new();
|
||||
let mut index_from_vertex=HashMap::new();//::<IndexedVertex,usize>
|
||||
//this mut be combined in a more complex way if the models use different render patterns per group
|
||||
let mut indices=Vec::new();
|
||||
for poly in model.polys.polys(){
|
||||
let mut poly_vertices=poly.iter()
|
||||
.map(|&vertex_index|*index_from_vertex.entry(vertex_index).or_insert_with(||{
|
||||
let i=vertices.len();
|
||||
let vertex=&model.unique_vertices[vertex_index.get() as usize];
|
||||
vertices.push(GraphicsVertex{
|
||||
pos:model.unique_pos[vertex.pos.get() as usize],
|
||||
tex:model.unique_tex[vertex.tex.get() as usize],
|
||||
normal:model.unique_normal[vertex.normal.get() as usize],
|
||||
color:model.unique_color[vertex.color.get() as usize],
|
||||
});
|
||||
i
|
||||
}));
|
||||
|
||||
let a=poly_vertices.next().unwrap();
|
||||
let mut b=poly_vertices.next().unwrap();
|
||||
|
||||
poly_vertices.for_each(|c|{
|
||||
indices.extend([a,b,c]);
|
||||
b=c;
|
||||
});
|
||||
}
|
||||
GraphicsMeshOwnedRenderConfig{
|
||||
instances:model.instances,
|
||||
indices:if (u32::MAX as usize)<vertices.len(){
|
||||
panic!("Model has too many vertices!")
|
||||
}else if (u16::MAX as usize)<vertices.len(){
|
||||
model_graphics::Indices::U32(indices.into_iter().map(|vertex_idx|vertex_idx as u32).collect())
|
||||
}else{
|
||||
model_graphics::Indices::U16(indices.into_iter().map(|vertex_idx|vertex_idx as u16).collect())
|
||||
},
|
||||
vertices,
|
||||
render_config:model.render_config,
|
||||
}
|
||||
}).collect();
|
||||
//.into_iter() the modeldata vec so entities can be /moved/ to models.entities
|
||||
let mut model_count=0;
|
||||
let mut instance_count=0;
|
||||
let uniform_buffer_binding_size=required_limits().max_uniform_buffer_binding_size as usize;
|
||||
let chunk_size=uniform_buffer_binding_size/MODEL_BUFFER_SIZE_BYTES;
|
||||
self.models.reserve(models.len());
|
||||
for model in models.into_iter(){
|
||||
instance_count+=model.instances.len();
|
||||
for instances_chunk in model.instances.rchunks(chunk_size){
|
||||
model_count+=1;
|
||||
let mut model_uniforms=get_instances_buffer_data(instances_chunk);
|
||||
//TEMP: fill with zeroes to pass validation
|
||||
model_uniforms.resize(MODEL_BUFFER_SIZE*512,0.0f32);
|
||||
let model_buf=device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
|
||||
label:Some(format!("Model{} Buf",model_count).as_str()),
|
||||
contents:bytemuck::cast_slice(&model_uniforms),
|
||||
usage:wgpu::BufferUsages::UNIFORM|wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
let render_config=&map.render_configs[model.render_config.get() as usize];
|
||||
let texture_view=render_config.texture.and_then(|texture_id|
|
||||
texture_views.get(&texture_id)
|
||||
).unwrap_or(&self.temp_squid_texture_view);
|
||||
let bind_group=device.create_bind_group(&wgpu::BindGroupDescriptor{
|
||||
layout:&self.bind_group_layouts.model,
|
||||
entries:&[
|
||||
wgpu::BindGroupEntry{
|
||||
binding:0,
|
||||
resource:model_buf.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry{
|
||||
binding:1,
|
||||
resource:wgpu::BindingResource::TextureView(texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry{
|
||||
binding:2,
|
||||
resource:wgpu::BindingResource::Sampler(&self.samplers.repeat),
|
||||
},
|
||||
],
|
||||
label:Some(format!("Model{} Bind Group",model_count).as_str()),
|
||||
});
|
||||
let vertex_buf=device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
|
||||
label:Some("Vertex"),
|
||||
contents:bytemuck::cast_slice(&model.vertices),
|
||||
usage:wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
//all of these are being moved here
|
||||
self.models.push(GraphicsModel{
|
||||
instance_count:instances_chunk.len() as u32,
|
||||
vertex_buf,
|
||||
indices:match &model.indices{
|
||||
model_graphics::Indices::U32(indices)=>Indices::new(device,indices,wgpu::IndexFormat::Uint32),
|
||||
model_graphics::Indices::U16(indices)=>Indices::new(device,indices,wgpu::IndexFormat::Uint16),
|
||||
},
|
||||
bind_group,
|
||||
});
|
||||
}
|
||||
}
|
||||
println!("Texture References={}",num_textures);
|
||||
println!("Textures Loaded={}",texture_views.len());
|
||||
println!("Indexed Models={}",indexed_models_len);
|
||||
println!("Deduplicated Models={}",deduplicated_models_len);
|
||||
println!("Graphics Objects:{}",self.models.len());
|
||||
println!("Graphics Instances:{}",instance_count);
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
device:&wgpu::Device,
|
||||
queue:&wgpu::Queue,
|
||||
config:&wgpu::SurfaceConfiguration,
|
||||
)->Self{
|
||||
let camera_bind_group_layout=device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor{
|
||||
label:None,
|
||||
entries:&[
|
||||
wgpu::BindGroupLayoutEntry{
|
||||
binding:0,
|
||||
visibility:wgpu::ShaderStages::VERTEX,
|
||||
ty:wgpu::BindingType::Buffer{
|
||||
ty:wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset:false,
|
||||
min_binding_size:None,
|
||||
},
|
||||
count:None,
|
||||
},
|
||||
],
|
||||
});
|
||||
let skybox_texture_bind_group_layout=device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor{
|
||||
label:Some("Skybox Texture Bind Group Layout"),
|
||||
entries:&[
|
||||
wgpu::BindGroupLayoutEntry{
|
||||
binding:0,
|
||||
visibility:wgpu::ShaderStages::FRAGMENT,
|
||||
ty:wgpu::BindingType::Texture{
|
||||
sample_type:wgpu::TextureSampleType::Float{filterable:true},
|
||||
multisampled:false,
|
||||
view_dimension:wgpu::TextureViewDimension::Cube,
|
||||
},
|
||||
count:None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry{
|
||||
binding:1,
|
||||
visibility:wgpu::ShaderStages::FRAGMENT,
|
||||
ty:wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count:None,
|
||||
},
|
||||
],
|
||||
});
|
||||
let model_bind_group_layout=device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor{
|
||||
label:Some("Model Bind Group Layout"),
|
||||
entries:&[
|
||||
wgpu::BindGroupLayoutEntry{
|
||||
binding:0,
|
||||
visibility:wgpu::ShaderStages::VERTEX,
|
||||
ty:wgpu::BindingType::Buffer{
|
||||
ty:wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset:false,
|
||||
min_binding_size:None,
|
||||
},
|
||||
count:None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry{
|
||||
binding:1,
|
||||
visibility:wgpu::ShaderStages::FRAGMENT,
|
||||
ty:wgpu::BindingType::Texture{
|
||||
sample_type:wgpu::TextureSampleType::Float{filterable:true},
|
||||
multisampled:false,
|
||||
view_dimension:wgpu::TextureViewDimension::D2,
|
||||
},
|
||||
count:None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry{
|
||||
binding:2,
|
||||
visibility:wgpu::ShaderStages::FRAGMENT,
|
||||
ty:wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count:None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let clamp_sampler=device.create_sampler(&wgpu::SamplerDescriptor{
|
||||
label:Some("Clamp 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::Linear,
|
||||
..Default::default()
|
||||
});
|
||||
let repeat_sampler=device.create_sampler(&wgpu::SamplerDescriptor{
|
||||
label:Some("Repeat Sampler"),
|
||||
address_mode_u:wgpu::AddressMode::Repeat,
|
||||
address_mode_v:wgpu::AddressMode::Repeat,
|
||||
address_mode_w:wgpu::AddressMode::Repeat,
|
||||
mag_filter:wgpu::FilterMode::Linear,
|
||||
min_filter:wgpu::FilterMode::Linear,
|
||||
mipmap_filter:wgpu::FilterMode::Linear,
|
||||
anisotropy_clamp:16,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Create the render pipeline
|
||||
let shader=device.create_shader_module(wgpu::ShaderModuleDescriptor{
|
||||
label:None,
|
||||
source:wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("../../../strafe-client/src/shader.wgsl"))),
|
||||
});
|
||||
|
||||
//load textures
|
||||
let device_features=device.features();
|
||||
|
||||
let skybox_texture_view={
|
||||
let skybox_format=if device_features.contains(wgpu::Features::TEXTURE_COMPRESSION_ASTC){
|
||||
println!("Using ASTC");
|
||||
wgpu::TextureFormat::Astc{
|
||||
block:AstcBlock::B4x4,
|
||||
channel:AstcChannel::UnormSrgb,
|
||||
}
|
||||
}else if device_features.contains(wgpu::Features::TEXTURE_COMPRESSION_ETC2){
|
||||
println!("Using ETC2");
|
||||
wgpu::TextureFormat::Etc2Rgb8UnormSrgb
|
||||
}else if device_features.contains(wgpu::Features::TEXTURE_COMPRESSION_BC){
|
||||
println!("Using BC");
|
||||
wgpu::TextureFormat::Bc1RgbaUnormSrgb
|
||||
}else{
|
||||
println!("Using plain");
|
||||
wgpu::TextureFormat::Bgra8UnormSrgb
|
||||
};
|
||||
|
||||
let bytes=match skybox_format{
|
||||
wgpu::TextureFormat::Astc{
|
||||
block:AstcBlock::B4x4,
|
||||
channel:AstcChannel::UnormSrgb,
|
||||
}=>&include_bytes!("../../../strafe-client/images/astc.dds")[..],
|
||||
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../../../strafe-client/images/etc2.dds")[..],
|
||||
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../../../strafe-client/images/bc1.dds")[..],
|
||||
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../../../strafe-client/images/bgra.dds")[..],
|
||||
_=>unreachable!(),
|
||||
};
|
||||
|
||||
let skybox_image=ddsfile::Dds::read(&mut std::io::Cursor::new(bytes)).unwrap();
|
||||
|
||||
let size=wgpu::Extent3d{
|
||||
width:skybox_image.get_width(),
|
||||
height:skybox_image.get_height(),
|
||||
depth_or_array_layers:6,
|
||||
};
|
||||
|
||||
let layer_size=wgpu::Extent3d{
|
||||
depth_or_array_layers:1,
|
||||
..size
|
||||
};
|
||||
let max_mips=layer_size.max_mips(wgpu::TextureDimension::D2);
|
||||
|
||||
let skybox_texture=device.create_texture_with_data(
|
||||
queue,
|
||||
&wgpu::TextureDescriptor{
|
||||
size,
|
||||
mip_level_count:max_mips,
|
||||
sample_count:1,
|
||||
dimension:wgpu::TextureDimension::D2,
|
||||
format:skybox_format,
|
||||
usage:wgpu::TextureUsages::TEXTURE_BINDING|wgpu::TextureUsages::COPY_DST,
|
||||
label:Some("Skybox Texture"),
|
||||
view_formats:&[],
|
||||
},
|
||||
wgpu::util::TextureDataOrder::LayerMajor,
|
||||
&skybox_image.data,
|
||||
);
|
||||
|
||||
skybox_texture.create_view(&wgpu::TextureViewDescriptor{
|
||||
label:Some("Skybox Texture View"),
|
||||
dimension:Some(wgpu::TextureViewDimension::Cube),
|
||||
..wgpu::TextureViewDescriptor::default()
|
||||
})
|
||||
};
|
||||
|
||||
//squid
|
||||
let squid_texture_view={
|
||||
let bytes=include_bytes!("../../../strafe-client/images/squid.dds");
|
||||
|
||||
let image=ddsfile::Dds::read(&mut std::io::Cursor::new(bytes)).unwrap();
|
||||
|
||||
let size=wgpu::Extent3d{
|
||||
width:image.get_width(),
|
||||
height:image.get_height(),
|
||||
depth_or_array_layers:1,
|
||||
};
|
||||
|
||||
let layer_size=wgpu::Extent3d{
|
||||
depth_or_array_layers:1,
|
||||
..size
|
||||
};
|
||||
let max_mips=layer_size.max_mips(wgpu::TextureDimension::D2);
|
||||
|
||||
let texture=device.create_texture_with_data(
|
||||
queue,
|
||||
&wgpu::TextureDescriptor{
|
||||
size,
|
||||
mip_level_count:max_mips,
|
||||
sample_count:1,
|
||||
dimension:wgpu::TextureDimension::D2,
|
||||
format:wgpu::TextureFormat::Bc7RgbaUnorm,
|
||||
usage:wgpu::TextureUsages::TEXTURE_BINDING|wgpu::TextureUsages::COPY_DST,
|
||||
label:Some("Squid Texture"),
|
||||
view_formats:&[],
|
||||
},
|
||||
wgpu::util::TextureDataOrder::LayerMajor,
|
||||
&image.data,
|
||||
);
|
||||
|
||||
texture.create_view(&wgpu::TextureViewDescriptor{
|
||||
label:Some("Squid Texture View"),
|
||||
dimension:Some(wgpu::TextureViewDimension::D2),
|
||||
..wgpu::TextureViewDescriptor::default()
|
||||
})
|
||||
};
|
||||
|
||||
let model_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{
|
||||
label:None,
|
||||
bind_group_layouts:&[
|
||||
&camera_bind_group_layout,
|
||||
&skybox_texture_bind_group_layout,
|
||||
&model_bind_group_layout,
|
||||
],
|
||||
push_constant_ranges:&[],
|
||||
});
|
||||
let sky_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{
|
||||
label:None,
|
||||
bind_group_layouts:&[
|
||||
&camera_bind_group_layout,
|
||||
&skybox_texture_bind_group_layout,
|
||||
],
|
||||
push_constant_ranges:&[],
|
||||
});
|
||||
|
||||
// Create the render pipelines
|
||||
let sky_pipeline=device.create_render_pipeline(&wgpu::RenderPipelineDescriptor{
|
||||
label:Some("Sky Pipeline"),
|
||||
layout:Some(&sky_pipeline_layout),
|
||||
vertex:wgpu::VertexState{
|
||||
module:&shader,
|
||||
entry_point:Some("vs_sky"),
|
||||
buffers:&[],
|
||||
compilation_options:wgpu::PipelineCompilationOptions::default(),
|
||||
},
|
||||
fragment:Some(wgpu::FragmentState{
|
||||
module:&shader,
|
||||
entry_point:Some("fs_sky"),
|
||||
targets:&[Some(config.view_formats[0].into())],
|
||||
compilation_options:wgpu::PipelineCompilationOptions::default(),
|
||||
}),
|
||||
primitive:wgpu::PrimitiveState{
|
||||
front_face:wgpu::FrontFace::Cw,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil:Some(wgpu::DepthStencilState{
|
||||
format:Self::DEPTH_FORMAT,
|
||||
depth_write_enabled:false,
|
||||
depth_compare:wgpu::CompareFunction::LessEqual,
|
||||
stencil:wgpu::StencilState::default(),
|
||||
bias:wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample:wgpu::MultisampleState::default(),
|
||||
multiview:None,
|
||||
cache:None,
|
||||
});
|
||||
let model_pipeline=device.create_render_pipeline(&wgpu::RenderPipelineDescriptor{
|
||||
label:Some("Model Pipeline"),
|
||||
layout:Some(&model_pipeline_layout),
|
||||
vertex:wgpu::VertexState{
|
||||
module:&shader,
|
||||
entry_point:Some("vs_entity_texture"),
|
||||
buffers:&[wgpu::VertexBufferLayout{
|
||||
array_stride:std::mem::size_of::<GraphicsVertex>() as wgpu::BufferAddress,
|
||||
step_mode:wgpu::VertexStepMode::Vertex,
|
||||
attributes:&wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Float32x3,3=>Float32x4],
|
||||
}],
|
||||
compilation_options:wgpu::PipelineCompilationOptions::default(),
|
||||
},
|
||||
fragment:Some(wgpu::FragmentState{
|
||||
module:&shader,
|
||||
entry_point:Some("fs_entity_texture"),
|
||||
targets:&[Some(config.view_formats[0].into())],
|
||||
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::LessEqual,
|
||||
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);
|
||||
let camera_buf=device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
|
||||
label:Some("Camera"),
|
||||
contents:bytemuck::cast_slice(&camera_uniforms),
|
||||
usage:wgpu::BufferUsages::UNIFORM|wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
let camera_bind_group=device.create_bind_group(&wgpu::BindGroupDescriptor{
|
||||
layout:&camera_bind_group_layout,
|
||||
entries:&[
|
||||
wgpu::BindGroupEntry{
|
||||
binding:0,
|
||||
resource:camera_buf.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label:Some("Camera"),
|
||||
});
|
||||
|
||||
let skybox_texture_bind_group=device.create_bind_group(&wgpu::BindGroupDescriptor{
|
||||
layout:&skybox_texture_bind_group_layout,
|
||||
entries:&[
|
||||
wgpu::BindGroupEntry{
|
||||
binding:0,
|
||||
resource:wgpu::BindingResource::TextureView(&skybox_texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry{
|
||||
binding:1,
|
||||
resource:wgpu::BindingResource::Sampler(&clamp_sampler),
|
||||
},
|
||||
],
|
||||
label:Some("Sky Texture"),
|
||||
});
|
||||
|
||||
let depth_view=Self::create_depth_texture(config,device);
|
||||
|
||||
Self{
|
||||
pipelines:GraphicsPipelines{
|
||||
skybox:sky_pipeline,
|
||||
model:model_pipeline
|
||||
},
|
||||
bind_groups:GraphicsBindGroups{
|
||||
camera:camera_bind_group,
|
||||
skybox_texture:skybox_texture_bind_group,
|
||||
},
|
||||
camera,
|
||||
camera_buf,
|
||||
models:Vec::new(),
|
||||
depth_view,
|
||||
staging_belt:wgpu::util::StagingBelt::new(0x100),
|
||||
bind_group_layouts:GraphicsBindGroupLayouts{model:model_bind_group_layout},
|
||||
samplers:GraphicsSamplers{repeat:repeat_sampler},
|
||||
temp_squid_texture_view:squid_texture_view,
|
||||
}
|
||||
}
|
||||
pub fn resize(
|
||||
&mut self,
|
||||
device:&wgpu::Device,
|
||||
config:&wgpu::SurfaceConfiguration,
|
||||
user_settings:&settings::UserSettings,
|
||||
){
|
||||
self.depth_view=Self::create_depth_texture(config,device);
|
||||
self.camera.screen_size=glam::uvec2(config.width,config.height);
|
||||
self.load_user_settings(user_settings);
|
||||
}
|
||||
pub fn render(
|
||||
&mut self,
|
||||
view:&wgpu::TextureView,
|
||||
device:&wgpu::Device,
|
||||
queue:&wgpu::Queue,
|
||||
frame_state:session::FrameState,
|
||||
){
|
||||
//TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input
|
||||
|
||||
let mut encoder=device.create_command_encoder(&wgpu::CommandEncoderDescriptor{label:None});
|
||||
|
||||
// update rotation
|
||||
let camera_uniforms=self.camera.to_uniform_data(
|
||||
frame_state.body.extrapolated_position(frame_state.time).map(Into::<f32>::into).to_array().into(),
|
||||
frame_state.camera.simulate_move_angles(glam::IVec2::ZERO)
|
||||
);
|
||||
self.staging_belt
|
||||
.write_buffer(
|
||||
&mut encoder,
|
||||
&self.camera_buf,
|
||||
0,
|
||||
wgpu::BufferSize::new((camera_uniforms.len() * 4) as wgpu::BufferAddress).unwrap(),
|
||||
device,
|
||||
)
|
||||
.copy_from_slice(bytemuck::cast_slice(&camera_uniforms));
|
||||
//This code only needs to run when the uniforms change
|
||||
/*
|
||||
for model in self.models.iter(){
|
||||
let model_uniforms=get_instances_buffer_data(&model.instances);
|
||||
self.staging_belt
|
||||
.write_buffer(
|
||||
&mut encoder,
|
||||
&model.model_buf,//description of where data will be written when command is executed
|
||||
0,//offset in staging belt?
|
||||
wgpu::BufferSize::new((model_uniforms.len() * 4) as wgpu::BufferAddress).unwrap(),
|
||||
device,
|
||||
)
|
||||
.copy_from_slice(bytemuck::cast_slice(&model_uniforms));
|
||||
}
|
||||
*/
|
||||
self.staging_belt.finish();
|
||||
|
||||
{
|
||||
let mut rpass=encoder.begin_render_pass(&wgpu::RenderPassDescriptor{
|
||||
label:None,
|
||||
color_attachments:&[Some(wgpu::RenderPassColorAttachment{
|
||||
view,
|
||||
resolve_target:None,
|
||||
ops:wgpu::Operations{
|
||||
load:wgpu::LoadOp::Clear(wgpu::Color{
|
||||
r:0.1,
|
||||
g:0.2,
|
||||
b:0.3,
|
||||
a:1.0,
|
||||
}),
|
||||
store:wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment:Some(wgpu::RenderPassDepthStencilAttachment{
|
||||
view:&self.depth_view,
|
||||
depth_ops:Some(wgpu::Operations{
|
||||
load:wgpu::LoadOp::Clear(1.0),
|
||||
store:wgpu::StoreOp::Discard,
|
||||
}),
|
||||
stencil_ops:None,
|
||||
}),
|
||||
timestamp_writes:Default::default(),
|
||||
occlusion_query_set:Default::default(),
|
||||
});
|
||||
|
||||
rpass.set_bind_group(0,&self.bind_groups.camera,&[]);
|
||||
rpass.set_bind_group(1,&self.bind_groups.skybox_texture,&[]);
|
||||
|
||||
rpass.set_pipeline(&self.pipelines.model);
|
||||
for model in &self.models{
|
||||
rpass.set_bind_group(2,&model.bind_group,&[]);
|
||||
rpass.set_vertex_buffer(0,model.vertex_buf.slice(..));
|
||||
rpass.set_index_buffer(model.indices.buf.slice(..),model.indices.format);
|
||||
//TODO: loop over triangle strips
|
||||
rpass.draw_indexed(0..model.indices.count,0,0..model.instance_count);
|
||||
}
|
||||
|
||||
rpass.set_pipeline(&self.pipelines.skybox);
|
||||
rpass.draw(0..3,0..1);
|
||||
}
|
||||
|
||||
queue.submit(std::iter::once(encoder.finish()));
|
||||
|
||||
self.staging_belt.recall();
|
||||
}
|
||||
}
|
||||
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;
|
||||
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
|
||||
}
|
||||
2
engine/graphics/src/lib.rs
Normal file
2
engine/graphics/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod model;
|
||||
pub mod graphics;
|
||||
48
engine/graphics/src/model.rs
Normal file
48
engine/graphics/src/model.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use bytemuck::{Pod,Zeroable};
|
||||
use strafesnet_common::model::{IndexedVertex,PolygonGroup,RenderConfigId};
|
||||
#[derive(Clone,Copy,Pod,Zeroable)]
|
||||
#[repr(C)]
|
||||
pub struct GraphicsVertex{
|
||||
pub pos:[f32;3],
|
||||
pub tex:[f32;2],
|
||||
pub normal:[f32;3],
|
||||
pub color:[f32;4],
|
||||
}
|
||||
#[derive(Clone,Copy,id::Id)]
|
||||
pub struct IndexedGraphicsMeshOwnedRenderConfigId(u32);
|
||||
pub struct IndexedGraphicsMeshOwnedRenderConfig{
|
||||
pub unique_pos:Vec<[f32;3]>,
|
||||
pub unique_tex:Vec<[f32;2]>,
|
||||
pub unique_normal:Vec<[f32;3]>,
|
||||
pub unique_color:Vec<[f32;4]>,
|
||||
pub unique_vertices:Vec<IndexedVertex>,
|
||||
pub render_config:RenderConfigId,
|
||||
pub polys:PolygonGroup,
|
||||
pub instances:Vec<GraphicsModelOwned>,
|
||||
}
|
||||
pub enum Indices{
|
||||
U32(Vec<u32>),
|
||||
U16(Vec<u16>),
|
||||
}
|
||||
pub struct GraphicsMeshOwnedRenderConfig{
|
||||
pub vertices:Vec<GraphicsVertex>,
|
||||
pub indices:Indices,
|
||||
pub render_config:RenderConfigId,
|
||||
pub instances:Vec<GraphicsModelOwned>,
|
||||
}
|
||||
#[derive(Clone,Copy,PartialEq,id::Id)]
|
||||
pub struct GraphicsModelColor4(glam::Vec4);
|
||||
impl std::hash::Hash for GraphicsModelColor4{
|
||||
fn hash<H:std::hash::Hasher>(&self,state:&mut H) {
|
||||
for &f in self.0.as_ref(){
|
||||
bytemuck::cast::<f32,u32>(f).hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Eq for GraphicsModelColor4{}
|
||||
#[derive(Clone)]
|
||||
pub struct GraphicsModelOwned{
|
||||
pub transform:glam::Mat4,
|
||||
pub normal_transform:glam::Mat3,
|
||||
pub color:GraphicsModelColor4,
|
||||
}
|
||||
10
engine/physics/Cargo.toml
Normal file
10
engine/physics/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "strafesnet_physics"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
arrayvec = "0.7.6"
|
||||
glam = "0.29.0"
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
||||
8
engine/physics/LICENSE
Normal file
8
engine/physics/LICENSE
Normal file
@@ -0,0 +1,8 @@
|
||||
/*******************************************************
|
||||
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
|
||||
*
|
||||
* This file is part of the StrafesNET bhop/surf client.
|
||||
*
|
||||
* StrafesNET can not be copied and/or distributed
|
||||
* without the express permission of Rhys Lloyd
|
||||
*******************************************************/
|
||||
160
engine/physics/src/body.rs
Normal file
160
engine/physics/src/body.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use strafesnet_common::aabb;
|
||||
use strafesnet_common::integer::{self,vec3,Time,Planar64,Planar64Vec3};
|
||||
#[derive(Clone,Copy,Debug,Hash)]
|
||||
pub struct Body<T>{
|
||||
pub position:Planar64Vec3,//I64 where 2^32 = 1 u
|
||||
pub velocity:Planar64Vec3,//I64 where 2^32 = 1 u/s
|
||||
pub acceleration:Planar64Vec3,//I64 where 2^32 = 1 u/s/s
|
||||
pub time:Time<T>,//nanoseconds x xxxxD!
|
||||
}
|
||||
impl<T> std::ops::Neg for Body<T>{
|
||||
type Output=Self;
|
||||
fn neg(self)->Self::Output{
|
||||
Self{
|
||||
position:self.position,
|
||||
velocity:-self.velocity,
|
||||
acceleration:self.acceleration,
|
||||
time:-self.time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Body<T>
|
||||
where Time<T>:Copy,
|
||||
{
|
||||
pub const ZERO:Self=Self::new(vec3::ZERO,vec3::ZERO,vec3::ZERO,Time::ZERO);
|
||||
pub const fn new(position:Planar64Vec3,velocity:Planar64Vec3,acceleration:Planar64Vec3,time:Time<T>)->Self{
|
||||
Self{
|
||||
position,
|
||||
velocity,
|
||||
acceleration,
|
||||
time,
|
||||
}
|
||||
}
|
||||
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
|
||||
let dt=time-self.time;
|
||||
self.position
|
||||
+(self.velocity*dt).map(|elem|elem.divide().fix_1())
|
||||
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().fix_1())
|
||||
}
|
||||
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
|
||||
let dt=time-self.time;
|
||||
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().fix_1())
|
||||
}
|
||||
pub fn advance_time(&mut self,time:Time<T>){
|
||||
self.position=self.extrapolated_position(time);
|
||||
self.velocity=self.extrapolated_velocity(time);
|
||||
self.time=time;
|
||||
}
|
||||
pub fn extrapolated_position_ratio_dt<Num,Den,N1,D1,N2,N3,D2,N4,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
|
||||
where
|
||||
// Why?
|
||||
// All of this can be removed with const generics because the type can be specified as
|
||||
// Ratio<Fixed<N,NF>,Fixed<D,DF>>
|
||||
// which is known to implement all the necessary traits
|
||||
Num:Copy,
|
||||
Den:Copy+core::ops::Mul<i64,Output=D1>,
|
||||
D1:Copy,
|
||||
Num:core::ops::Mul<Planar64,Output=N1>,
|
||||
Planar64:core::ops::Mul<D1,Output=N2>,
|
||||
N1:core::ops::Add<N2,Output=N3>,
|
||||
Num:core::ops::Mul<N3,Output=N4>,
|
||||
Den:core::ops::Mul<D1,Output=D2>,
|
||||
D2:Copy,
|
||||
Planar64:core::ops::Mul<D2,Output=N4>,
|
||||
N4:integer::Divide<D2,Output=T1>,
|
||||
T1:integer::Fix<Planar64>,
|
||||
{
|
||||
// a*dt^2/2 + v*dt + p
|
||||
// (a*dt/2+v)*dt+p
|
||||
(self.acceleration.map(|elem|dt*elem/2)+self.velocity).map(|elem|dt.mul_ratio(elem))
|
||||
.map(|elem|elem.divide().fix())+self.position
|
||||
}
|
||||
pub fn extrapolated_velocity_ratio_dt<Num,Den,N1,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
|
||||
where
|
||||
Num:Copy,
|
||||
Den:Copy,
|
||||
Num:core::ops::Mul<Planar64,Output=N1>,
|
||||
Planar64:core::ops::Mul<Den,Output=N1>,
|
||||
N1:integer::Divide<Den,Output=T1>,
|
||||
T1:integer::Fix<Planar64>,
|
||||
{
|
||||
// a*dt + v
|
||||
self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity
|
||||
}
|
||||
pub fn advance_time_ratio_dt(&mut self,dt:crate::model::GigaTime){
|
||||
self.position=self.extrapolated_position_ratio_dt(dt);
|
||||
self.velocity=self.extrapolated_velocity_ratio_dt(dt);
|
||||
self.time+=dt.into();
|
||||
}
|
||||
pub fn infinity_dir(&self)->Option<Planar64Vec3>{
|
||||
if self.velocity==vec3::ZERO{
|
||||
if self.acceleration==vec3::ZERO{
|
||||
None
|
||||
}else{
|
||||
Some(self.acceleration)
|
||||
}
|
||||
}else{
|
||||
Some(self.velocity)
|
||||
}
|
||||
}
|
||||
pub fn grow_aabb(&self,aabb:&mut aabb::Aabb,t0:Time<T>,t1:Time<T>){
|
||||
aabb.grow(self.extrapolated_position(t0));
|
||||
aabb.grow(self.extrapolated_position(t1));
|
||||
//v+a*t==0
|
||||
//goober code
|
||||
if !self.acceleration.x.is_zero(){
|
||||
let t=-self.velocity.x/self.acceleration.x;
|
||||
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
|
||||
aabb.grow(self.extrapolated_position_ratio_dt(t));
|
||||
}
|
||||
}
|
||||
if !self.acceleration.y.is_zero(){
|
||||
let t=-self.velocity.y/self.acceleration.y;
|
||||
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
|
||||
aabb.grow(self.extrapolated_position_ratio_dt(t));
|
||||
}
|
||||
}
|
||||
if !self.acceleration.z.is_zero(){
|
||||
let t=-self.velocity.z/self.acceleration.z;
|
||||
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
|
||||
aabb.grow(self.extrapolated_position_ratio_dt(t));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
impl<T> std::fmt::Display for Body<T>{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"p({}) v({}) a({}) t({})",self.position,self.velocity,self.acceleration,self.time)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtualBody<'a,T>{
|
||||
body0:&'a Body<T>,
|
||||
body1:&'a Body<T>,
|
||||
}
|
||||
impl<T> VirtualBody<'_,T>
|
||||
where Time<T>:Copy,
|
||||
{
|
||||
pub const fn relative<'a>(body0:&'a Body<T>,body1:&'a Body<T>)->VirtualBody<'a,T>{
|
||||
//(p0,v0,a0,t0)
|
||||
//(p1,v1,a1,t1)
|
||||
VirtualBody{
|
||||
body0,
|
||||
body1,
|
||||
}
|
||||
}
|
||||
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
|
||||
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
|
||||
}
|
||||
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
|
||||
self.body1.extrapolated_velocity(time)-self.body0.extrapolated_velocity(time)
|
||||
}
|
||||
pub fn acceleration(&self)->Planar64Vec3{
|
||||
self.body1.acceleration-self.body0.acceleration
|
||||
}
|
||||
pub fn body(&self,time:Time<T>)->Body<T>{
|
||||
Body::new(self.extrapolated_position(time),self.extrapolated_velocity(time),self.acceleration(),time)
|
||||
}
|
||||
}
|
||||
148
engine/physics/src/face_crawler.rs
Normal file
148
engine/physics/src/face_crawler.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge};
|
||||
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
|
||||
use crate::physics::{Time,Body};
|
||||
|
||||
enum Transition<M:MeshQuery>{
|
||||
Miss,
|
||||
Next(FEV<M>,GigaTime),
|
||||
Hit(M::Face,GigaTime),
|
||||
}
|
||||
|
||||
pub enum CrawlResult<M:MeshQuery>{
|
||||
Miss(FEV<M>),
|
||||
Hit(M::Face,GigaTime),
|
||||
}
|
||||
impl<M:MeshQuery> CrawlResult<M>{
|
||||
pub fn hit(self)->Option<(M::Face,GigaTime)>{
|
||||
match self{
|
||||
CrawlResult::Miss(_)=>None,
|
||||
CrawlResult::Hit(face,time)=>Some((face,time)),
|
||||
}
|
||||
}
|
||||
pub fn miss(self)->Option<FEV<M>>{
|
||||
match self{
|
||||
CrawlResult::Miss(fev)=>Some(fev),
|
||||
CrawlResult::Hit(_,_)=>None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
where
|
||||
// This is hardcoded for MinkowskiMesh lol
|
||||
M::Face:Copy,
|
||||
M::Edge:Copy,
|
||||
M::Vert:Copy,
|
||||
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>,
|
||||
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum,
|
||||
<M as MeshQuery>::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
|
||||
{
|
||||
fn next_transition(&self,body_time:GigaTime,mesh:&M,body:&Body,mut best_time:GigaTime)->Transition<M>{
|
||||
//conflicting derivative means it crosses in the wrong direction.
|
||||
//if the transition time is equal to an already tested transition, do not replace the current best.
|
||||
let mut best_transition=Transition::Miss;
|
||||
match self{
|
||||
&FEV::Face(face_id)=>{
|
||||
//test own face collision time, ignoring roots with zero or conflicting derivative
|
||||
//n=face.normal d=face.dot
|
||||
//n.a t^2+n.v t+n.p-d==0
|
||||
let (n,d)=mesh.face_nd(face_id);
|
||||
//TODO: use higher precision d value?
|
||||
//use the mesh transform translation instead of baking it into the d value.
|
||||
for dt in Fixed::<4,128>::zeroes2((n.dot(body.position)-d)*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
|
||||
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
best_time=dt;
|
||||
best_transition=Transition::Hit(face_id,dt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
//test each edge collision time, ignoring roots with zero or conflicting derivative
|
||||
for &directed_edge_id in mesh.face_edges(face_id).iter(){
|
||||
let edge_n=mesh.directed_edge_n(directed_edge_id);
|
||||
let n=n.cross(edge_n);
|
||||
let verts=mesh.edge_verts(directed_edge_id.as_undirected());
|
||||
//WARNING: d is moved out of the *2 block because of adding two vertices!
|
||||
//WARNING: precision is swept under the rug!
|
||||
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(verts[0])+mesh.vert(verts[1]))).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_4()){
|
||||
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
best_time=dt;
|
||||
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//if none:
|
||||
},
|
||||
&FEV::Edge(edge_id)=>{
|
||||
//test each face collision time, ignoring roots with zero or conflicting derivative
|
||||
let edge_n=mesh.edge_n(edge_id);
|
||||
let edge_verts=mesh.edge_verts(edge_id);
|
||||
let delta_pos=body.position*2-(mesh.vert(edge_verts[0])+mesh.vert(edge_verts[1]));
|
||||
for (i,&edge_face_id) in mesh.edge_faces(edge_id).iter().enumerate(){
|
||||
let face_n=mesh.face_nd(edge_face_id).0;
|
||||
//edge_n gets parity from the order of edge_faces
|
||||
let n=face_n.cross(edge_n)*((i as i64)*2-1);
|
||||
//WARNING yada yada d *2
|
||||
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_4()){
|
||||
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
best_time=dt;
|
||||
best_transition=Transition::Next(FEV::Face(edge_face_id),dt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//test each vertex collision time, ignoring roots with zero or conflicting derivative
|
||||
for (i,&vert_id) in edge_verts.iter().enumerate(){
|
||||
//vertex normal gets parity from vert index
|
||||
let n=edge_n*(1-2*(i as i64));
|
||||
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
|
||||
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
|
||||
best_time=dt;
|
||||
best_transition=Transition::Next(FEV::Vert(vert_id),dt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//if none:
|
||||
},
|
||||
&FEV::Vert(vert_id)=>{
|
||||
//test each edge collision time, ignoring roots with zero or conflicting derivative
|
||||
for &directed_edge_id in mesh.vert_edges(vert_id).iter(){
|
||||
//edge is directed away from vertex, but we want the dot product to turn out negative
|
||||
let n=-mesh.directed_edge_n(directed_edge_id);
|
||||
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
|
||||
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
|
||||
best_time=dt;
|
||||
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//if none:
|
||||
},
|
||||
}
|
||||
best_transition
|
||||
}
|
||||
pub fn crawl(mut self,mesh:&M,relative_body:&Body,start_time:Time,time_limit:Time)->CrawlResult<M>{
|
||||
let mut body_time={
|
||||
let r=(start_time-relative_body.time).to_ratio();
|
||||
Ratio::new(r.num.fix_4(),r.den.fix_4())
|
||||
};
|
||||
let time_limit={
|
||||
let r=(time_limit-relative_body.time).to_ratio();
|
||||
Ratio::new(r.num.fix_4(),r.den.fix_4())
|
||||
};
|
||||
for _ in 0..20{
|
||||
match self.next_transition(body_time,mesh,relative_body,time_limit){
|
||||
Transition::Miss=>return CrawlResult::Miss(self),
|
||||
Transition::Next(next_fev,next_time)=>(self,body_time)=(next_fev,next_time),
|
||||
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),
|
||||
}
|
||||
}
|
||||
//TODO: fix all bugs
|
||||
//println!("Too many iterations! Using default behaviour instead of crashing...");
|
||||
CrawlResult::Miss(self)
|
||||
}
|
||||
}
|
||||
6
engine/physics/src/lib.rs
Normal file
6
engine/physics/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod body;
|
||||
mod push_solve;
|
||||
mod face_crawler;
|
||||
mod model;
|
||||
|
||||
pub mod physics;
|
||||
1013
engine/physics/src/model.rs
Normal file
1013
engine/physics/src/model.rs
Normal file
File diff suppressed because it is too large
Load Diff
2328
engine/physics/src/physics.rs
Normal file
2328
engine/physics/src/physics.rs
Normal file
File diff suppressed because it is too large
Load Diff
349
engine/physics/src/push_solve.rs
Normal file
349
engine/physics/src/push_solve.rs
Normal file
@@ -0,0 +1,349 @@
|
||||
use strafesnet_common::integer::{self,vec3::{self,Vector3},Fixed,Planar64,Planar64Vec3,Ratio};
|
||||
|
||||
// This algorithm is based on Lua code
|
||||
// written by Trey Reynolds in 2021
|
||||
|
||||
// EPSILON=1/2^10
|
||||
|
||||
// A stack-allocated variable-size list that holds up to 4 elements
|
||||
// Direct references are used instead of indices i0, i1, i2, i3
|
||||
type Conts<'a>=arrayvec::ArrayVec<&'a Contact,4>;
|
||||
|
||||
// hack to allow comparing ratios to zero
|
||||
const RATIO_ZERO:Ratio<Fixed<1,32>,Fixed<1,32>>=Ratio::new(Fixed::ZERO,Fixed::EPSILON);
|
||||
|
||||
struct Ray{
|
||||
origin:Planar64Vec3,
|
||||
direction:Planar64Vec3,
|
||||
}
|
||||
impl Ray{
|
||||
fn extrapolate<Num,Den,N1,T1>(&self,t:Ratio<Num,Den>)->Planar64Vec3
|
||||
where
|
||||
Num:Copy,
|
||||
Den:Copy,
|
||||
Num:core::ops::Mul<Planar64,Output=N1>,
|
||||
Planar64:core::ops::Mul<Den,Output=N1>,
|
||||
N1:integer::Divide<Den,Output=T1>,
|
||||
T1:integer::Fix<Planar64>,
|
||||
{
|
||||
self.origin+self.direction.map(|elem|(t*elem).divide().fix())
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a contact restriction
|
||||
pub struct Contact{
|
||||
pub position:Planar64Vec3,
|
||||
pub velocity:Planar64Vec3,
|
||||
pub normal:Planar64Vec3,
|
||||
}
|
||||
impl Contact{
|
||||
fn relative_to(&self,point:Planar64Vec3)->Self{
|
||||
Self{
|
||||
position:self.position-point,
|
||||
velocity:self.velocity,
|
||||
normal:self.normal,
|
||||
}
|
||||
}
|
||||
fn relative_dot(&self,direction:Planar64Vec3)->Fixed<2,64>{
|
||||
(direction-self.velocity).dot(self.normal)
|
||||
}
|
||||
/// Calculate the time of intersection. (previously get_touch_time)
|
||||
fn solve(&self,ray:&Ray)->Ratio<Fixed<2,64>,Fixed<2,64>>{
|
||||
(self.position-ray.origin).dot(self.normal)/(ray.direction-self.velocity).dot(self.normal)
|
||||
}
|
||||
}
|
||||
|
||||
//note that this is horrible with fixed point arithmetic
|
||||
fn solve1(c0:&Contact)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<2,64>>>{
|
||||
const EPSILON:Fixed<2,64>=Fixed::from_bits(Fixed::<2,64>::ONE.to_bits().shr(10));
|
||||
let det=c0.normal.dot(c0.velocity);
|
||||
if det.abs()<EPSILON{
|
||||
return None;
|
||||
}
|
||||
let d0=c0.normal.dot(c0.position);
|
||||
Some(c0.normal*d0/det)
|
||||
}
|
||||
fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<Fixed<5,160>>,Fixed<4,128>>>{
|
||||
const EPSILON:Fixed<4,128>=Fixed::from_bits(Fixed::<4,128>::ONE.to_bits().shr(10));
|
||||
let u0_u1=c0.velocity.cross(c1.velocity);
|
||||
let n0_n1=c0.normal.cross(c1.normal);
|
||||
let det=u0_u1.dot(n0_n1);
|
||||
if det.abs()<EPSILON{
|
||||
return None;
|
||||
}
|
||||
let d0=c0.normal.dot(c0.position);
|
||||
let d1=c1.normal.dot(c1.position);
|
||||
Some((c1.normal.cross(u0_u1)*d0+u0_u1.cross(c0.normal)*d1)/det)
|
||||
}
|
||||
fn solve3(c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ratio<Vector3<Fixed<4,128>>,Fixed<3,96>>>{
|
||||
const EPSILON:Fixed<3,96>=Fixed::from_bits(Fixed::<3,96>::ONE.to_bits().shr(10));
|
||||
let n0_n1=c0.normal.cross(c1.normal);
|
||||
let det=c2.normal.dot(n0_n1);
|
||||
if det.abs()<EPSILON{
|
||||
return None;
|
||||
}
|
||||
let d0=c0.normal.dot(c0.position);
|
||||
let d1=c1.normal.dot(c1.position);
|
||||
let d2=c2.normal.dot(c2.position);
|
||||
Some((c1.normal.cross(c2.normal)*d0+c2.normal.cross(c0.normal)*d1+c0.normal.cross(c1.normal)*d2)/det)
|
||||
}
|
||||
|
||||
fn decompose1(point:Planar64Vec3,u0:Planar64Vec3)->Option<[Ratio<Fixed<2,64>,Fixed<2,64>>;1]>{
|
||||
let det=u0.dot(u0);
|
||||
if det==Fixed::ZERO{
|
||||
return None;
|
||||
}
|
||||
let s0=u0.dot(point)/det;
|
||||
Some([s0])
|
||||
}
|
||||
fn decompose2(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3)->Option<[Ratio<Fixed<4,128>,Fixed<4,128>>;2]>{
|
||||
let u0_u1=u0.cross(u1);
|
||||
let det=u0_u1.dot(u0_u1);
|
||||
if det==Fixed::ZERO{
|
||||
return None;
|
||||
}
|
||||
let s0=u0_u1.dot(point.cross(u1))/det;
|
||||
let s1=u0_u1.dot(u0.cross(point))/det;
|
||||
Some([s0,s1])
|
||||
}
|
||||
fn decompose3(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3,u2:Planar64Vec3)->Option<[Ratio<Fixed<3,96>,Fixed<3,96>>;3]>{
|
||||
let det=u0.cross(u1).dot(u2);
|
||||
if det==Fixed::ZERO{
|
||||
return None;
|
||||
}
|
||||
let s0=point.cross(u1).dot(u2)/det;
|
||||
let s1=u0.cross(point).dot(u2)/det;
|
||||
let s2=u0.cross(u1).dot(point)/det;
|
||||
Some([s0,s1,s2])
|
||||
}
|
||||
|
||||
fn is_space_enclosed_2(
|
||||
a:Planar64Vec3,
|
||||
b:Planar64Vec3,
|
||||
)->bool{
|
||||
a.cross(b)==Vector3::new([Fixed::ZERO;3])
|
||||
&&a.dot(b).is_negative()
|
||||
}
|
||||
fn is_space_enclosed_3(
|
||||
a:Planar64Vec3,
|
||||
b:Planar64Vec3,
|
||||
c:Planar64Vec3
|
||||
)->bool{
|
||||
a.cross(b).dot(c)==Fixed::ZERO
|
||||
&&{
|
||||
let det_abac=a.cross(b).dot(a.cross(c));
|
||||
let det_abbc=a.cross(b).dot(b.cross(c));
|
||||
let det_acbc=a.cross(c).dot(b.cross(c));
|
||||
return!( det_abac*det_abbc).is_positive()
|
||||
&&!( det_abbc*det_acbc).is_positive()
|
||||
&&!(-det_acbc*det_abac).is_positive()
|
||||
||is_space_enclosed_2(a,b)
|
||||
||is_space_enclosed_2(a,c)
|
||||
||is_space_enclosed_2(b,c)
|
||||
}
|
||||
}
|
||||
fn is_space_enclosed_4(
|
||||
a:Planar64Vec3,
|
||||
b:Planar64Vec3,
|
||||
c:Planar64Vec3,
|
||||
d:Planar64Vec3,
|
||||
)->bool{
|
||||
let det_abc=a.cross(b).dot(c);
|
||||
let det_abd=a.cross(b).dot(d);
|
||||
let det_acd=a.cross(c).dot(d);
|
||||
let det_bcd=b.cross(c).dot(d);
|
||||
return( det_abc*det_abd).is_negative()
|
||||
&&(-det_abc*det_acd).is_negative()
|
||||
&&( det_abd*det_acd).is_negative()
|
||||
&&( det_abc*det_bcd).is_negative()
|
||||
&&(-det_abd*det_bcd).is_negative()
|
||||
&&( det_acd*det_bcd).is_negative()
|
||||
||is_space_enclosed_3(a,b,c)
|
||||
||is_space_enclosed_3(a,b,d)
|
||||
||is_space_enclosed_3(a,c,d)
|
||||
||is_space_enclosed_3(b,c,d)
|
||||
}
|
||||
|
||||
const fn get_push_ray_0(point:Planar64Vec3)->Ray{
|
||||
Ray{origin:point,direction:vec3::ZERO}
|
||||
}
|
||||
fn get_push_ray_1(point:Planar64Vec3,c0:&Contact)->Option<Ray>{
|
||||
let direction=solve1(c0)?.divide().fix_1();
|
||||
let [s0]=decompose1(direction,c0.velocity)?;
|
||||
if s0.lt_ratio(RATIO_ZERO){
|
||||
return None;
|
||||
}
|
||||
let origin=point+solve1(
|
||||
&c0.relative_to(point),
|
||||
)?.divide().fix_1();
|
||||
Some(Ray{origin,direction})
|
||||
}
|
||||
fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
|
||||
let direction=solve2(c0,c1)?.divide().fix_1();
|
||||
let [s0,s1]=decompose2(direction,c0.velocity,c1.velocity)?;
|
||||
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO){
|
||||
return None;
|
||||
}
|
||||
let origin=point+solve2(
|
||||
&c0.relative_to(point),
|
||||
&c1.relative_to(point),
|
||||
)?.divide().fix_1();
|
||||
Some(Ray{origin,direction})
|
||||
}
|
||||
fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ray>{
|
||||
let direction=solve3(c0,c1,c2)?.divide().fix_1();
|
||||
let [s0,s1,s2]=decompose3(direction,c0.velocity,c1.velocity,c2.velocity)?;
|
||||
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO)||s2.lt_ratio(RATIO_ZERO){
|
||||
return None;
|
||||
}
|
||||
let origin=point+solve3(
|
||||
&c0.relative_to(point),
|
||||
&c1.relative_to(point),
|
||||
&c2.relative_to(point),
|
||||
)?.divide().fix_1();
|
||||
Some(Ray{origin,direction})
|
||||
}
|
||||
|
||||
const fn get_best_push_ray_and_conts_0<'a>(point:Planar64Vec3)->(Ray,Conts<'a>){
|
||||
(get_push_ray_0(point),Conts::new_const())
|
||||
}
|
||||
fn get_best_push_ray_and_conts_1(point:Planar64Vec3,c0:&Contact)->Option<(Ray,Conts)>{
|
||||
get_push_ray_1(point,c0)
|
||||
.map(|ray|(ray,Conts::from_iter([c0])))
|
||||
}
|
||||
fn get_best_push_ray_and_conts_2<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact)->Option<(Ray,Conts<'a>)>{
|
||||
if is_space_enclosed_2(c0.normal,c1.normal){
|
||||
return None;
|
||||
}
|
||||
if let Some(ray)=get_push_ray_2(point,c0,c1){
|
||||
return Some((ray,Conts::from_iter([c0,c1])));
|
||||
}
|
||||
if let Some(ray)=get_push_ray_1(point,c0){
|
||||
if !c1.relative_dot(ray.direction).is_negative(){
|
||||
return Some((ray,Conts::from_iter([c0])));
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
fn get_best_push_ray_and_conts_3<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact,c2:&'a Contact)->Option<(Ray,Conts<'a>)>{
|
||||
if is_space_enclosed_3(c0.normal,c1.normal,c2.normal){
|
||||
return None;
|
||||
}
|
||||
if let Some(ray)=get_push_ray_3(point,c0,c1,c2){
|
||||
return Some((ray,Conts::from_iter([c0,c1,c2])));
|
||||
}
|
||||
if let Some(ray)=get_push_ray_2(point,c0,c1){
|
||||
if !c2.relative_dot(ray.direction).is_negative(){
|
||||
return Some((ray,Conts::from_iter([c0,c1])));
|
||||
}
|
||||
}
|
||||
if let Some(ray)=get_push_ray_2(point,c0,c2){
|
||||
if !c1.relative_dot(ray.direction).is_negative(){
|
||||
return Some((ray,Conts::from_iter([c0,c2])));
|
||||
}
|
||||
}
|
||||
if let Some(ray)=get_push_ray_1(point,c0){
|
||||
if !c1.relative_dot(ray.direction).is_negative()
|
||||
&&!c2.relative_dot(ray.direction).is_negative(){
|
||||
return Some((ray,Conts::from_iter([c0])));
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
fn get_best_push_ray_and_conts_4<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact,c2:&'a Contact,c3:&'a Contact)->Option<(Ray,Conts<'a>)>{
|
||||
if is_space_enclosed_4(c0.normal,c1.normal,c2.normal,c3.normal){
|
||||
return None;
|
||||
}
|
||||
|
||||
let (ray012,conts012)=get_best_push_ray_and_conts_3(point,c0,c1,c2)?;
|
||||
let (ray013,conts013)=get_best_push_ray_and_conts_3(point,c0,c1,c3)?;
|
||||
let (ray023,conts023)=get_best_push_ray_and_conts_3(point,c0,c2,c3)?;
|
||||
|
||||
let err012=c3.relative_dot(ray012.direction);
|
||||
let err013=c2.relative_dot(ray013.direction);
|
||||
let err023=c1.relative_dot(ray023.direction);
|
||||
|
||||
let best_err=err012.max(err013).max(err023);
|
||||
|
||||
if best_err==err012{
|
||||
return Some((ray012,conts012))
|
||||
}else if best_err==err013{
|
||||
return Some((ray013,conts013))
|
||||
}else if best_err==err023{
|
||||
return Some((ray023,conts023))
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn get_best_push_ray_and_conts<'a>(
|
||||
point:Planar64Vec3,
|
||||
conts:&[&'a Contact],
|
||||
)->Option<(Ray,Conts<'a>)>{
|
||||
match conts{
|
||||
&[c0,c1,c2,c3]=>get_best_push_ray_and_conts_4(point,c0,c1,c2,c3),
|
||||
&[c0,c1,c2]=>get_best_push_ray_and_conts_3(point,c0,c1,c2),
|
||||
&[c0,c1]=>get_best_push_ray_and_conts_2(point,c0,c1),
|
||||
&[c0]=>get_best_push_ray_and_conts_1(point,c0),
|
||||
&[]=>Some(get_best_push_ray_and_conts_0(point)),
|
||||
_=>unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_first_touch<'a>(contacts:&'a [Contact],ray:&Ray,conts:&Conts)->Option<(Ratio<Fixed<2,64>,Fixed<2,64>>,&'a Contact)>{
|
||||
contacts.iter()
|
||||
.filter(|&contact|
|
||||
!conts.iter().any(|&c|std::ptr::eq(c,contact))
|
||||
&&contact.relative_dot(ray.direction).is_negative()
|
||||
)
|
||||
.map(|contact|(contact.solve(ray),contact))
|
||||
.min_by_key(|&(t,_)|t)
|
||||
}
|
||||
|
||||
pub fn push_solve(contacts:&[Contact],point:Planar64Vec3)->Planar64Vec3{
|
||||
let (mut ray,mut conts)=get_best_push_ray_and_conts_0(point);
|
||||
loop{
|
||||
let (next_t,next_cont)=match get_first_touch(contacts,&ray,&conts){
|
||||
Some((t,cont))=>(t,cont),
|
||||
None=>return ray.origin,
|
||||
};
|
||||
|
||||
if RATIO_ZERO.le_ratio(next_t){
|
||||
return ray.origin;
|
||||
}
|
||||
|
||||
//push_front
|
||||
if conts.len()==conts.capacity(){
|
||||
//this is a dead case, new_conts never has more than 3 elements
|
||||
conts.rotate_right(1);
|
||||
conts[0]=next_cont;
|
||||
}else{
|
||||
conts.push(next_cont);
|
||||
conts.rotate_right(1);
|
||||
}
|
||||
|
||||
let meet_point=ray.extrapolate(next_t);
|
||||
match get_best_push_ray_and_conts(meet_point,conts.as_slice()){
|
||||
Some((new_ray,new_conts))=>(ray,conts)=(new_ray,new_conts),
|
||||
None=>return meet_point,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests{
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_push_solve(){
|
||||
let contacts=vec![
|
||||
Contact{
|
||||
position:vec3::ZERO,
|
||||
velocity:vec3::Y,
|
||||
normal:vec3::Y,
|
||||
}
|
||||
];
|
||||
assert_eq!(
|
||||
vec3::ZERO,
|
||||
push_solve(&contacts,vec3::NEG_Y)
|
||||
);
|
||||
}
|
||||
}
|
||||
12
engine/session/Cargo.toml
Normal file
12
engine/session/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "strafesnet_session"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
glam = "0.29.0"
|
||||
replace_with = "0.1.7"
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
||||
strafesnet_physics = { path = "../physics", registry = "strafesnet" }
|
||||
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
|
||||
strafesnet_snf = { path = "../../lib/snf", registry = "strafesnet" }
|
||||
8
engine/session/LICENSE
Normal file
8
engine/session/LICENSE
Normal file
@@ -0,0 +1,8 @@
|
||||
/*******************************************************
|
||||
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
|
||||
*
|
||||
* This file is part of the StrafesNET bhop/surf client.
|
||||
*
|
||||
* StrafesNET can not be copied and/or distributed
|
||||
* without the express permission of Rhys Lloyd
|
||||
*******************************************************/
|
||||
2
engine/session/src/lib.rs
Normal file
2
engine/session/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod mouse_interpolator;
|
||||
pub mod session;
|
||||
281
engine/session/src/mouse_interpolator.rs
Normal file
281
engine/session/src/mouse_interpolator.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use strafesnet_common::mouse::MouseState;
|
||||
use strafesnet_common::physics::{
|
||||
MouseInstruction,SetControlInstruction,ModeInstruction,MiscInstruction,
|
||||
Instruction as PhysicsInstruction,
|
||||
TimeInner as PhysicsTimeInner,
|
||||
Time as PhysicsTime,
|
||||
};
|
||||
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
|
||||
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,TimedInstruction};
|
||||
|
||||
type TimedSelfInstruction=TimedInstruction<Instruction,PhysicsTimeInner>;
|
||||
type DoubleTimedSelfInstruction=TimedInstruction<TimedSelfInstruction,SessionTimeInner>;
|
||||
|
||||
type TimedPhysicsInstruction=TimedInstruction<PhysicsInstruction,PhysicsTimeInner>;
|
||||
|
||||
const MOUSE_TIMEOUT:SessionTime=SessionTime::from_millis(10);
|
||||
|
||||
/// To be fed into MouseInterpolator
|
||||
#[derive(Clone,Debug)]
|
||||
pub(crate) enum Instruction{
|
||||
MoveMouse(glam::IVec2),
|
||||
SetControl(SetControlInstruction),
|
||||
Mode(ModeInstruction),
|
||||
Misc(MiscInstruction),
|
||||
Idle,
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
enum UnbufferedInstruction{
|
||||
MoveMouse(glam::IVec2),
|
||||
NonMouse(NonMouseInstruction),
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
enum BufferedInstruction{
|
||||
Mouse(MouseInstruction),
|
||||
NonMouse(NonMouseInstruction),
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
pub(crate) enum NonMouseInstruction{
|
||||
SetControl(SetControlInstruction),
|
||||
Mode(ModeInstruction),
|
||||
Misc(MiscInstruction),
|
||||
Idle,
|
||||
}
|
||||
impl From<Instruction> for UnbufferedInstruction{
|
||||
#[inline]
|
||||
fn from(value:Instruction)->Self{
|
||||
match value{
|
||||
Instruction::MoveMouse(mouse_instruction)=>UnbufferedInstruction::MoveMouse(mouse_instruction),
|
||||
Instruction::SetControl(set_control_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::SetControl(set_control_instruction)),
|
||||
Instruction::Mode(mode_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Mode(mode_instruction)),
|
||||
Instruction::Misc(misc_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Misc(misc_instruction)),
|
||||
Instruction::Idle=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Idle),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<BufferedInstruction> for PhysicsInstruction{
|
||||
#[inline]
|
||||
fn from(value:BufferedInstruction)->Self{
|
||||
match value{
|
||||
BufferedInstruction::Mouse(mouse_instruction)=>PhysicsInstruction::Mouse(mouse_instruction),
|
||||
BufferedInstruction::NonMouse(non_mouse_instruction)=>match non_mouse_instruction{
|
||||
NonMouseInstruction::SetControl(set_control_instruction)=>PhysicsInstruction::SetControl(set_control_instruction),
|
||||
NonMouseInstruction::Mode(mode_instruction)=>PhysicsInstruction::Mode(mode_instruction),
|
||||
NonMouseInstruction::Misc(misc_instruction)=>PhysicsInstruction::Misc(misc_instruction),
|
||||
NonMouseInstruction::Idle=>PhysicsInstruction::Idle,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum StepInstruction{
|
||||
Pop,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
enum BufferState{
|
||||
Unbuffered,
|
||||
Initializing(SessionTime,MouseState<PhysicsTimeInner>),
|
||||
Buffered(SessionTime,MouseState<PhysicsTimeInner>),
|
||||
}
|
||||
|
||||
pub struct MouseInterpolator{
|
||||
buffer_state:BufferState,
|
||||
// double timestamped timeline?
|
||||
buffer:std::collections::VecDeque<TimedPhysicsInstruction>,
|
||||
output:std::collections::VecDeque<TimedPhysicsInstruction>,
|
||||
}
|
||||
// Maybe MouseInterpolator manipulation is better expressed using impls
|
||||
// and called from Instruction trait impls in session
|
||||
impl InstructionConsumer<TimedSelfInstruction> for MouseInterpolator{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn process_instruction(&mut self,ins:DoubleTimedSelfInstruction){
|
||||
self.push_unbuffered_input(ins.time,ins.instruction.time,ins.instruction.instruction.into())
|
||||
}
|
||||
}
|
||||
impl InstructionEmitter<StepInstruction> for MouseInterpolator{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
|
||||
self.buffered_instruction_with_timeout(time_limit)
|
||||
}
|
||||
}
|
||||
impl MouseInterpolator{
|
||||
pub fn new()->MouseInterpolator{
|
||||
MouseInterpolator{
|
||||
buffer_state:BufferState::Unbuffered,
|
||||
buffer:std::collections::VecDeque::new(),
|
||||
output:std::collections::VecDeque::new(),
|
||||
}
|
||||
}
|
||||
fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTimeInner>){
|
||||
self.buffer.push_front(TimedInstruction{
|
||||
time:ins.time,
|
||||
instruction:BufferedInstruction::Mouse(ins.instruction).into(),
|
||||
});
|
||||
// flush buffer to output
|
||||
if self.output.len()==0{
|
||||
// swap buffers
|
||||
core::mem::swap(&mut self.buffer,&mut self.output);
|
||||
}else{
|
||||
// append buffer contents to output
|
||||
self.output.append(&mut self.buffer);
|
||||
}
|
||||
}
|
||||
fn get_mouse_timedout_at(&self,time_limit:SessionTime)->Option<SessionTime>{
|
||||
match &self.buffer_state{
|
||||
BufferState::Unbuffered=>None,
|
||||
BufferState::Initializing(time,_mouse_state)
|
||||
|BufferState::Buffered(time,_mouse_state)=>{
|
||||
let timeout=*time+MOUSE_TIMEOUT;
|
||||
(timeout<time_limit).then_some(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn timeout_mouse(&mut self,timeout_time:PhysicsTime){
|
||||
// the state always changes to unbuffered
|
||||
let buffer_state=core::mem::replace(&mut self.buffer_state,BufferState::Unbuffered);
|
||||
match buffer_state{
|
||||
BufferState::Unbuffered=>(),
|
||||
BufferState::Initializing(_time,mouse_state)=>{
|
||||
// only a single mouse move was sent in 10ms, this is very much an edge case!
|
||||
self.push_mouse_and_flush_buffer(TimedInstruction{
|
||||
time:mouse_state.time,
|
||||
instruction:MouseInstruction::ReplaceMouse{
|
||||
m1:MouseState{pos:mouse_state.pos,time:timeout_time},
|
||||
m0:mouse_state,
|
||||
},
|
||||
});
|
||||
}
|
||||
BufferState::Buffered(_time,mouse_state)=>{
|
||||
// duplicate the currently buffered mouse state but at a later (future, from the physics perspective) time
|
||||
self.push_mouse_and_flush_buffer(TimedInstruction{
|
||||
time:mouse_state.time,
|
||||
instruction:MouseInstruction::SetNextMouse(MouseState{pos:mouse_state.pos,time:timeout_time}),
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
fn push_unbuffered_input(&mut self,session_time:SessionTime,physics_time:PhysicsTime,ins:UnbufferedInstruction){
|
||||
// new input
|
||||
// if there is zero instruction buffered, it means the mouse is not moving
|
||||
// case 1: unbuffered
|
||||
// no mouse event is buffered
|
||||
// - ins is mouse event? change to buffered
|
||||
// - ins other -> write to timeline
|
||||
// case 2: buffered
|
||||
// a mouse event is buffered, and exists within the last 10ms
|
||||
// case 3: stop
|
||||
// a mouse event is buffered, but no mouse events have transpired within 10ms
|
||||
|
||||
// replace_with allows the enum variant to safely be replaced
|
||||
// from behind a mutable reference, but a panic in the closure means that
|
||||
// the entire program terminates rather than completing an unwind.
|
||||
let (ins_mouse,ins_other)=replace_with::replace_with_or_abort_and_return(&mut self.buffer_state,|buffer_state|{
|
||||
match ins{
|
||||
UnbufferedInstruction::MoveMouse(pos)=>{
|
||||
let next_mouse_state=MouseState{pos,time:physics_time};
|
||||
match buffer_state{
|
||||
BufferState::Unbuffered=>{
|
||||
((None,None),BufferState::Initializing(session_time,next_mouse_state))
|
||||
},
|
||||
BufferState::Initializing(_time,mouse_state)=>{
|
||||
let ins_mouse=TimedInstruction{
|
||||
time:mouse_state.time,
|
||||
instruction:MouseInstruction::ReplaceMouse{
|
||||
m0:mouse_state,
|
||||
m1:next_mouse_state.clone(),
|
||||
},
|
||||
};
|
||||
((Some(ins_mouse),None),BufferState::Buffered(session_time,next_mouse_state))
|
||||
},
|
||||
BufferState::Buffered(_time,mouse_state)=>{
|
||||
let ins_mouse=TimedInstruction{
|
||||
time:mouse_state.time,
|
||||
instruction:MouseInstruction::SetNextMouse(next_mouse_state.clone()),
|
||||
};
|
||||
((Some(ins_mouse),None),BufferState::Buffered(session_time,next_mouse_state))
|
||||
},
|
||||
}
|
||||
},
|
||||
UnbufferedInstruction::NonMouse(other_instruction)=>((None,Some(TimedInstruction{
|
||||
time:physics_time,
|
||||
instruction:other_instruction,
|
||||
})),buffer_state),
|
||||
}
|
||||
});
|
||||
if let Some(ins)=ins_mouse{
|
||||
self.push_mouse_and_flush_buffer(ins);
|
||||
}
|
||||
if let Some(ins)=ins_other{
|
||||
let instruction=TimedInstruction{
|
||||
time:ins.time,
|
||||
instruction:BufferedInstruction::NonMouse(ins.instruction).into(),
|
||||
};
|
||||
if matches!(self.buffer_state,BufferState::Unbuffered){
|
||||
self.output.push_back(instruction);
|
||||
}else{
|
||||
self.buffer.push_back(instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTimeInner>>{
|
||||
match self.get_mouse_timedout_at(time_limit){
|
||||
Some(timeout)=>Some(TimedInstruction{
|
||||
time:timeout,
|
||||
instruction:StepInstruction::Timeout,
|
||||
}),
|
||||
None=>(self.output.len()!=0).then_some(TimedInstruction{
|
||||
// this timestamp should not matter
|
||||
time:time_limit,
|
||||
instruction:StepInstruction::Pop,
|
||||
}),
|
||||
}
|
||||
}
|
||||
pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTimeInner>)->Option<TimedPhysicsInstruction>{
|
||||
match ins.instruction{
|
||||
StepInstruction::Pop=>(),
|
||||
StepInstruction::Timeout=>self.timeout_mouse(ins.time),
|
||||
}
|
||||
self.output.pop_front()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test(){
|
||||
let mut interpolator=MouseInterpolator::new();
|
||||
|
||||
let timer=strafesnet_common::timer::Timer::<strafesnet_common::timer::Scaled<SessionTimeInner,PhysicsTimeInner>>::unpaused(SessionTime::ZERO,PhysicsTime::from_secs(1000));
|
||||
|
||||
macro_rules! push{
|
||||
($time:expr,$ins:expr)=>{
|
||||
println!("in={:?}",$ins);
|
||||
interpolator.push_unbuffered_input(
|
||||
$time,
|
||||
timer.time($time),
|
||||
$ins,
|
||||
);
|
||||
while let Some(ins)=interpolator.buffered_instruction_with_timeout($time){
|
||||
let ins_retimed=TimedInstruction{
|
||||
time:timer.time(ins.time),
|
||||
instruction:ins.instruction,
|
||||
};
|
||||
let out=interpolator.pop_buffered_instruction(ins_retimed);
|
||||
println!("out={out:?}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// test each buffer_state transition
|
||||
let mut t=SessionTime::ZERO;
|
||||
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
|
||||
t+=SessionTime::from_millis(5);
|
||||
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
|
||||
t+=SessionTime::from_millis(5);
|
||||
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
|
||||
t+=SessionTime::from_millis(1);
|
||||
}
|
||||
}
|
||||
433
engine/session/src/session.rs
Normal file
433
engine/session/src/session.rs
Normal file
@@ -0,0 +1,433 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use strafesnet_common::gameplay_modes::{ModeId,StageId};
|
||||
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,InstructionFeedback,TimedInstruction};
|
||||
// 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::{
|
||||
ModeInstruction,MiscInstruction,
|
||||
Instruction as PhysicsInputInstruction,
|
||||
TimeInner as PhysicsTimeInner,
|
||||
Time as PhysicsTime
|
||||
};
|
||||
use strafesnet_common::timer::{Scaled,Timer};
|
||||
use strafesnet_common::session::{TimeInner as SessionTimeInner,Time as SessionTime};
|
||||
|
||||
use crate::mouse_interpolator::{MouseInterpolator,StepInstruction,Instruction as MouseInterpolatorInstruction};
|
||||
use strafesnet_physics::physics::{self,PhysicsContext,PhysicsData};
|
||||
use strafesnet_settings::settings::UserSettings;
|
||||
|
||||
pub enum Instruction<'a>{
|
||||
Input(SessionInputInstruction),
|
||||
Control(SessionControlInstruction),
|
||||
Playback(SessionPlaybackInstruction),
|
||||
ChangeMap(&'a strafesnet_common::map::CompleteMap),
|
||||
LoadReplay(strafesnet_snf::bot::Segment),
|
||||
Idle,
|
||||
}
|
||||
|
||||
pub enum SessionInputInstruction{
|
||||
Mouse(glam::IVec2),
|
||||
SetControl(strafesnet_common::physics::SetControlInstruction),
|
||||
Mode(ImplicitModeInstruction),
|
||||
Misc(strafesnet_common::physics::MiscInstruction),
|
||||
}
|
||||
/// Implicit mode instruction are fed separately to session.
|
||||
/// Session generates the explicit mode instructions interlaced with a SetSensitivity instruction
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum ImplicitModeInstruction{
|
||||
ResetAndRestart,
|
||||
ResetAndSpawn(ModeId,StageId),
|
||||
}
|
||||
|
||||
pub enum SessionControlInstruction{
|
||||
SetPaused(bool),
|
||||
// copy the current session simulation recording into a replay and view it
|
||||
CopyRecordingIntoReplayAndSpectate,
|
||||
StopSpectate,
|
||||
SaveReplay,
|
||||
LoadIntoReplayState,
|
||||
}
|
||||
pub enum SessionPlaybackInstruction{
|
||||
SkipForward,
|
||||
SkipBack,
|
||||
TogglePaused,
|
||||
DecreaseTimescale,
|
||||
IncreaseTimescale,
|
||||
}
|
||||
|
||||
pub struct FrameState{
|
||||
pub body:physics::Body,
|
||||
pub camera:physics::PhysicsCamera,
|
||||
pub time:PhysicsTime,
|
||||
}
|
||||
|
||||
pub struct Simulation{
|
||||
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
|
||||
physics:physics::PhysicsState,
|
||||
}
|
||||
impl Simulation{
|
||||
pub const fn new(
|
||||
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
|
||||
physics:physics::PhysicsState,
|
||||
)->Self{
|
||||
Self{
|
||||
timer,
|
||||
physics,
|
||||
}
|
||||
}
|
||||
pub fn get_frame_state(&self,time:SessionTime)->FrameState{
|
||||
FrameState{
|
||||
body:self.physics.camera_body(),
|
||||
camera:self.physics.camera(),
|
||||
time:self.timer.time(time),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Recording{
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
|
||||
}
|
||||
impl Recording{
|
||||
pub fn new(
|
||||
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
|
||||
)->Self{
|
||||
Self{instructions}
|
||||
}
|
||||
fn clear(&mut self){
|
||||
self.instructions.clear();
|
||||
}
|
||||
}
|
||||
pub struct Replay{
|
||||
next_instruction_id:usize,
|
||||
recording:Recording,
|
||||
simulation:Simulation,
|
||||
}
|
||||
impl Replay{
|
||||
pub const fn new(
|
||||
recording:Recording,
|
||||
simulation:Simulation,
|
||||
)->Self{
|
||||
Self{
|
||||
next_instruction_id:0,
|
||||
recording,
|
||||
simulation,
|
||||
}
|
||||
}
|
||||
pub fn advance(&mut self,physics_data:&PhysicsData,time_limit:SessionTime){
|
||||
let mut time=self.simulation.timer.time(time_limit);
|
||||
loop{
|
||||
if let Some(ins)=self.recording.instructions.get(self.next_instruction_id){
|
||||
if ins.time<time{
|
||||
PhysicsContext::run_input_instruction(&mut self.simulation.physics,physics_data,ins.clone());
|
||||
self.next_instruction_id+=1;
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}else{
|
||||
// loop playback
|
||||
self.next_instruction_id=0;
|
||||
// No need to reset physics because the very first instruction is 'Reset'
|
||||
let new_time=self.recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
|
||||
self.simulation.timer.set_time(time_limit,new_time);
|
||||
time=new_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Hash,PartialEq,Eq)]
|
||||
struct BotId(u32);
|
||||
//#[derive(Clone,Copy,Hash,PartialEq,Eq)]
|
||||
//struct PlayerId(u32);
|
||||
|
||||
enum ViewState{
|
||||
Play,
|
||||
//Spectate(PlayerId),
|
||||
Replay(BotId),
|
||||
}
|
||||
|
||||
pub struct Session{
|
||||
user_settings:UserSettings,
|
||||
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
|
||||
view_state:ViewState,
|
||||
//gui:GuiState
|
||||
geometry_shared:physics::PhysicsData,
|
||||
simulation:Simulation,
|
||||
// below fields not included in lite session
|
||||
recording:Recording,
|
||||
//players:HashMap<PlayerId,Simulation>,
|
||||
replays:HashMap<BotId,Replay>,
|
||||
}
|
||||
impl Session{
|
||||
pub fn new(
|
||||
user_settings:UserSettings,
|
||||
simulation:Simulation,
|
||||
)->Self{
|
||||
Self{
|
||||
user_settings,
|
||||
mouse_interpolator:MouseInterpolator::new(),
|
||||
geometry_shared:Default::default(),
|
||||
simulation,
|
||||
view_state:ViewState::Play,
|
||||
recording:Default::default(),
|
||||
replays:HashMap::new(),
|
||||
}
|
||||
}
|
||||
fn clear_recording(&mut self){
|
||||
self.recording.clear();
|
||||
}
|
||||
fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
|
||||
self.simulation.physics.clear();
|
||||
self.geometry_shared.generate_models(map);
|
||||
}
|
||||
pub fn get_frame_state(&self,time:SessionTime)->Option<FrameState>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>Some(self.simulation.get_frame_state(time)),
|
||||
ViewState::Replay(bot_id)=>self.replays.get(bot_id).map(|replay|
|
||||
replay.simulation.get_frame_state(time)
|
||||
),
|
||||
}
|
||||
}
|
||||
pub fn user_settings(&self)->&UserSettings{
|
||||
&self.user_settings
|
||||
}
|
||||
}
|
||||
|
||||
// mouseinterpolator consumes RawInputInstruction
|
||||
// mouseinterpolator emits PhysicsInputInstruction
|
||||
// mouseinterpolator consumes DoStep to move on to the next emitted instruction
|
||||
// Session comsumes SessionInstruction -> forwards RawInputInstruction to mouseinterpolator
|
||||
// Session consumes DoStep -> forwards DoStep to mouseinterpolator
|
||||
// Session emits DoStep
|
||||
|
||||
impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Self::TimeInner>){
|
||||
// repetitive procedure macro
|
||||
macro_rules! run_mouse_interpolator_instruction{
|
||||
($instruction:expr)=>{
|
||||
self.mouse_interpolator.process_instruction(TimedInstruction{
|
||||
time:ins.time,
|
||||
instruction:TimedInstruction{
|
||||
time:self.simulation.timer.time(ins.time),
|
||||
instruction:$instruction,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// process any timeouts that occured since the last instruction
|
||||
self.process_exhaustive(ins.time);
|
||||
|
||||
match ins.instruction{
|
||||
// send it down to MouseInterpolator with two timestamps, SessionTime and PhysicsTime
|
||||
Instruction::Input(SessionInputInstruction::Mouse(pos))=>{
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::MoveMouse(pos));
|
||||
},
|
||||
Instruction::Input(SessionInputInstruction::SetControl(set_control_instruction))=>{
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::SetControl(set_control_instruction));
|
||||
},
|
||||
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndRestart))=>{
|
||||
self.clear_recording();
|
||||
let mode_id=self.simulation.physics.mode();
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Restart(mode_id)));
|
||||
},
|
||||
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndSpawn(mode_id,spawn_id)))=>{
|
||||
self.clear_recording();
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Spawn(mode_id,spawn_id)));
|
||||
},
|
||||
Instruction::Input(SessionInputInstruction::Misc(misc_instruction))=>{
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(misc_instruction));
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::SetPaused(paused))=>{
|
||||
// don't flush the buffered instructions in the mouse interpolator
|
||||
// until the mouse is confirmed to be not moving at a later time
|
||||
// what if they pause for 5ms lmao
|
||||
_=self.simulation.timer.set_paused(ins.time,paused);
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::CopyRecordingIntoReplayAndSpectate)=> if let ViewState::Play=self.view_state{
|
||||
// Bind: B
|
||||
|
||||
// pause simulation
|
||||
_=self.simulation.timer.set_paused(ins.time,true);
|
||||
|
||||
// create recording
|
||||
let mut recording=Recording::default();
|
||||
recording.instructions.extend(self.recording.instructions.iter().cloned());
|
||||
|
||||
// create timer starting at first instruction (or zero if the list is empty)
|
||||
let new_time=recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
|
||||
let timer=Timer::unpaused(ins.time,new_time);
|
||||
|
||||
// create default physics state
|
||||
let simulation=Simulation::new(timer,Default::default());
|
||||
|
||||
// invent a new bot id and insert the replay
|
||||
let bot_id=BotId(self.replays.len() as u32);
|
||||
self.replays.insert(bot_id,Replay::new(
|
||||
recording,
|
||||
simulation,
|
||||
));
|
||||
|
||||
// begin spectate
|
||||
self.view_state=ViewState::Replay(bot_id);
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::StopSpectate)=>{
|
||||
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
|
||||
// delete the bot, otherwise it's inaccessible and wastes CPU
|
||||
match view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>{
|
||||
self.replays.remove(&bot_id);
|
||||
},
|
||||
}
|
||||
_=self.simulation.timer.set_paused(ins.time,false);
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::SaveReplay)=>{
|
||||
// Bind: N
|
||||
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
|
||||
match view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
|
||||
let file_name=format!("replays/{}.snfb",ins.time);
|
||||
std::thread::spawn(move ||{
|
||||
std::fs::create_dir_all("replays").unwrap();
|
||||
let file=std::fs::File::create(file_name).unwrap();
|
||||
strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),physics::VERSION.get(),replay.recording.instructions).unwrap();
|
||||
println!("Finished writing bot file!");
|
||||
});
|
||||
},
|
||||
}
|
||||
_=self.simulation.timer.set_paused(ins.time,false);
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::LoadIntoReplayState)=>{
|
||||
// Bind: J
|
||||
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
|
||||
match view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
|
||||
self.recording.instructions=replay.recording.instructions.into_iter().take(replay.next_instruction_id).collect();
|
||||
self.simulation=replay.simulation;
|
||||
},
|
||||
}
|
||||
// don't unpause -- use the replay timer state whether it is pasued or unpaused
|
||||
},
|
||||
Instruction::Playback(SessionPlaybackInstruction::IncreaseTimescale)=>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>{
|
||||
// allow simulation timescale for fun
|
||||
let scale=self.simulation.timer.get_scale();
|
||||
self.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*5,scale.den()*4).unwrap());
|
||||
},
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
|
||||
let scale=replay.simulation.timer.get_scale();
|
||||
replay.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*5,scale.den()*4).unwrap());
|
||||
},
|
||||
}
|
||||
},
|
||||
Instruction::Playback(SessionPlaybackInstruction::DecreaseTimescale)=>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>{
|
||||
// allow simulation timescale for fun
|
||||
let scale=self.simulation.timer.get_scale();
|
||||
self.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*4,scale.den()*5).unwrap());
|
||||
},
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
|
||||
let scale=replay.simulation.timer.get_scale();
|
||||
replay.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*4,scale.den()*5).unwrap());
|
||||
},
|
||||
}
|
||||
},
|
||||
Instruction::Playback(SessionPlaybackInstruction::SkipForward)=>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
|
||||
let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
|
||||
replay.simulation.timer.set_time(ins.time,time);
|
||||
},
|
||||
}
|
||||
},
|
||||
Instruction::Playback(SessionPlaybackInstruction::SkipBack)=>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
|
||||
let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
|
||||
replay.simulation.timer.set_time(ins.time,time);
|
||||
// resimulate the entire playback lol
|
||||
replay.next_instruction_id=0;
|
||||
},
|
||||
}
|
||||
},
|
||||
Instruction::Playback(SessionPlaybackInstruction::TogglePaused)=>{
|
||||
match &self.view_state{
|
||||
ViewState::Play=>(),
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
|
||||
_=replay.simulation.timer.set_paused(ins.time,!replay.simulation.timer.is_paused());
|
||||
},
|
||||
}
|
||||
}
|
||||
Instruction::ChangeMap(complete_map)=>{
|
||||
self.clear_recording();
|
||||
self.change_map(complete_map);
|
||||
},
|
||||
Instruction::LoadReplay(bot)=>{
|
||||
// pause simulation
|
||||
_=self.simulation.timer.set_paused(ins.time,true);
|
||||
|
||||
// create recording
|
||||
let recording=Recording::new(bot.instructions);
|
||||
|
||||
// create timer starting at first instruction (or zero if the list is empty)
|
||||
let new_time=recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
|
||||
let timer=Timer::unpaused(ins.time,new_time);
|
||||
|
||||
// create default physics state
|
||||
let simulation=Simulation::new(timer,Default::default());
|
||||
|
||||
// invent a new bot id and insert the replay
|
||||
let bot_id=BotId(self.replays.len() as u32);
|
||||
self.replays.insert(bot_id,Replay::new(
|
||||
recording,
|
||||
simulation,
|
||||
));
|
||||
|
||||
// begin spectate
|
||||
self.view_state=ViewState::Replay(bot_id);
|
||||
},
|
||||
Instruction::Idle=>{
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Idle);
|
||||
// this just refreshes the replays
|
||||
for replay in self.replays.values_mut(){
|
||||
// TODO: filter idles from recording, inject new idles in real time
|
||||
replay.advance(&self.geometry_shared,ins.time);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// process all emitted output instructions
|
||||
self.process_exhaustive(ins.time);
|
||||
}
|
||||
}
|
||||
impl InstructionConsumer<StepInstruction> for Session{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn process_instruction(&mut self,ins:TimedInstruction<StepInstruction,Self::TimeInner>){
|
||||
let time=self.simulation.timer.time(ins.time);
|
||||
if let Some(instruction)=self.mouse_interpolator.pop_buffered_instruction(ins.set_time(time)){
|
||||
//record
|
||||
self.recording.instructions.push(instruction.clone());
|
||||
PhysicsContext::run_input_instruction(&mut self.simulation.physics,&self.geometry_shared,instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl InstructionEmitter<StepInstruction> for Session{
|
||||
type TimeInner=SessionTimeInner;
|
||||
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
|
||||
self.mouse_interpolator.next_instruction(time_limit)
|
||||
}
|
||||
}
|
||||
9
engine/settings/Cargo.toml
Normal file
9
engine/settings/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "strafesnet_settings"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
configparser = "3.0.2"
|
||||
glam = "0.29.0"
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
||||
8
engine/settings/LICENSE
Normal file
8
engine/settings/LICENSE
Normal file
@@ -0,0 +1,8 @@
|
||||
/*******************************************************
|
||||
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
|
||||
*
|
||||
* This file is part of the StrafesNET bhop/surf client.
|
||||
*
|
||||
* StrafesNET can not be copied and/or distributed
|
||||
* without the express permission of Rhys Lloyd
|
||||
*******************************************************/
|
||||
1
engine/settings/src/lib.rs
Normal file
1
engine/settings/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod settings;
|
||||
139
engine/settings/src/settings.rs
Normal file
139
engine/settings/src/settings.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use strafesnet_common::integer::{Ratio64,Ratio64Vec2};
|
||||
#[derive(Clone)]
|
||||
struct Ratio{
|
||||
ratio:f64,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
enum DerivedFov{
|
||||
FromScreenAspect,
|
||||
FromAspect(Ratio),
|
||||
}
|
||||
#[derive(Clone)]
|
||||
enum Fov{
|
||||
Exactly{x:f64,y:f64},
|
||||
SpecifyXDeriveY{x:f64,y:DerivedFov},
|
||||
SpecifyYDeriveX{x:DerivedFov,y:f64},
|
||||
}
|
||||
impl Default for Fov{
|
||||
fn default()->Self{
|
||||
Fov::SpecifyYDeriveX{x:DerivedFov::FromScreenAspect,y:1.0}
|
||||
}
|
||||
}
|
||||
#[derive(Clone)]
|
||||
enum DerivedSensitivity{
|
||||
FromRatio(Ratio64),
|
||||
}
|
||||
#[derive(Clone)]
|
||||
enum Sensitivity{
|
||||
Exactly{x:Ratio64,y:Ratio64},
|
||||
SpecifyXDeriveY{x:Ratio64,y:DerivedSensitivity},
|
||||
SpecifyYDeriveX{x:DerivedSensitivity,y:Ratio64},
|
||||
}
|
||||
impl Default for Sensitivity{
|
||||
fn default()->Self{
|
||||
Sensitivity::SpecifyXDeriveY{x:Ratio64::ONE*524288,y:DerivedSensitivity::FromRatio(Ratio64::ONE)}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default,Clone)]
|
||||
pub struct UserSettings{
|
||||
fov:Fov,
|
||||
sensitivity:Sensitivity,
|
||||
}
|
||||
impl UserSettings{
|
||||
pub fn calculate_fov(&self,zoom:f64,screen_size:&glam::UVec2)->glam::DVec2{
|
||||
zoom*match &self.fov{
|
||||
&Fov::Exactly{x,y}=>glam::dvec2(x,y),
|
||||
Fov::SpecifyXDeriveY{x,y}=>match y{
|
||||
DerivedFov::FromScreenAspect=>glam::dvec2(*x,x*(screen_size.y as f64/screen_size.x as f64)),
|
||||
DerivedFov::FromAspect(ratio)=>glam::dvec2(*x,x*ratio.ratio),
|
||||
},
|
||||
Fov::SpecifyYDeriveX{x,y}=>match x{
|
||||
DerivedFov::FromScreenAspect=>glam::dvec2(y*(screen_size.x as f64/screen_size.y as f64),*y),
|
||||
DerivedFov::FromAspect(ratio)=>glam::dvec2(y*ratio.ratio,*y),
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn calculate_sensitivity(&self)->Ratio64Vec2{
|
||||
match &self.sensitivity{
|
||||
Sensitivity::Exactly{x,y}=>Ratio64Vec2::new(x.clone(),y.clone()),
|
||||
Sensitivity::SpecifyXDeriveY{x,y}=>match y{
|
||||
DerivedSensitivity::FromRatio(ratio)=>Ratio64Vec2::new(x.clone(),x.mul_ref(ratio)),
|
||||
}
|
||||
Sensitivity::SpecifyYDeriveX{x,y}=>match x{
|
||||
DerivedSensitivity::FromRatio(ratio)=>Ratio64Vec2::new(y.mul_ref(ratio),y.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
//sensitivity is raw input dots (i.e. dpi = dots per inch) to radians conversion factor
|
||||
sensitivity_x=0.001
|
||||
sensitivity_y_from_x_ratio=1
|
||||
Sensitivity::DeriveY{x:0.0.001,y:DerivedSensitivity{ratio:1.0}}
|
||||
*/
|
||||
|
||||
pub fn read_user_settings()->UserSettings{
|
||||
let mut cfg=configparser::ini::Ini::new();
|
||||
if let Ok(_)=cfg.load("settings.conf"){
|
||||
let (cfg_fov_x,cfg_fov_y)=(cfg.getfloat("camera","fov_x"),cfg.getfloat("camera","fov_y"));
|
||||
let fov=match(cfg_fov_x,cfg_fov_y){
|
||||
(Ok(Some(fov_x)),Ok(Some(fov_y)))=>Fov::Exactly {
|
||||
x:fov_x,
|
||||
y:fov_y
|
||||
},
|
||||
(Ok(Some(fov_x)),Ok(None))=>Fov::SpecifyXDeriveY{
|
||||
x:fov_x,
|
||||
y:if let Ok(Some(fov_y_from_x_ratio))=cfg.getfloat("camera","fov_y_from_x_ratio"){
|
||||
DerivedFov::FromAspect(Ratio{ratio:fov_y_from_x_ratio})
|
||||
}else{
|
||||
DerivedFov::FromScreenAspect
|
||||
}
|
||||
},
|
||||
(Ok(None),Ok(Some(fov_y)))=>Fov::SpecifyYDeriveX{
|
||||
x:if let Ok(Some(fov_x_from_y_ratio))=cfg.getfloat("camera","fov_x_from_y_ratio"){
|
||||
DerivedFov::FromAspect(Ratio{ratio:fov_x_from_y_ratio})
|
||||
}else{
|
||||
DerivedFov::FromScreenAspect
|
||||
},
|
||||
y:fov_y,
|
||||
},
|
||||
_=>{
|
||||
Fov::default()
|
||||
},
|
||||
};
|
||||
let (cfg_sensitivity_x,cfg_sensitivity_y)=(cfg.getfloat("camera","sensitivity_x"),cfg.getfloat("camera","sensitivity_y"));
|
||||
let sensitivity=match(cfg_sensitivity_x,cfg_sensitivity_y){
|
||||
(Ok(Some(sensitivity_x)),Ok(Some(sensitivity_y)))=>Sensitivity::Exactly {
|
||||
x:Ratio64::try_from(sensitivity_x).unwrap(),
|
||||
y:Ratio64::try_from(sensitivity_y).unwrap(),
|
||||
},
|
||||
(Ok(Some(sensitivity_x)),Ok(None))=>Sensitivity::SpecifyXDeriveY{
|
||||
x:Ratio64::try_from(sensitivity_x).unwrap(),
|
||||
y:if let Ok(Some(sensitivity_y_from_x_ratio))=cfg.getfloat("camera","sensitivity_y_from_x_ratio"){
|
||||
DerivedSensitivity::FromRatio(Ratio64::try_from(sensitivity_y_from_x_ratio).unwrap())
|
||||
}else{
|
||||
DerivedSensitivity::FromRatio(Ratio64::ONE)
|
||||
},
|
||||
},
|
||||
(Ok(None),Ok(Some(sensitivity_y)))=>Sensitivity::SpecifyYDeriveX{
|
||||
x:if let Ok(Some(sensitivity_x_from_y_ratio))=cfg.getfloat("camera","sensitivity_x_from_y_ratio"){
|
||||
DerivedSensitivity::FromRatio(Ratio64::try_from(sensitivity_x_from_y_ratio).unwrap())
|
||||
}else{
|
||||
DerivedSensitivity::FromRatio(Ratio64::ONE)
|
||||
},
|
||||
y:Ratio64::try_from(sensitivity_y).unwrap(),
|
||||
},
|
||||
_=>{
|
||||
Sensitivity::default()
|
||||
},
|
||||
};
|
||||
UserSettings{
|
||||
fov,
|
||||
sensitivity,
|
||||
}
|
||||
}else{
|
||||
UserSettings::default()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user