Compare commits
195 Commits
debug-bug3
...
debug-26
| Author | SHA1 | Date | |
|---|---|---|---|
|
a92526407c
|
|||
|
47239b10a8
|
|||
|
f5588989e9
|
|||
|
979d46e42a
|
|||
|
f858fa86e6
|
|||
|
8e6d598ea3
|
|||
|
78c0cab05a
|
|||
|
dd972c91ee
|
|||
|
8a897ca377
|
|||
|
65f29fd395
|
|||
|
a673d62ffd
|
|||
|
8f04516f78
|
|||
|
e1313e1e73
|
|||
| 48316a677a | |||
|
9b6be8bc68
|
|||
|
93988affd7
|
|||
|
78a80212d2
|
|||
|
125e718ec8
|
|||
|
0be912cc61
|
|||
|
cd91cca93f
|
|||
|
78d89ed3d9
|
|||
|
87853d3de8
|
|||
|
e33a419b99
|
|||
|
797b5d2100
|
|||
|
36447a76cf
|
|||
|
44f988ea4a
|
|||
|
7c14acb377
|
|||
|
98421ed24a
|
|||
|
4ebd251273
|
|||
|
05534a4863
|
|||
|
4dab573a3c
|
|||
|
841ae94255
|
|||
|
c44f4863bb
|
|||
|
298a7fcbde
|
|||
|
7a3ea40551
|
|||
|
4615ee24f3
|
|||
|
251f7cc627
|
|||
|
5519a9c4d9
|
|||
|
378619045f
|
|||
|
67d39d5e03
|
|||
|
daeabab337
|
|||
|
c3b9496e15
|
|||
|
c606adaaeb
|
|||
|
85589f0b00
|
|||
|
9d26e8cf6d
|
|||
|
80c4c1e50d
|
|||
|
0df71c6c57
|
|||
|
1a04188b8a
|
|||
|
aae7a23753
|
|||
|
96234f6bb1
|
|||
|
1cdd4676d7
|
|||
|
b7e1c8d723
|
|||
|
f781d104ab
|
|||
|
a710e33072
|
|||
|
971e95afd9
|
|||
|
e8a13d86ec
|
|||
|
2e1e8f524c
|
|||
|
7151c53553
|
|||
|
e06e51628a
|
|||
|
ea362d18a8
|
|||
|
13734bfe17
|
|||
|
74bd6d0e84
|
|||
|
ab09e384d3
|
|||
|
99d38a6eae
|
|||
|
e0d1ad48a8
|
|||
|
58ea19fbea
|
|||
|
d5791ac7a1
|
|||
| db7d7b3b36 | |||
|
06d0d70791
|
|||
|
4aab9679bf
|
|||
|
d32dcc5a58
|
|||
|
fa43f764f6
|
|||
|
4ce8516664
|
|||
|
d6441c86bc
|
|||
|
3371f3359b
|
|||
|
34d5288620
|
|||
|
82de08312d
|
|||
|
4b224b1827
|
|||
|
42862ce573
|
|||
|
3690954042
|
|||
|
bfe4b491d7
|
|||
|
36f44ffd43
|
|||
|
7f16468104
|
|||
|
a870899743
|
|||
|
b5431c0732
|
|||
|
36ccbdc6b2
|
|||
|
a68f009658
|
|||
|
118a1639a7
|
|||
|
3212fb1d41
|
|||
|
424c7ce9a6
|
|||
|
3174f9caa9
|
|||
|
b8e5343464
|
|||
|
51e7703933
|
|||
|
e46f54efe0
|
|||
|
b2993995bb
|
|||
|
7963c3632b
|
|||
|
67680312dd
|
|||
|
1a0a3403f0
|
|||
|
9e65a6eb95
|
|||
|
ebc897aad8
|
|||
|
23a6655bb0
|
|||
|
0422c223cd
|
|||
|
5db2ce076d
|
|||
|
c98364b68d
|
|||
|
b7e8fb6e18
|
|||
|
d343056664
|
|||
|
534f2a2141
|
|||
|
79ea88fc74
|
|||
|
3fd507be94
|
|||
| 0fbe37e483 | |||
|
638c2b4329
|
|||
|
317e1d57c7
|
|||
|
562e46a87e
|
|||
|
3c13d5f8ec
|
|||
|
1f0f78f9d8
|
|||
|
a90cb53a20
|
|||
|
170e2b9bce
|
|||
| 3e0fc54852 | |||
|
acea52646a
|
|||
|
7220506fd5
|
|||
|
8f94234ddc
|
|||
|
36143b8b69
|
|||
|
3893b2f44f
|
|||
|
d62ff68baa
|
|||
|
2331bef281
|
|||
| 31b52f7c34 | |||
|
b12c495a33
|
|||
|
8329eadb18
|
|||
|
4b2f93be66
|
|||
|
469ab48156
|
|||
|
c2650adf54
|
|||
| cdafbb4077 | |||
|
087e95b1f7
|
|||
|
e46a51319f
|
|||
|
a3b0306430
|
|||
|
e024f37843
|
|||
|
4059cfa527
|
|||
|
e4f3418bc6
|
|||
|
6ca6d5e484
|
|||
|
0668ac2def
|
|||
|
73e3181d0c
|
|||
|
19ba8f2445
|
|||
|
0495d07e26
|
|||
|
0ea353b27d
|
|||
|
99706079d9
|
|||
|
730c5fb7dd
|
|||
|
d1b61bb997
|
|||
|
0343ad19cf
|
|||
|
43210b1417
|
|||
|
e9d28cf15f
|
|||
|
452bac4049
|
|||
|
48aad78f59
|
|||
|
d45a42f5aa
|
|||
|
c219fec3bc
|
|||
|
2a05d50abb
|
|||
|
fbb047f8d4
|
|||
|
c4d837a552
|
|||
|
a08bd44789
|
|||
|
4ae5359046
|
|||
|
15ecaf602a
|
|||
|
1e0511a7ba
|
|||
|
a9e4705d89
|
|||
| 98069859b5 | |||
|
64d3996fa9
|
|||
|
49c0c16e35
|
|||
|
255bed4803
|
|||
|
128e137829
|
|||
|
1e19f804cc
|
|||
|
f6f35c5f54
|
|||
|
4e7d580918
|
|||
|
8d5a100a2e
|
|||
|
91208db706
|
|||
|
5a320b514e
|
|||
| 661d706a22 | |||
| 5550d5771e | |||
| c834d1d1ca | |||
| ca9d2238a7 | |||
|
f3bb8dd067
|
|||
|
e58f9b9ff2
|
|||
|
54c4ed6bad
|
|||
|
9aceafa0df
|
|||
|
d065bac130
|
|||
|
a4d0393556
|
|||
|
3692d7f79e
|
|||
|
7e49840768
|
|||
|
4ecdd547c6
|
|||
|
b0365165e8
|
|||
|
c2ff52a2ae
|
|||
|
6e778869e8
|
|||
|
6509bef070
|
|||
|
0fa097a004
|
|||
|
55d4b1d264
|
|||
|
ea28663e95
|
|||
|
bac9be9684
|
|||
|
7e76f3309b
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/target
|
||||
.zed
|
||||
|
||||
2497
Cargo.lock
generated
2497
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@@ -24,3 +24,34 @@ resolver = "2"
|
||||
#lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
|
||||
[workspace.lints.rust]
|
||||
# unsafe_code = "forbid"
|
||||
# missing_docs = "warn"
|
||||
# missing_debug_implementations = "warn"
|
||||
single_use_lifetimes = "warn"
|
||||
trivial_casts = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_qualifications = "warn"
|
||||
# variant_size_differences = "warn"
|
||||
unexpected_cfgs = "warn"
|
||||
|
||||
[workspace.dependencies]
|
||||
glam = "0.32.0"
|
||||
wgpu = "29.0.0"
|
||||
|
||||
# engine
|
||||
strafesnet_graphics = { path = "engine/graphics", registry = "strafesnet" }
|
||||
strafesnet_physics = { version = "0.0.1", path = "engine/physics", registry = "strafesnet" }
|
||||
strafesnet_session = { path = "engine/session", registry = "strafesnet" }
|
||||
strafesnet_settings = { path = "engine/settings", registry = "strafesnet" }
|
||||
|
||||
# lib
|
||||
fixed_wide = { version = "0.2.2", path = "lib/fixed_wide", registry = "strafesnet" }
|
||||
linear_ops = { version = "0.1.1", path = "lib/linear_ops", registry = "strafesnet" }
|
||||
ratio_ops = { version = "0.1.0", path = "lib/ratio_ops", registry = "strafesnet" }
|
||||
strafesnet_bsp_loader = { version = "0.5.0", path = "lib/bsp_loader", registry = "strafesnet" }
|
||||
strafesnet_common = { version = "0.8.7", path = "lib/common", registry = "strafesnet" }
|
||||
strafesnet_deferred_loader = { version = "0.6.0", path = "lib/deferred_loader", registry = "strafesnet" }
|
||||
strafesnet_rbx_loader = { version = "0.10.2", path = "lib/rbx_loader", registry = "strafesnet" }
|
||||
strafesnet_snf = { version = "0.3.2", path = "lib/snf", registry = "strafesnet" }
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
[package]
|
||||
name = "strafesnet_graphics"
|
||||
version = "0.1.0"
|
||||
version = "0.0.8"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1.13.1", features = ["derive"] }
|
||||
ddsfile = "0.5.1"
|
||||
glam = "0.30.0"
|
||||
glam.workspace = true
|
||||
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 = "26.0.1"
|
||||
strafesnet_common.workspace = true
|
||||
wgpu.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -44,11 +44,11 @@ struct ModelInstance{
|
||||
transform:mat4x4<f32>,
|
||||
normal_transform:mat3x3<f32>,
|
||||
color:vec4<f32>,
|
||||
texture_color:vec4<f32>,
|
||||
}
|
||||
//my fancy idea is to create a megatexture for each model that includes all the textures each intance will need
|
||||
//the texture transform then maps the texture coordinates to the location of the specific texture
|
||||
//group 1 is the model
|
||||
const MAX_MODEL_INSTANCES=512;
|
||||
@group(2)
|
||||
@binding(0)
|
||||
var<uniform> model_instances: array<ModelInstance, MAX_MODEL_INSTANCES>;
|
||||
@@ -66,6 +66,7 @@ struct EntityOutputTexture {
|
||||
@location(3) view: vec3<f32>,
|
||||
@location(4) color: vec4<f32>,
|
||||
@location(5) @interpolate(flat) model_color: vec4<f32>,
|
||||
@location(6) @interpolate(flat) texture_color: vec4<f32>,
|
||||
};
|
||||
@vertex
|
||||
fn vs_entity_texture(
|
||||
@@ -81,6 +82,7 @@ fn vs_entity_texture(
|
||||
result.texture = texture;
|
||||
result.color = color;
|
||||
result.model_color = model_instances[instance].color;
|
||||
result.texture_color = model_instances[instance].texture_color;
|
||||
result.view = position.xyz - camera.view_inv[3].xyz;//col(3)
|
||||
result.position = camera.proj * camera.view * position;
|
||||
return result;
|
||||
@@ -106,7 +108,9 @@ fn fs_entity_texture(vertex: EntityOutputTexture) -> @location(0) vec4<f32> {
|
||||
let d = dot(normal, incident);
|
||||
let reflected = incident - 2.0 * d * normal;
|
||||
|
||||
let fragment_color = textureSample(model_texture, model_sampler, vertex.texture)*vertex.color;
|
||||
let fragment_color = textureSample(model_texture, model_sampler, vertex.texture)*vertex.texture_color;
|
||||
let reflected_color = textureSample(cube_texture, cube_sampler, reflected).rgb;
|
||||
return mix(vec4<f32>(vec3<f32>(0.05) + 0.2 * reflected_color,1.0),mix(vertex.model_color,vec4<f32>(fragment_color.rgb,1.0),fragment_color.a),0.5+0.5*abs(d));
|
||||
let entity_color = mix(vertex.model_color*vertex.color,vec4<f32>(fragment_color.rgb,1.0),fragment_color.a);
|
||||
let sky_color = vec4<f32>(vec3<f32>(0.05) + 0.2 * reflected_color,1.0);
|
||||
return mix(sky_color,entity_color,0.5+0.5*abs(d));
|
||||
}
|
||||
@@ -1,15 +1,9 @@
|
||||
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()
|
||||
}
|
||||
use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelOwned,GraphicsVertex};
|
||||
|
||||
struct Indices{
|
||||
count:u32,
|
||||
@@ -54,6 +48,10 @@ struct GraphicsPipelines{
|
||||
model:wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
pub fn view_inv(pos:glam::Vec3,angles:glam::Vec2)->glam::Mat4{
|
||||
//f32 good enough for view matrix
|
||||
glam::Mat4::from_mat3_translation(glam::Mat3::from_euler(glam::EulerRot::YXZ,angles.x,angles.y,0f32),pos)
|
||||
}
|
||||
struct GraphicsCamera{
|
||||
screen_size:glam::UVec2,
|
||||
fov:glam::Vec2,//slope
|
||||
@@ -75,15 +73,10 @@ 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]{
|
||||
pub fn to_uniform_data(&self,view_inv:glam::Mat4)->[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];
|
||||
@@ -94,7 +87,7 @@ impl GraphicsCamera{
|
||||
raw
|
||||
}
|
||||
}
|
||||
impl std::default::Default for GraphicsCamera{
|
||||
impl Default for GraphicsCamera{
|
||||
fn default()->Self{
|
||||
Self{
|
||||
screen_size:glam::UVec2::ONE,
|
||||
@@ -103,7 +96,7 @@ impl std::default::Default for GraphicsCamera{
|
||||
}
|
||||
}
|
||||
|
||||
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
|
||||
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4 + 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());
|
||||
@@ -118,7 +111,9 @@ fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
|
||||
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
|
||||
raw.extend_from_slice(&[0.0]);
|
||||
//color
|
||||
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
|
||||
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color));
|
||||
//texture color
|
||||
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.texture_color));
|
||||
}
|
||||
raw
|
||||
}
|
||||
@@ -134,18 +129,19 @@ pub struct GraphicsState{
|
||||
models:Vec<GraphicsModel>,
|
||||
depth_view:wgpu::TextureView,
|
||||
staging_belt:wgpu::util::StagingBelt,
|
||||
model_instances_uniform_len:usize,
|
||||
}
|
||||
|
||||
impl GraphicsState{
|
||||
const DEPTH_FORMAT:wgpu::TextureFormat=wgpu::TextureFormat::Depth24Plus;
|
||||
fn create_depth_texture(
|
||||
config:&wgpu::SurfaceConfiguration,
|
||||
size:glam::UVec2,
|
||||
device:&wgpu::Device,
|
||||
)->wgpu::TextureView{
|
||||
let depth_texture=device.create_texture(&wgpu::TextureDescriptor{
|
||||
size:wgpu::Extent3d{
|
||||
width:config.width,
|
||||
height:config.height,
|
||||
width:size.x,
|
||||
height:size.y,
|
||||
depth_or_array_layers:1,
|
||||
},
|
||||
mip_level_count:1,
|
||||
@@ -162,12 +158,9 @@ impl GraphicsState{
|
||||
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_views:HashMap<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,
|
||||
@@ -239,7 +232,8 @@ impl GraphicsState{
|
||||
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),
|
||||
color:model.color,
|
||||
texture_color:glam::Vec4::ONE,
|
||||
};
|
||||
//get or create owned mesh map
|
||||
let owned_mesh_map=owned_mesh_id_from_mesh_id_render_config_id
|
||||
@@ -269,16 +263,15 @@ impl GraphicsState{
|
||||
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()
|
||||
)
|
||||
),
|
||||
}
|
||||
let model::PolygonGroup::PolygonList(polygon_list)=&mut owned_mesh.polys;
|
||||
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
|
||||
@@ -286,10 +279,20 @@ impl GraphicsState{
|
||||
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());
|
||||
let no_texture=render_config.texture.is_none();
|
||||
let instance_clone=if no_texture{
|
||||
// this model has no chance of affecting pixels on the screen
|
||||
if model.color.w==0.0{
|
||||
continue;
|
||||
}
|
||||
// hack texture_color to zero to make the texture invisible
|
||||
let mut instance=instance.clone();
|
||||
instance.texture_color=glam::Vec4::ZERO;
|
||||
instance
|
||||
}else{
|
||||
instance.clone()
|
||||
};
|
||||
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
|
||||
@@ -315,7 +318,7 @@ impl GraphicsState{
|
||||
//separate instances by color
|
||||
for (instance_id,instance) in model.instances.iter().enumerate(){
|
||||
let model_instance_list=unique_color
|
||||
.entry(instance.color)
|
||||
.entry(instance.hashable_color())
|
||||
.or_insert_with(||Vec::new());
|
||||
//add model instance to list
|
||||
model_instance_list.push((model_id,instance_id));
|
||||
@@ -404,6 +407,7 @@ impl GraphicsState{
|
||||
).collect()
|
||||
));
|
||||
}
|
||||
let (color,texture_color)=color.color();
|
||||
//push model into dedup
|
||||
deduplicated_models.push(IndexedGraphicsMeshOwnedRenderConfig{
|
||||
unique_pos,
|
||||
@@ -416,7 +420,8 @@ impl GraphicsState{
|
||||
instances:vec![GraphicsModelOwned{
|
||||
transform:glam::Mat4::IDENTITY,
|
||||
normal_transform:glam::Mat3::IDENTITY,
|
||||
color
|
||||
color,
|
||||
texture_color,
|
||||
}],
|
||||
});
|
||||
}
|
||||
@@ -474,16 +479,14 @@ impl GraphicsState{
|
||||
//.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){
|
||||
for instances_chunk in model.instances.rchunks(self.model_instances_uniform_len){
|
||||
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);
|
||||
model_uniforms.resize(MODEL_BUFFER_SIZE*self.model_instances_uniform_len,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),
|
||||
@@ -539,7 +542,9 @@ impl GraphicsState{
|
||||
pub fn new(
|
||||
device:&wgpu::Device,
|
||||
queue:&wgpu::Queue,
|
||||
config:&wgpu::SurfaceConfiguration,
|
||||
size:glam::UVec2,
|
||||
view_format:wgpu::TextureFormat,
|
||||
limits:wgpu::Limits,
|
||||
)->Self{
|
||||
let camera_bind_group_layout=device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor{
|
||||
label:None,
|
||||
@@ -616,7 +621,7 @@ impl GraphicsState{
|
||||
address_mode_w:wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter:wgpu::FilterMode::Linear,
|
||||
min_filter:wgpu::FilterMode::Linear,
|
||||
mipmap_filter:wgpu::FilterMode::Linear,
|
||||
mipmap_filter:wgpu::MipmapFilterMode::Linear,
|
||||
..Default::default()
|
||||
});
|
||||
let repeat_sampler=device.create_sampler(&wgpu::SamplerDescriptor{
|
||||
@@ -626,15 +631,23 @@ impl GraphicsState{
|
||||
address_mode_w:wgpu::AddressMode::Repeat,
|
||||
mag_filter:wgpu::FilterMode::Linear,
|
||||
min_filter:wgpu::FilterMode::Linear,
|
||||
mipmap_filter:wgpu::FilterMode::Linear,
|
||||
mipmap_filter:wgpu::MipmapFilterMode::Linear,
|
||||
anisotropy_clamp:16,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let model_instances_uniform_len=limits.max_uniform_buffer_binding_size as usize/MODEL_BUFFER_SIZE_BYTES;
|
||||
|
||||
// write dynamic value into shader
|
||||
let shader=format!("
|
||||
// This is equal to the CHUNK_SIZE constant from graphics.rs
|
||||
const MAX_MODEL_INSTANCES={model_instances_uniform_len};
|
||||
")+include_str!("../shaders/shader.wgsl");
|
||||
|
||||
// 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"))),
|
||||
source:wgpu::ShaderSource::Wgsl(Cow::Owned(shader)),
|
||||
});
|
||||
|
||||
//load textures
|
||||
@@ -662,10 +675,10 @@ impl GraphicsState{
|
||||
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")[..],
|
||||
}=>&include_bytes!("../images/astc.dds")[..],
|
||||
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../images/etc2.dds")[..],
|
||||
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../images/bc1.dds")[..],
|
||||
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../images/bgra.dds")[..],
|
||||
_=>unreachable!(),
|
||||
};
|
||||
|
||||
@@ -708,7 +721,7 @@ impl GraphicsState{
|
||||
|
||||
//squid
|
||||
let squid_texture_view={
|
||||
let bytes=include_bytes!("../../../strafe-client/images/squid.dds");
|
||||
let bytes=include_bytes!("../images/squid.dds");
|
||||
|
||||
let image=ddsfile::Dds::read(&mut std::io::Cursor::new(bytes)).unwrap();
|
||||
|
||||
@@ -750,19 +763,19 @@ impl GraphicsState{
|
||||
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,
|
||||
Some(&camera_bind_group_layout),
|
||||
Some(&skybox_texture_bind_group_layout),
|
||||
Some(&model_bind_group_layout),
|
||||
],
|
||||
push_constant_ranges:&[],
|
||||
immediate_size:0,
|
||||
});
|
||||
let sky_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{
|
||||
label:None,
|
||||
bind_group_layouts:&[
|
||||
&camera_bind_group_layout,
|
||||
&skybox_texture_bind_group_layout,
|
||||
Some(&camera_bind_group_layout),
|
||||
Some(&skybox_texture_bind_group_layout),
|
||||
],
|
||||
push_constant_ranges:&[],
|
||||
immediate_size:0,
|
||||
});
|
||||
|
||||
// Create the render pipelines
|
||||
@@ -778,7 +791,7 @@ impl GraphicsState{
|
||||
fragment:Some(wgpu::FragmentState{
|
||||
module:&shader,
|
||||
entry_point:Some("fs_sky"),
|
||||
targets:&[Some(config.view_formats[0].into())],
|
||||
targets:&[Some(view_format.into())],
|
||||
compilation_options:wgpu::PipelineCompilationOptions::default(),
|
||||
}),
|
||||
primitive:wgpu::PrimitiveState{
|
||||
@@ -787,13 +800,13 @@ impl GraphicsState{
|
||||
},
|
||||
depth_stencil:Some(wgpu::DepthStencilState{
|
||||
format:Self::DEPTH_FORMAT,
|
||||
depth_write_enabled:false,
|
||||
depth_compare:wgpu::CompareFunction::LessEqual,
|
||||
depth_write_enabled:Some(false),
|
||||
depth_compare:Some(wgpu::CompareFunction::LessEqual),
|
||||
stencil:wgpu::StencilState::default(),
|
||||
bias:wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample:wgpu::MultisampleState::default(),
|
||||
multiview:None,
|
||||
multiview_mask:None,
|
||||
cache:None,
|
||||
});
|
||||
let model_pipeline=device.create_render_pipeline(&wgpu::RenderPipelineDescriptor{
|
||||
@@ -803,7 +816,7 @@ impl GraphicsState{
|
||||
module:&shader,
|
||||
entry_point:Some("vs_entity_texture"),
|
||||
buffers:&[wgpu::VertexBufferLayout{
|
||||
array_stride:std::mem::size_of::<GraphicsVertex>() as wgpu::BufferAddress,
|
||||
array_stride:size_of::<GraphicsVertex>() as wgpu::BufferAddress,
|
||||
step_mode:wgpu::VertexStepMode::Vertex,
|
||||
attributes:&wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Float32x3,3=>Float32x4],
|
||||
}],
|
||||
@@ -812,7 +825,7 @@ impl GraphicsState{
|
||||
fragment:Some(wgpu::FragmentState{
|
||||
module:&shader,
|
||||
entry_point:Some("fs_entity_texture"),
|
||||
targets:&[Some(config.view_formats[0].into())],
|
||||
targets:&[Some(view_format.into())],
|
||||
compilation_options:wgpu::PipelineCompilationOptions::default(),
|
||||
}),
|
||||
primitive:wgpu::PrimitiveState{
|
||||
@@ -822,18 +835,18 @@ impl GraphicsState{
|
||||
},
|
||||
depth_stencil:Some(wgpu::DepthStencilState{
|
||||
format:Self::DEPTH_FORMAT,
|
||||
depth_write_enabled:true,
|
||||
depth_compare:wgpu::CompareFunction::LessEqual,
|
||||
depth_write_enabled:Some(true),
|
||||
depth_compare:Some(wgpu::CompareFunction::LessEqual),
|
||||
stencil:wgpu::StencilState::default(),
|
||||
bias:wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample:wgpu::MultisampleState::default(),
|
||||
multiview:None,
|
||||
multiview_mask:None,
|
||||
cache:None,
|
||||
});
|
||||
|
||||
let camera=GraphicsCamera::default();
|
||||
let camera_uniforms=camera.to_uniform_data(glam::Vec3::ZERO,glam::Vec2::ZERO);
|
||||
let camera_uniforms=camera.to_uniform_data(view_inv(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),
|
||||
@@ -865,7 +878,7 @@ impl GraphicsState{
|
||||
label:Some("Sky Texture"),
|
||||
});
|
||||
|
||||
let depth_view=Self::create_depth_texture(config,device);
|
||||
let depth_view=Self::create_depth_texture(size,device);
|
||||
|
||||
Self{
|
||||
pipelines:GraphicsPipelines{
|
||||
@@ -880,45 +893,41 @@ impl GraphicsState{
|
||||
camera_buf,
|
||||
models:Vec::new(),
|
||||
depth_view,
|
||||
staging_belt:wgpu::util::StagingBelt::new(0x100),
|
||||
staging_belt:wgpu::util::StagingBelt::new(device.clone(),0x100),
|
||||
bind_group_layouts:GraphicsBindGroupLayouts{model:model_bind_group_layout},
|
||||
samplers:GraphicsSamplers{repeat:repeat_sampler},
|
||||
temp_squid_texture_view:squid_texture_view,
|
||||
model_instances_uniform_len,
|
||||
}
|
||||
}
|
||||
pub fn resize(
|
||||
&mut self,
|
||||
device:&wgpu::Device,
|
||||
config:&wgpu::SurfaceConfiguration,
|
||||
user_settings:&settings::UserSettings,
|
||||
size:glam::UVec2,
|
||||
fov:glam::Vec2,
|
||||
){
|
||||
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);
|
||||
self.depth_view=Self::create_depth_texture(size,device);
|
||||
self.camera.screen_size=size;
|
||||
self.camera.fov=fov;
|
||||
}
|
||||
pub fn render(
|
||||
pub fn encode_commands(
|
||||
&mut self,
|
||||
encoder:&mut wgpu::CommandEncoder,
|
||||
view:&wgpu::TextureView,
|
||||
device:&wgpu::Device,
|
||||
queue:&wgpu::Queue,
|
||||
frame_state:session::FrameState,
|
||||
camera:glam::Mat4,
|
||||
){
|
||||
//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});
|
||||
|
||||
// TODO: find a way to call this directly after queue.submit()
|
||||
self.staging_belt.recall();
|
||||
// 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)
|
||||
);
|
||||
let camera_uniforms=self.camera.to_uniform_data(camera);
|
||||
self.staging_belt
|
||||
.write_buffer(
|
||||
&mut encoder,
|
||||
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
|
||||
@@ -965,6 +974,7 @@ impl GraphicsState{
|
||||
}),
|
||||
timestamp_writes:Default::default(),
|
||||
occlusion_query_set:Default::default(),
|
||||
multiview_mask:None,
|
||||
});
|
||||
|
||||
rpass.set_bind_group(0,&self.bind_groups.camera,&[]);
|
||||
@@ -982,9 +992,5 @@ impl GraphicsState{
|
||||
rpass.set_pipeline(&self.pipelines.skybox);
|
||||
rpass.draw(0..3,0..1);
|
||||
}
|
||||
|
||||
queue.submit(std::iter::once(encoder.finish()));
|
||||
|
||||
self.staging_belt.recall();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
pub mod model;
|
||||
pub mod setup;
|
||||
pub mod surface;
|
||||
pub mod graphics;
|
||||
|
||||
@@ -30,19 +30,23 @@ pub struct GraphicsMeshOwnedRenderConfig{
|
||||
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,
|
||||
pub color:glam::Vec4,
|
||||
pub texture_color:glam::Vec4,
|
||||
}
|
||||
impl GraphicsModelOwned{
|
||||
pub fn hashable_color(&self)->HashableColor{
|
||||
HashableColor([bytemuck::cast::<[f32;4],[u32;4]>(self.color.to_array()),bytemuck::cast::<[f32;4],[u32;4]>(self.texture_color.to_array())])
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Copy,Eq,Hash,PartialEq)]
|
||||
pub struct HashableColor([[u32;4];2]);
|
||||
impl HashableColor{
|
||||
pub fn color(self)->(glam::Vec4,glam::Vec4){
|
||||
let [color,texture_color]=bytemuck::cast::<[[u32;4];2],[[f32;4];2]>(self.0);
|
||||
(glam::Vec4::from_array(color),glam::Vec4::from_array(texture_color))
|
||||
}
|
||||
}
|
||||
|
||||
116
engine/graphics/src/setup.rs
Normal file
116
engine/graphics/src/setup.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
fn optional_features()->wgpu::Features{
|
||||
wgpu::Features::TEXTURE_COMPRESSION_ASTC
|
||||
|wgpu::Features::TEXTURE_COMPRESSION_ETC2
|
||||
}
|
||||
fn required_features()->wgpu::Features{
|
||||
wgpu::Features::TEXTURE_COMPRESSION_BC
|
||||
}
|
||||
fn required_downlevel_capabilities()->wgpu::DownlevelCapabilities{
|
||||
wgpu::DownlevelCapabilities{
|
||||
flags:wgpu::DownlevelFlags::empty(),
|
||||
shader_model:wgpu::ShaderModel::Sm5,
|
||||
..wgpu::DownlevelCapabilities::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod step1{
|
||||
pub fn create_instance()->wgpu::Instance{
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod step2{
|
||||
pub fn create_surface<'window>(instance:&wgpu::Instance,target:impl Into<wgpu::SurfaceTarget<'window>>)->Result<wgpu::Surface<'window>,wgpu::CreateSurfaceError>{
|
||||
instance.create_surface(target)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod step3{
|
||||
pub async fn pick_adapter(instance:&wgpu::Instance,surface:&wgpu::Surface<'_>)->Option<wgpu::Adapter>{
|
||||
let backends=wgpu::Backends::from_env().unwrap_or_default();
|
||||
//TODO: prefer adapter that implements optional features
|
||||
//let optional_features=optional_features();
|
||||
let required_features=super::required_features();
|
||||
let required_downlevel_capabilities=super::required_downlevel_capabilities();
|
||||
|
||||
//no helper function smh gotta write it myself
|
||||
let adapters=instance.enumerate_adapters(backends).await;
|
||||
|
||||
let adapter=adapters.into_iter()
|
||||
// reverse because we want to select adapters that appear first in ties,
|
||||
// and max_by_key selects the last equal element in the iterator.
|
||||
.rev()
|
||||
.filter(|adapter|
|
||||
adapter.is_surface_supported(surface)
|
||||
&&adapter.features().contains(required_features)
|
||||
&&{
|
||||
let downlevel_capabilities=adapter.get_downlevel_capabilities();
|
||||
downlevel_capabilities.shader_model>=required_downlevel_capabilities.shader_model
|
||||
&&downlevel_capabilities.flags.contains(required_downlevel_capabilities.flags)
|
||||
}
|
||||
)
|
||||
.max_by_key(|adapter|match adapter.get_info().device_type{
|
||||
wgpu::DeviceType::IntegratedGpu=>3,
|
||||
wgpu::DeviceType::DiscreteGpu=>4,
|
||||
wgpu::DeviceType::VirtualGpu=>2,
|
||||
wgpu::DeviceType::Other|wgpu::DeviceType::Cpu=>1,
|
||||
})?;
|
||||
|
||||
Some(adapter)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod step4{
|
||||
pub async fn request_device(adapter:&wgpu::Adapter,limits:wgpu::Limits)->Result<(wgpu::Device,wgpu::Queue),wgpu::RequestDeviceError>{
|
||||
let optional_features=super::optional_features();
|
||||
let required_features=super::required_features();
|
||||
|
||||
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface.
|
||||
let needed_limits=limits.using_resolution(adapter.limits());
|
||||
|
||||
let (device, queue)=adapter
|
||||
.request_device(
|
||||
&wgpu::DeviceDescriptor{
|
||||
label:None,
|
||||
required_features:(optional_features&adapter.features())|required_features,
|
||||
required_limits:needed_limits,
|
||||
memory_hints:wgpu::MemoryHints::Performance,
|
||||
trace:wgpu::Trace::Off,
|
||||
experimental_features:wgpu::ExperimentalFeatures::disabled(),
|
||||
},
|
||||
).await?;
|
||||
|
||||
Ok((
|
||||
device,
|
||||
queue,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub mod step5{
|
||||
use crate::surface::Surface;
|
||||
#[derive(Debug)]
|
||||
pub struct ErrorSurfaceNotSupported;
|
||||
impl std::fmt::Display for ErrorSurfaceNotSupported{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"Surface isn't supported by the adapter.")
|
||||
}
|
||||
}
|
||||
pub fn configure_surface<'window>(
|
||||
adapter:&wgpu::Adapter,
|
||||
device:&wgpu::Device,
|
||||
surface:wgpu::Surface<'window>,
|
||||
(width,height):(u32,u32),
|
||||
)->Result<Surface<'window>,ErrorSurfaceNotSupported>{
|
||||
let mut config=surface
|
||||
.get_default_config(adapter, width, height)
|
||||
.ok_or(ErrorSurfaceNotSupported)?;
|
||||
|
||||
let surface_view_format=config.format.add_srgb_suffix();
|
||||
config.view_formats.push(surface_view_format);
|
||||
config.present_mode=wgpu::PresentMode::AutoNoVsync;
|
||||
surface.configure(device,&config);
|
||||
|
||||
Ok(Surface::new(surface,config))
|
||||
}
|
||||
}
|
||||
80
engine/graphics/src/surface.rs
Normal file
80
engine/graphics/src/surface.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
/// A texture view which can be targeted by draw calls in the command buffer, and then presented to the surface texture.
|
||||
pub struct Frame{
|
||||
surface_texture:wgpu::SurfaceTexture,
|
||||
view:wgpu::TextureView,
|
||||
}
|
||||
impl Frame{
|
||||
pub const fn view(&self)->&wgpu::TextureView{
|
||||
&self.view
|
||||
}
|
||||
pub fn present(self){
|
||||
self.surface_texture.present();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FrameError{
|
||||
Skip,
|
||||
DeviceLost,
|
||||
}
|
||||
impl core::fmt::Display for FrameError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl core::error::Error for FrameError{}
|
||||
|
||||
/// A render surface configuration, containing information such as resolution and pixel format
|
||||
pub struct Surface<'window>{
|
||||
surface:wgpu::Surface<'window>,
|
||||
config:wgpu::SurfaceConfiguration,
|
||||
}
|
||||
|
||||
impl<'window> Surface<'window>{
|
||||
pub(crate) fn new(
|
||||
surface:wgpu::Surface<'window>,
|
||||
config:wgpu::SurfaceConfiguration,
|
||||
)->Self{
|
||||
Self{surface,config}
|
||||
}
|
||||
#[must_use]
|
||||
pub fn new_frame(&self,device:&wgpu::Device)->Result<Frame,FrameError>{
|
||||
let frame=match self.surface.get_current_texture(){
|
||||
wgpu::CurrentSurfaceTexture::Success(surface_texture)=>surface_texture,
|
||||
wgpu::CurrentSurfaceTexture::Suboptimal(surface_texture)=>{
|
||||
self.surface.configure(device,&self.config);
|
||||
surface_texture
|
||||
},
|
||||
wgpu::CurrentSurfaceTexture::Outdated=>{
|
||||
self.surface.configure(device,&self.config);
|
||||
match self.surface.get_current_texture(){
|
||||
wgpu::CurrentSurfaceTexture::Success(surface_texture)=>surface_texture,
|
||||
_=>panic!("Failed to acquire next surface texture!"),
|
||||
}
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Timeout
|
||||
|wgpu::CurrentSurfaceTexture::Occluded=>return Err(FrameError::Skip),
|
||||
wgpu::CurrentSurfaceTexture::Lost=>return Err(FrameError::DeviceLost),
|
||||
wgpu::CurrentSurfaceTexture::Validation=>unreachable!(),
|
||||
};
|
||||
let view=frame.texture.create_view(&wgpu::TextureViewDescriptor{
|
||||
format:Some(self.config.view_formats[0]),
|
||||
..wgpu::TextureViewDescriptor::default()
|
||||
});
|
||||
Ok(Frame{
|
||||
surface_texture:frame,
|
||||
view,
|
||||
})
|
||||
}
|
||||
pub const fn size(&self)->glam::UVec2{
|
||||
glam::uvec2(self.config.width,self.config.height)
|
||||
}
|
||||
pub fn view_format(&self)->wgpu::TextureFormat{
|
||||
self.config.view_formats[0]
|
||||
}
|
||||
pub fn configure(&mut self,device:&wgpu::Device,size:glam::UVec2){
|
||||
self.config.width=size.x.max(1);
|
||||
self.config.height=size.y.max(1);
|
||||
self.surface.configure(device,&self.config);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
[package]
|
||||
name = "strafesnet_physics"
|
||||
version = "0.1.0"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
arrayvec = "0.7.6"
|
||||
glam = "0.30.0"
|
||||
glam.workspace = true
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
||||
mlua = { version = "0.11.5", features = ["luau"] }
|
||||
strafesnet_common.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -2,12 +2,18 @@ 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!
|
||||
pub position:Planar64Vec3,
|
||||
pub velocity:Planar64Vec3,
|
||||
pub time:Time<T>,
|
||||
}
|
||||
impl<T> std::ops::Neg for Body<T>{
|
||||
#[derive(Clone,Copy,Debug,Hash)]
|
||||
pub struct Trajectory<T>{
|
||||
pub position:Planar64Vec3,
|
||||
pub velocity:Planar64Vec3,
|
||||
pub acceleration:Planar64Vec3,
|
||||
pub time:Time<T>,
|
||||
}
|
||||
impl<T> std::ops::Neg for Trajectory<T>{
|
||||
type Output=Self;
|
||||
fn neg(self)->Self::Output{
|
||||
Self{
|
||||
@@ -18,10 +24,10 @@ impl<T> std::ops::Neg for Body<T>{
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T:Copy> std::ops::Neg for &Body<T>{
|
||||
type Output=Body<T>;
|
||||
impl<T:Copy> std::ops::Neg for &Trajectory<T>{
|
||||
type Output=Trajectory<T>;
|
||||
fn neg(self)->Self::Output{
|
||||
Body{
|
||||
Trajectory{
|
||||
position:self.position,
|
||||
velocity:-self.velocity,
|
||||
acceleration:self.acceleration,
|
||||
@@ -33,7 +39,33 @@ impl<T:Copy> std::ops::Neg for &Body<T>{
|
||||
impl<T> Body<T>
|
||||
where Time<T>:Copy,
|
||||
{
|
||||
pub const ZERO:Self=Self::new(vec3::ZERO,vec3::ZERO,vec3::ZERO,Time::ZERO);
|
||||
pub const ZERO:Self=Self::new(vec3::zero(),vec3::zero(),Time::ZERO);
|
||||
pub const fn new(position:Planar64Vec3,velocity:Planar64Vec3,time:Time<T>)->Self{
|
||||
Self{
|
||||
position,
|
||||
velocity,
|
||||
time,
|
||||
}
|
||||
}
|
||||
pub const fn with_acceleration(self,acceleration:Planar64Vec3)->Trajectory<T>{
|
||||
let Body{
|
||||
position,
|
||||
velocity,
|
||||
time,
|
||||
}=self;
|
||||
Trajectory{
|
||||
position,
|
||||
velocity,
|
||||
acceleration,
|
||||
time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Trajectory<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,
|
||||
@@ -42,29 +74,31 @@ impl<T> Body<T>
|
||||
time,
|
||||
}
|
||||
}
|
||||
pub const fn relative_to<'a>(&'a self,body0:&'a Body<T>)->VirtualBody<'a,T>{
|
||||
pub fn relative_to(&self,trj0:&Self,time:Time<T>)->Self{
|
||||
//(p0,v0,a0,t0)
|
||||
//(p1,v1,a1,t1)
|
||||
VirtualBody{
|
||||
body0,
|
||||
body1:self,
|
||||
}
|
||||
Trajectory::new(
|
||||
self.extrapolated_position(time)-trj0.extrapolated_position(time),
|
||||
self.extrapolated_velocity(time)-trj0.extrapolated_velocity(time),
|
||||
self.acceleration-trj0.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().clamp_1())
|
||||
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().clamp_1())
|
||||
+(self.velocity*dt).map(|elem|elem.divide().clamp_64())
|
||||
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().clamp_64())
|
||||
}
|
||||
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
|
||||
let dt=time-self.time;
|
||||
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().clamp_1())
|
||||
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().clamp_64())
|
||||
}
|
||||
pub fn advance_time(&mut self,time:Time<T>){
|
||||
println!("advance_time");
|
||||
self.position=self.extrapolated_position(time);
|
||||
self.velocity=self.extrapolated_velocity(time);
|
||||
self.time=time;
|
||||
pub fn extrapolated_body(&self,time:Time<T>)->Body<T>{
|
||||
Body::new(
|
||||
self.extrapolated_position(time),
|
||||
self.extrapolated_velocity(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
|
||||
@@ -102,15 +136,16 @@ impl<T> Body<T>
|
||||
// a*dt + v
|
||||
self.acceleration.map(|elem|(dt*elem).divide().clamp())+self.velocity
|
||||
}
|
||||
pub fn advance_time_ratio_dt(&mut self,dt:crate::model::GigaTime){
|
||||
println!("advance_time_ratio_dt");
|
||||
self.position=self.extrapolated_position_ratio_dt(dt);
|
||||
self.velocity=self.extrapolated_velocity_ratio_dt(dt);
|
||||
self.time+=dt.into();
|
||||
pub fn extrapolated_body_ratio_dt(&self,dt:crate::model::GigaTime)->Body<T>{
|
||||
Body::new(
|
||||
self.extrapolated_position_ratio_dt(dt),
|
||||
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{
|
||||
if self.velocity==vec3::zero(){
|
||||
if self.acceleration==vec3::zero(){
|
||||
None
|
||||
}else{
|
||||
Some(self.acceleration)
|
||||
@@ -146,28 +181,12 @@ impl<T> Body<T>
|
||||
|
||||
}
|
||||
impl<T> std::fmt::Display for Body<T>{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"p({}) v({}) t({})",self.position,self.velocity,self.time)
|
||||
}
|
||||
}
|
||||
impl<T> std::fmt::Display for Trajectory<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 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
use crate::model::{into_giga_time,GigaTime,FEV,MeshQuery,DirectedEdge};
|
||||
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
|
||||
use crate::physics::{Time,Body};
|
||||
use crate::model::{into_giga_time,GigaTime};
|
||||
use strafesnet_common::integer::fixed_types::{F64_32,F128_64,F256_128};
|
||||
use strafesnet_common::integer::vec3::Vector3;
|
||||
use strafesnet_common::integer::{Ratio,Planar64Vec3};
|
||||
use crate::physics::{Time,Trajectory};
|
||||
use crate::mesh_query::{FEV,DirectedEdge,MeshQuery,MeshTopology};
|
||||
|
||||
use core::ops::Bound;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Transition<M:MeshQuery>{
|
||||
enum Transition<M:MeshTopology>{
|
||||
Miss,
|
||||
Next(FEV<M>,GigaTime),
|
||||
Hit(M::Face,GigaTime),
|
||||
}
|
||||
|
||||
pub enum CrawlResult<M:MeshQuery>{
|
||||
pub enum CrawlResult<M:MeshTopology>{
|
||||
Miss(FEV<M>),
|
||||
Hit(M::Face,GigaTime),
|
||||
}
|
||||
impl<M:MeshQuery> CrawlResult<M>{
|
||||
impl<M:MeshTopology> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move predict_collision_face_out algorithm in here or something
|
||||
@@ -71,19 +67,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=F256_128,Position=Planar64Vec3,Direction=Planar64Vec3>> FEV<M>
|
||||
where
|
||||
// This is hardcoded for MinkowskiMesh lol
|
||||
M::Face:Copy,
|
||||
M::Edge:Copy,
|
||||
M::DirectedEdge:Copy,
|
||||
M::Vert:Copy,
|
||||
M:std::fmt::Debug,
|
||||
F:std::fmt::Display,
|
||||
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>,
|
||||
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum,
|
||||
M::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
|
||||
F:core::ops::Mul<F64_32,Output=F256_128>,
|
||||
<F as core::ops::Mul<F64_32>>::Output:core::iter::Sum,
|
||||
M::Offset:core::ops::Sub<<F as std::ops::Mul<F64_32>>::Output>,
|
||||
{
|
||||
fn next_transition(&self,mesh:&M,body:&Body,lower_bound:Bound<GigaTime>,mut upper_bound:Bound<GigaTime>)->Transition<M>{
|
||||
fn next_transition(&self,mesh:&M,trajectory:&Trajectory,lower_bound:Bound<GigaTime>,mut upper_bound:Bound<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;
|
||||
@@ -93,36 +88,31 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
//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);
|
||||
println!("Face n={} d={}",n,d);
|
||||
//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)){
|
||||
let low=low(&lower_bound,&dt);
|
||||
let upp=upp(&dt,&upper_bound);
|
||||
let into=n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative();
|
||||
println!("dt={} low={low} upp={upp} into={into}",dt.divide());
|
||||
if low&&upp&&into{
|
||||
for dt in F256_128::zeroes2((n.dot(trajectory.position)-d)*2,n.dot(trajectory.velocity)*2,n.dot(trajectory.acceleration)){
|
||||
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
upper_bound=Bound::Included(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).as_ref(){
|
||||
mesh.for_each_face_edge(face_id,|directed_edge_id|{
|
||||
let edge_n=mesh.directed_edge_n(directed_edge_id);
|
||||
let n=n.cross(edge_n);
|
||||
let &[v0,v1]=mesh.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
||||
//WARNING: d is moved out of the *2 block because of adding two vertices!
|
||||
//WARNING: precision is swept under the rug!
|
||||
//wrap for speed
|
||||
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(v0)+mesh.vert(v1))).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
|
||||
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
for dt in F256_128::zeroes2(n.dot(trajectory.position*2-(mesh.vert(v0)+mesh.vert(v1))).wrap_256(),n.dot(trajectory.velocity).wrap_256()*2,n.dot(trajectory.acceleration).wrap_256()){
|
||||
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
upper_bound=Bound::Included(dt);
|
||||
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
//if none:
|
||||
},
|
||||
&FEV::Edge(edge_id)=>{
|
||||
@@ -131,21 +121,15 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
let &[ev0,ev1]=edge_verts.as_ref();
|
||||
let (v0,v1)=(mesh.vert(ev0),mesh.vert(ev1));
|
||||
let edge_n=v1-v0;
|
||||
let delta_pos=body.position*2-(v0+v1);
|
||||
let delta_pos=trajectory.position*2-(v0+v1);
|
||||
for (i,&edge_face_id) in mesh.edge_faces(edge_id).as_ref().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);
|
||||
let d=n.dot(delta_pos).wrap_4();
|
||||
println!("Edge Face={:?} boundary_n={} boundary_d={}",edge_face_id,n,d>>1);
|
||||
//WARNING yada yada d *2
|
||||
//wrap for speed
|
||||
for dt in Fixed::<4,128>::zeroes2(d,n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
|
||||
let low=low(&lower_bound,&dt);
|
||||
let upp=upp(&dt,&upper_bound);
|
||||
let into=n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative();
|
||||
println!("dt={} low={low} upp={upp} into={into}",dt.divide());
|
||||
if low&&upp&&into{
|
||||
for dt in F256_128::zeroes2(n.dot(delta_pos).wrap_256(),n.dot(trajectory.velocity).wrap_256()*2,n.dot(trajectory.acceleration).wrap_256()){
|
||||
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
upper_bound=Bound::Included(dt);
|
||||
best_transition=Transition::Next(FEV::Face(edge_face_id),dt);
|
||||
break;
|
||||
@@ -156,9 +140,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
for (i,&vert_id) in edge_verts.as_ref().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 low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
|
||||
for dt in F128_64::zeroes2((n.dot(trajectory.position-mesh.vert(vert_id)))*2,n.dot(trajectory.velocity)*2,n.dot(trajectory.acceleration)){
|
||||
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
let dt=Ratio::new(dt.num.widen_256(),dt.den.widen_256());
|
||||
upper_bound=Bound::Included(dt);
|
||||
best_transition=Transition::Next(FEV::Vert(vert_id),dt);
|
||||
break;
|
||||
@@ -169,31 +153,28 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
|
||||
},
|
||||
&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).as_ref(){
|
||||
mesh.for_each_vert_edge(vert_id,|directed_edge_id|{
|
||||
//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 low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
|
||||
for dt in F128_64::zeroes2((n.dot(trajectory.position-mesh.vert(vert_id)))*2,n.dot(trajectory.velocity)*2,n.dot(trajectory.acceleration)){
|
||||
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
let dt=Ratio::new(dt.num.widen_256(),dt.den.widen_256());
|
||||
upper_bound=Bound::Included(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,lower_bound:Bound<&Time>,upper_bound:Bound<&Time>)->CrawlResult<M>{
|
||||
let mut lower_bound=lower_bound.map(|&t|into_giga_time(t,relative_body.time));
|
||||
let upper_bound=upper_bound.map(|&t|into_giga_time(t,relative_body.time));
|
||||
println!("crawl begin={self:?}");
|
||||
pub fn crawl(mut self,mesh:&M,trajectory:&Trajectory,lower_bound:Bound<&Time>,upper_bound:Bound<&Time>)->CrawlResult<M>{
|
||||
let mut lower_bound=lower_bound.map(|&t|into_giga_time(t,trajectory.time));
|
||||
let upper_bound=upper_bound.map(|&t|into_giga_time(t,trajectory.time));
|
||||
for _ in 0..20{
|
||||
let transition=self.next_transition(mesh,relative_body,lower_bound,upper_bound);
|
||||
println!("transition={transition:?}");
|
||||
match transition{
|
||||
match self.next_transition(mesh,trajectory,lower_bound,upper_bound){
|
||||
Transition::Miss=>return CrawlResult::Miss(self),
|
||||
Transition::Next(next_fev,next_time)=>(self,lower_bound)=(next_fev,Bound::Included(next_time)),
|
||||
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
mod body;
|
||||
mod push_solve;
|
||||
mod face_crawler;
|
||||
mod mesh_query;
|
||||
mod minkowski;
|
||||
mod model;
|
||||
mod push_solve;
|
||||
mod minimum_difference;
|
||||
mod minimum_difference_lua;
|
||||
|
||||
pub mod physics;
|
||||
|
||||
|
||||
57
engine/physics/src/mesh_query.rs
Normal file
57
engine/physics/src/mesh_query.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
#[derive(Debug)]
|
||||
pub enum FEV<M:MeshTopology>{
|
||||
Vert(M::Vert),
|
||||
Edge(M::Edge),
|
||||
Face(M::Face),
|
||||
}
|
||||
|
||||
pub trait UndirectedEdge{
|
||||
type DirectedEdge:DirectedEdge<UndirectedEdge=Self>;
|
||||
fn as_directed(self,parity:bool)->Self::DirectedEdge;
|
||||
}
|
||||
pub trait DirectedEdge{
|
||||
type UndirectedEdge:UndirectedEdge<DirectedEdge=Self>;
|
||||
fn as_undirected(self)->Self::UndirectedEdge;
|
||||
fn parity(&self)->bool;
|
||||
fn reverse(self)->Self
|
||||
where
|
||||
Self:Sized
|
||||
{
|
||||
let parity=!self.parity();
|
||||
self.as_undirected().as_directed(parity)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MeshTopology{
|
||||
type Face;
|
||||
type Edge:UndirectedEdge<DirectedEdge=Self::DirectedEdge>;
|
||||
type DirectedEdge:DirectedEdge<UndirectedEdge=Self::Edge>;
|
||||
type Vert;
|
||||
fn for_each_vert_edge(&self,vert_id:Self::Vert,f:impl FnMut(Self::DirectedEdge));
|
||||
fn for_each_vert_face(&self,vert_id:Self::Vert,f:impl FnMut(Self::Face));
|
||||
fn edge_faces(&self,edge_id:Self::Edge)->impl AsRef<[Self::Face;2]>;
|
||||
fn edge_verts(&self,edge_id:Self::Edge)->impl AsRef<[Self::Vert;2]>;
|
||||
#[expect(unused)]
|
||||
fn for_each_face_vert(&self,face_id:Self::Face,f:impl FnMut(Self::Vert));
|
||||
fn for_each_face_edge(&self,face_id:Self::Face,f:impl FnMut(Self::DirectedEdge));
|
||||
}
|
||||
|
||||
// Make face_nd d value relative
|
||||
// euclidean point?
|
||||
// Simplex physics
|
||||
// Directed edge necessary?
|
||||
// recursive for_each function calls
|
||||
// define faces from vertices (Fixed<2> vs Fixed<3>)
|
||||
pub trait MeshQuery:MeshTopology{
|
||||
type Position;
|
||||
type Direction;
|
||||
type Normal;
|
||||
type Offset;
|
||||
fn vert(&self,vert_id:Self::Vert)->Self::Position;
|
||||
fn farthest_vert(&self,dir:Self::Direction)->Self::Vert;
|
||||
/// This must return a point inside the mesh.
|
||||
fn hint_point(&self)->Self::Position;
|
||||
fn face_nd(&self,face_id:Self::Face)->(Self::Normal,Self::Offset);
|
||||
fn edge_n(&self,edge_id:Self::Edge)->Self::Direction;
|
||||
fn directed_edge_n(&self,directed_edge_id:Self::DirectedEdge)->Self::Direction;
|
||||
}
|
||||
962
engine/physics/src/minimum_difference.rs
Normal file
962
engine/physics/src/minimum_difference.rs
Normal file
@@ -0,0 +1,962 @@
|
||||
use strafesnet_common::integer::fixed_types::{F128_64,F192_96,F256_128};
|
||||
use strafesnet_common::integer::vec3;
|
||||
use strafesnet_common::integer::vec3::Vector3;
|
||||
use strafesnet_common::integer::{Fixed,Planar64,Planar64Vec3};
|
||||
|
||||
use crate::mesh_query::{FEV,DirectedEdge,MeshQuery,MeshTopology};
|
||||
// TODO: remove mesh invert
|
||||
use crate::minkowski::{MinkowskiMesh,MinkowskiVert};
|
||||
|
||||
// This algorithm is based on Lua code
|
||||
// written by Trey Reynolds in 2021
|
||||
|
||||
type Simplex<const N:usize,Vert>=[Vert;N];
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
enum Simplex1_3<Vert>{
|
||||
Simplex1(Simplex<1,Vert>),
|
||||
Simplex2(Simplex<2,Vert>),
|
||||
Simplex3(Simplex<3,Vert>),
|
||||
}
|
||||
impl<Vert> Simplex1_3<Vert>{
|
||||
fn push_front(self,v:Vert)->Simplex2_4<Vert>{
|
||||
match self{
|
||||
Simplex1_3::Simplex1([v0])=>Simplex2_4::Simplex2([v,v0]),
|
||||
Simplex1_3::Simplex2([v0,v1])=>Simplex2_4::Simplex3([v,v0,v1]),
|
||||
Simplex1_3::Simplex3([v0,v1,v2])=>Simplex2_4::Simplex4([v,v0,v1,v2]),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Copy)]
|
||||
enum Simplex2_4<Vert>{
|
||||
Simplex2(Simplex<2,Vert>),
|
||||
Simplex3(Simplex<3,Vert>),
|
||||
Simplex4(Simplex<4,Vert>),
|
||||
}
|
||||
|
||||
/*
|
||||
local function absDet(r, u, v, w)
|
||||
if w then
|
||||
return math.abs((u - r):Cross(v - r):Dot(w - r))
|
||||
elseif v then
|
||||
return (u - r):Cross(v - r).magnitude
|
||||
elseif u then
|
||||
return (u - r).magnitude
|
||||
else
|
||||
return 1
|
||||
end
|
||||
end
|
||||
*/
|
||||
impl<Vert> Simplex2_4<Vert>{
|
||||
fn det_is_zero<M:MeshQuery<Vert=Vert,Position=Planar64Vec3>>(self,mesh:&M)->bool{
|
||||
match self{
|
||||
Self::Simplex4([p0,p1,p2,p3])=>{
|
||||
let p0=mesh.vert(p0);
|
||||
let p1=mesh.vert(p1);
|
||||
let p2=mesh.vert(p2);
|
||||
let p3=mesh.vert(p3);
|
||||
(p1-p0).cross(p2-p0).dot(p3-p0)==Fixed::ZERO
|
||||
},
|
||||
Self::Simplex3([p0,p1,p2])=>{
|
||||
let p0=mesh.vert(p0);
|
||||
let p1=mesh.vert(p1);
|
||||
let p2=mesh.vert(p2);
|
||||
(p1-p0).cross(p2-p0)==vec3::zero()
|
||||
},
|
||||
Self::Simplex2([p0,p1])=>{
|
||||
let p0=mesh.vert(p0);
|
||||
let p1=mesh.vert(p1);
|
||||
p1-p0==vec3::zero()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
local function choosePerpendicularDirection(d)
|
||||
local x, y, z = d.x, d.y, d.z
|
||||
local best = math.min(x*x, y*y, z*z)
|
||||
if x*x == best then
|
||||
return Vector3.new(y*y + z*z, -x*y, -x*z)
|
||||
elseif y*y == best then
|
||||
return Vector3.new(-x*y, x*x + z*z, -y*z)
|
||||
else
|
||||
return Vector3.new(-x*z, -y*z, x*x + y*y)
|
||||
end
|
||||
end
|
||||
*/
|
||||
fn choose_perpendicular_direction(d:Planar64Vec3)->Planar64Vec3{
|
||||
let x=d.x.abs();
|
||||
let y=d.y.abs();
|
||||
let z=d.z.abs();
|
||||
if x<y&&x<z{
|
||||
Vector3::new([Fixed::ZERO,-d.z,d.y])
|
||||
}else if y<z{
|
||||
Vector3::new([d.z,Fixed::ZERO,-d.x])
|
||||
}else{
|
||||
Vector3::new([-d.y,d.x,Fixed::ZERO])
|
||||
}
|
||||
}
|
||||
|
||||
const fn choose_any_direction()->Planar64Vec3{
|
||||
vec3::X
|
||||
}
|
||||
|
||||
fn narrow_dir2(dir:Vector3<F128_64>)->Planar64Vec3{
|
||||
if dir==vec3::zero(){
|
||||
return dir.narrow_64().unwrap();
|
||||
}
|
||||
let x=dir.x.as_bits().unsigned_abs().bit_width();
|
||||
let y=dir.y.as_bits().unsigned_abs().bit_width();
|
||||
let z=dir.z.as_bits().unsigned_abs().bit_width();
|
||||
let big=x.max(y).max(z);
|
||||
const MAX_BITS:u32=64+31;
|
||||
if MAX_BITS<big{
|
||||
dir>>(big-MAX_BITS)
|
||||
}else{
|
||||
dir
|
||||
}.narrow_64().unwrap()
|
||||
}
|
||||
fn narrow_dir3(dir:Vector3<F192_96>)->Planar64Vec3{
|
||||
if dir==vec3::zero(){
|
||||
return dir.narrow_64().unwrap();
|
||||
}
|
||||
let x=dir.x.as_bits().unsigned_abs().bit_width();
|
||||
let y=dir.y.as_bits().unsigned_abs().bit_width();
|
||||
let z=dir.z.as_bits().unsigned_abs().bit_width();
|
||||
let big=x.max(y).max(z);
|
||||
const MAX_BITS:u32=96+31;
|
||||
if MAX_BITS<big{
|
||||
dir>>(big-MAX_BITS)
|
||||
}else{
|
||||
dir
|
||||
}.narrow_64().unwrap()
|
||||
}
|
||||
|
||||
fn reduce1<M:MeshQuery<Position=Planar64Vec3>>(
|
||||
[v0]:Simplex<1,M::Vert>,
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduced<M::Vert>
|
||||
where M::Vert:Copy,
|
||||
{
|
||||
println!("reduce1");
|
||||
// --debug.profilebegin("reduceSimplex0")
|
||||
// local a = a1 - a0
|
||||
let p0=mesh.vert(v0);
|
||||
|
||||
println!("p0={p0}");
|
||||
// local p = -a
|
||||
let p=-(p0+point);
|
||||
println!("p={p}");
|
||||
|
||||
// local direction = p
|
||||
let mut dir=p;
|
||||
|
||||
// if direction.magnitude == 0 then
|
||||
// direction = chooseAnyDirection()
|
||||
if dir==vec3::zero(){
|
||||
dir=choose_any_direction();
|
||||
}
|
||||
|
||||
// return direction, a0, a1
|
||||
Reduced{
|
||||
dir,
|
||||
simplex:Simplex1_3::Simplex1([v0]),
|
||||
}
|
||||
}
|
||||
|
||||
// local function reduceSimplex1(a0, a1, b0, b1)
|
||||
fn reduce2<M:MeshQuery<Position=Planar64Vec3>>(
|
||||
[v0,v1]:Simplex<2,M::Vert>,
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduced<M::Vert>
|
||||
where
|
||||
M::Vert:Copy
|
||||
{
|
||||
println!("reduce2");
|
||||
// --debug.profilebegin("reduceSimplex1")
|
||||
// local a = a1 - a0
|
||||
// local b = b1 - b0
|
||||
let p0=mesh.vert(v0);
|
||||
let p1=mesh.vert(v1);
|
||||
|
||||
// local p = -a
|
||||
// local u = b - a
|
||||
let p=-(p0+point);
|
||||
let u=p1-p0;
|
||||
|
||||
// -- modify to take into account the radiuses
|
||||
// local p_u = p:Dot(u)
|
||||
let p_u=p.dot(u);
|
||||
|
||||
// if p_u >= 0 then
|
||||
if !p_u.is_negative(){
|
||||
// local direction = u:Cross(p):Cross(u)
|
||||
let direction=u.cross(p).cross(u);
|
||||
|
||||
// if direction.magnitude == 0 then
|
||||
if direction==vec3::zero(){
|
||||
return Reduced{
|
||||
dir:choose_perpendicular_direction(u),
|
||||
simplex:Simplex1_3::Simplex2([v0,v1]),
|
||||
};
|
||||
}
|
||||
|
||||
// -- modify the direction to take into account a0R and b0R
|
||||
// return direction, a0, a1, b0, b1
|
||||
return Reduced{
|
||||
dir:narrow_dir3(direction),
|
||||
simplex:Simplex1_3::Simplex2([v0,v1]),
|
||||
};
|
||||
}
|
||||
|
||||
// local direction = p
|
||||
let mut dir=p;
|
||||
|
||||
// if direction.magnitude == 0 then
|
||||
if dir==vec3::zero(){
|
||||
dir=choose_perpendicular_direction(u);
|
||||
}
|
||||
|
||||
// return direction, a0, a1
|
||||
Reduced{
|
||||
dir,
|
||||
simplex:Simplex1_3::Simplex1([v0]),
|
||||
}
|
||||
}
|
||||
|
||||
// local function reduceSimplex2(a0, a1, b0, b1, c0, c1)
|
||||
fn reduce3<M:MeshQuery<Position=Planar64Vec3>>(
|
||||
[v0,mut v1,v2]:Simplex<3,M::Vert>,
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduced<M::Vert>
|
||||
where
|
||||
M::Vert:Copy
|
||||
{
|
||||
println!("reduce3");
|
||||
// --debug.profilebegin("reduceSimplex2")
|
||||
// local a = a1 - a0
|
||||
// local b = b1 - b0
|
||||
// local c = c1 - c0
|
||||
let p0=mesh.vert(v0);
|
||||
let p1=mesh.vert(v1);
|
||||
let p2=mesh.vert(v2);
|
||||
|
||||
// local p = -a
|
||||
// local u = b - a
|
||||
// local v = c - a
|
||||
let p=-(p0+point);
|
||||
let mut u=p1-p0;
|
||||
let v=p2-p0;
|
||||
|
||||
// local uv = u:Cross(v)
|
||||
// local up = u:Cross(p)
|
||||
// local pv = p:Cross(v)
|
||||
// local uv_up = uv:Dot(up)
|
||||
// local uv_pv = uv:Dot(pv)
|
||||
let mut uv=u.cross(v);
|
||||
let mut up=u.cross(p);
|
||||
let pv=p.cross(v);
|
||||
let uv_up=uv.dot(up);
|
||||
let uv_pv=uv.dot(pv);
|
||||
|
||||
// if uv_up >= 0 and uv_pv >= 0 then
|
||||
if !uv_up.is_negative()&&!uv_pv.is_negative(){
|
||||
// local uvp = uv:Dot(p)
|
||||
let uvp=uv.dot(p);
|
||||
|
||||
// local direction = uvp < 0 and -uv or uv
|
||||
let direction=if uvp.is_negative(){
|
||||
-uv
|
||||
}else{
|
||||
uv
|
||||
};
|
||||
|
||||
// return direction, a0, a1, b0, b1, c0, c1
|
||||
return Reduced{
|
||||
dir:narrow_dir2(direction),
|
||||
simplex:Simplex1_3::Simplex3([v0,v1,v2]),
|
||||
};
|
||||
}
|
||||
|
||||
// local u_u = u:Dot(u)
|
||||
// local v_v = v:Dot(v)
|
||||
// local uDist = uv_up/(u_u*v.magnitude)
|
||||
// local vDist = uv_pv/(v_v*u.magnitude)
|
||||
// local minDist2 = math.min(uDist, vDist)
|
||||
let u_dist=uv_up*v.length();
|
||||
let v_dist=uv_pv*u.length();
|
||||
|
||||
// if vDist == minDist2 then
|
||||
if v_dist<u_dist{
|
||||
u=v;
|
||||
up=-pv;
|
||||
uv=-uv;
|
||||
// b0 = c0
|
||||
// b1 = c1
|
||||
v1=v2;
|
||||
}
|
||||
|
||||
// local p_u = p:Dot(u)
|
||||
let p_u=p.dot(u);
|
||||
|
||||
// if p_u >= 0 then
|
||||
if !p_u.is_negative(){
|
||||
// local direction = up:Cross(u)
|
||||
let direction=up.cross(u);
|
||||
// if direction.magnitude == 0 then
|
||||
if direction==vec3::zero(){
|
||||
// direction = uv
|
||||
return Reduced{
|
||||
dir:narrow_dir2(uv),
|
||||
simplex:Simplex1_3::Simplex2([v0,v1]),
|
||||
};
|
||||
}
|
||||
|
||||
// return direction, a0, a1, b0, b1
|
||||
return Reduced{
|
||||
dir:narrow_dir3(direction),
|
||||
simplex:Simplex1_3::Simplex2([v0,v1]),
|
||||
};
|
||||
}
|
||||
|
||||
// local direction = p
|
||||
let dir=p;
|
||||
// if direction.magnitude == 0 then
|
||||
if dir==vec3::zero(){
|
||||
// direction = uv
|
||||
return Reduced{
|
||||
dir:narrow_dir2(uv),
|
||||
simplex:Simplex1_3::Simplex1([v0]),
|
||||
};
|
||||
}
|
||||
// return direction, a0, a0
|
||||
Reduced{
|
||||
dir,
|
||||
simplex:Simplex1_3::Simplex1([v0]),
|
||||
}
|
||||
}
|
||||
|
||||
// local function reduceSimplex3(a0, a1, b0, b1, c0, c1, d0, d1)
|
||||
fn reduce4<M:MeshQuery<Position=Planar64Vec3>>(
|
||||
[v0,mut v1,mut v2,v3]:Simplex<4,M::Vert>,
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
)->Reduce<M::Vert>
|
||||
where
|
||||
M::Vert:Copy
|
||||
{
|
||||
println!("reduce4");
|
||||
// --debug.profilebegin("reduceSimplex3")
|
||||
// local a = a1 - a0
|
||||
// local b = b1 - b0
|
||||
// local c = c1 - c0
|
||||
// local d = d1 - d0
|
||||
let p0=mesh.vert(v0);
|
||||
let p1=mesh.vert(v1);
|
||||
let p2=mesh.vert(v2);
|
||||
let p3=mesh.vert(v3);
|
||||
|
||||
// local p = -a
|
||||
// local u = b - a
|
||||
// local v = c - a
|
||||
// local w = d - a
|
||||
let p=-(p0+point);
|
||||
let mut u=p1-p0;
|
||||
let mut v=p2-p0;
|
||||
let w=p3-p0;
|
||||
|
||||
// local uv = u:Cross(v)
|
||||
// local vw = v:Cross(w)
|
||||
// local wu = w:Cross(u)
|
||||
// local uvw = uv:Dot(w)
|
||||
// local pvw = vw:Dot(p)
|
||||
// local upw = wu:Dot(p)
|
||||
// local uvp = uv:Dot(p)
|
||||
let mut uv=u.cross(v);
|
||||
let vw=v.cross(w);
|
||||
let wu=w.cross(u);
|
||||
let uv_w=uv.dot(w);
|
||||
let pv_w=vw.dot(p);
|
||||
let up_w=wu.dot(p);
|
||||
let uv_p=uv.dot(p);
|
||||
|
||||
// if pvw/uvw >= 0 and upw/uvw >= 0 and uvp/uvw >= 0 then
|
||||
if !pv_w.div_sign(uv_w).is_negative()
|
||||
&&!up_w.div_sign(uv_w).is_negative()
|
||||
&&!uv_p.div_sign(uv_w).is_negative(){
|
||||
// origin is contained, this is a positive detection
|
||||
// local direction = Vector3.new(0, 0, 0)
|
||||
// return direction, a0, a1, b0, b1, c0, c1, d0, d1
|
||||
return Reduce::Escape([v0,v1,v2,v3]);
|
||||
}
|
||||
|
||||
// local uvwSign = uvw < 0 and -1 or uvw > 0 and 1 or 0
|
||||
// local uvDist = uvp*uvwSign/uv.magnitude
|
||||
// local vwDist = pvw*uvwSign/vw.magnitude
|
||||
// local wuDist = upw*uvwSign/wu.magnitude
|
||||
// local minDist3 = math.min(uvDist, vwDist, wuDist)
|
||||
let uv_dist=uv_p.mul_sign(uv_w);
|
||||
let vw_dist=pv_w.mul_sign(uv_w);
|
||||
let wu_dist=up_w.mul_sign(uv_w);
|
||||
let wu_len=wu.length();
|
||||
let uv_len=uv.length();
|
||||
let vw_len=vw.length();
|
||||
|
||||
if vw_dist*wu_len<wu_dist*vw_len{
|
||||
// if vwDist == minDist3 then
|
||||
if vw_dist*uv_len<uv_dist*vw_len{
|
||||
(u,v)=(v,w);
|
||||
uv=vw;
|
||||
// uv_p=pv_w; // unused
|
||||
// b0, c0 = c0, d0
|
||||
// b1, c1 = c1, d1
|
||||
(v1,v2)=(v2,v3);
|
||||
}
|
||||
}else{
|
||||
// elseif wuDist == minDist3 then
|
||||
if wu_dist*uv_len<uv_dist*wu_len{
|
||||
(u,v)=(w,u);
|
||||
uv=wu;
|
||||
// uv_p=up_w; // unused
|
||||
// b0, c0 = d0, b0
|
||||
// b1, c1 = d1, b1
|
||||
// before [a,b,c,d]
|
||||
(v1,v2)=(v3,v1);
|
||||
// after [a,d,b]
|
||||
}
|
||||
}
|
||||
|
||||
// local up = u:Cross(p)
|
||||
// local pv = p:Cross(v)
|
||||
// local uv_up = uv:Dot(up)
|
||||
// local uv_pv = uv:Dot(pv)
|
||||
let mut up=u.cross(p);
|
||||
let pv=p.cross(v);
|
||||
let uv_up=uv.dot(up);
|
||||
let uv_pv=uv.dot(pv);
|
||||
|
||||
// if uv_up >= 0 and uv_pv >= 0 then
|
||||
if !uv_up.is_negative()&&!uv_pv.is_negative(){
|
||||
// local direction = uvw < 0 and uv or -uv
|
||||
// return direction, a0, a1, b0, b1, c0, c1
|
||||
let dir=if uv_w.is_negative(){
|
||||
narrow_dir2(uv)
|
||||
}else{
|
||||
narrow_dir2(-uv)
|
||||
};
|
||||
return Reduce::Reduced(Reduced{
|
||||
dir,
|
||||
simplex:Simplex1_3::Simplex3([v0,v1,v2]),
|
||||
});
|
||||
}
|
||||
|
||||
// local u_u = u:Dot(u)
|
||||
// local v_v = v:Dot(v)
|
||||
// local uDist = uv_up/(u_u*v.magnitude)
|
||||
// local vDist = uv_pv/(v_v*u.magnitude)
|
||||
// local minDist2 = math.min(uDist, vDist)
|
||||
let u_dist=uv_up*v.length();
|
||||
let v_dist=uv_pv*u.length();
|
||||
|
||||
// if vDist == minDist2 then
|
||||
if v_dist<u_dist{
|
||||
u=v;
|
||||
up=-pv;
|
||||
uv=-uv;
|
||||
// b0 = c0
|
||||
// b1 = c1
|
||||
v1=v2;
|
||||
}
|
||||
|
||||
// local p_u = p:Dot(u)
|
||||
let p_u=p.dot(u);
|
||||
|
||||
// if p_u >= 0 then
|
||||
if !p_u.is_negative(){
|
||||
// local direction = up:Cross(u)
|
||||
let direction=up.cross(u);
|
||||
// if direction.magnitude == 0 then
|
||||
if direction==vec3::zero(){
|
||||
// direction = uvw < 0 and uv or -uv
|
||||
// return direction, a0, a1, b0, b1
|
||||
let dir=if uv_w.is_negative(){
|
||||
narrow_dir2(uv)
|
||||
}else{
|
||||
narrow_dir2(-uv)
|
||||
};
|
||||
return Reduce::Reduced(Reduced{
|
||||
dir,
|
||||
simplex:Simplex1_3::Simplex2([v0,v1]),
|
||||
});
|
||||
}
|
||||
|
||||
// return direction, a0, a1, b0, b1
|
||||
return Reduce::Reduced(Reduced{
|
||||
dir:narrow_dir3(direction),
|
||||
simplex:Simplex1_3::Simplex2([v0,v1]),
|
||||
});
|
||||
}
|
||||
|
||||
// local direction = p
|
||||
let dir=p;
|
||||
// if direction.magnitude == 0 then
|
||||
if dir==vec3::zero(){
|
||||
// direction = uvw < 0 and uv or -uv
|
||||
let dir=if uv_w.is_negative(){
|
||||
narrow_dir2(uv)
|
||||
}else{
|
||||
narrow_dir2(-uv)
|
||||
};
|
||||
return Reduce::Reduced(Reduced{
|
||||
dir,
|
||||
simplex:Simplex1_3::Simplex1([v0]),
|
||||
});
|
||||
}
|
||||
|
||||
// return direction, a0, a1
|
||||
Reduce::Reduced(Reduced{
|
||||
dir,
|
||||
simplex:Simplex1_3::Simplex1([v0]),
|
||||
})
|
||||
}
|
||||
|
||||
struct Reduced<Vert>{
|
||||
dir:Planar64Vec3,
|
||||
simplex:Simplex1_3<Vert>,
|
||||
}
|
||||
|
||||
enum Reduce<Vert>{
|
||||
Escape(Simplex<4,Vert>),
|
||||
Reduced(Reduced<Vert>),
|
||||
}
|
||||
|
||||
impl<Vert> Simplex2_4<Vert>{
|
||||
fn reduce<M:MeshQuery<Vert=Vert,Position=Planar64Vec3>>(self,mesh:&M,point:Planar64Vec3)->Reduce<Vert>
|
||||
where
|
||||
M::Vert:Copy
|
||||
{
|
||||
match self{
|
||||
Self::Simplex2(simplex)=>Reduce::Reduced(reduce2(simplex,mesh,point)),
|
||||
Self::Simplex3(simplex)=>Reduce::Reduced(reduce3(simplex,mesh,point)),
|
||||
Self::Simplex4(simplex)=>reduce4(simplex,mesh,point),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//infinity fev algorithm state transition
|
||||
#[derive(Debug)]
|
||||
enum Transition<Vert>{
|
||||
Done,//found closest vert, no edges are better
|
||||
Vert(Vert),//transition to vert
|
||||
}
|
||||
enum EV<M:MeshTopology>{
|
||||
Vert(M::Vert),
|
||||
Edge(M::Edge),
|
||||
}
|
||||
impl<M:MeshTopology> From<EV<M>> for FEV<M>{
|
||||
fn from(value:EV<M>)->Self{
|
||||
match value{
|
||||
EV::Vert(minkowski_vert)=>FEV::Vert(minkowski_vert),
|
||||
EV::Edge(minkowski_edge)=>FEV::Edge(minkowski_edge),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait Contains{
|
||||
fn contains(&self,point:Planar64Vec3)->bool;
|
||||
}
|
||||
|
||||
// convenience type to check if a point is within some threshold of a plane.
|
||||
struct ThickPlane{
|
||||
point:Planar64Vec3,
|
||||
normal:Vector3<F128_64>,
|
||||
epsilon:F192_96,
|
||||
}
|
||||
impl ThickPlane{
|
||||
fn new<M:MeshQuery<Position=Planar64Vec3>>(mesh:&M,[v0,v1,v2]:Simplex<3,M::Vert>)->Self{
|
||||
let p0=mesh.vert(v0);
|
||||
let p1=mesh.vert(v1);
|
||||
let p2=mesh.vert(v2);
|
||||
let point=p0;
|
||||
let normal=(p1-p0).cross(p2-p0);
|
||||
// Allow ~ 2*sqrt(3) units of thickness on the plane
|
||||
// This is to account for the variance of two voxels across the longest diagonal
|
||||
let epsilon=(normal.length()*(Planar64::EPSILON*3)).wrap_192();
|
||||
Self{point,normal,epsilon}
|
||||
}
|
||||
}
|
||||
impl Contains for ThickPlane{
|
||||
fn contains(&self,point:Planar64Vec3)->bool{
|
||||
(point-self.point).dot(self.normal).abs()<=self.epsilon
|
||||
}
|
||||
}
|
||||
|
||||
struct ThickLine{
|
||||
point:Planar64Vec3,
|
||||
dir:Planar64Vec3,
|
||||
epsilon:F256_128,
|
||||
}
|
||||
impl ThickLine{
|
||||
fn new<M:MeshQuery<Position=Planar64Vec3>>(mesh:&M,[v0,v1]:Simplex<2,M::Vert>)->Self{
|
||||
let p0=mesh.vert(v0);
|
||||
let p1=mesh.vert(v1);
|
||||
let point=p0;
|
||||
let dir=p1-p0;
|
||||
// Allow ~ 2*sqrt(3) units of thickness on the plane
|
||||
// This is to account for the variance of two voxels across the longest diagonal
|
||||
let epsilon=(dir.length_squared()*(Planar64::EPSILON*3)).widen_256();
|
||||
Self{point,dir,epsilon}
|
||||
}
|
||||
}
|
||||
impl Contains for ThickLine{
|
||||
fn contains(&self,point:Planar64Vec3)->bool{
|
||||
(point-self.point).cross(self.dir).length_squared()<=self.epsilon
|
||||
}
|
||||
}
|
||||
|
||||
struct EVFinder<'a,M,C>{
|
||||
mesh:&'a M,
|
||||
constraint:C,
|
||||
best_distance_squared:F128_64,
|
||||
}
|
||||
|
||||
impl<M:MeshQuery<Position=Planar64Vec3>,C:Contains> EVFinder<'_,M,C>
|
||||
where
|
||||
M::Vert:Copy,
|
||||
M::DirectedEdge:Copy,
|
||||
{
|
||||
fn next_transition_vert(&mut self,vert_id:M::Vert,point:Planar64Vec3)->Transition<M::Vert>{
|
||||
let mut best_transition=Transition::Done;
|
||||
self.mesh.for_each_vert_edge(vert_id,|directed_edge_id|{
|
||||
//test if this edge's opposite vertex closer
|
||||
let edge_verts=self.mesh.edge_verts(directed_edge_id.as_undirected());
|
||||
//select opposite vertex
|
||||
let test_vert_id=edge_verts.as_ref()[directed_edge_id.parity() as usize];
|
||||
let test_pos=self.mesh.vert(test_vert_id);
|
||||
let diff=point-test_pos;
|
||||
let distance_squared=diff.dot(diff);
|
||||
// ensure test_vert_id is coplanar to simplex
|
||||
if distance_squared<self.best_distance_squared&&self.constraint.contains(test_pos){
|
||||
best_transition=Transition::Vert(test_vert_id);
|
||||
self.best_distance_squared=distance_squared;
|
||||
}
|
||||
});
|
||||
best_transition
|
||||
}
|
||||
fn final_ev(&mut self,vert_id:M::Vert,point:Planar64Vec3)->EV<M>{
|
||||
let mut best_transition=EV::Vert(vert_id);
|
||||
let vert_pos=self.mesh.vert(vert_id);
|
||||
let diff=point-vert_pos;
|
||||
self.mesh.for_each_vert_edge(vert_id,|directed_edge_id|{
|
||||
//test if this edge is closer
|
||||
let edge_verts=self.mesh.edge_verts(directed_edge_id.as_undirected());
|
||||
let test_vert_id=edge_verts.as_ref()[directed_edge_id.parity() as usize];
|
||||
let test_pos=self.mesh.vert(test_vert_id);
|
||||
let edge_n=test_pos-vert_pos;
|
||||
let d=edge_n.dot(diff);
|
||||
//test the edge
|
||||
let edge_nn=edge_n.dot(edge_n);
|
||||
// ensure edge contains closest point and directed_edge_id is coplanar to simplex
|
||||
if !d.is_negative()&&d<=edge_nn&&self.constraint.contains(test_pos){
|
||||
let distance_squared={
|
||||
let c=diff.cross(edge_n);
|
||||
//wrap for speed
|
||||
(c.dot(c)/edge_nn).divide().wrap_128()
|
||||
};
|
||||
if distance_squared<=self.best_distance_squared{
|
||||
best_transition=EV::Edge(directed_edge_id.as_undirected());
|
||||
self.best_distance_squared=distance_squared;
|
||||
}
|
||||
}
|
||||
});
|
||||
best_transition
|
||||
}
|
||||
fn crawl_boundaries(&mut self,mut vert_id:M::Vert,point:Planar64Vec3)->EV<M>
|
||||
where
|
||||
M::Vert:Copy
|
||||
{
|
||||
loop{
|
||||
match self.next_transition_vert(vert_id,point){
|
||||
Transition::Done=>return self.final_ev(vert_id,point),
|
||||
Transition::Vert(new_vert_id)=>vert_id=new_vert_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// This function hops along parallel vertices until it finds the EV which contains the closest point to `point`.
|
||||
fn crawl_to_closest_ev<M:MeshQuery<Position=Planar64Vec3>>(mesh:&M,simplex:Simplex<2,M::Vert>,point:Planar64Vec3)->EV<M>
|
||||
where
|
||||
M::Vert:Copy,
|
||||
M::DirectedEdge:Copy,
|
||||
{
|
||||
// naively start at the closest vertex
|
||||
// the closest vertex is not necessarily the one with the fewest boundary hops
|
||||
// but it doesn't matter, we will get there regardless.
|
||||
let (vert_id,best_distance_squared)=simplex.into_iter().map(|vert_id|{
|
||||
let diff=point-mesh.vert(vert_id);
|
||||
(vert_id,diff.dot(diff))
|
||||
}).min_by_key(|&(_,d)|d).unwrap();
|
||||
|
||||
let constraint=ThickLine::new(mesh,simplex);
|
||||
let mut finder=EVFinder{constraint,mesh,best_distance_squared};
|
||||
//start on any vertex
|
||||
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
|
||||
//cross edge-face boundary if it's uncrossable
|
||||
finder.crawl_boundaries(vert_id,point)
|
||||
}
|
||||
|
||||
/// This function hops along connected vertices until it finds the FEV which contains the closest point to `point`.
|
||||
fn crawl_to_closest_fev<'a>(mesh:&MinkowskiMesh<'a>,simplex:Simplex<3,MinkowskiVert>,point:Planar64Vec3)->FEV::<MinkowskiMesh<'a>>{
|
||||
// naively start at the closest vertex
|
||||
// the closest vertex is not necessarily the one with the fewest boundary hops
|
||||
// but it doesn't matter, we will get there regardless.
|
||||
let (vert_id,best_distance_squared)=simplex.into_iter().map(|vert_id|{
|
||||
let diff=point-mesh.vert(vert_id);
|
||||
(vert_id,diff.dot(diff))
|
||||
}).min_by_key(|&(_,d)|d).unwrap();
|
||||
|
||||
let constraint=ThickPlane::new(mesh,simplex);
|
||||
let mut finder=EVFinder{constraint,mesh,best_distance_squared};
|
||||
//start on any vertex
|
||||
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
|
||||
//cross edge-face boundary if it's uncrossable
|
||||
match finder.crawl_boundaries(vert_id,point){
|
||||
//if a vert is returned, it is the closest point to the infinity point
|
||||
EV::Vert(vert_id)=>FEV::Vert(vert_id),
|
||||
EV::Edge(edge_id)=>{
|
||||
//cross to face if we are on the wrong side
|
||||
let edge_n=mesh.edge_n(edge_id);
|
||||
// point is multiplied by two because vert_sum sums two vertices.
|
||||
let delta_pos=point*2-{
|
||||
let &[v0,v1]=mesh.edge_verts(edge_id).as_ref();
|
||||
mesh.vert(v0)+mesh.vert(v1)
|
||||
};
|
||||
for (i,&face_id) in mesh.edge_faces(edge_id).as_ref().iter().enumerate(){
|
||||
//test if this face is closer
|
||||
let (face_n,d)=mesh.face_nd(face_id);
|
||||
//if test point is behind face, the face is invalid
|
||||
// TODO: find out why I thought of this backwards
|
||||
if !(face_n.dot(point)-d).is_positive(){
|
||||
println!("behind");
|
||||
continue;
|
||||
}
|
||||
//edge-face boundary nd, n facing out of the face towards the edge
|
||||
let boundary_n=face_n.cross(edge_n)*(i as i64*2-1);
|
||||
let boundary_d=boundary_n.dot(delta_pos);
|
||||
//is test point behind edge, i.e. contained in the face
|
||||
if !boundary_d.is_positive(){
|
||||
//both faces cannot pass this condition, return early if one does.
|
||||
return FEV::Face(face_id);
|
||||
}else{
|
||||
println!("boundary_d is positive");
|
||||
}
|
||||
}
|
||||
FEV::Edge(edge_id)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Option<FEV<MinkowskiMesh<'a>>>{
|
||||
println!("=== LUA ===");
|
||||
let (hits,_details)=crate::minimum_difference_lua::minimum_difference_details(mesh,point).unwrap();
|
||||
println!("=== RUST ===");
|
||||
let closest_fev_not_inside=closest_fev_not_inside_inner(mesh,point);
|
||||
assert_eq!(hits,closest_fev_not_inside.is_none(),"algorithms disagree");
|
||||
closest_fev_not_inside
|
||||
}
|
||||
|
||||
pub fn closest_fev_not_inside_inner<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Option<FEV<MinkowskiMesh<'a>>>{
|
||||
const ENABLE_FAST_FAIL:bool=false;
|
||||
// TODO: remove mesh negation
|
||||
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
|
||||
// on_exact
|
||||
|is_intersecting,simplex|{
|
||||
println!("on_exact simplex={simplex:?}");
|
||||
if is_intersecting{
|
||||
return None;
|
||||
}
|
||||
// Convert simplex to FEV
|
||||
// Vertices must be inverted since the mesh is inverted
|
||||
Some(match simplex{
|
||||
Simplex1_3::Simplex1([v0])=>FEV::Vert(-v0),
|
||||
Simplex1_3::Simplex2([v0,v1])=>{
|
||||
// invert
|
||||
let (v0,v1)=(-v0,-v1);
|
||||
let ev=crawl_to_closest_ev(mesh,[v0,v1],point);
|
||||
if !matches!(ev,EV::Edge(_)){
|
||||
println!("I can't believe it's not an edge!");
|
||||
}
|
||||
ev.into()
|
||||
},
|
||||
Simplex1_3::Simplex3([v0,v1,v2])=>{
|
||||
// invert
|
||||
let (v0,v1,v2)=(-v0,-v1,-v2);
|
||||
// Shimmy to the side until you find a face that contains the closest point
|
||||
// it's ALWAYS representable as a face, but this algorithm may
|
||||
// return E or V in edge cases but I don't think that will break the face crawler
|
||||
let fev=crawl_to_closest_fev(mesh,[v0,v1,v2],point);
|
||||
if !matches!(fev,FEV::Face(_)){
|
||||
println!("I can't believe it's not a face!");
|
||||
}
|
||||
fev
|
||||
},
|
||||
})
|
||||
},
|
||||
// on_escape
|
||||
|_simplex|{
|
||||
// intersection is guaranteed at this point
|
||||
// local norm, dist, u0, u1, v0, v1, w0, w1 = expand(queryP, queryQ, a0, a1, b0, b1, c0, c1, d0, d1, 1e-5)
|
||||
// let simplex=refine_to_exact(mesh,simplex);
|
||||
None
|
||||
},
|
||||
// fast_fail value is irrelevant and will never be returned!
|
||||
||unreachable!()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn contains_point(mesh:&MinkowskiMesh<'_>,point:Planar64Vec3)->bool{
|
||||
const ENABLE_FAST_FAIL:bool=true;
|
||||
// TODO: remove mesh negation
|
||||
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
|
||||
// on_exact
|
||||
|is_intersecting,_simplex|{
|
||||
is_intersecting
|
||||
},
|
||||
// on_escape
|
||||
|_simplex|{
|
||||
// intersection is guaranteed at this point
|
||||
true
|
||||
},
|
||||
// fast_fail value
|
||||
||false
|
||||
)
|
||||
}
|
||||
|
||||
// local function minimumDifference(
|
||||
// queryP, radiusP,
|
||||
// queryQ, radiusQ,
|
||||
// exitRadius, testIntersection
|
||||
// )
|
||||
fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery<Position=Planar64Vec3,Direction=Planar64Vec3>>(
|
||||
mesh:&M,
|
||||
point:Planar64Vec3,
|
||||
on_exact:impl FnOnce(bool,Simplex1_3<M::Vert>)->T,
|
||||
on_escape:impl FnOnce(Simplex<4,M::Vert>)->T,
|
||||
on_fast_fail:impl FnOnce()->T,
|
||||
)->T
|
||||
where
|
||||
M::Vert:Copy
|
||||
{
|
||||
// local initialAxis = queryQ() - queryP()
|
||||
// local new_point_p = queryP(initialAxis)
|
||||
// local new_point_q = queryQ(-initialAxis)
|
||||
// local direction, a0, a1, b0, b1, c0, c1, d0, d1
|
||||
let mut initial_axis=mesh.hint_point()+point;
|
||||
// degenerate case
|
||||
if initial_axis==vec3::zero(){
|
||||
initial_axis=choose_any_direction();
|
||||
}
|
||||
println!("initial_axis={initial_axis}");
|
||||
let last_point=mesh.farthest_vert(-initial_axis);
|
||||
// this represents the 'a' value in the commented code
|
||||
let mut last_pos=mesh.vert(last_point);
|
||||
let Reduced{dir:mut direction,simplex:mut simplex_small}=reduce1([last_point],mesh,point);
|
||||
|
||||
// exitRadius = testIntersection and 0 or exitRadius or 1/0
|
||||
// for _ = 1, 100 do
|
||||
loop{
|
||||
println!("direction={direction}");
|
||||
|
||||
// new_point_p = queryP(-direction)
|
||||
// new_point_q = queryQ(direction)
|
||||
// local next_point = new_point_q - new_point_p
|
||||
let next_point=mesh.farthest_vert(direction);
|
||||
let next_pos=mesh.vert(next_point);
|
||||
|
||||
// if -direction:Dot(next_point) > (exitRadius + radiusP + radiusQ)*direction.magnitude then
|
||||
let d=direction.dot(next_pos+point);
|
||||
let fast_fail=d.is_negative();
|
||||
println!("ENABLE_FAST_FAIL={ENABLE_FAST_FAIL} fast_fail={fast_fail} next_point={} dot={d}",next_pos+point);
|
||||
if ENABLE_FAST_FAIL&&fast_fail{
|
||||
println!("on_fast_fail");
|
||||
return on_fast_fail();
|
||||
}
|
||||
|
||||
let simplex_big=simplex_small.push_front(next_point);
|
||||
|
||||
// if
|
||||
// direction:Dot(next_point - a) <= 0 or
|
||||
// absDet(next_point, a, b, c) < 1e-6
|
||||
let d1=direction.dot(next_pos-last_pos);
|
||||
let cond2=simplex_big.det_is_zero(mesh);
|
||||
println!("d1={d1:?} cond2={cond2}");
|
||||
if !d1.is_positive()||cond2{
|
||||
println!("on_exact");
|
||||
// Found enough information to compute the exact closest point.
|
||||
// local norm = direction.unit
|
||||
// local dist = a:Dot(norm)
|
||||
// local hits = -dist < radiusP + radiusQ
|
||||
let is_intersecting=(last_pos+point).dot(direction).is_positive();
|
||||
return on_exact(is_intersecting,simplex_small);
|
||||
}
|
||||
|
||||
// direction, a0, a1, b0, b1, c0, c1, d0, d1 = reduceSimplex(new_point_p, new_point_q, a0, a1, b0, b1, c0, c1)
|
||||
match simplex_big.reduce(mesh,point){
|
||||
// if a and b and c and d then
|
||||
Reduce::Escape(simplex)=>{
|
||||
println!("on_escape");
|
||||
// Enough information to conclude that the meshes are intersecting.
|
||||
// Topology information is computed if needed.
|
||||
return on_escape(simplex);
|
||||
},
|
||||
Reduce::Reduced(reduced)=>{
|
||||
direction=reduced.dir;
|
||||
simplex_small=reduced.simplex;
|
||||
},
|
||||
}
|
||||
|
||||
// next loop this will be a
|
||||
last_pos=next_pos;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use super::*;
|
||||
use crate::model::{PhysicsMesh,PhysicsMeshView};
|
||||
|
||||
fn mesh_contains_point(mesh:PhysicsMeshView<'_>,point:Planar64Vec3)->bool{
|
||||
const ENABLE_FAST_FAIL:bool=true;
|
||||
// TODO: remove mesh negation
|
||||
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&mesh,point,
|
||||
// on_exact
|
||||
|is_intersecting,_simplex|{
|
||||
is_intersecting
|
||||
},
|
||||
// on_escape
|
||||
|_simplex|{
|
||||
// intersection is guaranteed at this point
|
||||
true
|
||||
},
|
||||
// fast_fail value
|
||||
||false
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cube_points(){
|
||||
let mesh=PhysicsMesh::unit_cube();
|
||||
let mesh_view=mesh.complete_mesh_view();
|
||||
for x in -2..=2{
|
||||
for y in -2..=2{
|
||||
for z in -2..=2{
|
||||
let point=vec3::int(x,y,z)>>1;
|
||||
assert!(mesh_contains_point(mesh_view,point),"Mesh did not contain point {point}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
174
engine/physics/src/minimum_difference_lua.rs
Normal file
174
engine/physics/src/minimum_difference_lua.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use mlua::{Lua,FromLuaMulti,IntoLuaMulti,Function,Result as LuaResult,Vector};
|
||||
use strafesnet_common::integer::{Planar64,Planar64Vec3,FixedFromFloatError};
|
||||
|
||||
use crate::mesh_query::MeshQuery;
|
||||
use crate::minkowski::MinkowskiMesh;
|
||||
|
||||
pub fn contains_point(
|
||||
mesh:&MinkowskiMesh,
|
||||
point:Planar64Vec3,
|
||||
)->LuaResult<bool>{
|
||||
Ok(minimum_difference(mesh,point,true)?.hits)
|
||||
}
|
||||
pub fn minimum_difference_details(
|
||||
mesh:&MinkowskiMesh,
|
||||
point:Planar64Vec3,
|
||||
)->LuaResult<(bool,Option<Details>)>{
|
||||
let md=minimum_difference(mesh,point,false)?;
|
||||
Ok((md.hits,md.details))
|
||||
}
|
||||
fn p64v3(v:Vector)->Result<Planar64Vec3,FixedFromFloatError>{
|
||||
Ok(Planar64Vec3::new([
|
||||
v.x().try_into()?,
|
||||
v.y().try_into()?,
|
||||
v.z().try_into()?,
|
||||
]))
|
||||
}
|
||||
fn vec(v:Planar64Vec3)->Vector{
|
||||
Vector::new(v.x.into(),v.y.into(),v.z.into())
|
||||
}
|
||||
struct MinimumDifference{
|
||||
hits:bool,
|
||||
details:Option<Details>
|
||||
}
|
||||
pub struct Details{
|
||||
pub distance:Planar64,
|
||||
pub p_pos:Planar64Vec3,
|
||||
pub p_norm:Planar64Vec3,
|
||||
pub q_pos:Planar64Vec3,
|
||||
pub q_norm:Planar64Vec3,
|
||||
}
|
||||
impl FromLuaMulti for MinimumDifference{
|
||||
fn from_lua_multi(mut values:mlua::MultiValue,_lua:&Lua)->LuaResult<Self>{
|
||||
match values.make_contiguous(){
|
||||
&mut [
|
||||
mlua::Value::Boolean(hits),
|
||||
mlua::Value::Nil,
|
||||
mlua::Value::Nil,
|
||||
mlua::Value::Nil,
|
||||
mlua::Value::Nil,
|
||||
mlua::Value::Nil,
|
||||
]=>Ok(Self{hits,details:None}),
|
||||
&mut [
|
||||
mlua::Value::Boolean(hits),
|
||||
mlua::Value::Number(distance),
|
||||
mlua::Value::Vector(p_pos),
|
||||
mlua::Value::Vector(p_norm),
|
||||
mlua::Value::Vector(q_pos),
|
||||
mlua::Value::Vector(q_norm),
|
||||
]=>Ok(Self{
|
||||
hits,
|
||||
details:Some(Details{
|
||||
distance:distance.try_into().unwrap(),
|
||||
p_pos:p64v3(p_pos).unwrap(),
|
||||
p_norm:p64v3(p_norm).unwrap(),
|
||||
q_pos:p64v3(q_pos).unwrap(),
|
||||
q_norm:p64v3(q_norm).unwrap(),
|
||||
}),
|
||||
}),
|
||||
&mut [
|
||||
mlua::Value::Boolean(hits),
|
||||
mlua::Value::Integer(distance),
|
||||
mlua::Value::Vector(p_pos),
|
||||
mlua::Value::Vector(p_norm),
|
||||
mlua::Value::Vector(q_pos),
|
||||
mlua::Value::Vector(q_norm),
|
||||
]=>Ok(Self{
|
||||
hits,
|
||||
details:Some(Details{
|
||||
distance:distance.into(),
|
||||
p_pos:p64v3(p_pos).unwrap(),
|
||||
p_norm:p64v3(p_norm).unwrap(),
|
||||
q_pos:p64v3(q_pos).unwrap(),
|
||||
q_norm:p64v3(q_norm).unwrap(),
|
||||
}),
|
||||
}),
|
||||
values=>Err(mlua::Error::runtime(format!("Invalid return values: {values:?}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Args{
|
||||
query_p:Function,
|
||||
radius_p:f64,
|
||||
query_q:Function,
|
||||
radius_q:f64,
|
||||
test_intersection:bool,
|
||||
}
|
||||
impl Args{
|
||||
fn new(
|
||||
lua:&Lua,
|
||||
mesh:&'static MinkowskiMesh<'static>,
|
||||
point:Planar64Vec3,
|
||||
test_intersection:bool,
|
||||
)->LuaResult<Self>{
|
||||
let radius_p=0.0;
|
||||
let radius_q=0.0;
|
||||
// Query the farthest point on the mesh in the given direction.
|
||||
let query_p=lua.create_function(move|_,dir:Option<Vector>|{
|
||||
let Some(dir)=dir else{
|
||||
return Ok(vec(mesh.mesh0.hint_point()));
|
||||
};
|
||||
let dir=p64v3(dir).unwrap();
|
||||
let vert_id=mesh.mesh0.farthest_vert(dir);
|
||||
let dir=mesh.mesh0.vert(vert_id);
|
||||
Ok(vec(dir))
|
||||
})?;
|
||||
// query_q is different since it includes the test point offset.
|
||||
let query_q=lua.create_function(move|_,dir:Option<Vector>|{
|
||||
let Some(dir)=dir else{
|
||||
return Ok(vec(mesh.mesh1.hint_point()+point));
|
||||
};
|
||||
let dir=p64v3(dir).unwrap();
|
||||
let vert_id=mesh.mesh1.farthest_vert(dir);
|
||||
let dir=mesh.mesh1.vert(vert_id)+point;
|
||||
Ok(vec(dir))
|
||||
})?;
|
||||
Ok(Args{
|
||||
query_p,
|
||||
radius_p,
|
||||
query_q,
|
||||
radius_q,
|
||||
test_intersection,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl IntoLuaMulti for Args{
|
||||
fn into_lua_multi(self,lua:&Lua)->LuaResult<mlua::MultiValue>{
|
||||
use mlua::IntoLua;
|
||||
Ok(mlua::MultiValue::from_vec(vec![
|
||||
self.query_p.into_lua(lua)?,
|
||||
self.radius_p.into_lua(lua)?,
|
||||
self.query_q.into_lua(lua)?,
|
||||
self.radius_q.into_lua(lua)?,
|
||||
mlua::Value::Nil,
|
||||
self.test_intersection.into_lua(lua)?,
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
fn minimum_difference(
|
||||
mesh:&MinkowskiMesh,
|
||||
point:Planar64Vec3,
|
||||
test_intersection:bool,
|
||||
)->LuaResult<MinimumDifference>{
|
||||
let ctx=init_lua()?;
|
||||
// SAFETY: mesh lifetime must outlive args usages
|
||||
let mesh=unsafe{core::mem::transmute(mesh)};
|
||||
let args=Args::new(&ctx.lua,mesh,point,test_intersection)?;
|
||||
ctx.f.call(args)
|
||||
}
|
||||
|
||||
struct Ctx{
|
||||
lua:Lua,
|
||||
f:Function,
|
||||
}
|
||||
fn init_lua()->LuaResult<Ctx>{
|
||||
static SOURCE:std::sync::LazyLock<String>=std::sync::LazyLock::new(||std::fs::read_to_string("../../Trey-MinimumDifference.lua").unwrap());
|
||||
let lua=Lua::new();
|
||||
lua.sandbox(true)?;
|
||||
let lib_f=lua.load(SOURCE.as_str()).set_name("Trey-MinimumDifference").into_function()?;
|
||||
let lib:mlua::Table=lib_f.call(())?;
|
||||
let f=lib.raw_get("difference")?;
|
||||
Ok(Ctx{lua,f})
|
||||
}
|
||||
412
engine/physics/src/minkowski.rs
Normal file
412
engine/physics/src/minkowski.rs
Normal file
@@ -0,0 +1,412 @@
|
||||
use core::ops::{Bound,RangeBounds};
|
||||
|
||||
use strafesnet_common::integer::fixed_types::{F192_96,F256_128,F512_256};
|
||||
use strafesnet_common::integer::vec3::Vector3;
|
||||
use strafesnet_common::integer::{Planar64Vec3,Ratio,Fixed};
|
||||
use crate::model::into_giga_time;
|
||||
use crate::model::{SubmeshVertId,SubmeshEdgeId,SubmeshDirectedEdgeId,SubmeshFaceId,TransformedMesh,GigaTime};
|
||||
use crate::mesh_query::{MeshQuery,MeshTopology,DirectedEdge,UndirectedEdge};
|
||||
use crate::physics::{Time,Trajectory};
|
||||
|
||||
struct AsRefHelper<T>(T);
|
||||
impl<T> AsRef<T> for AsRefHelper<T>{
|
||||
fn as_ref(&self)->&T{
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
//Note that a face on a minkowski mesh refers to a pair of fevs on the meshes it's summed from
|
||||
//(face,vertex)
|
||||
//(edge,edge)
|
||||
//(vertex,face)
|
||||
#[derive(Clone,Copy,Debug,Eq,PartialEq)]
|
||||
pub enum MinkowskiVert{
|
||||
VertVert(SubmeshVertId,SubmeshVertId),
|
||||
}
|
||||
// TODO: remove this
|
||||
impl core::ops::Neg for MinkowskiVert{
|
||||
type Output=Self;
|
||||
fn neg(self)->Self::Output{
|
||||
match self{
|
||||
MinkowskiVert::VertVert(v0,v1)=>MinkowskiVert::VertVert(v1,v0),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum MinkowskiEdge{
|
||||
VertEdge(SubmeshVertId,SubmeshEdgeId),
|
||||
EdgeVert(SubmeshEdgeId,SubmeshVertId),
|
||||
//EdgeEdge when edges are parallel
|
||||
}
|
||||
impl UndirectedEdge for MinkowskiEdge{
|
||||
type DirectedEdge=MinkowskiDirectedEdge;
|
||||
fn as_directed(self,parity:bool)->Self::DirectedEdge{
|
||||
match self{
|
||||
MinkowskiEdge::VertEdge(v0,e1)=>MinkowskiDirectedEdge::VertEdge(v0,e1.as_directed(parity)),
|
||||
MinkowskiEdge::EdgeVert(e0,v1)=>MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),v1),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum MinkowskiDirectedEdge{
|
||||
VertEdge(SubmeshVertId,SubmeshDirectedEdgeId),
|
||||
EdgeVert(SubmeshDirectedEdgeId,SubmeshVertId),
|
||||
//EdgeEdge when edges are parallel
|
||||
}
|
||||
impl DirectedEdge for MinkowskiDirectedEdge{
|
||||
type UndirectedEdge=MinkowskiEdge;
|
||||
fn as_undirected(self)->Self::UndirectedEdge{
|
||||
match self{
|
||||
MinkowskiDirectedEdge::VertEdge(v0,e1)=>MinkowskiEdge::VertEdge(v0,e1.as_undirected()),
|
||||
MinkowskiDirectedEdge::EdgeVert(e0,v1)=>MinkowskiEdge::EdgeVert(e0.as_undirected(),v1),
|
||||
}
|
||||
}
|
||||
fn parity(&self)->bool{
|
||||
match self{
|
||||
MinkowskiDirectedEdge::VertEdge(_,e)
|
||||
|MinkowskiDirectedEdge::EdgeVert(e,_)=>e.parity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Copy,Debug,Hash)]
|
||||
pub enum MinkowskiFace{
|
||||
VertFace(SubmeshVertId,SubmeshFaceId),
|
||||
EdgeEdge(SubmeshEdgeId,SubmeshEdgeId,bool),
|
||||
FaceVert(SubmeshFaceId,SubmeshVertId),
|
||||
//EdgeFace
|
||||
//FaceEdge
|
||||
//FaceFace
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MinkowskiMesh<'a>{
|
||||
pub mesh0:TransformedMesh<'a>,
|
||||
pub mesh1:TransformedMesh<'a>,
|
||||
}
|
||||
|
||||
// TODO: remove this
|
||||
impl<'a> core::ops::Neg for &MinkowskiMesh<'a>{
|
||||
type Output=MinkowskiMesh<'a>;
|
||||
fn neg(self)->Self::Output{
|
||||
MinkowskiMesh::minkowski_sum(self.mesh1,self.mesh0)
|
||||
}
|
||||
}
|
||||
|
||||
impl MinkowskiMesh<'_>{
|
||||
pub fn minkowski_sum<'a>(mesh0:TransformedMesh<'a>,mesh1:TransformedMesh<'a>)->MinkowskiMesh<'a>{
|
||||
MinkowskiMesh{
|
||||
mesh0,
|
||||
mesh1,
|
||||
}
|
||||
}
|
||||
pub fn closest_point(&self,point:Planar64Vec3)->Option<crate::mesh_query::FEV<Self>>{
|
||||
crate::minimum_difference::closest_fev_not_inside(self,point)
|
||||
}
|
||||
pub fn predict_collision_in(&self,trajectory:&Trajectory,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
|
||||
let start_position=match range.start_bound(){
|
||||
Bound::Included(time)=>trajectory.extrapolated_position(*time),
|
||||
Bound::Excluded(time)=>trajectory.extrapolated_position(*time),
|
||||
Bound::Unbounded=>trajectory.position,
|
||||
};
|
||||
let fev=crate::minimum_difference::closest_fev_not_inside(self,start_position)?;
|
||||
//continue forwards along the body parabola
|
||||
fev.crawl(self,trajectory,range.start_bound(),range.end_bound()).hit()
|
||||
}
|
||||
pub fn predict_collision_out(&self,trajectory:&Trajectory,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
|
||||
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
|
||||
// TODO: handle unbounded collision using infinity fev
|
||||
let start_position=match upper_bound{
|
||||
Bound::Included(time)=>trajectory.extrapolated_position(*time),
|
||||
Bound::Excluded(time)=>trajectory.extrapolated_position(*time),
|
||||
Bound::Unbounded=>trajectory.position,
|
||||
};
|
||||
let fev=crate::minimum_difference::closest_fev_not_inside(self,start_position)?;
|
||||
// swap and negate bounds to do a time inversion
|
||||
let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t));
|
||||
let time_reversed_trajectory=-trajectory;
|
||||
//continue backwards along the body parabola
|
||||
fev.crawl(self,&time_reversed_trajectory,lower_bound.as_ref(),upper_bound.as_ref()).hit()
|
||||
//no need to test -time<time_limit because of the first step
|
||||
.map(|(face,time)|(face,-time))
|
||||
}
|
||||
pub fn predict_collision_face_out(&self,trajectory:&Trajectory,range:impl RangeBounds<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiDirectedEdge,GigaTime)>{
|
||||
// TODO: make better
|
||||
use crate::face_crawler::{low,upp};
|
||||
//no algorithm needed, there is only one state and two cases (Edge,None)
|
||||
//determine when it passes an edge ("sliding off" case)
|
||||
let start_time=range.start_bound().map(|&t|(t-trajectory.time).to_ratio());
|
||||
let mut best_time=range.end_bound().map(|&t|into_giga_time(t,trajectory.time));
|
||||
let mut best_edge=None;
|
||||
let face_n=self.face_nd(contact_face_id).0;
|
||||
self.for_each_face_edge(contact_face_id,|directed_edge_id|{
|
||||
let edge_n=self.directed_edge_n(directed_edge_id);
|
||||
//f x e points in
|
||||
let n=face_n.cross(edge_n);
|
||||
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
||||
let d=n.dot(self.vert(v0)+self.vert(v1));
|
||||
//WARNING! d outside of *2
|
||||
//WARNING: truncated precision
|
||||
//wrap for speed
|
||||
for dt in F256_128::zeroes2(((n.dot(trajectory.position))*2-d).wrap_256(),n.dot(trajectory.velocity).wrap_256()*2,n.dot(trajectory.acceleration).wrap_256()){
|
||||
if low(&start_time,&dt)&&upp(&dt,&best_time)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
best_time=Bound::Included(dt);
|
||||
best_edge=Some((directed_edge_id,dt));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
best_edge
|
||||
}
|
||||
pub fn contains_point(&self,point:Planar64Vec3)->bool{
|
||||
crate::minimum_difference::contains_point(self,point)
|
||||
}
|
||||
}
|
||||
impl MeshQuery for MinkowskiMesh<'_>{
|
||||
type Direction=Planar64Vec3;
|
||||
type Position=Planar64Vec3;
|
||||
type Normal=Vector3<F192_96>;
|
||||
type Offset=F256_128;
|
||||
// TODO: relative d
|
||||
fn face_nd(&self,face_id:MinkowskiFace)->(Self::Normal,Self::Offset){
|
||||
match face_id{
|
||||
MinkowskiFace::VertFace(v0,f1)=>{
|
||||
let (n,d)=self.mesh1.face_nd(f1);
|
||||
(-n,d-n.dot(self.mesh0.vert(v0)))
|
||||
},
|
||||
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
|
||||
let edge0_n=self.mesh0.edge_n(e0);
|
||||
let edge1_n=self.mesh1.edge_n(e1);
|
||||
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
|
||||
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
|
||||
let n=edge0_n.cross(edge1_n);
|
||||
let e0d=n.dot(self.mesh0.vert(e0v0)+self.mesh0.vert(e0v1));
|
||||
let e1d=n.dot(self.mesh1.vert(e1v0)+self.mesh1.vert(e1v1));
|
||||
((n*(parity as i64*4-2)).widen_192(),((e0d-e1d)*(parity as i64*2-1)).widen_256())
|
||||
},
|
||||
MinkowskiFace::FaceVert(f0,v1)=>{
|
||||
let (n,d)=self.mesh0.face_nd(f0);
|
||||
(n,d-n.dot(self.mesh1.vert(v1)))
|
||||
},
|
||||
}
|
||||
}
|
||||
fn vert(&self,vert_id:MinkowskiVert)->Planar64Vec3{
|
||||
match vert_id{
|
||||
MinkowskiVert::VertVert(v0,v1)=>{
|
||||
self.mesh0.vert(v0)-self.mesh1.vert(v1)
|
||||
},
|
||||
}
|
||||
}
|
||||
fn hint_point(&self)->Planar64Vec3{
|
||||
self.mesh0.hint_point()-self.mesh1.hint_point()
|
||||
}
|
||||
fn farthest_vert(&self,dir:Planar64Vec3)->MinkowskiVert{
|
||||
MinkowskiVert::VertVert(self.mesh0.farthest_vert(dir),self.mesh1.farthest_vert(-dir))
|
||||
}
|
||||
fn edge_n(&self,edge_id:Self::Edge)->Self::Direction{
|
||||
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
|
||||
self.vert(v1)-self.vert(v0)
|
||||
}
|
||||
fn directed_edge_n(&self,directed_edge_id:Self::DirectedEdge)->Self::Direction{
|
||||
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
||||
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
|
||||
}
|
||||
}
|
||||
impl MeshTopology for MinkowskiMesh<'_>{
|
||||
type Face=MinkowskiFace;
|
||||
type Edge=MinkowskiEdge;
|
||||
type DirectedEdge=MinkowskiDirectedEdge;
|
||||
type Vert=MinkowskiVert;
|
||||
fn for_each_vert_edge(&self,vert_id:Self::Vert,mut f:impl FnMut(Self::DirectedEdge)){
|
||||
match vert_id{
|
||||
MinkowskiVert::VertVert(v0,v1)=>{
|
||||
//detect shared volume when the other mesh is mirrored along a test edge dir
|
||||
let v0f={
|
||||
let mut faces=Vec::new();
|
||||
self.mesh0.for_each_vert_face(v0,|face|faces.push(face));
|
||||
faces
|
||||
};
|
||||
let v1f={
|
||||
let mut faces=Vec::new();
|
||||
self.mesh1.for_each_vert_face(v1,|face|faces.push(face));
|
||||
faces
|
||||
};
|
||||
let v0f_n:Vec<_>=v0f.iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
|
||||
let v1f_n:Vec<_>=v1f.iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
|
||||
// scratch vector
|
||||
let mut face_normals=Vec::with_capacity(v0f.len()+v1f.len());
|
||||
face_normals.clone_from(&v0f_n);
|
||||
self.mesh0.for_each_vert_edge(v0,|directed_edge_id|{
|
||||
let n=self.mesh0.directed_edge_n(directed_edge_id);
|
||||
let nn=n.dot(n);
|
||||
// TODO: there's gotta be a better way to do this
|
||||
// drop faces beyond v0f_n
|
||||
face_normals.truncate(v0f.len());
|
||||
// make a set of faces from mesh0's perspective
|
||||
for face_n in &v1f_n{
|
||||
//add reflected mesh1 faces
|
||||
//wrap for speed
|
||||
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_192());
|
||||
}
|
||||
if is_empty_volume(&face_normals){
|
||||
f(MinkowskiDirectedEdge::EdgeVert(directed_edge_id,v1));
|
||||
}
|
||||
});
|
||||
face_normals.clone_from(&v1f_n);
|
||||
self.mesh1.for_each_vert_edge(v1,|directed_edge_id|{
|
||||
let n=self.mesh1.directed_edge_n(directed_edge_id);
|
||||
let nn=n.dot(n);
|
||||
// drop faces beyond v1f_n
|
||||
face_normals.truncate(v1f.len());
|
||||
// make a set of faces from mesh1's perspective
|
||||
for face_n in &v0f_n{
|
||||
//wrap for speed
|
||||
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_192());
|
||||
}
|
||||
if is_empty_volume(&face_normals){
|
||||
f(MinkowskiDirectedEdge::VertEdge(v0,directed_edge_id));
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
fn for_each_vert_face(&self,_vert_id:Self::Vert,_f:impl FnMut(Self::Face)){
|
||||
unimplemented!()
|
||||
}
|
||||
fn edge_faces(&self,edge_id:Self::Edge)->impl AsRef<[Self::Face;2]>{
|
||||
match edge_id{
|
||||
MinkowskiEdge::VertEdge(v0,e1)=>{
|
||||
//faces are listed backwards from the minkowski mesh
|
||||
let v0e={
|
||||
let mut edges=Vec::new();
|
||||
self.mesh0.for_each_vert_edge(v0,|edge|edges.push(edge));
|
||||
edges
|
||||
};
|
||||
let &[e1f0,e1f1]=self.mesh1.edge_faces(e1).as_ref();
|
||||
AsRefHelper([(e1f1,false),(e1f0,true)].map(|(edge_face_id1,face_parity)|{
|
||||
let mut best_edge=None;
|
||||
let mut best_d:Ratio<F512_256,F512_256>=Ratio::new(Fixed::ZERO,Fixed::ONE);
|
||||
let edge_face1_n=self.mesh1.face_nd(edge_face_id1).0;
|
||||
let edge_face1_nn=edge_face1_n.dot(edge_face1_n);
|
||||
for &directed_edge_id0 in &v0e{
|
||||
let edge0_n=self.mesh0.directed_edge_n(directed_edge_id0);
|
||||
//must be behind other face.
|
||||
let d=edge_face1_n.dot(edge0_n);
|
||||
if d.is_negative(){
|
||||
let edge0_nn=edge0_n.dot(edge0_n);
|
||||
// Assume not every number is huge
|
||||
// TODO: revisit this
|
||||
let dd=(d*d)/(edge_face1_nn*edge0_nn);
|
||||
if best_d<dd{
|
||||
best_d=dd;
|
||||
best_edge=Some(directed_edge_id0);
|
||||
}
|
||||
}
|
||||
}
|
||||
best_edge.map_or(
|
||||
MinkowskiFace::VertFace(v0,edge_face_id1),
|
||||
|directed_edge_id0|MinkowskiFace::EdgeEdge(directed_edge_id0.as_undirected(),e1,directed_edge_id0.parity()^face_parity)
|
||||
)
|
||||
}))
|
||||
},
|
||||
MinkowskiEdge::EdgeVert(e0,v1)=>{
|
||||
//tracking index with an external variable because .enumerate() is not available
|
||||
let v1e={
|
||||
let mut edges=Vec::new();
|
||||
self.mesh1.for_each_vert_edge(v1,|edge|edges.push(edge));
|
||||
edges
|
||||
};
|
||||
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).as_ref();
|
||||
AsRefHelper([(e0f0,true),(e0f1,false)].map(|(edge_face_id0,face_parity)|{
|
||||
let mut best_edge=None;
|
||||
let mut best_d:Ratio<F512_256,F512_256>=Ratio::new(Fixed::ZERO,Fixed::ONE);
|
||||
let edge_face0_n=self.mesh0.face_nd(edge_face_id0).0;
|
||||
let edge_face0_nn=edge_face0_n.dot(edge_face0_n);
|
||||
for &directed_edge_id1 in &v1e{
|
||||
let edge1_n=self.mesh1.directed_edge_n(directed_edge_id1);
|
||||
let d=edge_face0_n.dot(edge1_n);
|
||||
if d.is_negative(){
|
||||
let edge1_nn=edge1_n.dot(edge1_n);
|
||||
let dd=(d*d)/(edge_face0_nn*edge1_nn);
|
||||
if best_d<dd{
|
||||
best_d=dd;
|
||||
best_edge=Some(directed_edge_id1);
|
||||
}
|
||||
}
|
||||
}
|
||||
best_edge.map_or(
|
||||
MinkowskiFace::FaceVert(edge_face_id0,v1),
|
||||
|directed_edge_id1|MinkowskiFace::EdgeEdge(e0,directed_edge_id1.as_undirected(),directed_edge_id1.parity()^face_parity)
|
||||
)
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
fn edge_verts(&self,edge_id:Self::Edge)->impl AsRef<[Self::Vert;2]>{
|
||||
AsRefHelper(match edge_id{
|
||||
MinkowskiEdge::VertEdge(v0,e1)=>self.mesh1.edge_verts(e1).as_ref().map(|vert_id1|
|
||||
MinkowskiVert::VertVert(v0,vert_id1)
|
||||
),
|
||||
MinkowskiEdge::EdgeVert(e0,v1)=>self.mesh0.edge_verts(e0).as_ref().map(|vert_id0|
|
||||
MinkowskiVert::VertVert(vert_id0,v1)
|
||||
),
|
||||
})
|
||||
}
|
||||
fn for_each_face_vert(&self,_face_id:Self::Face,_f:impl FnMut(Self::Vert)){
|
||||
unimplemented!()
|
||||
}
|
||||
fn for_each_face_edge(&self,face_id:Self::Face,mut f:impl FnMut(Self::DirectedEdge)){
|
||||
match face_id{
|
||||
MinkowskiFace::VertFace(v0,f1)=>{
|
||||
self.mesh1.for_each_face_edge(f1,|edge_id1|
|
||||
f(MinkowskiDirectedEdge::VertEdge(v0,edge_id1.reverse()))
|
||||
)
|
||||
},
|
||||
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
|
||||
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
|
||||
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
|
||||
//could sort this if ordered edges are needed
|
||||
//probably just need to reverse this list according to parity
|
||||
f(MinkowskiDirectedEdge::VertEdge(e0v0,e1.as_directed(parity)));
|
||||
f(MinkowskiDirectedEdge::EdgeVert(e0.as_directed(!parity),e1v0));
|
||||
f(MinkowskiDirectedEdge::VertEdge(e0v1,e1.as_directed(!parity)));
|
||||
f(MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),e1v1));
|
||||
},
|
||||
MinkowskiFace::FaceVert(f0,v1)=>{
|
||||
self.mesh0.for_each_face_edge(f0,|edge_id0|
|
||||
f(MinkowskiDirectedEdge::EdgeVert(edge_id0,v1))
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty_volume(normals:&[Vector3<F192_96>])->bool{
|
||||
let len=normals.len();
|
||||
for i in 0..len-1{
|
||||
for j in i+1..len{
|
||||
let n=normals[i].cross(normals[j]);
|
||||
let mut d_comp=None;
|
||||
for k in 0..len{
|
||||
if k!=i&&k!=j{
|
||||
let d=n.dot(normals[k]).is_negative();
|
||||
if let &Some(comp)=&d_comp{
|
||||
// This is testing if d_comp*d < 0
|
||||
if comp^d{
|
||||
return true;
|
||||
}
|
||||
}else{
|
||||
d_comp=Some(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_empty_volume(){
|
||||
use strafesnet_common::integer::vec3;
|
||||
assert!(!is_empty_volume(&[vec3::X.widen_192(),vec3::Y.widen_192(),vec3::Z.widen_192()]));
|
||||
assert!(is_empty_volume(&[vec3::X.widen_192(),vec3::Y.widen_192(),vec3::Z.widen_192(),vec3::NEG_X.widen_192()]));
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::collections::{HashSet,HashMap};
|
||||
use core::ops::{Bound,RangeBounds};
|
||||
use strafesnet_common::integer::fixed_types::{F128_64,F192_96,F256_128};
|
||||
use strafesnet_common::integer::vec3::Vector3;
|
||||
use strafesnet_common::model::{self,MeshId,PolygonIter};
|
||||
use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio};
|
||||
use strafesnet_common::physics::Time;
|
||||
|
||||
type Body=crate::body::Body<strafesnet_common::physics::TimeInner>;
|
||||
use crate::mesh_query::{MeshQuery,MeshTopology,DirectedEdge,UndirectedEdge};
|
||||
|
||||
struct AsRefHelper<T>(T);
|
||||
impl<T> AsRef<T> for AsRefHelper<T>{
|
||||
@@ -14,20 +13,6 @@ impl<T> AsRef<T> for AsRefHelper<T>{
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UndirectedEdge{
|
||||
type DirectedEdge:Copy+DirectedEdge;
|
||||
fn as_directed(&self,parity:bool)->Self::DirectedEdge;
|
||||
}
|
||||
pub trait DirectedEdge{
|
||||
type UndirectedEdge:Copy+std::fmt::Debug+UndirectedEdge;
|
||||
fn as_undirected(&self)->Self::UndirectedEdge;
|
||||
fn parity(&self)->bool;
|
||||
//this is stupid but may work fine
|
||||
fn reverse(&self)-><<Self as DirectedEdge>::UndirectedEdge as UndirectedEdge>::DirectedEdge{
|
||||
self.as_undirected().as_directed(!self.parity())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone,Copy,Hash,id::Id,Eq,PartialEq)]
|
||||
pub struct MeshVertId(u32);
|
||||
#[derive(Debug,Clone,Copy,Hash,id::Id,Eq,PartialEq)]
|
||||
@@ -45,13 +30,13 @@ pub struct SubmeshFaceId(u32);
|
||||
|
||||
impl UndirectedEdge for SubmeshEdgeId{
|
||||
type DirectedEdge=SubmeshDirectedEdgeId;
|
||||
fn as_directed(&self,parity:bool)->SubmeshDirectedEdgeId{
|
||||
fn as_directed(self,parity:bool)->SubmeshDirectedEdgeId{
|
||||
SubmeshDirectedEdgeId(self.0|((parity as u32)<<(u32::BITS-1)))
|
||||
}
|
||||
}
|
||||
impl DirectedEdge for SubmeshDirectedEdgeId{
|
||||
type UndirectedEdge=SubmeshEdgeId;
|
||||
fn as_undirected(&self)->SubmeshEdgeId{
|
||||
fn as_undirected(self)->SubmeshEdgeId{
|
||||
SubmeshEdgeId(self.0&!(1<<(u32::BITS-1)))
|
||||
}
|
||||
fn parity(&self)->bool{
|
||||
@@ -59,14 +44,6 @@ impl DirectedEdge for SubmeshDirectedEdgeId{
|
||||
}
|
||||
}
|
||||
|
||||
//Vertex <-> Edge <-> Face -> Collide
|
||||
#[derive(Debug)]
|
||||
pub enum FEV<M:MeshQuery>{
|
||||
Face(M::Face),
|
||||
Edge(<M::Edge as DirectedEdge>::UndirectedEdge),
|
||||
Vert(M::Vert),
|
||||
}
|
||||
|
||||
//use Unit32 #[repr(C)] for map files
|
||||
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
|
||||
struct Face{
|
||||
@@ -75,42 +52,23 @@ struct Face{
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Vert(Planar64Vec3);
|
||||
pub trait MeshQuery{
|
||||
type Face:Copy+std::fmt::Debug;
|
||||
type Edge:Copy+DirectedEdge+std::fmt::Debug;
|
||||
type Vert:Copy+std::fmt::Debug;
|
||||
// Vertex must be Planar64Vec3 because it represents an actual position
|
||||
type Normal;
|
||||
type Offset;
|
||||
fn edge_n(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Planar64Vec3{
|
||||
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
|
||||
self.vert(v1)-self.vert(v0)
|
||||
}
|
||||
fn directed_edge_n(&self,directed_edge_id:Self::Edge)->Planar64Vec3{
|
||||
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
||||
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
|
||||
}
|
||||
fn vert(&self,vert_id:Self::Vert)->Planar64Vec3;
|
||||
fn face_nd(&self,face_id:Self::Face)->(Self::Normal,Self::Offset);
|
||||
fn face_edges(&self,face_id:Self::Face)->impl AsRef<[Self::Edge]>;
|
||||
fn edge_faces(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->impl AsRef<[Self::Face;2]>;
|
||||
fn edge_verts(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->impl AsRef<[Self::Vert;2]>;
|
||||
fn vert_edges(&self,vert_id:Self::Vert)->impl AsRef<[Self::Edge]>;
|
||||
fn vert_faces(&self,vert_id:Self::Vert)->impl AsRef<[Self::Face]>;
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct FaceRefs{
|
||||
// I didn't write it down, but I assume the edges are directed
|
||||
// clockwise when looking towards the face normal, i.e. right hand rule.
|
||||
edges:Vec<SubmeshDirectedEdgeId>,
|
||||
//verts are redundant, use edge[i].verts[0]
|
||||
//verts:Vec<VertId>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct EdgeRefs{
|
||||
faces:[SubmeshFaceId;2],//left, right
|
||||
verts:[SubmeshVertId;2],//bottom, top
|
||||
verts:[SubmeshVertId;2],//start, end
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct VertRefs{
|
||||
faces:Vec<SubmeshFaceId>,
|
||||
// edges are always directed away from the vert
|
||||
edges:Vec<SubmeshDirectedEdgeId>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
@@ -392,8 +350,8 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
|
||||
}
|
||||
//assume face hash is stable, and there are no flush faces...
|
||||
let face=Face{
|
||||
normal:(normal/len as i64).divide().narrow_1().unwrap(),
|
||||
dot:(dot/(len*len) as i64).narrow_1().unwrap(),
|
||||
normal:(normal/len as i64).divide().narrow_64().unwrap(),
|
||||
dot:(dot/(len*len) as i64).narrow_64().unwrap(),
|
||||
};
|
||||
let face_id=*face_id_from_face.entry(face).or_insert_with(||{
|
||||
let face_id=MeshFaceId::new(faces.len() as u32);
|
||||
@@ -432,48 +390,80 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug,Clone,Copy)]
|
||||
pub struct PhysicsMeshView<'a>{
|
||||
data:&'a PhysicsMeshData,
|
||||
topology:&'a PhysicsMeshTopology,
|
||||
}
|
||||
impl MeshQuery for PhysicsMeshView<'_>{
|
||||
type Face=SubmeshFaceId;
|
||||
type Edge=SubmeshDirectedEdgeId;
|
||||
type Vert=SubmeshVertId;
|
||||
type Position=Planar64Vec3;
|
||||
type Direction=Planar64Vec3;
|
||||
type Normal=Planar64Vec3;
|
||||
type Offset=Planar64;
|
||||
fn face_nd(&self,face_id:SubmeshFaceId)->(Planar64Vec3,Planar64){
|
||||
let face_idx=self.topology.faces[face_id.get() as usize].get() as usize;
|
||||
(self.data.faces[face_idx].normal,self.data.faces[face_idx].dot)
|
||||
}
|
||||
fn hint_point(&self)->Planar64Vec3{
|
||||
// invariant: meshes always encompass the origin
|
||||
vec3::zero()
|
||||
}
|
||||
fn farthest_vert(&self,dir:Planar64Vec3)->SubmeshVertId{
|
||||
//this happens to be well-defined. there are no virtual virtices
|
||||
SubmeshVertId::new(
|
||||
self.topology.verts.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|&(_,&vert_id)|
|
||||
dir.dot(self.data.verts[vert_id.get() as usize].0)
|
||||
)
|
||||
//assume there is more than zero vertices.
|
||||
.unwrap().0 as u32
|
||||
)
|
||||
}
|
||||
//ideally I never calculate the vertex position, but I have to for the graphical meshes...
|
||||
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
|
||||
let vert_idx=self.topology.verts[vert_id.get() as usize].get() as usize;
|
||||
self.data.verts[vert_idx].0
|
||||
}
|
||||
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
|
||||
self.topology.face_topology[face_id.get() as usize].edges.as_slice()
|
||||
fn edge_n(&self,edge_id:Self::Edge)->Self::Direction{
|
||||
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
|
||||
self.vert(v1)-self.vert(v0)
|
||||
}
|
||||
fn edge_faces(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshFaceId;2]>{
|
||||
fn directed_edge_n(&self,directed_edge_id:Self::DirectedEdge)->Self::Direction{
|
||||
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
||||
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
|
||||
}
|
||||
}
|
||||
impl MeshTopology for PhysicsMeshView<'_>{
|
||||
type Face=SubmeshFaceId;
|
||||
type Edge=SubmeshEdgeId;
|
||||
type DirectedEdge=SubmeshDirectedEdgeId;
|
||||
type Vert=SubmeshVertId;
|
||||
fn for_each_vert_edge(&self,vert_id:Self::Vert,f:impl FnMut(Self::DirectedEdge)){
|
||||
self.topology.vert_topology[vert_id.get() as usize].edges.iter().copied().for_each(f);
|
||||
}
|
||||
fn for_each_vert_face(&self,vert_id:Self::Vert,f:impl FnMut(Self::Face)){
|
||||
self.topology.vert_topology[vert_id.get() as usize].faces.iter().copied().for_each(f);
|
||||
}
|
||||
fn edge_faces(&self,edge_id:Self::Edge)->impl AsRef<[Self::Face;2]>{
|
||||
AsRefHelper(self.topology.edge_topology[edge_id.get() as usize].faces)
|
||||
}
|
||||
fn edge_verts(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshVertId;2]>{
|
||||
fn edge_verts(&self,edge_id:Self::Edge)->impl AsRef<[Self::Vert;2]>{
|
||||
AsRefHelper(self.topology.edge_topology[edge_id.get() as usize].verts)
|
||||
}
|
||||
fn vert_edges(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshDirectedEdgeId]>{
|
||||
self.topology.vert_topology[vert_id.get() as usize].edges.as_slice()
|
||||
fn for_each_face_vert(&self,_face_id:Self::Face,_f:impl FnMut(Self::Vert)){
|
||||
unimplemented!()
|
||||
}
|
||||
fn vert_faces(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshFaceId]>{
|
||||
self.topology.vert_topology[vert_id.get() as usize].faces.as_slice()
|
||||
fn for_each_face_edge(&self,face_id:Self::Face,f:impl FnMut(Self::DirectedEdge)){
|
||||
self.topology.face_topology[face_id.get() as usize].edges.iter().copied().for_each(f);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PhysicsMeshTransform{
|
||||
pub vertex:integer::Planar64Affine3,
|
||||
pub normal:integer::mat3::Matrix3<Fixed<2,64>>,
|
||||
pub det:Fixed<3,96>,
|
||||
pub normal:integer::mat3::Matrix3<F128_64>,
|
||||
pub det:F192_96,
|
||||
}
|
||||
impl PhysicsMeshTransform{
|
||||
pub fn new(transform:integer::Planar64Affine3)->Self{
|
||||
@@ -485,10 +475,10 @@ impl PhysicsMeshTransform{
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug,Clone,Copy)]
|
||||
pub struct TransformedMesh<'a>{
|
||||
view:PhysicsMeshView<'a>,
|
||||
transform:&'a PhysicsMeshTransform,
|
||||
pub transform:&'a PhysicsMeshTransform,
|
||||
}
|
||||
impl TransformedMesh<'_>{
|
||||
pub const fn new<'a>(
|
||||
@@ -500,9 +490,28 @@ impl TransformedMesh<'_>{
|
||||
transform,
|
||||
}
|
||||
}
|
||||
pub fn verts<'a>(&'a self)->impl Iterator<Item=vec3::Vector3<Fixed<2,64>>>+'a{
|
||||
pub fn verts<'a>(&'a self)->impl Iterator<Item=Vector3<F128_64>>+'a{
|
||||
self.view.data.verts.iter().map(|&Vert(pos)|self.transform.vertex.transform_point3(pos))
|
||||
}
|
||||
}
|
||||
impl MeshQuery for TransformedMesh<'_>{
|
||||
type Direction=Planar64Vec3;
|
||||
type Position=Planar64Vec3;
|
||||
type Normal=Vector3<F192_96>;
|
||||
type Offset=F256_128;
|
||||
fn face_nd(&self,face_id:SubmeshFaceId)->(Self::Normal,Self::Offset){
|
||||
let (n,d)=self.view.face_nd(face_id);
|
||||
let transformed_n=self.transform.normal*n;
|
||||
let transformed_d=d*self.transform.det+transformed_n.dot(self.transform.vertex.translation);
|
||||
(transformed_n,transformed_d)
|
||||
}
|
||||
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
|
||||
// wrap for speed
|
||||
self.transform.vertex.transform_point3(self.view.vert(vert_id)).wrap_64()
|
||||
}
|
||||
fn hint_point(&self)->Planar64Vec3{
|
||||
self.transform.vertex.translation
|
||||
}
|
||||
fn farthest_vert(&self,dir:Planar64Vec3)->SubmeshVertId{
|
||||
//this happens to be well-defined. there are no virtual virtices
|
||||
SubmeshVertId::new(
|
||||
@@ -515,528 +524,47 @@ impl TransformedMesh<'_>{
|
||||
.unwrap().0 as u32
|
||||
)
|
||||
}
|
||||
fn edge_n(&self,edge_id:Self::Edge)->Self::Direction{
|
||||
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
|
||||
self.vert(v1)-self.vert(v0)
|
||||
}
|
||||
fn directed_edge_n(&self,directed_edge_id:Self::DirectedEdge)->Self::Direction{
|
||||
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
||||
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
|
||||
}
|
||||
}
|
||||
impl MeshQuery for TransformedMesh<'_>{
|
||||
impl MeshTopology for TransformedMesh<'_>{
|
||||
type Face=SubmeshFaceId;
|
||||
type Edge=SubmeshDirectedEdgeId;
|
||||
type Edge=SubmeshEdgeId;
|
||||
type DirectedEdge=SubmeshDirectedEdgeId;
|
||||
type Vert=SubmeshVertId;
|
||||
type Normal=Vector3<Fixed<3,96>>;
|
||||
type Offset=Fixed<4,128>;
|
||||
fn face_nd(&self,face_id:SubmeshFaceId)->(Self::Normal,Self::Offset){
|
||||
let (n,d)=self.view.face_nd(face_id);
|
||||
let transformed_n=self.transform.normal*n;
|
||||
let transformed_d=d*self.transform.det+transformed_n.dot(self.transform.vertex.translation);
|
||||
(transformed_n,transformed_d)
|
||||
}
|
||||
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
|
||||
// wrap for speed
|
||||
self.transform.vertex.transform_point3(self.view.vert(vert_id)).wrap_1()
|
||||
#[inline]
|
||||
fn for_each_vert_edge(&self,vert_id:Self::Vert,f:impl FnMut(Self::DirectedEdge)){
|
||||
self.view.for_each_vert_edge(vert_id,f)
|
||||
}
|
||||
#[inline]
|
||||
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
|
||||
self.view.face_edges(face_id)
|
||||
fn for_each_vert_face(&self,vert_id:Self::Vert,f:impl FnMut(Self::Face)){
|
||||
self.view.for_each_vert_face(vert_id,f)
|
||||
}
|
||||
#[inline]
|
||||
fn edge_faces(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshFaceId;2]>{
|
||||
fn edge_faces(&self,edge_id:Self::Edge)->impl AsRef<[Self::Face;2]>{
|
||||
self.view.edge_faces(edge_id)
|
||||
}
|
||||
#[inline]
|
||||
fn edge_verts(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshVertId;2]>{
|
||||
fn edge_verts(&self,edge_id:Self::Edge)->impl AsRef<[Self::Vert;2]>{
|
||||
self.view.edge_verts(edge_id)
|
||||
}
|
||||
#[inline]
|
||||
fn vert_edges(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshDirectedEdgeId]>{
|
||||
self.view.vert_edges(vert_id)
|
||||
fn for_each_face_vert(&self,_face_id:Self::Face,_f:impl FnMut(Self::Vert)){
|
||||
unimplemented!()
|
||||
}
|
||||
#[inline]
|
||||
fn vert_faces(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshFaceId]>{
|
||||
self.view.vert_faces(vert_id)
|
||||
fn for_each_face_edge(&self,face_id:Self::Face,f:impl FnMut(Self::DirectedEdge)){
|
||||
self.view.for_each_face_edge(face_id,f)
|
||||
}
|
||||
}
|
||||
|
||||
//Note that a face on a minkowski mesh refers to a pair of fevs on the meshes it's summed from
|
||||
//(face,vertex)
|
||||
//(edge,edge)
|
||||
//(vertex,face)
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum MinkowskiVert{
|
||||
VertVert(SubmeshVertId,SubmeshVertId),
|
||||
}
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum MinkowskiEdge{
|
||||
VertEdge(SubmeshVertId,SubmeshEdgeId),
|
||||
EdgeVert(SubmeshEdgeId,SubmeshVertId),
|
||||
//EdgeEdge when edges are parallel
|
||||
}
|
||||
impl UndirectedEdge for MinkowskiEdge{
|
||||
type DirectedEdge=MinkowskiDirectedEdge;
|
||||
fn as_directed(&self,parity:bool)->Self::DirectedEdge{
|
||||
match self{
|
||||
MinkowskiEdge::VertEdge(v0,e1)=>MinkowskiDirectedEdge::VertEdge(*v0,e1.as_directed(parity)),
|
||||
MinkowskiEdge::EdgeVert(e0,v1)=>MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),*v1),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum MinkowskiDirectedEdge{
|
||||
VertEdge(SubmeshVertId,SubmeshDirectedEdgeId),
|
||||
EdgeVert(SubmeshDirectedEdgeId,SubmeshVertId),
|
||||
//EdgeEdge when edges are parallel
|
||||
}
|
||||
impl DirectedEdge for MinkowskiDirectedEdge{
|
||||
type UndirectedEdge=MinkowskiEdge;
|
||||
fn as_undirected(&self)->Self::UndirectedEdge{
|
||||
match self{
|
||||
MinkowskiDirectedEdge::VertEdge(v0,e1)=>MinkowskiEdge::VertEdge(*v0,e1.as_undirected()),
|
||||
MinkowskiDirectedEdge::EdgeVert(e0,v1)=>MinkowskiEdge::EdgeVert(e0.as_undirected(),*v1),
|
||||
}
|
||||
}
|
||||
fn parity(&self)->bool{
|
||||
match self{
|
||||
MinkowskiDirectedEdge::VertEdge(_,e)
|
||||
|MinkowskiDirectedEdge::EdgeVert(e,_)=>e.parity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
|
||||
pub enum MinkowskiFace{
|
||||
VertFace(SubmeshVertId,SubmeshFaceId),
|
||||
EdgeEdge(SubmeshEdgeId,SubmeshEdgeId,bool),
|
||||
FaceVert(SubmeshFaceId,SubmeshVertId),
|
||||
//EdgeFace
|
||||
//FaceEdge
|
||||
//FaceFace
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MinkowskiMesh<'a>{
|
||||
mesh0:TransformedMesh<'a>,
|
||||
mesh1:TransformedMesh<'a>,
|
||||
}
|
||||
|
||||
//infinity fev algorithm state transition
|
||||
#[derive(Debug)]
|
||||
enum Transition{
|
||||
Done,//found closest vert, no edges are better
|
||||
Vert(MinkowskiVert),//transition to vert
|
||||
}
|
||||
enum EV{
|
||||
Vert(MinkowskiVert),
|
||||
Edge(MinkowskiEdge),
|
||||
}
|
||||
|
||||
pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>;
|
||||
pub type GigaTime=Ratio<F256_128,F256_128>;
|
||||
pub fn into_giga_time(time:Time,relative_to:Time)->GigaTime{
|
||||
let r=(time-relative_to).to_ratio();
|
||||
Ratio::new(r.num.widen_4(),r.den.widen_4())
|
||||
}
|
||||
|
||||
impl MinkowskiMesh<'_>{
|
||||
pub fn minkowski_sum<'a>(mesh0:TransformedMesh<'a>,mesh1:TransformedMesh<'a>)->MinkowskiMesh<'a>{
|
||||
MinkowskiMesh{
|
||||
mesh0,
|
||||
mesh1,
|
||||
}
|
||||
}
|
||||
fn farthest_vert(&self,dir:Planar64Vec3)->MinkowskiVert{
|
||||
MinkowskiVert::VertVert(self.mesh0.farthest_vert(dir),self.mesh1.farthest_vert(-dir))
|
||||
}
|
||||
fn next_transition_vert(&self,vert_id:MinkowskiVert,best_distance_squared:&mut Fixed<2,64>,infinity_dir:Planar64Vec3,point:Planar64Vec3)->Transition{
|
||||
let mut best_transition=Transition::Done;
|
||||
for &directed_edge_id in self.vert_edges(vert_id).as_ref(){
|
||||
let edge_n=self.directed_edge_n(directed_edge_id);
|
||||
//is boundary uncrossable by a crawl from infinity
|
||||
let edge_verts=self.edge_verts(directed_edge_id.as_undirected());
|
||||
//select opposite vertex
|
||||
let test_vert_id=edge_verts.as_ref()[directed_edge_id.parity() as usize];
|
||||
//test if it's closer
|
||||
let diff=point-self.vert(test_vert_id);
|
||||
if edge_n.dot(infinity_dir).is_zero(){
|
||||
let distance_squared=diff.dot(diff);
|
||||
if distance_squared<*best_distance_squared{
|
||||
best_transition=Transition::Vert(test_vert_id);
|
||||
*best_distance_squared=distance_squared;
|
||||
}
|
||||
}
|
||||
}
|
||||
best_transition
|
||||
}
|
||||
fn final_ev(&self,vert_id:MinkowskiVert,best_distance_squared:&mut Fixed<2,64>,infinity_dir:Planar64Vec3,point:Planar64Vec3)->EV{
|
||||
let mut best_transition=EV::Vert(vert_id);
|
||||
let diff=point-self.vert(vert_id);
|
||||
for &directed_edge_id in self.vert_edges(vert_id).as_ref(){
|
||||
let edge_n=self.directed_edge_n(directed_edge_id);
|
||||
//is boundary uncrossable by a crawl from infinity
|
||||
//check if time of collision is outside Time::MIN..Time::MAX
|
||||
if edge_n.dot(infinity_dir).is_zero(){
|
||||
let d=edge_n.dot(diff);
|
||||
//test the edge
|
||||
let edge_nn=edge_n.dot(edge_n);
|
||||
if !d.is_negative()&&d<=edge_nn{
|
||||
let distance_squared={
|
||||
let c=diff.cross(edge_n);
|
||||
//wrap for speed
|
||||
(c.dot(c)/edge_nn).divide().wrap_2()
|
||||
};
|
||||
if distance_squared<=*best_distance_squared{
|
||||
best_transition=EV::Edge(directed_edge_id.as_undirected());
|
||||
*best_distance_squared=distance_squared;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
best_transition
|
||||
}
|
||||
fn crawl_boundaries(&self,mut vert_id:MinkowskiVert,infinity_dir:Planar64Vec3,point:Planar64Vec3)->EV{
|
||||
let mut best_distance_squared={
|
||||
let diff=point-self.vert(vert_id);
|
||||
diff.dot(diff)
|
||||
};
|
||||
loop{
|
||||
match self.next_transition_vert(vert_id,&mut best_distance_squared,infinity_dir,point){
|
||||
Transition::Done=>return self.final_ev(vert_id,&mut best_distance_squared,infinity_dir,point),
|
||||
Transition::Vert(new_vert_id)=>vert_id=new_vert_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// This function drops a vertex down to an edge or a face if the path from infinity did not cross any vertex-edge boundaries but the point is supposed to have already crossed a boundary down from a vertex
|
||||
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiMesh<'_>>{
|
||||
//start on any vertex
|
||||
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
|
||||
//cross edge-face boundary if it's uncrossable
|
||||
match self.crawl_boundaries(self.farthest_vert(infinity_dir),infinity_dir,point){
|
||||
//if a vert is returned, it is the closest point to the infinity point
|
||||
EV::Vert(vert_id)=>FEV::Vert(vert_id),
|
||||
EV::Edge(edge_id)=>{
|
||||
//cross to face if the boundary is not crossable and we are on the wrong side
|
||||
let edge_n=self.edge_n(edge_id);
|
||||
// point is multiplied by two because vert_sum sums two vertices.
|
||||
let delta_pos=point*2-{
|
||||
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
|
||||
self.vert(v0)+self.vert(v1)
|
||||
};
|
||||
for (i,&face_id) in self.edge_faces(edge_id).as_ref().iter().enumerate(){
|
||||
let face_n=self.face_nd(face_id).0;
|
||||
//edge-face boundary nd, n facing out of the face towards the edge
|
||||
let boundary_n=face_n.cross(edge_n)*(i as i64*2-1);
|
||||
let boundary_d=boundary_n.dot(delta_pos);
|
||||
//check if time of collision is outside Time::MIN..Time::MAX
|
||||
//infinity_dir can always be treated as a velocity
|
||||
if !boundary_d.is_positive()&&boundary_n.dot(infinity_dir).is_zero(){
|
||||
//both faces cannot pass this condition, return early if one does.
|
||||
return FEV::Face(face_id);
|
||||
}
|
||||
}
|
||||
FEV::Edge(edge_id)
|
||||
},
|
||||
}
|
||||
}
|
||||
// TODO: fundamentally improve this algorithm.
|
||||
// All it needs to do is find the closest point on the mesh
|
||||
// and return the FEV which the point resides on.
|
||||
//
|
||||
// What it actually does is use the above functions to trace a ray in from infinity,
|
||||
// crawling the closest point along the mesh surface until the ray reaches
|
||||
// the starting point to discover the final FEV.
|
||||
//
|
||||
// The actual collision prediction probably does a single test
|
||||
// and then immediately returns with 0 FEV transitions on average,
|
||||
// because of the strict time_limit constraint.
|
||||
//
|
||||
// Most of the calculation time is just calculating the starting point
|
||||
// for the "actual" crawling algorithm below (predict_collision_{in|out}).
|
||||
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Bound<&Time>)->Option<FEV<MinkowskiMesh<'_>>>{
|
||||
infinity_body.infinity_dir().and_then(|dir|{
|
||||
let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
|
||||
//a line is simpler to solve than a parabola
|
||||
infinity_body.velocity=dir;
|
||||
infinity_body.acceleration=vec3::ZERO;
|
||||
//crawl in from negative infinity along a tangent line to get the closest fev
|
||||
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,start_time).miss()
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
|
||||
println!("@@@BEGIN SETUP@@@");
|
||||
self.closest_fev_not_inside(*relative_body,range.start_bound()).and_then(|fev|{
|
||||
println!("@@@BEGIN REAL CRAWL@@@");
|
||||
//continue forwards along the body parabola
|
||||
fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit()
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
|
||||
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
|
||||
// swap and negate bounds to do a time inversion
|
||||
let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t));
|
||||
let infinity_body=-relative_body;
|
||||
self.closest_fev_not_inside(infinity_body,lower_bound.as_ref()).and_then(|fev|{
|
||||
//continue backwards along the body parabola
|
||||
fev.crawl(self,&infinity_body,lower_bound.as_ref(),upper_bound.as_ref()).hit()
|
||||
//no need to test -time<time_limit because of the first step
|
||||
.map(|(face,time)|(face,-time))
|
||||
})
|
||||
}
|
||||
pub fn predict_collision_face_out(&self,relative_body:&Body,range:impl RangeBounds<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiDirectedEdge,GigaTime)>{
|
||||
// TODO: make better
|
||||
use crate::face_crawler::{low,upp};
|
||||
//no algorithm needed, there is only one state and two cases (Edge,None)
|
||||
//determine when it passes an edge ("sliding off" case)
|
||||
let start_time=range.start_bound().map(|&t|{
|
||||
let r=(t-relative_body.time).to_ratio();
|
||||
Ratio::new(r.num,r.den)
|
||||
});
|
||||
let mut best_time=range.end_bound().map(|&t|into_giga_time(t,relative_body.time));
|
||||
let mut best_edge=None;
|
||||
let face_n=self.face_nd(contact_face_id).0;
|
||||
for &directed_edge_id in self.face_edges(contact_face_id).as_ref(){
|
||||
let edge_n=self.directed_edge_n(directed_edge_id);
|
||||
//f x e points in
|
||||
let n=face_n.cross(edge_n);
|
||||
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
|
||||
let d=n.dot(self.vert(v0)+self.vert(v1));
|
||||
//WARNING! d outside of *2
|
||||
//WARNING: truncated precision
|
||||
//wrap for speed
|
||||
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).wrap_4(),n.dot(relative_body.velocity).wrap_4()*2,n.dot(relative_body.acceleration).wrap_4()){
|
||||
if low(&start_time,&dt)&&upp(&dt,&best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
|
||||
best_time=Bound::Included(dt);
|
||||
best_edge=Some((directed_edge_id,dt));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
best_edge
|
||||
}
|
||||
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
|
||||
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
|
||||
// Bound::Included means that the surface of the mesh is included in the mesh
|
||||
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,Bound::Included(&infinity_body.time)).hit()
|
||||
}
|
||||
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
|
||||
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
|
||||
//movement must escape the mesh forwards and backwards in time,
|
||||
//otherwise the point is not inside the mesh
|
||||
self.infinity_in(infinity_body)
|
||||
.is_some_and(|_|
|
||||
self.infinity_in(-infinity_body)
|
||||
.is_some()
|
||||
)
|
||||
}
|
||||
}
|
||||
impl MeshQuery for MinkowskiMesh<'_>{
|
||||
type Face=MinkowskiFace;
|
||||
type Edge=MinkowskiDirectedEdge;
|
||||
type Vert=MinkowskiVert;
|
||||
type Normal=Vector3<Fixed<3,96>>;
|
||||
type Offset=Fixed<4,128>;
|
||||
// TODO: relative d
|
||||
fn face_nd(&self,face_id:MinkowskiFace)->(Self::Normal,Self::Offset){
|
||||
match face_id{
|
||||
MinkowskiFace::VertFace(v0,f1)=>{
|
||||
let (n,d)=self.mesh1.face_nd(f1);
|
||||
(-n,d-n.dot(self.mesh0.vert(v0)))
|
||||
},
|
||||
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
|
||||
let edge0_n=self.mesh0.edge_n(e0);
|
||||
let edge1_n=self.mesh1.edge_n(e1);
|
||||
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
|
||||
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
|
||||
let n=edge0_n.cross(edge1_n);
|
||||
let e0d=n.dot(self.mesh0.vert(e0v0)+self.mesh0.vert(e0v1));
|
||||
let e1d=n.dot(self.mesh1.vert(e1v0)+self.mesh1.vert(e1v1));
|
||||
((n*(parity as i64*4-2)).widen_3(),((e0d-e1d)*(parity as i64*2-1)).widen_4())
|
||||
},
|
||||
MinkowskiFace::FaceVert(f0,v1)=>{
|
||||
let (n,d)=self.mesh0.face_nd(f0);
|
||||
(n,d-n.dot(self.mesh1.vert(v1)))
|
||||
},
|
||||
}
|
||||
}
|
||||
fn vert(&self,vert_id:MinkowskiVert)->Planar64Vec3{
|
||||
match vert_id{
|
||||
MinkowskiVert::VertVert(v0,v1)=>{
|
||||
self.mesh0.vert(v0)-self.mesh1.vert(v1)
|
||||
},
|
||||
}
|
||||
}
|
||||
fn face_edges(&self,face_id:MinkowskiFace)->impl AsRef<[MinkowskiDirectedEdge]>{
|
||||
match face_id{
|
||||
MinkowskiFace::VertFace(v0,f1)=>{
|
||||
self.mesh1.face_edges(f1).as_ref().iter().map(|&edge_id1|
|
||||
MinkowskiDirectedEdge::VertEdge(v0,edge_id1.reverse())
|
||||
).collect()
|
||||
},
|
||||
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
|
||||
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
|
||||
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
|
||||
//could sort this if ordered edges are needed
|
||||
//probably just need to reverse this list according to parity
|
||||
vec![
|
||||
MinkowskiDirectedEdge::VertEdge(e0v0,e1.as_directed(parity)),
|
||||
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(!parity),e1v0),
|
||||
MinkowskiDirectedEdge::VertEdge(e0v1,e1.as_directed(!parity)),
|
||||
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),e1v1),
|
||||
]
|
||||
},
|
||||
MinkowskiFace::FaceVert(f0,v1)=>{
|
||||
self.mesh0.face_edges(f0).as_ref().iter().map(|&edge_id0|
|
||||
MinkowskiDirectedEdge::EdgeVert(edge_id0,v1)
|
||||
).collect()
|
||||
},
|
||||
}
|
||||
}
|
||||
fn edge_faces(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiFace;2]>{
|
||||
match edge_id{
|
||||
MinkowskiEdge::VertEdge(v0,e1)=>{
|
||||
//faces are listed backwards from the minkowski mesh
|
||||
let v0e=self.mesh0.vert_edges(v0);
|
||||
let &[e1f0,e1f1]=self.mesh1.edge_faces(e1).as_ref();
|
||||
AsRefHelper([(e1f1,false),(e1f0,true)].map(|(edge_face_id1,face_parity)|{
|
||||
let mut best_edge=None;
|
||||
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
|
||||
let edge_face1_n=self.mesh1.face_nd(edge_face_id1).0;
|
||||
let edge_face1_nn=edge_face1_n.dot(edge_face1_n);
|
||||
for &directed_edge_id0 in v0e.as_ref(){
|
||||
let edge0_n=self.mesh0.directed_edge_n(directed_edge_id0);
|
||||
//must be behind other face.
|
||||
let d=edge_face1_n.dot(edge0_n);
|
||||
if d.is_negative(){
|
||||
let edge0_nn=edge0_n.dot(edge0_n);
|
||||
// Assume not every number is huge
|
||||
// TODO: revisit this
|
||||
let dd=(d*d)/(edge_face1_nn*edge0_nn);
|
||||
if best_d<dd{
|
||||
best_d=dd;
|
||||
best_edge=Some(directed_edge_id0);
|
||||
}
|
||||
}
|
||||
}
|
||||
best_edge.map_or(
|
||||
MinkowskiFace::VertFace(v0,edge_face_id1),
|
||||
|directed_edge_id0|MinkowskiFace::EdgeEdge(directed_edge_id0.as_undirected(),e1,directed_edge_id0.parity()^face_parity)
|
||||
)
|
||||
}))
|
||||
},
|
||||
MinkowskiEdge::EdgeVert(e0,v1)=>{
|
||||
//tracking index with an external variable because .enumerate() is not available
|
||||
let v1e=self.mesh1.vert_edges(v1);
|
||||
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).as_ref();
|
||||
AsRefHelper([(e0f0,true),(e0f1,false)].map(|(edge_face_id0,face_parity)|{
|
||||
let mut best_edge=None;
|
||||
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
|
||||
let edge_face0_n=self.mesh0.face_nd(edge_face_id0).0;
|
||||
let edge_face0_nn=edge_face0_n.dot(edge_face0_n);
|
||||
for &directed_edge_id1 in v1e.as_ref(){
|
||||
let edge1_n=self.mesh1.directed_edge_n(directed_edge_id1);
|
||||
let d=edge_face0_n.dot(edge1_n);
|
||||
if d.is_negative(){
|
||||
let edge1_nn=edge1_n.dot(edge1_n);
|
||||
let dd=(d*d)/(edge_face0_nn*edge1_nn);
|
||||
if best_d<dd{
|
||||
best_d=dd;
|
||||
best_edge=Some(directed_edge_id1);
|
||||
}
|
||||
}
|
||||
}
|
||||
best_edge.map_or(
|
||||
MinkowskiFace::FaceVert(edge_face_id0,v1),
|
||||
|directed_edge_id1|MinkowskiFace::EdgeEdge(e0,directed_edge_id1.as_undirected(),directed_edge_id1.parity()^face_parity)
|
||||
)
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
fn edge_verts(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiVert;2]>{
|
||||
AsRefHelper(match edge_id{
|
||||
MinkowskiEdge::VertEdge(v0,e1)=>self.mesh1.edge_verts(e1).as_ref().map(|vert_id1|
|
||||
MinkowskiVert::VertVert(v0,vert_id1)
|
||||
),
|
||||
MinkowskiEdge::EdgeVert(e0,v1)=>self.mesh0.edge_verts(e0).as_ref().map(|vert_id0|
|
||||
MinkowskiVert::VertVert(vert_id0,v1)
|
||||
),
|
||||
})
|
||||
}
|
||||
fn vert_edges(&self,vert_id:MinkowskiVert)->impl AsRef<[MinkowskiDirectedEdge]>{
|
||||
match vert_id{
|
||||
MinkowskiVert::VertVert(v0,v1)=>{
|
||||
let mut edges=Vec::new();
|
||||
//detect shared volume when the other mesh is mirrored along a test edge dir
|
||||
let v0f_thing=self.mesh0.vert_faces(v0);
|
||||
let v1f_thing=self.mesh1.vert_faces(v1);
|
||||
let v0f=v0f_thing.as_ref();
|
||||
let v1f=v1f_thing.as_ref();
|
||||
let v0f_n:Vec<_>=v0f.iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
|
||||
let v1f_n:Vec<_>=v1f.iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
|
||||
// scratch vector
|
||||
let mut face_normals=Vec::with_capacity(v0f.len()+v1f.len());
|
||||
face_normals.clone_from(&v0f_n);
|
||||
for &directed_edge_id in self.mesh0.vert_edges(v0).as_ref(){
|
||||
let n=self.mesh0.directed_edge_n(directed_edge_id);
|
||||
let nn=n.dot(n);
|
||||
// TODO: there's gotta be a better way to do this
|
||||
// drop faces beyond v0f_n
|
||||
face_normals.truncate(v0f.len());
|
||||
// make a set of faces from mesh0's perspective
|
||||
for face_n in &v1f_n{
|
||||
//add reflected mesh1 faces
|
||||
//wrap for speed
|
||||
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
|
||||
}
|
||||
if is_empty_volume(&face_normals){
|
||||
edges.push(MinkowskiDirectedEdge::EdgeVert(directed_edge_id,v1));
|
||||
}
|
||||
}
|
||||
face_normals.clone_from(&v1f_n);
|
||||
for &directed_edge_id in self.mesh1.vert_edges(v1).as_ref(){
|
||||
let n=self.mesh1.directed_edge_n(directed_edge_id);
|
||||
let nn=n.dot(n);
|
||||
// drop faces beyond v1f_n
|
||||
face_normals.truncate(v1f.len());
|
||||
// make a set of faces from mesh1's perspective
|
||||
for face_n in &v0f_n{
|
||||
//wrap for speed
|
||||
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
|
||||
}
|
||||
if is_empty_volume(&face_normals){
|
||||
edges.push(MinkowskiDirectedEdge::VertEdge(v0,directed_edge_id));
|
||||
}
|
||||
}
|
||||
edges
|
||||
},
|
||||
}
|
||||
}
|
||||
fn vert_faces(&self,_vert_id:MinkowskiVert)->impl AsRef<[MinkowskiFace]>{
|
||||
unimplemented!();
|
||||
#[expect(unreachable_code)]
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty_volume(normals:&[Vector3<Fixed<3,96>>])->bool{
|
||||
let len=normals.len();
|
||||
for i in 0..len-1{
|
||||
for j in i+1..len{
|
||||
let n=normals[i].cross(normals[j]);
|
||||
let mut d_comp=None;
|
||||
for k in 0..len{
|
||||
if k!=i&&k!=j{
|
||||
let d=n.dot(normals[k]).is_negative();
|
||||
if let &Some(comp)=&d_comp{
|
||||
// This is testing if d_comp*d < 0
|
||||
if comp^d{
|
||||
return true;
|
||||
}
|
||||
}else{
|
||||
d_comp=Some(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_empty_volume(){
|
||||
assert!(!is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()]));
|
||||
assert!(is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()]));
|
||||
Ratio::new(r.num.widen_256(),r.den.widen_256())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
use strafesnet_common::integer::fixed_types::{F64_32,F128_64,F192_96,F256_128,F320_160};
|
||||
use strafesnet_common::integer::vec3::{self,Vector3};
|
||||
use strafesnet_common::integer::{Fixed,Planar64Vec3,Ratio};
|
||||
use strafesnet_common::ray::Ray;
|
||||
@@ -12,9 +13,10 @@ use strafesnet_common::ray::Ray;
|
||||
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);
|
||||
const RATIO_ZERO:Ratio<F64_32,F64_32>=Ratio::new(Fixed::ZERO,Fixed::EPSILON);
|
||||
|
||||
/// Information about a contact restriction
|
||||
#[derive(Debug,PartialEq)]
|
||||
pub struct Contact{
|
||||
pub position:Planar64Vec3,
|
||||
pub velocity:Planar64Vec3,
|
||||
@@ -28,42 +30,39 @@ impl Contact{
|
||||
normal:self.normal,
|
||||
}
|
||||
}
|
||||
fn relative_dot(&self,direction:Planar64Vec3)->Fixed<2,64>{
|
||||
fn relative_dot(&self,direction:Planar64Vec3)->F128_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>>{
|
||||
fn solve(&self,ray:&Ray)->Ratio<F128_64,F128_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));
|
||||
fn solve1(c0:&Contact)->Option<Ratio<Vector3<F192_96>,F128_64>>{
|
||||
let det=c0.normal.dot(c0.velocity);
|
||||
if det.abs()<EPSILON{
|
||||
if det.abs()==Fixed::ZERO{
|
||||
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));
|
||||
fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<F320_160>,F256_128>>{
|
||||
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{
|
||||
if det.abs()==Fixed::ZERO{
|
||||
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));
|
||||
fn solve3(c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ratio<Vector3<F256_128>,F192_96>>{
|
||||
let n0_n1=c0.normal.cross(c1.normal);
|
||||
let det=c2.normal.dot(n0_n1);
|
||||
if det.abs()<EPSILON{
|
||||
if det.abs()==Fixed::ZERO{
|
||||
return None;
|
||||
}
|
||||
let d0=c0.normal.dot(c0.position);
|
||||
@@ -72,7 +71,7 @@ fn solve3(c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ratio<Vector3<Fixed<4,128
|
||||
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]>{
|
||||
fn decompose1(point:Planar64Vec3,u0:Planar64Vec3)->Option<[Ratio<F128_64,F128_64>;1]>{
|
||||
let det=u0.dot(u0);
|
||||
if det==Fixed::ZERO{
|
||||
return None;
|
||||
@@ -80,7 +79,7 @@ fn decompose1(point:Planar64Vec3,u0:Planar64Vec3)->Option<[Ratio<Fixed<2,64>,Fix
|
||||
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]>{
|
||||
fn decompose2(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3)->Option<[Ratio<F256_128,F256_128>;2]>{
|
||||
let u0_u1=u0.cross(u1);
|
||||
let det=u0_u1.dot(u0_u1);
|
||||
if det==Fixed::ZERO{
|
||||
@@ -90,7 +89,7 @@ fn decompose2(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3)->Option<[Ratio
|
||||
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]>{
|
||||
fn decompose3(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3,u2:Planar64Vec3)->Option<[Ratio<F192_96,F192_96>;3]>{
|
||||
let det=u0.cross(u1).dot(u2);
|
||||
if det==Fixed::ZERO{
|
||||
return None;
|
||||
@@ -149,22 +148,22 @@ fn is_space_enclosed_4(
|
||||
}
|
||||
|
||||
const fn get_push_ray_0(point:Planar64Vec3)->Ray{
|
||||
Ray{origin:point,direction:vec3::ZERO}
|
||||
Ray{origin:point,direction:vec3::zero()}
|
||||
}
|
||||
fn get_push_ray_1(point:Planar64Vec3,c0:&Contact)->Option<Ray>{
|
||||
//wrap for speed
|
||||
let direction=solve1(c0)?.divide().wrap_1();
|
||||
let direction=solve1(c0)?.divide().wrap_64();
|
||||
let [s0]=decompose1(direction,c0.velocity)?;
|
||||
if s0.lt_ratio(RATIO_ZERO){
|
||||
return None;
|
||||
}
|
||||
let origin=point+solve1(
|
||||
&c0.relative_to(point),
|
||||
)?.divide().wrap_1();
|
||||
)?.divide().wrap_64();
|
||||
Some(Ray{origin,direction})
|
||||
}
|
||||
fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
|
||||
let direction=solve2(c0,c1)?.divide().wrap_1();
|
||||
let direction=solve2(c0,c1)?.divide().wrap_64();
|
||||
let [s0,s1]=decompose2(direction,c0.velocity,c1.velocity)?;
|
||||
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO){
|
||||
return None;
|
||||
@@ -172,11 +171,11 @@ fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
|
||||
let origin=point+solve2(
|
||||
&c0.relative_to(point),
|
||||
&c1.relative_to(point),
|
||||
)?.divide().wrap_1();
|
||||
)?.divide().wrap_64();
|
||||
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().wrap_1();
|
||||
let direction=solve3(c0,c1,c2)?.divide().wrap_64();
|
||||
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;
|
||||
@@ -185,7 +184,7 @@ fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Optio
|
||||
&c0.relative_to(point),
|
||||
&c1.relative_to(point),
|
||||
&c2.relative_to(point),
|
||||
)?.divide().wrap_1();
|
||||
)?.divide().wrap_64();
|
||||
Some(Ray{origin,direction})
|
||||
}
|
||||
|
||||
@@ -274,26 +273,26 @@ fn get_best_push_ray_and_conts<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
fn get_first_touch<'a>(contacts:&'a [Contact],ray:&Ray,conts:&Conts)->Option<(Ratio<Fixed<2,64>,Fixed<2,64>>,&'a Contact)>{
|
||||
fn get_first_touch<'a>(contacts:&'a [Contact],ray:&Ray,conts:&Conts)->Option<(Ratio<F128_64,F128_64>,&'a Contact)>{
|
||||
contacts.iter()
|
||||
.filter(|&contact|
|
||||
!conts.iter().any(|&c|std::ptr::eq(c,contact))
|
||||
!conts.iter().any(|&c|core::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{
|
||||
pub fn push_solve(contacts:&[Contact],point:Planar64Vec3)->(Planar64Vec3,Conts<'_>){
|
||||
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,
|
||||
None=>return (ray.origin,conts),
|
||||
};
|
||||
|
||||
if RATIO_ZERO.le_ratio(next_t){
|
||||
return ray.origin;
|
||||
return (ray.origin,conts);
|
||||
}
|
||||
|
||||
//push_front
|
||||
@@ -309,7 +308,7 @@ pub fn push_solve(contacts:&[Contact],point:Planar64Vec3)->Planar64Vec3{
|
||||
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,
|
||||
None=>return (meet_point,conts),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,14 +320,13 @@ mod tests{
|
||||
fn test_push_solve(){
|
||||
let contacts=vec![
|
||||
Contact{
|
||||
position:vec3::ZERO,
|
||||
position:vec3::zero(),
|
||||
velocity:vec3::Y,
|
||||
normal:vec3::Y,
|
||||
}
|
||||
];
|
||||
assert_eq!(
|
||||
vec3::ZERO,
|
||||
push_solve(&contacts,vec3::NEG_Y)
|
||||
);
|
||||
let (point,conts)=push_solve(&contacts,vec3::NEG_Y);
|
||||
assert_eq!(point,vec3::zero());
|
||||
assert_eq!(conts.as_slice(),[&contacts[0]].as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
glam = "0.30.0"
|
||||
glam.workspace = true
|
||||
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" }
|
||||
strafesnet_common.workspace = true
|
||||
strafesnet_physics.workspace = true
|
||||
strafesnet_settings.workspace = true
|
||||
strafesnet_snf.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -31,7 +31,7 @@ pub enum SessionInputInstruction{
|
||||
Mouse(glam::IVec2),
|
||||
SetControl(strafesnet_common::physics::SetControlInstruction),
|
||||
Mode(ImplicitModeInstruction),
|
||||
Misc(strafesnet_common::physics::MiscInstruction),
|
||||
Misc(MiscInstruction),
|
||||
}
|
||||
/// Implicit mode instruction are fed separately to session.
|
||||
/// Session generates the explicit mode instructions interlaced with a SetSensitivity instruction
|
||||
@@ -52,16 +52,23 @@ pub enum SessionControlInstruction{
|
||||
pub enum SessionPlaybackInstruction{
|
||||
SkipForward,
|
||||
SkipBack,
|
||||
TogglePaused,
|
||||
DecreaseTimescale,
|
||||
IncreaseTimescale,
|
||||
}
|
||||
|
||||
pub struct FrameState{
|
||||
pub body:physics::Body,
|
||||
pub trajectory:physics::Trajectory,
|
||||
pub camera:physics::PhysicsCamera,
|
||||
pub time:PhysicsTime,
|
||||
}
|
||||
impl FrameState{
|
||||
pub fn pos(&self)->glam::Vec3{
|
||||
self.trajectory.extrapolated_position(self.time).map(Into::<f32>::into).to_array().into()
|
||||
}
|
||||
pub fn angles(&self)->glam::Vec2{
|
||||
self.camera.simulate_move_angles(glam::IVec2::ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Simulation{
|
||||
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
|
||||
@@ -77,9 +84,9 @@ impl Simulation{
|
||||
physics,
|
||||
}
|
||||
}
|
||||
pub fn get_frame_state(&self,time:SessionTime)->FrameState{
|
||||
pub fn get_frame_state(&self,time:SessionTime,data:&PhysicsData)->FrameState{
|
||||
FrameState{
|
||||
body:self.physics.camera_body(),
|
||||
trajectory:self.physics.camera_trajectory(data),
|
||||
camera:self.physics.camera(),
|
||||
time:self.timer.time(time),
|
||||
}
|
||||
@@ -152,10 +159,10 @@ enum ViewState{
|
||||
pub struct Session{
|
||||
directories:Directories,
|
||||
user_settings:UserSettings,
|
||||
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
|
||||
mouse_interpolator:MouseInterpolator,
|
||||
view_state:ViewState,
|
||||
//gui:GuiState
|
||||
geometry_shared:physics::PhysicsData,
|
||||
geometry_shared:PhysicsData,
|
||||
simulation:Simulation,
|
||||
// below fields not included in lite session
|
||||
recording:Recording,
|
||||
@@ -172,7 +179,7 @@ impl Session{
|
||||
user_settings,
|
||||
directories,
|
||||
mouse_interpolator:MouseInterpolator::new(),
|
||||
geometry_shared:Default::default(),
|
||||
geometry_shared:PhysicsData::empty(),
|
||||
simulation,
|
||||
view_state:ViewState::Play,
|
||||
recording:Default::default(),
|
||||
@@ -184,13 +191,13 @@ impl Session{
|
||||
}
|
||||
fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
|
||||
self.simulation.physics.clear();
|
||||
self.geometry_shared.generate_models(map);
|
||||
self.geometry_shared=PhysicsData::new(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::Play=>Some(self.simulation.get_frame_state(time,&self.geometry_shared)),
|
||||
ViewState::Replay(bot_id)=>self.replays.get(bot_id).map(|replay|
|
||||
replay.simulation.get_frame_state(time)
|
||||
replay.simulation.get_frame_state(time,&self.geometry_shared)
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -239,12 +246,16 @@ impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
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)));
|
||||
// TODO: think about this harder. This works around a bug where you fall infinitely when you reset.
|
||||
self.simulation.timer.set_time(ins.time,PhysicsTime::ZERO);
|
||||
},
|
||||
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)));
|
||||
// TODO: think about this harder. This works around a bug where you fall infinitely when you reset.
|
||||
self.simulation.timer.set_time(ins.time,PhysicsTime::ZERO);
|
||||
},
|
||||
Instruction::Input(SessionInputInstruction::Misc(misc_instruction))=>{
|
||||
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(misc_instruction));
|
||||
@@ -253,7 +264,14 @@ impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
// 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);
|
||||
match &self.view_state{
|
||||
ViewState::Play=>{
|
||||
_=self.simulation.timer.set_paused(ins.time,paused);
|
||||
},
|
||||
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
|
||||
_=replay.simulation.timer.set_paused(ins.time,paused);
|
||||
},
|
||||
}
|
||||
},
|
||||
Instruction::Control(SessionControlInstruction::CopyRecordingIntoReplayAndSpectate)=> if let ViewState::Play=self.view_state{
|
||||
// Bind: B
|
||||
@@ -374,14 +392,6 @@ impl InstructionConsumer<Instruction<'_>> for Session{
|
||||
},
|
||||
}
|
||||
},
|
||||
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);
|
||||
|
||||
@@ -6,5 +6,8 @@ edition = "2024"
|
||||
[dependencies]
|
||||
configparser = "3.0.2"
|
||||
directories = "6.0.0"
|
||||
glam = "0.30.0"
|
||||
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
|
||||
glam.workspace = true
|
||||
strafesnet_common.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -4,6 +4,12 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
|
||||
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
|
||||
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet" }
|
||||
glam.workspace = true
|
||||
strafesnet_common.workspace = true
|
||||
strafesnet_physics.workspace = true
|
||||
strafesnet_snf.workspace = true
|
||||
# this is just for the primitive constructor
|
||||
strafesnet_rbx_loader.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -3,6 +3,8 @@ mod util;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod test_scenes;
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -29,9 +31,8 @@ fn run_replay()->Result<(),ReplayError>{
|
||||
let bot=strafesnet_snf::read_bot(data)?.read_all()?;
|
||||
|
||||
// create recording
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
let physics_data=PhysicsData::new(&map);
|
||||
println!("simulating...");
|
||||
let mut physics=PhysicsState::default();
|
||||
for ins in bot.instructions{
|
||||
@@ -70,16 +71,16 @@ fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsDat
|
||||
for (i,ins) in bot.instructions.into_iter().enumerate(){
|
||||
let state_deterministic=physics_deterministic.clone();
|
||||
let state_filtered=physics_filtered.clone();
|
||||
PhysicsContext::run_input_instruction(&mut physics_deterministic,&physics_data,ins.clone());
|
||||
PhysicsContext::run_input_instruction(&mut physics_deterministic,physics_data,ins.clone());
|
||||
match ins{
|
||||
strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(),
|
||||
other=>{
|
||||
non_idle_count+=1;
|
||||
// run
|
||||
PhysicsContext::run_input_instruction(&mut physics_filtered,&physics_data,other.clone());
|
||||
PhysicsContext::run_input_instruction(&mut physics_filtered,physics_data,other.clone());
|
||||
// check if position matches
|
||||
let b0=physics_deterministic.camera_body();
|
||||
let b1=physics_filtered.camera_body();
|
||||
let b0=physics_deterministic.camera_trajectory(physics_data);
|
||||
let b1=physics_filtered.camera_trajectory(physics_data);
|
||||
if b0.position!=b1.position{
|
||||
let nanoseconds=start.elapsed().as_nanos() as u64;
|
||||
println!("desync at instruction #{}",i);
|
||||
@@ -139,9 +140,8 @@ fn test_determinism()->Result<(),ReplayError>{
|
||||
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
|
||||
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
|
||||
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
let physics_data=PhysicsData::new(&map);
|
||||
|
||||
let (send,recv)=std::sync::mpsc::channel();
|
||||
|
||||
|
||||
113
integration-testing/src/test_scenes.rs
Normal file
113
integration-testing/src/test_scenes.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use strafesnet_physics::physics::{InternalInstruction,PhysicsData,PhysicsState,PhysicsContext};
|
||||
use strafesnet_common::gameplay_modes::NormalizedModes;
|
||||
use strafesnet_common::gameplay_attributes::{CollisionAttributes,CollisionAttributesId};
|
||||
use strafesnet_common::integer::{vec3,mat3,Planar64Affine3,Time};
|
||||
use strafesnet_common::model::{Mesh,Model,MeshId,ModelId,RenderConfigId};
|
||||
use strafesnet_common::map::CompleteMap;
|
||||
use strafesnet_rbx_loader::primitives::{unit_cube,CubeFaceDescription};
|
||||
|
||||
struct TestSceneBuilder{
|
||||
meshes:Vec<Mesh>,
|
||||
models:Vec<Model>,
|
||||
}
|
||||
impl TestSceneBuilder{
|
||||
fn new()->Self{
|
||||
Self{
|
||||
meshes:Vec::new(),
|
||||
models:Vec::new(),
|
||||
}
|
||||
}
|
||||
fn push_mesh(&mut self,mesh:Mesh)->MeshId{
|
||||
let mesh_id=self.meshes.len();
|
||||
self.meshes.push(mesh);
|
||||
MeshId::new(mesh_id as u32)
|
||||
}
|
||||
fn push_mesh_instance(&mut self,mesh:MeshId,transform:Planar64Affine3)->ModelId{
|
||||
let model=Model{
|
||||
mesh,
|
||||
attributes:CollisionAttributesId::new(0),
|
||||
color:glam::Vec4::ONE,
|
||||
transform,
|
||||
};
|
||||
let model_id=self.models.len();
|
||||
self.models.push(model);
|
||||
ModelId::new(model_id as u32)
|
||||
}
|
||||
fn build(self)->PhysicsData{
|
||||
let modes=NormalizedModes::new(Vec::new());
|
||||
let attributes=vec![CollisionAttributes::contact_default()];
|
||||
let meshes=self.meshes;
|
||||
let models=self.models;
|
||||
let textures=Vec::new();
|
||||
let render_configs=Vec::new();
|
||||
PhysicsData::new(&CompleteMap{
|
||||
modes,
|
||||
attributes,
|
||||
meshes,
|
||||
models,
|
||||
textures,
|
||||
render_configs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn test_scene()->PhysicsData{
|
||||
let mut builder=TestSceneBuilder::new();
|
||||
let cube_face_description=CubeFaceDescription::new(Default::default(),RenderConfigId::new(0));
|
||||
let mesh=builder.push_mesh(unit_cube(cube_face_description));
|
||||
// place two 5x5x5 cubes.
|
||||
builder.push_mesh_instance(mesh,Planar64Affine3::new(
|
||||
mat3::from_diagonal(vec3::int(5,5,5)>>1),
|
||||
vec3::int(0,0,0)
|
||||
));
|
||||
builder.push_mesh_instance(mesh,Planar64Affine3::new(
|
||||
mat3::from_diagonal(vec3::int(5,5,5)>>1),
|
||||
vec3::int(5,-5,0)
|
||||
));
|
||||
builder.build()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simultaneous_collision(){
|
||||
let physics_data=test_scene();
|
||||
let body=strafesnet_physics::physics::Body::new(
|
||||
(vec3::int(5+2,0,0)>>1)+vec3::int(1,1,0),
|
||||
vec3::int(-1,-1,0),
|
||||
Time::ZERO,
|
||||
);
|
||||
let mut physics=PhysicsState::new_with_body(body);
|
||||
physics.style_mut().gravity=vec3::zero();
|
||||
let mut phys_iter=PhysicsContext::iter_internal(&mut physics,&physics_data,Time::from_secs(2))
|
||||
.filter(|ins|!matches!(ins.instruction,InternalInstruction::StrafeTick));
|
||||
// the order that they hit does matter, but we aren't currently worrying about that.
|
||||
// See multi-collision branch
|
||||
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(1));
|
||||
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(1));
|
||||
assert!(phys_iter.next().is_none());
|
||||
let body=physics.body();
|
||||
assert_eq!(body.position,vec3::int(5,0,0));
|
||||
assert_eq!(body.velocity,vec3::int(0,0,0));
|
||||
assert_eq!(body.time,Time::from_secs(1));
|
||||
}
|
||||
#[test]
|
||||
fn bug_3(){
|
||||
let physics_data=test_scene();
|
||||
let body=strafesnet_physics::physics::Body::new(
|
||||
(vec3::int(5+2,0,0)>>1)+vec3::int(1,2,0),
|
||||
vec3::int(-1,-1,0),
|
||||
Time::ZERO,
|
||||
);
|
||||
let mut physics=PhysicsState::new_with_body(body);
|
||||
physics.style_mut().gravity=vec3::zero();
|
||||
let mut phys_iter=PhysicsContext::iter_internal(&mut physics,&physics_data,Time::from_secs(3))
|
||||
.filter(|ins|!matches!(ins.instruction,InternalInstruction::StrafeTick));
|
||||
// touch side of part at 0,0,0
|
||||
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(1));
|
||||
// touch top of part at 5,-5,0
|
||||
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(2));
|
||||
assert!(phys_iter.next().is_none());
|
||||
let body=physics.body();
|
||||
assert_eq!(body.position,vec3::int(5+2,0,0)>>1);
|
||||
assert_eq!(body.velocity,vec3::int(0,0,0));
|
||||
assert_eq!(body.time,Time::from_secs(2));
|
||||
}
|
||||
@@ -10,9 +10,8 @@ fn physics_bug_2()->Result<(),ReplayError>{
|
||||
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
|
||||
|
||||
// create recording
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
let physics_data=PhysicsData::new(&map);
|
||||
println!("simulating...");
|
||||
|
||||
//teleport to bug
|
||||
@@ -23,14 +22,13 @@ fn physics_bug_2()->Result<(),ReplayError>{
|
||||
let body=strafesnet_physics::physics::Body::new(
|
||||
vec3::raw_xyz(555690659654,1490485868773,1277783839382),
|
||||
vec3::int(0,0,0),
|
||||
vec3::int(0,-100,0),
|
||||
Time::ZERO,
|
||||
);
|
||||
let mut physics=PhysicsState::new_with_body(body);
|
||||
// wait one second to activate the bug
|
||||
// hit=Some(ModelId(2262))
|
||||
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
|
||||
time:strafesnet_common::integer::Time::from_millis(500),
|
||||
time:Time::from_millis(500),
|
||||
instruction:strafesnet_common::physics::Instruction::Idle,
|
||||
});
|
||||
|
||||
@@ -45,9 +43,8 @@ fn physics_bug_3()->Result<(),ReplayError>{
|
||||
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
|
||||
|
||||
// create recording
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
let physics_data=PhysicsData::new(&map);
|
||||
println!("simulating...");
|
||||
|
||||
//teleport to bug
|
||||
@@ -62,17 +59,38 @@ fn physics_bug_3()->Result<(),ReplayError>{
|
||||
// vec3::raw_xyz(0,-96915585363,1265),
|
||||
// vec3::raw_xyz(0,-429496729600,0),
|
||||
// corner setup before wall hits
|
||||
vec3::raw_xyz(-1392580080675,3325402529458,-2444727738679),
|
||||
vec3::raw_xyz(-30259028820,-22950929553,-71141663007),
|
||||
vec3::raw_xyz(0,-429496729600,0),
|
||||
// vec3::raw_xyz(-1392580080675,3325402529458,-2444727738679),
|
||||
// vec3::raw_xyz(-30259028820,-22950929553,-71141663007),
|
||||
// vec3::raw_xyz(0,-429496729600,0),
|
||||
// Actual bug 3 repro
|
||||
vec3::raw_xyz(-2505538624455,3357963283914,557275711118),
|
||||
vec3::raw_xyz(204188283920,-282280474198,166172785440),
|
||||
Time::ZERO,
|
||||
);
|
||||
let mut physics=PhysicsState::new_with_body(body);
|
||||
// wait one second to activate the bug
|
||||
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
|
||||
time:strafesnet_common::integer::Time::from_millis(500),
|
||||
time:Time::from_millis(500),
|
||||
instruction:strafesnet_common::physics::Instruction::Idle,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn physics_bug_26()->Result<(),ReplayError>{
|
||||
println!("loading map file..");
|
||||
let data=read_entire_file("../tools/bhop_maps/5692124338.snfm")?;
|
||||
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
|
||||
|
||||
// create recording
|
||||
println!("generating models..");
|
||||
let physics_data=PhysicsData::new(&map);
|
||||
println!("reproducing bug...");
|
||||
|
||||
//teleport to bug
|
||||
let fev=physics_data.closest_point(1020,strafesnet_common::integer::vec3::try_from_f32_array([76.889,363.188,-309.263]).unwrap()).unwrap();
|
||||
println!("{fev:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strafesnet_bsp_loader"
|
||||
version = "0.3.1"
|
||||
version = "0.5.0"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -10,10 +10,13 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
glam = "0.30.0"
|
||||
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
|
||||
strafesnet_deferred_loader = { version = "0.5.1", path = "../deferred_loader", registry = "strafesnet" }
|
||||
glam.workspace = true
|
||||
strafesnet_common.workspace = true
|
||||
strafesnet_deferred_loader.workspace = true
|
||||
vbsp = "0.9.1"
|
||||
vbsp-entities-css = "0.6.0"
|
||||
vmdl = "0.2.0"
|
||||
vpk = "0.3.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
use strafesnet_common::integer::Planar64;
|
||||
use strafesnet_common::{model,integer};
|
||||
use strafesnet_common::integer::{vec3::Vector3,Fixed,Ratio};
|
||||
use strafesnet_common::integer::fixed_types::F192_96;
|
||||
use strafesnet_common::integer::vec3::Vector3;
|
||||
use strafesnet_common::integer::{Planar64,Planar64Vec3,Ratio};
|
||||
|
||||
use crate::{valve_transform_normal,valve_transform_dist};
|
||||
|
||||
#[derive(Hash,Eq,PartialEq)]
|
||||
struct Face{
|
||||
normal:integer::Planar64Vec3,
|
||||
dot:integer::Planar64,
|
||||
normal:Planar64Vec3,
|
||||
dot:Planar64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Faces{
|
||||
faces:Vec<Vec<integer::Planar64Vec3>>,
|
||||
faces:Vec<Vec<Planar64Vec3>>,
|
||||
}
|
||||
|
||||
fn solve3(c0:&Face,c1:&Face,c2:&Face)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<3,96>>>{
|
||||
fn solve3(c0:&Face,c1:&Face,c2:&Face)->Option<Ratio<Vector3<F192_96>,F192_96>>{
|
||||
let n0_n1=c0.normal.cross(c1.normal);
|
||||
let det=c2.normal.dot(n0_n1);
|
||||
if det.abs().is_zero(){
|
||||
@@ -82,12 +83,12 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
|
||||
// test if any *other* faces occlude the intersection
|
||||
for new_face in &face_list{
|
||||
// new face occludes intersection point
|
||||
if (new_face.dot.widen_2()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
|
||||
if (new_face.dot.widen_128()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
|
||||
// replace one of the faces with the new face
|
||||
// dont' try to replace face0 because we are exploring that face in particular
|
||||
if let Some(new_intersection)=solve3(face0,new_face,face2){
|
||||
// face1 does not occlude (or intersect) the new intersection
|
||||
if (face1.dot.widen_2()/Planar64::ONE).gt_ratio(face1.normal.dot(new_intersection.num)/new_intersection.den){
|
||||
if (face1.dot.widen_128()/Planar64::ONE).gt_ratio(face1.normal.dot(new_intersection.num)/new_intersection.den){
|
||||
face1=new_face;
|
||||
intersection=new_intersection;
|
||||
continue 'find;
|
||||
@@ -95,7 +96,7 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
|
||||
}
|
||||
if let Some(new_intersection)=solve3(face0,face1,new_face){
|
||||
// face2 does not occlude (or intersect) the new intersection
|
||||
if (face2.dot.widen_2()/Planar64::ONE).gt_ratio(face2.normal.dot(new_intersection.num)/new_intersection.den){
|
||||
if (face2.dot.widen_128()/Planar64::ONE).gt_ratio(face2.normal.dot(new_intersection.num)/new_intersection.den){
|
||||
face2=new_face;
|
||||
intersection=new_intersection;
|
||||
continue 'find;
|
||||
@@ -120,7 +121,7 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
|
||||
continue;
|
||||
}
|
||||
// new_face occludes intersection meaning intersection is not on convex solid and face0 is degenrate
|
||||
if (new_face.dot.widen_2()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
|
||||
if (new_face.dot.widen_128()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
|
||||
// abort! reject face0 entirely
|
||||
continue 'face;
|
||||
}
|
||||
@@ -138,7 +139,7 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
|
||||
loop{
|
||||
// push point onto vertices
|
||||
// problem: this may push a vertex that does not fit in the fixed point range and is thus meaningless
|
||||
face.push(intersection.divide().narrow_1().unwrap());
|
||||
face.push(intersection.divide().narrow_64().unwrap());
|
||||
|
||||
// we looped back around to face1, we're done!
|
||||
if core::ptr::eq(face1,face2){
|
||||
@@ -204,13 +205,13 @@ impl std::fmt::Display for BrushToMeshError{
|
||||
}
|
||||
impl core::error::Error for BrushToMeshError{}
|
||||
|
||||
pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
|
||||
pub fn faces_to_mesh(faces:Vec<Vec<Planar64Vec3>>)->model::Mesh{
|
||||
// generate the mesh
|
||||
let mut mb=model::MeshBuilder::new();
|
||||
let color=mb.acquire_color_id(glam::Vec4::ONE);
|
||||
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
|
||||
// normals are ignored by physics
|
||||
let normal=mb.acquire_normal_id(integer::vec3::ZERO);
|
||||
let normal=mb.acquire_normal_id(integer::vec3::zero());
|
||||
|
||||
let polygon_list=faces.into_iter().map(|face|{
|
||||
face.into_iter().map(|pos|{
|
||||
|
||||
@@ -105,7 +105,7 @@ pub fn convert<'a>(
|
||||
water:Some(attr::IntersectingWater{
|
||||
viscosity:integer::Planar64::ONE,
|
||||
density:integer::Planar64::ONE,
|
||||
velocity:integer::vec3::ZERO,
|
||||
velocity:integer::vec3::zero(),
|
||||
}),
|
||||
},
|
||||
general:attr::GeneralAttributes::default(),
|
||||
@@ -295,7 +295,7 @@ pub fn convert<'a>(
|
||||
attributes,
|
||||
transform:integer::Planar64Affine3::new(
|
||||
integer::mat3::identity(),
|
||||
integer::vec3::ZERO,
|
||||
integer::vec3::zero(),
|
||||
),
|
||||
color:glam::Vec4::ONE,
|
||||
});
|
||||
@@ -347,7 +347,7 @@ pub struct PartialMap1{
|
||||
modes:NormalizedModes,
|
||||
}
|
||||
impl PartialMap1{
|
||||
pub fn add_prop_meshes<'a>(
|
||||
pub fn add_prop_meshes(
|
||||
self,
|
||||
prop_meshes:Meshes<model::Mesh>,
|
||||
)->PartialMap2{
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
|
||||
|
||||
mod bsp;
|
||||
mod mesh;
|
||||
pub mod bsp;
|
||||
pub mod mesh;
|
||||
mod brush;
|
||||
pub mod loader;
|
||||
|
||||
const VALVE_SCALE:f32=1.0/16.0;
|
||||
pub(crate) fn valve_transform_dist(d:f32)->strafesnet_common::integer::Planar64{
|
||||
@@ -28,28 +25,6 @@ impl std::fmt::Display for ReadError{
|
||||
}
|
||||
impl std::error::Error for ReadError{}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoadError{
|
||||
Texture(loader::TextureError),
|
||||
Mesh(loader::MeshError),
|
||||
}
|
||||
impl std::fmt::Display for LoadError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for LoadError{}
|
||||
impl From<loader::TextureError> for LoadError{
|
||||
fn from(value:loader::TextureError)->Self{
|
||||
Self::Texture(value)
|
||||
}
|
||||
}
|
||||
impl From<loader::MeshError> for LoadError{
|
||||
fn from(value:loader::MeshError)->Self{
|
||||
Self::Mesh(value)
|
||||
}
|
||||
}
|
||||
pub struct Bsp{
|
||||
bsp:vbsp::Bsp,
|
||||
case_folded_file_names:std::collections::HashMap<String,String>,
|
||||
@@ -84,28 +59,6 @@ impl Bsp{
|
||||
None=>Ok(None),
|
||||
}
|
||||
}
|
||||
pub fn to_snf(&self,failure_mode:LoadFailureMode,vpk_list:&[Vpk])->Result<strafesnet_common::map::CompleteMap,LoadError>{
|
||||
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
|
||||
let mut mesh_deferred_loader=MeshDeferredLoader::new();
|
||||
|
||||
let map_step1=bsp::convert(
|
||||
self,
|
||||
&mut texture_deferred_loader,
|
||||
&mut mesh_deferred_loader,
|
||||
);
|
||||
|
||||
let mut mesh_loader=loader::MeshLoader::new(loader::BspFinder{bsp:self,vpks:vpk_list},&mut texture_deferred_loader);
|
||||
let prop_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,failure_mode).map_err(LoadError::Mesh)?;
|
||||
|
||||
let map_step2=map_step1.add_prop_meshes(prop_meshes);
|
||||
|
||||
let mut texture_loader=loader::TextureLoader::new();
|
||||
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?;
|
||||
|
||||
let map=map_step2.add_render_configs_and_textures(render_configs);
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
}
|
||||
pub struct Vpk{
|
||||
vpk:vpk::VPK,
|
||||
|
||||
@@ -5,6 +5,8 @@ use strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader;
|
||||
|
||||
use crate::valve_transform;
|
||||
|
||||
pub use model::Mesh;
|
||||
|
||||
fn ingest_vertex(mb:&mut model::MeshBuilder,vertex:&vmdl::vvd::Vertex,color:model::ColorId)->model::VertexId{
|
||||
let pos=mb.acquire_pos_id(valve_transform(vertex.position.into()));
|
||||
let normal=mb.acquire_normal_id(valve_transform(vertex.normal.into()));
|
||||
@@ -17,7 +19,7 @@ fn ingest_vertex(mb:&mut model::MeshBuilder,vertex:&vmdl::vvd::Vertex,color:mode
|
||||
})
|
||||
}
|
||||
|
||||
pub fn convert_mesh(model:vmdl::Model,deferred_loader:&mut RenderConfigDeferredLoader<Cow<str>>)->model::Mesh{
|
||||
pub fn convert_mesh(model:vmdl::Model,deferred_loader:&mut RenderConfigDeferredLoader<Cow<str>>)->Mesh{
|
||||
let texture_paths=model.texture_directories();
|
||||
if texture_paths.len()!=1{
|
||||
println!("WARNING: multiple texture paths");
|
||||
@@ -61,7 +63,7 @@ pub fn convert_mesh(model:vmdl::Model,deferred_loader:&mut RenderConfigDeferredL
|
||||
_=>None,
|
||||
}
|
||||
})
|
||||
}).flat_map(|[v1,v2,v3]|{
|
||||
}).filter_map(|[v1,v2,v3]|{
|
||||
// this should probably be a fatal error :D
|
||||
let v1=model_vertices.get(v1)?;
|
||||
let v2=model_vertices.get(v2)?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strafesnet_common"
|
||||
version = "0.7.0"
|
||||
version = "0.8.7"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -12,8 +12,11 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
[dependencies]
|
||||
arrayvec = "0.7.4"
|
||||
bitflags = "2.6.0"
|
||||
fixed_wide = { version = "0.2.0", path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
|
||||
linear_ops = { version = "0.1.1", path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] }
|
||||
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet" }
|
||||
glam = "0.30.0"
|
||||
fixed_wide = { workspace = true, features = ["deferred-division","zeroes","wide-mul"] }
|
||||
linear_ops = { workspace = true, features = ["deferred-division","named-fields"] }
|
||||
ratio_ops = { workspace = true }
|
||||
glam.workspace = true
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -2,7 +2,9 @@ use crate::integer::{vec3,Planar64Vec3};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Aabb{
|
||||
// min is inclusive
|
||||
min:Planar64Vec3,
|
||||
// max is not inclusive
|
||||
max:Planar64Vec3,
|
||||
}
|
||||
|
||||
@@ -43,7 +45,7 @@ impl Aabb{
|
||||
}
|
||||
#[inline]
|
||||
pub fn contains(&self,point:Planar64Vec3)->bool{
|
||||
let bvec=self.min.lt(point)&point.lt(self.max);
|
||||
let bvec=self.min.le(point)&point.lt(self.max);
|
||||
bvec.all()
|
||||
}
|
||||
#[inline]
|
||||
@@ -59,11 +61,11 @@ impl Aabb{
|
||||
pub fn center(&self)->Planar64Vec3{
|
||||
self.min.map_zip(self.max,|(min,max)|min.midpoint(max))
|
||||
}
|
||||
//probably use floats for area & volume because we don't care about precision
|
||||
// pub fn area_weight(&self)->f32{
|
||||
// let d=self.max-self.min;
|
||||
// d.x*d.y+d.y*d.z+d.z*d.x
|
||||
// }
|
||||
#[inline]
|
||||
pub fn area_weight(&self)->fixed_wide::types::F128_64{
|
||||
let d=self.max-self.min;
|
||||
d.x*d.y+d.y*d.z+d.z*d.x
|
||||
}
|
||||
// pub fn volume(&self)->f32{
|
||||
// let d=self.max-self.min;
|
||||
// d.x*d.y*d.z
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
use crate::aabb::Aabb;
|
||||
use crate::ray::Ray;
|
||||
use crate::integer::{Ratio,Planar64};
|
||||
use crate::integer::{Ratio,Planar64,Planar64Vec3};
|
||||
use crate::instruction::{InstructionCollector,TimedInstruction};
|
||||
|
||||
//da algaritum
|
||||
@@ -140,33 +140,34 @@ impl<L> BvhNode<L>{
|
||||
},
|
||||
}
|
||||
}
|
||||
fn populate_nodes<'a,T,F>(
|
||||
fn populate_nodes<'a,T,IntersectLeaf,IntersectAabb>(
|
||||
&'a self,
|
||||
collector:&mut InstructionCollector<&'a L,Ratio<Planar64,Planar64>>,
|
||||
nodes:&mut BTreeMap<Ratio<Planar64,Planar64>,&'a BvhNode<L>>,
|
||||
ray:&Ray,
|
||||
start_time:Ratio<Planar64,Planar64>,
|
||||
f:&F,
|
||||
start_point:Planar64Vec3,
|
||||
collector:&mut InstructionCollector<&'a L,T>,
|
||||
nodes:&mut BTreeMap<T,&'a BvhNode<L>>,
|
||||
start_time:T,
|
||||
intersect_leaf:&IntersectLeaf,
|
||||
intersect_aabb:&IntersectAabb,
|
||||
)
|
||||
where
|
||||
T:Ord+Copy,
|
||||
Ratio<Planar64,Planar64>:From<T>,
|
||||
F:Fn(&L,&Ray)->Option<T>,
|
||||
IntersectLeaf:Fn(&L)->Option<T>,
|
||||
IntersectAabb:Fn(&Aabb)->Option<T>,
|
||||
{
|
||||
match &self.content{
|
||||
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
|
||||
let ins=TimedInstruction{time:time.into(),instruction:leaf};
|
||||
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
|
||||
RecursiveContent::Leaf(leaf)=>if let Some(time)=intersect_leaf(leaf){
|
||||
let ins=TimedInstruction{time,instruction:leaf};
|
||||
if start_time<ins.time&&ins.time<collector.time(){
|
||||
collector.collect(Some(ins));
|
||||
}
|
||||
},
|
||||
RecursiveContent::Branch(children)=>for child in children{
|
||||
if child.aabb.contains(ray.origin){
|
||||
child.populate_nodes(collector,nodes,ray,start_time,f);
|
||||
if child.aabb.contains(start_point){
|
||||
child.populate_nodes(start_point,collector,nodes,start_time,intersect_leaf,intersect_aabb);
|
||||
}else{
|
||||
// Am I an upcoming superstar?
|
||||
if let Some(t)=intersect_aabb(ray,&child.aabb){
|
||||
if start_time.lt_ratio(t)&&t.lt_ratio(collector.time()){
|
||||
if let Some(t)=intersect_aabb(&child.aabb){
|
||||
if start_time<t&&t<collector.time(){
|
||||
nodes.insert(t,child);
|
||||
}
|
||||
}
|
||||
@@ -174,27 +175,29 @@ impl<L> BvhNode<L>{
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn sample_ray<T,F>(
|
||||
/// Traverse the BVH using the given sampling functions.
|
||||
/// Nodes are tested in order of T returned by IntersectAabb.
|
||||
/// The algorithm ends when T for the next node to test is
|
||||
/// greater than the current best collected T from IntersectLeaf.
|
||||
pub fn traverse<T,IntersectLeaf,IntersectAabb>(
|
||||
&self,
|
||||
ray:&Ray,
|
||||
start_point:Planar64Vec3,
|
||||
start_time:T,
|
||||
time_limit:T,
|
||||
f:F,
|
||||
intersect_leaf:IntersectLeaf,
|
||||
intersect_aabb:IntersectAabb,
|
||||
)->Option<(T,&L)>
|
||||
where
|
||||
T:Ord+Copy,
|
||||
T:From<Ratio<Planar64,Planar64>>,
|
||||
Ratio<Planar64,Planar64>:From<T>,
|
||||
F:Fn(&L,&Ray)->Option<T>,
|
||||
IntersectLeaf:Fn(&L)->Option<T>,
|
||||
IntersectAabb:Fn(&Aabb)->Option<T>,
|
||||
{
|
||||
// source of nondeterminism when Aabb boundaries are coplanar
|
||||
let mut nodes=BTreeMap::new();
|
||||
|
||||
let start_time=start_time.into();
|
||||
let time_limit=time_limit.into();
|
||||
let mut collector=InstructionCollector::new(time_limit);
|
||||
// break open all nodes that contain ray.origin and populate nodes with future intersection times
|
||||
self.populate_nodes(&mut collector,&mut nodes,ray,start_time,&f);
|
||||
self.populate_nodes(start_point,&mut collector,&mut nodes,start_time,&intersect_leaf,&intersect_aabb);
|
||||
|
||||
// swim through nodes one at a time
|
||||
while let Some((t,node))=nodes.pop_first(){
|
||||
@@ -202,18 +205,18 @@ impl<L> BvhNode<L>{
|
||||
break;
|
||||
}
|
||||
match &node.content{
|
||||
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
|
||||
RecursiveContent::Leaf(leaf)=>if let Some(time)=intersect_leaf(leaf){
|
||||
let ins=TimedInstruction{time:time.into(),instruction:leaf};
|
||||
// this lower bound can also be omitted
|
||||
// but it causes type inference errors lol
|
||||
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
|
||||
if start_time<ins.time&&ins.time<collector.time(){
|
||||
collector.collect(Some(ins));
|
||||
}
|
||||
},
|
||||
// break open the node and predict collisions with the child nodes
|
||||
RecursiveContent::Branch(children)=>for child in children{
|
||||
// Am I an upcoming superstar?
|
||||
if let Some(t)=intersect_aabb(ray,&child.aabb){
|
||||
if let Some(t)=intersect_aabb(&child.aabb){
|
||||
// we don't need to check the lower bound
|
||||
// because child aabbs are guaranteed to be within the parent bounds.
|
||||
if t<collector.time(){
|
||||
@@ -245,18 +248,19 @@ pub fn generate_bvh<T>(boxen:Vec<(T,Aabb)>)->BvhNode<T>{
|
||||
|
||||
fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
|
||||
let n=boxen.len();
|
||||
if force||n<20{
|
||||
let mut aabb=Aabb::default();
|
||||
let nodes=boxen.into_iter().map(|b|{
|
||||
aabb.join(&b.1);
|
||||
const MAX_TERMINAL_BRANCH_LEAF_NODES:usize=20;
|
||||
if force||n<MAX_TERMINAL_BRANCH_LEAF_NODES{
|
||||
let mut aabb_outer=Aabb::default();
|
||||
let nodes=boxen.into_iter().map(|(data,aabb)|{
|
||||
aabb_outer.join(&aabb);
|
||||
BvhNode{
|
||||
content:RecursiveContent::Leaf(b.0),
|
||||
aabb:b.1,
|
||||
content:RecursiveContent::Leaf(data),
|
||||
aabb,
|
||||
}
|
||||
}).collect();
|
||||
BvhNode{
|
||||
content:RecursiveContent::Branch(nodes),
|
||||
aabb,
|
||||
aabb:aabb_outer,
|
||||
}
|
||||
}else{
|
||||
let mut sort_x=Vec::with_capacity(n);
|
||||
@@ -272,9 +276,9 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
|
||||
sort_y.sort_by_key(|&(_,c)|c);
|
||||
sort_z.sort_by_key(|&(_,c)|c);
|
||||
let h=n/2;
|
||||
let median_x=sort_x[h].1;
|
||||
let median_y=sort_y[h].1;
|
||||
let median_z=sort_z[h].1;
|
||||
let (_,median_x)=sort_x[h];
|
||||
let (_,median_y)=sort_y[h];
|
||||
let (_,median_z)=sort_z[h];
|
||||
//locate a run of values equal to the median
|
||||
//partition point gives the first index for which the predicate evaluates to false
|
||||
let first_index_eq_median_x=sort_x.partition_point(|&(_,x)|x<median_x);
|
||||
@@ -313,10 +317,10 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
|
||||
};
|
||||
list_list[list_id].push((data,aabb));
|
||||
}
|
||||
let mut aabb=Aabb::default();
|
||||
if list_list.len()==1{
|
||||
generate_bvh_node(list_list.remove(0),true)
|
||||
}else{
|
||||
let mut aabb=Aabb::default();
|
||||
BvhNode{
|
||||
content:RecursiveContent::Branch(
|
||||
list_list.into_iter().map(|b|{
|
||||
|
||||
@@ -34,7 +34,7 @@ pub struct StyleModifiers{
|
||||
//unused
|
||||
pub mass:Planar64,
|
||||
}
|
||||
impl std::default::Default for StyleModifiers{
|
||||
impl Default for StyleModifiers{
|
||||
fn default()->Self{
|
||||
Self::roblox_bhop()
|
||||
}
|
||||
@@ -66,7 +66,7 @@ impl JumpImpulse{
|
||||
_mass:Planar64,
|
||||
)->Planar64Vec3{
|
||||
match self{
|
||||
&JumpImpulse::Time(time)=>velocity-(*gravity*time).map(|t|t.divide().clamp_1()),
|
||||
&JumpImpulse::Time(time)=>velocity-(*gravity*time).map(|t|t.divide().clamp_64()),
|
||||
&JumpImpulse::Height(height)=>{
|
||||
//height==-v.y*v.y/(2*g.y);
|
||||
//use energy to determine max height
|
||||
@@ -74,10 +74,10 @@ impl JumpImpulse{
|
||||
let g=gg.sqrt();
|
||||
let v_g=gravity.dot(velocity);
|
||||
//do it backwards
|
||||
let radicand=v_g*v_g+(g*height*2).widen_4();
|
||||
velocity-(*gravity*(radicand.sqrt().wrap_2()+v_g)/gg).divide().clamp_1()
|
||||
let radicand=v_g*v_g+(g*height*2).widen_256();
|
||||
velocity-(*gravity*(radicand.sqrt().wrap_128()+v_g)/gg).divide().clamp_64()
|
||||
},
|
||||
&JumpImpulse::Linear(jump_speed)=>velocity+(jump_dir*jump_speed/jump_dir.length()).divide().clamp_1(),
|
||||
&JumpImpulse::Linear(jump_speed)=>velocity+(jump_dir*jump_speed/jump_dir.length()).divide().clamp_64(),
|
||||
&JumpImpulse::Energy(_energy)=>{
|
||||
//calculate energy
|
||||
//let e=gravity.dot(velocity);
|
||||
@@ -91,10 +91,10 @@ impl JumpImpulse{
|
||||
pub fn get_jump_deltav(&self,gravity:&Planar64Vec3,mass:Planar64)->Planar64{
|
||||
//gravity.length() is actually the proper calculation because the jump is always opposite the gravity direction
|
||||
match self{
|
||||
&JumpImpulse::Time(time)=>(gravity.length().wrap_1()*time/2).divide().clamp_1(),
|
||||
&JumpImpulse::Height(height)=>(gravity.length()*height*2).sqrt().wrap_1(),
|
||||
&JumpImpulse::Time(time)=>(gravity.length().wrap_64()*time/2).divide().clamp_64(),
|
||||
&JumpImpulse::Height(height)=>(gravity.length()*height*2).sqrt().wrap_64(),
|
||||
&JumpImpulse::Linear(deltav)=>deltav,
|
||||
&JumpImpulse::Energy(energy)=>(energy.sqrt()*2/mass.sqrt()).divide().clamp_1(),
|
||||
&JumpImpulse::Energy(energy)=>(energy.sqrt()*2/mass.sqrt()).divide().clamp_64(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,10 +126,10 @@ impl JumpSettings{
|
||||
None=>rel_velocity,
|
||||
};
|
||||
let j=boost_vel.dot(jump_dir);
|
||||
let js=jump_speed.widen_2();
|
||||
let js=jump_speed.widen_128();
|
||||
if j<js{
|
||||
//weak booster: just do a regular jump
|
||||
boost_vel+jump_dir.with_length(js-j).divide().wrap_1()
|
||||
boost_vel+jump_dir.with_length(js-j).divide().wrap_64()
|
||||
}else{
|
||||
//activate booster normally, jump does nothing
|
||||
boost_vel
|
||||
@@ -142,13 +142,13 @@ impl JumpSettings{
|
||||
None=>rel_velocity,
|
||||
};
|
||||
let j=boost_vel.dot(jump_dir);
|
||||
let js=jump_speed.widen_2();
|
||||
let js=jump_speed.widen_128();
|
||||
if j<js{
|
||||
//speed in direction of jump cannot be lower than amount
|
||||
boost_vel+jump_dir.with_length(js-j).divide().wrap_1()
|
||||
boost_vel+jump_dir.with_length(js-j).divide().wrap_64()
|
||||
}else{
|
||||
//boost and jump add together
|
||||
boost_vel+jump_dir.with_length(js).divide().wrap_1()
|
||||
boost_vel+jump_dir.with_length(js).divide().wrap_64()
|
||||
}
|
||||
}
|
||||
(false,JumpCalculation::Max)=>{
|
||||
@@ -159,10 +159,10 @@ impl JumpSettings{
|
||||
None=>rel_velocity,
|
||||
};
|
||||
let boost_dot=boost_vel.dot(jump_dir);
|
||||
let js=jump_speed.widen_2();
|
||||
let js=jump_speed.widen_128();
|
||||
if boost_dot<js{
|
||||
//weak boost is extended to jump speed
|
||||
boost_vel+jump_dir.with_length(js-boost_dot).divide().wrap_1()
|
||||
boost_vel+jump_dir.with_length(js-boost_dot).divide().wrap_64()
|
||||
}else{
|
||||
//activate booster normally, jump does nothing
|
||||
boost_vel
|
||||
@@ -174,7 +174,7 @@ impl JumpSettings{
|
||||
Some(booster)=>booster.boost(rel_velocity),
|
||||
None=>rel_velocity,
|
||||
};
|
||||
boost_vel+jump_dir.with_length(jump_speed).divide().wrap_1()
|
||||
boost_vel+jump_dir.with_length(jump_speed).divide().wrap_64()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -267,9 +267,9 @@ pub struct StrafeSettings{
|
||||
impl StrafeSettings{
|
||||
pub fn tick_velocity(&self,velocity:Planar64Vec3,control_dir:Planar64Vec3)->Option<Planar64Vec3>{
|
||||
let d=velocity.dot(control_dir);
|
||||
let mv=self.mv.widen_2();
|
||||
let mv=self.mv.widen_128();
|
||||
match d<mv{
|
||||
true=>Some(velocity+(control_dir*self.air_accel_limit.map_or(mv-d,|limit|limit.widen_2().min(mv-d))).wrap_1()),
|
||||
true=>Some(velocity+(control_dir*self.air_accel_limit.map_or(mv-d,|limit|limit.widen_128().min(mv-d))).wrap_64()),
|
||||
false=>None,
|
||||
}
|
||||
}
|
||||
@@ -290,7 +290,7 @@ pub struct PropulsionSettings{
|
||||
}
|
||||
impl PropulsionSettings{
|
||||
pub fn acceleration(&self,control_dir:Planar64Vec3)->Planar64Vec3{
|
||||
(control_dir*self.magnitude).clamp_1()
|
||||
(control_dir*self.magnitude).clamp_64()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,16 +310,16 @@ pub struct WalkSettings{
|
||||
impl WalkSettings{
|
||||
pub fn accel(&self,target_diff:Planar64Vec3,gravity:Planar64Vec3)->Planar64{
|
||||
//TODO: fallible walk accel
|
||||
let diff_len=target_diff.length().wrap_1();
|
||||
let diff_len=target_diff.length().wrap_64();
|
||||
let friction=if diff_len<self.accelerate.topspeed{
|
||||
self.static_friction
|
||||
}else{
|
||||
self.kinetic_friction
|
||||
};
|
||||
self.accelerate.accel.min((-gravity.y*friction).clamp_1())
|
||||
self.accelerate.accel.min((-gravity.y*friction).clamp_64())
|
||||
}
|
||||
pub fn get_walk_target_velocity(&self,control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
|
||||
if control_dir==crate::integer::vec3::ZERO{
|
||||
if control_dir==crate::integer::vec3::zero(){
|
||||
return control_dir;
|
||||
}
|
||||
let nn=normal.length_squared();
|
||||
@@ -329,19 +329,19 @@ impl WalkSettings{
|
||||
let dd=d*d;
|
||||
if dd<nnmm{
|
||||
let cr=normal.cross(control_dir);
|
||||
if cr==crate::integer::vec3::ZERO_2{
|
||||
crate::integer::vec3::ZERO
|
||||
if cr==crate::integer::vec3::zero(){
|
||||
crate::integer::vec3::zero()
|
||||
}else{
|
||||
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
|
||||
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_64()
|
||||
}
|
||||
}else{
|
||||
crate::integer::vec3::ZERO
|
||||
crate::integer::vec3::zero()
|
||||
}
|
||||
}
|
||||
pub fn is_slope_walkable(&self,normal:Planar64Vec3,up:Planar64Vec3)->bool{
|
||||
//normal is not guaranteed to be unit length
|
||||
let ny=normal.dot(up);
|
||||
let h=normal.length().wrap_1();
|
||||
let h=normal.length().wrap_64();
|
||||
//remember this is a normal vector
|
||||
ny.is_positive()&&h*self.surf_dot<ny
|
||||
}
|
||||
@@ -360,7 +360,7 @@ impl LadderSettings{
|
||||
self.accelerate.accel
|
||||
}
|
||||
pub fn get_ladder_target_velocity(&self,mut control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
|
||||
if control_dir==crate::integer::vec3::ZERO{
|
||||
if control_dir==crate::integer::vec3::zero(){
|
||||
return control_dir;
|
||||
}
|
||||
let nn=normal.length_squared();
|
||||
@@ -368,13 +368,13 @@ impl LadderSettings{
|
||||
let nnmm=nn*mm;
|
||||
let d=normal.dot(control_dir);
|
||||
let mut dd=d*d;
|
||||
if (self.dot*self.dot*nnmm).clamp_4()<dd{
|
||||
if (self.dot*self.dot*nnmm).clamp_256()<dd{
|
||||
if d.is_negative(){
|
||||
control_dir=Planar64Vec3::new([Planar64::ZERO,mm.clamp_1(),Planar64::ZERO]);
|
||||
control_dir=Planar64Vec3::new([Planar64::ZERO,mm.clamp_64(),Planar64::ZERO]);
|
||||
}else{
|
||||
control_dir=Planar64Vec3::new([Planar64::ZERO,-mm.clamp_1(),Planar64::ZERO]);
|
||||
control_dir=Planar64Vec3::new([Planar64::ZERO,-mm.clamp_64(),Planar64::ZERO]);
|
||||
}
|
||||
dd=(normal.y*normal.y).widen_4();
|
||||
dd=(normal.y*normal.y).widen_256();
|
||||
}
|
||||
//n=d if you are standing on top of a ladder and press E.
|
||||
//two fixes:
|
||||
@@ -382,13 +382,13 @@ impl LadderSettings{
|
||||
//- fix the underlying issue
|
||||
if dd<nnmm{
|
||||
let cr=normal.cross(control_dir);
|
||||
if cr==crate::integer::vec3::ZERO_2{
|
||||
crate::integer::vec3::ZERO
|
||||
if cr==crate::integer::vec3::zero(){
|
||||
crate::integer::vec3::zero()
|
||||
}else{
|
||||
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
|
||||
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_64()
|
||||
}
|
||||
}else{
|
||||
crate::integer::vec3::ZERO
|
||||
crate::integer::vec3::zero()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,7 +417,7 @@ impl Hitbox{
|
||||
}
|
||||
pub fn source()->Self{
|
||||
Self{
|
||||
halfsize:((int3(33,73,33)>>1)*VALVE_SCALE).narrow_1().unwrap(),
|
||||
halfsize:((int3(33,73,33)>>1)*VALVE_SCALE).narrow_64().unwrap(),
|
||||
mesh:HitboxMesh::Box,
|
||||
}
|
||||
}
|
||||
@@ -538,11 +538,11 @@ impl StyleModifiers{
|
||||
tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
|
||||
}),
|
||||
jump:Some(JumpSettings{
|
||||
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_1().unwrap()),
|
||||
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_64().unwrap()),
|
||||
calculation:JumpCalculation::JumpThenBoost,
|
||||
limit_minimum:true,
|
||||
}),
|
||||
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_1().unwrap(),
|
||||
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_64().unwrap(),
|
||||
mass:int(1),
|
||||
rocket:None,
|
||||
walk:Some(WalkSettings{
|
||||
@@ -565,7 +565,7 @@ impl StyleModifiers{
|
||||
magnitude:int(12),//?
|
||||
}),
|
||||
hitbox:Hitbox::source(),
|
||||
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_1().unwrap(),
|
||||
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_64().unwrap(),
|
||||
}
|
||||
}
|
||||
pub fn source_surf()->Self{
|
||||
@@ -574,16 +574,16 @@ impl StyleModifiers{
|
||||
controls_mask_state:Controls::all(),
|
||||
strafe:Some(StrafeSettings{
|
||||
enable:ControlsActivation::full_2d(),
|
||||
air_accel_limit:Some((int(150)*66*VALVE_SCALE).narrow_1().unwrap()),
|
||||
air_accel_limit:Some((int(150)*66*VALVE_SCALE).narrow_64().unwrap()),
|
||||
mv:Planar64::raw(30<<28),
|
||||
tick_rate:Ratio64::new(66,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
|
||||
}),
|
||||
jump:Some(JumpSettings{
|
||||
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_1().unwrap()),
|
||||
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_64().unwrap()),
|
||||
calculation:JumpCalculation::JumpThenBoost,
|
||||
limit_minimum:true,
|
||||
}),
|
||||
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_1().unwrap(),
|
||||
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_64().unwrap(),
|
||||
mass:int(1),
|
||||
rocket:None,
|
||||
walk:Some(WalkSettings{
|
||||
@@ -606,7 +606,7 @@ impl StyleModifiers{
|
||||
magnitude:int(12),//?
|
||||
}),
|
||||
hitbox:Hitbox::source(),
|
||||
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_1().unwrap(),
|
||||
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_64().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,41 @@ pub trait InstructionFeedback<I,T>:InstructionEmitter<I,Time=T>+InstructionConsu
|
||||
self.process_instruction(instruction);
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
fn into_iter(self,time_limit:T)->InstructionIter<I,T,Self>
|
||||
where
|
||||
Self:Sized
|
||||
{
|
||||
InstructionIter{
|
||||
time_limit,
|
||||
feedback:self,
|
||||
_phantom:core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<I,T,X> InstructionFeedback<I,T> for X
|
||||
impl<I,T,F> InstructionFeedback<I,T> for F
|
||||
where
|
||||
T:Copy,
|
||||
X:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T>,
|
||||
F:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T>,
|
||||
{}
|
||||
pub struct InstructionIter<I,T:Copy,F:InstructionFeedback<I,T>>{
|
||||
time_limit:T,
|
||||
feedback:F,
|
||||
_phantom:core::marker::PhantomData<I>,
|
||||
}
|
||||
impl<I,T,F> Iterator for InstructionIter<I,T,F>
|
||||
where
|
||||
I:Clone,
|
||||
T:Clone+Copy,
|
||||
F:InstructionFeedback<I,T>,
|
||||
{
|
||||
type Item=TimedInstruction<I,T>;
|
||||
fn next(&mut self)->Option<Self::Item>{
|
||||
let instruction=self.feedback.next_instruction(self.time_limit)?;
|
||||
self.feedback.process_instruction(instruction.clone());
|
||||
Some(instruction)
|
||||
}
|
||||
}
|
||||
|
||||
//PROPER PRIVATE FIELDS!!!
|
||||
pub struct InstructionCollector<I,T>{
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
pub use fixed_wide::fixed::*;
|
||||
pub use ratio_ops::ratio::{Ratio,Divide,Parity};
|
||||
|
||||
pub mod fixed_types{
|
||||
pub use fixed_wide::types::*;
|
||||
}
|
||||
|
||||
use fixed_wide::types::F128_64;
|
||||
|
||||
//integer units
|
||||
|
||||
/// specific example of a "default" time type
|
||||
@@ -56,11 +62,19 @@ impl<T> Time<T>{
|
||||
pub const fn coerce<U>(self)->Time<U>{
|
||||
Time::raw(self.0)
|
||||
}
|
||||
#[inline]
|
||||
pub const fn div_euclid(self,other:Self)->Self{
|
||||
Time::raw(self.0.div_euclid(other.0))
|
||||
}
|
||||
#[inline]
|
||||
pub const fn rem_euclid(self,other:Self)->Self{
|
||||
Time::raw(self.0.rem_euclid(other.0))
|
||||
}
|
||||
}
|
||||
impl<T> From<Planar64> for Time<T>{
|
||||
#[inline]
|
||||
fn from(value:Planar64)->Self{
|
||||
Self::raw((value*Planar64::raw(1_000_000_000)).clamp_1().to_raw())
|
||||
Self::raw((value*Planar64::raw(1_000_000_000)).clamp_64().to_raw())
|
||||
}
|
||||
}
|
||||
impl<T> From<Time<T>> for Ratio<Planar64,Planar64>{
|
||||
@@ -86,7 +100,7 @@ impl<T> std::fmt::Display for Time<T>{
|
||||
write!(f,"{}s+{:09}ns",self.0/Self::ONE_SECOND.0,self.0%Self::ONE_SECOND.0)
|
||||
}
|
||||
}
|
||||
impl<T> std::default::Default for Time<T>{
|
||||
impl<T> Default for Time<T>{
|
||||
fn default()->Self{
|
||||
Self::raw(0)
|
||||
}
|
||||
@@ -126,10 +140,10 @@ impl_time_additive_assign_operator!(core::ops::AddAssign,add_assign);
|
||||
impl_time_additive_assign_operator!(core::ops::SubAssign,sub_assign);
|
||||
impl_time_additive_assign_operator!(core::ops::RemAssign,rem_assign);
|
||||
impl<T> std::ops::Mul for Time<T>{
|
||||
type Output=Ratio<fixed_wide::fixed::Fixed<2,64>,fixed_wide::fixed::Fixed<2,64>>;
|
||||
type Output=Ratio<F128_64,F128_64>;
|
||||
#[inline]
|
||||
fn mul(self,rhs:Self)->Self::Output{
|
||||
Ratio::new(Fixed::raw(self.0)*Fixed::raw(rhs.0),Fixed::raw_digit(1_000_000_000i64.pow(2)))
|
||||
Ratio::new(Fixed::raw(self.0)*Fixed::raw(rhs.0),Fixed::from_u64(1_000_000_000u64.pow(2)))
|
||||
}
|
||||
}
|
||||
macro_rules! impl_time_i64_rhs_operator {
|
||||
@@ -148,15 +162,29 @@ impl_time_i64_rhs_operator!(Mul,mul);
|
||||
impl_time_i64_rhs_operator!(Shr,shr);
|
||||
impl_time_i64_rhs_operator!(Shl,shl);
|
||||
impl<T> core::ops::Mul<Time<T>> for Planar64{
|
||||
type Output=Ratio<Fixed<2,64>,Planar64>;
|
||||
type Output=Ratio<F128_64,Planar64>;
|
||||
#[inline]
|
||||
fn mul(self,rhs:Time<T>)->Self::Output{
|
||||
Ratio::new(self*Fixed::raw(rhs.0),Planar64::raw(1_000_000_000))
|
||||
}
|
||||
}
|
||||
impl<T> From<Time<T>> for f32{
|
||||
#[inline]
|
||||
fn from(value:Time<T>)->Self{
|
||||
value.get() as f32/Time::<T>::ONE_SECOND.get() as f32
|
||||
}
|
||||
}
|
||||
impl<T> From<Time<T>> for f64{
|
||||
#[inline]
|
||||
fn from(value:Time<T>)->Self{
|
||||
value.get() as f64/Time::<T>::ONE_SECOND.get() as f64
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test_time{
|
||||
use super::*;
|
||||
type Time=super::AbsoluteTime;
|
||||
use fixed_wide::types::F64_32;
|
||||
type Time=AbsoluteTime;
|
||||
#[test]
|
||||
fn time_from_planar64(){
|
||||
let a:Time=Planar64::from(1).into();
|
||||
@@ -170,13 +198,13 @@ mod test_time{
|
||||
#[test]
|
||||
fn time_squared(){
|
||||
let a=Time::from_secs(2);
|
||||
assert_eq!(a*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))*4,Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))));
|
||||
assert_eq!(a*a,Ratio::new(F128_64::from_u64(1_000_000_000u64.pow(2))*4,F128_64::from_u64(1_000_000_000u64.pow(2))));
|
||||
}
|
||||
#[test]
|
||||
fn time_times_planar64(){
|
||||
let a=Time::from_secs(2);
|
||||
let b=Planar64::from(2);
|
||||
assert_eq!(b*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000*(1<<32))<<2,Fixed::<1,32>::raw_digit(1_000_000_000)));
|
||||
assert_eq!(b*a,Ratio::new(F128_64::from_u64(1_000_000_000*(1<<32))<<2,F64_32::from_u64(1_000_000_000)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,11 +242,11 @@ impl Ratio64{
|
||||
}
|
||||
#[inline]
|
||||
pub const fn mul_int(&self,rhs:i64)->i64{
|
||||
rhs*self.num/(self.den as i64)
|
||||
(rhs as i128*self.num as i128/self.den as i128) as i64
|
||||
}
|
||||
#[inline]
|
||||
pub const fn rhs_div_int(&self,rhs:i64)->i64{
|
||||
rhs*(self.den as i64)/self.num
|
||||
(rhs as i128*self.den as i128/self.num as i128) as i64
|
||||
}
|
||||
#[inline]
|
||||
pub const fn mul_ref(&self,rhs:&Ratio64)->Ratio64{
|
||||
@@ -263,7 +291,6 @@ fn integer_decode_f64(f: f64) -> (u64, i16, i8) {
|
||||
pub enum Ratio64TryFromFloatError{
|
||||
Nan,
|
||||
Infinite,
|
||||
Subnormal,
|
||||
HighlyNegativeExponent(i16),
|
||||
HighlyPositiveExponent(i16),
|
||||
}
|
||||
@@ -298,8 +325,10 @@ fn ratio64_from_mes((m,e,s):(u64,i16,i8))->Result<Ratio64,Ratio64TryFromFloatErr
|
||||
|
||||
Ok(Ratio64::new(num as i64,den as u64).unwrap())
|
||||
}else if e<0{
|
||||
// simple exact representation
|
||||
Ok(Ratio64::new((m as i64)*(s as i64),1<<-e).unwrap())
|
||||
}else if (64-m.leading_zeros() as i16)+e<64{
|
||||
// integer
|
||||
Ok(Ratio64::new((m as i64)*(s as i64)*(1<<e),1).unwrap())
|
||||
}else{
|
||||
Err(Ratio64TryFromFloatError::HighlyPositiveExponent(e))
|
||||
@@ -331,6 +360,29 @@ impl TryFrom<f64> for Ratio64{
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
fn req(r0:Ratio64,r1:Ratio64){
|
||||
println!("r0={r0:?} r1={r1:?}");
|
||||
assert_eq!(r0.num(),r1.num(),"Nums not eq");
|
||||
assert_eq!(r0.den(),r1.den(),"Dens not eq");
|
||||
}
|
||||
#[test]
|
||||
fn test_ratio64_from_float(){
|
||||
req(2.0.try_into().unwrap(),Ratio64::new(2,1).unwrap());
|
||||
req(1.0.try_into().unwrap(),Ratio64::new(1,1).unwrap());
|
||||
req(0.5.try_into().unwrap(),Ratio64::new(1,2).unwrap());
|
||||
req(1.1.try_into().unwrap(),Ratio64::new(2476979795053773,2251799813685248).unwrap());
|
||||
req(0.8.try_into().unwrap(),Ratio64::new(3602879701896397,4503599627370496).unwrap());
|
||||
req(0.61.try_into().unwrap(),Ratio64::new(5494391545392005,9007199254740992).unwrap());
|
||||
req(0.01.try_into().unwrap(),Ratio64::new(5764607523034235,576460752303423488).unwrap());
|
||||
req(0.001.try_into().unwrap(),Ratio64::new(1152921504606847,1152921504606846976).unwrap());
|
||||
req(0.00001.try_into().unwrap(),Ratio64::new(89605456633725,8960545663372499267).unwrap());
|
||||
req(0.00000000001.try_into().unwrap(),Ratio64::new(35204848,3520484800000000213).unwrap());
|
||||
req(0.000000000000000001.try_into().unwrap(),Ratio64::new(11,10999999999999999213).unwrap());
|
||||
req(2222222222222.0.try_into().unwrap(),Ratio64::new(2222222222222,1).unwrap());
|
||||
req(core::f64::consts::PI.try_into().unwrap(),Ratio64::new(884279719003555,281474976710656).unwrap());
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Ratio64> for Ratio64{
|
||||
type Output=Ratio64;
|
||||
#[inline]
|
||||
@@ -520,8 +572,8 @@ fn angle_sin_cos(){
|
||||
println!("cordic s={} c={}",(s/h).divide(),(c/h).divide());
|
||||
let (fs,fc)=f.sin_cos();
|
||||
println!("float s={} c={}",fs,fc);
|
||||
assert!(close_enough((c/h).divide().wrap_1(),Planar64::raw((fc*((1u64<<32) as f64)) as i64)));
|
||||
assert!(close_enough((s/h).divide().wrap_1(),Planar64::raw((fs*((1u64<<32) as f64)) as i64)));
|
||||
assert!(close_enough((c/h).divide().wrap_64(),Planar64::raw((fc*((1u64<<32) as f64)) as i64)));
|
||||
assert!(close_enough((s/h).divide().wrap_64(),Planar64::raw((fs*((1u64<<32) as f64)) as i64)));
|
||||
}
|
||||
test_angle(1.0);
|
||||
test_angle(std::f64::consts::PI/4.0);
|
||||
@@ -552,8 +604,8 @@ impl TryFrom<[f32;3]> for Unit32Vec3{
|
||||
}
|
||||
*/
|
||||
|
||||
pub type Planar64TryFromFloatError=fixed_wide::fixed::FixedFromFloatError;
|
||||
pub type Planar64=fixed_wide::types::I32F32;
|
||||
pub type Planar64TryFromFloatError=FixedFromFloatError;
|
||||
pub type Planar64=fixed_wide::types::F64_32;
|
||||
pub type Planar64Vec3=linear_ops::types::Vector3<Planar64>;
|
||||
pub type Planar64Mat3=linear_ops::types::Matrix3<Planar64>;
|
||||
pub mod vec3{
|
||||
@@ -561,12 +613,6 @@ pub mod vec3{
|
||||
pub use linear_ops::types::Vector3;
|
||||
pub const MIN:Planar64Vec3=Planar64Vec3::new([Planar64::MIN;3]);
|
||||
pub const MAX:Planar64Vec3=Planar64Vec3::new([Planar64::MAX;3]);
|
||||
pub const ZERO:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO;3]);
|
||||
pub const ZERO_2:linear_ops::types::Vector3<Fixed::<2,64>>=linear_ops::types::Vector3::new([Fixed::<2,64>::ZERO;3]);
|
||||
pub const ZERO_3:linear_ops::types::Vector3<Fixed::<3,96>>=linear_ops::types::Vector3::new([Fixed::<3,96>::ZERO;3]);
|
||||
pub const ZERO_4:linear_ops::types::Vector3<Fixed::<4,128>>=linear_ops::types::Vector3::new([Fixed::<4,128>::ZERO;3]);
|
||||
pub const ZERO_5:linear_ops::types::Vector3<Fixed::<5,160>>=linear_ops::types::Vector3::new([Fixed::<5,160>::ZERO;3]);
|
||||
pub const ZERO_6:linear_ops::types::Vector3<Fixed::<6,192>>=linear_ops::types::Vector3::new([Fixed::<6,192>::ZERO;3]);
|
||||
pub const X:Planar64Vec3=Planar64Vec3::new([Planar64::ONE,Planar64::ZERO,Planar64::ZERO]);
|
||||
pub const Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ONE,Planar64::ZERO]);
|
||||
pub const Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::ONE]);
|
||||
@@ -575,6 +621,10 @@ pub mod vec3{
|
||||
pub const NEG_Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::NEG_ONE,Planar64::ZERO]);
|
||||
pub const NEG_Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::NEG_ONE]);
|
||||
pub const NEG_ONE:Planar64Vec3=Planar64Vec3::new([Planar64::NEG_ONE,Planar64::NEG_ONE,Planar64::NEG_ONE]);
|
||||
// TODO: use #![feature(generic_const_items)] when stabilized https://github.com/rust-lang/rust/issues/113521
|
||||
pub const fn zero<const N:usize,const F:usize>()->Vector3<Fixed<N,F>>{
|
||||
Vector3::new([Fixed::ZERO;3])
|
||||
}
|
||||
#[inline]
|
||||
pub const fn int(x:i32,y:i32,z:i32)->Planar64Vec3{
|
||||
Planar64Vec3::new([Planar64::raw((x as i64)<<32),Planar64::raw((y as i64)<<32),Planar64::raw((z as i64)<<32)])
|
||||
@@ -634,8 +684,8 @@ pub mod mat3{
|
||||
let (yc,ys)=y.cos_sin();
|
||||
Planar64Mat3::from_cols([
|
||||
Planar64Vec3::new([xc,Planar64::ZERO,-xs]),
|
||||
Planar64Vec3::new([(xs*ys).wrap_1(),yc,(xc*ys).wrap_1()]),
|
||||
Planar64Vec3::new([(xs*yc).wrap_1(),-ys,(xc*yc).wrap_1()]),
|
||||
Planar64Vec3::new([(xs*ys).wrap_64(),yc,(xc*ys).wrap_64()]),
|
||||
Planar64Vec3::new([(xs*yc).wrap_64(),-ys,(xc*yc).wrap_64()]),
|
||||
])
|
||||
}
|
||||
#[inline]
|
||||
@@ -663,7 +713,7 @@ pub struct Planar64Affine3{
|
||||
pub translation:Planar64Vec3,
|
||||
}
|
||||
impl Planar64Affine3{
|
||||
pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::ZERO);
|
||||
pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::zero());
|
||||
#[inline]
|
||||
pub const fn new(matrix3:Planar64Mat3,translation:Planar64Vec3)->Self{
|
||||
Self{matrix3,translation}
|
||||
@@ -676,8 +726,8 @@ impl Planar64Affine3{
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn transform_point3(&self,point:Planar64Vec3)->vec3::Vector3<Fixed<2,64>>{
|
||||
self.translation.widen_2()+self.matrix3*point
|
||||
pub fn transform_point3(&self,point:Planar64Vec3)->vec3::Vector3<F128_64>{
|
||||
self.translation.widen_128()+self.matrix3*point
|
||||
}
|
||||
}
|
||||
impl Into<glam::Mat4> for Planar64Affine3{
|
||||
|
||||
@@ -36,6 +36,7 @@ pub enum SetControlInstruction{
|
||||
SetMoveForward(bool),
|
||||
SetJump(bool),
|
||||
SetZoom(bool),
|
||||
SetSprint(bool),
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum ModeInstruction{
|
||||
|
||||
@@ -53,36 +53,23 @@ impl std::fmt::Display for Error{
|
||||
impl std::error::Error for Error{}
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
enum RunState{
|
||||
pub enum RunState{
|
||||
Created,
|
||||
Started{timer:TimerFixed<Realtime<PhysicsTimeInner,TimeInner>,Unpaused>},
|
||||
Finished{timer:TimerFixed<Realtime<PhysicsTimeInner,TimeInner>,Paused>},
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct Run{
|
||||
state:RunState,
|
||||
flagged:Option<FlagReason>,
|
||||
}
|
||||
|
||||
impl Run{
|
||||
pub fn new()->Self{
|
||||
Self{
|
||||
state:RunState::Created,
|
||||
flagged:None,
|
||||
}
|
||||
}
|
||||
impl RunState{
|
||||
pub fn time(&self,time:PhysicsTime)->Time{
|
||||
match &self.state{
|
||||
match &self{
|
||||
RunState::Created=>Time::ZERO,
|
||||
RunState::Started{timer}=>timer.time(time),
|
||||
RunState::Finished{timer}=>timer.time(),
|
||||
}
|
||||
}
|
||||
pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{
|
||||
match &self.state{
|
||||
match &self{
|
||||
RunState::Created=>{
|
||||
self.state=RunState::Started{
|
||||
*self=RunState::Started{
|
||||
timer:TimerFixed::new(time,Time::ZERO),
|
||||
};
|
||||
Ok(())
|
||||
@@ -93,10 +80,10 @@ impl Run{
|
||||
}
|
||||
pub fn finish(&mut self,time:PhysicsTime)->Result<(),Error>{
|
||||
//this uses Copy
|
||||
match &self.state{
|
||||
match &self{
|
||||
RunState::Created=>Err(Error::NotStarted),
|
||||
RunState::Started{timer}=>{
|
||||
self.state=RunState::Finished{
|
||||
*self=RunState::Finished{
|
||||
timer:timer.into_paused(time),
|
||||
};
|
||||
Ok(())
|
||||
@@ -104,12 +91,39 @@ impl Run{
|
||||
RunState::Finished{..}=>Err(Error::AlreadyFinished),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct Run{
|
||||
state:RunState,
|
||||
flag_reason:Option<FlagReason>,
|
||||
}
|
||||
|
||||
impl Run{
|
||||
pub fn new()->Self{
|
||||
Self{
|
||||
state:RunState::Created,
|
||||
flag_reason:None,
|
||||
}
|
||||
}
|
||||
pub fn time(&self,time:PhysicsTime)->Time{
|
||||
self.state.time(time)
|
||||
}
|
||||
pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{
|
||||
self.state.start(time)
|
||||
}
|
||||
pub fn finish(&mut self,time:PhysicsTime)->Result<(),Error>{
|
||||
self.state.finish(time)
|
||||
}
|
||||
pub fn flag(&mut self,flag_reason:FlagReason){
|
||||
//don't replace the first reason the run was flagged
|
||||
if self.flagged.is_none(){
|
||||
self.flagged=Some(flag_reason);
|
||||
if self.flag_reason.is_none(){
|
||||
self.flag_reason=Some(flag_reason);
|
||||
}
|
||||
}
|
||||
pub fn flag_reason(&self)->Option<FlagReason>{
|
||||
self.flag_reason
|
||||
}
|
||||
pub fn get_finish_time(&self)->Option<Time>{
|
||||
match &self.state{
|
||||
RunState::Finished{timer}=>Some(timer.time()),
|
||||
|
||||
@@ -69,10 +69,8 @@ impl<In,Out> Scaled<In,Out>
|
||||
const fn get_scale(&self)->Ratio64{
|
||||
self.scale
|
||||
}
|
||||
fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
|
||||
let new_time=self.get_time(time);
|
||||
const fn set_scale(&mut self,new_scale:Ratio64){
|
||||
self.scale=new_scale;
|
||||
self.set_time(time,new_time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +130,7 @@ pub struct TimerFixed<T:TimerState,P:PauseState>{
|
||||
_paused:P,
|
||||
}
|
||||
|
||||
//scaled timer methods are generic across PauseState
|
||||
//some scaled timer methods are generic across PauseState
|
||||
impl<P:PauseState,In,Out> TimerFixed<Scaled<In,Out>,P>
|
||||
where Time<In>:Copy,
|
||||
{
|
||||
@@ -147,8 +145,22 @@ impl<P:PauseState,In,Out> TimerFixed<Scaled<In,Out>,P>
|
||||
pub const fn get_scale(&self)->Ratio64{
|
||||
self.state.get_scale()
|
||||
}
|
||||
}
|
||||
// setting the scale of an unpaused timer is different than a paused timer
|
||||
impl<In,Out> TimerFixed<Scaled<In,Out>,Unpaused>
|
||||
where Time<In>:Copy,
|
||||
{
|
||||
pub fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
|
||||
self.state.set_scale(time,new_scale)
|
||||
let new_time=self.state.get_time(time);
|
||||
self.state.set_scale(new_scale);
|
||||
self.state.set_time(time,new_time);
|
||||
}
|
||||
}
|
||||
impl<In,Out> TimerFixed<Scaled<In,Out>,Paused>
|
||||
where Time<In>:Copy,
|
||||
{
|
||||
pub fn set_scale(&mut self,new_scale:Ratio64){
|
||||
self.state.set_scale(new_scale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +317,7 @@ impl<In,Out> Timer<Scaled<In,Out>>
|
||||
}
|
||||
pub fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
|
||||
match self{
|
||||
Self::Paused(timer)=>timer.set_scale(time,new_scale),
|
||||
Self::Paused(timer)=>timer.set_scale(new_scale),
|
||||
Self::Unpaused(timer)=>timer.set_scale(time,new_scale),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strafesnet_deferred_loader"
|
||||
version = "0.5.1"
|
||||
version = "0.6.0"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -10,4 +10,7 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
|
||||
strafesnet_common.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -4,5 +4,5 @@ pub trait Loader{
|
||||
type Error:Error;
|
||||
type Index<'a> where Self:'a;
|
||||
type Resource;
|
||||
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>;
|
||||
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fixed_wide"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -14,7 +14,10 @@ wide-mul=[]
|
||||
zeroes=["dep:arrayvec"]
|
||||
|
||||
[dependencies]
|
||||
bnum = "0.13.0"
|
||||
bnum = "0.14.3"
|
||||
arrayvec = { version = "0.7.6", optional = true }
|
||||
paste = "1.0.15"
|
||||
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true }
|
||||
ratio_ops = { workspace = true, optional = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,208 +1,273 @@
|
||||
use crate::types::I32F32;
|
||||
use crate::types::I256F256;
|
||||
use crate::fixed::Fixed;
|
||||
use crate::types::{F64_32,F128_64,F192_96,F512_256};
|
||||
|
||||
#[test]
|
||||
fn you_can_add_numbers(){
|
||||
let a=I256F256::from((3i128*2).pow(4));
|
||||
assert_eq!(a+a,I256F256::from((3i128*2).pow(4)*2));
|
||||
let a=F512_256::from((3i128*2).pow(4));
|
||||
assert_eq!(a+a,F512_256::from((3i128*2).pow(4)*2));
|
||||
}
|
||||
|
||||
macro_rules! test_bit_by_bit{
|
||||
($n:expr,$float:ty,$mantissa_bits:expr)=>{{
|
||||
const MANT:u64=(1<<$mantissa_bits)-1;
|
||||
// all bits in range
|
||||
for i in 0..$n-$mantissa_bits{
|
||||
let a=Fixed::<{$n/8},{$n>>1}>::from_bits(bnum::cast::As::as_::<bnum::Int::<{$n/8}>>(MANT).shl(i));
|
||||
let b=(MANT as $float)*(2.0 as $float).powi(i as i32-{$n>>1});
|
||||
let f:$float=a.into();
|
||||
assert_eq!(f,b,"F{}_{} Into float {i}",$n,$n>>1);
|
||||
assert_eq!(a,b.try_into().unwrap(),"F{}_{} From float {i}",$n,$n>>1);
|
||||
}
|
||||
// underflow
|
||||
for i in 0u32..$mantissa_bits{
|
||||
let a=Fixed::<{$n/8},{$n>>1}>::from_bits(bnum::cast::As::as_::<bnum::Int::<{$n/8}>>(MANT>>i));
|
||||
let b=((MANT>>i) as $float)*(2.0 as $float).powi(-{$n>>1});
|
||||
let f:$float=a.into();
|
||||
assert_eq!(f,b,"Underflow F{}_{} Into float {i}",$n,$n>>1);
|
||||
assert_eq!(a,b.try_into().unwrap(),"Underflow F{}_{} From float {i}",$n,$n>>1);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_many(){
|
||||
test_bit_by_bit!(64,f32,24);
|
||||
test_bit_by_bit!(128,f32,24);
|
||||
// f32 is reaching its limits here
|
||||
// test_bit_by_bit!(256,f32,24);
|
||||
// test_bit_by_bit!(512,f32,24);
|
||||
test_bit_by_bit!(64,f64,53);
|
||||
test_bit_by_bit!(128,f64,53);
|
||||
test_bit_by_bit!(256,f64,53);
|
||||
test_bit_by_bit!(512,f64,53);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_f32(){
|
||||
let a=I256F256::from(1)>>2;
|
||||
let a=F64_32::ZERO;
|
||||
let f:f32=a.into();
|
||||
assert_eq!(f,0.0f32);
|
||||
let a=F64_32::from(1)>>2;
|
||||
let f:f32=a.into();
|
||||
assert_eq!(f,0.25f32);
|
||||
let f:f32=(-a).into();
|
||||
assert_eq!(f,-0.25f32);
|
||||
let a=I256F256::from(0);
|
||||
let a=F64_32::MIN;
|
||||
let f:f32=a.into();
|
||||
assert_eq!(f,i32::MIN as f32);
|
||||
let a=F512_256::from(1)>>2;
|
||||
let f:f32=a.into();
|
||||
assert_eq!(f,0.25f32);
|
||||
let f:f32=(-a).into();
|
||||
assert_eq!(f,-0.25f32);
|
||||
let a=F512_256::from(0);
|
||||
let f:f32=(-a).into();
|
||||
assert_eq!(f,0f32);
|
||||
let a=I256F256::from(237946589723468975i64)<<16;
|
||||
let a=F512_256::from(237946589723468975i64)<<16;
|
||||
let f:f32=a.into();
|
||||
assert_eq!(f,237946589723468975f32*2.0f32.powi(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_f64(){
|
||||
let a=I256F256::from(1)>>2;
|
||||
let a=F64_32::ZERO;
|
||||
let f:f64=a.into();
|
||||
assert_eq!(f,0.0f64);
|
||||
let a=F64_32::from(1)>>2;
|
||||
let f:f64=a.into();
|
||||
assert_eq!(f,0.25f64);
|
||||
let f:f64=(-a).into();
|
||||
assert_eq!(f,-0.25f64);
|
||||
let a=I256F256::from(0);
|
||||
let a=F64_32::MIN;
|
||||
let f:f64=a.into();
|
||||
assert_eq!(f,i32::MIN as f64);
|
||||
let a=F512_256::from(1)>>2;
|
||||
let f:f64=a.into();
|
||||
assert_eq!(f,0.25f64);
|
||||
let f:f64=(-a).into();
|
||||
assert_eq!(f,-0.25f64);
|
||||
let a=F512_256::from(0);
|
||||
let f:f64=(-a).into();
|
||||
assert_eq!(f,0f64);
|
||||
let a=I256F256::from(237946589723468975i64)<<16;
|
||||
let a=F512_256::from(237946589723468975i64)<<16;
|
||||
let f:f64=a.into();
|
||||
assert_eq!(f,237946589723468975f64*2.0f64.powi(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_f32(){
|
||||
let a=I256F256::from(1)>>2;
|
||||
let b:Result<I256F256,_>=0.25f32.try_into();
|
||||
let a=F64_32::ZERO;
|
||||
let b:Result<F64_32,_>=0.0f32.try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
let a=I256F256::from(-1)>>2;
|
||||
let b:Result<I256F256,_>=(-0.25f32).try_into();
|
||||
let a=F512_256::from(1)>>2;
|
||||
let b:Result<F512_256,_>=0.25f32.try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
let a=I256F256::from(0);
|
||||
let b:Result<I256F256,_>=0.try_into();
|
||||
let a=F512_256::from(-1)>>2;
|
||||
let b:Result<F512_256,_>=(-0.25f32).try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
let a=I256F256::from(0b101011110101001010101010000000000000000000000000000i64)<<16;
|
||||
let b:Result<I256F256,_>=(0b101011110101001010101010000000000000000000000000000u64 as f32*2.0f32.powi(16)).try_into();
|
||||
let a=F512_256::from(0);
|
||||
let b:Result<F512_256,_>=0.try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
let a=F512_256::from(0b101011110101001010101010000000000000000000000000000i64)<<16;
|
||||
let b:Result<F512_256,_>=(0b101011110101001010101010000000000000000000000000000u64 as f32*2.0f32.powi(16)).try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
//I32F32::MAX into f32 is truncated into this value
|
||||
let a=I32F32::raw(0b111111111111111111111111000000000000000000000000000000000000000i64);
|
||||
let b:Result<I32F32,_>=Into::<f32>::into(I32F32::MAX).try_into();
|
||||
let a=F64_32::raw(0b111111111111111111111111000000000000000000000000000000000000000i64);
|
||||
let b:Result<F64_32,_>=Into::<f32>::into(F64_32::MAX).try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
//I32F32::MIN hits a special case since it's not representable as a positive signed integer
|
||||
//TODO: don't return an overflow because this is technically possible
|
||||
let _a=I32F32::MIN;
|
||||
let b:Result<I32F32,_>=Into::<f32>::into(I32F32::MIN).try_into();
|
||||
let a=F64_32::MIN;
|
||||
let f:f32=a.into();
|
||||
let b:Result<F64_32,_>=f.try_into();
|
||||
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
|
||||
//16 is within the 24 bits of float precision
|
||||
let b:Result<I32F32,_>=Into::<f32>::into(-I32F32::MIN.widen_2()).try_into();
|
||||
let a=-F64_32::MIN.widen_128();
|
||||
let f:f32=a.into();
|
||||
let b:Result<F64_32,_>=f.try_into();
|
||||
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
|
||||
let b:Result<I32F32,_>=f32::MIN_POSITIVE.try_into();
|
||||
let b:Result<F64_32,_>=f32::MIN_POSITIVE.try_into();
|
||||
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Underflow));
|
||||
//test many cases
|
||||
for i in 0..64{
|
||||
let a=crate::fixed::Fixed::<2,64>::raw_digit(0b111111111111111111111111000000000000000000000000000000000000000i64)<<i;
|
||||
let a=F128_64::from_u64(0b111111111111111111111111000000000000000000000000000000000000000u64)<<i;
|
||||
let f:f32=a.into();
|
||||
let b:Result<crate::fixed::Fixed<2,64>,_>=f.try_into();
|
||||
let b:Result<F128_64,_>=f.try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_f64(){
|
||||
let a=I256F256::from(1)>>2;
|
||||
let b:Result<I256F256,_>=0.25f64.try_into();
|
||||
let a=F64_32::ZERO;
|
||||
let b:Result<F64_32,_>=0.0f64.try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
let a=I256F256::from(-1)>>2;
|
||||
let b:Result<I256F256,_>=(-0.25f64).try_into();
|
||||
let a=F512_256::from(1)>>2;
|
||||
let b:Result<F512_256,_>=0.25f64.try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
let a=I256F256::from(0);
|
||||
let b:Result<I256F256,_>=0.try_into();
|
||||
let a=F512_256::from(-1)>>2;
|
||||
let b:Result<F512_256,_>=(-0.25f64).try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
let a=I256F256::from(0b101011110101001010101010000000000000000000000000000i64)<<16;
|
||||
let b:Result<I256F256,_>=(0b101011110101001010101010000000000000000000000000000u64 as f64*2.0f64.powi(16)).try_into();
|
||||
let a=F512_256::from(0);
|
||||
let b:Result<F512_256,_>=0.try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
let a=F512_256::from(0b101011110101001010101010000000000000000000000000000i64)<<16;
|
||||
let b:Result<F512_256,_>=(0b101011110101001010101010000000000000000000000000000u64 as f64*2.0f64.powi(16)).try_into();
|
||||
assert_eq!(b,Ok(a));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn you_can_shr_numbers(){
|
||||
let a=I32F32::from(4);
|
||||
assert_eq!(a>>1,I32F32::from(2));
|
||||
let a=F64_32::from(4);
|
||||
assert_eq!(a>>1,F64_32::from(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wide_mul(){
|
||||
let a=I32F32::ONE;
|
||||
let aa=a.wide_mul_1_1(a);
|
||||
assert_eq!(aa,crate::types::I64F64::ONE);
|
||||
let a=F64_32::ONE;
|
||||
let aa=a.wide_mul_64_64(a);
|
||||
assert_eq!(aa,F128_64::ONE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wide_div(){
|
||||
let a=I32F32::ONE*4;
|
||||
let b=I32F32::ONE*2;
|
||||
let wide_a=a.wide_mul_1_1(I32F32::ONE);
|
||||
let wide_b=b.wide_mul_1_1(I32F32::ONE);
|
||||
let ab=a.wide_div_1_1(b);
|
||||
assert_eq!(ab,crate::types::I64F64::ONE*2);
|
||||
let wab=wide_a.wide_div_2_1(b);
|
||||
assert_eq!(wab,crate::fixed::Fixed::<3,96>::ONE*2);
|
||||
let awb=a.wide_div_1_2(wide_b);
|
||||
assert_eq!(awb,crate::fixed::Fixed::<3,96>::ONE*2);
|
||||
let a=F64_32::ONE*4;
|
||||
let b=F64_32::ONE*2;
|
||||
let wide_a=a.wide_mul_64_64(F64_32::ONE);
|
||||
let wide_b=b.wide_mul_64_64(F64_32::ONE);
|
||||
let ab=a.wide_div_64_64(b);
|
||||
assert_eq!(ab,F128_64::ONE*2);
|
||||
let wab=wide_a.wide_div_128_64(b);
|
||||
assert_eq!(wab,F192_96::ONE*2);
|
||||
let awb=a.wide_div_64_128(wide_b);
|
||||
assert_eq!(awb,F192_96::ONE*2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wide_mul_repeated() {
|
||||
let a=I32F32::from(2);
|
||||
let b=I32F32::from(3);
|
||||
let a=F64_32::from(2);
|
||||
let b=F64_32::from(3);
|
||||
|
||||
let w1=a.wide_mul_1_1(b);
|
||||
let w2=w1.wide_mul_2_2(w1);
|
||||
let w3=w2.wide_mul_4_4(w2);
|
||||
let w1=a.wide_mul_64_64(b);
|
||||
let w2=w1.wide_mul_128_128(w1);
|
||||
let w3=w2.wide_mul_256_256(w2);
|
||||
|
||||
assert_eq!(w3,I256F256::from((3i128*2).pow(4)));
|
||||
assert_eq!(w3,F512_256::from((3i128*2).pow(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bint(){
|
||||
let a=I32F32::ONE;
|
||||
assert_eq!(a*2,I32F32::from(2));
|
||||
let a=F64_32::ONE;
|
||||
assert_eq!(a*2,F64_32::from(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap(){
|
||||
assert_eq!(I32F32::ONE,I256F256::ONE.wrap_1());
|
||||
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.wrap_1());
|
||||
assert_eq!(F64_32::ONE,F512_256::ONE.wrap_64());
|
||||
assert_eq!(F64_32::NEG_ONE,F512_256::NEG_ONE.wrap_64());
|
||||
}
|
||||
#[test]
|
||||
fn test_narrow(){
|
||||
assert_eq!(Ok(I32F32::ONE),I256F256::ONE.narrow_1());
|
||||
assert_eq!(Ok(I32F32::NEG_ONE),I256F256::NEG_ONE.narrow_1());
|
||||
assert_eq!(Ok(F64_32::ONE),F512_256::ONE.narrow_64());
|
||||
assert_eq!(Ok(F64_32::NEG_ONE),F512_256::NEG_ONE.narrow_64());
|
||||
}
|
||||
#[test]
|
||||
fn test_widen(){
|
||||
assert_eq!(I32F32::ONE.widen_8(),I256F256::ONE);
|
||||
assert_eq!(I32F32::NEG_ONE.widen_8(),I256F256::NEG_ONE);
|
||||
assert_eq!(F64_32::ONE.widen_512(),F512_256::ONE);
|
||||
assert_eq!(F64_32::NEG_ONE.widen_512(),F512_256::NEG_ONE);
|
||||
}
|
||||
#[test]
|
||||
fn test_clamp(){
|
||||
assert_eq!(I32F32::ONE,I256F256::ONE.clamp_1());
|
||||
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.clamp_1());
|
||||
assert_eq!(F64_32::ONE,F512_256::ONE.clamp_64());
|
||||
assert_eq!(F64_32::NEG_ONE,F512_256::NEG_ONE.clamp_64());
|
||||
}
|
||||
#[test]
|
||||
fn test_sqrt(){
|
||||
let a=I32F32::ONE*4;
|
||||
assert_eq!(a.sqrt(),I32F32::from(2));
|
||||
let a=F64_32::ONE*4;
|
||||
assert_eq!(a.sqrt(),F64_32::from(2));
|
||||
}
|
||||
#[test]
|
||||
fn test_sqrt_zero(){
|
||||
let a=I32F32::ZERO;
|
||||
assert_eq!(a.sqrt(),I32F32::ZERO);
|
||||
let a=F64_32::ZERO;
|
||||
assert_eq!(a.sqrt(),F64_32::ZERO);
|
||||
}
|
||||
#[test]
|
||||
fn test_sqrt_low(){
|
||||
let a=I32F32::HALF;
|
||||
let a=F64_32::HALF;
|
||||
let b=a.fixed_mul(a);
|
||||
assert_eq!(b.sqrt(),a);
|
||||
}
|
||||
fn find_equiv_sqrt_via_f64(n:I32F32)->I32F32{
|
||||
fn find_equiv_sqrt_via_f64(n:F64_32)->F64_32{
|
||||
//GIMME THEM BITS BOY
|
||||
let &[bits]=n.to_bits().to_bits().digits();
|
||||
let ibits=bits as i64;
|
||||
let ibits=i64::from_le_bytes(n.to_bits().to_bytes());
|
||||
let f=(ibits as f64)/((1u64<<32) as f64);
|
||||
let f_ans=f.sqrt();
|
||||
let i=(f_ans*((1u64<<32) as f64)) as i64;
|
||||
let r=I32F32::from_bits(bnum::BInt::<1>::from(i));
|
||||
let i=(f_ans*((1u64<<32) as f64)) as u64;
|
||||
let r=F64_32::from_u64(i);
|
||||
//mimic the behaviour of the algorithm,
|
||||
//return the result if it truncates to the exact answer
|
||||
if (r+I32F32::EPSILON).wide_mul_1_1(r+I32F32::EPSILON)==n.wide_mul_1_1(I32F32::ONE){
|
||||
return r+I32F32::EPSILON;
|
||||
if (r+F64_32::EPSILON).wide_mul_64_64(r+F64_32::EPSILON)==n.wide_mul_64_64(F64_32::ONE){
|
||||
return r+F64_32::EPSILON;
|
||||
}
|
||||
if (r-I32F32::EPSILON).wide_mul_1_1(r-I32F32::EPSILON)==n.wide_mul_1_1(I32F32::ONE){
|
||||
return r-I32F32::EPSILON;
|
||||
if (r-F64_32::EPSILON).wide_mul_64_64(r-F64_32::EPSILON)==n.wide_mul_64_64(F64_32::ONE){
|
||||
return r-F64_32::EPSILON;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
fn test_exact(n:I32F32){
|
||||
fn test_exact(n:F64_32){
|
||||
assert_eq!(n.sqrt(),find_equiv_sqrt_via_f64(n));
|
||||
}
|
||||
#[test]
|
||||
fn test_sqrt_exact(){
|
||||
//43
|
||||
for i in 0..((i64::MAX as f32).ln() as u32){
|
||||
let n=I32F32::from_bits(bnum::BInt::<1>::from((i as f32).exp() as i64));
|
||||
let n=F64_32::from_u64((i as f32).exp() as u64);
|
||||
test_exact(n);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_sqrt_max(){
|
||||
let a=I32F32::MAX;
|
||||
let a=F64_32::MAX;
|
||||
test_exact(a);
|
||||
}
|
||||
#[test]
|
||||
@@ -210,9 +275,9 @@ fn test_sqrt_max(){
|
||||
fn test_zeroes_normal(){
|
||||
// (x-1)*(x+1)
|
||||
// x^2-1
|
||||
let zeroes=I32F32::zeroes2(I32F32::NEG_ONE,I32F32::ZERO,I32F32::ONE);
|
||||
let zeroes=F64_32::zeroes2(F64_32::NEG_ONE,F64_32::ZERO,F64_32::ONE);
|
||||
assert_eq!(zeroes,arrayvec::ArrayVec::from_iter([I32F32::NEG_ONE,I32F32::ONE]));
|
||||
let zeroes=I32F32::zeroes2(I32F32::NEG_ONE*3,I32F32::ONE*2,I32F32::ONE);
|
||||
let zeroes=F64_32::zeroes2(F64_32::NEG_ONE*3,F64_32::ONE*2,F64_32::ONE);
|
||||
assert_eq!(zeroes,arrayvec::ArrayVec::from_iter([I32F32::NEG_ONE*3,I32F32::ONE]));
|
||||
}
|
||||
#[test]
|
||||
@@ -220,12 +285,25 @@ fn test_zeroes_normal(){
|
||||
fn test_zeroes_deferred_division(){
|
||||
// (x-1)*(x+1)
|
||||
// x^2-1
|
||||
let zeroes=I32F32::zeroes2(I32F32::NEG_ONE,I32F32::ZERO,I32F32::ONE);
|
||||
let zeroes=F64_32::zeroes2(F64_32::NEG_ONE,F64_32::ZERO,F64_32::ONE);
|
||||
assert_eq!(
|
||||
zeroes,
|
||||
arrayvec::ArrayVec::from_iter([
|
||||
ratio_ops::ratio::Ratio::new(I32F32::ONE*2,I32F32::NEG_ONE*2),
|
||||
ratio_ops::ratio::Ratio::new(I32F32::ONE*2,I32F32::ONE*2),
|
||||
ratio_ops::ratio::Ratio::new(F64_32::ONE*2,F64_32::NEG_ONE*2),
|
||||
ratio_ops::ratio::Ratio::new(F64_32::ONE*2,F64_32::ONE*2),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_debug(){
|
||||
assert_eq!(format!("{:?}",F64_32::EPSILON),"0.00000001");
|
||||
assert_eq!(format!("{:?}",F64_32::ONE),"1.00000000");
|
||||
assert_eq!(format!("{:?}",F64_32::TWO),"2.00000000");
|
||||
assert_eq!(format!("{:?}",F64_32::MAX),"7fffffff.ffffffff");
|
||||
assert_eq!(format!("{:?}",F64_32::try_from(core::f64::consts::PI).unwrap()),"3.243f6a88");
|
||||
assert_eq!(format!("{:?}",F64_32::NEG_EPSILON),"-0.00000001");
|
||||
assert_eq!(format!("{:?}",F64_32::NEG_ONE),"-1.00000000");
|
||||
assert_eq!(format!("{:?}",F64_32::NEG_TWO),"-2.00000000");
|
||||
assert_eq!(format!("{:?}",F64_32::MIN),"-80000000.00000000");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
pub type I32F32=crate::fixed::Fixed<1,32>;
|
||||
pub type I64F64=crate::fixed::Fixed<2,64>;
|
||||
pub type I128F128=crate::fixed::Fixed<4,128>;
|
||||
pub type I256F256=crate::fixed::Fixed<8,256>;
|
||||
use crate::fixed::BNUM_DIGIT_WIDTH;
|
||||
pub type F64_32=crate::fixed::Fixed<{64/BNUM_DIGIT_WIDTH},32>;
|
||||
pub type F128_64=crate::fixed::Fixed<{128/BNUM_DIGIT_WIDTH},64>;
|
||||
pub type F192_96=crate::fixed::Fixed<{192/BNUM_DIGIT_WIDTH},96>;
|
||||
pub type F256_128=crate::fixed::Fixed<{256/BNUM_DIGIT_WIDTH},128>;
|
||||
pub type F320_160=crate::fixed::Fixed<{320/BNUM_DIGIT_WIDTH},160>;
|
||||
pub type F512_256=crate::fixed::Fixed<{512/BNUM_DIGIT_WIDTH},256>;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use crate::fixed::Fixed;
|
||||
use crate::fixed::BNUM_DIGIT_WIDTH;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use std::cmp::Ordering;
|
||||
macro_rules! impl_zeroes{
|
||||
($n:expr)=>{
|
||||
impl Fixed<$n,{$n*32}>{
|
||||
impl Fixed<{$n/BNUM_DIGIT_WIDTH},{$n>>1}>{
|
||||
#[inline]
|
||||
pub fn zeroes2(a0:Self,a1:Self,a2:Self)->ArrayVec<<Self as core::ops::Div>::Output,2>{
|
||||
let a2pos=match a2.cmp(&Self::ZERO){
|
||||
Ordering::Greater=>true,
|
||||
Ordering::Equal=>return ArrayVec::from_iter(Self::zeroes1(a0,a1).into_iter()),
|
||||
Ordering::Equal=>return ArrayVec::from_iter(Self::zeroes1(a0,a1)),
|
||||
Ordering::Less=>false,
|
||||
};
|
||||
let radicand=a1*a1-a2*a0*4;
|
||||
let radicand=a1*a1-((a2*a0)<<2);
|
||||
match radicand.cmp(&<Self as core::ops::Mul>::Output::ZERO){
|
||||
Ordering::Greater=>{
|
||||
// using wrap because sqrt always halves the number of leading digits.
|
||||
@@ -21,21 +22,21 @@ macro_rules! impl_zeroes{
|
||||
let planar_radicand=radicand.sqrt().[<wrap_ $n>]();
|
||||
}
|
||||
//sort roots ascending and avoid taking the difference of large numbers
|
||||
let zeroes=match (a2pos,Self::ZERO<a1){
|
||||
(true, true )=>[(-a1-planar_radicand)/(a2*2),(a0*2)/(-a1-planar_radicand)],
|
||||
(true, false)=>[(a0*2)/(-a1+planar_radicand),(-a1+planar_radicand)/(a2*2)],
|
||||
(false,true )=>[(a0*2)/(-a1-planar_radicand),(-a1-planar_radicand)/(a2*2)],
|
||||
(false,false)=>[(-a1+planar_radicand)/(a2*2),(a0*2)/(-a1+planar_radicand)],
|
||||
let zeroes=match (a2pos,a1.is_positive()){
|
||||
(true, true )=>[(-a1-planar_radicand)/(a2<<1),(a0<<1)/(-a1-planar_radicand)],
|
||||
(true, false)=>[(a0<<1)/(-a1+planar_radicand),(-a1+planar_radicand)/(a2<<1)],
|
||||
(false,true )=>[(a0<<1)/(-a1-planar_radicand),(-a1-planar_radicand)/(a2<<1)],
|
||||
(false,false)=>[(-a1+planar_radicand)/(a2<<1),(a0<<1)/(-a1+planar_radicand)],
|
||||
};
|
||||
ArrayVec::from_iter(zeroes)
|
||||
},
|
||||
Ordering::Equal=>ArrayVec::from_iter([(a1)/(a2*-2)]),
|
||||
Ordering::Equal=>ArrayVec::from_iter([(a1)/(-a2<<1)]),
|
||||
Ordering::Less=>ArrayVec::new_const(),
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn zeroes1(a0:Self,a1:Self)->ArrayVec<<Self as core::ops::Div>::Output,1>{
|
||||
if a1==Self::ZERO{
|
||||
if a1.is_zero(){
|
||||
ArrayVec::new_const()
|
||||
}else{
|
||||
ArrayVec::from_iter([(-a0)/(a1)])
|
||||
@@ -44,10 +45,10 @@ macro_rules! impl_zeroes{
|
||||
}
|
||||
};
|
||||
}
|
||||
impl_zeroes!(1);
|
||||
impl_zeroes!(2);
|
||||
impl_zeroes!(3);
|
||||
impl_zeroes!(4);
|
||||
impl_zeroes!(64);
|
||||
impl_zeroes!(128);
|
||||
impl_zeroes!(192);
|
||||
impl_zeroes!(256);
|
||||
//sqrt doubles twice!
|
||||
//impl_zeroes!(5);
|
||||
//impl_zeroes!(6);
|
||||
|
||||
@@ -14,9 +14,12 @@ fixed-wide=["dep:fixed_wide","dep:paste"]
|
||||
deferred-division=["dep:ratio_ops"]
|
||||
|
||||
[dependencies]
|
||||
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true }
|
||||
fixed_wide = { version = "0.2.0", path = "../fixed_wide", registry = "strafesnet", optional = true }
|
||||
ratio_ops = { workspace = true, optional = true }
|
||||
fixed_wide = { workspace = true, optional = true }
|
||||
paste = { version = "1.0.15", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
fixed_wide = { path = "../fixed_wide", registry = "strafesnet", features = ["wide-mul"] }
|
||||
fixed_wide = { workspace = true, features = ["wide-mul"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -5,17 +5,17 @@ macro_rules! impl_fixed_wide_vector_not_const_generic {
|
||||
(),
|
||||
$n:expr
|
||||
) => {
|
||||
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$n,{$n*32}>>{
|
||||
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<{$n>>3},{$n>>1}>>{
|
||||
#[inline]
|
||||
pub fn length(self)-><fixed_wide::fixed::Fixed::<$n,{$n*32}> as core::ops::Mul>::Output{
|
||||
pub fn length(self)-><fixed_wide::fixed::Fixed::<{$n>>3},{$n>>1}> as core::ops::Mul>::Output{
|
||||
self.length_squared().sqrt_unchecked()
|
||||
}
|
||||
#[inline]
|
||||
pub fn with_length<U,V>(self,length:U)-><Vector<N,V> as core::ops::Div<<fixed_wide::fixed::Fixed::<$n,{$n*32}> as core::ops::Mul>::Output>>::Output
|
||||
pub fn with_length<U,V>(self,length:U)-><Vector<N,V> as core::ops::Div<<fixed_wide::fixed::Fixed::<{$n>>3},{$n>>1}> as core::ops::Mul>::Output>>::Output
|
||||
where
|
||||
fixed_wide::fixed::Fixed<$n,{$n*32}>:core::ops::Mul<U,Output=V>,
|
||||
fixed_wide::fixed::Fixed<{$n>>3},{$n>>1}>:core::ops::Mul<U,Output=V>,
|
||||
U:Copy,
|
||||
V:core::ops::Div<<fixed_wide::fixed::Fixed::<$n,{$n*32}> as core::ops::Mul>::Output>,
|
||||
V:core::ops::Div<<fixed_wide::fixed::Fixed::<{$n>>3},{$n>>1}> as core::ops::Mul>::Output>,
|
||||
{
|
||||
self*length/self.length()
|
||||
}
|
||||
@@ -27,7 +27,7 @@ macro_rules! impl_fixed_wide_vector_not_const_generic {
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! macro_4 {
|
||||
( $macro: ident, $any:tt ) => {
|
||||
$crate::macro_repeated!($macro,$any,1,2,3,4);
|
||||
$crate::macro_repeated!($macro,$any,64,128,192,256);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,40 +39,40 @@ macro_rules! impl_fixed_wide_vector {
|
||||
// I LOVE NOT BEING ABLE TO USE CONST GENERICS
|
||||
$crate::macro_repeated!(
|
||||
impl_narrow_not_const_generic,(),
|
||||
(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1),(11,1),(12,1),(13,1),(14,1),(15,1),(16,1),(17,1),
|
||||
(3,2),(4,2),(5,2),(6,2),(7,2),(8,2),(9,2),(10,2),(11,2),(12,2),(13,2),(14,2),(15,2),(16,2),
|
||||
(4,3),(5,3),(6,3),(7,3),(8,3),(9,3),(10,3),(11,3),(12,3),(13,3),(14,3),(15,3),(16,3),
|
||||
(5,4),(6,4),(7,4),(8,4),(9,4),(10,4),(11,4),(12,4),(13,4),(14,4),(15,4),(16,4),
|
||||
(6,5),(7,5),(8,5),(9,5),(10,5),(11,5),(12,5),(13,5),(14,5),(15,5),(16,5),
|
||||
(7,6),(8,6),(9,6),(10,6),(11,6),(12,6),(13,6),(14,6),(15,6),(16,6),
|
||||
(8,7),(9,7),(10,7),(11,7),(12,7),(13,7),(14,7),(15,7),(16,7),
|
||||
(9,8),(10,8),(11,8),(12,8),(13,8),(14,8),(15,8),(16,8),
|
||||
(10,9),(11,9),(12,9),(13,9),(14,9),(15,9),(16,9),
|
||||
(11,10),(12,10),(13,10),(14,10),(15,10),(16,10),
|
||||
(12,11),(13,11),(14,11),(15,11),(16,11),
|
||||
(13,12),(14,12),(15,12),(16,12),
|
||||
(14,13),(15,13),(16,13),
|
||||
(15,14),(16,14),
|
||||
(16,15)
|
||||
(128,64),(192,64),(256,64),(320,64),(384,64),(448,64),(512,64),(576,64),(640,64),(704,64),(768,64),(832,64),(896,64),(960,64),(1024,64),(1088,64),
|
||||
(192,128),(256,128),(320,128),(384,128),(448,128),(512,128),(576,128),(640,128),(704,128),(768,128),(832,128),(896,128),(960,128),(1024,128),
|
||||
(256,192),(320,192),(384,192),(448,192),(512,192),(576,192),(640,192),(704,192),(768,192),(832,192),(896,192),(960,192),(1024,192),
|
||||
(320,256),(384,256),(448,256),(512,256),(576,256),(640,256),(704,256),(768,256),(832,256),(896,256),(960,256),(1024,256),
|
||||
(384,320),(448,320),(512,320),(576,320),(640,320),(704,320),(768,320),(832,320),(896,320),(960,320),(1024,320),
|
||||
(448,384),(512,384),(576,384),(640,384),(704,384),(768,384),(832,384),(896,384),(960,384),(1024,384),
|
||||
(512,448),(576,448),(640,448),(704,448),(768,448),(832,448),(896,448),(960,448),(1024,448),
|
||||
(576,512),(640,512),(704,512),(768,512),(832,512),(896,512),(960,512),(1024,512),
|
||||
(640,576),(704,576),(768,576),(832,576),(896,576),(960,576),(1024,576),
|
||||
(704,640),(768,640),(832,640),(896,640),(960,640),(1024,640),
|
||||
(768,704),(832,704),(896,704),(960,704),(1024,704),
|
||||
(832,768),(896,768),(960,768),(1024,768),
|
||||
(896,832),(960,832),(1024,832),
|
||||
(960,896),(1024,896),
|
||||
(1024,960)
|
||||
);
|
||||
$crate::macro_repeated!(
|
||||
impl_widen_not_const_generic,(),
|
||||
(1,2),
|
||||
(1,3),(2,3),
|
||||
(1,4),(2,4),(3,4),
|
||||
(1,5),(2,5),(3,5),(4,5),
|
||||
(1,6),(2,6),(3,6),(4,6),(5,6),
|
||||
(1,7),(2,7),(3,7),(4,7),(5,7),(6,7),
|
||||
(1,8),(2,8),(3,8),(4,8),(5,8),(6,8),(7,8),
|
||||
(1,9),(2,9),(3,9),(4,9),(5,9),(6,9),(7,9),(8,9),
|
||||
(1,10),(2,10),(3,10),(4,10),(5,10),(6,10),(7,10),(8,10),(9,10),
|
||||
(1,11),(2,11),(3,11),(4,11),(5,11),(6,11),(7,11),(8,11),(9,11),(10,11),
|
||||
(1,12),(2,12),(3,12),(4,12),(5,12),(6,12),(7,12),(8,12),(9,12),(10,12),(11,12),
|
||||
(1,13),(2,13),(3,13),(4,13),(5,13),(6,13),(7,13),(8,13),(9,13),(10,13),(11,13),(12,13),
|
||||
(1,14),(2,14),(3,14),(4,14),(5,14),(6,14),(7,14),(8,14),(9,14),(10,14),(11,14),(12,14),(13,14),
|
||||
(1,15),(2,15),(3,15),(4,15),(5,15),(6,15),(7,15),(8,15),(9,15),(10,15),(11,15),(12,15),(13,15),(14,15),
|
||||
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16),
|
||||
(1,17)
|
||||
(64,128),
|
||||
(64,192),(128,192),
|
||||
(64,256),(128,256),(192,256),
|
||||
(64,320),(128,320),(192,320),(256,320),
|
||||
(64,384),(128,384),(192,384),(256,384),(320,384),
|
||||
(64,448),(128,448),(192,448),(256,448),(320,448),(384,448),
|
||||
(64,512),(128,512),(192,512),(256,512),(320,512),(384,512),(448,512),
|
||||
(64,576),(128,576),(192,576),(256,576),(320,576),(384,576),(448,576),(512,576),
|
||||
(64,640),(128,640),(192,640),(256,640),(320,640),(384,640),(448,640),(512,640),(576,640),
|
||||
(64,704),(128,704),(192,704),(256,704),(320,704),(384,704),(448,704),(512,704),(576,704),(640,704),
|
||||
(64,768),(128,768),(192,768),(256,768),(320,768),(384,768),(448,768),(512,768),(576,768),(640,768),(704,768),
|
||||
(64,832),(128,832),(192,832),(256,832),(320,832),(384,832),(448,832),(512,832),(576,832),(640,832),(704,832),(768,832),
|
||||
(64,896),(128,896),(192,896),(256,896),(320,896),(384,896),(448,896),(512,896),(576,896),(640,896),(704,896),(768,896),(832,896),
|
||||
(64,960),(128,960),(192,960),(256,960),(320,960),(384,960),(448,960),(512,960),(576,960),(640,960),(704,960),(768,960),(832,960),(896,960),
|
||||
(64,1024),(128,1024),(192,1024),(256,1024),(320,1024),(384,1024),(448,1024),(512,1024),(576,1024),(640,1024),(704,1024),(768,1024),(832,1024),(896,1024),(960,1024),
|
||||
(64,1088)
|
||||
);
|
||||
impl<const N:usize,T:fixed_wide::fixed::Wrap<U>,U> fixed_wide::fixed::Wrap<Vector<N,U>> for Vector<N,T>
|
||||
{
|
||||
@@ -98,17 +98,17 @@ macro_rules! impl_narrow_not_const_generic{
|
||||
($lhs:expr,$rhs:expr)
|
||||
)=>{
|
||||
paste::item!{
|
||||
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$lhs,{$lhs*32}>>{
|
||||
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<{$lhs>>3},{$lhs>>1}>>{
|
||||
#[inline]
|
||||
pub fn [<wrap_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
|
||||
pub fn [<wrap_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<{$rhs>>3},{$rhs>>1}>>{
|
||||
self.map(|t|t.[<wrap_ $rhs>]())
|
||||
}
|
||||
#[inline]
|
||||
pub fn [<narrow_ $rhs>](self)->Vector<N,Result<fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>,fixed_wide::fixed::NarrowError>>{
|
||||
pub fn [<narrow_ $rhs>](self)->Vector<N,Result<fixed_wide::fixed::Fixed<{$rhs>>3},{$rhs>>1}>,fixed_wide::fixed::NarrowError>>{
|
||||
self.map(|t|t.[<narrow_ $rhs>]())
|
||||
}
|
||||
#[inline]
|
||||
pub fn [<clamp_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
|
||||
pub fn [<clamp_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<{$rhs>>3},{$rhs>>1}>>{
|
||||
self.map(|t|t.[<clamp_ $rhs>]())
|
||||
}
|
||||
}
|
||||
@@ -123,9 +123,9 @@ macro_rules! impl_widen_not_const_generic{
|
||||
($lhs:expr,$rhs:expr)
|
||||
)=>{
|
||||
paste::item!{
|
||||
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$lhs,{$lhs*32}>>{
|
||||
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<{$lhs>>3},{$lhs>>1}>>{
|
||||
#[inline]
|
||||
pub fn [<widen_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
|
||||
pub fn [<widen_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<{$rhs>>3},{$rhs>>1}>>{
|
||||
self.map(|t|t.[<widen_ $rhs>]())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,8 @@ macro_rules! impl_matrix_named_fields_shape {
|
||||
#[inline]
|
||||
fn deref(&self)->&Self::Target{
|
||||
// This cast is valid because Matrix has #[repr(transparent)]
|
||||
let ptr=&self.array as *const [[T;$size_inner];$size_outer] as *const Self::Target;
|
||||
let ptr:*const [[T;$size_inner];$size_outer]=&self.array;
|
||||
let ptr=ptr as *const Self::Target;
|
||||
// SAFETY: this pointer is non-null because it comes from a reference
|
||||
unsafe{&*ptr}
|
||||
}
|
||||
@@ -214,7 +215,8 @@ macro_rules! impl_matrix_named_fields_shape {
|
||||
#[inline]
|
||||
fn deref_mut(&mut self)->&mut Self::Target{
|
||||
// This cast is valid because Matrix has #[repr(transparent)]
|
||||
let ptr=&mut self.array as *mut [[T;$size_inner];$size_outer] as *mut Self::Target;
|
||||
let ptr:*mut [[T;$size_inner];$size_outer]=&mut self.array;
|
||||
let ptr=ptr as *mut Self::Target;
|
||||
// SAFETY: this pointer is non-null because it comes from a reference
|
||||
unsafe{&mut*ptr}
|
||||
}
|
||||
|
||||
@@ -331,7 +331,8 @@ macro_rules! impl_vector_named_fields {
|
||||
#[inline]
|
||||
fn deref(&self)->&Self::Target{
|
||||
// This cast is valid because Vector has #[repr(transparent)]
|
||||
let ptr=&self.array as *const [T;$size] as *const Self::Target;
|
||||
let ptr:*const [T;$size]=&self.array;
|
||||
let ptr=ptr as *const Self::Target;
|
||||
// SAFETY: this pointer is non-null because it comes from a reference
|
||||
unsafe{&*ptr}
|
||||
}
|
||||
@@ -340,7 +341,8 @@ macro_rules! impl_vector_named_fields {
|
||||
#[inline]
|
||||
fn deref_mut(&mut self)->&mut Self::Target{
|
||||
// This cast is valid because Vector has #[repr(transparent)]
|
||||
let ptr=&mut self.array as *mut [T;$size] as *mut Self::Target;
|
||||
let ptr:*mut [T;$size]=&mut self.array;
|
||||
let ptr=ptr as *mut Self::Target;
|
||||
// SAFETY: this pointer is non-null because it comes from a reference
|
||||
unsafe{&mut*ptr}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::types::{Matrix3,Matrix3x2,Matrix3x4,Matrix4x2,Vector3};
|
||||
|
||||
type Planar64=fixed_wide::types::I32F32;
|
||||
type Planar64Wide1=fixed_wide::types::I64F64;
|
||||
type Planar64=fixed_wide::types::F64_32;
|
||||
type Planar64Wide1=fixed_wide::types::F128_64;
|
||||
//type Planar64Wide2=fixed_wide::types::I128F128;
|
||||
type Planar64Wide3=fixed_wide::types::I256F256;
|
||||
type Planar64Wide3=fixed_wide::types::F512_256;
|
||||
|
||||
#[test]
|
||||
fn wide_vec3(){
|
||||
@@ -72,7 +72,7 @@ fn wide_matrix_det(){
|
||||
]);
|
||||
// In[2]:= Det[{{1, 2, 3}, {4, 5, 7}, {6, 8, 9}}]
|
||||
// Out[2]= 7
|
||||
assert_eq!(m.det(),fixed_wide::fixed::Fixed::<3,96>::from(7));
|
||||
assert_eq!(m.det(),fixed_wide::types::F192_96::from(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,3 +8,6 @@ description = "Ratio operations using trait bounds for avoiding division like th
|
||||
authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strafesnet_rbx_loader"
|
||||
version = "0.7.0"
|
||||
version = "0.10.2"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -11,15 +11,18 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
bytemuck = "1.14.3"
|
||||
glam = "0.30.0"
|
||||
lazy-regex = "3.1.0"
|
||||
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" }
|
||||
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] }
|
||||
rbx_mesh = "0.5.0"
|
||||
rbx_reflection = "5.0.0"
|
||||
rbx_reflection_database = "1.0.0"
|
||||
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" }
|
||||
glam.workspace = true
|
||||
regex = { version = "1.11.3", default-features = false, features = ["unicode-perl"] }
|
||||
rbx_mesh = "0.6.0"
|
||||
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
|
||||
roblox_emulator = { version = "0.5.1", path = "../roblox_emulator", default-features = false, registry = "strafesnet" }
|
||||
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
|
||||
strafesnet_deferred_loader = { version = "0.5.1", path = "../deferred_loader", registry = "strafesnet" }
|
||||
strafesnet_common.workspace = true
|
||||
strafesnet_deferred_loader.workspace = true
|
||||
rbx_binary = "2.0.1"
|
||||
rbx_dom_weak = "4.1.0"
|
||||
rbx_reflection = "6.1.0"
|
||||
rbx_reflection_database = "2.0.2"
|
||||
rbx_xml = "2.0.1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct RecoverableErrors{
|
||||
pub unsupported_class:HashSet<String>,
|
||||
/// A decal has an invalid / missing property.
|
||||
pub decal_property:Vec<InstancePath>,
|
||||
/// A SurfaceAppearance has an invalid / missing property.
|
||||
pub surface_appearance_property:Vec<InstancePath>,
|
||||
/// A decal has an invalid normal_id.
|
||||
pub normal_id:Vec<NormalIdError>,
|
||||
/// A texture has an invalid / missing property.
|
||||
@@ -58,6 +60,7 @@ impl RecoverableErrors{
|
||||
self.meshpart_content.len()+
|
||||
self.unsupported_class.len()+
|
||||
self.decal_property.len()+
|
||||
self.surface_appearance_property.len()+
|
||||
self.normal_id.len()+
|
||||
self.texture_property.len()+
|
||||
self.mode_id_parse_int.len()+
|
||||
@@ -145,6 +148,7 @@ impl core::fmt::Display for RecoverableErrors{
|
||||
}
|
||||
}
|
||||
write_instance_path_error!(f,self,decal_property,"Decal is","Decals are","missing a property");
|
||||
write_instance_path_error!(f,self,surface_appearance_property,"SurfaceAppearance is","SurfaceAppearances are","missing a property");
|
||||
write_bespoke_error!(f,self,normal_id,"Decal","Decals","NormalId is invalid",path,normal_id);
|
||||
write_instance_path_error!(f,self,texture_property,"Texture is","Textures are","missing a property");
|
||||
write_bespoke_error!(f,self,mode_id_parse_int,"ModeId","ModeIds","failed to parse",context,error);
|
||||
|
||||
@@ -1,41 +1,22 @@
|
||||
use std::io::Read;
|
||||
use rbx_dom_weak::WeakDom;
|
||||
use roblox_emulator::context::Context;
|
||||
use strafesnet_common::map::CompleteMap;
|
||||
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
|
||||
|
||||
pub use error::RecoverableErrors;
|
||||
pub use roblox_emulator::runner::Error as RunnerError;
|
||||
|
||||
mod rbx;
|
||||
mod mesh;
|
||||
pub mod rbx;
|
||||
pub mod mesh;
|
||||
mod error;
|
||||
mod union;
|
||||
pub mod loader;
|
||||
mod primitives;
|
||||
|
||||
pub mod data{
|
||||
pub struct RobloxMeshBytes(Vec<u8>);
|
||||
impl RobloxMeshBytes{
|
||||
pub fn new(bytes:Vec<u8>)->Self{
|
||||
Self(bytes)
|
||||
}
|
||||
pub(crate) fn cursor(self)->std::io::Cursor<Vec<u8>>{
|
||||
std::io::Cursor::new(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub mod union;
|
||||
pub mod primitives;
|
||||
|
||||
pub struct Model{
|
||||
dom:WeakDom,
|
||||
}
|
||||
impl Model{
|
||||
fn new(dom:WeakDom)->Self{
|
||||
pub fn new(dom:WeakDom)->Self{
|
||||
Self{dom}
|
||||
}
|
||||
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
|
||||
to_snf(self,failure_mode)
|
||||
}
|
||||
}
|
||||
impl AsRef<WeakDom> for Model{
|
||||
fn as_ref(&self)->&WeakDom{
|
||||
@@ -66,9 +47,6 @@ impl Place{
|
||||
}
|
||||
Ok(errors)
|
||||
}
|
||||
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
|
||||
to_snf(self,failure_mode)
|
||||
}
|
||||
}
|
||||
impl AsRef<WeakDom> for Place{
|
||||
fn as_ref(&self)->&WeakDom{
|
||||
@@ -83,72 +61,3 @@ impl From<Model> for Place{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ReadError{
|
||||
RbxBinary(rbx_binary::DecodeError),
|
||||
RbxXml(rbx_xml::DecodeError),
|
||||
Io(std::io::Error),
|
||||
UnknownFileFormat,
|
||||
}
|
||||
impl std::fmt::Display for ReadError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ReadError{}
|
||||
|
||||
pub fn read<R:Read>(input:R)->Result<Model,ReadError>{
|
||||
let mut buf=std::io::BufReader::new(input);
|
||||
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?;
|
||||
match peek.get(0..8){
|
||||
Some(b"<roblox!")=>rbx_binary::from_reader(buf).map(Model::new).map_err(ReadError::RbxBinary),
|
||||
Some(b"<roblox ")=>rbx_xml::from_reader_default(buf).map(Model::new).map_err(ReadError::RbxXml),
|
||||
_=>Err(ReadError::UnknownFileFormat),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoadError{
|
||||
Texture(loader::TextureError),
|
||||
Mesh(loader::MeshError),
|
||||
}
|
||||
impl std::fmt::Display for LoadError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for LoadError{}
|
||||
impl From<loader::TextureError> for LoadError{
|
||||
fn from(value:loader::TextureError)->Self{
|
||||
Self::Texture(value)
|
||||
}
|
||||
}
|
||||
impl From<loader::MeshError> for LoadError{
|
||||
fn from(value:loader::MeshError)->Self{
|
||||
Self::Mesh(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
|
||||
let dom=dom.as_ref();
|
||||
|
||||
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
|
||||
let mut mesh_deferred_loader=MeshDeferredLoader::new();
|
||||
|
||||
let map_step1=rbx::convert(
|
||||
dom,
|
||||
&mut texture_deferred_loader,
|
||||
&mut mesh_deferred_loader,
|
||||
);
|
||||
|
||||
let mut mesh_loader=loader::MeshLoader::new();
|
||||
let meshpart_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,failure_mode).map_err(LoadError::Mesh)?;
|
||||
|
||||
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(meshpart_meshes);
|
||||
|
||||
let mut texture_loader=loader::TextureLoader::new();
|
||||
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?;
|
||||
|
||||
Ok(map_step2.add_render_configs_and_textures(render_configs))
|
||||
}
|
||||
|
||||
@@ -5,12 +5,73 @@ use strafesnet_common::aabb::Aabb;
|
||||
use strafesnet_common::integer::vec3;
|
||||
use strafesnet_common::model::{self,ColorId,IndexedVertex,PolygonGroup,PolygonList,RenderConfigId,VertexId};
|
||||
|
||||
use crate::loader::MeshWithSize;
|
||||
use crate::rbx::RobloxPartDescription;
|
||||
|
||||
#[derive(Hash,Eq,PartialEq)]
|
||||
pub enum MeshType<'a>{
|
||||
FileMesh,
|
||||
Union{
|
||||
mesh_data:&'a [u8],
|
||||
physics_data:&'a [u8],
|
||||
size_float_bits:[u32;3],
|
||||
part_texture_description:RobloxPartDescription,
|
||||
},
|
||||
}
|
||||
#[derive(Hash,Eq,PartialEq)]
|
||||
pub struct MeshIndex<'a>{
|
||||
pub mesh_type:MeshType<'a>,
|
||||
pub content:&'a str,
|
||||
}
|
||||
impl MeshIndex<'_>{
|
||||
pub fn file_mesh(content:&str)->MeshIndex<'_>{
|
||||
MeshIndex{
|
||||
mesh_type:MeshType::FileMesh,
|
||||
content,
|
||||
}
|
||||
}
|
||||
pub fn union<'a>(
|
||||
content:&'a str,
|
||||
mesh_data:&'a [u8],
|
||||
physics_data:&'a [u8],
|
||||
size:&rbx_dom_weak::types::Vector3,
|
||||
part_texture_description:RobloxPartDescription,
|
||||
)->MeshIndex<'a>{
|
||||
MeshIndex{
|
||||
mesh_type:MeshType::Union{
|
||||
mesh_data,
|
||||
physics_data,
|
||||
size_float_bits:[size.x.to_bits(),size.y.to_bits(),size.z.to_bits()],
|
||||
part_texture_description,
|
||||
},
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MeshWithSize{
|
||||
mesh:model::Mesh,
|
||||
size:strafesnet_common::integer::Planar64Vec3,
|
||||
}
|
||||
impl MeshWithSize{
|
||||
pub(crate) const fn new(
|
||||
mesh:model::Mesh,
|
||||
size:strafesnet_common::integer::Planar64Vec3,
|
||||
)->Self{
|
||||
Self{mesh,size}
|
||||
}
|
||||
pub const fn mesh(&self)->&model::Mesh{
|
||||
&self.mesh
|
||||
}
|
||||
pub const fn size(&self)->strafesnet_common::integer::Planar64Vec3{
|
||||
self.size
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
|
||||
RbxMesh(rbx_mesh::mesh::Error)
|
||||
NoPolygons,
|
||||
RbxMesh(rbx_mesh::mesh::Error),
|
||||
}
|
||||
impl std::fmt::Display for Error{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
@@ -22,41 +83,47 @@ impl std::error::Error for Error{}
|
||||
fn ingest_vertices2(
|
||||
vertices:Vec<Vertex2>,
|
||||
mb:&mut model::MeshBuilder,
|
||||
)->Result<HashMap<rbx_mesh::mesh::VertexId2,VertexId>,Error>{
|
||||
)->HashMap<rbx_mesh::mesh::VertexId2,VertexId>{
|
||||
//this monster is collecting a map of old_vertices_index -> unique_vertices_index
|
||||
//while also doing the inserting unique entries into lists simultaneously
|
||||
Ok(vertices.into_iter().enumerate().map(|(vertex_id,vertex)|Ok((
|
||||
// vertex positions that fail to convert are DROPPED
|
||||
vertices.into_iter().enumerate().filter_map(|(vertex_id,vertex)|Some((
|
||||
rbx_mesh::mesh::VertexId2(vertex_id as u32),
|
||||
{
|
||||
let vertex=IndexedVertex{
|
||||
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?),
|
||||
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
|
||||
tex:mb.acquire_tex_id(glam::Vec2::from_array(vertex.tex)),
|
||||
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?),
|
||||
color:mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32)))
|
||||
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm).ok()?),
|
||||
color:mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32))),
|
||||
};
|
||||
mb.acquire_vertex_id(vertex)
|
||||
}
|
||||
))).collect::<Result<_,_>>().map_err(Error::Planar64Vec3)?)
|
||||
))).collect()
|
||||
}
|
||||
fn ingest_vertices_truncated2(
|
||||
vertices:Vec<Vertex2Truncated>,
|
||||
mb:&mut model::MeshBuilder,
|
||||
static_color_id:ColorId,//pick one color and fill everything with it
|
||||
)->Result<HashMap<rbx_mesh::mesh::VertexId2,VertexId>,Error>{
|
||||
)->HashMap<rbx_mesh::mesh::VertexId2,VertexId>{
|
||||
//this monster is collecting a map of old_vertices_index -> unique_vertices_index
|
||||
//while also doing the inserting unique entries into lists simultaneously
|
||||
Ok(vertices.into_iter().enumerate().map(|(vertex_id,vertex)|Ok((
|
||||
// vertex positions that fail to convert are DROPPED
|
||||
vertices.into_iter().enumerate().filter_map(|(vertex_id,vertex)|Some((
|
||||
rbx_mesh::mesh::VertexId2(vertex_id as u32),
|
||||
{
|
||||
let vertex=IndexedVertex{
|
||||
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?),
|
||||
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
|
||||
tex:mb.acquire_tex_id(glam::Vec2::from_array(vertex.tex)),
|
||||
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?),
|
||||
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm).ok()?),
|
||||
color:static_color_id,
|
||||
};
|
||||
mb.acquire_vertex_id(vertex)
|
||||
}
|
||||
))).collect::<Result<_,_>>().map_err(Error::Planar64Vec3)?)
|
||||
))).collect()
|
||||
}
|
||||
|
||||
fn new_polygon_list_checked(list:Vec<model::IndexedVertexList>)->Option<PolygonList>{
|
||||
(!list.is_empty()).then_some(PolygonList::new(list))
|
||||
}
|
||||
|
||||
fn ingest_faces2_lods3(
|
||||
@@ -66,32 +133,35 @@ fn ingest_faces2_lods3(
|
||||
lods:&[rbx_mesh::mesh::Lod3],
|
||||
){
|
||||
//faces have to be split into polygon groups based on lod
|
||||
polygon_groups.extend(lods.windows(2).map(|lod_pair|
|
||||
PolygonGroup::PolygonList(PolygonList::new(faces[lod_pair[0].0 as usize..lod_pair[1].0 as usize].iter().map(|rbx_mesh::mesh::Face2(v0,v1,v2)|
|
||||
vec![vertex_id_map[&v0],vertex_id_map[&v1],vertex_id_map[&v2]]
|
||||
).collect()))
|
||||
polygon_groups.extend(lods.windows(2).filter_map(|lod_pair|
|
||||
Some(PolygonGroup::PolygonList(new_polygon_list_checked(faces[lod_pair[0].0 as usize..lod_pair[1].0 as usize].iter().filter_map(|rbx_mesh::mesh::Face2(v0,v1,v2)|
|
||||
Some(vec![*vertex_id_map.get(&v0)?,*vertex_id_map.get(&v1)?,*vertex_id_map.get(&v2)?])
|
||||
).collect())?))
|
||||
))
|
||||
}
|
||||
|
||||
pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithSize,Error>{
|
||||
pub fn convert(roblox_mesh_bytes:&[u8])->Result<MeshWithSize,Error>{
|
||||
//generate that mesh boi
|
||||
let mut polygon_groups=Vec::new();
|
||||
let mut mb=model::MeshBuilder::new();
|
||||
match rbx_mesh::read_versioned(roblox_mesh_bytes.cursor()).map_err(Error::RbxMesh)?{
|
||||
match rbx_mesh::read_versioned(std::io::Cursor::new(roblox_mesh_bytes)).map_err(Error::RbxMesh)?{
|
||||
rbx_mesh::mesh::Mesh::V1(mesh)=>{
|
||||
let color_id=mb.acquire_color_id(glam::Vec4::ONE);
|
||||
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.vertices.chunks_exact(3).map(|trip|{
|
||||
let polygon_list=new_polygon_list_checked(mesh.vertices.chunks_exact(3).filter_map(|trip|{
|
||||
let mut ingest_vertex1=|vertex:&rbx_mesh::mesh::Vertex1|{
|
||||
let vertex=IndexedVertex{
|
||||
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?),
|
||||
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
|
||||
tex:mb.acquire_tex_id(glam::vec2(vertex.tex[0],vertex.tex[1])),
|
||||
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?),
|
||||
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm).ok()?),
|
||||
color:color_id,
|
||||
};
|
||||
Ok(mb.acquire_vertex_id(vertex))
|
||||
Some(mb.acquire_vertex_id(vertex))
|
||||
};
|
||||
Ok(vec![ingest_vertex1(&trip[0])?,ingest_vertex1(&trip[1])?,ingest_vertex1(&trip[2])?])
|
||||
}).collect::<Result<_,_>>().map_err(Error::Planar64Vec3)?)));
|
||||
Some(vec![ingest_vertex1(&trip[0])?,ingest_vertex1(&trip[1])?,ingest_vertex1(&trip[2])?])
|
||||
}).collect());
|
||||
if let Some(polygon_list)=polygon_list{
|
||||
polygon_groups.push(PolygonGroup::PolygonList(polygon_list));
|
||||
}
|
||||
},
|
||||
rbx_mesh::mesh::Mesh::V2(mesh)=>{
|
||||
let vertex_id_map=match mesh.header.sizeof_vertex{
|
||||
@@ -101,11 +171,14 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithS
|
||||
ingest_vertices_truncated2(mesh.vertices_truncated,&mut mb,color_id)
|
||||
},
|
||||
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut mb),
|
||||
}?;
|
||||
};
|
||||
//one big happy group for all the faces
|
||||
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|face|
|
||||
vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]]
|
||||
).collect())));
|
||||
let polygon_list=new_polygon_list_checked(mesh.faces.into_iter().filter_map(|face|
|
||||
Some(vec![*vertex_id_map.get(&face.0)?,*vertex_id_map.get(&face.1)?,*vertex_id_map.get(&face.2)?])
|
||||
).collect());
|
||||
if let Some(polygon_list)=polygon_list{
|
||||
polygon_groups.push(PolygonGroup::PolygonList(polygon_list));
|
||||
}
|
||||
},
|
||||
rbx_mesh::mesh::Mesh::V3(mesh)=>{
|
||||
let vertex_id_map=match mesh.header.sizeof_vertex{
|
||||
@@ -114,18 +187,21 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithS
|
||||
ingest_vertices_truncated2(mesh.vertices_truncated,&mut mb,color_id)
|
||||
},
|
||||
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut mb),
|
||||
}?;
|
||||
};
|
||||
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
|
||||
},
|
||||
rbx_mesh::mesh::Mesh::V4(mesh)=>{
|
||||
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb)?;
|
||||
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb);
|
||||
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
|
||||
},
|
||||
rbx_mesh::mesh::Mesh::V5(mesh)=>{
|
||||
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb)?;
|
||||
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb);
|
||||
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
|
||||
},
|
||||
}
|
||||
if polygon_groups.is_empty(){
|
||||
return Err(Error::NoPolygons);
|
||||
}
|
||||
let mesh=mb.build(
|
||||
polygon_groups,
|
||||
//these should probably be moved to the model...
|
||||
|
||||
@@ -519,7 +519,7 @@ pub fn unit_cylinder(face_descriptions:CubeFaceDescription)->Mesh{
|
||||
(glam::vec2(-x as f32,y as f32).normalize()+1.0)/2.0
|
||||
)
|
||||
);
|
||||
let pos=mb.acquire_pos_id($end+vec3::int(0,-x,y).with_length(Planar64::ONE).divide().wrap_1());
|
||||
let pos=mb.acquire_pos_id($end+vec3::int(0,-x,y).with_length(Planar64::ONE).divide().wrap_64());
|
||||
mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color})
|
||||
}).collect();
|
||||
|
||||
@@ -560,9 +560,9 @@ pub fn unit_cylinder(face_descriptions:CubeFaceDescription)->Mesh{
|
||||
let mut polygon_list=Vec::with_capacity(CubeFaceDescription::FACES);
|
||||
for $loop in -GON..GON{
|
||||
// lo Z
|
||||
let lz_dir=$lo_dir.with_length(Planar64::ONE).divide().wrap_1();
|
||||
let lz_dir=$lo_dir.with_length(Planar64::ONE).divide().wrap_64();
|
||||
// hi Z
|
||||
let hz_dir=$hi_dir.with_length(Planar64::ONE).divide().wrap_1();
|
||||
let hz_dir=$hi_dir.with_length(Planar64::ONE).divide().wrap_64();
|
||||
|
||||
// pos
|
||||
let lx_lz_pos=mb.acquire_pos_id(vec3::NEG_X+lz_dir);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::error::{RecoverableErrors,CFrameError,CFrameErrorType,DuplicateStageError,InstancePath,NormalIdError,Planar64ConvertError,ParseIntContext,ShapeError};
|
||||
use crate::loader::{MeshWithSize,MeshIndex};
|
||||
use crate::mesh::{MeshWithSize,MeshIndex};
|
||||
use crate::primitives::{self,CubeFace,CubeFaceDescription,WedgeFaceDescription,CornerWedgeFaceDescription,FaceDescription,Primitives};
|
||||
use strafesnet_common::map;
|
||||
use strafesnet_common::model;
|
||||
@@ -18,15 +18,24 @@ fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
|
||||
rbx_dom_weak::ustr(s)
|
||||
}
|
||||
|
||||
macro_rules! lazy_regex{
|
||||
($r:literal)=>{{
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
static RE:LazyLock<Regex>=LazyLock::new(||Regex::new($r).unwrap());
|
||||
&RE
|
||||
}};
|
||||
}
|
||||
|
||||
fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Result<Planar64Affine3,Planar64TryFromFloatError>{
|
||||
Ok(Planar64Affine3::new(
|
||||
Planar64Mat3::from_cols([
|
||||
(vec3::try_from_f32_array([cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x])?
|
||||
*integer::try_from_f32(size.x/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
|
||||
*integer::try_from_f32(size.x/2.0)?).narrow_64().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
|
||||
(vec3::try_from_f32_array([cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y])?
|
||||
*integer::try_from_f32(size.y/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
|
||||
*integer::try_from_f32(size.y/2.0)?).narrow_64().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
|
||||
(vec3::try_from_f32_array([cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z])?
|
||||
*integer::try_from_f32(size.z/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
|
||||
*integer::try_from_f32(size.z/2.0)?).narrow_64().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
|
||||
]),
|
||||
vec3::try_from_f32_array([cf.position.x,cf.position.y,cf.position.z])?
|
||||
))
|
||||
@@ -118,7 +127,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
|
||||
);
|
||||
},
|
||||
other=>{
|
||||
let regman=lazy_regex::regex!(r"^(BonusStart|WormholeOut)(\d+)$");
|
||||
let regman=lazy_regex!(r"^(BonusStart|WormholeOut)(\d+)$");
|
||||
if let Some(captures)=regman.captures(other){
|
||||
match &captures[1]{
|
||||
"BonusStart"=>{
|
||||
@@ -144,7 +153,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
|
||||
},
|
||||
_=>(),
|
||||
}
|
||||
}else if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$")
|
||||
}else if let Some(captures)=lazy_regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$")
|
||||
.captures(other){
|
||||
force_intersecting=true;
|
||||
let stage_id=StageId::new(ParseIntContext::parse(&captures[3]).map_err(GetAttributesError::StageIdParseInt)?);
|
||||
@@ -185,7 +194,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
|
||||
stage_element,
|
||||
),
|
||||
);
|
||||
}else if let Some(captures)=lazy_regex::regex!(r"^(Jump|WormholeIn)(\d+)$")
|
||||
}else if let Some(captures)=lazy_regex!(r"^(Jump|WormholeIn)(\d+)$")
|
||||
.captures(other){
|
||||
match &captures[1]{
|
||||
"Jump"=>modes_builder.push_mode_update(
|
||||
@@ -210,7 +219,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
|
||||
},
|
||||
_=>unreachable!("regex2[1] messed up bad"),
|
||||
}
|
||||
}else if let Some(captures)=lazy_regex::regex!(r"^Bonus(Finish|Anticheat)(\d+)$")
|
||||
}else if let Some(captures)=lazy_regex!(r"^Bonus(Finish|Anticheat)(\d+)$")
|
||||
.captures(other){
|
||||
force_can_collide=false;
|
||||
force_intersecting=true;
|
||||
@@ -242,7 +251,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
|
||||
}
|
||||
}
|
||||
//need some way to skip this
|
||||
if allow_booster&&velocity!=vec3::ZERO{
|
||||
if allow_booster&&velocity!=vec3::zero(){
|
||||
general.booster=Some(attr::Booster::Velocity(velocity));
|
||||
}
|
||||
Ok(match force_can_collide{
|
||||
@@ -335,12 +344,12 @@ pub struct RobloxFaceTextureDescription{
|
||||
pub color:glam::Vec4,
|
||||
pub transform:RobloxTextureTransform,
|
||||
}
|
||||
impl core::cmp::PartialEq for RobloxFaceTextureDescription{
|
||||
impl PartialEq for RobloxFaceTextureDescription{
|
||||
fn eq(&self,other:&Self)->bool{
|
||||
self.to_bits().eq(&other.to_bits())
|
||||
}
|
||||
}
|
||||
impl core::cmp::Eq for RobloxFaceTextureDescription{}
|
||||
impl Eq for RobloxFaceTextureDescription{}
|
||||
impl core::hash::Hash for RobloxFaceTextureDescription{
|
||||
fn hash<H:core::hash::Hasher>(&self,state:&mut H){
|
||||
self.to_bits().hash(state);
|
||||
@@ -499,6 +508,43 @@ fn get_texture_description<'a>(
|
||||
}
|
||||
part_texture_description
|
||||
}
|
||||
fn get_surface_appearance<'a>(
|
||||
recoverable_errors:&mut RecoverableErrors,
|
||||
db:&rbx_reflection::ReflectionDatabase,
|
||||
dom:&'a rbx_dom_weak::WeakDom,
|
||||
object:&rbx_dom_weak::Instance,
|
||||
)->Option<&'a str>{
|
||||
//use the biggest one and cut it down later...
|
||||
let decal=&db.classes["SurfaceAppearance"];
|
||||
let surface_appearances=object.children().iter().filter_map(|&referent|{
|
||||
let instance=dom.get_by_ref(referent)?;
|
||||
db.classes.get(instance.class.as_str()).is_some_and(|class|
|
||||
db.has_superclass(class,decal)
|
||||
).then_some(instance)
|
||||
});
|
||||
for surface_appearance in surface_appearances{
|
||||
// SurfaceAppearance should always have these properties,
|
||||
// but it is not guaranteed by the rbx_dom_weak data structure.
|
||||
let (
|
||||
Some(rbx_dom_weak::types::Variant::Content(color_map)),
|
||||
// Some(rbx_dom_weak::types::Variant::Content(_metalness_map)),
|
||||
// Some(rbx_dom_weak::types::Variant::Content(_normal_map)),
|
||||
// Some(rbx_dom_weak::types::Variant::Content(_roughness_map)),
|
||||
)=(
|
||||
surface_appearance.properties.get(&static_ustr("ColorMapContent")),
|
||||
// surface_appearance.properties.get(&static_ustr("MetalnessMapContent")),
|
||||
// surface_appearance.properties.get(&static_ustr("NormalMapContent")),
|
||||
// surface_appearance.properties.get(&static_ustr("RoughnessMapContent")),
|
||||
)else{
|
||||
recoverable_errors.surface_appearance_property.push(InstancePath::new(dom,surface_appearance));
|
||||
continue;
|
||||
};
|
||||
if let Some(texture_id)=get_content_url(color_map){
|
||||
return Some(texture_id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
enum Shape{
|
||||
Primitive(Primitives),
|
||||
MeshPart,
|
||||
@@ -550,7 +596,7 @@ pub fn convert<'a>(
|
||||
//just going to leave it like this for now instead of reworking the data structures for this whole thing
|
||||
let textureless_render_group=render_config_deferred_loader.acquire_render_config_id(None);
|
||||
|
||||
let db=rbx_reflection_database::get();
|
||||
let db=rbx_reflection_database::get().unwrap();
|
||||
let basepart=&db.classes["BasePart"];
|
||||
let baseparts=dom.descendants().filter(|&instance|
|
||||
db.classes.get(instance.class.as_str()).is_some_and(|class|
|
||||
@@ -697,7 +743,8 @@ pub fn convert<'a>(
|
||||
""
|
||||
}
|
||||
};
|
||||
let texture_asset_id=get_content_url(texture_content);
|
||||
// load SurfaceAppearance and then fall back to mesh texture
|
||||
let texture_asset_id=get_surface_appearance(&mut recoverable_errors,db,dom,object).or_else(||get_content_url(texture_content));
|
||||
(
|
||||
MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(texture_asset_id)),
|
||||
mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id)),
|
||||
@@ -766,19 +813,19 @@ struct MeshIdWithSize{
|
||||
mesh:model::MeshId,
|
||||
size:Planar64Vec3,
|
||||
}
|
||||
fn acquire_mesh_id_from_render_config_id<'a>(
|
||||
fn acquire_mesh_id_from_render_config_id(
|
||||
primitive_meshes:&mut Vec<model::Mesh>,
|
||||
mesh_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RenderConfigId,model::MeshId>>,
|
||||
loaded_meshes:&'a HashMap<model::MeshId,MeshWithSize>,
|
||||
loaded_meshes:&HashMap<model::MeshId,MeshWithSize>,
|
||||
old_mesh_id:model::MeshId,
|
||||
render:RenderConfigId,
|
||||
)->Option<MeshIdWithSize>{
|
||||
//ignore meshes that fail to load completely for now
|
||||
loaded_meshes.get(&old_mesh_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
|
||||
loaded_meshes.get(&old_mesh_id).map(|mesh_with_size|MeshIdWithSize{
|
||||
mesh:*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
|
||||
.entry(render).or_insert_with(||{
|
||||
let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
|
||||
let mut mesh_clone=mesh.clone();
|
||||
let mut mesh_clone=mesh_with_size.mesh().clone();
|
||||
//set the render group lool
|
||||
if let Some(graphics_group)=mesh_clone.graphics_groups.first_mut(){
|
||||
graphics_group.render=render;
|
||||
@@ -786,22 +833,22 @@ fn acquire_mesh_id_from_render_config_id<'a>(
|
||||
primitive_meshes.push(mesh_clone);
|
||||
mesh_id
|
||||
}),
|
||||
size,
|
||||
size:mesh_with_size.size(),
|
||||
})
|
||||
}
|
||||
fn acquire_union_id_from_render_config_id<'a>(
|
||||
fn acquire_union_id_from_render_config_id(
|
||||
primitive_meshes:&mut Vec<model::Mesh>,
|
||||
union_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RobloxPartDescription,model::MeshId>>,
|
||||
loaded_meshes:&'a HashMap<model::MeshId,MeshWithSize>,
|
||||
loaded_meshes:&HashMap<model::MeshId,MeshWithSize>,
|
||||
old_union_id:model::MeshId,
|
||||
part_texture_description:RobloxPartDescription,
|
||||
)->Option<MeshIdWithSize>{
|
||||
//ignore uniones that fail to load completely for now
|
||||
loaded_meshes.get(&old_union_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
|
||||
loaded_meshes.get(&old_union_id).map(|mesh_with_size|MeshIdWithSize{
|
||||
mesh:*union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new())
|
||||
.entry(part_texture_description.clone()).or_insert_with(||{
|
||||
let union_id=model::MeshId::new(primitive_meshes.len() as u32);
|
||||
let mut union_clone=mesh.clone();
|
||||
let mut union_clone=mesh_with_size.mesh().clone();
|
||||
//set the render groups
|
||||
for (graphics_group,maybe_face_texture_description) in union_clone.graphics_groups.iter_mut().zip(part_texture_description.0){
|
||||
if let Some(face_texture_description)=maybe_face_texture_description{
|
||||
@@ -811,7 +858,7 @@ fn acquire_union_id_from_render_config_id<'a>(
|
||||
primitive_meshes.push(union_clone);
|
||||
union_id
|
||||
}),
|
||||
size,
|
||||
size:mesh_with_size.size(),
|
||||
})
|
||||
}
|
||||
pub struct PartialMap1<'a>{
|
||||
@@ -858,19 +905,19 @@ impl PartialMap1<'_>{
|
||||
deferred_model_deferred_attributes.model.mesh,
|
||||
deferred_model_deferred_attributes.render
|
||||
)?;
|
||||
Some(ModelDeferredAttributes{
|
||||
mesh,
|
||||
deferred_attributes:deferred_model_deferred_attributes.model.deferred_attributes,
|
||||
color:deferred_model_deferred_attributes.model.color,
|
||||
transform:Planar64Affine3::new(
|
||||
Planar64Mat3::from_cols([
|
||||
(deferred_model_deferred_attributes.model.transform.matrix3.x_axis*2/mesh_size.x).divide().narrow_1().unwrap(),
|
||||
(deferred_model_deferred_attributes.model.transform.matrix3.y_axis*2/mesh_size.y).divide().narrow_1().unwrap(),
|
||||
(deferred_model_deferred_attributes.model.transform.matrix3.z_axis*2/mesh_size.z).divide().narrow_1().unwrap(),
|
||||
]),
|
||||
deferred_model_deferred_attributes.model.transform.translation
|
||||
),
|
||||
})
|
||||
let mut model=deferred_model_deferred_attributes.model;
|
||||
model.mesh=mesh;
|
||||
// avoid devide by zero but introduce more edge cases. not sure what the correct thing to do here is.
|
||||
if mesh_size.x!=integer::Fixed::ZERO{
|
||||
model.transform.matrix3.x_axis=(model.transform.matrix3.x_axis*2/mesh_size.x).divide().narrow_64().unwrap();
|
||||
}
|
||||
if mesh_size.y!=integer::Fixed::ZERO{
|
||||
model.transform.matrix3.y_axis=(model.transform.matrix3.y_axis*2/mesh_size.y).divide().narrow_64().unwrap();
|
||||
}
|
||||
if mesh_size.z!=integer::Fixed::ZERO{
|
||||
model.transform.matrix3.z_axis=(model.transform.matrix3.z_axis*2/mesh_size.z).divide().narrow_64().unwrap();
|
||||
}
|
||||
Some(model)
|
||||
}).chain(self.deferred_unions_deferred_attributes.into_iter().flat_map(|deferred_union_deferred_attributes|{
|
||||
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
|
||||
//insert into primitive_meshes
|
||||
@@ -881,19 +928,19 @@ impl PartialMap1<'_>{
|
||||
deferred_union_deferred_attributes.model.mesh,
|
||||
deferred_union_deferred_attributes.render
|
||||
)?;
|
||||
Some(ModelDeferredAttributes{
|
||||
mesh,
|
||||
deferred_attributes:deferred_union_deferred_attributes.model.deferred_attributes,
|
||||
color:deferred_union_deferred_attributes.model.color,
|
||||
transform:Planar64Affine3::new(
|
||||
Planar64Mat3::from_cols([
|
||||
(deferred_union_deferred_attributes.model.transform.matrix3.x_axis*2/size.x).divide().narrow_1().unwrap(),
|
||||
(deferred_union_deferred_attributes.model.transform.matrix3.y_axis*2/size.y).divide().narrow_1().unwrap(),
|
||||
(deferred_union_deferred_attributes.model.transform.matrix3.z_axis*2/size.z).divide().narrow_1().unwrap(),
|
||||
]),
|
||||
deferred_union_deferred_attributes.model.transform.translation
|
||||
),
|
||||
})
|
||||
let mut model=deferred_union_deferred_attributes.model;
|
||||
model.mesh=mesh;
|
||||
// avoid devide by zero but introduce more edge cases. not sure what the correct thing to do here is.
|
||||
if size.x!=integer::Fixed::ZERO{
|
||||
model.transform.matrix3.x_axis=(model.transform.matrix3.x_axis*2/size.x).divide().narrow_64().unwrap();
|
||||
}
|
||||
if size.y!=integer::Fixed::ZERO{
|
||||
model.transform.matrix3.y_axis=(model.transform.matrix3.y_axis*2/size.y).divide().narrow_64().unwrap();
|
||||
}
|
||||
if size.z!=integer::Fixed::ZERO{
|
||||
model.transform.matrix3.z_axis=(model.transform.matrix3.z_axis*2/size.z).divide().narrow_64().unwrap();
|
||||
}
|
||||
Some(model)
|
||||
}))
|
||||
.chain(self.primitive_models_deferred_attributes.into_iter())
|
||||
.filter_map(|model_deferred_attributes|{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::loader::MeshWithSize;
|
||||
use crate::mesh::MeshWithSize;
|
||||
use crate::rbx::RobloxPartDescription;
|
||||
use crate::primitives::{CUBE_DEFAULT_VERTICES,CUBE_DEFAULT_POLYS,FaceDescription};
|
||||
|
||||
@@ -104,7 +104,7 @@ fn build_mesh2(
|
||||
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
|
||||
polygon_groups_normal_id[normal_id as usize-1].push(face);
|
||||
}else{
|
||||
panic!("Empty face!");
|
||||
print!("[union] Empty face!");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -158,12 +158,16 @@ fn build_mesh5(
|
||||
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
|
||||
polygon_groups_normal_id[normal_id as usize-1].push(face);
|
||||
}else{
|
||||
panic!("Empty face!");
|
||||
print!("[union] Empty face!");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_polygon_list_checked(list:Vec<model::IndexedVertexList>)->Option<PolygonList>{
|
||||
(!list.is_empty()).then_some(PolygonList::new(list))
|
||||
}
|
||||
|
||||
const NORMAL_FACES:usize=6;
|
||||
impl std::error::Error for Error{}
|
||||
pub fn convert(
|
||||
@@ -172,12 +176,12 @@ pub fn convert(
|
||||
size:glam::Vec3,
|
||||
RobloxPartDescription(part_texture_description):RobloxPartDescription,
|
||||
)->Result<MeshWithSize,Error>{
|
||||
let mut polygon_groups_normal_id:[_;NORMAL_FACES]=[vec![],vec![],vec![],vec![],vec![],vec![]];
|
||||
|
||||
// build graphics and physics meshes
|
||||
let mut mb=MeshBuilder::new();
|
||||
// graphics
|
||||
let graphics_groups=if !roblox_mesh_data.is_empty(){
|
||||
let (polygon_groups_normal_id,graphics_groups)=if !roblox_mesh_data.is_empty(){
|
||||
let mut polygon_groups_normal_id:[_;NORMAL_FACES]=[vec![],vec![],vec![],vec![],vec![],vec![]];
|
||||
// create per-face texture coordinate affine transforms
|
||||
let cube_face_description=part_texture_description.map(|opt|opt.map(|mut t|{
|
||||
t.transform.set_size(1.0,1.0);
|
||||
@@ -193,20 +197,26 @@ pub fn convert(
|
||||
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V4(mesh_data4))=>build_mesh2(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data4.mesh)?,
|
||||
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V5(mesh_data4))=>build_mesh5(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data4)?,
|
||||
};
|
||||
(0..NORMAL_FACES).map(|polygon_group_id|{
|
||||
model::IndexedGraphicsGroup{
|
||||
render:cube_face_description[polygon_group_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
|
||||
groups:vec![PolygonGroupId::new(polygon_group_id as u32)]
|
||||
}
|
||||
}).collect()
|
||||
let graphics_groups=polygon_groups_normal_id
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(_,group)|!group.is_empty())
|
||||
.enumerate()
|
||||
.map(|(polygon_group_id,(normal_id,_))|{
|
||||
model::IndexedGraphicsGroup{
|
||||
render:cube_face_description[normal_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
|
||||
groups:vec![PolygonGroupId::new(polygon_group_id as u32)]
|
||||
}
|
||||
}).collect();
|
||||
(polygon_groups_normal_id,graphics_groups)
|
||||
}else{
|
||||
Vec::new()
|
||||
([vec![],vec![],vec![],vec![],vec![],vec![]],Vec::new())
|
||||
};
|
||||
|
||||
//physics
|
||||
let polygon_groups_normal_it=polygon_groups_normal_id.into_iter().map(|faces|
|
||||
let polygon_groups_normal_it=polygon_groups_normal_id.into_iter().filter_map(|faces|
|
||||
// graphics polygon groups (to be rendered)
|
||||
Ok(PolygonGroup::PolygonList(PolygonList::new(faces)))
|
||||
Some(PolygonGroup::PolygonList(new_polygon_list_checked(faces)?))
|
||||
);
|
||||
let polygon_groups:Vec<PolygonGroup>=if !roblox_physics_data.is_empty(){
|
||||
let physics_data=rbx_mesh::read_physics_data_versioned(
|
||||
@@ -225,12 +235,12 @@ pub fn convert(
|
||||
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V7(meshes))
|
||||
=>meshes.meshes,
|
||||
};
|
||||
let physics_convex_meshes_it=physics_convex_meshes.into_iter().map(|mesh|{
|
||||
let physics_convex_meshes_it=physics_convex_meshes.into_iter().filter_map(|mesh|{
|
||||
// this can be factored out of the loop but I am lazy
|
||||
let color=mb.acquire_color_id(glam::Vec4::ONE);
|
||||
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
|
||||
// physics polygon groups (to do physics)
|
||||
Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[PhysicsDataVertexId(vertex_id0),PhysicsDataVertexId(vertex_id1),PhysicsDataVertexId(vertex_id2)]|{
|
||||
let polygons=mesh.faces.into_iter().map(|[PhysicsDataVertexId(vertex_id0),PhysicsDataVertexId(vertex_id1),PhysicsDataVertexId(vertex_id2)]|{
|
||||
let face=[
|
||||
mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
|
||||
mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
|
||||
@@ -243,21 +253,26 @@ pub fn convert(
|
||||
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?);
|
||||
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
|
||||
}).collect()
|
||||
}).collect::<Result<_,_>>()?)))
|
||||
}).collect::<Result<_,_>>();
|
||||
let polygon_list=match polygons{
|
||||
Ok(polygons)=>new_polygon_list_checked(polygons)?,
|
||||
Err(e)=>return Some(Err(e)),
|
||||
};
|
||||
Some(Ok(PolygonGroup::PolygonList(polygon_list)))
|
||||
});
|
||||
polygon_groups_normal_it.chain(physics_convex_meshes_it).collect::<Result<_,_>>()?
|
||||
polygon_groups_normal_it.map(Ok).chain(physics_convex_meshes_it).collect::<Result<_,_>>()?
|
||||
}else{
|
||||
// generate a unit cube as default physics
|
||||
let pos_list=CUBE_DEFAULT_VERTICES.map(|pos|mb.acquire_pos_id(pos>>1));
|
||||
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
|
||||
let normal=mb.acquire_normal_id(vec3::ZERO);
|
||||
let normal=mb.acquire_normal_id(vec3::zero());
|
||||
let color=mb.acquire_color_id(glam::Vec4::ONE);
|
||||
let polygon_group=PolygonGroup::PolygonList(PolygonList::new(CUBE_DEFAULT_POLYS.map(|poly|poly.map(|[pos_id,_]|
|
||||
mb.acquire_vertex_id(IndexedVertex{pos:pos_list[pos_id as usize],tex,normal,color})
|
||||
).to_vec()).to_vec()));
|
||||
polygon_groups_normal_it.chain([Ok(polygon_group)]).collect::<Result<_,_>>()?
|
||||
polygon_groups_normal_it.chain([polygon_group]).collect()
|
||||
};
|
||||
let physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
|
||||
let physics_groups=(graphics_groups.len()..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
|
||||
groups:vec![PolygonGroupId::new(id as u32)]
|
||||
}).collect();
|
||||
let mesh=mb.build(
|
||||
@@ -265,8 +280,5 @@ pub fn convert(
|
||||
graphics_groups,
|
||||
physics_groups,
|
||||
);
|
||||
Ok(MeshWithSize{
|
||||
mesh,
|
||||
size:vec3::ONE,
|
||||
})
|
||||
Ok(MeshWithSize::new(mesh,vec3::ONE))
|
||||
}
|
||||
|
||||
@@ -9,3 +9,6 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
url = "2.5.4"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "roblox_emulator"
|
||||
version = "0.5.1"
|
||||
version = "0.5.3"
|
||||
edition = "2024"
|
||||
repository = "https://git.itzana.me/StrafesNET/strafe-project"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -12,10 +12,13 @@ default=["run-service"]
|
||||
run-service=[]
|
||||
|
||||
[dependencies]
|
||||
glam = "0.30.0"
|
||||
mlua = { version = "0.10.1", features = ["luau"] }
|
||||
phf = { version = "0.12.1", features = ["macros"] }
|
||||
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] }
|
||||
rbx_reflection = "5.0.0"
|
||||
rbx_reflection_database = "1.0.0"
|
||||
rbx_types = "2.0.0"
|
||||
glam.workspace = true
|
||||
mlua = { version = "0.11.3", features = ["luau"] }
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
rbx_dom_weak = "4.1.0"
|
||||
rbx_reflection = "6.1.0"
|
||||
rbx_reflection_database = "2.0.2"
|
||||
rbx_types = "3.1.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -52,7 +52,7 @@ impl Context{
|
||||
}
|
||||
/// Creates an iterator over all items of a particular class.
|
||||
pub fn superclass_iter<'a>(&'a self,superclass:&'a str)->impl Iterator<Item=Ref>+'a{
|
||||
let db=rbx_reflection_database::get();
|
||||
let db=rbx_reflection_database::get().unwrap();
|
||||
let Some(superclass)=db.classes.get(superclass)else{
|
||||
panic!("Invalid class");
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ impl<'a> EnumItem<'a>{
|
||||
Self{name:Some(name.as_ref()),value}
|
||||
}
|
||||
}
|
||||
impl<'a> From<rbx_types::Enum> for EnumItem<'a>{
|
||||
impl From<rbx_types::Enum> for EnumItem<'_>{
|
||||
fn from(e:rbx_types::Enum)->Self{
|
||||
EnumItem{
|
||||
name:None,
|
||||
@@ -37,7 +37,7 @@ impl PartialEq for EnumItem<'_>{
|
||||
pub struct Enums;
|
||||
impl Enums{
|
||||
pub fn get(&self,index:&str)->Option<EnumItems<'static>>{
|
||||
let db=rbx_reflection_database::get();
|
||||
let db=rbx_reflection_database::get().unwrap();
|
||||
db.enums.get(index).map(|ed|EnumItems{ed})
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ impl<'a> EnumItems<'a>{
|
||||
}
|
||||
|
||||
pub enum CoerceEnum<'a>{
|
||||
Integer(i32),
|
||||
Integer(i64),
|
||||
String(mlua::String),
|
||||
Enum(EnumItem<'a>),
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ use rbx_types::Ref;
|
||||
use rbx_dom_weak::{Ustr,InstanceBuilder,WeakDom};
|
||||
|
||||
use crate::util::static_ustr;
|
||||
use crate::runner::vector3::Vector3;
|
||||
use crate::runner::number::Number;
|
||||
|
||||
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
|
||||
//class functions store
|
||||
lua.set_app_data(ClassMethodsStore::default());
|
||||
lua.set_app_data(InstanceValueStore::default());
|
||||
|
||||
let table=lua.create_table()?;
|
||||
|
||||
@@ -37,13 +37,13 @@ pub fn dom_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result<T>
|
||||
}
|
||||
|
||||
pub fn class_is_a(class:&str,superclass:&str)->bool{
|
||||
let db=rbx_reflection_database::get();
|
||||
let db=rbx_reflection_database::get().unwrap();
|
||||
let (Some(class),Some(superclass))=(db.classes.get(class),db.classes.get(superclass))else{
|
||||
return false;
|
||||
};
|
||||
db.has_superclass(class,superclass)
|
||||
}
|
||||
fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->String{
|
||||
fn get_full_name(dom:&WeakDom,instance:&rbx_dom_weak::Instance)->String{
|
||||
let mut full_name=instance.name.clone();
|
||||
let mut pref=instance.parent();
|
||||
while let Some(parent)=dom.get_by_ref(pref){
|
||||
@@ -65,29 +65,29 @@ pub fn get_name_source(lua:&mlua::Lua,script:Instance)->Result<(String,String),m
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_first_child<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
pub fn find_first_child<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.name==name)
|
||||
}
|
||||
pub fn find_first_descendant<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
pub fn find_first_descendant<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
dom.descendants_of(instance.referent()).find(|&inst|inst.name==name)
|
||||
}
|
||||
|
||||
pub fn find_first_child_of_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
pub fn find_first_child_of_class<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.class==class)
|
||||
}
|
||||
pub fn find_first_descendant_of_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
pub fn find_first_descendant_of_class<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
dom.descendants_of(instance.referent()).find(|&inst|inst.class==class)
|
||||
}
|
||||
|
||||
pub fn find_first_child_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
let db=rbx_reflection_database::get();
|
||||
pub fn find_first_child_which_is_a<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
let db=rbx_reflection_database::get().unwrap();
|
||||
let superclass_descriptor=db.classes.get(superclass)?;
|
||||
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|{
|
||||
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
|
||||
})
|
||||
}
|
||||
pub fn find_first_descendant_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
let db=rbx_reflection_database::get();
|
||||
pub fn find_first_descendant_which_is_a<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
|
||||
let db=rbx_reflection_database::get().unwrap();
|
||||
let superclass_descriptor=db.classes.get(superclass)?;
|
||||
dom.descendants_of(instance.referent()).find(|inst|{
|
||||
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
|
||||
@@ -282,7 +282,7 @@ impl mlua::UserData for Instance{
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=this.get(dom)?;
|
||||
//println!("__index t={} i={index:?}",instance.name);
|
||||
let db=rbx_reflection_database::get();
|
||||
let db=rbx_reflection_database::get().unwrap();
|
||||
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
|
||||
// Find existing property
|
||||
// Interestingly, ustr can know ahead of time if
|
||||
@@ -325,13 +325,16 @@ impl mlua::UserData for Instance{
|
||||
}
|
||||
|
||||
//find or create an associated userdata object
|
||||
let instance=this.get_mut(dom)?;
|
||||
if let Some(value)=get_or_create_userdata(instance,lua,index_str)?{
|
||||
if let Some(value)=instance_value_store_mut(lua,|ivs|{
|
||||
//TODO: walk class tree somehow
|
||||
match ivs.get_or_create_instance_values(&instance){
|
||||
Some(mut instance_values)=>instance_values.get_or_create_value(lua,index_str),
|
||||
None=>Ok(None)
|
||||
}
|
||||
})?{
|
||||
return value.into_lua(lua);
|
||||
}
|
||||
// drop mutable borrow
|
||||
//find a child with a matching name
|
||||
let instance=this.get(dom)?;
|
||||
find_first_child(dom,instance,index_str)
|
||||
.map(|instance|Instance::new_unchecked(instance.referent()))
|
||||
.into_lua(lua)
|
||||
@@ -341,7 +344,7 @@ impl mlua::UserData for Instance{
|
||||
let index_str=&*index.to_str()?;
|
||||
dom_mut(lua,|dom|{
|
||||
let instance=this.get_mut(dom)?;
|
||||
let db=rbx_reflection_database::get();
|
||||
let db=rbx_reflection_database::get().unwrap();
|
||||
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
|
||||
let property=db.superclasses_iter(class).find_map(|cls|
|
||||
cls.properties.get(index_str)
|
||||
@@ -419,7 +422,7 @@ impl mlua::UserData for Instance{
|
||||
rbx_types::Variant::CFrame(typed_value.clone().into())
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::ContentId)=>{
|
||||
let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected string"))?.to_owned();
|
||||
let typed_value=value.as_string().ok_or_else(||mlua::Error::runtime("Expected string"))?.to_str()?.to_owned();
|
||||
rbx_types::Variant::ContentId(typed_value.into())
|
||||
},
|
||||
rbx_reflection::DataType::Value(rbx_types::VariantType::Ref)=>{
|
||||
@@ -484,8 +487,8 @@ static CLASS_FUNCTION_DATABASE:CFD=phf::phf_map!{
|
||||
"GetService"=>GET_SERVICE,
|
||||
},
|
||||
"Terrain"=>phf::phf_map!{
|
||||
"FillBall"=>cf!(|_lua,_,_:(Vector3,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
|
||||
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
|
||||
"FillBall"=>cf!(|_lua,_,_:(crate::runner::vector3::Vector3,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
|
||||
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,crate::runner::vector3::Vector3,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
|
||||
"FillCylinder"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Number,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
|
||||
"SetMaterialColor"=>cf!(|_lua,_,_:(crate::runner::r#enum::CoerceEnum,crate::runner::color3::Color3)|mlua::Result::Ok(())),
|
||||
},
|
||||
@@ -606,6 +609,8 @@ fn find_virtual_property(
|
||||
}
|
||||
|
||||
// lazy-loaded per-instance userdata values
|
||||
// This whole thing is a bad idea and a garbage collection nightmare.
|
||||
// TODO: recreate rbx_dom_weak with my own instance type that owns this data.
|
||||
type CreateUserData=fn(&mlua::Lua)->mlua::Result<mlua::AnyUserData>;
|
||||
type LUD=phf::Map<&'static str,// Class name
|
||||
phf::Map<&'static str,// Value name
|
||||
@@ -638,22 +643,47 @@ static LAZY_USER_DATA:LUD=phf::phf_map!{
|
||||
"MouseClick"=>create_script_signal,
|
||||
},
|
||||
};
|
||||
fn get_or_create_userdata(instance:&mut rbx_dom_weak::Instance,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
|
||||
use std::collections::hash_map::Entry;
|
||||
let db=rbx_reflection_database::get();
|
||||
let Some(class)=db.classes.get(instance.class.as_str())else{
|
||||
return Ok(None)
|
||||
};
|
||||
if let Some((&static_str,create_userdata))=db.superclasses_iter(class).find_map(|superclass|
|
||||
// find pair (class,index)
|
||||
LAZY_USER_DATA.get(&superclass.name)
|
||||
.and_then(|map|map.get_entry(index))
|
||||
){
|
||||
let index_ustr=static_ustr(static_str);
|
||||
return Ok(Some(match instance.userdata.entry(index_ustr){
|
||||
Entry::Occupied(entry)=>entry.get().clone(),
|
||||
Entry::Vacant(entry)=>entry.insert(create_userdata(lua)?).clone(),
|
||||
}));
|
||||
}
|
||||
Ok(None)
|
||||
#[derive(Default)]
|
||||
pub struct InstanceValueStore{
|
||||
values:HashMap<Ref,
|
||||
HashMap<&'static str,
|
||||
mlua::AnyUserData
|
||||
>
|
||||
>,
|
||||
}
|
||||
pub struct InstanceValues<'a>{
|
||||
named_values:&'static phf::Map<&'static str,CreateUserData>,
|
||||
values:&'a mut HashMap<&'static str,mlua::AnyUserData>,
|
||||
}
|
||||
impl InstanceValueStore{
|
||||
pub fn get_or_create_instance_values(&mut self,instance:&rbx_dom_weak::Instance)->Option<InstanceValues<'_>>{
|
||||
LAZY_USER_DATA.get(instance.class.as_str())
|
||||
.map(|named_values|
|
||||
InstanceValues{
|
||||
named_values,
|
||||
values:self.values.entry(instance.referent())
|
||||
.or_insert_with(||HashMap::new()),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
impl InstanceValues<'_>{
|
||||
pub fn get_or_create_value(&mut self,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
|
||||
Ok(match self.named_values.get_entry(index){
|
||||
Some((&static_index_str,&function_pointer))=>Some(
|
||||
match self.values.entry(static_index_str){
|
||||
Entry::Occupied(entry)=>entry.get().clone(),
|
||||
Entry::Vacant(entry)=>entry.insert(
|
||||
function_pointer(lua)?
|
||||
).clone(),
|
||||
}
|
||||
),
|
||||
None=>None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instance_value_store_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut InstanceValueStore)->mlua::Result<T>)->mlua::Result<T>{
|
||||
let mut cf=lua.app_data_mut::<InstanceValueStore>().ok_or_else(||mlua::Error::runtime("InstanceValueStore missing"))?;
|
||||
f(&mut *cf)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#[derive(Clone,Copy)]
|
||||
pub enum Number{
|
||||
Integer(i32),
|
||||
Integer(i64),
|
||||
Number(f64),
|
||||
}
|
||||
macro_rules! impl_ty{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::context::Context;
|
||||
use crate::util::static_ustr;
|
||||
#[cfg(feature="run-service")]
|
||||
use crate::scheduler::scheduler_mut;
|
||||
|
||||
@@ -49,6 +48,11 @@ fn init(lua:&mlua::Lua)->mlua::Result<()>{
|
||||
Ok(())
|
||||
}
|
||||
|
||||
unsafe fn extend_lifetime_mut<'a,T>(src:&mut T)->&'a mut T{
|
||||
let ptr:*mut T=src;
|
||||
unsafe{&mut*ptr}
|
||||
}
|
||||
|
||||
impl Runner{
|
||||
pub fn new()->Result<Self,Error>{
|
||||
let runner=Self{
|
||||
@@ -66,8 +70,7 @@ impl Runner{
|
||||
// SAFETY: This is not a &'static mut WeakDom,
|
||||
// but as long as Runnable<'a> holds the lifetime of &'a mut Context
|
||||
// it is a valid unique reference.
|
||||
let ptr=&mut context.dom as *mut rbx_dom_weak::WeakDom;
|
||||
self.lua.set_app_data::<crate::context::LuaAppData>(unsafe{&mut*ptr});
|
||||
self.lua.set_app_data::<crate::context::LuaAppData>(unsafe{extend_lifetime_mut(&mut context.dom)});
|
||||
#[cfg(feature="run-service")]
|
||||
self.lua.set_app_data::<crate::scheduler::Scheduler>(crate::scheduler::Scheduler::default());
|
||||
Ok(Runnable{
|
||||
@@ -98,7 +101,19 @@ impl Runnable<'_>{
|
||||
.set_name(name).into_function().map_err(Error::RustLua)?;
|
||||
// TODO: set_environment without losing the ability to print from Lua
|
||||
let thread=self.lua.create_thread(f).map_err(Error::RustLua)?;
|
||||
|
||||
// set timeout
|
||||
let start=std::time::Instant::now();
|
||||
self.lua.set_interrupt(move|lua|{
|
||||
if std::time::Duration::from_secs(1)<start.elapsed(){
|
||||
lua.remove_interrupt();
|
||||
return Err(mlua::Error::runtime("timeout"));
|
||||
}
|
||||
Ok(mlua::VmState::Continue)
|
||||
});
|
||||
|
||||
thread.resume::<mlua::MultiValue>(()).map_err(|error|Error::Lua{source,error})?;
|
||||
self.lua.remove_interrupt();
|
||||
// wait() is called from inside Lua and goes to a rust function that schedules the thread and then yields
|
||||
// No need to schedule the thread here
|
||||
Ok(())
|
||||
@@ -126,15 +141,20 @@ impl Runnable<'_>{
|
||||
}
|
||||
#[cfg(feature="run-service")]
|
||||
pub fn run_service_step(&self)->Result<(),mlua::Error>{
|
||||
let render_stepped_signal=super::instance::instance::dom_mut(&self.lua,|dom|{
|
||||
let render_stepped=super::instance::instance::dom_mut(&self.lua,|dom|{
|
||||
let run_service=super::instance::instance::find_first_child_of_class(dom,dom.root(),"RunService").ok_or_else(||mlua::Error::runtime("RunService missing"))?;
|
||||
Ok(match run_service.userdata.get(&static_ustr("RenderStepped")){
|
||||
Some(render_stepped)=>Some(render_stepped.borrow::<super::script_signal::ScriptSignal>()?.clone()),
|
||||
None=>None
|
||||
super::instance::instance::instance_value_store_mut(&self.lua,|instance_value_store|{
|
||||
//unwrap because I trust my find_first_child_of_class function to
|
||||
let mut instance_values=instance_value_store.get_or_create_instance_values(run_service).ok_or_else(||mlua::Error::runtime("RunService InstanceValues missing"))?;
|
||||
let render_stepped=instance_values.get_or_create_value(&self.lua,"RenderStepped")?;
|
||||
//let stepped=instance_values.get_or_create_value(&self.lua,"Stepped")?;
|
||||
//let heartbeat=instance_values.get_or_create_value(&self.lua,"Heartbeat")?;
|
||||
Ok(render_stepped)
|
||||
})
|
||||
})?;
|
||||
if let Some(render_stepped_signal)=render_stepped_signal{
|
||||
render_stepped_signal.fire(&mlua::MultiValue::new());
|
||||
if let Some(render_stepped)=render_stepped{
|
||||
let signal:&super::script_signal::ScriptSignal=&*render_stepped.borrow()?;
|
||||
signal.fire(&mlua::MultiValue::new());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ impl mlua::FromLua for ScriptSignal{
|
||||
}
|
||||
|
||||
impl mlua::UserData for ScriptConnection{
|
||||
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
|
||||
fn add_fields<F:UserDataFields<Self>>(fields:&mut F){
|
||||
fields.add_field_method_get("Connected",|_,this|{
|
||||
Ok(this.position().is_some())
|
||||
});
|
||||
|
||||
@@ -46,8 +46,8 @@ impl Scheduler{
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scheduler_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut crate::scheduler::Scheduler)->mlua::Result<T>)->mlua::Result<T>{
|
||||
let mut scheduler=lua.app_data_mut::<crate::scheduler::Scheduler>().ok_or_else(||mlua::Error::runtime("Scheduler missing"))?;
|
||||
pub fn scheduler_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut Scheduler)->mlua::Result<T>)->mlua::Result<T>{
|
||||
let mut scheduler=lua.app_data_mut::<Scheduler>().ok_or_else(||mlua::Error::runtime("Scheduler missing"))?;
|
||||
f(&mut *scheduler)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strafesnet_snf"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -8,4 +8,7 @@ edition = "2024"
|
||||
[dependencies]
|
||||
binrw = "0.15.0"
|
||||
id = { version = "0.1.0", registry = "strafesnet" }
|
||||
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
|
||||
strafesnet_common.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -6,7 +6,7 @@ use strafesnet_common::physics::Time;
|
||||
|
||||
const VERSION:u32=0;
|
||||
|
||||
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::Time>;
|
||||
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,Time>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
@@ -274,7 +274,7 @@ pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:i
|
||||
|
||||
//probe header length
|
||||
let mut bot_header_data=Vec::new();
|
||||
binrw::BinWrite::write_le(&header,&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;
|
||||
header.write_le(&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;
|
||||
|
||||
// the first block location is the map header
|
||||
block_location.push(offset);
|
||||
|
||||
@@ -97,8 +97,8 @@ enum ResourceType{
|
||||
}
|
||||
|
||||
struct ResourceMap<T>{
|
||||
meshes:HashMap<strafesnet_common::model::MeshId,T>,
|
||||
textures:HashMap<strafesnet_common::model::TextureId,T>,
|
||||
meshes:HashMap<model::MeshId,T>,
|
||||
textures:HashMap<model::TextureId,T>,
|
||||
}
|
||||
impl<T> Default for ResourceMap<T>{
|
||||
fn default()->Self{
|
||||
@@ -185,7 +185,7 @@ pub struct StreamableMap<R:BinReaderExt>{
|
||||
//this is every possible attribute... need some sort of streaming system
|
||||
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
|
||||
//this is every possible render configuration... shaders and such... need streaming
|
||||
render_configs:Vec<strafesnet_common::model::RenderConfig>,
|
||||
render_configs:Vec<model::RenderConfig>,
|
||||
//this makes sense to keep in memory for streaming, a map of which blocks occupy what space
|
||||
bvh:BvhNode<BlockId>,
|
||||
//something something resources hashmaps
|
||||
@@ -223,7 +223,7 @@ impl<R:BinReaderExt> StreamableMap<R>{
|
||||
}
|
||||
Ok(Self{
|
||||
file,
|
||||
modes:strafesnet_common::gameplay_modes::NormalizedModes::new(modes),
|
||||
modes:gameplay_modes::NormalizedModes::new(modes),
|
||||
attributes,
|
||||
render_configs,
|
||||
bvh:strafesnet_common::bvh::generate_bvh(bvh),
|
||||
@@ -366,12 +366,12 @@ fn collect_spacial_blocks(
|
||||
block_location.push(sequential_block_data.position());
|
||||
}else{
|
||||
match bvh_node.into_content(){
|
||||
strafesnet_common::bvh::RecursiveContent::Branch(bvh_node_list)=>{
|
||||
RecursiveContent::Branch(bvh_node_list)=>{
|
||||
for bvh_node in bvh_node_list{
|
||||
collect_spacial_blocks(block_location,block_headers,sequential_block_data,bvh_node)?;
|
||||
}
|
||||
},
|
||||
strafesnet_common::bvh::RecursiveContent::Leaf(_)=>panic!(),//bvh branches are 20 leaves minimum
|
||||
RecursiveContent::Leaf(_)=>panic!(),//bvh branches are 20 leaves minimum
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -384,13 +384,13 @@ pub fn write_map<W:BinWriterExt>(mut writer:W,map:strafesnet_common::map::Comple
|
||||
let boxen=map.models.into_iter().enumerate().map(|(model_id,model)|{
|
||||
//grow your own aabb
|
||||
let mesh=map.meshes.get(model.mesh.get() as usize).ok_or(Error::InvalidMeshId(model.mesh))?;
|
||||
let mut aabb=strafesnet_common::aabb::Aabb::default();
|
||||
let mut aabb=Aabb::default();
|
||||
for &pos in &mesh.unique_pos{
|
||||
aabb.grow(model.transform.transform_point3(pos).narrow_1().unwrap());
|
||||
aabb.grow(model.transform.transform_point3(pos).narrow_64().unwrap());
|
||||
}
|
||||
Ok(((model::ModelId::new(model_id as u32),model.into()),aabb))
|
||||
}).collect::<Result<Vec<_>,_>>()?;
|
||||
let bvh=weigh_contents(strafesnet_common::bvh::generate_bvh(boxen),&|_|std::mem::size_of::<newtypes::model::Model>());
|
||||
let bvh=weigh_contents(strafesnet_common::bvh::generate_bvh(boxen),&|_|size_of::<newtypes::model::Model>());
|
||||
//build blocks
|
||||
//block location is initialized with two values
|
||||
//the first value represents the location of the first byte after the file header
|
||||
|
||||
@@ -20,7 +20,7 @@ impl TryInto<TimedPhysicsInstruction> for TimedInstruction{
|
||||
}
|
||||
}
|
||||
impl TryFrom<TimedPhysicsInstruction> for TimedInstruction{
|
||||
type Error=super::physics::InstructionConvert;
|
||||
type Error=InstructionConvert;
|
||||
fn try_from(value:TimedPhysicsInstruction)->Result<Self,Self::Error>{
|
||||
Ok(Self{
|
||||
time:value.time.get(),
|
||||
@@ -88,6 +88,11 @@ pub enum Instruction{
|
||||
PracticeFly,
|
||||
#[brw(magic=14u8)]
|
||||
SetSensitivity(super::integer::Ratio64Vec2),
|
||||
#[brw(magic=15u8)]
|
||||
SetSprint(
|
||||
#[br(map=bool_from_u8)]
|
||||
#[bw(map=bool_into_u8)]
|
||||
bool),
|
||||
#[brw(magic=255u8)]
|
||||
Idle,
|
||||
}
|
||||
@@ -116,6 +121,7 @@ impl TryInto<strafesnet_common::physics::Instruction> for Instruction{
|
||||
Instruction::SetMoveForward(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state.into())),
|
||||
Instruction::SetJump(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state.into())),
|
||||
Instruction::SetZoom(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state.into())),
|
||||
Instruction::SetSprint(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetSprint(state.into())),
|
||||
Instruction::Reset=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset),
|
||||
Instruction::Restart(mode_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(strafesnet_common::gameplay_modes::ModeId::new(mode_id))),
|
||||
Instruction::Spawn(mode_id,stage_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(
|
||||
@@ -142,6 +148,7 @@ impl TryFrom<strafesnet_common::physics::Instruction> for Instruction{
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state))=>Ok(Instruction::SetMoveForward(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state))=>Ok(Instruction::SetJump(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state))=>Ok(Instruction::SetZoom(state.into())),
|
||||
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetSprint(state))=>Ok(Instruction::SetSprint(state.into())),
|
||||
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset)=>Ok(Instruction::Reset),
|
||||
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(mode_id))=>Ok(Instruction::Restart(mode_id.get())),
|
||||
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(mode_id,stage_id))=>Ok(Instruction::Spawn(
|
||||
|
||||
@@ -1,38 +1,81 @@
|
||||
[package]
|
||||
name = "map-tool"
|
||||
version = "1.7.2"
|
||||
version = "3.0.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = ["cli","source","roblox"]
|
||||
roblox = [
|
||||
"dep:strafesnet_rbx_loader",
|
||||
"dep:rbx_asset",
|
||||
"dep:rbx_binary",
|
||||
"dep:rbx_dom_weak",
|
||||
"dep:rbx_reflection_database",
|
||||
"dep:rbx_xml",
|
||||
"dep:rbxassetid",
|
||||
]
|
||||
source = [
|
||||
"dep:strafesnet_bsp_loader",
|
||||
"dep:vbsp",
|
||||
"dep:vbsp-entities-css",
|
||||
"dep:vmdl",
|
||||
"dep:vmt-parser",
|
||||
"dep:vpk",
|
||||
"dep:vtf",
|
||||
"tokio/sync",
|
||||
]
|
||||
cli = [
|
||||
"dep:clap",
|
||||
"dep:futures",
|
||||
"tokio/macros",
|
||||
"tokio/rt-multi-thread",
|
||||
"tokio/fs",
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "map_tool"
|
||||
|
||||
[[bin]]
|
||||
name = "map-tool"
|
||||
required-features = ["cli"]
|
||||
|
||||
[dependencies]
|
||||
strafesnet_deferred_loader.workspace = true
|
||||
strafesnet_snf.workspace = true
|
||||
|
||||
anyhow = "1.0.75"
|
||||
clap = { version = "4.4.2", features = ["derive"] }
|
||||
clap = { version = "4.4.2", features = ["derive"], optional = true }
|
||||
flate2 = "1.0.27"
|
||||
futures = "0.3.31"
|
||||
image = "0.25.2"
|
||||
image_dds = "0.7.1"
|
||||
lazy-regex = "3.1.0"
|
||||
rbx_asset = { version = "0.4.4", registry = "strafesnet" }
|
||||
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" }
|
||||
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet" }
|
||||
rbx_reflection_database = "1.0.0"
|
||||
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" }
|
||||
rbxassetid = { version = "0.1.0", registry = "strafesnet" }
|
||||
strafesnet_bsp_loader = { version = "0.3.1", path = "../lib/bsp_loader", registry = "strafesnet" }
|
||||
strafesnet_deferred_loader = { version = "0.5.1", path = "../lib/deferred_loader", registry = "strafesnet" }
|
||||
strafesnet_rbx_loader = { version = "0.7.0", path = "../lib/rbx_loader", registry = "strafesnet" }
|
||||
strafesnet_snf = { version = "0.3.1", path = "../lib/snf", registry = "strafesnet" }
|
||||
futures = { version = "0.3.31", optional = true }
|
||||
image = { version = "0.25.2", features = ["png", "jpeg"], default-features = false }
|
||||
image_dds = { version = "0.7.1", features = ["ddsfile","encode"], default-features = false }
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] }
|
||||
vbsp = "0.9.1"
|
||||
vbsp-entities-css = "0.6.0"
|
||||
vmdl = "0.2.0"
|
||||
vmt-parser = "0.2.0"
|
||||
vpk = "0.3.0"
|
||||
vtf = "0.3.0"
|
||||
tokio = { version = "1.43.0", features = ["time"] }
|
||||
|
||||
# roblox
|
||||
strafesnet_rbx_loader = { workspace = true, optional = true }
|
||||
rbx_asset = { version = "0.5.0", registry = "strafesnet", optional = true }
|
||||
rbx_binary = { version = "2.0.1", optional = true }
|
||||
rbx_dom_weak = { version = "4.1.0", optional = true }
|
||||
rbx_reflection_database = { version = "2.0.2", optional = true }
|
||||
rbx_xml = { version = "2.0.1", optional = true }
|
||||
rbxassetid = { version = "0.1.0", registry = "strafesnet", optional = true }
|
||||
|
||||
# source
|
||||
strafesnet_bsp_loader = { workspace = true, optional = true }
|
||||
vbsp = { version = "0.9.1", optional = true }
|
||||
vbsp-entities-css = { version = "0.6.0", optional = true }
|
||||
vmdl = { version = "0.2.0", optional = true }
|
||||
vmt-parser = { version = "0.2.0", optional = true }
|
||||
vpk = { version = "0.3.0", optional = true }
|
||||
vtf = { version = "0.3.0", optional = true }
|
||||
|
||||
#[profile.release]
|
||||
#lto = true
|
||||
#strip = true
|
||||
#codegen-units = 1
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
4
map-tool/src/lib.rs
Normal file
4
map-tool/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
#[cfg(feature="roblox")]
|
||||
pub mod roblox;
|
||||
#[cfg(feature="source")]
|
||||
pub mod source;
|
||||
@@ -1,30 +1,31 @@
|
||||
mod roblox;
|
||||
mod source;
|
||||
|
||||
use clap::{Parser,Subcommand};
|
||||
use anyhow::Result as AResult;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(propagate_version = true)]
|
||||
struct Cli {
|
||||
#[command(author,version,about,long_about=None)]
|
||||
#[command(propagate_version=true)]
|
||||
struct Cli{
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
command:Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands{
|
||||
#[command(flatten)]
|
||||
Roblox(roblox::Commands),
|
||||
#[cfg(feature="roblox")]
|
||||
Roblox(map_tool::roblox::Commands),
|
||||
#[command(flatten)]
|
||||
Source(source::Commands),
|
||||
#[cfg(feature="source")]
|
||||
Source(map_tool::source::Commands),
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main()->AResult<()>{
|
||||
let cli=Cli::parse();
|
||||
match cli.command{
|
||||
#[cfg(feature="roblox")]
|
||||
Commands::Roblox(commands)=>commands.run().await,
|
||||
#[cfg(feature="source")]
|
||||
Commands::Source(commands)=>commands.run().await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
use std::path::{Path,PathBuf};
|
||||
use std::io::{Cursor,Read,Seek};
|
||||
use std::collections::HashSet;
|
||||
use clap::{Args,Subcommand};
|
||||
use std::path::PathBuf;
|
||||
use std::io::Cursor;
|
||||
use anyhow::Result as AResult;
|
||||
use rbx_dom_weak::Instance;
|
||||
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
|
||||
use rbxassetid::RobloxAssetId;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
|
||||
|
||||
// disallow non-static lifetimes
|
||||
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
|
||||
rbx_dom_weak::ustr(s)
|
||||
}
|
||||
use super::{convert_texture_to_dds,get_unique_assets};
|
||||
use super::{ConvertTextureError,UniqueAssets};
|
||||
|
||||
const DOWNLOAD_LIMIT:usize=16;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[derive(clap::Subcommand)]
|
||||
pub enum Commands{
|
||||
RobloxToSNF(RobloxToSNFSubcommand),
|
||||
DownloadAssets(DownloadAssetsSubcommand),
|
||||
ExtractUnionData(ExtractUnionData),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct RobloxToSNFSubcommand {
|
||||
#[derive(clap::Args)]
|
||||
pub struct RobloxToSNFSubcommand{
|
||||
#[arg(long)]
|
||||
output_folder:PathBuf,
|
||||
#[arg(required=true)]
|
||||
input_files:Vec<PathBuf>,
|
||||
}
|
||||
#[derive(Args)]
|
||||
#[derive(clap::Args)]
|
||||
pub struct DownloadAssetsSubcommand{
|
||||
#[arg(required=true)]
|
||||
roblox_files:Vec<PathBuf>,
|
||||
@@ -39,6 +34,13 @@ pub struct DownloadAssetsSubcommand{
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_file:Option<PathBuf>,
|
||||
}
|
||||
#[derive(clap::Args)]
|
||||
pub struct ExtractUnionData{
|
||||
#[arg(long)]
|
||||
output_folder:PathBuf,
|
||||
#[arg(long)]
|
||||
input_file:PathBuf,
|
||||
}
|
||||
|
||||
impl Commands{
|
||||
pub async fn run(self)->AResult<()>{
|
||||
@@ -52,6 +54,7 @@ impl Commands{
|
||||
subcommand.cookie_file,
|
||||
).await?,
|
||||
).await,
|
||||
Commands::ExtractUnionData(subcommand)=>extract_union_data(subcommand.input_file,subcommand.output_folder).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,148 +69,6 @@ async fn cookie_from_args(literal:Option<String>,environment:Option<String>,file
|
||||
Ok(rbx_asset::cookie::Cookie::new(cookie))
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum LoadDomError{
|
||||
IO(std::io::Error),
|
||||
Binary(rbx_binary::DecodeError),
|
||||
Xml(rbx_xml::DecodeError),
|
||||
UnknownFormat,
|
||||
}
|
||||
fn load_dom<R:Read+Seek>(mut input:R)->Result<rbx_dom_weak::WeakDom,LoadDomError>{
|
||||
let mut first_8=[0u8;8];
|
||||
input.read_exact(&mut first_8).map_err(LoadDomError::IO)?;
|
||||
input.rewind().map_err(LoadDomError::IO)?;
|
||||
match &first_8{
|
||||
b"<roblox!"=>rbx_binary::from_reader(input).map_err(LoadDomError::Binary),
|
||||
b"<roblox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(LoadDomError::Xml),
|
||||
_=>Err(LoadDomError::UnknownFormat),
|
||||
}
|
||||
}
|
||||
|
||||
/* The ones I'm interested in:
|
||||
Beam.Texture
|
||||
Decal.Texture
|
||||
FileMesh.MeshId
|
||||
FileMesh.TextureId
|
||||
MaterialVariant.ColorMap
|
||||
MaterialVariant.MetalnessMap
|
||||
MaterialVariant.NormalMap
|
||||
MaterialVariant.RoughnessMap
|
||||
MeshPart.MeshId
|
||||
MeshPart.TextureID
|
||||
ParticleEmitter.Texture
|
||||
Sky.MoonTextureId
|
||||
Sky.SkyboxBk
|
||||
Sky.SkyboxDn
|
||||
Sky.SkyboxFt
|
||||
Sky.SkyboxLf
|
||||
Sky.SkyboxRt
|
||||
Sky.SkyboxUp
|
||||
Sky.SunTextureId
|
||||
SurfaceAppearance.ColorMap
|
||||
SurfaceAppearance.MetalnessMap
|
||||
SurfaceAppearance.NormalMap
|
||||
SurfaceAppearance.RoughnessMap
|
||||
SurfaceAppearance.TexturePack
|
||||
*/
|
||||
/* These properties now use Content
|
||||
BaseWrap.CageMeshContent
|
||||
Decal.TextureContent
|
||||
ImageButton.ImageContent
|
||||
ImageLabel.ImageContent
|
||||
MeshPart.MeshContent
|
||||
MeshPart.TextureContent
|
||||
SurfaceAppearance.ColorMapContent
|
||||
SurfaceAppearance.MetalnessMapContent
|
||||
SurfaceAppearance.NormalMapContent
|
||||
SurfaceAppearance.RoughnessMapContent
|
||||
WrapLayer.ReferenceMeshContent
|
||||
*/
|
||||
|
||||
fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
|
||||
let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(&static_ustr(property))else{
|
||||
println!("property={} does not exist for class={}",property,object.class.as_str());
|
||||
return;
|
||||
};
|
||||
let rbx_dom_weak::types::ContentType::Uri(uri)=content.value()else{
|
||||
println!("ContentType is not Uri");
|
||||
return;
|
||||
};
|
||||
let Ok(asset_id)=uri.parse()else{
|
||||
println!("Content failed to parse into AssetID: {:?}",content);
|
||||
return;
|
||||
};
|
||||
content_list.insert(asset_id);
|
||||
}
|
||||
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
|
||||
let Some(rbx_dom_weak::types::Variant::ContentId(content))=object.properties.get(&static_ustr(property))else{
|
||||
println!("property={} does not exist for class={}",property,object.class.as_str());
|
||||
return;
|
||||
};
|
||||
let Ok(asset_id)=content.as_str().parse()else{
|
||||
println!("Content failed to parse into AssetID: {:?}",content);
|
||||
return;
|
||||
};
|
||||
content_list.insert(asset_id);
|
||||
}
|
||||
async fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
|
||||
let mut file=tokio::fs::File::open(path).await?;
|
||||
let mut data=Vec::new();
|
||||
file.read_to_end(&mut data).await?;
|
||||
Ok(Cursor::new(data))
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct UniqueAssets{
|
||||
meshes:HashSet<RobloxAssetId>,
|
||||
unions:HashSet<RobloxAssetId>,
|
||||
textures:HashSet<RobloxAssetId>,
|
||||
}
|
||||
impl UniqueAssets{
|
||||
fn collect(&mut self,object:&Instance){
|
||||
match object.class.as_str(){
|
||||
"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"Decal"=>accumulate_content(&mut self.textures,object,"TextureContent"),
|
||||
"Texture"=>accumulate_content(&mut self.textures,object,"TextureContent"),
|
||||
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
|
||||
"MeshPart"=>{
|
||||
accumulate_content(&mut self.textures,object,"TextureContent");
|
||||
accumulate_content(&mut self.meshes,object,"MeshContent");
|
||||
},
|
||||
"SpecialMesh"=>accumulate_content_id(&mut self.meshes,object,"MeshId"),
|
||||
"ParticleEmitter"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"Sky"=>{
|
||||
accumulate_content_id(&mut self.textures,object,"MoonTextureId");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxBk");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxDn");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxFt");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxLf");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxRt");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxUp");
|
||||
accumulate_content_id(&mut self.textures,object,"SunTextureId");
|
||||
},
|
||||
"UnionOperation"=>accumulate_content_id(&mut self.unions,object,"AssetId"),
|
||||
_=>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum UniqueAssetError{
|
||||
IO(std::io::Error),
|
||||
LoadDom(LoadDomError),
|
||||
}
|
||||
async fn unique_assets(path:&Path)->Result<UniqueAssets,UniqueAssetError>{
|
||||
// read entire file
|
||||
let mut assets=UniqueAssets::default();
|
||||
let data=read_entire_file(path).await.map_err(UniqueAssetError::IO)?;
|
||||
let dom=load_dom(data).map_err(UniqueAssetError::LoadDom)?;
|
||||
for object in dom.into_raw().1.into_values(){
|
||||
assets.collect(&object);
|
||||
}
|
||||
Ok(assets)
|
||||
}
|
||||
enum DownloadType{
|
||||
Texture(RobloxAssetId),
|
||||
Mesh(RobloxAssetId),
|
||||
@@ -253,7 +114,6 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
|
||||
let asset_id=download_instruction.asset_id();
|
||||
// if not, download file
|
||||
let mut retry=0;
|
||||
const BACKOFF_MUL:f32=1.3956124250860895286;//exp(1/3)
|
||||
let mut backoff=1000f32;
|
||||
loop{
|
||||
let asset_result=context.get_asset(rbx_asset::cookie::GetAssetRequest{
|
||||
@@ -276,7 +136,7 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
|
||||
}
|
||||
println!("Hit roblox rate limit, waiting {:.0}ms...",backoff);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await;
|
||||
backoff*=BACKOFF_MUL;
|
||||
backoff*=super::BACKOFF_MUL;
|
||||
retry+=1;
|
||||
}else{
|
||||
stats.failed_downloads+=1;
|
||||
@@ -292,47 +152,27 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum ConvertTextureError{
|
||||
#[error("Io error {0:?}")]
|
||||
Io(#[from]std::io::Error),
|
||||
#[error("Image error {0:?}")]
|
||||
Image(#[from]image::ImageError),
|
||||
#[error("DDS create error {0:?}")]
|
||||
DDS(#[from]image_dds::CreateDdsError),
|
||||
#[error("DDS write error {0:?}")]
|
||||
DDSWrite(#[from]image_dds::ddsfile::Error),
|
||||
}
|
||||
async fn convert_texture(RobloxAssetId(asset_id):RobloxAssetId,download_result:DownloadResult)->Result<(),ConvertTextureError>{
|
||||
|
||||
async fn cli_convert_texture(RobloxAssetId(asset_id):RobloxAssetId,download_result:DownloadResult)->Result<(),CliConvertTextureError>{
|
||||
let data=match download_result{
|
||||
DownloadResult::Cached(path)=>tokio::fs::read(path).await?,
|
||||
DownloadResult::Data(data)=>data,
|
||||
DownloadResult::Failed=>return Ok(()),
|
||||
};
|
||||
// image::ImageFormat::Png
|
||||
// image::ImageFormat::Jpeg
|
||||
let image=image::load_from_memory(&data)?.to_rgba8();
|
||||
|
||||
// pick format
|
||||
let format=if image.width()%4!=0||image.height()%4!=0{
|
||||
image_dds::ImageFormat::Rgba8UnormSrgb
|
||||
}else{
|
||||
image_dds::ImageFormat::BC7RgbaUnormSrgb
|
||||
};
|
||||
|
||||
//this fails if the image dimensions are not a multiple of 4
|
||||
let dds=image_dds::dds_from_image(
|
||||
&image,
|
||||
format,
|
||||
image_dds::Quality::Slow,
|
||||
image_dds::Mipmaps::GeneratedAutomatic,
|
||||
)?;
|
||||
let dds=convert_texture_to_dds(&data)?;
|
||||
|
||||
let file_name=format!("textures/{asset_id}.dds");
|
||||
let mut file=std::fs::File::create(file_name)?;
|
||||
dds.write(&mut file)?;
|
||||
tokio::fs::write(file_name,&dds).await?;
|
||||
Ok(())
|
||||
}
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum CliConvertTextureError{
|
||||
#[error("Io error {0:?}")]
|
||||
Io(#[from]std::io::Error),
|
||||
#[error("ConvertTexture error {0:?}")]
|
||||
ConvertTexture(#[from]ConvertTextureError),
|
||||
}
|
||||
|
||||
async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->AResult<()>{
|
||||
tokio::try_join!(
|
||||
tokio::fs::create_dir_all("downloaded_textures"),
|
||||
@@ -356,27 +196,19 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A
|
||||
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
|
||||
let send=send_assets.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=unique_assets(path.as_path()).await;
|
||||
let result=match tokio::fs::read(&path).await{
|
||||
Ok(data)=>super::load_dom(&data).map(|dom|get_unique_assets(&dom)).map_err(|e|format!("{e:?}")),
|
||||
Err(e)=>Err(format!("{e:?}")),
|
||||
};
|
||||
_=send.send(result).await;
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
});
|
||||
// download manager
|
||||
// insert into global unique assets guy
|
||||
// add to download queue if the asset is globally unique and does not already exist on disk
|
||||
let mut stats=Stats::default();
|
||||
let context=rbx_asset::cookie::Context::new(cookie);
|
||||
let mut globally_unique_assets=UniqueAssets::default();
|
||||
// pop a job = retry_queue.pop_front() or ingest(recv.recv().await)
|
||||
// SLOW MODE:
|
||||
// acquire all permits
|
||||
// drop all permits
|
||||
// pop one job
|
||||
// if it succeeds go into fast mode
|
||||
// FAST MODE:
|
||||
// acquire one permit
|
||||
// pop a job
|
||||
let download_thread=tokio::spawn(async move{
|
||||
while let Some(result)=recv_assets.recv().await{
|
||||
let unique_assets=match result{
|
||||
@@ -410,7 +242,7 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A
|
||||
SEM.add_permits(thread_limit);
|
||||
while let (Ok(permit),Some((asset_id,download_result)))=(SEM.acquire().await,recv_texture.recv().await){
|
||||
tokio::spawn(async move{
|
||||
let result=convert_texture(asset_id,download_result).await;
|
||||
let result=cli_convert_texture(asset_id,download_result).await;
|
||||
drop(permit);
|
||||
result.unwrap();
|
||||
});
|
||||
@@ -420,52 +252,7 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[expect(dead_code)]
|
||||
enum ConvertError{
|
||||
IO(std::io::Error),
|
||||
SNFMap(strafesnet_snf::map::Error),
|
||||
RobloxRead(strafesnet_rbx_loader::ReadError),
|
||||
RobloxLoad(strafesnet_rbx_loader::LoadError),
|
||||
}
|
||||
impl std::fmt::Display for ConvertError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ConvertError{}
|
||||
|
||||
struct Errors{
|
||||
script_errors:Vec<strafesnet_rbx_loader::RunnerError>,
|
||||
convert_errors:strafesnet_rbx_loader::RecoverableErrors,
|
||||
}
|
||||
|
||||
fn convert_to_snf(path:&Path,output_folder:PathBuf)->Result<Errors,ConvertError>{
|
||||
let entire_file=std::fs::read(path).map_err(ConvertError::IO)?;
|
||||
|
||||
let model=strafesnet_rbx_loader::read(
|
||||
entire_file.as_slice()
|
||||
).map_err(ConvertError::RobloxRead)?;
|
||||
|
||||
let mut place=strafesnet_rbx_loader::Place::from(model);
|
||||
let script_errors=place.run_scripts().unwrap_or_else(|e|vec![e]);
|
||||
|
||||
let (map,convert_errors)=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?;
|
||||
|
||||
let mut dest=output_folder;
|
||||
dest.push(path.file_stem().unwrap());
|
||||
dest.set_extension("snfm");
|
||||
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
|
||||
|
||||
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
|
||||
|
||||
Ok(Errors{
|
||||
script_errors,
|
||||
convert_errors,
|
||||
})
|
||||
}
|
||||
|
||||
async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->AResult<()>{
|
||||
async fn roblox_to_snf(paths:Vec<PathBuf>,output_folder:PathBuf)->AResult<()>{
|
||||
let start=std::time::Instant::now();
|
||||
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
@@ -477,17 +264,27 @@ async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->ARe
|
||||
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
|
||||
let output_folder=output_folder.clone();
|
||||
tokio::task::spawn_blocking(move||{
|
||||
let result=convert_to_snf(path.as_path(),output_folder);
|
||||
let result=std::fs::read(&path).and_then(|data|{
|
||||
super::load_dom(&data).map_err(|e|std::io::Error::other(e))
|
||||
.and_then(|dom|convert_to_snf(dom).map_err(|e|std::io::Error::other(e)))
|
||||
.and_then(|output|{
|
||||
let mut dest=output_folder;
|
||||
dest.push(path.file_stem().unwrap());
|
||||
dest.set_extension("snfm");
|
||||
std::fs::write(dest,&output.snf)?;
|
||||
Ok(output)
|
||||
})
|
||||
});
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(errors)=>{
|
||||
for error in errors.script_errors{
|
||||
Ok(output)=>{
|
||||
for error in output.script_errors{
|
||||
println!("Script error: {error}");
|
||||
}
|
||||
let error_count=errors.convert_errors.count();
|
||||
let error_count=output.convert_errors.count();
|
||||
if error_count!=0{
|
||||
println!("Error count: {error_count}");
|
||||
println!("Errors: {}",errors.convert_errors);
|
||||
println!("Errors: {}",output.convert_errors);
|
||||
}
|
||||
},
|
||||
Err(e)=>println!("Convert error: {e:?}"),
|
||||
@@ -499,3 +296,88 @@ async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->ARe
|
||||
println!("elapsed={:?}", start.elapsed());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum ConvertError{
|
||||
IO(std::io::Error),
|
||||
SNFMap(strafesnet_snf::map::Error),
|
||||
RobloxLoadMesh(super::loader::MeshError),
|
||||
RobloxLoadTexture(super::loader::TextureError),
|
||||
}
|
||||
impl std::fmt::Display for ConvertError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ConvertError{}
|
||||
|
||||
pub struct ConvertOutput{
|
||||
pub snf:Vec<u8>,
|
||||
pub script_errors:Vec<strafesnet_rbx_loader::RunnerError>,
|
||||
pub convert_errors:strafesnet_rbx_loader::RecoverableErrors,
|
||||
}
|
||||
|
||||
pub fn convert_to_snf(dom:rbx_dom_weak::WeakDom)->Result<ConvertOutput,ConvertError>{
|
||||
const FAILURE_MODE:LoadFailureMode=LoadFailureMode::DefaultToNone;
|
||||
|
||||
// run scripts
|
||||
let model=strafesnet_rbx_loader::Model::new(dom);
|
||||
|
||||
let mut place=strafesnet_rbx_loader::Place::from(model);
|
||||
let script_errors=place.run_scripts().unwrap_or_else(|e|vec![e]);
|
||||
|
||||
// convert
|
||||
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
|
||||
let mut mesh_deferred_loader=MeshDeferredLoader::new();
|
||||
|
||||
let map_step1=strafesnet_rbx_loader::rbx::convert(
|
||||
place.as_ref(),
|
||||
&mut texture_deferred_loader,
|
||||
&mut mesh_deferred_loader,
|
||||
);
|
||||
|
||||
let mut mesh_loader=super::loader::MeshLoader::new();
|
||||
let meshpart_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,FAILURE_MODE).map_err(ConvertError::RobloxLoadMesh)?;
|
||||
|
||||
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(meshpart_meshes);
|
||||
|
||||
let mut texture_loader=super::loader::TextureLoader::new();
|
||||
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,FAILURE_MODE).map_err(ConvertError::RobloxLoadTexture)?;
|
||||
|
||||
let (map,convert_errors)=map_step2.add_render_configs_and_textures(render_configs);
|
||||
|
||||
let mut snf_buf=Vec::new();
|
||||
strafesnet_snf::map::write_map(Cursor::new(&mut snf_buf),map).map_err(ConvertError::SNFMap)?;
|
||||
|
||||
Ok(ConvertOutput{
|
||||
snf:snf_buf,
|
||||
script_errors,
|
||||
convert_errors,
|
||||
})
|
||||
}
|
||||
|
||||
async fn extract_union_data(input_file:PathBuf,output_folder:PathBuf)->AResult<()>{
|
||||
let data=tokio::fs::read(&input_file).await?;
|
||||
|
||||
let dom=rbx_binary::from_reader(Cursor::new(data))?;
|
||||
let &[referent]=dom.root().children()else{
|
||||
panic!("Expected one child");
|
||||
};
|
||||
let Some(instance)=dom.get_by_ref(referent)else{
|
||||
panic!("Missing instance");
|
||||
};
|
||||
let output_file=output_folder.join(input_file.file_stem().unwrap());
|
||||
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&rbx_dom_weak::ustr("PhysicsData")){
|
||||
let mut physics_data_path=output_file.clone();
|
||||
physics_data_path.set_extension("physicsdata");
|
||||
tokio::fs::write(physics_data_path,data).await?;
|
||||
}
|
||||
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&rbx_dom_weak::ustr("MeshData")){
|
||||
let mut mesh_data_path=output_file.clone();
|
||||
mesh_data_path.set_extension("meshdata");
|
||||
tokio::fs::write(mesh_data_path,data).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::io::Read;
|
||||
use rbxassetid::{RobloxAssetId,RobloxAssetIdParseErr};
|
||||
use strafesnet_common::model::Mesh;
|
||||
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
|
||||
|
||||
use crate::data::RobloxMeshBytes;
|
||||
use crate::rbx::RobloxPartDescription;
|
||||
use strafesnet_rbx_loader::mesh::{MeshIndex,MeshType,MeshWithSize};
|
||||
|
||||
// disallow non-static lifetimes
|
||||
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
|
||||
@@ -18,6 +16,7 @@ fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<Vec<u8>,std::io::E
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum TextureError{
|
||||
Io(std::io::Error),
|
||||
@@ -50,7 +49,7 @@ impl Loader for TextureLoader{
|
||||
type Error=TextureError;
|
||||
type Index<'a>=&'a str;
|
||||
type Resource=Texture;
|
||||
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
|
||||
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
|
||||
let RobloxAssetId(asset_id)=index.parse()?;
|
||||
let file_name=format!("textures/{}.dds",asset_id);
|
||||
let data=read_entire_file(file_name)?;
|
||||
@@ -58,12 +57,13 @@ impl Loader for TextureLoader{
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum MeshError{
|
||||
Io(std::io::Error),
|
||||
RobloxAssetIdParse(RobloxAssetIdParseErr),
|
||||
Mesh(crate::mesh::Error),
|
||||
Union(crate::union::Error),
|
||||
Mesh(strafesnet_rbx_loader::mesh::Error),
|
||||
Union(strafesnet_rbx_loader::union::Error),
|
||||
DecodeBinary(rbx_binary::DecodeError),
|
||||
OneChildPolicy,
|
||||
MissingInstance,
|
||||
@@ -84,13 +84,13 @@ impl From<RobloxAssetIdParseErr> for MeshError{
|
||||
Self::RobloxAssetIdParse(value)
|
||||
}
|
||||
}
|
||||
impl From<crate::mesh::Error> for MeshError{
|
||||
fn from(value:crate::mesh::Error)->Self{
|
||||
impl From<strafesnet_rbx_loader::mesh::Error> for MeshError{
|
||||
fn from(value:strafesnet_rbx_loader::mesh::Error)->Self{
|
||||
Self::Mesh(value)
|
||||
}
|
||||
}
|
||||
impl From<crate::union::Error> for MeshError{
|
||||
fn from(value:crate::union::Error)->Self{
|
||||
impl From<strafesnet_rbx_loader::union::Error> for MeshError{
|
||||
fn from(value:strafesnet_rbx_loader::union::Error)->Self{
|
||||
Self::Union(value)
|
||||
}
|
||||
}
|
||||
@@ -100,53 +100,6 @@ impl From<rbx_binary::DecodeError> for MeshError{
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash,Eq,PartialEq)]
|
||||
pub enum MeshType<'a>{
|
||||
FileMesh,
|
||||
Union{
|
||||
mesh_data:&'a [u8],
|
||||
physics_data:&'a [u8],
|
||||
size_float_bits:[u32;3],
|
||||
part_texture_description:RobloxPartDescription,
|
||||
},
|
||||
}
|
||||
#[derive(Hash,Eq,PartialEq)]
|
||||
pub struct MeshIndex<'a>{
|
||||
mesh_type:MeshType<'a>,
|
||||
content:&'a str,
|
||||
}
|
||||
impl MeshIndex<'_>{
|
||||
pub fn file_mesh(content:&str)->MeshIndex<'_>{
|
||||
MeshIndex{
|
||||
mesh_type:MeshType::FileMesh,
|
||||
content,
|
||||
}
|
||||
}
|
||||
pub fn union<'a>(
|
||||
content:&'a str,
|
||||
mesh_data:&'a [u8],
|
||||
physics_data:&'a [u8],
|
||||
size:&rbx_dom_weak::types::Vector3,
|
||||
part_texture_description:RobloxPartDescription,
|
||||
)->MeshIndex<'a>{
|
||||
MeshIndex{
|
||||
mesh_type:MeshType::Union{
|
||||
mesh_data,
|
||||
physics_data,
|
||||
size_float_bits:[size.x.to_bits(),size.y.to_bits(),size.z.to_bits()],
|
||||
part_texture_description,
|
||||
},
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MeshWithSize{
|
||||
pub(crate) mesh:Mesh,
|
||||
pub(crate) size:strafesnet_common::integer::Planar64Vec3,
|
||||
}
|
||||
|
||||
pub struct MeshLoader;
|
||||
impl MeshLoader{
|
||||
pub fn new()->Self{
|
||||
@@ -157,17 +110,17 @@ impl Loader for MeshLoader{
|
||||
type Error=MeshError;
|
||||
type Index<'a>=MeshIndex<'a>;
|
||||
type Resource=MeshWithSize;
|
||||
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
|
||||
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
|
||||
let mesh=match index.mesh_type{
|
||||
MeshType::FileMesh=>{
|
||||
let RobloxAssetId(asset_id)=index.content.parse()?;
|
||||
let file_name=format!("meshes/{}",asset_id);
|
||||
let data=read_entire_file(file_name)?;
|
||||
crate::mesh::convert(RobloxMeshBytes::new(data))?
|
||||
strafesnet_rbx_loader::mesh::convert(&data)?
|
||||
},
|
||||
MeshType::Union{mut physics_data,mut mesh_data,size_float_bits,part_texture_description}=>{
|
||||
// decode asset
|
||||
let size=glam::Vec3::from_array(size_float_bits.map(f32::from_bits));
|
||||
let size=size_float_bits.map(f32::from_bits).into();
|
||||
if !index.content.is_empty()&&(physics_data.is_empty()||mesh_data.is_empty()){
|
||||
let RobloxAssetId(asset_id)=index.content.parse()?;
|
||||
let file_name=format!("unions/{}",asset_id);
|
||||
@@ -189,9 +142,9 @@ impl Loader for MeshLoader{
|
||||
mesh_data=data.as_ref();
|
||||
}
|
||||
}
|
||||
crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
|
||||
strafesnet_rbx_loader::union::convert(physics_data,mesh_data,size,part_texture_description)?
|
||||
}else{
|
||||
crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
|
||||
strafesnet_rbx_loader::union::convert(physics_data,mesh_data,size,part_texture_description)?
|
||||
}
|
||||
},
|
||||
};
|
||||
210
map-tool/src/roblox/mod.rs
Normal file
210
map-tool/src/roblox/mod.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
#[cfg(feature="cli")]
|
||||
mod cli;
|
||||
#[cfg(feature="cli")]
|
||||
pub use cli::Commands;
|
||||
#[cfg(feature="cli")]
|
||||
mod loader;
|
||||
|
||||
use std::io::{Cursor,Read,Seek};
|
||||
use std::collections::HashSet;
|
||||
pub use rbxassetid::RobloxAssetId;
|
||||
use rbx_dom_weak::Instance;
|
||||
|
||||
// disallow non-static lifetimes
|
||||
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
|
||||
rbx_dom_weak::ustr(s)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoadDomError{
|
||||
IO(std::io::Error),
|
||||
Binary(rbx_binary::DecodeError),
|
||||
Xml(rbx_xml::DecodeError),
|
||||
UnknownFormat,
|
||||
}
|
||||
impl std::fmt::Display for LoadDomError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for LoadDomError{}
|
||||
pub fn load_dom(data:&[u8])->Result<rbx_dom_weak::WeakDom,LoadDomError>{
|
||||
let mut input=Cursor::new(data);
|
||||
let mut first_8=[0u8;8];
|
||||
input.read_exact(&mut first_8).map_err(LoadDomError::IO)?;
|
||||
input.rewind().map_err(LoadDomError::IO)?;
|
||||
match &first_8{
|
||||
b"<roblox!"=>rbx_binary::from_reader(input).map_err(LoadDomError::Binary),
|
||||
b"<roblox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(LoadDomError::Xml),
|
||||
_=>Err(LoadDomError::UnknownFormat),
|
||||
}
|
||||
}
|
||||
|
||||
fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
|
||||
let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(&static_ustr(property))else{
|
||||
println!("property={} does not exist for class={}",property,object.class.as_str());
|
||||
return;
|
||||
};
|
||||
let rbx_dom_weak::types::ContentType::Uri(uri)=content.value()else{
|
||||
println!("ContentType is not Uri");
|
||||
return;
|
||||
};
|
||||
let Ok(asset_id)=uri.parse()else{
|
||||
println!("Content failed to parse into AssetID: {:?}",content);
|
||||
return;
|
||||
};
|
||||
content_list.insert(asset_id);
|
||||
}
|
||||
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
|
||||
let Some(rbx_dom_weak::types::Variant::ContentId(content))=object.properties.get(&static_ustr(property))else{
|
||||
println!("property={} does not exist for class={}",property,object.class.as_str());
|
||||
return;
|
||||
};
|
||||
let Ok(asset_id)=content.as_str().parse()else{
|
||||
println!("Content failed to parse into AssetID: {:?}",content);
|
||||
return;
|
||||
};
|
||||
content_list.insert(asset_id);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UniqueAssets{
|
||||
pub meshes:HashSet<RobloxAssetId>,
|
||||
pub unions:HashSet<RobloxAssetId>,
|
||||
pub textures:HashSet<RobloxAssetId>,
|
||||
}
|
||||
impl UniqueAssets{
|
||||
fn collect(&mut self,object:&Instance){
|
||||
match object.class.as_str(){
|
||||
"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"Decal"=>accumulate_content(&mut self.textures,object,"TextureContent"),
|
||||
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
|
||||
"MeshPart"=>{
|
||||
accumulate_content(&mut self.textures,object,"TextureContent");
|
||||
accumulate_content(&mut self.meshes,object,"MeshContent");
|
||||
},
|
||||
"ParticleEmitter"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"SpecialMesh"=>accumulate_content_id(&mut self.meshes,object,"MeshId"),
|
||||
"Sky"=>{
|
||||
accumulate_content_id(&mut self.textures,object,"MoonTextureId");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxBk");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxDn");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxFt");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxLf");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxRt");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxUp");
|
||||
accumulate_content_id(&mut self.textures,object,"SunTextureId");
|
||||
},
|
||||
"SurfaceAppearance"=>{
|
||||
accumulate_content(&mut self.textures,object,"ColorMapContent");
|
||||
accumulate_content(&mut self.textures,object,"MetalnessMapContent");
|
||||
accumulate_content(&mut self.textures,object,"NormalMapContent");
|
||||
accumulate_content(&mut self.textures,object,"RoughnessMapContent");
|
||||
}
|
||||
"Texture"=>accumulate_content(&mut self.textures,object,"TextureContent"),
|
||||
"UnionOperation"=>accumulate_content_id(&mut self.unions,object,"AssetId"),
|
||||
_=>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_unique_assets(dom:&rbx_dom_weak::WeakDom)->UniqueAssets{
|
||||
let mut assets=UniqueAssets::default();
|
||||
for object in dom.descendants(){
|
||||
assets.collect(object);
|
||||
}
|
||||
assets
|
||||
}
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
pub enum ConvertTextureError{
|
||||
#[error("Image error {0:?}")]
|
||||
Image(#[from]image::ImageError),
|
||||
#[error("DDS encode error {0:?}")]
|
||||
DDSEncode(#[from]image_dds::error::SurfaceError),
|
||||
#[error("DDS create error {0:?}")]
|
||||
DDS(#[from]image_dds::CreateDdsError),
|
||||
#[error("DDS write error {0:?}")]
|
||||
DDSWrite(#[from]image_dds::ddsfile::Error),
|
||||
}
|
||||
pub fn convert_texture_to_dds(data:&[u8])->Result<Vec<u8>,ConvertTextureError>{
|
||||
let image=image::load_from_memory(data)?.to_rgba8();
|
||||
|
||||
let format=if image.width()%4!=0||image.height()%4!=0{
|
||||
image_dds::ImageFormat::Rgba8UnormSrgb
|
||||
}else{
|
||||
image_dds::ImageFormat::BC7RgbaUnormSrgb
|
||||
};
|
||||
|
||||
let dds=image_dds::SurfaceRgba8{
|
||||
width:image.width(),
|
||||
height:image.height(),
|
||||
depth:1,
|
||||
layers:1,
|
||||
mipmaps:1,
|
||||
data:image.as_raw(),
|
||||
}.encode(format,image_dds::Quality::Slow,image_dds::Mipmaps::GeneratedAutomatic)?.to_dds()?;
|
||||
|
||||
let mut buf=Vec::new();
|
||||
dds.write(&mut Cursor::new(&mut buf))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DownloadAssetError{
|
||||
Get(rbx_asset::cookie::GetError),
|
||||
IO(std::io::Error),
|
||||
}
|
||||
impl std::fmt::Display for DownloadAssetError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for DownloadAssetError{}
|
||||
|
||||
pub async fn download_asset(context:&rbx_asset::cookie::Context,asset_id:u64)->Result<Vec<u8>,DownloadAssetError>{
|
||||
let data=context.get_asset(rbx_asset::cookie::GetAssetRequest{
|
||||
asset_id,
|
||||
version:None,
|
||||
}).await.map_err(DownloadAssetError::Get)?;
|
||||
Ok(data.to_vec().map_err(DownloadAssetError::IO)?)
|
||||
}
|
||||
|
||||
pub(crate) const BACKOFF_MUL:f32=1.3956124250860895286;//exp(1/3)
|
||||
const RETRY_LIMIT:u32=12;
|
||||
|
||||
pub enum DownloadRetryResult{
|
||||
Ok(Vec<u8>),
|
||||
TimedOut,
|
||||
}
|
||||
|
||||
pub async fn download_asset_retry(context:&rbx_asset::cookie::Context,asset_id:u64)->Result<DownloadRetryResult,DownloadAssetError>{
|
||||
let mut retry=0u32;
|
||||
let mut backoff=1000f32;
|
||||
loop{
|
||||
match context.get_asset(rbx_asset::cookie::GetAssetRequest{
|
||||
asset_id,
|
||||
version:None,
|
||||
}).await{
|
||||
Ok(data)=>{
|
||||
let bytes=data.to_vec().map_err(DownloadAssetError::IO)?;
|
||||
break Ok(DownloadRetryResult::Ok(bytes));
|
||||
},
|
||||
Err(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::Details{status_code,url_and_body}))=>{
|
||||
if status_code.as_u16()==429{
|
||||
if retry==RETRY_LIMIT{
|
||||
break Ok(DownloadRetryResult::TimedOut);
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await;
|
||||
backoff*=BACKOFF_MUL;
|
||||
retry+=1;
|
||||
}else{
|
||||
break Err(DownloadAssetError::Get(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::Details{status_code,url_and_body})));
|
||||
}
|
||||
},
|
||||
Err(e)=>{
|
||||
break Err(DownloadAssetError::Get(e));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,514 +0,0 @@
|
||||
use std::path::{Path,PathBuf};
|
||||
use std::borrow::Cow;
|
||||
use clap::{Args,Subcommand};
|
||||
use anyhow::Result as AResult;
|
||||
use futures::StreamExt;
|
||||
use strafesnet_bsp_loader::loader::BspFinder;
|
||||
use strafesnet_deferred_loader::loader::Loader;
|
||||
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
|
||||
use vbsp_entities_css::Entity;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands{
|
||||
SourceToSNF(SourceToSNFSubcommand),
|
||||
ExtractTextures(ExtractTexturesSubcommand),
|
||||
VPKContents(VPKContentsSubcommand),
|
||||
BSPContents(BSPContentsSubcommand),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct SourceToSNFSubcommand {
|
||||
#[arg(long)]
|
||||
output_folder:PathBuf,
|
||||
#[arg(required=true)]
|
||||
input_files:Vec<PathBuf>,
|
||||
#[arg(long)]
|
||||
vpks:Vec<PathBuf>,
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct ExtractTexturesSubcommand{
|
||||
#[arg(required=true)]
|
||||
bsp_files:Vec<PathBuf>,
|
||||
#[arg(long)]
|
||||
vpks:Vec<PathBuf>,
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct VPKContentsSubcommand {
|
||||
#[arg(long)]
|
||||
input_file:PathBuf,
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct BSPContentsSubcommand {
|
||||
#[arg(long)]
|
||||
input_file:PathBuf,
|
||||
}
|
||||
|
||||
impl Commands{
|
||||
pub async fn run(self)->AResult<()>{
|
||||
match self{
|
||||
Commands::SourceToSNF(subcommand)=>source_to_snf(subcommand.input_files,subcommand.output_folder,subcommand.vpks).await,
|
||||
Commands::ExtractTextures(subcommand)=>extract_textures(subcommand.bsp_files,subcommand.vpks).await,
|
||||
Commands::VPKContents(subcommand)=>vpk_contents(subcommand.input_file),
|
||||
Commands::BSPContents(subcommand)=>bsp_contents(subcommand.input_file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum VMTContent{
|
||||
VMT(String),
|
||||
VTF(String),
|
||||
Patch(vmt_parser::material::PatchMaterial),
|
||||
Unsupported,//don't want to deal with whatever vmt variant
|
||||
Unresolved,//could not locate a texture because of vmt content
|
||||
}
|
||||
impl VMTContent{
|
||||
fn vtf(opt:Option<String>)->Self{
|
||||
match opt{
|
||||
Some(s)=>Self::VTF(s),
|
||||
None=>Self::Unresolved,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_some_texture(material:vmt_parser::material::Material)->VMTContent{
|
||||
//just grab some texture from somewhere for now
|
||||
match material{
|
||||
vmt_parser::material::Material::LightMappedGeneric(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::VertexLitGeneric(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),//this just dies if there is none
|
||||
vmt_parser::material::Material::VertexLitGenericDx6(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),
|
||||
vmt_parser::material::Material::UnlitGeneric(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::UnlitTwoTexture(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Water(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::WorldVertexTransition(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::EyeRefract(mat)=>VMTContent::vtf(Some(mat.cornea_texture)),
|
||||
vmt_parser::material::Material::SubRect(mat)=>VMTContent::VMT(mat.material),//recursive
|
||||
vmt_parser::material::Material::Sprite(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::SpriteCard(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Cable(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Refract(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Modulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::DecalModulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Sky(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Replacements(_mat)=>VMTContent::Unsupported,
|
||||
vmt_parser::material::Material::Patch(mat)=>VMTContent::Patch(mat),
|
||||
_=>unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum GetVMTError{
|
||||
#[error("Bsp error {0:?}")]
|
||||
Bsp(#[from]vbsp::BspError),
|
||||
#[error("Utf8 error {0:?}")]
|
||||
Utf8(#[from]std::str::Utf8Error),
|
||||
#[error("Vdf error {0:?}")]
|
||||
Vdf(#[from]vmt_parser::VdfError),
|
||||
#[error("Vmt not found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
fn get_vmt(finder:BspFinder,search_name:&str)->Result<vmt_parser::material::Material,GetVMTError>{
|
||||
let vmt_data=finder.find(search_name)?.ok_or(GetVMTError::NotFound)?;
|
||||
//decode vmt and then write
|
||||
let vmt_str=core::str::from_utf8(&vmt_data)?;
|
||||
let material=vmt_parser::from_str(vmt_str)?;
|
||||
//println!("vmt material={:?}",material);
|
||||
Ok(material)
|
||||
}
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum LoadVMTError{
|
||||
#[error("Bsp error {0:?}")]
|
||||
Bsp(#[from]vbsp::BspError),
|
||||
#[error("GetVMT error {0:?}")]
|
||||
GetVMT(#[from]GetVMTError),
|
||||
#[error("FromUtf8 error {0:?}")]
|
||||
FromUtf8(#[from]std::string::FromUtf8Error),
|
||||
#[error("Vdf error {0:?}")]
|
||||
Vdf(#[from]vmt_parser::VdfError),
|
||||
#[error("Vmt unsupported")]
|
||||
Unsupported,
|
||||
#[error("Vmt unresolved")]
|
||||
Unresolved,
|
||||
#[error("Vmt not found")]
|
||||
NotFound,
|
||||
}
|
||||
fn recursive_vmt_loader<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,material:vmt_parser::material::Material)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
|
||||
where
|
||||
'bsp:'a,
|
||||
'vpk:'a,
|
||||
{
|
||||
match get_some_texture(material){
|
||||
VMTContent::VMT(mut s)=>{
|
||||
s.make_ascii_lowercase();
|
||||
recursive_vmt_loader(finder,get_vmt(finder,&s)?)
|
||||
},
|
||||
VMTContent::VTF(s)=>{
|
||||
let mut texture_file_name=PathBuf::from("materials");
|
||||
texture_file_name.push(s);
|
||||
texture_file_name.set_extension("vtf");
|
||||
texture_file_name.as_mut_os_str().make_ascii_lowercase();
|
||||
Ok(finder.find(texture_file_name.to_str().unwrap())?)
|
||||
},
|
||||
VMTContent::Patch(mat)=>recursive_vmt_loader(finder,
|
||||
mat.resolve(|search_name|{
|
||||
let name_lowercase=search_name.to_lowercase();
|
||||
match finder.find(&name_lowercase)?{
|
||||
Some(bytes)=>Ok(String::from_utf8(bytes.into_owned())?),
|
||||
None=>Err(LoadVMTError::NotFound),
|
||||
}
|
||||
})?
|
||||
),
|
||||
VMTContent::Unsupported=>Err(LoadVMTError::Unsupported),
|
||||
VMTContent::Unresolved=>Err(LoadVMTError::Unresolved),
|
||||
}
|
||||
}
|
||||
fn load_texture<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,texture_name:&str)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
|
||||
where
|
||||
'bsp:'a,
|
||||
'vpk:'a,
|
||||
{
|
||||
let mut texture_file_name=PathBuf::from("materials");
|
||||
//lower case
|
||||
texture_file_name.push(texture_name);
|
||||
texture_file_name.as_mut_os_str().make_ascii_lowercase();
|
||||
//remove stem and search for both vtf and vmt files
|
||||
let stem=texture_file_name.file_stem().unwrap().to_owned();
|
||||
texture_file_name.pop();
|
||||
texture_file_name.push(stem);
|
||||
if let Some(stuff)=finder.find(texture_file_name.to_str().unwrap())?{
|
||||
return Ok(Some(stuff));
|
||||
}
|
||||
|
||||
// search for both vmt,vtf
|
||||
let mut texture_file_name_vmt=texture_file_name.clone();
|
||||
texture_file_name_vmt.set_extension("vmt");
|
||||
|
||||
let get_vmt_result=get_vmt(finder,texture_file_name_vmt.to_str().unwrap());
|
||||
match get_vmt_result{
|
||||
Ok(material)=>{
|
||||
let vmt_result=recursive_vmt_loader(finder,material);
|
||||
match vmt_result{
|
||||
Ok(Some(stuff))=>return Ok(Some(stuff)),
|
||||
Ok(None)
|
||||
|Err(LoadVMTError::NotFound)=>(),
|
||||
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
|
||||
Err(e)=>return Err(e),
|
||||
}
|
||||
}
|
||||
|Err(GetVMTError::NotFound)=>(),
|
||||
Err(e)=>Err(e)?,
|
||||
}
|
||||
|
||||
// try looking for vtf
|
||||
let mut texture_file_name_vtf=texture_file_name.clone();
|
||||
texture_file_name_vtf.set_extension("vtf");
|
||||
|
||||
let get_vtf_result=get_vmt(finder,texture_file_name_vtf.to_str().unwrap());
|
||||
match get_vtf_result{
|
||||
Ok(material)=>{
|
||||
let vtf_result=recursive_vmt_loader(finder,material);
|
||||
match vtf_result{
|
||||
Ok(Some(stuff))=>return Ok(Some(stuff)),
|
||||
Ok(None)
|
||||
|Err(LoadVMTError::NotFound)=>(),
|
||||
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
|
||||
Err(e)=>return Err(e),
|
||||
}
|
||||
}
|
||||
|Err(GetVMTError::NotFound)=>(),
|
||||
Err(e)=>Err(e)?,
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum ExtractTextureError{
|
||||
#[error("Io error {0:?}")]
|
||||
Io(#[from]std::io::Error),
|
||||
#[error("Bsp error {0:?}")]
|
||||
Bsp(#[from]vbsp::BspError),
|
||||
#[error("MeshLoad error {0:?}")]
|
||||
MeshLoad(#[from]strafesnet_bsp_loader::loader::MeshError),
|
||||
#[error("Load VMT error {0:?}")]
|
||||
LoadVMT(#[from]LoadVMTError),
|
||||
}
|
||||
async fn gimme_them_textures(path:&Path,vpk_list:&[strafesnet_bsp_loader::Vpk],send_texture:tokio::sync::mpsc::Sender<(Vec<u8>,String)>)->Result<(),ExtractTextureError>{
|
||||
let bsp=vbsp::Bsp::read(tokio::fs::read(path).await?.as_ref())?;
|
||||
let loader_bsp=strafesnet_bsp_loader::Bsp::new(bsp);
|
||||
let bsp=loader_bsp.as_ref();
|
||||
|
||||
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
|
||||
for texture in bsp.textures(){
|
||||
texture_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(texture.name())));
|
||||
}
|
||||
|
||||
let mut mesh_deferred_loader=MeshDeferredLoader::new();
|
||||
for name in &bsp.static_props.dict.name{
|
||||
mesh_deferred_loader.acquire_mesh_id(name.as_str());
|
||||
}
|
||||
|
||||
for raw_ent in &bsp.entities{
|
||||
let model=match raw_ent.parse(){
|
||||
Ok(Entity::Cycler(brush))=>brush.model,
|
||||
Ok(Entity::EnvSprite(brush))=>brush.model,
|
||||
Ok(Entity::FuncBreakable(brush))=>brush.model,
|
||||
Ok(Entity::FuncBrush(brush))=>brush.model,
|
||||
Ok(Entity::FuncButton(brush))=>brush.model,
|
||||
Ok(Entity::FuncDoor(brush))=>brush.model,
|
||||
Ok(Entity::FuncDoorRotating(brush))=>brush.model,
|
||||
Ok(Entity::FuncIllusionary(brush))=>brush.model,
|
||||
Ok(Entity::FuncMonitor(brush))=>brush.model,
|
||||
Ok(Entity::FuncMovelinear(brush))=>brush.model,
|
||||
Ok(Entity::FuncPhysbox(brush))=>brush.model,
|
||||
Ok(Entity::FuncPhysboxMultiplayer(brush))=>brush.model,
|
||||
Ok(Entity::FuncRotButton(brush))=>brush.model,
|
||||
Ok(Entity::FuncRotating(brush))=>brush.model,
|
||||
Ok(Entity::FuncTracktrain(brush))=>brush.model,
|
||||
Ok(Entity::FuncTrain(brush))=>brush.model,
|
||||
Ok(Entity::FuncWall(brush))=>brush.model,
|
||||
Ok(Entity::FuncWallToggle(brush))=>brush.model,
|
||||
Ok(Entity::FuncWaterAnalog(brush))=>brush.model,
|
||||
Ok(Entity::PropDoorRotating(brush))=>brush.model,
|
||||
Ok(Entity::PropDynamic(brush))=>brush.model,
|
||||
Ok(Entity::PropDynamicOverride(brush))=>brush.model,
|
||||
Ok(Entity::PropPhysics(brush))=>brush.model,
|
||||
Ok(Entity::PropPhysicsMultiplayer(brush))=>brush.model,
|
||||
Ok(Entity::PropPhysicsOverride(brush))=>brush.model,
|
||||
Ok(Entity::PropRagdoll(brush))=>brush.model,
|
||||
Ok(Entity::TriggerGravity(brush))=>brush.model,
|
||||
Ok(Entity::TriggerHurt(brush))=>brush.model,
|
||||
Ok(Entity::TriggerLook(brush))=>brush.model,
|
||||
Ok(Entity::TriggerMultiple(brush))=>brush.model.unwrap_or_default(),
|
||||
Ok(Entity::TriggerOnce(brush))=>brush.model,
|
||||
Ok(Entity::TriggerProximity(brush))=>brush.model,
|
||||
Ok(Entity::TriggerPush(brush))=>brush.model,
|
||||
Ok(Entity::TriggerSoundscape(brush))=>brush.model,
|
||||
Ok(Entity::TriggerTeleport(brush))=>brush.model.unwrap_or_default(),
|
||||
Ok(Entity::TriggerVphysicsMotion(brush))=>brush.model,
|
||||
Ok(Entity::TriggerWind(brush))=>brush.model,
|
||||
_=>continue,
|
||||
};
|
||||
match model.chars().next(){
|
||||
Some('*')=>(),
|
||||
_=>{
|
||||
mesh_deferred_loader.acquire_mesh_id(model);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let finder=BspFinder{
|
||||
bsp:&loader_bsp,
|
||||
vpks:vpk_list
|
||||
};
|
||||
|
||||
let mut mesh_loader=strafesnet_bsp_loader::loader::ModelLoader::new(finder);
|
||||
// load models and collect requested textures
|
||||
for model_path in mesh_deferred_loader.into_indices(){
|
||||
let model:vmdl::Model=match mesh_loader.load(model_path){
|
||||
Ok(model)=>model,
|
||||
Err(e)=>{
|
||||
println!("Model={model_path} Load model error: {e}");
|
||||
continue;
|
||||
},
|
||||
};
|
||||
for texture in model.textures(){
|
||||
for search_path in &texture.search_paths{
|
||||
let mut path=PathBuf::from(search_path.as_str());
|
||||
path.push(texture.name.as_str());
|
||||
let path=path.to_str().unwrap().to_owned();
|
||||
texture_deferred_loader.acquire_render_config_id(Some(Cow::Owned(path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for texture_path in texture_deferred_loader.into_indices(){
|
||||
match load_texture(finder,&texture_path){
|
||||
Ok(Some(texture))=>send_texture.send(
|
||||
(texture.into_owned(),texture_path.into_owned())
|
||||
).await.unwrap(),
|
||||
Ok(None)=>(),
|
||||
Err(e)=>println!("Texture={texture_path} Load error: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum ConvertTextureError{
|
||||
#[error("Bsp error {0:?}")]
|
||||
Bsp(#[from]vbsp::BspError),
|
||||
#[error("Vtf error {0:?}")]
|
||||
Vtf(#[from]vtf::Error),
|
||||
#[error("DDS create error {0:?}")]
|
||||
DDS(#[from]image_dds::CreateDdsError),
|
||||
#[error("DDS write error {0:?}")]
|
||||
DDSWrite(#[from]image_dds::ddsfile::Error),
|
||||
#[error("Io error {0:?}")]
|
||||
Io(#[from]std::io::Error),
|
||||
}
|
||||
|
||||
async fn convert_texture(texture:Vec<u8>,write_file_name:impl AsRef<Path>)->Result<(),ConvertTextureError>{
|
||||
let image=vtf::from_bytes(&texture)?.highres_image.decode(0)?.to_rgba8();
|
||||
|
||||
let format=if image.width()%4!=0||image.height()%4!=0{
|
||||
image_dds::ImageFormat::Rgba8UnormSrgb
|
||||
}else{
|
||||
image_dds::ImageFormat::BC7RgbaUnormSrgb
|
||||
};
|
||||
//this fails if the image dimensions are not a multiple of 4
|
||||
let dds = image_dds::dds_from_image(
|
||||
&image,
|
||||
format,
|
||||
image_dds::Quality::Slow,
|
||||
image_dds::Mipmaps::GeneratedAutomatic,
|
||||
)?;
|
||||
|
||||
//write dds
|
||||
let mut dest=PathBuf::from("textures");
|
||||
dest.push(write_file_name);
|
||||
dest.set_extension("dds");
|
||||
std::fs::create_dir_all(dest.parent().unwrap())?;
|
||||
let mut writer=std::io::BufWriter::new(std::fs::File::create(dest)?);
|
||||
dds.write(&mut writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_vpks(vpk_paths:Vec<PathBuf>,thread_limit:usize)->Vec<strafesnet_bsp_loader::Vpk>{
|
||||
futures::stream::iter(vpk_paths).map(|vpk_path|async{
|
||||
// idk why it doesn't want to pass out the errors but this is fatal anyways
|
||||
tokio::task::spawn_blocking(move||Ok::<_,vpk::Error>(strafesnet_bsp_loader::Vpk::new(vpk::VPK::read(&vpk_path)?))).await.unwrap().unwrap()
|
||||
})
|
||||
.buffer_unordered(thread_limit)
|
||||
.collect().await
|
||||
}
|
||||
|
||||
async fn extract_textures(paths:Vec<PathBuf>,vpk_paths:Vec<PathBuf>)->AResult<()>{
|
||||
tokio::try_join!(
|
||||
tokio::fs::create_dir_all("extracted_textures"),
|
||||
tokio::fs::create_dir_all("textures"),
|
||||
tokio::fs::create_dir_all("meshes"),
|
||||
)?;
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
|
||||
// load vpk list and leak for static lifetime
|
||||
let vpk_list:&[strafesnet_bsp_loader::Vpk]=read_vpks(vpk_paths,thread_limit).await.leak();
|
||||
|
||||
let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
|
||||
let mut it=paths.into_iter();
|
||||
let extract_thread=tokio::spawn(async move{
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
|
||||
let send=send_texture.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=gimme_them_textures(&path,vpk_list,send).await;
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Map={path:?} Decode error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// convert images
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
while let (Ok(permit),Some((data,dest)))=(SEM.acquire().await,recv_texture.recv().await){
|
||||
// TODO: dedup dest?
|
||||
tokio::spawn(async move{
|
||||
let result=convert_texture(data,dest).await;
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Convert error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
extract_thread.await?;
|
||||
_=SEM.acquire_many(thread_limit as u32).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn vpk_contents(vpk_path:PathBuf)->AResult<()>{
|
||||
let vpk_index=vpk::VPK::read(&vpk_path)?;
|
||||
for (label,entry) in vpk_index.tree.into_iter(){
|
||||
println!("vpk label={} entry={:?}",label,entry);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bsp_contents(path:PathBuf)->AResult<()>{
|
||||
let bsp=vbsp::Bsp::read(std::fs::read(path)?.as_ref())?;
|
||||
for file_name in bsp.pack.into_zip().into_inner().unwrap().file_names(){
|
||||
println!("file_name={:?}",file_name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[expect(dead_code)]
|
||||
enum ConvertError{
|
||||
IO(std::io::Error),
|
||||
SNFMap(strafesnet_snf::map::Error),
|
||||
BspRead(strafesnet_bsp_loader::ReadError),
|
||||
BspLoad(strafesnet_bsp_loader::LoadError),
|
||||
}
|
||||
impl std::fmt::Display for ConvertError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ConvertError{}
|
||||
|
||||
async fn convert_to_snf(path:&Path,vpk_list:&[strafesnet_bsp_loader::Vpk],output_folder:PathBuf)->AResult<()>{
|
||||
let entire_file=tokio::fs::read(path).await?;
|
||||
|
||||
let bsp=strafesnet_bsp_loader::read(
|
||||
std::io::Cursor::new(entire_file)
|
||||
).map_err(ConvertError::BspRead)?;
|
||||
|
||||
let map=bsp.to_snf(LoadFailureMode::DefaultToNone,vpk_list).map_err(ConvertError::BspLoad)?;
|
||||
|
||||
let mut dest=output_folder;
|
||||
dest.push(path.file_stem().unwrap());
|
||||
dest.set_extension("snfm");
|
||||
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
|
||||
|
||||
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
async fn source_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf,vpk_paths:Vec<PathBuf>)->AResult<()>{
|
||||
let start=std::time::Instant::now();
|
||||
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
|
||||
// load vpk list and leak for static lifetime
|
||||
let vpk_list:&[strafesnet_bsp_loader::Vpk]=read_vpks(vpk_paths,thread_limit).await.leak();
|
||||
|
||||
let mut it=paths.into_iter();
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
|
||||
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
|
||||
let output_folder=output_folder.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=convert_to_snf(path.as_path(),vpk_list,output_folder).await;
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Convert error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
|
||||
|
||||
println!("elapsed={:?}", start.elapsed());
|
||||
Ok(())
|
||||
}
|
||||
348
map-tool/src/source/cli.rs
Normal file
348
map-tool/src/source/cli.rs
Normal file
@@ -0,0 +1,348 @@
|
||||
use std::borrow::Cow;
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use strafesnet_deferred_loader::loader::Loader;
|
||||
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
|
||||
use vbsp_entities_css::Entity;
|
||||
|
||||
use anyhow::Result as AResult;
|
||||
use futures::StreamExt;
|
||||
|
||||
use super::{convert_texture_to_dds,load_texture};
|
||||
use super::{BspFinder,ConvertTextureError,LoadVMTError};
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
pub enum Commands{
|
||||
SourceToSNF(SourceToSNFSubcommand),
|
||||
ExtractTextures(ExtractTexturesSubcommand),
|
||||
VPKContents(VPKContentsSubcommand),
|
||||
BSPContents(BSPContentsSubcommand),
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub struct SourceToSNFSubcommand{
|
||||
#[arg(long)]
|
||||
output_folder:PathBuf,
|
||||
#[arg(required=true)]
|
||||
input_files:Vec<PathBuf>,
|
||||
#[arg(long)]
|
||||
vpks:Vec<PathBuf>,
|
||||
}
|
||||
#[derive(clap::Args)]
|
||||
pub struct ExtractTexturesSubcommand{
|
||||
#[arg(required=true)]
|
||||
bsp_files:Vec<PathBuf>,
|
||||
#[arg(long)]
|
||||
vpks:Vec<PathBuf>,
|
||||
}
|
||||
#[derive(clap::Args)]
|
||||
pub struct VPKContentsSubcommand{
|
||||
#[arg(long)]
|
||||
input_file:PathBuf,
|
||||
}
|
||||
#[derive(clap::Args)]
|
||||
pub struct BSPContentsSubcommand{
|
||||
#[arg(long)]
|
||||
input_file:PathBuf,
|
||||
}
|
||||
|
||||
impl Commands{
|
||||
pub async fn run(self)->AResult<()>{
|
||||
match self{
|
||||
Commands::SourceToSNF(subcommand)=>source_to_snf(subcommand.input_files,subcommand.output_folder,subcommand.vpks).await,
|
||||
Commands::ExtractTextures(subcommand)=>extract_textures(subcommand.bsp_files,subcommand.vpks).await,
|
||||
Commands::VPKContents(subcommand)=>vpk_contents(subcommand.input_file),
|
||||
Commands::BSPContents(subcommand)=>bsp_contents(subcommand.input_file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum ExtractTextureError{
|
||||
#[error("Io error {0:?}")]
|
||||
Io(#[from]std::io::Error),
|
||||
#[error("Bsp error {0:?}")]
|
||||
Bsp(#[from]vbsp::BspError),
|
||||
#[error("MeshLoad error {0:?}")]
|
||||
MeshLoad(#[from]super::loader::MeshError),
|
||||
#[error("Load VMT error {0:?}")]
|
||||
LoadVMT(#[from]LoadVMTError),
|
||||
}
|
||||
async fn gimme_them_textures(path:&std::path::Path,vpk_list:&[strafesnet_bsp_loader::Vpk],send_texture:tokio::sync::mpsc::Sender<(Vec<u8>,String)>)->Result<(),ExtractTextureError>{
|
||||
let bsp=vbsp::Bsp::read(tokio::fs::read(path).await?.as_ref())?;
|
||||
let loader_bsp=strafesnet_bsp_loader::Bsp::new(bsp);
|
||||
let bsp=loader_bsp.as_ref();
|
||||
|
||||
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
|
||||
for texture in bsp.textures(){
|
||||
texture_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(texture.name())));
|
||||
}
|
||||
|
||||
let mut mesh_deferred_loader=MeshDeferredLoader::new();
|
||||
for name in &bsp.static_props.dict.name{
|
||||
mesh_deferred_loader.acquire_mesh_id(name.as_str());
|
||||
}
|
||||
|
||||
for raw_ent in &bsp.entities{
|
||||
let model=match raw_ent.parse(){
|
||||
Ok(Entity::Cycler(brush))=>brush.model,
|
||||
Ok(Entity::EnvSprite(brush))=>brush.model,
|
||||
Ok(Entity::FuncBreakable(brush))=>brush.model,
|
||||
Ok(Entity::FuncBrush(brush))=>brush.model,
|
||||
Ok(Entity::FuncButton(brush))=>brush.model,
|
||||
Ok(Entity::FuncDoor(brush))=>brush.model,
|
||||
Ok(Entity::FuncDoorRotating(brush))=>brush.model,
|
||||
Ok(Entity::FuncIllusionary(brush))=>brush.model,
|
||||
Ok(Entity::FuncMonitor(brush))=>brush.model,
|
||||
Ok(Entity::FuncMovelinear(brush))=>brush.model,
|
||||
Ok(Entity::FuncPhysbox(brush))=>brush.model,
|
||||
Ok(Entity::FuncPhysboxMultiplayer(brush))=>brush.model,
|
||||
Ok(Entity::FuncRotButton(brush))=>brush.model,
|
||||
Ok(Entity::FuncRotating(brush))=>brush.model,
|
||||
Ok(Entity::FuncTracktrain(brush))=>brush.model,
|
||||
Ok(Entity::FuncTrain(brush))=>brush.model,
|
||||
Ok(Entity::FuncWall(brush))=>brush.model,
|
||||
Ok(Entity::FuncWallToggle(brush))=>brush.model,
|
||||
Ok(Entity::FuncWaterAnalog(brush))=>brush.model,
|
||||
Ok(Entity::PropDoorRotating(brush))=>brush.model,
|
||||
Ok(Entity::PropDynamic(brush))=>brush.model,
|
||||
Ok(Entity::PropDynamicOverride(brush))=>brush.model,
|
||||
Ok(Entity::PropPhysics(brush))=>brush.model,
|
||||
Ok(Entity::PropPhysicsMultiplayer(brush))=>brush.model,
|
||||
Ok(Entity::PropPhysicsOverride(brush))=>brush.model,
|
||||
Ok(Entity::PropRagdoll(brush))=>brush.model,
|
||||
Ok(Entity::TriggerGravity(brush))=>brush.model,
|
||||
Ok(Entity::TriggerHurt(brush))=>brush.model,
|
||||
Ok(Entity::TriggerLook(brush))=>brush.model,
|
||||
Ok(Entity::TriggerMultiple(brush))=>brush.model.unwrap_or_default(),
|
||||
Ok(Entity::TriggerOnce(brush))=>brush.model,
|
||||
Ok(Entity::TriggerProximity(brush))=>brush.model,
|
||||
Ok(Entity::TriggerPush(brush))=>brush.model,
|
||||
Ok(Entity::TriggerSoundscape(brush))=>brush.model,
|
||||
Ok(Entity::TriggerTeleport(brush))=>brush.model.unwrap_or_default(),
|
||||
Ok(Entity::TriggerVphysicsMotion(brush))=>brush.model,
|
||||
Ok(Entity::TriggerWind(brush))=>brush.model,
|
||||
_=>continue,
|
||||
};
|
||||
match model.chars().next(){
|
||||
Some('*')=>(),
|
||||
_=>{
|
||||
mesh_deferred_loader.acquire_mesh_id(model);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let finder=BspFinder{
|
||||
bsp:&loader_bsp,
|
||||
vpks:vpk_list
|
||||
};
|
||||
|
||||
let mut mesh_loader=super::loader::ModelLoader::new(finder);
|
||||
// load models and collect requested textures
|
||||
for model_path in mesh_deferred_loader.into_indices(){
|
||||
let model:vmdl::Model=match mesh_loader.load(model_path){
|
||||
Ok(model)=>model,
|
||||
Err(e)=>{
|
||||
println!("Model={model_path} Load model error: {e}");
|
||||
continue;
|
||||
},
|
||||
};
|
||||
for texture in model.textures(){
|
||||
for search_path in &texture.search_paths{
|
||||
let mut path=PathBuf::from(search_path.as_str());
|
||||
path.push(texture.name.as_str());
|
||||
let path=path.to_str().unwrap().to_owned();
|
||||
texture_deferred_loader.acquire_render_config_id(Some(Cow::Owned(path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for texture_path in texture_deferred_loader.into_indices(){
|
||||
match load_texture(finder,&texture_path){
|
||||
Ok(Some(texture))=>send_texture.send(
|
||||
(texture.into_owned(),texture_path.into_owned())
|
||||
).await.unwrap(),
|
||||
Ok(None)=>(),
|
||||
Err(e)=>println!("Texture={texture_path} Load error: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
enum CliConvertTextureError{
|
||||
#[error("Io error {0:?}")]
|
||||
Io(#[from]std::io::Error),
|
||||
#[error("ConvertTexture error {0:?}")]
|
||||
ConvertTexture(#[from]ConvertTextureError),
|
||||
}
|
||||
|
||||
async fn cli_convert_texture(texture:Vec<u8>,write_file_name:impl AsRef<std::path::Path>)->Result<(),CliConvertTextureError>{
|
||||
let dds=convert_texture_to_dds(&texture)?;
|
||||
|
||||
let mut dest=PathBuf::from("textures");
|
||||
dest.push(write_file_name);
|
||||
dest.set_extension("dds");
|
||||
std::fs::create_dir_all(dest.parent().unwrap())?;
|
||||
std::fs::write(&dest,&dds)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn async_read_vpks(vpk_paths:Vec<PathBuf>,thread_limit:usize)->Vec<strafesnet_bsp_loader::Vpk>{
|
||||
futures::stream::iter(vpk_paths).map(|vpk_path|async{
|
||||
tokio::task::spawn_blocking(move||Ok::<_,vpk::Error>(strafesnet_bsp_loader::Vpk::new(vpk::VPK::read(&vpk_path)?))).await.unwrap().unwrap()
|
||||
})
|
||||
.buffer_unordered(thread_limit)
|
||||
.collect().await
|
||||
}
|
||||
|
||||
async fn extract_textures(paths:Vec<PathBuf>,vpk_paths:Vec<PathBuf>)->AResult<()>{
|
||||
tokio::try_join!(
|
||||
tokio::fs::create_dir_all("extracted_textures"),
|
||||
tokio::fs::create_dir_all("textures"),
|
||||
tokio::fs::create_dir_all("meshes"),
|
||||
)?;
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
|
||||
// load vpk list and leak for static lifetime
|
||||
let vpk_list:&[strafesnet_bsp_loader::Vpk]=async_read_vpks(vpk_paths,thread_limit).await.leak();
|
||||
|
||||
let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
|
||||
let mut it=paths.into_iter();
|
||||
let extract_thread=tokio::spawn(async move{
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
|
||||
let send=send_texture.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=gimme_them_textures(&path,vpk_list,send).await;
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Map={path:?} Decode error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// convert images
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
while let (Ok(permit),Some((data,dest)))=(SEM.acquire().await,recv_texture.recv().await){
|
||||
// TODO: dedup dest?
|
||||
tokio::spawn(async move{
|
||||
let result=cli_convert_texture(data,dest).await;
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Convert error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
extract_thread.await?;
|
||||
_=SEM.acquire_many(thread_limit as u32).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn vpk_contents(vpk_path:PathBuf)->AResult<()>{
|
||||
let vpk_index=vpk::VPK::read(&vpk_path)?;
|
||||
for (label,entry) in vpk_index.tree.into_iter(){
|
||||
println!("vpk label={} entry={:?}",label,entry);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bsp_contents(path:PathBuf)->AResult<()>{
|
||||
let bsp=vbsp::Bsp::read(std::fs::read(path)?.as_ref())?;
|
||||
for file_name in bsp.pack.into_zip().into_inner().unwrap().file_names(){
|
||||
println!("file_name={:?}",file_name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn source_to_snf(paths:Vec<PathBuf>,output_folder:PathBuf,vpk_paths:Vec<PathBuf>)->AResult<()>{
|
||||
let start=std::time::Instant::now();
|
||||
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
|
||||
// load vpk list and leak for static lifetime
|
||||
let vpk_list:&[strafesnet_bsp_loader::Vpk]=async_read_vpks(vpk_paths,thread_limit).await.leak();
|
||||
|
||||
let mut it=paths.into_iter();
|
||||
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
|
||||
SEM.add_permits(thread_limit);
|
||||
|
||||
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
|
||||
let output_folder=output_folder.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=match tokio::fs::read(&path).await{
|
||||
Ok(data)=>convert_to_snf(&data,vpk_list).map_err(|e|anyhow::anyhow!("{e:?}")).and_then(|snf_buf|{
|
||||
let mut dest=output_folder;
|
||||
dest.push(path.file_stem().unwrap());
|
||||
dest.set_extension("snfm");
|
||||
std::fs::write(dest,&snf_buf)?;
|
||||
Ok(())
|
||||
}),
|
||||
Err(e)=>Err(e.into()),
|
||||
};
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Convert error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
|
||||
|
||||
println!("elapsed={:?}", start.elapsed());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum ConvertError{
|
||||
IO(std::io::Error),
|
||||
SNFMap(strafesnet_snf::map::Error),
|
||||
BspRead(strafesnet_bsp_loader::ReadError),
|
||||
BspLoadMesh(super::loader::MeshError),
|
||||
BspLoadTexture(super::loader::TextureError),
|
||||
}
|
||||
impl std::fmt::Display for ConvertError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ConvertError{}
|
||||
|
||||
pub fn convert_to_snf(bsp_data:&[u8],vpk_list:&[strafesnet_bsp_loader::Vpk])->Result<Vec<u8>,ConvertError>{
|
||||
const FAILURE_MODE:LoadFailureMode=LoadFailureMode::DefaultToNone;
|
||||
let bsp=strafesnet_bsp_loader::read(
|
||||
Cursor::new(bsp_data)
|
||||
).map_err(ConvertError::BspRead)?;
|
||||
|
||||
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
|
||||
let mut mesh_deferred_loader=MeshDeferredLoader::new();
|
||||
|
||||
let map_step1=strafesnet_bsp_loader::bsp::convert(
|
||||
&bsp,
|
||||
&mut texture_deferred_loader,
|
||||
&mut mesh_deferred_loader,
|
||||
);
|
||||
|
||||
let mut mesh_loader=super::loader::MeshLoader::new(BspFinder{bsp:&bsp,vpks:vpk_list},&mut texture_deferred_loader);
|
||||
let prop_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,FAILURE_MODE).map_err(ConvertError::BspLoadMesh)?;
|
||||
|
||||
let map_step2=map_step1.add_prop_meshes(prop_meshes);
|
||||
|
||||
let mut texture_loader=super::loader::TextureLoader::new();
|
||||
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,FAILURE_MODE).map_err(ConvertError::BspLoadTexture)?;
|
||||
|
||||
let map=map_step2.add_render_configs_and_textures(render_configs);
|
||||
|
||||
let mut snf_buf=Vec::new();
|
||||
strafesnet_snf::map::write_map(Cursor::new(&mut snf_buf),map).map_err(ConvertError::SNFMap)?;
|
||||
|
||||
Ok(snf_buf)
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::{borrow::Cow, io::Read};
|
||||
|
||||
use strafesnet_common::model::Mesh;
|
||||
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
|
||||
|
||||
use crate::{Bsp,Vpk};
|
||||
use super::BspFinder;
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum TextureError{
|
||||
Io(std::io::Error),
|
||||
@@ -31,7 +31,7 @@ impl Loader for TextureLoader{
|
||||
type Error=TextureError;
|
||||
type Index<'a>=Cow<'a,str>;
|
||||
type Resource=Texture;
|
||||
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
|
||||
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
|
||||
let file_name=format!("textures/{}.dds",index);
|
||||
let mut file=std::fs::File::open(file_name)?;
|
||||
let mut data=Vec::new();
|
||||
@@ -40,6 +40,7 @@ impl Loader for TextureLoader{
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum MeshError{
|
||||
Io(std::io::Error),
|
||||
@@ -71,33 +72,6 @@ impl From<vbsp::BspError> for MeshError{
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct BspFinder<'bsp,'vpk>{
|
||||
pub bsp:&'bsp Bsp,
|
||||
pub vpks:&'vpk [Vpk],
|
||||
}
|
||||
impl<'bsp,'vpk> BspFinder<'bsp,'vpk>{
|
||||
pub fn find<'a>(&self,path:&str)->Result<Option<Cow<'a,[u8]>>,vbsp::BspError>
|
||||
where
|
||||
'bsp:'a,
|
||||
'vpk:'a,
|
||||
{
|
||||
// search bsp
|
||||
if let Some(data)=self.bsp.pack_get(path)?{
|
||||
return Ok(Some(Cow::Owned(data)));
|
||||
}
|
||||
|
||||
//search each vpk
|
||||
for vpk in self.vpks{
|
||||
if let Some(vpk_entry)=vpk.tree_get(path){
|
||||
return Ok(Some(vpk_entry.get()?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModelLoader<'bsp,'vpk>{
|
||||
finder:BspFinder<'bsp,'vpk>,
|
||||
}
|
||||
@@ -111,7 +85,7 @@ impl ModelLoader<'_,'_>{
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'bsp,'vpk> Loader for ModelLoader<'bsp,'vpk>{
|
||||
impl Loader for ModelLoader<'_,'_>{
|
||||
type Error=MeshError;
|
||||
type Index<'a>=&'a str where Self:'a;
|
||||
type Resource=vmdl::Model;
|
||||
@@ -151,13 +125,13 @@ impl MeshLoader<'_,'_,'_,'_>{
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'str,'bsp,'vpk,'load> Loader for MeshLoader<'bsp,'vpk,'load,'str>{
|
||||
impl Loader for MeshLoader<'_,'_,'_,'_>{
|
||||
type Error=MeshError;
|
||||
type Index<'a>=&'a str where Self:'a;
|
||||
type Resource=Mesh;
|
||||
type Resource=strafesnet_bsp_loader::mesh::Mesh;
|
||||
fn load<'a>(&'a mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
|
||||
let model=ModelLoader::new(self.finder).load(index)?;
|
||||
let mesh=crate::mesh::convert_mesh(model,&mut self.deferred_loader);
|
||||
let mesh=strafesnet_bsp_loader::mesh::convert_mesh(model,&mut self.deferred_loader);
|
||||
Ok(mesh)
|
||||
}
|
||||
}
|
||||
247
map-tool/src/source/mod.rs
Normal file
247
map-tool/src/source/mod.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
#[cfg(feature="cli")]
|
||||
mod cli;
|
||||
#[cfg(feature="cli")]
|
||||
pub use cli::Commands;
|
||||
#[cfg(feature="cli")]
|
||||
mod loader;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::borrow::Cow;
|
||||
use std::io::Cursor;
|
||||
|
||||
use strafesnet_bsp_loader::{Bsp,Vpk};
|
||||
#[derive(Clone,Copy)]
|
||||
pub struct BspFinder<'bsp,'vpk>{
|
||||
pub bsp:&'bsp Bsp,
|
||||
pub vpks:&'vpk [Vpk],
|
||||
}
|
||||
impl<'bsp,'vpk> BspFinder<'bsp,'vpk>{
|
||||
pub fn find<'a>(&self,path:&str)->Result<Option<Cow<'a,[u8]>>,vbsp::BspError>
|
||||
where
|
||||
'bsp:'a,
|
||||
'vpk:'a,
|
||||
{
|
||||
// search bsp
|
||||
if let Some(data)=self.bsp.pack_get(path)?{
|
||||
return Ok(Some(Cow::Owned(data)));
|
||||
}
|
||||
|
||||
//search each vpk
|
||||
for vpk in self.vpks{
|
||||
if let Some(vpk_entry)=vpk.tree_get(path){
|
||||
return Ok(Some(vpk_entry.get()?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
enum VMTContent{
|
||||
VMT(String),
|
||||
VTF(String),
|
||||
Patch(vmt_parser::material::PatchMaterial),
|
||||
Unsupported,//don't want to deal with whatever vmt variant
|
||||
Unresolved,//could not locate a texture because of vmt content
|
||||
}
|
||||
impl VMTContent{
|
||||
fn vtf(opt:Option<String>)->Self{
|
||||
match opt{
|
||||
Some(s)=>Self::VTF(s),
|
||||
None=>Self::Unresolved,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_some_texture(material:vmt_parser::material::Material)->VMTContent{
|
||||
//just grab some texture from somewhere for now
|
||||
match material{
|
||||
vmt_parser::material::Material::LightMappedGeneric(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::VertexLitGeneric(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),//this just dies if there is none
|
||||
vmt_parser::material::Material::VertexLitGenericDx6(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),
|
||||
vmt_parser::material::Material::UnlitGeneric(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::UnlitTwoTexture(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Water(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::WorldVertexTransition(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::EyeRefract(mat)=>VMTContent::vtf(Some(mat.cornea_texture)),
|
||||
vmt_parser::material::Material::SubRect(mat)=>VMTContent::VMT(mat.material),//recursive
|
||||
vmt_parser::material::Material::Sprite(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::SpriteCard(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Cable(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Refract(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Modulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::DecalModulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Sky(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Replacements(_mat)=>VMTContent::Unsupported,
|
||||
vmt_parser::material::Material::Patch(mat)=>VMTContent::Patch(mat),
|
||||
_=>unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
pub enum GetVMTError{
|
||||
#[error("Bsp error {0:?}")]
|
||||
Bsp(#[from]vbsp::BspError),
|
||||
#[error("Utf8 error {0:?}")]
|
||||
Utf8(#[from]std::str::Utf8Error),
|
||||
#[error("Vdf error {0:?}")]
|
||||
Vdf(#[from]vmt_parser::VdfError),
|
||||
#[error("Vmt not found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
fn get_vmt(finder:BspFinder,search_name:&str)->Result<vmt_parser::material::Material,GetVMTError>{
|
||||
let vmt_data=finder.find(search_name)?.ok_or(GetVMTError::NotFound)?;
|
||||
//decode vmt and then write
|
||||
let vmt_str=core::str::from_utf8(&vmt_data)?;
|
||||
let material=vmt_parser::from_str(vmt_str)?;
|
||||
Ok(material)
|
||||
}
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
pub enum LoadVMTError{
|
||||
#[error("Bsp error {0:?}")]
|
||||
Bsp(#[from]vbsp::BspError),
|
||||
#[error("GetVMT error {0:?}")]
|
||||
GetVMT(#[from]GetVMTError),
|
||||
#[error("FromUtf8 error {0:?}")]
|
||||
FromUtf8(#[from]std::string::FromUtf8Error),
|
||||
#[error("Vdf error {0:?}")]
|
||||
Vdf(#[from]vmt_parser::VdfError),
|
||||
#[error("Vmt unsupported")]
|
||||
Unsupported,
|
||||
#[error("Vmt unresolved")]
|
||||
Unresolved,
|
||||
#[error("Vmt not found")]
|
||||
NotFound,
|
||||
}
|
||||
fn recursive_vmt_loader<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,material:vmt_parser::material::Material)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
|
||||
where
|
||||
'bsp:'a,
|
||||
'vpk:'a,
|
||||
{
|
||||
match get_some_texture(material){
|
||||
VMTContent::VMT(mut s)=>{
|
||||
s.make_ascii_lowercase();
|
||||
recursive_vmt_loader(finder,get_vmt(finder,&s)?)
|
||||
},
|
||||
VMTContent::VTF(s)=>{
|
||||
let mut texture_file_name=PathBuf::from("materials");
|
||||
texture_file_name.push(s);
|
||||
texture_file_name.set_extension("vtf");
|
||||
texture_file_name.as_mut_os_str().make_ascii_lowercase();
|
||||
Ok(finder.find(texture_file_name.to_str().unwrap())?)
|
||||
},
|
||||
VMTContent::Patch(mat)=>recursive_vmt_loader(finder,
|
||||
mat.resolve(|search_name|{
|
||||
let name_lowercase=search_name.to_lowercase();
|
||||
match finder.find(&name_lowercase)?{
|
||||
Some(bytes)=>Ok(String::from_utf8(bytes.into_owned())?),
|
||||
None=>Err(LoadVMTError::NotFound),
|
||||
}
|
||||
})?
|
||||
),
|
||||
VMTContent::Unsupported=>Err(LoadVMTError::Unsupported),
|
||||
VMTContent::Unresolved=>Err(LoadVMTError::Unresolved),
|
||||
}
|
||||
}
|
||||
pub fn load_texture<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,texture_name:&str)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
|
||||
where
|
||||
'bsp:'a,
|
||||
'vpk:'a,
|
||||
{
|
||||
let mut texture_file_name=PathBuf::from("materials");
|
||||
//lower case
|
||||
texture_file_name.push(texture_name);
|
||||
texture_file_name.as_mut_os_str().make_ascii_lowercase();
|
||||
//remove stem and search for both vtf and vmt files
|
||||
let stem=texture_file_name.file_stem().unwrap().to_owned();
|
||||
texture_file_name.pop();
|
||||
texture_file_name.push(stem);
|
||||
if let Some(stuff)=finder.find(texture_file_name.to_str().unwrap())?{
|
||||
return Ok(Some(stuff));
|
||||
}
|
||||
|
||||
// search for both vmt,vtf
|
||||
let mut texture_file_name_vmt=texture_file_name.clone();
|
||||
texture_file_name_vmt.set_extension("vmt");
|
||||
|
||||
let get_vmt_result=get_vmt(finder,texture_file_name_vmt.to_str().unwrap());
|
||||
match get_vmt_result{
|
||||
Ok(material)=>{
|
||||
let vmt_result=recursive_vmt_loader(finder,material);
|
||||
match vmt_result{
|
||||
Ok(Some(stuff))=>return Ok(Some(stuff)),
|
||||
Ok(None)
|
||||
|Err(LoadVMTError::NotFound)=>(),
|
||||
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
|
||||
Err(e)=>return Err(e),
|
||||
}
|
||||
}
|
||||
|Err(GetVMTError::NotFound)=>(),
|
||||
Err(e)=>Err(e)?,
|
||||
}
|
||||
|
||||
// try looking for vtf
|
||||
let mut texture_file_name_vtf=texture_file_name.clone();
|
||||
texture_file_name_vtf.set_extension("vtf");
|
||||
|
||||
let get_vtf_result=get_vmt(finder,texture_file_name_vtf.to_str().unwrap());
|
||||
match get_vtf_result{
|
||||
Ok(material)=>{
|
||||
let vtf_result=recursive_vmt_loader(finder,material);
|
||||
match vtf_result{
|
||||
Ok(Some(stuff))=>return Ok(Some(stuff)),
|
||||
Ok(None)
|
||||
|Err(LoadVMTError::NotFound)=>(),
|
||||
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
|
||||
Err(e)=>return Err(e),
|
||||
}
|
||||
}
|
||||
|Err(GetVMTError::NotFound)=>(),
|
||||
Err(e)=>Err(e)?,
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[derive(Debug,thiserror::Error)]
|
||||
pub enum ConvertTextureError{
|
||||
#[error("Vtf error {0:?}")]
|
||||
Vtf(#[from]vtf::Error),
|
||||
#[error("DDS encode error {0:?}")]
|
||||
DDSEncode(#[from]image_dds::error::SurfaceError),
|
||||
#[error("DDS create error {0:?}")]
|
||||
DDS(#[from]image_dds::CreateDdsError),
|
||||
#[error("DDS write error {0:?}")]
|
||||
DDSWrite(#[from]image_dds::ddsfile::Error),
|
||||
}
|
||||
|
||||
pub fn convert_texture_to_dds(vtf_data:&[u8])->Result<Vec<u8>,ConvertTextureError>{
|
||||
let vtf_vec=vtf_data.to_vec();
|
||||
let image=vtf::from_bytes(&vtf_vec)?.highres_image.decode(0)?.to_rgba8();
|
||||
|
||||
let format=if image.width()%4!=0||image.height()%4!=0{
|
||||
image_dds::ImageFormat::Rgba8UnormSrgb
|
||||
}else{
|
||||
image_dds::ImageFormat::BC7RgbaUnormSrgb
|
||||
};
|
||||
let dds=image_dds::SurfaceRgba8{
|
||||
width:image.width(),
|
||||
height:image.height(),
|
||||
depth:1,
|
||||
layers:1,
|
||||
mipmaps:1,
|
||||
data:image.as_raw(),
|
||||
}.encode(format,image_dds::Quality::Slow,image_dds::Mipmaps::GeneratedAutomatic)?.to_dds()?;
|
||||
|
||||
let mut buf=Vec::new();
|
||||
dds.write(&mut Cursor::new(&mut buf))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn read_vpks(vpk_paths:&[PathBuf])->Result<Vec<Vpk>,vpk::Error>{
|
||||
vpk_paths.iter().map(|vpk_path|{
|
||||
Ok(Vpk::new(vpk::VPK::read(vpk_path)?))
|
||||
}).collect()
|
||||
}
|
||||
@@ -16,21 +16,24 @@ source = ["dep:strafesnet_deferred_loader", "dep:strafesnet_bsp_loader"]
|
||||
roblox = ["dep:strafesnet_deferred_loader", "dep:strafesnet_rbx_loader"]
|
||||
|
||||
[dependencies]
|
||||
glam = "0.30.0"
|
||||
glam.workspace = true
|
||||
parking_lot = "0.12.1"
|
||||
pollster = "0.4.0"
|
||||
strafesnet_bsp_loader = { path = "../lib/bsp_loader", registry = "strafesnet", optional = true }
|
||||
strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
|
||||
strafesnet_deferred_loader = { path = "../lib/deferred_loader", registry = "strafesnet", optional = true }
|
||||
strafesnet_graphics = { path = "../engine/graphics", registry = "strafesnet" }
|
||||
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
|
||||
strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", optional = true }
|
||||
strafesnet_session = { path = "../engine/session", registry = "strafesnet" }
|
||||
strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" }
|
||||
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true }
|
||||
wgpu = "26.0.1"
|
||||
strafesnet_bsp_loader = { workspace = true, optional = true }
|
||||
strafesnet_common.workspace = true
|
||||
strafesnet_deferred_loader = { workspace = true, optional = true }
|
||||
strafesnet_graphics.workspace = true
|
||||
strafesnet_physics.workspace = true
|
||||
strafesnet_rbx_loader = { workspace = true, optional = true }
|
||||
strafesnet_session.workspace = true
|
||||
strafesnet_settings.workspace = true
|
||||
strafesnet_snf = { workspace = true, optional = true }
|
||||
wgpu.workspace = true
|
||||
winit = "0.30.7"
|
||||
|
||||
[profile.dev]
|
||||
strip = false
|
||||
opt-level = 3
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
||||
# Blender MTL File: 'teslacyberv3.0.blend'
|
||||
# Material Count: 6
|
||||
|
||||
newmtl Material
|
||||
Ns 65.476285
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 0.411568 0.411568 0.411568
|
||||
Ks 0.614679 0.614679 0.614679
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 36.750000
|
||||
d 1.000000
|
||||
illum 3
|
||||
|
||||
newmtl Материал
|
||||
Ns 323.999994
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 0.800000 0.800000 0.800000
|
||||
Ks 0.500000 0.500000 0.500000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.000000
|
||||
d 1.000000
|
||||
illum 2
|
||||
|
||||
newmtl Материал.001
|
||||
Ns 900.000000
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 0.026240 0.026240 0.026240
|
||||
Ks 0.000000 0.000000 0.000000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.450000
|
||||
d 1.000000
|
||||
illum 1
|
||||
|
||||
newmtl Материал.002
|
||||
Ns 0.000000
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 0.031837 0.032429 0.029425
|
||||
Ks 0.169725 0.169725 0.169725
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 0.000000
|
||||
d 1.000000
|
||||
illum 2
|
||||
|
||||
newmtl Материал.003
|
||||
Ns 900.000000
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 0.023585 0.083235 0.095923
|
||||
Ks 1.000000 1.000000 1.000000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 45.049999
|
||||
d 1.000000
|
||||
illum 3
|
||||
|
||||
newmtl Материал.004
|
||||
Ns 323.999994
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 0.800000 0.800000 0.800000
|
||||
Ks 0.500000 0.500000 0.500000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.000000
|
||||
d 1.000000
|
||||
illum 2
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,7 @@ WorkerDescription{
|
||||
|
||||
pub fn new(
|
||||
mut graphics:graphics::GraphicsState,
|
||||
mut config:wgpu::SurfaceConfiguration,
|
||||
surface:wgpu::Surface<'_>,
|
||||
mut surface:strafesnet_graphics::surface::Surface<'_>,
|
||||
device:wgpu::Device,
|
||||
queue:wgpu::Queue,
|
||||
)->crate::compat_worker::INWorker<'_,Instruction>{
|
||||
@@ -34,29 +33,22 @@ pub fn new(
|
||||
Instruction::Resize(size,user_settings)=>{
|
||||
println!("Resizing to {:?}",size);
|
||||
let t0=std::time::Instant::now();
|
||||
config.width=size.width.max(1);
|
||||
config.height=size.height.max(1);
|
||||
surface.configure(&device,&config);
|
||||
graphics.resize(&device,&config,&user_settings);
|
||||
let size=glam::uvec2(size.width,size.height);
|
||||
surface.configure(&device,size);
|
||||
let size=surface.size();
|
||||
let fov=user_settings.calculate_fov(1.0,&size).as_vec2();
|
||||
graphics.resize(&device,size,fov);
|
||||
println!("Resize took {:?}",t0.elapsed());
|
||||
}
|
||||
Instruction::Render(frame_state)=>{
|
||||
//this has to go deeper somehow
|
||||
let frame=match surface.get_current_texture(){
|
||||
Ok(frame)=>frame,
|
||||
Err(_)=>{
|
||||
surface.configure(&device,&config);
|
||||
surface
|
||||
.get_current_texture()
|
||||
.expect("Failed to acquire next surface texture!")
|
||||
}
|
||||
};
|
||||
let view=frame.texture.create_view(&wgpu::TextureViewDescriptor{
|
||||
format:Some(config.view_formats[0]),
|
||||
..wgpu::TextureViewDescriptor::default()
|
||||
});
|
||||
let frame=surface.new_frame(&device).expect("Error creating new frame");
|
||||
|
||||
graphics.render(&view,&device,&queue,frame_state);
|
||||
let mut encoder=device.create_command_encoder(&wgpu::CommandEncoderDescriptor{label:None});
|
||||
|
||||
graphics.encode_commands(&mut encoder,frame.view(),graphics::view_inv(frame_state.pos(),frame_state.angles()));
|
||||
|
||||
queue.submit([encoder.finish()]);
|
||||
|
||||
frame.present();
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ mod graphics_worker;
|
||||
const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION"));
|
||||
|
||||
fn main(){
|
||||
setup::setup_and_start(TITLE);
|
||||
pollster::block_on(setup::setup_and_start(TITLE));
|
||||
}
|
||||
|
||||
@@ -1,198 +1,44 @@
|
||||
fn optional_features()->wgpu::Features{
|
||||
wgpu::Features::TEXTURE_COMPRESSION_ASTC
|
||||
|wgpu::Features::TEXTURE_COMPRESSION_ETC2
|
||||
}
|
||||
fn required_features()->wgpu::Features{
|
||||
wgpu::Features::TEXTURE_COMPRESSION_BC
|
||||
}
|
||||
fn required_downlevel_capabilities()->wgpu::DownlevelCapabilities{
|
||||
wgpu::DownlevelCapabilities{
|
||||
flags:wgpu::DownlevelFlags::empty(),
|
||||
shader_model:wgpu::ShaderModel::Sm5,
|
||||
..wgpu::DownlevelCapabilities::default()
|
||||
}
|
||||
}
|
||||
use strafesnet_graphics::setup;
|
||||
|
||||
const LIMITS:wgpu::Limits=wgpu::Limits::defaults();
|
||||
|
||||
struct SetupContextPartial1{
|
||||
backends:wgpu::Backends,
|
||||
instance:wgpu::Instance,
|
||||
}
|
||||
fn create_window(title:&str,event_loop:&winit::event_loop::EventLoop<()>)->Result<winit::window::Window,winit::error::OsError>{
|
||||
let mut attr=winit::window::WindowAttributes::default();
|
||||
attr=attr.with_title(title);
|
||||
event_loop.create_window(attr)
|
||||
}
|
||||
fn create_instance()->SetupContextPartial1{
|
||||
let backends=wgpu::Backends::from_env().unwrap_or_default();
|
||||
SetupContextPartial1{
|
||||
backends,
|
||||
instance:Default::default(),
|
||||
}
|
||||
}
|
||||
impl SetupContextPartial1{
|
||||
fn create_surface<'a>(self,window:&'a winit::window::Window)->Result<SetupContextPartial2<'a>,wgpu::CreateSurfaceError>{
|
||||
Ok(SetupContextPartial2{
|
||||
backends:self.backends,
|
||||
surface:self.instance.create_surface(window)?,
|
||||
instance:self.instance,
|
||||
})
|
||||
}
|
||||
}
|
||||
struct SetupContextPartial2<'a>{
|
||||
backends:wgpu::Backends,
|
||||
instance:wgpu::Instance,
|
||||
surface:wgpu::Surface<'a>,
|
||||
}
|
||||
impl<'a> SetupContextPartial2<'a>{
|
||||
fn pick_adapter(self)->SetupContextPartial3<'a>{
|
||||
let adapter;
|
||||
|
||||
//TODO: prefer adapter that implements optional features
|
||||
//let optional_features=optional_features();
|
||||
let required_features=required_features();
|
||||
|
||||
//no helper function smh gotta write it myself
|
||||
let adapters=self.instance.enumerate_adapters(self.backends);
|
||||
|
||||
let mut chosen_adapter=None;
|
||||
let mut chosen_adapter_score=0;
|
||||
for adapter in adapters {
|
||||
if !adapter.is_surface_supported(&self.surface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let score=match adapter.get_info().device_type{
|
||||
wgpu::DeviceType::IntegratedGpu=>3,
|
||||
wgpu::DeviceType::DiscreteGpu=>4,
|
||||
wgpu::DeviceType::VirtualGpu=>2,
|
||||
wgpu::DeviceType::Other|wgpu::DeviceType::Cpu=>1,
|
||||
};
|
||||
|
||||
let adapter_features=adapter.features();
|
||||
if chosen_adapter_score<score&&adapter_features.contains(required_features) {
|
||||
chosen_adapter_score=score;
|
||||
chosen_adapter=Some(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(maybe_chosen_adapter)=chosen_adapter{
|
||||
adapter=maybe_chosen_adapter;
|
||||
}else{
|
||||
panic!("No suitable GPU adapters found on the system!");
|
||||
}
|
||||
|
||||
|
||||
let adapter_info=adapter.get_info();
|
||||
println!("Using {} ({:?})", adapter_info.name, adapter_info.backend);
|
||||
|
||||
let required_downlevel_capabilities=required_downlevel_capabilities();
|
||||
let downlevel_capabilities=adapter.get_downlevel_capabilities();
|
||||
assert!(
|
||||
downlevel_capabilities.shader_model >= required_downlevel_capabilities.shader_model,
|
||||
"Adapter does not support the minimum shader model required to run this example: {:?}",
|
||||
required_downlevel_capabilities.shader_model
|
||||
);
|
||||
assert!(
|
||||
downlevel_capabilities
|
||||
.flags
|
||||
.contains(required_downlevel_capabilities.flags),
|
||||
"Adapter does not support the downlevel capabilities required to run this example: {:?}",
|
||||
required_downlevel_capabilities.flags - downlevel_capabilities.flags
|
||||
);
|
||||
SetupContextPartial3{
|
||||
surface:self.surface,
|
||||
adapter,
|
||||
}
|
||||
}
|
||||
}
|
||||
struct SetupContextPartial3<'a>{
|
||||
surface:wgpu::Surface<'a>,
|
||||
adapter:wgpu::Adapter,
|
||||
}
|
||||
impl<'a> SetupContextPartial3<'a>{
|
||||
fn request_device(self)->SetupContextPartial4<'a>{
|
||||
let optional_features=optional_features();
|
||||
let required_features=required_features();
|
||||
|
||||
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface.
|
||||
let needed_limits=strafesnet_graphics::graphics::required_limits().using_resolution(self.adapter.limits());
|
||||
|
||||
let (device, queue)=pollster::block_on(self.adapter
|
||||
.request_device(
|
||||
&wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
required_features: (optional_features & self.adapter.features()) | required_features,
|
||||
required_limits: needed_limits,
|
||||
memory_hints:wgpu::MemoryHints::Performance,
|
||||
trace: wgpu::Trace::Off,
|
||||
},
|
||||
))
|
||||
.expect("Unable to find a suitable GPU adapter!");
|
||||
|
||||
SetupContextPartial4{
|
||||
surface:self.surface,
|
||||
adapter:self.adapter,
|
||||
device,
|
||||
queue,
|
||||
}
|
||||
}
|
||||
}
|
||||
struct SetupContextPartial4<'a>{
|
||||
surface:wgpu::Surface<'a>,
|
||||
adapter:wgpu::Adapter,
|
||||
device:wgpu::Device,
|
||||
queue:wgpu::Queue,
|
||||
}
|
||||
impl<'a> SetupContextPartial4<'a>{
|
||||
fn configure_surface(self,size:&'a winit::dpi::PhysicalSize<u32>)->SetupContext<'a>{
|
||||
let mut config=self.surface
|
||||
.get_default_config(&self.adapter, size.width, size.height)
|
||||
.expect("Surface isn't supported by the adapter.");
|
||||
let surface_view_format=config.format.add_srgb_suffix();
|
||||
config.view_formats.push(surface_view_format);
|
||||
config.present_mode=wgpu::PresentMode::AutoNoVsync;
|
||||
self.surface.configure(&self.device, &config);
|
||||
|
||||
SetupContext{
|
||||
surface:self.surface,
|
||||
device:self.device,
|
||||
queue:self.queue,
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct SetupContext<'a>{
|
||||
pub surface:wgpu::Surface<'a>,
|
||||
pub device:wgpu::Device,
|
||||
pub queue:wgpu::Queue,
|
||||
pub config:wgpu::SurfaceConfiguration,
|
||||
}
|
||||
|
||||
pub fn setup_and_start(title:&str){
|
||||
pub async fn setup_and_start(title:&str){
|
||||
let event_loop=winit::event_loop::EventLoop::new().unwrap();
|
||||
|
||||
println!("Initializing the surface...");
|
||||
|
||||
let partial_1=create_instance();
|
||||
|
||||
let window=create_window(title,&event_loop).unwrap();
|
||||
|
||||
let partial_2=partial_1.create_surface(&window).unwrap();
|
||||
println!("Initializing the surface...");
|
||||
|
||||
let partial_3=partial_2.pick_adapter();
|
||||
let desc=wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(event_loop.owned_display_handle()));
|
||||
let instance=wgpu::Instance::new(desc);
|
||||
|
||||
let partial_4=partial_3.request_device();
|
||||
let surface=setup::step2::create_surface(&instance,&window).unwrap();
|
||||
|
||||
let adapter=setup::step3::pick_adapter(&instance,&surface).await.expect("No suitable GPU adapters found on the system!");
|
||||
|
||||
let adapter_info=adapter.get_info();
|
||||
println!("Using {} ({:?})",adapter_info.name,adapter_info.backend);
|
||||
|
||||
let (device,queue)=setup::step4::request_device(&adapter,LIMITS).await.unwrap();
|
||||
|
||||
let size=window.inner_size();
|
||||
|
||||
let setup_context=partial_4.configure_surface(&size);
|
||||
let surface=setup::step5::configure_surface(&adapter,&device,surface,(size.width,size.height)).unwrap();
|
||||
|
||||
//dedicated thread to ping request redraw back and resize the window doesn't seem logical
|
||||
|
||||
//the thread that spawns the physics thread
|
||||
let mut window_thread=crate::window::worker(
|
||||
&window,
|
||||
setup_context,
|
||||
device,
|
||||
queue,
|
||||
surface,
|
||||
LIMITS,
|
||||
);
|
||||
|
||||
for arg in std::env::args().skip(1){
|
||||
|
||||
@@ -15,6 +15,7 @@ pub enum Instruction{
|
||||
struct WindowContext<'a>{
|
||||
manual_mouse_lock:bool,
|
||||
mouse_pos:glam::DVec2,
|
||||
simulation_paused:bool,
|
||||
screen_size:glam::UVec2,
|
||||
window:&'a winit::window::Window,
|
||||
physics_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<PhysicsWorkerInstruction,SessionTime>>,
|
||||
@@ -24,6 +25,35 @@ impl WindowContext<'_>{
|
||||
fn get_middle_of_screen(&self)->winit::dpi::PhysicalPosition<u32>{
|
||||
winit::dpi::PhysicalPosition::new(self.screen_size.x/2,self.screen_size.y/2)
|
||||
}
|
||||
fn free_mouse(&mut self){
|
||||
self.manual_mouse_lock=false;
|
||||
match self.window.set_cursor_position(self.get_middle_of_screen()){
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Could not set cursor position: {:?}",e),
|
||||
}
|
||||
match self.window.set_cursor_grab(winit::window::CursorGrabMode::None){
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Could not release cursor: {:?}",e),
|
||||
}
|
||||
self.window.set_cursor_visible(true);
|
||||
}
|
||||
fn lock_mouse(&mut self){
|
||||
//if cursor is outside window don't lock but apparently there's no get pos function
|
||||
//let pos=window.get_cursor_pos();
|
||||
match self.window.set_cursor_grab(winit::window::CursorGrabMode::Locked){
|
||||
Ok(())=>(),
|
||||
Err(_)=>{
|
||||
match self.window.set_cursor_grab(winit::window::CursorGrabMode::Confined){
|
||||
Ok(())=>(),
|
||||
Err(e)=>{
|
||||
self.manual_mouse_lock=true;
|
||||
println!("Could not confine cursor: {:?}",e)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
self.window.set_cursor_visible(false);
|
||||
}
|
||||
fn window_event(&mut self,time:SessionTime,event:winit::event::WindowEvent){
|
||||
match event{
|
||||
winit::event::WindowEvent::DroppedFile(path)=>{
|
||||
@@ -34,6 +64,10 @@ impl WindowContext<'_>{
|
||||
}
|
||||
},
|
||||
winit::event::WindowEvent::Focused(state)=>{
|
||||
// don't unpause if manually paused
|
||||
if self.simulation_paused{
|
||||
return;
|
||||
}
|
||||
//pause unpause
|
||||
self.physics_thread.send(TimedInstruction{
|
||||
time,
|
||||
@@ -46,35 +80,8 @@ impl WindowContext<'_>{
|
||||
..
|
||||
}=>{
|
||||
match (logical_key,state){
|
||||
(winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab),winit::event::ElementState::Pressed)=>{
|
||||
self.manual_mouse_lock=false;
|
||||
match self.window.set_cursor_position(self.get_middle_of_screen()){
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Could not set cursor position: {:?}",e),
|
||||
}
|
||||
match self.window.set_cursor_grab(winit::window::CursorGrabMode::None){
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Could not release cursor: {:?}",e),
|
||||
}
|
||||
self.window.set_cursor_visible(state.is_pressed());
|
||||
},
|
||||
(winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab),winit::event::ElementState::Released)=>{
|
||||
//if cursor is outside window don't lock but apparently there's no get pos function
|
||||
//let pos=window.get_cursor_pos();
|
||||
match self.window.set_cursor_grab(winit::window::CursorGrabMode::Locked){
|
||||
Ok(())=>(),
|
||||
Err(_)=>{
|
||||
match self.window.set_cursor_grab(winit::window::CursorGrabMode::Confined){
|
||||
Ok(())=>(),
|
||||
Err(e)=>{
|
||||
self.manual_mouse_lock=true;
|
||||
println!("Could not confine cursor: {:?}",e)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
self.window.set_cursor_visible(state.is_pressed());
|
||||
},
|
||||
(winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab),winit::event::ElementState::Pressed)=>self.free_mouse(),
|
||||
(winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab),winit::event::ElementState::Released)=>self.lock_mouse(),
|
||||
(winit::keyboard::Key::Named(winit::keyboard::NamedKey::F11),winit::event::ElementState::Pressed)=>{
|
||||
if self.window.fullscreen().is_some(){
|
||||
self.window.set_fullscreen(None);
|
||||
@@ -131,8 +138,18 @@ impl WindowContext<'_>{
|
||||
|
||||
if let Some(session_instruction)=match keycode{
|
||||
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Space)=>input_ctrl!(SetJump,s),
|
||||
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Shift)=>input_ctrl!(SetSprint,s),
|
||||
// TODO: bind system so playback pausing can use spacebar
|
||||
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Enter)=>session_playback!(TogglePaused,s),
|
||||
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Enter)=>if s{
|
||||
let paused=!self.simulation_paused;
|
||||
self.simulation_paused=paused;
|
||||
if paused{
|
||||
self.free_mouse();
|
||||
}else{
|
||||
self.lock_mouse();
|
||||
}
|
||||
Some(SessionInstructionSubset::Control(SessionControlInstruction::SetPaused(paused)))
|
||||
}else{None},
|
||||
winit::keyboard::Key::Named(winit::keyboard::NamedKey::ArrowUp)=>session_playback!(IncreaseTimescale,s),
|
||||
winit::keyboard::Key::Named(winit::keyboard::NamedKey::ArrowDown)=>session_playback!(DecreaseTimescale,s),
|
||||
winit::keyboard::Key::Named(winit::keyboard::NamedKey::ArrowLeft)=>session_playback!(SkipBack,s),
|
||||
@@ -188,7 +205,7 @@ impl WindowContext<'_>{
|
||||
}
|
||||
}
|
||||
|
||||
fn device_event(&mut self,time:SessionTime,event: winit::event::DeviceEvent){
|
||||
fn device_event(&mut self,time:SessionTime,event:winit::event::DeviceEvent){
|
||||
match event{
|
||||
winit::event::DeviceEvent::MouseMotion{
|
||||
delta,
|
||||
@@ -222,7 +239,10 @@ impl WindowContext<'_>{
|
||||
}
|
||||
pub fn worker<'a>(
|
||||
window:&'a winit::window::Window,
|
||||
setup_context:crate::setup::SetupContext<'a>,
|
||||
device:wgpu::Device,
|
||||
queue:wgpu::Queue,
|
||||
surface:strafesnet_graphics::surface::Surface<'a>,
|
||||
limits:wgpu::Limits,
|
||||
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTime>>{
|
||||
// WindowContextSetup::new
|
||||
#[cfg(feature="user-install")]
|
||||
@@ -232,15 +252,17 @@ pub fn worker<'a>(
|
||||
|
||||
let user_settings=directories.settings();
|
||||
|
||||
let mut graphics=strafesnet_graphics::graphics::GraphicsState::new(&setup_context.device,&setup_context.queue,&setup_context.config);
|
||||
graphics.load_user_settings(&user_settings);
|
||||
let screen_size=surface.size();
|
||||
let mut graphics=strafesnet_graphics::graphics::GraphicsState::new(&device,&queue,screen_size,surface.view_format(),limits);
|
||||
|
||||
//WindowContextSetup::into_context
|
||||
let screen_size=glam::uvec2(setup_context.config.width,setup_context.config.height);
|
||||
let graphics_thread=crate::graphics_worker::new(graphics,setup_context.config,setup_context.surface,setup_context.device,setup_context.queue);
|
||||
let fov=user_settings.calculate_fov(1.0,&screen_size).as_vec2();
|
||||
graphics.resize(&device,screen_size,fov);
|
||||
let graphics_thread=crate::graphics_worker::new(graphics,surface,device,queue);
|
||||
let mut window_context=WindowContext{
|
||||
manual_mouse_lock:false,
|
||||
mouse_pos:glam::DVec2::ZERO,
|
||||
simulation_paused:false,
|
||||
//make sure to update this!!!!!
|
||||
screen_size,
|
||||
window,
|
||||
|
||||
Reference in New Issue
Block a user