mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Load embedded retail collision and decode unsigned vertex offsets
This commit is contained in:
1 parent
63d9b7d269
commit
01ebd215de
15 files changed
+894
-74
No files matched your search
@@ -4,7 +4,7 @@
|
||||
//! determine the physical result, in world-triangle then moving-volume order.
|
||||
pub mod query_metadata;
|
||||
use crate::math::Vector3;
|
||||
use query_metadata::QueryMetadata;
|
||||
use query_metadata::{Bounds, QueryMetadata};
|
||||
|
||||
use super::{
|
||||
board::BodyId,
|
||||
@@ -105,11 +105,17 @@ pub struct BoardWorld {
|
||||
query_metadata: Option<QueryMetadata>,
|
||||
contacts: Vec<BoardCollision>,
|
||||
buffer: ContactBuffer,
|
||||
maximum_fatness: f32,
|
||||
}
|
||||
|
||||
impl BoardWorld {
|
||||
pub fn new(triangles: Vec<WorldTriangle>) -> Self {
|
||||
let maximum_fatness = triangles
|
||||
.iter()
|
||||
.map(|t| t.triangle.fatness)
|
||||
.fold(0., f32::max);
|
||||
Self {
|
||||
maximum_fatness,
|
||||
triangles,
|
||||
query_metadata: None,
|
||||
contacts: Vec::new(),
|
||||
@@ -147,6 +153,44 @@ impl BoardWorld {
|
||||
&self.triangles
|
||||
}
|
||||
|
||||
/// Conservative cluster culling, retaining canonical traversal order and
|
||||
/// every triangle in intersecting clusters. Narrow-phase remains unchanged.
|
||||
pub fn candidate_ranges(&self, bounds: Option<Bounds>) -> Vec<std::ops::Range<usize>> {
|
||||
let (Some(metadata), Some(bounds)) = (&self.query_metadata, bounds) else {
|
||||
return vec![0..self.triangles.len()];
|
||||
};
|
||||
let scale = [
|
||||
bounds.min.x,
|
||||
bounds.min.y,
|
||||
bounds.min.z,
|
||||
bounds.max.x,
|
||||
bounds.max.y,
|
||||
bounds.max.z,
|
||||
]
|
||||
.into_iter()
|
||||
.map(f32::abs)
|
||||
.fold(1., f32::max);
|
||||
let bounds = bounds.expanded(self.maximum_fatness + scale * (8. * f32::EPSILON));
|
||||
metadata
|
||||
.meshes
|
||||
.iter()
|
||||
.filter(|m| m.local_bounds.overlaps(bounds))
|
||||
.map(|m| m.triangle_range.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn line_candidates(
|
||||
&self,
|
||||
start: Vector3,
|
||||
end: Vector3,
|
||||
radius: f32,
|
||||
) -> impl Iterator<Item = (usize, &WorldTriangle)> {
|
||||
self.candidate_ranges(Bounds::from_points([start, end]).map(|b| b.expanded(radius)))
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|i| (i, &self.triangles[i]))
|
||||
}
|
||||
|
||||
/// Board wheel segments use zero query radius; rounded world triangles
|
||||
/// still select the native swept branch through their own fatness.
|
||||
pub fn query_thin_line(
|
||||
@@ -170,7 +214,7 @@ impl BoardWorld {
|
||||
}
|
||||
let direction = Vector3::new(end.x - start.x, end.y - start.y, end.z - start.z);
|
||||
let mut nearest: Option<WorldLineHit> = None;
|
||||
for entry in &self.triangles {
|
||||
for (_, entry) in self.line_candidates(start, end, radius) {
|
||||
let mut geometry = TriangleLineHit {
|
||||
position: Vector3::ZERO,
|
||||
normal: Vector3::ZERO,
|
||||
@@ -226,8 +270,8 @@ impl BoardWorld {
|
||||
}
|
||||
|
||||
/// World-space shapes come from the live part poses and authored children.
|
||||
/// The host's small level visits every pair, preserving source query order
|
||||
/// without a broad-phase bound that could drop valid predictive contacts.
|
||||
/// Cluster bounds include shape radii and the maximum predictive padding.
|
||||
/// Candidate pairs retain source query order.
|
||||
pub fn query_primitives(
|
||||
&mut self,
|
||||
volumes: &[BoardWorldVolume],
|
||||
@@ -242,11 +286,21 @@ impl BoardWorld {
|
||||
self.buffer.capacity = retention.capacity;
|
||||
self.buffer.distance_squared_threshold = retention.duplicate_distance_squared;
|
||||
self.buffer.deferred_reduction = u8::from(retention.deferred_reduction);
|
||||
let padding = query.volume_padding.max(0.) + query.maximum_separating_distance.max(0.);
|
||||
let bounds: Option<Vec<_>> = volumes
|
||||
.iter()
|
||||
.map(|v| primitive_bounds(v.primitive))
|
||||
.collect();
|
||||
let bounds = bounds
|
||||
.and_then(|b| Bounds::from_points(b.iter().flat_map(|b| [b.min, b.max])))
|
||||
.map(|b| b.expanded(padding));
|
||||
let ranges = self.candidate_ranges(bounds);
|
||||
let output = &mut self.contacts;
|
||||
let mut publish = |records: &[ContactRecord]| {
|
||||
output.extend(records.iter().map(collision_from_record));
|
||||
};
|
||||
for entry in &self.triangles {
|
||||
for index in ranges.into_iter().flatten() {
|
||||
let entry = &self.triangles[index];
|
||||
for volume in volumes {
|
||||
let Some(manifold) = primitive_triangle_world_contacts(
|
||||
volume.primitive,
|
||||
@@ -292,6 +346,43 @@ impl BoardWorld {
|
||||
}
|
||||
}
|
||||
|
||||
fn primitive_bounds(primitive: ContactPrimitive) -> Option<Bounds> {
|
||||
let (center, radius) = match primitive {
|
||||
ContactPrimitive::Sphere(s) => (s.center, s.radius),
|
||||
ContactPrimitive::Capsule {
|
||||
center,
|
||||
axis,
|
||||
half_length,
|
||||
radius,
|
||||
} => (
|
||||
center,
|
||||
radius + half_length.abs() * (axis.x.abs() + axis.y.abs() + axis.z.abs()),
|
||||
),
|
||||
ContactPrimitive::RoundedBox {
|
||||
center,
|
||||
basis,
|
||||
half_extents,
|
||||
radius,
|
||||
} => {
|
||||
let half = [half_extents.x, half_extents.y, half_extents.z];
|
||||
let extent: f32 = basis
|
||||
.columns
|
||||
.iter()
|
||||
.zip(half)
|
||||
.map(|(axis, h)| h.abs() * axis.iter().map(|v| v.abs()).sum::<f32>())
|
||||
.sum();
|
||||
(center, radius + extent)
|
||||
}
|
||||
ContactPrimitive::Triangle(t) => {
|
||||
return Bounds::from_points(t.vertices).map(|b| b.expanded(t.fatness));
|
||||
}
|
||||
};
|
||||
if !radius.is_finite() {
|
||||
return None;
|
||||
}
|
||||
Bounds::from_points([center]).map(|b| b.expanded(radius.abs()))
|
||||
}
|
||||
|
||||
/// Retention reads only contact header geometry, body IDs and material/tag.
|
||||
/// BoardStep builds the actual body workspaces after this frame's force queue
|
||||
/// has been applied, preventing stale acceleration copies in solver contacts.
|
||||
|
||||
@@ -16,6 +16,28 @@ pub struct Bounds {
|
||||
pub max: Vector3,
|
||||
}
|
||||
impl Bounds {
|
||||
pub fn expanded(self, padding: f32) -> Self {
|
||||
Self {
|
||||
min: Vector3::new(
|
||||
self.min.x - padding,
|
||||
self.min.y - padding,
|
||||
self.min.z - padding,
|
||||
),
|
||||
max: Vector3::new(
|
||||
self.max.x + padding,
|
||||
self.max.y + padding,
|
||||
self.max.z + padding,
|
||||
),
|
||||
}
|
||||
}
|
||||
pub fn overlaps(self, other: Self) -> bool {
|
||||
!(self.max.x < other.min.x
|
||||
|| self.min.x > other.max.x
|
||||
|| self.max.y < other.min.y
|
||||
|| self.min.y > other.max.y
|
||||
|| self.max.z < other.min.z
|
||||
|| self.min.z > other.max.z)
|
||||
}
|
||||
pub fn from_points(points: impl IntoIterator<Item = Vector3>) -> Option<Self> {
|
||||
let mut points = points.into_iter();
|
||||
let first = points.next()?;
|
||||
|
||||
@@ -90,6 +90,121 @@ fn close(a: f32, b: f32) {
|
||||
assert!((a - b).abs() < 2e-6, "{a} != {b}");
|
||||
}
|
||||
|
||||
fn clustered(triangles: Vec<WorldTriangle>) -> BoardWorld {
|
||||
use crate::physics::board_world::query_metadata::{
|
||||
Bounds, QueryMesh, QueryMetadata, QueryPool,
|
||||
};
|
||||
let metadata = QueryMetadata {
|
||||
packed_surfaces: vec![0; triangles.len()],
|
||||
meshes: triangles
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, t)| QueryMesh {
|
||||
triangle_range: i..i + 1,
|
||||
local_to_world: RetailAffineTransform::IDENTITY,
|
||||
world_to_local: RetailAffineTransform::IDENTITY,
|
||||
local_bounds: Bounds::from_points(t.triangle.vertices).unwrap(),
|
||||
matching_group: -1,
|
||||
pool: QueryPool::Ground,
|
||||
})
|
||||
.collect(),
|
||||
static_edges: vec![],
|
||||
island_flags: 0,
|
||||
};
|
||||
BoardWorld::with_query_metadata(triangles, metadata).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_culling_preserves_swept_hits_and_equal_hit_order() {
|
||||
let mut rounded = triangle();
|
||||
rounded.triangle.fatness = 0.5;
|
||||
let mut later = rounded;
|
||||
later.tag = 999;
|
||||
let indexed = clustered(vec![rounded, later]);
|
||||
let full = BoardWorld::new(vec![rounded, later]);
|
||||
for x in [-100., -10., -1., 0., 9., 100.] {
|
||||
for radius in [0., 0.2, 1.] {
|
||||
let start = Vector3::new(x, 2., 0.);
|
||||
let end = Vector3::new(x, -2., 0.);
|
||||
assert_eq!(
|
||||
format!("{:?}", indexed.query_swept_line(start, end, radius)),
|
||||
format!("{:?}", full.query_swept_line(start, end, radius))
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
indexed
|
||||
.query_thin_line(Vector3::new(0., 2., 0.), Vector3::new(0., -2., 0.))
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.tag,
|
||||
42
|
||||
);
|
||||
assert_eq!(
|
||||
indexed
|
||||
.line_candidates(Vector3::new(100., 2., 0.), Vector3::new(100., -2., 0.), 0.)
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_culling_preserves_predictive_contacts_for_every_shape() {
|
||||
let mut floor = triangle();
|
||||
floor.triangle.fatness = 0.1;
|
||||
let mut indexed = clustered(vec![floor]);
|
||||
let mut full = BoardWorld::new(vec![floor]);
|
||||
let mut settings = config();
|
||||
settings.query.volume_padding = 0.05;
|
||||
settings.query.maximum_separating_distance = 0.8;
|
||||
for y in [0.1, 0.4, 0.9, 2.] {
|
||||
let center = Vector3::new(0., y, 0.);
|
||||
let basis = Basis3 {
|
||||
columns: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]],
|
||||
};
|
||||
let mut moving_triangle = triangle().triangle;
|
||||
for p in &mut moving_triangle.vertices {
|
||||
p.y += y;
|
||||
}
|
||||
for primitive in [
|
||||
ContactPrimitive::Sphere(Sphere {
|
||||
center,
|
||||
radius: 0.2,
|
||||
}),
|
||||
ContactPrimitive::Capsule {
|
||||
center,
|
||||
axis: Vector3::new(1., 0., 0.),
|
||||
half_length: 0.4,
|
||||
radius: 0.2,
|
||||
},
|
||||
ContactPrimitive::RoundedBox {
|
||||
center,
|
||||
basis,
|
||||
half_extents: Vector3::new(0.4, 0.2, 0.3),
|
||||
radius: 0.05,
|
||||
},
|
||||
ContactPrimitive::Triangle(moving_triangle),
|
||||
] {
|
||||
let volumes = [BoardWorldVolume {
|
||||
body: CollisionBody::Board(BodyId::ORDER[0]),
|
||||
primitive,
|
||||
linear_velocity: Vector3::new(0., -60., 0.),
|
||||
material: settings.material,
|
||||
}];
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{:?}",
|
||||
indexed.query_primitives(&volumes, settings.query, settings.retention)
|
||||
),
|
||||
format!(
|
||||
"{:?}",
|
||||
full.query_primitives(&volumes, settings.query, settings.retention)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finite_world_geometry_produces_four_ordered_wheel_contacts_with_combined_materials() {
|
||||
let mut world = BoardWorld::new(vec![triangle()]);
|
||||
@@ -241,7 +356,11 @@ fn attached_volume_contact_reaches_its_actual_solver_body() {
|
||||
let mut world = BoardWorld::new(vec![triangle(), triangle()]);
|
||||
let config = config();
|
||||
let contacts = world.query_primitives(&[volume], config.query, config.retention);
|
||||
assert_eq!(contacts.len(), 1, "retention must preserve attached body identity");
|
||||
assert_eq!(
|
||||
contacts.len(),
|
||||
1,
|
||||
"retention must preserve attached body identity"
|
||||
);
|
||||
assert_eq!(contacts[0].body_a, CollisionBody::Attached(0));
|
||||
let settings = BoardStepSettings {
|
||||
simulation: RetailSimulationStep::fixed_60_hz(30, 0.001, Vector3::ZERO),
|
||||
@@ -250,12 +369,20 @@ fn attached_volume_contact_reaches_its_actual_solver_body() {
|
||||
truck_dynamics: retail_truck_drive_dynamics(RetailTruckDriveSettings::STOCK),
|
||||
force_point_y_offset: 0.0,
|
||||
};
|
||||
board.advance_attached(contacts, [0.0; 2], settings, AttachedStep {
|
||||
bodies: vec![&mut attached],
|
||||
contacts: &mut [],
|
||||
joints: &mut [],
|
||||
drives: &mut [],
|
||||
});
|
||||
board.advance_attached(
|
||||
contacts,
|
||||
[0.0; 2],
|
||||
settings,
|
||||
AttachedStep {
|
||||
bodies: vec![&mut attached],
|
||||
contacts: &mut [],
|
||||
joints: &mut [],
|
||||
drives: &mut [],
|
||||
},
|
||||
);
|
||||
assert!(attached.rates.linear_velocity.y > before + 0.5);
|
||||
assert!(board.contact_reports().is_empty(), "skater contacts are not board observations");
|
||||
assert!(
|
||||
board.contact_reports().is_empty(),
|
||||
"skater contacts are not board observations"
|
||||
);
|
||||
}
|
||||
@@ -2,21 +2,42 @@ fn main() {
|
||||
let mut failed = false;
|
||||
for path in std::env::args_os().skip(1) {
|
||||
match skate_data::skate_map::SkateMap::load(std::path::Path::new(&path)) {
|
||||
Ok(m) => println!(
|
||||
"{} v{}: vertices={} triangles={} collision={} textures={} rails={} doors={} lights={} routes={} spawn={:?} heading={}",
|
||||
m.name,
|
||||
m.version,
|
||||
m.geometry.vertices.len(),
|
||||
m.geometry.indices.len() / 3,
|
||||
m.geometry.collision.len(),
|
||||
m.textures.len(),
|
||||
m.rails.len(),
|
||||
m.doors.len(),
|
||||
m.lights.len(),
|
||||
m.routes.len(),
|
||||
m.spawn,
|
||||
m.heading
|
||||
),
|
||||
Ok(m) => {
|
||||
println!(
|
||||
"{} v{}: vertices={} triangles={} collision={} textures={} rails={} doors={} lights={} routes={} spawn={:?} heading={}",
|
||||
m.name,
|
||||
m.version,
|
||||
m.geometry.vertices.len(),
|
||||
m.geometry.indices.len() / 3,
|
||||
m.geometry.collision.len(),
|
||||
m.textures.len(),
|
||||
m.rails.len(),
|
||||
m.doors.len(),
|
||||
m.lights.len(),
|
||||
m.routes.len(),
|
||||
m.spawn,
|
||||
m.heading
|
||||
);
|
||||
for e in &m.extensions {
|
||||
println!(
|
||||
"extension {} schema={} decoded_bytes={}",
|
||||
String::from_utf8_lossy(&e.tag),
|
||||
e.schema,
|
||||
e.payload.len()
|
||||
);
|
||||
}
|
||||
let mut angles = [0_usize; 32];
|
||||
let mut native = 0;
|
||||
for t in &m.geometry.collision {
|
||||
if let Some(edges) = t.native_edges {
|
||||
native += 1;
|
||||
for e in edges {
|
||||
angles[usize::from(e & 31)] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("native_collision_triangles={native} angle_histogram={angles:?}");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
failed = true;
|
||||
|
||||
@@ -13,3 +13,4 @@ pub mod physics_skeleton;
|
||||
mod sha256;
|
||||
pub mod state_graph;
|
||||
pub mod skate_map;
|
||||
pub mod retail_collision;
|
||||
@@ -0,0 +1,203 @@
|
||||
//! Embedded RWCM v1 / RWCMSET1 archives. Cluster coordinates are already in
|
||||
//! world space. Never weld them or replace their feature bytes with adjacency.
|
||||
//! Layout reference: SK8R15 tools/vanilla_map_extraction/tools/retail_collision_mesh.py.
|
||||
//! Its signed-16 interpretation is incorrect: native GetVertex82AC79D0
|
||||
//! zero-extends the offsets before signed saturating addition to the base.
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct RetailTriangle {
|
||||
pub points: [[f32; 3]; 3],
|
||||
pub edges: Option<[u8; 3]>,
|
||||
pub surface: u16,
|
||||
pub group: u16,
|
||||
pub one_sided: bool,
|
||||
}
|
||||
|
||||
fn span(data: &[u8], at: usize, n: usize) -> Result<&[u8], String> {
|
||||
data.get(at..at.checked_add(n).ok_or("RWCM offset overflow")?)
|
||||
.ok_or_else(|| format!("Truncated RWCM at {at} (need {n})"))
|
||||
}
|
||||
fn be16(data: &[u8], at: usize) -> Result<u16, String> {
|
||||
Ok(u16::from_be_bytes(span(data, at, 2)?.try_into().unwrap()))
|
||||
}
|
||||
fn be32(data: &[u8], at: usize) -> Result<u32, String> {
|
||||
Ok(u32::from_be_bytes(span(data, at, 4)?.try_into().unwrap()))
|
||||
}
|
||||
fn le32(data: &[u8], at: &mut usize) -> Result<usize, String> {
|
||||
let value = u32::from_le_bytes(span(data, *at, 4)?.try_into().unwrap());
|
||||
*at += 4;
|
||||
Ok(value as usize)
|
||||
}
|
||||
fn id(data: &[u8], at: &mut usize, width: u8) -> Result<u16, String> {
|
||||
let value = match width {
|
||||
1 => u16::from(span(data, *at, 1)?[0]),
|
||||
2 => u16::from_le_bytes(span(data, *at, 2)?.try_into().unwrap()),
|
||||
_ => return Err(format!("Unsupported RWCM ID width {width}")),
|
||||
};
|
||||
*at += usize::from(width);
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Visits complete clusters in archive order, retaining only one decoded
|
||||
/// cluster at a time. The callback can build its own spatial query index.
|
||||
pub fn visit_clusters(
|
||||
data: &[u8],
|
||||
mut visit: impl FnMut(&str, &[RetailTriangle]) -> Result<(), String>,
|
||||
) -> Result<usize, String> {
|
||||
if span(data, 0, 8)? != b"RWCMSET1" {
|
||||
return Err("Invalid RWCMSET1 magic".into());
|
||||
}
|
||||
let mut at = 8;
|
||||
let count = le32(data, &mut at)?;
|
||||
if count == 0 || count > data.len() / 105 {
|
||||
return Err("Invalid RWCM mesh count".into());
|
||||
}
|
||||
let mut total = 0;
|
||||
for _ in 0..count {
|
||||
let size = le32(data, &mut at)?;
|
||||
if size == 0 || size > 4096 {
|
||||
return Err("Invalid RWCM mesh name length".into());
|
||||
}
|
||||
let name =
|
||||
std::str::from_utf8(span(data, at, size)?).map_err(|_| "Invalid RWCM mesh name")?;
|
||||
at += size;
|
||||
let size = le32(data, &mut at)?;
|
||||
let mesh = span(data, at, size)?;
|
||||
at += size;
|
||||
total += mesh_clusters(mesh, &mut |triangles| visit(name, triangles))
|
||||
.map_err(|e| format!("RWCM mesh {name:?}: {e}"))?;
|
||||
}
|
||||
if at != data.len() {
|
||||
return Err("RWCM archive has trailing bytes".into());
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn mesh_clusters(
|
||||
data: &[u8],
|
||||
visit: &mut impl FnMut(&[RetailTriangle]) -> Result<(), String>,
|
||||
) -> Result<usize, String> {
|
||||
span(data, 0, 96)?;
|
||||
let size = be32(data, 80)? as usize;
|
||||
if size < 96 || size > data.len() {
|
||||
return Err("Invalid RWCM mesh size".into());
|
||||
}
|
||||
let data = &data[..size];
|
||||
let table = be32(data, 52)? as usize;
|
||||
let count = be32(data, 64)? as usize;
|
||||
let granularity = f32::from_bits(be32(data, 56)?);
|
||||
if !granularity.is_finite() || granularity <= 0. {
|
||||
return Err("Invalid RWCM granularity".into());
|
||||
}
|
||||
if table < 96 || count == 0 || count > data.len() / 4 {
|
||||
return Err("Invalid RWCM cluster table".into());
|
||||
}
|
||||
span(data, table, count * 4)?;
|
||||
let one_sided = be16(data, 60)? & 0x10 != 0;
|
||||
let mut previous_end = table + count * 4;
|
||||
let mut total = 0;
|
||||
for i in 0..count {
|
||||
let offset = be32(data, table + i * 4)? as usize;
|
||||
if offset < previous_end {
|
||||
return Err("Overlapping RWCM clusters".into());
|
||||
}
|
||||
let size = be16(data, offset + 8)? as usize;
|
||||
if size < 16 {
|
||||
return Err("Invalid RWCM cluster size".into());
|
||||
}
|
||||
let cluster = span(data, offset, size)?;
|
||||
previous_end = offset + size;
|
||||
let triangles = decode_cluster(cluster, granularity, data[62], data[63], one_sided)?;
|
||||
total += triangles.len();
|
||||
visit(&triangles)?;
|
||||
}
|
||||
if total != be32(data, 40)? as usize {
|
||||
return Err("RWCM triangle count mismatch".into());
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn decode_cluster(
|
||||
data: &[u8],
|
||||
granularity: f32,
|
||||
group_width: u8,
|
||||
surface_width: u8,
|
||||
one_sided: bool,
|
||||
) -> Result<Vec<RetailTriangle>, String> {
|
||||
let unit_count = usize::from(be16(data, 0)?);
|
||||
let unit_start = (usize::from(be16(data, 4)?) + 1) * 16;
|
||||
let units = span(data, unit_start, usize::from(be16(data, 2)?))?;
|
||||
let vertex_data = span(data, 0, unit_start)?;
|
||||
let mut vertices = Vec::with_capacity(usize::from(data[10]));
|
||||
for i in 0..usize::from(data[10]) {
|
||||
let mut p = [0.; 3];
|
||||
for axis in 0..3 {
|
||||
p[axis] = match data[12] {
|
||||
0 => f32::from_bits(be32(vertex_data, 16 + i * 16 + axis * 4)?),
|
||||
1 => {
|
||||
let base = be32(vertex_data, 16 + axis * 4)? as i32;
|
||||
// vmrghh with a zero vector produces unsigned halfwords;
|
||||
// vaddsws then adds these to the signed base with saturation.
|
||||
let delta = be16(vertex_data, 28 + i * 6 + axis * 2)?;
|
||||
let integer = base.saturating_add(i32::from(delta));
|
||||
// Float conversion precedes multiplication in GetVertex.
|
||||
integer as f32 * granularity
|
||||
}
|
||||
2 => (be32(vertex_data, 16 + i * 12 + axis * 4)? as i32) as f32 * granularity,
|
||||
mode => return Err(format!("Unsupported RWCM vertex compression {mode}")),
|
||||
};
|
||||
if !p[axis].is_finite() {
|
||||
return Err("Non-finite RWCM vertex".into());
|
||||
}
|
||||
}
|
||||
vertices.push(p);
|
||||
}
|
||||
let mut at = 0;
|
||||
let mut triangles = Vec::new();
|
||||
for _ in 0..unit_count {
|
||||
let flags = span(units, at, 1)?[0];
|
||||
at += 1;
|
||||
if flags & 15 != 1 {
|
||||
return Err(format!(
|
||||
"Unsupported RWCM unit type {}; triangle units are required",
|
||||
flags & 15
|
||||
));
|
||||
}
|
||||
let indices = span(units, at, 3)?;
|
||||
at += 3;
|
||||
let mut points = [[0.; 3]; 3];
|
||||
for i in 0..3 {
|
||||
points[i] = *vertices
|
||||
.get(usize::from(indices[i]))
|
||||
.ok_or("RWCM missing vertex")?;
|
||||
}
|
||||
let edges = if flags & 0x20 != 0 {
|
||||
let e = span(units, at, 3)?.try_into().unwrap();
|
||||
at += 3;
|
||||
Some(e)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let group = if flags & 0x40 != 0 {
|
||||
id(units, &mut at, group_width)?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let surface = if flags & 0x80 != 0 {
|
||||
id(units, &mut at, surface_width)?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
triangles.push(RetailTriangle {
|
||||
points,
|
||||
edges,
|
||||
surface,
|
||||
group,
|
||||
one_sided,
|
||||
});
|
||||
}
|
||||
if at != units.len() {
|
||||
return Err("RWCM unit stream has trailing bytes".into());
|
||||
}
|
||||
Ok(triangles)
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,88 @@
|
||||
use skate_data::retail_collision::visit_clusters;
|
||||
|
||||
const FIXTURE: &[u8] = include_bytes!("fixtures/retail-collision.rwcmset");
|
||||
|
||||
#[test]
|
||||
fn compressed_offsets_zero_extend_across_the_sign_boundary() {
|
||||
let mut bytes = FIXTURE.to_vec();
|
||||
// Locate the second fixture mesh, which uses 16-bit offsets.
|
||||
let mut at = 12;
|
||||
let mut mesh = 0;
|
||||
for _ in 0..2 {
|
||||
let name_len = u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) as usize;
|
||||
at += 4 + name_len;
|
||||
let size = u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) as usize;
|
||||
mesh = at + 4;
|
||||
at = mesh + size;
|
||||
}
|
||||
let cluster = mesh + 160;
|
||||
let base = [-40000_i32, -32768, i32::MAX - 2];
|
||||
for axis in 0..3 {
|
||||
bytes[cluster + 16 + axis * 4..cluster + 20 + axis * 4]
|
||||
.copy_from_slice(&base[axis].to_be_bytes());
|
||||
}
|
||||
for (i, raw) in [0x7fff_u16, 0x8000, 0xffff].into_iter().enumerate() {
|
||||
for axis in 0..3 {
|
||||
bytes[cluster + 28 + i * 6 + axis * 2..cluster + 30 + i * 6 + axis * 2]
|
||||
.copy_from_slice(&raw.to_be_bytes());
|
||||
}
|
||||
}
|
||||
visit_clusters(&bytes, |name, triangles| {
|
||||
if name == "compression-1" {
|
||||
assert_eq!(
|
||||
triangles[0].points,
|
||||
[
|
||||
[-1808.25, -0.25, 536870912.],
|
||||
[-1808., 0., 536870912.],
|
||||
[6383.75, 8191.75, 536870912.],
|
||||
]
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_all_vertex_encodings_and_little_endian_unit_ids() {
|
||||
let mut clusters = 0;
|
||||
let total = visit_clusters(FIXTURE, |name, triangles| {
|
||||
assert_eq!(name, format!("compression-{clusters}"));
|
||||
assert_eq!(triangles.len(), 1);
|
||||
let t = triangles[0];
|
||||
let x = clusters as f32 * 10.;
|
||||
assert_eq!(t.points, [[x, 0., 0.], [x, 0., 1.], [x + 1., 0., 0.]]);
|
||||
assert_eq!(t.edges, Some([0x20, 0x42, 0x9a]));
|
||||
assert_eq!(t.surface, 0x4321);
|
||||
assert_eq!(t.group, 0x1234);
|
||||
assert!(t.one_sided);
|
||||
clusters += 1;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(total, 3);
|
||||
assert_eq!(clusters, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_every_truncated_prefix_and_trailing_bytes() {
|
||||
for end in 0..FIXTURE.len() {
|
||||
assert!(
|
||||
visit_clusters(&FIXTURE[..end], |_, _| Ok(())).is_err(),
|
||||
"prefix {end}"
|
||||
);
|
||||
}
|
||||
let mut bytes = FIXTURE.to_vec();
|
||||
bytes.push(0);
|
||||
assert!(visit_clusters(&bytes, |_, _| Ok(())).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires SKATE_RWCM_TEST_PATH pointing to a private archive"]
|
||||
fn private_archive_decodes_all_clusters() {
|
||||
let path = std::env::var("SKATE_RWCM_TEST_PATH").unwrap();
|
||||
let bytes = std::fs::read(path).unwrap();
|
||||
let count = visit_clusters(&bytes, |_, _| Ok(())).unwrap();
|
||||
assert!(count > 0);
|
||||
eprintln!("RWCM triangles decoded: {count}");
|
||||
}
|
||||
@@ -12,7 +12,7 @@ pub(super) fn line(world: &BoardWorld, start: [f32; 4], end: [f32; 4], radius: f
|
||||
let delta = Vector3::new(end[0] - start.x, end[1] - start.y, end[2] - start.z);
|
||||
let mut fraction = f32::MAX;
|
||||
let mut result = no_hit();
|
||||
for entry in world.triangles() {
|
||||
for (_, entry) in world.line_candidates(start, vector(end), radius) {
|
||||
let mut hit = TriangleLineHit { position: Vector3::ZERO, normal: Vector3::ZERO,
|
||||
fraction: 0.0, volume_parameter: [0.0; 3] };
|
||||
// Zero triangle fatness and zero material exclusion mask are native.
|
||||
|
||||
@@ -37,7 +37,7 @@ pub(super) fn line(
|
||||
let delta = Vector3::new(end[0] - start.x, end[1] - start.y, end[2] - start.z);
|
||||
let mut nearest = f32::MAX;
|
||||
let mut result = None;
|
||||
for (index, entry) in world.triangles().iter().enumerate() {
|
||||
for (index, entry) in world.line_candidates(start, vec3(end), radius) {
|
||||
let mut hit = TriangleLineHit {
|
||||
position: Vector3::ZERO,
|
||||
normal: Vector3::ZERO,
|
||||
@@ -76,7 +76,7 @@ pub(super) fn nearby(
|
||||
let minimum = std::array::from_fn(|i| center[i] - radius);
|
||||
let maximum = std::array::from_fn(|i| center[i] + radius);
|
||||
let mut triangles = Vec::with_capacity(64);
|
||||
for entry in world.triangles() {
|
||||
for (_, entry) in world.line_candidates(vec3(center), vec3(center), radius) {
|
||||
let vertices = entry.triangle.vertices.map(lanes);
|
||||
if triangle_box(vertices, minimum, maximum) {
|
||||
triangles.push(vertices);
|
||||
|
||||
@@ -105,7 +105,7 @@ fn query_trajectory(
|
||||
let direction = Vector3::new(end[0] - start[0], end[1] - start[1], end[2] - start[2]);
|
||||
let mut nearest = f32::MAX;
|
||||
let mut position = None;
|
||||
for triangle in world.triangles() {
|
||||
for (_, triangle) in world.line_candidates(origin, vector(end), radius) {
|
||||
let mut geometry = TriangleLineHit {
|
||||
position: Vector3::ZERO,
|
||||
normal: Vector3::ZERO,
|
||||
|
||||
@@ -21,16 +21,14 @@ use skate_data::skate_map::SkateMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) fn validate_runtime(map: &SkateMap) -> Result<(), String> {
|
||||
if map.geometry.collision.is_empty() {
|
||||
let archive = retail_archive(map)?;
|
||||
if map.geometry.collision.is_empty() && archive.is_none() {
|
||||
return Err("SKATE map has no collision geometry".into());
|
||||
}
|
||||
if map
|
||||
.geometry
|
||||
.collision
|
||||
.iter()
|
||||
.any(|t| t.native_edges.is_some())
|
||||
{
|
||||
return Err("SKATE native collision edge codes are preserved by the reader, but the compressed retail-edge decoder is not yet connected to this game's triangle adapter. Refusing to replace them with regenerated edges.".into());
|
||||
for triangle in map.geometry.collision.iter().filter(|_| archive.is_none()) {
|
||||
if let Some(edges) = triangle.native_edges {
|
||||
decode_native_edges(edges)?;
|
||||
}
|
||||
}
|
||||
if !map.doors.is_empty() {
|
||||
return Err(format!(
|
||||
@@ -41,6 +39,15 @@ pub(crate) fn validate_runtime(map: &SkateMap) -> Result<(), String> {
|
||||
}
|
||||
for extension in &map.extensions {
|
||||
let tag = String::from_utf8_lossy(&extension.tag);
|
||||
if extension.tag == *b"RWCM" {
|
||||
continue;
|
||||
}
|
||||
if extension.tag == *b"SKYB" && extension.schema == 1 {
|
||||
eprintln!(
|
||||
"SKATE LIMITATION: SKYB retail sky retained; using the map horizon until its shader adapter is available."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if extension.tag != *b"WMET" && extension.tag != *b"WCFG" && extension.tag != *b"BMAT" {
|
||||
return Err(format!(
|
||||
"SKATE extension {tag} schema {} is decoded but its runtime adapter is not implemented. Refusing to silently omit potentially required world geometry.",
|
||||
@@ -94,10 +101,122 @@ pub(crate) fn validate_runtime(map: &SkateMap) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// TU3 ClusteredMesh::GetUnitVolumes (82AC8A68): fdivs then fsubs,
|
||||
/// using the pi-squared word at 822F88D0. This is not acos/angle decoding.
|
||||
/// Bit 7 denotes an unmatched compiler edge and is not a triangle flag.
|
||||
fn decode_native_edges(edges: [u8; 3]) -> Result<(u32, [f32; 3]), String> {
|
||||
let mut flags = 1 | TriangleFeature::ONE_SIDED | TriangleFeature::USE_EDGE_COSINES;
|
||||
let mut cosines = [0.; 3];
|
||||
for (i, code) in edges.into_iter().enumerate() {
|
||||
let exponent = code & 0x1f;
|
||||
// The native signed 32-bit shift becomes negative at 28 and zero
|
||||
// above it. Reject those malformed codes instead of producing NaNs.
|
||||
if exponent >= 28 {
|
||||
return Err(format!(
|
||||
"Invalid SKATE native edge angle code {exponent} at corner {i}"
|
||||
));
|
||||
}
|
||||
cosines[i] = 1.0 - f32::from_bits(0x411d_e9e7) / ((8_u32 << exponent) as f32);
|
||||
flags |= u32::from(code & 0x20) << i;
|
||||
flags |= u32::from(code & 0x40) << (i + 3);
|
||||
}
|
||||
Ok((flags, cosines))
|
||||
}
|
||||
|
||||
fn retail_archive(map: &SkateMap) -> Result<Option<&[u8]>, String> {
|
||||
let mut archives = map.extensions.iter().filter(|e| e.tag == *b"RWCM");
|
||||
let Some(archive) = archives.next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if archive.schema != 1 || archives.next().is_some() {
|
||||
return Err("SKATE requires one RWCM extension with schema 1".into());
|
||||
}
|
||||
Ok(Some(&archive.payload))
|
||||
}
|
||||
|
||||
fn retail_collision_world(
|
||||
archive: &[u8],
|
||||
material: RetailContactMaterial,
|
||||
) -> Result<BoardWorld, String> {
|
||||
let mut triangles = Vec::new();
|
||||
let mut packed_surfaces = Vec::new();
|
||||
let mut meshes = Vec::new();
|
||||
let count = skate_data::retail_collision::visit_clusters(archive, |_, cluster| {
|
||||
// Preserve cluster order and partition further only for group filters.
|
||||
let mut cursor = 0;
|
||||
while cursor < cluster.len() {
|
||||
let group = cluster[cursor].group;
|
||||
let start = triangles.len();
|
||||
while cursor < cluster.len() && cluster[cursor].group == group {
|
||||
let source = cluster[cursor];
|
||||
let (flags, cosines) = match source.edges {
|
||||
Some(edges) => {
|
||||
let (mut flags, cosines) = decode_native_edges(edges)?;
|
||||
flags &= !TriangleFeature::ONE_SIDED;
|
||||
if source.one_sided {
|
||||
flags |= TriangleFeature::ONE_SIDED;
|
||||
}
|
||||
(flags, cosines)
|
||||
}
|
||||
// TriangleVolume ctor82AC7770 retains these defaults when
|
||||
// the unit has no edge data; the mesh sidedness is not read.
|
||||
None => (0x1e1, [-1.; 3]),
|
||||
};
|
||||
triangles.push(
|
||||
WorldTriangle::from_vertices(
|
||||
source.points.map(|p| Vector3::new(p[0], p[1], p[2])),
|
||||
material,
|
||||
u32::from(source.surface),
|
||||
flags,
|
||||
cosines,
|
||||
0.,
|
||||
)
|
||||
.ok_or("Invalid RWCM collision triangle")?,
|
||||
);
|
||||
packed_surfaces.push(source.surface);
|
||||
cursor += 1;
|
||||
}
|
||||
let range = start..triangles.len();
|
||||
let bounds = Bounds::from_points(
|
||||
triangles[range.clone()]
|
||||
.iter()
|
||||
.flat_map(|t| t.triangle.vertices),
|
||||
)
|
||||
.ok_or("Invalid RWCM cluster bounds")?;
|
||||
meshes.push(QueryMesh {
|
||||
triangle_range: range,
|
||||
local_to_world: RetailAffineTransform::IDENTITY,
|
||||
world_to_local: RetailAffineTransform::IDENTITY,
|
||||
local_bounds: bounds,
|
||||
matching_group: i32::from(group),
|
||||
pool: QueryPool::Ground,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
eprintln!(
|
||||
"SKATE_RWCM_READY triangles={count} query_clusters={} source=embedded",
|
||||
meshes.len()
|
||||
);
|
||||
BoardWorld::with_query_metadata(
|
||||
triangles,
|
||||
QueryMetadata {
|
||||
packed_surfaces,
|
||||
meshes,
|
||||
static_edges: vec![],
|
||||
island_flags: 0,
|
||||
},
|
||||
)
|
||||
.map_err(str::to_owned)
|
||||
}
|
||||
|
||||
pub(crate) fn collision_world(
|
||||
map: &SkateMap,
|
||||
material: RetailContactMaterial,
|
||||
) -> Result<BoardWorld, String> {
|
||||
if let Some(archive) = retail_archive(map)? {
|
||||
return retail_collision_world(archive, material);
|
||||
}
|
||||
// Match the reference RW mesh compiler's 1 mm vertex welding and reversed
|
||||
// edge pairing. Triangle diagonals are adjacency, never authored ledges.
|
||||
let mut welded = HashMap::<[i64; 3], usize>::new();
|
||||
@@ -125,43 +244,52 @@ pub(crate) fn collision_world(
|
||||
let mut cosines = vec![[1.; 3]; vertices.len()];
|
||||
let mut flags =
|
||||
vec![TriangleFeature::ONE_SIDED | TriangleFeature::USE_EDGE_COSINES | 0xe0; vertices.len()];
|
||||
let mut open = HashMap::<(usize, usize), (usize, usize)>::new();
|
||||
for (i, ids) in vertices.iter().enumerate() {
|
||||
for edge in 0..3 {
|
||||
let (a, b) = (ids[edge], ids[(edge + 1) % 3]);
|
||||
if let Some((other, oe)) = open.remove(&(b, a)) {
|
||||
let cosine = normals[i].dot(normals[other]).clamp(-1., 1.);
|
||||
let orientation =
|
||||
(positions[b] - positions[a]).dot(normals[i].cross(normals[other]));
|
||||
// ExtendedEdgeCosine / MakeEdgeCode in rw_collision_mesh.cpp:
|
||||
// orientation >= -1e-6 is convex; flat edges have no convex bit.
|
||||
for (ti, e) in [(i, edge), (other, oe)] {
|
||||
cosines[ti][e] = cosine;
|
||||
if orientation <= -1.0e-6 || cosine >= 1. {
|
||||
flags[ti] &= !(0x20 << e);
|
||||
// Fully native maps need no reconstructed adjacency. Mixed maps still
|
||||
// include every face when finding neighbors for their authored geometry.
|
||||
if map
|
||||
.geometry
|
||||
.collision
|
||||
.iter()
|
||||
.any(|t| t.native_edges.is_none())
|
||||
{
|
||||
let mut open = HashMap::<(usize, usize), (usize, usize)>::new();
|
||||
for (i, ids) in vertices.iter().enumerate() {
|
||||
for edge in 0..3 {
|
||||
let (a, b) = (ids[edge], ids[(edge + 1) % 3]);
|
||||
if let Some((other, oe)) = open.remove(&(b, a)) {
|
||||
let cosine = normals[i].dot(normals[other]).clamp(-1., 1.);
|
||||
let orientation =
|
||||
(positions[b] - positions[a]).dot(normals[i].cross(normals[other]));
|
||||
// ExtendedEdgeCosine / MakeEdgeCode in rw_collision_mesh.cpp:
|
||||
// orientation >= -1e-6 is convex; flat edges have no convex bit.
|
||||
for (ti, e) in [(i, edge), (other, oe)] {
|
||||
cosines[ti][e] = cosine;
|
||||
if orientation <= -1.0e-6 || cosine >= 1. {
|
||||
flags[ti] &= !(0x20 << e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
open.entry((a, b)).or_insert((i, edge));
|
||||
}
|
||||
} else {
|
||||
open.entry((a, b)).or_insert((i, edge));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut adjacent = vec![Vec::new(); positions.len()];
|
||||
for (i, ids) in vertices.iter().enumerate() {
|
||||
for &v in ids {
|
||||
adjacent[v].push(i);
|
||||
let mut adjacent = vec![Vec::new(); positions.len()];
|
||||
for (i, ids) in vertices.iter().enumerate() {
|
||||
for &v in ids {
|
||||
adjacent[v].push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (v, faces) in adjacent.iter().enumerate() {
|
||||
let reference = normals[faces[0]];
|
||||
if faces
|
||||
.iter()
|
||||
.all(|&i| (reference.dot(normals[i]) - 1.).abs() <= 0.01)
|
||||
{
|
||||
for &i in faces {
|
||||
for corner in 0..3 {
|
||||
if vertices[i][corner] == v {
|
||||
flags[i] |= 0x200 << corner;
|
||||
for (v, faces) in adjacent.iter().enumerate() {
|
||||
let reference = normals[faces[0]];
|
||||
if faces
|
||||
.iter()
|
||||
.all(|&i| (reference.dot(normals[i]) - 1.).abs() <= 0.01)
|
||||
{
|
||||
for &i in faces {
|
||||
for corner in 0..3 {
|
||||
if vertices[i][corner] == v {
|
||||
flags[i] |= 0x200 << corner;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +298,9 @@ pub(crate) fn collision_world(
|
||||
let mut triangles = Vec::with_capacity(vertices.len());
|
||||
let mut packed_surfaces = Vec::with_capacity(vertices.len());
|
||||
for (i, source) in map.geometry.collision.iter().enumerate() {
|
||||
if let Some(edges) = source.native_edges {
|
||||
(flags[i], cosines[i]) = decode_native_edges(edges)?;
|
||||
}
|
||||
let m = &map.materials[source.material as usize - 1];
|
||||
// Exact EncodeRwSurfaceId mapping from the reference native adapter.
|
||||
packed_surfaces.push((m.audio | (m.physics << 7) | (m.pattern << 12)) as u16);
|
||||
@@ -440,6 +571,76 @@ mod tests {
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn native_edges_override_generated_adjacency() {
|
||||
let mut map = demo();
|
||||
map.geometry.collision[0].native_edges = Some([0x20, 0x42, 0x9a]);
|
||||
validate_runtime(&map).unwrap();
|
||||
let world = collision_world(&map, material()).unwrap();
|
||||
let f = world.triangles()[0].triangle.feature;
|
||||
assert!(f.edge_convex(0));
|
||||
assert!(!f.edge_convex(1));
|
||||
assert!(!f.edge_convex(2));
|
||||
assert!(!f.vertex_disabled(0));
|
||||
assert!(f.vertex_disabled(1));
|
||||
assert!(!f.vertex_disabled(2));
|
||||
assert_eq!(f.edge_cosines[0].to_bits(), 0xbe6f4f38);
|
||||
assert_eq!(f.edge_cosines[1].to_bits(), 0x3f310b0c);
|
||||
assert_eq!(f.edge_cosines[2], 1.);
|
||||
assert!(decode_native_edges([31, 0, 0]).is_err());
|
||||
assert_eq!(decode_native_edges([0, 2, 26]).unwrap().1, f.edge_cosines);
|
||||
}
|
||||
#[test]
|
||||
fn embedded_archive_is_authoritative_and_preserves_cluster_metadata() {
|
||||
let mut map = demo();
|
||||
map.geometry.collision.clear();
|
||||
map.extensions.push(skate_data::skate_map::Extension {
|
||||
tag: *b"RWCM",
|
||||
schema: 1,
|
||||
payload: include_bytes!("../../skate-data/tests/fixtures/retail-collision.rwcmset")
|
||||
.to_vec(),
|
||||
});
|
||||
validate_runtime(&map).unwrap();
|
||||
let world = collision_world(&map, material()).unwrap();
|
||||
assert_eq!(world.triangles().len(), 3);
|
||||
let metadata = world.query_metadata().unwrap();
|
||||
assert_eq!(metadata.meshes.len(), 3);
|
||||
assert_eq!(metadata.meshes[1].matching_group, 0x1234);
|
||||
assert_eq!(metadata.packed_surfaces, vec![0x4321; 3]);
|
||||
assert_eq!(world.triangles()[1].triangle.vertices[0].x, 10.);
|
||||
}
|
||||
#[test]
|
||||
fn native_unit_without_edge_data_keeps_constructor_defaults() {
|
||||
let mut archive =
|
||||
include_bytes!("../../skate-data/tests/fixtures/retail-collision.rwcmset").to_vec();
|
||||
let name_len = u32::from_le_bytes(archive[12..16].try_into().unwrap()) as usize;
|
||||
let cluster = 16 + name_len + 4 + 160;
|
||||
archive[cluster + 2..cluster + 4].copy_from_slice(&8_u16.to_be_bytes());
|
||||
archive[cluster + 80] = 0xc1;
|
||||
archive[cluster + 84..cluster + 88].copy_from_slice(&[0x34, 0x12, 0x21, 0x43]);
|
||||
let world = retail_collision_world(&archive, material()).unwrap();
|
||||
let f = world.triangles()[0].triangle.feature;
|
||||
assert_eq!(f.flags, 0x1e1);
|
||||
assert_eq!(f.edge_cosines, [-1.; 3]);
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires SKATE_MAP_TEST_PATH pointing to a private map"]
|
||||
fn private_map_builds_collision_world() {
|
||||
let path = std::env::var("SKATE_MAP_TEST_PATH").unwrap();
|
||||
let map = SkateMap::load(std::path::Path::new(&path)).unwrap();
|
||||
validate_runtime(&map).unwrap();
|
||||
let world = collision_world(&map, material()).unwrap();
|
||||
let [x, y, z] = map.spawn;
|
||||
let hit = world
|
||||
.query_thin_line(Vector3::new(x, y + 1., z), Vector3::new(x, y - 10., z))
|
||||
.unwrap();
|
||||
assert!(hit.is_some(), "spawn has no supporting collision");
|
||||
eprintln!(
|
||||
"Private map world: {} triangles, spawn hit {:?}",
|
||||
world.triangles().len(),
|
||||
hit
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn map_collision_uses_separate_geometry_and_surface_metadata() {
|
||||
let mut map = demo();
|
||||
map.geometry.vertices[0].position = [1000.; 3];
|
||||
|
||||
Reference in new issue
Block a user