Files
DoomRTX/neo/engine/tools/radiant/DukeMapImporter.cpp
T
Justin Marshall 8c4a087aa9 Filesystem update.
2026-05-09 07:54:14 -07:00

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()
);
}
}