Вьюмодель: доворот в координатах модели и камерный базис; коллизия из меша
Some checks are pending
CI / checks (push) Waiting to run
Some checks are pending
CI / checks (push) Waiting to run
Вьюмодель (жалобы пользователя): - доворот ствола (-90°) теперь применяется в координатах модели, до поворота камеры: прибавка к мировому yaw при взгляде вниз превращалась в крен; - смещения (вправо/вниз) задаются в базисе камеры, а не мировым (0,0,-12) — иначе при наклоне камеры вьюмодель уезжала к центру экрана («ось ниже»); - play_model_animation принимает model_yaw (доворот модели), rewrite_instance тоже — путь без скелета больше не теряет ориентацию. Коллизия пропов из настоящего меша: - Physics::add_prop_hull — выпуклая оболочка вершин модели для динамических пропов (как vcollide в Source), вместо коробки с выступами; - Physics::add_static_mesh — тримеш из треугольников модели для prop_static: раньше у статических пропов коллизии не было вовсе, игрок проходил сквозь мебель; - PropMesh хранит индексы модели; оба места создания тел используют оболочку (с откатом на коробку, если вершин < 4). Все сьюты и 29 картовых тестов зелёные.
This commit is contained in:
parent
b52da2ed2e
commit
a491010aa6
4 changed files with 166 additions and 27 deletions
|
|
@ -779,23 +779,30 @@ pub fn map(assets: Option<PathBuf>, 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<PathBuf>, 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();
|
||||
|
|
|
|||
|
|
@ -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<BodyId, PhysicsError> {
|
||||
if self.handles.len() >= MAX_BODIES {
|
||||
return Err(PhysicsError::TooManyBodies(self.handles.len()));
|
||||
}
|
||||
let points: Vec<Vector> = 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<BodyId, PhysicsError> {
|
||||
if self.handles.len() >= MAX_BODIES {
|
||||
return Err(PhysicsError::TooManyBodies(self.handles.len()));
|
||||
}
|
||||
let points: Vec<Vector> = 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,
|
||||
|
|
|
|||
|
|
@ -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<formats::mdl::Bone>,
|
||||
/// Привязка вершин к костям, по одной на `positions`.
|
||||
pub skins: Vec<crate::model::VertexSkin>,
|
||||
/// Индексы треугольников в координатах модели — для настоящих
|
||||
/// коллайдеров (оболочка/тримеш вместо коробки).
|
||||
pub indices: Vec<u32>,
|
||||
/// Данные анимаций: своя модель и включённые библиотеки (A26).
|
||||
pub anim: Vec<std::sync::Arc<AnimData>>,
|
||||
/// Нормали в локальных координатах модели.
|
||||
|
|
@ -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]],
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue