bloodrun-editor/q3map2/q3map2/bspfile_ibsp.cpp
serge_shubin 89b825ece5 Add L2 spherical harmonics light grid to q3map2
Implements a new SH light grid that runs alongside the legacy Q3 light grid,
storing 9 RGB L2 spherical harmonic coefficients per grid point for accurate
directional lighting of dynamic objects from all angles.

BSP format: v47 with 19-lump header (160 bytes) when -sh is used, v46 with
17-lump header (144 bytes) otherwise. SH data stored in LUMP_LIGHTGRID_SH
(index 18) with a header containing grid bounds/size/mins followed by the
coefficient array. Stock Q3 engines read v46 lumps unchanged.

New CLI flags: -sh (enable), -gridscalesh (density multiplier, default 2x),
-gridsh (explicit cell size). SH grid receives bounced light with -bouncegrid.

Also adds libjpeg-turbo as a proper build dependency with its own vcxproj,
fixing the previous external engine path requirement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:28:28 +08:00

434 lines
14 KiB
C++
Executable file

/* -------------------------------------------------------------------------------
Copyright (C) 1999-2007 id Software, Inc. and contributors.
For a list of contributors, see the accompanying CONTRIBUTORS file.
This file is part of GtkRadiant.
GtkRadiant is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
GtkRadiant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with GtkRadiant; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
----------------------------------------------------------------------------------
This code has been altered significantly from its original form, to support
several games based on the Quake III Arena engine, in the form of "Q3Map2."
------------------------------------------------------------------------------- */
/* dependencies */
#include "q3map2.h"
#include "bspfile_abstract.h"
#include <ctime>
/* -------------------------------------------------------------------------------
this file handles translating the bsp file format used by quake 3, rtcw, and ef
into the abstracted bsp file used by q3map2.
------------------------------------------------------------------------------- */
/* constants */
#define LUMP_ENTITIES 0
#define LUMP_SHADERS 1
#define LUMP_PLANES 2
#define LUMP_NODES 3
#define LUMP_LEAFS 4
#define LUMP_LEAFSURFACES 5
#define LUMP_LEAFBRUSHES 6
#define LUMP_MODELS 7
#define LUMP_BRUSHES 8
#define LUMP_BRUSHSIDES 9
#define LUMP_DRAWVERTS 10
#define LUMP_DRAWINDEXES 11
#define LUMP_FOGS 12
#define LUMP_SURFACES 13
#define LUMP_LIGHTMAPS 14
#define LUMP_LIGHTGRID 15
#define LUMP_VISIBILITY 16
#define LUMP_ADVERTISEMENTS 17
#define LUMP_LIGHTGRID_SH 18
#define HEADER_LUMPS_V46 17 /* standard Q3: lumps 0-16, header = 144 bytes */
#define HEADER_LUMPS_V47 19 /* extended: lumps 0-18, header = 160 bytes */
#define HEADER_LUMPS_MAX 19 /* max for internal use */
#define IBSP_VERSION_V46 46
#define IBSP_VERSION_V47 47
/* types */
struct ibspHeader_v46_t
{
char ident[ 4 ];
int version;
bspLump_t lumps[ HEADER_LUMPS_V46 ];
};
struct ibspHeader_v47_t
{
char ident[ 4 ];
int version;
bspLump_t lumps[ HEADER_LUMPS_V47 ];
};
/* internal header -- always uses max lump count for convenience */
struct ibspHeader_t
{
char ident[ 4 ];
int version;
bspLump_t lumps[ HEADER_LUMPS_MAX ];
};
/* brush sides */
struct ibspBrushSide_t
{
int planeNum;
int shaderNum;
ibspBrushSide_t( const bspBrushSide_t& other ) :
planeNum ( other.planeNum ),
shaderNum( other.shaderNum ){}
operator bspBrushSide_t() const {
return { planeNum, shaderNum, -1 };
}
};
/* drawsurfaces */
struct ibspDrawSurface_t
{
int shaderNum;
int fogNum;
bspSurfaceType_t surfaceType;
int firstVert;
int numVerts;
int firstIndex;
int numIndexes;
int lightmapNum;
int lightmapX, lightmapY;
int lightmapWidth, lightmapHeight;
Vector3 lightmapOrigin;
std::array<Vector3, 3> lightmapVecs;
int patchWidth;
int patchHeight;
ibspDrawSurface_t( const bspDrawSurface_t& other ) :
shaderNum ( other.shaderNum ),
fogNum ( other.fogNum ),
surfaceType ( other.surfaceType ),
firstVert ( other.firstVert ),
numVerts ( other.numVerts ),
firstIndex ( other.firstIndex ),
numIndexes ( other.numIndexes ),
lightmapNum ( other.lightmapNum[0] ),
lightmapX ( other.lightmapX[0] ),
lightmapY ( other.lightmapY[0] ),
lightmapWidth ( other.lightmapWidth ),
lightmapHeight( other.lightmapHeight ),
lightmapOrigin( other.lightmapOrigin ),
lightmapVecs ( other.lightmapVecs ),
patchWidth ( other.patchWidth ),
patchHeight ( other.patchHeight )
{}
operator bspDrawSurface_t() const {
return bspDrawSurface_t{
.shaderNum = shaderNum,
.fogNum = fogNum,
.surfaceType = surfaceType,
.firstVert = firstVert,
.numVerts = numVerts,
.firstIndex = firstIndex,
.numIndexes = numIndexes,
.lightmapStyles { LS_NORMAL, LS_NONE, LS_NONE, LS_NONE },
.vertexStyles { LS_NORMAL, LS_NONE, LS_NONE, LS_NONE },
.lightmapNum { lightmapNum, LIGHTMAP_BY_VERTEX, LIGHTMAP_BY_VERTEX, LIGHTMAP_BY_VERTEX },
.lightmapX { lightmapX, 0, 0, 0 },
.lightmapY { lightmapY, 0, 0, 0 },
.lightmapWidth = lightmapWidth,
.lightmapHeight = lightmapHeight,
.lightmapOrigin = lightmapOrigin,
.lightmapVecs = lightmapVecs,
.patchWidth = patchWidth,
.patchHeight = patchHeight
};
}
};
/* drawverts */
struct ibspDrawVert_t
{
Vector3 xyz;
Vector2 st;
Vector2 lightmap;
Vector3 normal;
Color4b color;
ibspDrawVert_t( const bspDrawVert_t& other ) :
xyz ( other.xyz ),
st ( other.st ),
lightmap( other.lightmap[0] ),
normal ( other.normal ),
color ( other.color[0] )
{}
operator bspDrawVert_t() const {
return bspDrawVert_t{
.xyz = xyz,
.st = st,
.lightmap{ lightmap, Vector2( 0 ), Vector2( 0 ), Vector2( 0 ) },
.normal = normal,
.color { color, Color4b( 0 ), Color4b( 0 ), Color4b( 0 ) }
};
}
};
/* light grid */
struct ibspGridPoint_t
{
Vector3b ambient;
Vector3b directed;
byte latLong[ 2 ];
ibspGridPoint_t( const bspGridPoint_t& other ) :
ambient ( other.ambient[0] ),
directed( other.directed[0] ),
latLong { other.latLong[0], other.latLong[1] }
{}
operator bspGridPoint_t() const {
return bspGridPoint_t{
.ambient = makeArray4( ambient ),
.directed = makeArray4( directed ),
.styles { LS_NORMAL, LS_NONE, LS_NONE, LS_NONE },
.latLong { latLong[0], latLong[1] }
};
}
};
/*
LoadIBSPFile()
loads a quake 3 bsp file into memory
*/
void LoadIBSPFile( const char *filename ){
/* load the file */
MemBuffer file = LoadFile( filename );
/* read ident and version to determine header size */
const byte *data = (const byte *)file.data();
const int version = LittleLong( *(const int *)( data + 4 ) );
/* make sure it matches the format we're trying to load */
if ( !force && memcmp( data, g_game->bspIdent, 4 ) ) {
Error( "%s is not a %s file", filename, g_game->bspIdent );
}
if ( !force && version != IBSP_VERSION_V46 && version != IBSP_VERSION_V47 ) {
Error( "%s is version %d, expected %d or %d", filename, version, IBSP_VERSION_V46, IBSP_VERSION_V47 );
}
/* determine lump count from version */
const int numLumps = ( version == IBSP_VERSION_V47 ) ? HEADER_LUMPS_V47 : HEADER_LUMPS_V46;
/* CopyLump uses (byte*)header + offset to access file data,
so we overlay a bspHeader_t at the file buffer start and
copy/swap the lump directory into it. bspHeader_t has lumps[100]
so it can hold all our lumps. */
bspHeader_t *header = (bspHeader_t *)file.data();
/* swap just the lump directory entries we have */
SwapBlock( (int *)( (byte *)header + 8 ), numLumps * sizeof( bspLump_t ) );
/* note: lumps beyond numLumps are not zeroed -- they overlap file data
in the buffer. We only access extended lumps under version checks. */
/* load/convert standard lumps (0-16) */
CopyLump( header, LUMP_SHADERS, bspShaders );
CopyLump( header, LUMP_MODELS, bspModels );
CopyLump( header, LUMP_PLANES, bspPlanes );
CopyLump( header, LUMP_LEAFS, bspLeafs );
CopyLump( header, LUMP_NODES, bspNodes );
CopyLump( header, LUMP_LEAFSURFACES, bspLeafSurfaces );
CopyLump( header, LUMP_LEAFBRUSHES, bspLeafBrushes );
CopyLump( header, LUMP_BRUSHES, bspBrushes );
CopyLump<bspBrushSide_t, ibspBrushSide_t>( header, LUMP_BRUSHSIDES, bspBrushSides );
CopyLump<bspDrawVert_t, ibspDrawVert_t>( header, LUMP_DRAWVERTS, bspDrawVerts );
CopyLump<bspDrawSurface_t, ibspDrawSurface_t>( header, LUMP_SURFACES, bspDrawSurfaces );
CopyLump( header, LUMP_FOGS, bspFogs );
CopyLump( header, LUMP_DRAWINDEXES, bspDrawIndexes );
CopyLump( header, LUMP_VISIBILITY, bspVisBytes );
CopyLump( header, LUMP_LIGHTMAPS, bspLightBytes );
CopyLump( header, LUMP_ENTITIES, bspEntData );
CopyLump<bspGridPoint_t, ibspGridPoint_t>( header, LUMP_LIGHTGRID, bspGridPoints );
/* v47 extended lumps (17-18) */
if ( version == IBSP_VERSION_V47 ) {
/* advertisements */
CopyLump( header, LUMP_ADVERTISEMENTS, bspAds );
/* SH light grid */
const int length = header->lumps[LUMP_LIGHTGRID_SH].length;
const int offset = header->lumps[LUMP_LIGHTGRID_SH].offset;
if ( length > (int)sizeof( bspGridSHHeader_t ) ) {
const bspGridSHHeader_t *shHeader = (const bspGridSHHeader_t *)( data + offset );
gridMinsSH = shHeader->gridMins;
gridSizeSH = shHeader->gridSize;
gridBoundsSH[0] = shHeader->gridBounds[0];
gridBoundsSH[1] = shHeader->gridBounds[1];
gridBoundsSH[2] = shHeader->gridBounds[2];
const int numPoints = shHeader->numPoints;
const bspGridPointSH_t *points = (const bspGridPointSH_t *)( data + offset + sizeof( bspGridSHHeader_t ) );
bspGridPointsSH.assign( points, points + numPoints );
}
else {
bspGridPointsSH.clear();
}
}
else {
bspAds.clear();
bspGridPointsSH.clear();
}
}
/*
LoadIBSPorRBSPFilePartially()
loads bsp file parts meaningful for autopacker
*/
void LoadIBSPorRBSPFilePartially( const char *filename ){
/* load the file */
MemBuffer file = LoadFile( filename );
const byte *data = (const byte *)file.data();
const int version = LittleLong( *(const int *)( data + 4 ) );
/* make sure it matches the format we're trying to load */
if ( !force && memcmp( data, g_game->bspIdent, 4 ) ) {
Error( "%s is not a %s file", filename, g_game->bspIdent );
}
if ( !force && version != IBSP_VERSION_V46 && version != IBSP_VERSION_V47 ) {
Error( "%s is version %d, expected %d or %d", filename, version, IBSP_VERSION_V46, IBSP_VERSION_V47 );
}
const int numLumps = ( version == IBSP_VERSION_V47 ) ? HEADER_LUMPS_V47 : HEADER_LUMPS_V46;
bspHeader_t *header = (bspHeader_t *)file.data();
SwapBlock( (int *)( (byte *)header + 8 ), numLumps * sizeof( bspLump_t ) );
for ( int i = numLumps; i < 100; ++i )
header->lumps[i] = {};
/* load/convert lumps */
CopyLump( header, LUMP_SHADERS, bspShaders );
if( g_game->load == LoadIBSPFile )
CopyLump<bspDrawSurface_t, ibspDrawSurface_t>( header, LUMP_SURFACES, bspDrawSurfaces );
else
CopyLump( header, LUMP_SURFACES, bspDrawSurfaces );
CopyLump( header, LUMP_FOGS, bspFogs );
CopyLump( header, LUMP_ENTITIES, bspEntData );
}
/*
WriteIBSPFile()
writes an id bsp file
*/
void WriteIBSPFile( const char *filename ){
/* determine version: v47 if SH data exists, v46 otherwise */
const bool extendedBSP = !bspGridPointsSH.empty();
const int bspVersion = extendedBSP ? IBSP_VERSION_V47 : IBSP_VERSION_V46;
const int numLumps = extendedBSP ? HEADER_LUMPS_V47 : HEADER_LUMPS_V46;
const int headerSize = 8 + numLumps * (int)sizeof( bspLump_t );
ibspHeader_t header{};
//% Swapfile();
/* set up header */
memcpy( header.ident, g_game->bspIdent, 4 );
header.version = LittleLong( bspVersion );
/* write initial header (placeholder, overwritten at the end) */
FILE *file = SafeOpenWrite( filename );
SafeWrite( file, &header, headerSize );
{ /* add marker lump */
time_t t;
time( &t );
/* asctime adds an implicit trailing \n */
const auto marker = StringStream( "I LOVE MY Q3MAP2 " Q3MAP_VERSION " on ", asctime( localtime( &t ) ) );
AddLump( file, header.lumps[0], std::vector<char>( marker.cbegin(), marker.cend() + 1 ) );
}
/* add standard lumps (0-16) */
AddLump( file, header.lumps[LUMP_SHADERS], bspShaders );
AddLump( file, header.lumps[LUMP_PLANES], bspPlanes );
AddLump( file, header.lumps[LUMP_LEAFS], bspLeafs );
AddLump( file, header.lumps[LUMP_NODES], bspNodes );
AddLump( file, header.lumps[LUMP_BRUSHES], bspBrushes );
AddLump( file, header.lumps[LUMP_BRUSHSIDES], std::vector<ibspBrushSide_t>( bspBrushSides.begin(), bspBrushSides.end() ) );
AddLump( file, header.lumps[LUMP_LEAFSURFACES], bspLeafSurfaces );
AddLump( file, header.lumps[LUMP_LEAFBRUSHES], bspLeafBrushes );
AddLump( file, header.lumps[LUMP_MODELS], bspModels );
AddLump( file, header.lumps[LUMP_DRAWVERTS], std::vector<ibspDrawVert_t>( bspDrawVerts.begin(), bspDrawVerts.end() ) );
AddLump( file, header.lumps[LUMP_SURFACES], std::vector<ibspDrawSurface_t>( bspDrawSurfaces.begin(), bspDrawSurfaces.end() ) );
AddLump( file, header.lumps[LUMP_VISIBILITY], bspVisBytes );
AddLump( file, header.lumps[LUMP_LIGHTMAPS], bspLightBytes );
AddLump( file, header.lumps[LUMP_LIGHTGRID], std::vector<ibspGridPoint_t>( bspGridPoints.begin(), bspGridPoints.end() ) );
AddLump( file, header.lumps[LUMP_ENTITIES], bspEntData );
AddLump( file, header.lumps[LUMP_FOGS], bspFogs );
AddLump( file, header.lumps[LUMP_DRAWINDEXES], bspDrawIndexes );
/* v47 extended lumps (17-18) */
if ( extendedBSP ) {
/* advertisements */
AddLump( file, header.lumps[LUMP_ADVERTISEMENTS], bspAds );
/* SH light grid -- header + points packed into a byte lump */
const size_t shHeaderSz = sizeof( bspGridSHHeader_t );
const size_t dataSz = bspGridPointsSH.size() * sizeof( bspGridPointSH_t );
std::vector<byte> shLump( shHeaderSz + dataSz );
bspGridSHHeader_t shHeader;
shHeader.gridMins = gridMinsSH;
shHeader.gridSize = gridSizeSH;
shHeader.gridBounds[0] = gridBoundsSH[0];
shHeader.gridBounds[1] = gridBoundsSH[1];
shHeader.gridBounds[2] = gridBoundsSH[2];
shHeader.numPoints = (int)bspGridPointsSH.size();
memcpy( shLump.data(), &shHeader, shHeaderSz );
memcpy( shLump.data() + shHeaderSz, bspGridPointsSH.data(), dataSz );
AddLump( file, header.lumps[LUMP_LIGHTGRID_SH], shLump );
}
/* emit bsp size */
const int size = ftell( file );
Sys_Printf( "Wrote %.1f MB (%d bytes)\n", (float) size / ( 1024 * 1024 ), size );
/* write the completed header */
fseek( file, 0, SEEK_SET );
SafeWrite( file, &header, headerSize );
/* close the file */
fclose( file );
}