]> de.git.xonotic.org Git - xonotic/netradiant.git/blobdiff - tools/quake3/q3map2/map.c
slow down non-fast operation but make it more accurate by not using falloff tolerance...
[xonotic/netradiant.git] / tools / quake3 / q3map2 / map.c
index 51d87afac744d5883cfc6854166e17d97f9a8e26..12892573c354fd9c8b6e5b0349703117b2e75686 100644 (file)
@@ -44,7 +44,7 @@ several games based on the Quake III Arena engine, in the form of "Q3Map2."
 #define        USE_HASHING
 #define        PLANE_HASHES    8192
 
-plane_t                                        *planehash[ PLANE_HASHES ];
+int                                            planehash[ PLANE_HASHES ];
 
 int                                            c_boxbevels;
 int                                            c_edgebevels;
@@ -59,9 +59,6 @@ PlaneEqual()
 ydnar: replaced with variable epsilon for djbob
 */
 
-#define        NORMAL_EPSILON  0.00001
-#define        DIST_EPSILON    0.01
-
 qboolean PlaneEqual( plane_t *p, vec3_t normal, vec_t dist )
 {
        float   ne, de;
@@ -72,10 +69,14 @@ qboolean PlaneEqual( plane_t *p, vec3_t normal, vec_t dist )
        de = distanceEpsilon;
        
        /* compare */
-       if( fabs( p->dist - dist ) <= de &&
-               fabs( p->normal[ 0 ] - normal[ 0 ] ) <= ne &&
-               fabs( p->normal[ 1 ] - normal[ 1 ] ) <= ne &&
-               fabs( p->normal[ 2 ] - normal[ 2 ] ) <= ne )
+       // We check equality of each component since we're using '<', not '<='
+       // (the epsilons may be zero).  We want to use '<' intead of '<=' to be
+       // consistent with the true meaning of "epsilon", and also because other
+       // parts of the code uses this inequality.
+       if ((p->dist == dist || fabs(p->dist - dist) < de) &&
+                       (p->normal[0] == normal[0] || fabs(p->normal[0] - normal[0]) < ne) &&
+                       (p->normal[1] == normal[1] || fabs(p->normal[1] - normal[1]) < ne) &&
+                       (p->normal[2] == normal[2] || fabs(p->normal[2] - normal[2]) < ne))
                return qtrue;
        
        /* different */
@@ -96,7 +97,7 @@ void AddPlaneToHash( plane_t *p )
        hash = (PLANE_HASHES - 1) & (int) fabs( p->dist );
 
        p->hash_chain = planehash[hash];
-       planehash[hash] = p;
+       planehash[hash] = p - mapplanes + 1;
 }
 
 /*
@@ -115,8 +116,7 @@ int CreateNewFloatPlane (vec3_t normal, vec_t dist)
        }
 
        // create a new plane
-       if (nummapplanes+2 > MAX_MAP_PLANES)
-               Error ("MAX_MAP_PLANES");
+       AUTOEXPAND_BY_REALLOC(mapplanes, nummapplanes+1, allocatedmapplanes, 1024);
 
        p = &mapplanes[nummapplanes];
        VectorCopy (normal, p->normal);
@@ -153,12 +153,73 @@ int CreateNewFloatPlane (vec3_t normal, vec_t dist)
 
 /*
 SnapNormal()
-snaps a near-axial normal vector
+Snaps a near-axial normal vector.
+Returns qtrue if and only if the normal was adjusted.
 */
 
