A24: спавн пропа из игры — ядро спавн-меню
Some checks are pending
CI / checks (push) Waiting to run
Some checks are pending
CI / checks (push) Waiting to run
Движок: геометрия модели вынесена в append_model_geometry (меш, материалы, вершины с трансформацией, батчи со смещением индексов на вершины экземпляра). Поверх неё World::spawn_prop — то же, что делает Q-меню GMod: геометрия, сущность класса prop_physics (её видят Lua и физика), тело в мире физики по габаритам меша, экземпляр пропа для move_prop. Клиент: клавиша G — спавн пропа по взгляду (модель из SOURCE_RS_SPAWN_MODEL, по умолчанию models/props_junk/watermelon01.mdl), и аргумент --spawn <модель> для отладки и скриптов. Проп сразу попадает в габариты коллизии игрока — их клиент пересобирает каждый кадр. Проверено кадром: арбуз заспавнен в 96 юнитах по взгляду и отрисован (--player-pos="-2438 -966 48", камера у "-2500 -1050 60|5 0"). Грабли записаны в дневник: буферы рендера собраны на сборке мира, поэтому после спавна рендерер пересобирается целиком — позже заменим до-аллокацией вершин.
This commit is contained in:
parent
8a2eb4ef60
commit
35e04409c4
3 changed files with 151 additions and 7 deletions
|
|
@ -101,6 +101,10 @@ pub struct MapArgs {
|
|||
#[arg(long, value_name = "X Y Z")]
|
||||
pub pos: Option<String>,
|
||||
|
||||
/// Заспавнить проп на старте: путь к модели (отладка спавна)
|
||||
#[arg(long, value_name = "MODEL")]
|
||||
pub spawn: Vec<String>,
|
||||
|
||||
/// Позиция игрока «x y z» (отладка; по умолчанию — спавн)
|
||||
#[arg(long, value_name = "X Y Z")]
|
||||
pub player_pos: Option<String>,
|
||||
|
|
@ -144,6 +148,7 @@ impl MapArgs {
|
|||
speed: 400.0,
|
||||
pos: None,
|
||||
player_pos: None,
|
||||
spawn: Vec::new(),
|
||||
angles: None,
|
||||
debug_materials: false,
|
||||
only_material: None,
|
||||
|
|
@ -233,6 +238,20 @@ pub fn map(assets: Option<PathBuf>, args: MapArgs) -> anyhow::Result<()> {
|
|||
index
|
||||
});
|
||||
|
||||
// `--spawn <модель>` — спавн пропов на старте (отладка Q-меню, A24).
|
||||
for model in &args.spawn {
|
||||
let origin = player.eye_position() + math::Vec3::new(1.0, 0.0, 0.0) * 96.0;
|
||||
let angles = math::Vec3::new(0.0, 0.0, 0.0);
|
||||
let mut budget = usize::MAX;
|
||||
match world
|
||||
.borrow_mut()
|
||||
.spawn_prop(&vfs, model, origin, angles, 10.0, &mut budget)
|
||||
{
|
||||
Some(index) => info!(index, model = %model, "проп заспавнен на старте"),
|
||||
None => warn!(model = %model, "проп на старте не заспавнен"),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(dir) = &args.dump_textures {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
for (index, material) in world.borrow().materials.iter().enumerate() {
|
||||
|
|
@ -377,6 +396,48 @@ pub fn map(assets: Option<PathBuf>, args: MapArgs) -> anyhow::Result<()> {
|
|||
keycode: Some(Keycode::Escape),
|
||||
..
|
||||
} => break 'running,
|
||||
Event::KeyDown {
|
||||
keycode: Some(Keycode::G),
|
||||
repeat: false,
|
||||
..
|
||||
} => {
|
||||
// Спавн пропа по взгляду — первый шаг к Q-меню (A24).
|
||||
// Модель задаётся `SOURCE_RS_SPAWN_MODEL`, по умолчанию
|
||||
// классический арбуз Sandbox.
|
||||
let model = std::env::var("SOURCE_RS_SPAWN_MODEL")
|
||||
.unwrap_or_else(|_| "models/props_junk/watermelon01.mdl".to_string());
|
||||
let origin = player.eye_position() + camera.forward() * 96.0;
|
||||
let angles = math::Vec3::new(0.0, camera.yaw, 0.0);
|
||||
let mut budget = usize::MAX;
|
||||
let spawned = world.borrow_mut().spawn_prop(
|
||||
&vfs,
|
||||
&model,
|
||||
origin,
|
||||
angles,
|
||||
10.0,
|
||||
&mut budget,
|
||||
);
|
||||
match spawned {
|
||||
Some(index) => {
|
||||
info!(index, model = %model, "проп заспавнен");
|
||||
log.push_back(format!("spawn: {model}"));
|
||||
// Буферы рендера собраны на сборке мира: чтобы
|
||||
// новый проп было видно, пересобираем рендерер.
|
||||
// Позже — до-аллокация вершин под спавн.
|
||||
match WorldRenderer::new(&gfx, &world.borrow(), args.debug_materials) {
|
||||
Ok(fresh) => renderer = fresh,
|
||||
Err(err) => warn!("рендерер не пересобран: {err:#}"),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!(model = %model, "проп не заспавнен");
|
||||
log.push_back(format!("spawn не удался: {model}"));
|
||||
}
|
||||
}
|
||||
while log.len() > 8 {
|
||||
log.pop_front();
|
||||
}
|
||||
}
|
||||
Event::KeyDown {
|
||||
keycode: Some(Keycode::V),
|
||||
repeat: false,
|
||||
|
|
|
|||
|
|
@ -2254,14 +2254,19 @@ impl World {
|
|||
/// Путь тот же, что у пропов при сборке мира: модель с диска, материалы с
|
||||
/// дедупликацией по имени, вершины с трансформацией, батчи на материалы.
|
||||
/// Дальше экземпляр двигается `move_model_instance`.
|
||||
pub fn spawn_model_instance(
|
||||
/// Добавляет геометрию модели в мир: меш, материалы, вершины, батчи.
|
||||
///
|
||||
/// Возвращает (индекс меша, первый индекс вершины, сколько вершин) —
|
||||
/// этим пользуются и экземпляры без сущности (модель игрока, вьюмодель),
|
||||
/// и спавн пропов на ходу.
|
||||
pub fn append_model_geometry(
|
||||
&mut self,
|
||||
vfs: &VirtualFs,
|
||||
model_name: &str,
|
||||
origin: Vec3,
|
||||
angles: Vec3,
|
||||
budget: &mut usize,
|
||||
) -> Option<usize> {
|
||||
) -> Option<(usize, u32, u32)> {
|
||||
let key = crate::model::normalize_model_name(model_name);
|
||||
let model = crate::model::load_model(vfs, &key, budget).ok()?;
|
||||
self.prop_meshes
|
||||
|
|
@ -2317,8 +2322,6 @@ impl World {
|
|||
if to > model.indices.len() {
|
||||
continue;
|
||||
}
|
||||
// Индексы модели локальные — смещаем на вершины экземпляра в
|
||||
// общем буфере мира, как это делает сборка пропов.
|
||||
self.indices
|
||||
.extend(model.indices[from..to].iter().map(|i| first_vertex + i));
|
||||
let index_count = self.indices.len() as u32 - first_index;
|
||||
|
|
@ -2328,8 +2331,22 @@ impl World {
|
|||
index_count,
|
||||
});
|
||||
}
|
||||
Some((mesh, first_vertex, vertex_count))
|
||||
}
|
||||
|
||||
/// Добавляет экземпляр модели без сущности (модель игрока, вьюмодель).
|
||||
pub fn spawn_model_instance(
|
||||
&mut self,
|
||||
vfs: &VirtualFs,
|
||||
model_name: &str,
|
||||
origin: Vec3,
|
||||
angles: Vec3,
|
||||
budget: &mut usize,
|
||||
) -> Option<usize> {
|
||||
let (mesh, first_vertex, vertex_count) =
|
||||
self.append_model_geometry(vfs, model_name, origin, angles, budget)?;
|
||||
self.model_instances.push(ModelInstance {
|
||||
model: key,
|
||||
model: crate::model::normalize_model_name(model_name),
|
||||
mesh,
|
||||
origin,
|
||||
angles,
|
||||
|
|
@ -2339,6 +2356,60 @@ impl World {
|
|||
Some(self.model_instances.len() - 1)
|
||||
}
|
||||
|
||||
/// Спавнит проп на ходу — то, что делает Q-меню спавна в GMod.
|
||||
///
|
||||
/// Путь тот же, что у пропов карты: геометрия модели, сущность класса
|
||||
/// `prop_physics` (её видят Lua и физика), тело в мире физики по габаритам
|
||||
/// меша и экземпляр пропа для `move_prop`.
|
||||
pub fn spawn_prop(
|
||||
&mut self,
|
||||
vfs: &VirtualFs,
|
||||
model_name: &str,
|
||||
origin: Vec3,
|
||||
angles: Vec3,
|
||||
mass: f32,
|
||||
budget: &mut usize,
|
||||
) -> Option<usize> {
|
||||
let (mesh, first_vertex, vertex_count) =
|
||||
self.append_model_geometry(vfs, model_name, origin, angles, budget)?;
|
||||
|
||||
let mut entity = crate::entity::Entity::new("prop_physics".to_string());
|
||||
entity.model = Some(model_name.to_string());
|
||||
entity.origin = origin;
|
||||
entity.angles = angles;
|
||||
let entity_index = self.entities.spawn(entity)?;
|
||||
|
||||
let half = self.prop_meshes[mesh].bounds_half_extents;
|
||||
let physics_body = if half.length_squared() > 1.0 {
|
||||
match self
|
||||
.physics
|
||||
.add_prop(entity_index, origin, angles, half, mass.max(0.1))
|
||||
{
|
||||
Ok(body) => Some(body),
|
||||
Err(err) => {
|
||||
tracing::warn!(model = %model_name, "тело пропа не создано: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.props.push(PropInstance {
|
||||
entity: entity_index,
|
||||
classname: "prop_physics".to_string(),
|
||||
model: crate::model::normalize_model_name(model_name),
|
||||
mesh,
|
||||
origin,
|
||||
angles,
|
||||
first_vertex,
|
||||
vertex_count,
|
||||
physics_body,
|
||||
});
|
||||
self.update_prop_bounds();
|
||||
Some(self.props.len() - 1)
|
||||
}
|
||||
|
||||
/// Двигает экземпляр модели без сущности.
|
||||
pub fn move_model_instance(&mut self, index: usize, origin: Vec3, angles: Vec3) -> bool {
|
||||
let Some(instance) = self.model_instances.get(index) else {
|
||||
|
|
|
|||
|
|
@ -46,8 +46,20 @@ SOURCE_RS_PLAYER_MODEL= DISPLAY=:99 SDL_VIDEODRIVER=x11 cargo run -q -p client -
|
|||
при вставке в общий буфер к ним обязательно прибавляется `first_vertex`
|
||||
(без этого был мусор в кадре).
|
||||
|
||||
**Следующее:** (а) вьюмодель в первом лице и скрытие своей модели, когда камера
|
||||
внутри неё; (б) реестр SWEP из Lua; (в) физган; (г) скелет+анимации моделей.
|
||||
**Спавн пропа из игры сделан (ядро A24).** `World::spawn_prop` (геометрия из
|
||||
`append_model_geometry`, сущность `prop_physics`, тело в физике, экземпляр
|
||||
пропа) + в клиенте клавиша `G` (спавн по взгляду) и аргумент
|
||||
`--spawn <модель>` (отладка и скрипты). Проверено кадром: арбуз заспавнен и
|
||||
отрисован (`--spawn models/props_junk/watermelon01.mdl`, камера у
|
||||
`-2500 -1050 60|5 0`).
|
||||
|
||||
Грабли: буферы рендера созданы на сборке мира — после спавна рендерер
|
||||
пересобирается целиком (медленно, но работает); позже нужна до-аллокация
|
||||
вершин под спавн.
|
||||
|
||||
**Следующее:** (а) до-аллокация вершин под спавн вместо пересборки рендерера;
|
||||
(б) реестр SWEP из Lua и вьюмодель; (в) физган (луч → проп → таскание);
|
||||
(г) скрытие своей модели, когда камера внутри неё; (д) скелет+анимации.
|
||||
|
||||
## 3. Что уже сделано и лежит в `main`
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue