Added Quake 1 map loading support, adjusted lighting.

This commit is contained in:
Justin Marshall
2026-05-11 14:15:30 -07:00
parent 13b73a3c01
commit 01c93a2742
3 changed files with 604 additions and 2 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
<LocalDebuggerCommandArguments>+set r_fullscreen 0 +set r_mode 1</LocalDebuggerCommandArguments>
<LocalDebuggerWorkingDirectory>D:\projects\Doom3</LocalDebuggerWorkingDirectory>
<LocalDebuggerCommandArgumentsHistory>+set r_fullscreen 0 +set r_mode 6|+set r_fullscreen 1 +set r_mode 6|+set r_fullscreen 1 +set r_mode 1|+set r_fullscreen 0 +set r_mode 1|</LocalDebuggerCommandArgumentsHistory>
<LocalDebuggerCommandArgumentsHistory>+set r_fullscreen 0 +set r_mode 6|+set r_fullscreen 1 +set r_mode 6|+set r_fullscreen 1 +set r_mode 1|+set r_fullscreen 0 +set r_mode 1 +set fs_game q1base|+set r_fullscreen 0 +set r_mode 1|</LocalDebuggerCommandArgumentsHistory>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Q4 Debug|x64'">
<LocalDebuggerCommand>D:\projects\Doom3\Quake4.exe</LocalDebuggerCommand>
+602
View File
@@ -112,6 +112,544 @@ static void ComputeAxisBase(const idVec3& normal, idVec3& texS, idVec3& texT) {
#endif
}
/*
=================
Quake 1 .map compatibility helpers
Quake 1 and Valve 220 maps use the same outer entity/brush braces as the
legacy Quake 3 parser, but their texture projection data must be interpreted
as Quake axial vectors instead of the existing Quake 3 fallback texture matrix.
The detector below enables this mode only for files that advertise Quake 1 or
Valve 220 metadata, so the original Quake 3 fallback path remains unchanged.
=================
*/
static bool g_parseQuake1Map = false;
typedef struct quake1MapTextureSizeCacheEntry_s {
idStr name;
int width;
int height;
} quake1MapTextureSizeCacheEntry_t;
static idList<quake1MapTextureSizeCacheEntry_t> g_quake1MapTextureSizeCache;
/*
=================
Quake1MapTokenToFloat
=================
*/
static bool Quake1MapTokenToFloat(const idToken& token, float& value) {
char* end;
const char* str;
str = token.c_str();
if (!str || !str[0]) {
return false;
}
value = (float)strtod(str, &end);
return end != str && *end == '\0';
}
/*
=================
Quake1MapReadFloatToken
idLexer may emit a negative number as either one token, "-1", or as a
punctuation sign followed by a number, "-" "1". Quake .map texture scales
commonly use negative values, so parse both forms from a token list.
=================
*/
static bool Quake1MapReadFloatToken(const idList<idToken>& tokens, int& index, float& value) {
float parsed;
float sign;
if (index >= tokens.Num()) {
return false;
}
sign = 1.0f;
if (tokens[index] == "-" || tokens[index] == "+") {
if (tokens[index] == "-") {
sign = -1.0f;
}
index++;
if (index >= tokens.Num()) {
return false;
}
}
if (!Quake1MapTokenToFloat(tokens[index], parsed)) {
return false;
}
value = sign * parsed;
index++;
return true;
}
/*
=================
Quake1MapTokensAreFloatSuffix
=================
*/
static bool Quake1MapTokensAreFloatSuffix(const idList<idToken>& tokens, int first, int minimumFloats) {
int index, count;
float value;
index = first;
count = 0;
while (index < tokens.Num()) {
if (!Quake1MapReadFloatToken(tokens, index, value)) {
return false;
}
count++;
}
return count >= minimumFloats;
}
/*
=================
JoinQuake1MapTextureName
Quake texture names may begin with punctuation, for example +0WALL or *WATER.
Collecting the whole texture-name token run avoids treating those prefixes as
separate syntax.
=================
*/
static void JoinQuake1MapTextureName(const idList<idToken>& tokens, int first, int count, idStr& material) {
int i;
material.Clear();
for (i = 0; i < count; i++) {
material += tokens[first + i].c_str();
}
}
/*
=================
FindQuake1MapStandardTextureParms
Classic Quake faces end with:
textureName offsetX offsetY rotation scaleX scaleY [optional numeric flags]
Return the first token of that numeric suffix.
=================
*/
static int FindQuake1MapStandardTextureParms(const idList<idToken>& tokens) {
int i;
for (i = 1; i < tokens.Num(); i++) {
if (Quake1MapTokensAreFloatSuffix(tokens, i, 5)) {
return i;
}
}
return -1;
}
/*
=================
ParseQuake1MapValve220Vector
=================
*/
static bool ParseQuake1MapValve220Vector(const idList<idToken>& tokens, int& index, idVec3& axis, float& offset) {
float v[4];
int i;
if (index >= tokens.Num() || tokens[index] != "[") {
return false;
}
index++;
for (i = 0; i < 4; i++) {
if (!Quake1MapReadFloatToken(tokens, index, v[i])) {
return false;
}
}
if (index >= tokens.Num() || tokens[index] != "]") {
return false;
}
index++;
axis[0] = v[0];
axis[1] = v[1];
axis[2] = v[2];
offset = v[3];
return true;
}
/*
=================
Quake1MapTextureAxisFromPlane
This is the historical dominant-axis basis used by Quake-family map compilers
for the classic texture tuple.
=================
*/
static void Quake1MapTextureAxisFromPlane(const idVec3& normal, idVec3& texS, idVec3& texT) {
static const float baseAxis[18][3] = {
{ 0, 0, 1 }, { 1, 0, 0 }, { 0, -1, 0 },
{ 0, 0, -1 }, { 1, 0, 0 }, { 0, -1, 0 },
{ 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, -1 },
{ -1, 0, 0 }, { 0, 1, 0 }, { 0, 0, -1 },
{ 0, 1, 0 }, { 1, 0, 0 }, { 0, 0, -1 },
{ 0, -1, 0 }, { 1, 0, 0 }, { 0, 0, -1 }
};
int i, bestAxis;
float best, dot;
best = 0.0f;
bestAxis = 0;
for (i = 0; i < 6; i++) {
dot = normal[0] * baseAxis[i * 3][0] + normal[1] * baseAxis[i * 3][1] + normal[2] * baseAxis[i * 3][2];
if (dot > best) {
best = dot;
bestAxis = i;
}
}
for (i = 0; i < 3; i++) {
texS[i] = baseAxis[bestAxis * 3 + 1][i];
texT[i] = baseAxis[bestAxis * 3 + 2][i];
}
}
/*
=================
RotateQuake1MapTextureAxes
=================
*/
static void RotateQuake1MapTextureAxes(idVec3 axes[2], float rotate) {
int i, sv, tv;
float angle, sinv, cosv, ns, nt;
angle = rotate * (3.14159265358979323846f / 180.0f);
if (rotate == 0.0f) {
sinv = 0.0f;
cosv = 1.0f;
}
else if (rotate == 90.0f) {
sinv = 1.0f;
cosv = 0.0f;
}
else if (rotate == 180.0f) {
sinv = 0.0f;
cosv = -1.0f;
}
else if (rotate == 270.0f) {
sinv = -1.0f;
cosv = 0.0f;
}
else {
#ifdef QUAKE4
sinv = idMath::Sin(angle);
cosv = idMath::Cos(angle);
#else
sinv = sin(angle);
cosv = cos(angle);
#endif
}
if (axes[0][0]) {
sv = 0;
}
else if (axes[0][1]) {
sv = 1;
}
else {
sv = 2;
}
if (axes[1][0]) {
tv = 0;
}
else if (axes[1][1]) {
tv = 1;
}
else {
tv = 2;
}
for (i = 0; i < 2; i++) {
ns = cosv * axes[i][sv] - sinv * axes[i][tv];
nt = sinv * axes[i][sv] + cosv * axes[i][tv];
axes[i][sv] = ns;
axes[i][tv] = nt;
}
}
/*
=================
ClearQuake1MapTextureSizeCache
=================
*/
static void ClearQuake1MapTextureSizeCache() {
g_quake1MapTextureSizeCache.Clear();
}
/*
=================
GetQuake1MapTextureDimensions
Quake texture shifts and scales are stored in texture pixels. Convert them
to Doom 3 brushDef UV space by normalizing S by the real texture width and T
by the real texture height. renderSystem->LoadImage accepts names without an
extension and resolves the actual image file. Missing textures fall back to
32x32, preserving the old hardcoded behavior for unresolved assets.
=================
*/
static bool GetQuake1MapTextureDimensions(const idStr& materialName, int& width, int& height) {
int i;
byte* pic;
ID_TIME_T timestamp;
quake1MapTextureSizeCacheEntry_t entry;
for (i = 0; i < g_quake1MapTextureSizeCache.Num(); i++) {
if (!g_quake1MapTextureSizeCache[i].name.Icmp(materialName)) {
width = g_quake1MapTextureSizeCache[i].width;
height = g_quake1MapTextureSizeCache[i].height;
return width > 0 && height > 0;
}
}
width = 0;
height = 0;
pic = NULL;
timestamp = 0;
if (renderSystem) {
renderSystem->LoadImage(materialName.c_str(), &pic, &width, &height, &timestamp, false);
}
if (pic) {
Mem_Free(pic);
}
if (width <= 0 || height <= 0) {
width = 32;
height = 32;
}
entry.name = materialName;
entry.width = width;
entry.height = height;
g_quake1MapTextureSizeCache.Append(entry);
return width > 0 && height > 0;
}
/*
=================
GetQuake1MapTextureUnitScales
=================
*/
static void GetQuake1MapTextureUnitScales(const idStr& materialName, float& sUnitScale, float& tUnitScale) {
int textureWidth, textureHeight;
GetQuake1MapTextureDimensions(materialName, textureWidth, textureHeight);
sUnitScale = 1.0f / (float)textureWidth;
tUnitScale = 1.0f / (float)textureHeight;
}
/*
=================
BuildQuake1MapTextureMatrix
Convert Quake world-space texture vectors into the brushDef texture matrix
basis used by idMapBrushSide::GetTextureVectors. Any component along the
plane normal is folded into the constant offset because all points on the face
share the same normal distance.
=================
*/
static void BuildQuake1MapTextureMatrix(const idPlane& plane, const idVec3& origin, const idVec3& sVecIn, float sOffsetIn, const idVec3& tVecIn, float tOffsetIn, idVec3 texMat[2]) {
idVec3 normal, texX, texY, sVec, tVec;
float sNormal, tNormal, planeDistance;
int i;
normal = plane.Normal();
ComputeAxisBase(normal, texX, texY);
sNormal = sVecIn * normal;
tNormal = tVecIn * normal;
for (i = 0; i < 3; i++) {
sVec[i] = sVecIn[i] - normal[i] * sNormal;
tVec[i] = tVecIn[i] - normal[i] * tNormal;
}
planeDistance = -plane[3] + (origin * normal);
texMat[0][0] = sVec * texX;
texMat[0][1] = sVec * texY;
texMat[0][2] = sOffsetIn + sNormal * planeDistance;
texMat[1][0] = tVec * texX;
texMat[1][1] = tVec * texY;
texMat[1][2] = tOffsetIn + tNormal * planeDistance;
}
/*
=================
SetQuake1MapMaterial
=================
*/
static void SetQuake1MapMaterial(idStr& out, const idStr& material) {
// Keep the old id map convention for legacy texture names.
out = "textures/";
out += material;
}
/*
=================
ParseQuake1MapTextureInfo
Supports both classic Quake 1 faces:
TEXTURE offsetX offsetY rotation scaleX scaleY
and Valve 220 faces:
TEXTURE [ ux uy uz offsetX ] [ vx vy vz offsetY ] rotation scaleX scaleY
=================
*/
static bool ParseQuake1MapTextureInfo(const idList<idToken>& tokens, const idPlane& plane, const idVec3& origin, idStr& materialOut, idVec3 texMat[2]) {
idStr material;
int i, j, index, parmIndex;
float rotate, scale[2], shift[2], sUnitScale, tUnitScale;
idVec3 axes[2];
if (tokens.Num() < 6) {
return false;
}
for (i = 0; i < tokens.Num(); i++) {
if (tokens[i] == "[") {
JoinQuake1MapTextureName(tokens, 0, i, material);
if (material.Length() == 0) {
return false;
}
index = i;
if (!ParseQuake1MapValve220Vector(tokens, index, axes[0], shift[0]) ||
!ParseQuake1MapValve220Vector(tokens, index, axes[1], shift[1]) ||
!Quake1MapReadFloatToken(tokens, index, rotate) ||
!Quake1MapReadFloatToken(tokens, index, scale[0]) ||
!Quake1MapReadFloatToken(tokens, index, scale[1])) {
return false;
}
if (scale[0] == 0.0f) {
scale[0] = 1.0f;
}
if (scale[1] == 0.0f) {
scale[1] = 1.0f;
}
SetQuake1MapMaterial(materialOut, material);
GetQuake1MapTextureUnitScales(materialOut, sUnitScale, tUnitScale);
// Valve 220 stores the texture axes directly. The rotation token is an
// editor compatibility field and should not be applied again here.
for (j = 0; j < 3; j++) {
axes[0][j] *= sUnitScale / scale[0];
axes[1][j] *= tUnitScale / scale[1];
}
shift[0] *= sUnitScale;
shift[1] *= tUnitScale;
BuildQuake1MapTextureMatrix(plane, origin, axes[0], shift[0], axes[1], shift[1], texMat);
return true;
}
}
parmIndex = FindQuake1MapStandardTextureParms(tokens);
if (parmIndex <= 0) {
return false;
}
JoinQuake1MapTextureName(tokens, 0, parmIndex, material);
index = parmIndex;
if (material.Length() == 0 ||
!Quake1MapReadFloatToken(tokens, index, shift[0]) ||
!Quake1MapReadFloatToken(tokens, index, shift[1]) ||
!Quake1MapReadFloatToken(tokens, index, rotate) ||
!Quake1MapReadFloatToken(tokens, index, scale[0]) ||
!Quake1MapReadFloatToken(tokens, index, scale[1])) {
return false;
}
if (scale[0] == 0.0f) {
scale[0] = 1.0f;
}
if (scale[1] == 0.0f) {
scale[1] = 1.0f;
}
SetQuake1MapMaterial(materialOut, material);
GetQuake1MapTextureUnitScales(materialOut, sUnitScale, tUnitScale);
Quake1MapTextureAxisFromPlane(plane.Normal(), axes[0], axes[1]);
RotateQuake1MapTextureAxes(axes, rotate);
for (i = 0; i < 3; i++) {
axes[0][i] *= sUnitScale / scale[0];
axes[1][i] *= tUnitScale / scale[1];
}
shift[0] *= sUnitScale;
shift[1] *= tUnitScale;
BuildQuake1MapTextureMatrix(plane, origin, axes[0], shift[0], axes[1], shift[1], texMat);
return true;
}
/*
=================
DetectQuake1MapFile
This deliberately avoids classifying every old brace-only map as Quake 1,
because that would change existing Quake 3 fallback behavior. Instead it
requires common Quake 1/Valve220 signals: a wad key, mapversion 220, TrenchBroom
texture metadata, or Valve 220 bracketed face axes.
=================
*/
static bool DetectQuake1MapFile(const idStr& fullName, bool osPath) {
#ifdef QUAKE4
idAutoPtr<Lexer> lexer(LexerFactory::MakeLexer(LEXFL_NOSTRINGCONCAT | LEXFL_NOSTRINGESCAPECHARS | LEXFL_ALLOWPATHNAMES));
Lexer& detectSrc(*lexer);
#else
idLexer detectSrc(LEXFL_NOSTRINGCONCAT | LEXFL_NOSTRINGESCAPECHARS | LEXFL_ALLOWPATHNAMES);
#endif
idToken token;
idToken value;
detectSrc.LoadFile(fullName, osPath);
if (!detectSrc.IsLoaded()) {
return false;
}
// idTech map files advertise their format with a top-level Version token;
// Quake 1 maps do not.
if (detectSrc.CheckTokenString("Version")) {
return false;
}
while (detectSrc.ReadToken(&token)) {
if (token == "[") {
return true;
}
if (!token.Icmp("wad") || !token.Icmp("_tb_textures")) {
return true;
}
if (!token.Icmp("mapversion")) {
if (detectSrc.ReadTokenOnLine(&value) && atoi(value.c_str()) == 220) {
return true;
}
}
}
return false;
}
#ifdef QUAKE4
/*
===============
@@ -507,6 +1045,63 @@ idMapBrush* idMapBrush::ParseQ3(Lexer & src, const idVec3 & origin) {
#else
idMapBrush* idMapBrush::ParseQ3(idLexer & src, const idVec3 & origin) {
#endif
if (g_parseQuake1Map) {
int i;
idVec3 planepts[3];
idToken token;
idList<idToken> textureTokens;
idList<idMapBrushSide*> sides;
idMapBrushSide* side;
idDict epairs;
do {
if (src.CheckTokenString("}")) {
break;
}
side = new idMapBrushSide();
sides.Append(side);
// read the three point plane definition
if (!src.Parse1DMatrix(3, planepts[0].ToFloatPtr()) ||
!src.Parse1DMatrix(3, planepts[1].ToFloatPtr()) ||
!src.Parse1DMatrix(3, planepts[2].ToFloatPtr())) {
src.Error("idMapBrush::ParseQ3: unable to read Quake 1 brush side plane definition");
sides.DeleteContents(true);
return NULL;
}
planepts[0] -= origin;
planepts[1] -= origin;
planepts[2] -= origin;
side->plane.FromPoints(planepts[0], planepts[1], planepts[2]);
side->origin = origin;
// Read the rest of the face line. Quake texture names may begin with
// punctuation, so keep the whole line and identify the numeric suffix.
textureTokens.Clear();
while (src.ReadTokenOnLine(&token)) {
textureTokens.Append(token);
}
if (!ParseQuake1MapTextureInfo(textureTokens, side->plane, origin, side->material, side->texMat)) {
src.Error("idMapBrush::ParseQ3: unable to read Quake 1 brush side texture info");
sides.DeleteContents(true);
return NULL;
}
} while (1);
idMapBrush* brush = new idMapBrush();
for (i = 0; i < sides.Num(); i++) {
brush->AddSide(sides[i]);
}
brush->epairs = epairs;
return brush;
}
int i, shift[2], rotate;
float scale[2];
idVec3 planepts[3];
@@ -925,6 +1520,8 @@ bool idMapFile::Parse(const char* filename, bool ignoreRegion, bool osPath) {
name.StripFileExtension();
fullName = name;
hasPrimitiveData = false;
g_parseQuake1Map = false;
ClearQuake1MapTextureSizeCache();
#ifdef QUAKE4
mHasExportEntities = false;
mHasFuncGroups = false;
@@ -953,6 +1550,7 @@ bool idMapFile::Parse(const char* filename, bool ignoreRegion, bool osPath) {
version = OLD_MAP_VERSION;
fileTime = src.GetFileTime();
entities.DeleteContents(true);
g_parseQuake1Map = DetectQuake1MapFile(fullName, osPath);
if (src.CheckTokenString("Version")) {
src.ReadTokenOnLine(&token);
@@ -1050,6 +1648,8 @@ bool idMapFile::Parse(const char* filename, bool ignoreRegion, bool osPath) {
#endif
hasPrimitiveData = true;
g_parseQuake1Map = false;
ClearQuake1MapTextureSizeCache();
return true;
}
@@ -1382,6 +1982,8 @@ bool idMapFile::ParseExport(const char* filename, bool osPath)
numMerged = numNotMerged = numAdded = numRemoved = 0;
mHasExportEntities = false;
g_parseQuake1Map = false;
ClearQuake1MapTextureSizeCache();
fullName = filename;
fullName.StripFileExtension();
+1 -1
View File
@@ -176,7 +176,7 @@ void RB_DXDrawInteractions(void)
const float r = srcLight.shaderParms[SHADERPARM_RED];
const float g = srcLight.shaderParms[SHADERPARM_GREEN];
const float b = srcLight.shaderParms[SHADERPARM_BLUE];
const float intensity = 4.0f;
const float intensity = 6.0f;
glRaytracingLight_t light = {};
bool supported = true;