Let the free camera go anywhere, and make ground avoidance a switch that is off unless asked for.

The camera kept its height (only ZR and ZL changed it) and rose whenever the closest surface in the lower part of the view came within about three metres, so flying up to a character, a rock or a wall sent it upwards. It now moves where it looks and nothing stops it, the ground included.

Ground avoidance is a checkbox in the debug menu's free camera section. With it on, the nine view distances read back from the lower view become points of the scene (from the view's angles and the camera's pitch), and only a column whose lowest surface rises by less than about 30 degrees counts as ground: the camera goes no lower than a metre above it. Steeper things, objects and walls, are left alone. The read-back runs only while the switch is on.

Tested on a village save, 4 m/s forward for 5 s and then ZL for 4 s: with avoidance off the camera ended 17.4 m lower, through the floor; with it on it ended 0.7 m lower and held there. Flown at Link with the first version of the ground test, its height did not change.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
epicmeme546
2026-11-12 12:07:56 -05:00
parent 81e7da91aa
commit 7581bb887d
5 changed files with 98 additions and 43 deletions

View File

@@ -20,6 +20,14 @@
to 18 degrees a frame) was taken for a cut to a new view. Only a jump of the camera or a turn of 30 degrees or more
in one frame counts as a cut now.
## Changed
- **Debug menu: the free camera has no limits.** It flies where it looks and goes through walls, objects and the
ground; it used to keep its height and climb whenever anything in the lower part of the view came within three
metres, a character or a rock it was flown towards included. *Ground avoidance* is now a switch in the free camera's
section, off unless you turn it on: with it the camera stays a metre above fields and gentle hills, and objects and
walls never push it.
## Known issues
- At FSR's and DLSS's smaller modes, Link and other characters look a little coarser while they move: the upscaler

View File

