mirror of
https://github.com/jmarshall23/DoomRTX.git
synced 2026-08-12 16:21:04 +02:00
2466 lines
77 KiB
C++
2466 lines
77 KiB
C++
#include "precompiled.h"
|
|
#pragma hdrstop
|
|
|
|
#include "qe3.h"
|
|
#include "Radiant.h"
|
|
|
|
#define DUKE_BUILD_IMPORTER_PATCH_VERSION "V5 real-face-planepts hole-bridged 2026-04-26"
|
|
|
|
// These are present in the classic Doom 3 Radiant/DoomEdit tree.
|
|
// In some forks you may need to include the exact headers instead of externs.
|
|
extern brush_t active_brushes;
|
|
extern brush_t selected_brushes;
|
|
extern entity_t* world_entity;
|
|
|
|
extern brush_t* Brush_Alloc(void);
|
|
extern face_t* Face_Alloc(void);
|
|
extern void Brush_AddToList(brush_t* b, brush_t* list);
|
|
extern void Entity_LinkBrush(entity_t* e, brush_t* b);
|
|
extern void Brush_Build(brush_t* b, bool bSnap, bool bMarkMap, bool bConvert, bool updateLights);
|
|
extern void Sys_UpdateWindows(int nBits);
|
|
extern const idMaterial* Texture_ForName(const char* name);
|
|
|
|
#ifndef W_ALL
|
|
#define W_ALL 0xFFFFFFFF
|
|
#endif
|
|
|
|
class idDukeBuildMapImporter {
|
|
public:
|
|
struct Options {
|
|
Options() {
|
|
mapScale = 1.0f; // Build XY units -> Doom units
|
|
zScale = 1.0f / 16.0f; // Build z is much finer
|
|
flipY = true; // Build Y -> Doom -Y
|
|
zOffset = 0.0f;
|
|
|
|
wallThickness = 8.0f;
|
|
floorCeilingThickness = 8.0f;
|
|
|
|
importFloors = true;
|
|
importCeilings = true;
|
|
importWalls = true;
|
|
importMaskedWalls = false; // masked portal walls become solid if true
|
|
|
|
defaultTileWidth = 64;
|
|
defaultTileHeight = 64;
|
|
|
|
// Build repeat tuning.
|
|
// These give sensible Duke-style results and keep repeat/pan behavior visible.
|
|
// If your imported maps look uniformly too stretched or too dense, tune these.
|
|
wallRepeatBase = 8.0f;
|
|
flatRepeatBase = 16.0f;
|
|
|
|
caulkMaterial = "textures/common/caulk";
|
|
tileMaterialPrefix = "textures/duke3d/tiles";
|
|
}
|
|
|
|
float mapScale;
|
|
float zScale;
|
|
bool flipY;
|
|
float zOffset;
|
|
|
|
float wallThickness;
|
|
float floorCeilingThickness;
|
|
|
|
bool importFloors;
|
|
bool importCeilings;
|
|
bool importWalls;
|
|
bool importMaskedWalls;
|
|
|
|
int defaultTileWidth;
|
|
int defaultTileHeight;
|
|
|
|
float wallRepeatBase;
|
|
float flatRepeatBase;
|
|
|
|
idStr caulkMaterial;
|
|
idStr tileMaterialPrefix;
|
|
};
|
|
|
|
public:
|
|
idDukeBuildMapImporter() {
|
|
lastError.Clear();
|
|
}
|
|
|
|
const char* GetLastError() const {
|
|
return lastError.c_str();
|
|
}
|
|
|
|
bool ImportIntoCurrentRadiantMap(const char* buildMapFileName, const Options& opt = Options()) {
|
|
options = opt;
|
|
lastError.Clear();
|
|
|
|
if (world_entity == NULL) {
|
|
lastError = "No world entity exists in the current Radiant map.";
|
|
return false;
|
|
}
|
|
|
|
if (!LoadBuildMap(buildMapFileName)) {
|
|
return false;
|
|
}
|
|
|
|
ResetRunStats();
|
|
PrintPatchVerificationStats(buildMapFileName);
|
|
|
|
int madeBrushes = 0;
|
|
|
|
for (int s = 0; s < sectors.Num(); s++) {
|
|
BuildSector& sec = sectors[s];
|
|
|
|
if (sec.wallptr < 0 || sec.wallnum < 3 || sec.wallptr + sec.wallnum > walls.Num()) {
|
|
continue;
|
|
}
|
|
|
|
if (options.importWalls) {
|
|
madeBrushes += ImportSectorWalls(s);
|
|
}
|
|
|
|
if (options.importFloors || options.importCeilings) {
|
|
madeBrushes += ImportSectorFlats(s);
|
|
}
|
|
}
|
|
|
|
Sys_UpdateWindows(W_ALL);
|
|
|
|
if (madeBrushes <= 0) {
|
|
lastError = "MAP loaded, but no brushes were generated.";
|
|
return false;
|
|
}
|
|
|
|
common->Printf(
|
|
"DukeBuildMapImporter %s: imported %d brushes from '%s' rejectedBrushes=%d rejectedWallSpans=%d rejectedFlatTris=%d rejectedDegenerateSides=%d\n",
|
|
DUKE_BUILD_IMPORTER_PATCH_VERSION,
|
|
madeBrushes,
|
|
buildMapFileName,
|
|
runStats.rejectedBrushes,
|
|
runStats.rejectedWallSpans,
|
|
runStats.rejectedFlatTris,
|
|
runStats.rejectedDegenerateSides
|
|
);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
Helper for generating:
|
|
base/materials/duke3d_tiles.mtr
|
|
|
|
Example:
|
|
idDukeBuildMapImporter::WriteDukeTileMaterialFile(
|
|
"c:/doom3/base/materials/duke3d_tiles.mtr", 0, 4095 );
|
|
*/
|
|
static bool WriteDukeTileMaterialFile(const char* outFileName, int firstTile, int lastTile) {
|
|
// FILE* f = fopen(outFileName, "wt");
|
|
// if (f == NULL) {
|
|
// return false;
|
|
// }
|
|
//
|
|
// for (int i = firstTile; i <= lastTile; i++) {
|
|
// fprintf(f,
|
|
// "textures/duke3d/tiles/%d\n"
|
|
// "{\n"
|
|
// "\tqer_editorimage textures/duke3d/tiles/%d.tga\n"
|
|
// "\tdiffusemap textures/duke3d/tiles/%d.tga\n"
|
|
// "}\n\n",
|
|
// i, i, i);
|
|
// }
|
|
//
|
|
// fclose(f);
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
/*
|
|
===========================================================================
|
|
|
|
Build map structs
|
|
|
|
===========================================================================
|
|
*/
|
|
|
|
struct BuildSector {
|
|
short wallptr;
|
|
short wallnum;
|
|
int ceilingz;
|
|
int floorz;
|
|
short ceilingstat;
|
|
short floorstat;
|
|
short ceilingpicnum;
|
|
short ceilingheinum;
|
|
byte ceilingshade;
|
|
byte ceilingpal;
|
|
byte ceilingxpanning;
|
|
byte ceilingypanning;
|
|
short floorpicnum;
|
|
short floorheinum;
|
|
byte floorshade;
|
|
byte floorpal;
|
|
byte floorxpanning;
|
|
byte floorypanning;
|
|
byte visibility;
|
|
byte filler;
|
|
short lotag;
|
|
short hitag;
|
|
short extra;
|
|
};
|
|
|
|
struct BuildWall {
|
|
int x;
|
|
int y;
|
|
short point2;
|
|
short nextwall;
|
|
short nextsector;
|
|
short cstat;
|
|
short picnum;
|
|
short overpicnum;
|
|
byte shade;
|
|
byte pal;
|
|
byte xrepeat;
|
|
byte yrepeat;
|
|
byte xpanning;
|
|
byte ypanning;
|
|
short lotag;
|
|
short hitag;
|
|
short extra;
|
|
};
|
|
|
|
struct BuildSprite {
|
|
int x, y, z;
|
|
short cstat;
|
|
short picnum;
|
|
signed char shade;
|
|
byte pal;
|
|
byte clipdist;
|
|
byte filler;
|
|
byte xrepeat;
|
|
byte yrepeat;
|
|
signed char xoffset;
|
|
signed char yoffset;
|
|
short sectnum;
|
|
short statnum;
|
|
short ang;
|
|
short owner;
|
|
short xvel, yvel, zvel;
|
|
short lotag, hitag, extra;
|
|
};
|
|
|
|
struct TileInfo {
|
|
int w;
|
|
int h;
|
|
};
|
|
|
|
struct SideDesc {
|
|
idPlane plane;
|
|
idStr material;
|
|
brushprimit_texdef_t bp;
|
|
|
|
// DoomEdit/Radiant may rebuild face planes from planepts during
|
|
// Brush_Build. Keep these as real points from the generated face,
|
|
// not huge synthetic base-plane points, so failing brush windings
|
|
// don't leave enormous editor triangles behind.
|
|
idVec3 planePts[3];
|
|
};
|
|
|
|
struct Tri {
|
|
int a;
|
|
int b;
|
|
int c;
|
|
};
|
|
|
|
struct SectorLoop {
|
|
idList<int> wallIndices;
|
|
idList<idVec2> pts2;
|
|
float area;
|
|
};
|
|
|
|
struct FlatPolyPoint {
|
|
idVec2 p; // Doom-space XY, used for triangulation
|
|
int buildX; // Build-space XY, used for slope Z sampling
|
|
int buildY;
|
|
};
|
|
|
|
struct RunStats {
|
|
int rejectedBrushes;
|
|
int rejectedWallSpans;
|
|
int rejectedFlatTris;
|
|
int rejectedDegenerateSides;
|
|
int rejectedBrushLogCount;
|
|
};
|
|
|
|
private:
|
|
Options options;
|
|
idStr lastError;
|
|
|
|
idList<BuildSector> sectors;
|
|
idList<BuildWall> walls;
|
|
idList<BuildSprite> sprites;
|
|
RunStats runStats;
|
|
|
|
void ResetRunStats() {
|
|
memset(&runStats, 0, sizeof(runStats));
|
|
}
|
|
|
|
void LogRejectedBrush(const char* context, const char* reason) {
|
|
runStats.rejectedBrushes++;
|
|
|
|
if (runStats.rejectedBrushLogCount < 64) {
|
|
common->Printf(
|
|
"DukeBuildMapImporter %s: rejected brush: %s : %s\n",
|
|
DUKE_BUILD_IMPORTER_PATCH_VERSION,
|
|
context ? context : "",
|
|
reason ? reason : "invalid brush"
|
|
);
|
|
}
|
|
else if (runStats.rejectedBrushLogCount == 64) {
|
|
common->Printf(
|
|
"DukeBuildMapImporter %s: further rejected-brush messages suppressed.\n",
|
|
DUKE_BUILD_IMPORTER_PATCH_VERSION
|
|
);
|
|
}
|
|
|
|
runStats.rejectedBrushLogCount++;
|
|
}
|
|
|
|
void PrintPatchVerificationStats(const char* buildMapFileName) {
|
|
int slopedFloors = 0;
|
|
int slopedCeilings = 0;
|
|
int multiLoopSectors = 0;
|
|
int badPoint2Walls = 0;
|
|
|
|
for (int s = 0; s < sectors.Num(); s++) {
|
|
const BuildSector& sec = sectors[s];
|
|
if ((sec.floorstat & 2) && sec.floorheinum != 0) {
|
|
slopedFloors++;
|
|
}
|
|
if ((sec.ceilingstat & 2) && sec.ceilingheinum != 0) {
|
|
slopedCeilings++;
|
|
}
|
|
|
|
int loops = 0;
|
|
if (sec.wallptr >= 0 && sec.wallnum >= 3 && sec.wallptr + sec.wallnum <= walls.Num()) {
|
|
idList<int> used;
|
|
used.SetNum(sec.wallnum);
|
|
for (int i = 0; i < used.Num(); i++) {
|
|
used[i] = 0;
|
|
}
|
|
|
|
for (int startOfs = 0; startOfs < sec.wallnum; startOfs++) {
|
|
if (used[startOfs]) {
|
|
continue;
|
|
}
|
|
|
|
const int startWall = sec.wallptr + startOfs;
|
|
int wi = startWall;
|
|
bool closed = false;
|
|
|
|
for (int guard = 0; guard <= sec.wallnum; guard++) {
|
|
if (wi < sec.wallptr || wi >= sec.wallptr + sec.wallnum) {
|
|
badPoint2Walls++;
|
|
break;
|
|
}
|
|
|
|
const int local = wi - sec.wallptr;
|
|
if (used[local]) {
|
|
closed = (wi == startWall);
|
|
break;
|
|
}
|
|
|
|
used[local] = 1;
|
|
const BuildWall& w = walls[wi];
|
|
|
|
if (w.point2 == startWall) {
|
|
closed = true;
|
|
break;
|
|
}
|
|
|
|
wi = w.point2;
|
|
}
|
|
|
|
if (closed) {
|
|
loops++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (loops > 1) {
|
|
multiLoopSectors++;
|
|
}
|
|
}
|
|
|
|
common->Printf(
|
|
"DukeBuildMapImporter %s ACTIVE: file='%s' sectors=%d walls=%d sprites=%d slopedFloors=%d slopedCeilings=%d multiLoopSectors=%d badPoint2Walls=%d\n",
|
|
DUKE_BUILD_IMPORTER_PATCH_VERSION,
|
|
buildMapFileName ? buildMapFileName : "",
|
|
sectors.Num(),
|
|
walls.Num(),
|
|
sprites.Num(),
|
|
slopedFloors,
|
|
slopedCeilings,
|
|
multiLoopSectors,
|
|
badPoint2Walls
|
|
);
|
|
}
|
|
|
|
/*
|
|
===========================================================================
|
|
Binary loader
|
|
===========================================================================
|
|
*/
|
|
|
|
static short ReadLE16(FILE* f) {
|
|
byte b[2];
|
|
fread(b, 1, 2, f);
|
|
return (short)(b[0] | (b[1] << 8));
|
|
}
|
|
|
|
static int ReadLE32(FILE* f) {
|
|
byte b[4];
|
|
fread(b, 1, 4, f);
|
|
return (int)(b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24));
|
|
}
|
|
|
|
static byte ReadU8(FILE* f) {
|
|
byte b = 0;
|
|
fread(&b, 1, 1, f);
|
|
return b;
|
|
}
|
|
|
|
static signed char ReadS8(FILE* f) {
|
|
signed char b = 0;
|
|
fread(&b, 1, 1, f);
|
|
return b;
|
|
}
|
|
|
|
bool LoadBuildMap(const char* fileName) {
|
|
FILE* f = fopen(fileName, "rb");
|
|
if (f == NULL) {
|
|
lastError = va("Could not open Build MAP file '%s'.", fileName);
|
|
return false;
|
|
}
|
|
|
|
const int mapVersion = ReadLE32(f);
|
|
|
|
// Duke3D/Build commercial maps are normally version 7.
|
|
// Some derivatives may use 8 or 9, but the sector/wall core is similar.
|
|
if (mapVersion != 7 && mapVersion != 8 && mapVersion != 9) {
|
|
fclose(f);
|
|
lastError = va("Unsupported Build MAP version %d. Expected 7, 8, or 9.", mapVersion);
|
|
return false;
|
|
}
|
|
|
|
// Player start. We read and ignore it here.
|
|
const int startX = ReadLE32(f);
|
|
const int startY = ReadLE32(f);
|
|
const int startZ = ReadLE32(f);
|
|
const short startAng = ReadLE16(f);
|
|
const short startSect = ReadLE16(f);
|
|
(void)startX;
|
|
(void)startY;
|
|
(void)startZ;
|
|
(void)startAng;
|
|
(void)startSect;
|
|
|
|
const short numSectors = ReadLE16(f);
|
|
if (numSectors < 0 || numSectors > 4096) {
|
|
fclose(f);
|
|
lastError = va("Bad sector count: %d.", numSectors);
|
|
return false;
|
|
}
|
|
|
|
sectors.SetNum(numSectors);
|
|
|
|
for (int i = 0; i < numSectors; i++) {
|
|
BuildSector& s = sectors[i];
|
|
|
|
s.wallptr = ReadLE16(f);
|
|
s.wallnum = ReadLE16(f);
|
|
s.ceilingz = ReadLE32(f);
|
|
s.floorz = ReadLE32(f);
|
|
s.ceilingstat = ReadLE16(f);
|
|
s.floorstat = ReadLE16(f);
|
|
s.ceilingpicnum = ReadLE16(f);
|
|
s.ceilingheinum = ReadLE16(f);
|
|
s.ceilingshade = ReadU8(f);
|
|
s.ceilingpal = ReadU8(f);
|
|
s.ceilingxpanning = ReadU8(f);
|
|
s.ceilingypanning = ReadU8(f);
|
|
s.floorpicnum = ReadLE16(f);
|
|
s.floorheinum = ReadLE16(f);
|
|
s.floorshade = ReadU8(f);
|
|
s.floorpal = ReadU8(f);
|
|
s.floorxpanning = ReadU8(f);
|
|
s.floorypanning = ReadU8(f);
|
|
s.visibility = ReadU8(f);
|
|
s.filler = ReadU8(f);
|
|
s.lotag = ReadLE16(f);
|
|
s.hitag = ReadLE16(f);
|
|
s.extra = ReadLE16(f);
|
|
}
|
|
|
|
const short numWalls = ReadLE16(f);
|
|
if (numWalls < 0 || numWalls > 32767) {
|
|
fclose(f);
|
|
lastError = va("Bad wall count: %d.", numWalls);
|
|
return false;
|
|
}
|
|
|
|
walls.SetNum(numWalls);
|
|
|
|
for (int i = 0; i < numWalls; i++) {
|
|
BuildWall& w = walls[i];
|
|
|
|
w.x = ReadLE32(f);
|
|
w.y = ReadLE32(f);
|
|
w.point2 = ReadLE16(f);
|
|
w.nextwall = ReadLE16(f);
|
|
w.nextsector = ReadLE16(f);
|
|
w.cstat = ReadLE16(f);
|
|
w.picnum = ReadLE16(f);
|
|
w.overpicnum = ReadLE16(f);
|
|
w.shade = ReadU8(f);
|
|
w.pal = ReadU8(f);
|
|
w.xrepeat = ReadU8(f);
|
|
w.yrepeat = ReadU8(f);
|
|
w.xpanning = ReadU8(f);
|
|
w.ypanning = ReadU8(f);
|
|
w.lotag = ReadLE16(f);
|
|
w.hitag = ReadLE16(f);
|
|
w.extra = ReadLE16(f);
|
|
}
|
|
|
|
const short numSprites = ReadLE16(f);
|
|
if (numSprites < 0 || numSprites > 32767) {
|
|
fclose(f);
|
|
lastError = va("Bad sprite count: %d.", numSprites);
|
|
return false;
|
|
}
|
|
|
|
sprites.SetNum(numSprites);
|
|
|
|
for (int i = 0; i < numSprites; i++) {
|
|
BuildSprite& sp = sprites[i];
|
|
|
|
sp.x = ReadLE32(f);
|
|
sp.y = ReadLE32(f);
|
|
sp.z = ReadLE32(f);
|
|
sp.cstat = ReadLE16(f);
|
|
sp.picnum = ReadLE16(f);
|
|
sp.shade = ReadS8(f);
|
|
sp.pal = ReadU8(f);
|
|
sp.clipdist = ReadU8(f);
|
|
sp.filler = ReadU8(f);
|
|
sp.xrepeat = ReadU8(f);
|
|
sp.yrepeat = ReadU8(f);
|
|
sp.xoffset = ReadS8(f);
|
|
sp.yoffset = ReadS8(f);
|
|
sp.sectnum = ReadLE16(f);
|
|
sp.statnum = ReadLE16(f);
|
|
sp.ang = ReadLE16(f);
|
|
sp.owner = ReadLE16(f);
|
|
sp.xvel = ReadLE16(f);
|
|
sp.yvel = ReadLE16(f);
|
|
sp.zvel = ReadLE16(f);
|
|
sp.lotag = ReadLE16(f);
|
|
sp.hitag = ReadLE16(f);
|
|
sp.extra = ReadLE16(f);
|
|
}
|
|
|
|
fclose(f);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
===========================================================================
|
|
Coordinate / material helpers
|
|
===========================================================================
|
|
*/
|
|
|
|
idVec3 BuildPointToDoomFloat(float x, float y, float z) const {
|
|
const float dx = x * options.mapScale;
|
|
const float dy = y * options.mapScale * (options.flipY ? -1.0f : 1.0f);
|
|
|
|
// Build z grows downward. Doom z grows upward.
|
|
const float dz = options.zOffset - (z * options.zScale);
|
|
|
|
return idVec3(dx, dy, dz);
|
|
}
|
|
|
|
idVec3 BuildPointToDoom(int x, int y, int z) const {
|
|
return BuildPointToDoomFloat((float)x, (float)y, (float)z);
|
|
}
|
|
|
|
float BuildZToDoomZ(float z) const {
|
|
return options.zOffset - (z * options.zScale);
|
|
}
|
|
|
|
bool SurfaceIsSloped(short stat, short heinum) const {
|
|
return ((stat & 2) != 0) && (heinum != 0);
|
|
}
|
|
|
|
/*
|
|
Build sector slopes use the first wall of the sector as the hinge.
|
|
floorstat/ceilingstat bit 1, i.e. value 2, enables the slope, and
|
|
floorheinum/ceilingheinum stores the signed slope amount.
|
|
|
|
Equivalent floating-point form of Build's get*ofslope behavior:
|
|
z = baseZ + heinum * perpendicularDistanceFromHinge / 256
|
|
where perpendicularDistanceFromHinge is signed in Build XY space.
|
|
*/
|
|
float BuildSurfaceZAt(const BuildSector& sec, bool ceiling, float buildX, float buildY) const {
|
|
const float baseZ = (float)(ceiling ? sec.ceilingz : sec.floorz);
|
|
const short stat = ceiling ? sec.ceilingstat : sec.floorstat;
|
|
const short heinum = ceiling ? sec.ceilingheinum : sec.floorheinum;
|
|
|
|
if (!SurfaceIsSloped(stat, heinum)) {
|
|
return baseZ;
|
|
}
|
|
|
|
if (sec.wallptr < 0 || sec.wallptr >= walls.Num()) {
|
|
return baseZ;
|
|
}
|
|
|
|
const BuildWall& hinge = walls[sec.wallptr];
|
|
if (hinge.point2 < 0 || hinge.point2 >= walls.Num()) {
|
|
return baseZ;
|
|
}
|
|
|
|
const BuildWall& hinge2 = walls[hinge.point2];
|
|
|
|
const float dx = (float)hinge2.x - (float)hinge.x;
|
|
const float dy = (float)hinge2.y - (float)hinge.y;
|
|
const float len = idMath::Sqrt(dx * dx + dy * dy);
|
|
|
|
if (len < 0.001f) {
|
|
return baseZ;
|
|
}
|
|
|
|
// Signed distance times hinge length: dx * relY - dy * relX.
|
|
const float cross = dx * (buildY - (float)hinge.y) - dy * (buildX - (float)hinge.x);
|
|
return baseZ + (((float)heinum * cross) / (len * 256.0f));
|
|
}
|
|
|
|
idVec3 BuildSurfacePointToDoom(const BuildSector& sec, bool ceiling, int buildX, int buildY) const {
|
|
return BuildPointToDoomFloat((float)buildX, (float)buildY, BuildSurfaceZAt(sec, ceiling, (float)buildX, (float)buildY));
|
|
}
|
|
|
|
idStr TileMaterialName(int picnum) const {
|
|
if (picnum < 0) {
|
|
picnum = 0;
|
|
}
|
|
|
|
idStr out;
|
|
out = va("%s/%d", options.tileMaterialPrefix.c_str(), picnum);
|
|
return out;
|
|
}
|
|
|
|
TileInfo GetTileInfo(int picnum) const {
|
|
TileInfo info;
|
|
info.w = Max(1, options.defaultTileWidth);
|
|
info.h = Max(1, options.defaultTileHeight);
|
|
|
|
idStr matName = TileMaterialName(picnum);
|
|
const idMaterial* mat = Texture_ForName(matName.c_str());
|
|
|
|
if (mat != NULL && mat->GetEditorImage() != NULL) {
|
|
if (mat->GetEditorImage()->uploadWidth > 0) {
|
|
info.w = mat->GetEditorImage()->uploadWidth;
|
|
}
|
|
if (mat->GetEditorImage()->uploadHeight > 0) {
|
|
info.h = mat->GetEditorImage()->uploadHeight;
|
|
}
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
/*
|
|
Doom 3 brush primitive matrices are 2x3 transforms on a plane-local basis.
|
|
This computes the same kind of plane basis used by Doom 3 map code, then
|
|
projects desired world-space UV gradients into that local basis.
|
|
*/
|
|
static void ComputeAxisBaseDoom3(const idVec3& normal, idVec3& texX, idVec3& texY) {
|
|
idVec3 n = normal;
|
|
n.Normalize();
|
|
|
|
float rotY = -idMath::ATan(n.z, idMath::Sqrt(n.y * n.y + n.x * n.x));
|
|
float rotZ = idMath::ATan(n.y, n.x);
|
|
|
|
// Snap very small values to avoid drifting texture bases.
|
|
if (idMath::Fabs(rotY) < 1e-6f) {
|
|
rotY = 0.0f;
|
|
}
|
|
if (idMath::Fabs(rotZ) < 1e-6f) {
|
|
rotZ = 0.0f;
|
|
}
|
|
|
|
// Start with x/y texture axes.
|
|
texX.Set(-idMath::Sin(rotZ), idMath::Cos(rotZ), 0.0f);
|
|
texY.Set(
|
|
-idMath::Sin(rotY) * idMath::Cos(rotZ),
|
|
-idMath::Sin(rotY) * idMath::Sin(rotZ),
|
|
-idMath::Cos(rotY)
|
|
);
|
|
|
|
texX.Normalize();
|
|
texY.Normalize();
|
|
}
|
|
|
|
brushprimit_texdef_t MakeBrushPrimMatrixForWorldUV(
|
|
const idVec3& planeNormal,
|
|
const idVec3& worldUGradient,
|
|
const idVec3& worldVGradient,
|
|
float uOffset,
|
|
float vOffset) const {
|
|
|
|
brushprimit_texdef_t bp;
|
|
memset(&bp, 0, sizeof(bp));
|
|
|
|
idVec3 texX;
|
|
idVec3 texY;
|
|
ComputeAxisBaseDoom3(planeNormal, texX, texY);
|
|
|
|
bp.coords[0][0] = worldUGradient * texX;
|
|
bp.coords[0][1] = worldUGradient * texY;
|
|
bp.coords[0][2] = uOffset;
|
|
|
|
bp.coords[1][0] = worldVGradient * texX;
|
|
bp.coords[1][1] = worldVGradient * texY;
|
|
bp.coords[1][2] = vOffset;
|
|
|
|
return bp;
|
|
}
|
|
|
|
brushprimit_texdef_t DefaultMatrix() const {
|
|
brushprimit_texdef_t bp;
|
|
memset(&bp, 0, sizeof(bp));
|
|
bp.coords[0][0] = 1.0f / 64.0f;
|
|
bp.coords[1][1] = 1.0f / 64.0f;
|
|
return bp;
|
|
}
|
|
|
|
/*
|
|
===========================================================================
|
|
Plane / brush helpers
|
|
===========================================================================
|
|
*/
|
|
|
|
static void FlipPlane(idPlane& p) {
|
|
p[0] = -p[0];
|
|
p[1] = -p[1];
|
|
p[2] = -p[2];
|
|
p[3] = -p[3];
|
|
}
|
|
|
|
static bool PlaneFromPointsOutward(const idVec3& a, const idVec3& b, const idVec3& c, const idVec3& insidePoint, idPlane& out) {
|
|
if (!out.FromPoints(a, b, c, false)) {
|
|
return false;
|
|
}
|
|
|
|
// A brush face plane's normal should point away from the brush center.
|
|
const float d = out.Normal() * insidePoint + out[3];
|
|
if (d > 0.0f) {
|
|
FlipPlane(out);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
static bool NormalizePlaneForCompare(const idPlane& plane, idVec3& normal, float& dist) {
|
|
normal = plane.Normal();
|
|
const float len = normal.Normalize();
|
|
if (len <= 0.000001f) {
|
|
dist = 0.0f;
|
|
return false;
|
|
}
|
|
dist = plane[3] / len;
|
|
return true;
|
|
}
|
|
|
|
static bool IsDuplicatePlane(const idPlane& a, const idPlane& b) {
|
|
idVec3 na;
|
|
idVec3 nb;
|
|
float da;
|
|
float db;
|
|
if (!NormalizePlaneForCompare(a, na, da) || !NormalizePlaneForCompare(b, nb, db)) {
|
|
return false;
|
|
}
|
|
|
|
// Radiant's Brush_MakeFaceWinding treats repeated coplanar faces as a
|
|
// hard error. Keep one copy before calling Brush_Build.
|
|
return (na * nb) > 0.9999f && idMath::Fabs(da - db) < 0.05f;
|
|
}
|
|
|
|
static bool IsZeroVolumeOppositePlane(const idPlane& a, const idPlane& b) {
|
|
idVec3 na;
|
|
idVec3 nb;
|
|
float da;
|
|
float db;
|
|
if (!NormalizePlaneForCompare(a, na, da) || !NormalizePlaneForCompare(b, nb, db)) {
|
|
return false;
|
|
}
|
|
|
|
// Opposite normals on the same plane mean zero thickness. That usually
|
|
// happens at sloped portal crossings or tiny Build detail sectors.
|
|
return (na * nb) < -0.9999f && idMath::Fabs(da + db) < 0.05f;
|
|
}
|
|
|
|
bool CleanSideList(const idList<SideDesc>& inSides, idList<SideDesc>& outSides, const char* context) {
|
|
outSides.SetNum(0);
|
|
|
|
for (int i = 0; i < inSides.Num(); i++) {
|
|
if (!PlaneLooksSane(inSides[i].plane)) {
|
|
LogRejectedBrush(context, va("bad source plane %d", i));
|
|
return false;
|
|
}
|
|
|
|
bool duplicate = false;
|
|
for (int j = 0; j < outSides.Num(); j++) {
|
|
if (IsDuplicatePlane(inSides[i].plane, outSides[j].plane)) {
|
|
duplicate = true;
|
|
break;
|
|
}
|
|
|
|
if (IsZeroVolumeOppositePlane(inSides[i].plane, outSides[j].plane)) {
|
|
LogRejectedBrush(context, va("opposing zero-volume planes %d/%d", i, j));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!duplicate) {
|
|
outSides.Append(inSides[i]);
|
|
}
|
|
}
|
|
|
|
if (outSides.Num() < 4) {
|
|
LogRejectedBrush(context, va("too few unique planes after cleanup: %d", outSides.Num()));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void PointsForPlane(const idPlane& plane, idVec3 outPts[3]) {
|
|
idVec3 normal = plane.Normal();
|
|
normal.Normalize();
|
|
|
|
idVec3 up;
|
|
if (idMath::Fabs(normal.z) < 0.999f) {
|
|
up.Set(0.0f, 0.0f, 1.0f);
|
|
}
|
|
else {
|
|
up.Set(1.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
idVec3 right = up.Cross(normal);
|
|
right.Normalize();
|
|
|
|
idVec3 planeUp = normal.Cross(right);
|
|
planeUp.Normalize();
|
|
|
|
const idVec3 origin = normal * -plane[3];
|
|
|
|
// Keep these points large enough for Radiant code paths that seed face
|
|
// windings from planepts instead of rebuilding an infinite base winding.
|
|
// 256 is too small for many scaled Duke floors/ceilings.
|
|
const float s = 131072.0f;
|
|
|
|
// Ordered so Face_MakePlane-style cross products produce plane.Normal().
|
|
outPts[0] = origin + right * s + planeUp * s;
|
|
outPts[1] = origin - right * s - planeUp * s;
|
|
outPts[2] = origin - right * s + planeUp * s;
|
|
}
|
|
|
|
static bool FloatLooksSane(float f) {
|
|
return (f == f) && idMath::Fabs(f) < 1.0e20f;
|
|
}
|
|
|
|
static bool PlaneLooksSane(const idPlane& plane) {
|
|
const idVec3 n = plane.Normal();
|
|
if (!FloatLooksSane(n.x) || !FloatLooksSane(n.y) || !FloatLooksSane(n.z) || !FloatLooksSane(plane[3])) {
|
|
return false;
|
|
}
|
|
|
|
const float lenSqr = n * n;
|
|
return lenSqr > 0.5f && lenSqr < 2.0f;
|
|
}
|
|
|
|
static void BaseWindingForPlane(const idPlane& plane, idList<idVec3>& outPoly) {
|
|
idVec3 normal = plane.Normal();
|
|
normal.Normalize();
|
|
|
|
idVec3 up;
|
|
if (idMath::Fabs(normal.z) < 0.999f) {
|
|
up.Set(0.0f, 0.0f, 1.0f);
|
|
}
|
|
else {
|
|
up.Set(1.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
idVec3 right = up.Cross(normal);
|
|
right.Normalize();
|
|
|
|
idVec3 planeUp = normal.Cross(right);
|
|
planeUp.Normalize();
|
|
|
|
const idVec3 origin = normal * -plane[3];
|
|
const float s = 131072.0f;
|
|
|
|
outPoly.SetNum(4);
|
|
outPoly[0] = origin + right * s + planeUp * s;
|
|
outPoly[1] = origin - right * s + planeUp * s;
|
|
outPoly[2] = origin - right * s - planeUp * s;
|
|
outPoly[3] = origin + right * s - planeUp * s;
|
|
}
|
|
|
|
static void CleanClipPolygon(idList<idVec3>& poly) {
|
|
const float sameEpsSqr = 0.000001f;
|
|
for (int guard = 0; guard < 8192 && poly.Num() > 2; guard++) {
|
|
bool removed = false;
|
|
|
|
for (int i = 0; i < poly.Num(); i++) {
|
|
const int n = (i + 1) % poly.Num();
|
|
const idVec3 d = poly[i] - poly[n];
|
|
if ((d * d) <= sameEpsSqr) {
|
|
poly.RemoveIndex(n);
|
|
removed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (removed) {
|
|
continue;
|
|
}
|
|
|
|
for (int i = 0; i < poly.Num(); i++) {
|
|
const int p = (i + poly.Num() - 1) % poly.Num();
|
|
const int n = (i + 1) % poly.Num();
|
|
|
|
idVec3 a = poly[i] - poly[p];
|
|
idVec3 b = poly[n] - poly[i];
|
|
if ((a * a) <= sameEpsSqr || (b * b) <= sameEpsSqr) {
|
|
continue;
|
|
}
|
|
|
|
a.Normalize();
|
|
b.Normalize();
|
|
if (idMath::Fabs(a * b) > 0.99999f) {
|
|
poly.RemoveIndex(i);
|
|
removed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!removed) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool ClipPolygonByPlane(const idList<idVec3>& inPoly, const idPlane& plane, idList<idVec3>& outPoly) {
|
|
outPoly.SetNum(0);
|
|
|
|
if (inPoly.Num() < 3) {
|
|
return false;
|
|
}
|
|
|
|
const float epsilon = 0.01f;
|
|
|
|
for (int i = 0; i < inPoly.Num(); i++) {
|
|
const idVec3& s = inPoly[i];
|
|
const idVec3& e = inPoly[(i + 1) % inPoly.Num()];
|
|
|
|
const float sd = plane.Normal() * s + plane[3];
|
|
const float ed = plane.Normal() * e + plane[3];
|
|
|
|
const bool sInside = sd <= epsilon;
|
|
const bool eInside = ed <= epsilon;
|
|
|
|
if (sInside && eInside) {
|
|
outPoly.Append(e);
|
|
}
|
|
else if (sInside && !eInside) {
|
|
const float denom = sd - ed;
|
|
if (idMath::Fabs(denom) > 1e-6f) {
|
|
outPoly.Append(s + (e - s) * (sd / denom));
|
|
}
|
|
}
|
|
else if (!sInside && eInside) {
|
|
const float denom = sd - ed;
|
|
if (idMath::Fabs(denom) > 1e-6f) {
|
|
outPoly.Append(s + (e - s) * (sd / denom));
|
|
}
|
|
outPoly.Append(e);
|
|
}
|
|
}
|
|
|
|
CleanClipPolygon(outPoly);
|
|
return outPoly.Num() >= 3;
|
|
}
|
|
|
|
static float PolygonAreaOnPlane(const idList<idVec3>& poly, const idVec3& normal) {
|
|
if (poly.Num() < 3) {
|
|
return 0.0f;
|
|
}
|
|
|
|
idVec3 sum(0.0f, 0.0f, 0.0f);
|
|
for (int i = 0; i < poly.Num(); i++) {
|
|
sum += poly[i].Cross(poly[(i + 1) % poly.Num()]);
|
|
}
|
|
|
|
idVec3 n = normal;
|
|
if (n.Normalize() == 0.0f) {
|
|
return 0.0f;
|
|
}
|
|
|
|
return idMath::Fabs(sum * n) * 0.5f;
|
|
}
|
|
|
|
bool ValidateConvexBrush(const idList<SideDesc>& sides, const char* context) {
|
|
if (sides.Num() < 4) {
|
|
LogRejectedBrush(context, "not enough sides");
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < sides.Num(); i++) {
|
|
if (!PlaneLooksSane(sides[i].plane)) {
|
|
LogRejectedBrush(context, va("bad plane on side %d", i));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < sides.Num(); i++) {
|
|
idList<idVec3> poly;
|
|
BaseWindingForPlane(sides[i].plane, poly);
|
|
|
|
for (int j = 0; j < sides.Num(); j++) {
|
|
if (i == j) {
|
|
continue;
|
|
}
|
|
|
|
idList<idVec3> clipped;
|
|
if (!ClipPolygonByPlane(poly, sides[j].plane, clipped)) {
|
|
LogRejectedBrush(context, va("face %d clipped away by side %d", i, j));
|
|
return false;
|
|
}
|
|
poly = clipped;
|
|
}
|
|
|
|
const float area = PolygonAreaOnPlane(poly, sides[i].plane.Normal());
|
|
if (area < 0.001f) {
|
|
LogRejectedBrush(context, va("face %d has tiny winding area %.6f", i, area));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AddBrushFromSides(const idList<SideDesc>& sides, const char* context) {
|
|
idList<SideDesc> cleanSides;
|
|
if (!CleanSideList(sides, cleanSides, context)) {
|
|
return false;
|
|
}
|
|
|
|
if (!ValidateConvexBrush(cleanSides, context)) {
|
|
return false;
|
|
}
|
|
|
|
brush_t* b = Brush_Alloc();
|
|
if (b == NULL) {
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < cleanSides.Num(); i++) {
|
|
face_t* f = Face_Alloc();
|
|
if (f == NULL) {
|
|
continue;
|
|
}
|
|
|
|
f->plane = cleanSides[i].plane;
|
|
f->originalPlane = cleanSides[i].plane;
|
|
f->dirty = false;
|
|
|
|
// Use actual generated face points. The previous importer used
|
|
// synthetic huge triangles on the same plane; that can make failed
|
|
// windings look like faces stretching forever in the editor.
|
|
f->planepts[0] = cleanSides[i].planePts[0];
|
|
f->planepts[1] = cleanSides[i].planePts[1];
|
|
f->planepts[2] = cleanSides[i].planePts[2];
|
|
|
|
f->texdef.SetName(cleanSides[i].material.c_str());
|
|
f->d_texture = Texture_ForName(cleanSides[i].material.c_str());
|
|
f->brushprimit_texdef = cleanSides[i].bp;
|
|
|
|
f->next = b->brush_faces;
|
|
b->brush_faces = f;
|
|
}
|
|
|
|
Brush_AddToList(b, &selected_brushes);
|
|
Entity_LinkBrush(world_entity, b);
|
|
|
|
// Do not snap imported Build geometry. Duke slopes and tiny detail sectors
|
|
// are intentionally off-grid after scale conversion; snapping the plane
|
|
// points is a common cause of "Unable to create face winding on brush".
|
|
Brush_Build(b, false, true, false, true);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AddSide(
|
|
idList<SideDesc>& sides,
|
|
const idVec3& a,
|
|
const idVec3& b,
|
|
const idVec3& c,
|
|
const idVec3& inside,
|
|
const char* material,
|
|
const brushprimit_texdef_t& bp) {
|
|
|
|
SideDesc s;
|
|
if (!PlaneFromPointsOutward(a, b, c, inside, s.plane)) {
|
|
runStats.rejectedDegenerateSides++;
|
|
return false;
|
|
}
|
|
|
|
// Match planepts winding to the possibly flipped outward plane.
|
|
// Brush_Build/Face_MakePlane in DoomEdit can recompute the plane
|
|
// from these three points, so their order must agree with s.plane.
|
|
idPlane rawPlane;
|
|
if (rawPlane.FromPoints(a, b, c, false) && (rawPlane.Normal() * s.plane.Normal()) < 0.0f) {
|
|
s.planePts[0] = a;
|
|
s.planePts[1] = c;
|
|
s.planePts[2] = b;
|
|
}
|
|
else {
|
|
s.planePts[0] = a;
|
|
s.planePts[1] = b;
|
|
s.planePts[2] = c;
|
|
}
|
|
|
|
s.material = material;
|
|
s.bp = bp;
|
|
sides.Append(s);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
===========================================================================
|
|
UV helpers
|
|
===========================================================================
|
|
*/
|
|
|
|
brushprimit_texdef_t MakeWallUV(
|
|
const BuildWall& w,
|
|
const idVec3& startAnchor,
|
|
const idVec3& endAnchor,
|
|
const idVec3& vAnchor,
|
|
const idVec3& planeNormal) const {
|
|
|
|
TileInfo tile = GetTileInfo(w.picnum);
|
|
|
|
idVec3 wallDir = endAnchor - startAnchor;
|
|
wallDir.z = 0.0f;
|
|
if (wallDir.Normalize() == 0.0f) {
|
|
wallDir.Set(1.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
float xRep = w.xrepeat > 0 ? (float)w.xrepeat : options.wallRepeatBase;
|
|
float yRep = w.yrepeat > 0 ? (float)w.yrepeat : options.wallRepeatBase;
|
|
|
|
float uScale = xRep / ((float)tile.w * options.wallRepeatBase);
|
|
float vScale = yRep / ((float)tile.h * options.wallRepeatBase);
|
|
|
|
idVec3 uGrad = wallDir * uScale;
|
|
|
|
// Texture V goes down the wall. Doom Z goes up.
|
|
idVec3 vGrad(0.0f, 0.0f, -vScale);
|
|
|
|
// Build wall cstat common flip bits.
|
|
// bit 3: x-flip, bit 8: y-flip in classic Build usage.
|
|
if (w.cstat & 8) {
|
|
uGrad = -uGrad;
|
|
}
|
|
if (w.cstat & 256) {
|
|
vGrad = -vGrad;
|
|
}
|
|
|
|
// Anchor U on the wall's first point and V on either top or bottom.
|
|
// Without this, large map coordinates can make brush-primitive offsets
|
|
// look arbitrary, and portal upper/lower wall pieces will not line up.
|
|
float uOff = ((float)w.xpanning / (float)tile.w) - (uGrad * startAnchor);
|
|
float vOff = ((float)w.ypanning / (float)tile.h) - (vGrad * vAnchor);
|
|
|
|
return MakeBrushPrimMatrixForWorldUV(planeNormal, uGrad, vGrad, uOff, vOff);
|
|
}
|
|
|
|
brushprimit_texdef_t MakeFlatUV(const BuildSector& sec, bool ceiling, const idVec3& planeNormal) const {
|
|
const int picnum = ceiling ? sec.ceilingpicnum : sec.floorpicnum;
|
|
TileInfo tile = GetTileInfo(picnum);
|
|
|
|
short stat = ceiling ? sec.ceilingstat : sec.floorstat;
|
|
byte xpan = ceiling ? sec.ceilingxpanning : sec.floorxpanning;
|
|
byte ypan = ceiling ? sec.ceilingypanning : sec.floorypanning;
|
|
|
|
float repeatBase = options.flatRepeatBase;
|
|
if (repeatBase <= 0.001f) {
|
|
repeatBase = 16.0f;
|
|
}
|
|
|
|
/*
|
|
Build flat texture scale is in Build map units. The importer works
|
|
in Doom units, so fold mapScale into the denominator. With the
|
|
default mapScale 1/8 and flatRepeatBase 16, a 64px tile spans
|
|
1024 Build units / 128 Doom units instead of 1024 Doom units.
|
|
*/
|
|
const float doomRepeatBase = Max(0.001f, repeatBase * Max(0.001f, options.mapScale));
|
|
const float uScale = 1.0f / ((float)tile.w * doomRepeatBase);
|
|
const float vScale = 1.0f / ((float)tile.h * doomRepeatBase);
|
|
|
|
idVec3 uGrad;
|
|
idVec3 vGrad;
|
|
idVec3 anchor(0.0f, 0.0f, 0.0f);
|
|
|
|
// Build stat bit 6/value 64 is relative alignment. This matters most
|
|
// on slopes: U follows the hinge wall and V follows the sloped surface
|
|
// instead of raw world XY, avoiding the giant stretched look.
|
|
if ((stat & 64) && sec.wallptr >= 0 && sec.wallptr < walls.Num()) {
|
|
const BuildWall& hinge = walls[sec.wallptr];
|
|
if (hinge.point2 >= 0 && hinge.point2 < walls.Num()) {
|
|
const BuildWall& hinge2 = walls[hinge.point2];
|
|
const idVec3 p0 = BuildSurfacePointToDoom(sec, ceiling, hinge.x, hinge.y);
|
|
const idVec3 p1 = BuildSurfacePointToDoom(sec, ceiling, hinge2.x, hinge2.y);
|
|
|
|
idVec3 xAxis = p1 - p0;
|
|
if (xAxis.Normalize() == 0.0f) {
|
|
xAxis.Set(1.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
idVec3 n = planeNormal;
|
|
if (n.Normalize() == 0.0f) {
|
|
n.Set(0.0f, 0.0f, ceiling ? -1.0f : 1.0f);
|
|
}
|
|
|
|
idVec3 yAxis = xAxis.Cross(n);
|
|
if (yAxis.Normalize() == 0.0f) {
|
|
yAxis.Set(0.0f, options.flipY ? -1.0f : 1.0f, 0.0f);
|
|
}
|
|
|
|
uGrad = xAxis * uScale;
|
|
vGrad = yAxis * vScale;
|
|
anchor = p0;
|
|
}
|
|
else {
|
|
uGrad.Set(uScale, 0.0f, 0.0f);
|
|
vGrad.Set(0.0f, options.flipY ? -vScale : vScale, 0.0f);
|
|
}
|
|
}
|
|
else {
|
|
uGrad.Set(uScale, 0.0f, 0.0f);
|
|
vGrad.Set(0.0f, options.flipY ? -vScale : vScale, 0.0f);
|
|
}
|
|
|
|
// Build flat stat bits commonly used by Duke:
|
|
// bit 2/value 4: swap x/y
|
|
// bit 4/value 16: x flip
|
|
// bit 5/value 32: y flip
|
|
// bit 3/value 8: double smooshiness. Here we double density.
|
|
if (stat & 4) {
|
|
idVec3 tmp = uGrad;
|
|
uGrad = vGrad;
|
|
vGrad = tmp;
|
|
}
|
|
|
|
if (stat & 16) {
|
|
uGrad = -uGrad;
|
|
}
|
|
|
|
if (stat & 32) {
|
|
vGrad = -vGrad;
|
|
}
|
|
|
|
if (stat & 8) {
|
|
uGrad *= 2.0f;
|
|
vGrad *= 2.0f;
|
|
}
|
|
|
|
// Ceiling is viewed from below; flipping V usually matches Build better.
|
|
if (ceiling) {
|
|
vGrad = -vGrad;
|
|
}
|
|
|
|
float uOff = ((float)xpan / (float)tile.w) - (uGrad * anchor);
|
|
float vOff = ((float)ypan / (float)tile.h) - (vGrad * anchor);
|
|
|
|
return MakeBrushPrimMatrixForWorldUV(planeNormal, uGrad, vGrad, uOff, vOff);
|
|
}
|
|
|
|
/*
|
|
===========================================================================
|
|
Wall brushes
|
|
===========================================================================
|
|
*/
|
|
|
|
bool AddWallSpanBrushSegment(
|
|
int wallIndex,
|
|
float tA,
|
|
float tB,
|
|
float buildZTopA,
|
|
float buildZTopB,
|
|
float buildZBottomA,
|
|
float buildZBottomB,
|
|
int picnum) {
|
|
|
|
if (wallIndex < 0 || wallIndex >= walls.Num()) {
|
|
return false;
|
|
}
|
|
|
|
BuildWall& w = walls[wallIndex];
|
|
|
|
if (w.point2 < 0 || w.point2 >= walls.Num()) {
|
|
return false;
|
|
}
|
|
|
|
BuildWall& w2 = walls[w.point2];
|
|
|
|
if (tB <= tA + 0.0001f) {
|
|
runStats.rejectedWallSpans++;
|
|
return false;
|
|
}
|
|
|
|
// Build z grows downward: bottom must be lower than top at least
|
|
// somewhere on the segment. Exact endpoint-zero spans create duplicate
|
|
// planes, so AddWallSpanBrushClipped trims those before this is called.
|
|
if ((buildZBottomA - buildZTopA) <= 0.1f && (buildZBottomB - buildZTopB) <= 0.1f) {
|
|
runStats.rejectedWallSpans++;
|
|
return false;
|
|
}
|
|
|
|
const float xA = (float)w.x + ((float)w2.x - (float)w.x) * tA;
|
|
const float yA = (float)w.y + ((float)w2.y - (float)w.y) * tA;
|
|
const float xB = (float)w.x + ((float)w2.x - (float)w.x) * tB;
|
|
const float yB = (float)w.y + ((float)w2.y - (float)w.y) * tB;
|
|
|
|
idVec3 aTop = BuildPointToDoomFloat(xA, yA, buildZTopA);
|
|
idVec3 bTop = BuildPointToDoomFloat(xB, yB, buildZTopB);
|
|
idVec3 aBot = BuildPointToDoomFloat(xA, yA, buildZBottomA);
|
|
idVec3 bBot = BuildPointToDoomFloat(xB, yB, buildZBottomB);
|
|
|
|
if (Max(idMath::Fabs(aTop.z - aBot.z), idMath::Fabs(bTop.z - bBot.z)) < 0.1f) {
|
|
runStats.rejectedWallSpans++;
|
|
return false;
|
|
}
|
|
|
|
idVec3 dir = bBot - aBot;
|
|
dir.z = 0.0f;
|
|
if (dir.Normalize() == 0.0f) {
|
|
runStats.rejectedWallSpans++;
|
|
return false;
|
|
}
|
|
|
|
idVec3 sideN(-dir.y, dir.x, 0.0f);
|
|
sideN.Normalize();
|
|
|
|
const float halfThick = Max(1.0f, options.wallThickness * 0.5f);
|
|
|
|
idVec3 p0 = aBot - sideN * halfThick;
|
|
idVec3 p1 = bBot - sideN * halfThick;
|
|
idVec3 p2 = bBot + sideN * halfThick;
|
|
idVec3 p3 = aBot + sideN * halfThick;
|
|
|
|
idVec3 p4 = aTop - sideN * halfThick;
|
|
idVec3 p5 = bTop - sideN * halfThick;
|
|
idVec3 p6 = bTop + sideN * halfThick;
|
|
idVec3 p7 = aTop + sideN * halfThick;
|
|
|
|
idVec3 center = (p0 + p1 + p2 + p3 + p4 + p5 + p6 + p7) * (1.0f / 8.0f);
|
|
|
|
idStr wallMat = TileMaterialName(picnum);
|
|
brushprimit_texdef_t caulkBP = DefaultMatrix();
|
|
|
|
idList<SideDesc> sides;
|
|
sides.SetNum(0);
|
|
|
|
// Use the full original wall length for U anchoring so clipped wall
|
|
// pieces line up with unclipped pieces instead of restarting at tA.
|
|
const idVec3 originalStart = BuildPointToDoomFloat((float)w.x, (float)w.y, (w.cstat & 4) ? buildZBottomA : buildZTopA);
|
|
const idVec3 originalEnd = BuildPointToDoomFloat((float)w2.x, (float)w2.y, (w.cstat & 4) ? buildZBottomB : buildZTopB);
|
|
const idVec3 vAnchor = (w.cstat & 4) ? aBot : aTop;
|
|
|
|
// First broad side.
|
|
{
|
|
idPlane tmp;
|
|
if (!PlaneFromPointsOutward(p0, p1, p5, center, tmp)) {
|
|
runStats.rejectedDegenerateSides++;
|
|
return false;
|
|
}
|
|
brushprimit_texdef_t wallBP = MakeWallUV(w, originalStart, originalEnd, vAnchor, tmp.Normal());
|
|
if (!AddSide(sides, p0, p1, p5, center, wallMat.c_str(), wallBP)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Second broad side, also textured so the brush looks right from either side.
|
|
{
|
|
idPlane tmp;
|
|
if (!PlaneFromPointsOutward(p3, p7, p6, center, tmp)) {
|
|
runStats.rejectedDegenerateSides++;
|
|
return false;
|
|
}
|
|
brushprimit_texdef_t wallBP = MakeWallUV(w, originalStart, originalEnd, vAnchor, tmp.Normal());
|
|
if (!AddSide(sides, p3, p7, p6, center, wallMat.c_str(), wallBP)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Top, bottom, and end caps are caulk.
|
|
if (!AddSide(sides, p4, p5, p6, center, options.caulkMaterial.c_str(), caulkBP)) {
|
|
return false;
|
|
}
|
|
if (!AddSide(sides, p0, p2, p1, center, options.caulkMaterial.c_str(), caulkBP)) {
|
|
return false;
|
|
}
|
|
if (!AddSide(sides, p0, p4, p7, center, options.caulkMaterial.c_str(), caulkBP)) {
|
|
return false;
|
|
}
|
|
if (!AddSide(sides, p1, p2, p6, center, options.caulkMaterial.c_str(), caulkBP)) {
|
|
return false;
|
|
}
|
|
|
|
idStr context = va(
|
|
"wallSpan wall=%d pic=%d tA=%.4f tB=%.4f topA=%.3f topB=%.3f botA=%.3f botB=%.3f",
|
|
wallIndex,
|
|
picnum,
|
|
tA,
|
|
tB,
|
|
buildZTopA,
|
|
buildZTopB,
|
|
buildZBottomA,
|
|
buildZBottomB
|
|
);
|
|
return AddBrushFromSides(sides, context.c_str());
|
|
}
|
|
|
|
bool AddWallSpanBrush(
|
|
int wallIndex,
|
|
float buildZTopA,
|
|
float buildZTopB,
|
|
float buildZBottomA,
|
|
float buildZBottomB,
|
|
int picnum) {
|
|
|
|
return AddWallSpanBrushSegment(wallIndex, 0.0f, 1.0f, buildZTopA, buildZTopB, buildZBottomA, buildZBottomB, picnum);
|
|
}
|
|
|
|
bool AddWallSpanBrushClipped(
|
|
int wallIndex,
|
|
float buildZTopA,
|
|
float buildZTopB,
|
|
float buildZBottomA,
|
|
float buildZBottomB,
|
|
int picnum) {
|
|
|
|
const float minBuildHeight = Max(0.5f, 0.5f / Max(0.001f, options.zScale));
|
|
const float hA = buildZBottomA - buildZTopA;
|
|
const float hB = buildZBottomB - buildZTopB;
|
|
|
|
if (hA <= minBuildHeight && hB <= minBuildHeight) {
|
|
runStats.rejectedWallSpans++;
|
|
return false;
|
|
}
|
|
|
|
float t0 = 0.0f;
|
|
float t1 = 1.0f;
|
|
|
|
// If sloped sector surfaces cross along a portal wall, the visible
|
|
// upper/lower step may exist only along part of the wall. Clamping the
|
|
// bad endpoint makes a zero-area cap; trimming the segment avoids the
|
|
// duplicate/zero-volume planes that cause Radiant winding failures.
|
|
if (hA <= minBuildHeight && hB > minBuildHeight) {
|
|
t0 = (minBuildHeight - hA) / (hB - hA);
|
|
}
|
|
else if (hB <= minBuildHeight && hA > minBuildHeight) {
|
|
t1 = (minBuildHeight - hA) / (hB - hA);
|
|
}
|
|
|
|
if (t0 < 0.0f) {
|
|
t0 = 0.0f;
|
|
}
|
|
if (t0 > 1.0f) {
|
|
t0 = 1.0f;
|
|
}
|
|
if (t1 < 0.0f) {
|
|
t1 = 0.0f;
|
|
}
|
|
if (t1 > 1.0f) {
|
|
t1 = 1.0f;
|
|
}
|
|
if (t1 <= t0 + 0.0001f) {
|
|
runStats.rejectedWallSpans++;
|
|
return false;
|
|
}
|
|
|
|
const float top0 = buildZTopA + (buildZTopB - buildZTopA) * t0;
|
|
const float top1 = buildZTopA + (buildZTopB - buildZTopA) * t1;
|
|
const float bot0 = buildZBottomA + (buildZBottomB - buildZBottomA) * t0;
|
|
const float bot1 = buildZBottomA + (buildZBottomB - buildZBottomA) * t1;
|
|
|
|
return AddWallSpanBrushSegment(wallIndex, t0, t1, top0, top1, bot0, bot1, picnum);
|
|
}
|
|
|
|
int ImportSectorWalls(int sectorIndex) {
|
|
BuildSector& sec = sectors[sectorIndex];
|
|
int count = 0;
|
|
|
|
for (int i = 0; i < sec.wallnum; i++) {
|
|
const int wi = sec.wallptr + i;
|
|
if (wi < 0 || wi >= walls.Num()) {
|
|
continue;
|
|
}
|
|
|
|
BuildWall& w = walls[wi];
|
|
if (w.point2 < 0 || w.point2 >= walls.Num()) {
|
|
continue;
|
|
}
|
|
BuildWall& w2 = walls[w.point2];
|
|
|
|
const float secCeilA = BuildSurfaceZAt(sec, true, (float)w.x, (float)w.y);
|
|
const float secCeilB = BuildSurfaceZAt(sec, true, (float)w2.x, (float)w2.y);
|
|
const float secFloorA = BuildSurfaceZAt(sec, false, (float)w.x, (float)w.y);
|
|
const float secFloorB = BuildSurfaceZAt(sec, false, (float)w2.x, (float)w2.y);
|
|
|
|
// Solid outer wall. Use the clipped path too, because some Build
|
|
// maps contain nearly-zero endpoint spans on sloped detail sectors.
|
|
if (w.nextsector < 0 || w.nextsector >= sectors.Num()) {
|
|
if (AddWallSpanBrushClipped(wi, secCeilA, secCeilB, secFloorA, secFloorB, w.picnum)) {
|
|
count++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Portal wall. Build has no solid brush here, but the visible upper
|
|
// and lower steps need geometry if neighboring sector heights differ.
|
|
BuildSector& next = sectors[w.nextsector];
|
|
|
|
const float nextCeilA = BuildSurfaceZAt(next, true, (float)w.x, (float)w.y);
|
|
const float nextCeilB = BuildSurfaceZAt(next, true, (float)w2.x, (float)w2.y);
|
|
const float nextFloorA = BuildSurfaceZAt(next, false, (float)w.x, (float)w.y);
|
|
const float nextFloorB = BuildSurfaceZAt(next, false, (float)w2.x, (float)w2.y);
|
|
|
|
// Build z: smaller is higher, larger is lower.
|
|
if (secCeilA < nextCeilA || secCeilB < nextCeilB) {
|
|
if (AddWallSpanBrushClipped(wi, secCeilA, secCeilB, nextCeilA, nextCeilB, w.picnum)) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if (secFloorA > nextFloorA || secFloorB > nextFloorB) {
|
|
if (AddWallSpanBrushClipped(wi, nextFloorA, nextFloorB, secFloorA, secFloorB, w.picnum)) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
// Masked mid texture. This will be solid if enabled, so keep off for
|
|
// gameplay/blocking imports unless you specifically want brush grates.
|
|
if (options.importMaskedWalls && (w.cstat & 16) && w.overpicnum >= 0) {
|
|
const float topA = Max(secCeilA, nextCeilA);
|
|
const float topB = Max(secCeilB, nextCeilB);
|
|
const float botA = Min(secFloorA, nextFloorA);
|
|
const float botB = Min(secFloorB, nextFloorB);
|
|
|
|
if (botA > topA || botB > topB) {
|
|
if (AddWallSpanBrushClipped(wi, topA, topB, botA, botB, w.overpicnum)) {
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/*
|
|
===========================================================================
|
|
Flat triangulation
|
|
===========================================================================
|
|
*/
|
|
|
|
static bool SamePoint2D(const idVec2& a, const idVec2& b) {
|
|
return idMath::Fabs(a.x - b.x) < 0.01f && idMath::Fabs(a.y - b.y) < 0.01f;
|
|
}
|
|
|
|
static float Area2D(const idList<idVec2>& pts) {
|
|
float a = 0.0f;
|
|
for (int i = 0; i < pts.Num(); i++) {
|
|
const idVec2& p = pts[i];
|
|
const idVec2& q = pts[(i + 1) % pts.Num()];
|
|
a += p.x * q.y - q.x * p.y;
|
|
}
|
|
return a * 0.5f;
|
|
}
|
|
|
|
static float Cross2D(const idVec2& a, const idVec2& b, const idVec2& c) {
|
|
const idVec2 ab = b - a;
|
|
const idVec2 ac = c - a;
|
|
return ab.x * ac.y - ab.y * ac.x;
|
|
}
|
|
|
|
static bool PointInTri2D(const idVec2& p, const idVec2& a, const idVec2& b, const idVec2& c) {
|
|
const float c1 = Cross2D(a, b, p);
|
|
const float c2 = Cross2D(b, c, p);
|
|
const float c3 = Cross2D(c, a, p);
|
|
|
|
const bool hasNeg = (c1 < -0.001f) || (c2 < -0.001f) || (c3 < -0.001f);
|
|
const bool hasPos = (c1 > 0.001f) || (c2 > 0.001f) || (c3 > 0.001f);
|
|
|
|
return !(hasNeg && hasPos);
|
|
}
|
|
|
|
static bool TriangulateSimplePolygon(const idList<idVec2>& pts, idList<Tri>& tris) {
|
|
tris.SetNum(0);
|
|
|
|
const int n = pts.Num();
|
|
if (n < 3) {
|
|
return false;
|
|
}
|
|
|
|
idList<int> indices;
|
|
indices.SetNum(n);
|
|
for (int i = 0; i < n; i++) {
|
|
indices[i] = i;
|
|
}
|
|
|
|
const bool ccw = Area2D(pts) > 0.0f;
|
|
|
|
int guard = 0;
|
|
while (indices.Num() > 3 && guard++ < 8192) {
|
|
bool clipped = false;
|
|
|
|
for (int ii = 0; ii < indices.Num(); ii++) {
|
|
const int ip = indices[(ii + indices.Num() - 1) % indices.Num()];
|
|
const int ic = indices[ii];
|
|
const int in = indices[(ii + 1) % indices.Num()];
|
|
|
|
const float cross = Cross2D(pts[ip], pts[ic], pts[in]);
|
|
const bool convex = ccw ? (cross > 0.001f) : (cross < -0.001f);
|
|
|
|
if (!convex) {
|
|
continue;
|
|
}
|
|
|
|
bool contains = false;
|
|
for (int jj = 0; jj < indices.Num(); jj++) {
|
|
const int test = indices[jj];
|
|
if (test == ip || test == ic || test == in) {
|
|
continue;
|
|
}
|
|
|
|
// Hole-bridged polygons intentionally contain duplicated
|
|
// bridge endpoints at different indices. Do not let those
|
|
// duplicated vertices falsely block every possible ear.
|
|
if (SamePoint2D(pts[test], pts[ip]) || SamePoint2D(pts[test], pts[ic]) || SamePoint2D(pts[test], pts[in])) {
|
|
continue;
|
|
}
|
|
|
|
if (PointInTri2D(pts[test], pts[ip], pts[ic], pts[in])) {
|
|
contains = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (contains) {
|
|
continue;
|
|
}
|
|
|
|
Tri t;
|
|
if (ccw) {
|
|
t.a = ip;
|
|
t.b = ic;
|
|
t.c = in;
|
|
}
|
|
else {
|
|
t.a = ip;
|
|
t.b = in;
|
|
t.c = ic;
|
|
}
|
|
|
|
tris.Append(t);
|
|
indices.RemoveIndex(ii);
|
|
clipped = true;
|
|
break;
|
|
}
|
|
|
|
if (!clipped) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (indices.Num() == 3) {
|
|
Tri t;
|
|
if (ccw) {
|
|
t.a = indices[0];
|
|
t.b = indices[1];
|
|
t.c = indices[2];
|
|
}
|
|
else {
|
|
t.a = indices[0];
|
|
t.b = indices[2];
|
|
t.c = indices[1];
|
|
}
|
|
tris.Append(t);
|
|
}
|
|
|
|
return tris.Num() > 0;
|
|
}
|
|
|
|
static float TriangleArea3D(const idVec3& a, const idVec3& b, const idVec3& c) {
|
|
return ((b - a).Cross(c - a)).Length() * 0.5f;
|
|
}
|
|
|
|
bool AddSideFromNormal(
|
|
idList<SideDesc>& sides,
|
|
const idVec3& normalIn,
|
|
const idVec3& pointOnPlane,
|
|
const idVec3& inside,
|
|
const char* material,
|
|
const brushprimit_texdef_t& bp) {
|
|
|
|
idVec3 n = normalIn;
|
|
if (n.Normalize() == 0.0f) {
|
|
runStats.rejectedDegenerateSides++;
|
|
return false;
|
|
}
|
|
|
|
SideDesc s;
|
|
s.plane.SetNormal(n);
|
|
s.plane[3] = -(n * pointOnPlane);
|
|
|
|
// Make absolutely sure normal points away from the brush interior.
|
|
const float d = s.plane.Normal() * inside + s.plane[3];
|
|
if (d > 0.0f) {
|
|
FlipPlane(s.plane);
|
|
}
|
|
|
|
if (!PlaneLooksSane(s.plane)) {
|
|
runStats.rejectedDegenerateSides++;
|
|
return false;
|
|
}
|
|
|
|
// IMPORTANT:
|
|
// Use generated plane points that match the final plane.
|
|
// Do NOT use the tiny real triangle points here; DoomEdit may rebuild
|
|
// the face plane/winding from planepts and lose the surface face.
|
|
PointsForPlane(s.plane, s.planePts);
|
|
|
|
s.material = material ? material : options.caulkMaterial.c_str();
|
|
s.bp = bp;
|
|
|
|
sides.Append(s);
|
|
return true;
|
|
}
|
|
bool AddTriSlabBrush(
|
|
const idVec3& inA,
|
|
const idVec3& inB,
|
|
const idVec3& inC,
|
|
float thickness,
|
|
bool visibleNormalShouldPointUp,
|
|
const char* visibleMaterial,
|
|
const brushprimit_texdef_t& visibleBP) {
|
|
|
|
if (thickness <= 0.1f) {
|
|
thickness = 4.0f;
|
|
}
|
|
|
|
if (TriangleArea3D(inA, inB, inC) < 0.001f) {
|
|
runStats.rejectedFlatTris++;
|
|
return false;
|
|
}
|
|
|
|
idVec3 a = inA;
|
|
idVec3 b = inB;
|
|
idVec3 c = inC;
|
|
|
|
idVec3 n = (b - a).Cross(c - a);
|
|
if (n.Normalize() == 0.0f) {
|
|
runStats.rejectedFlatTris++;
|
|
return false;
|
|
}
|
|
|
|
// Correct triangle winding for the visible side.
|
|
if (visibleNormalShouldPointUp) {
|
|
if (n.z < 0.0f) {
|
|
idVec3 tmp = b;
|
|
b = c;
|
|
c = tmp;
|
|
n = -n;
|
|
}
|
|
}
|
|
else {
|
|
if (n.z > 0.0f) {
|
|
idVec3 tmp = b;
|
|
b = c;
|
|
c = tmp;
|
|
n = -n;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Do NOT extrude along the triangle normal.
|
|
|
|
Doom 3 brush generation is much happier if floor/ceiling triangle
|
|
brushes are vertical columns. Normal extrusion creates tiny angled
|
|
side planes and can numerically clip the main face away, which is the
|
|
exact "outline but no filled middle" symptom.
|
|
*/
|
|
idVec3 verticalOffset;
|
|
|
|
if (visibleNormalShouldPointUp) {
|
|
// Floor: visible top face, solid goes downward.
|
|
verticalOffset.Set(0.0f, 0.0f, -thickness);
|
|
}
|
|
else {
|
|
// Ceiling: visible bottom face, solid goes upward.
|
|
verticalOffset.Set(0.0f, 0.0f, thickness);
|
|
}
|
|
|
|
const idVec3 a2 = a + verticalOffset;
|
|
const idVec3 b2 = b + verticalOffset;
|
|
const idVec3 c2 = c + verticalOffset;
|
|
|
|
const idVec3 center = (a + b + c + a2 + b2 + c2) * (1.0f / 6.0f);
|
|
|
|
brushprimit_texdef_t caulkBP = DefaultMatrix();
|
|
|
|
idList<SideDesc> sides;
|
|
sides.SetNum(0);
|
|
|
|
/*
|
|
Visible surface.
|
|
Use AddSide(), not AddSideFromNormal(), so planepts are real generated
|
|
triangle points.
|
|
*/
|
|
if (!AddSide(
|
|
sides,
|
|
a, b, c,
|
|
center,
|
|
visibleMaterial ? visibleMaterial : options.caulkMaterial.c_str(),
|
|
visibleBP)) {
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
Back face.
|
|
Reverse winding relative to visible face.
|
|
*/
|
|
if (!AddSide(
|
|
sides,
|
|
a2, c2, b2,
|
|
center,
|
|
options.caulkMaterial.c_str(),
|
|
caulkBP)) {
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
Vertical edge faces.
|
|
These are deliberately vertical quads represented by 3 points.
|
|
*/
|
|
|
|
// Edge AB.
|
|
if (!AddSide(
|
|
sides,
|
|
a, a2, b2,
|
|
center,
|
|
options.caulkMaterial.c_str(),
|
|
caulkBP)) {
|
|
return false;
|
|
}
|
|
|
|
// Edge BC.
|
|
if (!AddSide(
|
|
sides,
|
|
b, b2, c2,
|
|
center,
|
|
options.caulkMaterial.c_str(),
|
|
caulkBP)) {
|
|
return false;
|
|
}
|
|
|
|
// Edge CA.
|
|
if (!AddSide(
|
|
sides,
|
|
c, c2, a2,
|
|
center,
|
|
options.caulkMaterial.c_str(),
|
|
caulkBP)) {
|
|
return false;
|
|
}
|
|
|
|
idStr context = va(
|
|
"flatTriColumn material=%s up=%d a=(%.3f %.3f %.3f) b=(%.3f %.3f %.3f) c=(%.3f %.3f %.3f)",
|
|
visibleMaterial ? visibleMaterial : "",
|
|
visibleNormalShouldPointUp ? 1 : 0,
|
|
a.x, a.y, a.z,
|
|
b.x, b.y, b.z,
|
|
c.x, c.y, c.z
|
|
);
|
|
|
|
return AddBrushFromSides(sides, context.c_str());
|
|
}
|
|
|
|
static void CleanLoop(SectorLoop& loop) {
|
|
for (int guard = 0; guard < 8192 && loop.pts2.Num() > 2; guard++) {
|
|
bool removed = false;
|
|
|
|
for (int i = 0; i < loop.pts2.Num(); i++) {
|
|
const int n = (i + 1) % loop.pts2.Num();
|
|
if (SamePoint2D(loop.pts2[i], loop.pts2[n])) {
|
|
loop.pts2.RemoveIndex(n);
|
|
loop.wallIndices.RemoveIndex(n);
|
|
removed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (removed) {
|
|
continue;
|
|
}
|
|
|
|
for (int i = 0; i < loop.pts2.Num(); i++) {
|
|
const int p = (i + loop.pts2.Num() - 1) % loop.pts2.Num();
|
|
const int n = (i + 1) % loop.pts2.Num();
|
|
if (idMath::Fabs(Cross2D(loop.pts2[p], loop.pts2[i], loop.pts2[n])) < 0.001f) {
|
|
loop.pts2.RemoveIndex(i);
|
|
loop.wallIndices.RemoveIndex(i);
|
|
removed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!removed) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
loop.area = loop.pts2.Num() >= 3 ? Area2D(loop.pts2) : 0.0f;
|
|
}
|
|
|
|
bool ExtractSectorLoops(int sectorIndex, idList<SectorLoop>& loops) {
|
|
loops.SetNum(0);
|
|
|
|
if (sectorIndex < 0 || sectorIndex >= sectors.Num()) {
|
|
return false;
|
|
}
|
|
|
|
const BuildSector& sec = sectors[sectorIndex];
|
|
if (sec.wallptr < 0 || sec.wallnum < 3 || sec.wallptr + sec.wallnum > walls.Num()) {
|
|
return false;
|
|
}
|
|
|
|
idList<int> used;
|
|
used.SetNum(sec.wallnum);
|
|
for (int i = 0; i < used.Num(); i++) {
|
|
used[i] = 0;
|
|
}
|
|
|
|
for (int startOfs = 0; startOfs < sec.wallnum; startOfs++) {
|
|
if (used[startOfs]) {
|
|
continue;
|
|
}
|
|
|
|
const int startWall = sec.wallptr + startOfs;
|
|
int wi = startWall;
|
|
bool closed = false;
|
|
|
|
SectorLoop loop;
|
|
loop.wallIndices.SetNum(0);
|
|
loop.pts2.SetNum(0);
|
|
loop.area = 0.0f;
|
|
|
|
for (int guard = 0; guard <= sec.wallnum; guard++) {
|
|
if (wi < sec.wallptr || wi >= sec.wallptr + sec.wallnum) {
|
|
break;
|
|
}
|
|
|
|
const int local = wi - sec.wallptr;
|
|
if (used[local]) {
|
|
if (wi == startWall) {
|
|
closed = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
used[local] = 1;
|
|
|
|
const BuildWall& w = walls[wi];
|
|
const idVec3 p = BuildPointToDoomFloat((float)w.x, (float)w.y, 0.0f);
|
|
loop.wallIndices.Append(wi);
|
|
loop.pts2.Append(idVec2(p.x, p.y));
|
|
|
|
if (w.point2 == startWall) {
|
|
closed = true;
|
|
break;
|
|
}
|
|
|
|
wi = w.point2;
|
|
}
|
|
|
|
CleanLoop(loop);
|
|
|
|
if (closed && loop.pts2.Num() >= 3 && idMath::Fabs(loop.area) > 0.01f) {
|
|
loops.Append(loop);
|
|
}
|
|
}
|
|
|
|
return loops.Num() > 0;
|
|
}
|
|
|
|
static bool PointOnSegment2D(const idVec2& a, const idVec2& p, const idVec2& b) {
|
|
if (idMath::Fabs(Cross2D(a, b, p)) > 0.01f) {
|
|
return false;
|
|
}
|
|
if (p.x < Min(a.x, b.x) - 0.01f || p.x > Max(a.x, b.x) + 0.01f) {
|
|
return false;
|
|
}
|
|
if (p.y < Min(a.y, b.y) - 0.01f || p.y > Max(a.y, b.y) + 0.01f) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool SegmentIntersects2D(const idVec2& a, const idVec2& b, const idVec2& c, const idVec2& d) {
|
|
const float o1 = Cross2D(a, b, c);
|
|
const float o2 = Cross2D(a, b, d);
|
|
const float o3 = Cross2D(c, d, a);
|
|
const float o4 = Cross2D(c, d, b);
|
|
const float eps = 0.01f;
|
|
|
|
if (((o1 > eps && o2 < -eps) || (o1 < -eps && o2 > eps)) &&
|
|
((o3 > eps && o4 < -eps) || (o3 < -eps && o4 > eps))) {
|
|
return true;
|
|
}
|
|
|
|
if (idMath::Fabs(o1) <= eps && PointOnSegment2D(a, c, b)) {
|
|
return true;
|
|
}
|
|
if (idMath::Fabs(o2) <= eps && PointOnSegment2D(a, d, b)) {
|
|
return true;
|
|
}
|
|
if (idMath::Fabs(o3) <= eps && PointOnSegment2D(c, a, d)) {
|
|
return true;
|
|
}
|
|
if (idMath::Fabs(o4) <= eps && PointOnSegment2D(c, b, d)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool PointInLoop2D(const idVec2& p, const SectorLoop& loop) {
|
|
const idList<idVec2>& pts = loop.pts2;
|
|
if (pts.Num() < 3) {
|
|
return false;
|
|
}
|
|
|
|
bool inside = false;
|
|
for (int i = 0, j = pts.Num() - 1; i < pts.Num(); j = i++) {
|
|
const idVec2& a = pts[i];
|
|
const idVec2& b = pts[j];
|
|
|
|
if (PointOnSegment2D(a, p, b)) {
|
|
return true;
|
|
}
|
|
|
|
if (((a.y > p.y) != (b.y > p.y))) {
|
|
const float x = (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x;
|
|
if (p.x < x) {
|
|
inside = !inside;
|
|
}
|
|
}
|
|
}
|
|
return inside;
|
|
}
|
|
|
|
static float Area2DFlat(const idList<FlatPolyPoint>& pts) {
|
|
float a = 0.0f;
|
|
for (int i = 0; i < pts.Num(); i++) {
|
|
const idVec2& p = pts[i].p;
|
|
const idVec2& q = pts[(i + 1) % pts.Num()].p;
|
|
a += p.x * q.y - q.x * p.y;
|
|
}
|
|
return a * 0.5f;
|
|
}
|
|
|
|
static void ReverseFlatPoly(idList<FlatPolyPoint>& pts) {
|
|
const int n = pts.Num();
|
|
for (int i = 0; i < n / 2; i++) {
|
|
FlatPolyPoint tmp = pts[i];
|
|
pts[i] = pts[n - 1 - i];
|
|
pts[n - 1 - i] = tmp;
|
|
}
|
|
}
|
|
|
|
static void FlatPolyToVec2(const idList<FlatPolyPoint>& in, idList<idVec2>& out) {
|
|
out.SetNum(0);
|
|
for (int i = 0; i < in.Num(); i++) {
|
|
out.Append(in[i].p);
|
|
}
|
|
}
|
|
|
|
bool LoopToFlatPoly(const SectorLoop& loop, idList<FlatPolyPoint>& out) const {
|
|
out.SetNum(0);
|
|
if (loop.wallIndices.Num() != loop.pts2.Num()) {
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < loop.wallIndices.Num(); i++) {
|
|
const int wi = loop.wallIndices[i];
|
|
if (wi < 0 || wi >= walls.Num()) {
|
|
return false;
|
|
}
|
|
|
|
const BuildWall& w = walls[wi];
|
|
FlatPolyPoint fp;
|
|
fp.p = loop.pts2[i];
|
|
fp.buildX = w.x;
|
|
fp.buildY = w.y;
|
|
out.Append(fp);
|
|
}
|
|
|
|
return out.Num() >= 3;
|
|
}
|
|
|
|
static bool BridgeSegmentClearForPoly(const idList<FlatPolyPoint>& poly, const idVec2& a, const idVec2& b) {
|
|
for (int i = 0; i < poly.Num(); i++) {
|
|
const idVec2& c = poly[i].p;
|
|
const idVec2& d = poly[(i + 1) % poly.Num()].p;
|
|
|
|
// Intersections at the intended bridge endpoint are allowed.
|
|
if (SamePoint2D(c, b) || SamePoint2D(d, b) || SamePoint2D(c, a) || SamePoint2D(d, a)) {
|
|
continue;
|
|
}
|
|
|
|
if (SegmentIntersects2D(a, b, c, d)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool CanBridgeHoleToPoly(const idList<FlatPolyPoint>& poly, const idList<FlatPolyPoint>& hole, int polyIndex, int holeIndex) {
|
|
if (polyIndex < 0 || polyIndex >= poly.Num() || holeIndex < 0 || holeIndex >= hole.Num()) {
|
|
return false;
|
|
}
|
|
|
|
const idVec2 a = hole[holeIndex].p;
|
|
const idVec2 b = poly[polyIndex].p;
|
|
if (SamePoint2D(a, b)) {
|
|
return false;
|
|
}
|
|
|
|
if (!BridgeSegmentClearForPoly(poly, a, b)) {
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < hole.Num(); i++) {
|
|
const idVec2& c = hole[i].p;
|
|
const idVec2& d = hole[(i + 1) % hole.Num()].p;
|
|
|
|
// Intersections at the hole bridge endpoint are allowed.
|
|
if (SamePoint2D(c, a) || SamePoint2D(d, a) || SamePoint2D(c, b) || SamePoint2D(d, b)) {
|
|
continue;
|
|
}
|
|
|
|
if (SegmentIntersects2D(a, b, c, d)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool BridgeHoleIntoPolygon(idList<FlatPolyPoint>& poly, const idList<FlatPolyPoint>& hole) {
|
|
if (poly.Num() < 3 || hole.Num() < 3) {
|
|
return false;
|
|
}
|
|
|
|
int holeIndex = 0;
|
|
for (int i = 1; i < hole.Num(); i++) {
|
|
if (hole[i].p.x > hole[holeIndex].p.x ||
|
|
(idMath::Fabs(hole[i].p.x - hole[holeIndex].p.x) < 0.01f && hole[i].p.y < hole[holeIndex].p.y)) {
|
|
holeIndex = i;
|
|
}
|
|
}
|
|
|
|
int bestPolyIndex = -1;
|
|
float bestDistSqr = 1.0e30f;
|
|
|
|
// Prefer a rightward bridge from the hole's rightmost point. If that
|
|
// fails because the polygon is oddly shaped, do a second pass allowing
|
|
// any visible vertex. E1L1's multi-loop sectors are small enough that
|
|
// this O(n^2) visibility test is fine.
|
|
for (int pass = 0; pass < 2; pass++) {
|
|
for (int i = 0; i < poly.Num(); i++) {
|
|
if (pass == 0 && poly[i].p.x < hole[holeIndex].p.x - 0.01f) {
|
|
continue;
|
|
}
|
|
if (!CanBridgeHoleToPoly(poly, hole, i, holeIndex)) {
|
|
continue;
|
|
}
|
|
|
|
const idVec2 d = poly[i].p - hole[holeIndex].p;
|
|
const float ds = d.x * d.x + d.y * d.y;
|
|
if (ds < bestDistSqr) {
|
|
bestDistSqr = ds;
|
|
bestPolyIndex = i;
|
|
}
|
|
}
|
|
if (bestPolyIndex >= 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bestPolyIndex < 0) {
|
|
return false;
|
|
}
|
|
|
|
idList<FlatPolyPoint> merged;
|
|
merged.SetNum(0);
|
|
|
|
for (int i = 0; i <= bestPolyIndex; i++) {
|
|
merged.Append(poly[i]);
|
|
}
|
|
|
|
// The duplicate hole and poly endpoints create the two sides of the
|
|
// bridge. TriangulateSimplePolygon skips duplicated bridge endpoints
|
|
// when testing whether an ear contains another vertex.
|
|
for (int k = 0; k < hole.Num(); k++) {
|
|
merged.Append(hole[(holeIndex + k) % hole.Num()]);
|
|
}
|
|
merged.Append(hole[holeIndex]);
|
|
merged.Append(poly[bestPolyIndex]);
|
|
|
|
for (int i = bestPolyIndex + 1; i < poly.Num(); i++) {
|
|
merged.Append(poly[i]);
|
|
}
|
|
|
|
poly = merged;
|
|
return true;
|
|
}
|
|
|
|
int AddFlatTrianglesForPoly(const BuildSector& sec, const idList<FlatPolyPoint>& poly, const idList<Tri>& tris) {
|
|
idList<idVec3> floorPts;
|
|
idList<idVec3> ceilPts;
|
|
floorPts.SetNum(0);
|
|
ceilPts.SetNum(0);
|
|
|
|
for (int i = 0; i < poly.Num(); i++) {
|
|
floorPts.Append(BuildSurfacePointToDoom(sec, false, poly[i].buildX, poly[i].buildY));
|
|
ceilPts.Append(BuildSurfacePointToDoom(sec, true, poly[i].buildX, poly[i].buildY));
|
|
}
|
|
|
|
int count = 0;
|
|
|
|
if (options.importFloors) {
|
|
idStr floorMat = TileMaterialName(sec.floorpicnum);
|
|
|
|
for (int i = 0; i < tris.Num(); i++) {
|
|
const idVec3& a = floorPts[tris[i].a];
|
|
const idVec3& b = floorPts[tris[i].b];
|
|
const idVec3& c = floorPts[tris[i].c];
|
|
|
|
idPlane p;
|
|
if (!p.FromPoints(a, b, c, false)) {
|
|
runStats.rejectedFlatTris++;
|
|
continue;
|
|
}
|
|
|
|
// Floor visible face should point up.
|
|
if (p.Normal().z < 0.0f) {
|
|
FlipPlane(p);
|
|
}
|
|
|
|
brushprimit_texdef_t bp = MakeFlatUV(sec, false, p.Normal());
|
|
if (AddTriSlabBrush(a, b, c, options.floorCeilingThickness, true, floorMat.c_str(), bp)) {
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.importCeilings) {
|
|
idStr ceilMat = TileMaterialName(sec.ceilingpicnum);
|
|
|
|
for (int i = 0; i < tris.Num(); i++) {
|
|
const idVec3& a = ceilPts[tris[i].a];
|
|
const idVec3& b = ceilPts[tris[i].b];
|
|
const idVec3& c = ceilPts[tris[i].c];
|
|
|
|
idPlane p;
|
|
if (!p.FromPoints(a, b, c, false)) {
|
|
runStats.rejectedFlatTris++;
|
|
continue;
|
|
}
|
|
|
|
// Ceiling visible face should point downward.
|
|
if (p.Normal().z > 0.0f) {
|
|
FlipPlane(p);
|
|
}
|
|
|
|
brushprimit_texdef_t bp = MakeFlatUV(sec, true, p.Normal());
|
|
if (AddTriSlabBrush(a, b, c, options.floorCeilingThickness, false, ceilMat.c_str(), bp)) {
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
int ImportSectorFlats(int sectorIndex) {
|
|
BuildSector& sec = sectors[sectorIndex];
|
|
|
|
idList<SectorLoop> loops;
|
|
if (!ExtractSectorLoops(sectorIndex, loops)) {
|
|
common->Printf("DukeBuildMapImporter: no valid wall loops for sector %d\n", sectorIndex);
|
|
return 0;
|
|
}
|
|
|
|
idList<int> depth;
|
|
idList<int> parent;
|
|
depth.SetNum(loops.Num());
|
|
parent.SetNum(loops.Num());
|
|
|
|
for (int i = 0; i < loops.Num(); i++) {
|
|
depth[i] = 0;
|
|
parent[i] = -1;
|
|
float parentArea = 1.0e30f;
|
|
|
|
if (loops[i].pts2.Num() <= 0) {
|
|
continue;
|
|
}
|
|
|
|
const idVec2 testPoint = loops[i].pts2[0];
|
|
const float myArea = idMath::Fabs(loops[i].area);
|
|
|
|
for (int j = 0; j < loops.Num(); j++) {
|
|
if (i == j) {
|
|
continue;
|
|
}
|
|
|
|
const float otherArea = idMath::Fabs(loops[j].area);
|
|
if (otherArea <= myArea + 0.01f) {
|
|
continue;
|
|
}
|
|
|
|
if (PointInLoop2D(testPoint, loops[j])) {
|
|
depth[i]++;
|
|
if (otherArea < parentArea) {
|
|
parentArea = otherArea;
|
|
parent[i] = j;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int count = 0;
|
|
|
|
for (int l = 0; l < loops.Num(); l++) {
|
|
if ((depth[l] & 1) != 0) {
|
|
// Odd-depth loops are holes. They are bridged into their parent
|
|
// even-depth island below, not imported as independent floors.
|
|
continue;
|
|
}
|
|
|
|
idList<FlatPolyPoint> poly;
|
|
if (!LoopToFlatPoly(loops[l], poly)) {
|
|
continue;
|
|
}
|
|
|
|
// Work in CCW order for hole bridging. Triangle winding is corrected
|
|
// per floor/ceiling face after 3D slope sampling.
|
|
if (Area2DFlat(poly) < 0.0f) {
|
|
ReverseFlatPoly(poly);
|
|
}
|
|
|
|
int bridgedHoles = 0;
|
|
int failedHoleBridges = 0;
|
|
for (int h = 0; h < loops.Num(); h++) {
|
|
if (parent[h] != l || (depth[h] & 1) == 0) {
|
|
continue;
|
|
}
|
|
|
|
idList<FlatPolyPoint> hole;
|
|
if (!LoopToFlatPoly(loops[h], hole)) {
|
|
continue;
|
|
}
|
|
|
|
// Holes are traversed clockwise inside the CCW outer loop.
|
|
if (Area2DFlat(hole) > 0.0f) {
|
|
ReverseFlatPoly(hole);
|
|
}
|
|
|
|
if (BridgeHoleIntoPolygon(poly, hole)) {
|
|
bridgedHoles++;
|
|
}
|
|
else {
|
|
failedHoleBridges++;
|
|
}
|
|
}
|
|
|
|
idList<idVec2> pts2;
|
|
FlatPolyToVec2(poly, pts2);
|
|
|
|
idList<Tri> tris;
|
|
if (!TriangulateSimplePolygon(pts2, tris)) {
|
|
common->Printf(
|
|
"DukeBuildMapImporter: could not triangulate sector %d loop %d after hole bridging, holes=%d failed=%d\n",
|
|
sectorIndex,
|
|
l,
|
|
bridgedHoles,
|
|
failedHoleBridges
|
|
);
|
|
runStats.rejectedFlatTris++;
|
|
continue;
|
|
}
|
|
|
|
count += AddFlatTrianglesForPoly(sec, poly, tris);
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
};
|
|
|
|
void DoImportDukeBuildMap(const char* fileName) {
|
|
idDukeBuildMapImporter importer;
|
|
idDukeBuildMapImporter::Options opt;
|
|
|
|
/*
|
|
Good defaults.
|
|
*/
|
|
opt.mapScale = 1.0f / 8.0f;
|
|
opt.zScale = opt.mapScale / 16.0f;
|
|
|
|
opt.wallThickness = 2.0f;
|
|
opt.floorCeilingThickness = 8.0f;
|
|
|
|
opt.importWalls = true;
|
|
opt.importFloors = true;
|
|
opt.importCeilings = true;
|
|
opt.importMaskedWalls = false;
|
|
|
|
opt.tileMaterialPrefix = "textures/duke3d/tiles";
|
|
opt.caulkMaterial = "textures/common/caulk";
|
|
|
|
/*
|
|
Texture tuning.
|
|
*/
|
|
opt.wallRepeatBase = 8.0f;
|
|
opt.flatRepeatBase = 16.0f;
|
|
|
|
if (!importer.ImportIntoCurrentRadiantMap(fileName, opt)) {
|
|
common->Warning(
|
|
"Duke MAP import failed: %s",
|
|
importer.GetLastError()
|
|
);
|
|
}
|
|
}
|