From 69593fd5c781c08330214d47242494cb7c7578c9 Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Sat, 11 Dec 2021 23:00:38 +0100 Subject: [PATCH] - Triangulation WIP # Conflicts: # source/core/sectorgeometry.cpp --- source/build/include/buildtypes.h | 8 + source/build/src/engine.cpp | 2 +- source/core/gamefuncs.h | 10 + source/core/interpolate.cpp | 4 +- source/core/maploader.cpp | 2 +- source/core/rendering/hw_sections.cpp | 504 +++++++++++++++++++++++++ source/core/rendering/hw_sections2.cpp | 77 +++- source/core/rendering/hw_sections2.h | 8 +- source/core/sectorgeometry.cpp | 433 ++++++++------------- source/core/sectorgeometry.h | 53 ++- 10 files changed, 817 insertions(+), 284 deletions(-) diff --git a/source/build/include/buildtypes.h b/source/build/include/buildtypes.h index 5390f6844..141a37ea1 100644 --- a/source/build/include/buildtypes.h +++ b/source/build/include/buildtypes.h @@ -50,6 +50,14 @@ enum PORTAL_SECTOR_GEOMETRY = 8, }; +enum EDirty +{ + FloorDirty = 1, + CeilingDirty = 2, + GeometryDirty = 4, + AllDirty = 7 +}; + BEGIN_BLD_NS struct XWALL; struct XSECTOR; diff --git a/source/build/src/engine.cpp b/source/build/src/engine.cpp index ccf91bb20..553313c6c 100644 --- a/source/build/src/engine.cpp +++ b/source/build/src/engine.cpp @@ -626,7 +626,7 @@ void dragpoint(int w, int32_t dax, int32_t day) while (1) { auto wal = &wall[w]; - sector[wal->sector].dirty = 255; + sector[wal->sector].dirty = EDirty::AllDirty; wal->x = dax; wal->y = day; walbitmap.Set(w); diff --git a/source/core/gamefuncs.h b/source/core/gamefuncs.h index 1d36d1100..83779200d 100644 --- a/source/core/gamefuncs.h +++ b/source/core/gamefuncs.h @@ -133,6 +133,16 @@ bool sectorsConnected(int sect1, int sect2); // y is negated so that the orientation is the same as in GZDoom, in order to use its utilities. // The render code should NOT use Build coordinates for anything! +inline double RenderX(int x) +{ + return x * (1 / 16.); +} + +inline double RenderY(int y) +{ + return y * (1 / -16.); +} + inline double WallStartX(int wallnum) { return wall[wallnum].x * (1 / 16.); diff --git a/source/core/interpolate.cpp b/source/core/interpolate.cpp index b5afa2612..2c40e277b 100644 --- a/source/core/interpolate.cpp +++ b/source/core/interpolate.cpp @@ -72,8 +72,8 @@ void Set(int index, int type, double val) case Interp_Sect_CeilingPanX: sector[index].ceilingxpan_ = float(val); break; case Interp_Sect_CeilingPanY: sector[index].ceilingypan_ = float(val); break; - case Interp_Wall_X: old = wall[index].x; wall[index].x = xs_CRoundToInt(val); if (wall[index].x != old) sector[wall[index].sector].dirty = 255; break; - case Interp_Wall_Y: old = wall[index].y; wall[index].y = xs_CRoundToInt(val); if (wall[index].y != old) sector[wall[index].sector].dirty = 255; break; + case Interp_Wall_X: old = wall[index].x; wall[index].x = xs_CRoundToInt(val); if (wall[index].x != old) sector[wall[index].sector].dirty = EDirty::AllDirty; break; + case Interp_Wall_Y: old = wall[index].y; wall[index].y = xs_CRoundToInt(val); if (wall[index].y != old) sector[wall[index].sector].dirty = EDirty::AllDirty; break; case Interp_Wall_PanX: wall[index].xpan_ = float(val); break; case Interp_Wall_PanY: wall[index].ypan_ = float(val); break; } diff --git a/source/core/maploader.cpp b/source/core/maploader.cpp index 4548ab049..c3fdeb4e5 100644 --- a/source/core/maploader.cpp +++ b/source/core/maploader.cpp @@ -576,7 +576,7 @@ void setWallSectors() for(auto& sect : sectors()) { - sect.dirty = 255; + sect.dirty = EDirty::AllDirty; sect.exflags = 0; for (auto& wal : wallsofsector(§)) { diff --git a/source/core/rendering/hw_sections.cpp b/source/core/rendering/hw_sections.cpp index 7c5209a99..62e595868 100644 --- a/source/core/rendering/hw_sections.cpp +++ b/source/core/rendering/hw_sections.cpp @@ -40,6 +40,10 @@ #include "hw_sections.h" +#include "sectorgeometry.h" +#include "gamefuncs.h" +#include "earcut.hpp" +#include "nodebuilder/nodebuild.h" TArray sectionLines; @@ -240,3 +244,503 @@ void hw_ClearSplitSector() { splits.Clear(); } + +// this got dumped here to move it out of the way. +static FVector3 CalcNormal(sectortype* sector, int plane) +{ + return { 0,0,0 }; +} + +class UVCalculator1 +{ + sectortype* sect; + int myplane; + int stat; + float z1; + int ix1; + int iy1; + int ix2; + int iy2; + float sinalign, cosalign; + FGameTexture* tex; + float xpanning, ypanning; + float xscaled, yscaled; + FVector2 offset; + +public: + + UVCalculator1(sectortype* sec, int plane, FGameTexture* tx, const FVector2& off) + { + float xpan, ypan; + + sect = sec; + tex = tx; + myplane = plane; + offset = off; + + auto firstwall = sec->firstWall(); + ix1 = firstwall->x; + iy1 = firstwall->y; + ix2 = firstwall->point2Wall()->x; + iy2 = firstwall->point2Wall()->y; + + if (plane == 0) + { + stat = sec->floorstat; + xpan = sec->floorxpan_; + ypan = sec->floorypan_; + PlanesAtPoint(sec, ix1, iy1, nullptr, &z1); + } + else + { + stat = sec->ceilingstat; + xpan = sec->ceilingxpan_; + ypan = sec->ceilingypan_; + PlanesAtPoint(sec, ix1, iy1, &z1, nullptr); + } + + DVector2 dv = { double(ix2 - ix1), -double(iy2 - iy1) }; + auto vang = dv.Angle() - 90.; + + cosalign = float(vang.Cos()); + sinalign = float(vang.Sin()); + + int pow2width = 1 << sizeToBits((int)tx->GetDisplayWidth()); + int pow2height = 1 << sizeToBits((int)tx->GetDisplayHeight()); + + xpanning = xpan / 256.f; + ypanning = ypan / 256.f; + + float scalefactor = (stat & CSTAT_SECTOR_TEXHALF) ? 8.0f : 16.0f; + + if ((stat & (CSTAT_SECTOR_SLOPE | CSTAT_SECTOR_ALIGN)) == (CSTAT_SECTOR_ALIGN)) + { + // This is necessary to adjust for some imprecisions in the math. + // To calculate the inverse Build performs an integer division with significant loss of precision + // that can cause the texture to be shifted by multiple pixels. + // The code below calculates the amount of this deviation so that it can be added back to the formula. + int len = ksqrt(uhypsq(ix2 - ix1, iy2 - iy1)); + if (len != 0) + { + int i = 1048576 / len; + scalefactor *= 1048576.f / (i * len); + } + } + + xscaled = scalefactor * pow2width; + yscaled = scalefactor * pow2height; + } + + FVector2 GetUV(int x, int y, float z) + { + float tv, tu; + + if (stat & CSTAT_SECTOR_ALIGN) + { + float dx = (float)(x - ix1); + float dy = (float)(y - iy1); + + tu = -(dx * sinalign + dy * cosalign); + tv = (dx * cosalign - dy * sinalign); + + if (stat & CSTAT_SECTOR_SLOPE) + { + float dz = (z - z1) * 16; + float newtv = sqrt(tv * tv + dz * dz); + tv = tv < 0 ? -newtv : newtv; + } + } + else + { + tu = x - offset.X; + tv = -y - offset.Y; + } + + if (stat & CSTAT_SECTOR_SWAPXY) + std::swap(tu, tv); + + if (stat & CSTAT_SECTOR_XFLIP) tu = -tu; + if (stat & CSTAT_SECTOR_YFLIP) tv = -tv; + + + + return { tu / xscaled + xpanning, tv / yscaled + ypanning }; + + } +}; + +// moved out of the way + +//========================================================================== +// +// +// +//========================================================================== + +bool SectorGeometry::MakeVertices(unsigned int secnum, int plane, const FVector2& offset) +{ + auto sec = &Sections[secnum]; + auto sectorp = §or[sec->sector]; + int numvertices = sec->lines.Size(); + + TArray points(numvertices, true); + using Point = std::pair; + std::vector> polygon; + std::vector* curPoly; + + polygon.resize(1); + curPoly = &polygon.back(); + FixedBitArray done; + + int fz = sectorp->floorz, cz = sectorp->ceilingz; + + int vertstoadd = numvertices; + + done.Zero(); + while (vertstoadd > 0) + { + int start = 0; + while (done[start] && start < numvertices) start++; + int s = start; + if (start >= 0 && start < numvertices) + { + while (start >= 0 && start < numvertices && !done[start]) + { + auto sline = §ionLines[sec->lines[start]]; + auto wallp = &wall[sline->startpoint]; + float X = float(WallStartX(wallp)); + float Y = float(WallStartY(wallp)); + if (fabs(X) > 32768.f || fabs(Y) > 32768.f) + { + // If we get here there's some fuckery going around with the coordinates. Let's better abort and wait for things to realign. + // Do not try alternative methods if this happens. + return true; + } + curPoly->push_back(std::make_pair(X, Y)); + done.Set(start); + vertstoadd--; + start = sline->point2index; + } + polygon.resize(polygon.size() + 1); + curPoly = &polygon.back(); + if (start != s) return false; // means the sector is badly defined. RRRA'S E1L3 triggers this. + } + } + // Now make sure that the outer boundary is the first polygon by picking a point that's as much to the outside as possible. + int outer = 0; + float minx = FLT_MAX; + float miny = FLT_MAX; + for (size_t a = 0; a < polygon.size(); a++) + { + for (auto& pt : polygon[a]) + { + if (pt.first < minx || (pt.first == minx && pt.second < miny)) + { + minx = pt.first; + miny = pt.second; + outer = int(a); + } + } + } + if (outer != 0) std::swap(polygon[0], polygon[outer]); + auto indices = mapbox::earcut(polygon); + if (indices.size() < 3 * (sec->lines.Size() - 2)) + { + // this means that full triangulation failed. + return false; + } + sectorp->floorz = sectorp->ceilingz = 0; + + int p = 0; + for (size_t a = 0; a < polygon.size(); a++) + { + for (auto& pt : polygon[a]) + { + float planez = 0; + PlanesAtPoint(sectorp, (pt.first * 16), (pt.second * -16), plane ? &planez : nullptr, !plane ? &planez : nullptr); + FVector3 point = { pt.first, pt.second, planez }; + points[p++] = point; + } + } + + auto& entry = data[secnum].planes[plane]; + entry.vertices.Resize((unsigned)indices.size()); + entry.texcoords.Resize((unsigned)indices.size()); + entry.normal = CalcNormal(sectorp, plane); + + auto texture = tileGetTexture(plane ? sectorp->ceilingpicnum : sectorp->floorpicnum); + + UVCalculator1 uvcalc(sectorp, plane, texture, offset); + + for(unsigned i = 0; i < entry.vertices.Size(); i++) + { + auto& pt = points[indices[i]]; + entry.vertices[i] = pt; + entry.texcoords[i] = uvcalc.GetUV(int(pt.X * 16), int(pt.Y * -16), pt.Z); + } + + sectorp->floorz = fz; + sectorp->ceilingz = cz; + return true; +} + +//========================================================================== +// +// Use ZDoom's node builder if the simple approach fails. +// This will create something usable in the vast majority of cases, +// even if the result is less efficient. +// +//========================================================================== + +bool SectorGeometry::MakeVertices2(unsigned int secnum, int plane, const FVector2& offset) +{ + auto sec = &Sections[secnum]; + auto sectorp = §or[sec->sector]; + int numvertices = sec->lines.Size(); + + // Convert our sector into something the node builder understands + TArray vertexes(sectorp->wallnum, true); + TArray lines(numvertices, true); + TArray sides(numvertices, true); + int j = 0; + + for (int i = 0; i < numvertices; i++) + { + auto sline = §ionLines[sec->lines[i]]; + if (sline->point2index < 0) continue; // Exhumed LEV14 triggers this on sector 169. + + auto wallp = &wall[sline->startpoint]; + vertexes[j].p = { wallp->x * (1 / 16.), wallp->y * (1 / -16.) }; + + if (fabs(vertexes[j].p.X) > 32768.f || fabs(vertexes[j].p.Y) > 32768.f) + { + // If we get here there's some fuckery going around with the coordinates. Let's better abort and wait for things to realign. + return true; + } + + lines[j].backsector = nullptr; + lines[j].frontsector = sectorp; + lines[j].linenum = j; + lines[j].wallnum = sline->wall; + lines[j].sidedef[0] = &sides[j]; + lines[j].sidedef[1] = nullptr; + lines[j].v1 = &vertexes[j]; + lines[j].v2 = &vertexes[sline->point2index + j - i]; + + sides[j].sidenum = j; + sides[j].sector = sectorp; + j++; + } + + vertexes.Clamp(j); + lines.Clamp(j); + sides.Clamp(j); + // Weed out any overlaps. These often happen with door setups and can lead to bad subsectors + for (unsigned i = 0; i < lines.Size(); i++) + { + auto p1 = lines[i].v1->p, p2 = lines[i].v2->p; + + // Throw out any line with zero length. + if (p1 == p2) + { + lines.Delete(i); + i--; + continue; + } + + for (unsigned j = i + 1; j < lines.Size(); j++) + { + auto pp1 = lines[j].v1->p, pp2 = lines[j].v2->p; + + if (pp1 == p2 && pp2 == p1) + { + // handle the simple case first, i.e. line j is the inverse of line i. + // in this case both lines need to be deleted. + lines.Delete(j); + lines.Delete(i); + i--; + goto nexti; + } + else if (pp1 == p2) + { + // only the second line's start point matches. + // In this case we have to delete the shorter line and truncate the other one. + + // check if the second line's end point is on the line we are checking + double d1 = PointOnLineSide(pp2, p1, p2); + if (fabs(d1) > FLT_EPSILON) continue; // not colinear + bool vert = p1.X == p2.X; + double p1x = vert ? p1.X : p1.Y; + double p2x = vert ? p2.X : p2.Y; + double pp1x = vert ? pp1.X : pp1.Y; + double pp2x = vert ? pp2.X : pp2.Y; + + if (pp2x > min(p1x, p2x) && pp2x < max(p1x, p2x)) + { + // pp2 is on line i. + lines[i].v2 = lines[j].v2; + lines.Delete(j); + continue; + } + else if (p1x > min(pp1x, pp2x) && p1x < max(pp1x, pp2x)) + { + // p1 is on line j + lines[j].v1 = lines[j].v2; + lines.Delete(i); + goto nexti; + } + } + else if (pp2 == p1) + { + // only the second line's start point matches. + // In this case we have to delete the shorter line and truncate the other one. + + // check if the second line's end point is on the line we are checking + double d1 = PointOnLineSide(pp1, p1, p2); + if (fabs(d1) > FLT_EPSILON) continue; // not colinear + bool vert = p1.X == p2.X; + double p1x = vert ? p1.X : p1.Y; + double p2x = vert ? p2.X : p2.Y; + double pp1x = vert ? pp1.X : pp1.Y; + double pp2x = vert ? pp2.X : pp2.Y; + + if (pp1x > min(p1x, p2x) && pp1x < max(p1x, p2x)) + { + // pp1 is on line i. + lines[i].v1 = lines[j].v1; + lines.Delete(j); + continue; + } + else if (p2x > min(pp1x, pp2x) && p2x < max(pp1x, pp2x)) + { + // p2 is on line j + lines[j].v2 = lines[j].v1; + lines.Delete(i); + goto nexti; + } + } + else + { + // no idea if we should do further checks here. Blood's doors do not need them. We'll see. + } + } +nexti:; + } + + if (lines.Size() < 4) + { + // nothing to generate. If line count is < 4 this sector is degenerate and should not be processed further. + auto& entry = data[secnum].planes[plane]; + entry.vertices.Clear(); + entry.texcoords.Clear(); + return true; + } + + + FNodeBuilder::FLevel leveldata = + { + &vertexes[0], (int)vertexes.Size(), + &sides[0], (int)sides.Size(), + &lines[0], (int)lines.Size(), + 0, 0, 0, 0 + }; + leveldata.FindMapBounds(); + FNodeBuilder builder(leveldata); + + FLevelLocals Level; + builder.Extract(Level); + + // Now turn the generated subsectors into triangle meshes + + auto& entry = data[secnum].planes[plane]; + entry.vertices.Clear(); + entry.texcoords.Clear(); + + int fz = sectorp->floorz, cz = sectorp->ceilingz; + sectorp->floorz = sectorp->ceilingz = 0; + + for (auto& sub : Level.subsectors) + { + if (sub.numlines <= 2) continue; + auto v0 = sub.firstline->v1; + for (unsigned i = 1; i < sub.numlines - 1; i++) + { + auto v1 = sub.firstline[i].v1; + auto v2 = sub.firstline[i].v2; + + entry.vertices.Push({ (float)v0->fX(), (float)v0->fY(), 0 }); + entry.vertices.Push({ (float)v1->fX(), (float)v1->fY(), 0 }); + entry.vertices.Push({ (float)v2->fX(), (float)v2->fY(), 0 }); + + } + + } + + // calculate the rest. + auto texture = tileGetTexture(plane ? sectorp->ceilingpicnum : sectorp->floorpicnum); + + UVCalculator uvcalc(sectorp, plane, texture, offset); + + entry.texcoords.Resize(entry.vertices.Size()); + for (unsigned i = 0; i < entry.vertices.Size(); i++) + { + auto& pt = entry.vertices[i]; + + float planez = 0; + PlanesAtPoint(sectorp, (pt.X * 16), (pt.Y * -16), plane ? &planez : nullptr, !plane ? &planez : nullptr); + entry.vertices[i].Z = planez; + entry.texcoords[i] = uvcalc.GetUV(int(pt.X * 16.), int(pt.Y * -16.), pt.Z); + } + entry.normal = CalcNormal(sectorp, plane); + sectorp->floorz = fz; + sectorp->ceilingz = cz; + return true; +} + +//========================================================================== +// +// +// +//========================================================================== + +void SectorGeometry::ValidateSector(unsigned int secnum, int plane, const FVector2& offset) +{ + auto sec = §or[Sections[secnum].sector]; + + auto compare = &data[secnum].compare[plane]; + if (plane == 0) + { + if (sec->floorheinum == compare->floorheinum && + sec->floorpicnum == compare->floorpicnum && + ((sec->floorstat ^ compare->floorstat) & (CSTAT_SECTOR_ALIGN | CSTAT_SECTOR_YFLIP | CSTAT_SECTOR_XFLIP | CSTAT_SECTOR_TEXHALF | CSTAT_SECTOR_SWAPXY)) == 0 && + sec->floorxpan_ == compare->floorxpan_ && + sec->floorypan_ == compare->floorypan_ && + sec->firstWall()->pos == data[secnum].poscompare[0] && + sec->firstWall()->point2Wall()->pos == data[secnum].poscompare2[0] && + !(sec->dirty & 1) && data[secnum].planes[plane].vertices.Size() ) return; + + sec->dirty &= ~1; + } + else + { + if (sec->ceilingheinum == compare->ceilingheinum && + sec->ceilingpicnum == compare->ceilingpicnum && + ((sec->ceilingstat ^ compare->ceilingstat) & (CSTAT_SECTOR_ALIGN | CSTAT_SECTOR_YFLIP | CSTAT_SECTOR_XFLIP | CSTAT_SECTOR_TEXHALF | CSTAT_SECTOR_SWAPXY)) == 0 && + sec->ceilingxpan_ == compare->ceilingxpan_ && + sec->ceilingypan_ == compare->ceilingypan_ && + sec->firstWall()->pos == data[secnum].poscompare[1] && + sec->firstWall()->point2Wall()->pos == data[secnum].poscompare2[1] && + !(sec->dirty & 2) && data[secnum].planes[1].vertices.Size()) return; + + sec->dirty &= ~2; + } + *compare = *sec; + data[secnum].poscompare[plane] = sec->firstWall()->pos; + data[secnum].poscompare2[plane] = sec->firstWall()->point2Wall()->pos; + if (data[secnum].degenerate || !MakeVertices(secnum, plane, offset)) + { + data[secnum].degenerate = true; + //Printf(TEXTCOLOR_YELLOW "Normal triangulation failed for sector %d. Retrying with alternative approach\n", secnum); + MakeVertices2(secnum, plane, offset); + } +} diff --git a/source/core/rendering/hw_sections2.cpp b/source/core/rendering/hw_sections2.cpp index f2748f940..97e49505a 100644 --- a/source/core/rendering/hw_sections2.cpp +++ b/source/core/rendering/hw_sections2.cpp @@ -1,3 +1,43 @@ +/* +** hw_sections.cpp +** For decoupling the renderer from internal Build structures +** +**--------------------------------------------------------------------------- +** Copyright 2021 Christoph Oelckers +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +** The sole reason for existence of this file is that Build's sector setup +** does not allow for easy splitting of sectors, either for having disjoint parts +** or requiring partial rendering. So we need to add a superstructure +** where we can shuffle around the map content without disturbing the original +** order... +** +*/ + #include "build.h" #include "hw_sections2.h" #include "memarena.h" @@ -44,6 +84,13 @@ static int sgn(int v) return (v > 0) - (v < 0); } +static int dist(const vec2_t& a, const vec2_t& b) +{ + // We only need to know if it's 1 or higher, so this is enough. + return abs(a.x - b.x) + abs(a.y - b.y); +} + + using cmp = bool(*)(int, int); //========================================================================== @@ -59,7 +106,7 @@ void StripLoop(TArray& points) unsigned prev = p == 0 ? points.Size() - 1 : p - 1; unsigned next = p == points.Size() - 1 ? 0 : p + 1; - if (points[next] == points[prev]) + if (points[next] == points[prev]) // if the two neighboring points are equal, this one dos not contribute to the sector's area. { if (next == 0) { @@ -74,9 +121,10 @@ void StripLoop(TArray& points) if (p > 0) p--; // backtrack one point more to ensure we can check the newly formed connection as well. } else if ((points[prev].x == points[p].x && points[next].x == points[p].x && sgn(points[next].y - points[p].y) == sgn(points[prev].y - points[p].y)) || - (points[prev].y == points[p].y && points[next].y == points[p].y && sgn(points[next].x - points[p].x) == sgn(points[prev].x - points[p].x))) + (points[prev].y == points[p].y && points[next].y == points[p].y && sgn(points[next].x - points[p].x) == sgn(points[prev].x - points[p].x)) || + dist(points[prev], points[next]) <= 1) // if the two points are extremely close together, we may also ignore the intermediate point. { - // both points exit the point into the same direction. Here it is sufficient to just delete it so that the neighboring ones connect directly. + // both connections exit the point into the same direction. Here it is sufficient to just delete it so that the neighboring ones connect directly. points.Delete(p); p--; if (p > 0) p--; // backtrack one point more to ensure we can check the newly formed connection as well. @@ -85,6 +133,7 @@ void StripLoop(TArray& points) } } + //========================================================================== // // @@ -600,6 +649,28 @@ void hw_CreateSections2() } } +//========================================================================== +// +// Create a set of vertex loops from a given session +// +//========================================================================== + +Outline BuildOutline(Section2* section) +{ + Outline output(section->loops.Size(), true); + for (unsigned i = 0; i < section->loops.Size(); i++) + { + output[i].Resize(section->loops[i].walls.Size()); + for (unsigned j = 0; j < section->loops[i].walls.Size(); j++) + { + output[i][j] = *section->loops[i].walls[j]->v1; + } + StripLoop(output[i]); + } + return output; +} + + #include "c_dispatch.h" #include "engineerrors.h" diff --git a/source/core/rendering/hw_sections2.h b/source/core/rendering/hw_sections2.h index c4fefe1f9..0575ac378 100644 --- a/source/core/rendering/hw_sections2.h +++ b/source/core/rendering/hw_sections2.h @@ -29,7 +29,7 @@ struct Section2Loop struct Section2 { int flags; - int index; + unsigned index; sectortype* sector; // this uses a memory arena for storage, so use TArrayView instead of TArray TArrayView walls; @@ -39,4 +39,8 @@ struct Section2 extern TArray sections2; extern TArrayView> sections2PerSector; -void hw_CreateSections2(); \ No newline at end of file +void hw_CreateSections2(); +using Outline = TArray>; +using Point = std::pair; +using FOutline = std::vector> ; // Data type was chosen so it can be passed directly into Earcut. +Outline BuildOutline(Section2* section); diff --git a/source/core/sectorgeometry.cpp b/source/core/sectorgeometry.cpp index e91afe685..39da7d9fe 100644 --- a/source/core/sectorgeometry.cpp +++ b/source/core/sectorgeometry.cpp @@ -1,4 +1,5 @@ /* +/* ** sectorgeometry.cpp ** ** caches the triangle meshes used for rendering sector planes. @@ -39,7 +40,8 @@ #include "gamefuncs.h" #include "texturemanager.h" #include "earcut.hpp" -#include "hw_sections.h" +#include "hw_sections2.h" +#include "tesselator.h" #include "nodebuilder/nodebuild.h" SectorGeometry sectorGeometry; @@ -206,277 +208,179 @@ public: }; -//========================================================================== -// -// -// -//========================================================================== - -bool SectorGeometry::MakeVertices(unsigned int secnum, int plane, const FVector2& offset) +enum class ETriangulateResult { - auto sec = &Sections[secnum]; - auto sectorp = §or[sec->sector]; - int numvertices = sec->lines.Size(); - - TArray points(numvertices, true); - using Point = std::pair; - std::vector> polygon; - std::vector* curPoly; + Ok = 0, + Failed = 1, // unable to triangulate + Invalid = 2, // input data invalid. +}; - polygon.resize(1); - curPoly = &polygon.back(); - FixedBitArray done; +//========================================================================== +// +// Convert the outline to render coordinates. +// +//========================================================================== - int fz = sectorp->floorz, cz = sectorp->ceilingz; - - int vertstoadd = numvertices; - - done.Zero(); - while (vertstoadd > 0) +static int OutlineToFloat(Outline& outl, FOutline& polygon) +{ + polygon.resize(outl.Size()); + unsigned count = 0; + for (unsigned i = 0; i < outl.Size(); i++) { - int start = 0; - while (done[start] && start < numvertices) start++; - int s = start; - if (start >= 0 && start < numvertices) + polygon[i].resize(outl[i].Size()); + count += outl[i].Size(); + for (unsigned j = 0; j < outl[i].Size(); j++) { - while (start >= 0 && start < numvertices && !done[start]) - { - auto sline = §ionLines[sec->lines[start]]; - auto wallp = &wall[sline->startpoint]; - float X = float(WallStartX(wallp)); - float Y = float(WallStartY(wallp)); + float X = RenderX(outl[i][j].x); + float Y = RenderX(outl[i][j].y); if (fabs(X) > 32768.f || fabs(Y) > 32768.f) { // If we get here there's some fuckery going around with the coordinates. Let's better abort and wait for things to realign. // Do not try alternative methods if this happens. - return true; + return -1; } - curPoly->push_back(std::make_pair(X, Y)); - done.Set(start); - vertstoadd--; - start = sline->point2index; - } - polygon.resize(polygon.size() + 1); - curPoly = &polygon.back(); - if (start != s) return false; // means the sector is badly defined. RRRA'S E1L3 triggers this. - } - } - // Now make sure that the outer boundary is the first polygon by picking a point that's as much to the outside as possible. - int outer = 0; - float minx = FLT_MAX; - float miny = FLT_MAX; - for (size_t a = 0; a < polygon.size(); a++) - { - for (auto& pt : polygon[a]) - { - if (pt.first < minx || (pt.first == minx && pt.second < miny)) - { - minx = pt.first; - miny = pt.second; - outer = int(a); + polygon[i][j] = { X, Y }; } } - } - if (outer != 0) std::swap(polygon[0], polygon[outer]); + return count; +} + +//========================================================================== +// +// Try to triangulate a given outline with Earcut. +// +//========================================================================== + +ETriangulateResult TriangulateOutlineEarcut(const FOutline& polygon, int count, TArray& points, TArray& indicesOut) +{ + // Sections are already validated so we can assume that the data is well defined here. + auto indices = mapbox::earcut(polygon); - if (indices.size() < 3 * (sec->lines.Size() - 2)) + if (indices.size() < 3 * count) { // this means that full triangulation failed. - return false; + return ETriangulateResult::Failed; } - sectorp->floorz = sectorp->ceilingz = 0; - + points.Resize(count); int p = 0; for (size_t a = 0; a < polygon.size(); a++) { for (auto& pt : polygon[a]) { - float planez = 0; - PlanesAtPoint(sectorp, (pt.first * 16), (pt.second * -16), plane ? &planez : nullptr, !plane ? &planez : nullptr); - FVector3 point = { pt.first, pt.second, planez }; - points[p++] = point; + points[p++] = { pt.first, pt.second }; } } - - auto& entry = data[secnum].planes[plane]; - entry.vertices.Resize((unsigned)indices.size()); - entry.texcoords.Resize((unsigned)indices.size()); - entry.normal = CalcNormal(sectorp, plane); - - auto texture = tileGetTexture(plane ? sectorp->ceilingpicnum : sectorp->floorpicnum); - - UVCalculator uvcalc(sectorp, plane, texture, offset); - - for(unsigned i = 0; i < entry.vertices.Size(); i++) + indicesOut.Resize(count * 3); + for (unsigned i = 0; i < indices.size(); i++) { - auto& pt = points[indices[i]]; - entry.vertices[i] = pt; - entry.texcoords[i] = uvcalc.GetUV(int(pt.X * 16), int(pt.Y * -16), pt.Z); + indicesOut[i] = indices[i]; } - - sectorp->floorz = fz; - sectorp->ceilingz = cz; - return true; + return ETriangulateResult::Ok; } //========================================================================== // -// Use ZDoom's node builder if the simple approach fails. -// This will create something usable in the vast majority of cases, -// even if the result is less efficient. +// Try to triangulate a given outline with libtess2. // //========================================================================== -bool SectorGeometry::MakeVertices2(unsigned int secnum, int plane, const FVector2& offset) +ETriangulateResult TriangulateOutlineLibtess(const FOutline& polygon, int count, TArray& points, TArray& indicesOut) { - auto sec = &Sections[secnum]; - auto sectorp = §or[sec->sector]; - int numvertices = sec->lines.Size(); + FMemArena tessArena(100000); - // Convert our sector into something the node builder understands - TArray vertexes(sectorp->wallnum, true); - TArray lines(numvertices, true); - TArray sides(numvertices, true); - int j = 0; - - for (int i = 0; i < numvertices; i++) + auto poolAlloc = [](void* userData, unsigned int size) -> void* { - auto sline = §ionLines[sec->lines[i]]; - if (sline->point2index < 0) continue; // Exhumed LEV14 triggers this on sector 169. + FMemArena* pool = (FMemArena*)userData; + return pool->Alloc(size); + }; - auto wallp = &wall[sline->startpoint]; - vertexes[j].p = { wallp->x * (1 / 16.), wallp->y * (1 / -16.) }; + auto poolFree = [](void*, void*) + { + }; - if (fabs(vertexes[j].p.X) > 32768.f || fabs(vertexes[j].p.Y) > 32768.f) + TESSalloc ma{}; + ma.memalloc = poolAlloc; + ma.memfree = poolFree; + ma.userData = (void*)&tessArena; + ma.extraVertices = 256; // realloc not provided, allow 256 extra vertices. + + auto tess = tessNewTess(&ma); + if (!tess) + return ETriangulateResult::Failed; + + //tessSetOption(tess, TESS_CONSTRAINED_DELAUNAY_TRIANGULATION, 1); + + // Add contours. + for (auto& loop : polygon) + tessAddContour(tess, 2, &loop.data()->first, (int)sizeof(*loop.data()), loop.size()); + + int result = tessTesselate(tess, TESS_WINDING_POSITIVE, TESS_POLYGONS, 3, 2, 0); + if (!result) { - // If we get here there's some fuckery going around with the coordinates. Let's better abort and wait for things to realign. - return true; + tessDeleteTess(tess); + return ETriangulateResult::Failed; } - lines[j].backsector = nullptr; - lines[j].frontsector = sectorp; - lines[j].linenum = j; - lines[j].wallnum = sline->wall; - lines[j].sidedef[0] = &sides[j]; - lines[j].sidedef[1] = nullptr; - lines[j].v1 = &vertexes[j]; - lines[j].v2 = &vertexes[sline->point2index + j - i]; + const float* verts = tessGetVertices(tess); + const int* vinds = tessGetVertexIndices(tess); + const int* elems = tessGetElements(tess); + const int nverts = tessGetVertexCount(tess); + const int nelems = tessGetElementCount(tess); - sides[j].sidenum = j; - sides[j].sector = sectorp; - j++; + points.Resize(nverts); + indicesOut.Resize(nelems); + for (int i = 0; i < nverts; i++) + { + points[i] = { verts[i * 2], verts[i * 2 + 1] }; + } + for (int i = 0; i < nelems; i++) + { + indicesOut[i] = elems[i]; } + return ETriangulateResult::Ok; +} - vertexes.Clamp(j); - lines.Clamp(j); - sides.Clamp(j); - // Weed out any overlaps. These often happen with door setups and can lead to bad subsectors - for (unsigned i = 0; i < lines.Size(); i++) - { - auto p1 = lines[i].v1->p, p2 = lines[i].v2->p; +//========================================================================== +// +// Try to triangulate a given outline with the ZDoom node builder. +// +//========================================================================== - // Throw out any line with zero length. - if (p1 == p2) - { - lines.Delete(i); - i--; - continue; - } +ETriangulateResult TriangulateOutlineNodeBuild(const FOutline& polygon, int count, TArray& points, TArray& indicesOut) +{ + TArray vertexes(count, true); + TArray lines(count, true); + TArray sides(count, true); + sectortype dummy; - for (unsigned j = i + 1; j < lines.Size(); j++) - { - auto pp1 = lines[j].v1->p, pp2 = lines[j].v2->p; - - if (pp1 == p2 && pp2 == p1) - { - // handle the simple case first, i.e. line j is the inverse of line i. - // in this case both lines need to be deleted. - lines.Delete(j); - lines.Delete(i); - i--; - goto nexti; - } - else if (pp1 == p2) - { - // only the second line's start point matches. - // In this case we have to delete the shorter line and truncate the other one. - - // check if the second line's end point is on the line we are checking - double d1 = PointOnLineSide(pp2, p1, p2); - if (fabs(d1) > FLT_EPSILON) continue; // not colinear - bool vert = p1.X == p2.X; - double p1x = vert ? p1.X : p1.Y; - double p2x = vert ? p2.X : p2.Y; - double pp1x = vert ? pp1.X : pp1.Y; - double pp2x = vert ? pp2.X : pp2.Y; - - if (pp2x > min(p1x, p2x) && pp2x < max(p1x, p2x)) + int n = 0; + for (size_t i = 0; i < polygon.size(); i++) { - // pp2 is on line i. - lines[i].v2 = lines[j].v2; - lines.Delete(j); - continue; - } - else if (p1x > min(pp1x, pp2x) && p1x < max(pp1x, pp2x)) + int firstn = n; + for (size_t j = 0; j < polygon[i].size(); i++) { - // p1 is on line j - lines[j].v1 = lines[j].v2; - lines.Delete(i); - goto nexti; + vertexes[n].set(polygon[i][j].first, polygon[i][j].second); + + lines[n].backsector = nullptr; + lines[n].frontsector = &dummy; + lines[n].linenum = (int)j; + lines[n].wallnum = n; + lines[n].sidedef[0] = &sides[n]; + lines[n].sidedef[1] = nullptr; + lines[n].v1 = &vertexes[n]; + lines[n].v2 = j == polygon[i].size() ? &vertexes[firstn] : &vertexes[n + 1]; + + sides[n].sidenum = n; + sides[n].sector = &dummy; + } - } - else if (pp2 == p1) - { - // only the second line's start point matches. - // In this case we have to delete the shorter line and truncate the other one. - - // check if the second line's end point is on the line we are checking - double d1 = PointOnLineSide(pp1, p1, p2); - if (fabs(d1) > FLT_EPSILON) continue; // not colinear - bool vert = p1.X == p2.X; - double p1x = vert ? p1.X : p1.Y; - double p2x = vert ? p2.X : p2.Y; - double pp1x = vert ? pp1.X : pp1.Y; - double pp2x = vert ? pp2.X : pp2.Y; - - if (pp1x > min(p1x, p2x) && pp1x < max(p1x, p2x)) - { - // pp1 is on line i. - lines[i].v1 = lines[j].v1; - lines.Delete(j); - continue; } - else if (p2x > min(pp1x, pp2x) && p2x < max(pp1x, pp2x)) - { - // p2 is on line j - lines[j].v2 = lines[j].v1; - lines.Delete(i); - goto nexti; - } - } - else - { - // no idea if we should do further checks here. Blood's doors do not need them. We'll see. - } - } -nexti:; - } - - if (lines.Size() < 4) - { - // nothing to generate. If line count is < 4 this sector is degenerate and should not be processed further. - auto& entry = data[secnum].planes[plane]; - entry.vertices.Clear(); - entry.texcoords.Clear(); - return true; - } - FNodeBuilder::FLevel leveldata = { - &vertexes[0], (int)vertexes.Size(), - &sides[0], (int)sides.Size(), - &lines[0], (int)lines.Size(), + &vertexes[0], count, + &sides[0], count, + &lines[0], count, 0, 0, 0, 0 }; leveldata.FindMapBounds(); @@ -485,51 +389,27 @@ nexti:; FLevelLocals Level; builder.Extract(Level); - // Now turn the generated subsectors into triangle meshes - - auto& entry = data[secnum].planes[plane]; - entry.vertices.Clear(); - entry.texcoords.Clear(); - - int fz = sectorp->floorz, cz = sectorp->ceilingz; - sectorp->floorz = sectorp->ceilingz = 0; + points.Resize(Level.vertexes.Size()); + for (unsigned i = 0; i < Level.vertexes.Size(); i++) + { + points[i] = { (float)Level.vertexes[i].fX(), (float)Level.vertexes[i].fY() }; + } + indicesOut.Clear(); for (auto& sub : Level.subsectors) { if (sub.numlines <= 2) continue; - auto v0 = sub.firstline->v1; + auto v0 = Level.vertexes.IndexOf(sub.firstline->v1); for (unsigned i = 1; i < sub.numlines - 1; i++) { - auto v1 = sub.firstline[i].v1; - auto v2 = sub.firstline[i].v2; - - entry.vertices.Push({ (float)v0->fX(), (float)v0->fY(), 0 }); - entry.vertices.Push({ (float)v1->fX(), (float)v1->fY(), 0 }); - entry.vertices.Push({ (float)v2->fX(), (float)v2->fY(), 0 }); - + auto v1 = Level.vertexes.IndexOf(sub.firstline->v1); + auto v2 = Level.vertexes.IndexOf(sub.firstline->v2); + indicesOut.Push(v0); + indicesOut.Push(v1); + indicesOut.Push(v2); } - } - - // calculate the rest. - auto texture = tileGetTexture(plane ? sectorp->ceilingpicnum : sectorp->floorpicnum); - - UVCalculator uvcalc(sectorp, plane, texture, offset); - - entry.texcoords.Resize(entry.vertices.Size()); - for (unsigned i = 0; i < entry.vertices.Size(); i++) - { - auto& pt = entry.vertices[i]; - - float planez = 0; - PlanesAtPoint(sectorp, (pt.X * 16), (pt.Y * -16), plane ? &planez : nullptr, !plane ? &planez : nullptr); - entry.vertices[i].Z = planez; - entry.texcoords[i] = uvcalc.GetUV(int(pt.X * 16.), int(pt.Y * -16.), pt.Z); - } - entry.normal = CalcNormal(sectorp, plane); - sectorp->floorz = fz; - sectorp->ceilingz = cz; - return true; + return ETriangulateResult::Ok; } //========================================================================== @@ -538,11 +418,12 @@ nexti:; // //========================================================================== -void SectorGeometry::ValidateSector(unsigned int secnum, int plane, const FVector2& offset) +bool SectionGeometry::ValidateSection(Section2* section, int plane, const FVector2& offset) { - auto sec = §or[Sections[secnum].sector]; + auto sec = section->sector; + auto& sdata = data[section->index]; - auto compare = &data[secnum].compare[plane]; + auto compare = &sdata.compare[plane]; if (plane == 0) { if (sec->floorheinum == compare->floorheinum && @@ -550,11 +431,11 @@ void SectorGeometry::ValidateSector(unsigned int secnum, int plane, const FVecto ((sec->floorstat ^ compare->floorstat) & (CSTAT_SECTOR_ALIGN | CSTAT_SECTOR_YFLIP | CSTAT_SECTOR_XFLIP | CSTAT_SECTOR_TEXHALF | CSTAT_SECTOR_SWAPXY)) == 0 && sec->floorxpan_ == compare->floorxpan_ && sec->floorypan_ == compare->floorypan_ && - sec->firstWall()->pos == data[secnum].poscompare[0] && - sec->firstWall()->point2Wall()->pos == data[secnum].poscompare2[0] && - !(sec->dirty & 1) && data[secnum].planes[plane].vertices.Size() ) return; + sec->firstWall()->pos == sdata.poscompare[0] && + sec->firstWall()->point2Wall()->pos == sdata.poscompare2[0] && + !(sdata.dirty & EDirty::FloorDirty) && sdata.planes[plane].vertices.Size() ) return true; - sec->dirty &= ~1; + sdata.dirty &= EDirty::FloorDirty; } else { @@ -563,19 +444,23 @@ void SectorGeometry::ValidateSector(unsigned int secnum, int plane, const FVecto ((sec->ceilingstat ^ compare->ceilingstat) & (CSTAT_SECTOR_ALIGN | CSTAT_SECTOR_YFLIP | CSTAT_SECTOR_XFLIP | CSTAT_SECTOR_TEXHALF | CSTAT_SECTOR_SWAPXY)) == 0 && sec->ceilingxpan_ == compare->ceilingxpan_ && sec->ceilingypan_ == compare->ceilingypan_ && - sec->firstWall()->pos == data[secnum].poscompare[1] && - sec->firstWall()->point2Wall()->pos == data[secnum].poscompare2[1] && - !(sec->dirty & 2) && data[secnum].planes[1].vertices.Size()) return; + sec->firstWall()->pos == sdata.poscompare[1] && + sec->firstWall()->point2Wall()->pos == sdata.poscompare2[1] && + !(sdata.dirty & EDirty::CeilingDirty) && sdata.planes[1].vertices.Size()) return true; - sec->dirty &= ~2; + sdata.dirty &= ~EDirty::CeilingDirty; } *compare = *sec; - data[secnum].poscompare[plane] = sec->firstWall()->pos; - data[secnum].poscompare2[plane] = sec->firstWall()->point2Wall()->pos; - if (data[secnum].degenerate || !MakeVertices(secnum, plane, offset)) + sdata.poscompare[plane] = sec->firstWall()->pos; + sdata.poscompare2[plane] = sec->firstWall()->point2Wall()->pos; + return false; +} + + +void SectionGeometry::MarkDirty(sectortype* sector) +{ + for (auto section : sections2PerSector[sectnum(sector)]) { - data[secnum].degenerate = true; - //Printf(TEXTCOLOR_YELLOW "Normal triangulation failed for sector %d. Retrying with alternative approach\n", secnum); - MakeVertices2(secnum, plane, offset); + data[section->index].dirty = sector->dirty; } } diff --git a/source/core/sectorgeometry.h b/source/core/sectorgeometry.h index 32ce750f8..39641e259 100644 --- a/source/core/sectorgeometry.h +++ b/source/core/sectorgeometry.h @@ -4,6 +4,8 @@ #include "vectors.h" #include "build.h" +struct Section2; + struct SectorGeometryPlane { TArray vertices; @@ -45,4 +47,53 @@ public: extern SectorGeometry sectorGeometry; - \ No newline at end of file + +enum GeomFlags +{ + NoEarcut = 1, + NoLibtess = 2, + FloorDone = 4, + CeilingDone = 8, +}; + +struct SectionGeometryData +{ + SectorGeometryPlane planes[2]; + sectortype compare[2] = {}; + vec2_t poscompare[2] = {}; + vec2_t poscompare2[2] = {}; + FVector3 normal[2]; + int dirty; + int flags; +}; + +class SectionGeometry +{ + TArray meshVerts; + TArray meshIndices; + TArray data; + + bool ValidateSection(Section2* section, int plane, const FVector2& offset); + void MarkDirty(sectortype* sector); + +public: + /* + SectionGeometryPlane* get(Section2* section, int plane, const FVector2& offset) + { + if (section->index >= data.Size()) return nullptr; + ValidateSector(section->index, plane, offset); + return &data[section->index].planes[plane]; + } + */ + + void SetSize(unsigned sectcount) + { + data.Clear(); // delete old content + data.Resize(sectcount); + } +}; + +extern SectorGeometry sectorGeometry; +extern SectionGeometry sectionGeometry; + + \ No newline at end of file