A24: спавн пропа из игры — ядро спавн-меню
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:
Niko Marmeladkov 2026-10-16 23:49:43 +03:00
parent 8a2eb4ef60
commit 35e04409c4
3 changed files with 151 additions and 7 deletions

View file

@ -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,

View file

@ -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 {

View file

@ -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`