-void SnapNormal( vec3_t normal )
+qboolean SnapNormal( vec3_t normal )
 {
+#if Q3MAP2_EXPERIMENTAL_SNAP_NORMAL_FIX
        int             i;
+       qboolean        adjusted = qfalse;
+
+       // A change from the original SnapNormal() is that we snap each
+       // component that's close to 0.  So for example if a normal is
+       // (0.707, 0.707, 0.0000001), it will get snapped to lie perfectly in the
+       // XY plane (its Z component will be set to 0 and its length will be
+       // normalized).  The original SnapNormal() didn't snap such vectors - it
+       // only snapped vectors that were near a perfect axis.
+
+       for (i = 0; i < 3; i++)
+       {
+               if (normal[i] != 0.0 && -normalEpsilon < normal[i] && normal[i] < normalEpsilon)
+               {
+                       normal[i] = 0.0;
+                       adjusted = qtrue;
+               }
+       }
+
+       if (adjusted)
+       {
+               VectorNormalize(normal, normal);
+               return qtrue;
+       }
+       return qfalse;
+#else
+       int             i;
+
+       // I would suggest that you uncomment the following code and look at the
+       // results:
+
+       /*
+       Sys_Printf("normalEpsilon is %f\n", normalEpsilon);
+       for (i = 0;; i++)
+       {
+               normal[0] = 1.0;
+               normal[1] = 0.0;
+               normal[2] = i * 0.000001;
+               VectorNormalize(normal, normal);
+               if (1.0 - normal[0] >= normalEpsilon) {
+                       Sys_Printf("(%f %f %f)\n", normal[0], normal[1], normal[2]);
+                       Error("SnapNormal: test completed");
+               }
+       }
+       */
+
+       // When the normalEpsilon is 0.00001, the loop will break out when normal is
+       // (0.999990 0.000000 0.004469).  In other words, this is the vector closest
+       // to axial that will NOT be snapped.  Anything closer will be snaped.  Now,
+       // 0.004469 is close to 1/225.  The length of a circular quarter-arc of radius
+       // 1 is PI/2, or about 1.57.  And 0.004469/1.57 is about 0.0028, or about
+       // 1/350.  Expressed a different way, 1/350 is also about 0.26/90.
+       // This means is that a normal with an angle that is within 1/4 of a degree
+       // from axial will be "snapped".  My belief is that the person who wrote the
+       // code below did not intend it this way.  I think the person intended that
+       // the epsilon be measured against the vector components close to 0, not 1.0.
+       // I think the logic should be: if 2 of the normal components are within
+       // epsilon of 0, then the vector can be snapped to be perfectly axial.
+       // We may consider adjusting the epsilon to a larger value when we make this
+       // code fix.
 
        for( i = 0; i < 3; i++ )
        {
@@ -166,15 +227,17 @@ void SnapNormal( vec3_t normal )
                {
                        VectorClear( normal );
                        normal[ i ] = 1;
-                       break;
+                       return qtrue;
                }
                if( fabs( normal[ i ] - -1 ) < normalEpsilon )
                {
                        VectorClear( normal );
                        normal[ i ] = -1;
-                       break;
+                       return qtrue;
                }
        }
+       return qfalse;
+#endif
 }
 
 
@@ -193,18 +256,70 @@ void SnapPlane( vec3_t normal, vec_t *dist, vec3_t center )
   SnapPlane reenabled by namespace because of multiple reports of
   q3map2-crashes which were triggered by this patch.
 */
-       // div0: ensure the point "center" stays on the plane (actually, this
-       // rotates the plane around the point center).
-       // if center lies on the plane, it is guaranteed to stay on the plane by
-       // this fix.
-       vec_t centerDist = DotProduct(normal, center);
        SnapNormal( normal );
-       *dist += (DotProduct(normal, center) - centerDist);
+
+       // TODO: Rambetter has some serious comments here as well.  First off,
+       // in the case where a normal is non-axial, there is nothing special
+       // about integer distances.  I would think that snapping a distance might
+       // make sense for axial normals, but I'm not so sure about snapping
+       // non-axial normals.  A shift by 0.01 in a plane, multiplied by a clipping
+       // against another plane that is 5 degrees off, and we introduce 0.1 error
+       // easily.  A 0.1 error in a vertex is where problems start to happen, such
+       // as disappearing triangles.
+
+       // Second, assuming we have snapped the normal above, let's say that the
+       // plane we just snapped was defined for some points that are actually
+       // quite far away from normal * dist.  Well, snapping the normal in this
+       // case means that we've just moved those points by potentially many units!
+       // Therefore, if we are going to snap the normal, we need to know the
+       // points we're snapping for so that the plane snaps with those points in
+       // mind (points remain close to the plane).
+
+       // I would like to know exactly which problems SnapPlane() is trying to
+       // solve so that we can better engineer it (I'm not saying that SnapPlane()
+       // should be removed altogether).  Fix all this snapping code at some point!
 
        if( fabs( *dist - Q_rint( *dist ) ) < distanceEpsilon )
                *dist = Q_rint( *dist );
 }
 
