Files
DoomRTX/neo/engine/models/TerrainModel.cpp
T
2026-05-28 17:15:52 -07:00

579 lines
17 KiB
C++

/*
===========================================================================
Shared terrain source model
===========================================================================
*/
#include "precompiled.h"
#pragma hdrstop
#include "TerrainModel.h"
idTerrainPatch::idTerrainPatch() {
name.Clear();
material.Clear();
origin.Zero();
width = TERRAIN_DEFAULT_VERTS;
height = TERRAIN_DEFAULT_VERTS;
cellSize = TERRAIN_DEFAULT_CELL_SIZE;
paintWidth = TERRAIN_DEFAULT_PAINT_SIZE;
paintHeight = TERRAIN_DEFAULT_PAINT_SIZE;
paintTexture = 0;
paintTextureDirty = true;
paintDirtyMinX = 0;
paintDirtyMinY = 0;
paintDirtyMaxX = paintWidth - 1;
paintDirtyMaxY = paintHeight - 1;
bounds.Clear();
}
int idTerrainPatch::SampleIndex( int x, int y ) const {
return y * width + x;
}
int idTerrainPatch::PaintIndex( int x, int y ) const {
return y * paintWidth + x;
}
float idTerrainPatch::SampleHeight( int x, int y ) const {
return heights[SampleIndex( x, y )];
}
void idTerrainPatch::SetHeight( int x, int y, float height ) {
heights[SampleIndex( x, y )] = height;
}
bool idTerrainPatch::SetHeightBySample( int sample, float height ) {
if ( sample < 0 || sample >= heights.Num() ) {
return false;
}
heights[sample] = height;
return true;
}
idVec4 idTerrainPatch::SamplePaintWeight( int x, int y ) const {
return paintWeights[PaintIndex( x, y )];
}
void idTerrainPatch::SetPaintWeight( int x, int y, const idVec4 &weight ) {
paintWeights[PaintIndex( x, y )] = NormalizeLayerWeight( weight );
MarkPaintTextureDirtyRect( x, y, x, y );
}
bool idTerrainPatch::SetPaintWeightBySample( int sample, const idVec4 &weight ) {
if ( sample < 0 || sample >= paintWeights.Num() || paintWidth <= 0 ) {
return false;
}
paintWeights[sample] = NormalizeLayerWeight( weight );
MarkPaintTextureDirtyRect( sample % paintWidth, sample / paintWidth, sample % paintWidth, sample / paintWidth );
return true;
}
idVec4 idTerrainPatch::BaseLayerWeight() {
return idVec4( 1.0f, 0.0f, 0.0f, 0.0f );
}
idVec4 idTerrainPatch::NormalizeLayerWeight( const idVec4 &weight ) {
idVec4 normalized;
normalized.x = weight.x > 0.0f ? weight.x : 0.0f;
normalized.y = weight.y > 0.0f ? weight.y : 0.0f;
normalized.z = weight.z > 0.0f ? weight.z : 0.0f;
normalized.w = weight.w > 0.0f ? weight.w : 0.0f;
const float total = normalized.x + normalized.y + normalized.z + normalized.w;
if ( total <= 0.0001f ) {
return BaseLayerWeight();
}
const float invTotal = 1.0f / total;
normalized.x *= invTotal;
normalized.y *= invTotal;
normalized.z *= invTotal;
normalized.w *= invTotal;
return normalized;
}
float idTerrainPatch::WorldWidth() const {
return ( width - 1 ) * cellSize;
}
float idTerrainPatch::WorldHeight() const {
return ( height - 1 ) * cellSize;
}
float idTerrainPatch::BilinearHeight( float u, float v ) const {
u = idMath::ClampFloat( 0.0f, 1.0f, u );
v = idMath::ClampFloat( 0.0f, 1.0f, v );
const float fx = u * ( width - 1 );
const float fy = v * ( height - 1 );
const int x0 = idMath::ClampInt( 0, width - 1, (int)floorf( fx ) );
const int y0 = idMath::ClampInt( 0, height - 1, (int)floorf( fy ) );
const int x1 = idMath::ClampInt( 0, width - 1, x0 + 1 );
const int y1 = idMath::ClampInt( 0, height - 1, y0 + 1 );
const float tx = fx - (float)x0;
const float ty = fy - (float)y0;
const float h00 = SampleHeight( x0, y0 );
const float h10 = SampleHeight( x1, y0 );
const float h01 = SampleHeight( x0, y1 );
const float h11 = SampleHeight( x1, y1 );
const float h0 = h00 + ( h10 - h00 ) * tx;
const float h1 = h01 + ( h11 - h01 ) * tx;
return h0 + ( h1 - h0 ) * ty;
}
idVec3 idTerrainPatch::VertexUV( float u, float v ) const {
idVec3 p;
p.x = origin.x + WorldWidth() * u;
p.y = origin.y + WorldHeight() * v;
p.z = origin.z + BilinearHeight( u, v );
return p;
}
idVec3 idTerrainPatch::Vertex( int x, int y ) const {
idVec3 v;
v.x = origin.x + x * cellSize;
v.y = origin.y + y * cellSize;
v.z = origin.z + SampleHeight( x, y );
return v;
}
idVec3 idTerrainPatch::VertexNormal( int x, int y ) const {
const int x0 = idMath::ClampInt( 0, width - 1, x - 1 );
const int x1 = idMath::ClampInt( 0, width - 1, x + 1 );
const int y0 = idMath::ClampInt( 0, height - 1, y - 1 );
const int y1 = idMath::ClampInt( 0, height - 1, y + 1 );
const idVec3 sx = Vertex( x1, y ) - Vertex( x0, y );
const idVec3 sy = Vertex( x, y1 ) - Vertex( x, y0 );
idVec3 normal = sx.Cross( sy );
if ( normal.LengthSqr() <= 0.0001f ) {
normal.Set( 0.0f, 0.0f, 1.0f );
} else {
normal.Normalize();
}
return normal;
}
void idTerrainPatch::UpdateBounds() {
bounds.Clear();
if ( width <= 0 || height <= 0 ) {
bounds.AddPoint( origin );
return;
}
for ( int y = 0; y < height; y++ ) {
for ( int x = 0; x < width; x++ ) {
bounds.AddPoint( Vertex( x, y ) );
}
}
}
void idTerrainPatch::EnsureLayerData( const char *defaultMaterial ) {
if ( material.IsEmpty() ) {
material = defaultMaterial && defaultMaterial[0] ? defaultMaterial : "textures/common/caulk";
}
if ( layerMaterials[0].IsEmpty() ) {
layerMaterials[0] = material;
}
material = layerMaterials[0];
if ( width < 2 ) {
width = 2;
}
if ( height < 2 ) {
height = 2;
}
const int heightSampleCount = width * height;
const int oldHeightCount = heights.Num();
heights.SetNum( heightSampleCount );
for ( int i = oldHeightCount; i < heightSampleCount; i++ ) {
heights[i] = 0.0f;
}
if ( paintWidth < 2 ) {
paintWidth = TERRAIN_DEFAULT_PAINT_SIZE;
}
if ( paintHeight < 2 ) {
paintHeight = TERRAIN_DEFAULT_PAINT_SIZE;
}
const int paintSampleCount = paintWidth * paintHeight;
if ( paintSampleCount <= 0 ) {
paintWeights.Clear();
return;
}
const int oldCount = paintWeights.Num();
paintWeights.SetNum( paintSampleCount );
if ( oldCount != paintSampleCount ) {
MarkPaintTextureDirty();
}
for ( int i = 0; i < paintSampleCount; i++ ) {
if ( i >= oldCount ) {
paintWeights[i] = BaseLayerWeight();
} else {
paintWeights[i] = NormalizeLayerWeight( paintWeights[i] );
}
}
UpdateBounds();
}
void idTerrainPatch::ClearLayerWeight( int layer ) {
if ( layer < 0 || layer >= TERRAIN_MAX_LAYERS ) {
return;
}
for ( int i = 0; i < paintWeights.Num(); i++ ) {
idVec4 weight = paintWeights[i];
weight[layer] = 0.0f;
paintWeights[i] = NormalizeLayerWeight( weight );
}
MarkPaintTextureDirty();
}
bool idTerrainPatch::SetLayerMaterials( const idStr newLayerMaterials[TERRAIN_MAX_LAYERS], const char *defaultMaterial ) {
bool changed = false;
for ( int layer = 0; layer < TERRAIN_MAX_LAYERS; layer++ ) {
idStr materialName = newLayerMaterials[layer];
materialName.StripLeading( ' ' );
materialName.StripTrailing( ' ' );
if ( layer == 0 && materialName.IsEmpty() ) {
materialName = defaultMaterial && defaultMaterial[0] ? defaultMaterial : "textures/common/caulk";
}
if ( layerMaterials[layer].Icmp( materialName.c_str() ) == 0 ) {
continue;
}
layerMaterials[layer] = materialName;
if ( layer > 0 && materialName.IsEmpty() ) {
ClearLayerWeight( layer );
}
changed = true;
}
material = layerMaterials[0];
return changed;
}
void idTerrainPatch::MarkPaintTextureDirty() {
paintTextureDirty = true;
paintDirtyMinX = 0;
paintDirtyMinY = 0;
paintDirtyMaxX = paintWidth > 0 ? paintWidth - 1 : 0;
paintDirtyMaxY = paintHeight > 0 ? paintHeight - 1 : 0;
}
void idTerrainPatch::ClearPaintTextureDirtyRect() {
paintTextureDirty = false;
paintDirtyMinX = paintWidth;
paintDirtyMinY = paintHeight;
paintDirtyMaxX = -1;
paintDirtyMaxY = -1;
}
void idTerrainPatch::MarkPaintTextureDirtyRect( int minX, int minY, int maxX, int maxY ) {
if ( paintWidth <= 0 || paintHeight <= 0 ) {
return;
}
minX = idMath::ClampInt( 0, paintWidth - 1, minX );
maxX = idMath::ClampInt( 0, paintWidth - 1, maxX );
minY = idMath::ClampInt( 0, paintHeight - 1, minY );
maxY = idMath::ClampInt( 0, paintHeight - 1, maxY );
if ( maxX < minX || maxY < minY ) {
return;
}
if ( !paintTextureDirty || paintDirtyMaxX < paintDirtyMinX || paintDirtyMaxY < paintDirtyMinY ) {
paintDirtyMinX = minX;
paintDirtyMinY = minY;
paintDirtyMaxX = maxX;
paintDirtyMaxY = maxY;
} else {
if ( minX < paintDirtyMinX ) {
paintDirtyMinX = minX;
}
if ( minY < paintDirtyMinY ) {
paintDirtyMinY = minY;
}
if ( maxX > paintDirtyMaxX ) {
paintDirtyMaxX = maxX;
}
if ( maxY > paintDirtyMaxY ) {
paintDirtyMaxY = maxY;
}
}
paintTextureDirty = true;
}
idTerrainModel::idTerrainModel() {
}
idTerrainModel::~idTerrainModel() {
Clear();
}
void idTerrainModel::Clear() {
terrains.DeleteContents( true );
}
int idTerrainModel::NumTerrains() const {
return terrains.Num();
}
idTerrainPatch *idTerrainModel::Terrain( int index ) {
if ( index < 0 || index >= terrains.Num() ) {
return NULL;
}
return terrains[index];
}
const idTerrainPatch *idTerrainModel::Terrain( int index ) const {
if ( index < 0 || index >= terrains.Num() ) {
return NULL;
}
return terrains[index];
}
idList<idTerrainPatch *> &idTerrainModel::Terrains() {
return terrains;
}
const idList<idTerrainPatch *> &idTerrainModel::Terrains() const {
return terrains;
}
static float TerrainModel_SnapToGrid( float value, float gridSize ) {
if ( gridSize <= 0.0f ) {
gridSize = 1.0f;
}
return floorf( value / gridSize + ( value >= 0.0f ? 0.5f : -0.5f ) ) * gridSize;
}
idTerrainPatch *idTerrainModel::AllocDefaultTerrain( const idVec3 &center, const char *defaultMaterial, float gridSize ) {
idTerrainPatch *terrain = new idTerrainPatch;
terrain->name = va( "terrain_%i", terrains.Num() + 1 );
terrain->material = defaultMaterial && defaultMaterial[0] ? defaultMaterial : "textures/common/caulk";
terrain->layerMaterials[0] = terrain->material;
const float worldSize = ( terrain->width - 1 ) * terrain->cellSize;
terrain->origin.x = TerrainModel_SnapToGrid( center.x - worldSize * 0.5f, gridSize );
terrain->origin.y = TerrainModel_SnapToGrid( center.y - worldSize * 0.5f, gridSize );
terrain->origin.z = TerrainModel_SnapToGrid( center.z, gridSize );
terrain->heights.SetNum( terrain->width * terrain->height );
for ( int i = 0; i < terrain->heights.Num(); i++ ) {
terrain->heights[i] = 0.0f;
}
terrain->EnsureLayerData( defaultMaterial );
return terrain;
}
void idTerrainModel::AddTerrain( idTerrainPatch *terrain ) {
if ( terrain ) {
terrains.Append( terrain );
}
}
idStr idTerrainModel::FileNameForMap( const char *mapFileName ) {
idStr fileName = mapFileName ? mapFileName : "";
fileName.StripFileExtension();
fileName.SetFileExtension( ".terrain" );
return fileName;
}
bool idTerrainModel::SaveFile( const char *filename ) {
if ( !filename || !filename[0] ) {
return false;
}
const bool osPath = strstr( filename, ":" ) != NULL;
idFile *file = osPath ? fileSystem->OpenExplicitFileWrite( filename ) : fileSystem->OpenFileWrite( filename );
if ( !file ) {
common->Warning( "idTerrainModel::SaveFile: couldn't write %s", filename );
return false;
}
file->WriteFloatString( "terrainAuthoring 3 {\n" );
for ( int i = 0; i < terrains.Num(); i++ ) {
idTerrainPatch *terrain = terrains[i];
if ( !terrain ) {
continue;
}
terrain->EnsureLayerData( "textures/common/caulk" );
file->WriteFloatString( "\tterrain {\n" );
file->WriteFloatString( "\t\tname \"%s\"\n", terrain->name.c_str() );
file->WriteFloatString( "\t\tmaterial \"%s\"\n", terrain->material.c_str() );
for ( int layer = 0; layer < TERRAIN_MAX_LAYERS; layer++ ) {
if ( layer == 0 || !terrain->layerMaterials[layer].IsEmpty() ) {
file->WriteFloatString( "\t\tlayer %i \"%s\"\n", layer, terrain->layerMaterials[layer].c_str() );
}
}
file->WriteFloatString( "\t\torigin ( %f %f %f )\n", terrain->origin.x, terrain->origin.y, terrain->origin.z );
file->WriteFloatString( "\t\tsize %i %i\n", terrain->width, terrain->height );
file->WriteFloatString( "\t\tcellSize %f\n", terrain->cellSize );
file->WriteFloatString( "\t\tpaintSize %i %i\n", terrain->paintWidth, terrain->paintHeight );
file->WriteFloatString( "\t\theights {\n" );
for ( int y = 0; y < terrain->height; y++ ) {
file->WriteFloatString( "\t\t\t" );
for ( int x = 0; x < terrain->width; x++ ) {
file->WriteFloatString( "%f ", terrain->SampleHeight( x, y ) );
}
file->WriteFloatString( "\n" );
}
file->WriteFloatString( "\t\t}\n" );
file->WriteFloatString( "\t\tpaintWeights {\n" );
for ( int y = 0; y < terrain->paintHeight; y++ ) {
file->WriteFloatString( "\t\t\t" );
for ( int x = 0; x < terrain->paintWidth; x++ ) {
const idVec4 weights = terrain->SamplePaintWeight( x, y );
file->WriteFloatString( "%f %f %f %f ", weights.x, weights.y, weights.z, weights.w );
}
file->WriteFloatString( "\n" );
}
file->WriteFloatString( "\t\t}\n" );
file->WriteFloatString( "\t}\n" );
}
file->WriteFloatString( "}\n" );
fileSystem->CloseFile( file );
return true;
}
bool idTerrainModel::LoadFile( const char *filename, const char *defaultMaterial ) {
Clear();
if ( !filename || !filename[0] ) {
return false;
}
const char *fallbackMaterial = defaultMaterial && defaultMaterial[0] ? defaultMaterial : "textures/common/caulk";
const bool osPath = strstr( filename, ":" ) != NULL;
idLexer src( LEXFL_NOSTRINGCONCAT | LEXFL_NODOLLARPRECOMPILE | LEXFL_ALLOWPATHNAMES );
src.LoadFile( filename, osPath );
if ( !src.IsLoaded() ) {
return false;
}
idToken token;
if ( !src.ReadToken( &token ) || token.Icmp( "terrainAuthoring" ) ) {
return false;
}
src.ReadToken( &token );
if ( token != "{" ) {
src.ExpectTokenString( "{" );
}
while ( src.ReadToken( &token ) ) {
if ( token == "}" ) {
break;
}
if ( token.Icmp( "terrain" ) ) {
break;
}
idTerrainPatch *terrain = new idTerrainPatch;
terrain->name = va( "terrain_%i", terrains.Num() + 1 );
terrain->material = fallbackMaterial;
terrain->layerMaterials[0] = terrain->material;
terrain->MarkPaintTextureDirty();
src.ExpectTokenString( "{" );
while ( src.ReadToken( &token ) ) {
if ( token == "}" ) {
break;
}
if ( !token.Icmp( "name" ) ) {
src.ExpectAnyToken( &token );
terrain->name = token;
} else if ( !token.Icmp( "material" ) ) {
src.ExpectAnyToken( &token );
terrain->material = token;
terrain->layerMaterials[0] = token;
} else if ( !token.Icmp( "layer" ) ) {
const int layer = src.ParseInt();
src.ExpectAnyToken( &token );
if ( layer >= 0 && layer < TERRAIN_MAX_LAYERS ) {
terrain->layerMaterials[layer] = token;
if ( layer == 0 ) {
terrain->material = token;
}
}
} else if ( !token.Icmp( "origin" ) ) {
src.Parse1DMatrix( 3, terrain->origin.ToFloatPtr() );
} else if ( !token.Icmp( "size" ) ) {
terrain->width = src.ParseInt();
terrain->height = src.ParseInt();
if ( terrain->width < 2 ) {
terrain->width = 2;
}
if ( terrain->height < 2 ) {
terrain->height = 2;
}
} else if ( !token.Icmp( "cellSize" ) ) {
terrain->cellSize = src.ParseFloat();
if ( terrain->cellSize <= 0.0f ) {
terrain->cellSize = TERRAIN_DEFAULT_CELL_SIZE;
}
} else if ( !token.Icmp( "paintSize" ) ) {
terrain->paintWidth = src.ParseInt();
terrain->paintHeight = src.ParseInt();
if ( terrain->paintWidth < 2 ) {
terrain->paintWidth = 2;
}
if ( terrain->paintHeight < 2 ) {
terrain->paintHeight = 2;
}
} else if ( !token.Icmp( "heights" ) ) {
src.ExpectTokenString( "{" );
terrain->heights.SetNum( terrain->width * terrain->height );
for ( int i = 0; i < terrain->heights.Num(); i++ ) {
terrain->heights[i] = 0.0f;
}
int sample = 0;
while ( src.ReadToken( &token ) ) {
if ( token == "}" ) {
break;
}
if ( sample < terrain->heights.Num() ) {
terrain->heights[sample++] = (float)atof( token.c_str() );
}
}
} else if ( !token.Icmp( "weights" ) || !token.Icmp( "paintWeights" ) ) {
const bool legacyWeights = !token.Icmp( "weights" );
src.ExpectTokenString( "{" );
if ( legacyWeights && terrain->paintWidth == TERRAIN_DEFAULT_PAINT_SIZE && terrain->paintHeight == TERRAIN_DEFAULT_PAINT_SIZE ) {
terrain->paintWidth = terrain->width;
terrain->paintHeight = terrain->height;
}
terrain->paintWeights.SetNum( terrain->paintWidth * terrain->paintHeight );
for ( int i = 0; i < terrain->paintWeights.Num(); i++ ) {
terrain->paintWeights[i] = idTerrainPatch::BaseLayerWeight();
}
int sample = 0;
int component = 0;
idVec4 weight;
weight.Zero();
while ( src.ReadToken( &token ) ) {
if ( token == "}" ) {
break;
}
weight[component++] = (float)atof( token.c_str() );
if ( component == 4 ) {
if ( sample < terrain->paintWeights.Num() ) {
terrain->paintWeights[sample] = idTerrainPatch::NormalizeLayerWeight( weight );
}
sample++;
component = 0;
weight.Zero();
}
}
}
}
terrain->EnsureLayerData( fallbackMaterial );
terrain->MarkPaintTextureDirty();
terrain->UpdateBounds();
terrains.Append( terrain );
}
return true;
}