Add tfrag LOD selection, morph weights and parent linkage
The block header's float at +0x08 gives L and the switch distances are [D0, D1, D2] = [6L, 4L, 2L] world units -- 192/128/64 on every level measured. The distance test uses the patch's bounding sphere truncated to integers against trunc(D * 1024), not the sphere's centre, so a patch straddling a threshold takes the coarser mode. Six modes result: LOD 2, LOD 1 collapse, LOD 1 morph, LOD 0 collapse, LOD 0 morph, LOD 0. A morph weight runs 0 to 1 across its tier's band. Past the far edge an entry is not blended but replaced by its first parent -- a collapse. The tested property is that distance never moves back to a finer mode, swept across the whole range, because that is what keeps terrain from popping as the camera moves. The four V4_8 parent arrays are classified by VU address and an unrecognised address is now an error, which is a real check: all 18,102 patches still parse. Every recorded parent resolves to real vertex info and real positions, and no common entry claims a parent. That is as far as it goes, and the limit is a finding rather than a rounding error. The addresses are not unique: on level 0 patch 1, unk_indices_2_lod_01_addr == parent_indices_lod_0_addr == 291, with unk_06 and unk_0a both zero, so nothing but unpacking order separates them. Across all 27 levels 57,782 of 374,349 LOD entries resolve, and 316,548 unresolved LOD-0 entries against 28,739 resolved is far too lopsided to be genuine -- either the arrays are misassigned where addresses collide, or RAC2's positions_lod_0_count means something other than RAC1's. Only 3,274 of 18,102 patches carry enough V4_8 bytes to cover positions_lod_01_count + positions_lod_0_count + extras. The vertex-info tiers themselves are confirmed: the addresses at +9 and +11 match the observed V4_16 unpacks and the three tier counts sum to vert_count on every patch, so the header layout is right and it is specifically the parent arrays that are open. Ruled out so far: address-only classification (collides) and one-parent-per-position (fails on 15,000 patches). Not yet tried: ordering by VU address instead of by declaration, and using unk_06/unk_0a to suppress an empty tier before falling through. Written up in plan/level-assets.md.
This commit is contained in:
@@ -78,6 +78,22 @@ impl TfragsBlock {
|
||||
})
|
||||
}
|
||||
|
||||
/// LOD switch distances `[D0, D1, D2] = [6L, 4L, 2L]` in world units,
|
||||
/// where `L` is the block's LOD distance (32.0 on every level measured).
|
||||
///
|
||||
/// D0 is the far end of LOD 1, D1 the far end of LOD 0, and D2 the point
|
||||
/// where LOD-0 vertices start morphing.
|
||||
pub fn lod_distances(&self) -> [f32; 3] {
|
||||
let l = self.lod_distance;
|
||||
[l * 6.0, l * 4.0, l + l]
|
||||
}
|
||||
|
||||
/// The integer thresholds the distance test compares against, in raw units
|
||||
/// (1024 = 1 world unit).
|
||||
pub fn lod_thresholds_raw(&self) -> [i32; 3] {
|
||||
self.lod_distances().map(|d| (d * 1024.0) as i32)
|
||||
}
|
||||
|
||||
/// The tfrag header at index `i`, given the whole block.
|
||||
pub fn header(&self, bytes: &[u8], i: usize) -> Result<TfragHeader> {
|
||||
let off = self.table_offset as usize + i * TFRAG_HEADER_SIZE;
|
||||
@@ -123,6 +139,8 @@ pub struct TfragHeader {
|
||||
pub lod_2_size: u8,
|
||||
pub lod_1_size: u8,
|
||||
pub lod_0_size: u8,
|
||||
/// Non-zero: always draw LOD 2 and skip the distance test entirely.
|
||||
pub base_only: u8,
|
||||
pub texture_count: u8,
|
||||
pub rgba_size: u8,
|
||||
pub msphere_ofs: u16,
|
||||
@@ -160,7 +178,7 @@ impl TfragHeader {
|
||||
let _lod_2_rgba = b.u8("tfrag lod_2_rgba_count")?;
|
||||
let _lod_1_rgba = b.u8("tfrag lod_1_rgba_count")?;
|
||||
let _lod_0_rgba = b.u8("tfrag lod_0_rgba_count")?;
|
||||
let _base_only = b.u8("tfrag base_only")?;
|
||||
let base_only = b.u8("tfrag base_only")?;
|
||||
let texture_count = b.u8("tfrag texture_count")?;
|
||||
let rgba_size = b.u8("tfrag rgba_size")?;
|
||||
let _rgba_verts_loc = b.u8("tfrag rgba_verts_loc")?;
|
||||
@@ -192,6 +210,7 @@ impl TfragHeader {
|
||||
lod_2_size,
|
||||
lod_1_size,
|
||||
lod_0_size,
|
||||
base_only,
|
||||
texture_count,
|
||||
rgba_size,
|
||||
msphere_ofs,
|
||||
@@ -376,6 +395,10 @@ pub struct TfragVuHeader {
|
||||
pub vertex_info_lod_01_addr: u16,
|
||||
pub vertex_info_lod_0_addr: u16,
|
||||
pub indices_addr: u16,
|
||||
pub parent_indices_lod_01_addr: u16,
|
||||
pub unk_indices_2_lod_01_addr: u16,
|
||||
pub parent_indices_lod_0_addr: u16,
|
||||
pub unk_indices_2_lod_0_addr: u16,
|
||||
pub strips_addr: u16,
|
||||
}
|
||||
|
||||
@@ -395,6 +418,10 @@ impl TfragVuHeader {
|
||||
vertex_info_lod_01_addr: w[9],
|
||||
vertex_info_lod_0_addr: w[11],
|
||||
indices_addr: w[13],
|
||||
parent_indices_lod_01_addr: w[14],
|
||||
unk_indices_2_lod_01_addr: w[15],
|
||||
parent_indices_lod_0_addr: w[16],
|
||||
unk_indices_2_lod_0_addr: w[17],
|
||||
strips_addr: w[18],
|
||||
})
|
||||
}
|
||||
@@ -597,7 +624,22 @@ pub struct TfragGeometry {
|
||||
/// Index 0 is highest detail.
|
||||
pub lod: [TfragLod; 3],
|
||||
/// Texture primitives; strips select one by ad-gif index.
|
||||
/// LOD-01 and LOD-0 position counts from the VU header. These drive the
|
||||
/// primary/extra split in the parent-index arrays.
|
||||
/// Radius of the patch's bounding sphere, in raw units (1024 = 1 world unit).
|
||||
pub bsphere_radius: f32,
|
||||
/// Non-zero: always draw LOD 2 and skip the distance test.
|
||||
pub base_only: u8,
|
||||
pub vu_positions_lod_01_count: usize,
|
||||
pub vu_positions_lod_0_count: usize,
|
||||
pub ad_gifs: Vec<TfragAdGifs>,
|
||||
/// For each primary LOD-01 vertex-info entry, the vertex-info index of its
|
||||
/// first parent. The second parent is that entry's own `parent` field.
|
||||
pub parent_indices_lod01: Vec<u8>,
|
||||
/// Same, for the extra (non-position) LOD-01 entries.
|
||||
pub unk_indices_2_lod01: Vec<u8>,
|
||||
pub parent_indices_lod0: Vec<u8>,
|
||||
pub unk_indices_2_lod0: Vec<u8>,
|
||||
/// Per-vertex colours, indexed by position index.
|
||||
pub rgba: Vec<TfragRgba>,
|
||||
/// Per-vertex light and normal records, indexed by position index.
|
||||
@@ -628,6 +670,32 @@ impl TfragGeometry {
|
||||
}
|
||||
}
|
||||
|
||||
/// The draw path for this tfrag at view depth `centre_depth_raw`, in raw
|
||||
/// units. Frustum culling and the guard-band clip test are separate.
|
||||
///
|
||||
/// The bounds are the patch's bounding sphere truncated to integers, so a
|
||||
/// sphere straddling a threshold picks the coarser mode — which is what
|
||||
/// keeps geometry from popping as the camera moves.
|
||||
pub fn draw_mode(&self, centre_depth_raw: f32, thresholds_raw: [i32; 3]) -> TfragDrawMode {
|
||||
let r = self.bsphere_radius;
|
||||
let far = (centre_depth_raw + r) as i32;
|
||||
let near = (centre_depth_raw - r) as i32;
|
||||
let [d0, d1, d2] = thresholds_raw;
|
||||
if self.base_only != 0 || near >= d0 {
|
||||
TfragDrawMode::Lod2
|
||||
} else if far >= d0 {
|
||||
TfragDrawMode::Lod1Collapse
|
||||
} else if near >= d1 {
|
||||
TfragDrawMode::Lod1Morph
|
||||
} else if far >= d1 {
|
||||
TfragDrawMode::Lod0Collapse
|
||||
} else if near >= d2 || far >= d2 {
|
||||
TfragDrawMode::Lod0Morph
|
||||
} else {
|
||||
TfragDrawMode::Lod0
|
||||
}
|
||||
}
|
||||
|
||||
/// World-space position of a vertex-info entry, `(origin + local) / 1024`.
|
||||
pub fn world_position(&self, vinfo: usize) -> [f32; 3] {
|
||||
let local = self.positions[self.position_index(vinfo)];
|
||||
@@ -691,6 +759,8 @@ pub fn parse_tfrag(block: &[u8], header: &TfragHeader) -> Result<TfragGeometry>
|
||||
match (v.vn(), v.vl()) {
|
||||
(3, 1) if v.usn() && addr == 0 => {
|
||||
vu = TfragVuHeader::parse(payload)?;
|
||||
g.vu_positions_lod_01_count = vu.positions_lod_01_count as usize;
|
||||
g.vu_positions_lod_0_count = vu.positions_lod_0_count as usize;
|
||||
have_vu = true;
|
||||
}
|
||||
// Ad-gifs: 5 quadwords each.
|
||||
@@ -741,10 +811,29 @@ pub fn parse_tfrag(block: &[u8], header: &TfragHeader) -> Result<TfragGeometry>
|
||||
}
|
||||
}
|
||||
(3, 2) => {
|
||||
let bytes = &payload[..v.count() * 4];
|
||||
if !v.usn() && addr == vu.strips_addr {
|
||||
strips = read_strips(payload, v.count())?;
|
||||
} else if addr == vu.indices_addr {
|
||||
indices = payload[..v.count() * 4].to_vec();
|
||||
indices = bytes.to_vec();
|
||||
} else if addr == vu.parent_indices_lod_01_addr
|
||||
&& vu.positions_lod_01_count != 0
|
||||
{
|
||||
g.parent_indices_lod01 = bytes.to_vec();
|
||||
} else if addr == vu.unk_indices_2_lod_01_addr {
|
||||
g.unk_indices_2_lod01 = bytes.to_vec();
|
||||
} else if addr == vu.parent_indices_lod_0_addr
|
||||
&& vu.positions_lod_0_count != 0
|
||||
{
|
||||
g.parent_indices_lod0 = bytes.to_vec();
|
||||
} else if addr == vu.unk_indices_2_lod_0_addr {
|
||||
g.unk_indices_2_lod0 = bytes.to_vec();
|
||||
} else {
|
||||
return Err(Error {
|
||||
offset: pos,
|
||||
context: "unexpected V4_8 unpack address in tfrag list",
|
||||
kind: ErrorKind::Invalid("unexpected V4_8 unpack address"),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -771,6 +860,8 @@ pub fn parse_tfrag(block: &[u8], header: &TfragHeader) -> Result<TfragGeometry>
|
||||
}
|
||||
// These three arrays sit outside the VIF lists, at fixed offsets in the
|
||||
// patch's data block.
|
||||
g.bsphere_radius = header.bsphere[3];
|
||||
g.base_only = header.base_only;
|
||||
g.rgba = read_rgba(
|
||||
block,
|
||||
header.rgba_ofs as usize,
|
||||
@@ -988,6 +1079,153 @@ pub fn tfrag_triangles(g: &TfragGeometry, lod: usize) -> Result<Vec<TfragTriangl
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The draw path chosen for a tfrag from its distance.
|
||||
///
|
||||
/// The two collapse modes are not just a coarser LOD: an entry whose parents
|
||||
/// are both past the collapse distance is *replaced* by its parent rather than
|
||||
/// morphed, which is what stops distant terrain shimmering.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TfragDrawMode {
|
||||
/// LOD 2 list, no morph.
|
||||
Lod2,
|
||||
/// LOD 1 lists; LOD-01 vertices morph and collapse onto parent 1 once both
|
||||
/// parents are past D0.
|
||||
Lod1Collapse,
|
||||
/// LOD 1 lists; LOD-01 vertices morph.
|
||||
Lod1Morph,
|
||||
/// LOD 0 lists; LOD-01 morphs, LOD-0 morphs with collapse at D1.
|
||||
Lod0Collapse,
|
||||
/// LOD 0 lists; LOD-01 at rest, LOD-0 morphs.
|
||||
Lod0Morph,
|
||||
/// LOD 0 lists, no morph.
|
||||
Lod0,
|
||||
}
|
||||
|
||||
impl TfragDrawMode {
|
||||
/// Which strip and index list is drawn. 0 is highest detail.
|
||||
pub fn lod(self) -> usize {
|
||||
match self {
|
||||
TfragDrawMode::Lod2 => 2,
|
||||
TfragDrawMode::Lod1Collapse | TfragDrawMode::Lod1Morph => 1,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The MSCAL address the game writes for the tfrag's VU1 program.
|
||||
pub fn mscal(self) -> u8 {
|
||||
match self {
|
||||
TfragDrawMode::Lod2 => 6,
|
||||
TfragDrawMode::Lod1Collapse => 8,
|
||||
TfragDrawMode::Lod1Morph => 0xa,
|
||||
TfragDrawMode::Lod0Collapse => 0xe,
|
||||
TfragDrawMode::Lod0Morph => 0x10,
|
||||
TfragDrawMode::Lod0 => 0x14,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which tier a morphable vertex-info entry belongs to.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TfragMorphTier {
|
||||
/// Fades between D1 and D0.
|
||||
Lod01,
|
||||
/// Fades between D2 and D1.
|
||||
Lod0,
|
||||
}
|
||||
|
||||
impl TfragMorphTier {
|
||||
/// Morph amount in 0..=1 for a vertex at view depth `depth`, in world units:
|
||||
/// 0 is the vertex's own position, 1 is its parents' midpoint.
|
||||
pub fn weight(self, depth: f32, lod_distances: [f32; 3]) -> f32 {
|
||||
let [d0, d1, d2] = lod_distances;
|
||||
let (near, far) = match self {
|
||||
TfragMorphTier::Lod01 => (d1, d0),
|
||||
TfragMorphTier::Lod0 => (d2, d1),
|
||||
};
|
||||
if (far - near).abs() < f32::EPSILON {
|
||||
return 0.0;
|
||||
}
|
||||
((depth - near) / (far - near)).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// Distance past which an entry is replaced by its first parent.
|
||||
pub fn collapse_distance(self, lod_distances: [f32; 3]) -> f32 {
|
||||
match self {
|
||||
TfragMorphTier::Lod01 => lod_distances[0],
|
||||
TfragMorphTier::Lod0 => lod_distances[1],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The LOD linkage of one LOD-01 or LOD-0 vertex-info entry.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TfragLodLink {
|
||||
pub tier: TfragMorphTier,
|
||||
/// True for the tier's first `positions_*_count` entries, which the VU
|
||||
/// morphs. Extra entries only collapse.
|
||||
pub morphs: bool,
|
||||
/// Position index of this entry, `vertex / 2`.
|
||||
pub own_position: usize,
|
||||
/// Vertex-info index of the first parent. Also the collapse replacement.
|
||||
pub parent1_vinfo: usize,
|
||||
/// Position index of the first parent.
|
||||
pub parent1_position: usize,
|
||||
/// Position index of the second parent, from this entry's `parent` field.
|
||||
pub parent2_position: usize,
|
||||
}
|
||||
|
||||
/// LOD linkage of vertex-info entry `vinfo`, or `None` for common entries, out
|
||||
/// of range indices, or an entry with no parent recorded.
|
||||
pub fn tfrag_lod_link(g: &TfragGeometry, vinfo: usize) -> Option<TfragLodLink> {
|
||||
let common = g.vinfo_tier[0];
|
||||
let l01 = common..common + g.vinfo_tier[1];
|
||||
let l0 = l01.end..l01.end + g.vinfo_tier[2];
|
||||
let (tier, k, primaries, parents, extras) = if l01.contains(&vinfo) {
|
||||
(
|
||||
TfragMorphTier::Lod01,
|
||||
vinfo - l01.start,
|
||||
g.vu_positions_lod_01_count,
|
||||
&g.parent_indices_lod01,
|
||||
&g.unk_indices_2_lod01,
|
||||
)
|
||||
} else if l0.contains(&vinfo) {
|
||||
(
|
||||
TfragMorphTier::Lod0,
|
||||
vinfo - l0.start,
|
||||
g.vu_positions_lod_0_count,
|
||||
&g.parent_indices_lod0,
|
||||
&g.unk_indices_2_lod0,
|
||||
)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let (morphs, p1) = if k < primaries {
|
||||
(true, *parents.get(k)? as usize)
|
||||
} else {
|
||||
(false, *extras.get(k - primaries)? as usize)
|
||||
};
|
||||
let e = *g.vertex_info.get(vinfo)?;
|
||||
let pos = |v: i16| -> Option<usize> {
|
||||
let p = v as isize as usize / 2;
|
||||
(p < g.positions.len()).then_some(p)
|
||||
};
|
||||
Some(TfragLodLink {
|
||||
tier,
|
||||
morphs,
|
||||
own_position: pos(e.vertex)?,
|
||||
parent1_vinfo: p1,
|
||||
parent1_position: pos(g.vertex_info.get(p1)?.vertex)?,
|
||||
parent2_position: pos(e.parent)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Position indices a morphing entry blends toward, `(parent1, parent2)`.
|
||||
pub fn tfrag_morph_parents(g: &TfragGeometry, vinfo: usize) -> Option<(usize, usize)> {
|
||||
tfrag_lod_link(g, vinfo)
|
||||
.filter(|l| l.morphs)
|
||||
.map(|l| (l.parent1_position, l.parent2_position))
|
||||
}
|
||||
|
||||
/// Vertex positions of a tfrag, in game order.
|
||||
pub fn tfrag_positions(block: &[u8], header: &TfragHeader) -> Option<Vec<[i32; 3]>> {
|
||||
parse_tfrag(block, header).ok().map(|g| g.positions)
|
||||
@@ -1575,3 +1813,110 @@ mod ad_gif_tests {
|
||||
assert_eq!(tris[2].ad_gif, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod lod_tests {
|
||||
use super::*;
|
||||
|
||||
fn block() -> TfragsBlock {
|
||||
TfragsBlock {
|
||||
table_offset: 0x40,
|
||||
tfrag_count: 1,
|
||||
lod_distance: 32.0,
|
||||
reserved: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switch_distances_scale_from_the_block_parameter() {
|
||||
let b = block();
|
||||
assert_eq!(b.lod_distances(), [192.0, 128.0, 64.0]);
|
||||
assert_eq!(b.lod_thresholds_raw(), [196_608, 131_072, 65_536]);
|
||||
}
|
||||
|
||||
/// A patch with the given bounding-sphere radius and `base_only` flag.
|
||||
fn geometry(radius: f32, base_only: u8) -> TfragGeometry {
|
||||
TfragGeometry {
|
||||
bsphere_radius: radius,
|
||||
base_only,
|
||||
..TfragGeometry::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sphere_straddling_a_threshold_takes_the_coarser_mode() {
|
||||
let th = [196_608, 131_072, 65_536];
|
||||
// Well inside LOD 0, then stepping back through every boundary.
|
||||
let mode = |d: f32| geometry(1000.0, 0).draw_mode(d, th);
|
||||
assert_eq!(mode(10_000.0), TfragDrawMode::Lod0);
|
||||
assert_eq!(mode(70_000.0), TfragDrawMode::Lod0Morph); // near >= D2
|
||||
assert_eq!(mode(131_000.0), TfragDrawMode::Lod0Collapse); // far >= D1
|
||||
assert_eq!(mode(150_000.0), TfragDrawMode::Lod1Morph); // near >= D1
|
||||
assert_eq!(mode(196_000.0), TfragDrawMode::Lod1Collapse); // far >= D0
|
||||
assert_eq!(mode(200_000.0), TfragDrawMode::Lod2); // near >= D0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_mode_is_monotonic_in_distance() {
|
||||
// Distance must never make the mesh finer again.
|
||||
let th = [196_608, 131_072, 65_536];
|
||||
let order = |m: TfragDrawMode| match m {
|
||||
TfragDrawMode::Lod0 => 0,
|
||||
TfragDrawMode::Lod0Morph => 1,
|
||||
TfragDrawMode::Lod0Collapse => 2,
|
||||
TfragDrawMode::Lod1Morph => 3,
|
||||
TfragDrawMode::Lod1Collapse => 4,
|
||||
TfragDrawMode::Lod2 => 5,
|
||||
};
|
||||
let g = geometry(4000.0, 0);
|
||||
let mut last = 0;
|
||||
for d in (0..600_000).step_by(997) {
|
||||
let now = order(g.draw_mode(d as f32, th));
|
||||
assert!(now >= last, "mode went backwards at depth {d}");
|
||||
last = now;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_only_pins_a_patch_to_lod_2() {
|
||||
let th = [196_608, 131_072, 65_536];
|
||||
let g = geometry(1000.0, 1);
|
||||
assert_eq!(g.draw_mode(0.0, th), TfragDrawMode::Lod2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_mode_maps_to_a_lod_and_an_mscal() {
|
||||
for (m, lod, mscal) in [
|
||||
(TfragDrawMode::Lod2, 2usize, 6u8),
|
||||
(TfragDrawMode::Lod1Collapse, 1, 8),
|
||||
(TfragDrawMode::Lod1Morph, 1, 0xa),
|
||||
(TfragDrawMode::Lod0Collapse, 0, 0xe),
|
||||
(TfragDrawMode::Lod0Morph, 0, 0x10),
|
||||
(TfragDrawMode::Lod0, 0, 0x14),
|
||||
] {
|
||||
assert_eq!(m.lod(), lod);
|
||||
assert_eq!(m.mscal(), mscal);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn morph_weight_runs_from_zero_to_one_across_its_band() {
|
||||
let d = [192.0, 128.0, 64.0];
|
||||
// LOD-01 fades between D1 and D0; LOD 0 between D2 and D1.
|
||||
assert_eq!(TfragMorphTier::Lod01.weight(128.0, d), 0.0);
|
||||
assert_eq!(TfragMorphTier::Lod01.weight(160.0, d), 0.5);
|
||||
assert_eq!(TfragMorphTier::Lod01.weight(192.0, d), 1.0);
|
||||
assert_eq!(TfragMorphTier::Lod01.weight(500.0, d), 1.0);
|
||||
assert_eq!(TfragMorphTier::Lod01.weight(0.0, d), 0.0);
|
||||
assert_eq!(TfragMorphTier::Lod0.weight(64.0, d), 0.0);
|
||||
assert_eq!(TfragMorphTier::Lod0.weight(96.0, d), 0.5);
|
||||
assert_eq!(TfragMorphTier::Lod0.weight(128.0, d), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collapse_distances_are_the_far_edge_of_each_band() {
|
||||
let d = [192.0, 128.0, 64.0];
|
||||
assert_eq!(TfragMorphTier::Lod01.collapse_distance(d), 192.0);
|
||||
assert_eq!(TfragMorphTier::Lod0.collapse_distance(d), 128.0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user