feat(msh): decode triangle descriptors and query segments
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s
This commit is contained in:
6 files changed
+933
-3
No files matched your search
@@ -1381,6 +1381,7 @@ mod tests {
|
|||||||
uv1: None,
|
uv1: None,
|
||||||
indices,
|
indices,
|
||||||
batches,
|
batches,
|
||||||
|
triangle_descriptors: None,
|
||||||
node_names: None,
|
node_names: None,
|
||||||
animation: None,
|
animation: None,
|
||||||
}
|
}
|
||||||
@@ -1446,6 +1447,7 @@ mod tests {
|
|||||||
..batch(0, 0, 0)
|
..batch(0, 0, 0)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
triangle_descriptors: None,
|
||||||
node_names: None,
|
node_names: None,
|
||||||
animation: None,
|
animation: None,
|
||||||
};
|
};
|
||||||
@@ -1686,6 +1688,7 @@ mod tests {
|
|||||||
uv1: None,
|
uv1: None,
|
||||||
indices: vec![0, 1, 2, 0, 1, 2],
|
indices: vec![0, 1, 2, 0, 1, 2],
|
||||||
batches: vec![batch(0, 3, 0), batch(3, 3, 0)],
|
batches: vec![batch(0, 3, 0), batch(3, 3, 0)],
|
||||||
|
triangle_descriptors: None,
|
||||||
node_names: None,
|
node_names: None,
|
||||||
animation: Some(ModelAnimation {
|
animation: Some(ModelAnimation {
|
||||||
keys: vec![
|
keys: vec![
|
||||||
@@ -1745,6 +1748,7 @@ mod tests {
|
|||||||
uv1: None,
|
uv1: None,
|
||||||
indices: Vec::new(),
|
indices: Vec::new(),
|
||||||
batches: Vec::new(),
|
batches: Vec::new(),
|
||||||
|
triangle_descriptors: None,
|
||||||
node_names: None,
|
node_names: None,
|
||||||
animation: Some(ModelAnimation {
|
animation: Some(ModelAnimation {
|
||||||
keys: vec![
|
keys: vec![
|
||||||
@@ -1851,6 +1855,7 @@ mod tests {
|
|||||||
uv1: None,
|
uv1: None,
|
||||||
indices: vec![0, 1, 2, 0, 1, 2],
|
indices: vec![0, 1, 2, 0, 1, 2],
|
||||||
batches: vec![batch(0, 3, 0), batch(3, 3, 0)],
|
batches: vec![batch(0, 3, 0), batch(3, 3, 0)],
|
||||||
|
triangle_descriptors: None,
|
||||||
node_names: None,
|
node_names: None,
|
||||||
animation: Some(ModelAnimation {
|
animation: Some(ModelAnimation {
|
||||||
keys: vec![AnimKey24 {
|
keys: vec![AnimKey24 {
|
||||||
@@ -1942,6 +1947,7 @@ mod tests {
|
|||||||
uv1: None,
|
uv1: None,
|
||||||
indices: vec![0, 1, 2],
|
indices: vec![0, 1, 2],
|
||||||
batches: vec![batch(0, 3, 0)],
|
batches: vec![batch(0, 3, 0)],
|
||||||
|
triangle_descriptors: None,
|
||||||
node_names: None,
|
node_names: None,
|
||||||
animation: Some(ModelAnimation {
|
animation: Some(ModelAnimation {
|
||||||
keys: vec![AnimKey24 {
|
keys: vec![AnimKey24 {
|
||||||
|
|||||||
@@ -7344,6 +7344,7 @@ mod tests {
|
|||||||
uv1: None,
|
uv1: None,
|
||||||
indices: vec![],
|
indices: vec![],
|
||||||
batches: vec![],
|
batches: vec![],
|
||||||
|
triangle_descriptors: None,
|
||||||
node_names: None,
|
node_names: None,
|
||||||
animation: Some(fparkan_msh::ModelAnimation {
|
animation: Some(fparkan_msh::ModelAnimation {
|
||||||
keys: vec![
|
keys: vec![
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ pub const STREAM_UV0: u32 = 5;
|
|||||||
pub const STREAM_UV1: u32 = 18;
|
pub const STREAM_UV1: u32 = 18;
|
||||||
/// Triangle index stream.
|
/// Triangle index stream.
|
||||||
pub const STREAM_INDICES: u32 = 6;
|
pub const STREAM_INDICES: u32 = 6;
|
||||||
|
/// Optional triangle-descriptor stream.
|
||||||
|
pub const STREAM_TRIANGLE_DESCRIPTORS: u32 = 7;
|
||||||
|
const TRIANGLE_NORMAL_SCALE: f32 = 1.0 / 32_767.0;
|
||||||
/// Animation key stream.
|
/// Animation key stream.
|
||||||
pub const STREAM_ANIMATION_KEYS: u32 = 8;
|
pub const STREAM_ANIMATION_KEYS: u32 = 8;
|
||||||
/// Node names stream.
|
/// Node names stream.
|
||||||
@@ -102,6 +105,8 @@ pub struct ModelAsset {
|
|||||||
pub indices: Vec<u16>,
|
pub indices: Vec<u16>,
|
||||||
/// Draw batches.
|
/// Draw batches.
|
||||||
pub batches: Vec<Batch>,
|
pub batches: Vec<Batch>,
|
||||||
|
/// Optional type-7 triangle descriptors.
|
||||||
|
pub triangle_descriptors: Option<Vec<TriangleDescriptor>>,
|
||||||
/// Optional decoded node names.
|
/// Optional decoded node names.
|
||||||
pub node_names: Option<Vec<Option<String>>>,
|
pub node_names: Option<Vec<Option<String>>>,
|
||||||
/// Optional decoded node-animation streams.
|
/// Optional decoded node-animation streams.
|
||||||
@@ -201,9 +206,48 @@ pub struct Batch {
|
|||||||
pub base_vertex: u32,
|
pub base_vertex: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Preserved triangle descriptor stream marker.
|
/// One 16-byte type-7 triangle descriptor.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub struct TriangleDescriptor;
|
pub struct TriangleDescriptor {
|
||||||
|
/// Triangle mask flags at byte offset `+0`.
|
||||||
|
pub tri_flags: u16,
|
||||||
|
/// Opaque 16-bit words from offsets `+2`, `+4`, `+6` and `+14`.
|
||||||
|
pub opaque: [u16; 4],
|
||||||
|
/// Signed packed plane normal at byte offsets `+8`, `+10` and `+12`.
|
||||||
|
pub normal: [i16; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Include-all and exclude-any masks used by the native MSH query.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct SegmentQueryMasks {
|
||||||
|
/// Node38 header bits that must all be set.
|
||||||
|
pub node_include: u16,
|
||||||
|
/// Node38 header bits that must all be clear.
|
||||||
|
pub node_exclude: u16,
|
||||||
|
/// Batch20 leading-DWORD bits that must all be set.
|
||||||
|
pub batch_include: u32,
|
||||||
|
/// Batch20 leading-DWORD bits that must all be clear.
|
||||||
|
pub batch_exclude: u32,
|
||||||
|
/// Type-7 triangle bits that must all be set.
|
||||||
|
pub triangle_include: u16,
|
||||||
|
/// Type-7 triangle bits that must all be clear.
|
||||||
|
pub triangle_exclude: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One accepted segment hit from the native MSH triangle-query path.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct SegmentHit {
|
||||||
|
/// Hit position in the same coordinate frame as the input segment.
|
||||||
|
pub position: [f32; 3],
|
||||||
|
/// Squared distance from the input segment's first endpoint.
|
||||||
|
pub distance_squared: f32,
|
||||||
|
/// Global Batch20 index.
|
||||||
|
pub batch_index: usize,
|
||||||
|
/// Global type-7 descriptor index.
|
||||||
|
pub triangle_descriptor_index: usize,
|
||||||
|
/// Global Res3 position indices of the hit triangle.
|
||||||
|
pub vertex_indices: [usize; 3],
|
||||||
|
}
|
||||||
|
|
||||||
/// Vertex stream view.
|
/// Vertex stream view.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -685,6 +729,287 @@ pub fn draw_batches(model: &ModelAsset, slot: SlotId) -> Result<&[Batch], MshErr
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Queries a segment against one slot using the native Node38, Batch20 and
|
||||||
|
/// type-7 include-all/exclude-any masks.
|
||||||
|
///
|
||||||
|
/// The first endpoint is the native forward direction. Batch bit `0x0002`
|
||||||
|
/// retries the plane test in reverse only when the forward plane test misses.
|
||||||
|
/// The returned distance is always measured from the original first endpoint.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`MshError`] when type 7 is absent, the model does not use Node38,
|
||||||
|
/// or the selected slot's descriptor, batch, index or vertex ranges are invalid.
|
||||||
|
pub fn query_slot_segment(
|
||||||
|
model: &ModelAsset,
|
||||||
|
node: NodeId,
|
||||||
|
slot: SlotId,
|
||||||
|
endpoints: [[f32; 3]; 2],
|
||||||
|
masks: SegmentQueryMasks,
|
||||||
|
) -> Result<Option<SegmentHit>, MshError> {
|
||||||
|
let descriptors = model
|
||||||
|
.triangle_descriptors
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(MshError::MissingStream {
|
||||||
|
type_id: STREAM_TRIANGLE_DESCRIPTORS,
|
||||||
|
label: "Res7",
|
||||||
|
})?;
|
||||||
|
if model.node_stride != 38 {
|
||||||
|
return Err(MshError::InvalidGeometry(format!(
|
||||||
|
"segment query requires Node38 metadata, got node stride {}",
|
||||||
|
model.node_stride
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if !endpoints.iter().flatten().all(|value| value.is_finite()) {
|
||||||
|
return Err(MshError::InvalidGeometry(
|
||||||
|
"segment endpoints must be finite".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let node_flags = node38_metadata(model, node)
|
||||||
|
.ok_or_else(|| MshError::InvalidGeometry("invalid Node38 metadata".to_string()))?
|
||||||
|
.header_flags;
|
||||||
|
let slot_index = usize::try_from(slot.0).map_err(|_| MshError::SlotOutOfBounds {
|
||||||
|
slot: slot.0,
|
||||||
|
slot_count: model.slots.len(),
|
||||||
|
})?;
|
||||||
|
let slot_ref = model
|
||||||
|
.slots
|
||||||
|
.get(slot_index)
|
||||||
|
.ok_or(MshError::SlotOutOfBounds {
|
||||||
|
slot: slot.0,
|
||||||
|
slot_count: model.slots.len(),
|
||||||
|
})?;
|
||||||
|
let batches = draw_batches(model, slot)?;
|
||||||
|
let descriptor_start = usize::from(slot_ref.tri_start);
|
||||||
|
let descriptor_end = descriptor_start
|
||||||
|
.checked_add(usize::from(slot_ref.tri_count))
|
||||||
|
.ok_or_else(|| MshError::InvalidGeometry("type-7 range overflow".to_string()))?;
|
||||||
|
if descriptor_end > descriptors.len() {
|
||||||
|
return Err(MshError::InvalidGeometry(format!(
|
||||||
|
"slot {slot_index} type-7 range {descriptor_start}..{descriptor_end} exceeds {} descriptors",
|
||||||
|
descriptors.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut face_count = 0usize;
|
||||||
|
for (local_batch, batch) in batches.iter().enumerate() {
|
||||||
|
if batch.index_count % 3 != 0 {
|
||||||
|
return Err(MshError::InvalidGeometry(format!(
|
||||||
|
"batch {} index count {} is not triangle-aligned",
|
||||||
|
usize::from(slot_ref.batch_start) + local_batch,
|
||||||
|
batch.index_count
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let start = usize::try_from(batch.index_start)
|
||||||
|
.map_err(|_| MshError::InvalidGeometry("batch index range overflow".to_string()))?;
|
||||||
|
let end = start
|
||||||
|
.checked_add(usize::from(batch.index_count))
|
||||||
|
.ok_or_else(|| MshError::InvalidGeometry("batch index range overflow".to_string()))?;
|
||||||
|
if end > model.indices.len() {
|
||||||
|
return Err(MshError::InvalidGeometry(format!(
|
||||||
|
"batch {} index range {start}..{end} exceeds {} indices",
|
||||||
|
usize::from(slot_ref.batch_start) + local_batch,
|
||||||
|
model.indices.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
face_count = face_count
|
||||||
|
.checked_add(usize::from(batch.index_count) / 3)
|
||||||
|
.ok_or_else(|| MshError::InvalidGeometry("slot triangle count overflow".to_string()))?;
|
||||||
|
}
|
||||||
|
if face_count != usize::from(slot_ref.tri_count) {
|
||||||
|
return Err(MshError::InvalidGeometry(format!(
|
||||||
|
"slot {slot_index} declares {} type-7 descriptors but its batches contain {face_count} triangles",
|
||||||
|
slot_ref.tri_count
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if !mask_matches(
|
||||||
|
u32::from(node_flags),
|
||||||
|
u32::from(masks.node_include),
|
||||||
|
u32::from(masks.node_exclude),
|
||||||
|
) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut best = None;
|
||||||
|
let mut descriptor_cursor = descriptor_start;
|
||||||
|
for (local_batch, batch) in batches.iter().enumerate() {
|
||||||
|
let batch_index = usize::from(slot_ref.batch_start) + local_batch;
|
||||||
|
let batch_flags = u32::from(batch.batch_flags) | (u32::from(batch.material_index_hi) << 16);
|
||||||
|
let batch_face_count = usize::from(batch.index_count) / 3;
|
||||||
|
if mask_matches(batch_flags, masks.batch_include, masks.batch_exclude) {
|
||||||
|
for local_face in 0..batch_face_count {
|
||||||
|
let triangle_descriptor_index = descriptor_cursor + local_face;
|
||||||
|
let descriptor = descriptors[triangle_descriptor_index];
|
||||||
|
if !mask_matches(
|
||||||
|
u32::from(descriptor.tri_flags),
|
||||||
|
u32::from(masks.triangle_include),
|
||||||
|
u32::from(masks.triangle_exclude),
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let index_start = usize::try_from(batch.index_start)
|
||||||
|
.expect("validated batch index range")
|
||||||
|
+ local_face * 3;
|
||||||
|
let base_vertex = usize::try_from(batch.base_vertex)
|
||||||
|
.map_err(|_| MshError::InvalidGeometry("base vertex overflow".to_string()))?;
|
||||||
|
let mut vertex_indices = [0usize; 3];
|
||||||
|
for (triangle_corner, index) in vertex_indices.iter_mut().enumerate() {
|
||||||
|
let raw_index = usize::from(model.indices[index_start + triangle_corner]);
|
||||||
|
*index = base_vertex.checked_add(raw_index).ok_or_else(|| {
|
||||||
|
MshError::InvalidGeometry("resolved vertex index overflow".to_string())
|
||||||
|
})?;
|
||||||
|
if *index >= model.positions.len() {
|
||||||
|
return Err(MshError::VertexIndexOutOfBounds {
|
||||||
|
batch: batch_index,
|
||||||
|
vertex: u64::try_from(*index).unwrap_or(u64::MAX),
|
||||||
|
position_count: model.positions.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let triangle = vertex_indices.map(|index| model.positions[index]);
|
||||||
|
if !triangle.iter().flatten().all(|value| value.is_finite()) {
|
||||||
|
return Err(MshError::InvalidGeometry(format!(
|
||||||
|
"triangle descriptor {triangle_descriptor_index} has a non-finite position"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let normal = descriptor
|
||||||
|
.normal
|
||||||
|
.map(|value| f32::from(value) * TRIANGLE_NORMAL_SCALE);
|
||||||
|
if normal == [0.0; 3] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let forward =
|
||||||
|
native_segment_plane_hit(normal, triangle[0], endpoints[0], endpoints[1])?;
|
||||||
|
let position = match forward {
|
||||||
|
Some(position) => Some(position),
|
||||||
|
None if batch.batch_flags & 0x0002 != 0 => {
|
||||||
|
native_segment_plane_hit(normal, triangle[0], endpoints[1], endpoints[0])?
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let Some(position) = position else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !native_point_in_triangle(normal, triangle, position)? {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let offset = [
|
||||||
|
position[0] - endpoints[0][0],
|
||||||
|
position[1] - endpoints[0][1],
|
||||||
|
position[2] - endpoints[0][2],
|
||||||
|
];
|
||||||
|
let distance_squared = dot3(offset, offset);
|
||||||
|
if !distance_squared.is_finite() {
|
||||||
|
return Err(MshError::InvalidGeometry(
|
||||||
|
"segment hit distance is non-finite".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if best.is_none_or(|hit: SegmentHit| distance_squared <= hit.distance_squared) {
|
||||||
|
best = Some(SegmentHit {
|
||||||
|
position,
|
||||||
|
distance_squared,
|
||||||
|
batch_index,
|
||||||
|
triangle_descriptor_index,
|
||||||
|
vertex_indices,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
descriptor_cursor += batch_face_count;
|
||||||
|
}
|
||||||
|
Ok(best)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask_matches(flags: u32, include: u32, exclude: u32) -> bool {
|
||||||
|
flags & include == include && flags & exclude == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn native_segment_plane_hit(
|
||||||
|
normal: [f32; 3],
|
||||||
|
anchor: [f32; 3],
|
||||||
|
start: [f32; 3],
|
||||||
|
end: [f32; 3],
|
||||||
|
) -> Result<Option<[f32; 3]>, MshError> {
|
||||||
|
let direction = [end[0] - start[0], end[1] - start[1], end[2] - start[2]];
|
||||||
|
let denominator = dot3(normal, direction);
|
||||||
|
let start_distance = dot3(normal, start) - dot3(normal, anchor);
|
||||||
|
let end_distance = start_distance + denominator;
|
||||||
|
if !denominator.is_finite() || !start_distance.is_finite() || !end_distance.is_finite() {
|
||||||
|
return Err(MshError::InvalidGeometry(
|
||||||
|
"segment plane calculation is non-finite".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if denominator >= 0.0 || start_distance <= 0.0 || end_distance > 0.0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let ratio = start_distance / denominator;
|
||||||
|
let position = [
|
||||||
|
start[0] - direction[0] * ratio,
|
||||||
|
start[1] - direction[1] * ratio,
|
||||||
|
start[2] - direction[2] * ratio,
|
||||||
|
];
|
||||||
|
if !position.iter().all(|value| value.is_finite()) {
|
||||||
|
return Err(MshError::InvalidGeometry(
|
||||||
|
"segment intersection is non-finite".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Some(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn native_point_in_triangle(
|
||||||
|
normal: [f32; 3],
|
||||||
|
triangle: [[f32; 3]; 3],
|
||||||
|
point: [f32; 3],
|
||||||
|
) -> Result<bool, MshError> {
|
||||||
|
let absolute = normal.map(f32::abs);
|
||||||
|
let xy_max = if absolute[0] < absolute[1] {
|
||||||
|
absolute[1]
|
||||||
|
} else {
|
||||||
|
absolute[0]
|
||||||
|
};
|
||||||
|
let dominant_axis = if absolute[2] <= xy_max {
|
||||||
|
if absolute[0] < absolute[1] {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
2
|
||||||
|
};
|
||||||
|
for index in 0..3 {
|
||||||
|
let previous = triangle[(index + 2) % 3];
|
||||||
|
let current = triangle[index];
|
||||||
|
let relative = [
|
||||||
|
point[0] - previous[0],
|
||||||
|
point[1] - previous[1],
|
||||||
|
point[2] - previous[2],
|
||||||
|
];
|
||||||
|
let edge = [
|
||||||
|
current[0] - previous[0],
|
||||||
|
current[1] - previous[1],
|
||||||
|
current[2] - previous[2],
|
||||||
|
];
|
||||||
|
let side = match dominant_axis {
|
||||||
|
0 => (relative[2] * edge[1] + relative[1] * -edge[2]) * normal[0],
|
||||||
|
1 => (-edge[0] * relative[2] + relative[0] * edge[2]) * normal[1],
|
||||||
|
_ => (relative[1] * edge[0] - relative[0] * edge[1]) * normal[2],
|
||||||
|
};
|
||||||
|
if !side.is_finite() {
|
||||||
|
return Err(MshError::InvalidGeometry(
|
||||||
|
"triangle edge calculation is non-finite".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if side < 0.0 {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dot3(left: [f32; 3], right: [f32; 3]) -> f32 {
|
||||||
|
left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
|
||||||
|
}
|
||||||
|
|
||||||
impl MshDocument {
|
impl MshDocument {
|
||||||
/// Returns original stream descriptors.
|
/// Returns original stream descriptors.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -791,6 +1116,9 @@ fn parse_model_document(document: &NresDocument) -> Result<ModelAsset, MshError>
|
|||||||
let uv1 = read_optional_stream(document, STREAM_UV1)?
|
let uv1 = read_optional_stream(document, STREAM_UV1)?
|
||||||
.map(|raw| parse_secondary_uv_stream(raw, positions.len()))
|
.map(|raw| parse_secondary_uv_stream(raw, positions.len()))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
let triangle_descriptors = read_optional_triangle_descriptors(document)?
|
||||||
|
.map(|raw| parse_triangle_descriptors(&raw.bytes))
|
||||||
|
.transpose()?;
|
||||||
let node_names = read_optional_stream(document, STREAM_NAMES)?
|
let node_names = read_optional_stream(document, STREAM_NAMES)?
|
||||||
.map(|raw| parse_res10_names(&raw.bytes, node_count))
|
.map(|raw| parse_res10_names(&raw.bytes, node_count))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
@@ -809,6 +1137,7 @@ fn parse_model_document(document: &NresDocument) -> Result<ModelAsset, MshError>
|
|||||||
uv1,
|
uv1,
|
||||||
indices,
|
indices,
|
||||||
batches,
|
batches,
|
||||||
|
triangle_descriptors,
|
||||||
node_names,
|
node_names,
|
||||||
animation,
|
animation,
|
||||||
})
|
})
|
||||||
@@ -879,6 +1208,28 @@ fn read_optional_stream(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_optional_triangle_descriptors(
|
||||||
|
document: &NresDocument,
|
||||||
|
) -> Result<Option<RawStream>, MshError> {
|
||||||
|
let mut entries = document
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.meta().type_id == STREAM_TRIANGLE_DESCRIPTORS);
|
||||||
|
let Some(entry) = entries.next() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if entries.next().is_some() {
|
||||||
|
return Err(MshError::DuplicateStream {
|
||||||
|
type_id: STREAM_TRIANGLE_DESCRIPTORS,
|
||||||
|
label: "Res7",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Some(RawStream {
|
||||||
|
attributes: attributes(entry.meta()),
|
||||||
|
bytes: document.payload(entry.id())?.to_vec(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_slots(data: &[u8]) -> Result<Vec<Slot>, MshError> {
|
fn parse_slots(data: &[u8]) -> Result<Vec<Slot>, MshError> {
|
||||||
if data.len() < 0x8C {
|
if data.len() < 0x8C {
|
||||||
return Err(MshError::InvalidGeometry(format!(
|
return Err(MshError::InvalidGeometry(format!(
|
||||||
@@ -997,6 +1348,30 @@ fn parse_batches(data: &[u8]) -> Result<Vec<Batch>, MshError> {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_triangle_descriptors(data: &[u8]) -> Result<Vec<TriangleDescriptor>, MshError> {
|
||||||
|
if !data.len().is_multiple_of(16) {
|
||||||
|
return Err(invalid_resource_size("Res7", data.len(), 16));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(data.len() / 16);
|
||||||
|
for offset in (0..data.len()).step_by(16) {
|
||||||
|
out.push(TriangleDescriptor {
|
||||||
|
tri_flags: read_u16_required(data, offset)?,
|
||||||
|
opaque: [
|
||||||
|
read_u16_required(data, offset + 2)?,
|
||||||
|
read_u16_required(data, offset + 4)?,
|
||||||
|
read_u16_required(data, offset + 6)?,
|
||||||
|
read_u16_required(data, offset + 14)?,
|
||||||
|
],
|
||||||
|
normal: [
|
||||||
|
read_i16_required(data, offset + 8)?,
|
||||||
|
read_i16_required(data, offset + 10)?,
|
||||||
|
read_i16_required(data, offset + 12)?,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_u16_array(data: &[u8], label: &'static str) -> Result<Vec<u16>, MshError> {
|
fn parse_u16_array(data: &[u8], label: &'static str) -> Result<Vec<u16>, MshError> {
|
||||||
if !data.len().is_multiple_of(2) {
|
if !data.len().is_multiple_of(2) {
|
||||||
return Err(invalid_resource_size(label, data.len(), 2));
|
return Err(invalid_resource_size(label, data.len(), 2));
|
||||||
@@ -1212,6 +1587,12 @@ fn read_u16_required(bytes: &[u8], offset: usize) -> Result<u16, MshError> {
|
|||||||
Ok(u16::from_le_bytes(arr))
|
Ok(u16::from_le_bytes(arr))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_i16_required(bytes: &[u8], offset: usize) -> Result<i16, MshError> {
|
||||||
|
Ok(i16::from_le_bytes(
|
||||||
|
read_u16_required(bytes, offset)?.to_le_bytes(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn read_i8(bytes: &[u8], offset: usize) -> Result<i8, MshError> {
|
fn read_i8(bytes: &[u8], offset: usize) -> Result<i8, MshError> {
|
||||||
let byte = bytes
|
let byte = bytes
|
||||||
.get(offset)
|
.get(offset)
|
||||||
@@ -1259,6 +1640,352 @@ mod tests {
|
|||||||
assert_eq!(model.node_stride, 38);
|
assert_eq!(model.node_stride, 38);
|
||||||
assert_eq!(model.node_count, 0);
|
assert_eq!(model.node_count, 0);
|
||||||
assert!(model.slots.is_empty());
|
assert!(model.slots.is_empty());
|
||||||
|
assert_eq!(model.triangle_descriptors, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type7_decodes_optional_records_and_keeps_opaque_words() {
|
||||||
|
let mut raw = Vec::new();
|
||||||
|
push_u16(&mut raw, 0x3456);
|
||||||
|
push_u16(&mut raw, 0x1111);
|
||||||
|
push_u16(&mut raw, 0x2222);
|
||||||
|
push_u16(&mut raw, 0x3333);
|
||||||
|
raw.extend_from_slice(&(-1_i16).to_le_bytes());
|
||||||
|
raw.extend_from_slice(&i16::MIN.to_le_bytes());
|
||||||
|
raw.extend_from_slice(&i16::MAX.to_le_bytes());
|
||||||
|
push_u16(&mut raw, 0x4444);
|
||||||
|
let document = decode_nested(&build_nres(&[
|
||||||
|
stream(STREAM_NODE_TABLE, 38, b"Res1", &[]),
|
||||||
|
stream(STREAM_SLOTS, 0, b"Res2", &slots_payload(&[])),
|
||||||
|
stream(STREAM_POSITIONS, 0, b"Res3", &[]),
|
||||||
|
stream(STREAM_INDICES, 0, b"Res6", &[]),
|
||||||
|
stream(STREAM_BATCHES, 0, b"Res13", &[]),
|
||||||
|
stream(STREAM_TRIANGLE_DESCRIPTORS, 0, b"Res7", &raw),
|
||||||
|
]))
|
||||||
|
.expect("nested NRes");
|
||||||
|
let msh = decode_msh(&document).expect("optional type7 stream");
|
||||||
|
let model = validate_msh(&msh).expect("model");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
model.triangle_descriptors,
|
||||||
|
Some(vec![TriangleDescriptor {
|
||||||
|
tri_flags: 0x3456,
|
||||||
|
opaque: [0x1111, 0x2222, 0x3333, 0x4444],
|
||||||
|
normal: [-1, i16::MIN, i16::MAX],
|
||||||
|
}])
|
||||||
|
);
|
||||||
|
let preserved = msh.preserved_streams().expect("preserved streams");
|
||||||
|
assert_eq!(
|
||||||
|
preserved
|
||||||
|
.iter()
|
||||||
|
.find(|stream| stream.type_id == STREAM_TRIANGLE_DESCRIPTORS)
|
||||||
|
.unwrap()
|
||||||
|
.bytes
|
||||||
|
.as_ref(),
|
||||||
|
raw
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_or_duplicate_optional_type7_stream_is_an_error() {
|
||||||
|
for entries in [
|
||||||
|
vec![
|
||||||
|
stream(STREAM_NODE_TABLE, 38, b"Res1", &[]),
|
||||||
|
stream(STREAM_SLOTS, 0, b"Res2", &slots_payload(&[])),
|
||||||
|
stream(STREAM_POSITIONS, 0, b"Res3", &[]),
|
||||||
|
stream(STREAM_INDICES, 0, b"Res6", &[]),
|
||||||
|
stream(STREAM_BATCHES, 0, b"Res13", &[]),
|
||||||
|
stream(STREAM_TRIANGLE_DESCRIPTORS, 0, b"Res7", &[0; 15]),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
stream(STREAM_NODE_TABLE, 38, b"Res1", &[]),
|
||||||
|
stream(STREAM_SLOTS, 0, b"Res2", &slots_payload(&[])),
|
||||||
|
stream(STREAM_POSITIONS, 0, b"Res3", &[]),
|
||||||
|
stream(STREAM_INDICES, 0, b"Res6", &[]),
|
||||||
|
stream(STREAM_BATCHES, 0, b"Res13", &[]),
|
||||||
|
stream(STREAM_TRIANGLE_DESCRIPTORS, 0, b"Res7a", &[0; 16]),
|
||||||
|
stream(STREAM_TRIANGLE_DESCRIPTORS, 0, b"Res7b", &[0; 16]),
|
||||||
|
],
|
||||||
|
] {
|
||||||
|
let document = decode_nested(&build_nres(&entries)).expect("nested NRes");
|
||||||
|
let msh = decode_msh(&document).expect("required streams");
|
||||||
|
assert!(validate_msh(&msh).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn segment_query_uses_native_one_sided_edge_and_endpoint_rules() {
|
||||||
|
let model = segment_model(&[(0, 0)], &[0]);
|
||||||
|
let hit = query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
)
|
||||||
|
.expect("forward segment")
|
||||||
|
.expect("front-to-back hit");
|
||||||
|
assert_eq!(hit.position, [0.0, 0.0, 0.0]);
|
||||||
|
assert_eq!(hit.distance_squared, 1.0);
|
||||||
|
assert_eq!(hit.vertex_indices, [0, 1, 2]);
|
||||||
|
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, -1.0, 0.0], [0.0, 1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
)
|
||||||
|
.expect("reverse segment")
|
||||||
|
.is_none());
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, 0.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
)
|
||||||
|
.expect("endpoint on plane")
|
||||||
|
.is_some());
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 0.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
)
|
||||||
|
.expect("start on plane")
|
||||||
|
.is_none());
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, -1.0], [0.0, -1.0, -1.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
)
|
||||||
|
.expect("segment crossing triangle edge")
|
||||||
|
.is_some());
|
||||||
|
|
||||||
|
let two_sided = segment_model(&[(2, 0)], &[0]);
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&two_sided,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, -1.0, 0.0], [0.0, 1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
)
|
||||||
|
.expect("two-sided reverse retry")
|
||||||
|
.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn segment_query_skips_triangles_with_zero_packed_normals() {
|
||||||
|
let mut model = segment_model(&[(0, 0)], &[0]);
|
||||||
|
model
|
||||||
|
.triangle_descriptors
|
||||||
|
.as_mut()
|
||||||
|
.expect("type-7 descriptors")[0]
|
||||||
|
.normal = [0; 3];
|
||||||
|
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
)
|
||||||
|
.expect("zero normal is a missed triangle")
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn segment_query_applies_native_masks_and_advances_filtered_descriptors() {
|
||||||
|
let mut model = segment_model(&[(0x0200, 1), (0, 1), (0, 1)], &[0, 0x0020, 0x0004]);
|
||||||
|
model
|
||||||
|
.triangle_descriptors
|
||||||
|
.as_mut()
|
||||||
|
.expect("type-7 descriptors")
|
||||||
|
.insert(
|
||||||
|
0,
|
||||||
|
TriangleDescriptor {
|
||||||
|
tri_flags: 0,
|
||||||
|
opaque: [0; 4],
|
||||||
|
normal: [0, i16::MAX, 0],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
model.slots[0].tri_start = 1;
|
||||||
|
let masks = SegmentQueryMasks {
|
||||||
|
node_include: 0x0004,
|
||||||
|
node_exclude: 0,
|
||||||
|
batch_include: 0x0001_0000,
|
||||||
|
batch_exclude: 0x0000_0200,
|
||||||
|
triangle_include: 0x0004,
|
||||||
|
triangle_exclude: 0x0020,
|
||||||
|
};
|
||||||
|
let hit = query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
masks,
|
||||||
|
)
|
||||||
|
.expect("native masks")
|
||||||
|
.expect("third descriptor survives");
|
||||||
|
assert_eq!(hit.batch_index, 2);
|
||||||
|
assert_eq!(hit.triangle_descriptor_index, 3);
|
||||||
|
assert_eq!(hit.vertex_indices, [6, 7, 8]);
|
||||||
|
|
||||||
|
for masks in [
|
||||||
|
SegmentQueryMasks {
|
||||||
|
node_include: 0x0008,
|
||||||
|
..masks
|
||||||
|
},
|
||||||
|
SegmentQueryMasks {
|
||||||
|
node_exclude: 0x0004,
|
||||||
|
..masks
|
||||||
|
},
|
||||||
|
SegmentQueryMasks {
|
||||||
|
batch_include: 0x0002_0000,
|
||||||
|
..masks
|
||||||
|
},
|
||||||
|
SegmentQueryMasks {
|
||||||
|
triangle_exclude: 0x0004,
|
||||||
|
..masks
|
||||||
|
},
|
||||||
|
] {
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
masks,
|
||||||
|
)
|
||||||
|
.expect("filtered query")
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn segment_query_replaces_equal_distance_hits_with_the_later_candidate() {
|
||||||
|
let model = segment_model(&[(0, 0), (0, 0)], &[0, 0]);
|
||||||
|
let hit = query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
)
|
||||||
|
.expect("equal-distance candidates")
|
||||||
|
.expect("hit");
|
||||||
|
assert_eq!(hit.distance_squared, 1.0);
|
||||||
|
assert_eq!(hit.batch_index, 1);
|
||||||
|
assert_eq!(hit.triangle_descriptor_index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_point_in_triangle_projects_on_each_dominant_normal_axis() {
|
||||||
|
for (normal, triangle, edge, outside) in [
|
||||||
|
(
|
||||||
|
[1.0, 0.0, 0.0],
|
||||||
|
[[0.0, -1.0, -1.0], [0.0, 1.0, -1.0], [0.0, 0.0, 1.0]],
|
||||||
|
[0.0, 0.0, -1.0],
|
||||||
|
[0.0, 2.0, 0.0],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[0.0, 1.0, 0.0],
|
||||||
|
[[-1.0, 0.0, -1.0], [0.0, 0.0, 1.0], [1.0, 0.0, -1.0]],
|
||||||
|
[0.0, 0.0, -1.0],
|
||||||
|
[0.0, 0.0, 2.0],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[0.0, 0.0, 1.0],
|
||||||
|
[[-1.0, -1.0, 0.0], [1.0, -1.0, 0.0], [0.0, 1.0, 0.0]],
|
||||||
|
[0.0, -1.0, 0.0],
|
||||||
|
[2.0, 0.0, 0.0],
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
assert!(native_point_in_triangle(normal, triangle, [0.0; 3]).expect("inside"));
|
||||||
|
assert!(native_point_in_triangle(normal, triangle, edge).expect("edge"));
|
||||||
|
assert!(!native_point_in_triangle(normal, triangle, outside).expect("outside"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn segment_query_reports_missing_type7_and_invalid_metadata_or_ranges() {
|
||||||
|
let mut model = segment_model(&[(0, 0)], &[0]);
|
||||||
|
model.triangle_descriptors = None;
|
||||||
|
assert!(matches!(
|
||||||
|
query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
),
|
||||||
|
Err(MshError::MissingStream {
|
||||||
|
type_id: STREAM_TRIANGLE_DESCRIPTORS,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut model = segment_model(&[(0, 0)], &[0]);
|
||||||
|
model.node_stride = 24;
|
||||||
|
assert!(matches!(
|
||||||
|
query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
),
|
||||||
|
Err(MshError::InvalidGeometry(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut model = segment_model(&[(0, 0)], &[0]);
|
||||||
|
model.slots[0].tri_start = 1;
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
let mut model = segment_model(&[(0, 0)], &[0]);
|
||||||
|
model.slots[0].batch_count = 2;
|
||||||
|
assert!(matches!(
|
||||||
|
query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
),
|
||||||
|
Err(MshError::BatchRangeOutOfBounds { .. })
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut model = segment_model(&[(0, 0)], &[0]);
|
||||||
|
model.batches[0].index_count = 4;
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
let mut model = segment_model(&[(0, 0)], &[0]);
|
||||||
|
model.batches[0].index_start = 3;
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(0),
|
||||||
|
SlotId(0),
|
||||||
|
[[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]],
|
||||||
|
SegmentQueryMasks::default(),
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1537,6 +2264,7 @@ mod tests {
|
|||||||
uv1: None,
|
uv1: None,
|
||||||
indices: Vec::new(),
|
indices: Vec::new(),
|
||||||
batches: Vec::new(),
|
batches: Vec::new(),
|
||||||
|
triangle_descriptors: None,
|
||||||
node_names: None,
|
node_names: None,
|
||||||
animation: Some(ModelAnimation {
|
animation: Some(ModelAnimation {
|
||||||
keys: vec![
|
keys: vec![
|
||||||
@@ -1598,6 +2326,7 @@ mod tests {
|
|||||||
uv1: None,
|
uv1: None,
|
||||||
indices: Vec::new(),
|
indices: Vec::new(),
|
||||||
batches: Vec::new(),
|
batches: Vec::new(),
|
||||||
|
triangle_descriptors: None,
|
||||||
node_names: None,
|
node_names: None,
|
||||||
animation: Some(ModelAnimation {
|
animation: Some(ModelAnimation {
|
||||||
keys: vec![
|
keys: vec![
|
||||||
@@ -1662,6 +2391,7 @@ mod tests {
|
|||||||
uv1: None,
|
uv1: None,
|
||||||
indices: Vec::new(),
|
indices: Vec::new(),
|
||||||
batches: Vec::new(),
|
batches: Vec::new(),
|
||||||
|
triangle_descriptors: None,
|
||||||
node_names: None,
|
node_names: None,
|
||||||
animation: Some(ModelAnimation {
|
animation: Some(ModelAnimation {
|
||||||
keys: vec![
|
keys: vec![
|
||||||
@@ -2420,6 +3150,155 @@ mod tests {
|
|||||||
track.sample(AnimationTime(frame as f32))
|
track.sample(AnimationTime(frame as f32))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires licensed corpus"]
|
||||||
|
fn factory_wall_door_and_reversed_segments_match_native_query() {
|
||||||
|
let root = corpus_root("IS");
|
||||||
|
let archive_path = files_under(&root)
|
||||||
|
.into_iter()
|
||||||
|
.find(|path| {
|
||||||
|
path.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.is_some_and(|name| name.eq_ignore_ascii_case("fortif.rlb"))
|
||||||
|
})
|
||||||
|
.expect("fortif.rlb in licensed corpus");
|
||||||
|
let archive_bytes = std::fs::read(&archive_path).expect("fortif.rlb bytes");
|
||||||
|
let archive = fparkan_nres::decode(
|
||||||
|
Arc::from(archive_bytes.into_boxed_slice()),
|
||||||
|
ReadProfile::Compatible,
|
||||||
|
)
|
||||||
|
.expect("fortif.rlb archive");
|
||||||
|
let entry = archive
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.name_bytes().eq_ignore_ascii_case(b"fr_l_plant.msh"))
|
||||||
|
.expect("plant factory MSH");
|
||||||
|
let nested = fparkan_nres::decode(
|
||||||
|
Arc::from(
|
||||||
|
archive
|
||||||
|
.payload(entry.id())
|
||||||
|
.expect("MSH payload")
|
||||||
|
.to_vec()
|
||||||
|
.into_boxed_slice(),
|
||||||
|
),
|
||||||
|
ReadProfile::Compatible,
|
||||||
|
)
|
||||||
|
.expect("nested plant MSH");
|
||||||
|
let msh = decode_msh(&nested).expect("plant MSH document");
|
||||||
|
let model = validate_msh(&msh).expect("plant MSH model");
|
||||||
|
let slot = selected_slot(&model, NodeId(1), Lod(0), Group(0)).expect("Node 1 slot");
|
||||||
|
assert_eq!(slot, SlotId(0));
|
||||||
|
let hierarchy = node38_fallback_hierarchy(&model).expect("Node38 poses");
|
||||||
|
let pose = hierarchy.poses[1];
|
||||||
|
assert_eq!(pose.rotation, [0.0, 0.0, 0.0, 1.0]);
|
||||||
|
|
||||||
|
let masks = SegmentQueryMasks {
|
||||||
|
node_include: 0,
|
||||||
|
node_exclude: 0,
|
||||||
|
batch_include: 0,
|
||||||
|
batch_exclude: 0x0208,
|
||||||
|
triangle_include: 0,
|
||||||
|
triangle_exclude: 0x0024,
|
||||||
|
};
|
||||||
|
let local = |point: [f32; 3]| {
|
||||||
|
[
|
||||||
|
point[0] - pose.translation[0],
|
||||||
|
point[1] - pose.translation[1],
|
||||||
|
point[2] - pose.translation[2],
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let wall = query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(1),
|
||||||
|
slot,
|
||||||
|
[local([-10.5, 22.16, 2.5]), local([-10.5, 19.16, 2.5])],
|
||||||
|
masks,
|
||||||
|
)
|
||||||
|
.expect("wall query")
|
||||||
|
.expect("solid wall hit");
|
||||||
|
assert_eq!(wall.batch_index, 5);
|
||||||
|
assert_eq!(wall.triangle_descriptor_index, 62);
|
||||||
|
assert_eq!(wall.distance_squared, 4.0);
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(1),
|
||||||
|
slot,
|
||||||
|
[local([0.5, 22.16, 2.5]), local([0.5, 19.16, 2.5])],
|
||||||
|
masks,
|
||||||
|
)
|
||||||
|
.expect("door query")
|
||||||
|
.is_none());
|
||||||
|
assert!(query_slot_segment(
|
||||||
|
&model,
|
||||||
|
NodeId(1),
|
||||||
|
slot,
|
||||||
|
[local([-10.5, 19.16, 2.5]), local([-10.5, 22.16, 2.5])],
|
||||||
|
masks,
|
||||||
|
)
|
||||||
|
.expect("reversed wall query")
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segment_model(batch_fields: &[(u16, u16)], tri_flags: &[u16]) -> ModelAsset {
|
||||||
|
assert_eq!(batch_fields.len(), tri_flags.len());
|
||||||
|
let mut nodes_raw = node38([u16::MAX; 15]);
|
||||||
|
nodes_raw[0..2].copy_from_slice(&0x0004_u16.to_le_bytes());
|
||||||
|
let mut positions = Vec::with_capacity(batch_fields.len() * 3);
|
||||||
|
let mut indices = Vec::with_capacity(batch_fields.len() * 3);
|
||||||
|
let mut batches = Vec::with_capacity(batch_fields.len());
|
||||||
|
for index in 0..batch_fields.len() {
|
||||||
|
positions.extend_from_slice(&[[-1.0, 0.0, -1.0], [0.0, 0.0, 1.0], [1.0, 0.0, -1.0]]);
|
||||||
|
indices.extend_from_slice(&[0, 1, 2]);
|
||||||
|
batches.push(Batch {
|
||||||
|
batch_flags: batch_fields[index].0,
|
||||||
|
material_index_hi: batch_fields[index].1,
|
||||||
|
material_index: 0,
|
||||||
|
lightmap_index: 0,
|
||||||
|
local_batch_index: u16::try_from(index).expect("local batch index"),
|
||||||
|
index_count: 3,
|
||||||
|
index_start: u32::try_from(index * 3).expect("index start"),
|
||||||
|
vertex_count: 3,
|
||||||
|
base_vertex: u32::try_from(index * 3).expect("base vertex"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ModelAsset {
|
||||||
|
node_stride: 38,
|
||||||
|
node_count: 1,
|
||||||
|
nodes_raw,
|
||||||
|
bounding_sphere_center: [0.0; 3],
|
||||||
|
bounding_sphere_radius: 1.0,
|
||||||
|
slots: vec![Slot {
|
||||||
|
tri_start: 0,
|
||||||
|
tri_count: u16::try_from(tri_flags.len()).expect("triangle count"),
|
||||||
|
batch_start: 0,
|
||||||
|
batch_count: u16::try_from(batch_fields.len()).expect("batch count"),
|
||||||
|
aabb_min: [-1.0, 0.0, -1.0],
|
||||||
|
aabb_max: [1.0, 0.0, 1.0],
|
||||||
|
sphere_center: [0.0; 3],
|
||||||
|
sphere_radius: 1.0,
|
||||||
|
opaque: [0; 5],
|
||||||
|
}],
|
||||||
|
positions,
|
||||||
|
normals: None,
|
||||||
|
uv0: None,
|
||||||
|
uv1: None,
|
||||||
|
indices,
|
||||||
|
batches,
|
||||||
|
node_names: None,
|
||||||
|
animation: None,
|
||||||
|
triangle_descriptors: Some(
|
||||||
|
tri_flags
|
||||||
|
.iter()
|
||||||
|
.map(|tri_flags| TriangleDescriptor {
|
||||||
|
tri_flags: *tri_flags,
|
||||||
|
opaque: [0; 4],
|
||||||
|
normal: [0, i16::MAX, 0],
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn decode_nested(bytes: &[u8]) -> Result<NresDocument, NresError> {
|
fn decode_nested(bytes: &[u8]) -> Result<NresDocument, NresError> {
|
||||||
fparkan_nres::decode(
|
fparkan_nres::decode(
|
||||||
Arc::from(bytes.to_vec().into_boxed_slice()),
|
Arc::from(bytes.to_vec().into_boxed_slice()),
|
||||||
|
|||||||
@@ -6810,6 +6810,7 @@ mod tests {
|
|||||||
uv1: None,
|
uv1: None,
|
||||||
indices: Vec::new(),
|
indices: Vec::new(),
|
||||||
batches: Vec::new(),
|
batches: Vec::new(),
|
||||||
|
triangle_descriptors: None,
|
||||||
node_names: None,
|
node_names: None,
|
||||||
animation: None,
|
animation: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ state transitions и окончательный порядок части transp
|
|||||||
рельефом. Она пока не проверяет стены и проёмы строений; эти ограничения не
|
рельефом. Она пока не проверяет стены и проёмы строений; эти ограничения не
|
||||||
подтверждают поведение стратегической камеры или камеры робота оригинала.
|
подтверждают поведение стратегической камеры или камеры робота оригинала.
|
||||||
|
|
||||||
|
Локальный MSH-запрос `query_slot_segment` сверён с оригиналом на фабричной
|
||||||
|
стене и дверном проёме. Мировое преобразование, фильтры категории и
|
||||||
|
подключение к свободной камере остаются открытыми ([MSH](../reference/msh.md)).
|
||||||
|
|
||||||
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
|
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
|
||||||
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
|
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
|
||||||
log на минимальных сценах с одним типом материала.
|
log на минимальных сценах с одним типом материала.
|
||||||
|
|||||||
@@ -154,6 +154,45 @@ Byte `material_index` выбирает строку WEAR; high word at `+0x02`
|
|||||||
индекс через `base_vertex` попадает в position stream. `vertex_count` и
|
индекс через `base_vertex` попадает в position stream. `vertex_count` и
|
||||||
`local_batch_index` доступны renderer для исходного draw contract.
|
`local_batch_index` доступны renderer для исходного draw contract.
|
||||||
|
|
||||||
|
## Type 7 и сегментный запрос
|
||||||
|
|
||||||
|
Type 7 необязателен для обычного MSH preview. Если stream есть, каждые 16 bytes
|
||||||
|
описывают одну грань: `tri_flags` находится по `+0`, четыре неинтерпретируемых
|
||||||
|
слова — по `+2`, `+4`, `+6` и `+14`, а signed `normal[3]` — по `+8`, `+0xA` и
|
||||||
|
`+0xC`. `ModelAsset.triangle_descriptors` сохраняет декодированную таблицу как
|
||||||
|
`Option`; отсутствие Type 7 не делает статическую модель недействительной.
|
||||||
|
|
||||||
|
`query_slot_segment` использует Node38 header mask, Batch20 leading DWORD
|
||||||
|
`batch_flags | (material_index_hi << 16)` и `tri_flags`. Для каждого поля
|
||||||
|
include mask требует все установленные биты, exclude mask пропускает запись при
|
||||||
|
любом совпавшем бите. Descriptor cursor начинается с `Slot68.tri_start` и
|
||||||
|
сдвигается на `Batch20.index_count / 3` после каждой записи, даже если batch
|
||||||
|
или грань отфильтрованы. Индексы грани читаются из type 6 с учётом
|
||||||
|
`base_vertex`.
|
||||||
|
|
||||||
|
Правило восстановлено для GOG-бинарей `AniMesh.dll`
|
||||||
|
(`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`) и
|
||||||
|
`Ngi32.dll` (`bab9840d94f4e4e74ffc26677724fa896cf4823845504d09a9e025f80016edf5`).
|
||||||
|
Callback `AniMesh.dll+0x11090` переводит signed type-7 normal множителем
|
||||||
|
`1 / 32767`; функция `AniMesh.dll+0x11EB0` записывает этот scale в глобальное
|
||||||
|
поле. Callback вызывает FastProc slot `+0xB4`: в этой сборке он указывает на
|
||||||
|
`Ngi32.dll+0x1D560`. После обычной загрузки DLL порог `Ngi32.dll+0x37224`
|
||||||
|
равен нулю, поэтому plane test принимает только переход от положительного
|
||||||
|
расстояния к неположительному (`dot(normal, end - start) < 0`, расстояние в
|
||||||
|
начале `> 0`, в конце `<= 0`). `Batch20.batch_flags & 0x0002` разрешает повторный
|
||||||
|
plane test в обратном направлении, только если прямой plane test не пересёк
|
||||||
|
плоскость. Пересечение проверяется export ordinal 202
|
||||||
|
(`Ngi32.dll+0x1E60`, `mrnPointInPoly`): проекция выбирает ось с наибольшим по
|
||||||
|
модулю normal component, а точка проходит грань, пока ни одно edge expression
|
||||||
|
не меньше нуля. Равенство нулю принимается, поэтому попадание на ребро или
|
||||||
|
вершину считается попаданием. Поздний кандидат с тем же squared distance
|
||||||
|
заменяет предыдущий; расстояние считается от исходного первого endpoint.
|
||||||
|
|
||||||
|
Native helper получает точку пересечения через `RCPSS` и один шаг уточнения;
|
||||||
|
Rust использует обычное деление `f32`, поэтому последние биты координат могут
|
||||||
|
отличаться. Игнорируемый тест с ресурсами проверяет исходную стену, дверной
|
||||||
|
проём и обратное направление на `fortif.rlb/fr_l_plant.msh`.
|
||||||
|
|
||||||
## Сторона грани в цветовом проходе
|
## Сторона грани в цветовом проходе
|
||||||
|
|
||||||
В цветном проходе `Terrain.dll`, функция `0x100296F0`, сравнивает камеру с
|
В цветном проходе `Terrain.dll`, функция `0x100296F0`, сравнивает камеру с
|
||||||
|
|||||||
Reference in new issue
Block a user