Skip to content

Commit 2bc6c1a

Browse files
committed
blam: Experimental BSP switching
1 parent 10709b2 commit 2bc6c1a

6 files changed

Lines changed: 155 additions & 27 deletions

File tree

examples/blam/cblam-testing/caching.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ BSPItem BSPCache<V>::predict_impl(const blam::bsp::info& bsp)
3232
auto const& section = *section_.value();
3333

3434
BSPItem out;
35-
out.mesh = &section;
36-
out.tag = &(*index.find(bsp.tag));
35+
out.mesh = &section;
36+
out.tag = &(*index.find(bsp.tag));
37+
out.section_idx = next_section_idx++;
3738

3839
if(!out.tag->valid())
3940
return {};

examples/blam/cblam-testing/caching.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ struct BSPCache
195195
vert_ptr = 0, element_ptr = 0, light_ptr = 0;
196196
evict_all();
197197

198+
bsp_switches.clear();
199+
active_section = 0;
200+
next_section_idx = 0;
198201
sky_palette.clear();
199202
if(auto scen_opt = map.tags->scenario(map.map, map.magic))
200203
{
@@ -224,6 +227,19 @@ struct BSPCache
224227
blam::map_ptr magic;
225228
std::vector<blam::scn::skybox const*> sky_palette;
226229

230+
/* Structure BSP switching (scenario bsp_switch_triggers): crossing
231+
* `volume` while `source` is the active section activates `destination`.
232+
* Only the active section is culled/rendered. */
233+
struct bsp_switch_t
234+
{
235+
blam::scn::trigger_volume const* volume;
236+
libc_types::i16 source;
237+
libc_types::i16 destination;
238+
};
239+
std::vector<bsp_switch_t> bsp_switches;
240+
libc_types::i16 active_section{0};
241+
libc_types::i16 next_section_idx{0}; /* predict_impl ordering */
242+
227243
Span<byte_t> vert_buffer;
228244
Span<byte_t> light_buffer;
229245
Span<blam::vert::face> element_buffer;

examples/blam/cblam-testing/caching_item.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,11 @@ struct BSPItem
218218

219219
static_assert(sizeof(FlatSubcluster) == 32);
220220

221-
blam::bsp::header const* mesh{nullptr};
222-
blam::tag_t const* tag{nullptr};
221+
blam::bsp::header const* mesh{nullptr};
222+
blam::tag_t const* tag{nullptr};
223+
/* Index into the scenario's structure BSP list (bsp_info order); used to
224+
* match against bsp_switch_trigger source/destination. */
225+
libc_types::i16 section_idx{-1};
223226
std::vector<Group> groups;
224227
std::vector<Cluster> clusters;
225228
std::vector<FlatSubcluster> sorted_subclusters;

examples/blam/cblam-testing/loading.h

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ void load_scenario_bsp(
4646
auto trigger_vols = scenario->trigger_volumes.data(magic).value();
4747
for(blam::scn::trigger_volume const& trigger : trigger_vols)
4848
{
49-
auto [origin, second] = trigger.box.points();
49+
Vecf3 origin = trigger.position;
50+
Vecf3 second = trigger.position + trigger.extents;
5051

5152
auto trig = e.create_entity(trigger_obj);
5253
TriggerVolume& volume = trig.get<TriggerVolume>();
@@ -199,6 +200,29 @@ void load_scenario_bsp(
199200
Vecf3{0.5f, 1.f, 0});
200201
}
201202

203+
/* Structure BSP switching: collect the scenario's switch triggers so the
204+
* occluder can track the active section, and start in the section the
205+
* first player spawn belongs to. */
206+
if(auto switches = scenario->bsp_switch_triggers.data(magic);
207+
switches.has_value())
208+
{
209+
for(blam::scn::bsp_trigger const& sw : switches.value())
210+
{
211+
if(sw.trigger_volume < 0 ||
212+
static_cast<size_t>(sw.trigger_volume) >= trigger_vols.size())
213+
continue;
214+
bsp_cache.bsp_switches.push_back({
215+
.volume = &trigger_vols[sw.trigger_volume],
216+
.source = sw.source,
217+
.destination = sw.destination,
218+
});
219+
cDebug(
220+
"BSP switch: {} → {} via volume '{}'",
221+
sw.source,
222+
sw.destination,
223+
trigger_vols[sw.trigger_volume].name.str());
224+
}
225+
}
202226
std::vector<generation_idx_t> bsp_meshes;
203227
if(auto bsps = scenario->bsp_info.data(magic); bsps.has_value())
204228
{
@@ -210,6 +234,26 @@ void load_scenario_bsp(
210234
}
211235
}
212236

237+
/* Initial active section: trust the first spawn's bsp_index unless its
238+
* position resolves into a different section's BSP tree (b40's first
239+
* spawn claims section 0 but sits in section 3). */
240+
{
241+
auto locations = scenario->player_start.locations.data(magic);
242+
if(locations.has_value() && !locations.value().empty())
243+
{
244+
auto const& loc = locations.value()[0];
245+
bsp_cache.active_section = static_cast<libc_types::i16>(
246+
loc.bsp_index);
247+
for(auto& [id, item] : bsp_cache.m_cache)
248+
if(item.find_cluster_tree(loc.pos).has_value())
249+
{
250+
bsp_cache.active_section = item.section_idx;
251+
break;
252+
}
253+
}
254+
cDebug("Initial BSP section: {}", bsp_cache.active_section);
255+
}
256+
213257
gpu.bsp_buf->unmap();
214258
gpu.bsp_index->unmap();
215259
gpu.bsp_light_buf->unmap();

examples/blam/cblam-testing/occluder.cpp

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -104,23 +104,52 @@ struct Occluder : compo::RestrictedSubsystem<Occluder<V>, OccluderManifest<V>>
104104
u32 current_cluster{0};
105105
generation_idx_t current_bsp_id{};
106106

107-
for(auto& ent : p.select(ObjectBsp))
107+
/* Structure BSP switching: the active section changes only when the
108+
* camera crosses a bsp-switch trigger volume whose source is the
109+
* active section — flying into another section's space without
110+
* crossing one keeps that section hidden, exactly like the original
111+
* engine. Everything below (cluster lookup, PVS, culling) is
112+
* restricted to the active section, so inactive sections cost
113+
* nothing per frame. */
114+
for(auto const& sw : bsp_cache->bsp_switches)
108115
{
109-
auto ref = p.template ref<Proxy>(ent);
110-
BspReference& bsp_ref = ref.template get<BspReference>();
116+
if(sw.source != bsp_cache->active_section)
117+
continue;
118+
if(!sw.volume->contains(camera_pos))
119+
continue;
120+
cDebug(
121+
"BSP switch: section {} → {} ('{}')",
122+
sw.source,
123+
sw.destination,
124+
sw.volume->name.str());
125+
bsp_cache->active_section = sw.destination;
126+
break;
127+
}
128+
129+
BSPItem const* active_bsp{nullptr};
130+
generation_idx_t active_bsp_id{};
131+
for(auto& [id, item] : bsp_cache->m_cache)
132+
if(item.valid() && item.section_idx == bsp_cache->active_section)
133+
{
134+
active_bsp = &item;
135+
active_bsp_id = {id, bsp_cache->generation};
136+
break;
137+
}
111138

112-
BSPItem const& bsp = bsp_cache->find(bsp_ref.bsp)->second;
113-
for(auto const& cluster : bsp.clusters)
139+
if(active_bsp)
140+
{
141+
for(auto const& cluster : active_bsp->clusters)
114142
for(auto const& sub : cluster.sub)
115143
portal_colors[sub.debug_color_idx] = Vecf3(1, 0, 0);
116144

117-
if(auto cluster = bsp.find_cluster(camera_pos); cluster.has_value())
145+
if(auto cluster = active_bsp->find_cluster(camera_pos);
146+
cluster.has_value())
118147
{
119148
auto [cluster_, sub_] = cluster.value();
120-
current_bsp = &bsp;
149+
current_bsp = active_bsp;
121150
current_cluster = cluster_;
122-
current_bsp_id = bsp_ref.bsp;
123-
auto const& subs = bsp.clusters.at(cluster_).sub;
151+
current_bsp_id = active_bsp_id;
152+
auto const& subs = active_bsp->clusters.at(cluster_).sub;
124153
if(sub_ < subs.size())
125154
portal_colors[subs.at(sub_).debug_color_idx] =
126155
Vecf3(0, 1, 0);
@@ -132,24 +161,30 @@ struct Occluder : compo::RestrictedSubsystem<Occluder<V>, OccluderManifest<V>>
132161
/* When the camera enters a new cluster, recompute the portal-traversal
133162
* visible set. When between clusters, keep the last valid set so
134163
* culling doesn't snap to all-visible at cluster boundaries. */
135-
bool cluster_changed = (current_bsp != nullptr) != last_found ||
164+
bool section_changed = active_bsp != pvs_bsp;
165+
bool cluster_changed = section_changed ||
166+
(current_bsp != nullptr) != last_found ||
136167
current_cluster != last_cluster;
137168
last_found = current_bsp != nullptr;
138169
last_cluster = current_cluster;
139170

171+
/* The active section is culled even while the camera is outside its
172+
* clusters; other sections stay hidden wholesale. */
173+
pvs_bsp = active_bsp;
174+
pvs_bsp_id = active_bsp_id;
175+
if(section_changed)
176+
pvs_visible.clear(); /* stale per-cluster bits of old section */
177+
140178
if(current_bsp)
141179
{
142-
pvs_bsp = current_bsp;
143180
pvs_cluster = current_cluster;
144-
pvs_bsp_id = current_bsp_id;
145181
pvs_visible = pvs_bsp->portal_visible_set(
146182
pvs_cluster, camera_pos, camera_mvp);
147-
} else if(pvs_bsp)
148-
{
149-
/* Camera outside all clusters (noclip outside the map shell):
150-
* keep the last valid visible set. Snapping to all-visible here
151-
* is what used to flash far-off geometry into view. */
152183
}
184+
/* else: camera outside the active section's clusters (noclip through
185+
* rock) — keep the last valid set; empty set = all-visible within the
186+
* active section. Snapping to all-visible across sections is what
187+
* used to flash far-off geometry into view. */
153188

154189
rendering->current_bsp_cluster = pvs_cluster;
155190

src/coffee/blam/include/blam/volta/blam_scenario.h

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -458,11 +458,22 @@ struct device_group
458458
u32 unk[3];
459459
};
460460

461+
/* "Structure BSP switch trigger volume": while `source` is the active
462+
* structure BSP and the player enters `trigger_volume`, the engine makes
463+
* `destination` the active BSP. Decoded from b40.map, where the referenced
464+
* trigger volumes are named 'bsp <source>,<destination>' and entries come
465+
* in bidirectional pairs. */
461466
struct bsp_trigger
462467
{
463-
u32 unk[2];
468+
i16 trigger_volume; /* index into scenario trigger_volumes */
469+
i16 source; /* index into scenario structure BSPs (bsp_info) */
470+
i16 destination; /* index into scenario structure BSPs (bsp_info) */
471+
i16 unknown; /* structured (paired like the volumes, -1 for
472+
script-only transitions) but undeciphered */
464473
};
465474

475+
static_assert(sizeof(bsp_trigger) == 8);
476+
466477
struct move_positions
467478
{
468479
bl_string unk1[32];
@@ -479,10 +490,28 @@ struct object_name
479490

480491
struct trigger_volume
481492
{
482-
u32 unk;
483-
bl_string name;
484-
f32 unk2[9];
485-
bounding_box box;
493+
u32 unk;
494+
bl_string name;
495+
f32 unk2[9]; /* likely 3×Vecf3 orientation axes */
496+
Vecf3 position;
497+
Vecf3 extents; /* volume spans position .. position + extents
498+
(verified against b40 'null' and door volumes) */
499+
500+
inline bool contains(Vecf3 const& point) const
501+
{
502+
auto in = [](f32 v, f32 a, f32 b) {
503+
if(a > b)
504+
{
505+
f32 t = a;
506+
a = b;
507+
b = t;
508+
}
509+
return v >= a && v <= b;
510+
};
511+
Vecf3 hi = position + extents;
512+
return in(point.x, position.x, hi.x) &&
513+
in(point.y, position.y, hi.y) && in(point.z, position.z, hi.z);
514+
}
486515
};
487516

488517
enum class actor_flags_t : u32

0 commit comments

Comments
 (0)