- Reimplements autosave logic to handle levels, subscenes and terrains in a more consistent, reliable way.

- Adds entry to RMB menu in Asset Browser to restore an asset to a backup copy taken from autosaves
- Adds reparent out-of-bounds objects button to SceneGroup inspector
- Adds ability to have SubScene have a different loading bounds from the actual subscene bounds, allowing load triggering to happen ahead of the bounds of the subscene itself
- Fixes asset importer handling of animFPS field to be the correct type
- Adds onInspect handling to GameBase allowing better handling for any game class type with editor integration
- Add getAssetLooseFileCount and getAssetLooseFile to AssetManager to be able to iterate over all loose files associated to an asset
- Add standard/default preload function def to forestItem
- Fixes handling of text placement on GuiIconButtonCtrl when text is set to the right
- Adds setGlobalCenter utility function
- Adds ability to set guiInputCtrl active state
- Matched util functions for tracking if left and right mouse buttons are down to EditTSCtrl alongside the existing middle mouse
- Add empty element sanity check to appMesh loader
- Add callback for GameBase when game is created
- Add default graphics options config for steamdeck
- Fix typo in assetImportConfig default
- Filters SceneGroup utility buttons in inspector to only show for relevent class types
This commit is contained in:
JeffR 2025-05-25 07:40:10 -05:00
parent 70502d1b0f
commit bb7ee38bf4
33 changed files with 978 additions and 237 deletions

View file

