/*** * * Copyright (c) 1996-2002, Valve LLC. All rights reserved. * * This product contains software technology licensed from Id * Software, Inc. ("Id Technology"). Id Technology (c) 1996 Id Software, Inc. * All Rights Reserved. * ****/ // studio.c #include "qrad.h" #include "..\..\engine\studio.h" #include "model_trace.h" #include "imagelib.h" typedef struct { char name[64]; mstudiomodel_t *pmodel; bool shadow; } TmpModel_t; static void ExtractAnimValue( int frame, mstudioanim_t *panim, int dof, float scale, float &v1 ) { if( !panim || panim->offset[dof] == 0 ) { v1 = 0.0f; return; } const mstudioanimvalue_t *panimvalue = (mstudioanimvalue_t *)((byte *)panim + panim->offset[dof]); int k = frame; while( panimvalue->num.total <= k ) { k -= panimvalue->num.total; panimvalue += panimvalue->num.valid + 1; if( panimvalue->num.total == 0 ) { v1 = 0.0f; return; } } // Bah, missing blend! if( panimvalue->num.valid > k ) { v1 = panimvalue[k+1].value * scale; } else { // get last valid data block v1 = panimvalue[panimvalue->num.valid].value * scale; } } static void StudioCalcBoneTransform( int frame, mstudiobone_t *pbone, mstudioanim_t *panim, vec3_t pos, vec4_t q ) { vec3_t origin; vec3_t angles; ExtractAnimValue( frame, panim, 0, pbone->scale[0], origin[0] ); ExtractAnimValue( frame, panim, 1, pbone->scale[1], origin[1] ); ExtractAnimValue( frame, panim, 2, pbone->scale[2], origin[2] ); ExtractAnimValue( frame, panim, 3, pbone->scale[3], angles[0] ); ExtractAnimValue( frame, panim, 4, pbone->scale[4], angles[1] ); ExtractAnimValue( frame, panim, 5, pbone->scale[5], angles[2] ); for( int j = 0; j < 3; j++ ) { origin[j] = pbone->value[j+0] + origin[j]; angles[j] = pbone->value[j+3] + angles[j]; } AngleQuaternion( angles, q ); VectorCopy( origin, pos ); } static void StudioSetupTriangle( tmesh_t *mesh, int index ) { tface_t *face = &mesh->faces[index]; // calculate mins & maxs ClearBounds( face->absmin, face->absmax ); face->contents = CONTENTS_SOLID; assert( face->a < mesh->numverts ); assert( face->b < mesh->numverts ); assert( face->c < mesh->numverts ); // calc bounds AddPointToBounds( mesh->verts[face->a].point, face->absmin, face->absmax ); AddPointToBounds( mesh->verts[face->a].point, mesh->absmin, mesh->absmax ); AddPointToBounds( mesh->verts[face->b].point, face->absmin, face->absmax ); AddPointToBounds( mesh->verts[face->b].point, mesh->absmin, mesh->absmax ); AddPointToBounds( mesh->verts[face->c].point, face->absmin, face->absmax ); AddPointToBounds( mesh->verts[face->c].point, mesh->absmin, mesh->absmax ); ExpandBounds( face->absmin, face->absmax, 1.0 ); // convert skinref to texture pointer face->texture = &mesh->textures[face->skinref]; // setup efficiency intersection data face->PrepareIntersectionData( mesh->verts ); } static void StudioRelinkFace( aabb_tree_t *tree, tface_t *face ) { // only shadow faces must be linked if( !face->shadow ) return; // find the first node that the facet box crosses areanode_t *node = tree->areanodes; while( 1 ) { if( node->axis == -1 ) break; if( face->absmin[node->axis] > node->dist ) node = node->children[0]; else if( face->absmax[node->axis] < node->dist ) node = node->children[1]; else break; // crosses the node } // link it in InsertLinkBefore( &face->area, &node->solid_edicts ); } static int StudioCreateMeshFromTriangles( entity_t *ent, studiohdr_t *phdr, const char *modname, int body, int skin, int flags, matrix3x4 transform[] ) { TmpModel_t submodel[MAXSTUDIOMODELS]; // list of unique models int i, j, k, totalVertSize = 0; int num_submodels = 0; mstudiobodyparts_t *pbodypart; mstudiomodel_t *psubmodel; memset( submodel, 0, sizeof( submodel )); // build list of unique submodels (by name) for( i = 0; i < phdr->numbodyparts; i++ ) { pbodypart = (mstudiobodyparts_t *)((byte *)phdr + phdr->bodypartindex) + i; for( j = 0; j < pbodypart->nummodels; j++ ) { psubmodel = (mstudiomodel_t *)((byte *)phdr + pbodypart->modelindex) + j; if( !psubmodel->nummesh ) continue; // blank submodel, ignore it for( k = 0; k < num_submodels; k++ ) { if( !Q_stricmp( submodel[k].name, psubmodel->name )) break; } // add new one if( k == num_submodels ) { Q_strncpy( submodel[k].name, psubmodel->name, sizeof( submodel[k].name )); submodel[k].pmodel = psubmodel; num_submodels++; } } } // find which parts are used by current body for( i = 0; i < phdr->numbodyparts; i++ ) { pbodypart = (mstudiobodyparts_t *)((byte *)phdr + phdr->bodypartindex) + i; int index = body / pbodypart->base; index = index % pbodypart->nummodels; psubmodel = (mstudiomodel_t *)((byte *)phdr + pbodypart->modelindex) + index; for( j = 0; j < num_submodels; j++ ) { if( submodel[j].pmodel == psubmodel ) { submodel[j].shadow = true; break; } } } // count unique model vertices for( i = 0; i < num_submodels; i++ ) { psubmodel = submodel[i].pmodel; totalVertSize += psubmodel->numverts; } tface_t *faces = (tface_t *)Mem_Alloc( sizeof( tface_t ) * totalVertSize * 8 ); // allocate face lighting array tvert_t *verts = (tvert_t *)Mem_Alloc( sizeof( tvert_t ) * totalVertSize * 8 ); // allocate vertex array mstudiotexture_t *ptexture = (mstudiotexture_t *)((byte *)phdr + phdr->textureindex); dvlightofs_t vsubmodels[MAXSTUDIOMODELS]; dflightofs_t fsubmodels[MAXSTUDIOMODELS]; int numFaces = 0, numVerts = 0; memset( vsubmodels, 0, sizeof( vsubmodels )); memset( fsubmodels, 0, sizeof( fsubmodels )); for( k = 0; k < num_submodels; k++ ) { mstudiomodel_t *psubmodel = submodel[k].pmodel; if( psubmodel->numverts <= 0 ) continue; // a blank submodel vec3_t *pstudioverts = (vec3_t *)((byte *)phdr + psubmodel->vertindex); vec3_t *pstudionorms = (vec3_t *)((byte *)phdr + psubmodel->normindex); vec3_t *m_verts = (vec3_t *)Mem_Alloc( sizeof( vec3_t ) * psubmodel->numverts ); vec3_t *m_norms = (vec3_t *)Mem_Alloc( sizeof( vec3_t ) * psubmodel->numnorms ); byte *pvertbone = ((byte *)phdr + psubmodel->vertinfoindex); byte *pnormbone = ((byte *)phdr + psubmodel->norminfoindex); short *pskinref = (short *)((byte *)phdr + phdr->skinindex); int m_skinnum = bound( 0, skin, MAXSTUDIOSKINS - 1 ); // setup skin if( m_skinnum != 0 && m_skinnum < phdr->numskinfamilies ) pskinref += (m_skinnum * phdr->numskinref); // setup all the vertices for( i = 0; i < psubmodel->numverts; i++ ) Matrix3x4_VectorTransform( transform[pvertbone[i]], pstudioverts[i], m_verts[i] ); // setup all the normals for( i = 0; i < psubmodel->numnorms; i++ ) Matrix3x4_VectorRotate( transform[pnormbone[i]], pstudionorms[i], m_norms[i] ); vsubmodels[k].submodel_offset = (byte *)psubmodel - (byte *)phdr; vsubmodels[k].vertex_offset = numVerts; fsubmodels[k].submodel_offset = (byte *)psubmodel - (byte *)phdr; fsubmodels[k].surface_offset = numFaces; // build all the light vertices because we should include all the bodies into the buffer for( j = 0; j < psubmodel->nummesh; j++ ) { mstudiomesh_t *pmesh = (mstudiomesh_t *)((byte *)phdr + psubmodel->meshindex) + j; float s = 1.0f / (float)ptexture[pskinref[pmesh->skinref]].width; float t = 1.0f / (float)ptexture[pskinref[pmesh->skinref]].height; short *ptricmds = (short *)((byte *)phdr + pmesh->triindex); int flags = ptexture[pskinref[pmesh->skinref]].flags; while( i = *( ptricmds++ )) { int vertexState = 0; bool tri_strip; if( i < 0 ) { tri_strip = false; i = -i; } else tri_strip = true; for( ; i > 0; i--, ptricmds += 4 ) { tvert_t *tv = &verts[numVerts]; tface_t *tf = &faces[numFaces]; if( submodel[k].shadow ) tf->shadow = true; tf->skinref = pskinref[pmesh->skinref]; // build in indices if( vertexState++ < 3 ) { switch( vertexState ) { case 1: tf->a = numVerts; break; case 2: tf->b = numVerts; break; case 3: tf->c = numVerts; numFaces++; break; } } else if( tri_strip ) { // flip triangles between clockwise and counter clockwise if( vertexState & 1 ) { // draw triangle [n-2 n-1 n] tf->a = numVerts - 2; tf->b = numVerts - 1; tf->c = numVerts; } else { // draw triangle [n-1 n-2 n] tf->a = numVerts - 1; tf->b = numVerts - 2; tf->c = numVerts; } numFaces++; } else { // draw triangle fan [0 n-1 n] tf->a = numVerts - ( vertexState - 1 ); tf->b = numVerts - 1; tf->c = numVerts; numFaces++; } if( FBitSet( flags, STUDIO_NF_CHROME )) { // probably always equal 64 (see studiomdl.c for details) tv->st[0] = float( s ); tv->st[1] = float( t ); } else if( FBitSet( flags, STUDIO_NF_UV_COORDS )) { tv->st[0] = HalfToFloat( ptricmds[2] ); tv->st[1] = HalfToFloat( ptricmds[3] ); } else { tv->st[0] = float( ptricmds[2] * s ); tv->st[1] = float( ptricmds[3] * t ); } VectorCopy( m_verts[ptricmds[0]], tv->point ); VectorCopy( m_norms[ptricmds[1]], tv->normal ); VectorNormalize2( tv->normal ); numVerts++; } } } // don't keep this because different submodels may have difference count of normals Mem_Free( m_norms ); Mem_Free( m_verts ); } // perfomance warning if( numFaces >= MAX_TRIANGLES ) { MsgDev( D_ERROR, "%s have too many triangles (%i). Ignored\n", modname, numFaces ); Mem_Free( verts ); Mem_Free( faces ); return 0; // failed to build (too many triangles) } else if( numFaces >= (MAX_TRIANGLES>>1)) MsgDev( D_WARN, "%s have too many triangles (%i)\n", modname, numFaces ); size_t texdata_size = sizeof( timage_t ) * phdr->numtextures; char diffuse[128], texname[128], mdlname[64]; rgbdata_t *external_textures[256]; COM_FileBase( modname, mdlname ); memset( external_textures, 0 , sizeof( external_textures )); // store only texdata where we has alpha-testing for( i = 0; i < phdr->numtextures; i++ ) { mstudiotexture_t *tex = &ptexture[i]; COM_FileBase( tex->name, texname ); Q_snprintf( diffuse, sizeof( diffuse ), "textures/%s/%s", mdlname, texname ); #ifdef HLRAD_EXTERNAL_TEXTURES if( FBitSet( tex->flags, STUDIO_NF_ADDITIVE|STUDIO_NF_MASKED ) && IMAGE_EXISTS( diffuse )) { rgbdata_t *test = COM_LoadImage( diffuse ); // external texture has alpha - use it if( test ) { if( FBitSet( test->flags, IMAGE_HAS_ALPHA )) { MsgDev( D_REPORT, "load external texture %s\n", diffuse ); texdata_size += test->width * test->height; external_textures[i] = test; } else { // has no alpha Mem_Free( test ); } } } #endif if( external_textures[i] ) continue; // already loaded // NOTE: store the only pixels, we doesn't need a palette if( FBitSet( tex->flags, STUDIO_NF_MASKED )) texdata_size += tex->width * tex->height; } size_t memsize = sizeof( tmesh_t ) + ( sizeof( tface_t ) * numFaces ) + ( sizeof( tvert_t ) * numVerts ) + texdata_size; // alloc vislight matrix if( FBitSet( flags, FMESH_MODEL_LIGHTMAPS|FMESH_VERTEX_LIGHTING )) memsize += (g_numworldlights + 7) / 8; // alloc lighting faces if( FBitSet( flags, FMESH_MODEL_LIGHTMAPS )) memsize += sizeof( lface_t ) * numFaces; // alloc lighting verts if( FBitSet( flags, FMESH_VERTEX_LIGHTING )) memsize += sizeof( lvert_t ) * numVerts; // Msg( "%s alloc %s\n", modname, Q_memprint( memsize )); byte *meshdata = (byte *)Mem_Alloc( memsize ); byte *meshend = meshdata + memsize; // bounds checking tmesh_t *mesh = (tmesh_t *)meshdata; if( ent->cache ) Mem_Free( ent->cache ); // throw previous instance ent->cache = meshdata; // FreeEntities will be automatically free that // setup pointers meshdata += sizeof( tmesh_t ); mesh->textures = (timage_t *)meshdata; meshdata += sizeof( timage_t ) * phdr->numtextures; mesh->faces = (tface_t *)meshdata; meshdata += sizeof( tface_t ) * numFaces; mesh->numfaces = numFaces; // store faces memcpy( mesh->faces, faces, sizeof( tface_t ) * mesh->numfaces ); Mem_Free( faces ); // setup additional lightdata if present if( FBitSet( flags, FMESH_MODEL_LIGHTMAPS )) { for( int i = 0; i < numFaces; i++ ) { mesh->faces[i].light = (lface_t *)meshdata; meshdata += sizeof( lface_t ); // clearing lightdata for( int j = 0; j < MAXLIGHTMAPS; j++ ) mesh->faces[i].light->styles[j] = 255; mesh->faces[i].light->lightofs = -1; } } mesh->verts = (tvert_t *)meshdata; meshdata += sizeof( tvert_t ) * numVerts; mesh->numverts = numVerts; // store vertexes memcpy( mesh->verts, verts, sizeof( tvert_t ) * mesh->numverts ); Mem_Free( verts ); // setup additional lightdata if present if( FBitSet( flags, FMESH_VERTEX_LIGHTING )) { for( int i = 0; i < numVerts; i++ ) { mesh->verts[i].light = (lvert_t *)meshdata; meshdata += sizeof( lvert_t ); VectorCopy( mesh->verts[i].point, mesh->verts[i].light->pos ); } } memcpy( mesh->vsubmodels, vsubmodels, sizeof( mesh->vsubmodels )); memcpy( mesh->fsubmodels, fsubmodels, sizeof( mesh->fsubmodels )); if( FBitSet( flags, FMESH_MODEL_LIGHTMAPS|FMESH_VERTEX_LIGHTING )) { mesh->vislight = (byte *)meshdata; meshdata += (g_numworldlights + 7) / 8; } for( int l = 0; l < MAXLIGHTMAPS; l++ ) mesh->styles[l] = 255; // store only texdata where we has alpha-testing for( i = 0; i < phdr->numtextures; i++ ) { mstudiotexture_t *src = &ptexture[i]; timage_t *dst = &mesh->textures[i]; dst->width = src->width; dst->height = src->height; dst->flags = src->flags; if( external_textures[i] ) { rgbdata_t *test = external_textures[i]; dst->data = meshdata; dst->width = test->width; dst->height = test->height; // copy alpha channel from external texture for( j = 0; j < test->width * test->height; j++ ) dst->data[j] = test->buffer[j*4+3] < 255 ? 255 : 0; // because we used palette index, not alpha value meshdata += test->width * test->height; // move pointer external_textures[i] = NULL; Mem_Free( test ); } else if( FBitSet( src->flags, STUDIO_NF_MASKED )) { // NOTE: store the only pixels, we doesn't need a palette dst->data = meshdata; memcpy( dst->data, (byte *)phdr + src->index, src->width * src->height ); meshdata += src->width * src->height; // move pointer } } if( meshdata != meshend ) Msg( "%s memory corrupted\n", modname ); ClearBounds( mesh->absmin, mesh->absmax ); // do some post-initialization for( i = 0; i < numFaces; i++ ) StudioSetupTriangle( mesh, i ); return numFaces; } bool StudioConstructMesh( entity_t *ent, void *extradata, const char *modname, uint modelCRC, int flags ) { studiohdr_t *phdr = (studiohdr_t *)extradata; double start = I_FloatTime(); vec3_t origin, angles; int body, skin; int numFaces; vec3_t xform; float scale; if( phdr->numbones < 1 ) return false; // get model properties GetVectorForKey( ent, "origin", origin ); GetVectorForKey( ent, "angles", angles ); angles[0] = -angles[0]; // Stupid quake bug workaround scale = FloatForKey( ent, "scale" ); body = IntForKey( ent, "body" ); skin = IntForKey( ent, "skin" ); GetVectorForKey( ent, "xform", xform ); if( VectorIsNull( xform )) VectorFill( xform, scale ); // check xform values if( xform[0] < 0.01f ) xform[0] = 1.0f; if( xform[1] < 0.01f ) xform[1] = 1.0f; if( xform[2] < 0.01f ) xform[2] = 1.0f; if( xform[0] > 16.0f ) xform[0] = 16.0f; if( xform[1] > 16.0f ) xform[1] = 16.0f; if( xform[2] > 16.0f ) xform[2] = 16.0f; // compute default pose for building mesh from mstudioseqdesc_t *pseqdesc = (mstudioseqdesc_t *)((byte *)phdr + phdr->seqindex); mstudioseqgroup_t *pseqgroup = (mstudioseqgroup_t *)((byte *)phdr + phdr->seqgroupindex); mstudioanim_t *panim = (mstudioanim_t *)((byte *)phdr + pseqdesc->animindex); mstudiobone_t *pbone = (mstudiobone_t *)((byte *)phdr + phdr->boneindex); static vec3_t pos[MAXSTUDIOBONES]; static vec4_t q[MAXSTUDIOBONES]; for( int i = 0; i < phdr->numbones; i++, pbone++, panim++ ) StudioCalcBoneTransform( 0, pbone, panim, pos[i], q[i] ); pbone = (mstudiobone_t *)((byte *)phdr + phdr->boneindex); matrix3x4 transform, bonematrix, bonetransform[MAXSTUDIOBONES]; Matrix3x4_CreateFromEntityScale3f( transform, angles, origin, xform ); // compute bones for default anim for( i = 0; i < phdr->numbones; i++ ) { // initialize bonematrix Matrix3x4_FromOriginQuat( bonematrix, q[i], pos[i] ); if( pbone[i].parent == -1 ) Matrix3x4_ConcatTransforms( bonetransform[i], transform, bonematrix ); else Matrix3x4_ConcatTransforms( bonetransform[i], bonetransform[pbone[i].parent], bonematrix ); } if(( numFaces = StudioCreateMeshFromTriangles( ent, phdr, modname, body, skin, flags, bonetransform )) == 0 ) return false; tmesh_t *mesh = (tmesh_t *)ent->cache; size_t memsize = Mem_Size( ent->cache ); int maxDepth = AREA_MIN_DEPTH; // FIXME: check this efficiency if( numFaces < 1000 ) maxDepth = AREA_MIN_DEPTH; else if( numFaces >= 1000 && numFaces <= 10000 ) maxDepth = 5; else if( numFaces >= 10000 && numFaces <= 20000 ) maxDepth = 6; else if( numFaces >= 20000 && numFaces <= 50000 ) maxDepth = 7; else if( numFaces >= 50000 && numFaces <= 100000 ) maxDepth = 8; else if( numFaces >= 100000 && numFaces <= 200000 ) maxDepth = 9; else maxDepth = AREA_MAX_DEPTH; // create AABB tree for speedup reasons CreateAreaNode( &mesh->face_tree, 0, maxDepth, mesh->absmin, mesh->absmax ); for( i = 0; i < numFaces; i++ ) StudioRelinkFace( &mesh->face_tree, &mesh->faces[i] ); ent->modtype = mod_studio; // now our mesh is valid and ready to trace mesh->modelCRC = modelCRC; VectorCopy( origin, mesh->origin ); VectorCopy( angles, mesh->angles ); VectorCopy( xform, mesh->scale ); mesh->backfrac = FloatForKey( ent, "zhlt_backfrac" ); mesh->flags = flags; // copy settings MsgDev( D_REPORT, "%s: build time %g secs, size %s\n", modname, I_FloatTime() - start, Q_memprint( memsize )); return true; } void LoadStudio( entity_t *ent, void *extradata, long fileLength, int flags ) { const char *modname = ValueForKey( ent, "model" ); dword modelCRC = 0; studiohdr_t *phdr; if( !extradata ) { MsgDev( D_WARN, "LoadStudio: couldn't load %s\n", modname ); return; } phdr = (studiohdr_t *)extradata; if( phdr->ident != IDSTUDIOHEADER || phdr->version != STUDIO_VERSION ) { if( phdr->ident != IDSTUDIOHEADER ) MsgDev( D_WARN, "LoadStudio: %s not a studio model\n", modname ); else if( phdr->version != STUDIO_VERSION ) MsgDev( D_WARN, "LoadStudio: %s has wrong version number (%i should be %i)\n", modname, phdr->version, STUDIO_VERSION ); Mem_Free( extradata, C_FILESYSTEM ); return; } #ifndef HLRAD_VERTEXLIGHTING ClearBits( flags, FMESH_VERTEX_LIGHTING ); #endif #ifndef HLRAD_LIGHTMAPMODELS ClearBits( flags, FMESH_MODEL_LIGHTMAPS ); #endif #if defined( HLRAD_VERTEXLIGHTING ) || defined( HLRAD_LIGHTMAPMODELS ) CRC32_Init( &modelCRC ); CRC32_ProcessBuffer( &modelCRC, extradata, phdr->length ); CRC32_Final( &modelCRC ); #endif // well the textures place in separate file (very stupid case) if( phdr->numtextures == 0 ) { char texname[128], texpath[128]; byte *texdata, *moddata; studiohdr_t *thdr, *newhdr; Q_strncpy( texname, modname, sizeof( texname )); COM_StripExtension( texname ); Q_snprintf( texpath, sizeof( texpath ), "%sT.mdl", texname ); MsgDev( D_REPORT, "loading %s\n", texpath ); texdata = FS_LoadFile( texpath, NULL, false ); if( !texdata ) { MsgDev( D_WARN, "LoadStudioModel: couldn't load %s\n", texpath ); Mem_Free( extradata, C_FILESYSTEM ); return; } moddata = (byte *)extradata; phdr = (studiohdr_t *)moddata; thdr = (studiohdr_t *)texdata; // merge textures with main model buffer extradata = Mem_Alloc( phdr->length + thdr->length - sizeof( studiohdr_t ), C_FILESYSTEM ); // we don't need two headers memcpy( extradata, moddata, phdr->length ); memcpy((byte *)extradata + phdr->length, texdata + sizeof( studiohdr_t ), thdr->length - sizeof( studiohdr_t )); // merge header newhdr = (studiohdr_t *)extradata; newhdr->numskinfamilies = thdr->numskinfamilies; newhdr->numtextures = thdr->numtextures; newhdr->numskinref = thdr->numskinref; newhdr->textureindex = phdr->length; newhdr->skinindex = newhdr->textureindex + ( newhdr->numtextures * sizeof( mstudiotexture_t )); newhdr->texturedataindex = newhdr->skinindex + (newhdr->numskinfamilies * newhdr->numskinref * sizeof( short )); newhdr->length = phdr->length + thdr->length - sizeof( studiohdr_t ); // and finally merge datapointers for textures for( int i = 0; i < newhdr->numtextures; i++ ) { mstudiotexture_t *ptexture = (mstudiotexture_t *)(((byte *)newhdr) + newhdr->textureindex); ptexture[i].index += ( phdr->length - sizeof( studiohdr_t )); } Mem_Free( moddata, C_FILESYSTEM ); Mem_Free( texdata, C_FILESYSTEM ); } StudioConstructMesh( ent, extradata, modname, modelCRC, flags ); Mem_Free( extradata, C_FILESYSTEM ); } void StudioGetBounds( entity_t *ent, vec3_t mins, vec3_t maxs ) { tmesh_t *mesh = (tmesh_t *)ent->cache; // assume point hull VectorClear( mins ); VectorClear( maxs ); if( !mesh || ent->modtype != mod_studio ) return; VectorSubtract( mesh->absmin, ent->origin, mins ); VectorSubtract( mesh->absmax, ent->origin, maxs ); }