+/*
+SnapPlaneImproved()
+snaps a plane to normal/distance epsilons, improved code
+*/
+void SnapPlaneImproved(vec3_t normal, vec_t *dist, int numPoints, const vec3_t *points)
+{
+       int     i;
+       vec3_t  center;
+       vec_t   distNearestInt;
+
+       if (SnapNormal(normal))
+       {
+               if (numPoints > 0)
+               {
+                       // Adjust the dist so that the provided points don't drift away.
+                       VectorClear(center);
+                       for (i = 0; i < numPoints; i++)
+                       {
+                               VectorAdd(center, points[i], center);
+                       }
+                       for (i = 0; i < 3; i++) { center[i] = center[i] / numPoints; }
+                       *dist = DotProduct(normal, center);
+               }
+       }
+
+       if (VectorIsOnAxis(normal))
+       {
+               // Only snap distance if the normal is an axis.  Otherwise there
+               // is nothing "natural" about snapping the distance to an integer.
+               distNearestInt = Q_rint(*dist);
+               if (-distanceEpsilon < *dist - distNearestInt && *dist - distNearestInt < distanceEpsilon)
+               {
+                       *dist = distNearestInt;
+               }
+       }
+}
+
 
 
 /*
@@ -213,30 +328,34 @@ ydnar: changed to allow a number of test points to be supplied that
 must be within an epsilon distance of the plane
 */
 
