//########################################################################### // // btvisgnd.cpp // // PORT ADDITION (presentation layer -- NOT a reconstruction): visual-ground // conform for the external chase view. // // WHY: the mech SIM rides the coarse 1995 collision solids (cones/blocks; // the authentic snap puts origin.y = solid surfaceY exactly -- see // Mech::AuthenticGroundAndCollide). The VISUAL terrain mesh runs 0..~2.1u // ABOVE those solids on mound/mesa slopes (LOD0-exact measurement, dbase), // so an upright mech's feet clip into the visible sand there. In 1995 this // was INVISIBLE: the game rendered cockpit-only (you can never see your own // feet; other mechs are 100+m away). Our port adds a close external camera, // so the port also adds this presentation fix: sample the RENDERED terrain // triangles under the mech and lift the render matrix (localToWorld ONLY -- // never localOrigin) by the visual-minus-solid gap. Collision, aim, damage, // network dead-reckoning all stay on the authentic solid model. // // Data source: the map's MakeMessage stream in BTL4.RES (class-42 static // terrain instances) + each instance's LOD0 geometry via the engine BGF // loader (bgfload.h). No tuning constants -- the exact content geometry. // // Gate: BT_VISUAL_GROUND (default ON; =0 restores raw solid-height render). // Diagnostics under BT_GROUND_LOG. // //########################################################################### #include #include // application / GetCurrentMission / GetResourceFile #include // Mission::GetMapID #include #include #include #include #include #include #include //---------------------------------------------------------------------------// // Geometry cache //---------------------------------------------------------------------------// namespace { struct VgTri { float ax, ay, az, bx, by, bz, cx, cy, cz; }; struct VgMesh { std::vector tris; float minx, maxx, minz, maxz; // model-local XZ bound }; struct VgInst { int mesh; // index into gMeshes float px, py, pz; // world placement float minx, maxx, minz, maxz; // world XZ bound (placed) }; std::vector gMeshes; std::vector gInsts; int gState = 0; // 0 = not tried, 1 = ready, -1 = unavailable bool SkippedName(const char *n) { // backdrop / sky pieces -- vertical scenery, never ground underfoot if (strstr(n, "sky") || strstr(n, "SKY")) return true; if (_strnicmp(n, "profile", 7) == 0) return true; if (_strnicmp(n, "rwin", 4) == 0) return true; return false; } int LoadMeshFor(const char *resource_name) { static std::map cache; // name -> gMeshes idx (-1 = no geometry) std::string key(resource_name); std::map::iterator it = cache.find(key); if (it != cache.end()) return it->second; int idx = -1; BgfData data; std::string file = key + ".bgf"; if (LoadBgfFile(file, data) && data.ok && !data.indices.empty()) { VgMesh m; m.minx = m.minz = 1e9f; m.maxx = m.maxz = -1e9f; // The loader emits DOUBLE-SIDED triangles: emitTri pushes (a,b,c) then // (a,c,b) -- 6 indices per source triangle. Take the forward one. for (size_t i = 0; i + 5 < data.indices.size(); i += 6) { const BgfVtx &A = data.verts[data.indices[i]]; const BgfVtx &B = data.verts[data.indices[i + 1]]; const BgfVtx &C = data.verts[data.indices[i + 2]]; VgTri t = { A.x, A.y, A.z, B.x, B.y, B.z, C.x, C.y, C.z }; m.tris.push_back(t); float xs[3] = { A.x, B.x, C.x }, zs[3] = { A.z, B.z, C.z }; for (int k = 0; k < 3; ++k) { if (xs[k] < m.minx) m.minx = xs[k]; if (xs[k] > m.maxx) m.maxx = xs[k]; if (zs[k] < m.minz) m.minz = zs[k]; if (zs[k] > m.maxz) m.maxz = zs[k]; } } if (!m.tris.empty()) { gMeshes.push_back(m); idx = (int)gMeshes.size() - 1; } } cache[key] = idx; return idx; } // Build the instance table from the CURRENT mission's map message stream. // Map stream record layout (BTL4.RES, byte-verified): // [0] u32 count // records: u32 mlen | i32 mid | u32 mflags | ... i32 class @+28 | // i32 resourceID @+40 | float pos[3] @+48 ; advance (mlen+3)&~3 // mflags bit1 = include-marker (skip); class 42 = static terrain/cultural. bool BuildTables() { if (application == 0) return false; Mission *mission = application->GetCurrentMission(); ResourceFile *rf = application->GetResourceFile(); if (mission == 0 || rf == 0) return false; ResourceDescription *map = rf->FindResourceDescription(mission->GetMapID()); if (map == 0) return false; map->Lock(); const unsigned char *base = (const unsigned char *)map->resourceAddress; size_t size = map->resourceSize; if (base == 0 || size < 4) return false; int count = *(const int *)base; size_t p = 4; int made = 0, skipped = 0; for (int i = 0; i < count && p + 52 <= size; ++i) { unsigned mlen = *(const unsigned *)(base + p); unsigned mflags = *(const unsigned *)(base + p + 8); int cls = *(const int *)(base + p + 28); int rid = *(const int *)(base + p + 40); const float *pos = (const float *)(base + p + 48); p += (mlen + 3) & ~3u; if (mflags & 0x2) continue; // include marker if (cls != 42) continue; // terrain/static cultural only ResourceDescription *rd = rf->FindResourceDescription(rid); if (rd == 0) continue; if (SkippedName(rd->resourceName)) { ++skipped; continue; } int mi = LoadMeshFor(rd->resourceName); if (mi < 0) { ++skipped; continue; } VgInst inst; inst.mesh = mi; inst.px = pos[0]; inst.py = pos[1]; inst.pz = pos[2]; inst.minx = gMeshes[mi].minx + pos[0]; inst.maxx = gMeshes[mi].maxx + pos[0]; inst.minz = gMeshes[mi].minz + pos[2]; inst.maxz = gMeshes[mi].maxz + pos[2]; gInsts.push_back(inst); ++made; } if (getenv("BT_GROUND_LOG")) DEBUG_STREAM << "[visgnd] sampler ready: " << made << " terrain instances (" << gMeshes.size() << " meshes, " << skipped << " skipped)\n" << std::flush; return made > 0; } // Highest visual surface at world (x,z) whose height lies in [yLo, yHi]. // The band keeps the query LOCAL: standing at a butte base it picks the skirt // underfoot, not the mesa top 40u above. bool SampleBand(float x, float z, float yLo, float yHi, float *out) { bool found = false; float best = 0.0f; for (size_t ii = 0; ii < gInsts.size(); ++ii) { const VgInst &inst = gInsts[ii]; if (x < inst.minx || x > inst.maxx || z < inst.minz || z > inst.maxz) continue; const float lx = x - inst.px, lz = z - inst.pz; const VgMesh &m = gMeshes[inst.mesh]; for (size_t ti = 0; ti < m.tris.size(); ++ti) { const VgTri &t = m.tris[ti]; // point-in-triangle (XZ), sign-consistent const float d1 = (t.bx - t.ax) * (lz - t.az) - (t.bz - t.az) * (lx - t.ax); const float d2 = (t.cx - t.bx) * (lz - t.bz) - (t.cz - t.bz) * (lx - t.bx); const float d3 = (t.ax - t.cx) * (lz - t.cz) - (t.az - t.cz) * (lx - t.cx); if (!((d1 >= 0 && d2 >= 0 && d3 >= 0) || (d1 <= 0 && d2 <= 0 && d3 <= 0))) continue; const float area = (t.bx - t.ax) * (t.cz - t.az) - (t.bz - t.az) * (t.cx - t.ax); if (area > -1e-6f && area < 1e-6f) continue; const float w1 = ((t.bx - lx) * (t.cz - lz) - (t.bz - lz) * (t.cx - lx)) / area; const float w2 = ((t.cx - lx) * (t.az - lz) - (t.cz - lz) * (t.ax - lx)) / area; const float w3 = 1.0f - w1 - w2; const float y = w1 * t.ay + w2 * t.by + w3 * t.cy + inst.py; if (y < yLo || y > yHi) continue; if (!found || y > best) { found = true; best = y; } } } if (found) *out = best; return found; } } // namespace //---------------------------------------------------------------------------// // BTVisualGroundLift -- render-only Y delta so the mech's feet meet the // VISIBLE ground. Returns 0 when gated off, unavailable, or no local surface. //---------------------------------------------------------------------------// float BTVisualGroundLift(float x, float y, float z) { static int s_on = -1; if (s_on < 0) { const char *v = getenv("BT_VISUAL_GROUND"); s_on = (v == 0 || v[0] != '0') ? 1 : 0; } if (!s_on) return 0.0f; if (gState == 0) gState = BuildTables() ? 1 : -1; if (gState != 1) return 0.0f; // Local band: up to 3u of visual rising above the solid (measured max ~2.2), // up to 1u of visual dipping below it. Outside the band = not the ground // underfoot (cliff tops, building roofs) -> no conform. float surf; if (!SampleBand(x, z, y - 1.0f, y + 3.0f, &surf)) return 0.0f; float lift = surf - y; if (lift < -1.0f) lift = -1.0f; if (lift > 3.0f) lift = 3.0f; return lift; } //---------------------------------------------------------------------------// // BTGroundRayHit -- march a ray (o + dir*t) against the visible terrain and // return the first point where it meets the surface (rising ground: butte // faces, dune fronts). Used by the weapon fire path to end the beam + place // the impact on the ground. Returns false (no hit) for a level shot over open // desert -- the caller then flies the beam to max range. //---------------------------------------------------------------------------// bool BTGroundRayHit(float ox, float oy, float oz, float dx, float dy, float dz, float maxDist, float *hx, float *hy, float *hz) { if (gState == 0) gState = BuildTables() ? 1 : -1; if (gState != 1) return false; const float step = 4.0f; for (float t = step; t <= maxDist; t += step) { const float x = ox + dx * t; const float y = oy + dy * t; const float z = oz + dz * t; float surf; // wide downward band: catch terrain rising into a mostly-horizontal ray. if (SampleBand(x, z, y - 90.0f, y + 6.0f, &surf) && y <= surf) { *hx = x; *hy = surf; *hz = z; return true; } } return false; }