diff --git a/crates/client/src/map.rs b/crates/client/src/map.rs index d9ea83d..96db6bf 100644 --- a/crates/client/src/map.rs +++ b/crates/client/src/map.rs @@ -779,23 +779,30 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { { let mut w = world.borrow_mut(); if let Some(index) = view_model_instance { - let angles = math::Vec3::new(camera.pitch, camera.yaw + view_yaw, view_roll); + let angles = math::Vec3::new(camera.pitch, camera.yaw, view_roll); // Базовая поза вьюмодели: чуть справа и ниже глаз, ствол вперёд. - // Крен — потому что модели оружия лежат в мире на боку. let eye = camera.pos; let forward = camera.forward(); let (sin_yaw, cos_yaw) = camera.yaw.to_radians().sin_cos(); let right = math::Vec3::new(sin_yaw, -cos_yaw, 0.0); - let centre = engine::model::rotate_model_vector( - view_model_center, - angles.x, - angles.y, - angles.z, + // Доворот модели — в её собственных координатах (ствол вдоль + // +Y модели), а не прибавкой к мировому yaw: иначе при взгляде + // вниз доворот превращается в крен (жалоба пользователя). + let centre_local = + engine::model::rotate_model_vector(view_model_center, 0.0, view_yaw, 0.0); + let centre = + engine::model::rotate_model_vector(centre_local, angles.x, angles.y, angles.z); + // Смещения — в базисе камеры, а не в мировом: «вниз» мировым + // вектором при наклоне камеры уезжает к центру экрана + // (жалоба пользователя: «ось ниже где-то»). + let up = engine::model::rotate_model_vector( + math::Vec3::new(0.0, 0.0, 1.0), + camera.pitch, + camera.yaw, + 0.0, ); - let origin = eye + forward * view_distance + right * 5.0 - - math::Vec3::new(0.0, 0.0, 8.0) - - centre; - w.play_model_animation(index, origin, angles, "idle01", wave_time); + let origin = eye + forward * view_distance + right * 8.0 - up * 12.0 - centre; + w.play_model_animation(index, origin, angles, "idle01", wave_time, view_yaw); } if let Some(index) = player_model_instance { let inside = camera.pos.distance(player.eye_position()) < 48.0; @@ -810,6 +817,7 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { math::Vec3::new(0.0, camera.yaw + 180.0, 0.0), "idle_all_01", wave_time, + 0.0, ); } moved_ranges = w.take_moved_ranges(); diff --git a/crates/engine/src/physics.rs b/crates/engine/src/physics.rs index b15e150..577e1a2 100644 --- a/crates/engine/src/physics.rs +++ b/crates/engine/src/physics.rs @@ -106,6 +106,8 @@ const STAND_HALF: Vec3 = Vec3 { pub(crate) enum Kind { /// `prop_physics` — динамическое твёрдое тело. Dynamic, + /// `prop_static` — тримеш, прибитый к карте. + Static, /// Игрок — кинематический контроллер. Character, } @@ -413,6 +415,97 @@ impl PhysicsWorld { }) } + /// Динамический проп по настоящему мешу: выпуклая оболочка вершин. + /// + /// Коробка давала выступы за пределы предмета (жалоба пользователя: + /// «колизия по размеру, но с выступами»). Source тоже хранит для пропов + /// выпуклые оболочки (`vcollide`), а не точный меш, — оболочка из вершин + /// модели повторяет её форму и не торчит углами. + pub fn add_prop_hull( + &mut self, + entity: EntityIndex, + origin: Vec3, + angles_deg: Vec3, + vertices: &[[f32; 3]], + mass: f32, + ) -> Result { + if self.handles.len() >= MAX_BODIES { + return Err(PhysicsError::TooManyBodies(self.handles.len())); + } + let points: Vec = vertices + .iter() + .filter(|p| p.iter().all(|v| v.is_finite())) + .map(|p| Vector::new(p[0], p[1], p[2])) + .collect(); + if points.len() < 4 { + return Err(PhysicsError::TooManyBodies(0)); + } + let Some(hull) = ColliderBuilder::convex_hull(&points) else { + return Err(PhysicsError::TooManyBodies(0)); + }; + let collider = hull + .mass(mass.max(0.1)) + .friction(0.6) + .restitution(0.1) + .build(); + let desc = RigidBodyBuilder::dynamic() + .translation(Vector::new(origin.x, origin.y, origin.z)) + .rotation(quat_from_angles(angles_deg).to_scaled_axis()); + let handle = self.world.insert_body(desc); + self.world.insert_collider(collider, Some(handle)); + self.handles.push((handle, entity, Kind::Dynamic)); + Ok(BodyId { + inner: handle, + kind: Kind::Dynamic, + }) + } + + /// Статический проп: тримеш из настоящих треугольников модели. + /// + /// `prop_static` в Source имеет коллизию (его физическая модель), у нас + /// раньше её не было вовсе — игрок проходил сквозь мебель. Тримеш даёт + /// точное совпадение с видимой геометрией. + pub fn add_static_mesh( + &mut self, + entity: EntityIndex, + origin: Vec3, + angles_deg: Vec3, + vertices: &[[f32; 3]], + indices: &[u32], + ) -> Result { + if self.handles.len() >= MAX_BODIES { + return Err(PhysicsError::TooManyBodies(self.handles.len())); + } + let points: Vec = vertices + .iter() + .filter(|p| p.iter().all(|v| v.is_finite())) + .map(|p| Vector::new(p[0], p[1], p[2])) + .collect(); + let tris: Vec<[u32; 3]> = indices + .chunks_exact(3) + .filter(|t| t.iter().all(|i| (*i as usize) < points.len())) + .map(|t| [t[0], t[1], t[2]]) + .collect(); + if points.is_empty() || tris.is_empty() { + return Err(PhysicsError::TooManyBodies(0)); + } + let Ok(collider) = ColliderBuilder::trimesh(points, tris) else { + return Err(PhysicsError::TooManyBodies(0)); + }; + let collider = collider.friction(0.6).restitution(0.1).build(); + let desc = RigidBodyBuilder::fixed() + .translation(Vector::new(origin.x, origin.y, origin.z)) + .rotation(quat_from_angles(angles_deg).to_scaled_axis()); + let handle = self.world.insert_body(desc); + self.world.insert_collider(collider, Some(handle)); + self.static_bodies += 1; + self.handles.push((handle, entity, Kind::Static)); + Ok(BodyId { + inner: handle, + kind: Kind::Static, + }) + } + /// Добавляет кинематический контроллер игрока. pub fn add_character( &mut self, diff --git a/crates/engine/src/world.rs b/crates/engine/src/world.rs index e264b86..000f9ab 100644 --- a/crates/engine/src/world.rs +++ b/crates/engine/src/world.rs @@ -2235,6 +2235,7 @@ impl World { count: u32, origin: Vec3, angles: Vec3, + model_yaw: f32, ) -> bool { let (first, count) = (first as usize, count as usize); let Some(mesh) = self.prop_meshes.get(mesh_index) else { @@ -2245,13 +2246,18 @@ impl World { } let (pitch, yaw, roll) = (angles.x, angles.y, angles.z); for i in 0..count { - let local = Vec3::new( + let local_model = Vec3::new( mesh.positions[i][0], mesh.positions[i][1], mesh.positions[i][2], ); + // Доворот в координатах модели (ось вперёд у оружия своя), затем + // поворот камеры. Порядок важен: доворот по мировому yaw при + // взгляде вниз превращался в крен. + let local = crate::model::rotate_model_vector(local_model, 0.0, model_yaw, 0.0); let pos = crate::model::rotate_model_vector(local, pitch, yaw, roll) + origin; let local_n = Vec3::new(mesh.normals[i][0], mesh.normals[i][1], mesh.normals[i][2]); + let local_n = crate::model::rotate_model_vector(local_n, 0.0, model_yaw, 0.0); let normal = crate::model::rotate_model_vector(local_n, pitch, yaw, roll); let vertex = &mut self.vertices[first + i]; vertex.pos = [pos.x, pos.y, pos.z]; @@ -2303,6 +2309,7 @@ impl World { let mut mesh_data = PropMesh::from_vertices(&model.vertices); mesh_data.bones = model.bones.clone(); mesh_data.skins = model.skins.clone(); + mesh_data.indices = model.indices.clone(); mesh_data.anim = model.anim_data.clone(); self.prop_meshes.push(mesh_data); let mesh = self.prop_meshes.len() - 1; @@ -2424,15 +2431,24 @@ impl World { let half = self.prop_meshes[mesh].bounds_half_extents; let centre = self.prop_meshes[mesh].bounds_center; + let positions = self.prop_meshes[mesh].positions.clone(); let physics_body = if half.length_squared() > 1.0 { - match self.physics.add_prop_offset( - entity_index, - origin, - angles, - half, - centre, - mass.max(0.1), - ) { + // Оболочка из вершин модели — вместо коробки, которая торчала + // выступами за пределы предмета. + let body = self + .physics + .add_prop_hull(entity_index, origin, angles, &positions, mass.max(0.1)) + .or_else(|_| { + self.physics.add_prop_offset( + entity_index, + origin, + angles, + half, + centre, + mass.max(0.1), + ) + }); + match body { Ok(body) => Some(body), Err(err) => { tracing::warn!(model = %model_name, "тело пропа не создано: {err}"); @@ -2475,6 +2491,7 @@ impl World { angles: Vec3, animation: &str, time: f32, + model_yaw: f32, ) -> bool { let Some(instance) = self.model_instances.get(index) else { return false; @@ -2491,7 +2508,14 @@ impl World { return false; }; if mesh.bones.is_empty() || mesh.skins.len() < count { - return self.rewrite_instance(mesh_index, first as u32, count as u32, origin, angles); + return self.rewrite_instance( + mesh_index, + first as u32, + count as u32, + origin, + angles, + model_yaw, + ); } // Данные анимации — ссылкой: дальше меш понадобится снять из мира, // чтобы одновременно читать его вершины и писать в общий буфер. @@ -2501,7 +2525,14 @@ impl World { }); let Some((data, anim)) = found else { // Кадров нет — экземпляр остаётся в позе покоя (обычный перенос). - return self.rewrite_instance(mesh_index, first as u32, count as u32, origin, angles); + return self.rewrite_instance( + mesh_index, + first as u32, + count as u32, + origin, + angles, + model_yaw, + ); }; // Кадр по времени; дробная часть — бленд между кадром и следующим. @@ -2571,6 +2602,7 @@ impl World { } else { blended }; + let local = crate::model::rotate_model_vector(local, 0.0, model_yaw, 0.0); let pos = crate::model::rotate_model_vector(local, angles.x, angles.y, angles.z) + origin; if let Some(vertex) = self.vertices.get_mut(first + i) { @@ -2590,7 +2622,7 @@ impl World { let (mesh, first, count) = (instance.mesh, instance.first_vertex, instance.vertex_count); self.model_instances[index].origin = origin; self.model_instances[index].angles = angles; - self.rewrite_instance(mesh, first, count, origin, angles) + self.rewrite_instance(mesh, first, count, origin, angles, 0.0) } pub fn move_prop(&mut self, index: usize, origin: Vec3, angles: Vec3) -> bool { @@ -2615,7 +2647,7 @@ impl World { // запоминает диапазон вершин для заливки на GPU. Раньше здесь была // своя копия перезаписи, и диапазоны не попадали в `moved_ranges` — // физика считала, а картинка стояла (жалоба пользователя). - let rewritten = self.rewrite_instance(mesh_index, first, count, origin, angles); + let rewritten = self.rewrite_instance(mesh_index, first, count, origin, angles, 0.0); self.update_prop_bounds(); rewritten } @@ -2912,6 +2944,9 @@ pub struct PropMesh { pub bones: Vec, /// Привязка вершин к костям, по одной на `positions`. pub skins: Vec, + /// Индексы треугольников в координатах модели — для настоящих + /// коллайдеров (оболочка/тримеш вместо коробки). + pub indices: Vec, /// Данные анимаций: своя модель и включённые библиотеки (A26). pub anim: Vec>, /// Нормали в локальных координатах модели. @@ -2947,6 +2982,7 @@ impl PropMesh { normals, bones: Vec::new(), skins: Vec::new(), + indices: Vec::new(), anim: Vec::new(), bounds_center: center, bounds_half_extents: half_extents, @@ -4452,6 +4488,7 @@ mod tests { prop_meshes: vec![PropMesh { bones: Vec::new(), skins: Vec::new(), + indices: Vec::new(), anim: Vec::new(), positions: vec![[1.0, 2.0, 3.0]], normals: vec![[0.0, 0.0, 1.0]], @@ -4514,6 +4551,7 @@ mod tests { prop_meshes: vec![PropMesh { bones: Vec::new(), skins: Vec::new(), + indices: Vec::new(), anim: Vec::new(), positions: vec![[1.0, 0.0, 0.0]], normals: vec![[1.0, 0.0, 0.0]], diff --git a/crates/engine/tests/world_real.rs b/crates/engine/tests/world_real.rs index 9c7d8d0..2db5651 100644 --- a/crates/engine/tests/world_real.rs +++ b/crates/engine/tests/world_real.rs @@ -436,7 +436,7 @@ fn real_view_model_skin_stays_coherent() { assert!(world.move_model_instance(index, origin, math::Vec3::ZERO)); let bind = instance_positions(&world, index); - assert!(world.play_model_animation(index, origin, math::Vec3::ZERO, "idle01", 0.0)); + assert!(world.play_model_animation(index, origin, math::Vec3::ZERO, "idle01", 0.0, 0.0)); let posed = instance_positions(&world, index); let mut max_delta = 0.0f32; let mut moved = 0usize; @@ -747,14 +747,14 @@ fn real_player_model_animation_moves_vertices() { // Первый кадр: поза из библиотеки анимаций, имя без `@`. assert!( - world.play_model_animation(index, origin, angles, "idle_all_01", 0.0), + world.play_model_animation(index, origin, angles, "idle_all_01", 0.0, 0.0), "анимация не найдена" ); let first = instance_positions(&world, index); assert!(!first.is_empty(), "вершин нет"); // Кадр через секунду (30 кадров): поза обязана отличаться. - assert!(world.play_model_animation(index, origin, angles, "idle_all_01", 1.0)); + assert!(world.play_model_animation(index, origin, angles, "idle_all_01", 1.0, 0.0)); let second = instance_positions(&world, index); let moved = first .iter()