You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2694 lines
88 KiB
2694 lines
88 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $Revision: $ |
|
// $NoKeywords: $ |
|
// |
|
// This file contains code to allow us to associate client data with bsp leaves. |
|
// |
|
//=============================================================================// |
|
|
|
#include "vrad.h" |
|
#include "mathlib/vector.h" |
|
#include "UtlBuffer.h" |
|
#include "utlvector.h" |
|
#include "GameBSPFile.h" |
|
#include "BSPTreeData.h" |
|
#include "VPhysics_Interface.h" |
|
#include "Studio.h" |
|
#include "Optimize.h" |
|
#include "Bsplib.h" |
|
#include "CModel.h" |
|
#include "PhysDll.h" |
|
#include "phyfile.h" |
|
#include "collisionutils.h" |
|
#include "tier1/KeyValues.h" |
|
#include "pacifier.h" |
|
#include "materialsystem/imaterial.h" |
|
#include "materialsystem/hardwareverts.h" |
|
#include "materialsystem/hardwaretexels.h" |
|
#include "byteswap.h" |
|
#include "mpivrad.h" |
|
#include "vtf/vtf.h" |
|
#include "tier1/utldict.h" |
|
#include "tier1/utlsymbol.h" |
|
#include "bitmap/tgawriter.h" |
|
|
|
#include "messbuf.h" |
|
#include "vmpi.h" |
|
#include "vmpi_distribute_work.h" |
|
|
|
|
|
#define ALIGN_TO_POW2(x,y) (((x)+(y-1))&~(y-1)) |
|
|
|
// identifies a vertex embedded in solid |
|
// lighting will be copied from nearest valid neighbor |
|
struct badVertex_t |
|
{ |
|
int m_ColorVertex; |
|
Vector m_Position; |
|
Vector m_Normal; |
|
}; |
|
|
|
// a final colored vertex |
|
struct colorVertex_t |
|
{ |
|
Vector m_Color; |
|
Vector m_Position; |
|
bool m_bValid; |
|
}; |
|
|
|
// a texel suitable for a model |
|
struct colorTexel_t |
|
{ |
|
Vector m_Color; |
|
Vector m_WorldPosition; |
|
Vector m_WorldNormal; |
|
float m_fDistanceToTri; // If we are outside of the triangle, how far away is it? |
|
bool m_bValid; |
|
bool m_bPossiblyInteresting; |
|
|
|
}; |
|
|
|
class CComputeStaticPropLightingResults |
|
{ |
|
public: |
|
~CComputeStaticPropLightingResults() |
|
{ |
|
m_ColorVertsArrays.PurgeAndDeleteElements(); |
|
m_ColorTexelsArrays.PurgeAndDeleteElements(); |
|
} |
|
|
|
CUtlVector< CUtlVector<colorVertex_t>* > m_ColorVertsArrays; |
|
CUtlVector< CUtlVector<colorTexel_t>* > m_ColorTexelsArrays; |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
struct Rasterizer |
|
{ |
|
struct Location |
|
{ |
|
Vector barycentric; |
|
Vector2D uv; |
|
bool insideTriangle; |
|
}; |
|
|
|
Rasterizer(Vector2D t0, Vector2D t1, Vector2D t2, size_t resX, size_t resY) |
|
: mT0(t0) |
|
, mT1(t1) |
|
, mT2(t2) |
|
, mResX(resX) |
|
, mResY(resY) |
|
, mUvStepX(1.0f / resX) |
|
, mUvStepY(1.0f / resY) |
|
{ |
|
Build(); |
|
} |
|
|
|
CUtlVector< Location >::iterator begin() { return mRasterizedLocations.begin(); } |
|
CUtlVector< Location >::iterator end() { return mRasterizedLocations.end(); } |
|
|
|
void Build(); |
|
|
|
inline size_t GetRow(float y) const { return size_t(y * mResY); } |
|
inline size_t GetCol(float x) const { return size_t(x * mResX); } |
|
|
|
inline size_t GetLinearPos( const CUtlVector< Location >::iterator& it ) const |
|
{ |
|
// Given an iterator, return what the linear position in the buffer would be for the data. |
|
return (size_t)(GetRow(it->uv.y) * mResX) |
|
+ (size_t)(GetCol(it->uv.x)); |
|
} |
|
|
|
private: |
|
const Vector2D mT0, mT1, mT2; |
|
const size_t mResX, mResY; |
|
const float mUvStepX, mUvStepY; |
|
|
|
// Right now, we just fill this out and directly iterate over it. |
|
// It could be large. This is a memory/speed tradeoff. We could instead generate them |
|
// on demand. |
|
CUtlVector< Location > mRasterizedLocations; |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
inline Vector ComputeBarycentric( Vector2D _edgeC, Vector2D _edgeA, Vector2D _edgeB, float _dAA, float _dAB, float _dBB, float _invDenom ) |
|
{ |
|
float dCA = _edgeC.Dot(_edgeA); |
|
float dCB = _edgeC.Dot(_edgeB); |
|
|
|
Vector retVal; |
|
retVal.y = (_dBB * dCA - _dAB * dCB) * _invDenom; |
|
retVal.z = (_dAA * dCB - _dAB * dCA) * _invDenom; |
|
retVal.x = 1.0f - retVal.y - retVal.z; |
|
|
|
return retVal; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void Rasterizer::Build() |
|
{ |
|
// For now, use the barycentric method. It's easy, I'm lazy. |
|
// We can optimize later if it's a performance issue. |
|
const float baseX = mUvStepX / 2.0f; |
|
const float baseY = mUvStepY / 2.0f; |
|
|
|
|
|
float fMinX = min(min(mT0.x, mT1.x), mT2.x); |
|
float fMinY = min(min(mT0.y, mT1.y), mT2.y); |
|
float fMaxX = max(max(mT0.x, mT1.x), mT2.x); |
|
float fMaxY = max(max(mT0.y, mT1.y), mT2.y); |
|
|
|
// Degenerate. Consider warning about these, but otherwise no problem. |
|
if (fMinX == fMaxX || fMinY == fMaxY) |
|
return; |
|
|
|
// Clamp to 0..1 |
|
fMinX = max(0, fMinX); |
|
fMinY = max(0, fMinY); |
|
fMaxX = min(1.0f, fMaxX); |
|
fMaxY = min(1.0f, fMaxY); |
|
|
|
// We puff the interesting area up by 1 so we can hit an inflated region for the necessary bilerp data. |
|
// If we wanted to support better texturing (almost definitely unnecessary), we'd change this to a larger size. |
|
const int kFilterSampleRadius = 1; |
|
|
|
int iMinX = GetCol(fMinX) - kFilterSampleRadius; |
|
int iMinY = GetRow(fMinY) - kFilterSampleRadius; |
|
int iMaxX = GetCol(fMaxX) + 1 + kFilterSampleRadius; |
|
int iMaxY = GetRow(fMaxY) + 1 + kFilterSampleRadius; |
|
|
|
// Clamp to valid texture (integer) locations |
|
iMinX = max(0, iMinX); |
|
iMinY = max(0, iMinY); |
|
iMaxX = min(iMaxX, mResX - 1); |
|
iMaxY = min(iMaxY, mResY - 1); |
|
|
|
// Set the size to be as expected. |
|
// TODO: Pass this in from outside to minimize allocations |
|
int count = (iMaxY - iMinY + 1) |
|
* (iMaxX - iMinX + 1); |
|
mRasterizedLocations.EnsureCount(count); |
|
memset( mRasterizedLocations.Base(), 0, mRasterizedLocations.Count() * sizeof( Location ) ); |
|
|
|
// Computing Barycentrics adapted from here http://gamedev.stackexchange.com/questions/23743/whats-the-most-efficient-way-to-find-barycentric-coordinates |
|
Vector2D edgeA = mT1 - mT0; |
|
Vector2D edgeB = mT2 - mT0; |
|
|
|
float dAA = edgeA.Dot(edgeA); |
|
float dAB = edgeA.Dot(edgeB); |
|
float dBB = edgeB.Dot(edgeB); |
|
float invDenom = 1.0f / (dAA * dBB - dAB * dAB); |
|
|
|
int linearPos = 0; |
|
for (int j = iMinY; j <= iMaxY; ++j) { |
|
for (int i = iMinX; i <= iMaxX; ++i) { |
|
Vector2D testPt( i * mUvStepX + baseX, j * mUvStepY + baseY ); |
|
Vector barycentric = ComputeBarycentric( testPt - mT0, edgeA, edgeB, dAA, dAB, dBB, invDenom ); |
|
|
|
// Test whether the point is inside the triangle. |
|
// MCJOHNTODO: Edge rules and whatnot--right now we re-rasterize points on the edge. |
|
Location& newLoc = mRasterizedLocations[linearPos++]; |
|
newLoc.barycentric = barycentric; |
|
newLoc.uv = testPt; |
|
|
|
newLoc.insideTriangle = (barycentric.x >= 0.0f && barycentric.x <= 1.0f && barycentric.y >= 0.0f && barycentric.y <= 1.0f && barycentric.z >= 0.0f && barycentric.z <= 1.0f); |
|
} |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Globals |
|
//----------------------------------------------------------------------------- |
|
CUtlSymbolTable g_ForcedTextureShadowsModels; |
|
|
|
// DON'T USE THIS FROM WITHIN A THREAD. THERE IS A THREAD CONTEXT CREATED |
|
// INSIDE PropTested_t. USE THAT INSTEAD. |
|
IPhysicsCollision *s_pPhysCollision = NULL; |
|
|
|
static void ConvertTexelDataToTexture(unsigned int _resX, unsigned int _resY, ImageFormat _destFmt, const CUtlVector<colorTexel_t>& _srcTexels, CUtlMemory<byte>* _outTexture); |
|
|
|
// Such a monstrosity. :( |
|
static void GenerateLightmapSamplesForMesh( const matrix3x4_t& _matPos, const matrix3x4_t& _matNormal, int _iThread, int _skipProp, int _nFlags, int _lightmapResX, int _lightmapResY, |
|
studiohdr_t* _pStudioHdr, mstudiomodel_t* _pStudioModel, OptimizedModel::ModelHeader_t* _pVtxModel, int _meshID, |
|
CComputeStaticPropLightingResults *_pResults ); |
|
|
|
// Debug function, converts lightmaps to linear space then dumps them out. |
|
// TODO: Write out the file in a .dds instead of a .tga, in whatever format we're supposed to use. |
|
static void DumpLightmapLinear( const char* _dstFilename, const CUtlVector<colorTexel_t>& _srcTexels, int _width, int _height ); |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Vrad's static prop manager |
|
//----------------------------------------------------------------------------- |
|
|
|
class CVradStaticPropMgr : public IVradStaticPropMgr |
|
{ |
|
public: |
|
// constructor, destructor |
|
CVradStaticPropMgr(); |
|
virtual ~CVradStaticPropMgr(); |
|
|
|
// methods of IStaticPropMgr |
|
void Init(); |
|
void Shutdown(); |
|
|
|
// iterate all the instanced static props and compute their vertex lighting |
|
void ComputeLighting( int iThread ); |
|
|
|
private: |
|
// VMPI stuff. |
|
static void VMPI_ProcessStaticProp_Static( int iThread, uint64 iStaticProp, MessageBuffer *pBuf ); |
|
static void VMPI_ReceiveStaticPropResults_Static( uint64 iStaticProp, MessageBuffer *pBuf, int iWorker ); |
|
void VMPI_ProcessStaticProp( int iThread, int iStaticProp, MessageBuffer *pBuf ); |
|
void VMPI_ReceiveStaticPropResults( int iStaticProp, MessageBuffer *pBuf, int iWorker ); |
|
|
|
// local thread version |
|
static void ThreadComputeStaticPropLighting( int iThread, void *pUserData ); |
|
void ComputeLightingForProp( int iThread, int iStaticProp ); |
|
|
|
// Methods associated with unserializing static props |
|
void UnserializeModelDict( CUtlBuffer& buf ); |
|
void UnserializeModels( CUtlBuffer& buf ); |
|
void UnserializeStaticProps(); |
|
|
|
// Creates a collision model |
|
void CreateCollisionModel( char const* pModelName ); |
|
|
|
private: |
|
// Unique static prop models |
|
struct StaticPropDict_t |
|
{ |
|
vcollide_t m_loadedModel; |
|
CPhysCollide* m_pModel; |
|
Vector m_Mins; // Bounding box is in local coordinates |
|
Vector m_Maxs; |
|
studiohdr_t* m_pStudioHdr; |
|
CUtlBuffer m_VtxBuf; |
|
CUtlVector<int> m_textureShadowIndex; // each texture has an index if this model casts texture shadows |
|
CUtlVector<int> m_triangleMaterialIndex;// each triangle has an index if this model casts texture shadows |
|
}; |
|
|
|
struct MeshData_t |
|
{ |
|
CUtlVector<Vector> m_VertexColors; |
|
CUtlMemory<byte> m_TexelsEncoded; |
|
int m_nLod; |
|
}; |
|
|
|
// A static prop instance |
|
struct CStaticProp |
|
{ |
|
Vector m_Origin; |
|
QAngle m_Angles; |
|
Vector m_mins; |
|
Vector m_maxs; |
|
Vector m_LightingOrigin; |
|
int m_ModelIdx; |
|
BSPTreeDataHandle_t m_Handle; |
|
CUtlVector<MeshData_t> m_MeshData; |
|
int m_Flags; |
|
bool m_bLightingOriginValid; |
|
|
|
// Note that all lightmaps for a given prop share the same resolution (and format)--and there can be multiple lightmaps |
|
// per prop (if there are multiple pieces--the watercooler is an example). |
|
// This is effectively because there's not a good way in hammer for a prop to say "this should be the resolution |
|
// of each of my sub-pieces." |
|
ImageFormat m_LightmapImageFormat; |
|
unsigned int m_LightmapImageWidth; |
|
unsigned int m_LightmapImageHeight; |
|
|
|
}; |
|
|
|
// Enumeration context |
|
struct EnumContext_t |
|
{ |
|
PropTested_t* m_pPropTested; |
|
Ray_t const* m_pRay; |
|
}; |
|
|
|
// The list of all static props |
|
CUtlVector <StaticPropDict_t> m_StaticPropDict; |
|
CUtlVector <CStaticProp> m_StaticProps; |
|
|
|
bool m_bIgnoreStaticPropTrace; |
|
|
|
void ComputeLighting( CStaticProp &prop, int iThread, int prop_index, CComputeStaticPropLightingResults *pResults ); |
|
void ApplyLightingToStaticProp( int iStaticProp, CStaticProp &prop, const CComputeStaticPropLightingResults *pResults ); |
|
|
|
void SerializeLighting(); |
|
void AddPolysForRayTrace(); |
|
void BuildTriList( CStaticProp &prop ); |
|
}; |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Expose IVradStaticPropMgr to vrad |
|
//----------------------------------------------------------------------------- |
|
|
|
static CVradStaticPropMgr g_StaticPropMgr; |
|
IVradStaticPropMgr* StaticPropMgr() |
|
{ |
|
return &g_StaticPropMgr; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// constructor, destructor |
|
//----------------------------------------------------------------------------- |
|
|
|
CVradStaticPropMgr::CVradStaticPropMgr() |
|
{ |
|
// set to ignore static prop traces |
|
m_bIgnoreStaticPropTrace = false; |
|
} |
|
|
|
CVradStaticPropMgr::~CVradStaticPropMgr() |
|
{ |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Makes sure the studio model is a static prop |
|
//----------------------------------------------------------------------------- |
|
|
|
bool IsStaticProp( studiohdr_t* pHdr ) |
|
{ |
|
if (!(pHdr->flags & STUDIOHDR_FLAGS_STATIC_PROP)) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Load a file into a Utlbuf |
|
//----------------------------------------------------------------------------- |
|
static bool LoadFile( char const* pFileName, CUtlBuffer& buf ) |
|
{ |
|
if ( !g_pFullFileSystem ) |
|
return false; |
|
|
|
return g_pFullFileSystem->ReadFile( pFileName, NULL, buf ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Constructs the file name from the model name |
|
//----------------------------------------------------------------------------- |
|
static char const* ConstructFileName( char const* pModelName ) |
|
{ |
|
static char buf[1024]; |
|
sprintf( buf, "%s%s", gamedir, pModelName ); |
|
return buf; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Computes a convex hull from a studio mesh |
|
//----------------------------------------------------------------------------- |
|
static CPhysConvex* ComputeConvexHull( mstudiomesh_t* pMesh, studiohdr_t *pStudioHdr ) |
|
{ |
|
const mstudio_meshvertexdata_t *vertData = pMesh->GetVertexData( (void *)pStudioHdr ); |
|
Assert( vertData ); // This can only return NULL on X360 for now |
|
|
|
// Generate a list of all verts in the mesh |
|
Vector** ppVerts = (Vector**)_alloca(pMesh->numvertices * sizeof(Vector*) ); |
|
for (int i = 0; i < pMesh->numvertices; ++i) |
|
{ |
|
ppVerts[i] = vertData->Position(i); |
|
} |
|
|
|
// Generate a convex hull from the verts |
|
return s_pPhysCollision->ConvexFromVerts( ppVerts, pMesh->numvertices ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Computes a convex hull from the studio model |
|
//----------------------------------------------------------------------------- |
|
CPhysCollide* ComputeConvexHull( studiohdr_t* pStudioHdr ) |
|
{ |
|
CUtlVector<CPhysConvex*> convexHulls; |
|
|
|
for (int body = 0; body < pStudioHdr->numbodyparts; ++body ) |
|
{ |
|
mstudiobodyparts_t *pBodyPart = pStudioHdr->pBodypart( body ); |
|
for( int model = 0; model < pBodyPart->nummodels; ++model ) |
|
{ |
|
mstudiomodel_t *pStudioModel = pBodyPart->pModel( model ); |
|
for( int mesh = 0; mesh < pStudioModel->nummeshes; ++mesh ) |
|
{ |
|
// Make a convex hull for each mesh |
|
// NOTE: This won't work unless the model has been compiled |
|
// with $staticprop |
|
mstudiomesh_t *pStudioMesh = pStudioModel->pMesh( mesh ); |
|
convexHulls.AddToTail( ComputeConvexHull( pStudioMesh, pStudioHdr ) ); |
|
} |
|
} |
|
} |
|
|
|
// Convert an array of convex elements to a compiled collision model |
|
// (this deletes the convex elements) |
|
return s_pPhysCollision->ConvertConvexToCollide( convexHulls.Base(), convexHulls.Size() ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Load studio model vertex data from a file... |
|
//----------------------------------------------------------------------------- |
|
|
|
bool LoadStudioModel( char const* pModelName, CUtlBuffer& buf ) |
|
{ |
|
// No luck, gotta build it |
|
// Construct the file name... |
|
if (!LoadFile( pModelName, buf )) |
|
{ |
|
Warning("Error! Unable to load model \"%s\"\n", pModelName ); |
|
return false; |
|
} |
|
|
|
// Check that it's valid |
|
if (strncmp ((const char *) buf.PeekGet(), "IDST", 4) && |
|
strncmp ((const char *) buf.PeekGet(), "IDAG", 4)) |
|
{ |
|
Warning("Error! Invalid model file \"%s\"\n", pModelName ); |
|
return false; |
|
} |
|
|
|
studiohdr_t* pHdr = (studiohdr_t*)buf.PeekGet(); |
|
|
|
Studio_ConvertStudioHdrToNewVersion( pHdr ); |
|
|
|
if (pHdr->version != STUDIO_VERSION) |
|
{ |
|
Warning("Error! Invalid model version \"%s\"\n", pModelName ); |
|
return false; |
|
} |
|
|
|
if (!IsStaticProp(pHdr)) |
|
{ |
|
Warning("Error! To use model \"%s\"\n" |
|
" as a static prop, it must be compiled with $staticprop!\n", pModelName ); |
|
return false; |
|
} |
|
|
|
// ensure reset |
|
pHdr->pVertexBase = NULL; |
|
pHdr->pIndexBase = NULL; |
|
|
|
return true; |
|
} |
|
|
|
bool LoadStudioCollisionModel( char const* pModelName, CUtlBuffer& buf ) |
|
{ |
|
char tmp[1024]; |
|
Q_strncpy( tmp, pModelName, sizeof( tmp ) ); |
|
Q_SetExtension( tmp, ".phy", sizeof( tmp ) ); |
|
// No luck, gotta build it |
|
if (!LoadFile( tmp, buf )) |
|
{ |
|
// this is not an error, the model simply has no PHY file |
|
return false; |
|
} |
|
|
|
phyheader_t *header = (phyheader_t *)buf.PeekGet(); |
|
|
|
if ( header->size != sizeof(*header) || header->solidCount <= 0 ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
bool LoadVTXFile( char const* pModelName, const studiohdr_t *pStudioHdr, CUtlBuffer& buf ) |
|
{ |
|
char filename[MAX_PATH]; |
|
|
|
// construct filename |
|
Q_StripExtension( pModelName, filename, sizeof( filename ) ); |
|
strcat( filename, ".dx80.vtx" ); |
|
|
|
if ( !LoadFile( filename, buf ) ) |
|
{ |
|
Warning( "Error! Unable to load file \"%s\"\n", filename ); |
|
return false; |
|
} |
|
|
|
OptimizedModel::FileHeader_t* pVtxHdr = (OptimizedModel::FileHeader_t *)buf.Base(); |
|
|
|
// Check that it's valid |
|
if ( pVtxHdr->version != OPTIMIZED_MODEL_FILE_VERSION ) |
|
{ |
|
Warning( "Error! Invalid VTX file version: %d, expected %d \"%s\"\n", pVtxHdr->version, OPTIMIZED_MODEL_FILE_VERSION, filename ); |
|
return false; |
|
} |
|
if ( pVtxHdr->checkSum != pStudioHdr->checksum ) |
|
{ |
|
Warning( "Error! Invalid VTX file checksum: %d, expected %d \"%s\"\n", pVtxHdr->checkSum, pStudioHdr->checksum, filename ); |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Gets a vertex position from a strip index |
|
//----------------------------------------------------------------------------- |
|
inline static Vector* PositionFromIndex( const mstudio_meshvertexdata_t *vertData, mstudiomesh_t* pMesh, OptimizedModel::StripGroupHeader_t* pStripGroup, int i ) |
|
{ |
|
OptimizedModel::Vertex_t* pVert = pStripGroup->pVertex( i ); |
|
return vertData->Position( pVert->origMeshVertID ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Writes a glview text file containing the collision surface in question |
|
// Input : *pCollide - |
|
// *pFilename - |
|
//----------------------------------------------------------------------------- |
|
void DumpCollideToGlView( vcollide_t *pCollide, const char *pFilename ) |
|
{ |
|
if ( !pCollide ) |
|
return; |
|
|
|
Msg("Writing %s...\n", pFilename ); |
|
|
|
FILE *fp = fopen( pFilename, "w" ); |
|
for (int i = 0; i < pCollide->solidCount; ++i) |
|
{ |
|
Vector *outVerts; |
|
int vertCount = s_pPhysCollision->CreateDebugMesh( pCollide->solids[i], &outVerts ); |
|
int triCount = vertCount / 3; |
|
int vert = 0; |
|
|
|
unsigned char r = (i & 1) * 64 + 64; |
|
unsigned char g = (i & 2) * 64 + 64; |
|
unsigned char b = (i & 4) * 64 + 64; |
|
|
|
float fr = r / 255.0f; |
|
float fg = g / 255.0f; |
|
float fb = b / 255.0f; |
|
|
|
for ( int i = 0; i < triCount; i++ ) |
|
{ |
|
fprintf( fp, "3\n" ); |
|
fprintf( fp, "%6.3f %6.3f %6.3f %.2f %.3f %.3f\n", |
|
outVerts[vert].x, outVerts[vert].y, outVerts[vert].z, fr, fg, fb ); |
|
vert++; |
|
fprintf( fp, "%6.3f %6.3f %6.3f %.2f %.3f %.3f\n", |
|
outVerts[vert].x, outVerts[vert].y, outVerts[vert].z, fr, fg, fb ); |
|
vert++; |
|
fprintf( fp, "%6.3f %6.3f %6.3f %.2f %.3f %.3f\n", |
|
outVerts[vert].x, outVerts[vert].y, outVerts[vert].z, fr, fg, fb ); |
|
vert++; |
|
} |
|
s_pPhysCollision->DestroyDebugMesh( vertCount, outVerts ); |
|
} |
|
fclose( fp ); |
|
} |
|
|
|
|
|
static bool PointInTriangle( const Vector2D &p, const Vector2D &v0, const Vector2D &v1, const Vector2D &v2 ) |
|
{ |
|
float coords[3]; |
|
GetBarycentricCoords2D( v0, v1, v2, p, coords ); |
|
for ( int i = 0; i < 3; i++ ) |
|
{ |
|
if ( coords[i] < 0.0f || coords[i] > 1.0f ) |
|
return false; |
|
} |
|
float sum = coords[0] + coords[1] + coords[2]; |
|
if ( sum > 1.0f ) |
|
return false; |
|
return true; |
|
} |
|
|
|
bool LoadFileIntoBuffer( CUtlBuffer &buf, const char *pFilename ) |
|
{ |
|
FileHandle_t fileHandle = g_pFileSystem->Open( pFilename, "rb" ); |
|
if ( !fileHandle ) |
|
return false; |
|
|
|
// Get the file size |
|
int texSize = g_pFileSystem->Size( fileHandle ); |
|
buf.EnsureCapacity( texSize ); |
|
int nBytesRead = g_pFileSystem->Read( buf.Base(), texSize, fileHandle ); |
|
g_pFileSystem->Close( fileHandle ); |
|
buf.SeekPut( CUtlBuffer::SEEK_HEAD, nBytesRead ); |
|
buf.SeekGet( CUtlBuffer::SEEK_HEAD, 0 ); |
|
return true; |
|
} |
|
|
|
// keeps a list of all textures that cast shadows via alpha channel |
|
class CShadowTextureList |
|
{ |
|
public: |
|
// This loads a vtf and converts it to RGB8888 format |
|
unsigned char *LoadVTFRGB8888( const char *pName, int *pWidth, int *pHeight, bool *pClampU, bool *pClampV ) |
|
{ |
|
char szPath[MAX_PATH]; |
|
Q_strncpy( szPath, "materials/", sizeof( szPath ) ); |
|
Q_strncat( szPath, pName, sizeof( szPath ), COPY_ALL_CHARACTERS ); |
|
Q_strncat( szPath, ".vtf", sizeof( szPath ), COPY_ALL_CHARACTERS ); |
|
Q_FixSlashes( szPath, CORRECT_PATH_SEPARATOR ); |
|
|
|
CUtlBuffer buf; |
|
if ( !LoadFileIntoBuffer( buf, szPath ) ) |
|
return NULL; |
|
IVTFTexture *pTex = CreateVTFTexture(); |
|
if (!pTex->Unserialize( buf )) |
|
return NULL; |
|
Msg("Loaded alpha texture %s\n", szPath ); |
|
unsigned char *pSrcImage = pTex->ImageData( 0, 0, 0, 0, 0, 0 ); |
|
int iWidth = pTex->Width(); |
|
int iHeight = pTex->Height(); |
|
ImageFormat dstFormat = IMAGE_FORMAT_RGBA8888; |
|
ImageFormat srcFormat = pTex->Format(); |
|
*pClampU = (pTex->Flags() & TEXTUREFLAGS_CLAMPS) ? true : false; |
|
*pClampV = (pTex->Flags() & TEXTUREFLAGS_CLAMPT) ? true : false; |
|
unsigned char *pDstImage = new unsigned char[ImageLoader::GetMemRequired( iWidth, iHeight, 1, dstFormat, false )]; |
|
|
|
if( !ImageLoader::ConvertImageFormat( pSrcImage, srcFormat, |
|
pDstImage, dstFormat, iWidth, iHeight, 0, 0 ) ) |
|
{ |
|
delete[] pDstImage; |
|
return NULL; |
|
} |
|
|
|
*pWidth = iWidth; |
|
*pHeight = iHeight; |
|
return pDstImage; |
|
} |
|
|
|
// Checks the database for the material and loads if necessary |
|
// returns true if found and pIndex will be the index, -1 if no alpha shadows |
|
bool FindOrLoadIfValid( const char *pMaterialName, int *pIndex ) |
|
{ |
|
*pIndex = -1; |
|
int index = m_Textures.Find(pMaterialName); |
|
bool bFound = false; |
|
if ( index != m_Textures.InvalidIndex() ) |
|
{ |
|
bFound = true; |
|
*pIndex = index; |
|
} |
|
else |
|
{ |
|
KeyValues *pVMT = new KeyValues("vmt"); |
|
CUtlBuffer buf(0,0,CUtlBuffer::TEXT_BUFFER); |
|
LoadFileIntoBuffer( buf, pMaterialName ); |
|
if ( pVMT->LoadFromBuffer( pMaterialName, buf ) ) |
|
{ |
|
bFound = true; |
|
if ( pVMT->FindKey("$translucent") || pVMT->FindKey("$alphatest") ) |
|
{ |
|
KeyValues *pBaseTexture = pVMT->FindKey("$basetexture"); |
|
if ( pBaseTexture ) |
|
{ |
|
const char *pBaseTextureName = pBaseTexture->GetString(); |
|
if ( pBaseTextureName ) |
|
{ |
|
int w, h; |
|
bool bClampU = false; |
|
bool bClampV = false; |
|
unsigned char *pImageBits = LoadVTFRGB8888( pBaseTextureName, &w, &h, &bClampU, &bClampV ); |
|
if ( pImageBits ) |
|
{ |
|
int index = m_Textures.Insert( pMaterialName ); |
|
m_Textures[index].InitFromRGB8888( w, h, pImageBits ); |
|
*pIndex = index; |
|
if ( pVMT->FindKey("$nocull") ) |
|
{ |
|
// UNDONE: Support this? Do we need to emit two triangles? |
|
m_Textures[index].allowBackface = true; |
|
} |
|
m_Textures[index].clampU = bClampU; |
|
m_Textures[index].clampV = bClampV; |
|
delete[] pImageBits; |
|
} |
|
} |
|
} |
|
} |
|
|
|
} |
|
pVMT->deleteThis(); |
|
} |
|
|
|
return bFound; |
|
} |
|
|
|
|
|
// iterate the textures for the model and load each one into the database |
|
// this is used on models marked to cast texture shadows |
|
void LoadAllTexturesForModel( studiohdr_t *pHdr, int *pTextureList ) |
|
{ |
|
for ( int i = 0; i < pHdr->numtextures; i++ ) |
|
{ |
|
int textureIndex = -1; |
|
// try to add each texture to the transparent shadow manager |
|
char szPath[MAX_PATH]; |
|
|
|
// iterate quietly through all specified directories until a valid material is found |
|
for ( int j = 0; j < pHdr->numcdtextures; j++ ) |
|
{ |
|
Q_strncpy( szPath, "materials/", sizeof( szPath ) ); |
|
Q_strncat( szPath, pHdr->pCdtexture( j ), sizeof( szPath ) ); |
|
const char *textureName = pHdr->pTexture( i )->pszName(); |
|
Q_strncat( szPath, textureName, sizeof( szPath ), COPY_ALL_CHARACTERS ); |
|
Q_strncat( szPath, ".vmt", sizeof( szPath ), COPY_ALL_CHARACTERS ); |
|
Q_FixSlashes( szPath, CORRECT_PATH_SEPARATOR ); |
|
if ( FindOrLoadIfValid( szPath, &textureIndex ) ) |
|
break; |
|
} |
|
|
|
pTextureList[i] = textureIndex; |
|
} |
|
} |
|
|
|
int AddMaterialEntry( int shadowTextureIndex, const Vector2D &t0, const Vector2D &t1, const Vector2D &t2 ) |
|
{ |
|
int index = m_MaterialEntries.AddToTail(); |
|
m_MaterialEntries[index].textureIndex = shadowTextureIndex; |
|
m_MaterialEntries[index].uv[0] = t0; |
|
m_MaterialEntries[index].uv[1] = t1; |
|
m_MaterialEntries[index].uv[2] = t2; |
|
return index; |
|
} |
|
|
|
// HACKHACK: Compute the average coverage for this triangle by sampling the AABB of its texture space |
|
float ComputeCoverageForTriangle( int shadowTextureIndex, const Vector2D &t0, const Vector2D &t1, const Vector2D &t2 ) |
|
{ |
|
float umin = min(t0.x, t1.x); |
|
umin = min(umin, t2.x); |
|
float umax = max(t0.x, t1.x); |
|
umax = max(umax, t2.x); |
|
|
|
float vmin = min(t0.y, t1.y); |
|
vmin = min(vmin, t2.y); |
|
float vmax = max(t0.y, t1.y); |
|
vmax = max(vmax, t2.y); |
|
|
|
// UNDONE: Do something about tiling |
|
umin = clamp(umin, 0, 1); |
|
umax = clamp(umax, 0, 1); |
|
vmin = clamp(vmin, 0, 1); |
|
vmax = clamp(vmax, 0, 1); |
|
Assert(umin>=0.0f && umax <= 1.0f); |
|
Assert(vmin>=0.0f && vmax <= 1.0f); |
|
const alphatexture_t &tex = m_Textures.Element(shadowTextureIndex); |
|
int u0 = umin * (tex.width-1); |
|
int u1 = umax * (tex.width-1); |
|
int v0 = vmin * (tex.height-1); |
|
int v1 = vmax * (tex.height-1); |
|
|
|
int total = 0; |
|
int count = 0; |
|
for ( int v = v0; v <= v1; v++ ) |
|
{ |
|
int row = (v * tex.width); |
|
for ( int u = u0; u <= u1; u++ ) |
|
{ |
|
total += tex.pAlphaTexels[row + u]; |
|
count++; |
|
} |
|
} |
|
if ( count ) |
|
{ |
|
float coverage = float(total) / (count * 255.0f); |
|
return coverage; |
|
} |
|
return 1.0f; |
|
} |
|
|
|
int SampleMaterial( int materialIndex, const Vector &coords, bool bBackface ) |
|
{ |
|
const materialentry_t &mat = m_MaterialEntries[materialIndex]; |
|
const alphatexture_t &tex = m_Textures.Element(m_MaterialEntries[materialIndex].textureIndex); |
|
if ( bBackface && !tex.allowBackface ) |
|
return 0; |
|
Vector2D uv = coords.x * mat.uv[0] + coords.y * mat.uv[1] + coords.z * mat.uv[2]; |
|
int u = RoundFloatToInt( uv[0] * tex.width ); |
|
int v = RoundFloatToInt( uv[1] * tex.height ); |
|
|
|
// asume power of 2, clamp or wrap |
|
// UNDONE: Support clamp? This code should work |
|
#if 0 |
|
u = tex.clampU ? clamp(u,0,(tex.width-1)) : (u & (tex.width-1)); |
|
v = tex.clampV ? clamp(v,0,(tex.height-1)) : (v & (tex.height-1)); |
|
#else |
|
// for now always wrap |
|
u &= (tex.width-1); |
|
v &= (tex.height-1); |
|
#endif |
|
|
|
return tex.pAlphaTexels[v * tex.width + u]; |
|
} |
|
|
|
struct alphatexture_t |
|
{ |
|
short width; |
|
short height; |
|
bool allowBackface; |
|
bool clampU; |
|
bool clampV; |
|
unsigned char *pAlphaTexels; |
|
|
|
void InitFromRGB8888( int w, int h, unsigned char *pTexels ) |
|
{ |
|
width = w; |
|
height = h; |
|
pAlphaTexels = new unsigned char[w*h]; |
|
for ( int i = 0; i < h; i++ ) |
|
{ |
|
for ( int j = 0; j < w; j++ ) |
|
{ |
|
int index = (i*w) + j; |
|
pAlphaTexels[index] = pTexels[index*4 + 3]; |
|
} |
|
} |
|
} |
|
}; |
|
struct materialentry_t |
|
{ |
|
int textureIndex; |
|
Vector2D uv[3]; |
|
}; |
|
// this is the list of textures we've loaded |
|
// only load each one once |
|
CUtlDict< alphatexture_t, unsigned short > m_Textures; |
|
CUtlVector<materialentry_t> m_MaterialEntries; |
|
}; |
|
|
|
// global to keep the shadow-casting texture list and their alpha bits |
|
CShadowTextureList g_ShadowTextureList; |
|
|
|
float ComputeCoverageFromTexture( float b0, float b1, float b2, int32 hitID ) |
|
{ |
|
const float alphaScale = 1.0f / 255.0f; |
|
// UNDONE: Pass ray down to determine backfacing? |
|
//Vector normal( tri.m_flNx, tri.m_flNy, tri.m_flNz ); |
|
//bool bBackface = DotProduct(delta, tri.N) > 0 ? true : false; |
|
Vector coords(b0,b1,b2); |
|
return alphaScale * g_ShadowTextureList.SampleMaterial( g_RtEnv.GetTriangleMaterial(hitID), coords, false ); |
|
} |
|
|
|
// this is here to strip models/ or .mdl or whatnot |
|
void CleanModelName( const char *pModelName, char *pOutput, int outLen ) |
|
{ |
|
// strip off leading models/ if it exists |
|
const char *pModelDir = "models/"; |
|
int modelLen = Q_strlen(pModelDir); |
|
|
|
if ( !Q_strnicmp(pModelName, pModelDir, modelLen ) ) |
|
{ |
|
pModelName += modelLen; |
|
} |
|
Q_strncpy( pOutput, pModelName, outLen ); |
|
|
|
// truncate any .mdl extension |
|
char *dot = strchr(pOutput,'.'); |
|
if ( dot ) |
|
{ |
|
*dot = 0; |
|
} |
|
|
|
} |
|
|
|
|
|
void ForceTextureShadowsOnModel( const char *pModelName ) |
|
{ |
|
char buf[1024]; |
|
CleanModelName( pModelName, buf, sizeof(buf) ); |
|
if ( !g_ForcedTextureShadowsModels.Find(buf).IsValid()) |
|
{ |
|
g_ForcedTextureShadowsModels.AddString(buf); |
|
} |
|
} |
|
|
|
bool IsModelTextureShadowsForced( const char *pModelName ) |
|
{ |
|
char buf[1024]; |
|
CleanModelName( pModelName, buf, sizeof(buf) ); |
|
return g_ForcedTextureShadowsModels.Find(buf).IsValid(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Creates a collision model (based on the render geometry!) |
|
//----------------------------------------------------------------------------- |
|
void CVradStaticPropMgr::CreateCollisionModel( char const* pModelName ) |
|
{ |
|
CUtlBuffer buf; |
|
CUtlBuffer bufvtx; |
|
CUtlBuffer bufphy; |
|
|
|
int i = m_StaticPropDict.AddToTail(); |
|
m_StaticPropDict[i].m_pModel = NULL; |
|
m_StaticPropDict[i].m_pStudioHdr = NULL; |
|
|
|
if ( !LoadStudioModel( pModelName, buf ) ) |
|
{ |
|
VectorCopy( vec3_origin, m_StaticPropDict[i].m_Mins ); |
|
VectorCopy( vec3_origin, m_StaticPropDict[i].m_Maxs ); |
|
return; |
|
} |
|
|
|
studiohdr_t* pHdr = (studiohdr_t*)buf.Base(); |
|
|
|
VectorCopy( pHdr->hull_min, m_StaticPropDict[i].m_Mins ); |
|
VectorCopy( pHdr->hull_max, m_StaticPropDict[i].m_Maxs ); |
|
|
|
if ( LoadStudioCollisionModel( pModelName, bufphy ) ) |
|
{ |
|
phyheader_t header; |
|
bufphy.Get( &header, sizeof(header) ); |
|
|
|
vcollide_t *pCollide = &m_StaticPropDict[i].m_loadedModel; |
|
s_pPhysCollision->VCollideLoad( pCollide, header.solidCount, (const char *)bufphy.PeekGet(), bufphy.TellPut() - bufphy.TellGet() ); |
|
m_StaticPropDict[i].m_pModel = m_StaticPropDict[i].m_loadedModel.solids[0]; |
|
|
|
/* |
|
static int propNum = 0; |
|
char tmp[128]; |
|
sprintf( tmp, "staticprop%03d.txt", propNum ); |
|
DumpCollideToGlView( pCollide, tmp ); |
|
++propNum; |
|
*/ |
|
} |
|
else |
|
{ |
|
// mark this as unused |
|
m_StaticPropDict[i].m_loadedModel.solidCount = 0; |
|
|
|
// CPhysCollide* pPhys = CreatePhysCollide( pHdr, pVtxHdr ); |
|
m_StaticPropDict[i].m_pModel = ComputeConvexHull( pHdr ); |
|
} |
|
|
|
// clone it |
|
m_StaticPropDict[i].m_pStudioHdr = (studiohdr_t *)malloc( buf.Size() ); |
|
memcpy( m_StaticPropDict[i].m_pStudioHdr, (studiohdr_t*)buf.Base(), buf.Size() ); |
|
|
|
if ( !LoadVTXFile( pModelName, m_StaticPropDict[i].m_pStudioHdr, m_StaticPropDict[i].m_VtxBuf ) ) |
|
{ |
|
// failed, leave state identified as disabled |
|
m_StaticPropDict[i].m_VtxBuf.Purge(); |
|
} |
|
|
|
if ( g_bTextureShadows ) |
|
{ |
|
if ( (pHdr->flags & STUDIOHDR_FLAGS_CAST_TEXTURE_SHADOWS) || IsModelTextureShadowsForced(pModelName) ) |
|
{ |
|
m_StaticPropDict[i].m_textureShadowIndex.RemoveAll(); |
|
m_StaticPropDict[i].m_triangleMaterialIndex.RemoveAll(); |
|
m_StaticPropDict[i].m_textureShadowIndex.AddMultipleToTail( pHdr->numtextures ); |
|
g_ShadowTextureList.LoadAllTexturesForModel( pHdr, m_StaticPropDict[i].m_textureShadowIndex.Base() ); |
|
} |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Unserialize static prop model dictionary |
|
//----------------------------------------------------------------------------- |
|
void CVradStaticPropMgr::UnserializeModelDict( CUtlBuffer& buf ) |
|
{ |
|
int count = buf.GetInt(); |
|
while ( --count >= 0 ) |
|
{ |
|
StaticPropDictLump_t lump; |
|
buf.Get( &lump, sizeof(StaticPropDictLump_t) ); |
|
|
|
CreateCollisionModel( lump.m_Name ); |
|
} |
|
} |
|
|
|
void CVradStaticPropMgr::UnserializeModels( CUtlBuffer& buf ) |
|
{ |
|
int count = buf.GetInt(); |
|
|
|
|
|
m_StaticProps.AddMultipleToTail(count); |
|
for ( int i = 0; i < count; ++i ) |
|
{ |
|
StaticPropLump_t lump; |
|
buf.Get( &lump, sizeof(StaticPropLump_t) ); |
|
|
|
VectorCopy( lump.m_Origin, m_StaticProps[i].m_Origin ); |
|
VectorCopy( lump.m_Angles, m_StaticProps[i].m_Angles ); |
|
VectorCopy( lump.m_LightingOrigin, m_StaticProps[i].m_LightingOrigin ); |
|
m_StaticProps[i].m_bLightingOriginValid = ( lump.m_Flags & STATIC_PROP_USE_LIGHTING_ORIGIN ) > 0; |
|
m_StaticProps[i].m_ModelIdx = lump.m_PropType; |
|
m_StaticProps[i].m_Handle = TREEDATA_INVALID_HANDLE; |
|
m_StaticProps[i].m_Flags = lump.m_Flags; |
|
|
|
// Changed this from using DXT1 to RGB888 because the compression artifacts were pretty nasty. |
|
// TODO: Consider changing back or basing this on user selection in hammer. |
|
m_StaticProps[i].m_LightmapImageFormat = IMAGE_FORMAT_RGB888; |
|
m_StaticProps[i].m_LightmapImageWidth = lump.m_nLightmapResolutionX; |
|
m_StaticProps[i].m_LightmapImageHeight = lump.m_nLightmapResolutionY; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Unserialize static props |
|
//----------------------------------------------------------------------------- |
|
|
|
void CVradStaticPropMgr::UnserializeStaticProps() |
|
{ |
|
// Unserialize static props, insert them into the appropriate leaves |
|
GameLumpHandle_t handle = g_GameLumps.GetGameLumpHandle( GAMELUMP_STATIC_PROPS ); |
|
int size = g_GameLumps.GameLumpSize( handle ); |
|
if (!size) |
|
return; |
|
|
|
if ( g_GameLumps.GetGameLumpVersion( handle ) != GAMELUMP_STATIC_PROPS_VERSION ) |
|
{ |
|
Error( "Cannot load the static props... encountered a stale map version. Re-vbsp the map." ); |
|
} |
|
|
|
if ( g_GameLumps.GetGameLump( handle ) ) |
|
{ |
|
CUtlBuffer buf( g_GameLumps.GetGameLump(handle), size, CUtlBuffer::READ_ONLY ); |
|
UnserializeModelDict( buf ); |
|
|
|
// Skip the leaf list data |
|
int count = buf.GetInt(); |
|
buf.SeekGet( CUtlBuffer::SEEK_CURRENT, count * sizeof(StaticPropLeafLump_t) ); |
|
|
|
UnserializeModels( buf ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Level init, shutdown |
|
//----------------------------------------------------------------------------- |
|
|
|
void CVradStaticPropMgr::Init() |
|
{ |
|
CreateInterfaceFn physicsFactory = GetPhysicsFactory(); |
|
if ( !physicsFactory ) |
|
Error( "Unable to load vphysics DLL." ); |
|
|
|
s_pPhysCollision = (IPhysicsCollision *)physicsFactory( VPHYSICS_COLLISION_INTERFACE_VERSION, NULL ); |
|
if( !s_pPhysCollision ) |
|
{ |
|
Error( "Unable to get '%s' for physics interface.", VPHYSICS_COLLISION_INTERFACE_VERSION ); |
|
return; |
|
} |
|
|
|
// Read in static props that have been compiled into the bsp file |
|
UnserializeStaticProps(); |
|
} |
|
|
|
void CVradStaticPropMgr::Shutdown() |
|
{ |
|
|
|
// Remove all static prop model data |
|
for (int i = m_StaticPropDict.Size(); --i >= 0; ) |
|
{ |
|
studiohdr_t *pStudioHdr = m_StaticPropDict[i].m_pStudioHdr; |
|
if ( pStudioHdr ) |
|
{ |
|
if ( pStudioHdr->pVertexBase ) |
|
{ |
|
free( pStudioHdr->pVertexBase ); |
|
} |
|
free( pStudioHdr ); |
|
} |
|
} |
|
|
|
m_StaticProps.Purge(); |
|
m_StaticPropDict.Purge(); |
|
} |
|
|
|
void ComputeLightmapColor( dface_t* pFace, Vector &color ) |
|
{ |
|
texinfo_t* pTex = &texinfo[pFace->texinfo]; |
|
if ( pTex->flags & SURF_SKY ) |
|
{ |
|
// sky ambient already accounted for in direct component |
|
return; |
|
} |
|
} |
|
|
|
bool PositionInSolid( Vector &position ) |
|
{ |
|
int ndxLeaf = PointLeafnum( position ); |
|
if ( dleafs[ndxLeaf].contents & CONTENTS_SOLID ) |
|
{ |
|
// position embedded in solid |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Trace from a vertex to each direct light source, accumulating its contribution. |
|
//----------------------------------------------------------------------------- |
|
void ComputeDirectLightingAtPoint( Vector &position, Vector &normal, Vector &outColor, int iThread, |
|
int static_prop_id_to_skip=-1, int nLFlags = 0) |
|
{ |
|
SSE_sampleLightOutput_t sampleOutput; |
|
|
|
outColor.Init(); |
|
|
|
// Iterate over all direct lights and accumulate their contribution |
|
int cluster = ClusterFromPoint( position ); |
|
for ( directlight_t *dl = activelights; dl != NULL; dl = dl->next ) |
|
{ |
|
if ( dl->light.style ) |
|
{ |
|
// skip lights with style |
|
continue; |
|
} |
|
|
|
// is this lights cluster visible? |
|
if ( !PVSCheck( dl->pvs, cluster ) ) |
|
continue; |
|
|
|
// push the vertex towards the light to avoid surface acne |
|
Vector adjusted_pos = position; |
|
float flEpsilon = 0.0; |
|
|
|
if (dl->light.type != emit_skyambient) |
|
{ |
|
// push towards the light |
|
Vector fudge; |
|
if ( dl->light.type == emit_skylight ) |
|
fudge = -( dl->light.normal); |
|
else |
|
{ |
|
fudge = dl->light.origin-position; |
|
VectorNormalize( fudge ); |
|
} |
|
fudge *= 4.0; |
|
adjusted_pos += fudge; |
|
} |
|
else |
|
{ |
|
// push out along normal |
|
adjusted_pos += 4.0 * normal; |
|
// flEpsilon = 1.0; |
|
} |
|
|
|
FourVectors adjusted_pos4; |
|
FourVectors normal4; |
|
adjusted_pos4.DuplicateVector( adjusted_pos ); |
|
normal4.DuplicateVector( normal ); |
|
|
|
GatherSampleLightSSE( sampleOutput, dl, -1, adjusted_pos4, &normal4, 1, iThread, nLFlags | GATHERLFLAGS_FORCE_FAST, |
|
static_prop_id_to_skip, flEpsilon ); |
|
|
|
VectorMA( outColor, sampleOutput.m_flFalloff.m128_f32[0] * sampleOutput.m_flDot[0].m128_f32[0], dl->light.intensity, outColor ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Takes the results from a ComputeLighting call and applies it to the static prop in question. |
|
//----------------------------------------------------------------------------- |
|
void CVradStaticPropMgr::ApplyLightingToStaticProp( int iStaticProp, CStaticProp &prop, const CComputeStaticPropLightingResults *pResults ) |
|
{ |
|
if ( pResults->m_ColorVertsArrays.Count() == 0 && pResults->m_ColorTexelsArrays.Count() == 0 ) |
|
return; |
|
|
|
StaticPropDict_t &dict = m_StaticPropDict[prop.m_ModelIdx]; |
|
studiohdr_t *pStudioHdr = dict.m_pStudioHdr; |
|
OptimizedModel::FileHeader_t *pVtxHdr = (OptimizedModel::FileHeader_t *)dict.m_VtxBuf.Base(); |
|
Assert( pStudioHdr && pVtxHdr ); |
|
|
|
int iCurColorVertsArray = 0; |
|
int iCurColorTexelsArray = 0; |
|
|
|
for ( int bodyID = 0; bodyID < pStudioHdr->numbodyparts; ++bodyID ) |
|
{ |
|
OptimizedModel::BodyPartHeader_t* pVtxBodyPart = pVtxHdr->pBodyPart( bodyID ); |
|
mstudiobodyparts_t *pBodyPart = pStudioHdr->pBodypart( bodyID ); |
|
|
|
for ( int modelID = 0; modelID < pBodyPart->nummodels; ++modelID ) |
|
{ |
|
OptimizedModel::ModelHeader_t* pVtxModel = pVtxBodyPart->pModel( modelID ); |
|
mstudiomodel_t *pStudioModel = pBodyPart->pModel( modelID ); |
|
|
|
const CUtlVector<colorVertex_t> *colorVerts = pResults->m_ColorVertsArrays.Count() ? pResults->m_ColorVertsArrays[iCurColorVertsArray++] : nullptr; |
|
const CUtlVector<colorTexel_t> *colorTexels = pResults->m_ColorTexelsArrays.Count() ? pResults->m_ColorTexelsArrays[iCurColorTexelsArray++] : nullptr; |
|
|
|
for ( int nLod = 0; nLod < pVtxHdr->numLODs; nLod++ ) |
|
{ |
|
OptimizedModel::ModelLODHeader_t *pVtxLOD = pVtxModel->pLOD( nLod ); |
|
|
|
for ( int nMesh = 0; nMesh < pStudioModel->nummeshes; ++nMesh ) |
|
{ |
|
mstudiomesh_t* pMesh = pStudioModel->pMesh( nMesh ); |
|
OptimizedModel::MeshHeader_t* pVtxMesh = pVtxLOD->pMesh( nMesh ); |
|
|
|
for ( int nGroup = 0; nGroup < pVtxMesh->numStripGroups; ++nGroup ) |
|
{ |
|
OptimizedModel::StripGroupHeader_t* pStripGroup = pVtxMesh->pStripGroup( nGroup ); |
|
int nMeshIdx = prop.m_MeshData.AddToTail(); |
|
|
|
if (colorVerts) |
|
{ |
|
prop.m_MeshData[nMeshIdx].m_VertexColors.AddMultipleToTail( pStripGroup->numVerts ); |
|
prop.m_MeshData[nMeshIdx].m_nLod = nLod; |
|
|
|
for ( int nVertex = 0; nVertex < pStripGroup->numVerts; ++nVertex ) |
|
{ |
|
int nIndex = pMesh->vertexoffset + pStripGroup->pVertex( nVertex )->origMeshVertID; |
|
|
|
Assert( nIndex < pStudioModel->numvertices ); |
|
prop.m_MeshData[nMeshIdx].m_VertexColors[nVertex] = (*colorVerts)[nIndex].m_Color; |
|
} |
|
} |
|
|
|
if (colorTexels) |
|
{ |
|
// TODO: Consider doing this work in the worker threads, because then we distribute it. |
|
ConvertTexelDataToTexture(prop.m_LightmapImageWidth, prop.m_LightmapImageHeight, prop.m_LightmapImageFormat, (*colorTexels), &prop.m_MeshData[nMeshIdx].m_TexelsEncoded); |
|
|
|
if (g_bDumpPropLightmaps) |
|
{ |
|
char buffer[_MAX_PATH]; |
|
V_snprintf( |
|
buffer, |
|
_MAX_PATH - 1, |
|
"staticprop_lightmap_%d_%.0f_%.0f_%.0f_%s_%d_%d_%d_%d_%d.tga", |
|
iStaticProp, |
|
prop.m_Origin.x, |
|
prop.m_Origin.y, |
|
prop.m_Origin.z, |
|
dict.m_pStudioHdr->pszName(), |
|
bodyID, |
|
modelID, |
|
nLod, |
|
nMesh, |
|
nGroup |
|
); |
|
|
|
for ( int i = 0; buffer[i]; ++i ) |
|
{ |
|
if (buffer[i] == '/' || buffer[i] == '\\') |
|
buffer[i] = '-'; |
|
} |
|
DumpLightmapLinear( buffer, (*colorTexels), prop.m_LightmapImageWidth, prop.m_LightmapImageHeight ); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Trace rays from each unique vertex, accumulating direct and indirect |
|
// sources at each ray termination. Use the winding data to distribute the unique vertexes |
|
// into the rendering layout. |
|
//----------------------------------------------------------------------------- |
|
void CVradStaticPropMgr::ComputeLighting( CStaticProp &prop, int iThread, int prop_index, CComputeStaticPropLightingResults *pResults ) |
|
{ |
|
CUtlVector<badVertex_t> badVerts; |
|
|
|
StaticPropDict_t &dict = m_StaticPropDict[prop.m_ModelIdx]; |
|
studiohdr_t *pStudioHdr = dict.m_pStudioHdr; |
|
OptimizedModel::FileHeader_t *pVtxHdr = (OptimizedModel::FileHeader_t *)dict.m_VtxBuf.Base(); |
|
if ( !pStudioHdr || !pVtxHdr ) |
|
{ |
|
// must have model and its verts for lighting computation |
|
// game will fallback to fullbright |
|
return; |
|
} |
|
|
|
const bool withVertexLighting = (prop.m_Flags & STATIC_PROP_NO_PER_VERTEX_LIGHTING) == 0; |
|
const bool withTexelLighting = (prop.m_Flags & STATIC_PROP_NO_PER_TEXEL_LIGHTING) == 0; |
|
|
|
if (!withVertexLighting && !withTexelLighting) |
|
return; |
|
|
|
const int skip_prop = (g_bDisablePropSelfShadowing || (prop.m_Flags & STATIC_PROP_NO_SELF_SHADOWING)) ? prop_index : -1; |
|
const int nFlags = ( prop.m_Flags & STATIC_PROP_IGNORE_NORMALS ) ? GATHERLFLAGS_IGNORE_NORMALS : 0; |
|
|
|
VMPI_SetCurrentStage( "ComputeLighting" ); |
|
|
|
matrix3x4_t matPos, matNormal; |
|
AngleMatrix(prop.m_Angles, prop.m_Origin, matPos); |
|
AngleMatrix(prop.m_Angles, matNormal); |
|
|
|
for ( int bodyID = 0; bodyID < pStudioHdr->numbodyparts; ++bodyID ) |
|
{ |
|
OptimizedModel::BodyPartHeader_t* pVtxBodyPart = pVtxHdr->pBodyPart( bodyID ); |
|
mstudiobodyparts_t *pBodyPart = pStudioHdr->pBodypart( bodyID ); |
|
|
|
for ( int modelID = 0; modelID < pBodyPart->nummodels; ++modelID ) |
|
{ |
|
OptimizedModel::ModelHeader_t* pVtxModel = pVtxBodyPart->pModel(modelID); |
|
mstudiomodel_t *pStudioModel = pBodyPart->pModel( modelID ); |
|
|
|
if (withTexelLighting) |
|
{ |
|
CUtlVector<colorTexel_t> *pColorTexelArray = new CUtlVector<colorTexel_t>; |
|
pResults->m_ColorTexelsArrays.AddToTail(pColorTexelArray); |
|
} |
|
|
|
// light all unique vertexes |
|
CUtlVector<colorVertex_t> *pColorVertsArray = new CUtlVector<colorVertex_t>; |
|
pResults->m_ColorVertsArrays.AddToTail( pColorVertsArray ); |
|
|
|
CUtlVector<colorVertex_t> &colorVerts = *pColorVertsArray; |
|
colorVerts.EnsureCount( pStudioModel->numvertices ); |
|
memset( colorVerts.Base(), 0, colorVerts.Count() * sizeof(colorVertex_t) ); |
|
|
|
int numVertexes = 0; |
|
for ( int meshID = 0; meshID < pStudioModel->nummeshes; ++meshID ) |
|
{ |
|
mstudiomesh_t *pStudioMesh = pStudioModel->pMesh( meshID ); |
|
const mstudio_meshvertexdata_t *vertData = pStudioMesh->GetVertexData((void *)pStudioHdr); |
|
|
|
Assert(vertData); // This can only return NULL on X360 for now |
|
|
|
// TODO: Move this into its own function. In fact, refactor this whole function. |
|
if (withTexelLighting) |
|
{ |
|
GenerateLightmapSamplesForMesh( matPos, matNormal, iThread, skip_prop, nFlags, prop.m_LightmapImageWidth, prop.m_LightmapImageHeight, pStudioHdr, pStudioModel, pVtxModel, meshID, pResults ); |
|
} |
|
|
|
// If we do lightmapping, we also do vertex lighting as a potential fallback. This may change. |
|
for ( int vertexID = 0; vertexID < pStudioMesh->numvertices; ++vertexID ) |
|
{ |
|
Vector sampleNormal; |
|
Vector samplePosition; |
|
// transform position and normal into world coordinate system |
|
VectorTransform(*vertData->Position(vertexID), matPos, samplePosition); |
|
VectorTransform(*vertData->Normal(vertexID), matNormal, sampleNormal); |
|
|
|
if ( PositionInSolid( samplePosition ) ) |
|
{ |
|
// vertex is in solid, add to the bad list, and recover later |
|
badVertex_t badVertex; |
|
badVertex.m_ColorVertex = numVertexes; |
|
badVertex.m_Position = samplePosition; |
|
badVertex.m_Normal = sampleNormal; |
|
badVerts.AddToTail( badVertex ); |
|
} |
|
else |
|
{ |
|
Vector direct_pos=samplePosition; |
|
|
|
|
|
|
|
Vector directColor(0,0,0); |
|
ComputeDirectLightingAtPoint( direct_pos, |
|
sampleNormal, directColor, iThread, |
|
skip_prop, nFlags ); |
|
Vector indirectColor(0,0,0); |
|
|
|
if (g_bShowStaticPropNormals) |
|
{ |
|
directColor= sampleNormal; |
|
directColor += Vector(1.0,1.0,1.0); |
|
directColor *= 50.0; |
|
} |
|
else |
|
{ |
|
if (numbounce >= 1) |
|
ComputeIndirectLightingAtPoint( |
|
samplePosition, sampleNormal, |
|
indirectColor, iThread, true, |
|
( prop.m_Flags & STATIC_PROP_IGNORE_NORMALS) != 0 ); |
|
} |
|
|
|
colorVerts[numVertexes].m_bValid = true; |
|
colorVerts[numVertexes].m_Position = samplePosition; |
|
VectorAdd( directColor, indirectColor, colorVerts[numVertexes].m_Color ); |
|
} |
|
|
|
numVertexes++; |
|
} |
|
} |
|
|
|
// color in the bad vertexes |
|
// when entire model has no lighting origin and no valid neighbors |
|
// must punt, leave black coloring |
|
if ( badVerts.Count() && ( prop.m_bLightingOriginValid || badVerts.Count() != numVertexes ) ) |
|
{ |
|
for ( int nBadVertex = 0; nBadVertex < badVerts.Count(); nBadVertex++ ) |
|
{ |
|
Vector bestPosition; |
|
if ( prop.m_bLightingOriginValid ) |
|
{ |
|
// use the specified lighting origin |
|
VectorCopy( prop.m_LightingOrigin, bestPosition ); |
|
} |
|
else |
|
{ |
|
// find the closest valid neighbor |
|
int best = 0; |
|
float closest = FLT_MAX; |
|
for ( int nColorVertex = 0; nColorVertex < numVertexes; nColorVertex++ ) |
|
{ |
|
if ( !colorVerts[nColorVertex].m_bValid ) |
|
{ |
|
// skip invalid neighbors |
|
continue; |
|
} |
|
Vector delta; |
|
VectorSubtract( colorVerts[nColorVertex].m_Position, badVerts[nBadVertex].m_Position, delta ); |
|
float distance = VectorLength( delta ); |
|
if ( distance < closest ) |
|
{ |
|
closest = distance; |
|
best = nColorVertex; |
|
} |
|
} |
|
|
|
// use the best neighbor as the direction to crawl |
|
VectorCopy( colorVerts[best].m_Position, bestPosition ); |
|
} |
|
|
|
// crawl toward best position |
|
// sudivide to determine a closer valid point to the bad vertex, and re-light |
|
Vector midPosition; |
|
int numIterations = 20; |
|
while ( --numIterations > 0 ) |
|
{ |
|
VectorAdd( bestPosition, badVerts[nBadVertex].m_Position, midPosition ); |
|
VectorScale( midPosition, 0.5f, midPosition ); |
|
if ( PositionInSolid( midPosition ) ) |
|
break; |
|
bestPosition = midPosition; |
|
} |
|
|
|
// re-light from better position |
|
Vector directColor; |
|
ComputeDirectLightingAtPoint( bestPosition, badVerts[nBadVertex].m_Normal, directColor, iThread ); |
|
|
|
Vector indirectColor; |
|
ComputeIndirectLightingAtPoint( bestPosition, badVerts[nBadVertex].m_Normal, |
|
indirectColor, iThread, true ); |
|
|
|
// save results, not changing valid status |
|
// to ensure this offset position is not considered as a viable candidate |
|
colorVerts[badVerts[nBadVertex].m_ColorVertex].m_Position = bestPosition; |
|
VectorAdd( directColor, indirectColor, colorVerts[badVerts[nBadVertex].m_ColorVertex].m_Color ); |
|
} |
|
} |
|
|
|
// discard bad verts |
|
badVerts.Purge(); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Write the lighitng to bsp pak lump |
|
//----------------------------------------------------------------------------- |
|
void CVradStaticPropMgr::SerializeLighting() |
|
{ |
|
char filename[MAX_PATH]; |
|
CUtlBuffer utlBuf; |
|
|
|
// illuminate them all |
|
int count = m_StaticProps.Count(); |
|
if ( !count ) |
|
{ |
|
// nothing to do |
|
return; |
|
} |
|
|
|
char mapName[MAX_PATH]; |
|
Q_FileBase( source, mapName, sizeof( mapName ) ); |
|
|
|
int size; |
|
for (int i = 0; i < count; ++i) |
|
{ |
|
// no need to write this file if we didn't compute the data |
|
// props marked this way will not load the info anyway |
|
if ( m_StaticProps[i].m_Flags & STATIC_PROP_NO_PER_VERTEX_LIGHTING ) |
|
continue; |
|
|
|
if (g_bHDR) |
|
{ |
|
sprintf( filename, "sp_hdr_%d.vhv", i ); |
|
} |
|
else |
|
{ |
|
sprintf( filename, "sp_%d.vhv", i ); |
|
} |
|
|
|
int totalVertexes = 0; |
|
for ( int j=0; j<m_StaticProps[i].m_MeshData.Count(); j++ ) |
|
{ |
|
totalVertexes += m_StaticProps[i].m_MeshData[j].m_VertexColors.Count(); |
|
} |
|
|
|
// allocate a buffer with enough padding for alignment |
|
size = sizeof( HardwareVerts::FileHeader_t ) + |
|
m_StaticProps[i].m_MeshData.Count()*sizeof(HardwareVerts::MeshHeader_t) + |
|
totalVertexes*4 + 2*512; |
|
utlBuf.EnsureCapacity( size ); |
|
Q_memset( utlBuf.Base(), 0, size ); |
|
|
|
HardwareVerts::FileHeader_t *pVhvHdr = (HardwareVerts::FileHeader_t *)utlBuf.Base(); |
|
|
|
// align to start of vertex data |
|
unsigned char *pVertexData = (unsigned char *)(sizeof( HardwareVerts::FileHeader_t ) + m_StaticProps[i].m_MeshData.Count()*sizeof(HardwareVerts::MeshHeader_t)); |
|
pVertexData = (unsigned char*)pVhvHdr + ALIGN_TO_POW2( (unsigned int)pVertexData, 512 ); |
|
|
|
// construct header |
|
pVhvHdr->m_nVersion = VHV_VERSION; |
|
pVhvHdr->m_nChecksum = m_StaticPropDict[m_StaticProps[i].m_ModelIdx].m_pStudioHdr->checksum; |
|
pVhvHdr->m_nVertexFlags = VERTEX_COLOR; |
|
pVhvHdr->m_nVertexSize = 4; |
|
pVhvHdr->m_nVertexes = totalVertexes; |
|
pVhvHdr->m_nMeshes = m_StaticProps[i].m_MeshData.Count(); |
|
|
|
for (int n=0; n<pVhvHdr->m_nMeshes; n++) |
|
{ |
|
// construct mesh dictionary |
|
HardwareVerts::MeshHeader_t *pMesh = pVhvHdr->pMesh( n ); |
|
pMesh->m_nLod = m_StaticProps[i].m_MeshData[n].m_nLod; |
|
pMesh->m_nVertexes = m_StaticProps[i].m_MeshData[n].m_VertexColors.Count(); |
|
pMesh->m_nOffset = (unsigned int)pVertexData - (unsigned int)pVhvHdr; |
|
|
|
// construct vertexes |
|
for (int k=0; k<pMesh->m_nVertexes; k++) |
|
{ |
|
Vector &vertexColor = m_StaticProps[i].m_MeshData[n].m_VertexColors[k]; |
|
|
|
ColorRGBExp32 rgbColor; |
|
VectorToColorRGBExp32( vertexColor, rgbColor ); |
|
unsigned char dstColor[4]; |
|
ConvertRGBExp32ToRGBA8888( &rgbColor, dstColor ); |
|
|
|
// b,g,r,a order |
|
pVertexData[0] = dstColor[2]; |
|
pVertexData[1] = dstColor[1]; |
|
pVertexData[2] = dstColor[0]; |
|
pVertexData[3] = dstColor[3]; |
|
pVertexData += 4; |
|
} |
|
} |
|
|
|
// align to end of file |
|
pVertexData = (unsigned char *)((unsigned int)pVertexData - (unsigned int)pVhvHdr); |
|
pVertexData = (unsigned char*)pVhvHdr + ALIGN_TO_POW2( (unsigned int)pVertexData, 512 ); |
|
|
|
AddBufferToPak( GetPakFile(), filename, (void*)pVhvHdr, pVertexData - (unsigned char*)pVhvHdr, false ); |
|
} |
|
|
|
for (int i = 0; i < count; ++i) |
|
{ |
|
const int kAlignment = 512; |
|
// no need to write this file if we didn't compute the data |
|
// props marked this way will not load the info anyway |
|
if (m_StaticProps[i].m_Flags & STATIC_PROP_NO_PER_TEXEL_LIGHTING) |
|
continue; |
|
|
|
sprintf(filename, "texelslighting_%d.ppl", i); |
|
|
|
ImageFormat fmt = m_StaticProps[i].m_LightmapImageFormat; |
|
|
|
unsigned int totalTexelSizeBytes = 0; |
|
for (int j = 0; j < m_StaticProps[i].m_MeshData.Count(); j++) |
|
{ |
|
totalTexelSizeBytes += m_StaticProps[i].m_MeshData[j].m_TexelsEncoded.Count(); |
|
} |
|
|
|
// allocate a buffer with enough padding for alignment |
|
size = sizeof(HardwareTexels::FileHeader_t) |
|
+ m_StaticProps[i].m_MeshData.Count() * sizeof(HardwareTexels::MeshHeader_t) |
|
+ totalTexelSizeBytes |
|
+ 2 * kAlignment; |
|
|
|
utlBuf.EnsureCapacity(size); |
|
Q_memset(utlBuf.Base(), 0, size); |
|
|
|
HardwareTexels::FileHeader_t *pVhtHdr = (HardwareTexels::FileHeader_t *)utlBuf.Base(); |
|
|
|
// align start of texel data |
|
unsigned char *pTexelData = (unsigned char *)(sizeof(HardwareTexels::FileHeader_t) + m_StaticProps[i].m_MeshData.Count() * sizeof(HardwareTexels::MeshHeader_t)); |
|
pTexelData = (unsigned char*)pVhtHdr + ALIGN_TO_POW2((unsigned int)pTexelData, kAlignment); |
|
|
|
pVhtHdr->m_nVersion = VHT_VERSION; |
|
pVhtHdr->m_nChecksum = m_StaticPropDict[m_StaticProps[i].m_ModelIdx].m_pStudioHdr->checksum; |
|
pVhtHdr->m_nTexelFormat = fmt; |
|
pVhtHdr->m_nMeshes = m_StaticProps[i].m_MeshData.Count(); |
|
|
|
for (int n = 0; n < pVhtHdr->m_nMeshes; n++) |
|
{ |
|
HardwareTexels::MeshHeader_t *pMesh = pVhtHdr->pMesh(n); |
|
pMesh->m_nLod = m_StaticProps[i].m_MeshData[n].m_nLod; |
|
pMesh->m_nOffset = (unsigned int)pTexelData - (unsigned int)pVhtHdr; |
|
pMesh->m_nBytes = m_StaticProps[i].m_MeshData[n].m_TexelsEncoded.Count(); |
|
pMesh->m_nWidth = m_StaticProps[i].m_LightmapImageWidth; |
|
pMesh->m_nHeight = m_StaticProps[i].m_LightmapImageHeight; |
|
|
|
Q_memcpy(pTexelData, m_StaticProps[i].m_MeshData[n].m_TexelsEncoded.Base(), m_StaticProps[i].m_MeshData[n].m_TexelsEncoded.Count()); |
|
pTexelData += m_StaticProps[i].m_MeshData[n].m_TexelsEncoded.Count(); |
|
} |
|
|
|
pTexelData = (unsigned char *)((unsigned int)pTexelData - (unsigned int)pVhtHdr); |
|
pTexelData = (unsigned char*)pVhtHdr + ALIGN_TO_POW2((unsigned int)pTexelData, kAlignment); |
|
|
|
AddBufferToPak(GetPakFile(), filename, (void*)pVhtHdr, pTexelData - (unsigned char*)pVhtHdr, false); |
|
} |
|
} |
|
|
|
void CVradStaticPropMgr::VMPI_ProcessStaticProp_Static( int iThread, uint64 iStaticProp, MessageBuffer *pBuf ) |
|
{ |
|
g_StaticPropMgr.VMPI_ProcessStaticProp( iThread, iStaticProp, pBuf ); |
|
} |
|
|
|
void CVradStaticPropMgr::VMPI_ReceiveStaticPropResults_Static( uint64 iStaticProp, MessageBuffer *pBuf, int iWorker ) |
|
{ |
|
g_StaticPropMgr.VMPI_ReceiveStaticPropResults( iStaticProp, pBuf, iWorker ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Called on workers to do the computation for a static prop and send |
|
// it to the master. |
|
//----------------------------------------------------------------------------- |
|
void CVradStaticPropMgr::VMPI_ProcessStaticProp( int iThread, int iStaticProp, MessageBuffer *pBuf ) |
|
{ |
|
// Compute the lighting. |
|
CComputeStaticPropLightingResults results; |
|
ComputeLighting( m_StaticProps[iStaticProp], iThread, iStaticProp, &results ); |
|
|
|
VMPI_SetCurrentStage( "EncodeLightingResults" ); |
|
|
|
// Encode the results. |
|
int nLists = results.m_ColorVertsArrays.Count(); |
|
pBuf->write( &nLists, sizeof( nLists ) ); |
|
|
|
for ( int i=0; i < nLists; i++ ) |
|
{ |
|
CUtlVector<colorVertex_t> &curList = *results.m_ColorVertsArrays[i]; |
|
int count = curList.Count(); |
|
pBuf->write( &count, sizeof( count ) ); |
|
pBuf->write( curList.Base(), curList.Count() * sizeof( colorVertex_t ) ); |
|
} |
|
|
|
nLists = results.m_ColorTexelsArrays.Count(); |
|
pBuf->write(&nLists, sizeof(nLists)); |
|
|
|
for (int i = 0; i < nLists; i++) |
|
{ |
|
CUtlVector<colorTexel_t> &curList = *results.m_ColorTexelsArrays[i]; |
|
int count = curList.Count(); |
|
pBuf->write(&count, sizeof(count)); |
|
pBuf->write(curList.Base(), curList.Count() * sizeof(colorTexel_t)); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Called on the master when a worker finishes processing a static prop. |
|
//----------------------------------------------------------------------------- |
|
void CVradStaticPropMgr::VMPI_ReceiveStaticPropResults( int iStaticProp, MessageBuffer *pBuf, int iWorker ) |
|
{ |
|
// Read in the results. |
|
CComputeStaticPropLightingResults results; |
|
|
|
int nLists; |
|
pBuf->read( &nLists, sizeof( nLists ) ); |
|
|
|
for ( int i=0; i < nLists; i++ ) |
|
{ |
|
CUtlVector<colorVertex_t> *pList = new CUtlVector<colorVertex_t>; |
|
results.m_ColorVertsArrays.AddToTail( pList ); |
|
|
|
int count; |
|
pBuf->read( &count, sizeof( count ) ); |
|
pList->SetSize( count ); |
|
pBuf->read( pList->Base(), count * sizeof( colorVertex_t ) ); |
|
} |
|
|
|
pBuf->read(&nLists, sizeof(nLists)); |
|
|
|
for (int i = 0; i < nLists; i++) |
|
{ |
|
CUtlVector<colorTexel_t> *pList = new CUtlVector<colorTexel_t>; |
|
results.m_ColorTexelsArrays.AddToTail(pList); |
|
|
|
int count; |
|
pBuf->read(&count, sizeof(count)); |
|
pList->SetSize(count); |
|
pBuf->read(pList->Base(), count * sizeof(colorTexel_t)); |
|
} |
|
|
|
// Apply the results. |
|
ApplyLightingToStaticProp( iStaticProp, m_StaticProps[iStaticProp], &results ); |
|
} |
|
|
|
|
|
void CVradStaticPropMgr::ComputeLightingForProp( int iThread, int iStaticProp ) |
|
{ |
|
// Compute the lighting. |
|
CComputeStaticPropLightingResults results; |
|
ComputeLighting( m_StaticProps[iStaticProp], iThread, iStaticProp, &results ); |
|
ApplyLightingToStaticProp( iStaticProp, m_StaticProps[iStaticProp], &results ); |
|
} |
|
|
|
void CVradStaticPropMgr::ThreadComputeStaticPropLighting( int iThread, void *pUserData ) |
|
{ |
|
while (1) |
|
{ |
|
int j = GetThreadWork (); |
|
if (j == -1) |
|
break; |
|
CComputeStaticPropLightingResults results; |
|
g_StaticPropMgr.ComputeLightingForProp( iThread, j ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Computes lighting for the static props. |
|
// Must be after all other surface lighting has been computed for the indirect sampling. |
|
//----------------------------------------------------------------------------- |
|
void CVradStaticPropMgr::ComputeLighting( int iThread ) |
|
{ |
|
// illuminate them all |
|
int count = m_StaticProps.Count(); |
|
if ( !count ) |
|
{ |
|
// nothing to do |
|
return; |
|
} |
|
|
|
StartPacifier( "Computing static prop lighting : " ); |
|
|
|
// ensure any traces against us are ignored because we have no inherit lighting contribution |
|
m_bIgnoreStaticPropTrace = true; |
|
|
|
if ( g_bUseMPI ) |
|
{ |
|
// Distribute the work among the workers. |
|
VMPI_SetCurrentStage( "CVradStaticPropMgr::ComputeLighting" ); |
|
|
|
DistributeWork( |
|
count, |
|
VMPI_DISTRIBUTEWORK_PACKETID, |
|
&CVradStaticPropMgr::VMPI_ProcessStaticProp_Static, |
|
&CVradStaticPropMgr::VMPI_ReceiveStaticPropResults_Static ); |
|
} |
|
else |
|
{ |
|
RunThreadsOn(count, true, ThreadComputeStaticPropLighting); |
|
} |
|
|
|
// restore default |
|
m_bIgnoreStaticPropTrace = false; |
|
|
|
// save data to bsp |
|
SerializeLighting(); |
|
|
|
EndPacifier( true ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Adds all static prop polys to the ray trace store. |
|
//----------------------------------------------------------------------------- |
|
void CVradStaticPropMgr::AddPolysForRayTrace( void ) |
|
{ |
|
int count = m_StaticProps.Count(); |
|
if ( !count ) |
|
{ |
|
// nothing to do |
|
return; |
|
} |
|
|
|
// Triangle coverage of 1 (full coverage) |
|
Vector fullCoverage; |
|
fullCoverage.x = 1.0f; |
|
|
|
for ( int nProp = 0; nProp < count; ++nProp ) |
|
{ |
|
CStaticProp &prop = m_StaticProps[nProp]; |
|
StaticPropDict_t &dict = m_StaticPropDict[prop.m_ModelIdx]; |
|
|
|
if ( prop.m_Flags & STATIC_PROP_NO_SHADOW ) |
|
continue; |
|
|
|
// If not using static prop polys, use AABB |
|
if ( !g_bStaticPropPolys ) |
|
{ |
|
if ( dict.m_pModel ) |
|
{ |
|
VMatrix xform; |
|
xform.SetupMatrixOrgAngles ( prop.m_Origin, prop.m_Angles ); |
|
ICollisionQuery *queryModel = s_pPhysCollision->CreateQueryModel( dict.m_pModel ); |
|
for ( int nConvex = 0; nConvex < queryModel->ConvexCount(); ++nConvex ) |
|
{ |
|
for ( int nTri = 0; nTri < queryModel->TriangleCount( nConvex ); ++nTri ) |
|
{ |
|
Vector verts[3]; |
|
queryModel->GetTriangleVerts( nConvex, nTri, verts ); |
|
for ( int nVert = 0; nVert < 3; ++nVert ) |
|
verts[nVert] = xform.VMul4x3(verts[nVert]); |
|
g_RtEnv.AddTriangle ( TRACE_ID_STATICPROP | nProp, verts[0], verts[1], verts[2], fullCoverage ); |
|
} |
|
} |
|
s_pPhysCollision->DestroyQueryModel( queryModel ); |
|
} |
|
else |
|
{ |
|
VectorAdd ( dict.m_Mins, prop.m_Origin, prop.m_mins ); |
|
VectorAdd ( dict.m_Maxs, prop.m_Origin, prop.m_maxs ); |
|
g_RtEnv.AddAxisAlignedRectangularSolid ( TRACE_ID_STATICPROP | nProp, prop.m_mins, prop.m_maxs, fullCoverage ); |
|
} |
|
|
|
continue; |
|
} |
|
|
|
studiohdr_t *pStudioHdr = dict.m_pStudioHdr; |
|
OptimizedModel::FileHeader_t *pVtxHdr = (OptimizedModel::FileHeader_t *)dict.m_VtxBuf.Base(); |
|
if ( !pStudioHdr || !pVtxHdr ) |
|
{ |
|
// must have model and its verts for decoding triangles |
|
return; |
|
} |
|
// only init the triangle table the first time |
|
bool bInitTriangles = dict.m_triangleMaterialIndex.Count() ? false : true; |
|
int triangleIndex = 0; |
|
|
|
// meshes are deeply hierarchial, divided between three stores, follow the white rabbit |
|
// body parts -> models -> lod meshes -> strip groups -> strips |
|
// the vertices and indices are pooled, the trick is knowing the offset to determine your indexed base |
|
for ( int bodyID = 0; bodyID < pStudioHdr->numbodyparts; ++bodyID ) |
|
{ |
|
OptimizedModel::BodyPartHeader_t* pVtxBodyPart = pVtxHdr->pBodyPart( bodyID ); |
|
mstudiobodyparts_t *pBodyPart = pStudioHdr->pBodypart( bodyID ); |
|
|
|
for ( int modelID = 0; modelID < pBodyPart->nummodels; ++modelID ) |
|
{ |
|
OptimizedModel::ModelHeader_t* pVtxModel = pVtxBodyPart->pModel( modelID ); |
|
mstudiomodel_t *pStudioModel = pBodyPart->pModel( modelID ); |
|
|
|
// assuming lod 0, could iterate if required |
|
int nLod = 0; |
|
OptimizedModel::ModelLODHeader_t *pVtxLOD = pVtxModel->pLOD( nLod ); |
|
|
|
for ( int nMesh = 0; nMesh < pStudioModel->nummeshes; ++nMesh ) |
|
{ |
|
// check if this mesh's material is in the no shadow material name list |
|
mstudiomesh_t* pMesh = pStudioModel->pMesh( nMesh ); |
|
mstudiotexture_t *pTxtr=pStudioHdr->pTexture(pMesh->material); |
|
//printf("mat idx=%d mat name=%s\n",pMesh->material,pTxtr->pszName()); |
|
bool bSkipThisMesh = false; |
|
for(int check=0; check<g_NonShadowCastingMaterialStrings.Count(); check++) |
|
{ |
|
if ( Q_stristr( pTxtr->pszName(), |
|
g_NonShadowCastingMaterialStrings[check] ) ) |
|
{ |
|
//printf("skip mat name=%s\n",pTxtr->pszName()); |
|
bSkipThisMesh = true; |
|
break; |
|
} |
|
} |
|
if ( bSkipThisMesh) |
|
continue; |
|
|
|
int shadowTextureIndex = -1; |
|
if ( dict.m_textureShadowIndex.Count() ) |
|
{ |
|
shadowTextureIndex = dict.m_textureShadowIndex[pMesh->material]; |
|
} |
|
|
|
|
|
OptimizedModel::MeshHeader_t* pVtxMesh = pVtxLOD->pMesh( nMesh ); |
|
const mstudio_meshvertexdata_t *vertData = pMesh->GetVertexData( (void *)pStudioHdr ); |
|
Assert( vertData ); // This can only return NULL on X360 for now |
|
|
|
for ( int nGroup = 0; nGroup < pVtxMesh->numStripGroups; ++nGroup ) |
|
{ |
|
OptimizedModel::StripGroupHeader_t* pStripGroup = pVtxMesh->pStripGroup( nGroup ); |
|
|
|
int nStrip; |
|
for ( nStrip = 0; nStrip < pStripGroup->numStrips; nStrip++ ) |
|
{ |
|
OptimizedModel::StripHeader_t *pStrip = pStripGroup->pStrip( nStrip ); |
|
|
|
if ( pStrip->flags & OptimizedModel::STRIP_IS_TRILIST ) |
|
{ |
|
for ( int i = 0; i < pStrip->numIndices; i += 3 ) |
|
{ |
|
int idx = pStrip->indexOffset + i; |
|
|
|
unsigned short i1 = *pStripGroup->pIndex( idx ); |
|
unsigned short i2 = *pStripGroup->pIndex( idx + 1 ); |
|
unsigned short i3 = *pStripGroup->pIndex( idx + 2 ); |
|
|
|
int vertex1 = pStripGroup->pVertex( i1 )->origMeshVertID; |
|
int vertex2 = pStripGroup->pVertex( i2 )->origMeshVertID; |
|
int vertex3 = pStripGroup->pVertex( i3 )->origMeshVertID; |
|
|
|
// transform position into world coordinate system |
|
matrix3x4_t matrix; |
|
AngleMatrix( prop.m_Angles, prop.m_Origin, matrix ); |
|
|
|
Vector position1; |
|
Vector position2; |
|
Vector position3; |
|
VectorTransform( *vertData->Position( vertex1 ), matrix, position1 ); |
|
VectorTransform( *vertData->Position( vertex2 ), matrix, position2 ); |
|
VectorTransform( *vertData->Position( vertex3 ), matrix, position3 ); |
|
unsigned short flags = 0; |
|
int materialIndex = -1; |
|
Vector color = vec3_origin; |
|
if ( shadowTextureIndex >= 0 ) |
|
{ |
|
if ( bInitTriangles ) |
|
{ |
|
// add texture space and texture index to material database |
|
// now |
|
float coverage = g_ShadowTextureList.ComputeCoverageForTriangle(shadowTextureIndex, *vertData->Texcoord(vertex1), *vertData->Texcoord(vertex2), *vertData->Texcoord(vertex3) ); |
|
if ( coverage < 1.0f ) |
|
{ |
|
materialIndex = g_ShadowTextureList.AddMaterialEntry( shadowTextureIndex, *vertData->Texcoord(vertex1), *vertData->Texcoord(vertex2), *vertData->Texcoord(vertex3) ); |
|
color.x = coverage; |
|
} |
|
else |
|
{ |
|
materialIndex = -1; |
|
} |
|
dict.m_triangleMaterialIndex.AddToTail(materialIndex); |
|
} |
|
else |
|
{ |
|
materialIndex = dict.m_triangleMaterialIndex[triangleIndex]; |
|
triangleIndex++; |
|
} |
|
if ( materialIndex >= 0 ) |
|
{ |
|
flags = FCACHETRI_TRANSPARENT; |
|
} |
|
} |
|
// printf( "\ngl 3\n" ); |
|
// printf( "gl %6.3f %6.3f %6.3f 1 0 0\n", XYZ(position1)); |
|
// printf( "gl %6.3f %6.3f %6.3f 0 1 0\n", XYZ(position2)); |
|
// printf( "gl %6.3f %6.3f %6.3f 0 0 1\n", XYZ(position3)); |
|
g_RtEnv.AddTriangle( TRACE_ID_STATICPROP | nProp, |
|
position1, position2, position3, |
|
color, flags, materialIndex); |
|
} |
|
} |
|
else |
|
{ |
|
// all tris expected to be discrete tri lists |
|
// must fixme if stripping ever occurs |
|
printf( "unexpected strips found\n" ); |
|
Assert( 0 ); |
|
return; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
struct tl_tri_t |
|
{ |
|
Vector p0; |
|
Vector p1; |
|
Vector p2; |
|
Vector n0; |
|
Vector n1; |
|
Vector n2; |
|
|
|
bool operator == (const tl_tri_t &t) const |
|
{ |
|
return ( p0 == t.p0 && |
|
p1 == t.p1 && |
|
p2 == t.p2 && |
|
n0 == t.n0 && |
|
n1 == t.n1 && |
|
n2 == t.n2 ); |
|
} |
|
}; |
|
|
|
struct tl_vert_t |
|
{ |
|
Vector m_position; |
|
CUtlLinkedList< tl_tri_t, int > m_triList; |
|
}; |
|
|
|
void AddTriVertsToList( CUtlVector< tl_vert_t > &triListVerts, int vertIndex, Vector vertPosition, Vector p0, Vector p1, Vector p2, Vector n0, Vector n1, Vector n2 ) |
|
{ |
|
tl_tri_t tlTri; |
|
|
|
tlTri.p0 = p0; |
|
tlTri.p1 = p1; |
|
tlTri.p2 = p2; |
|
tlTri.n0 = n0; |
|
tlTri.n1 = n1; |
|
tlTri.n2 = n2; |
|
|
|
triListVerts.EnsureCapacity( vertIndex+1 ); |
|
|
|
triListVerts[vertIndex].m_position = vertPosition; |
|
|
|
int index = triListVerts[vertIndex].m_triList.Find( tlTri ); |
|
if ( !triListVerts[vertIndex].m_triList.IsValidIndex( index ) ) |
|
{ |
|
// not in list, add to list of triangles |
|
triListVerts[vertIndex].m_triList.AddToTail( tlTri ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Builds a list of tris for every vertex |
|
//----------------------------------------------------------------------------- |
|
void CVradStaticPropMgr::BuildTriList( CStaticProp &prop ) |
|
{ |
|
// the generated list will consist of a list of verts |
|
// each vert will have a linked list of triangles that it belongs to |
|
CUtlVector< tl_vert_t > triListVerts; |
|
|
|
StaticPropDict_t &dict = m_StaticPropDict[prop.m_ModelIdx]; |
|
studiohdr_t *pStudioHdr = dict.m_pStudioHdr; |
|
OptimizedModel::FileHeader_t *pVtxHdr = (OptimizedModel::FileHeader_t *)dict.m_VtxBuf.Base(); |
|
if ( !pStudioHdr || !pVtxHdr ) |
|
{ |
|
// must have model and its verts for decoding triangles |
|
return; |
|
} |
|
|
|
// meshes are deeply hierarchial, divided between three stores, follow the white rabbit |
|
// body parts -> models -> lod meshes -> strip groups -> strips |
|
// the vertices and indices are pooled, the trick is knowing the offset to determine your indexed base |
|
for ( int bodyID = 0; bodyID < pStudioHdr->numbodyparts; ++bodyID ) |
|
{ |
|
OptimizedModel::BodyPartHeader_t* pVtxBodyPart = pVtxHdr->pBodyPart( bodyID ); |
|
mstudiobodyparts_t *pBodyPart = pStudioHdr->pBodypart( bodyID ); |
|
|
|
for ( int modelID = 0; modelID < pBodyPart->nummodels; ++modelID ) |
|
{ |
|
OptimizedModel::ModelHeader_t* pVtxModel = pVtxBodyPart->pModel( modelID ); |
|
mstudiomodel_t *pStudioModel = pBodyPart->pModel( modelID ); |
|
|
|
// get the specified lod, assuming lod 0 |
|
int nLod = 0; |
|
OptimizedModel::ModelLODHeader_t *pVtxLOD = pVtxModel->pLOD( nLod ); |
|
|
|
// must reset because each model has their own vertexes [0..n] |
|
// in order for this to be monolithic for the entire prop the list must be segmented |
|
triListVerts.Purge(); |
|
|
|
for ( int nMesh = 0; nMesh < pStudioModel->nummeshes; ++nMesh ) |
|
{ |
|
mstudiomesh_t* pMesh = pStudioModel->pMesh( nMesh ); |
|
OptimizedModel::MeshHeader_t* pVtxMesh = pVtxLOD->pMesh( nMesh ); |
|
const mstudio_meshvertexdata_t *vertData = pMesh->GetVertexData( (void *)pStudioHdr ); |
|
Assert( vertData ); // This can only return NULL on X360 for now |
|
|
|
for ( int nGroup = 0; nGroup < pVtxMesh->numStripGroups; ++nGroup ) |
|
{ |
|
OptimizedModel::StripGroupHeader_t* pStripGroup = pVtxMesh->pStripGroup( nGroup ); |
|
|
|
int nStrip; |
|
for ( nStrip = 0; nStrip < pStripGroup->numStrips; nStrip++ ) |
|
{ |
|
OptimizedModel::StripHeader_t *pStrip = pStripGroup->pStrip( nStrip ); |
|
|
|
if ( pStrip->flags & OptimizedModel::STRIP_IS_TRILIST ) |
|
{ |
|
for ( int i = 0; i < pStrip->numIndices; i += 3 ) |
|
{ |
|
int idx = pStrip->indexOffset + i; |
|
|
|
unsigned short i1 = *pStripGroup->pIndex( idx ); |
|
unsigned short i2 = *pStripGroup->pIndex( idx + 1 ); |
|
unsigned short i3 = *pStripGroup->pIndex( idx + 2 ); |
|
|
|
int vertex1 = pStripGroup->pVertex( i1 )->origMeshVertID; |
|
int vertex2 = pStripGroup->pVertex( i2 )->origMeshVertID; |
|
int vertex3 = pStripGroup->pVertex( i3 )->origMeshVertID; |
|
|
|
// transform position into world coordinate system |
|
matrix3x4_t matrix; |
|
AngleMatrix( prop.m_Angles, prop.m_Origin, matrix ); |
|
|
|
Vector position1; |
|
Vector position2; |
|
Vector position3; |
|
VectorTransform( *vertData->Position( vertex1 ), matrix, position1 ); |
|
VectorTransform( *vertData->Position( vertex2 ), matrix, position2 ); |
|
VectorTransform( *vertData->Position( vertex3 ), matrix, position3 ); |
|
|
|
Vector normal1; |
|
Vector normal2; |
|
Vector normal3; |
|
VectorTransform( *vertData->Normal( vertex1 ), matrix, normal1 ); |
|
VectorTransform( *vertData->Normal( vertex2 ), matrix, normal2 ); |
|
VectorTransform( *vertData->Normal( vertex3 ), matrix, normal3 ); |
|
|
|
AddTriVertsToList( triListVerts, pMesh->vertexoffset + vertex1, position1, position1, position2, position3, normal1, normal2, normal3 ); |
|
AddTriVertsToList( triListVerts, pMesh->vertexoffset + vertex2, position2, position1, position2, position3, normal1, normal2, normal3 ); |
|
AddTriVertsToList( triListVerts, pMesh->vertexoffset + vertex3, position3, position1, position2, position3, normal1, normal2, normal3 ); |
|
} |
|
} |
|
else |
|
{ |
|
// all tris expected to be discrete tri lists |
|
// must fixme if stripping ever occurs |
|
printf( "unexpected strips found\n" ); |
|
Assert( 0 ); |
|
return; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
const vertexFileHeader_t * mstudiomodel_t::CacheVertexData( void *pModelData ) |
|
{ |
|
studiohdr_t *pActiveStudioHdr = static_cast<studiohdr_t *>(pModelData); |
|
Assert( pActiveStudioHdr ); |
|
|
|
if ( pActiveStudioHdr->pVertexBase ) |
|
{ |
|
return (vertexFileHeader_t *)pActiveStudioHdr->pVertexBase; |
|
} |
|
|
|
// mandatory callback to make requested data resident |
|
// load and persist the vertex file |
|
char fileName[MAX_PATH]; |
|
strcpy( fileName, "models/" ); |
|
strcat( fileName, pActiveStudioHdr->pszName() ); |
|
Q_StripExtension( fileName, fileName, sizeof( fileName ) ); |
|
strcat( fileName, ".vvd" ); |
|
|
|
// load the model |
|
FileHandle_t fileHandle = g_pFileSystem->Open( fileName, "rb" ); |
|
if ( !fileHandle ) |
|
{ |
|
Error( "Unable to load vertex data \"%s\"\n", fileName ); |
|
} |
|
|
|
// Get the file size |
|
int vvdSize = g_pFileSystem->Size( fileHandle ); |
|
if ( vvdSize == 0 ) |
|
{ |
|
g_pFileSystem->Close( fileHandle ); |
|
Error( "Bad size for vertex data \"%s\"\n", fileName ); |
|
} |
|
|
|
vertexFileHeader_t *pVvdHdr = (vertexFileHeader_t *)malloc( vvdSize ); |
|
g_pFileSystem->Read( pVvdHdr, vvdSize, fileHandle ); |
|
g_pFileSystem->Close( fileHandle ); |
|
|
|
// check header |
|
if ( pVvdHdr->id != MODEL_VERTEX_FILE_ID ) |
|
{ |
|
Error("Error Vertex File %s id %d should be %d\n", fileName, pVvdHdr->id, MODEL_VERTEX_FILE_ID); |
|
} |
|
if ( pVvdHdr->version != MODEL_VERTEX_FILE_VERSION ) |
|
{ |
|
Error("Error Vertex File %s version %d should be %d\n", fileName, pVvdHdr->version, MODEL_VERTEX_FILE_VERSION); |
|
} |
|
if ( pVvdHdr->checksum != pActiveStudioHdr->checksum ) |
|
{ |
|
Error("Error Vertex File %s checksum %d should be %d\n", fileName, pVvdHdr->checksum, pActiveStudioHdr->checksum); |
|
} |
|
|
|
// need to perform mesh relocation fixups |
|
// allocate a new copy |
|
vertexFileHeader_t *pNewVvdHdr = (vertexFileHeader_t *)malloc( vvdSize ); |
|
if ( !pNewVvdHdr ) |
|
{ |
|
Error( "Error allocating %d bytes for Vertex File '%s'\n", vvdSize, fileName ); |
|
} |
|
|
|
// load vertexes and run fixups |
|
Studio_LoadVertexes( pVvdHdr, pNewVvdHdr, 0, true ); |
|
|
|
// discard original |
|
free( pVvdHdr ); |
|
pVvdHdr = pNewVvdHdr; |
|
|
|
pActiveStudioHdr->pVertexBase = (void*)pVvdHdr; |
|
return pVvdHdr; |
|
} |
|
|
|
// ------------------------------------------------------------------------------------------------ |
|
// ------------------------------------------------------------------------------------------------ |
|
// ------------------------------------------------------------------------------------------------ |
|
struct ColorTexelValue |
|
{ |
|
Vector mLinearColor; // Linear color value for this texel |
|
bool mValidData; // Whether there is valid data in this texel. |
|
size_t mTriangleIndex; // Which triangle we used to generate the texel. |
|
}; |
|
|
|
// ------------------------------------------------------------------------------------------------ |
|
inline int ComputeLinearPos( int _x, int _y, int _resX, int _resY ) |
|
{ |
|
return Min( Max( 0, _y ), _resY - 1 ) * _resX |
|
+ Min( Max( 0, _x ), _resX - 1 ); |
|
} |
|
|
|
// ------------------------------------------------------------------------------------------------ |
|
inline float ComputeBarycentricDistanceToTri( Vector _barycentricCoord, Vector2D _v[3] ) |
|
{ |
|
Vector2D realPos = _barycentricCoord.x * _v[0] |
|
+ _barycentricCoord.y * _v[1] |
|
+ _barycentricCoord.z * _v[2]; |
|
|
|
int minIndex = 0; |
|
float minVal = _barycentricCoord[0]; |
|
for (int i = 1; i < 3; ++i) { |
|
if (_barycentricCoord[i] < minVal) { |
|
minVal = _barycentricCoord[i]; |
|
minIndex = i; |
|
} |
|
} |
|
|
|
Vector2D& first = _v[ (minIndex + 1) % 3]; |
|
Vector2D& second = _v[ (minIndex + 2) % 3]; |
|
|
|
return CalcDistanceToLineSegment2D( realPos, first, second ); |
|
} |
|
|
|
// ------------------------------------------------------------------------------------------------ |
|
static void GenerateLightmapSamplesForMesh( const matrix3x4_t& _matPos, const matrix3x4_t& _matNormal, int _iThread, int _skipProp, int _flags, int _lightmapResX, int _lightmapResY, studiohdr_t* _pStudioHdr, mstudiomodel_t* _pStudioModel, OptimizedModel::ModelHeader_t* _pVtxModel, int _meshID, CComputeStaticPropLightingResults *_outResults ) |
|
{ |
|
// Could iterate and gen this if needed. |
|
int nLod = 0; |
|
|
|
OptimizedModel::ModelLODHeader_t *pVtxLOD = _pVtxModel->pLOD(nLod); |
|
|
|
CUtlVector<colorTexel_t> &colorTexels = (*_outResults->m_ColorTexelsArrays.Tail()); |
|
const int cTotalPixelCount = _lightmapResX * _lightmapResY; |
|
colorTexels.EnsureCount(cTotalPixelCount); |
|
memset(colorTexels.Base(), 0, colorTexels.Count() * sizeof(colorTexel_t)); |
|
|
|
for (int i = 0; i < colorTexels.Count(); ++i) { |
|
colorTexels[i].m_fDistanceToTri = FLT_MAX; |
|
} |
|
|
|
mstudiomesh_t* pMesh = _pStudioModel->pMesh(_meshID); |
|
OptimizedModel::MeshHeader_t* pVtxMesh = pVtxLOD->pMesh(_meshID); |
|
const mstudio_meshvertexdata_t *vertData = pMesh->GetVertexData((void *)_pStudioHdr); |
|
Assert(vertData); // This can only return NULL on X360 for now |
|
|
|
for (int nGroup = 0; nGroup < pVtxMesh->numStripGroups; ++nGroup) |
|
{ |
|
OptimizedModel::StripGroupHeader_t* pStripGroup = pVtxMesh->pStripGroup(nGroup); |
|
|
|
int nStrip; |
|
for (nStrip = 0; nStrip < pStripGroup->numStrips; nStrip++) |
|
{ |
|
OptimizedModel::StripHeader_t *pStrip = pStripGroup->pStrip(nStrip); |
|
|
|
// If this hits, re-factor the code to iterate over triangles, and build the triangles |
|
// from the underlying structures. |
|
Assert((pStrip->flags & OptimizedModel::STRIP_IS_TRISTRIP) == 0); |
|
|
|
if (pStrip->flags & OptimizedModel::STRIP_IS_TRILIST) |
|
{ |
|
for (int i = 0; i < pStrip->numIndices; i += 3) |
|
{ |
|
int idx = pStrip->indexOffset + i; |
|
|
|
unsigned short i1 = *pStripGroup->pIndex(idx); |
|
unsigned short i2 = *pStripGroup->pIndex(idx + 1); |
|
unsigned short i3 = *pStripGroup->pIndex(idx + 2); |
|
|
|
int vertex1 = pStripGroup->pVertex(i1)->origMeshVertID; |
|
int vertex2 = pStripGroup->pVertex(i2)->origMeshVertID; |
|
int vertex3 = pStripGroup->pVertex(i3)->origMeshVertID; |
|
|
|
Vector modelPos[3] = { |
|
*vertData->Position(vertex1), |
|
*vertData->Position(vertex2), |
|
*vertData->Position(vertex3) |
|
}; |
|
|
|
Vector modelNormal[3] = { |
|
*vertData->Normal(vertex1), |
|
*vertData->Normal(vertex2), |
|
*vertData->Normal(vertex3) |
|
}; |
|
|
|
Vector worldPos[3]; |
|
Vector worldNormal[3]; |
|
|
|
VectorTransform(modelPos[0], _matPos, worldPos[0]); |
|
VectorTransform(modelPos[1], _matPos, worldPos[1]); |
|
VectorTransform(modelPos[2], _matPos, worldPos[2]); |
|
|
|
VectorTransform(modelNormal[0], _matNormal, worldNormal[0]); |
|
VectorTransform(modelNormal[1], _matNormal, worldNormal[1]); |
|
VectorTransform(modelNormal[2], _matNormal, worldNormal[2]); |
|
|
|
Vector2D texcoord[3] = { |
|
*vertData->Texcoord(vertex1), |
|
*vertData->Texcoord(vertex2), |
|
*vertData->Texcoord(vertex3) |
|
}; |
|
|
|
Rasterizer rasterizer(texcoord[0], texcoord[1], texcoord[2], |
|
_lightmapResX, _lightmapResY); |
|
|
|
for (auto it = rasterizer.begin(); it != rasterizer.end(); ++it) |
|
{ |
|
size_t linearPos = rasterizer.GetLinearPos(it); |
|
Assert(linearPos < cTotalPixelCount); |
|
|
|
if ( colorTexels[linearPos].m_bValid ) |
|
{ |
|
continue; |
|
} |
|
|
|
float ourDistancetoTri = ComputeBarycentricDistanceToTri( it->barycentric, texcoord ); |
|
|
|
bool doWrite = it->insideTriangle |
|
|| !colorTexels[linearPos].m_bPossiblyInteresting |
|
|| colorTexels[linearPos].m_fDistanceToTri > ourDistancetoTri; |
|
|
|
if (doWrite) |
|
{ |
|
Vector itWorldPos = worldPos[0] * it->barycentric.x |
|
+ worldPos[1] * it->barycentric.y |
|
+ worldPos[2] * it->barycentric.z; |
|
|
|
Vector itWorldNormal = worldNormal[0] * it->barycentric.x |
|
+ worldNormal[1] * it->barycentric.y |
|
+ worldNormal[2] * it->barycentric.z; |
|
itWorldNormal.NormalizeInPlace(); |
|
|
|
colorTexels[linearPos].m_WorldPosition = itWorldPos; |
|
colorTexels[linearPos].m_WorldNormal = itWorldNormal; |
|
colorTexels[linearPos].m_bValid = it->insideTriangle; |
|
colorTexels[linearPos].m_bPossiblyInteresting = true; |
|
colorTexels[linearPos].m_fDistanceToTri = ourDistancetoTri; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Process neighbors to the valid region. Walk through the existing array, look for samples that |
|
// are not valid but are adjacent to valid samples. Works if we are only bilinearly sampling |
|
// on the other side. |
|
// First attempt: Just pretend the triangle was larger and cast a ray from this new world pos |
|
// as above. |
|
int linearPos = 0; |
|
for ( int j = 0; j < _lightmapResY; ++j ) |
|
{ |
|
for (int i = 0; i < _lightmapResX; ++i ) |
|
{ |
|
bool shouldProcess = colorTexels[linearPos].m_bValid; |
|
// Are any of the eight neighbors valid?? |
|
if ( colorTexels[linearPos].m_bPossiblyInteresting ) |
|
{ |
|
// Look at our neighborhood (3x3 centerd on us). |
|
shouldProcess = shouldProcess |
|
|| colorTexels[ComputeLinearPos( i - 1, j - 1, _lightmapResX, _lightmapResY )].m_bValid // TL |
|
|| colorTexels[ComputeLinearPos( i , j - 1, _lightmapResX, _lightmapResY )].m_bValid // T |
|
|| colorTexels[ComputeLinearPos( i + 1, j - 1, _lightmapResX, _lightmapResY )].m_bValid // TR |
|
|
|
|| colorTexels[ComputeLinearPos( i - 1, j , _lightmapResX, _lightmapResY )].m_bValid // L |
|
|| colorTexels[ComputeLinearPos( i + 1, j , _lightmapResX, _lightmapResY )].m_bValid // R |
|
|
|
|| colorTexels[ComputeLinearPos( i - 1, j + 1, _lightmapResX, _lightmapResY )].m_bValid // BL |
|
|| colorTexels[ComputeLinearPos( i , j + 1, _lightmapResX, _lightmapResY )].m_bValid // B |
|
|| colorTexels[ComputeLinearPos( i + 1, j + 1, _lightmapResX, _lightmapResY )].m_bValid; // BR |
|
} |
|
|
|
if (shouldProcess) |
|
{ |
|
Vector directColor(0, 0, 0), |
|
indirectColor(0, 0, 0); |
|
|
|
|
|
ComputeDirectLightingAtPoint( colorTexels[linearPos].m_WorldPosition, colorTexels[linearPos].m_WorldNormal, directColor, _iThread, _skipProp, _flags); |
|
|
|
if (numbounce >= 1) { |
|
ComputeIndirectLightingAtPoint( colorTexels[linearPos].m_WorldPosition, colorTexels[linearPos].m_WorldNormal, indirectColor, _iThread, true, (_flags & GATHERLFLAGS_IGNORE_NORMALS) != 0 ); |
|
} |
|
|
|
VectorAdd(directColor, indirectColor, colorTexels[linearPos].m_Color); |
|
} |
|
|
|
++linearPos; |
|
} |
|
} |
|
} |
|
|
|
// ------------------------------------------------------------------------------------------------ |
|
static int GetTexelCount(unsigned int _resX, unsigned int _resY, bool _mipmaps) |
|
{ |
|
// Because they are unsigned, this is a != check--but if we were to change to ints, this would be |
|
// the right assert (and it's no worse than != now). |
|
Assert(_resX > 0 && _resY > 0); |
|
|
|
if (_mipmaps == false) |
|
return _resX * _resY; |
|
|
|
int retVal = 0; |
|
while (_resX > 1 || _resY > 1) |
|
{ |
|
retVal += _resX * _resY; |
|
_resX = max(1, _resX >> 1); |
|
_resY = max(1, _resY >> 1); |
|
} |
|
|
|
// Add in the 1x1 mipmap level, which wasn't hit above. This could be done in the initializer of |
|
// retVal, but it's more obvious here. |
|
retVal += 1; |
|
|
|
return retVal; |
|
} |
|
|
|
// ------------------------------------------------------------------------------------------------ |
|
static void FilterFineMipmap(unsigned int _resX, unsigned int _resY, const CUtlVector<colorTexel_t>& _srcTexels, CUtlVector<Vector>* _outLinear) |
|
{ |
|
Assert(_outLinear); |
|
// We can't filter in place, so go ahead and create a linear buffer here. |
|
CUtlVector<Vector> filterSrc; |
|
filterSrc.EnsureCount(_srcTexels.Count()); |
|
|
|
for (int i = 0; i < _srcTexels.Count(); ++i) |
|
{ |
|
ColorRGBExp32 rgbColor; |
|
VectorToColorRGBExp32(_srcTexels[i].m_Color, rgbColor); |
|
ConvertRGBExp32ToLinear( &rgbColor, &(filterSrc[i]) ); |
|
} |
|
|
|
const int cRadius = 1; |
|
const float cOneOverDiameter = 1.0f / pow(2.0f * cRadius + 1.0f, 2.0f) ; |
|
// Filter here. |
|
for (int j = 0; j < _resY; ++j) |
|
{ |
|
for (int i = 0; i < _resX; ++i) |
|
{ |
|
Vector value(0, 0, 0); |
|
int thisIndex = ComputeLinearPos(i, j, _resX, _resY); |
|
|
|
if (!_srcTexels[thisIndex].m_bValid) |
|
{ |
|
(*_outLinear)[thisIndex] = filterSrc[thisIndex]; |
|
continue; |
|
} |
|
|
|
// TODO: Check ASM for this, unroll by hand if needed. |
|
for ( int offsetJ = -cRadius; offsetJ <= cRadius; ++offsetJ ) |
|
{ |
|
for ( int offsetI = -cRadius; offsetI <= cRadius; ++offsetI ) |
|
{ |
|
int finalIndex = ComputeLinearPos( i + offsetI, j + offsetJ, _resX, _resY ); |
|
if ( !_srcTexels[finalIndex].m_bValid ) |
|
{ |
|
finalIndex = thisIndex; |
|
} |
|
|
|
value += filterSrc[finalIndex]; |
|
} |
|
} |
|
|
|
(*_outLinear)[thisIndex] = value * cOneOverDiameter; |
|
} |
|
} |
|
} |
|
|
|
// ------------------------------------------------------------------------------------------------ |
|
static void BuildFineMipmap(unsigned int _resX, unsigned int _resY, bool _applyFilter, const CUtlVector<colorTexel_t>& _srcTexels, CUtlVector<RGB888_t>* _outTexelsRGB888, CUtlVector<Vector>* _outLinear) |
|
{ |
|
// At least one of these needs to be non-null, otherwise what are we doing here? |
|
Assert(_outTexelsRGB888 || _outLinear); |
|
Assert(!_applyFilter || _outLinear); |
|
Assert(_srcTexels.Count() == GetTexelCount(_resX, _resY, false)); |
|
|
|
int texelCount = GetTexelCount(_resX, _resY, true); |
|
|
|
if (_outTexelsRGB888) |
|
(*_outTexelsRGB888).EnsureCount(texelCount); |
|
|
|
if (_outLinear) |
|
(*_outLinear).EnsureCount(GetTexelCount(_resX, _resY, false)); |
|
|
|
// This code can take awhile, so minimize the branchiness of the inner-loop. |
|
if (_applyFilter) |
|
{ |
|
|
|
FilterFineMipmap(_resX, _resY, _srcTexels, _outLinear); |
|
|
|
if ( _outTexelsRGB888 ) |
|
{ |
|
for (int i = 0; i < _srcTexels.Count(); ++i) |
|
{ |
|
RGBA8888_t encodedColor; |
|
|
|
Vector linearColor = (*_outLinear)[i]; |
|
|
|
ConvertLinearToRGBA8888( &linearColor, (unsigned char*)&encodedColor ); |
|
(*_outTexelsRGB888)[i].r = encodedColor.r; |
|
(*_outTexelsRGB888)[i].g = encodedColor.g; |
|
(*_outTexelsRGB888)[i].b = encodedColor.b; |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
for (int i = 0; i < _srcTexels.Count(); ++i) |
|
{ |
|
ColorRGBExp32 rgbColor; |
|
RGBA8888_t encodedColor; |
|
VectorToColorRGBExp32(_srcTexels[i].m_Color, rgbColor); |
|
ConvertRGBExp32ToRGBA8888(&rgbColor, (unsigned char*)&encodedColor, (_outLinear ? (&(*_outLinear)[i]) : NULL) ); |
|
// We drop alpha on the floor here, if this were to fire we'd need to consider using a different compressed format. |
|
Assert(encodedColor.a == 0xFF); |
|
|
|
if (_outTexelsRGB888) |
|
{ |
|
(*_outTexelsRGB888)[i].r = encodedColor.r; |
|
(*_outTexelsRGB888)[i].g = encodedColor.g; |
|
(*_outTexelsRGB888)[i].b = encodedColor.b; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// ------------------------------------------------------------------------------------------------ |
|
static void FilterCoarserMipmaps(unsigned int _resX, unsigned int _resY, CUtlVector<Vector>* _scratchLinear, CUtlVector<RGB888_t> *_outTexelsRGB888) |
|
{ |
|
Assert(_outTexelsRGB888); |
|
|
|
int srcResX = _resX; |
|
int srcResY = _resY; |
|
int dstResX = max(1, (srcResX >> 1)); |
|
int dstResY = max(1, (srcResY >> 1)); |
|
int dstOffset = GetTexelCount(srcResX, srcResY, false); |
|
|
|
// Build mipmaps here, after being converted to linear space. |
|
// TODO: Should do better filtering for downsampling. But this will work for now. |
|
while (srcResX > 1 || srcResY > 1) |
|
{ |
|
for (int j = 0; j < srcResY; j += 2) { |
|
for (int i = 0; i < srcResX; i += 2) { |
|
int srcCol0 = i; |
|
int srcCol1 = i + 1 > srcResX - 1 ? srcResX - 1 : i + 1; |
|
int srcRow0 = j; |
|
int srcRow1 = j + 1 > srcResY - 1 ? srcResY - 1 : j + 1;; |
|
|
|
int dstCol = i >> 1; |
|
int dstRow = j >> 1; |
|
|
|
|
|
const Vector& tl = (*_scratchLinear)[srcCol0 + (srcRow0 * srcResX)]; |
|
const Vector& tr = (*_scratchLinear)[srcCol1 + (srcRow0 * srcResX)]; |
|
const Vector& bl = (*_scratchLinear)[srcCol0 + (srcRow1 * srcResX)]; |
|
const Vector& br = (*_scratchLinear)[srcCol1 + (srcRow1 * srcResX)]; |
|
|
|
Vector sample = (tl + tr + bl + br) / 4.0f; |
|
|
|
ConvertLinearToRGBA8888(&sample, (unsigned char*)&(*_outTexelsRGB888)[dstOffset + dstCol + dstRow * dstResX]); |
|
|
|
// Also overwrite the srcBuffer to filter the next loop. This is safe because we won't be reading this source value |
|
// again during this mipmap level. |
|
(*_scratchLinear)[dstCol + dstRow * dstResX] = sample; |
|
} |
|
} |
|
|
|
srcResX = dstResX; |
|
srcResY = dstResY; |
|
dstResX = max(1, (srcResX >> 1)); |
|
dstResY = max(1, (srcResY >> 1)); |
|
dstOffset += GetTexelCount(srcResX, srcResY, false); |
|
} |
|
} |
|
|
|
// ------------------------------------------------------------------------------------------------ |
|
static void ConvertToDestinationFormat(unsigned int _resX, unsigned int _resY, ImageFormat _destFmt, const CUtlVector<RGB888_t>& _scratchRBG888, CUtlMemory<byte>* _outTexture) |
|
{ |
|
const ImageFormat cSrcImageFormat = IMAGE_FORMAT_RGB888; |
|
|
|
// Converts from the scratch RGB888 buffer, which should be fully filled out to the output texture. |
|
int destMemoryUsage = ImageLoader::GetMemRequired(_resX, _resY, 1, _destFmt, true); |
|
(*_outTexture).EnsureCapacity(destMemoryUsage); |
|
|
|
int srcResX = _resX; |
|
int srcResY = _resY; |
|
int srcOffset = 0; |
|
int dstOffset = 0; |
|
|
|
// The usual case--that they'll be different. |
|
if (cSrcImageFormat != _destFmt) |
|
{ |
|
while (srcResX > 1 || srcResY > 1) |
|
{ |
|
// Convert this mipmap level. |
|
ImageLoader::ConvertImageFormat((unsigned char*)(&_scratchRBG888[srcOffset]), cSrcImageFormat, (*_outTexture).Base() + dstOffset, _destFmt, srcResX, srcResY); |
|
|
|
// Then update offsets for the next mipmap level. |
|
srcOffset += GetTexelCount(srcResX, srcResY, false); |
|
dstOffset += ImageLoader::GetMemRequired(srcResX, srcResY, 1, _destFmt, false); |
|
|
|
srcResX = max(1, (srcResX >> 1)); |
|
srcResY = max(1, (srcResY >> 1)); |
|
} |
|
|
|
// Do the 1x1 level also. |
|
ImageLoader::ConvertImageFormat((unsigned char*)_scratchRBG888.Base() + srcOffset, cSrcImageFormat, (*_outTexture).Base() + dstOffset, _destFmt, srcResX, srcResY); |
|
} else { |
|
// But sometimes (particularly for debugging) they will be the same. |
|
Q_memcpy( (*_outTexture).Base(), _scratchRBG888.Base(), destMemoryUsage ); |
|
} |
|
} |
|
|
|
// ------------------------------------------------------------------------------------------------ |
|
static void ConvertTexelDataToTexture(unsigned int _resX, unsigned int _resY, ImageFormat _destFmt, const CUtlVector<colorTexel_t>& _srcTexels, CUtlMemory<byte>* _outTexture) |
|
{ |
|
Assert(_outTexture); |
|
Assert(_srcTexels.Count() == _resX * _resY); |
|
|
|
CUtlVector<RGB888_t> scratchRGB888; |
|
CUtlVector<Vector> scratchLinear; |
|
|
|
BuildFineMipmap(_resX, _resY, true, _srcTexels, &scratchRGB888, &scratchLinear); |
|
FilterCoarserMipmaps(_resX, _resY, &scratchLinear, &scratchRGB888 ); |
|
ConvertToDestinationFormat(_resX, _resY, _destFmt, scratchRGB888, _outTexture); |
|
} |
|
|
|
// ------------------------------------------------------------------------------------------------ |
|
static void DumpLightmapLinear( const char* _dstFilename, const CUtlVector<colorTexel_t>& _srcTexels, int _width, int _height ) |
|
{ |
|
CUtlVector< Vector > linearFloats; |
|
CUtlVector< BGR888_t > linearBuffer; |
|
BuildFineMipmap( _width, _height, true, _srcTexels, NULL, &linearFloats ); |
|
linearBuffer.SetCount( linearFloats.Count() ); |
|
|
|
for ( int i = 0; i < linearFloats.Count(); ++i ) { |
|
linearBuffer[i].b = RoundFloatToByte(linearFloats[i].z * 255.0f); |
|
linearBuffer[i].g = RoundFloatToByte(linearFloats[i].y * 255.0f); |
|
linearBuffer[i].r = RoundFloatToByte(linearFloats[i].x * 255.0f); |
|
} |
|
|
|
TGAWriter::WriteTGAFile( _dstFilename, _width, _height, IMAGE_FORMAT_BGR888, (uint8*)(linearBuffer.Base()), _width * ImageLoader::SizeInBytes(IMAGE_FORMAT_BGR888) ); |
|
}
|
|
|