port from perforce
This commit is contained in:
582
evoke-64k/trunk/ev10/meshviewer.cpp
Normal file
582
evoke-64k/trunk/ev10/meshviewer.cpp
Normal file
@@ -0,0 +1,582 @@
|
||||
#include "meshviewer.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <fstream>
|
||||
|
||||
#include "globals.h"
|
||||
extern float g_SunX;
|
||||
extern float g_SunY;
|
||||
extern float g_SunZ;
|
||||
|
||||
#include "StringHelper.h"
|
||||
#include "TextFileReader.h"
|
||||
|
||||
#include "Renderpipe.h"
|
||||
#include "camerahelper.h"
|
||||
#include "objmesh.h"
|
||||
#include "BinMeshData.h"
|
||||
#include "globals.h"
|
||||
|
||||
extern int c_iScreenSizeX;
|
||||
extern int c_iScreenSizeY;
|
||||
|
||||
extern ObjMesh g_objMesh;
|
||||
|
||||
std::string g_strMeshPath= "3dmodel\\";
|
||||
|
||||
const int g_iMeshHelperObjectFirst= 0;
|
||||
const int g_iMeshHelperObjectLast= 15;
|
||||
const int g_iMeshMainObject= 16;
|
||||
|
||||
HANDLE g_MeshNotification= NULL;
|
||||
|
||||
bool bInstantQuit= false;
|
||||
std::string strBaseMeshName= "script";
|
||||
std::string strObjName= "";
|
||||
bool bLoadFlatten= false;
|
||||
int iFloatBits= 16;
|
||||
|
||||
BinMesh g_BM;
|
||||
int g_IndexBytes;
|
||||
int g_VertexBytes;
|
||||
|
||||
DWORD g_dwMeshColor;
|
||||
|
||||
std::string g_strScriptCode;
|
||||
|
||||
void ReadMeshviewerCommandLine()
|
||||
{
|
||||
std::string strAct( GetCommandLine() );
|
||||
std::string strCommand;
|
||||
std::string strLastCommand;
|
||||
while( FrameWork::StringHelper::splitLoop( strAct, " ", strCommand ) )
|
||||
{
|
||||
FrameWork::StringHelper::toUpper( strCommand );
|
||||
if( strLastCommand == "/BASE" )
|
||||
{
|
||||
strBaseMeshName= strCommand;
|
||||
}
|
||||
else if( strCommand == "/QUIT" )
|
||||
{
|
||||
bInstantQuit= true;
|
||||
}
|
||||
strLastCommand= strCommand;
|
||||
}
|
||||
}
|
||||
|
||||
void PrepareMeshViewer()
|
||||
{
|
||||
g_FullScreenQuad.InitFullScreenQuad();
|
||||
Texture::Prepare1DTextures();
|
||||
|
||||
g_PSSM.m_iSplitCount = 6; //config
|
||||
g_PSSM.m_fRange = 240.0f;
|
||||
|
||||
InitMeshViewerScene();
|
||||
StartMeshFileWatch();
|
||||
}
|
||||
|
||||
void InitMeshViewerScene()
|
||||
{
|
||||
// Sky
|
||||
g_SkyBox.Create( 24, 36 );
|
||||
g_SkyBox.m_iUsedPreShader= SkyDepth;
|
||||
g_SkyBox.m_iUsedShader[ 0 ]= Sky;
|
||||
g_SkyBox.Lock();
|
||||
g_SkyBox.AddBox(
|
||||
D3DXVECTOR3( 0.0f, 0.0f, 0.0f),
|
||||
D3DXVECTOR3( 1.0f, 1.0f, 1.0f),
|
||||
D3DXVECTOR3( 0.0f, 0.0f, 0.0f ),
|
||||
0xff20b0f0);
|
||||
g_SkyBox.Unlock();
|
||||
|
||||
g_Objects[ g_iMeshHelperObjectFirst ].Create( 4 * 9, 6 * 9 );
|
||||
g_Objects[ g_iMeshHelperObjectFirst ].m_iUsedPreShader= PreDepth;
|
||||
g_Objects[ g_iMeshHelperObjectFirst ].m_iUsedShader[ 0 ]= Text;
|
||||
|
||||
g_Objects[ g_iMeshHelperObjectFirst ].Lock();
|
||||
g_Objects[ g_iMeshHelperObjectFirst ].AddPlane(
|
||||
D3DXVECTOR3( 0.0f, -1.0f, 0.0f ),
|
||||
D3DXVECTOR3( 0.0f, 0.0f, 0.0f ),
|
||||
0xFFFFDCB5, // 0x8040ff40
|
||||
D3DXVECTOR3( 100.0f, 1.0f, 100.0f ),
|
||||
D3DXVECTOR3( 400.0f, 1.0f, 400.0f ) );
|
||||
|
||||
g_Objects[ g_iMeshHelperObjectFirst ].Unlock();
|
||||
|
||||
LoadMeshViewerData();
|
||||
}
|
||||
|
||||
FrameWork::TextFileReader tfrMesh;
|
||||
|
||||
void LoadMeshViewerScript()
|
||||
{
|
||||
std::string strFile= g_strMeshPath;
|
||||
strFile+= strBaseMeshName;
|
||||
strFile+= ".txt";
|
||||
|
||||
tfrMesh.read( strFile.c_str() );
|
||||
}
|
||||
|
||||
void PreParseScript( const std::vector <std::string>& vecScript )
|
||||
{
|
||||
for( size_t i= 0; i < vecScript.size(); ++i )
|
||||
{
|
||||
std::string strAct= vecScript[ i ];
|
||||
for( size_t j= 0; j < strAct.size(); ++j )
|
||||
{
|
||||
if( strAct[ j ] == '\t' )
|
||||
{
|
||||
strAct[ j ]= ' ';
|
||||
}
|
||||
}
|
||||
std::string strCommand;
|
||||
if( FrameWork::StringHelper::splitAt( strAct, " ", strCommand ) )
|
||||
{
|
||||
FrameWork::StringHelper::trim(
|
||||
strCommand,
|
||||
FrameWork::StringHelper::getSpaceTab() );
|
||||
}
|
||||
else
|
||||
{
|
||||
strAct= "";
|
||||
}
|
||||
FrameWork::StringHelper::toUpper( strCommand );
|
||||
|
||||
if( strCommand == "FLATTEN" )
|
||||
{
|
||||
bLoadFlatten= true;
|
||||
}
|
||||
else if( strCommand == "FILE" )
|
||||
{
|
||||
strObjName= strAct;
|
||||
}
|
||||
else if( strCommand == "COLOR" )
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << strAct;
|
||||
float fR;
|
||||
float fG;
|
||||
float fB;
|
||||
float fA;
|
||||
ss >> fR;
|
||||
ss >> fG;
|
||||
ss >> fB;
|
||||
ss >> fA;
|
||||
fR= min( 255.0f, fR );
|
||||
fG= min( 255.0f, fG );
|
||||
fB= min( 255.0f, fB );
|
||||
fA= min( 255.0f, fA );
|
||||
|
||||
g_dwMeshColor= ( (int)fA << 24 ) |
|
||||
( (int)fR << 16 ) |
|
||||
( (int)fG << 8 ) |
|
||||
( (int)fB );
|
||||
}
|
||||
else if( strCommand == "FLOATBITS" )
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << strAct;
|
||||
iFloatBits= 16;
|
||||
ss >> iFloatBits;
|
||||
if( iFloatBits < 10 )
|
||||
{
|
||||
iFloatBits= 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ParseScript( const std::vector <std::string>& vecScript )
|
||||
{
|
||||
g_strScriptCode= "";
|
||||
|
||||
for( size_t i= 0; i < vecScript.size(); ++i )
|
||||
{
|
||||
std::string strAct= vecScript[ i ];
|
||||
for( size_t j= 0; j < strAct.size(); ++j )
|
||||
{
|
||||
if( strAct[ j ] == '\t' )
|
||||
{
|
||||
strAct[ j ]= ' ';
|
||||
}
|
||||
}
|
||||
std::string strCommand;
|
||||
if( FrameWork::StringHelper::splitAt( strAct, " ", strCommand ) )
|
||||
{
|
||||
FrameWork::StringHelper::trim(
|
||||
strCommand,
|
||||
FrameWork::StringHelper::getSpaceTab() );
|
||||
}
|
||||
else
|
||||
{
|
||||
strAct= "";
|
||||
}
|
||||
FrameWork::StringHelper::toUpper( strCommand );
|
||||
|
||||
if( strCommand == "SUBDIVIDE" )
|
||||
{
|
||||
g_objMesh.CatmullClarkSubdivide();
|
||||
g_strScriptCode+= "\tg_objMesh.CatmullClarkSubdivide();\n";
|
||||
}
|
||||
else if( strCommand == "UNINDEX" )
|
||||
{
|
||||
g_objMesh.UnIndex();
|
||||
g_strScriptCode+= "\tg_objMesh.UnIndex();\n";
|
||||
}
|
||||
else if( strCommand == "EXTRUDE" )
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << strAct;
|
||||
float fParam= 1.0f;
|
||||
ss >> fParam;
|
||||
fParam= ObjMesh::RoundFloat(fParam, iFloatBits);
|
||||
|
||||
g_objMesh.Extrude( fParam );
|
||||
g_strScriptCode+= "\tg_objMesh.Extrude( ";
|
||||
g_strScriptCode+= FloatToCodeString(fParam );
|
||||
g_strScriptCode+= " );\n";
|
||||
}
|
||||
else if( strCommand == "SUPER" )
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << strAct;
|
||||
float fParam= 1.0f;
|
||||
ss >> fParam;
|
||||
fParam= ObjMesh::RoundFloat(fParam, iFloatBits);
|
||||
|
||||
g_objMesh.SuperEllip( fParam );
|
||||
g_strScriptCode+= "\tg_objMesh.SuperEllip( ";
|
||||
g_strScriptCode+= FloatToCodeString(fParam );
|
||||
g_strScriptCode+= " );\n";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LoadMeshViewerData()
|
||||
{
|
||||
ObjMesh::m_fMaxRoundError= 0.0f;
|
||||
|
||||
LoadMeshViewerScript();
|
||||
PreParseScript( tfrMesh.getFileLines() );
|
||||
|
||||
std::string strObjFile= g_strMeshPath;
|
||||
strObjFile+= strObjName;
|
||||
|
||||
|
||||
g_objMesh.LoadMesh(
|
||||
strObjFile.c_str(),
|
||||
iFloatBits,
|
||||
bLoadFlatten,
|
||||
g_BM,
|
||||
g_IndexBytes,
|
||||
g_VertexBytes );
|
||||
ParseScript( tfrMesh.getFileLines() );
|
||||
g_objMesh.GenerateNormals();
|
||||
g_Objects[ g_iMeshMainObject ].CreateObjMesh( &g_objMesh, g_dwMeshColor );
|
||||
D3DXMATRIX mat;
|
||||
D3DXMatrixTranslation( &mat, 0.0f, 10.0f, 0.0f );
|
||||
g_Objects[ g_iMeshMainObject ].SetTransformation( mat );
|
||||
g_Objects[ g_iMeshMainObject ].m_iUsedPreShader= PreDepth;
|
||||
g_Objects[ g_iMeshMainObject ].m_iUsedShader[ 0 ]= Text;
|
||||
|
||||
}
|
||||
|
||||
void MeshViewerMainLoop()
|
||||
{
|
||||
g_dwTimeReplaceStart= timeGetTime();
|
||||
|
||||
/***********************************************************************************/
|
||||
//Mainloop
|
||||
//int LastCamTick= 0;
|
||||
|
||||
do
|
||||
{
|
||||
if( bInstantQuit )
|
||||
{
|
||||
break;
|
||||
}
|
||||
g_dwSamplesPassed= (timeGetTime() - g_dwTimeReplaceStart ) * 441 / 10;
|
||||
DWORD dwNewSamples= g_dwSamplesPassed - g_dwSamples;
|
||||
g_dwSamples= g_dwSamplesPassed;
|
||||
|
||||
EditorHelp::CheckKeys();
|
||||
float fCamTime= (float)dwNewSamples / (float)g_iTempo * (float)g_iCamTickFactor;
|
||||
UserCam( (int)(fCamTime * 1.0f ) );
|
||||
GetEditCam( g_Camera );
|
||||
|
||||
/***********************************************************************************/
|
||||
// Sonne bewegen
|
||||
{
|
||||
D3DXMATRIX mSunRotX, mSunRotY, mSunRotZ, mSun;
|
||||
g_SunX= 1.0f;
|
||||
g_SunY= 2.75f - g_Camera.m_vec3Rot.x;
|
||||
g_SunZ= 0.0f;
|
||||
D3DXMatrixRotationX( &mSunRotX, g_SunX );
|
||||
D3DXMatrixRotationY( &mSunRotY, g_SunY );
|
||||
D3DXMatrixRotationZ( &mSunRotZ, g_SunZ );
|
||||
mSun = mSunRotX * mSunRotY * mSunRotZ;
|
||||
g_LightDir.x = -mSun._21; g_LightDir.y = -mSun._22; g_LightDir.z = -mSun._23;
|
||||
D3DXVec3Normalize(&g_LightDir, &g_LightDir);
|
||||
}
|
||||
/***********************************************************************************/
|
||||
//begin paint loop
|
||||
g_d3d_device->BeginScene();
|
||||
|
||||
/***********************************************************************************/
|
||||
// normaler Renderdurchlauf
|
||||
|
||||
// Projektion
|
||||
D3DXMATRIX& mat= g_matProjection;
|
||||
|
||||
float zf= 1200.0f;
|
||||
float zn= 0.125f;
|
||||
float xScale = g_Camera.m_fFOV * g_Camera.m_fFOV;
|
||||
float yScale= xScale / (float)c_iScreenSizeY * (float)c_iScreenSizeX;
|
||||
|
||||
mat._11= xScale; mat._12= 0; mat._13= 0; mat._14= 0;
|
||||
mat._21= 0; mat._22= yScale; mat._23= 0; mat._24= 0;
|
||||
mat._31= 0; mat._32= 0; mat._33= zf/(zf-zn); mat._34= 1;
|
||||
mat._41= 0; mat._42= 0; mat._43= -zn*zf/(zf-zn);mat._44= 0;
|
||||
|
||||
FillCameraMatrix( g_Camera.m_vec3Pos, g_Camera.m_vec3Rot, &g_matView );
|
||||
|
||||
// Shadow splits aktualisieren
|
||||
g_PSSM.UpdateSplits(g_LightDir, g_matView, g_matProjection);
|
||||
|
||||
g_d3d_device->SetTransform( D3DTS_PROJECTION, &g_matProjection );
|
||||
g_d3d_device->SetTransform( D3DTS_VIEW, &g_matView );
|
||||
|
||||
Shader::SetConstants();
|
||||
Shader::SetCamera(g_matView, g_matProjection,
|
||||
g_Camera.m_vec3Pos, g_CamFront);
|
||||
|
||||
#ifndef DISABLEPOSTPROCESSING
|
||||
Renderpipe::FullRenderPass();
|
||||
#else
|
||||
Renderpipe::SimpleRenderPass();
|
||||
#endif
|
||||
/***********************************************************************************/
|
||||
// Renderdurchlauf abschliessen
|
||||
|
||||
//Shader aus
|
||||
Shader::Deactivate();
|
||||
|
||||
// Kinobalken
|
||||
//RenderBars();
|
||||
|
||||
DrawMeshText();
|
||||
|
||||
g_d3d_device->EndScene();
|
||||
g_d3d_device->Present( NULL, NULL, NULL, NULL );
|
||||
Sleep( 5 );
|
||||
|
||||
CheckMeshFileWatch();
|
||||
|
||||
// Allow the window to get focus when clicking in it or at the taskbar icon.
|
||||
MSG msg;
|
||||
if (PeekMessage(&msg, d3dpp.hDeviceWindow, 0, 0, PM_REMOVE))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
bool bEsc= GetAsyncKeyState( VK_ESCAPE ) != 0;
|
||||
bool bCtrl= GetAsyncKeyState( VK_CONTROL ) != 0 || GetAsyncKeyState( VK_SHIFT) != 0;
|
||||
if( bEsc && bCtrl )
|
||||
{
|
||||
break;
|
||||
}
|
||||
} while ( true );
|
||||
|
||||
WriteMeshHeader();
|
||||
}
|
||||
|
||||
void StopMeshFileWatch()
|
||||
{
|
||||
if (g_MeshNotification != INVALID_HANDLE_VALUE && g_MeshNotification != NULL)
|
||||
{
|
||||
FindCloseChangeNotification(g_MeshNotification);
|
||||
}
|
||||
g_MeshNotification= NULL;
|
||||
}
|
||||
|
||||
void StartMeshFileWatch()
|
||||
{
|
||||
StopMeshFileWatch();
|
||||
|
||||
g_MeshNotification= FindFirstChangeNotification(
|
||||
g_strMeshPath.c_str(),
|
||||
FALSE,
|
||||
FILE_NOTIFY_CHANGE_LAST_WRITE );
|
||||
}
|
||||
|
||||
bool CheckMeshFileWatch()
|
||||
{
|
||||
if (g_MeshNotification == INVALID_HANDLE_VALUE || g_MeshNotification == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD dwRet= WaitForSingleObject( g_MeshNotification, 1 );
|
||||
|
||||
if( WAIT_OBJECT_0 == dwRet )
|
||||
{
|
||||
FindNextChangeNotification( g_MeshNotification );
|
||||
OnMeshFileChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnMeshFileChanged()
|
||||
{
|
||||
LoadMeshViewerData();
|
||||
}
|
||||
|
||||
void DrawMeshText()
|
||||
{
|
||||
bool bHelp= EditorHelp::m_KeyDown[ VK_F1 ];
|
||||
if( bHelp )
|
||||
{
|
||||
int l= 0;
|
||||
EditorHelp::PrintDebug( l++,0, "F1 - Hilfeanzeige aktivieren" );
|
||||
EditorHelp::PrintDebug( l++,0, "WASD - Kamera bewegen Mouse + mittlere oder rechte Taste - Kamera rotieren" );
|
||||
EditorHelp::PrintDebug( l++,0, "FV - Kamera auf/ abbewegen Home - Kamera zuruecksetzen" );
|
||||
EditorHelp::PrintDebug( l++,0, "QE - Kamera um Sichtachse rotieren Z - Reset" );
|
||||
EditorHelp::PrintDebug( l++,0, "YH - Fov aendern TG - DOF Fokusebene" );
|
||||
return;
|
||||
}
|
||||
|
||||
if( EditorHelp::m_KeyPressed[ VK_F2 ] )
|
||||
{
|
||||
EditorHelp::m_bShowDebugData= !EditorHelp::m_bShowDebugData;
|
||||
}
|
||||
|
||||
char pcBuffer[ 1024 ];
|
||||
pcBuffer[ 0 ]= 0;
|
||||
|
||||
sprintf_s( pcBuffer,
|
||||
"compressed - VertexBytes: %d IndexBytes: %d total: %d",
|
||||
g_VertexBytes, g_IndexBytes, g_VertexBytes + g_IndexBytes );
|
||||
EditorHelp::PrintDebug( 0,0, pcBuffer );
|
||||
sprintf_s( pcBuffer,
|
||||
"final mesh - Vertex: %d Faces: %d",
|
||||
g_objMesh.GetVertexCount(), g_objMesh.GetFaceCount() );
|
||||
EditorHelp::PrintDebug( 1,0, pcBuffer );
|
||||
sprintf_s( pcBuffer,
|
||||
"float Bits: %d max rounding error: %7.7f",
|
||||
iFloatBits,
|
||||
ObjMesh::m_fMaxRoundError );
|
||||
EditorHelp::PrintDebug( 2,0, pcBuffer );
|
||||
sprintf_s( pcBuffer,
|
||||
"%30s",
|
||||
strObjName.c_str() );
|
||||
EditorHelp::PrintDebug( 0,85, pcBuffer );
|
||||
}
|
||||
|
||||
void WriteMeshHeader()
|
||||
{
|
||||
std::string strBaseNameCamel= strBaseMeshName;
|
||||
FrameWork::StringHelper::toLower( strBaseNameCamel );
|
||||
strBaseNameCamel[ 0 ]= toupper( strBaseNameCamel[ 0 ] );
|
||||
|
||||
std::string strFileName= "meshdata\\";
|
||||
strFileName+= strBaseNameCamel;
|
||||
strFileName+= ".h";
|
||||
|
||||
std::ofstream ofs( strFileName.c_str() );
|
||||
|
||||
std::string strBytes;
|
||||
|
||||
ofs << "#pragma once\n";
|
||||
ofs << "#include \"objmesh.h\"\n";
|
||||
ofs << "#include \"BinMeshData.h\"\n";
|
||||
ofs << "#include \"renderjob.h\"\n";
|
||||
ofs << "\n";
|
||||
ofs << "unsigned char g_Vert" << strBaseNameCamel << "[]=\n";
|
||||
strBytes= "";
|
||||
CreateByteString( strBytes, (unsigned char*)g_BM.m_pVertex, g_VertexBytes );
|
||||
ofs << strBytes;
|
||||
ofs << "\n";
|
||||
ofs << "unsigned char g_Topology" << strBaseNameCamel << "[]=\n";
|
||||
strBytes= "";
|
||||
CreateByteString( strBytes, (unsigned char*)g_BM.m_pTopology, g_IndexBytes );
|
||||
ofs << strBytes;
|
||||
ofs << "\n";
|
||||
ofs << "void Create" << strBaseNameCamel << "( Renderjob& r )\n";
|
||||
ofs << "{\n";
|
||||
ofs << "\tBinMesh b;\n";
|
||||
ofs << "\tb.Set( "<< g_BM.m_PrimitiveCount << ", g_Vert" << strBaseNameCamel << ", g_Topology" << strBaseNameCamel << " );\n";
|
||||
ofs << "\tg_objMesh.LoadMesh(&b);\n";
|
||||
ofs << g_strScriptCode;
|
||||
ofs << "\tg_objMesh.GenerateNormals();\n";
|
||||
ofs << "\tr.CreateObjMesh( &g_objMesh, " << g_dwMeshColor << " );\n";
|
||||
ofs << "}\n";
|
||||
|
||||
}
|
||||
|
||||
std::string FloatToCodeString( float f )
|
||||
{
|
||||
char pcSpecial[ 1024 ];
|
||||
sprintf_s( pcSpecial,
|
||||
1024,
|
||||
"%8.7ff",
|
||||
f );
|
||||
|
||||
return std::string( pcSpecial );
|
||||
}
|
||||
std::string IntToCodeString( int d )
|
||||
{
|
||||
char pcSpecial[ 1024 ];
|
||||
sprintf_s( pcSpecial,
|
||||
1024,
|
||||
"%d",
|
||||
d );
|
||||
|
||||
return std::string( pcSpecial );
|
||||
}
|
||||
|
||||
void CreateByteString( std::string& strOut, unsigned char* pData, int Count )
|
||||
{
|
||||
strOut+= "{\n";
|
||||
if( Count == 0 )
|
||||
{
|
||||
strOut+= "\t0\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
int iEnterCount= 0;
|
||||
for( int i= 0; i < Count; ++i )
|
||||
{
|
||||
if( iEnterCount == 0 )
|
||||
{
|
||||
strOut+= "\t";
|
||||
iEnterCount= 0;
|
||||
}
|
||||
strOut+= IntToCodeString( pData[ i ] );
|
||||
strOut+= ", ";
|
||||
|
||||
iEnterCount++;
|
||||
if( iEnterCount >= 20 && i + 1 < Count)
|
||||
{
|
||||
strOut+= "\n";
|
||||
iEnterCount= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
strOut+= "\n};\n";
|
||||
}
|
||||
|
||||
bool IsInstantQuit()
|
||||
{
|
||||
return bInstantQuit;
|
||||
}
|
||||
|
||||
void DoInstantQuit()
|
||||
{
|
||||
LoadMeshViewerData();
|
||||
WriteMeshHeader();
|
||||
}
|
||||
Reference in New Issue
Block a user