2025-07-09 20:24:11 -04:00
|
|
|
use std::ptr::{null, null_mut};
|
|
|
|
|
2025-07-09 14:59:02 -04:00
|
|
|
use bevy::{
|
|
|
|
asset::RenderAssetUsages,
|
2025-07-14 20:30:14 -04:00
|
|
|
color::palettes::{
|
|
|
|
css::WHITE,
|
|
|
|
tailwind::{BLUE_100, GREEN_100, PINK_100, RED_500},
|
|
|
|
},
|
|
|
|
pbr::wireframe::{Wireframe, WireframeConfig, WireframePlugin},
|
2025-07-09 22:55:35 -04:00
|
|
|
picking::pointer::PointerInteraction,
|
2025-07-09 14:59:02 -04:00
|
|
|
prelude::*,
|
2025-07-14 20:30:14 -04:00
|
|
|
render::{
|
|
|
|
RenderPlugin,
|
|
|
|
mesh::{Indices, PrimitiveTopology},
|
|
|
|
settings::{RenderCreation, WgpuFeatures, WgpuSettings},
|
|
|
|
},
|
2025-07-09 14:59:02 -04:00
|
|
|
};
|
2025-07-08 23:33:33 -04:00
|
|
|
use icarus::TerritoryType::TerritoryTypeSheet;
|
2025-07-14 20:30:14 -04:00
|
|
|
use kawari::{
|
|
|
|
config::get_config,
|
|
|
|
world::{Navmesh, NavmeshParams},
|
|
|
|
};
|
2025-07-08 23:33:33 -04:00
|
|
|
use physis::{
|
|
|
|
common::{Language, Platform},
|
2025-07-09 14:59:02 -04:00
|
|
|
layer::{LayerEntryData, LayerGroup, ModelCollisionType, Transformation},
|
2025-07-08 23:33:33 -04:00
|
|
|
lvb::Lvb,
|
2025-07-14 20:30:14 -04:00
|
|
|
model::MDL,
|
2025-07-09 14:59:02 -04:00
|
|
|
pcb::{Pcb, ResourceNode},
|
2025-07-08 23:33:33 -04:00
|
|
|
resource::{Resource, SqPackResource},
|
2025-07-14 20:30:14 -04:00
|
|
|
tera::{PlateModel, Terrain},
|
2025-07-08 23:33:33 -04:00
|
|
|
};
|
2025-07-09 19:17:07 -04:00
|
|
|
use recastnavigation_sys::{
|
2025-07-14 20:30:14 -04:00
|
|
|
CreateContext, DT_SUCCESS, RC_MESH_NULL_IDX, dtCreateNavMeshData, dtNavMeshCreateParams,
|
|
|
|
dtNavMeshQuery, dtNavMeshQuery_findNearestPoly, dtPolyRef, dtQueryFilter,
|
2025-07-09 22:55:35 -04:00
|
|
|
rcAllocCompactHeightfield, rcAllocContourSet, rcAllocHeightfield, rcAllocPolyMesh,
|
|
|
|
rcAllocPolyMeshDetail, rcBuildCompactHeightfield, rcBuildContours,
|
|
|
|
rcBuildContoursFlags_RC_CONTOUR_TESS_WALL_EDGES, rcBuildDistanceField, rcBuildPolyMesh,
|
|
|
|
rcBuildPolyMeshDetail, rcBuildRegions, rcCalcGridSize, rcContext, rcCreateHeightfield,
|
|
|
|
rcErodeWalkableArea, rcHeightfield, rcMarkWalkableTriangles, rcRasterizeTriangles,
|
2025-07-09 19:17:07 -04:00
|
|
|
};
|
2025-07-08 23:33:33 -04:00
|
|
|
|
2025-07-09 14:59:02 -04:00
|
|
|
#[derive(Resource)]
|
|
|
|
struct ZoneToLoad(u16);
|
|
|
|
|
2025-07-09 22:55:35 -04:00
|
|
|
#[derive(Resource, Default)]
|
|
|
|
struct NavigationState {
|
2025-07-14 20:30:14 -04:00
|
|
|
navmesh: Navmesh,
|
2025-07-09 22:55:35 -04:00
|
|
|
path: Vec<Vec3>,
|
2025-07-14 20:30:14 -04:00
|
|
|
from_position: Vec3,
|
|
|
|
to_position: Vec3,
|
2025-07-09 22:55:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
impl NavigationState {
|
2025-07-14 20:30:14 -04:00
|
|
|
pub fn calculate_path(&mut self) {
|
|
|
|
let start_pos = [
|
|
|
|
self.from_position.x,
|
|
|
|
self.from_position.y,
|
|
|
|
self.from_position.z,
|
|
|
|
];
|
|
|
|
let end_pos = [self.to_position.x, self.to_position.y, self.to_position.z];
|
|
|
|
|
|
|
|
self.path = self
|
|
|
|
.navmesh
|
|
|
|
.calculate_path(start_pos, end_pos)
|
|
|
|
.iter()
|
|
|
|
.map(|x| Vec3::from_slice(x))
|
|
|
|
.collect();
|
2025-07-09 22:55:35 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
unsafe impl Send for NavigationState {}
|
|
|
|
unsafe impl Sync for NavigationState {}
|
|
|
|
|
2025-07-08 23:33:33 -04:00
|
|
|
fn main() {
|
|
|
|
tracing_subscriber::fmt::init();
|
|
|
|
|
|
|
|
let args: Vec<String> = std::env::args().collect();
|
|
|
|
let zone_id: u16 = args[1].parse().unwrap();
|
|
|
|
|
2025-07-09 14:59:02 -04:00
|
|
|
App::new()
|
2025-07-09 22:55:35 -04:00
|
|
|
.add_event::<Navigate>()
|
2025-07-14 20:30:14 -04:00
|
|
|
.add_event::<SetOrigin>()
|
|
|
|
.add_event::<SetTarget>()
|
|
|
|
.add_plugins((
|
|
|
|
DefaultPlugins.set(RenderPlugin {
|
|
|
|
render_creation: RenderCreation::Automatic(WgpuSettings {
|
|
|
|
features: WgpuFeatures::POLYGON_MODE_LINE,
|
|
|
|
..default()
|
|
|
|
}),
|
|
|
|
..default()
|
|
|
|
}),
|
|
|
|
MeshPickingPlugin,
|
|
|
|
WireframePlugin::default(),
|
|
|
|
))
|
2025-07-09 14:59:02 -04:00
|
|
|
.add_systems(Startup, setup)
|
2025-07-09 22:55:35 -04:00
|
|
|
.add_systems(Update, draw_mesh_intersections)
|
2025-07-14 20:30:14 -04:00
|
|
|
.insert_resource(WireframeConfig {
|
|
|
|
global: false,
|
|
|
|
default_color: WHITE.into(),
|
|
|
|
})
|
2025-07-09 14:59:02 -04:00
|
|
|
.insert_resource(ZoneToLoad(zone_id))
|
2025-07-09 22:55:35 -04:00
|
|
|
.insert_resource(NavigationState::default())
|
2025-07-09 14:59:02 -04:00
|
|
|
.run();
|
|
|
|
}
|
|
|
|
|
2025-07-09 22:55:35 -04:00
|
|
|
#[derive(Event, Reflect, Clone, Debug)]
|
2025-07-14 20:30:14 -04:00
|
|
|
struct Navigate();
|
|
|
|
|
|
|
|
#[derive(Event, Reflect, Clone, Debug)]
|
|
|
|
struct SetOrigin(Vec3);
|
|
|
|
|
|
|
|
#[derive(Event, Reflect, Clone, Debug)]
|
|
|
|
struct SetTarget(Vec3);
|
2025-07-09 22:55:35 -04:00
|
|
|
|
2025-07-09 14:59:02 -04:00
|
|
|
/// Walk each node, add it's collision model to the scene.
|
|
|
|
fn walk_node(
|
|
|
|
node: &ResourceNode,
|
|
|
|
commands: &mut Commands,
|
|
|
|
meshes: &mut ResMut<Assets<Mesh>>,
|
|
|
|
materials: &mut ResMut<Assets<StandardMaterial>>,
|
|
|
|
transform: &Transformation,
|
2025-07-09 19:17:07 -04:00
|
|
|
context: *mut rcContext,
|
|
|
|
height_field: *mut rcHeightfield,
|
2025-07-09 14:59:02 -04:00
|
|
|
) {
|
|
|
|
if !node.vertices.is_empty() {
|
2025-07-09 22:55:35 -04:00
|
|
|
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::all());
|
2025-07-09 14:59:02 -04:00
|
|
|
|
|
|
|
let mut positions = Vec::new();
|
|
|
|
for vec in &node.vertices {
|
2025-07-14 20:30:14 -04:00
|
|
|
positions.push(Vec3::from_slice(vec));
|
2025-07-09 14:59:02 -04:00
|
|
|
}
|
|
|
|
|
2025-07-09 15:58:57 -04:00
|
|
|
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions.clone());
|
2025-07-09 14:59:02 -04:00
|
|
|
|
|
|
|
let mut indices = Vec::new();
|
|
|
|
for polygon in &node.polygons {
|
|
|
|
let mut vec: Vec<u32> = Vec::from(&polygon.vertex_indices)
|
|
|
|
.iter()
|
|
|
|
.map(|x| *x as u32)
|
|
|
|
.collect();
|
|
|
|
assert!(vec.len() == 3);
|
|
|
|
indices.append(&mut vec);
|
|
|
|
}
|
|
|
|
|
2025-07-09 15:58:57 -04:00
|
|
|
mesh.insert_indices(Indices::U32(indices.clone()));
|
2025-07-09 14:59:02 -04:00
|
|
|
|
2025-07-09 22:55:35 -04:00
|
|
|
mesh.compute_normals();
|
|
|
|
|
2025-07-14 20:30:14 -04:00
|
|
|
let transform = Transform {
|
|
|
|
translation: Vec3::from_array(transform.translation),
|
|
|
|
rotation: Quat::from_euler(
|
|
|
|
EulerRot::XYZ,
|
|
|
|
transform.rotation[0],
|
|
|
|
transform.rotation[1],
|
|
|
|
transform.rotation[2],
|
|
|
|
),
|
|
|
|
scale: Vec3::from_array(transform.scale),
|
|
|
|
};
|
|
|
|
|
2025-07-09 15:58:57 -04:00
|
|
|
// insert into 3d scene
|
2025-07-09 22:55:35 -04:00
|
|
|
commands
|
|
|
|
.spawn((
|
|
|
|
Mesh3d(meshes.add(mesh)),
|
|
|
|
MeshMaterial3d(materials.add(Color::srgb(
|
|
|
|
fastrand::f32(),
|
|
|
|
fastrand::f32(),
|
|
|
|
fastrand::f32(),
|
|
|
|
))),
|
2025-07-14 20:30:14 -04:00
|
|
|
transform,
|
2025-07-09 22:55:35 -04:00
|
|
|
))
|
|
|
|
.observe(
|
2025-07-14 20:30:14 -04:00
|
|
|
|mut trigger: Trigger<Pointer<Click>>,
|
|
|
|
mut navigate_events: EventWriter<Navigate>,
|
|
|
|
mut target_events: EventWriter<SetTarget>,
|
|
|
|
mut origin_events: EventWriter<SetOrigin>| {
|
2025-07-09 22:55:35 -04:00
|
|
|
let click_event: &Pointer<Click> = trigger.event();
|
2025-07-14 20:30:14 -04:00
|
|
|
match click_event.button {
|
|
|
|
PointerButton::Primary => {
|
|
|
|
target_events.write(SetTarget(click_event.hit.position.unwrap()));
|
|
|
|
}
|
|
|
|
PointerButton::Secondary => {
|
|
|
|
origin_events.write(SetOrigin(click_event.hit.position.unwrap()));
|
|
|
|
}
|
|
|
|
PointerButton::Middle => {
|
|
|
|
navigate_events.write(Navigate());
|
|
|
|
}
|
|
|
|
}
|
2025-07-09 22:55:35 -04:00
|
|
|
trigger.propagate(false);
|
|
|
|
},
|
|
|
|
);
|
2025-07-09 15:58:57 -04:00
|
|
|
|
2025-07-09 19:17:07 -04:00
|
|
|
// Step 2: insert geoemtry into heightfield
|
|
|
|
let tile_indices: Vec<i32> = indices.iter().map(|x| *x as i32).collect();
|
2025-07-09 20:24:11 -04:00
|
|
|
let mut tri_area_ids: Vec<u8> = vec![0; tile_indices.len() / 3];
|
2025-07-09 15:58:57 -04:00
|
|
|
|
2025-07-14 20:30:14 -04:00
|
|
|
// transform the vertices on the CPU
|
|
|
|
let mut tile_vertices: Vec<[f32; 3]> = Vec::new();
|
|
|
|
let transform_matrix = transform.compute_matrix();
|
|
|
|
for vertex in &positions {
|
|
|
|
let transformed_vertex = transform_matrix.transform_point3(*vertex);
|
|
|
|
tile_vertices.push([
|
|
|
|
transformed_vertex.x,
|
|
|
|
transformed_vertex.y,
|
|
|
|
transformed_vertex.z,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2025-07-09 19:17:07 -04:00
|
|
|
unsafe {
|
2025-07-09 20:24:11 -04:00
|
|
|
let ntris = tile_indices.len() as i32 / 3;
|
|
|
|
|
|
|
|
// mark areas as walkable
|
|
|
|
rcMarkWalkableTriangles(
|
|
|
|
context,
|
|
|
|
45.0,
|
2025-07-14 20:30:14 -04:00
|
|
|
std::mem::transmute::<*const [f32; 3], *const f32>(tile_vertices.as_ptr()),
|
2025-07-09 20:24:11 -04:00
|
|
|
positions.len() as i32,
|
|
|
|
tile_indices.as_ptr(),
|
|
|
|
ntris,
|
|
|
|
tri_area_ids.as_mut_ptr(),
|
|
|
|
);
|
|
|
|
|
2025-07-09 19:17:07 -04:00
|
|
|
assert!(rcRasterizeTriangles(
|
|
|
|
context,
|
2025-07-14 20:30:14 -04:00
|
|
|
std::mem::transmute::<*const [f32; 3], *const f32>(tile_vertices.as_ptr()),
|
2025-07-09 19:17:07 -04:00
|
|
|
positions.len() as i32,
|
|
|
|
tile_indices.as_ptr(),
|
|
|
|
tri_area_ids.as_ptr(),
|
2025-07-09 20:24:11 -04:00
|
|
|
ntris,
|
2025-07-09 19:17:07 -04:00
|
|
|
height_field,
|
2025-07-09 20:24:11 -04:00
|
|
|
2
|
2025-07-09 19:17:07 -04:00
|
|
|
));
|
|
|
|
}
|
2025-07-09 14:59:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
for child in &node.children {
|
2025-07-09 19:17:07 -04:00
|
|
|
walk_node(
|
|
|
|
&child,
|
|
|
|
commands,
|
|
|
|
meshes,
|
|
|
|
materials,
|
|
|
|
transform,
|
|
|
|
context,
|
|
|
|
height_field,
|
|
|
|
);
|
2025-07-09 14:59:02 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-14 20:30:14 -04:00
|
|
|
fn add_plate(
|
|
|
|
plate: &PlateModel,
|
|
|
|
tera_path: &str,
|
|
|
|
sqpack_resource: &mut SqPackResource,
|
|
|
|
commands: &mut Commands,
|
|
|
|
meshes: &mut ResMut<Assets<Mesh>>,
|
|
|
|
materials: &mut ResMut<Assets<StandardMaterial>>,
|
|
|
|
context: *mut rcContext,
|
|
|
|
height_field: *mut rcHeightfield,
|
|
|
|
) {
|
|
|
|
let mdl_path = format!("{}/bgplate/{}", tera_path, plate.filename);
|
|
|
|
let mdl_bytes = sqpack_resource.read(&mdl_path).unwrap();
|
|
|
|
let mdl = MDL::from_existing(&mdl_bytes).unwrap();
|
|
|
|
|
|
|
|
let lod = &mdl.lods[0];
|
|
|
|
for part in &lod.parts {
|
|
|
|
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::all());
|
|
|
|
|
|
|
|
let mut positions = Vec::new();
|
|
|
|
let mut normals = Vec::new();
|
|
|
|
for vec in &part.vertices {
|
|
|
|
positions.push(Vec3::from_slice(&vec.position));
|
|
|
|
normals.push(Vec3::from_slice(&vec.normal));
|
|
|
|
}
|
|
|
|
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions.clone());
|
|
|
|
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals.clone());
|
|
|
|
|
|
|
|
mesh.insert_indices(Indices::U16(part.indices.clone()));
|
|
|
|
|
|
|
|
let transform = Transform::from_xyz(plate.position.0, 0.0, plate.position.1);
|
|
|
|
|
|
|
|
// insert into 3d scene
|
|
|
|
commands
|
|
|
|
.spawn((
|
|
|
|
Mesh3d(meshes.add(mesh)),
|
|
|
|
MeshMaterial3d(materials.add(Color::srgb(
|
|
|
|
fastrand::f32(),
|
|
|
|
fastrand::f32(),
|
|
|
|
fastrand::f32(),
|
|
|
|
))),
|
|
|
|
transform,
|
|
|
|
))
|
|
|
|
.observe(
|
|
|
|
|mut trigger: Trigger<Pointer<Click>>,
|
|
|
|
mut navigate_events: EventWriter<Navigate>,
|
|
|
|
mut target_events: EventWriter<SetTarget>,
|
|
|
|
mut origin_events: EventWriter<SetOrigin>| {
|
|
|
|
let click_event: &Pointer<Click> = trigger.event();
|
|
|
|
match click_event.button {
|
|
|
|
PointerButton::Primary => {
|
|
|
|
target_events.write(SetTarget(click_event.hit.position.unwrap()));
|
|
|
|
}
|
|
|
|
PointerButton::Secondary => {
|
|
|
|
origin_events.write(SetOrigin(click_event.hit.position.unwrap()));
|
|
|
|
}
|
|
|
|
PointerButton::Middle => {
|
|
|
|
navigate_events.write(Navigate());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
trigger.propagate(false);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// Step 2: insert geoemtry into heightfield
|
|
|
|
let tile_indices: Vec<i32> = part.indices.iter().map(|x| *x as i32).collect();
|
|
|
|
let mut tri_area_ids: Vec<u8> = vec![0; tile_indices.len() / 3];
|
|
|
|
|
|
|
|
// transform the vertices on the CPU
|
|
|
|
let mut tile_vertices: Vec<[f32; 3]> = Vec::new();
|
|
|
|
let transform_matrix = transform.compute_matrix();
|
|
|
|
for vertex in &positions {
|
|
|
|
let transformed_vertex = transform_matrix.transform_point3(*vertex);
|
|
|
|
tile_vertices.push([
|
|
|
|
transformed_vertex.x,
|
|
|
|
transformed_vertex.y,
|
|
|
|
transformed_vertex.z,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
unsafe {
|
|
|
|
let ntris = tile_indices.len() as i32 / 3;
|
|
|
|
|
|
|
|
// mark areas as walkable
|
|
|
|
rcMarkWalkableTriangles(
|
|
|
|
context,
|
|
|
|
45.0,
|
|
|
|
std::mem::transmute::<*const [f32; 3], *const f32>(tile_vertices.as_ptr()),
|
|
|
|
positions.len() as i32,
|
|
|
|
tile_indices.as_ptr(),
|
|
|
|
ntris,
|
|
|
|
tri_area_ids.as_mut_ptr(),
|
|
|
|
);
|
|
|
|
|
|
|
|
assert!(rcRasterizeTriangles(
|
|
|
|
context,
|
|
|
|
std::mem::transmute::<*const [f32; 3], *const f32>(tile_vertices.as_ptr()),
|
|
|
|
positions.len() as i32,
|
|
|
|
tile_indices.as_ptr(),
|
|
|
|
tri_area_ids.as_ptr(),
|
|
|
|
ntris,
|
|
|
|
height_field,
|
|
|
|
2
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-09 21:23:32 -04:00
|
|
|
fn get_polygon_at_location(
|
|
|
|
query: *const dtNavMeshQuery,
|
|
|
|
position: [f32; 3],
|
|
|
|
filter: &dtQueryFilter,
|
|
|
|
) -> (dtPolyRef, [f32; 3]) {
|
2025-07-14 20:30:14 -04:00
|
|
|
let extents = [2.0, 4.0, 2.0];
|
2025-07-09 21:23:32 -04:00
|
|
|
|
|
|
|
unsafe {
|
|
|
|
let mut nearest_ref = 0;
|
|
|
|
let mut nearest_pt = [0.0; 3];
|
|
|
|
assert!(
|
|
|
|
dtNavMeshQuery_findNearestPoly(
|
|
|
|
query,
|
|
|
|
position.as_ptr(),
|
|
|
|
extents.as_ptr(),
|
|
|
|
filter,
|
|
|
|
&mut nearest_ref,
|
|
|
|
nearest_pt.as_mut_ptr()
|
|
|
|
) == DT_SUCCESS
|
|
|
|
);
|
|
|
|
|
|
|
|
return (nearest_ref, nearest_pt);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-09 14:59:02 -04:00
|
|
|
/// Setup 3D scene.
|
|
|
|
fn setup(
|
|
|
|
mut commands: Commands,
|
|
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
|
|
zone_id: Res<ZoneToLoad>,
|
2025-07-09 22:55:35 -04:00
|
|
|
mut navigation_state: ResMut<NavigationState>,
|
2025-07-09 14:59:02 -04:00
|
|
|
) {
|
|
|
|
let zone_id = zone_id.0;
|
|
|
|
let config = get_config();
|
|
|
|
|
2025-07-08 23:33:33 -04:00
|
|
|
tracing::info!("Generating navmesh for zone {zone_id}!");
|
|
|
|
|
|
|
|
let mut sqpack_resource =
|
|
|
|
SqPackResource::from_existing(Platform::Win32, &config.filesystem.game_path);
|
|
|
|
let sheet = TerritoryTypeSheet::read_from(&mut sqpack_resource, Language::None).unwrap();
|
|
|
|
let Some(row) = sheet.get_row(zone_id as u32) else {
|
|
|
|
tracing::error!("Invalid zone id {zone_id}!");
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
// e.g. ffxiv/fst_f1/fld/f1f3/level/f1f3
|
|
|
|
let bg_path = row.Bg().into_string().unwrap();
|
|
|
|
|
|
|
|
let path = format!("bg/{}.lvb", &bg_path);
|
|
|
|
let lvb_file = sqpack_resource.read(&path).unwrap();
|
|
|
|
let lvb = Lvb::from_existing(&lvb_file).unwrap();
|
|
|
|
|
2025-07-09 19:17:07 -04:00
|
|
|
let context;
|
|
|
|
let height_field;
|
2025-07-09 20:24:11 -04:00
|
|
|
let cell_size = 0.25;
|
|
|
|
let cell_height = 0.25;
|
2025-07-09 15:58:57 -04:00
|
|
|
unsafe {
|
2025-07-09 19:17:07 -04:00
|
|
|
context = CreateContext(true);
|
|
|
|
|
|
|
|
// Step 1: Create a heightfield
|
2025-07-09 20:24:11 -04:00
|
|
|
let mut size_x: i32 = 0;
|
|
|
|
let mut size_z: i32 = 0;
|
2025-07-09 19:17:07 -04:00
|
|
|
let min_bounds = [-100.0, -100.0, -100.0];
|
|
|
|
let max_bounds = [100.0, 100.0, 100.0];
|
2025-07-09 20:24:11 -04:00
|
|
|
|
|
|
|
rcCalcGridSize(
|
|
|
|
min_bounds.as_ptr(),
|
|
|
|
max_bounds.as_ptr(),
|
|
|
|
cell_size,
|
|
|
|
&mut size_x,
|
|
|
|
&mut size_z,
|
|
|
|
);
|
2025-07-09 19:17:07 -04:00
|
|
|
|
|
|
|
height_field = rcAllocHeightfield();
|
|
|
|
assert!(rcCreateHeightfield(
|
|
|
|
context,
|
|
|
|
height_field,
|
|
|
|
size_x,
|
|
|
|
size_z,
|
|
|
|
min_bounds.as_ptr(),
|
|
|
|
max_bounds.as_ptr(),
|
|
|
|
cell_size,
|
|
|
|
cell_height
|
|
|
|
));
|
2025-07-09 15:58:57 -04:00
|
|
|
}
|
|
|
|
|
2025-07-14 20:30:14 -04:00
|
|
|
let scene = &lvb.scns[0];
|
|
|
|
|
|
|
|
let tera_bytes = sqpack_resource
|
|
|
|
.read(&*format!(
|
|
|
|
"{}/bgplate/terrain.tera",
|
|
|
|
scene.general.path_terrain
|
|
|
|
))
|
|
|
|
.unwrap();
|
|
|
|
let tera = Terrain::from_existing(&tera_bytes).unwrap();
|
|
|
|
for plate in tera.plates {
|
|
|
|
add_plate(
|
|
|
|
&plate,
|
|
|
|
&scene.general.path_terrain,
|
|
|
|
&mut sqpack_resource,
|
|
|
|
&mut commands,
|
|
|
|
&mut meshes,
|
|
|
|
&mut materials,
|
|
|
|
context,
|
|
|
|
height_field,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
for path in &scene.header.path_layer_group_resources {
|
2025-07-08 23:33:33 -04:00
|
|
|
if path.contains("bg.lgb") {
|
2025-07-09 14:59:02 -04:00
|
|
|
tracing::info!("Processing {path}...");
|
2025-07-08 23:38:03 -04:00
|
|
|
|
|
|
|
let lgb_file = sqpack_resource.read(path).unwrap();
|
|
|
|
let lgb = LayerGroup::from_existing(&lgb_file);
|
|
|
|
let Some(lgb) = lgb else {
|
|
|
|
tracing::error!(
|
|
|
|
"Failed to parse {path}, this is most likely a bug in Physis and should be reported somewhere!"
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
// TODO: i think we know which layer is specifically used for navmesh gen, better check that LVB
|
|
|
|
for chunk in &lgb.chunks {
|
|
|
|
for layer in &chunk.layers {
|
|
|
|
for object in &layer.objects {
|
|
|
|
if let LayerEntryData::BG(bg) = &object.data {
|
|
|
|
if !bg.collision_asset_path.value.is_empty() {
|
|
|
|
tracing::info!("Considering {} for navimesh", object.instance_id);
|
|
|
|
tracing::info!("- Loading {}", bg.collision_asset_path.value);
|
2025-07-09 14:59:02 -04:00
|
|
|
|
|
|
|
// NOTE: assert is here to find out the unknown
|
|
|
|
assert!(bg.collision_type == ModelCollisionType::Replace);
|
|
|
|
|
|
|
|
let pcb_file = sqpack_resource
|
|
|
|
.read(&bg.collision_asset_path.value)
|
|
|
|
.unwrap();
|
|
|
|
let pcb = Pcb::from_existing(&pcb_file).unwrap();
|
|
|
|
|
|
|
|
walk_node(
|
|
|
|
&pcb.root_node,
|
|
|
|
&mut commands,
|
|
|
|
&mut meshes,
|
|
|
|
&mut materials,
|
|
|
|
&object.transform,
|
2025-07-09 19:17:07 -04:00
|
|
|
context,
|
|
|
|
height_field,
|
2025-07-09 14:59:02 -04:00
|
|
|
);
|
2025-07-08 23:38:03 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-07-08 23:33:33 -04:00
|
|
|
}
|
|
|
|
}
|
2025-07-09 14:59:02 -04:00
|
|
|
|
2025-07-09 19:17:07 -04:00
|
|
|
unsafe {
|
|
|
|
// Step 3: Build a compact heightfield out of the normal heightfield
|
|
|
|
let compact_heightfield = rcAllocCompactHeightfield();
|
2025-07-09 20:24:11 -04:00
|
|
|
let walkable_height = 2;
|
2025-07-09 19:17:07 -04:00
|
|
|
let walkable_climb = 1;
|
2025-07-09 20:24:11 -04:00
|
|
|
let walkable_radius = 0.5;
|
2025-07-09 19:17:07 -04:00
|
|
|
assert!(rcBuildCompactHeightfield(
|
|
|
|
context,
|
|
|
|
walkable_height,
|
|
|
|
walkable_climb,
|
|
|
|
height_field,
|
|
|
|
compact_heightfield
|
|
|
|
));
|
2025-07-09 20:24:11 -04:00
|
|
|
assert!((*compact_heightfield).spanCount > 0);
|
|
|
|
|
|
|
|
assert!(rcErodeWalkableArea(
|
|
|
|
context,
|
|
|
|
walkable_radius as i32,
|
|
|
|
compact_heightfield
|
|
|
|
));
|
|
|
|
|
|
|
|
assert!(rcBuildDistanceField(context, compact_heightfield));
|
|
|
|
|
|
|
|
let border_size = 2;
|
|
|
|
let min_region_area = 1;
|
|
|
|
let merge_region_area = 0;
|
|
|
|
assert!(rcBuildRegions(
|
|
|
|
context,
|
|
|
|
compact_heightfield,
|
|
|
|
border_size,
|
|
|
|
min_region_area,
|
|
|
|
merge_region_area
|
|
|
|
));
|
2025-07-09 19:17:07 -04:00
|
|
|
|
|
|
|
// Step 4: Build the contour set from the compact heightfield
|
|
|
|
let contour_set = rcAllocContourSet();
|
2025-07-09 20:24:11 -04:00
|
|
|
let max_error = 1.5;
|
|
|
|
let max_edge_len = (12.0 / cell_size) as i32;
|
|
|
|
let build_flags = rcBuildContoursFlags_RC_CONTOUR_TESS_WALL_EDGES as i32;
|
2025-07-09 19:17:07 -04:00
|
|
|
assert!(rcBuildContours(
|
|
|
|
context,
|
|
|
|
compact_heightfield,
|
|
|
|
max_error,
|
|
|
|
max_edge_len,
|
|
|
|
contour_set,
|
|
|
|
build_flags
|
|
|
|
));
|
2025-07-09 20:24:11 -04:00
|
|
|
assert!((*contour_set).nconts > 0);
|
2025-07-09 19:17:07 -04:00
|
|
|
|
|
|
|
// Step 5: Build the polymesh out of the contour set
|
|
|
|
let poly_mesh = rcAllocPolyMesh();
|
2025-07-09 20:24:11 -04:00
|
|
|
let nvp = 6;
|
2025-07-09 19:17:07 -04:00
|
|
|
assert!(rcBuildPolyMesh(context, contour_set, nvp, poly_mesh));
|
2025-07-09 20:24:11 -04:00
|
|
|
assert!((*poly_mesh).verts != null_mut());
|
|
|
|
assert!((*poly_mesh).nverts > 0);
|
|
|
|
|
2025-07-14 20:30:14 -04:00
|
|
|
let nvp = (*poly_mesh).nvp;
|
|
|
|
let cs = (*poly_mesh).cs;
|
|
|
|
let ch = (*poly_mesh).ch;
|
|
|
|
let orig = (*poly_mesh).bmin;
|
|
|
|
|
|
|
|
// add polymesh to visualization
|
|
|
|
{
|
|
|
|
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::all());
|
|
|
|
|
|
|
|
let mut positions = Vec::new();
|
|
|
|
for i in 0..(*poly_mesh).nverts as usize {
|
|
|
|
let v = (*poly_mesh).verts.wrapping_add(i * 3);
|
|
|
|
let x = orig[0] + *v as f32 * cs as f32;
|
|
|
|
let y = orig[1] + (*v.wrapping_add(1) + 1) as f32 * ch as f32 + 0.1;
|
|
|
|
let z = orig[2] + (*v.wrapping_add(2)) as f32 * cs as f32;
|
|
|
|
|
|
|
|
positions.push(Vec3::new(x, y, z));
|
|
|
|
}
|
|
|
|
|
|
|
|
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions.clone());
|
|
|
|
|
|
|
|
let mut indices = Vec::new();
|
|
|
|
for i in 0..(*poly_mesh).npolys as usize {
|
|
|
|
let p = (*poly_mesh).polys.wrapping_add(i * nvp as usize * 2);
|
|
|
|
for j in 2..nvp as usize {
|
|
|
|
if *(p.wrapping_add(j)) == RC_MESH_NULL_IDX {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
indices.push(*p);
|
|
|
|
indices.push(*p.wrapping_add(j - 1));
|
|
|
|
indices.push(*p.wrapping_add(j));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mesh.insert_indices(Indices::U16(indices.clone()));
|
|
|
|
|
|
|
|
//mesh.compute_normals();
|
|
|
|
|
|
|
|
// insert into 3d scene
|
|
|
|
commands.spawn((
|
|
|
|
Mesh3d(meshes.add(mesh)),
|
|
|
|
MeshMaterial3d(materials.add(Color::srgba(0.0, 0.0, 1.0, 0.5))),
|
|
|
|
Pickable::IGNORE,
|
|
|
|
Wireframe,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2025-07-09 20:24:11 -04:00
|
|
|
let flags =
|
|
|
|
std::slice::from_raw_parts_mut((*poly_mesh).flags, (*poly_mesh).npolys as usize);
|
|
|
|
for flag in flags {
|
|
|
|
*flag = 1;
|
|
|
|
}
|
2025-07-09 19:17:07 -04:00
|
|
|
|
|
|
|
// Step 6: Build the polymesh detail
|
|
|
|
let poly_mesh_detail = rcAllocPolyMeshDetail();
|
2025-07-09 20:24:11 -04:00
|
|
|
let sample_dist = 1.0;
|
|
|
|
let sample_max_error = 0.1;
|
2025-07-09 19:17:07 -04:00
|
|
|
assert!(rcBuildPolyMeshDetail(
|
|
|
|
context,
|
|
|
|
poly_mesh,
|
|
|
|
compact_heightfield,
|
|
|
|
sample_dist,
|
|
|
|
sample_max_error,
|
|
|
|
poly_mesh_detail
|
|
|
|
));
|
2025-07-09 20:24:11 -04:00
|
|
|
|
|
|
|
let mut create_params = dtNavMeshCreateParams {
|
|
|
|
// Polygon Mesh Attributes
|
|
|
|
verts: (*poly_mesh).verts,
|
|
|
|
vertCount: (*poly_mesh).nverts,
|
|
|
|
polys: (*poly_mesh).polys,
|
|
|
|
polyFlags: (*poly_mesh).flags,
|
|
|
|
polyAreas: (*poly_mesh).areas,
|
|
|
|
polyCount: (*poly_mesh).npolys,
|
|
|
|
nvp: (*poly_mesh).nvp,
|
|
|
|
|
|
|
|
// Height Detail Attributes
|
|
|
|
detailMeshes: (*poly_mesh_detail).meshes,
|
|
|
|
detailVerts: (*poly_mesh_detail).verts,
|
|
|
|
detailVertsCount: (*poly_mesh_detail).nverts,
|
|
|
|
detailTris: (*poly_mesh_detail).tris,
|
|
|
|
detailTriCount: (*poly_mesh_detail).ntris,
|
|
|
|
|
|
|
|
// Off-Mesh Connections Attributes
|
|
|
|
offMeshConVerts: null(),
|
|
|
|
offMeshConRad: null(),
|
|
|
|
offMeshConFlags: null(),
|
|
|
|
offMeshConAreas: null(),
|
|
|
|
offMeshConDir: null(),
|
|
|
|
offMeshConUserID: null(),
|
|
|
|
offMeshConCount: 0,
|
|
|
|
|
|
|
|
// Tile Attributes
|
|
|
|
userId: 0,
|
|
|
|
tileX: 0,
|
|
|
|
tileY: 0,
|
|
|
|
tileLayer: 0,
|
|
|
|
bmin: (*poly_mesh).bmin,
|
|
|
|
bmax: (*poly_mesh).bmax,
|
|
|
|
|
|
|
|
// General Configuration Attributes
|
|
|
|
walkableHeight: walkable_height as f32,
|
|
|
|
walkableRadius: walkable_radius,
|
|
|
|
walkableClimb: walkable_climb as f32,
|
|
|
|
cs: cell_size,
|
|
|
|
ch: cell_height,
|
|
|
|
buildBvTree: true,
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut out_data: *mut u8 = null_mut();
|
|
|
|
let mut out_data_size = 0;
|
|
|
|
assert!(dtCreateNavMeshData(
|
|
|
|
&mut create_params,
|
|
|
|
&mut out_data,
|
|
|
|
&mut out_data_size
|
|
|
|
));
|
|
|
|
assert!(out_data != null_mut());
|
|
|
|
assert!(out_data_size > 0);
|
2025-07-09 20:37:11 -04:00
|
|
|
|
2025-07-14 20:30:14 -04:00
|
|
|
navigation_state.navmesh = Navmesh::new(
|
|
|
|
NavmeshParams {
|
|
|
|
orig: (*poly_mesh).bmin,
|
|
|
|
tile_width: (*poly_mesh).bmax[0] - (*poly_mesh).bmin[0],
|
|
|
|
tile_height: (*poly_mesh).bmax[2] - (*poly_mesh).bmin[2],
|
|
|
|
max_tiles: 1,
|
|
|
|
max_polys: (*poly_mesh).npolys,
|
|
|
|
},
|
|
|
|
Vec::from_raw_parts(out_data, out_data_size as usize, out_data_size as usize),
|
2025-07-09 20:37:11 -04:00
|
|
|
);
|
2025-07-09 21:23:32 -04:00
|
|
|
|
2025-07-14 20:30:14 -04:00
|
|
|
// TODO: output in the correct directory
|
|
|
|
let serialized_navmesh = navigation_state.navmesh.write_to_buffer().unwrap();
|
|
|
|
std::fs::write("test.nvm", &serialized_navmesh).unwrap();
|
2025-07-09 19:17:07 -04:00
|
|
|
}
|
|
|
|
|
2025-07-09 14:59:02 -04:00
|
|
|
// camera
|
|
|
|
commands.spawn((
|
|
|
|
Camera3d::default(),
|
2025-07-14 20:30:14 -04:00
|
|
|
Transform::from_xyz(55.0, 55.0, 55.0).looking_at(Vec3::ZERO, Vec3::Y),
|
2025-07-09 14:59:02 -04:00
|
|
|
));
|
2025-07-08 23:33:33 -04:00
|
|
|
}
|
2025-07-09 22:55:35 -04:00
|
|
|
|
|
|
|
fn draw_mesh_intersections(
|
|
|
|
pointers: Query<&PointerInteraction>,
|
|
|
|
mut gizmos: Gizmos,
|
|
|
|
mut navigate_events: EventReader<Navigate>,
|
2025-07-14 20:30:14 -04:00
|
|
|
mut origin_events: EventReader<SetOrigin>,
|
|
|
|
mut target_events: EventReader<SetTarget>,
|
2025-07-09 22:55:35 -04:00
|
|
|
mut navigation_state: ResMut<NavigationState>,
|
|
|
|
) {
|
2025-07-14 20:30:14 -04:00
|
|
|
gizmos.sphere(navigation_state.from_position, 0.05, GREEN_100);
|
|
|
|
gizmos.sphere(navigation_state.to_position, 0.05, BLUE_100);
|
|
|
|
|
2025-07-09 22:55:35 -04:00
|
|
|
for pos in &navigation_state.path {
|
|
|
|
gizmos.sphere(*pos, 0.05, RED_500);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (point, normal) in pointers
|
|
|
|
.iter()
|
|
|
|
.filter_map(|interaction| interaction.get_nearest_hit())
|
|
|
|
.filter_map(|(_entity, hit)| hit.position.zip(hit.normal))
|
|
|
|
{
|
|
|
|
gizmos.sphere(point, 0.05, RED_500);
|
|
|
|
gizmos.arrow(point, point + normal.normalize() * 0.5, PINK_100);
|
|
|
|
}
|
|
|
|
|
2025-07-14 20:30:14 -04:00
|
|
|
for event in origin_events.read() {
|
|
|
|
navigation_state.from_position = event.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
for event in target_events.read() {
|
|
|
|
navigation_state.to_position = event.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
for _ in navigate_events.read() {
|
|
|
|
navigation_state.calculate_path();
|
2025-07-09 22:55:35 -04:00
|
|
|
}
|
|
|
|
}
|