-int FindFloatPlane( vec3_t normal, vec_t dist, int numPoints, vec3_t *points ) // NOTE: this has a side effect on the normal. Good or bad?
+int FindFloatPlane( vec3_t innormal, vec_t dist, int numPoints, vec3_t *points ) // NOTE: this has a side effect on the normal. Good or bad?
 
 #ifdef USE_HASHING
 
 {
        int             i, j, hash, h;
+       int pidx;
        plane_t *p;
        vec_t   d;
-       vec3_t centerofweight;
+       vec3_t normal;
 
-       VectorClear(centerofweight);
-       for(i = 0; i < numPoints; ++i)
-               VectorMA(centerofweight, 1.0 / numPoints, points[i], centerofweight);
-       
+       VectorCopy(innormal, normal);
+#if Q3MAP2_EXPERIMENTAL_SNAP_PLANE_FIX
+       SnapPlaneImproved(normal, &dist, numPoints, (const vec3_t *) points);
+#else
+       SnapPlane( normal, &dist );
+#endif
        /* hash the plane */
-       SnapPlane( normal, &dist, centerofweight );
        hash = (PLANE_HASHES - 1) & (int) fabs( dist );
        
        /* search the border bins as well */
        for( i = -1; i <= 1; i++ )
        {
                h = (hash + i) & (PLANE_HASHES - 1);
-               for( p = planehash[ h ]; p != NULL; p = p->hash_chain )
+               for( pidx = planehash[ h ] - 1; pidx != -1; pidx = mapplanes[pidx].hash_chain - 1 )
                {
+                       p = &mapplanes[pidx];
+
                        /* do standard plane compare */
                        if( !PlaneEqual( p, normal, dist ) )
                                continue;
@@ -247,9 +366,15 @@ int FindFloatPlane( vec3_t normal, vec_t dist, int numPoints, vec3_t *points ) /
                        /* ydnar: test supplied points against this plane */
                        for( j = 0; j < numPoints; j++ )
                        {
-                               d = DotProduct( points[ j ], normal ) - dist;
-                               if( fabs( d ) > distanceEpsilon )
-                                       break;
+                               // NOTE: When dist approaches 2^16, the resolution of 32 bit floating
+                               // point number is greatly decreased.  The distanceEpsilon cannot be
+                               // very small when world coordinates extend to 2^16.  Making the
+                               // dot product here in 64 bit land will not really help the situation
+                               // because the error will already be carried in dist.
+                               d = DotProduct( points[ j ], p->normal ) - p->dist;
+                               d = fabs(d);
+                               if (d != 0.0 && d >= distanceEpsilon)
+                                       break; // Point is too far from plane.
                        }
                        
                        /* found a matching plane */
@@ -267,19 +392,36 @@ int FindFloatPlane( vec3_t normal, vec_t dist, int numPoints, vec3_t *points ) /
 {
        int             i;
        plane_t *p;
+       vec3_t normal;
        
-
-       vec3_t centerofweight;
-
-       VectorClear(centerofweight);
-       for(i = 0; i < numPoints; ++i)
-               VectorMA(centerofweight, 1.0 / numPoints, points[i], centerofweight);
-
-       SnapPlane( normal, &dist, centerofweight );
+       VectorCopy(innormal, normal);
+#if Q3MAP2_EXPERIMENTAL_SNAP_PLANE_FIX
+       SnapPlaneImproved(normal, &dist, numPoints, (const vec3_t *) points);
+#else
+       SnapPlane( normal, &dist );
+#endif
        for( i = 0, p = mapplanes; i < nummapplanes; i++, p++ )
        {
-               if( PlaneEqual( p, normal, dist ) )
+               if( !PlaneEqual( p, normal, dist ) )
+                       continue;
+
+               /* ydnar: uncomment the following line for old-style plane finding */
+               //%     return i;
+                       
+               /* ydnar: test supplied points against this plane */
+               for( j = 0; j < numPoints; j++ )
+               {
+                       d = DotProduct( points[ j ], p->normal ) - p->dist;
+                       if( fabs( d ) > distanceEpsilon )
+                               break;
+               }
+               
+               /* found a matching plane */
+               if( j >= numPoints )
                        return i;
+               // TODO: Note that the non-USE_HASHING code does not compute epsilons
+               // for the provided points.  It should do that.  I think this code
+               // is unmaintained because nobody sets USE_HASHING to off.
        }
        
        return CreateNewFloatPlane( normal, dist );
@@ -296,6 +438,28 @@ takes 3 points and finds the plane they lie in
 
 int MapPlaneFromPoints( vec3_t *p )
 {
+#if Q3MAP2_EXPERIMENTAL_HIGH_PRECISION_MATH_FIXES
+       vec3_accu_t     paccu[3];
+       vec3_accu_t     t1, t2, normalAccu;
+       vec3_t          normal;
+       vec_t           dist;
+
+       VectorCopyRegularToAccu(p[0], paccu[0]);
+       VectorCopyRegularToAccu(p[1], paccu[1]);
+       VectorCopyRegularToAccu(p[2], paccu[2]);
+
+       VectorSubtractAccu(paccu[0], paccu[1], t1);
+       VectorSubtractAccu(paccu[2], paccu[1], t2);
+       CrossProductAccu(t1, t2, normalAccu);
+       VectorNormalizeAccu(normalAccu, normalAccu);
+       // TODO: A 32 bit float for the plane distance isn't enough resolution
+       // if the plane is 2^16 units away from the origin (the "epsilon" approaches
+       // 0.01 in that case).
+       dist = (vec_t) DotProductAccu(paccu[0], normalAccu);
+       VectorCopyAccuToRegular(normalAccu, normal);
+
+       return FindFloatPlane(normal, dist, 3, p);
+#else
        vec3_t  t1, t2, normal;
        vec_t   dist;
        
@@ -311,6 +475,7 @@ int MapPlaneFromPoints( vec3_t *p )
        
        /* store the plane */
        return FindFloatPlane( normal, dist, 3, p );
+#endif
 }
 
 
@@ -325,7 +490,7 @@ void SetBrushContents( brush_t *b )
        int                     contentFlags, compileFlags;
        side_t          *s;
        int                     i;
-       qboolean        mixed;
+       //%     qboolean        mixed;
        
        
        /* get initial compile flags from first side */
@@ -333,7 +498,7 @@ void SetBrushContents( brush_t *b )
        contentFlags = s->contentFlags;
        compileFlags = s->compileFlags;
        b->contentShader = s->shaderInfo;
-       mixed = qfalse;
+       //%     mixed = qfalse;
        
        /* get the content/compile flags for every side in the brush */
        for( i = 1; i < b->numsides; i++, s++ )
@@ -341,8 +506,11 @@ void SetBrushContents( brush_t *b )
                s = &b->sides[ i ];
                if( s->shaderInfo == NULL )
                        continue;
-               if( s->contentFlags != contentFlags || s->compileFlags != compileFlags )
-                       mixed = qtrue;
+               //%     if( s->contentFlags != contentFlags || s->compileFlags != compileFlags )
+               //%             mixed = qtrue;
+
+               contentFlags |= s->contentFlags;
+               compileFlags |= s->compileFlags;
        }
        
        /* ydnar: getting rid of this stupid warning */
@@ -598,6 +766,7 @@ and links it to the current entity
 static void MergeOrigin(entity_t *ent, vec3_t origin)
 {
        vec3_t adjustment;
+       char string[128];
 
        /* we have not parsed the brush completely yet... */
        GetVectorForKey( ent, "origin", ent->origin );
@@ -606,12 +775,11 @@ static void MergeOrigin(entity_t *ent, vec3_t origin)
        VectorAdd(adjustment, ent->origin, ent->origin);
        VectorCopy(origin, ent->originbrush_origin);
 
-       char string[128];
        sprintf(string, "%f %f %f", ent->origin[0], ent->origin[1], ent->origin[2]);
        SetKeyValue(ent, "origin", string);
 }
 
-brush_t *FinishBrush( void )
+brush_t *FinishBrush( qboolean noCollapseGroups )
 {
        brush_t         *b;
        
@@ -624,7 +792,6 @@ brush_t *FinishBrush( void )
           after the entire entity is parsed, the planenums and texinfos will be adjusted for the origin brush */
        if( buildBrush->compileFlags & C_ORIGIN )
        {
-               char    string[ 32 ];
                vec3_t  origin;
 
                Sys_Printf( "Entity %i, Brush %i: origin brush detected\n", 
@@ -657,7 +824,8 @@ brush_t *FinishBrush( void )
        }
        
        /* add bevel planes */
-       AddBrushBevels();
+       if(!noCollapseGroups)
+               AddBrushBevels();
        
        /* keep it */
        b = CopyBrush( buildBrush );
@@ -829,7 +997,7 @@ static void ParseRawBrush( qboolean onlyLights )
        int                             planenum;
        shaderInfo_t    *si;
        vec_t                   shift[ 2 ];
-       vec_t                   rotate;
+       vec_t                   rotate = 0;
        vec_t                   scale[ 2 ];
        char                    name[ MAX_QPATH ];
        char                    shader[ MAX_QPATH ];
@@ -1026,11 +1194,8 @@ ParseBrush()
 parses a brush out of a map file and sets it up
 */
 
-static void ParseBrush( qboolean onlyLights )
+static void ParseBrush( qboolean onlyLights, qboolean noCollapseGroups )
 {
-       brush_t *b;
-       
-       
        /* parse the brush out of the map */
        ParseRawBrush( onlyLights );
        
@@ -1073,7 +1238,7 @@ static void ParseBrush( qboolean onlyLights )
        }
        
        /* finish the brush */
-       b = FinishBrush();
+       FinishBrush(noCollapseGroups);
 }
 
 
@@ -1410,11 +1575,12 @@ ParseMapEntity()
 parses a single entity out of a map file
 */
 
-static qboolean ParseMapEntity( qboolean onlyLights )
+static qboolean ParseMapEntity( qboolean onlyLights, qboolean noCollapseGroups )
 {
        epair_t                 *ep;
        const char              *classname, *value;
-       float                   lightmapScale;
+       float                   lightmapScale, shadeAngle;
+       int                             lightmapSampleSize;
        char                    shader[ MAX_QPATH ];
        shaderInfo_t    *celShader = NULL;
        brush_t                 *brush;
@@ -1437,8 +1603,7 @@ static qboolean ParseMapEntity( qboolean onlyLights )
        }
        
        /* range check */
-       if( numEntities >= MAX_MAP_ENTITIES )
-               Error( "numEntities == MAX_MAP_ENTITIES" );
+       AUTOEXPAND_BY_REALLOC(entities, numEntities, allocatedEntities, 32);
        
        /* setup */
        entitySourceBrushes = 0;
@@ -1488,7 +1653,7 @@ static qboolean ParseMapEntity( qboolean onlyLights )
                                g_bBrushPrimit = BPRIMIT_NEWBRUSHES;
                                
                                /* parse brush primitive */
-                               ParseBrush( onlyLights );
+                               ParseBrush( onlyLights, noCollapseGroups );
                        }
                        else
                        {
@@ -1498,7 +1663,7 @@ static qboolean ParseMapEntity( qboolean onlyLights )
                                
                                /* parse old brush format */
                                UnGetToken();
-                               ParseBrush( onlyLights );
+                               ParseBrush( onlyLights, noCollapseGroups );
                        }
                        entitySourceBrushes++;
                }
@@ -1551,19 +1716,24 @@ static qboolean ParseMapEntity( qboolean onlyLights )
        /* get explicit shadow flags */
        GetEntityShadowFlags( mapEnt, NULL, &castShadows, &recvShadows );
        
+       /* vortex: added _ls key (short name of lightmapscale) */
        /* ydnar: get lightmap scaling value for this entity */
+       lightmapScale = 0.0f;
        if( strcmp( "", ValueForKey( mapEnt, "lightmapscale" ) ) ||
-               strcmp( "", ValueForKey( mapEnt, "_lightmapscale" ) ) )
+               strcmp( "", ValueForKey( mapEnt, "_lightmapscale" ) ) || 
+               strcmp( "", ValueForKey( mapEnt, "_ls" ) ) )
        {
                /* get lightmap scale from entity */
                lightmapScale = FloatForKey( mapEnt, "lightmapscale" );
                if( lightmapScale <= 0.0f )
                        lightmapScale = FloatForKey( mapEnt, "_lightmapscale" );
+               if( lightmapScale <= 0.0f )
+                       lightmapScale = FloatForKey( mapEnt, "_ls" );
+               if( lightmapScale < 0.0f )
+                       lightmapScale = 0.0f;
                if( lightmapScale > 0.0f )
                        Sys_Printf( "Entity %d (%s) has lightmap scale of %.4f\n", mapEnt->mapEntityNum, classname, lightmapScale );
        }
-       else
-               lightmapScale = 0.0f;
        
        /* ydnar: get cel shader :) for this entity */
        value = ValueForKey( mapEnt, "_celshader" );
@@ -1571,12 +1741,50 @@ static qboolean ParseMapEntity( qboolean onlyLights )
                value = ValueForKey( &entities[ 0 ], "_celshader" );
        if( value[ 0 ] != '\0' )
        {
-               sprintf( shader, "textures/%s", value );
-               celShader = ShaderInfoForShader( shader );
-               Sys_Printf( "Entity %d (%s) has cel shader %s\n", mapEnt->mapEntityNum, classname, celShader->shader );
+               if(strcmp(value, "none"))
+               {
+                       sprintf( shader, "textures/%s", value );
+                       celShader = ShaderInfoForShader( shader );
+                       Sys_Printf( "Entity %d (%s) has cel shader %s\n", mapEnt->mapEntityNum, classname, celShader->shader );
+               }
+               else
+               {
+                       celShader = NULL;
+               }
        }
        else
-               celShader = NULL;
+               celShader = (*globalCelShader ? ShaderInfoForShader(globalCelShader) : NULL);
+
+       /* jal : entity based _shadeangle */
+       shadeAngle = 0.0f;
+       if ( strcmp( "", ValueForKey( mapEnt, "_shadeangle" ) ) )
+               shadeAngle = FloatForKey( mapEnt, "_shadeangle" );
+       /* vortex' aliases */
+       else if ( strcmp( "", ValueForKey( mapEnt, "_smoothnormals" ) ) )
+               shadeAngle = FloatForKey( mapEnt, "_smoothnormals" );
+       else if ( strcmp( "", ValueForKey( mapEnt, "_sn" ) ) )
+               shadeAngle = FloatForKey( mapEnt, "_sn" );
+       else if ( strcmp( "", ValueForKey( mapEnt, "_smooth" ) ) )
+               shadeAngle = FloatForKey( mapEnt, "_smooth" );
+       
+       if( shadeAngle < 0.0f )
+               shadeAngle = 0.0f;
+
+       if( shadeAngle > 0.0f )
+               Sys_Printf( "Entity %d (%s) has shading angle of %.4f\n", mapEnt->mapEntityNum, classname, shadeAngle );
+       
+       /* jal : entity based _samplesize */
+       lightmapSampleSize = 0;
+       if ( strcmp( "", ValueForKey( mapEnt, "_lightmapsamplesize" ) ) )
+               lightmapSampleSize = IntForKey( mapEnt, "_lightmapsamplesize" );
+       else if ( strcmp( "", ValueForKey( mapEnt, "_samplesize" ) ) )
+               lightmapSampleSize = IntForKey( mapEnt, "_samplesize" );
+       
+       if( lightmapSampleSize < 0 )
+               lightmapSampleSize = 0;
+
+       if( lightmapSampleSize > 0 )
+               Sys_Printf( "Entity %d (%s) has lightmap sample size of %d\n", mapEnt->mapEntityNum, classname, lightmapSampleSize );
        
        /* attach stuff to everything in the entity */
        for( brush = mapEnt->brushes; brush != NULL; brush = brush->next )
@@ -1584,8 +1792,10 @@ static qboolean ParseMapEntity( qboolean onlyLights )
                brush->entityNum = mapEnt->mapEntityNum;
                brush->castShadows = castShadows;
                brush->recvShadows = recvShadows;
+               brush->lightmapSampleSize = lightmapSampleSize;
                brush->lightmapScale = lightmapScale;
                brush->celShader = celShader;
+               brush->shadeAngleDegrees = shadeAngle;
        }
        
        for( patch = mapEnt->patches; patch != NULL; patch = patch->next )
