10 Commits
bvh ... master

Author SHA1 Message Date
102dd7fa6f improve closest_time_to_point accuracy 2026-03-17 11:19:33 -07:00
f8996c958c integration test 2026-03-17 09:35:50 -07:00
f91fcf6b6f fix huge bvh node at end 2026-03-17 09:35:50 -07:00
4593514954 fix implementation mistake 2026-03-16 09:19:05 -07:00
31a3e31e70 avoid ugly .0 tuple access 2026-03-14 18:26:02 -07:00
4873e0298c discard unnecessary comment information 2026-03-14 09:36:46 -07:00
637fb38131 factor in world offset 2026-03-13 18:38:24 -07:00
ae624f90dc bvh demo 2026-03-13 10:56:04 -07:00
1d17e6acf0 add Bvh 2026-03-13 10:56:04 -07:00
a53cf8a8c7 delete get_angles 2026-03-13 10:40:40 -07:00
6 changed files with 135 additions and 30 deletions

9
Cargo.lock generated
View File

@@ -858,6 +858,15 @@ dependencies = [
"hashbrown 0.16.1",
]
[[package]]
name = "integration-tests"
version = "0.1.0"
dependencies = [
"strafesnet_common",
"strafesnet_roblox_bot_file",
"strafesnet_roblox_bot_player",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"

View File

@@ -1,5 +1,6 @@
[workspace]
members = [
"integration-tests",
"lib",
"native-player",
"video-encoder",

View File

@@ -0,0 +1,9 @@
[package]
name = "integration-tests"
version = "0.1.0"
edition = "2024"
[dependencies]
strafesnet_common.workspace = true
strafesnet_roblox_bot_file.workspace = true
strafesnet_roblox_bot_player.workspace = true

View File

@@ -0,0 +1,25 @@
use strafesnet_roblox_bot_file::v0;
use strafesnet_roblox_bot_player::{bot,bvh,head};
use head::Time as PlaybackTime;
use strafesnet_common::session::Time as SessionTime;
fn main(){
let bot=include_bytes!("../../web-demo/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d.qbot");
let timelines=v0::read_all_to_block(std::io::Cursor::new(bot)).unwrap();
let bot=bot::CompleteBot::new(timelines);
let bvh=bvh::Bvh::new(&bot);
// sample the position at 0.24s
let mut playback0=head::PlaybackHead::new(&bot,SessionTime::ZERO);
for i in 0..10{
let sample_time=PlaybackTime::from_millis(6543+1*i);
playback0.set_time(&bot,SessionTime::ZERO,sample_time);
let pos=playback0.get_position(&bot,SessionTime::ZERO);
// get the closest time on the timeline (convert to PlaybackTime which starts at 0)
let closest_time=bot.playback_time(bvh.closest_time_to_point(&bot,pos).unwrap());
println!("time={sample_time} closest_time={closest_time}");
}
// let mut playback1=head::PlaybackHead::new(&bot,SessionTime::ZERO);
// playback1.set_time(&bot,SessionTime::ZERO,sample_time);
}

View File

@@ -2,12 +2,16 @@ use core::ops::Range;
use strafesnet_common::aabb::Aabb;
use strafesnet_common::bvh::generate_bvh;
use strafesnet_common::integer::vec3;
use strafesnet_common::integer::Fixed;
use strafesnet_common::integer::{Fixed,Planar64};
use strafesnet_common::physics::Time as PhysicsTime;
use crate::bot::CompleteBot;
use strafesnet_roblox_bot_file::v0;
const MAX_SLICE_LEN:usize=16;
struct EventSlice(Range<usize>);
struct EventSlice{
slice:Range<usize>,
inclusive:bool,
}
pub struct Bvh{
bvh:strafesnet_common::bvh::BvhNode<EventSlice>,
@@ -16,7 +20,7 @@ pub struct Bvh{
impl Bvh{
pub fn new(bot:&CompleteBot)->Self{
let output_events=&bot.timelines().output_events;
// iterator over the event timeline and capture slizes of contiguous output events.
// iterator over the event timeline and capture slices of contiguous output events.
// create an Aabb for each slice and then generate a BVH.
let mut bvh_nodes=Vec::new();
let it=output_events
@@ -25,9 +29,9 @@ impl Bvh{
// find discontinuities
.filter(|&(_,[event0,event1])|
event0.time==event1.time&&!(
event0.event.position.x==event0.event.position.x
&&event0.event.position.y==event0.event.position.y
&&event0.event.position.z==event0.event.position.z
event0.event.position.x==event1.event.position.x
&&event0.event.position.y==event1.event.position.y
&&event0.event.position.z==event1.event.position.z
)
);
@@ -35,33 +39,31 @@ impl Bvh{
let mut push_slices=|index:usize|{
let len=index-last_index;
let count=len.div_ceil(MAX_SLICE_LEN);
let slice_len=len/count;
let slice_len=MAX_SLICE_LEN;
bvh_nodes.reserve(count);
// 0123456789
// last_index=0
// split_index=9
// index=10
// len=10
// count=3
// node_len=4
// split into groups of 4
// split into groups of MAX_SLICE_LEN=4
// [0123][4567][89]
let mut push_slice=|slice:Range<usize>|{
let mut push_slice=|slice:Range<usize>,inclusive:bool|{
let mut aabb=Aabb::default();
for event in &output_events[slice.start..slice.end]{
aabb.grow(vec3::try_from_f32_array([event.event.position.x,event.event.position.y,event.event.position.z]).unwrap());
}
bvh_nodes.push((EventSlice(slice),aabb));
if inclusive{
let event=&output_events[slice.end];
aabb.grow(vec3::try_from_f32_array([event.event.position.x,event.event.position.y,event.event.position.z]).unwrap());
}
bvh_nodes.push((EventSlice{slice,inclusive},aabb));
};
// push fixed-size groups
for i in 0..count-1{
push_slice((last_index+i*slice_len)..(last_index+(i+1)*slice_len));
push_slice((last_index+i*slice_len)..(last_index+(i+1)*slice_len),true);
}
// push last group which may be shorter
push_slice((last_index+(count-1)*slice_len)..index);
push_slice((last_index+(count-1)*slice_len)..index,false);
last_index=index;
};
// find discontinuities (teleports) and avoid forming a nvh node across them
// find discontinuities (teleports) and avoid forming a bvh node across them
for (split_index,_) in it{
// we want to use the index of event1
push_slices(split_index+1);
@@ -72,30 +74,89 @@ impl Bvh{
Self{bvh}
}
/// Find the exact timestamp on the bot timeline that is closest to the given point.
pub fn closest_time_to_point(&self,bot:&CompleteBot,point:glam::Vec3)->Option<PhysicsTime>{
pub fn closest_time_to_point<'a>(&self,bot:&'a CompleteBot,point:glam::Vec3)->Option<PhysicsTime>{
let point=point+bot.world_offset();
let start_point=vec3::try_from_f32_array(point.to_array()).unwrap();
let output_events=&bot.timelines().output_events;
// grow a sphere starting at start_point until we find the closest point on the bot output events
let intersect_leaf=|leaf:&EventSlice|{
let intersect_leaf=|event_slice:&EventSlice|{
// calculate the distance to the leaf contents
output_events[leaf.0.start..leaf.0.end].iter().map(|event|{
let mut best_distance=output_events[event_slice.slice.start..event_slice.slice.end].iter().map(|event|{
let p=event.event.position;
let p=vec3::try_from_f32_array([p.x,p.y,p.z]).unwrap();
(start_point-p).length_squared()
}).min()
}).min()?;
let mut prev_event=&output_events[event_slice.slice.start];
let mut f=|event:&'a v0::Timed<v0::OutputEvent>|{
let p0=vec3::try_from_f32_array([prev_event.event.position.x,prev_event.event.position.y,prev_event.event.position.z]).unwrap();
let p1=vec3::try_from_f32_array([event.event.position.x,event.event.position.y,event.event.position.z]).unwrap();
let d=p1-p0;
let d0=p0.dot(d);
let d1=p1.dot(d);
let sp_d=start_point.dot(d);
// must be on the segment
if d0<sp_d&&sp_d<d1{
let t0=d1-sp_d;
let t1=sp_d-d0;
let dt=d1-d0;
let distance=(((p0*t0+p1*t1)/dt).divide().wrap_1()-start_point).length_squared();
if distance<best_distance{
best_distance=distance;
}
}
prev_event=event;
};
for event in &output_events[event_slice.slice.start+1..event_slice.slice.end]{
f(event);
}
if event_slice.inclusive{
f(&output_events[event_slice.slice.end]);
}
Some(best_distance)
};
let intersect_aabb=|aabb:&Aabb|{
// calculate the distance to the aabb
let clamped_point=start_point.min(aabb.max()).max(aabb.min());
Some((start_point-clamped_point).length_squared())
};
let (_,leaf)=self.bvh.traverse(start_point,Fixed::ZERO,Fixed::MAX,intersect_leaf,intersect_aabb)?;
let closest_event=output_events[leaf.0.start..leaf.0.end].iter().min_by_key(|&event|{
let (_,event_slice)=self.bvh.traverse(start_point,Fixed::ZERO,Fixed::MAX,intersect_leaf,intersect_aabb)?;
// find time at the closest point
let (best_time,mut best_distance)=output_events[event_slice.slice.start..event_slice.slice.end].iter().map(|event|{
let p=event.event.position;
let p=vec3::try_from_f32_array([p.x,p.y,p.z]).unwrap();
(start_point-p).length_squared()
})?;
// TODO: project start_point onto the edges connected to the closest_event and return the true time
crate::time::from_float(closest_event.time).ok()
(event.time,(start_point-p).length_squared())
}).min_by_key(|&(_,distance)|distance)?;
let mut best_time=crate::time::from_float(best_time).unwrap();
let mut prev_event=&output_events[event_slice.slice.start];
let mut f=|event:&'a v0::Timed<v0::OutputEvent>|{
let p0=vec3::try_from_f32_array([prev_event.event.position.x,prev_event.event.position.y,prev_event.event.position.z]).unwrap();
let p1=vec3::try_from_f32_array([event.event.position.x,event.event.position.y,event.event.position.z]).unwrap();
let d=p1-p0;
let d0=p0.dot(d);
let d1=p1.dot(d);
let sp_d=start_point.dot(d);
// must be on the segment
if d0<sp_d&&sp_d<d1{
let t0=d1-sp_d;
let t1=sp_d-d0;
let dt=d1-d0;
let distance=(((p0*t0+p1*t1)/dt).divide().wrap_1()-start_point).length_squared();
if distance<best_distance{
best_distance=distance;
let p0:Planar64=prev_event.time.try_into().unwrap();
let p1:Planar64=event.time.try_into().unwrap();
best_time=((p0*t0+p1*t1)/dt).into();
}
}
prev_event=event;
};
for event in &output_events[event_slice.slice.start+1..event_slice.slice.end]{
f(event);
}
if event_slice.inclusive{
f(&output_events[event_slice.slice.end]);
}
Some(best_time)
}
}

View File

@@ -121,7 +121,7 @@ impl PlaybackHead{
}
pub fn get_position(&self,bot:&CompleteBot,time:SessionTime)->glam::Vec3{
let interp=self.interpolate_output(bot,time);
interp.position()
interp.position()-bot.world_offset()
}
pub fn get_velocity(&self,bot:&CompleteBot,time:SessionTime)->glam::Vec3{
let interp=self.interpolate_output(bot,time);