@@ -765,28 +765,64 @@ int dbg_search_next(int mode, uint32_t value) {
static struct {
volatile int on, capture, sticks; // the menu's: on; take the game's view at the next update; the sticks fly it
volatile int avoid; // the menu's: ground avoidance (dbg_pad), off unless asked for
volatile float speed; // metres a second
double pos[3], reach; // the game thread's: where it is, and the game's distance to its target
float yaw, pitch;
uint64_t last_ns, updates;
volatile uint32_t ground_bits; // dbg_freecam_ground: the closest distance seen in the lower view (a float's bits)
float ground[9], ground_tan, ground_aspect; // dbg_freecam_ground's, under ground_lock
volatile uint64_t ground_ns; // and when; 0 none
} fc = {.sticks = 1, .speed = 8.0f, .reach = 5.0};
static PlatMutex ground_lock = PLAT_MUTEX_INIT;
int dbg_freecam_flying(void) { return fc.on && fc.sticks && !fc.capture; }
int dbg_freecam_flying(void) { return fc.on && fc.sticks && !fc.capture && fc.avoid; }
void dbg_freecam_avoid_set(int on) {
if ((on != 0) != fc.avoid) rt_log("debug: free camera ground avoidance %s", on ? "on" : "off");
fc.avoid = on != 0;
}
int dbg_freecam_avoid(void) { return fc.avoid; }
// The replay's thread (vk_dlaa.c fcam_probe): view distances read back from the lower part of the view a few frames
// ago, 0 where it saw the sky. The closest is kept for dbg_pad.
void dbg_freecam_ground(const float *dist, int n) {
float best = 0;
for (int i = 0; i < n; i++)
if (dist[i] > 0 && isfinite(dist[i]) && (best == 0 || dist[i] < best)) best = dist[i];
uint32_t bits;
memcpy(&bits, &best, 4);
__atomic_store_n(&fc.ground_bits, bits, __ATOMIC_RELAXED);
// ago, three rows of three (0.6, 0.78 and 0.96 of the way down; 0.2, 0.5 and 0.8 across), 0 where it saw the sky. Kept
// for dbg_pad (ground_under).
void dbg_freecam_ground(const float *dist, int n, float tan_half_y, float aspect) {
if (n != 9 || !(tan_half_y > 0) || !(aspect > 0)) return;
plat_mutex_lock(&ground_lock);
for (int i = 0; i < 9; i++) fc.ground[i] = dist[i] > 0 && isfinite(dist[i]) ? dist[i] : 0;
fc.ground_tan = tan_half_y, fc.ground_aspect = aspect;
plat_mutex_unlock(&ground_lock);
__atomic_store_n(&fc.ground_ns, plat_now_ns(), __ATOMIC_RELEASE);
}
// How far below the camera the ground is, in metres, from the view distances above, or a negative number when they show
// no ground within reach metres ahead. Each sample is a point of the scene: its height against the camera and its
// distance ahead follow from the view's angles and the camera's pitch. In each of the three columns the lowest sample
// counts as ground only when the surface from it to the one above it rises by less than GROUND_SLOPE: the ground the
// camera flies over, a field or a gentle hill. Whatever stands on it or in front of the camera (a character, a rock, a
// wall, a tree) is steeper and is left alone, so the camera can go right up to it; taken for ground too, as at first,
// anything within three metres in the lower view sent the camera upwards on its way to it.
#define GROUND_SLOPE 0.6 /* rise over run: about 30 degrees */
static double ground_under(double reach) {
float d[9], ty, as;
plat_mutex_lock(&ground_lock);
memcpy(d, fc.ground, sizeof d), ty = fc.ground_tan, as = fc.ground_aspect;
plat_mutex_unlock(&ground_lock);
static const double xn[3] = {-0.6, 0.0, 0.6}, yn[3] = {-0.2, -0.56, -0.92}; // the samples in the view, -1..1, up positive
double sp = sin(fc.pitch), cp = cos(fc.pitch), best = -1;
for (int col = 0; col < 3; col++) {
double up[3], run[3]; // per row: the point's height against the camera, and how far ahead it is, level
for (int row = 0; row < 3; row++) {
double z = d[row * 3 + col], yv = yn[row] * ty * z, xv = xn[col] * ty * as * z, fwd = z * cp - yv * sp;
up[row] = yv * cp + z * sp, run[row] = sqrt(fwd * fwd + xv * xv);
}
if (!(d[6 + col] > 0) || !(d[3 + col] > 0)) continue; // the lowest sample and the one above it
double rise = fabs(up[1] - up[2]), along = fabs(run[1] - run[2]);
if (rise > GROUND_SLOPE * along || up[2] >= 0 || run[2] > reach) continue;
if (best < 0 || -up[2] < best) best = -up[2];
}
return best;
}
static double reg_d(Cpu *c, int r) {
uint64_t b = (uint64_t)c->v[r];
double v;
@@ -946,10 +982,10 @@ static void route_pad(int32_t *lx, int32_t *ly) {
}
// The game's thread, with the pad's state before the game gets it: while the free camera has the sticks, the left one
// moves it level, ahead where it faces and sideways, the right one turns it, ZR and ZL raise and lower it, R makes it
// four times as fast and L four times as slow, and the game is told that none of those is touched, so Link stays where
// he is. Its height changes only with ZR and ZL: moved along its view, a camera looking down at the ground flew into
// it and showed the void below the map, which looks like ground the game failed to draw.
// moves it where it looks and sideways, the right one turns it, ZR and ZL raise and lower it, R makes it four times as
// fast and L four times as slow, and the game is told that none of those is touched, so Link stays where he is.
// Nothing stops it: it goes through walls, objects and the ground, below which the picture shows the void under the
// map. Ground avoidance (the menu's switch, off unless asked for) keeps it a metre above ground-like surfaces instead.
void dbg_pad(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
route_pad(lx, ly);
if (!fc.on || !fc.sticks || fc.capture) {
@@ -968,24 +1004,25 @@ void dbg_pad(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *
fc.yaw -= (float)(in[2] * 2.4 * dt);
fc.pitch += (float)(in[3] * 1.8 * dt);
fc.pitch = fc.pitch > 1.5f ? 1.5f : fc.pitch < -1.5f ? -1.5f : fc.pitch;
double ahead[3] = {sin(fc.yaw), 0, cos(fc.yaw)}; // where it faces, level
double cp = cos(fc.pitch), look[3] = {cp * sin(fc.yaw), sin(fc.pitch), cp * cos(fc.yaw)}; // where it looks
double right[3] = {-cos(fc.yaw), 0, sin(fc.yaw)}; // the view's direction crossed with up
for (int i = 0; i < 3; i++) fc.pos[i] += (ahead[i] * in[1] + right[i] * in[0]) * step;
// Clear of the ground: where the closest surface in the lower part of the view (vk_dlaa.c fcam_probe, with DLSS,
// DLAA or FSR 3.1/4 on) is nearer than the clearance, the camera rises instead of going on into a hill and under
// the map, and ZL does not take it lower. The clearance grows with the speed: the distances are a few frames old.
double move[3];
for (int i = 0; i < 3; i++) move[i] = (look[i] * in[1] + right[i] * in[0]) * step;
move[1] += rise * step;
// Ground avoidance, when it is on: where the ground under the view (ground_under, from vk_dlaa.c fcam_probe with
// DLSS, DLAA or FSR on) is less than a metre below, the camera goes no lower and rises to that metre, instead of
// going on into a field or a gentle hill and under the map. Objects and walls do not count. The reach ahead grows
// with the speed: the distances are a few frames old.
uint64_t seen = __atomic_load_n(&fc.ground_ns, __ATOMIC_ACQUIRE);
if (seen && now - seen < 300000000ull && dt > 0) {
uint32_t bits = __atomic_load_n(&fc.ground_bits, __ATOMIC_RELAXED);
float near_m;
memcpy(&near_m, &bits, 4);
double clear = 2.5 + 0.15 * step / dt;
if (near_m > 0 && near_m < clear) {
if (rise < 0) rise = 0;
fc.pos[1] += (clear - near_m) * (dt * 8 < 1 ? dt * 8 : 1);
if (fc.avoid && seen && now - seen < 300000000ull && dt > 0) {
const double clear = 1.0;
double below = ground_under(3.0 + 0.3 * step / dt);
if (below >= 0 && below < clear) {
if (move[1] < 0) move[1] = 0;
move[1] += (clear - below) * (dt * 8 < 1 ? dt * 8 : 1);
}
}
fc.pos[1] += rise * step;
for (int i = 0; i < 3; i++) fc.pos[i] += move[i];
*lx = *ly = *rx = *ry = 0;
*buttons &= ~(zl | zr | l | r);
}
@@ -993,7 +1030,8 @@ void dbg_pad(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *
// ---- NX2NAVI_DBG_TEST="<tick>:flag <hash hex> <0|1>;<tick>:speed <factor>;<tick>:find <value> <size>;
// <tick>:next <mode> <value>;<tick>:setfirst <value>;<tick>:holdfirst <value>;<tick>:show [<hash hex>];
// <tick>:warp <area> [<start position>];<tick>:event <name>;<tick>:text <game data text's name>;
// <tick>:freecam <0|1> <sticks 0|1> <speed>;<tick>:camgo <dx> <dy> <dz> [<yaw> <pitch>];<tick>:camreset;<tick>:campos":
// <tick>:freecam <0|1> <sticks 0|1> <speed>;<tick>:camavoid <0|1>;<tick>:camgo <dx> <dy> <dz> [<yaw> <pitch>];
// <tick>:camreset;<tick>:campos":
// the menu's requests without the menu, for test runs. Ticks count the game's pad reads from the player's first appearance.
static void test_script(uint64_t tick) {
static char buf[2048];
@@ -1020,6 +1058,8 @@ static void test_script(uint64_t tick) {
rt_log("debug: tick %llu: free camera %s at %.3f %.3f %.3f, yaw %.3f, pitch %.3f; %llu camera updates through it",
(unsigned long long)tick, fc.on ? "on" : "off", fc.pos[0], fc.pos[1], fc.pos[2], fc.yaw, fc.pitch,
(unsigned long long)fc.updates);
} else if (!strncmp(colon + 1, "camavoid ", 9)) { // camavoid <0|1>: the free camera's ground avoidance
dbg_freecam_avoid_set(atoi(colon + 10));
} else if (!strncmp(colon + 1, "freecam ", 8)) { // freecam <on> <sticks> <speed>
int cam_on = 0, sticks = 1;
float speed = 8;

View File

@@ -48,8 +48,12 @@ int dbg_freecam_available(void); // the build has the camera's patch set
void dbg_freecam_set(int on, int sticks, float speed);
void dbg_freecam_reset(void); // back to the game's view, free again from there
int dbg_freecam(int *sticks, float *speed, double pos[3]); // on or off
int dbg_freecam_flying(void); // on, with the sticks: vk_dlaa.c reads the ground's distance back for it
void dbg_freecam_ground(const float *dist, int n); // the replay's thread: view distances in the lower view, 0 sky
int dbg_freecam_flying(void); // on, with the sticks and ground avoidance: vk_dlaa.c reads the ground's distances back
void dbg_freecam_avoid_set(int on); // ground avoidance (off by default): the camera stays a metre above ground-like surfaces
int dbg_freecam_avoid(void);
// The replay's thread: nine view distances of the lower view (three rows of three, the top row first; 0 sky), with the
// tangent of half the view's vertical angle and its width over its height
void dbg_freecam_ground(const float *dist, int n, float tan_half_y, float aspect);
// Memory: values of 1, 2 or 4 bytes in the game's heap.
int dbg_search_new(uint32_t value, int size); // returns the number of places

View File

@@ -304,17 +304,19 @@ void tab_world(void) {
if (ImGui::RadioButton("The camera stays where it is, the pad plays the game", sticks == 0)) sticks = 0, changed = true;
ImGui::SetNextItemWidth(-1);
if (ImGui::SliderFloat("##camspeed", &cam_speed, 1.0f, 100.0f, "%.0f metres a second", ImGuiSliderFlags_Logarithmic)) changed = true;
bool avoid = dbg_freecam_avoid() != 0;
if (ImGui::Checkbox("Ground avoidance: stay a metre above the ground", &avoid)) dbg_freecam_avoid_set(avoid);
if (ImGui::Button("Back to the game's view")) dbg_freecam_reset();
ImGui::SameLine();
ImGui::TextDisabled("x %.1f y %.1f z %.1f", at[0], at[1], at[2]);
}
if (changed) dbg_freecam_set(on, sticks, cam_speed);
ImGui::TextWrapped("Close this menu to fly: the left stick moves level, ahead and sideways, the right stick turns, ZR and ZL "
"go up and down, R is four times as fast and L four times as slow. The height changes only with ZR "
"and ZL, and with DLSS, DLAA or FSR 3.1 or 4 on the camera rises where the ground ahead or below comes "
"within a few metres. The game goes on meanwhile and draws what the camera sees; the game speed above "
"slows it down. Without those upscalers it does not stop at hills: below the ground the picture shows "
"the void under the map, not a missing floor.");
ImGui::TextWrapped("Close this menu to fly: the left stick moves where the camera looks and sideways, the right stick "
"turns, ZR and ZL go up and down, R is four times as fast and L four times as slow. Nothing stops the "
"camera: it goes through walls, objects and the ground, and below the ground the picture shows the "
"void under the map, not a missing floor. With ground avoidance on (it needs DLSS, DLAA or FSR 3.1 or "
"4) it stays a metre above fields and gentle hills; objects and walls never push it. The game goes on "
"meanwhile and draws what the camera sees; the game speed above slows it down.");
}
// ---- memory

View File

@@ -1475,12 +1475,12 @@ static void hl_destroy(void) {
// The device is idle here. Release NGX features and parameters before their Vulkan resources, then shut down NGX even
// if initialization succeeded but the layout check failed. The Linux core's Shutdown1 takes an extra output pointer
// compared with the SDK's public wrapper; it reports the number of devices still using NGX.
// ---- the free camera's view of the ground (debug_tools.c dbg_pad). While it flies, nine of the view distances
// mv_pass wrote (a grid over the lower part of the view, where the ground is) go back to the host, and the camera rises
// where the closest is nearer than its clearance: flown through a hill, it showed the void below the map. A slot is
// read FC_SLOTS evaluations after its copy, when the GPU has long been past it.
// ---- the free camera's view of the ground (debug_tools.c dbg_pad, ground avoidance). While it flies with that on,
// nine of the view distances mv_pass wrote (a grid over the lower part of the view, where the ground is) go back to
// the host, which works out how far below the camera the ground is (ground_under). A slot is read FC_SLOTS evaluations
// after its copy, when the GPU has long been past it.
int dbg_freecam_flying(void); // debug_tools.c
void dbg_freecam_ground(const float *dist, int n);
void dbg_freecam_ground(const float *dist, int n, float tan_half_y, float aspect);
#define FC_N 9
#define FC_SLOTS 4
static VkBuffer fc_buf;
@@ -1510,7 +1510,8 @@ static void fcam_probe(VkCommandBuffer cb, const struct Size *e, uint32_t w, uin
uint32_t s = fc_next++ % FC_SLOTS;
if (fc_have[s]) {
vmaInvalidateAllocation(c->vma, fc_mem, s * FC_N * sizeof(float), FC_N * sizeof(float));
dbg_freecam_ground(fc_map + s * FC_N, FC_N);
dbg_freecam_ground(fc_map + s * FC_N, FC_N, D.fg_cam_ok ? tanf(0.5f * D.fg_cam.fov_y) : 0.0f,
D.fg_cam_ok ? D.fg_cam.aspect : 0.0f);
}
// the replay's viewports have negative heights: row 0 is the top of the view
static const float fx[3] = {0.2f, 0.5f, 0.8f}, fy[3] = {0.6f, 0.78f, 0.96f};