diff --git a/neo/engine/doomdll.vcxproj.user b/neo/engine/doomdll.vcxproj.user index 27c8058f..0d14ea58 100644 --- a/neo/engine/doomdll.vcxproj.user +++ b/neo/engine/doomdll.vcxproj.user @@ -17,7 +17,7 @@ WindowsLocalDebugger +set r_fullscreen 0 +set r_mode 1 D:\projects\Doom3 - +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 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| D:\projects\Doom3\Quake4.exe diff --git a/neo/engine/idlib/MapFile.cpp b/neo/engine/idlib/MapFile.cpp index 21609bbd..8cca1e06 100644 --- a/neo/engine/idlib/MapFile.cpp +++ b/neo/engine/idlib/MapFile.cpp @@ -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 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& 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& 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& 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& 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& 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, ×tamp, 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& 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(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 textureTokens; + idList 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(); diff --git a/neo/engine/renderer/draw_dx.cpp b/neo/engine/renderer/draw_dx.cpp index 14b4df25..f64008e7 100644 --- a/neo/engine/renderer/draw_dx.cpp +++ b/neo/engine/renderer/draw_dx.cpp @@ -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;