Compare commits

..

2 Commits

Author SHA1 Message Date
chexit f2838a8f08 HD stage 2: sun and moon shadows, the world relit, light shafts, fog and a colour grade (fork patch 12)
build / mod (push) Successful in 2m28s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:59:51 +03:00
chexit c42d97af08 HD effects preset: mod lights for flames, flares, blasts, wrecks, searchlights; searchlight beams no longer add shader lights (FPS)
build / mod (push) Successful in 2m26s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:18:43 +03:00
15 changed files with 2950 additions and 29 deletions
+8
View File
@@ -97,6 +97,14 @@ CI: `.gitea/workflows/build.yml` на каждый пуш собирает мо
25 с на цель в темноте), игрок — **осветительным пистолетом** или `/airdef flare`. Ракета висит ~30 с и
освещает всё вокруг на ~60–90 м.
**Графика: три пресета** (меню N → «Огонь, ремонт, эффекты…» → «Эффекты», или `/airdef shader off|on|hd`):
выкл — ванильная картинка; обычные — шейдерные огненные шары, ударная волна и подсветка; **HD** — свой рендер
освещения в форке: мир дополнительно рисуется в G-буфер (цвет без света и нормали), и все источники света мода
освещают его попиксельно в HDR — факелы ракет и двигатели, раскалённые боевые блоки, осветительные ракеты, вспышки
выстрелов, трассеры, взрывы (вспышка и догорающий шар), горящие остовы, пятна прожекторов, — затем bloom и мягкий
тонмаппинг (ванильный диапазон не трогается, сжимается только то, что ярче). Источники передаются движку новым
API мода `render.set-lights` (до 512, плавно интерполируются), режим — `render.set-lighting`.
**Сетка J** после сбития ещё 3,5 с держит камеру на сбитой цели, и комплекс в это время не пропадает из сетки.
**Повреждения.** Взрыв рядом с комплексом повреждает его: он хуже поворачивается, хуже видит и попадает,
+2 -1
View File
@@ -115,7 +115,8 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
// the pixel sees, much stronger at night.
let night = clamp(0.35 - frame.time.z * 1.8, 0.0, 1.0);
let strength = mix(0.35, 2.2, night);
if wall < 1.0e5 {
// Under the HD lighting (params[0].w) the engine lights the world from the same lights.
if wall < 1.0e5 && frame.params[0].w < 0.5 {
let p = dir * wall;
var light = vec3<f32>(0.0);
for (var i = 0; i < blasts; i++) {
+94 -18
View File
@@ -7,7 +7,7 @@ use horizon_mod_sdk::{
Mod, Registration, chat, command, game,
hud::{self, Corner},
input::{self, Event},
render::{self, ModelInstance, ParticleSpawn},
render::{self, Lighting, ModelInstance, ParticleSpawn, PointLight},
storage,
ui::{self, ButtonWidget, ItemWidget, ListWidget, Screen, Widget},
};
@@ -72,6 +72,8 @@ struct AirDef {
far: bool,
/// The unit the move remote picked (its id), waiting for where to go.
moving: Option<u32>,
/// The HD lighting preset (the engine's lights, bloom, tone mapping).
hd: bool,
/// The world whose state is loaded (`game.world-id`), once one is.
world: Option<Option<String>>,
/// The systems' and objects' health as last saved, to save again when it changes.
@@ -276,11 +278,79 @@ impl AirDef {
self.emit(out);
}
/// The HD lighting's lights: flames, glowing warheads, flares, muzzle flashes, tracers,
/// blasts (a flash, then the fireball's glow dying down), burning wrecks and the
/// searchlights' spots.
fn lights(&self, out: &Out) -> Vec<PointLight> {
let mut lights: Vec<PointLight> = Vec::new();
let mut add = |id: u32, at: DVec3, rgb: (f32, f32, f32), intensity: f32, radius: f32| {
if lights.len() < 500 && intensity > 0.05 {
lights.push(PointLight { id, x: at.x, y: at.y, z: at.z, color: rgb, intensity, radius: radius.clamp(1.0, 256.0) });
}
};
let tick = self.sim.tick;
let flicker = |id: u32| 0.85 + 0.15 * (((tick as u32).wrapping_mul(2654435761) ^ id.wrapping_mul(97)) % 100) as f32 / 100.0;
for m in out.models.iter().filter(|m| m.glow) {
let s = m.scale.max(0.1);
match m.name {
"airdef:flame" => add(m.id, m.at, (1.0, 0.55, 0.2), 45.0 * s * flicker(m.id), 32.0 * s.sqrt()),
"airdef:geran3_flame" => add(m.id, m.at, (1.0, 0.6, 0.25), 25.0 * s * flicker(m.id), 22.0),
"airdef:reentry" => add(m.id, m.at, (1.0, 0.6, 0.32), 60.0 * s, 40.0),
"airdef:flare" => add(m.id, m.at, (1.0, 0.93, 0.78), 420.0 * s, 110.0),
"airdef:flash" => add(m.id, m.at, (1.0, 0.8, 0.45), 30.0, 14.0),
"airdef:tracer" => add(m.id, m.at, (1.0, 0.25, 0.1), 2.5, 6.0),
_ => {}
}
}
for b in &self.sim.blasts {
let age = tick.saturating_sub(b.start) as f32;
let size = b.size;
let glow = (-age / (b.life as f32 * 0.15).max(1.0)).exp();
let flash = (-age * 0.35).exp() * 4.0;
let id = 0x6100_0000 ^ (b.start as u32).wrapping_mul(31) ^ (b.at.x as i32 as u32).wrapping_mul(7);
add(id, b.at + DVec3::Y * size as f64 * 0.5, (1.0, 0.58, 0.25), size * size * 18.0 * (glow + flash), size * 14.0);
}
for s in &self.sim.sams {
if !s.alive() && s.burning() {
add(s.id + 15, s.pos + DVec3::Y * 1.5, (1.0, 0.5, 0.18), 80.0 * flicker(s.id), 30.0);
}
// A searchlight's spot where its beam meets the target.
if s.kind == SamKind::Light
&& let Some(t) = s.target.and_then(|t| self.sim.drones.iter().find(|d| d.id == t))
{
add(s.id + 14, t.pos, (0.95, 0.97, 1.0), 220.0, 30.0);
}
}
lights
}
/// The effects preset: none, the post pass, or the post pass under the HD lighting.
fn set_effects(&mut self, shader: bool, hd: bool) {
self.sim.shader = shader;
if shader {
render::set_post(include_str!("../shaders/effects.wgsl"));
} else {
render::clear_post();
}
self.hd = shader && hd;
self.apply_hd();
self.dirty = true;
}
/// Switches the HD lighting.
fn apply_hd(&self) {
render::set_lighting(Lighting { hd: self.hd, exposure: 1.0, bloom: 0.55 });
if !self.hd {
render::set_lights(&[]);
}
}
/// Hands the simulation's output to the game.
/// The post pass's parameters: blasts and glowing parts (shaders/effects.wgsl).
fn shader_params(&self, out: &Out) -> Vec<f32> {
let mut p = vec![0.0f32; 4];
p[0] = self.sim.tick as f32;
p[3] = if self.hd { 1.0 } else { 0.0 };
let blasts: Vec<&sim::Blast> = self.sim.blasts.iter().rev().take(48).collect();
p[1] = blasts.len() as f32;
for b in &blasts {
@@ -300,7 +370,6 @@ impl AirDef {
"airdef:tracer" => (1.0, 0.12, 0.05, 0.9),
"airdef:reentry" => (1.0, 0.6, 0.3, 1.4),
"airdef:flare" => (1.0, 0.94, 0.78, 1.2),
"airdef:beam" => (1.0, 0.97, 0.88, 0.15),
_ => continue,
};
// The shader lights about six times the radius round a light and draws a glow of
@@ -318,6 +387,9 @@ impl AirDef {
if self.sim.shader {
render::set_params(&self.shader_params(&out));
}
if self.hd {
render::set_lights(&self.lights(&out));
}
let models: Vec<ModelInstance> = out
.models
.into_iter()
@@ -423,6 +495,7 @@ impl AirDef {
}
text += &format!("damage {}\n", self.sim.damage);
text += &format!("far {}\n", self.far);
text += &format!("effects {} {}\n", self.sim.shader, self.hd);
for (kind, roles) in &self.sim.roles {
text += &format!("roles {} {}\n", kind.key(), roles);
}
@@ -469,6 +542,11 @@ impl AirDef {
}
Some(&"damage") => self.sim.damage = w.get(1) != Some(&"false"),
Some(&"far") => self.far = w.get(1) != Some(&"false"),
Some(&"effects") => {
let shader = w.get(1) != Some(&"false");
let hd = w.get(2) == Some(&"true");
self.set_effects(shader, hd);
}
Some(&"roles") => {
if let (Some(kind), Some(r)) = (w.get(1).and_then(|k| SamKind::from_key(k)), w.get(2).and_then(|v| v.parse().ok())) {
self.sim.roles.insert(kind, r);
@@ -699,7 +777,8 @@ impl AirDef {
}));
let broken = self.sim.sams.iter().filter(|s| s.health < 1.0).count();
widgets.push(Widget::Button(ButtonWidget { id: BTN_REPAIR, label: format!("Ремонт всех (повреждено: {broken})") }));
widgets.push(Widget::Button(ButtonWidget { id: BTN_SHADER, label: format!("Эффекты: {}", onoff(self.sim.shader)) }));
let fx = if !self.sim.shader { "выкл" } else if self.hd { "HD (свет, bloom)" } else { "обычные" };
widgets.push(Widget::Button(ButtonWidget { id: BTN_SHADER, label: format!("Эффекты: {fx}") }));
widgets.push(Widget::Button(ButtonWidget { id: BTN_DAMAGE, label: format!("Разрушения: {}", onoff(self.sim.damage)) }));
widgets.push(Widget::Button(ButtonWidget { id: BTN_FAR, label: format!("Дальний ландшафт: {}", onoff(self.far)) }));
widgets.push(Widget::Button(ButtonWidget { id: BTN_BACK, label: "← Назад".into() }));
@@ -764,7 +843,7 @@ impl AirDef {
}
}
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | raid | repair | import | ew on|off | flare | move <номер> | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | shader on|off | hold on|off | roles [комплекс классы] | stats";
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | raid | repair | import | ew on|off | flare | move <номер> | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | shader on|off|hd | hold on|off | roles [комплекс классы] | stats";
impl Mod for AirDef {
fn init() -> Result<(Self, Registration), String> {
@@ -782,6 +861,7 @@ impl Mod for AirDef {
replay: None,
far: true,
moving: None,
hd: false,
world: None,
health_mark: 0,
menu: None,
@@ -1032,12 +1112,13 @@ impl Mod for AirDef {
return;
}
(Page::More, BTN_SHADER) => {
self.sim.shader = !self.sim.shader;
if self.sim.shader {
render::set_post(include_str!("../shaders/effects.wgsl"));
} else {
render::clear_post();
}
// Off → effects → HD → off.
let (shader, hd) = match (self.sim.shader, self.hd) {
(false, _) => (true, false),
(true, false) => (true, true),
(true, true) => (false, false),
};
self.set_effects(shader, hd);
}
(Page::More, BTN_FAR) => {
self.far = !self.far;
@@ -1224,14 +1305,9 @@ impl Mod for AirDef {
self.sim.hold = *v == "on";
Some(if self.sim.hold { "ПВО: огонь запрещён".into() } else { "ПВО: огонь разрешён".into() })
}
["shader", v @ ("on" | "off")] => {
self.sim.shader = *v == "on";
if self.sim.shader {
render::set_post(include_str!("../shaders/effects.wgsl"));
} else {
render::clear_post();
}
Some(format!("Шейдерные эффекты: {}", if self.sim.shader { "вкл" } else { "выкл" }))
["shader", v @ ("on" | "off" | "hd")] => {
self.set_effects(*v != "off", *v == "hd");
Some(format!("Эффекты: {}", match *v { "off" => "выкл", "hd" => "HD", _ => "обычные" }))
}
["stats"] => Some(format!(
"В воздухе: {}, ракет: {}, ПВО: {}, сбито: {}, поражений: {}",
@@ -1,7 +1,7 @@
From 3de7a6875140a107e28563cc44489b55f5794517 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 12:37:45 +0300
Subject: [PATCH 01/10] Mod API (AirDef fork): world models, client particles,
Subject: [PATCH 01/12] Mod API (AirDef fork): world models, client particles,
sounds, ground heights
render.set-models draws block models with an interpolated pose per id;
@@ -1,7 +1,7 @@
From a6d64e6415bf618539af464e521091e72b72d72f Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 12:39:45 +0300
Subject: [PATCH 02/10] cargo-horizon-mod: report particles and sounds
Subject: [PATCH 02/12] cargo-horizon-mod: report particles and sounds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
@@ -1,7 +1,7 @@
From ee79b3562feebe51959a01f1585a9e85bd53ccb5 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 13:33:10 +0300
Subject: [PATCH 03/10] Mod API (AirDef fork): full-screen post pass for mods
Subject: [PATCH 03/12] Mod API (AirDef fork): full-screen post pass for mods
render.set-post compiles a mod's WGSL after a prelude (camera matrices,
time, 256 vec4 of params, the world's colour and depth) and runs it after
@@ -1,7 +1,7 @@
From 2b137dbbedd540daeebc7f5e387847aeb56a1355 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 22:09:30 +0300
Subject: [PATCH 04/10] Mod models: draw far models as scaled impostors past
Subject: [PATCH 04/12] Mod models: draw far models as scaled impostors past
the far plane, cull at 4096
---
@@ -1,7 +1,7 @@
From c8ebca6e0af68630e65211844b34d25c5b985c03 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 22:31:00 +0300
Subject: [PATCH 05/10] Mod cameras: render.set-views draws the world from
Subject: [PATCH 05/12] Mod cameras: render.set-views draws the world from
other points into pictures that HUD view shapes show (API level 9)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 8d2261fc4f8072c32e454139cfea7a8c0cd9d664 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 23:08:21 +0300
Subject: [PATCH 06/10] Mods: more fuel per tick and per event (AirDef plans
Subject: [PATCH 06/12] Mods: more fuel per tick and per event (AirDef plans
raids)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 6c548de34def8e4a624d734243418090798bd16a Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 23:25:29 +0300
Subject: [PATCH 07/10] Mod cameras up to 1920x1080 (full-screen replays)
Subject: [PATCH 07/12] Mod cameras up to 1920x1080 (full-screen replays)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
@@ -1,7 +1,7 @@
From 9b9e93269d67401cc620567a884d5ce97f68d76e Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 11:15:11 +0300
Subject: [PATCH 08/10] Mod cameras: distant terrain tiles, fog measured from
Subject: [PATCH 08/12] Mod cameras: distant terrain tiles, fog measured from
the camera, no impostors while one is shown
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 065d9fa02966238a5bd9ff460392a1a809db0b11 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 11:25:45 +0300
Subject: [PATCH 09/10] Mods: game.world-id, so mods keep state per world (API
Subject: [PATCH 09/12] Mods: game.world-id, so mods keep state per world (API
level 9)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 1bf28135fd9a31f837323dc3a8d81ed73343d816 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 12:01:08 +0300
Subject: [PATCH 10/10] cargo-horizon-mod: trial runs behind feature full, so a
Subject: [PATCH 10/12] cargo-horizon-mod: trial runs behind feature full, so a
packing-only tool skips wasmtime
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff