Add options: --count-meshes, --verbose, --help, --exit. See changelog for more.

master
poikilos 2021-11-28 16:17:17 -05:00
parent 56c0e4b0ff
commit 2e08e563a6
7 changed files with 237 additions and 77 deletions

View File

@ -340,7 +340,10 @@ s32 Engine::getNumberOfVertices()
Engine::Engine()
{
this->m_EnableTestAndExit = false;
this->m_EnableTests = false;
this->m_EnableCountMeshes = false;
this->m_EnableVerbose = false;
this->m_EnableExit = false;
settings.set_int("max_recent", 10);
std::string profile = std::getenv("HOME");
// ^ changes to USERPROFILE below if blank
@ -473,15 +476,19 @@ bool Engine::loadMesh(const wstring& fileName, bool enableAddRecent)
m_Device->setWindowCaption((wstring(L"b3view - ") + fileName).c_str());
m_LoadedMesh = m_Scene->addAnimatedMeshSceneNode(mesh);
Utility::dumpMeshInfoToConsole(m_LoadedMesh);
std::cerr << "Arranging scene..." << std::flush;
if (this->m_EnableVerbose) {
Utility::dumpMeshInfoToConsole(m_LoadedMesh);
std::cerr << "Arranging scene..." << std::flush;
}
if (Utility::toLower(Utility::extensionOf(fileName)) == L"3ds") {
m_View->setZUp(true);
} else {
m_View->setZUp(false);
}
if (m_LoadedMesh != nullptr) {
std::cerr << "unloading old mesh..." << std::flush;
if (this->m_EnableVerbose) {
std::cerr << "unloading old mesh..." << std::flush;
}
ret = true;
this->m_UserInterface->playbackFPSEditBox->setText(
Utility::toWstring(m_LoadedMesh->getAnimationSpeed()).c_str()
@ -534,14 +541,20 @@ bool Engine::loadMesh(const wstring& fileName, bool enableAddRecent)
// EMT_TRANSPARENT_ALPHA_CHANNEL: constant transparency
}
std::cerr << "setting display mode..." << std::flush;
if (this->m_EnableVerbose) {
std::cerr << "setting display mode..." << std::flush;
}
this->setMeshDisplayMode(this->m_EnableWireframe, this->m_EnableLighting,
this->m_EnableTextureInterpolation);
std::cerr << "preparing UI..." << std::flush;
if (this->m_EnableVerbose) {
std::cerr << "preparing UI..." << std::flush;
}
if (this->m_UserInterface != nullptr)
this->m_UserInterface->OnSelectMesh();
std::cerr << "checking for textures..." << std::flush;
std::cerr << "OK" << std::endl;
if (this->m_EnableVerbose) {
std::cerr << "checking for textures..." << std::flush;
std::cerr << "OK" << std::endl;
}
if (Utility::getTextureCount(m_LoadedMesh) == 0) {
// NOTE: getMaterialCount doesn't work, since there may not
// be loaded textures in any material.
@ -549,7 +562,9 @@ bool Engine::loadMesh(const wstring& fileName, bool enableAddRecent)
this->m_UserInterface->loadNextTexture(0);
}
}
std::cerr << "detecting last frame..." << std::flush;
if (this->m_EnableVerbose) {
std::cerr << "detecting last frame..." << std::flush;
}
std::wstring prevStartStr;
std::wstring prevEndStr;
if (this->m_UserInterface->playbackMenu->getItemText(UIE_PLAYBACKSTARTFRAMEEDITBOX) != nullptr)
@ -565,16 +580,22 @@ bool Engine::loadMesh(const wstring& fileName, bool enableAddRecent)
prevEnd = Utility::toF32(prevEndStr);
// std::cerr << prevEnd << "..." << std::flush;
f32 endFrameF32 = static_cast<f32>(m_LoadedMesh->getEndFrame());
std::cerr << endFrameF32 << "..." << std::flush;
if (this->m_EnableVerbose) {
std::cerr << endFrameF32 << "..." << std::flush;
}
if (prevEnd < 0 || prevEnd > endFrameF32) {
std::cerr << "showing End Frame..." << std::flush;
if (this->m_EnableVerbose) {
std::cerr << "showing End Frame..." << std::flush;
}
this->m_UserInterface->setPlaybackText(
UIE_PLAYBACKENDFRAMEEDITBOX,
Utility::toWstring(endFrameF32).c_str()
);
}
if (prevStart < 0 || prevStart > endFrameF32) {
std::cerr << "showing Start Frame..." << std::flush;
if (this->m_EnableVerbose) {
std::cerr << "showing Start Frame..." << std::flush;
}
this->m_UserInterface->setPlaybackText(
UIE_PLAYBACKSTARTFRAMEEDITBOX,
L"0.0"
@ -582,7 +603,9 @@ bool Engine::loadMesh(const wstring& fileName, bool enableAddRecent)
}
//this->m_UserInterface->playbackMenu->setItemText(UIE_PLAYBACKSTARTFRAMEEDITBOX, );
//;
std::cerr << "OK" << std::endl;
if (this->m_EnableVerbose) {
std::cerr << "OK" << std::endl;
}
}
// Don't do anything outside of the mesh != nullptr case that will try to
// use mesh!
@ -592,8 +615,26 @@ bool Engine::loadMesh(const wstring& fileName, bool enableAddRecent)
bool Engine::pushOption(const std::wstring& optionStr)
{
if (optionStr == L"--test-and-exit") {
this->m_EnableTestAndExit = true;
std::cerr << "* using option --test-and-exit" << std::endl;
this->m_EnableTests = true;
this->m_EnableExit = true;
}
else if (optionStr == L"--exit") {
this->m_EnableExit = true;
}
else if (optionStr == L"--count-meshes") {
this->m_EnableCountMeshes = true;
}
else if (optionStr == L"--verbose") {
this->m_EnableVerbose = true;
}
else if (optionStr == L"--help") {
std::cerr
<< "--test-and-exit Run tests then exit the program." << std::endl
<< "--count-meshes Count the number of meshes in the file." << std::endl
<< "--verbose Show mesh metadata (must be before mesh filename to show that) and internal events." << std::endl
<< "--exit Exit the program after processing other options." << std::endl
;
this->m_EnableExit = true;
}
else {
std::cerr << "The option is not valid: " << Utility::toString(optionStr) << std::endl;
@ -742,13 +783,17 @@ bool Engine::loadTexture(const wstring& fileName, bool reload)
debug() << "* failed to load " << "" << std::endl;
}
this->m_LoadedTexturePath = fileName;
std::cerr << "Setting texture path box to " << Utility::toString(this->m_LoadedTexturePath) << std::endl;
if (this->m_EnableVerbose) {
std::cerr << "Setting texture path box to " << Utility::toString(this->m_LoadedTexturePath) << std::endl;
}
this->m_UserInterface->texturePathEditBox->setText(
this->m_LoadedTexturePath.c_str()
);
}
else {
std::cerr << "NOT Setting texture path box to " << Utility::toString(this->m_LoadedTexturePath) << std::endl;
if (this->m_EnableVerbose) {
std::cerr << "NOT Setting texture path box to " << Utility::toString(this->m_LoadedTexturePath) << std::endl;
}
}
return ret;
}
@ -916,9 +961,15 @@ void Engine::run()
// Run the Device with fps frames/sec
while (m_Device->run() && m_RunEngine) {
if (this->m_EnableTestAndExit) {
if (this->m_EnableCountMeshes) {
this->m_EnableCountMeshes = false;
if (this->m_LoadedMesh != nullptr) {
std::cout << "mesh_count=" << this->m_LoadedMesh->getMesh()->getMeshBufferCount() << std::endl;
}
}
if (this->m_EnableTests) {
std::cerr << "* running tests..." << std::endl;
this->m_EnableTestAndExit = false;
this->m_EnableTests = false;
std::cerr << "* loading test model..." << std::endl;
if (!this->loadMesh(L"dist/share/b3view/meshes/penguin-lowpoly-poikilos.b3d", false)) {
throw "loading dist/share/b3view/meshes/penguin-lowpoly-poikilos.b3d failed.";
@ -927,9 +978,15 @@ void Engine::run()
if (!this->m_UserInterface->loadNextTexture(1)) {
throw "loading the next texture for dist/share/b3view/meshes/penguin-lowpoly-poikilos.b3d failed.";
}
this->m_RunEngine = false;
// Don't break yet. Test the main event loop tooo.
}
if (this->m_EnableExit) {
this->m_RunEngine = false;
if (!this->m_EnableTests) {
break;
}
// else don't break yet: Test the main event loop too.
}
u32 startTime = timer->getRealTime();
checkResize();

View File

@ -40,7 +40,10 @@ private:
bool m_EnableWireframe;
bool m_EnableLighting;
bool m_EnableTextureInterpolation;
bool m_EnableTestAndExit;
bool m_EnableTests;
bool m_EnableExit;
bool m_EnableCountMeshes;
bool m_EnableVerbose;
EventHandler* m_EventHandler;
UserInterface* m_UserInterface;

View File

@ -37,7 +37,12 @@ void UserInterface::setupUserInterface()
{
this->recent_initialized = false;
this->recentMenu = nullptr;
bool enableVerbose = false;
if (this->m_Engine != nullptr) {
enableVerbose = this->m_Engine->m_EnableVerbose;
} else {
std::cerr << "Error: The engine is not ready in setupUserInterface." << std::endl;
}
// Menu
menu = m_Gui->addMenu();
this->fileMenuIdx = menu->addItem(L"File", UIE_FILEMENU, true, true);
@ -63,13 +68,16 @@ void UserInterface::setupUserInterface()
// File, Open Recent submenu
this->recentMenu = fileMenu->getSubMenu(this->fileRecentIdx);
std::cerr << "+this->recentMenu text:\"" << Utility::toString((wstring)this->recentMenu->getText()) << "\""
<< " idx:" << Utility::toString((int)this->fileRecentIdx)
<< " id:" << Utility::toString((int)this->recentMenu->getID())
<< std::endl;
if (enableVerbose) {
std::cerr << "+this->recentMenu text:\"" << Utility::toString((wstring)this->recentMenu->getText()) << "\""
<< " idx:" << Utility::toString((int)this->fileRecentIdx)
<< " id:" << Utility::toString((int)this->recentMenu->getID())
<< std::endl;
}
this->fileRecentClearIdx = this->recentMenu->addItem(L"Clear Recent", UIC_FILE_RECENT_CLEAR);
std::cerr << "+this->fileRecentClearIdx: " << this->fileRecentClearIdx << std::endl;
if (enableVerbose) {
std::cerr << "+this->fileRecentClearIdx: " << this->fileRecentClearIdx << std::endl;
}
this->uic_file_recent_next = UserInterface::UIC_FILE_RECENT_FIRST;
this->m_file_recent_first_idx = -1;
this->m_file_recent_last_idx = -1;
@ -303,12 +311,16 @@ void UserInterface::setupUserInterface()
m_GuiFont->attach(m_GuiFontFace, 14);
m_Gui->getSkin()->setFont(m_GuiFont);
} else {
std::wcerr << L"WARNING: '" << m_Engine->m_FontPath << L"' is missing."
<< endl;
if (enableVerbose) {
std::wcerr << L"WARNING: '" << m_Engine->m_FontPath << L"' is missing."
<< endl;
}
delete m_GuiFontFace;
m_GuiFontFace = nullptr;
if (m_GuiFont != nullptr) {
std::wcerr << L" - The old font will remain loaded." << endl;
if (enableVerbose) {
std::wcerr << L" - The old font will remain loaded." << endl;
}
}
}
// }
@ -359,21 +371,26 @@ bool UserInterface::handleMenuItemPressed(const SEvent::SGUIEvent* ge)
s32 callerID = ge->Caller->getID();
s32 selected = menu->getSelectedItem();
s32 commandID = menu->getItemCommandId(static_cast<u32>(selected));
bool enableVerbose = this->m_Engine->m_EnableVerbose;
switch (callerID) {
case UIE_RECENTMENU:
// if ((ge->Caller->getID() >= this->m_file_recent_first_idx)
// && (ge->Caller->getID() <= m_file_recent_last_idx)) {
// NOTE: ge->Caller->getID() is probably UIE_RECENTMENU now, but that is not to be used directly!
cerr << "selected " << selected << std::endl;
if (enableVerbose) {
cerr << "selected " << selected << std::endl;
}
if (std::find(this->recentIndices.begin(), this->recentIndices.end(), commandID) != this->recentIndices.end()) {
// ge->Caller->getText() // Don't do this. Caller is the parent!
cerr << "parent callerID: " << callerID << endl;
// ^ callerID is the parent such as 1100 (or whatever UI_RECENTMENU is)
cerr << " commandID: " << commandID << std::endl;
// ^ commandID is a menu id specified on creation
// such as starting from 1101
// or from whatever UIC_FILE_RECENT_FIRST is--usually UI_RECENTMENU+1).
// selectedItemID is a sequential number.
if (enableVerbose) {
cerr << "parent callerID: " << callerID << endl;
// ^ callerID is the parent such as 1100 (or whatever UI_RECENTMENU is)
cerr << " commandID: " << commandID << std::endl;
// ^ commandID is a menu id specified on creation
// such as starting from 1101
// or from whatever UIC_FILE_RECENT_FIRST is--usually UI_RECENTMENU+1).
// selectedItemID is a sequential number.
}
// std::wstring menuItemText = menu->getItemText(selected);
this->openRecent(commandID, selected);
}
@ -381,13 +398,16 @@ bool UserInterface::handleMenuItemPressed(const SEvent::SGUIEvent* ge)
cerr << "Unknown commandID: " << commandID << " Text:" << Utility::toString(menu->getItemText(selected)) << endl;
// ^ getItemText takes the index (NOT the commandID specified on creation)
if (this->recentIndices.size() < 1) {
cerr << "- recentIndices.size(): " << recentIndices.size() << endl;
}
else {
cerr << " recentIndices: " << recentIndices.size() << endl;
// range based for loop requires C++11 or higher:
for(irr::u32 i : this->recentIndices) {
cerr << " - " << i << endl;
if (enableVerbose) {
cerr << " recentIndices: " << recentIndices.size() << endl;
// range based for loop requires C++11 or higher:
for(irr::u32 i : this->recentIndices) {
cerr << " - " << i << endl;
}
}
}
handled = false;
@ -653,7 +673,13 @@ void UserInterface::setPlaybackText(s32 id, const wchar_t* text)
*/
bool UserInterface::loadNextTexture(int direction)
{
cerr << "Loading texture..." << flush;
bool enableVerbose = false;
if (this->m_Engine != nullptr) {
enableVerbose = this->m_Engine->m_EnableVerbose;
}
if (enableVerbose) {
cerr << "Loading texture..." << flush;
}
bool ret = false;
std::wstring basePath = L".";
if (this->m_Engine->m_LoadedMeshPath.length() > 0) {
@ -746,9 +772,11 @@ bool UserInterface::loadNextTexture(int direction)
vector<wstring> paths = this->m_MatchingTextures;
if (this->m_MatchingTextures.size() < 1) {
paths = this->m_AllTextures;
debug() << "There were no matching textures so"
<< " the entire list of " << this->m_AllTextures.size()
<< " found textures will be used..." << std::flush;
if (enableVerbose) {
debug() << "There were no matching textures so"
<< " the entire list of " << this->m_AllTextures.size()
<< " found textures will be used..." << std::flush;
}
}
else {
// Assume the user wants to view name-matched texture using
@ -796,43 +824,60 @@ bool UserInterface::loadNextTexture(int direction)
}
if (lastTexture.length() > 0) {
if (direction < 0) {
cerr << "loading the previous texture...";
if (enableVerbose) {
cerr << "loading the previous texture...";
}
ret = this->m_Engine->loadTexture(prevTexture, false);
}
else if (direction > 0) {
cerr << "loading the next texture...";
if (enableVerbose) {
cerr << "loading the next texture...";
}
ret = this->m_Engine->loadTexture(nextTexture, false);
}
else {
// If direction is 0 (such as when a model is loaded that has
// no texture), only load a specified or matching texture.
if (this->m_Engine->m_LoadedTexturePath.length() > 0) {
cerr << "using a specified texture...";
if (enableVerbose) {
cerr << "using a specified texture...";
}
ret = this->m_Engine->loadTexture(
this->m_Engine->m_LoadedTexturePath,
false
);
}
else if (this->m_MatchingTextures.size() >= 1) {
cerr << "loading matching texture...";
if (enableVerbose) {
cerr << "loading matching texture...";
}
ret = this->m_Engine->loadTexture(firstTexture, false);
}
else {
ret = true;
cerr << "(cycling was off and there is no matching texture) ";
if (enableVerbose) {
cerr << "(cycling was off and there is no matching texture) ";
}
}
}
}
else if (this->m_Engine->m_LoadedTexturePath.length() > 0) {
cerr << "loading the first texture...";
if (enableVerbose) {
cerr << "loading the first texture...";
}
ret = this->m_Engine->loadTexture(
this->m_Engine->m_LoadedTexturePath,
false
);
}
} else
debug() << "Can't cycle texture since no file was opened" << endl;
cerr << (ret?"OK":"FAILED") << endl;
} else {
if (enableVerbose) {
debug() << "Can't cycle texture since no file was opened" << endl;
}
}
if (enableVerbose) {
cerr << (ret?"OK":"FAILED") << endl;
}
return ret;
}
@ -888,12 +933,22 @@ void UserInterface::clearRecent()
void UserInterface::addRecentMenuItem(std::string path, bool addToEngine)
{
bool enableVerbose = false;
if (this->m_Engine != nullptr) {
enableVerbose = this->m_Engine->m_EnableVerbose;
} else {
std::cerr << "Error: m_Engine isn't ready in addRecentMenuItem." << std::endl;
}
if (!this->recent_initialized) {
throw std::runtime_error("The UI is not ready in addRecentMenuItem.");
}
std::cerr << "[addRecentMenuItem] " << path << "..." << std::endl;
if (enableVerbose) {
std::cerr << "[addRecentMenuItem] " << path << "..." << std::endl;
}
if (!this->hasRecent(path)) {
std::cerr << "* adding since new..." << std::endl;
if (enableVerbose) {
std::cerr << "* adding since new..." << std::endl;
}
wstring path_ws = Utility::toWstring(path);
if (this->uic_file_recent_next < UserInterface::UIC_FILE_RECENT_FIRST) {
throw std::runtime_error("this->uic_file_recent_next is "
@ -904,10 +959,12 @@ void UserInterface::addRecentMenuItem(std::string path, bool addToEngine)
// The first this->uic_file_recent_next is 1101 or whatever
// UserInterface::UIC_FILE_RECENT_FIRST (usually UIC_FILE_RECENT+1) is.
u32 newI = this->recentMenu->addItem(path_ws.c_str(), this->uic_file_recent_next);
std::cerr << "+this->recentMenu->addItem"
<< " idx:" << newI
<< " commandID:" << this->uic_file_recent_next
<< std::endl;
if (enableVerbose) {
std::cerr << "+this->recentMenu->addItem"
<< " idx:" << newI
<< " commandID:" << this->uic_file_recent_next
<< std::endl;
}
// IGUIContextMenu* menu = this->recentMenu->getSubMenu(newI);
// NOTE: Caller would be the parent menu id on click!
// newI is a sequential number starting at 1 which becomes the
@ -945,12 +1002,19 @@ void UserInterface::addRecentMenuItems(std::vector<std::string> paths, bool addT
bool UserInterface::hasRecent(std::string path)
{
bool enableVerbose = false;
if (this->m_Engine != nullptr) {
enableVerbose = this->m_Engine->m_EnableVerbose;
} else {
std::cerr << "Error: The engine is not ready in hasRecent." << std::endl;
}
if (!this->recent_initialized) {
throw std::runtime_error("The UI is not ready in addRecent.");
}
std::cerr << " [hasRecent]" << std::endl;
std::cerr << " * checking children..." << std::endl;
if (enableVerbose) {
std::cerr << " [hasRecent]" << std::endl;
std::cerr << " * checking children..." << std::endl;
}
// See http://irrlicht.sourceforge.net/docu/_i_g_u_i_element_8h_source.html#l00570
// core::list< IGUIElement * > Children = this->getChildren();
// ^ class UserInterface has no member named getChildren
@ -1264,7 +1328,7 @@ bool UserInterface::OnEvent(const SEvent& event)
handled = false;
break;
case EGET_ELEMENT_LEFT:
debug() << "left " << callerID << "." << std::endl;
// debug() << "left " << callerID << "." << std::endl;
handled = false;
break;
case EGET_MENU_ITEM_SELECTED:

View File

@ -120,7 +120,15 @@ View::View(Engine* engine)
// m_Yaw = rotationVec3.Y;
// m_Pitch = rotationVec3.X;
debug() << "STARTING Yaw: " << radToDeg(m_YawFromTarget)<< " Pitch: " << radToDeg(m_PitchFromTarget) << endl;
bool enableVerbose = false;
if (this->m_Engine != nullptr) {
enableVerbose = this->m_Engine->m_EnableVerbose;
} else {
std::cerr << "Error: The engine is not ready in View::View." << std::endl;
}
if (enableVerbose) {
debug() << "STARTING Yaw: " << radToDeg(m_YawFromTarget)<< " Pitch: " << radToDeg(m_PitchFromTarget) << endl;
}
setNewCameraPosition();
}
@ -225,12 +233,18 @@ bool View::OnEvent(const SEvent& event)
m_CameraDistance /= 2;
}
setNewCameraPosition();
debug() << "View got the event and used event.MouseInput."
// << " event.GUIEvent.Caller: " << callerID // not avail in Irrlicht (Use this->m_MouseUser instead)
<< ", m_CamPos: " << m_Engine->m_CamPos.X
<< "," << m_Engine->m_CamPos.Y
<< " m_CamTarget: " << m_Engine->m_CamTarget.X
<< "," << m_Engine->m_CamTarget.Y << endl;
bool enableVerbose = true;
if (this->m_Engine != nullptr) {
enableVerbose = this->m_Engine->m_EnableVerbose;
}
if (enableVerbose) {
debug() << "View got the event and used event.MouseInput."
// << " event.GUIEvent.Caller: " << callerID // not avail in Irrlicht (Use this->m_MouseUser instead)
<< ", m_CamPos: " << m_Engine->m_CamPos.X
<< "," << m_Engine->m_CamPos.Y
<< " m_CamTarget: " << m_Engine->m_CamTarget.X
<< "," << m_Engine->m_CamTarget.Y << endl;
}
} else if (m_RotMouse) {
// debug() << "Yaw: " << radToDeg(m_Yaw) << " Pitch: " << radToDeg(m_Pitch) << endl;
int dx = mouseEvent->X - m_LastMousePosition->X;

View File

@ -3,9 +3,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [git] - 2021-11-28
### Added
- --exit option
### Changed
- Split the `m_EnableTests` and `m_EnableExit` options (from
`m_EnableTestAndExit`).
- Reduce verbosity unless `--verbose` is specified.
## [git] - 2021-03-28
### Added
- a `--test-and-exit ` option
- a `--test-and-exit` option
## [git] - 2021-02-22
### Added
@ -14,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed
- identification of recent menu items
## [git] - 2021-02-21
### Added
- codeblocks support

View File

@ -40,8 +40,8 @@ int main(int argc, char** argv)
Engine* engine = new Engine();
if (argc >= 2) {
for (int i = 1; i < argc; i++) {
wchar_t* optionCS = getWideCharString(argv[1]);
if ((strlen(argv[i]) >=2) && (argv[i][0] == '-') && argv[i][1] == '-') {
wchar_t* optionCS = getWideCharString(argv[i]);
if ((strlen(argv[i]) >=2 ) && (argv[i][0] == '-') && (argv[i][1] == '-')) {
engine->pushOption(wstring(optionCS));
}
else {

View File

@ -9,6 +9,7 @@ bat: [github.com/poikilos/mobs_sky](https://github.com/poikilos/mobs_sky)
Website: [poikilos.org](https://poikilos.org)
## Requirements
- libirrlicht (such as libirrlicht1.8 and libirrlicht-dev on Debian 10)
- freetype (such as libfreetype6 and libfreetype6-dev on Debian 10)
@ -36,6 +37,7 @@ CodeBlocks says it is looking for boost_filesystem and boost_system, which may b
- In CodeBlocks (once per computer): Settings, Compiler, Search paths, /usr/include/freetype2
* libboost-system-dev
## Main Features in poikilos fork
* stabilized (makes sure font, model or texture loads before using;
makes sure model is loaded before setting View options)
@ -57,6 +59,13 @@ CodeBlocks says it is looking for boost_filesystem and boost_system, which may b
or `../textures` (where model would be in a parallel directory next to
textures).
* Set the animation loop range (the animation includes the end frame).
* Use the `--count-meshes` option to count the number of meshes in the
model file (such as for knowing how many copies of the Minetest
texture to apply for each variant).
* Use the `--verbose` option to see model metadata (**before** the model
filename if a filename is part of the command) and various internal
events.
* Use the --help option to see additional features.
## Related Software