@ -291,7 +291,7 @@ StringTableEntry Scene::getLevelAsset()
return query->mAssetList[0];
}
bool Scene::saveScene(StringTableEntry fileName)
bool Scene::saveScene(StringTableEntry fileName, const bool& saveSubScenes)
{
if (!isServerObject())
return false;
@ -316,9 +316,12 @@ bool Scene::saveScene(StringTableEntry fileName)
//Inform our subscenes we're saving so they can do any
//special work required as well
for (U32 i = 0; i < mSubScenes.size(); i++)
if (saveSubScenes)
{
mSubScenes[i]->save();
for (U32 i = 0; i < mSubScenes.size(); i++)
{
mSubScenes[i]->save();
}
}
bool saveSuccess = save(fileName);
@ -381,9 +384,30 @@ void Scene::getUtilizedAssetsFromSceneObject(SimObject* object, Vector<StringTab
}
//
Vector<SceneObject*> Scene::getObjectsByClass(String className)
void Scene::getObjectsByClass(SimObject* object, StringTableEntry className, Vector<SimObject*>* objectsList, bool checkSubscenes)
{
return Vector<SceneObject*>();
if(object->getClassName() == className)
{
objectsList->push_back(object);
}
//If it's a subscene and we DON'T want to scan through them, bail out now
SubScene* subScene = dynamic_cast<SubScene*>(object);
if (subScene && !checkSubscenes)
return;
//If possible, now we iterate over the children
SimGroup* group = dynamic_cast<SimGroup*>(object);
if (group)
{
for (U32 c = 0; c < group->size(); c++)
{
SimObject* childObj = dynamic_cast<SimObject*>(group->getObject(c));
//Recurse down
getObjectsByClass(childObj, className, objectsList);
}
}
}
void Scene::loadAtPosition(const Point3F& position)
@ -460,15 +484,37 @@ DefineEngineMethod(Scene, removeDynamicObject, void, (SceneObject* sceneObj), (n
object->removeDynamicObject(sceneObj);
}
DefineEngineMethod(Scene, getObjectsByClass, String, (String className), (""),
DefineEngineMethod(Scene, getObjectsByClass, String, (String className, bool checkSubScenes), ("", false),
"Get the root Scene object that is loaded.\n"
"@return The id of the Root Scene. Will be 0 if no root scene is loaded")
"@param className The name of the class of objects to get a list of.\n"
"@param checkSubScenes If true, will also scan through currently loaded subscenes to get matching objects.\n"
"@return A space-separated list of object ids that match the searched-for className")
{
if (className == String::EmptyString)
return "";
//return object->getObjectsByClass(className);
return "";
Vector<SimObject*>* objectsList = new Vector<SimObject*>();
object->getObjectsByClass(object, StringTable->insert(className.c_str()), objectsList, checkSubScenes);
char* retBuffer = Con::getReturnBuffer(1024);
U32 len = 0;
S32 i;
//Get the length of our return string
for(U32 i=0; i < objectsList->size(); i++)
len += dStrlen((*objectsList)[i]->getIdString());
char* ret = Con::getReturnBuffer(len + 1);
ret[0] = 0;
for (U32 i = 0; i < objectsList->size(); i++)
{
dStrcat(ret, (*objectsList)[i]->getIdString(), len + 1);
dStrcat(ret, " ", len + 1);
}
return ret;
}
DefineEngineMethod(Scene, dumpUtilizedAssets, void, (), ,
@ -492,12 +538,12 @@ DefineEngineMethod(Scene, getLevelAsset, const char*, (), ,
return object->getLevelAsset();
}
DefineEngineMethod(Scene, save, bool, (const char* fileName), (""),
DefineEngineMethod(Scene, save, bool, (const char* fileName, bool saveSubScenes), ("", true),
"Save out the object to the given file.\n"
"@param fileName The name of the file to save to."
"@param True on success, false on failure.")
{
return object->saveScene(StringTable->insert(fileName));
return object->saveScene(StringTable->insert(fileName), saveSubScenes);
}
DefineEngineMethod(Scene, loadAtPosition, void, (Point3F position), (Point3F::Zero),

View file

@ -78,7 +78,7 @@ public:
StringTableEntry getOriginatingFile();
StringTableEntry getLevelAsset();
bool saveScene(StringTableEntry fileName);
bool saveScene(StringTableEntry fileName, const bool& saveSubScenes = true);
//
//Networking
@ -86,7 +86,7 @@ public:
void unpackUpdate(NetConnection *conn, BitStream *stream) override;
//
Vector<SceneObject*> getObjectsByClass(String className);
void getObjectsByClass(SimObject* object, StringTableEntry className, Vector<SimObject*>* objectsList, bool checkSubscenes = false);
void getUtilizedAssetsFromSceneObject(SimObject* object, Vector<StringTableEntry>* usedAssetsList);

View file

@ -9,6 +9,7 @@
#include "physics/physicsShape.h"
#include "renderInstance/renderPassManager.h"
#include "scene/sceneRenderState.h"
#include "Scene.h"
IMPLEMENT_CO_NETOBJECT_V1(SceneGroup);
@ -156,6 +157,37 @@ void SceneGroup::onInspect(GuiInspector* inspector)
regenButton->setConsoleCommand(rgBuffer);
regenFieldGui->addObject(regenButton);
//
//Regen bounds button
GuiInspectorField* reparentFieldGui = sceneGroupGrp->createInspectorField();
reparentFieldGui->init(inspector, sceneGroupGrp);
reparentFieldGui->setSpecialEditField(true);
reparentFieldGui->setTargetObject(this);
fldnm = StringTable->insert("ReparentOOBObjs");
reparentFieldGui->setSpecialEditVariableName(fldnm);
reparentFieldGui->setInspectorField(NULL, fldnm);
reparentFieldGui->setDocs("");
stack->addObject(reparentFieldGui);
GuiButtonCtrl* reparentButton = new GuiButtonCtrl();
reparentButton->registerObject();
reparentButton->setDataField(StringTable->insert("profile"), NULL, "ToolsGuiButtonProfile");
reparentButton->setText("Reparent Out-of-bounds Objs");
reparentButton->resize(Point2I::Zero, regenFieldGui->getExtent());
reparentButton->setHorizSizing(GuiControl::horizResizeWidth);
reparentButton->setVertSizing(GuiControl::vertResizeHeight);
char rprntBuffer[512];
dSprintf(rprntBuffer, 512, "%d.reparentOOBObjects();", this->getId());
reparentButton->setConsoleCommand(rprntBuffer);
reparentFieldGui->addObject(reparentButton);
#endif
}
@ -279,6 +311,27 @@ void SceneGroup::recalculateBoundingBox()
setMaskBits(TransformMask);
}
void SceneGroup::reparentOOBObjects()
{
if (empty())
return;
// Extend the bounding box to include each child's bounding box
for (SimSetIterator itr(this); *itr; ++itr)
{
SceneObject* child = dynamic_cast<SceneObject*>(*itr);
if (child)
{
const Box3F& childBox = child->getWorldBox();
if(!mWorldBox.isOverlapped(childBox))
{
Scene::getRootScene()->addObject(child);
}
}
}
}
U32 SceneGroup::packUpdate(NetConnection* conn, U32 mask, BitStream* stream)
{
U32 retMask = Parent::packUpdate(conn, mask, stream);
@ -363,3 +416,9 @@ DefineEngineMethod(SceneGroup, recalculateBounds, void, (), ,
{
object->recalculateBoundingBox();
}
DefineEngineMethod(SceneGroup, reparentOOBObjects, void, (), ,
"Finds objects that are children of the SceneGroup and, if not overlapping or in the bounds, reparents them to the root scene.\n")
{
object->reparentOOBObjects();
}

View file

@ -46,6 +46,7 @@ public:
void addObject(SimObject* object) override;
void removeObject(SimObject* object) override;
void recalculateBoundingBox();
void reparentOOBObjects();
///
bool buildPolyList(PolyListContext context, AbstractPolyList* polyList, const Box3F& box, const SphereF& sphere) override;

View file

@ -10,6 +10,8 @@
#include "gfx/gfxDrawUtil.h"
#include "gfx/gfxTransformSaver.h"
#include "gui/editor/inspector/group.h"
#include "gui/worldEditor/editor.h"
#include "math/mathIO.h"
#include "T3D/gameBase/gameBase.h"
bool SubScene::smTransformChildren = false;
@ -32,7 +34,9 @@ SubScene::SubScene() :
mTickPeriodMS(1000),
mCurrTick(0),
mGlobalLayer(false),
mSaving(false)
mSaving(false),
mUseSeparateLoadBounds(false),
mLoadBounds(Point3F::One)
{
mNetFlags.set(Ghostable | ScopeAlways);
@ -70,6 +74,8 @@ void SubScene::initPersistFields()
INITPERSISTFIELD_SUBSCENEASSET(SubScene, SubScene, "The subscene asset to load.");
addField("tickPeriodMS", TypeS32, Offset(mTickPeriodMS, SubScene), "evaluation rate (ms)");
addField("gameModes", TypeGameModeList, Offset(mGameModesNames, SubScene), "The game modes that this subscene is associated with.");
addField("UseSeparateLoadBounds", TypeBool, Offset(mUseSeparateLoadBounds, SubScene), "If true, this subscene will utilize a separate bounds for triggering loading/unloading than it's object bounds");
addField("LoadBounds", TypePoint3F, Offset(mLoadBounds, SubScene), "If UseSeparateLoadBounds is true, this subscene will use this value to set up the load/unload bounds");
endGroup("SubScene");
addGroup("LoadingManagement");
@ -113,6 +119,11 @@ U32 SubScene::packUpdate(NetConnection* conn, U32 mask, BitStream* stream)
U32 retMask = Parent::packUpdate(conn, mask, stream);
stream->writeFlag(mGlobalLayer);
if(stream->writeFlag(mUseSeparateLoadBounds))
{
mathWrite(*stream, mLoadBounds);
}
return retMask;
}
@ -123,6 +134,11 @@ void SubScene::unpackUpdate(NetConnection* conn, BitStream* stream)
mGlobalLayer = stream->readFlag();
mUseSeparateLoadBounds = stream->readFlag();
if(mUseSeparateLoadBounds)
{
mathRead(*stream, &mLoadBounds);
}
}
void SubScene::onInspect(GuiInspector* inspector)
@ -220,7 +236,21 @@ bool SubScene::testBox(const Box3F& testBox)
bool passes = mGlobalLayer;
if (!passes)
passes = getWorldBox().isOverlapped(testBox);
{
if(mUseSeparateLoadBounds)
{
Box3F loadBox = Box3F(-mLoadBounds.x, -mLoadBounds.y, -mLoadBounds.z,
mLoadBounds.x, mLoadBounds.y, mLoadBounds.z);
loadBox.setCenter(getPosition());
passes = loadBox.isOverlapped(testBox);
}
else
{
passes = getWorldBox().isOverlapped(testBox);
}
}
if (passes)
passes = evaluateCondition();
@ -268,6 +298,9 @@ void SubScene::processTick(const Move* move)
void SubScene::_onFileChanged(const Torque::Path& path)
{
if (gEditingMission)
return;
if(mSubSceneAsset.isNull() || Torque::Path(mSubSceneAsset->getLevelPath()) != path)
return;
@ -426,7 +459,7 @@ void SubScene::unload()
}
bool SubScene::save()
bool SubScene::save(const String& filename)
{
if (!isServerObject())
return false;
@ -451,6 +484,9 @@ bool SubScene::save()
StringTableEntry levelPath = mSubSceneAsset->getLevelPath();
if (filename.isNotEmpty())
levelPath = StringTable->insert(filename.c_str());
FileStream fs;
fs.open(levelPath, Torque::FS::File::Write);
fs.close();
@ -547,8 +583,26 @@ void SubScene::renderObject(ObjectRenderInst* ri,
//Box3F scale = getScale()
//Box3F bounds = Box3F(-m)
if(mUseSeparateLoadBounds && !mGlobalLayer)
{
Box3F loadBounds = Box3F(-mLoadBounds.x, -mLoadBounds.y, -mLoadBounds.z,
mLoadBounds.x, mLoadBounds.y, mLoadBounds.z);
//bounds.setCenter(getPosition());
ColorI loadBoundsColor = ColorI(200, 200, 100, 50);
drawer->drawCube(desc, loadBounds, loadBoundsColor);
// Render wireframe.
desc.setFillModeWireframe();
drawer->drawCube(desc, loadBounds, ColorI::BLACK);
desc.setFillModeSolid();
}
Point3F scale = getScale();
Box3F bounds = Box3F(-scale/2, scale/2);
Box3F bounds = Box3F(-scale / 2, scale / 2);
ColorI boundsColor = ColorI(135, 206, 235, 50);
@ -565,10 +619,11 @@ void SubScene::renderObject(ObjectRenderInst* ri,
drawer->drawCube(desc, bounds, ColorI::BLACK);
}
DefineEngineMethod(SubScene, save, bool, (),,
"Save out the subScene.\n")
DefineEngineMethod(SubScene, save, bool, (const char* filename), (""),
"Save out the subScene.\n"
"@param filename (optional) If empty, the subScene will save to it's regular asset path. If defined, it will save out to the filename provided")
{
return object->save();
return object->save(filename);
}

View file

@ -52,6 +52,9 @@ private:
bool mGlobalLayer;
bool mUseSeparateLoadBounds;
Point3F mLoadBounds;
public:
SubScene();
virtual ~SubScene();
@ -118,7 +121,7 @@ public:
return mStartUnloadTimerMS;
}
bool save();
bool save(const String& filename = String());
DECLARE_CALLBACK(void, onLoaded, ());
DECLARE_CALLBACK(void, onUnloaded, ());

View file

@ -78,7 +78,7 @@ AssetImportConfig::AssetImportConfig() :
SeparateAnimations(false),
SeparateAnimationPrefix(""),
animTiming("FrameCount"),
animFPS(false),
animFPS(30),
AlwaysAddShapeAnimationSuffix(true),
AddedShapeAnimationSuffix("_anim"),
GenerateCollisions(false),
@ -193,7 +193,7 @@ void AssetImportConfig::initPersistFields()
addField("SeparateAnimations", TypeBool, Offset(SeparateAnimations, AssetImportConfig), "When importing a shape file, should the animations within be separated out into unique files");
addField("SeparateAnimationPrefix", TypeRealString, Offset(SeparateAnimationPrefix, AssetImportConfig), "If separating animations out from a source file, what prefix should be added to the names for grouping association");
addField("animTiming", TypeRealString, Offset(animTiming, AssetImportConfig), "Defines the animation timing for the given animation sequence. Options are FrameTime, Seconds, Milliseconds");
addField("animFPS", TypeBool, Offset(animFPS, AssetImportConfig), "The FPS of the animation sequence");
addField("animFPS", TypeF32, Offset(animFPS, AssetImportConfig), "The FPS of the animation sequence");
addField("AlwaysAddShapeAnimationSuffix", TypeBool, Offset(AlwaysAddShapeAnimationSuffix, AssetImportConfig), "When importing a shape animation, this indicates if it should automatically add a standard suffix onto the name");
addField("AddedShapeAnimationSuffix", TypeString, Offset(AddedShapeAnimationSuffix, AssetImportConfig), " If AlwaysAddShapeAnimationSuffix is on, this is the suffix to be added");
endGroup("Animation");

View file

@ -349,6 +349,14 @@ void GameBase::inspectPostApply()
setMaskBits(ExtendedInfoMask);
}
void GameBase::onInspect(GuiInspector* inspector)
{
if (mDataBlock && mDataBlock->isMethod("onInspect"))
Con::executef(mDataBlock, "onInspect", this, inspector);
else
Parent::onInspect(inspector);
}
//----------------------------------------------------------------------------
void GameBase::processTick(const Move * move)

View file

@ -262,6 +262,8 @@ public:
static void initPersistFields();
static void consoleInit();
virtual void onInspect(GuiInspector*) override;
/// @}
///@name Datablock

View file

@ -2435,6 +2435,69 @@ AssetManager::typeAssetDependsOnHash* AssetManager::getDependedOnAssets()
// Find any asset dependencies.
return &mAssetDependsOn;
}
//-----------------------------------------------------------------------------
S32 AssetManager::getAssetLooseFileCount(const char* pAssetId)
{
// Debug Profiling.
PROFILE_SCOPE(AssetManager_getAssetLooseFileCount);
// Sanity!
AssertFatal(pAssetId != NULL, "Cannot get loose files for NULL asset Id.");
// Find asset.
AssetDefinition* pAssetDefinition = findAsset(pAssetId);
// Did we find the asset?
if (pAssetDefinition == NULL)
{
// No, so warn.
Con::warnf("Asset Manager: Failed to get loose files for asset Id '%s' as it does not exist.", pAssetId);
return false;
}
S32 looseFileCount = pAssetDefinition->mAssetLooseFiles.size();
// Cleanup our reference
pAssetDefinition = NULL;
return looseFileCount;
}
//-----------------------------------------------------------------------------
const char* AssetManager::getAssetLooseFile(const char* pAssetId, const S32& index)
{
// Debug Profiling.
PROFILE_SCOPE(AssetManager_getAssetLooseFile);
// Sanity!
AssertFatal(pAssetId != NULL, "Cannot get loose file for NULL asset Id.");
// Find asset.
AssetDefinition* pAssetDefinition = findAsset(pAssetId);
// Did we find the asset?
if (pAssetDefinition == NULL)
{
// No, so warn.
Con::warnf("Asset Manager: Failed to get loose file for asset Id '%s' as it does not exist.", pAssetId);
return false;
}
if(index < 0 || index >= pAssetDefinition->mAssetLooseFiles.size())
{
Con::warnf("Asset Manager : Failed to get loose file for asset Id '%s' as the index was out of range.", pAssetId);
}
StringTableEntry looseFile = pAssetDefinition->mAssetLooseFiles[index];
// Cleanup our reference
pAssetDefinition = NULL;
return looseFile;
}
//-----------------------------------------------------------------------------
bool AssetManager::scanDeclaredAssets( const char* pPath, const char* pExtension, const bool recurse, ModuleDefinition* pModuleDefinition )

View file

@ -376,6 +376,9 @@ public:
typeAssetDependsOnHash* getDependedOnAssets();
S32 getAssetLooseFileCount(const char* pAssetId);
const char* getAssetLooseFile(const char* pAssetId, const S32& index);
/// Declare Console Object.
DECLARE_CONOBJECT( AssetManager );

View file

@ -763,6 +763,36 @@ DefineEngineMethod(AssetManager, findAssetLooseFile, S32, (const char* assetQuer
//-----------------------------------------------------------------------------
DefineEngineMethod(AssetManager, getAssetLooseFileCount, S32, (const char* assetId), (""),
"Gets the number of loose files associated with the given assetId.\n"
"@param assetId The assetId to check.\n"
"@return The number of loose files associated with the assetId.\n")
{
// Fetch asset loose file.
const char* pAssetId = assetId;
// Perform query.
return object->getAssetLooseFileCount(pAssetId);
}
//-----------------------------------------------------------------------------
DefineEngineMethod(AssetManager, getAssetLooseFile, const char*, (const char* assetId, S32 index), ("", 0),
"Gets the loose file associated to the given assetId at the provided index.\n"
"@param assetId The assetId to check.\n"
"@param index The index of the loose file to get.\n"
"@return The file name of the associated loose file.\n")
{
// Fetch asset loose file.
const char* pAssetId = assetId;
// Perform query.
return object->getAssetLooseFile(pAssetId, index);
}
//-----------------------------------------------------------------------------
DefineEngineMethod(AssetManager, getDeclaredAssetCount, bool, (),,
"Gets the number of declared assets.\n"
"@return Returns the number of declared assets.\n")

View file

@ -118,9 +118,10 @@ public:
/// Called from Forest the first time a datablock is used
/// in order to lazy load content.
void preload()
{
if ( !mNeedPreload )
bool preload(bool server, String& errorStr) override { return false; }; // we don't ghost ForestItemData specifically. we do do so for TSForestItemData
void preload()
{
if (!mNeedPreload)
return;
_preload();

View file

@ -382,7 +382,7 @@ void GuiIconButtonCtrl::renderButton( Point2I &offset, const RectI& updateRect )
Point2I start( mTextMargin, ( getHeight() - mProfile->mFont->getHeight() ) / 2 );
if (mBitmapAsset.notNull() && mIconLocation != IconLocNone)
{
start.x = iconRect.extent.x + mButtonMargin.x + mTextMargin;
start.x = getWidth() - (iconRect.extent.x + mButtonMargin.x + textWidth);
}
drawer->setBitmapModulation(fontColor);

View file

@ -2855,6 +2855,23 @@ DefineEngineMethod( GuiControl, getGlobalCenter, Point2I, (),,
//-----------------------------------------------------------------------------
DefineEngineMethod(GuiControl, setGlobalCenter, void, (S32 x, S32 y), ,
"Set the coordinate of the control's center point in coordinates relative to the root control in its control hierarchy.\n"
"@param x The X coordinate of the new center point of the control relative to the root control's.\n"
"@param y The Y coordinate of the new center point of the control relative to the root control's.")
{
//see if we can turn the x/y into ints directly,
Point2I lPosOffset = object->globalToLocalCoord(Point2I(x, y));
lPosOffset += object->getPosition();
const Point2I ext = object->getExtent();
Point2I newpos(lPosOffset.x - ext.x / 2, lPosOffset.y - ext.y / 2);
object->setPosition(newpos);
}
//-----------------------------------------------------------------------------
DefineEngineMethod( GuiControl, getGlobalPosition, Point2I, (),,
"Get the position of the control relative to the root of the GuiControl hierarchy it is contained in.\n"
"@return The control's current position in root-relative coordinates." )

View file

@ -133,6 +133,25 @@ void GuiInputCtrl::onSleep()
clearFirstResponder();
}
void GuiInputCtrl::setActive(bool value)
{
Parent::setActive(value);
if (value)
{
if (!smDesignTime && !mIgnoreMouseEvents)
mouseLock();
setFirstResponder();
}
else
{
mouseUnlock();
clearFirstResponder();
}
}
//------------------------------------------------------------------------------
static bool isModifierKey( U16 keyCode )

View file

@ -51,6 +51,8 @@ public:
bool onWake() override;
void onSleep() override;
virtual void setActive(bool state);
bool onInputEvent( const InputEventInfo &event ) override;
static void initPersistFields();

View file

@ -1409,3 +1409,13 @@ DefineEngineMethod( EditTSCtrl, isMiddleMouseDown, bool, (),, "" )
{
return object->isMiddleMouseDown();
}
DefineEngineMethod(EditTSCtrl, isLeftMouseDown, bool, (), , "")
{
return object->isLeftMouseDown();
}
DefineEngineMethod(EditTSCtrl, isRightMouseDown, bool, (), , "")
{
return object->isRightMouseDown();
}

View file

@ -189,7 +189,9 @@ class EditTSCtrl : public GuiTSCtrl
virtual void on3DMouseWheelDown(const Gui3DMouseEvent &){};
virtual void get3DCursor(GuiCursor *&cursor, bool &visible, const Gui3DMouseEvent &);
virtual bool isLeftMouseDown() { return mLeftMouseDown; }
virtual bool isMiddleMouseDown() {return mMiddleMouseDown;}
virtual bool isRightMouseDown() { return mLeftMouseDown; }
bool resize(const Point2I& newPosition, const Point2I& newExtent) override;

View file

@ -101,6 +101,9 @@ void AssimpAppMesh::computeBounds(Box3F& bounds)
TSMesh* AssimpAppMesh::constructTSMesh()
{
if (points.empty() || normals.empty() || primitives.empty() || indices.empty())
return NULL;
TSMesh* tsmesh;
if (isSkin())
{

View file

@ -126,6 +126,9 @@ void AppMesh::computeNormals()
TSMesh* AppMesh::constructTSMesh()
{
if (points.empty() || normals.empty() || primitives.empty() || indices.empty())
return NULL;
TSMesh* tsmesh;
if (isSkin())
{