mirror of
https://github.com/ZDoom/zdbsp.git
synced 2024-11-28 14:41:59 +00:00
1146 lines
28 KiB
Text
1146 lines
28 KiB
Text
|
#include <stdlib.h>
|
||
|
#include <assert.h>
|
||
|
#include <malloc.h>
|
||
|
#include <string.h>
|
||
|
#include <stdio.h>
|
||
|
#include <math.h>
|
||
|
|
||
|
#include "zdbsp.h"
|
||
|
#include "nodebuild.h"
|
||
|
#include "templates.h"
|
||
|
|
||
|
static const int PO_LINE_START = 1;
|
||
|
static const int PO_LINE_EXPLICIT = 5;
|
||
|
|
||
|
#define Printf printf
|
||
|
#define STACK_ARGS
|
||
|
|
||
|
#if 0
|
||
|
#define D(x) x
|
||
|
#else
|
||
|
#define D(x) do{}while(0)
|
||
|
#endif
|
||
|
|
||
|
#if 0
|
||
|
#define P(x) x
|
||
|
#else
|
||
|
#define P(x) do{}while(0)
|
||
|
#endif
|
||
|
|
||
|
FNodeBuilder::FNodeBuilder (FLevel &level,
|
||
|
TArray<FPolyStart> &polyspots, TArray<FPolyStart> &anchors,
|
||
|
const char *name)
|
||
|
: Level (level), SegsStuffed (0), MapName (name)
|
||
|
{
|
||
|
FindUsedVertices (Level.Vertices, Level.NumVertices);
|
||
|
MakeSegsFromSides ();
|
||
|
FindPolyContainers (polyspots, anchors);
|
||
|
GroupSegPlanes ();
|
||
|
BuildTree ();
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::FindUsedVertices (WideVertex *oldverts, int max)
|
||
|
{
|
||
|
size_t *map = (size_t *)alloca (max*sizeof(size_t));
|
||
|
int i;
|
||
|
FPrivVert newvert;
|
||
|
|
||
|
memset (&map[0], -1, sizeof(size_t)*max);
|
||
|
|
||
|
newvert.segs = NO_INDEX;
|
||
|
for (i = 0; i < Level.NumLines; ++i)
|
||
|
{
|
||
|
int v1 = Level.Lines[i].v1;
|
||
|
int v2 = Level.Lines[i].v2;
|
||
|
|
||
|
if (map[v1] == (size_t)-1)
|
||
|
{
|
||
|
newvert.x = oldverts[v1].x;
|
||
|
newvert.y = oldverts[v1].y;
|
||
|
map[v1] = Vertices.Push (newvert);
|
||
|
}
|
||
|
if (map[v2] == (size_t)-1)
|
||
|
{
|
||
|
newvert.x = oldverts[v2].x;
|
||
|
newvert.y = oldverts[v2].y;
|
||
|
map[v2] = Vertices.Push (newvert);
|
||
|
}
|
||
|
|
||
|
Level.Lines[i].v1 = map[v1];
|
||
|
Level.Lines[i].v2 = map[v2];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::MakeSegsFromSides ()
|
||
|
{
|
||
|
FPrivSeg seg;
|
||
|
int i, j;
|
||
|
|
||
|
seg.next = NO_INDEX;
|
||
|
seg.loopnum = 0;
|
||
|
seg.offset = 0;
|
||
|
|
||
|
for (i = 0; i < Level.NumLines; ++i)
|
||
|
{
|
||
|
if (Level.Lines[i].sidenum[0] != NO_INDEX)
|
||
|
{
|
||
|
WORD backside;
|
||
|
|
||
|
seg.linedef = i;
|
||
|
seg.sidedef = Level.Lines[i].sidenum[0];
|
||
|
backside = Level.Lines[i].sidenum[1];
|
||
|
seg.frontsector = Level.Sides[seg.sidedef].sector;
|
||
|
seg.backsector = backside != NO_INDEX ? Level.Sides[backside].sector : -1;
|
||
|
seg.v1 = Level.Lines[i].v1;
|
||
|
seg.v2 = Level.Lines[i].v2;
|
||
|
seg.nextforvert = Vertices[seg.v1].segs;
|
||
|
seg.angle = PointToAngle (Vertices[seg.v2].x-Vertices[seg.v1].x,
|
||
|
Vertices[seg.v2].y-Vertices[seg.v1].y);
|
||
|
j = (int)Segs.Push (seg);
|
||
|
Vertices[seg.v1].segs = j;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
printf ("Linedef %d does not have a front side.\n", i);
|
||
|
}
|
||
|
|
||
|
if (Level.Lines[i].sidenum[1] != NO_INDEX)
|
||
|
{
|
||
|
WORD backside;
|
||
|
|
||
|
seg.linedef = i;
|
||
|
seg.sidedef = Level.Lines[i].sidenum[1];
|
||
|
backside = Level.Lines[i].sidenum[0];
|
||
|
seg.frontsector = Level.Sides[seg.sidedef].sector;
|
||
|
seg.backsector = backside != NO_INDEX ? Level.Sides[backside].sector : -1;
|
||
|
seg.v1 = Level.Lines[i].v2;
|
||
|
seg.v2 = Level.Lines[i].v1;
|
||
|
seg.nextforvert = Vertices[seg.v1].segs;
|
||
|
seg.angle = PointToAngle (Vertices[seg.v2].x-Vertices[seg.v1].x,
|
||
|
Vertices[seg.v2].y-Vertices[seg.v1].y);
|
||
|
j = (int)Segs.Push (seg);
|
||
|
Vertices[seg.v1].segs = j;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::GroupSegPlanes ()
|
||
|
{
|
||
|
const int bucketbits = 12;
|
||
|
FPrivSeg *buckets[1<<bucketbits] = { 0 };
|
||
|
int i, planenum;
|
||
|
|
||
|
for (i = 0; i < (int)Segs.Size(); ++i)
|
||
|
{
|
||
|
FPrivSeg *seg = &Segs[i];
|
||
|
seg->next = i+1;
|
||
|
seg->hashnext = NULL;
|
||
|
}
|
||
|
|
||
|
Segs[Segs.Size()-1].next = NO_INDEX;
|
||
|
|
||
|
for (i = planenum = 0; i < (int)Segs.Size(); ++i)
|
||
|
{
|
||
|
FPrivSeg *seg = &Segs[i];
|
||
|
fixed_t x1 = Vertices[seg->v1].x;
|
||
|
fixed_t y1 = Vertices[seg->v1].y;
|
||
|
fixed_t x2 = Vertices[seg->v2].x;
|
||
|
fixed_t y2 = Vertices[seg->v2].y;
|
||
|
angle_t ang = PointToAngle (x2 - x1, y2 - y1);
|
||
|
|
||
|
if (ang >= 1<<31)
|
||
|
ang += 1<<31;
|
||
|
|
||
|
FPrivSeg *check = buckets[ang >>= 31-bucketbits];
|
||
|
|
||
|
while (check != NULL)
|
||
|
{
|
||
|
fixed_t cx1 = Vertices[check->v1].x;
|
||
|
fixed_t cy1 = Vertices[check->v1].y;
|
||
|
fixed_t cdx = Vertices[check->v2].x - cx1;
|
||
|
fixed_t cdy = Vertices[check->v2].y - cy1;
|
||
|
if (PointOnSide (x1, y1, cx1, cy1, cdx, cdy) == 0 &&
|
||
|
PointOnSide (x2, y2, cx1, cy1, cdx, cdy) == 0)
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
check = check->hashnext;
|
||
|
}
|
||
|
if (check != NULL)
|
||
|
{
|
||
|
seg->planenum = check->planenum;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
seg->hashnext = buckets[ang];
|
||
|
buckets[ang] = seg;
|
||
|
seg->planenum = planenum++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
D(Printf ("%d planes from %d segs\n", planenum, Segs.Size()));
|
||
|
|
||
|
planenum = (planenum+7)/8;
|
||
|
PlaneChecked.Reserve (planenum);
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::FindPolyContainers (TArray<FPolyStart> &spots, TArray<FPolyStart> &anchors)
|
||
|
{
|
||
|
int loop = 1;
|
||
|
|
||
|
for (size_t i = 0; i < spots.Size(); ++i)
|
||
|
{
|
||
|
FPolyStart *spot = &spots[i];
|
||
|
fixed_t bbox[4];
|
||
|
|
||
|
if (GetPolyExtents (spot->polynum, bbox))
|
||
|
{
|
||
|
FPolyStart *anchor;
|
||
|
|
||
|
size_t j;
|
||
|
|
||
|
for (j = 0; j < anchors.Size(); ++j)
|
||
|
{
|
||
|
anchor = &anchors[j];
|
||
|
if (anchor->polynum == spot->polynum)
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (j < anchors.Size())
|
||
|
{
|
||
|
vertex_t mid;
|
||
|
vertex_t center;
|
||
|
|
||
|
mid.x = bbox[BOXLEFT] + (bbox[BOXRIGHT]-bbox[BOXLEFT])/2;
|
||
|
mid.y = bbox[BOXBOTTOM] + (bbox[BOXTOP]-bbox[BOXBOTTOM])/2;
|
||
|
|
||
|
center.x = mid.x - anchor->x + spot->x;
|
||
|
center.y = mid.y - anchor->y + spot->y;
|
||
|
|
||
|
// Scan right for the seg closest to the polyobject's center after it
|
||
|
// gets moved to its start spot.
|
||
|
fixed_t closestdist = FIXED_MAX;
|
||
|
long closestseg = 0;
|
||
|
|
||
|
P(Printf ("start %d,%d -- center %d, %d\n", spot->x>>16, spot->y>>16, center.x>>16, center.y>>16));
|
||
|
|
||
|
for (size_t j = 0; j < Segs.Size(); ++j)
|
||
|
{
|
||
|
FPrivSeg *seg = &Segs[j];
|
||
|
FPrivVert *v1 = &Vertices[seg->v1];
|
||
|
FPrivVert *v2 = &Vertices[seg->v2];
|
||
|
fixed_t dy = v2->y - v1->y;
|
||
|
|
||
|
if (dy == 0)
|
||
|
{ // Horizontal, so skip it
|
||
|
continue;
|
||
|
}
|
||
|
if ((v1->y < center.y && v2->y < center.y) || (v1->y > center.y && v2->y > center.y))
|
||
|
{ // Not crossed
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
fixed_t dx = v2->x - v1->x;
|
||
|
|
||
|
if (PointOnSide (center.x, center.y, v1->x, v1->y, dx, dy) <= 0)
|
||
|
{
|
||
|
fixed_t t = DivScale30 (center.y - v1->y, dy);
|
||
|
fixed_t sx = v1->x + MulScale30 (dx, t);
|
||
|
fixed_t dist = sx - spot->x;
|
||
|
|
||
|
if (dist < closestdist && dist >= 0)
|
||
|
{
|
||
|
closestdist = dist;
|
||
|
closestseg = (long)j;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (closestseg >= 0)
|
||
|
{
|
||
|
loop = MarkLoop (closestseg, loop);
|
||
|
P(Printf ("Found polyobj in sector %d (loop %d)\n", Segs[closestseg].frontsector,
|
||
|
Segs[closestseg].loopnum));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool FNodeBuilder::GetPolyExtents (int polynum, fixed_t bbox[4])
|
||
|
{
|
||
|
size_t i;
|
||
|
|
||
|
bbox[BOXLEFT] = bbox[BOXBOTTOM] = FIXED_MAX;
|
||
|
bbox[BOXRIGHT] = bbox[BOXTOP] = FIXED_MIN;
|
||
|
|
||
|
// Try to find a polyobj marked with a start line
|
||
|
for (i = 0; i < Segs.Size(); ++i)
|
||
|
{
|
||
|
if (Level.Lines[Segs[i].linedef].special == PO_LINE_START &&
|
||
|
Level.Lines[Segs[i].linedef].args[0] == polynum)
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (i < Segs.Size())
|
||
|
{
|
||
|
vertex_t start;
|
||
|
size_t vert;
|
||
|
|
||
|
vert = Segs[i].v1;
|
||
|
|
||
|
start.x = Vertices[vert].x;
|
||
|
start.y = Vertices[vert].y;
|
||
|
|
||
|
do
|
||
|
{
|
||
|
AddSegToBBox (bbox, &Segs[i]);
|
||
|
vert = Segs[i].v2;
|
||
|
i = Vertices[vert].segs;
|
||
|
} while (i != NO_INDEX && (Vertices[vert].x != start.x || Vertices[vert].y != start.y));
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Try to find a polyobj marked with explicit lines
|
||
|
bool found = false;
|
||
|
|
||
|
for (i = 0; i < Segs.Size(); ++i)
|
||
|
{
|
||
|
if (Level.Lines[Segs[i].linedef].special == PO_LINE_EXPLICIT &&
|
||
|
Level.Lines[Segs[i].linedef].args[0] == polynum)
|
||
|
{
|
||
|
AddSegToBBox (bbox, &Segs[i]);
|
||
|
found = true;
|
||
|
}
|
||
|
}
|
||
|
return found;
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::AddSegToBBox (fixed_t bbox[4], const FPrivSeg *seg)
|
||
|
{
|
||
|
FPrivVert *v1 = &Vertices[seg->v1];
|
||
|
FPrivVert *v2 = &Vertices[seg->v2];
|
||
|
|
||
|
if (v1->x < bbox[BOXLEFT]) bbox[BOXLEFT] = v1->x;
|
||
|
if (v1->x > bbox[BOXRIGHT]) bbox[BOXRIGHT] = v1->x;
|
||
|
if (v1->y < bbox[BOXBOTTOM]) bbox[BOXBOTTOM] = v1->y;
|
||
|
if (v1->y > bbox[BOXTOP]) bbox[BOXTOP] = v1->y;
|
||
|
|
||
|
if (v2->x < bbox[BOXLEFT]) bbox[BOXLEFT] = v2->x;
|
||
|
if (v2->x > bbox[BOXRIGHT]) bbox[BOXRIGHT] = v2->x;
|
||
|
if (v2->y < bbox[BOXBOTTOM]) bbox[BOXBOTTOM] = v2->y;
|
||
|
if (v2->y > bbox[BOXTOP]) bbox[BOXTOP] = v2->y;
|
||
|
}
|
||
|
|
||
|
int FNodeBuilder::MarkLoop (int firstseg, int loopnum)
|
||
|
{
|
||
|
int seg;
|
||
|
int sec = Segs[firstseg].frontsector;
|
||
|
|
||
|
if (Segs[firstseg].loopnum != 0)
|
||
|
{ // already marked
|
||
|
return loopnum;
|
||
|
}
|
||
|
|
||
|
seg = firstseg;
|
||
|
|
||
|
do
|
||
|
{
|
||
|
FPrivSeg *s1 = &Segs[seg];
|
||
|
|
||
|
s1->loopnum = loopnum;
|
||
|
|
||
|
P(Printf ("Mark seg %d (%d,%d)-(%d,%d)\n", seg,
|
||
|
Vertices[s1->v1].x>>16, Vertices[s1->v1].y>>16,
|
||
|
Vertices[s1->v2].x>>16, Vertices[s1->v2].y>>16));
|
||
|
|
||
|
int bestseg = NO_INDEX;
|
||
|
int tryseg = Vertices[s1->v2].segs;
|
||
|
angle_t bestang = ANGLE_MAX;
|
||
|
angle_t ang1 = s1->angle;
|
||
|
|
||
|
while (tryseg != NO_INDEX)
|
||
|
{
|
||
|
FPrivSeg *s2 = &Segs[tryseg];
|
||
|
|
||
|
if (s2->frontsector == sec)
|
||
|
{
|
||
|
angle_t ang2 = s2->angle + (1<<31);
|
||
|
angle_t angdiff = ang2 - ang1;
|
||
|
|
||
|
if (angdiff < bestang && angdiff > 0)
|
||
|
{
|
||
|
bestang = angdiff;
|
||
|
bestseg = tryseg;
|
||
|
}
|
||
|
}
|
||
|
tryseg = s2->nextforvert;
|
||
|
}
|
||
|
|
||
|
seg = bestseg;
|
||
|
} while (seg != NO_INDEX && Segs[seg].loopnum == 0);
|
||
|
|
||
|
return loopnum + 1;
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::BuildTree ()
|
||
|
{
|
||
|
fixed_t bbox[4];
|
||
|
|
||
|
printf (" BSP: 0.0%%\r");
|
||
|
CreateNode (0, bbox);
|
||
|
printf (" BSP: 100.0%%\n");
|
||
|
}
|
||
|
|
||
|
int FNodeBuilder::CreateNode (WORD set, fixed_t bbox[4])
|
||
|
{
|
||
|
node_t node;
|
||
|
int skip, count, selstat;
|
||
|
|
||
|
count = CountSegs (set);
|
||
|
skip = count / MaxSegs;
|
||
|
|
||
|
if ((selstat = SelectSplitter (set, node, skip, true)) > 0 ||
|
||
|
(skip > 0 && (selstat = SelectSplitter (set, node, 1, true)) > 0) ||
|
||
|
(selstat < 0 && (SelectSplitter (set, node, skip, false) > 0) ||
|
||
|
(skip > 0 && SelectSplitter (set, node, 1, false))) ||
|
||
|
CheckSubsector (set, node, count))
|
||
|
{
|
||
|
// Create a normal node
|
||
|
WORD set1, set2;
|
||
|
|
||
|
SplitSegs (set, node, set1, set2);
|
||
|
D(PrintSet (1, set1));
|
||
|
D(Printf ("(%d,%d) delta (%d,%d)\n", node.x>>16, node.y>>16, node.dx>>16, node.dy>>16));
|
||
|
D(PrintSet (2, set2));
|
||
|
node.children[0] = CreateNode (set1, node.bbox[0]);
|
||
|
node.children[1] = CreateNode (set2, node.bbox[1]);
|
||
|
bbox[BOXTOP] = MAX (node.bbox[0][BOXTOP], node.bbox[1][BOXTOP]);
|
||
|
bbox[BOXBOTTOM] = MIN (node.bbox[0][BOXBOTTOM], node.bbox[1][BOXBOTTOM]);
|
||
|
bbox[BOXLEFT] = MIN (node.bbox[0][BOXLEFT], node.bbox[1][BOXLEFT]);
|
||
|
bbox[BOXRIGHT] = MAX (node.bbox[0][BOXRIGHT], node.bbox[1][BOXRIGHT]);
|
||
|
return (int)Nodes.Push (node);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return NF_SUBSECTOR | CreateSubsector (set, bbox);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
int FNodeBuilder::CreateSubsector (WORD set, fixed_t bbox[4])
|
||
|
{
|
||
|
MapSubsector sub;
|
||
|
|
||
|
bbox[BOXTOP] = bbox[BOXRIGHT] = INT_MIN;
|
||
|
bbox[BOXBOTTOM] = bbox[BOXLEFT] = INT_MAX;
|
||
|
|
||
|
D(Printf ("Subsector from set %d\n", set));
|
||
|
|
||
|
assert (set != NO_INDEX);
|
||
|
|
||
|
sub.firstline = (WORD)SegList.Size();
|
||
|
|
||
|
while (set != NO_INDEX)
|
||
|
{
|
||
|
USegPtr ptr;
|
||
|
|
||
|
ptr.SegPtr = &Segs[set];
|
||
|
AddSegToBBox (bbox, ptr.SegPtr);
|
||
|
SegList.Push (ptr);
|
||
|
set = ptr.SegPtr->next;
|
||
|
}
|
||
|
sub.numlines = (WORD)(SegList.Size() - sub.firstline);
|
||
|
|
||
|
// Sort segs by linedef for special effects
|
||
|
qsort (&SegList[sub.firstline], sub.numlines, sizeof(int), SortSegs);
|
||
|
|
||
|
// Convert seg pointers into indices
|
||
|
for (size_t i = sub.firstline; i < SegList.Size(); ++i)
|
||
|
{
|
||
|
SegList[i].SegNum = SegList[i].SegPtr - &Segs[0];
|
||
|
}
|
||
|
|
||
|
SegsStuffed += sub.numlines;
|
||
|
if ((SegsStuffed & ~63) != ((SegsStuffed - sub.numlines) & ~63))
|
||
|
{
|
||
|
int percent = (int)(SegsStuffed * 1000.0 / Segs.Size());
|
||
|
printf (" BSP: %3d.%d%%\r", percent/10, percent%10);
|
||
|
}
|
||
|
|
||
|
D(Printf ("bbox (%d,%d)-(%d,%d)\n", bbox[BOXLEFT]>>16, bbox[BOXBOTTOM]>>16, bbox[BOXRIGHT]>>16, bbox[BOXTOP]>>16));
|
||
|
|
||
|
return (int)Subsectors.Push (sub);
|
||
|
}
|
||
|
|
||
|
int STACK_ARGS FNodeBuilder::SortSegs (const void *a, const void *b)
|
||
|
{
|
||
|
const FPrivSeg *x = ((const USegPtr *)a)->SegPtr;
|
||
|
const FPrivSeg *y = ((const USegPtr *)b)->SegPtr;
|
||
|
|
||
|
// Segs with the same sector on the back and front belong at the end of the list.
|
||
|
// This is so that the subsector does not inherit its sector from them.
|
||
|
|
||
|
if (x->frontsector == x->backsector && y->frontsector != y->backsector)
|
||
|
{
|
||
|
return 1;
|
||
|
}
|
||
|
else if (y->frontsector == y->backsector && x->frontsector != x->backsector)
|
||
|
{
|
||
|
return -1;
|
||
|
}
|
||
|
else if (x->linedef == y->linedef)
|
||
|
{
|
||
|
return x - y;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return x->linedef - y->linedef;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
int FNodeBuilder::CountSegs (WORD set) const
|
||
|
{
|
||
|
int count = 0;
|
||
|
|
||
|
while (set != NO_INDEX)
|
||
|
{
|
||
|
count++;
|
||
|
set = Segs[set].next;
|
||
|
}
|
||
|
return count;
|
||
|
}
|
||
|
|
||
|
// Given a set of segs, checks to make sure they all belong to a single
|
||
|
// sector. If so, false is returned, and they become a subsector. If not,
|
||
|
// a splitter is synthesized, and true is returned to continue processing
|
||
|
// down this branch of the tree.
|
||
|
|
||
|
bool FNodeBuilder::CheckSubsector (WORD set, node_t &node, int setsize)
|
||
|
{
|
||
|
int sec;
|
||
|
int seg;
|
||
|
|
||
|
sec = -1;
|
||
|
seg = set;
|
||
|
|
||
|
do
|
||
|
{
|
||
|
if (Segs[seg].frontsector != sec&&
|
||
|
// Segs with the same front and back sectors are allowed to reside
|
||
|
// in a subsector with segs from a different sector, because the
|
||
|
// only effect they can have on the display is to place masked
|
||
|
// mid textures.
|
||
|
Segs[seg].frontsector != Segs[seg].backsector)
|
||
|
{
|
||
|
if (sec == -1)
|
||
|
{
|
||
|
sec = Segs[seg].frontsector;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
seg = Segs[seg].next;
|
||
|
} while (seg != NO_INDEX);
|
||
|
|
||
|
if (seg == NO_INDEX)
|
||
|
{ // It's a valid subsector
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
D(Printf("Need to synthesize a splitter for set %d\n", set));
|
||
|
|
||
|
// If there are only two segs in the set, and they form two sides
|
||
|
// of a triangle, the splitter should pass through their shared
|
||
|
// point and the (imaginary) third side of the triangle
|
||
|
if (setsize == 2)
|
||
|
{
|
||
|
FPrivVert *v1, *v2, *v3;
|
||
|
|
||
|
if (Vertices[Segs[set].v2] == Vertices[Segs[seg].v1])
|
||
|
{
|
||
|
v1 = &Vertices[Segs[set].v1];
|
||
|
v2 = &Vertices[Segs[seg].v2];
|
||
|
v3 = &Vertices[Segs[set].v2];
|
||
|
}
|
||
|
else if (Vertices[Segs[set].v1] == Vertices[Segs[seg].v2])
|
||
|
{
|
||
|
v1 = &Vertices[Segs[seg].v1];
|
||
|
v2 = &Vertices[Segs[set].v2];
|
||
|
v3 = &Vertices[Segs[seg].v2];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
v1 = v2 = v3 = NULL;
|
||
|
}
|
||
|
if (v1 != NULL)
|
||
|
{
|
||
|
node.x = v3->x;
|
||
|
node.y = v3->y;
|
||
|
node.dx = v1->x + (v2->x-v1->x)/2 - node.x;
|
||
|
node.dy = v1->y + (v2->y-v1->y)/2 - node.y;
|
||
|
return Heuristic (node, set, false) != 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool nosplit = true;
|
||
|
int firsthit = seg;
|
||
|
|
||
|
do
|
||
|
{
|
||
|
seg = firsthit;
|
||
|
do
|
||
|
{
|
||
|
if (Segs[seg].frontsector != sec)
|
||
|
{
|
||
|
node.x = Vertices[Segs[set].v1].x;
|
||
|
node.y = Vertices[Segs[set].v1].y;
|
||
|
node.dx = Vertices[Segs[seg].v2].x - node.x;
|
||
|
node.dy = Vertices[Segs[seg].v2].y - node.y;
|
||
|
|
||
|
if (Heuristic (node, set, nosplit) != 0)
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
node.dx = Vertices[Segs[seg].v1].x - node.x;
|
||
|
node.dy = Vertices[Segs[seg].v1].y - node.y;
|
||
|
|
||
|
if (Heuristic (node, set, nosplit) != 0)
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
seg = Segs[seg].next;
|
||
|
} while (seg != NO_INDEX);
|
||
|
} while ((nosplit ^= 1) == 0);
|
||
|
|
||
|
// Give up.
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Splitters are chosen to coincide with segs in the given set. To reduce the
|
||
|
// number of segs that need to be considered as splitters, segs are grouped into
|
||
|
// according to the planes that they lie on. Because one seg on the plane is just
|
||
|
// as good as any other seg on the plane at defining a split, only one seg from
|
||
|
// each unique plane needs to be considered as a splitter. A result of 0 means
|
||
|
// this set is a convex region. A result of -1 means that there were possible
|
||
|
// splitters, but they all split segs we want to keep intact.
|
||
|
int FNodeBuilder::SelectSplitter (WORD set, node_t &node, int step, bool nosplit)
|
||
|
{
|
||
|
int stepleft;
|
||
|
int bestvalue;
|
||
|
WORD bestseg;
|
||
|
WORD seg;
|
||
|
bool nosplitters = false;
|
||
|
|
||
|
bestvalue = 0;
|
||
|
bestseg = NO_INDEX;
|
||
|
|
||
|
seg = set;
|
||
|
stepleft = 0;
|
||
|
|
||
|
memset (&PlaneChecked[0], 0, PlaneChecked.Size());
|
||
|
|
||
|
while (seg != NO_INDEX)
|
||
|
{
|
||
|
FPrivSeg *pseg = &Segs[seg];
|
||
|
|
||
|
if (--stepleft <= 0)
|
||
|
{
|
||
|
int l = pseg->planenum >> 3;
|
||
|
int r = 1 << (pseg->planenum & 7);
|
||
|
|
||
|
if ((PlaneChecked[l] & r) == 0)
|
||
|
{
|
||
|
PlaneChecked[l] |= r;
|
||
|
|
||
|
stepleft = step;
|
||
|
node.x = Vertices[pseg->v1].x;
|
||
|
node.y = Vertices[pseg->v1].y;
|
||
|
node.dx = Vertices[pseg->v2].x - node.x;
|
||
|
node.dy = Vertices[pseg->v2].y - node.y;
|
||
|
|
||
|
int value = Heuristic (node, set, nosplit);
|
||
|
|
||
|
D(Printf ("Seg %d (%4d,%4d)-(%4d,%4d) scores %d\n", seg, node.x>>16, node.y>>16,
|
||
|
(node.x+node.dx)>>16, (node.y+node.dy)>>16, value));
|
||
|
|
||
|
if (value > bestvalue)
|
||
|
{
|
||
|
bestvalue = value;
|
||
|
bestseg = seg;
|
||
|
}
|
||
|
else if (value < 0)
|
||
|
{
|
||
|
nosplitters = true;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
pseg = pseg;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
seg = pseg->next;
|
||
|
}
|
||
|
|
||
|
if (bestseg == NO_INDEX)
|
||
|
{ // No lines split any others into two sets, so this is a convex region.
|
||
|
D(Printf ("set %d, step %d, nosplit %d has no good splitter (%d)\n", set, step, nosplit, nosplitters));
|
||
|
return nosplitters ? -1 : 0;
|
||
|
}
|
||
|
|
||
|
D(Printf ("split seg %d in set %d, score %d, step %d, nosplit %d\n", bestseg, set, bestvalue, step, nosplit));
|
||
|
|
||
|
node.x = Vertices[Segs[bestseg].v1].x;
|
||
|
node.y = Vertices[Segs[bestseg].v1].y;
|
||
|
node.dx = Vertices[Segs[bestseg].v2].x - node.x;
|
||
|
node.dy = Vertices[Segs[bestseg].v2].y - node.y;
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
// Given a splitter (node), returns a score based on how "good" the resulting
|
||
|
// split in a set of segs is. Higher scores are better. -1 means this splitter
|
||
|
// splits something it shouldn't and will only be returned if honorNoSplit is
|
||
|
// true. A score of 0 means that the splitter does not split any of the segs
|
||
|
// in the set.
|
||
|
|
||
|
int FNodeBuilder::Heuristic (node_t &node, WORD set, bool honorNoSplit)
|
||
|
{
|
||
|
int score = 0;
|
||
|
int segsInSet = 0;
|
||
|
int counts[2] = { 0, 0 };
|
||
|
WORD i = set;
|
||
|
int sidev1, sidev2;
|
||
|
int side;
|
||
|
bool splitter = false;
|
||
|
size_t max, m2, p, q;
|
||
|
|
||
|
Touched.Clear ();
|
||
|
Colinear.Clear ();
|
||
|
|
||
|
while (i != NO_INDEX)
|
||
|
{
|
||
|
const FPrivSeg *test = &Segs[i];
|
||
|
|
||
|
switch ((side = ClassifyLine (node, test, sidev1, sidev2)))
|
||
|
{
|
||
|
case 0: // Seg is on only one side of the partition
|
||
|
case 1:
|
||
|
// If we don't split this line, but it abuts the splitter, also reject it.
|
||
|
// The "right" thing to do in this case is to only reject it if there is
|
||
|
// another nosplit seg from the same sector at this vertex. Note that a line
|
||
|
// that lies exactly on top of the splitter is okay.
|
||
|
if (test->loopnum && honorNoSplit && (sidev1 == 0 || sidev2 == 0))
|
||
|
{
|
||
|
if ((sidev1 | sidev2) != 0)
|
||
|
{
|
||
|
max = Touched.Size();
|
||
|
for (p = 0; p < max; ++p)
|
||
|
{
|
||
|
if (Touched[p] == test->loopnum)
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (p == max)
|
||
|
{
|
||
|
Touched.Push (test->loopnum);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
max = Colinear.Size();
|
||
|
for (p = 0; p < max; ++p)
|
||
|
{
|
||
|
if (Colinear[p] == test->loopnum)
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (p == max)
|
||
|
{
|
||
|
Colinear.Push (test->loopnum);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
counts[side]++;
|
||
|
score += SplitCost; // Add some weight to the score for unsplit lines
|
||
|
break;
|
||
|
|
||
|
default: // Seg is cut by the partition
|
||
|
// If we are not allowed to split this seg, reject this splitter
|
||
|
if (test->loopnum)
|
||
|
{
|
||
|
if (honorNoSplit)
|
||
|
{
|
||
|
D(Printf ("Splits seg %d\n", i));
|
||
|
return -1;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
splitter = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
counts[0]++;
|
||
|
counts[1]++;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
segsInSet++;
|
||
|
i = test->next;
|
||
|
}
|
||
|
|
||
|
// If this line is outside all the others, return a special score
|
||
|
if (counts[0] == 0 || counts[1] == 0)
|
||
|
{
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// If this splitter intersects any vertices of segs that should not be split,
|
||
|
// check if it is also colinear with another seg from the same sector. If it
|
||
|
// is, the splitter is okay. If not, it should be rejected. Why? Assuming that
|
||
|
// polyobject containers are convex (which they should be), a splitter that
|
||
|
// is colinear with one of the sector's segs and crosses the vertex of another
|
||
|
// seg of that sector must be crossing the container's corner and does not
|
||
|
// actually split the container.
|
||
|
|
||
|
max = Touched.Size ();
|
||
|
m2 = Colinear.Size ();
|
||
|
|
||
|
// If honorNoSplit is false, then both these lists will be empty.
|
||
|
|
||
|
// If the splitter touches some vertices without being colinear to any, we
|
||
|
// can skip further checks and reject this right away.
|
||
|
if (m2 == 0 && max > 0)
|
||
|
{
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
for (p = 0; p < max; ++p)
|
||
|
{
|
||
|
int look = Touched[p];
|
||
|
for (q = 0; q < m2; ++q)
|
||
|
{
|
||
|
if (look == Colinear[q])
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (q == m2)
|
||
|
{ // Not a good one
|
||
|
return -1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Doom maps are primarily axis-aligned lines, so it's usually a good
|
||
|
// idea to prefer axis-aligned splitters over diagonal ones. Doom originally
|
||
|
// had special-casing for orthogonal lines, so they performed better. ZDoom
|
||
|
// does not care about the line's direction, so this is merely a choice to
|
||
|
// try and improve the final tree.
|
||
|
|
||
|
if ((node.dx == 0) || (node.dy == 0))
|
||
|
{
|
||
|
// If we have to split a seg we would prefer to keep unsplit, give
|
||
|
// extra precedence to orthogonal lines so that the polyobjects
|
||
|
// outside the entrance to MAP06 in Hexen MAP02 display properly.
|
||
|
if (splitter)
|
||
|
{
|
||
|
score += segsInSet*8;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
score += segsInSet/AAPreference;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
score += (counts[0] + counts[1]) - abs(counts[0] - counts[1]);
|
||
|
|
||
|
return score;
|
||
|
}
|
||
|
|
||
|
int FNodeBuilder::ClassifyLine (node_t &node, const FPrivSeg *seg, int &sidev1, int &sidev2)
|
||
|
{
|
||
|
const FPrivVert *v1 = &Vertices[seg->v1];
|
||
|
const FPrivVert *v2 = &Vertices[seg->v2];
|
||
|
sidev1 = PointOnSide (v1->x, v1->y, node.x, node.y, node.dx, node.dy);
|
||
|
sidev2 = PointOnSide (v2->x, v2->y, node.x, node.y, node.dx, node.dy);
|
||
|
|
||
|
if ((sidev1 | sidev2) == 0)
|
||
|
{ // seg is coplanar with the splitter, so use its orientation to determine
|
||
|
// which child it ends up in. If it faces the same direction as the splitter,
|
||
|
// it goes in front. Otherwise, it goes in back.
|
||
|
|
||
|
if (node.dx != 0)
|
||
|
{
|
||
|
if ((node.dx > 0 && v2->x > v1->x) || (node.dx < 0 && v2->x < v1->x))
|
||
|
{
|
||
|
return 0;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return 1;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if ((node.dy > 0 && v2->y > v1->y) || (node.dy < 0 && v2->y < v1->y))
|
||
|
{
|
||
|
return 0;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return 1;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (sidev1 <= 0 && sidev2 <= 0)
|
||
|
{
|
||
|
return 0;
|
||
|
}
|
||
|
else if (sidev1 >= 0 && sidev2 >= 0)
|
||
|
{
|
||
|
return 1;
|
||
|
}
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::SplitSegs (WORD set, node_t &node, WORD &outset0, WORD &outset1)
|
||
|
{
|
||
|
fixed_t dx, dy;
|
||
|
|
||
|
outset0 = NO_INDEX;
|
||
|
outset1 = NO_INDEX;
|
||
|
|
||
|
while (set != NO_INDEX)
|
||
|
{
|
||
|
FPrivSeg *seg = &Segs[set];
|
||
|
int next = seg->next;
|
||
|
|
||
|
int sidev1, sidev2, side;
|
||
|
|
||
|
side = ClassifyLine (node, seg, sidev1, sidev2);
|
||
|
|
||
|
switch (side)
|
||
|
{
|
||
|
case 0:
|
||
|
seg->next = outset0;
|
||
|
outset0 = set;
|
||
|
break;
|
||
|
|
||
|
case 1:
|
||
|
seg->next = outset1;
|
||
|
outset1 = set;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
fixed_t frac;
|
||
|
FPrivVert newvert;
|
||
|
FPrivSeg newseg;
|
||
|
size_t vertnum;
|
||
|
int seg2;
|
||
|
|
||
|
if (seg->loopnum)
|
||
|
{
|
||
|
Printf (" Split seg %d (%d,%d)-(%d,%d) of sector %d in loop %d\n",
|
||
|
set,
|
||
|
Vertices[seg->v1].x>>16, Vertices[seg->v1].y>>16,
|
||
|
Vertices[seg->v2].x>>16, Vertices[seg->v2].y>>16,
|
||
|
seg->frontsector, seg->loopnum);
|
||
|
}
|
||
|
|
||
|
frac = InterceptVector (node, *seg);
|
||
|
newvert.x = Vertices[seg->v1].x;
|
||
|
newvert.y = Vertices[seg->v1].y;
|
||
|
newvert.x = newvert.x + MulScale30 (frac, dx = Vertices[seg->v2].x - newvert.x);
|
||
|
newvert.y = newvert.y + MulScale30 (frac, dy = Vertices[seg->v2].y - newvert.y);
|
||
|
vertnum = Vertices.Push (newvert);
|
||
|
|
||
|
newseg = *seg;
|
||
|
newseg.offset += fixed_t (sqrt (double(dx)*double(dx)+double(dy)*double(dy)));
|
||
|
if (sidev1 > 0)
|
||
|
{
|
||
|
newseg.v1 = vertnum;
|
||
|
seg->v2 = vertnum;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
seg->v1 = vertnum;
|
||
|
newseg.v2 = vertnum;
|
||
|
}
|
||
|
|
||
|
seg2 = (int)Segs.Push (newseg);
|
||
|
|
||
|
Segs[seg2].next = outset0;
|
||
|
outset0 = seg2;
|
||
|
Segs[set].next = outset1;
|
||
|
outset1 = set;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
set = next;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fixed_t FNodeBuilder::InterceptVector (const node_t &splitter, const FPrivSeg &seg)
|
||
|
{
|
||
|
double v2x = (double)Vertices[seg.v1].x;
|
||
|
double v2y = (double)Vertices[seg.v1].y;
|
||
|
double v2dx = (double)Vertices[seg.v2].x - v2x;
|
||
|
double v2dy = (double)Vertices[seg.v2].y - v2y;
|
||
|
double v1dx = (double)splitter.dx;
|
||
|
double v1dy = (double)splitter.dy;
|
||
|
|
||
|
double den = v1dy*v2dx - v1dx*v2dy;
|
||
|
|
||
|
if (den == 0.0)
|
||
|
return 0; // parallel
|
||
|
|
||
|
double v1x = (double)splitter.x;
|
||
|
double v1y = (double)splitter.y;
|
||
|
|
||
|
double num = (v1x - v2x)*v1dy + (v2y - v1y)*v1dx;
|
||
|
double frac = num / den;
|
||
|
|
||
|
return (fixed_t)(1073741824.0*frac);
|
||
|
}
|
||
|
|
||
|
inline int FNodeBuilder::PointOnSide (int x, int y, int x1, int y1, int dx, int dy)
|
||
|
{
|
||
|
int foo = DMulScale32 (y-y1, dx, x1-x, dy);
|
||
|
return abs(foo) < 4 ? 0 : foo;
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::PrintSet (int l, WORD set)
|
||
|
{
|
||
|
Printf ("set %d: ", l);
|
||
|
for (; set != NO_INDEX; set = Segs[set].next)
|
||
|
{
|
||
|
Printf ("%d(%d:%ld,%ld-%ld,%ld) ", set, Segs[set].frontsector,
|
||
|
Vertices[Segs[set].v1].x>>16, Vertices[Segs[set].v1].y>>16,
|
||
|
Vertices[Segs[set].v2].x>>16, Vertices[Segs[set].v2].y>>16);
|
||
|
}
|
||
|
Printf ("*\n");
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::GetVertices (WideVertex *&verts, int &count)
|
||
|
{
|
||
|
count = Vertices.Size ();
|
||
|
verts = new WideVertex[count];
|
||
|
|
||
|
for (int i = 0; i < count; ++i)
|
||
|
{
|
||
|
verts[i].x = Vertices[i].x;
|
||
|
verts[i].y = Vertices[i].y;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::GetSegs (MapSeg *&segs, int &count)
|
||
|
{
|
||
|
count = SegList.Size ();
|
||
|
segs = new MapSeg[count];
|
||
|
|
||
|
for (int i = 0; i < count; ++i)
|
||
|
{
|
||
|
const FPrivSeg *org = &Segs[SegList[i].SegNum];
|
||
|
|
||
|
segs[i].v1 = org->v1;
|
||
|
segs[i].v2 = org->v2;
|
||
|
segs[i].angle = short(org->angle >> 16);
|
||
|
segs[i].linedef = org->linedef;
|
||
|
segs[i].side = Level.Lines[org->linedef].sidenum[1] == org->sidedef ? 1 : 0;
|
||
|
segs[i].offset = org->offset >> 16;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::GetSubsectors (MapSubsector *&ssecs, int &count)
|
||
|
{
|
||
|
count = Subsectors.Size ();
|
||
|
ssecs = new MapSubsector[count];
|
||
|
|
||
|
for (int i = 0; i < count; ++i)
|
||
|
{
|
||
|
ssecs[i].numlines = Subsectors[i].numlines;
|
||
|
ssecs[i].firstline = Subsectors[i].firstline;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void FNodeBuilder::GetNodes (MapNode *&nodes, int &count)
|
||
|
{
|
||
|
count = Nodes.Size ();
|
||
|
nodes = new MapNode[count];
|
||
|
|
||
|
for (int i = 0; i < count; ++i)
|
||
|
{
|
||
|
nodes[i].x = Nodes[i].x >> 16;
|
||
|
nodes[i].y = Nodes[i].y >> 16;
|
||
|
nodes[i].dx = Nodes[i].dx >> 16;
|
||
|
nodes[i].dy = Nodes[i].dy >> 16;
|
||
|
for (int j = 0; j < 2; ++j)
|
||
|
{
|
||
|
for (int k = 0; k < 4; ++k)
|
||
|
{
|
||
|
nodes[i].bbox[j][k] = Nodes[i].bbox[j][k] >> 16;
|
||
|
}
|
||
|
nodes[i].children[j] = Nodes[i].children[j];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
#include "z_zone.h"
|
||
|
|
||
|
void FNodeBuilder::Create (node_t *&nodes, int &numnodes,
|
||
|
seg_t *&segs, int &numsegs,
|
||
|
subsector_t *&subsectors, int &numsubsectors,
|
||
|
vertex_t *&vertices, int &numvertices)
|
||
|
{
|
||
|
int i;
|
||
|
|
||
|
numnodes = (int)Nodes.Size();
|
||
|
nodes = (node_t *)Z_Malloc (numnodes*sizeof(node_t), PU_LEVEL, 0);
|
||
|
numsegs = (int)SegList.Size();
|
||
|
segs = (seg_t *)Z_Malloc (numsegs*sizeof(seg_t), PU_LEVEL, 0);
|
||
|
numsubsectors = (int)Subsectors.Size();
|
||
|
subsectors = (subsector_t *)Z_Malloc (numsubsectors*sizeof(subsector_t), PU_LEVEL, 0);
|
||
|
numvertices = (int)Vertices.Size();
|
||
|
vertices = (vertex_t *)Z_Malloc (numvertices*sizeof(vertex_t), PU_LEVEL, 0);
|
||
|
|
||
|
memcpy (nodes, &Nodes[0], numnodes*sizeof(node_t));
|
||
|
memcpy (subsectors, &Subsectors[0], numsubsectors*sizeof(subsector_t));
|
||
|
|
||
|
for (i = 0; i < numvertices; ++i)
|
||
|
{
|
||
|
vertices[i].x = Vertices[i].x;
|
||
|
vertices[i].y = Vertices[i].y;
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < numsegs; ++i)
|
||
|
{
|
||
|
FPrivSeg *org = &Segs[SegList[i].SegNum];
|
||
|
seg_t *out = &segs[i];
|
||
|
|
||
|
out->backsector = org->backsector;
|
||
|
out->frontsector = org->frontsector;
|
||
|
out->linedef = org->linedef;
|
||
|
out->sidedef = org->sidedef;
|
||
|
out->v1 = vertices + org->v1;
|
||
|
out->v2 = vertices + org->v2;
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < Level.NumLines; ++i)
|
||
|
{
|
||
|
Level.Lines[i].v1 = vertices + (size_t)Level.Lines[i].v1;
|
||
|
Level.Lines[i].v2 = vertices + (size_t)Level.Lines[i].v2;
|
||
|
}
|
||
|
}
|
||
|
*/
|