From 182dff76ef7a73651e7faa8ccf9f94a75721afb8 Mon Sep 17 00:00:00 2001 From: Garux Date: Mon, 12 Jan 2026 06:57:10 +0500 Subject: [PATCH] * more robust deduction of triangle for texture alignment during bsp to map decompilation * more than ten times faster bsp to map decompilation --- docs/changelog-custom.txt | 2 + tools/quake3/q3map2/convert_map.cpp | 231 +++++++++++++++++----------- 2 files changed, 146 insertions(+), 87 deletions(-) diff --git a/docs/changelog-custom.txt b/docs/changelog-custom.txt index 689b037a..e1036a3e 100644 --- a/docs/changelog-custom.txt +++ b/docs/changelog-custom.txt @@ -198,6 +198,8 @@ Q3map2: * .dpk .dpkdir game archives support * .webp image format support * move -brightness/q3map_lightmapBrightness application to the end to match r_mapOverbrightBits effect + * more robust deduction of triangle for texture alignment during bsp to map decompilation + * more than ten times faster bsp to map decompilation diff --git a/tools/quake3/q3map2/convert_map.cpp b/tools/quake3/q3map2/convert_map.cpp index a9713da3..5604339c 100644 --- a/tools/quake3/q3map2/convert_map.cpp +++ b/tools/quake3/q3map2/convert_map.cpp @@ -31,6 +31,8 @@ /* dependencies */ #include "q3map2.h" #include "bspfile_rbsp.h" +#include "qspatial.h" +#include @@ -48,87 +50,140 @@ inline double Det3x3( double a00, double a01, double a02, + a02 * ( a10 * a21 - a11 * a20 ); } -static TriRef GetBestSurfaceTriangleMatchForBrushside( const side_t& buildSide, const bspModel_t& model ){ - float best = 0; - float thisarea; - const plane_t& buildPlane = mapplanes[buildSide.planenum]; - int matches = 0; +struct BspTriangleRef +{ + int surfaceType; + TriRef tri; + MinMax minmax; // X is on c_spatial_sort_direction - // first, start out with NULLs - TriRef bestVert{ nullptr }; - - // brute force through all bmodel surfaces - for ( const bspDrawSurface_t& s : Span( &bspDrawSurfaces[ model.firstBSPSurface ], model.numBSPSurfaces ) ) + BspTriangleRef( int surfaceType, const bspDrawVert_t& v0, const bspDrawVert_t& v1, const bspDrawVert_t& v2 ) + : surfaceType( surfaceType ), + tri{ &v0, &v1, &v2 } { - if ( s.surfaceType != MST_PLANAR && s.surfaceType != MST_TRIANGLE_SOUP ) { - continue; - } - if ( !strEqual( buildSide.shaderInfo->shader, bspShaders[s.shaderNum].shader ) ) { - continue; - } - for ( int t = 0; t + 3 <= s.numIndexes; t += 3 ) - { - const TriRef vert{ - &bspDrawVerts[s.firstVert + bspDrawIndexes[s.firstIndex + t + 0]], - &bspDrawVerts[s.firstVert + bspDrawIndexes[s.firstIndex + t + 1]], - &bspDrawVerts[s.firstVert + bspDrawIndexes[s.firstIndex + t + 2]] - }; - if ( s.surfaceType == MST_PLANAR && VectorCompare( vert[0]->normal, vert[1]->normal ) && VectorCompare( vert[1]->normal, vert[2]->normal ) ) { - if ( vector3_length( vert[0]->normal - buildPlane.normal() ) >= normalEpsilon - || vector3_length( vert[1]->normal - buildPlane.normal() ) >= normalEpsilon - || vector3_length( vert[2]->normal - buildPlane.normal() ) >= normalEpsilon ) { - continue; - } - } - else - { - // this is more prone to roundoff errors, but with embedded - // models, there is no better way - Plane3f plane; - PlaneFromPoints( plane, vert[0]->xyz, vert[1]->xyz, vert[2]->xyz ); - if ( vector3_length( plane.normal() - buildPlane.normal() ) >= normalEpsilon ) { - continue; - } - } - // fixme? better distance epsilon - if ( abs( plane3_distance_to_point( buildPlane.plane, vert[0]->xyz ) ) > 1 - || abs( plane3_distance_to_point( buildPlane.plane, vert[1]->xyz ) ) > 1 - || abs( plane3_distance_to_point( buildPlane.plane, vert[2]->xyz ) ) > 1 ) { - continue; - } - // Okay. Correct surface type, correct shader, correct plane. Let's start with the business... - winding_t polygon( buildSide.winding ); - for ( int i = 0; i < 3; ++i ) - { - // 0: 1, 2 - // 1: 2, 0 - // 2; 0, 1 - const Vector3& v1 = vert[( i + 1 ) % 3]->xyz; - const Vector3& v2 = vert[( i + 2 ) % 3]->xyz; - // we now need to generate the plane spanned by normal and (v2 - v1). - Plane3f plane( vector3_cross( v2 - v1, buildPlane.normal() ), 0 ); - plane.dist() = vector3_dot( v1, plane.normal() ); - ChopWindingInPlace( polygon, plane, distanceEpsilon ); - if ( polygon.empty() ) { - goto exwinding; - } - } - thisarea = WindingArea( polygon ); - if ( thisarea > 0 ) { - ++matches; - } - if ( thisarea > best ) { - best = thisarea; - bestVert = vert; - } -exwinding: - ; - } + minmax.extend( Vector3( spatial_distance( v0.xyz ), v0.xyz.y(), v0.xyz.z() ) ); + minmax.extend( Vector3( spatial_distance( v1.xyz ), v1.xyz.y(), v1.xyz.z() ) ); + minmax.extend( Vector3( spatial_distance( v2.xyz ), v2.xyz.y(), v2.xyz.z() ) ); } - //if( !striEqualPrefix( buildSide.shaderInfo->shader, "textures/common/" ) ) - // fprintf( stderr, "brushside with %s: %d matches (%f area)\n", buildSide.shaderInfo->shader, matches, best ); - return bestVert; -} + bool operator<( const BspTriangleRef& other ) const noexcept { + return minmax.maxs.x() < other.minmax.maxs.x(); + } +}; + +class ModelTriangles +{ + std::map> m_modelTriangles; +public: + ModelTriangles( const bspModel_t& model ){ + for ( const bspDrawSurface_t& s : Span( &bspDrawSurfaces[ model.firstBSPSurface ], model.numBSPSurfaces ) ) + { + if ( s.surfaceType == MST_PLANAR || s.surfaceType == MST_TRIANGLE_SOUP ) { + auto& vec = m_modelTriangles[bspShaders[s.shaderNum].shader]; + for ( int t = 0; t + 3 <= s.numIndexes; t += 3 ) + { + vec.push_back( BspTriangleRef( s.surfaceType, + bspDrawVerts[s.firstVert + bspDrawIndexes[s.firstIndex + t + 0]], + bspDrawVerts[s.firstVert + bspDrawIndexes[s.firstIndex + t + 1]], + bspDrawVerts[s.firstVert + bspDrawIndexes[s.firstIndex + t + 2]] + ) ); + } + } + } + + for( auto& [ k, v ] : m_modelTriangles ) + std::sort( v.begin(), v.end() ); + } + TriRef GetBestSurfaceTriangleMatchForBrushside( const side_t& buildSide ) const { + const float nepsilon = normalEpsilon * 100; // default target 0.005 - gives worthy results practically + const float depsilon = 2; + winding_t polygon; + float bestarea = 0; + float thisarea; + const plane_t& buildPlane = mapplanes[buildSide.planenum]; + int matches = 0; + + // first, start out with NULLs + TriRef bestVert{ nullptr }; + + // brute force through all bmodel surfaces + if( const auto triangles = m_modelTriangles.find( buildSide.shaderInfo->shader.c_str() ); triangles != m_modelTriangles.end() ){ + MinMax minmax; + for( const Vector3& v : buildSide.winding ) + minmax.extend( Vector3( spatial_distance( v ), v.y(), v.z() ) ); + minmax.mins -= Vector3( 32, depsilon, depsilon ); // 32 helps to spot more triangles, when brush is noticeably smaller + minmax.maxs += Vector3( 32, depsilon, depsilon ); // e.g. produced by original model autoclip + + auto tri = std::lower_bound( triangles->second.begin(), triangles->second.end(), minmax.mins.x(), + []( const BspTriangleRef& tri, const float spatialMin ){ + return tri.minmax.maxs.x() < spatialMin; + } ); + + for( const auto end = triangles->second.end(); tri != end && minmax.maxs.x() > tri->minmax.maxs.x(); ++tri ) + { + if ( !minmax.test( tri->minmax ) ) { + continue; + } + const TriRef& vert = tri->tri; + if ( tri->surfaceType == MST_PLANAR + && VectorCompare( vert[0]->normal, vert[1]->normal ) + && VectorCompare( vert[1]->normal, vert[2]->normal ) ) { + if ( !vector3_equal_epsilon( vert[0]->normal, buildPlane.normal(), float( nepsilon ) ) + || !vector3_equal_epsilon( vert[1]->normal, buildPlane.normal(), float( nepsilon ) ) + || !vector3_equal_epsilon( vert[2]->normal, buildPlane.normal(), float( nepsilon ) ) ) { + continue; + } + } + else + { + // this is more prone to roundoff errors, but with embedded + // models, there is no better way + Plane3f plane; + PlaneFromPoints( plane, vert[0]->xyz, vert[1]->xyz, vert[2]->xyz ); + if ( !vector3_equal_epsilon( plane.normal(), buildPlane.normal(), float( nepsilon ) ) ) { + continue; + } + } + + if ( std::fabs( plane3_distance_to_point( buildPlane.plane, vert[0]->xyz ) ) > depsilon + || std::fabs( plane3_distance_to_point( buildPlane.plane, vert[1]->xyz ) ) > depsilon + || std::fabs( plane3_distance_to_point( buildPlane.plane, vert[2]->xyz ) ) > depsilon ) { + continue; + } + // Okay. Correct surface type, correct shader, correct plane. Let's start with the business... + // we now need to generate the plane spanned by normal and (v2 - v1). + Plane3f planes[3]{ + { VectorNormalized( vector3_cross( vert[ 0 ]->xyz - vert[ 2 ]->xyz, buildPlane.normal() ) ), 0 }, + { VectorNormalized( vector3_cross( vert[ 1 ]->xyz - vert[ 0 ]->xyz, buildPlane.normal() ) ), 0 }, + { VectorNormalized( vector3_cross( vert[ 2 ]->xyz - vert[ 1 ]->xyz, buildPlane.normal() ) ), 0 }, + }; + planes[ 0 ].dist() = vector3_dot( vert[ 2 ]->xyz, planes[ 0 ].normal() ); + planes[ 1 ].dist() = vector3_dot( vert[ 0 ]->xyz, planes[ 1 ].normal() ); + planes[ 2 ].dist() = vector3_dot( vert[ 1 ]->xyz, planes[ 2 ].normal() ); + + polygon = buildSide.winding; + for ( const Plane3f& plane : planes ) + { + ChopWindingInPlace( polygon, plane, distanceEpsilon ); + if ( polygon.empty() ) { + goto exwinding; + } + } + thisarea = WindingArea( polygon ); + if ( thisarea > 0 ) { + ++matches; + } + if ( thisarea > bestarea ) { + bestarea = thisarea; + bestVert = vert; + } + exwinding: + ; + } + } + //if( !striEqualPrefix( buildSide.shaderInfo->shader, "textures/common/" ) ) + // fprintf( stderr, "brushside with %s: %d matches (%f area)\n", buildSide.shaderInfo->shader, matches, bestarea ); + return bestVert; + } +}; #define FRAC( x ) ( ( x ) - floor( x ) ) static void ConvertOriginBrush( FILE *f, int num, const Vector3& origin, EBrushType brushType ){ @@ -254,7 +309,7 @@ static void bspBrush_to_buildBrush( const bspBrush_t& brush ){ } } -static void ConvertBrushFast( FILE *f, const bspModel_t& model, int bspBrushNum, const Vector3& origin, EBrushType brushType ){ +static void ConvertBrushFast( FILE *f, int bspBrushNum, const Vector3& origin, EBrushType brushType ){ bspBrush_to_buildBrush( bspBrushes[bspBrushNum] ); @@ -334,7 +389,7 @@ static void ConvertBrushFast( FILE *f, const bspModel_t& model, int bspBrushNum, fprintf( f, "\t}\n\n" ); } -static void ConvertBrush( FILE *f, const bspModel_t& model, int bspBrushNum, const Vector3& origin, EBrushType brushType ){ +static void ConvertBrush( FILE *f, int bspBrushNum, const Vector3& origin, EBrushType brushType, const ModelTriangles& modelTriangles ){ bspBrush_to_buildBrush( bspBrushes[bspBrushNum] ); @@ -389,7 +444,7 @@ static void ConvertBrush( FILE *f, const bspModel_t& model, int bspBrushNum, con // - meshverts point in pairs of three into verts // - (triangles) // - find the triangle that has most in common with our - const TriRef vert = GetBestSurfaceTriangleMatchForBrushside( buildSide, model ); + const TriRef vert = modelTriangles.GetBestSurfaceTriangleMatchForBrushside( buildSide ); /* get texture name */ const char *texture = striEqualPrefix( buildSide.shaderInfo->shader, "textures/" ) @@ -837,12 +892,14 @@ static void ConvertModel( FILE *f, const bspModel_t& model, const Vector3& origi } /* go through each brush in the model */ - for ( int i = 0; i < model.numBSPBrushes; ++i ) - { - if( fast ) - ConvertBrushFast( f, model, model.firstBSPBrush + i, origin, brushType ); - else - ConvertBrush( f, model, model.firstBSPBrush + i, origin, brushType ); + if( fast ){ + for ( int i = 0; i < model.numBSPBrushes; ++i ) + ConvertBrushFast( f, model.firstBSPBrush + i, origin, brushType ); + } + else{ + ModelTriangles modelTriangles( model ); + for ( int i = 0; i < model.numBSPBrushes; ++i ) + ConvertBrush( f, model.firstBSPBrush + i, origin, brushType, modelTriangles ); } /* go through each drawsurf in the model */