@@ -1593,6 +1803,7 @@ static qboolean ParseMapEntity( qboolean onlyLights )
                patch->entityNum = mapEnt->mapEntityNum;
                patch->castShadows = castShadows;
                patch->recvShadows = recvShadows;
+               patch->lightmapSampleSize = lightmapSampleSize;
                patch->lightmapScale = lightmapScale;
                patch->celShader = celShader;
        }
@@ -1609,14 +1820,14 @@ static qboolean ParseMapEntity( qboolean onlyLights )
                AdjustBrushesForOrigin( mapEnt );
 
        /* group_info entities are just for editor grouping (fixme: leak!) */
-       if( !Q_stricmp( "group_info", classname ) )
+       if( !noCollapseGroups && !Q_stricmp( "group_info", classname ) )
        {
                numEntities--;
                return qtrue;
        }
        
        /* group entities are just for editor convenience, toss all brushes into worldspawn */
-       if( funcGroup )
+       if( !noCollapseGroups && funcGroup )
        {
                MoveBrushesToWorld( mapEnt );
                numEntities--;
@@ -1634,11 +1845,11 @@ LoadMapFile()
 loads a map file into a list of entities
 */
 
-void LoadMapFile( char *filename, qboolean onlyLights )
+void LoadMapFile( char *filename, qboolean onlyLights, qboolean noCollapseGroups )
 {              
        FILE            *file;
        brush_t         *b;
-       int                     oldNumEntities, numMapBrushes;
+       int                     oldNumEntities = 0, numMapBrushes;
        
        
        /* note it */
@@ -1667,7 +1878,7 @@ void LoadMapFile( char *filename, qboolean onlyLights )
        buildBrush = AllocBrush( MAX_BUILD_SIDES );
        
        /* parse the map file */
-       while( ParseMapEntity( onlyLights ) );
+       while( ParseMapEntity( onlyLights, noCollapseGroups ) );
        
        /* light loading */
        if( onlyLights )