#include "Base.h" #include "EncoderArguments.h" #include "StringUtil.h" #ifdef WIN32 #define PATH_MAX _MAX_PATH #define realpath(A,B) _fullpath(B,A,PATH_MAX) #endif // The encoder version number should be incremented when a feature is added to the encoder. // The encoder version is not the same as the GPB version. #define ENCODER_VERSION "3.0.0" #define HEIGHTMAP_SIZE_MAX 2049 namespace gameplay { static EncoderArguments* __instance; extern int __logVerbosity = 1; EncoderArguments::EncoderArguments(size_t argc, const char** argv) : _normalMap(false), _parseError(false), _fontPreview(false), _fontFormat(Font::BITMAP), _textOutput(false), _optimizeAnimations(false), _animationGrouping(ANIMATIONGROUP_PROMPT), _outputMaterial(false), _generateTextureGutter(false) { __instance = this; memset(_heightmapResolution, 0, sizeof(int) * 2); if (argc > 1) { // read the options std::vector arguments; for (size_t i = 1; i < argc; ++i) { arguments.push_back(argv[i]); } size_t index = 0; for (size_t i = 0; i < arguments.size(); ++i) { if (arguments[i][0] == '-') { readOption(arguments, &i); index = i + 1; } } if (arguments.size() - index == 2) { setInputfilePath(arguments[index]); setOutputfilePath(arguments[index + 1]); } else if (arguments.size() - index == 1) { setInputfilePath(arguments[index]); } } else { _parseError = true; } } EncoderArguments::~EncoderArguments(void) { } EncoderArguments* EncoderArguments::getInstance() { return __instance; } const std::string& EncoderArguments::getFilePath() const { return _filePath; } const std::string EncoderArguments::getFileDirPath() const { int pos = _filePath.find_last_of('/'); return (pos == -1 ? _filePath : _filePath.substr(0, pos)); } const char* EncoderArguments::getFilePathPointer() const { return _filePath.c_str(); } const std::string EncoderArguments::getFileName() const { int pos = _filePath.find_last_of('/'); return (pos == -1 ? _filePath : _filePath.substr(pos + 1)); } std::string EncoderArguments::getOutputDirPath() const { if (_fileOutputPath.size() > 0) { int pos = _fileOutputPath.find_last_of('/'); return (pos == -1 ? _fileOutputPath : _fileOutputPath.substr(0, pos)); } else { int pos = _filePath.find_last_of('/'); return (pos == -1 ? _filePath : _filePath.substr(0, pos)); } } std::string EncoderArguments::getOutputFileExtension() const { switch (getFileFormat()) { case FILEFORMAT_TMX: return ".scene"; case FILEFORMAT_PNG: case FILEFORMAT_RAW: if (_normalMap) return ".png"; default: return ".gpb"; } } std::string EncoderArguments::getOutputFilePath() const { if (_fileOutputPath.size() > 0) { // Output file explicitly set return _fileOutputPath; } else { // Generate an output file path int pos = _filePath.find_last_of('.'); std::string outputFilePath(pos > 0 ? _filePath.substr(0, pos) : _filePath); // Modify the original file name if the output extension can be the same as the input if (_normalMap) { outputFilePath.append("_normalmap"); } outputFilePath.append(getOutputFileExtension()); return outputFilePath; } } const std::vector& EncoderArguments::getGroupAnimationNodeId() const { return _groupAnimationNodeId; } const std::vector& EncoderArguments::getGroupAnimationAnimationId() const { return _groupAnimationAnimationId; } bool EncoderArguments::containsGroupNodeId(const std::string& nodeId) const { return find(_groupAnimationNodeId.begin(), _groupAnimationNodeId.end(), nodeId) != _groupAnimationNodeId.end(); } const std::string EncoderArguments::getAnimationId(const std::string& nodeId) const { for (size_t i = 0, size = _groupAnimationNodeId.size(); i < size; ++i) { if (_groupAnimationNodeId[i].compare(nodeId) == 0) { return _groupAnimationAnimationId[i]; } } return ""; } EncoderArguments::AnimationGroupOption EncoderArguments::getAnimationGrouping() const { return _animationGrouping; } const std::vector& EncoderArguments::getHeightmapOptions() const { return _heightmaps; } unsigned int EncoderArguments::tangentBinormalIdCount() const { return _tangentBinormalId.size(); } bool EncoderArguments::isGenerateTangentBinormalId(const std::string& id) const { return _tangentBinormalId.find(id) != _tangentBinormalId.end(); } bool EncoderArguments::normalMapGeneration() const { return _normalMap; } void EncoderArguments::getHeightmapResolution(int* x, int* y) const { *x = _heightmapResolution[0]; *y = _heightmapResolution[1]; } const Vector3& EncoderArguments::getHeightmapWorldSize() const { return _heightmapWorldSize; } bool EncoderArguments::parseErrorOccured() const { return _parseError; } bool EncoderArguments::fileExists() const { if (_filePath.length() > 0) { struct stat buf; if (stat(_filePath.c_str(), &buf) != -1) { return true; } } return false; } void splitString(const char* str, std::vector* tokens) { // Split node id list into tokens unsigned int length = strlen(str); char* temp = new char[length + 1]; strcpy(temp, str); char* tok = strtok(temp, ","); while (tok) { tokens->push_back(tok); tok = strtok(NULL, ","); } delete[] temp; } void EncoderArguments::printUsage() const { LOG(1, "Usage: gameplay-encoder [options] \n\n" \ "Encoder version: " ENCODER_VERSION "\n\n" \ "Supported file extensions:\n" \ " .fbx\t(FBX scenes)\n" \ " .ttf\t(TrueType fonts)\n" \ "\n" \ "General options:\n" \ " -v \tVerbosity level (0-4).\n" \ "\n" \ "FBX file options:\n" \ " -i \tFilter by node ID.\n" \ " -t\t\tWrite text/xml.\n" \ " -g:auto\tAutomatically group animation channels into a new animation.\n" \ " -g:none\tDo not prompt to group animations.\n" \ " -g \n" \ "\t\tGroup all animation channels targeting the nodes into a \n" \ "\t\tnew animation.\n" \ " -m\t\tOutput material file for scene.\n" \ " -tb \n" \ "\t\tGenerates tangents and binormals for the given node.\n" \ " -oa\n" \ "\t\tOptimizes animations by analyzing animation channel data and\n" \ "\t\tremoving any channels that contain default/identity values\n" \ "\t\tand removing any duplicate contiguous keyframes, which are \n" \ "\t\tcommon when exporting baked animation data.\n" \ " -h \"\" \n" \ "\t\tGenerates a single heightmap image using meshes from the \n" \ "\t\tspecified nodes. \n" \ "\t\t is two comma-separated numbers in the format \"X,Y\", \n" \ "\t\tindicating the dimensions of the produced heightmap image.\n" \ "\t\t should be in quotes with a space between each id.\n" \ "\t\tFilename is the name of the image (PNG) to be saved.\n" \ "\t\tMultiple -h arguments can be supplied to generate more than one \n" \ "\t\theightmap. For 24-bit packed height data use -hp instead of -h.\n" \ "\n" \ "TMX file options:\n" \ " -tg\tEnable texture gutter's around tiles. This will modify any referenced\n" \ " \ttile sets to add a 1px border around it to prevent seams.\n" " -tg:none\tDo not priduce a texture gutter.\n" "\n" \ "Normal map options:\n" \ " -n\t\tGenerate normal map (requires input file of type PNG or RAW)\n" \ " -s\t\tSize/resolution of the input heightmap image (required for RAW files)\n" \ " -w \tSpecifies the size of an input terrain heightmap file in world\n" \ "\t\tunits, along the X, Y and Z axes. should be three \n" \ "\t\tcomma-separated numbers in the format \"X,Y,Z\". \n" \ "\t\tThe Y value represents the maximum possible height value of a \n" \ "\t\tfull intensity heightmap pixel.\n" \ "\n" \ " \t\tNormal map generation can be used to create object-space normal maps from \n" \ " \t\theightmap images. Heightmaps must be in either PNG format (where the \n" \ " \t\tintensity of each pixel represents a height value), or in RAW format \n" \ " \t\t(8 or 16-bit), which is a common headerless format supported by most \n" \ " \t\tterrain generation tools.\n" \ "\n" \ "TTF file options:\n" \ " -s \tComma-separated list of font sizes (in pixels).\n" \ " -p\t\tOutput font preview.\n" \ " -f\t\tFormat of font. -f:b (BITMAP), -f:d (DISTANCE_FIELD).\n" \ "\n"); exit(8); } bool EncoderArguments::fontPreviewEnabled() const { return _fontPreview; } Font::FontFormat EncoderArguments::getFontFormat() const { return _fontFormat; } bool EncoderArguments::textOutputEnabled() const { return _textOutput; } bool EncoderArguments::optimizeAnimationsEnabled() const { return _optimizeAnimations; } bool EncoderArguments::outputMaterialEnabled() const { return _outputMaterial; } bool EncoderArguments::generateTextureGutter() const { return _generateTextureGutter; } const char* EncoderArguments::getNodeId() const { if (_nodeId.length() == 0) { return NULL; } return _nodeId.c_str(); } std::vector EncoderArguments::getFontSizes() const { return _fontSizes; } EncoderArguments::FileFormat EncoderArguments::getFileFormat() const { if (_filePath.length() < 5) { return FILEFORMAT_UNKNOWN; } // Extract the extension std::string ext = ""; size_t pos = _filePath.find_last_of("."); if (pos != std::string::npos) { ext = _filePath.substr(pos + 1); } for (size_t i = 0; i < ext.size(); ++i) ext[i] = (char)tolower(ext[i]); // Match every supported extension with its format constant if (ext.compare("fbx") == 0) { return FILEFORMAT_FBX; } if (ext.compare("tmx") == 0) { return FILEFORMAT_TMX; } if (ext.compare("ttf") == 0) { return FILEFORMAT_TTF; } if (ext.compare("gpb") == 0) { return FILEFORMAT_GPB; } if (ext.compare("png") == 0) { return FILEFORMAT_PNG; } if (ext.compare("raw") == 0) { return FILEFORMAT_RAW; } return FILEFORMAT_UNKNOWN; } void EncoderArguments::readOption(const std::vector& options, size_t* index) { const std::string& str = options[*index]; if (str.length() == 0 && str[0] != '-') { return; } switch (str[1]) { case 'f': if (str.compare("-f:b") == 0) { _fontFormat = Font::BITMAP; } else if (str.compare("-f:d") == 0) { _fontFormat = Font::DISTANCE_FIELD; } break; case 'g': if (str.compare("-groupAnimations:auto") == 0 || str.compare("-g:auto") == 0) { _animationGrouping = ANIMATIONGROUP_AUTO; } else if (str.compare("-groupAnimations:off") == 0 || str.compare("-g:off") == 0) { _animationGrouping = ANIMATIONGROUP_OFF; } else if (str.compare("-groupAnimations") == 0 || str.compare("-g") == 0) { // read two strings, make sure not to go out of bounds if ((*index + 2) >= options.size()) { LOG(1, "Error: -g requires 2 arguments.\n"); _parseError = true; return; } (*index)++; _groupAnimationNodeId.push_back(options[*index]); (*index)++; _groupAnimationAnimationId.push_back(options[*index]); } break; case 'i': // Node ID (*index)++; if (*index < options.size()) { _nodeId.assign(options[*index]); } else { LOG(1, "Error: missing arguemnt for -%c.\n", str[1]); _parseError = true; return; } break; case 'o': // Optimization flag if (str == "-oa") { // Optimize animations _optimizeAnimations = true; } break; case 'h': { bool isHighPrecision = str.compare("-hp") == 0; if (str.compare("-heightmap") == 0 || str.compare("-h") == 0 || isHighPrecision) { (*index)++; if (*index < (options.size() + 2)) { _heightmaps.resize(_heightmaps.size() + 1); HeightmapOption& heightmap = _heightmaps.back(); heightmap.isHighPrecision = isHighPrecision; // Read heightmap size std::vector parts; splitString(options[*index].c_str(), &parts); if (parts.size() != 2) { LOG(1, "Error: invalid size argument for -h|-heightmap.\n"); _parseError = true; return; } heightmap.width = atoi(parts[0].c_str()); heightmap.height = atoi(parts[1].c_str()); // Put some artificial bounds on heightmap dimensions if (heightmap.width <= 0 || heightmap.height <= 0 || heightmap.width > HEIGHTMAP_SIZE_MAX || heightmap.height > HEIGHTMAP_SIZE_MAX) { LOG(1, "Error: size argument for -h|-heightmap must be between (1,1) and (%d,%d).\n", (int)HEIGHTMAP_SIZE_MAX, (int)HEIGHTMAP_SIZE_MAX); _parseError = true; return; } // Split node id list into tokens (*index)++; splitString(options[*index].c_str(), &heightmap.nodeIds); // Store output filename (*index)++; heightmap.filename = options[*index]; if (heightmap.filename.empty()) { LOG(1, "Error: missing filename argument for -h|-heightmap.\n"); _parseError = true; return; } // Ensure the output filename has a .png extention if (heightmap.filename.length() > 5) { const char* ext = heightmap.filename.c_str() + (heightmap.filename.length() - 4); if (ext[0] != '.' || tolower(ext[1]) != 'p' || tolower(ext[2]) != 'n' || tolower(ext[3]) != 'g') heightmap.filename += ".png"; } else heightmap.filename += ".png"; } else { LOG(1, "Error: missing argument for -h|-heightmap.\n"); _parseError = true; return; } } } break; case 'm': if (str.compare("-m") == 0) { // generate a material file _outputMaterial = true; } break; case 'n': _normalMap = true; break; case 'w': { // Read world size (*index)++; if (*index >= options.size()) { LOG(1, "Error: missing world size argument for -w.\n"); _parseError = true; return; } std::vector parts; splitString(options[*index].c_str(), &parts); if (parts.size() != 3) { LOG(1, "Error: invalid world size argument for -w.\n"); _parseError = true; return; } _heightmapWorldSize.x = (float)atof(parts[0].c_str()); _heightmapWorldSize.y = (float)atof(parts[1].c_str()); _heightmapWorldSize.z = (float)atof(parts[2].c_str()); if (_heightmapWorldSize.x == 0 || _heightmapWorldSize.y == 0 || _heightmapWorldSize.z == 0) { LOG(1, "Error: invalid world size argument for -w.\n"); _parseError = true; return; } } break; case 'p': _fontPreview = true; break; case 's': if (_normalMap) { (*index)++; if (*index >= options.size()) { LOG(1, "Error: missing argument for -s.\n"); _parseError = true; return; } // Heightmap size std::vector parts; splitString(options[*index].c_str(), &parts); if (parts.size() != 2 || (_heightmapResolution[0] = atoi(parts[0].c_str())) <= 0 || (_heightmapResolution[1] = atoi(parts[1].c_str())) <= 0) { LOG(1, "Error: invalid argument for -s.\n"); _parseError = true; return; } } else { // Font Sizes // old format was -s## const char* sizes = NULL; if (str.length() > 2) { char n = str[2]; if (n > '0' && n <= '9') { sizes = str.c_str() + 2; } } else { (*index)++; if (*index < options.size()) { sizes = options[*index].c_str(); } } if (sizes == NULL) { LOG(1, "Error: invalid format for argument: -s"); _parseError = true; return; } // Parse comma-separated list of font sizes char* ptr = const_cast(sizes); std::string sizeStr; while (ptr) { char* end = strchr(ptr, ','); if (end) { sizeStr = std::string(ptr, end - ptr); ptr = end + 1; } else { sizeStr = ptr; ptr = NULL; } if (sizeStr.length() > 0) { int size = atoi(sizeStr.c_str()); if (size <= 0) { LOG(1, "Error: invalid font size provided: %s", sizeStr.c_str()); _parseError = true; return; } _fontSizes.push_back((unsigned int)size); } } } break; case 't': if (str.compare("-t") == 0) { _textOutput = true; } else if (str.compare("-tb") == 0) { if ((*index + 1) >= options.size()) { LOG(1, "Error: -tb requires 1 argument.\n"); _parseError = true; return; } (*index)++; std::string nodeId = options[*index]; if (nodeId.length() > 0) { _tangentBinormalId.insert(nodeId); } } else if (str.compare("-textureGutter:none") == 0 || str.compare("-tg:none") == 0) { _generateTextureGutter = false; } else if (str.compare("-textureGutter") == 0 || str.compare("-tg") == 0) { _generateTextureGutter = true; } break; case 'v': (*index)++; if (*index < options.size()) { __logVerbosity = atoi(options[*index].c_str()); if (__logVerbosity < 0) __logVerbosity = 0; else if (__logVerbosity > 4) __logVerbosity = 4; } break; default: break; } } void EncoderArguments::setInputfilePath(const std::string& inputPath) { _filePath.assign(getRealPath(inputPath)); } void EncoderArguments::setOutputfilePath(const std::string& outputPath) { std::string ext = getOutputFileExtension(); if (outputPath.size() > 0 && outputPath[0] != '\0') { std::string realPath = getRealPath(outputPath); if (endsWith(realPath.c_str(), ext.c_str())) { _fileOutputPath.assign(realPath); } else if (endsWith(outputPath.c_str(), "/")) { std::string filenameNoExt = getFilenameNoExt(getFilenameFromFilePath(_filePath)); _fileOutputPath.assign(outputPath); _fileOutputPath.append(filenameNoExt); _fileOutputPath.append(ext); } else { std::string filenameNoExt = getFilenameNoExt(getFilenameFromFilePath(realPath)); int pos = realPath.find_last_of("/"); if (pos) { _fileOutputPath = realPath.substr(0, pos); _fileOutputPath.append("/"); _fileOutputPath.append(filenameNoExt); _fileOutputPath.append(ext); } } } } std::string EncoderArguments::getRealPath(const std::string& filepath) { char path[PATH_MAX + 1]; /* not sure about the "+ 1" */ realpath(filepath.c_str(), path); replace_char(path, '\\', '/'); return std::string(path); } void EncoderArguments::replace_char(char* str, char oldChar, char newChar) { for (; *str != '\0'; ++str) { if (*str == oldChar) { *str = newChar; } } } std::string concat(const std::string& a, const char* b) { std::string str(a); str.append(b); return str; } void unittestsEncoderArguments() { std::string dir = EncoderArguments::getRealPath("."); std::string exePath = EncoderArguments::getRealPath("."); exePath.append("/gameplay-encoder.exe"); const char* exe = exePath.c_str(); { const char* argv[] = {exe, "-g", "root", "movements", "C:\\Git\\gaming\\GamePlay\\gameplay-encoder\\res\\duck.fbx"}; EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv); assert(equals(args.getAnimationId("root"), ("movements"))); assert(equals(args.getGroupAnimationNodeId()[0], ("root"))); assert(equals(args.getOutputFilePath(), "C:/Git/gaming/GamePlay/gameplay-encoder/res/duck.gpb")); } { // Test with only input file name (relative) const char* argv[] = {exe, "input.fbx"}; EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv); assert(equals(args.getFilePath(), concat(dir, "/input.fbx"))); assert(equals(args.getOutputFilePath(), concat(dir, "/input.gpb"))); equals(args.getOutputDirPath(), dir); } { // Test specifying a relative output path const char* argv[] = {exe, "input.fbx", "output.gpb"}; EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv); assert(equals(args.getFilePath(), concat(dir, "/input.fbx"))); assert(equals(args.getOutputFilePath(), concat(dir, "/output.gpb"))); } { // Test specifying a relative output path const char* argv[] = {exe, "input.fbx", "output.gpb"}; EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv); assert(equals(args.getFilePath(), concat(dir, "/input.fbx"))); assert(equals(args.getOutputFilePath(), concat(dir, "/output.gpb"))); } { // Test specifying a relative output path to a directory const char* argv[] = {exe, "input.fbx", "stuff/output.gpb"}; EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv); assert(equals(args.getFilePath(), concat(dir, "/input.fbx"))); assert(equals(args.getOutputFilePath(), concat(dir, "/stuff/output.gpb"))); } { // Test parsing some arguments const char* argv[] = {exe, "test.fbx", "-t", "input.fbx", "output.gpb"}; EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv); assert(equals(args.getFilePath(), concat(dir, "/input.fbx"))); assert(equals(args.getOutputFilePath(), concat(dir, "/output.gpb"))); assert(args.textOutputEnabled()); } { // Test output file with no file extension const char* argv[] = {exe, "input.fbx", "output"}; EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv); assert(equals(args.getFilePath(), concat(dir, "/input.fbx"))); assert(equals(args.getOutputFilePath(), concat(dir, "/output.gpb"))); } { // Test output file with wrong file extension const char* argv[] = {exe, "input.fbx", "output.fbx"}; EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv); assert(equals(args.getFilePath(), concat(dir, "/input.fbx"))); assert(equals(args.getOutputFilePath(), concat(dir, "/output.gpb"))); } } }