diff --git a/README.md b/README.md index 72f1506..434a148 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,29 @@ +# Microsoft glTF Toolkit -# Contributing +This project contains a collection of tools and libraries to modify and optimize glTF assets. + +## Features + +The current release includes code for: +- Packing PBR material textures using [DirectXTex](http://github.com/Microsoft/DirectXTex) for use with the [MSFT_packing_occlusionRoughnessMetallic](https://github.com/sbtron/glTF/tree/MSFT_lod/extensions/Vendor/MSFT_packing_occlusionRoughnessMetallic) extension. +- Compressing textures as BC3, BC5 and BC7 and generate mip maps using [DirectXTex](http://github.com/Microsoft/DirectXTex) for use with the [MSFT_texture_dds](https://github.com/sbtron/glTF/tree/MSFT_lod/extensions/Vendor/MSFT_texture_dds) extension. +- Merging multiple glTF assets into a asset with multiple levels of detail using the [MSFT_lod](https://github.com/sbtron/glTF/tree/MSFT_lod/extensions/Vendor/MSFT_lod) extension. + +It also includes a command line tool that uses these steps in sequence in order to convert a glTF 2.0 core asset for use in the Windows Mixed Reality home, following the published [documentation](https://developer.microsoft.com/en-us/windows/mixed-reality/creating_3d_models_for_use_in_the_windows_mixed_reality_home). + +## Dependencies + +This project consumes the following projects through NuGet packages: +- Microsoft GLTF SDK, licensed under the MIT license +- [DirectXTex](http://github.com/Microsoft/DirectXTex), licensed under the MIT license +- [RapidJSON](https://github.com/Tencent/rapidjson/), licensed under the MIT license + + +## Building + +This project can be built using Visual Studio 2017 Update 4 on Windows 10 Fall Creators Update (16299.0). + +## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us @@ -12,3 +36,9 @@ provided by the bot. You will only need to do this once across all repos using o This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/WindowsMRAssetConverter/AssetType.cpp b/WindowsMRAssetConverter/AssetType.cpp new file mode 100644 index 0000000..2d8ff05 --- /dev/null +++ b/WindowsMRAssetConverter/AssetType.cpp @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "stdafx.h" +#include "AssetType.h" + +const wchar_t * EXTENSION_GLTF = L".gltf"; +const wchar_t * EXTENSION_GLB = L".glb"; + +AssetType AssetTypeUtils::AssetTypeFromFilePath(const std::wstring& assetPath) +{ + const wchar_t *inputExtensionRaw = nullptr; + if (FAILED(PathCchFindExtension(assetPath.c_str(), assetPath.length() + 1, &inputExtensionRaw))) + { + throw std::invalid_argument("Invalid input file extension."); + } + + if (_wcsicmp(inputExtensionRaw, EXTENSION_GLTF) == 0) + { + return AssetType::GLTF; + } + else if (_wcsicmp(inputExtensionRaw, EXTENSION_GLB) == 0) + { + return AssetType::GLB; + } + else + { + throw std::invalid_argument("Invalid file, please provide a GLTF or GLB."); + } +} \ No newline at end of file diff --git a/WindowsMRAssetConverter/AssetType.h b/WindowsMRAssetConverter/AssetType.h new file mode 100644 index 0000000..a67da45 --- /dev/null +++ b/WindowsMRAssetConverter/AssetType.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +extern const wchar_t * EXTENSION_GLTF; +extern const wchar_t * EXTENSION_GLB; + +enum class AssetType +{ + GLTF, + GLB +}; + +namespace AssetTypeUtils +{ + AssetType AssetTypeFromFilePath(const std::wstring& assetPath); +} diff --git a/WindowsMRAssetConverter/CommandLine.cpp b/WindowsMRAssetConverter/CommandLine.cpp new file mode 100644 index 0000000..96d83b1 --- /dev/null +++ b/WindowsMRAssetConverter/CommandLine.cpp @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "stdafx.h" +#include "CommandLine.h" +#include "FileSystem.h" + +// Constants +const wchar_t * PARAM_OUTFILE = L"-o"; +const wchar_t * PARAM_TMPDIR = L"-temp-directory"; +const wchar_t * PARAM_LOD = L"-lod"; +const wchar_t * PARAM_SCREENCOVERAGE = L"-screen-coverage"; +const wchar_t * PARAM_MAXTEXTURESIZE = L"-max-texture-size"; +const wchar_t * SUFFIX_CONVERTED = L"_converted"; +const wchar_t * CLI_INDENT = L" "; +const size_t MAXTEXTURESIZE_DEFAULT = 512; +const size_t MAXTEXTURESIZE_MAX = 4096; + +enum class CommandLineParsingState +{ + Initial, + InputRead, + ReadOutFile, + ReadTmpDir, + ReadLods, + ReadScreenCoverage, + ReadMaxTextureSize +}; + +void CommandLine::PrintHelp() +{ + auto indent = std::wstring(CLI_INDENT); + std::wcerr << std::endl + << L"Windows Mixed Reality Asset Converter" << std::endl + << L"=====================================" << std::endl + << std::endl + << L"A command line tool to convert core GLTF 2.0 assets for use in " + << L"the Windows Mixed Reality home, with the proper texture packing, compression and merged LODs." << std::endl << std::endl + << L"Usage: WindowsMRAssetConverter " << std::endl + << std::endl + << L"Optional arguments:" << std::endl + << indent << "[" << std::wstring(PARAM_OUTFILE) << L" ]" << std::endl + << indent << "[" << std::wstring(PARAM_TMPDIR) << L" ]" << std::endl + << indent << "[" << std::wstring(PARAM_LOD) << " ]" << std::endl + << indent << "[" << std::wstring(PARAM_SCREENCOVERAGE) << " ]" << std::endl + << indent << "[" << std::wstring(PARAM_MAXTEXTURESIZE) << " ]" << std::endl + << std::endl + << "Example:" << std::endl + << indent << "WindowsMRAssetConverter FileToConvert.gltf " + << std::wstring(PARAM_OUTFILE) << " ConvertedFile.glb " + << std::wstring(PARAM_LOD) << " Lod1.gltf Lod2.gltf " + << std::wstring(PARAM_SCREENCOVERAGE) << " 0.5 0.2 0.01" << std::endl + << std::endl + << "The above will convert \"FileToConvert.gltf\" into \"ConvertedFile.glb\" in the " + << "current directory." << std::endl + << std::endl + << "If the file is a GLB and the output name is not specified, defaults to the same name as input " + << "+ \"_converted.glb\"." << std::endl + << std::endl; +} + +void CommandLine::ParseCommandLineArguments( + int argc, wchar_t *argv[], + std::wstring& inputFilePath, AssetType& inputAssetType, std::wstring& outFilePath, std::wstring& tempDirectory, + std::vector& lodFilePaths, std::vector& screenCoveragePercentages, size_t& maxTextureSize) +{ + CommandLineParsingState state = CommandLineParsingState::Initial; + + inputFilePath = FileSystem::GetFullPath(std::wstring(argv[1])); + + inputAssetType = AssetTypeUtils::AssetTypeFromFilePath(inputFilePath); + + // Reset input parameters + outFilePath = L""; + tempDirectory = L""; + lodFilePaths.clear(); + screenCoveragePercentages.clear(); + maxTextureSize = MAXTEXTURESIZE_DEFAULT; + + state = CommandLineParsingState::InputRead; + + std::wstring outFile; + std::wstring tmpDir; + for (int i = 2; i < argc; i++) + { + std::wstring param = argv[i]; + + if (param == PARAM_OUTFILE) + { + outFile = L""; + state = CommandLineParsingState::ReadOutFile; + } + else if (param == PARAM_TMPDIR) + { + tmpDir = L""; + state = CommandLineParsingState::ReadTmpDir; + } + else if (param == PARAM_LOD) + { + lodFilePaths.clear(); + state = CommandLineParsingState::ReadLods; + } + else if (param == PARAM_SCREENCOVERAGE) + { + screenCoveragePercentages.clear(); + state = CommandLineParsingState::ReadScreenCoverage; + } + else if (param == PARAM_MAXTEXTURESIZE) + { + maxTextureSize = MAXTEXTURESIZE_DEFAULT; + state = CommandLineParsingState::ReadMaxTextureSize; + } + else + { + switch (state) + { + case CommandLineParsingState::ReadOutFile: + outFile = FileSystem::GetFullPath(param); + state = CommandLineParsingState::InputRead; + break; + case CommandLineParsingState::ReadTmpDir: + tmpDir = FileSystem::GetFullPath(param); + state = CommandLineParsingState::InputRead; + break; + case CommandLineParsingState::ReadLods: + lodFilePaths.push_back(FileSystem::GetFullPath(param)); + break; + case CommandLineParsingState::ReadScreenCoverage: + { + auto paramA = std::string(param.begin(), param.end()); + screenCoveragePercentages.push_back(std::atof(paramA.c_str())); + break; + } + case CommandLineParsingState::ReadMaxTextureSize: + maxTextureSize = std::min(static_cast(std::stoul(param.c_str())), MAXTEXTURESIZE_MAX); + break; + case CommandLineParsingState::Initial: + case CommandLineParsingState::InputRead: + default: + // Invalid argument detected + throw std::invalid_argument("Invalid usage. For help, try the command again without parameters."); + } + } + } + + if (outFile.empty()) + { + std::wstring inputFilePathWithoutExtension = inputFilePath; + if (FAILED(PathCchRemoveExtension(&inputFilePathWithoutExtension[0], inputFilePathWithoutExtension.length() + 1))) + { + throw std::invalid_argument("Invalid input file extension."); + } + + outFile = std::wstring(&inputFilePathWithoutExtension[0]); + + if (inputAssetType == AssetType::GLB) + { + outFile += SUFFIX_CONVERTED; + } + + outFile += EXTENSION_GLB; + } + + outFilePath = outFile; + + if (tmpDir.empty()) + { + tmpDir = FileSystem::CreateTempFolder(); + } + + tempDirectory = tmpDir; +} diff --git a/WindowsMRAssetConverter/CommandLine.h b/WindowsMRAssetConverter/CommandLine.h new file mode 100644 index 0000000..eed4c23 --- /dev/null +++ b/WindowsMRAssetConverter/CommandLine.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include +#include "AssetType.h" + +namespace CommandLine +{ + void PrintHelp(); + + void ParseCommandLineArguments( + int argc, wchar_t *argv[], + std::wstring& inputFilePath, AssetType& inputAssetType, std::wstring& outFilePath, std::wstring& tempDirectory, + std::vector& lodFilePaths, std::vector& screenCoveragePercentages, size_t& maxTextureSize); +}; + diff --git a/WindowsMRAssetConverter/FileSystem.cpp b/WindowsMRAssetConverter/FileSystem.cpp new file mode 100644 index 0000000..c3b0f32 --- /dev/null +++ b/WindowsMRAssetConverter/FileSystem.cpp @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "stdafx.h" +#include "FileSystem.h" + +std::wstring FileSystem::GetBasePath(const std::wstring& path) +{ + std::wstring pathCopy(path); + wchar_t *basePath = &pathCopy[0]; + if (FAILED(PathCchRemoveFileSpec(basePath, pathCopy.length() + 1))) + { + throw std::invalid_argument("Invalid input path."); + } + + return std::move(std::wstring(basePath)); +} + + +std::wstring FileSystem::GetFullPath(const std::wstring& path) +{ + wchar_t fullPath[MAX_PATH]; + if (GetFullPathName(path.c_str(), ARRAYSIZE(fullPath), fullPath, NULL) == 0) + { + throw std::invalid_argument("Invalid input file path."); + } + return std::move(std::wstring(fullPath)); +} + +std::wstring FileSystem::CreateSubFolder(const std::wstring& parentPath, const std::wstring& subFolderName) +{ + std::wstring errorMessageW = L"Could not create a sub-folder of " + parentPath + L"."; + std::string errorMessage(errorMessageW.begin(), errorMessageW.end()); + + wchar_t subFolderPath[MAX_PATH]; + if (FAILED(PathCchCombine(subFolderPath, ARRAYSIZE(subFolderPath), parentPath.c_str(), (subFolderName + L"\\").c_str()))) + { + throw std::runtime_error(errorMessage); + } + + if (CreateDirectory(subFolderPath, NULL) == 0 && GetLastError() != ERROR_ALREADY_EXISTS) + { + throw std::runtime_error(errorMessage); + } + + return std::move(std::wstring(subFolderPath)); +} + +std::wstring FileSystem::CreateTempFolder() +{ + std::wstring errorMessageW = L"Could not get a temporary folder. Try specifying one in the command line."; + std::string errorMessage(errorMessageW.begin(), errorMessageW.end()); + + wchar_t tmpDirRaw[MAX_PATH]; + auto returnValue = GetTempPath(MAX_PATH, tmpDirRaw); + if (returnValue > MAX_PATH || (returnValue == 0)) + { + throw std::runtime_error(errorMessage); + } + + // Get a random folder to drop the files + GUID guid = { 0 }; + if (FAILED(CoCreateGuid(&guid))) + { + throw std::runtime_error(errorMessage); + } + + wchar_t guidRaw[MAX_PATH]; + if (StringFromGUID2(guid, guidRaw, ARRAYSIZE(guidRaw)) == 0) + { + throw std::runtime_error(errorMessage); + } + + return std::move(CreateSubFolder(tmpDirRaw, guidRaw)); +} \ No newline at end of file diff --git a/WindowsMRAssetConverter/FileSystem.h b/WindowsMRAssetConverter/FileSystem.h new file mode 100644 index 0000000..6621748 --- /dev/null +++ b/WindowsMRAssetConverter/FileSystem.h @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +namespace FileSystem +{ + std::wstring GetBasePath(const std::wstring& path); + std::wstring GetFullPath(const std::wstring& path); + std::wstring CreateSubFolder(const std::wstring& parentPath, const std::wstring& subFolderName); + std::wstring CreateTempFolder(); +}; + diff --git a/WindowsMRAssetConverter/README.md b/WindowsMRAssetConverter/README.md new file mode 100644 index 0000000..3516f4c --- /dev/null +++ b/WindowsMRAssetConverter/README.md @@ -0,0 +1,50 @@ +# Windows Mixed Reality Asset Converter + +A command line tool to convert core GLTF 2.0 for use in the Windows Mixed Reality home, following the published [documentation](https://developer.microsoft.com/en-us/windows/mixed-reality/creating_3d_models_for_use_in_the_windows_mixed_reality_home). + +Note that this tool does not enforce any limits specified in the documentation (polygon count, texture size, etc.), so you might still encounter issues placing models if you do not conform to those limits. + +## Usage +WindowsMRAssetConverter _<path to GLTF/GLB>_ + +## Optional arguments +- `-o ` + - Specifies the output file name and directory for the output GLB + - If the file is a GLB and the output name is not specified, the tool defaults to the same name as input + "_converted.glb". + +- `-lod ` + - Specifies a list of assets that represent levels of detail, from higher to lower, that should be merged with the main asset and used as alternates when the asset is displayed from a distance (with limited screen coverage). + +- `-screen-coverage ` + - Specifies the maximum screen coverage values for each of the levels of detail, according to the [MSFT_lod](https://github.com/sbtron/glTF/tree/MSFT_lod/extensions/Vendor/MSFT_lod) extension specification. + +- `-temp-directory ` + - Allows overriding the temporary folder where intermediate files (packed/compressed textures, converted GLBs) will be placed. + +- `-max-texture-size ` + - Allows overriding the maximum texture dimension (width/height) when compressing textures. The recommended maximum dimension in the [documentation](https://developer.microsoft.com/en-us/windows/mixed-reality/creating_3d_models_for_use_in_the_windows_mixed_reality_home#texture_resolutions_and_workflow) is 512, and the allowed maximum is 4096. + +## Example +`WindowsMRAssetConverter FileToConvert.gltf -o ConvertedFile.glb -lod Lod1.gltf Lod2.gltf -screen-coverage 0.5 0.2 0.01` + +The above will convert _FileToConvert.gltf_ into _ConvertedFile.glb_ in the current directory. + +## Pipeline overview + +Each asset goes through the following steps when converting for compatibility with the Windows Mixed Reality home: + +1. **Conversion from GLB** - any GLB files are converted to loose glTF + assets, to simplify the code for reading resources +1. **Texture packing** - The textures that are relevant for the Windows MR home are packed according to the [documentation](https://developer.microsoft.com/en-us/windows/mixed-reality/creating_3d_models_for_use_in_the_windows_mixed_reality_home#materials) using the [MSFT\_packing\_occlusionRoughnessMetallic](https://github.com/sbtron/glTF/tree/MSFT_lod/extensions/Vendor/MSFT_packing_occlusionRoughnessMetallic) extension if necessary +1. **Texture compression** - All textures that are used in the Windows MR home must be compressed as DDS BC5 or BC7 according to the [documentation](https://developer.microsoft.com/en-us/windows/mixed-reality/creating_3d_models_for_use_in_the_windows_mixed_reality_home#materials). This step also generates mip maps for the textures, and resizes them down if necessary +1. **LOD merging** - All assets that represent levels of detail are merged into the main asset using the [MSFT_lod](https://github.com/sbtron/glTF/tree/MSFT_lod/extensions/Vendor/MSFT_lod) extension +1. **GLB export** - The resulting assets are exported as a GLB with all resources. As part of this step, accessors are modified to conform to the [glTF implementation notes in the documentation](https://developer.microsoft.com/en-us/windows/mixed-reality/creating_3d_models_for_use_in_the_windows_mixed_reality_home#gltf_implementation_notes): component types are converted to types supported by the Windows MR home, and the min and max values are calculated before serializing the accessors to the GLB + +## Additional resources + +- [Creating 3D models for use in the Windows Mixed Reality home](https://developer.microsoft.com/en-us/windows/mixed-reality/creating_3d_models_for_use_in_the_windows_mixed_reality_home) +- [Microsoft glTF LOD Extension Specification](https://github.com/sbtron/glTF/tree/MSFT_lod/extensions/Vendor/MSFT_lod) +- [PC Mixed Reality Texture Packing Extensions Specification](https://github.com/sbtron/glTF/tree/MSFT_lod/extensions/Vendor/MSFT_packing_occlusionRoughnessMetallic) +- [Microsoft DDS Textures glTF extensions specification](https://github.com/sbtron/glTF/tree/MSFT_lod/extensions/Vendor/MSFT_texture_dds) +- [Implementing 3D app launchers](https://developer.microsoft.com/en-us/windows/mixed-reality/implementing_3d_app_launchers) +- [Implementing 3D deep links for your app in the Windows Mixed Reality home](https://developer.microsoft.com/en-us/windows/mixed-reality/implementing_3d_deep_links_for_your_app_in_the_windows_mixed_reality_home) +- [Navigating the Windows Mixed Reality home](https://developer.microsoft.com/en-us/windows/mixed-reality/navigating_the_windows_mixed_reality_home) diff --git a/WindowsMRAssetConverter/WindowsMRAssetConverter.cpp b/WindowsMRAssetConverter/WindowsMRAssetConverter.cpp new file mode 100644 index 0000000..6d19f80 --- /dev/null +++ b/WindowsMRAssetConverter/WindowsMRAssetConverter.cpp @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "stdafx.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CommandLine.h" +#include "FileSystem.h" + +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +class GLTFStreamReader : public IStreamReader +{ +public: + GLTFStreamReader(std::wstring uriBase) : m_uriBase(uriBase) {} + + virtual ~GLTFStreamReader() override {} + virtual std::shared_ptr GetInputStream(const std::string& filename) const override + { + std::wstring filenameW = std::wstring(filename.begin(), filename.end()); + + wchar_t uriAbsoluteRaw[MAX_PATH]; + // Note: PathCchCombine will return the last argument if it's an absolute path + if (FAILED(::PathCchCombine(uriAbsoluteRaw, ARRAYSIZE(uriAbsoluteRaw), m_uriBase.c_str(), filenameW.c_str()))) + { + throw std::invalid_argument("Could not get the base path for the GLTF resources. Try specifying the full path."); + } + + return std::make_shared(uriAbsoluteRaw, std::ios::binary); + } +private: + const std::wstring m_uriBase; +}; + +class GLBStreamFactory : public Microsoft::glTF::IStreamFactory +{ +public: + GLBStreamFactory(const std::wstring& filename) : + m_stream(std::make_shared(filename, std::ios_base::binary | std::ios_base::out)), + m_tempStream(std::make_shared(std::ios_base::binary | std::ios_base::in | std::ios_base::out)) + { } + + std::shared_ptr GetInputStream(const std::string&) const override + { + throw std::logic_error("Not implemented"); + } + + std::shared_ptr GetOutputStream(const std::string&) const override + { + return m_stream; + } + + std::shared_ptr GetTemporaryStream(const std::string&) const override + { + return m_tempStream; + } +private: + std::shared_ptr m_stream; + std::shared_ptr m_tempStream; +}; + +GLTFDocument LoadAndConvertDocumentForWindowsMR( + std::wstring& inputFilePath, + AssetType inputAssetType, + const std::wstring& tempDirectory, + size_t maxTextureSize) +{ + // Load the document + std::wstring inputFileName = PathFindFileName(inputFilePath.c_str()); + std::wcout << L"Loading input document: " << inputFileName << L"..." << std::endl; + + if (inputAssetType == AssetType::GLB) + { + // Convert the GLB to GLTF in the temp directory + + std::string inputFilePathA(inputFilePath.begin(), inputFilePath.end()); + std::string tempDirectoryA(tempDirectory.begin(), tempDirectory.end()); + + wchar_t *inputFileNameRaw = &inputFileName[0]; + PathRemoveExtension(inputFileNameRaw); + + // inputGltfName is the path to the converted GLTF without extension + std::wstring inputGltfName = inputFileNameRaw; + std::string inputGltfNameA = std::string(inputGltfName.begin(), inputGltfName.end()); + + GLBToGLTF::UnpackGLB(inputFilePathA, tempDirectoryA, inputGltfNameA); + + inputFilePath = tempDirectory + inputGltfName + EXTENSION_GLTF; + } + + auto stream = std::make_shared(inputFilePath, std::ios::binary); + GLTFDocument document = DeserializeJson(*stream); + + // Get the base path from where to read all the assets + + GLTFStreamReader streamReader(FileSystem::GetBasePath(inputFilePath)); + + std::wcout << L"Packing textures..." << std::endl; + + // 1. Texture Packing + auto tempDirectoryA = std::string(tempDirectory.begin(), tempDirectory.end()); + document = GLTFTexturePackingUtils::PackAllMaterialsForWindowsMR(streamReader, document, TexturePacking::RoughnessMetallicOcclusion, tempDirectoryA); + + std::wcout << L"Compressing textures - this can take a few minutes..." << std::endl; + + // 2. Texture Compression + document = GLTFTextureCompressionUtils::CompressAllTexturesForWindowsMR(streamReader, document, tempDirectoryA, maxTextureSize); + + return document; +} + +int wmain(int argc, wchar_t *argv[]) +{ + if (argc < 2) + { + CommandLine::PrintHelp(); + return 0; + } + + // Initialize COM + CoInitialize(NULL); + + try + { + // Arguments + std::wstring inputFilePath; + AssetType inputAssetType; + std::wstring outFilePath; + std::wstring tempDirectory; + std::vector lodFilePaths; + std::vector screenCoveragePercentages; + size_t maxTextureSize; + + CommandLine::ParseCommandLineArguments(argc, argv, inputFilePath, inputAssetType, outFilePath, tempDirectory, lodFilePaths, screenCoveragePercentages, maxTextureSize); + + // Load document, and perform steps: + // 1. Texture Packing + // 2. Texture Compression + auto document = LoadAndConvertDocumentForWindowsMR(inputFilePath, inputAssetType, tempDirectory, maxTextureSize); + + // 3. LOD Merging + if (lodFilePaths.size() > 0) + { + std::wcout << L"Merging LODs..." << std::endl; + + std::vector lodDocuments; + lodDocuments.push_back(document); + + for (size_t i = 0; i < lodFilePaths.size(); i++) + { + // Apply the same optimizations for each LOD + auto lod = lodFilePaths[i]; + auto subFolder = FileSystem::CreateSubFolder(tempDirectory, L"lod" + std::to_wstring(i + 1)); + + lodDocuments.push_back(LoadAndConvertDocumentForWindowsMR(lod, AssetTypeUtils::AssetTypeFromFilePath(lod), subFolder, maxTextureSize)); + } + + // TODO: LOD assets can be in different places in disk, so the merged document will not have + // the right relative paths to resources. We must either compute the correct relative paths or embed + // all resources as base64 in the source document, otherwise the export to GLB will fail. + document = GLTFLODUtils::MergeDocumentsAsLODs(lodDocuments, screenCoveragePercentages); + } + + // 4. GLB Export + std::wcout << L"Exporting as GLB..." << std::endl; + + // The Windows MR Fall Creators update has restrictions on the supported + // component types of accessors. + AccessorConversionStrategy accessorConversion = [](const Accessor& accessor) + { + if (accessor.type == AccessorType::TYPE_SCALAR) + { + switch (accessor.componentType) + { + case ComponentType::COMPONENT_BYTE: + case ComponentType::COMPONENT_UNSIGNED_BYTE: + case ComponentType::COMPONENT_SHORT: + return ComponentType::COMPONENT_UNSIGNED_SHORT; + default: + return accessor.componentType; + } + } + else if (accessor.type == AccessorType::TYPE_VEC2 || accessor.type == AccessorType::TYPE_VEC3) + { + return ComponentType::COMPONENT_FLOAT; + } + + return accessor.componentType; + }; + + GLTFStreamReader streamReader(FileSystem::GetBasePath(inputFilePath)); + std::unique_ptr streamFactory = std::make_unique(outFilePath); + SerializeBinary(document, streamReader, streamFactory, accessorConversion); + + std::wcout << L"Done!" << std::endl; + std::wcout << L"Output file: " << outFilePath << std::endl; + } + catch (std::exception ex) + { + std::cerr << ex.what() << std::endl; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/WindowsMRAssetConverter/WindowsMRAssetConverter.vcxproj b/WindowsMRAssetConverter/WindowsMRAssetConverter.vcxproj new file mode 100644 index 0000000..323abcc --- /dev/null +++ b/WindowsMRAssetConverter/WindowsMRAssetConverter.vcxproj @@ -0,0 +1,220 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {8A19D99C-78DC-4267-AB57-DB1DDBFBEFDF} + Win32Proj + WindowsMRAssetConverter + 10.0.16299.0 + + + + Application + true + v141 + Unicode + + + Application + false + v141 + true + Unicode + + + Application + true + v141 + Unicode + + + Application + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + true + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + false + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + false + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + + Use + Level4 + Disabled + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(SolutionDir)glTF-Toolkit\inc + stdcpp17 + /permissive- %(AdditionalOptions) + true + 4996 + + + Console + true + d3d11.lib;dxgi.lib;pathcch.lib;shlwapi.lib;%(AdditionalDependencies) + + + + + Use + Level4 + Disabled + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(SolutionDir)glTF-Toolkit\inc + stdcpp17 + /permissive- %(AdditionalOptions) + true + 4996 + + + Console + true + d3d11.lib;dxgi.lib;pathcch.lib;shlwapi.lib;%(AdditionalDependencies) + + + + + Use + Level4 + MaxSpeed + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(SolutionDir)glTF-Toolkit\inc + stdcpp17 + /permissive- %(AdditionalOptions) + true + 4996 + + + Console + true + true + true + d3d11.lib;dxgi.lib;pathcch.lib;shlwapi.lib;%(AdditionalDependencies) + + + + + Use + Level4 + MaxSpeed + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(SolutionDir)glTF-Toolkit\inc + stdcpp17 + /permissive- %(AdditionalOptions) + true + 4996 + + + Console + true + true + true + d3d11.lib;dxgi.lib;pathcch.lib;shlwapi.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + {ff0275f1-58cb-4745-ba81-f6c1df66e206} + false + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/WindowsMRAssetConverter/WindowsMRAssetConverter.vcxproj.filters b/WindowsMRAssetConverter/WindowsMRAssetConverter.vcxproj.filters new file mode 100644 index 0000000..f173604 --- /dev/null +++ b/WindowsMRAssetConverter/WindowsMRAssetConverter.vcxproj.filters @@ -0,0 +1,55 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + + + \ No newline at end of file diff --git a/WindowsMRAssetConverter/packages.config b/WindowsMRAssetConverter/packages.config new file mode 100644 index 0000000..b98bb7f --- /dev/null +++ b/WindowsMRAssetConverter/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/WindowsMRAssetConverter/stdafx.cpp b/WindowsMRAssetConverter/stdafx.cpp new file mode 100644 index 0000000..83328c0 --- /dev/null +++ b/WindowsMRAssetConverter/stdafx.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "stdafx.h" \ No newline at end of file diff --git a/WindowsMRAssetConverter/stdafx.h b/WindowsMRAssetConverter/stdafx.h new file mode 100644 index 0000000..826e830 --- /dev/null +++ b/WindowsMRAssetConverter/stdafx.h @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +// Use the C++ standard templated min/max +#define NOMINMAX + +// DirectX apps don't need GDI +#define NODRAWTEXT +#define NOGDI +#define NOBITMAP + +// Include if you need this +#define NOMCX + +// Include if you need this +#define NOSERVICE + +// WinHelp is deprecated +#define NOHELP + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include \ No newline at end of file diff --git a/WindowsMRAssetConverter/targetver.h b/WindowsMRAssetConverter/targetver.h new file mode 100644 index 0000000..a628652 --- /dev/null +++ b/WindowsMRAssetConverter/targetver.h @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/glTF-Toolkit.Test/GLBSerializerTests.cpp b/glTF-Toolkit.Test/GLBSerializerTests.cpp new file mode 100644 index 0000000..6981bcf --- /dev/null +++ b/glTF-Toolkit.Test/GLBSerializerTests.cpp @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" +#include + +#include "GLTFSDK/IStreamWriter.h" +#include "GLTFSDK/GLTFConstants.h" +#include "GLTFSDK/Deserialize.h" +#include "GLTFSDK/GLBResourceWriter.h" +#include "GLTFSDK/GLBResourceReader.h" +#include "GLTFSDK/GLTFResourceWriter.h" + +#include "SerializeBinary.h" + +#include "Helpers/TestUtils.h" +#include "Helpers/WStringUtils.h" +#include "Helpers/StreamMock.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +namespace Microsoft::glTF::Toolkit::Test +{ + class InMemoryStreamFactory : public Microsoft::glTF::IStreamFactory + { + public: + InMemoryStreamFactory(std::shared_ptr stream) : + m_stream(stream), + m_tempStream(std::make_shared(std::ios_base::app | std::ios_base::binary | std::ios_base::in | std::ios_base::out)) + { } + + std::shared_ptr GetInputStream(const std::string& uri) const override + { + return uri == GLB_BUFFER_ID ? m_tempStream : m_stream; + } + + std::shared_ptr GetOutputStream(const std::string& uri) const override + { + return uri == GLB_BUFFER_ID ? m_tempStream : m_stream; + } + + std::shared_ptr GetTemporaryStream(const std::string&) const override + { + return m_tempStream; + } + private: + std::shared_ptr m_stream; + std::shared_ptr m_tempStream; + }; + + TEST_CLASS(GLBSerializerTests) + { + + static std::string ReadLocalJson(const char * relativePath) + { + auto input = TestUtils::ReadLocalAsset(TestUtils::GetAbsolutePath(relativePath)); + auto json = std::string(std::istreambuf_iterator(*input), std::istreambuf_iterator()); + return json; + } + + static std::shared_ptr ImportGLB(const std::shared_ptr& streamReader, const std::shared_ptr& glbStream) + { + GLBResourceReader resourceReader(*streamReader, glbStream); + auto json = resourceReader.GetJson(); + + auto doc = DeserializeJson(json); + + return std::make_shared(doc); + } + + static std::shared_ptr ImportGLTF(const std::shared_ptr& streamReader, const std::shared_ptr& stream) + { + GLTFResourceReader resourceReader(*streamReader); + auto json = std::string(std::istreambuf_iterator(*stream), std::istreambuf_iterator()); + + auto doc = DeserializeJson(json); + + return std::make_shared(doc); + } + + const char* c_waterBottleJson = "Resources\\gltf\\WaterBottle\\WaterBottle.gltf"; + + TEST_METHOD(GLBSerializerTests_RoundTrip_Simple) + { + auto data = ReadLocalJson(c_waterBottleJson); + auto input = std::make_shared(data); + try + { + // Deserialize input json + auto inputJson = std::string(std::istreambuf_iterator(*input), std::istreambuf_iterator()); + auto doc = DeserializeJson(inputJson); + + // Serialize GLTFDocument to GLB + TestStreamReader streamReader(TestUtils::GetAbsolutePath(c_waterBottleJson)); + auto stream = std::make_shared(std::ios_base::app | std::ios_base::binary | std::ios_base::in | std::ios_base::out); + std::unique_ptr streamFactory = std::make_unique(stream); + SerializeBinary(doc, streamReader, streamFactory); + + // Deserialize the GLB again + auto glbReader = std::make_unique(streamReader, stream); + auto outputJson = glbReader->GetJson(); + auto outputDoc = DeserializeJson(outputJson); + + // Check some structural elements + Assert::AreEqual(doc.nodes.Size(), outputDoc.nodes.Size()); + Assert::AreEqual(doc.images.Size(), outputDoc.images.Size()); + + // There must be only one buffer, and it can't have a URI + Assert::AreEqual(static_cast(1), outputDoc.buffers.Size()); + auto glbBuffer = outputDoc.buffers.Elements()[0]; + Assert::IsTrue(glbBuffer.uri.empty()); + + // Check that the images that were stored as URI are now bufferViews + for (auto image : outputDoc.images.Elements()) + { + // Images in GLB don't have a URI + Assert::IsTrue(image.uri.empty()); + + // Images in GLB are stored in a buffer + Assert::IsFalse(image.bufferViewId.empty()); + + // Images in original GLTF have a URI + Assert::IsFalse(doc.images.Get(image.id).uri.empty()); + } + + // All buffer views must point to the GLB buffer + for (auto bufferView : outputDoc.bufferViews.Elements()) + { + Assert::IsTrue(bufferView.bufferId == glbBuffer.id); + } + + // Read one of the images and check it's identical + auto gltfReader = std::make_unique(streamReader); + std::vector gltfImage = gltfReader->ReadBinaryData(doc, doc.images.Elements()[0]); + std::vector glbImage = glbReader->ReadBinaryData(outputDoc, outputDoc.images.Elements()[0]); + Assert::IsTrue(gltfImage == glbImage); // Vector comparison + } + catch (std::exception ex) + { + std::stringstream ss; + ss << "Received exception was unexpected. Got: " << ex.what(); + Assert::Fail(WStringUtils::ToWString(ss).c_str()); + } + } + }; +} \ No newline at end of file diff --git a/glTF-Toolkit.Test/GLBtoGLTFTests.cpp b/glTF-Toolkit.Test/GLBtoGLTFTests.cpp new file mode 100644 index 0000000..dd72477 --- /dev/null +++ b/glTF-Toolkit.Test/GLBtoGLTFTests.cpp @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" +#include + +#include +#include +#include +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +namespace Microsoft::glTF::Toolkit::Test +{ + std::wstring utf8Decode(const std::string& s) + { + std::wstring ret; + std::for_each(s.begin(), s.end(), [&ret](char c) + { + ret += (wchar_t)c; + }); + return ret; + } + + std::string binBufferString(const std::vector& vec) + { + std::string ret = "{"; + for (size_t i = 0; i < vec.size(); i++) + { + ret += (i ? "," : "") + std::to_string((int)vec[i]); + } + return ret += "}"; + } + + // Setup a GLTF document with 3 bufferviews and 2 images + GLTFDocument setupGLBDocument1() + { + GLTFDocument glbDoc("0", {}); + Scene sc; sc.id = "0"; + glbDoc.scenes.Append(std::move(sc)); + Accessor acc0; acc0.bufferViewId = "0"; acc0.byteOffset = 0; acc0.id = "0"; + Accessor acc1; acc1.bufferViewId = "2"; acc1.byteOffset = 12; acc1.id = "1"; + Accessor acc2; acc2.bufferViewId = "1"; acc2.byteOffset = 4; acc2.id = "2"; + const std::vector accessors = { acc0, acc1, acc2 }; + std::for_each(accessors.begin(), accessors.end(), [&glbDoc](auto a) + { + glbDoc.accessors.Append(std::move(a)); + }); + BufferView bv0; bv0.bufferId = "0"; bv0.byteOffset = 0; bv0.byteLength = 8; bv0.id = "0"; + BufferView bv1; bv1.bufferId = "0"; bv1.byteOffset = 32; bv1.byteLength = 4; bv1.id = "1"; + BufferView bv2; bv2.bufferId = "0"; bv2.byteOffset = 72; bv2.byteLength = 2; bv2.id = "2"; + const std::vector bufferViews = { bv0, bv1, bv2 }; + std::for_each(bufferViews.begin(), bufferViews.end(), [&glbDoc](auto b) + { + glbDoc.bufferViews.Append(std::move(b)); + }); + Buffer b; b.id = "0", b.byteLength = 100; + const std::vector buffers = { b }; + std::for_each(buffers.begin(), buffers.end(), [&glbDoc](auto b) + { + glbDoc.buffers.Append(std::move(b)); + }); + Image img0; img0.id = "0"; img0.mimeType = "image/png"; img0.bufferViewId = "1"; + Image img1; img1.id = "1"; img1.mimeType = "image/jpeg"; img1.bufferViewId = "2"; + const std::vector images = { img0, img1 }; + std::for_each(images.begin(), images.end(), [&glbDoc](auto img) + { + glbDoc.images.Append(std::move(img)); + }); + return glbDoc; + } + + //sets up a stream with 'size' number of bytes, where reading the stream k times returns k + std::stringstream* setupGLBStream(char size) + { + std::stringstream* ss = new std::stringstream(); + char* inputStream = new char[size]; + for (int i = 0; i < size; i++) inputStream[i] = (char)i; + ss->write(inputStream, size); + return ss; + } + + TEST_CLASS(GLBToGLTFTests) + { + TEST_METHOD(GLBtoGLTF_NoImagesJSON) + { + GLTFDocument glbDoc("0", {}); + Scene s1; s1.id = "0"; + glbDoc.scenes.Append(std::move(s1)); + Accessor acc; acc.bufferViewId = "0"; acc.byteOffset = 36; acc.id = "0"; + const std::vector accessors = {}; + std::for_each(accessors.begin(), accessors.end(), [&glbDoc](auto a) + { + glbDoc.accessors.Append(std::move(a)); + }); + const std::vector bufferViews = {}; + std::for_each(bufferViews.begin(), bufferViews.end(), [&glbDoc](auto b) + { + glbDoc.bufferViews.Append(std::move(b)); + }); + const std::vector buffers = {}; + std::for_each(buffers.begin(), buffers.end(), [&glbDoc](auto b) + { + glbDoc.buffers.Append(std::move(b)); + }); + const std::vector images = {}; + std::for_each(images.begin(), images.end(), [&glbDoc](auto img) + { + glbDoc.images.Append(std::move(img)); + }); + GLTFDocument expectedGLTFDoc("0", {}); + Scene s2; s2.id = "0"; + expectedGLTFDoc.scenes.Append(std::move(s2)); + auto actualGLTFDoc = GLBToGLTF::CreateGLTFDocument(glbDoc, "name"); + + // for debugging + const auto expectedJSON = Serialize(expectedGLTFDoc, SerializeFlags::Pretty); + const auto actualJSON = Serialize(actualGLTFDoc, SerializeFlags::Pretty); + Assert::IsTrue(expectedGLTFDoc == actualGLTFDoc, utf8Decode(expectedJSON + "\n\n" + actualJSON).c_str()); + } + + TEST_METHOD(GLBtoGLTF_ImagesWithOffsetJSON) + { + GLTFDocument glbDoc("0", {}); + Scene sc; sc.id = "0"; + glbDoc.scenes.Append(std::move(sc)); + Accessor acc0; acc0.bufferViewId = "0"; acc0.byteOffset = 0; acc0.id = "0"; + Accessor acc1; acc1.bufferViewId = "3"; acc1.byteOffset = 12; acc1.id = "1"; + Accessor acc2; acc2.bufferViewId = "1"; acc2.byteOffset = 4; acc2.id = "2"; + Accessor acc3; acc3.bufferViewId = "2"; acc3.byteOffset = 4; acc3.id = "3"; + const std::vector accessors = { acc0, acc1, acc2, acc3 }; + std::for_each(accessors.begin(), accessors.end(), [&glbDoc](auto a) + { + glbDoc.accessors.Append(std::move(a)); + }); + BufferView bv0; bv0.bufferId = "0"; bv0.byteOffset = 0; bv0.byteLength = 400; bv0.id = "0"; + BufferView bv1; bv1.bufferId = "0"; bv1.byteOffset = 420; bv1.byteLength = 200; bv1.id = "1"; + BufferView bv2; bv2.bufferId = "0"; bv2.byteOffset = 620; bv2.byteLength = 320; bv2.id = "2"; + BufferView bv3; bv3.bufferId = "0"; bv3.byteOffset = 960; bv3.byteLength = 2000; bv3.id = "3"; + const std::vector bufferViews = { bv0, bv1, bv2, bv3 }; + std::for_each(bufferViews.begin(), bufferViews.end(), [&glbDoc](auto b) + { + glbDoc.bufferViews.Append(std::move(b)); + }); + Buffer b; b.id = "0", b.byteLength = 3000; + const std::vector buffers = { b }; + std::for_each(buffers.begin(), buffers.end(), [&glbDoc](auto b) + { + glbDoc.buffers.Append(std::move(b)); + }); + Image img0; img0.id = "0"; img0.mimeType = "image/png"; img0.bufferViewId = "1"; + Image img1; img1.id = "1"; img1.mimeType = "image/jpeg"; img1.bufferViewId = "3"; + const std::vector images = { img0, img1 }; + std::for_each(images.begin(), images.end(), [&glbDoc](auto img) + { + glbDoc.images.Append(std::move(img)); + }); + auto actualGLTFDoc = GLBToGLTF::CreateGLTFDocument(glbDoc, "test"); + + GLTFDocument expectedGLTFDoc("0", {}); + Accessor exp_acc0; exp_acc0.bufferViewId = "0"; exp_acc0.byteOffset = 0; exp_acc0.id = "0"; + Accessor exp_acc1; exp_acc1.bufferViewId = "1"; exp_acc1.byteOffset = 4; exp_acc1.id = "3"; + expectedGLTFDoc.accessors.Append(std::move(exp_acc0)); + expectedGLTFDoc.accessors.Append(std::move(exp_acc1)); + + BufferView exp_bv0; exp_bv0.bufferId = "0"; exp_bv0.byteOffset = 0; exp_bv0.byteLength = 400; exp_bv0.id = "0"; + BufferView exp_bv1; exp_bv1.bufferId = "0"; exp_bv1.byteOffset = 400; exp_bv1.byteLength = 320; exp_bv1.id = "1"; + expectedGLTFDoc.bufferViews.Append(std::move(exp_bv0)); + expectedGLTFDoc.bufferViews.Append(std::move(exp_bv1)); + + Image exp_img0; exp_img0.id = "0"; exp_img0.uri = "test_image0.png"; + Image exp_img1; exp_img1.id = "1"; exp_img1.uri = "test_image1.jpg"; + expectedGLTFDoc.images.Append(std::move(exp_img0)); + expectedGLTFDoc.images.Append(std::move(exp_img1)); + + Buffer exp_b; exp_b.id = "0"; exp_b.byteLength = 720; exp_b.uri = "test.bin"; + expectedGLTFDoc.buffers.Append(std::move(exp_b)); + + Scene sc2; sc2.id = "0"; + expectedGLTFDoc.scenes.Append(std::move(sc2)); + + // for debugging + const auto expectedJSON = Serialize(expectedGLTFDoc, SerializeFlags::Pretty); + const auto actualJSON = Serialize(actualGLTFDoc, SerializeFlags::Pretty); + Assert::IsTrue(expectedGLTFDoc == actualGLTFDoc, utf8Decode(expectedJSON + "\n\n" + actualJSON).c_str()); + } + + TEST_METHOD(GLBtoGLTF_ImageDataTest) + { + auto glbDoc = setupGLBDocument1(); + auto glbStream = setupGLBStream(100); + const std::string TEST_NAME = "test3"; + const size_t BYTE_OFFSET = 12; + auto imageData = GLBToGLTF::GetImagesData(glbStream, glbDoc, TEST_NAME, BYTE_OFFSET); + delete glbStream; + + // these bytes corresponds to bytes of image0 and image1 in setupGLBDocument1 + std::vector> expectedImage = { + {BYTE_OFFSET + 32, BYTE_OFFSET + 33, BYTE_OFFSET + 34, BYTE_OFFSET + 35}, + {BYTE_OFFSET + 72, BYTE_OFFSET + 73}, + }; + + int imgId = 0; + for (auto it = imageData.begin(); it != imageData.end(); it++, imgId++) + { + Assert::IsTrue(it->second == expectedImage[imgId], utf8Decode(binBufferString(it->second) + '\n' + binBufferString(expectedImage[imgId])).c_str()); + } + } + + TEST_METHOD(GLBtoGLTF_MeshDataTest) + { + auto glbDoc = setupGLBDocument1(); + auto glbStream = setupGLBStream(100); + const size_t BYTE_OFFSET = 12; + auto actualData = GLBToGLTF::SaveBin(glbStream, glbDoc, BYTE_OFFSET, 8); + + //these bytes correspond to bytes of bufferviews in steupGLTFDocument1 which don't belong to any image + std::vector expectedData = { BYTE_OFFSET + 0, BYTE_OFFSET + 1, BYTE_OFFSET + 2, BYTE_OFFSET + 3, + BYTE_OFFSET + 4, BYTE_OFFSET + 5, BYTE_OFFSET + 6, BYTE_OFFSET + 7 }; + Assert::IsTrue(actualData == expectedData, utf8Decode(binBufferString(actualData) + '\n' + binBufferString(expectedData)).c_str()); + } + }; +} \ No newline at end of file diff --git a/glTF-Toolkit.Test/GLTFLODUtilsTests.cpp b/glTF-Toolkit.Test/GLTFLODUtilsTests.cpp new file mode 100644 index 0000000..9fc4213 --- /dev/null +++ b/glTF-Toolkit.Test/GLTFLODUtilsTests.cpp @@ -0,0 +1,326 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" +#include + +#include "GLTFSDK/IStreamWriter.h" +#include "GLTFSDK/GLTFConstants.h" +#include "GLTFSDK/Serialize.h" +#include "GLTFSDK/Deserialize.h" +#include "GLTFSDK/GLBResourceReader.h" +#include "GLTFSDK/GLTFResourceWriter.h" +#include "GLTFSDK/RapidJsonUtils.h" + +#include "GLTFLODUtils.h" + +#include "Helpers/WStringUtils.h" +#include "Helpers/TestUtils.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +namespace Microsoft::glTF::Toolkit::Test +{ + TEST_CLASS(GLTFLODUtilsTests) + { + static void CheckGLTFLODNodeCountAgainstOriginal(GLTFDocument& doc, GLTFDocument& docWLod, size_t lodCount) + { + // All elements in the lod'd doc should be double the original + Assert::IsTrue(doc.buffers.Size() * lodCount == docWLod.buffers.Size()); + Assert::IsTrue(doc.accessors.Size() * lodCount == docWLod.accessors.Size()); + Assert::IsTrue(doc.bufferViews.Size() * lodCount == docWLod.bufferViews.Size()); + Assert::IsTrue(doc.materials.Size() * lodCount == docWLod.materials.Size()); + Assert::IsTrue(doc.images.Size() * lodCount == docWLod.images.Size()); + Assert::IsTrue(doc.meshes.Size() * lodCount == docWLod.meshes.Size()); + Assert::IsTrue(doc.nodes.Size() * lodCount == docWLod.nodes.Size()); + Assert::IsTrue(doc.textures.Size() * lodCount == docWLod.textures.Size()); + Assert::IsTrue(doc.samplers.Size() * lodCount == docWLod.samplers.Size()); + + // Scene count should be untouched + Assert::IsTrue(doc.scenes.Size() == docWLod.scenes.Size()); + } + + static void CheckGLTFLODCount(const char * gltfDocPath, uint32_t expectedNumberOfLods) + { + auto input = TestUtils::ReadLocalAsset(TestUtils::GetAbsolutePath(gltfDocPath)); + auto readwriter = std::make_shared(); + try + { + GLTFResourceReader resourceReader(*readwriter); + auto inputJson = std::string(std::istreambuf_iterator(*input), std::istreambuf_iterator()); + auto doc = DeserializeJson(inputJson); + auto lods = GLTFLODUtils::ParseDocumentNodeLODs(doc); + + Assert::IsTrue(GLTFLODUtils::NumberOfNodeLODLevels(doc, lods) == expectedNumberOfLods); + } + catch (std::exception ex) + { + std::stringstream ss; + ss << "Received exception was unexpected. Got: " << ex.what(); + Assert::Fail(WStringUtils::ToWString(ss).c_str()); + } + } + + static std::shared_ptr ImportGLTF(const std::shared_ptr& streamReader, const std::shared_ptr& stream) + { + GLTFResourceReader resourceReader(*streamReader); + auto json = std::string(std::istreambuf_iterator(*stream), std::istreambuf_iterator()); + + auto doc = DeserializeJson(json); + + return std::make_shared(doc); + } + + const char* c_cubeAsset3DJson = "Resources\\gltf\\cubeAsset3D.gltf"; + const char* c_cubeWithLODJson = "Resources\\gltf\\cubeWithLOD.gltf"; + + TEST_METHOD(GLTFLODUtils_NodeLodCount) + { + CheckGLTFLODCount(c_cubeAsset3DJson, 0); + } + TEST_METHOD(GLTFLODUtils_NodeLodCount_DocWithLODs) + { + CheckGLTFLODCount(c_cubeWithLODJson, 1); + } + + TEST_METHOD(GLTFLODUtils_GLTFNodeLODMerge) + { + auto input = TestUtils::ReadLocalAsset(TestUtils::GetAbsolutePath(c_cubeAsset3DJson)); + auto readwriter = std::make_shared(); + try + { + // Deserialize input json + GLTFResourceReader resourceReader(*readwriter); + auto inputJson = std::string(std::istreambuf_iterator(*input), std::istreambuf_iterator()); + auto doc = DeserializeJson(inputJson); + + std::vector docs; + docs.push_back(doc); + docs.push_back(doc); + + auto newlodgltfDoc = GLTFLODUtils::MergeDocumentsAsLODs(docs); + + // Serialize GLTFDocument back to json + auto outputJson = Serialize(newlodgltfDoc); + + CheckGLTFLODNodeCountAgainstOriginal(doc, newlodgltfDoc, 2); + + // Check Node Lods are correctly stored and labelled in the document + auto nodes = newlodgltfDoc.nodes.Elements(); + auto lods = GLTFLODUtils::ParseDocumentNodeLODs(newlodgltfDoc); + bool validLodExtension = false; + bool containsLOD1RootNode = false; + bool containsLOD1PolyNode = false; + for (auto node : nodes) + { + if (node.name == "root" && (std::find(lods[node.id]->begin(), lods[node.id]->end(), "3") != lods[node.id]->end())) + { + validLodExtension = true; + } + if (node.name == "root_lod1") + { + containsLOD1RootNode = true; + } + if (node.name == "polygon_lod1") + { + containsLOD1PolyNode = true; + } + } + Assert::IsTrue(validLodExtension); + Assert::IsTrue(containsLOD1RootNode); + Assert::IsTrue(containsLOD1PolyNode); + } + catch (std::exception ex) + { + std::stringstream ss; + ss << "Received exception was unexpected. Got: " << ex.what(); + Assert::Fail(WStringUtils::ToWString(ss).c_str()); + } + } + + TEST_METHOD(GLTFLODUTils_GLTFNodeLODMergeMultiple) + { + auto input = TestUtils::ReadLocalAsset(TestUtils::GetAbsolutePath(c_cubeAsset3DJson)); + auto readwriter = std::make_shared(); + try + { + // Deserialize input json + GLTFResourceReader resourceReader(*readwriter); + auto inputJson = std::string(std::istreambuf_iterator(*input), std::istreambuf_iterator()); + auto doc = DeserializeJson(inputJson); + + std::vector docs; + docs.push_back(doc); + docs.push_back(doc); + docs.push_back(doc); + + auto newlodgltfDoc = GLTFLODUtils::MergeDocumentsAsLODs(docs); + + CheckGLTFLODNodeCountAgainstOriginal(doc, newlodgltfDoc, 3); + + // Check Node Lods are correctly stored and labelled in the document + auto nodes = newlodgltfDoc.nodes.Elements(); + auto lods = GLTFLODUtils::ParseDocumentNodeLODs(newlodgltfDoc); + bool validLodExtension = false; + bool containsLOD1RootNode = false; + bool containsLOD1PolyNode = false; + bool containsLOD2RootNode = false; + bool containsLOD2PolyNode = false; + for (auto node : nodes) + { + if (node.name == "root" && + (std::find(lods[node.id]->begin(), lods[node.id]->end(), "3") != lods[node.id]->end()) && + (std::find(lods[node.id]->begin(), lods[node.id]->end(), "5") != lods[node.id]->end()) + ) + { + validLodExtension = true; + } + if (node.name == "root_lod1") containsLOD1RootNode = true; + if (node.name == "polygon_lod1") containsLOD1PolyNode = true; + if (node.name == "root_lod2") containsLOD2RootNode = true; + if (node.name == "polygon_lod2") containsLOD2PolyNode = true; + } + + Assert::IsTrue(validLodExtension); + Assert::IsTrue(containsLOD1RootNode); + Assert::IsTrue(containsLOD1PolyNode); + Assert::IsTrue(containsLOD2RootNode); + Assert::IsTrue(containsLOD2PolyNode); + + // Serialize GLTFDocument back to json + auto outputJson = Serialize(newlodgltfDoc); + } + catch (std::exception ex) + { + std::stringstream ss; + ss << "Received exception was unexpected. Got: " << ex.what(); + Assert::Fail(WStringUtils::ToWString(ss).c_str()); + } + } + + TEST_METHOD(GLTFLODUtils_GLTFNodeLODMergeScreenCoverage) + { + auto input = TestUtils::ReadLocalAsset(TestUtils::GetAbsolutePath(c_cubeAsset3DJson)); + auto readwriter = std::make_shared(); + try + { + // Deserialize input json + GLTFResourceReader resourceReader(*readwriter); + auto inputJson = std::string(std::istreambuf_iterator(*input), std::istreambuf_iterator()); + auto doc = DeserializeJson(inputJson); + + std::vector docs; + docs.push_back(doc); + docs.push_back(doc); + docs.push_back(doc); + + std::vector screenCoverages{ 0.5, 0.2, 0.01 }; + + auto newlodgltfDoc = GLTFLODUtils::MergeDocumentsAsLODs(docs, screenCoverages); + + CheckGLTFLODNodeCountAgainstOriginal(doc, newlodgltfDoc, 3); + + // Check Node Lods have correct screen coverage values + auto nodes = newlodgltfDoc.nodes.Elements(); + bool rootNodeContainsCoverage = false; + for (auto node : nodes) + { + if (node.name == "root" && !node.extras.empty()) + { + auto extrasJson = RapidJsonUtils::CreateDocumentFromString(node.extras); + + Assert::IsTrue(extrasJson.IsObject()); + Assert::IsTrue(extrasJson["MSFT_screencoverage"].IsArray()); + Assert::IsTrue(extrasJson["MSFT_screencoverage"].GetArray().Size() == 3); + + rootNodeContainsCoverage = true; + } + } + + Assert::IsTrue(rootNodeContainsCoverage); + + // Serialize GLTFDocument back to json + auto outputJson = Serialize(newlodgltfDoc); + } + catch (std::exception ex) + { + std::stringstream ss; + ss << "Received exception was unexpected. Got: " << ex.what(); + Assert::Fail(WStringUtils::ToWString(ss).c_str()); + } + } + + TEST_METHOD(GLTFLODUtils_DeserialiseNodeLODExtension) + { + auto input = TestUtils::ReadLocalAsset(TestUtils::GetAbsolutePath(c_cubeWithLODJson)); + auto readwriter = std::make_shared(); + try + { + auto gltfDoc = ImportGLTF(readwriter, input); + auto nodes = gltfDoc->nodes.Elements(); + Assert::IsTrue(nodes.size() == 4); + auto lods = GLTFLODUtils::ParseDocumentNodeLODs(*gltfDoc); + bool validLodExtension = false; + for (auto node : nodes) + { + if (node.name == "root" && (std::find(lods[node.id]->begin(), lods[node.id]->end(), "3") != lods[node.id]->end())) + { + validLodExtension = true; + break; + } + } + Assert::IsTrue(validLodExtension); + } + catch (std::exception ex) + { + std::stringstream ss; + ss << "Received exception was unexpected. Got: " << ex.what(); + Assert::Fail(WStringUtils::ToWString(ss).c_str()); + } + } + + TEST_METHOD(GLTFLODUtils_DeserializeSerializeLoopNodeLODExtension) + { + auto input = TestUtils::ReadLocalAsset(TestUtils::GetAbsolutePath(c_cubeWithLODJson)); + auto readwriter = std::make_shared(); + try + { + // Deserialize input json + GLTFResourceReader resourceReader(*readwriter); + auto inputJson = std::string(std::istreambuf_iterator(*input), std::istreambuf_iterator()); + auto doc = DeserializeJson(inputJson); + + // Serialize GLTFDocument back to json + auto outputJson = Serialize(doc); + auto outputDoc = DeserializeJson(outputJson); + + // Compare input and output GLTFDocuments + Assert::AreNotSame(doc == outputDoc, true, L"Input gltf and output gltf are not equal"); + + // Specifically ensure Node LODs are preserved through de/serialization loop + auto nodes = outputDoc.nodes.Elements(); + Assert::IsTrue(nodes.size() == 4); + auto lods = GLTFLODUtils::ParseDocumentNodeLODs(outputDoc); + bool validLodExtension = false; + for (auto node : nodes) + { + if (node.name == "root" && (std::find(lods[node.id]->begin(), lods[node.id]->end(), "3") != lods[node.id]->end())) + { + validLodExtension = true; + break; + } + } + Assert::IsTrue(validLodExtension); + } + catch (std::exception ex) + { + std::stringstream ss; + ss << "Received exception was unexpected. Got: " << ex.what(); + Assert::Fail(WStringUtils::ToWString(ss).c_str()); + } + } + }; +} + diff --git a/glTF-Toolkit.Test/GLTFTextureCompressionUtilsTests.cpp b/glTF-Toolkit.Test/GLTFTextureCompressionUtilsTests.cpp new file mode 100644 index 0000000..2f1d2c5 --- /dev/null +++ b/glTF-Toolkit.Test/GLTFTextureCompressionUtilsTests.cpp @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +#include "pch.h" +#include + +#include "GLTFSDK/IStreamWriter.h" +#include "GLTFSDK/GLTFConstants.h" +#include "GLTFSDK/Serialize.h" +#include "GLTFSDK/Deserialize.h" +#include "GLTFSDK/GLBResourceReader.h" +#include "GLTFSDK/GLTFResourceWriter.h" + +#include "GLTFTextureCompressionUtils.h" + +#include "Helpers/WStringUtils.h" +#include "Helpers/StreamMock.h" +#include "Helpers/TestUtils.h" + +#include + + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +namespace Microsoft::glTF::Toolkit::Test +{ + // Note: some tests are using BC3 since it's faster to run that algorithm vs. BC7 + + TEST_CLASS(GLTFTextureCompressionUtilsTests) + { + const char* c_baseColorPng = "Resources\\gltf\\WaterBottle_ORM\\WaterBottle_baseColor.png"; + const char* c_baseColorBC7 = "Resources\\gltf\\WaterBottle_ORM\\WaterBottle_baseColor.DDS"; + const char* c_waterBottleORMJson = "Resources\\gltf\\WaterBottle_ORM\\WaterBottle.gltf"; + + TEST_METHOD(GLTFTextureCompressionUtils_CompressImage_BC7) + { + // Load png + auto png = TestUtils::ReadLocalAsset(TestUtils::GetAbsolutePath(c_baseColorPng)); + std::vector pngData = StreamUtils::ReadBinaryFull(*png); + + // ddsImage <= load DDS + DirectX::ScratchImage ddsImage; + DirectX::TexMetadata info; + DirectX::LoadFromDDSFile(TestUtils::GetAbsolutePathW(c_baseColorBC7).c_str(), DirectX::WIC_FLAGS_NONE, &info, ddsImage); + + // compressedPng <= convert using BC7 + DirectX::ScratchImage compressedPng; + DirectX::LoadFromWICMemory(pngData.data(), pngData.size(), DirectX::WIC_FLAGS_NONE, &info, compressedPng); + + GLTFTextureCompressionUtils::CompressImage(compressedPng, TextureCompression::BC7); + + auto ddsMip0 = ddsImage.GetImage(0, 0, 0); + size_t ddsImageSize = ddsMip0->height * ddsMip0->width; + Assert::AreEqual(ddsImageSize, compressedPng.GetPixelsSize(), L"ddsImage and compressedPng lengths are not the same"); + + Assert::IsTrue(memcmp(ddsMip0->pixels, compressedPng.GetPixels(), ddsImageSize), L"ddsImage and compressedPng are not the same"); + } + + TEST_METHOD(GLTFTextureCompressionUtils_CompressTextureAsDDS_NoCompression) + { + // This asset has all textures + TestUtils::LoadAndExecuteGLTFTest(c_waterBottleORMJson, [](auto doc, auto path) + { + auto compressedDoc = GLTFTextureCompressionUtils::CompressTextureAsDDS(TestStreamReader(path), doc, doc.textures.Get("0"), TextureCompression::None, ""); + + // Check that nothing changed + Assert::IsTrue(doc == compressedDoc); + }); + } + + TEST_METHOD(GLTFTextureCompressionUtils_CompressTextureAsDDS_CompressBC3_NoMips_Retain) + { + // This asset has all textures + TestUtils::LoadAndExecuteGLTFTest(c_waterBottleORMJson, [](auto doc, auto path) + { + auto maxTextureSize = std::numeric_limits::max(); + auto generateMipMaps = false; + auto retainOriginalImages = true; + auto compressedDoc = GLTFTextureCompressionUtils::CompressTextureAsDDS(TestStreamReader(path), doc, doc.textures.Get("0"), TextureCompression::BC3, "", maxTextureSize, generateMipMaps, retainOriginalImages); + + auto originalTexture = doc.textures.Get("0"); + auto compressedTexture = compressedDoc.textures.Get("0"); + + // Check that the image has not been replaced + Assert::IsTrue(originalTexture.imageId == compressedTexture.imageId); + + // Check that the image has been added + Assert::IsTrue(doc.images.Size() + 1 == compressedDoc.images.Size()); + + // Check that the texture now has the extension + Assert::IsTrue(originalTexture.extensions.size() + 1 == compressedTexture.extensions.size()); + + // Check the new extension is not empty + auto ddsExtension = compressedTexture.extensions.at(std::string(EXTENSION_MSFT_TEXTURE_DDS)); + Assert::IsFalse(ddsExtension.empty()); + + // Check the new extension contains a DDS image + rapidjson::Document ddsJson; + ddsJson.Parse(ddsExtension.c_str()); + + Assert::IsTrue(ddsJson["source"].IsInt()); + + auto ddsImageId = std::to_string(ddsJson["source"].GetInt()); + + Assert::IsTrue(compressedDoc.images.Get(ddsImageId).mimeType == "image/vnd-ms.dds"); + Assert::IsTrue(compressedDoc.images.Get(ddsImageId).uri == "texture_0_nomips_BC3.dds"); + }); + } + + TEST_METHOD(GLTFTextureCompressionUtils_CompressTextureAsDDS_CompressBC3_NoMips_Replace) + { + // This asset has all textures + TestUtils::LoadAndExecuteGLTFTest(c_waterBottleORMJson, [](auto doc, auto path) + { + auto maxTextureSize = std::numeric_limits::max(); + auto generateMipMaps = false; + auto retainOriginalImages = false; + auto compressedDoc = GLTFTextureCompressionUtils::CompressTextureAsDDS(TestStreamReader(path), doc, doc.textures.Get("0"), TextureCompression::BC3, "", maxTextureSize, generateMipMaps, retainOriginalImages); + + auto originalTexture = doc.textures.Get("0"); + auto compressedTexture = compressedDoc.textures.Get("0"); + + // Check that the texture is still pointing to the same image + Assert::IsTrue(originalTexture.imageId == compressedTexture.imageId); + + // Check that an image has not been added + Assert::IsTrue(doc.images.Size() == compressedDoc.images.Size()); + + // Check that the texture now has the extension + Assert::IsTrue(originalTexture.extensions.size() + 1 == compressedTexture.extensions.size()); + + // Check the new extension is not empty + auto ddsExtension = compressedTexture.extensions.at(std::string(EXTENSION_MSFT_TEXTURE_DDS)); + Assert::IsFalse(ddsExtension.empty()); + + // Check the new extension contains a DDS image + rapidjson::Document ddsJson; + ddsJson.Parse(ddsExtension.c_str()); + + Assert::IsTrue(ddsJson["source"].IsInt()); + + auto ddsImageId = std::to_string(ddsJson["source"].GetInt()); + + Assert::IsTrue(compressedDoc.images.Get(ddsImageId).mimeType == "image/vnd-ms.dds"); + Assert::IsTrue(compressedDoc.images.Get(ddsImageId).uri == "texture_0_nomips_BC3.dds"); + + // Check the extension points to the same image as the source (image was replaced) + Assert::AreEqual(compressedTexture.imageId, ddsImageId); + }); + } + + TEST_METHOD(GLTFTextureCompressionUtils_CompressTextureAsDDS_CompressBC7_Mips_Retain) + { + // This asset has all textures + TestUtils::LoadAndExecuteGLTFTest(c_waterBottleORMJson, [](auto doc, auto path) + { + auto maxTextureSize = std::numeric_limits::max(); + auto generateMipMaps = true; + auto retainOriginalImages = true; + auto compressedDoc = GLTFTextureCompressionUtils::CompressTextureAsDDS(TestStreamReader(path), doc, doc.textures.Get("0"), TextureCompression::BC7, "", maxTextureSize, generateMipMaps, retainOriginalImages); + + auto originalTexture = doc.textures.Get("0"); + auto compressedTexture = compressedDoc.textures.Get("0"); + + // Check that the image has not been replaced + Assert::IsTrue(originalTexture.imageId == compressedTexture.imageId); + + // Check that the image has been added + Assert::IsTrue(doc.images.Size() + 1 == compressedDoc.images.Size()); + + // Check that the texture now has the extension + Assert::IsTrue(originalTexture.extensions.size() + 1 == compressedTexture.extensions.size()); + + // Check the new extension is not empty + auto ddsExtension = compressedTexture.extensions.at(std::string(EXTENSION_MSFT_TEXTURE_DDS)); + Assert::IsFalse(ddsExtension.empty()); + + // Check the new extension contains a DDS image + rapidjson::Document ddsJson; + ddsJson.Parse(ddsExtension.c_str()); + + Assert::IsTrue(ddsJson["source"].IsInt()); + + auto ddsImageId = std::to_string(ddsJson["source"].GetInt()); + + Assert::IsTrue(compressedDoc.images.Get(ddsImageId).mimeType == "image/vnd-ms.dds"); + Assert::IsTrue(compressedDoc.images.Get(ddsImageId).uri == "texture_0_BC7.dds"); + }); + } + + TEST_METHOD(GLTFTextureCompressionUtils_CompressAllTexturesForWindowsMR_Retain) + { + // This asset has all textures + TestUtils::LoadAndExecuteGLTFTest(c_waterBottleORMJson, [](auto doc, auto path) + { + auto maxTextureSize = std::numeric_limits::max(); + auto retainOriginalImages = true; + auto compressedDoc = GLTFTextureCompressionUtils::CompressAllTexturesForWindowsMR(TestStreamReader(path), doc, "", maxTextureSize, retainOriginalImages); + + // Check that the materials and textures have not been replaced + // Check that the textures has not been replaced + Assert::IsTrue(doc.textures.Size() == compressedDoc.textures.Size()); + Assert::IsTrue(doc.materials.Size() == compressedDoc.materials.Size()); + + // Check that the images have been added (base, emissive, RMO and normal) + Assert::AreEqual(doc.images.Size() + 4, compressedDoc.images.Size()); + + auto originalMaterial = doc.materials.Get("0"); + auto compressedMaterial = compressedDoc.materials.Get("0"); + + // Check that all relevant textures now have the extension + Assert::IsTrue(doc.textures.Get(originalMaterial.metallicRoughness.baseColorTextureId).extensions.size() + 1 == compressedDoc.textures.Get(compressedMaterial.metallicRoughness.baseColorTextureId).extensions.size()); + Assert::IsTrue(doc.textures.Get(originalMaterial.emissiveTextureId).extensions.size() + 1 == compressedDoc.textures.Get(compressedMaterial.emissiveTextureId).extensions.size()); + // TODO: read the WMR (MSFT_packing...) textures as well + + // Check the new extension is not empty + auto ddsExtension = compressedDoc.textures.Get(compressedMaterial.emissiveTextureId).extensions.at(std::string(EXTENSION_MSFT_TEXTURE_DDS)); + Assert::IsFalse(ddsExtension.empty()); + + // Check the new extension contains a DDS image + rapidjson::Document ddsJson; + ddsJson.Parse(ddsExtension.c_str()); + + Assert::IsTrue(ddsJson["source"].IsInt()); + + auto ddsImageId = std::to_string(ddsJson["source"].GetInt()); + + Assert::IsTrue(compressedDoc.images.Get(ddsImageId).mimeType == "image/vnd-ms.dds"); + std::string expectedSuffix = "_BC7.dds"; + Assert::IsTrue(compressedDoc.images.Get(ddsImageId).uri.compare(9, expectedSuffix.size(), expectedSuffix) == 0); // The emissive texture should have mips and be BC7 + }); + } + }; +} diff --git a/glTF-Toolkit.Test/GLTFTexturePackingUtilsTests.cpp b/glTF-Toolkit.Test/GLTFTexturePackingUtilsTests.cpp new file mode 100644 index 0000000..516bb9c --- /dev/null +++ b/glTF-Toolkit.Test/GLTFTexturePackingUtilsTests.cpp @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +#include "pch.h" +#include + +#include "GLTFSDK/IStreamWriter.h" +#include "GLTFSDK/GLTFConstants.h" +#include "GLTFSDK/Serialize.h" +#include "GLTFSDK/Deserialize.h" +#include "GLTFSDK/GLBResourceReader.h" +#include "GLTFSDK/GLTFResourceWriter.h" + +#include "GLTFTexturePackingUtils.h" + +#include "Helpers/WStringUtils.h" +#include "Helpers/TestUtils.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +namespace Microsoft::glTF::Toolkit::Test +{ + TEST_CLASS(GLTFTexturePackingUtilsTests) + { + // Asset with no materials + const char* c_cubeAsset3DJson = "Resources\\gltf\\cubeAsset3D.gltf"; + + // Asset with loose images and all textures + const char* c_waterBottleJson = "Resources\\gltf\\WaterBottle\\WaterBottle.gltf"; + + // Loads and packs a complex asset with the specified packing + void ExecutePackingTest(const char* gltfRelativePath, TexturePacking packing) + { + // This asset has all textures + TestUtils::LoadAndExecuteGLTFTest(gltfRelativePath, [packing](auto doc, auto path) + { + auto material = doc.materials.Elements()[0]; + auto packedDoc = GLTFTexturePackingUtils::PackMaterialForWindowsMR(TestStreamReader(path), doc, material, packing, ""); + auto packedMaterial = packedDoc.materials.Elements()[0]; + + // Check that the material changed + Assert::IsTrue(material != packedMaterial); + + // Check that the new material replaces the old one + Assert::IsTrue(material.id == packedMaterial.id); + Assert::IsTrue(doc.materials.Size() == packedDoc.materials.Size()); + + // Check that the packed material has the new extension + Assert::IsTrue(material.extensions.size() + 1 == packedMaterial.extensions.size()); + + // Check the new extension is not empty + auto packingOrmExtension = packedMaterial.extensions.at(std::string(EXTENSION_MSFT_PACKING_ORM)); + Assert::IsFalse(packingOrmExtension.empty()); + + // Check the new extension contains an ORM texture + rapidjson::Document ormJson; + ormJson.Parse(packingOrmExtension.c_str()); + + if (packing & TexturePacking::OcclusionRoughnessMetallic) + { + Assert::IsTrue(ormJson["occlusionRoughnessMetallicTexture"].IsObject()); + Assert::IsTrue(ormJson["occlusionRoughnessMetallicTexture"].HasMember("index")); + } + + if (packing & TexturePacking::RoughnessMetallicOcclusion) + { + Assert::IsTrue(ormJson["roughnessMetallicOcclusionTexture"].IsObject()); + Assert::IsTrue(ormJson["roughnessMetallicOcclusionTexture"].HasMember("index")); + } + + if (!material.normalTexture.id.empty()) + { + // Check the new extension contains a normal texture + Assert::IsTrue(ormJson["normalTexture"].IsObject()); + Assert::IsTrue(ormJson["normalTexture"].HasMember("index")); + } + }); + } + + TEST_METHOD(GLTFTexturePackingUtils_NoMaterials) + { + // This asset has no textures + TestUtils::LoadAndExecuteGLTFTest(c_cubeAsset3DJson, [](auto doc, auto path) + { + auto material = doc.materials.Elements()[0]; + auto packedDoc = GLTFTexturePackingUtils::PackMaterialForWindowsMR(TestStreamReader(path), doc, material, TexturePacking::OcclusionRoughnessMetallic, ""); + + // Check that nothing changed + Assert::IsTrue(doc == packedDoc); + }); + } + + TEST_METHOD(GLTFTexturePackingUtils_NoPacking) + { + // This asset has all textures + TestUtils::LoadAndExecuteGLTFTest(c_waterBottleJson, [](auto doc, auto path) + { + auto material = doc.materials.Elements()[0]; + auto packedDoc = GLTFTexturePackingUtils::PackMaterialForWindowsMR(TestStreamReader(path), doc, material, TexturePacking::None, ""); + + // Check that nothing changed + Assert::IsTrue(doc == packedDoc); + }); + } + + TEST_METHOD(GLTFTexturePackingUtils_PackORM) + { + ExecutePackingTest(c_waterBottleJson, TexturePacking::OcclusionRoughnessMetallic); + } + + TEST_METHOD(GLTFTexturePackingUtils_PackRMO) + { + ExecutePackingTest(c_waterBottleJson, TexturePacking::RoughnessMetallicOcclusion); + } + + TEST_METHOD(GLTFTexturePackingUtils_PackORMandRMO) + { + ExecutePackingTest(c_waterBottleJson, (TexturePacking)(TexturePacking::OcclusionRoughnessMetallic | TexturePacking::RoughnessMetallicOcclusion)); + } + + TEST_METHOD(GLTFTexturePackingUtils_PackAllWithNoMaterials) + { + // This asset has no materials + TestUtils::LoadAndExecuteGLTFTest(c_cubeAsset3DJson, [](auto doc, auto path) + { + auto packedDoc = GLTFTexturePackingUtils::PackAllMaterialsForWindowsMR(TestStreamReader(path), doc, TexturePacking::OcclusionRoughnessMetallic, ""); + + // Check that nothing changed + Assert::IsTrue(doc == packedDoc); + }); + } + + TEST_METHOD(GLTFTexturePackingUtils_PackAllWithPackingNone) + { + // This asset has all textures + TestUtils::LoadAndExecuteGLTFTest(c_waterBottleJson, [](auto doc, auto path) + { + auto packedDoc = GLTFTexturePackingUtils::PackAllMaterialsForWindowsMR(TestStreamReader(path), doc, TexturePacking::None, ""); + + // Check that nothing changed + Assert::IsTrue(doc == packedDoc); + }); + } + + TEST_METHOD(GLTFTexturePackingUtils_PackAllWithOneMaterial) + { + std::unique_ptr documentPackedSingleTexture; + std::unique_ptr documentPackedAllTextures; + + // This asset has all textures + TestUtils::LoadAndExecuteGLTFTest(c_waterBottleJson, [&documentPackedSingleTexture](auto doc, auto path) + { + documentPackedSingleTexture = std::make_unique(GLTFTexturePackingUtils::PackMaterialForWindowsMR(TestStreamReader(path), doc, doc.materials.Elements()[0], TexturePacking::OcclusionRoughnessMetallic, "")); + }); + + TestUtils::LoadAndExecuteGLTFTest(c_waterBottleJson, [&documentPackedAllTextures](auto doc, auto path) + { + documentPackedAllTextures = std::make_unique(GLTFTexturePackingUtils::PackAllMaterialsForWindowsMR(TestStreamReader(path), doc, TexturePacking::OcclusionRoughnessMetallic, "")); + }); + + // Assert there's one material + Assert::IsTrue(documentPackedSingleTexture->materials.Size() == 1); + Assert::IsTrue(documentPackedAllTextures->materials.Size() == 1); + + // Check that they're the same when there's one material + Assert::IsTrue(*documentPackedSingleTexture == *documentPackedAllTextures); + } + }; +} + diff --git a/glTF-Toolkit.Test/Helpers/StreamMock.h b/glTF-Toolkit.Test/Helpers/StreamMock.h new file mode 100644 index 0000000..4a5d5f1 --- /dev/null +++ b/glTF-Toolkit.Test/Helpers/StreamMock.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include +#include +#include + +#include +#include + +namespace Microsoft::glTF::Toolkit::Test +{ + class StreamMock : public Microsoft::glTF::IStreamWriter, public Microsoft::glTF::IStreamReader + { + public: + StreamMock() : m_stream(std::make_shared(std::ios_base::app | std::ios_base::binary | std::ios_base::in | std::ios_base::out)) + { + } + + std::shared_ptr GetOutputStream(const std::string&) const override + { + return m_stream; + } + + std::shared_ptr GetInputStream(const std::string&) const override + { + return m_stream; + } + private: + std::shared_ptr m_stream; + }; +} \ No newline at end of file diff --git a/glTF-Toolkit.Test/Helpers/TestUtils.h b/glTF-Toolkit.Test/Helpers/TestUtils.h new file mode 100644 index 0000000..019a181 --- /dev/null +++ b/glTF-Toolkit.Test/Helpers/TestUtils.h @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include + +#include +#include "GLTFSDK/IStreamWriter.h" +#include "GLTFSDK/GLTFConstants.h" +#include "GLTFSDK/Serialize.h" +#include "GLTFSDK/Deserialize.h" + +#include +#include +#include +#include "WStringUtils.h" +#include "StreamMock.h" +#include +#include +#include +#include +#include + +namespace Microsoft::glTF::Toolkit::Test +{ + class TestUtils + { + public: + + static std::string GetBasePath(const char * absolutePath) + { + std::string path(absolutePath); +#ifdef __APPLE__ + return path.substr(0, path.rfind('/') + 1); +#else + return path.substr(0, path.rfind('\\') + 1); +#endif + } + + static std::string GetAbsolutePath(const char * relativePath) + { +#ifdef __APPLE__ + // Leaving Win32 alone (below), but macOS requires working directory to be set + std::string finalPath(relativePath); + std::replace(finalPath.begin(), finalPath.end(), '\\', '/'); + return finalPath; +#else + std::string currentPath = __FILE__; + std::string sourcePath = currentPath.substr(0, currentPath.rfind('\\')); + std::string resourcePath = sourcePath.substr(0, sourcePath.rfind('\\')); + return resourcePath + "\\" + relativePath; +#endif + } + + static std::wstring GetAbsolutePathW(const char * relativePath) + { + std::string absolutePath = GetAbsolutePath(relativePath); + std::wstringstream wss; + wss << absolutePath.c_str(); + return wss.str(); + } + + static std::shared_ptr ReadLocalAsset(const std::string& filename) + { + // Read local file + int64_t m_readPosition = 0; + std::shared_ptr> m_buffer; + std::ifstream ifs; + ifs.open(filename.c_str(), std::ifstream::in | std::ifstream::binary); + if (ifs.is_open()) + { + std::streampos start = ifs.tellg(); + ifs.seekg(0, std::ios::end); + m_buffer = std::make_shared>(static_cast(ifs.tellg() - start)); + ifs.seekg(0, std::ios::beg); + ifs.read(reinterpret_cast(const_cast(m_buffer->data())), m_buffer->size()); + ifs.close(); + } + else + { + throw std::runtime_error("Could not open the file for reading"); + } + + // To IStream + unsigned long writeBufferLength = 4096L * 1024L; + auto tempStream = std::make_shared(); + auto tempBuffer = new char[writeBufferLength]; + // Read the file for as long as we can fill the buffer completely. + // This means there is more content to be read. + unsigned long bytesRead; + do + { + auto bytesAvailable = m_buffer->size() - m_readPosition; + unsigned long br = std::min(static_cast(bytesAvailable), writeBufferLength); +#ifdef _WIN32 + memcpy_s(tempBuffer, br, m_buffer->data() + m_readPosition, br); +#else + memcpy(tempBuffer, m_buffer->data() + m_readPosition, br); +#endif + m_readPosition += br; + bytesRead = br; + + tempStream->write(tempBuffer, bytesRead); + } while (bytesRead == writeBufferLength); + + delete[] tempBuffer; + + if (tempStream.get()->bad()) + { + throw std::runtime_error("Bad std::stringstream after copying the file"); + } + + return tempStream; + } + + typedef std::function GLTFAction; + + static void LoadAndExecuteGLTFTest(const char * gltfRelativePath, GLTFAction action) + { + // This asset has all textures + auto absolutePath = TestUtils::GetAbsolutePath(gltfRelativePath); + auto input = TestUtils::ReadLocalAsset(absolutePath); + try + { + // Deserialize input json + auto inputJson = std::string(std::istreambuf_iterator(*input), std::istreambuf_iterator()); + auto doc = DeserializeJson(inputJson); + + action(doc, absolutePath); + } + catch (std::exception ex) + { + std::stringstream ss; + ss << "Received exception was unexpected. Got: " << ex.what(); + Microsoft::VisualStudio::CppUnitTestFramework::Assert::Fail(WStringUtils::ToWString(ss).c_str()); + } + } + }; + + class TestStreamReader : public IStreamReader + { + public: + TestStreamReader(std::string gltfAbsolutePath) : m_basePath(TestUtils::GetBasePath(gltfAbsolutePath.c_str())) {} + + virtual ~TestStreamReader() override {} + virtual std::shared_ptr GetInputStream(const std::string& filename) const override + { + auto path = m_basePath; + +#ifdef __APPLE__ + path += "/" + filename; +#else + path += "\\" + filename; +#endif + + return std::make_shared(path, std::ios::binary); + } + private: + const std::string m_basePath; + }; +} \ No newline at end of file diff --git a/glTF-Toolkit.Test/Helpers/WStringUtils.h b/glTF-Toolkit.Test/Helpers/WStringUtils.h new file mode 100644 index 0000000..290f0fd --- /dev/null +++ b/glTF-Toolkit.Test/Helpers/WStringUtils.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include +#include +#include +#include + +namespace Microsoft::glTF::Toolkit::Test +{ + class WStringUtils + { + public: + static std::wstring ToWString(const std::string& str) + { + return ToWString(str.c_str()); + } + + static std::wstring ToWString(const std::stringstream& ss) + { + return ToWString(ss.str()); + } + + static std::wstring ToWString(const char* str) + { + std::wstring_convert> converter; + return converter.from_bytes(str); + } + }; +} diff --git a/glTF-Toolkit.Test/Resources/gltf/CubeAsset3D.gltf b/glTF-Toolkit.Test/Resources/gltf/CubeAsset3D.gltf new file mode 100644 index 0000000..af2f88b --- /dev/null +++ b/glTF-Toolkit.Test/Resources/gltf/CubeAsset3D.gltf @@ -0,0 +1,124 @@ +{ + "asset": { + "version": "2.0", + "generator": "Microsoft GLTF Exporter 2.1.2-b21" + }, + "accessors": [{ + "bufferView": 0, + "byteOffset": 0, + "componentType": 5123, + "count": 36, + "type": "SCALAR", + "max": [23.0], + "min": [0.0] + }, { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "max": [1.0, 1.0, 1.0], + "min": [0.0, 0.0, 0.0] + }, { + "bufferView": 2, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "max": [1.0, 1.0, 0.0], + "min": [-1.0, -1.0, -1.0] + } + ], + "bufferViews": [{ + "buffer": 0, + "byteLength": 72, + "byteOffset": 0, + "target": 34963 + }, { + "buffer": 0, + "byteLength": 288, + "byteOffset": 72, + "target": 34962 + }, { + "buffer": 0, + "byteLength": 288, + "byteOffset": 360, + "target": 34962 + } + ], + "buffers": [{ + "byteLength": 648 + } + ], + "materials": [{ + "pbrMetallicRoughness": { + "baseColorFactor": [0.5187909007072449, 0.5187909007072449, 0.5187909007072449, 1.0], + "metallicFactor": 0.0, + "roughnessFactor": 0.0 + }, + "name": "DefaultMaterial", + "extensions": { + "KHR_materials_pbrSpecularGlossiness": { + "diffuseFactor": [0.49803921580314639, 0.49803921580314639, 0.49803921580314639, 1.0], + "specularFactor": [0.0, 0.0, 0.0] + } + } + }, { + "pbrMetallicRoughness": { + "baseColorFactor": [0.5187909007072449, 0.5187909007072449, 0.5187909007072449, 1.0], + "metallicFactor": 0.0, + "roughnessFactor": 0.0 + }, + "name": "DefaultMaterial", + "extensions": { + "KHR_materials_pbrSpecularGlossiness": { + "diffuseFactor": [0.49803921580314639, 0.49803921580314639, 0.49803921580314639, 1.0], + "specularFactor": [0.0, 0.0, 0.0] + } + } + }, { + "pbrMetallicRoughness": { + "baseColorFactor": [0.5187909007072449, 0.5187909007072449, 0.5187909007072449, 1.0], + "metallicFactor": 0.0, + "roughnessFactor": 0.0 + }, + "name": "DefaultMaterial", + "extensions": { + "KHR_materials_pbrSpecularGlossiness": { + "diffuseFactor": [0.49803921580314639, 0.49803921580314639, 0.49803921580314639, 1.0], + "specularFactor": [0.0, 0.0, 0.0] + } + } + } + ], + "meshes": [{ + "name": "polygon", + "primitives": [{ + "attributes": { + "NORMAL": 2, + "POSITION": 1 + }, + "indices": 0, + "material": 0, + "mode": 4 + } + ] + } + ], + "nodes": [{ + "matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0], + "mesh": 0, + "name": "polygon" + }, { + "children": [0], + "matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0], + "name": "root" + } + ], + "scenes": [{ + "nodes": [1] + } + ], + "scene": 0, + "extensionsUsed": ["KHR_materials_PBRSpecularGlossiness"] +} diff --git a/glTF-Toolkit.Test/Resources/gltf/CubeWithLOD.gltf b/glTF-Toolkit.Test/Resources/gltf/CubeWithLOD.gltf new file mode 100644 index 0000000..4855e1e --- /dev/null +++ b/glTF-Toolkit.Test/Resources/gltf/CubeWithLOD.gltf @@ -0,0 +1,101 @@ +{ + "asset": { + "version": "2.0", + "generator": "Microsoft GLTF Exporter 2.1.2-b21" + }, + "accessors": [{ + "bufferView": 0, + "byteOffset": 0, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, { + "bufferView": 2, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "type": "VEC3" + } + ], + "bufferViews": [{ + "buffer": 0, + "byteLength": 72, + "byteOffset": 0, + "target": 34963 + }, { + "buffer": 0, + "byteLength": 288, + "byteOffset": 72, + "target": 34962 + }, { + "buffer": 0, + "byteLength": 288, + "byteOffset": 360, + "target": 34962 + } + ], + "buffers": [{ + "byteLength": 648 + } + ], + "materials": [{ + "pbrMetallicRoughness": { + "baseColorFactor": [0.5187909007072449, 0.5187909007072449, 0.5187909007072449, 1.0], + "metallicFactor": 0.0, + "roughnessFactor": 0.0 + }, + "name": "DefaultMaterial", + "extensions": { + "KHR_materials_pbrSpecularGlossiness": { + "diffuseFactor": [0.49803921580314639, 0.49803921580314639, 0.49803921580314639, 1.0], + "specularFactor": [0.0, 0.0, 0.0] + } + } + } + ], + "meshes": [{ + "name": "polygon", + "primitives": [{ + "attributes": { + "NORMAL": 2, + "POSITION": 1 + }, + "indices": 0, + "material": 0, + "mode": 4 + } + ] + } + ], + "nodes": [{ + "mesh": 0, + "name": "polygon" + }, { + "children": [0], + "name": "root", + "extensions": { + "MSFT_lod": { + "ids": [ 3 ] + } + } + }, { + "mesh": 0, + "name": "polygon_lod1" + }, { + "children": [2], + "name": "root_lod1" + } + ], + "scenes": [{ + "nodes": [1] + } + ], + "scene": 0, + "extensionsUsed": ["KHR_materials_PBRSpecularGlossiness"] +} diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle.bin b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle.bin new file mode 100644 index 0000000..8addba7 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle.bin differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle.gltf b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle.gltf new file mode 100644 index 0000000..fa394a4 --- /dev/null +++ b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle.gltf @@ -0,0 +1,197 @@ +{ + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 2549, + "type": "VEC2" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 2549, + "type": "VEC3" + }, + { + "bufferView": 2, + "componentType": 5126, + "count": 2549, + "type": "VEC4" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 2549, + "type": "VEC3", + "max": [ + 0.05445001, + 0.130220339, + 0.0544500239 + ], + "min": [ + -0.05445001, + -0.130220339, + -0.0544500239 + ] + }, + { + "bufferView": 4, + "componentType": 5123, + "count": 13530, + "type": "SCALAR" + } + ], + "asset": { + "generator": "glTF Tools for Unity", + "version": "2.0" + }, + "bufferViews": [ + { + "buffer": 0, + "byteLength": 20392 + }, + { + "buffer": 0, + "byteOffset": 20392, + "byteLength": 30588 + }, + { + "buffer": 0, + "byteOffset": 50980, + "byteLength": 40784 + }, + { + "buffer": 0, + "byteOffset": 91764, + "byteLength": 30588 + }, + { + "buffer": 0, + "byteOffset": 122352, + "byteLength": 27060 + } + ], + "buffers": [ + { + "uri": "WaterBottle.bin", + "byteLength": 149412 + } + ], + "extensionsUsed": [ + "KHR_materials_pbrSpecularGlossiness" + ], + "images": [ + { + "uri": "WaterBottle_baseColor.png" + }, + { + "uri": "WaterBottle_roughnessMetallic.png" + }, + { + "uri": "WaterBottle_normal.png" + }, + { + "uri": "WaterBottle_emissive.png" + }, + { + "uri": "WaterBottle_occlusion.png" + }, + { + "uri": "WaterBottle_diffuse.png" + }, + { + "uri": "WaterBottle_specularGlossiness.png" + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "TEXCOORD_0": 0, + "NORMAL": 1, + "TANGENT": 2, + "POSITION": 3 + }, + "indices": 4, + "material": 0 + } + ], + "name": "WaterBottle" + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0 + }, + "metallicRoughnessTexture": { + "index": 1 + } + }, + "normalTexture": { + "index": 2 + }, + "occlusionTexture": { + "index": 4 + }, + "emissiveFactor": [ + 1.0, + 1.0, + 1.0 + ], + "emissiveTexture": { + "index": 3 + }, + "name": "BottleMat", + "extensions": { + "KHR_materials_pbrSpecularGlossiness": { + "diffuseTexture": { + "index": 5 + }, + "specularGlossinessTexture": { + "index": 6 + } + } + } + } + ], + "nodes": [ + { + "mesh": 0, + "name": "WaterBottle" + } + ], + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "textures": [ + { + "source": 0 + }, + { + "source": 1 + }, + { + "source": 2 + }, + { + "source": 3 + }, + { + "source": 4 + }, + { + "source": 5 + }, + { + "source": 6 + } + ] +} \ No newline at end of file diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_baseColor.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_baseColor.png new file mode 100644 index 0000000..f28c3f2 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_baseColor.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_diffuse.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_diffuse.png new file mode 100644 index 0000000..34160cc Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_diffuse.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_emissive.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_emissive.png new file mode 100644 index 0000000..338905f Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_emissive.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_normal.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_normal.png new file mode 100644 index 0000000..82c690c Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_normal.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_occlusion.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_occlusion.png new file mode 100644 index 0000000..6314204 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_occlusion.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_roughnessMetallic.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_roughnessMetallic.png new file mode 100644 index 0000000..14ea254 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_roughnessMetallic.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_specularGlossiness.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_specularGlossiness.png new file mode 100644 index 0000000..0b9fc37 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle/WaterBottle_specularGlossiness.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle.bin b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle.bin new file mode 100644 index 0000000..8addba7 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle.bin differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle.gltf b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle.gltf new file mode 100644 index 0000000..15aec6c --- /dev/null +++ b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle.gltf @@ -0,0 +1,212 @@ +{ + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 2549, + "type": "VEC2" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 2549, + "type": "VEC3" + }, + { + "bufferView": 2, + "componentType": 5126, + "count": 2549, + "type": "VEC4" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 2549, + "type": "VEC3", + "max": [ + 0.05445001, + 0.130220339, + 0.0544500239 + ], + "min": [ + -0.05445001, + -0.130220339, + -0.0544500239 + ] + }, + { + "bufferView": 4, + "componentType": 5123, + "count": 13530, + "type": "SCALAR" + } + ], + "asset": { + "generator": "glTF Tools for Unity", + "version": "2.0" + }, + "bufferViews": [ + { + "buffer": 0, + "byteLength": 20392 + }, + { + "buffer": 0, + "byteOffset": 20392, + "byteLength": 30588 + }, + { + "buffer": 0, + "byteOffset": 50980, + "byteLength": 40784 + }, + { + "buffer": 0, + "byteOffset": 91764, + "byteLength": 30588 + }, + { + "buffer": 0, + "byteOffset": 122352, + "byteLength": 27060 + } + ], + "buffers": [ + { + "uri": "WaterBottle.bin", + "byteLength": 149412 + } + ], + "extensionsUsed": [ + "KHR_materials_pbrSpecularGlossiness", + "MSFT_packing_occlusionRoughnessMetallic" + ], + "images": [ + { + "uri": "WaterBottle_baseColor.png" + }, + { + "uri": "WaterBottle_roughnessMetallic.png" + }, + { + "uri": "WaterBottle_normal.png" + }, + { + "uri": "WaterBottle_emissive.png" + }, + { + "uri": "WaterBottle_occlusion.png" + }, + { + "uri": "WaterBottle_diffuse.png" + }, + { + "uri": "WaterBottle_specularGlossiness.png" + }, + { + "uri": "WaterBottle_occlusionRoughnessMetallic.png" + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "TEXCOORD_0": 0, + "NORMAL": 1, + "TANGENT": 2, + "POSITION": 3 + }, + "indices": 4, + "material": 0 + } + ], + "name": "WaterBottle" + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0 + }, + "metallicRoughnessTexture": { + "index": 1 + } + }, + "normalTexture": { + "index": 2 + }, + "occlusionTexture": { + "index": 4 + }, + "emissiveFactor": [ + 1.0, + 1.0, + 1.0 + ], + "emissiveTexture": { + "index": 3 + }, + "name": "BottleMat", + "extensions": { + "KHR_materials_pbrSpecularGlossiness": { + "diffuseTexture": { + "index": 5 + }, + "specularGlossinessTexture": { + "index": 6 + } + }, + "MSFT_packing_occlusionRoughnessMetallic": { + "occlusionRoughnessMetallicTexture": { + "index": 7 + }, + "normalTexture": { + "index": 2 + } + } + } + } + ], + "nodes": [ + { + "mesh": 0, + "name": "WaterBottle" + } + ], + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "textures": [ + { + "source": 0 + }, + { + "source": 1 + }, + { + "source": 2 + }, + { + "source": 3 + }, + { + "source": 4 + }, + { + "source": 5 + }, + { + "source": 6 + }, + { + "source": 7 + } + ] +} \ No newline at end of file diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_WindowsMR.gltf b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_WindowsMR.gltf new file mode 100644 index 0000000..05f87eb --- /dev/null +++ b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_WindowsMR.gltf @@ -0,0 +1,252 @@ +{ + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 2549, + "type": "VEC2" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 2549, + "type": "VEC3" + }, + { + "bufferView": 2, + "componentType": 5126, + "count": 2549, + "type": "VEC4" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 2549, + "type": "VEC3", + "max": [ + 0.05445001, + 0.130220339, + 0.0544500239 + ], + "min": [ + -0.05445001, + -0.130220339, + -0.0544500239 + ] + }, + { + "bufferView": 4, + "componentType": 5123, + "count": 13530, + "type": "SCALAR" + } + ], + "asset": { + "generator": "glTF Tools for Unity", + "version": "2.0" + }, + "bufferViews": [ + { + "buffer": 0, + "byteLength": 20392 + }, + { + "buffer": 0, + "byteOffset": 20392, + "byteLength": 30588 + }, + { + "buffer": 0, + "byteOffset": 50980, + "byteLength": 40784 + }, + { + "buffer": 0, + "byteOffset": 91764, + "byteLength": 30588 + }, + { + "buffer": 0, + "byteOffset": 122352, + "byteLength": 27060 + } + ], + "buffers": [ + { + "uri": "WaterBottle.bin", + "byteLength": 149412 + } + ], + "extensionsUsed": [ + "KHR_materials_pbrSpecularGlossiness", + "MSFT_packing_occlusionRoughnessMetallic", + "MSFT_texture_dds" + ], + "images": [ + { + "uri": "WaterBottle_baseColor.png" + }, + { + "uri": "WaterBottle_roughnessMetallic.png" + }, + { + "uri": "WaterBottle_normal.png" + }, + { + "uri": "WaterBottle_emissive.png" + }, + { + "uri": "WaterBottle_occlusion.png" + }, + { + "uri": "WaterBottle_diffuse.png" + }, + { + "uri": "WaterBottle_specularGlossiness.png" + }, + { + "uri": "WaterBottle_occlusionRoughnessMetallic.png" + }, + { + "mimeType": "image/vnd-ms.dds", + "uri": "WaterBottle_occlusionRoughnessMetallic.dds" + }, + { + "mimeType": "image/vnd-ms.dds", + "uri": "WaterBottle_normal.dds" + }, + { + "mimeType": "image/vnd-ms.dds", + "uri": "WaterBottle_baseColor.dds" + }, + { + "mimeType": "image/vnd-ms.dds", + "uri": "WaterBottle_emissive.dds" + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "TEXCOORD_0": 0, + "NORMAL": 1, + "TANGENT": 2, + "POSITION": 3 + }, + "indices": 4, + "material": 0 + } + ], + "name": "WaterBottle" + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0 + }, + "metallicRoughnessTexture": { + "index": 1 + } + }, + "normalTexture": { + "index": 2 + }, + "occlusionTexture": { + "index": 4 + }, + "emissiveFactor": [ + 1.0, + 1.0, + 1.0 + ], + "emissiveTexture": { + "index": 3 + }, + "name": "BottleMat", + "extensions": { + "KHR_materials_pbrSpecularGlossiness": { + "diffuseTexture": { + "index": 5 + }, + "specularGlossinessTexture": { + "index": 6 + } + }, + "MSFT_packing_occlusionRoughnessMetallic": { + "occlusionRoughnessMetallicTexture": { + "index": 8 + }, + "normalTexture": { + "index": 2 + } + } + } + } + ], + "nodes": [ + { + "mesh": 0, + "name": "WaterBottle" + } + ], + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "textures": [ + { + "source": 0, + "extensions": { + "MSFT_texture_dds": { + "source": 10 + } + } + }, + { + "source": 1 + }, + { + "source": 2, + "extensions": { + "MSFT_texture_dds": { + "source": 9 + } + } + }, + { + "source": 3, + "extensions": { + "MSFT_texture_dds": { + "source": 11 + } + } + }, + { + "source": 4 + }, + { + "source": 5 + }, + { + "source": 6 + }, + { + "source": 7 + }, + { + "source": 8, + "extensions": { + "MSFT_texture_dds": { + "source": 8 + } + } + } + ] +} \ No newline at end of file diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_baseColor.DDS b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_baseColor.DDS new file mode 100644 index 0000000..8404ed5 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_baseColor.DDS differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_baseColor.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_baseColor.png new file mode 100644 index 0000000..f28c3f2 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_baseColor.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_diffuse.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_diffuse.png new file mode 100644 index 0000000..34160cc Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_diffuse.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_emissive.DDS b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_emissive.DDS new file mode 100644 index 0000000..8260f50 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_emissive.DDS differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_emissive.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_emissive.png new file mode 100644 index 0000000..338905f Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_emissive.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_normal.dds b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_normal.dds new file mode 100644 index 0000000..48c96be Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_normal.dds differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_normal.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_normal.png new file mode 100644 index 0000000..82c690c Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_normal.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_occlusion.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_occlusion.png new file mode 100644 index 0000000..6314204 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_occlusion.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_occlusionRoughnessMetallic.dds b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_occlusionRoughnessMetallic.dds new file mode 100644 index 0000000..cb91cd0 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_occlusionRoughnessMetallic.dds differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_occlusionRoughnessMetallic.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_occlusionRoughnessMetallic.png new file mode 100644 index 0000000..c517cc3 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_occlusionRoughnessMetallic.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_roughnessMetallic.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_roughnessMetallic.png new file mode 100644 index 0000000..14ea254 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_roughnessMetallic.png differ diff --git a/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_specularGlossiness.png b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_specularGlossiness.png new file mode 100644 index 0000000..0b9fc37 Binary files /dev/null and b/glTF-Toolkit.Test/Resources/gltf/WaterBottle_ORM/WaterBottle_specularGlossiness.png differ diff --git a/glTF-Toolkit.Test/glTF-Toolkit.Test.vcxproj b/glTF-Toolkit.Test/glTF-Toolkit.Test.vcxproj new file mode 100644 index 0000000..7ff01f0 --- /dev/null +++ b/glTF-Toolkit.Test/glTF-Toolkit.Test.vcxproj @@ -0,0 +1,249 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + Create + Create + Create + Create + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {ff0275f1-58cb-4745-ba81-f6c1df66e206} + + + + 15.0 + {B2AF77B5-8433-46AD-860D-23A4831F6830} + Win32Proj + glTFToolkitTest + 10.0.16299.0 + NativeUnitTestProject + + + + DynamicLibrary + true + v141 + Unicode + false + + + DynamicLibrary + false + v141 + true + Unicode + false + + + DynamicLibrary + true + v141 + Unicode + false + + + DynamicLibrary + false + v141 + true + Unicode + false + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + true + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + true + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + true + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + + Use + Level4 + Disabled + $(SolutionDir)glTF-Toolkit\inc;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + WIN32;_DEBUG;%(PreprocessorDefinitions) + true + pch.h + stdcpp17 + /permissive- %(AdditionalOptions) + true + 4996 + + + Windows + d3d11.lib;dxgi.lib;pathcch.lib;%(AdditionalDependencies) + + + + + Use + Level4 + Disabled + $(SolutionDir)glTF-Toolkit\inc;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + _DEBUG;%(PreprocessorDefinitions) + true + pch.h + stdcpp17 + /permissive- %(AdditionalOptions) + true + 4996 + + + Windows + d3d11.lib;dxgi.lib;pathcch.lib;%(AdditionalDependencies) + + + + + Level4 + Use + MaxSpeed + true + true + $(SolutionDir)glTF-Toolkit\inc;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + WIN32;NDEBUG;%(PreprocessorDefinitions) + true + pch.h + stdcpp17 + /permissive- %(AdditionalOptions) + true + 4996 + + + Windows + true + true + d3d11.lib;dxgi.lib;pathcch.lib;%(AdditionalDependencies) + + + + + Level4 + Use + MaxSpeed + true + true + $(SolutionDir)glTF-Toolkit\inc;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + NDEBUG;%(PreprocessorDefinitions) + true + pch.h + stdcpp17 + /permissive- %(AdditionalOptions) + true + 4996 + + + Windows + true + true + d3d11.lib;dxgi.lib;pathcch.lib;%(AdditionalDependencies) + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/glTF-Toolkit.Test/glTF-Toolkit.Test.vcxproj.filters b/glTF-Toolkit.Test/glTF-Toolkit.Test.vcxproj.filters new file mode 100644 index 0000000..9c0e6f4 --- /dev/null +++ b/glTF-Toolkit.Test/glTF-Toolkit.Test.vcxproj.filters @@ -0,0 +1,119 @@ + + + + + + + + + + + + + {7294793d-6c71-438a-828f-68361c3db329} + + + {53c10f0a-e8fc-42e4-b612-17631dcd7755} + + + {29129a4d-ff37-427e-9047-6d1044de85b0} + + + {c7fc3736-b68e-4f6d-b93b-1bb4eda5b1ff} + + + + + Resources + + + Resources + + + + Resources\WaterBottle + + + Resources\WaterBottle + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + + + Resources\WaterBottle + + + Resources\WaterBottle + + + Resources\WaterBottle + + + Resources\WaterBottle + + + Resources\WaterBottle + + + Resources\WaterBottle + + + Resources\WaterBottle + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + Resources\WaterBottle_ORM + + + + + Helpers + + + Helpers + + + Helpers + + + \ No newline at end of file diff --git a/glTF-Toolkit.Test/packages.config b/glTF-Toolkit.Test/packages.config new file mode 100644 index 0000000..b98bb7f --- /dev/null +++ b/glTF-Toolkit.Test/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/glTF-Toolkit.sln b/glTF-Toolkit.sln new file mode 100644 index 0000000..b5dd055 --- /dev/null +++ b/glTF-Toolkit.sln @@ -0,0 +1,58 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2009 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "glTF-Toolkit", "glTF-Toolkit\glTF-Toolkit.vcxproj", "{FF0275F1-58CB-4745-BA81-F6C1DF66E206}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "glTF-Toolkit.Test", "glTF-Toolkit.Test\glTF-Toolkit.Test.vcxproj", "{B2AF77B5-8433-46AD-860D-23A4831F6830}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WindowsMRAssetConverter", "WindowsMRAssetConverter\WindowsMRAssetConverter.vcxproj", "{8A19D99C-78DC-4267-AB57-DB1DDBFBEFDF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E50FE4A3-A16D-4A05-A221-71CA2D972CF7}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FF0275F1-58CB-4745-BA81-F6C1DF66E206}.Debug|x64.ActiveCfg = Debug|x64 + {FF0275F1-58CB-4745-BA81-F6C1DF66E206}.Debug|x64.Build.0 = Debug|x64 + {FF0275F1-58CB-4745-BA81-F6C1DF66E206}.Debug|x86.ActiveCfg = Debug|Win32 + {FF0275F1-58CB-4745-BA81-F6C1DF66E206}.Debug|x86.Build.0 = Debug|Win32 + {FF0275F1-58CB-4745-BA81-F6C1DF66E206}.Release|x64.ActiveCfg = Release|x64 + {FF0275F1-58CB-4745-BA81-F6C1DF66E206}.Release|x64.Build.0 = Release|x64 + {FF0275F1-58CB-4745-BA81-F6C1DF66E206}.Release|x86.ActiveCfg = Release|Win32 + {FF0275F1-58CB-4745-BA81-F6C1DF66E206}.Release|x86.Build.0 = Release|Win32 + {B2AF77B5-8433-46AD-860D-23A4831F6830}.Debug|x64.ActiveCfg = Debug|x64 + {B2AF77B5-8433-46AD-860D-23A4831F6830}.Debug|x64.Build.0 = Debug|x64 + {B2AF77B5-8433-46AD-860D-23A4831F6830}.Debug|x86.ActiveCfg = Debug|Win32 + {B2AF77B5-8433-46AD-860D-23A4831F6830}.Debug|x86.Build.0 = Debug|Win32 + {B2AF77B5-8433-46AD-860D-23A4831F6830}.Release|x64.ActiveCfg = Release|x64 + {B2AF77B5-8433-46AD-860D-23A4831F6830}.Release|x64.Build.0 = Release|x64 + {B2AF77B5-8433-46AD-860D-23A4831F6830}.Release|x86.ActiveCfg = Release|Win32 + {B2AF77B5-8433-46AD-860D-23A4831F6830}.Release|x86.Build.0 = Release|Win32 + {8A19D99C-78DC-4267-AB57-DB1DDBFBEFDF}.Debug|x64.ActiveCfg = Debug|x64 + {8A19D99C-78DC-4267-AB57-DB1DDBFBEFDF}.Debug|x64.Build.0 = Debug|x64 + {8A19D99C-78DC-4267-AB57-DB1DDBFBEFDF}.Debug|x86.ActiveCfg = Debug|Win32 + {8A19D99C-78DC-4267-AB57-DB1DDBFBEFDF}.Debug|x86.Build.0 = Debug|Win32 + {8A19D99C-78DC-4267-AB57-DB1DDBFBEFDF}.Release|x64.ActiveCfg = Release|x64 + {8A19D99C-78DC-4267-AB57-DB1DDBFBEFDF}.Release|x64.Build.0 = Release|x64 + {8A19D99C-78DC-4267-AB57-DB1DDBFBEFDF}.Release|x86.ActiveCfg = Release|Win32 + {8A19D99C-78DC-4267-AB57-DB1DDBFBEFDF}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {04F37F7A-2349-4424-B26C-33237D33B0A4} + EndGlobalSection +EndGlobal diff --git a/glTF-Toolkit/glTF-Toolkit.vcxproj b/glTF-Toolkit/glTF-Toolkit.vcxproj new file mode 100644 index 0000000..f6b36d9 --- /dev/null +++ b/glTF-Toolkit/glTF-Toolkit.vcxproj @@ -0,0 +1,247 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {FF0275F1-58CB-4745-BA81-F6C1DF66E206} + Win32Proj + glTFToolkit + 10.0.16299.0 + + + + StaticLibrary + true + v141 + Unicode + + + StaticLibrary + false + v141 + true + Unicode + + + StaticLibrary + true + v141 + Unicode + + + StaticLibrary + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + true + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + false + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + false + $(SolutionDir)Built\Out\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(SolutionDir)Built\Int\$(PlatformToolset)\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + + + + Use + Level4 + Disabled + true + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + inc;%(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) + pch.h + stdcpp17 + /permissive- %(AdditionalOptions) + true + true + 4634;4996 + + + Windows + true + + + + + + + + + + + Use + Level4 + Disabled + true + _DEBUG;_LIB;%(PreprocessorDefinitions) + inc;%(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) + pch.h + stdcpp17 + /permissive- %(AdditionalOptions) + true + true + 4634;4996 + + + Windows + true + + + + + + + + + + + Use + Level4 + MaxSpeed + true + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + inc;%(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) + pch.h + stdcpp17 + /permissive- %(AdditionalOptions) + true + true + 4634;4996 + + + Windows + true + true + true + + + + + + + + + + + Use + Level4 + MaxSpeed + true + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + inc;%(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) + pch.h + stdcpp17 + /permissive- %(AdditionalOptions) + true + true + 4634;4996 + + + Windows + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/glTF-Toolkit/glTF-Toolkit.vcxproj.filters b/glTF-Toolkit/glTF-Toolkit.vcxproj.filters new file mode 100644 index 0000000..1322ab3 --- /dev/null +++ b/glTF-Toolkit/glTF-Toolkit.vcxproj.filters @@ -0,0 +1,69 @@ + + + + + + + + {2156fb82-8d5e-4cc3-b2ac-e13a18bc665f} + + + {620de9b6-9c44-44b6-9d93-99834c5efdf2} + + + + + inc + + + inc + + + inc + + + inc + + + inc + + + inc + + + inc + + + inc + + + inc + + + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + \ No newline at end of file diff --git a/glTF-Toolkit/inc/AccessorUtils.h b/glTF-Toolkit/inc/AccessorUtils.h new file mode 100644 index 0000000..e05a925 --- /dev/null +++ b/glTF-Toolkit/inc/AccessorUtils.h @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include +#include +#include + +namespace Microsoft::glTF::Toolkit +{ + /// + /// Utilities to manipulate accessors in a glTF asset. + /// + class AccessorUtils + { + public: + /// + /// Calculates the min and max values for an accessor according to the glTF 2.0 specification. + /// The accessor definition for which the min and max values will be calculated. + /// The raw data contained in the accessor. + /// + /// A pair containing the min and max vectors for the accessor, in that order. + /// + /// + template + static std::pair, std::vector> CalculateMinMax(const Accessor& accessor, const std::vector& accessorContents) + { + auto typeCount = Accessor::GetTypeCount(accessor.type); + auto min = std::vector(typeCount); + auto max = std::vector(typeCount); + + if (accessorContents.size() < typeCount) + { + throw std::invalid_argument("The accessor must contain data in order to calculate min and max."); + } + + // Initialize min and max with the first elements of the array + for (size_t j = 0; j < typeCount; j++) + { + auto current = static_cast(accessorContents[j]); + min[j] = current; + max[j] = current; + } + + for (size_t i = 1; i < accessor.count; i++) + { + for (size_t j = 0; j < typeCount; j++) + { + auto current = static_cast(accessorContents[i * typeCount + j]); + min[j] = std::min(min[j], current); + max[j] = std::max(max[j], current); + } + } + + return std::make_pair(min, max); + } + }; +} + diff --git a/glTF-Toolkit/inc/DeviceResources.h b/glTF-Toolkit/inc/DeviceResources.h new file mode 100644 index 0000000..71387a9 --- /dev/null +++ b/glTF-Toolkit/inc/DeviceResources.h @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +namespace DX +{ + /// + /// Controls all the DirectX device resources. + /// + class DeviceResources + { + public: + DeviceResources(D3D_FEATURE_LEVEL minFeatureLevel = D3D_FEATURE_LEVEL_10_0); + + void CreateDeviceResources(); + void HandleDeviceLost(); + + // Direct3D Accessors. + ID3D11Device1* GetD3DDevice() const { return m_d3dDevice.Get(); } + ID3D11DeviceContext1* GetD3DDeviceContext() const { return m_d3dContext.Get(); } + IDXGISwapChain1* GetSwapChain() const { return m_swapChain.Get(); } + D3D_FEATURE_LEVEL GetDeviceFeatureLevel() const { return m_d3dFeatureLevel; } + + private: + void GetHardwareAdapter(IDXGIAdapter1** ppAdapter); + + // Direct3D objects. + Microsoft::WRL::ComPtr m_d3dDevice; + Microsoft::WRL::ComPtr m_d3dContext; + Microsoft::WRL::ComPtr m_swapChain; + Microsoft::WRL::ComPtr m_d3dAnnotation; + + // Direct3D properties. + D3D_FEATURE_LEVEL m_d3dMinFeatureLevel; + + // Cached device properties. + D3D_FEATURE_LEVEL m_d3dFeatureLevel; + }; + + // Helper class for COM exceptions + class com_exception : public std::exception + { + public: + com_exception(HRESULT hr) : result(hr) {} + + virtual const char* what() const override + { + static char s_str[64] = { 0 }; + sprintf_s(s_str, "Failure with HRESULT of %08X", result); + return s_str; + } + + private: + HRESULT result; + }; + + // Helper utility converts D3D API failures into exceptions. + inline void ThrowIfFailed(HRESULT hr) + { + if (FAILED(hr)) + { + throw com_exception(hr); + } + } + +} \ No newline at end of file diff --git a/glTF-Toolkit/inc/GLBtoGLTF.h b/glTF-Toolkit/inc/GLBtoGLTF.h new file mode 100644 index 0000000..50d7d80 --- /dev/null +++ b/glTF-Toolkit/inc/GLBtoGLTF.h @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include +#include +#include +#include + +namespace Microsoft::glTF::Toolkit +{ + /// + /// Utilities to convert glTF-Binary files (GLB) to + /// unpacked glTF assets. + /// + class GLBToGLTF + { + public: + /// + /// Unpacks a GLB asset into a GLTF manifest and its + /// resources (bin files and images). + /// + /// The path to the GLB file to unpack. + /// The directory to which the glTF manifest and resources will be unpacked. + /// + /// The name of the output glTF manifest file, without the extension. + /// This name will be used as a prefix to all unpacked resources. + /// + static void UnpackGLB(std::string glbPath, std::string outDirectory, std::string gltfName); + + /// + /// Extracts the contents of all buffer views from a GLB file into a + /// byte vector that can be saves as a bin file to be used in a glTF file. + /// + /// A stream pointing to the GLB file. + /// The manifest describing the GLB asset. + /// The offset on the input file where the GLB buffer starts. + /// The length of the new buffer (sum of all buffer view lengths). + /// + /// The binary content of the buffer views as a vector. + /// + static std::vector SaveBin(std::istream* in, const Microsoft::glTF::GLTFDocument& glbDoc, const size_t bufferOffset, const size_t newBufferlength); + + /// + /// Loads all images in a glTF-Binary (GLB) asset into a map relating each image identifier to the contents of that image. + /// + /// A stream pointing to the GLB file. + /// The manifest describing the GLB asset. + /// The name that should be used when creating the identifiers for the image files. + /// The offset on the input file where the GLB buffer starts. + /// + /// A map relating each image identifier to the contents of that image. + /// + static std::unordered_map> GetImagesData(std::istream* in, const Microsoft::glTF::GLTFDocument& glbDoc, const std::string& name, const size_t bufferOffset); + + /// + /// Creates the glTF manifest that represents a GLB file after unpacking. + /// + /// The original manifest contained in the GLB file. + /// The name that should be used when creating the identifiers for the image and bin files when unpacking. + /// + /// A new glTF manifest that represents the same file, but with images and resources referenced by URI instead of embedded ina GLB buffer. + /// + static Microsoft::glTF::GLTFDocument CreateGLTFDocument(const Microsoft::glTF::GLTFDocument& glbDoc, const std::string& name); + }; +} diff --git a/glTF-Toolkit/inc/GLTFLODUtils.h b/glTF-Toolkit/inc/GLTFLODUtils.h new file mode 100644 index 0000000..eb7eacf --- /dev/null +++ b/glTF-Toolkit/inc/GLTFLODUtils.h @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include + +namespace Microsoft::glTF::Toolkit +{ + extern const char* EXTENSION_MSFT_LOD; + extern const char* MSFT_LOD_IDS_KEY; + typedef std::unordered_map>> LODMap; + + /// + /// Utilities to load and merge levels of detail (LOD) in glTF assets using the MSFT_lod extension. + /// + class GLTFLODUtils + { + public: + /// + /// Parses the node LODs in a GLTF asset as a map that can be used to read LOD values for each node. + /// + /// A map that relates each node ID to a vector of its levels of detail node IDs. + /// The glTF document containing LODs to be parsed. + static LODMap ParseDocumentNodeLODs(const GLTFDocument& doc); + + /// + /// Inserts each LOD GLTFDocument as a node LOD (at the root level) of the specified primary GLTF asset. + /// Note: Animation is not currently supported. + /// + /// The primary GLTF Document with the inserted LOD node. + /// A vector of glTF documents to merge as LODs. The first element of the vector is assumed to be the primary LOD. + static GLTFDocument MergeDocumentsAsLODs(const std::vector& docs); + + /// + /// Inserts each LOD GLTFDocument as a node LOD (at the root level) of the specified primary GLTF asset. + /// Note: Animation is not currently supported. + /// + /// The primary GLTF Document with the inserted LOD node. + /// A vector of glTF documents to merge as LODs. The first element of the vector is assumed to be the primary LOD. + /// A vector with the screen coverage percentages corresponding to each LOD. If the size of this + /// vector is larger than the size of , lower coverage values will cause the asset to be invisible. + static GLTFDocument MergeDocumentsAsLODs(const std::vector& docs, const std::vector& screenCoveragePercentages); + + /// + /// Determines the highest number of Node LODs for a given glTF asset. + /// + /// The glTF asset for which to count the max number of node LODs. + /// A map containing the parsed node LODs in the document. + /// The highest number of Node LODs in the asset. + static uint32_t NumberOfNodeLODLevels(const GLTFDocument& doc, const LODMap& lods); + }; +} + diff --git a/glTF-Toolkit/inc/GLTFTextureCompressionUtils.h b/glTF-Toolkit/inc/GLTFTextureCompressionUtils.h new file mode 100644 index 0000000..2d78924 --- /dev/null +++ b/glTF-Toolkit/inc/GLTFTextureCompressionUtils.h @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include +#include + +namespace DirectX +{ + class ScratchImage; +} + +namespace Microsoft::glTF::Toolkit +{ + extern const char* EXTENSION_MSFT_TEXTURE_DDS; + + /// Supported compression algorithms for textures. + enum class TextureCompression + { + None, + BC3, + BC5, + BC7, + }; + + /// + /// Utilities to compress textures in a glTF asset. + /// + class GLTFTextureCompressionUtils + { + public: + /// Compresses a texture in a glTF from a WIC-readable format (PNG, JPEG, BMP, GIF, TIFF, HD Photo, ICO) + /// into a DDS with the appropriate compression. + /// If a dds extension already exists for this texture, do nothing. + /// The stream reader that will be used to get streams to each image from its URI. + /// Input glTF document. + /// Texture object that is contained in input document. If texture does not exist in document, + /// throws exception. + /// The desired block compression method (e.g. BC5, BC7). + /// The output directory to which compressed files should be saved. + /// The maximum texture size to which textures should be resized before compression, in pixels. + /// If true, also generates mip maps when compressing. + /// If true, retains the original image on the resulting glTF. If false, + /// replaces that image (making the glTF incompatible with most core glTF 2.0 viewers). + /// Returns a new GLTFDocument that contains a new reference to the compressed dds file added as part + /// of the MSFT_texture_dds extension. + /// + /// Example Input: + /// + /// "textures": [ + /// { + /// "source": 0, + /// } + /// ], + /// "images": [ + /// { + /// "uri": "defaultTexture.png" + /// } + /// ] + /// + /// + /// Example Output (BC7 Compression, with retainOriginalImage == true): + /// + /// "textures": + /// [ + /// { + /// "source": 0, + /// "extensions": { + /// "MSFT_texture_dds": { + /// "source": 1 + /// } + /// } + /// } + /// ], + /// "images": [ + /// { + /// "uri": "defaultTexture.png" + /// }, + /// { + /// "uri": "defaultTexture-BC7.DDS" + /// } + /// ] + /// + /// + /// + static GLTFDocument CompressTextureAsDDS(const IStreamReader& streamReader, const GLTFDocument & doc, const Texture & texture, TextureCompression compression, const std::string& outputDirectory, size_t maxTextureSize = std::numeric_limits::max(), bool generateMipMaps = true, bool retainOriginalImage = true); + + /// + /// Applies to all textures in the document that are accessible via materials according to the + /// requirements of the Windows Mixed Reality home. + /// Normal textures get compressed with BC5, while baseColorTexture, occlusion, metallicRoughness and emissive textures get compressed with BC7. + /// The stream reader that will be used to get streams to each image from its URI. + /// Input glTF document. + /// The output directory to which compressed files should be saved. + /// The maximum texture size to which textures should be resized before compression, in pixels. + /// If true, also generates mip maps when compressing. + /// If true, retains the original image on the resulting glTF. If false, + /// replaces that image (making the glTF incompatible with most core glTF 2.0 viewers). + /// Returns a new GLTFDocument that contains alternate textures for all applicable materials following the requirements of the Windows + /// Mixed Reality home using the MSFT_texture_dds extension. + /// + static GLTFDocument CompressAllTexturesForWindowsMR(const IStreamReader& streamReader, const GLTFDocument & doc, const std::string& outputDirectory, size_t maxTextureSize = std::numeric_limits::max(), bool retainOriginalImages = true); + + /// + /// Compresses a DirectX::ScratchImage in place using the specified compression. + /// + /// The image to compress. + /// The desired compression algorithm. + static void CompressImage(DirectX::ScratchImage& image, TextureCompression compression); + }; +} \ No newline at end of file diff --git a/glTF-Toolkit/inc/GLTFTextureLoadingUtils.h b/glTF-Toolkit/inc/GLTFTextureLoadingUtils.h new file mode 100644 index 0000000..ff10ed2 --- /dev/null +++ b/glTF-Toolkit/inc/GLTFTextureLoadingUtils.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information.#pragma once + +#include +#include +#include + +namespace Microsoft::glTF::Toolkit +{ + /// + /// Utilities to load textures from glTF assets. + /// + class GLTFTextureLoadingUtils + { + public: + /// + /// Loads a texture into a scratch image in the DXGI_FORMAT_R32G32B32A32_FLOAT format for in-memory processing. + /// + /// A scratch image containing the loaded texture in the DXGI_FORMAT_R32G32B32A32_FLOAT format. + /// A stream reader that is capable of accessing the resources used in the glTF asset by URI. + /// The document from which the texture will be loaded. + /// The identifier of the texture to be loaded. + static DirectX::ScratchImage LoadTexture(const IStreamReader& streamReader, const GLTFDocument& doc, const std::string& textureId); + }; +} + diff --git a/glTF-Toolkit/inc/GLTFTexturePackingUtils.h b/glTF-Toolkit/inc/GLTFTexturePackingUtils.h new file mode 100644 index 0000000..462ea86 --- /dev/null +++ b/glTF-Toolkit/inc/GLTFTexturePackingUtils.h @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include + +namespace Microsoft::glTF::Toolkit +{ + extern const char* EXTENSION_MSFT_PACKING_ORM; + + /// Texture packing flags. May be combined to pack multiple formats at once. + enum TexturePacking + { + None = 0x0, + OcclusionRoughnessMetallic = 0x1, + RoughnessMetallicOcclusion = 0x2 + }; + + /// + /// Utilities to pack textures from glTF assets and refer to them from an asset + /// using the MSFT_packing_occlusionRoughnessMetallic extension. + /// + class GLTFTexturePackingUtils + { + public: + /// + /// Packs a single material's textures for Windows Mixed Reality for all the packing schemes selected, and adds the resulting texture(s) back to the material in the document. + /// + /// A stream reader that is capable of accessing the resources used in the glTF asset by URI. + /// The document from which the texture will be loaded. + /// The material to be packed. + /// The packing scheme that will be used to pick the textures and choose their order. + /// The output directory to which packed textures should be saved. + /// + /// A new glTF manifest that uses the MSFT_packing_occlusionRoughnessMetallic extension to point to the packed textures. + /// + static GLTFDocument PackMaterialForWindowsMR(const IStreamReader& streamReader, const GLTFDocument & doc, const Material & material, TexturePacking packing, const std::string& outputDirectory); + + /// + /// Applies to every material in the document, following the same parameter structure as that function. + /// + /// A stream reader that is capable of accessing the resources used in the glTF asset by URI. + /// The document from which the texture will be loaded. + /// The packing scheme that will be used to pick the textures and choose their order. + /// The output directory to which packed textures should be saved. + /// + /// A new glTF manifest that uses the MSFT_packing_occlusionRoughnessMetallic extension to point to the packed textures. + /// + static GLTFDocument PackAllMaterialsForWindowsMR(const IStreamReader& streamReader, const GLTFDocument & doc, TexturePacking packing, const std::string& outputDirectory); + }; +} + diff --git a/glTF-Toolkit/inc/SerializeBinary.h b/glTF-Toolkit/inc/SerializeBinary.h new file mode 100644 index 0000000..62916e3 --- /dev/null +++ b/glTF-Toolkit/inc/SerializeBinary.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information.#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "AccessorUtils.h" + +namespace Microsoft::glTF::Toolkit +{ + /// + /// A function that determines to which type an accessor should be converted, + /// based on the accessor metadata. + /// + typedef std::function AccessorConversionStrategy; + + /// + /// Serializes a glTF asset as a glTF binary (GLB) file. + /// + /// The glTF asset manifest to be serialized. + /// A stream reader that is capable of accessing the resources used in the glTF asset by URI. + /// A stream factory that is capable of creating an output stream where the GLB will be saved, and a temporary stream for + /// use during the serialization process. + void SerializeBinary(const GLTFDocument& gltfDocument, const IStreamReader& inputStreamReader, std::unique_ptr& outputStreamFactory, const AccessorConversionStrategy& accessorConversion = nullptr); +} diff --git a/glTF-Toolkit/inc/pch.h b/glTF-Toolkit/inc/pch.h new file mode 100644 index 0000000..4c60be5 --- /dev/null +++ b/glTF-Toolkit/inc/pch.h @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +// Use the C++ standard templated min/max +#define NOMINMAX + +// DirectX apps don't need GDI +#define NODRAWTEXT +#define NOGDI +#define NOBITMAP + +// Include if you need this +#define NOMCX + +// Include if you need this +#define NOSERVICE + +// WinHelp is deprecated +#define NOHELP + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include \ No newline at end of file diff --git a/glTF-Toolkit/packages.config b/glTF-Toolkit/packages.config new file mode 100644 index 0000000..b98bb7f --- /dev/null +++ b/glTF-Toolkit/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/glTF-Toolkit/src/DeviceResources.cpp b/glTF-Toolkit/src/DeviceResources.cpp new file mode 100644 index 0000000..0e5ae15 --- /dev/null +++ b/glTF-Toolkit/src/DeviceResources.cpp @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" + +#include "DeviceResources.h" + +using namespace DirectX; +using namespace DX; + +using Microsoft::WRL::ComPtr; + +namespace +{ +#if defined(_DEBUG) + // Check for SDK Layer support. + inline bool SdkLayersAvailable() + { + HRESULT hr = D3D11CreateDevice( + nullptr, + D3D_DRIVER_TYPE_NULL, // There is no need to create a real hardware device. + 0, + D3D11_CREATE_DEVICE_DEBUG, // Check for the SDK layers. + nullptr, // Any feature level will do. + 0, + D3D11_SDK_VERSION, + nullptr, // No need to keep the D3D device reference. + nullptr, // No need to know the feature level. + nullptr // No need to keep the D3D device context reference. + ); + + return SUCCEEDED(hr); + } +#endif +}; + +// Constructor for DeviceResources. +DeviceResources::DeviceResources(D3D_FEATURE_LEVEL minFeatureLevel) : + m_d3dMinFeatureLevel(minFeatureLevel), + m_d3dFeatureLevel(D3D_FEATURE_LEVEL_9_1) +{ +} + +// Configures the Direct3D device, and stores handles to it and the device context. +void DeviceResources::CreateDeviceResources() +{ + UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; + +#if defined(_DEBUG) + if (SdkLayersAvailable()) + { + // If the project is in a debug build, enable debugging via SDK Layers with this flag. + creationFlags |= D3D11_CREATE_DEVICE_DEBUG; + } + else + { + OutputDebugStringA("WARNING: Direct3D Debug Device is not available\n"); + } +#endif + + // Determine DirectX hardware feature levels this app will support. + static const D3D_FEATURE_LEVEL s_featureLevels[] = + { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL_9_1, + }; + + UINT featLevelCount = 0; + for (; featLevelCount < _countof(s_featureLevels); ++featLevelCount) + { + if (s_featureLevels[featLevelCount] < m_d3dMinFeatureLevel) + break; + } + + if (!featLevelCount) + { + throw std::out_of_range("minFeatureLevel too high"); + } + + ComPtr adapter; + GetHardwareAdapter(adapter.GetAddressOf()); + + // Create the Direct3D 11 API device object and a corresponding context. + ComPtr device; + ComPtr context; + + HRESULT hr = E_FAIL; + if (adapter) + { + hr = D3D11CreateDevice( + adapter.Get(), + D3D_DRIVER_TYPE_UNKNOWN, + 0, + creationFlags, + s_featureLevels, + featLevelCount, + D3D11_SDK_VERSION, + device.GetAddressOf(), // Returns the Direct3D device created. + &m_d3dFeatureLevel, // Returns feature level of device created. + context.GetAddressOf() // Returns the device immediate context. + ); + } +#if defined(NDEBUG) + else + { + throw std::exception("No Direct3D hardware device found"); + } +#else + if (FAILED(hr)) + { + // If the initialization fails, fall back to the WARP device. + // For more information on WARP, see: + // http://go.microsoft.com/fwlink/?LinkId=286690 + hr = D3D11CreateDevice( + nullptr, + D3D_DRIVER_TYPE_WARP, // Create a WARP device instead of a hardware device. + 0, + creationFlags, + s_featureLevels, + featLevelCount, + D3D11_SDK_VERSION, + device.GetAddressOf(), + &m_d3dFeatureLevel, + context.GetAddressOf() + ); + + if (SUCCEEDED(hr)) + { + OutputDebugStringA("Direct3D Adapter - WARP\n"); + } + } +#endif + + ThrowIfFailed(hr); + +#ifndef NDEBUG + ComPtr d3dDebug; + if (SUCCEEDED(device.As(&d3dDebug))) + { + ComPtr d3dInfoQueue; + if (SUCCEEDED(d3dDebug.As(&d3dInfoQueue))) + { +#ifdef _DEBUG + d3dInfoQueue->SetBreakOnSeverity(D3D11_MESSAGE_SEVERITY_CORRUPTION, true); + d3dInfoQueue->SetBreakOnSeverity(D3D11_MESSAGE_SEVERITY_ERROR, true); +#endif + D3D11_MESSAGE_ID hide [] = + { + D3D11_MESSAGE_ID_SETPRIVATEDATA_CHANGINGPARAMS, + }; + D3D11_INFO_QUEUE_FILTER filter = {}; + filter.DenyList.NumIDs = _countof(hide); + filter.DenyList.pIDList = hide; + d3dInfoQueue->AddStorageFilterEntries(&filter); + } + } +#endif + + ThrowIfFailed(device.As(&m_d3dDevice)); + ThrowIfFailed(context.As(&m_d3dContext)); + ThrowIfFailed(context.As(&m_d3dAnnotation)); +} + +// Recreate all device resources and set them back to the current state. +void DeviceResources::HandleDeviceLost() +{ + m_swapChain.Reset(); + m_d3dContext.Reset(); + m_d3dAnnotation.Reset(); + +#ifdef _DEBUG + { + ComPtr d3dDebug; + if (SUCCEEDED(m_d3dDevice.As(&d3dDebug))) + { + d3dDebug->ReportLiveDeviceObjects(D3D11_RLDO_SUMMARY); + } + } +#endif + + m_d3dDevice.Reset(); + + CreateDeviceResources(); +} + +// This method acquires the first available hardware adapter. +// If no such adapter can be found, *ppAdapter will be set to nullptr. +void DeviceResources::GetHardwareAdapter(IDXGIAdapter1** ppAdapter) +{ + *ppAdapter = nullptr; + + ComPtr dxgiFactory; + ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(dxgiFactory.GetAddressOf()))); + + ComPtr adapter; + for (UINT adapterIndex = 0; DXGI_ERROR_NOT_FOUND != dxgiFactory->EnumAdapters1(adapterIndex, adapter.ReleaseAndGetAddressOf()); adapterIndex++) + { + DXGI_ADAPTER_DESC1 desc; + adapter->GetDesc1(&desc); + + if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) + { + // Don't select the Basic Render Driver adapter. + continue; + } + +#ifdef _DEBUG + wchar_t buff[256] = {}; + swprintf_s(buff, L"Direct3D Adapter (%u): VID:%04X, PID:%04X - %ls\n", adapterIndex, desc.VendorId, desc.DeviceId, desc.Description); + OutputDebugStringW(buff); +#endif + + break; + } + + *ppAdapter = adapter.Detach(); +} \ No newline at end of file diff --git a/glTF-Toolkit/src/GLBtoGLTF.cpp b/glTF-Toolkit/src/GLBtoGLTF.cpp new file mode 100644 index 0000000..e4752d7 --- /dev/null +++ b/glTF-Toolkit/src/GLBtoGLTF.cpp @@ -0,0 +1,326 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" +#include "GLBtoGLTF.h" + +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +namespace +{ + class StreamMock : public IStreamReader + { + public: + StreamMock() : m_stream(std::make_shared(std::ios_base::app | std::ios_base::binary | std::ios_base::in | std::ios_base::out)) + { + } + + std::shared_ptr GetInputStream(const std::string&) const override + { + return m_stream; + } + + private: + std::shared_ptr m_stream; + }; + + size_t GetGLBBufferChunkOffset(std::ifstream* input) + { + // get offset from beginning of glb binary to beginning of buffer chunk + input->seekg(GLB2_HEADER_BYTE_SIZE, std::ios::beg); + uint32_t length = 0; + for (int i = 0; i < GLB_CHUNK_TYPE_SIZE*CHAR_BIT; i += CHAR_BIT) + { + uint8_t c = static_cast(input->get()); + length |= ((uint16_t)c << i); + } + // 28 is total length of non-json blocks from start of glb blob + // 28 = (GLB2_HEADER_BYTE_SIZE = 12bytes) + (uint32 = 4bytes) * 4 + return length + GLB2_HEADER_BYTE_SIZE + GLB_CHUNK_TYPE_SIZE * 4; + } +} + +std::vector GLBToGLTF::SaveBin(std::istream* input, const GLTFDocument& glbDoc, const size_t bufferOffset, const size_t newBufferlength) +{ + if (newBufferlength == 0) + { + return {}; + } + + const auto images = glbDoc.images.Elements(); + const auto bufferViews = glbDoc.bufferViews.Elements(); + std::unordered_set imagesBufferViews; + for (const auto& im : images) + { + // save a copy of image buffer view IDs + imagesBufferViews.insert(im.bufferViewId); + } + + // gather all non-image bufferViews in UsedBufferViews + std::vector usedBufferViews(bufferViews.size()); + auto last = copy_if(bufferViews.begin(), bufferViews.end(), usedBufferViews.begin(), [imagesBufferViews](const auto& a) + { + return imagesBufferViews.count(a.id) == 0; + }); + usedBufferViews.resize(distance(usedBufferViews.begin(), last)); + + // sort buffer views by offset + sort(usedBufferViews.begin(), usedBufferViews.end(), [](const BufferView& a, const BufferView& b) + { + return a.byteOffset < b.byteOffset; + }); + + std::vector result(newBufferlength); + size_t vecpos = 0; // number of chunks read + size_t currOffset = bufferOffset; // offset into buffer + input->seekg(bufferOffset, std::ios::beg); + + for (const auto& bufferView : usedBufferViews) + { + // traverse through original buffer while grabbing non-image buffer segments + size_t nextOffset = bufferOffset + bufferView.byteOffset; + if (currOffset < nextOffset) + { + // skip over buffer segments of no interest + size_t chunkLength = nextOffset - currOffset; + input->seekg(chunkLength, std::ios::cur); + currOffset += chunkLength; + } + + if (vecpos % GLB_BUFFER_OFFSET_ALIGNMENT != 0) + { + // Alignment padding + // Accessor component sizes can be 1, 2, 4. + // Aligning to 4 will satisfy requirements but wastes space + vecpos += (GLB_BUFFER_OFFSET_ALIGNMENT - (vecpos % GLB_BUFFER_OFFSET_ALIGNMENT)); + } + + // read and increment vecpos + offset + input->read(&result[vecpos], bufferView.byteLength); + currOffset += bufferView.byteLength; + vecpos += bufferView.byteLength; + } + + if (vecpos == 0) + { + return {}; + } + + return result; +} + +std::unordered_map> GLBToGLTF::GetImagesData(std::istream* input, const GLTFDocument& glbDoc, const std::string& name, const size_t bufferOffset) +{ + input->seekg(0, std::ios::beg); + std::unordered_map imageIDs; + std::vector images = std::vector(glbDoc.images.Elements()); + if (images.size() == 0) + { + return {}; + } + + int imgId = 0; + for (const auto& img : images) + { + // save mapping of original image order + imageIDs[img.bufferViewId] = imgId; + imgId++; + } + + // sort images by buffer offset so only traverse once + sort(images.begin(), images.end(), [glbDoc](const auto& a, const auto& b) + { + return glbDoc.bufferViews.Get(a.bufferViewId).byteOffset < glbDoc.bufferViews.Get(b.bufferViewId).byteOffset; + }); + + size_t currOffset = bufferOffset; // offset into buffer + input->seekg(bufferOffset, std::ios::beg); + + std::unordered_map> imageStream; + for (const auto& img : images) + { + // traverse through buffer while saving images + auto bufferView = glbDoc.bufferViews.Get(img.bufferViewId); + size_t nextImageOffset = bufferOffset + bufferView.byteOffset; + if (currOffset < nextImageOffset) + { + // skip over non-image buffer segments + size_t chunkLength = nextImageOffset - currOffset; + input->seekg(chunkLength, std::ios::cur); + currOffset = nextImageOffset; + } + // read and increment offset + std::vector result; + result.resize(bufferView.byteLength); + input->read(&result[0], bufferView.byteLength); + currOffset += bufferView.byteLength; + + // write image file + std::string outname; + if (img.mimeType == MIMETYPE_PNG) + { + outname = name + "_image" + std::to_string(imageIDs[img.bufferViewId]) + "." + FILE_EXT_PNG; + } + else if (img.mimeType == MIMETYPE_JPEG) + { + outname = name + "_image" + std::to_string(imageIDs[img.bufferViewId]) + "." + FILE_EXT_JPEG; + } + else + { + // unknown mimetype + outname = name + "_image" + std::to_string(imageIDs[img.bufferViewId]); + } + + imageStream[outname] = result; + } + return imageStream; +} + +// Create modified gltf from original by removing image buffer segments and updating +// images, bufferViews and accessors fields accordingly +GLTFDocument GLBToGLTF::CreateGLTFDocument(const GLTFDocument& glbDoc, const std::string& name) +{ + GLTFDocument gltfDoc(glbDoc); + + gltfDoc.images.Clear(); + gltfDoc.buffers.Clear(); + gltfDoc.bufferViews.Clear(); + gltfDoc.accessors.Clear(); + + const auto images = glbDoc.images.Elements(); + const auto buffers = glbDoc.buffers.Elements(); + const auto bufferViews = glbDoc.bufferViews.Elements(); + const auto accessors = glbDoc.accessors.Elements(); + std::unordered_set imagesBufferViews; + std::unordered_map bufferViewIndex; + + size_t updatedBufferSize = 0; + for (const auto& im : images) + { + // find which buffer segments correspond to images + imagesBufferViews.insert(im.bufferViewId); + } + + // gather all non-image bufferViews in UsedBufferViews + std::vector usedBufferViews(bufferViews.size()); + auto last = copy_if(bufferViews.begin(), bufferViews.end(), usedBufferViews.begin(), [imagesBufferViews](const auto& a) + { + return imagesBufferViews.count(a.id) == 0; + }); + + usedBufferViews.resize(distance(usedBufferViews.begin(), last)); + + // group buffer views by buffer, then sort them by byteOffset to calculate their new byteOffsets + sort(usedBufferViews.begin(), usedBufferViews.end(), [](const auto& a, const auto& b) + { + return a.byteOffset < b.byteOffset; + }); + + int updatedBufferViewId = 0; + size_t currentOffset = 0; + for (const auto& b : usedBufferViews) + { + // provide new byte ranges for bufferviews + size_t padding = 0; + auto updatedBufferView = b; + updatedBufferView.id = std::to_string(updatedBufferViewId); + + if (currentOffset % GLB_BUFFER_OFFSET_ALIGNMENT != 0) + { + // alignment padding as in SaveBin + padding = (GLB_BUFFER_OFFSET_ALIGNMENT - (currentOffset % GLB_BUFFER_OFFSET_ALIGNMENT)); + currentOffset += padding; + } + + updatedBufferView.byteOffset = currentOffset; + currentOffset += b.byteLength; + gltfDoc.bufferViews.Append(std::move(updatedBufferView)); + bufferViewIndex[b.id] = std::to_string(updatedBufferViewId); + updatedBufferSize += (b.byteLength + padding); + updatedBufferViewId++; + } + + if (!buffers.empty()) + { + auto updatedBuffer = buffers[0]; + updatedBuffer.byteLength = updatedBufferSize; + updatedBuffer.uri = name + "." + BUFFER_EXTENSION; + gltfDoc.buffers.Append(std::move(updatedBuffer)); + } + + for (const auto& a : accessors) + { + if (imagesBufferViews.find(a.bufferViewId) == imagesBufferViews.end()) + { + // update acessors with new bufferview IDs, the above check may not be needed + auto updatedAccessor = a; + updatedAccessor.bufferViewId = bufferViewIndex[a.bufferViewId]; + gltfDoc.accessors.Append(std::move(updatedAccessor)); + } + } + + int imgId = 0; + for (const auto& im : images) + { + // update image fields with image names instead of buffer views + Image updatedImage; + updatedImage.id = std::to_string(imgId); + if (im.mimeType == MIMETYPE_PNG) + { + updatedImage.uri = name + "_image" + std::to_string(imgId) + "." + FILE_EXT_PNG; + } + else if (im.mimeType == MIMETYPE_JPEG) + { + updatedImage.uri = name + "_image" + std::to_string(imgId) + "." + FILE_EXT_JPEG; + } + else + { + // unknown mimetype + updatedImage.uri = name + "_image" + std::to_string(imgId); + } + + gltfDoc.images.Append(std::move(updatedImage)); + imgId++; + } + + return gltfDoc; +} + +void GLBToGLTF::UnpackGLB(std::string glbPath, std::string outDirectory, std::string gltfName) +{ + // read glb file into json + auto glbStream = std::make_shared(glbPath, std::ios::binary); + auto streamReader = std::make_unique(); + GLBResourceReader reader(*streamReader, glbStream); + + // get original json + auto json = reader.GetJson(); + auto doc = DeserializeJson(json); + + // create new modified json + auto gltfDoc = GLBToGLTF::CreateGLTFDocument(doc, gltfName); + + // serialize and write new gltf json + auto gltfJson = Serialize(gltfDoc); + std::ofstream outputStream(outDirectory + gltfName + "." + GLTF_EXTENSION); + outputStream << gltfJson; + outputStream.flush(); + + // write images + size_t bufferOffset = GetGLBBufferChunkOffset(glbStream.get()); + for (auto image : GLBToGLTF::GetImagesData(glbStream.get(), doc, gltfName, bufferOffset)) + { + std::ofstream out(outDirectory + image.first, std::ios::binary); + out.write(&image.second[0], image.second.size()); + } + + // get new buffer size and write new buffer + if (gltfDoc.buffers.Size() != 0) + { + size_t newBufferSize = gltfDoc.buffers[0].byteLength; + auto binFileData = GLBToGLTF::SaveBin(glbStream.get(), doc, bufferOffset, newBufferSize); + std::ofstream out(outDirectory + gltfName + "." + BUFFER_EXTENSION, std::ios::binary); + out.write(&binFileData[0], binFileData.size()); + } +} diff --git a/glTF-Toolkit/src/GLTFLODUtils.cpp b/glTF-Toolkit/src/GLTFLODUtils.cpp new file mode 100644 index 0000000..9a1c2f0 --- /dev/null +++ b/glTF-Toolkit/src/GLTFLODUtils.cpp @@ -0,0 +1,450 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" + +#include "GLTFTextureCompressionUtils.h" +#include "GLTFTexturePackingUtils.h" +#include "GLTFLODUtils.h" + +#include "GLTFSDK/GLTF.h" +#include "GLTFSDK/GLTFConstants.h" +#include "GLTFSDK/Deserialize.h" +#include "GLTFSDK/RapidJsonUtils.h" +#include "GLTFSDK/Schema.h" + +#include +#include +#include + +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +const char* Microsoft::glTF::Toolkit::EXTENSION_MSFT_LOD = "MSFT_lod"; +const char* Microsoft::glTF::Toolkit::MSFT_LOD_IDS_KEY = "ids"; + +namespace +{ + inline void AddIndexOffset(std::string& id, size_t offset) + { + // an empty id string indicates that the id is not inuse and therefore should not be updated + id = (id.empty()) ? "" : std::to_string(std::stoi(id) + offset); + } + + inline void AddIndexOffsetPacked(rapidjson::Value& json, const char* textureId, size_t offset) + { + if (json.HasMember(textureId)) + { + if (json[textureId].HasMember("index")) + { + auto index = json[textureId]["index"].GetInt(); + json[textureId]["index"] = index + offset; + } + } + } + + std::vector ParseExtensionMSFTLod(const Node& node) + { + std::vector lodIds; + + auto lodExtension = node.extensions.find(Toolkit::EXTENSION_MSFT_LOD); + if (lodExtension != node.extensions.end()) + { + auto json = RapidJsonUtils::CreateDocumentFromString(lodExtension->second); + + auto idIt = json.FindMember(Toolkit::MSFT_LOD_IDS_KEY); + if (idIt != json.MemberEnd()) + { + for (rapidjson::Value::ConstValueIterator ait = idIt->value.Begin(); ait != idIt->value.End(); ++ait) + { + lodIds.push_back(std::to_string(ait->GetInt())); + } + } + } + + return lodIds; + } + + template + std::string SerializeExtensionMSFTLod(const T&, const std::vector& lods, const GLTFDocument& gltfDocument) + { + // Omit MSFT_lod entirely if no LODs are available + if (lods.empty()) + { + return std::string(); + } + + rapidjson::Document doc(rapidjson::kObjectType); + rapidjson::Document::AllocatorType& a = doc.GetAllocator(); + + std::vector lodIndices; + lodIndices.reserve(lods.size()); + + if (std::is_same()) + { + for (const auto& lodId : lods) + { + lodIndices.push_back(ToKnownSizeType(gltfDocument.materials.GetIndex(lodId))); + } + } + else if (std::is_same()) + { + for (const auto& lodId : lods) + { + lodIndices.push_back(ToKnownSizeType(gltfDocument.nodes.GetIndex(lodId))); + } + } + else + { + throw GLTFException("LODs can only be applied to materials or nodes."); + } + + doc.AddMember(RapidJsonUtils::ToStringValue(Toolkit::MSFT_LOD_IDS_KEY, a), RapidJsonUtils::ToJsonArray(lodIndices, a), a); + + rapidjson::StringBuffer stringBuffer; + rapidjson::Writer writer(stringBuffer); + doc.Accept(writer); + + return stringBuffer.GetString(); + } + + GLTFDocument AddGLTFNodeLOD(const GLTFDocument& primary, LODMap& primaryLods, const GLTFDocument& lod) + { + Microsoft::glTF::GLTFDocument gltfLod(primary); + + auto primaryScenes = primary.scenes.Elements(); + auto lodScenes = lod.scenes.Elements(); + + size_t MaxLODLevel = 0; + + // Both GLTF must have equivalent number and order of scenes and root nodes per scene otherwise merge will not be possible + bool sceneNodeMatch = false; + if (primaryScenes.size() == lodScenes.size()) + { + for (size_t sceneIdx = 0; sceneIdx < primaryScenes.size(); sceneIdx++) + { + + if ((primaryScenes[sceneIdx].nodes.size() == lodScenes[sceneIdx].nodes.size()) && + (lodScenes[sceneIdx].nodes.size() == 1 || + std::equal(primaryScenes[sceneIdx].nodes.begin(), primaryScenes[sceneIdx].nodes.end(), lodScenes[sceneIdx].nodes.begin())) + ) + { + sceneNodeMatch = true; + auto primaryRootNode = gltfLod.nodes.Get(primaryScenes[sceneIdx].nodes[0]); + MaxLODLevel = std::max(MaxLODLevel, primaryLods.at(primaryRootNode.id)->size()); + } + else + { + sceneNodeMatch = false; + break; + } + } + } + + MaxLODLevel++; + + if (!sceneNodeMatch || primaryScenes.empty()) + { + // Mis-match or empty scene; either way cannot merge Lod in + throw new std::runtime_error("Primary Scene either empty or does not match scene node count of LOD gltf"); + } + + std::string nodeLodLabel = "_lod" + std::to_string(MaxLODLevel); + + // lod merge is performed from the lowest reference back upwards + // e.g. buffers/samplers/extensions do not reference any other part of the gltf manifest + size_t buffersOffset = gltfLod.buffers.Size(); + size_t samplersOffset = gltfLod.samplers.Size(); + { + auto lodBuffers = lod.buffers.Elements(); + for (auto buffer : lodBuffers) + { + AddIndexOffset(buffer.id, buffersOffset); + gltfLod.buffers.Append(std::move(buffer)); + } + + auto lodSamplers = lod.samplers.Elements(); + for (auto sampler : lodSamplers) + { + AddIndexOffset(sampler.id, samplersOffset); + gltfLod.samplers.Append(std::move(sampler)); + } + + for (const auto& extension : lod.extensionsUsed) + { + gltfLod.extensionsUsed.insert(extension); + } + // ensure that MSFT_LOD extension is specified as being used + gltfLod.extensionsUsed.insert(Toolkit::EXTENSION_MSFT_LOD); + } + + size_t accessorOffset = gltfLod.accessors.Size(); + size_t texturesOffset = gltfLod.textures.Size(); + { + // Buffer Views depend upon Buffers + size_t bufferViewsOffset = gltfLod.bufferViews.Size(); + auto lodBufferViews = lod.bufferViews.Elements(); + for (auto bufferView : lodBufferViews) + { + AddIndexOffset(bufferView.id, bufferViewsOffset); + AddIndexOffset(bufferView.bufferId, buffersOffset); + gltfLod.bufferViews.Append(std::move(bufferView)); + } + + // Accessors depend upon Buffer views + auto lodAccessors = lod.accessors.Elements(); + for (auto accessor : lodAccessors) + { + AddIndexOffset(accessor.id, accessorOffset); + AddIndexOffset(accessor.bufferViewId, bufferViewsOffset); + gltfLod.accessors.Append(std::move(accessor)); + } + + // Images depend upon Buffer views + size_t imageOffset = gltfLod.images.Size(); + auto lodImages = lod.images.Elements(); + for (auto image : lodImages) + { + AddIndexOffset(image.id, imageOffset); + AddIndexOffset(image.bufferViewId, bufferViewsOffset); + gltfLod.images.Append(std::move(image)); + } + + // Textures depend upon Samplers and Images + auto lodTextures = lod.textures.Elements(); + for (auto texture : lodTextures) + { + AddIndexOffset(texture.id, texturesOffset); + AddIndexOffset(texture.samplerId, samplersOffset); + AddIndexOffset(texture.imageId, imageOffset); + + // MSFT_texture_dds extension + auto ddsExtensionIt = texture.extensions.find(EXTENSION_MSFT_TEXTURE_DDS); + if (ddsExtensionIt != texture.extensions.end() && !ddsExtensionIt->second.empty()) + { + rapidjson::Document ddsJson = RapidJsonUtils::CreateDocumentFromString(ddsExtensionIt->second); + + if (ddsJson.HasMember("source")) + { + auto index = ddsJson["source"].GetInt(); + ddsJson["source"] = index + imageOffset; + } + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + ddsJson.Accept(writer); + + ddsExtensionIt->second = buffer.GetString(); + } + + gltfLod.textures.Append(std::move(texture)); + } + } + + // Material Merge + // Note the extension KHR_materials_pbrSpecularGlossiness will be also updated + // Materials depend upon textures + size_t materialOffset = gltfLod.materials.Size(); + { + auto lodMaterials = lod.materials.Elements(); + for (auto material : lodMaterials) + { + // post-fix with lod level indication; + // no functional reason other than making it easier to natively read gltf files with lods + material.name += nodeLodLabel; + AddIndexOffset(material.id, materialOffset); + + AddIndexOffset(material.normalTexture.id, texturesOffset); + AddIndexOffset(material.occlusionTexture.id, texturesOffset); + AddIndexOffset(material.emissiveTextureId, texturesOffset); + + AddIndexOffset(material.metallicRoughness.baseColorTextureId, texturesOffset); + AddIndexOffset(material.metallicRoughness.metallicRoughnessTextureId, texturesOffset); + + AddIndexOffset(material.specularGlossiness.diffuseTextureId, texturesOffset); + AddIndexOffset(material.specularGlossiness.specularGlossinessTextureId, texturesOffset); + + // MSFT_packing_occlusionRoughnessMetallic packed textures + auto ormExtensionIt = material.extensions.find(EXTENSION_MSFT_PACKING_ORM); + if (ormExtensionIt != material.extensions.end() && !ormExtensionIt->second.empty()) + { + rapidjson::Document ormJson = RapidJsonUtils::CreateDocumentFromString(ormExtensionIt->second); + + AddIndexOffsetPacked(ormJson, "occlusionRoughnessMetallicTexture", texturesOffset); + AddIndexOffsetPacked(ormJson, "roughnessMetallicOcclusionTexture", texturesOffset); + AddIndexOffsetPacked(ormJson, "normalTexture", texturesOffset); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + ormJson.Accept(writer); + + ormExtensionIt->second = buffer.GetString(); + } + + gltfLod.materials.Append(std::move(material)); + } + } + + // Meshs depend upon Accessors and Materials + size_t meshOffset = gltfLod.meshes.Size(); + { + auto lodMeshes = lod.meshes.Elements(); + for (auto mesh : lodMeshes) + { + // post-fix with lod level indication; + // no functional reason other than making it easier to natively read gltf files with lods + mesh.name += nodeLodLabel; + AddIndexOffset(mesh.id, meshOffset); + + for (auto Itr = mesh.primitives.begin(); Itr != mesh.primitives.end(); Itr++) + { + AddIndexOffset(Itr->positionsAccessorId, accessorOffset); + AddIndexOffset(Itr->normalsAccessorId, accessorOffset); + AddIndexOffset(Itr->indicesAccessorId, accessorOffset); + AddIndexOffset(Itr->uv0AccessorId, accessorOffset); + AddIndexOffset(Itr->uv1AccessorId, accessorOffset); + AddIndexOffset(Itr->color0AccessorId, accessorOffset); + + AddIndexOffset(Itr->materialId, materialOffset); + } + + gltfLod.meshes.Append(std::move(mesh)); + } + } + + // Nodes depend upon Nodes and Meshes + size_t nodeOffset = gltfLod.nodes.Size(); + { + auto nodes = lod.nodes.Elements(); + for (auto node : nodes) + { + // post-fix with lod level indication; + // no functional reason other than making it easier to natively read gltf files with lods + node.name += nodeLodLabel; + AddIndexOffset(node.id, nodeOffset); + AddIndexOffset(node.meshId, meshOffset); + + for (auto Itr = node.children.begin(); Itr != node.children.end(); Itr++) + { + AddIndexOffset(*Itr, nodeOffset); + } + + gltfLod.nodes.Append(std::move(node)); + }; + } + + // update the primary GLTF root nodes lod extension to reference the new lod root node + // N.B. new lods are always added to the back + for (size_t sceneIdx = 0; sceneIdx < primaryScenes.size(); sceneIdx++) + { + for (size_t rootNodeIdx = 0; rootNodeIdx < primaryScenes[sceneIdx].nodes.size(); rootNodeIdx++) + { + auto idx = primaryScenes[sceneIdx].nodes[rootNodeIdx]; + Node nodeWithLods(gltfLod.nodes.Get(idx)); + int lodRootIdx = std::stoi(lodScenes[sceneIdx].nodes[rootNodeIdx]) + static_cast(nodeOffset); + auto primaryNodeLod = primaryLods.at(nodeWithLods.id); + primaryNodeLod->emplace_back(std::to_string(lodRootIdx)); + } + } + + return gltfLod; + } +} + +LODMap GLTFLODUtils::ParseDocumentNodeLODs(const GLTFDocument& doc) +{ + LODMap lodMap; + + for (auto node : doc.nodes.Elements()) + { + lodMap.emplace(node.id, std::move(std::make_shared>(ParseExtensionMSFTLod(node)))); + } + + return lodMap; +} + +GLTFDocument GLTFLODUtils::MergeDocumentsAsLODs(const std::vector& docs) +{ + if (docs.empty()) + { + throw std::invalid_argument("MergeDocumentsAsLODs passed empty vector"); + } + + GLTFDocument gltfPrimary(docs[0]); + LODMap lods = ParseDocumentNodeLODs(gltfPrimary); + + for (size_t i = 1; i < docs.size(); i++) + { + gltfPrimary = AddGLTFNodeLOD(gltfPrimary, lods, docs[i]); + } + + for (auto lod : lods) + { + if (lod.second == nullptr || lod.second->size() == 0) + { + continue; + } + + auto node = gltfPrimary.nodes.Get(lod.first); + + auto lodExtensionValue = SerializeExtensionMSFTLod(node, *lod.second, gltfPrimary); + if (!lodExtensionValue.empty()) + { + node.extensions.emplace(EXTENSION_MSFT_LOD, lodExtensionValue); + gltfPrimary.nodes.Replace(node); + } + } + + return gltfPrimary; +} + +GLTFDocument GLTFLODUtils::MergeDocumentsAsLODs(const std::vector& docs, const std::vector& screenCoveragePercentages) +{ + GLTFDocument merged = MergeDocumentsAsLODs(docs); + + if (screenCoveragePercentages.size() == 0) + { + return merged; + } + + for (auto scene : merged.scenes.Elements()) + { + for (auto rootNodeIndex : scene.nodes) + { + auto primaryRootNode = merged.nodes.Get(rootNodeIndex); + + rapidjson::Document extrasJson(rapidjson::kObjectType); + if (!primaryRootNode.extras.empty()) + { + extrasJson.Parse(primaryRootNode.extras.c_str()); + } + rapidjson::Document::AllocatorType& allocator = extrasJson.GetAllocator(); + + rapidjson::Value screenCoverageArray = RapidJsonUtils::ToJsonArray(screenCoveragePercentages, allocator); + + extrasJson.AddMember("MSFT_screencoverage", screenCoverageArray, allocator); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + extrasJson.Accept(writer); + + primaryRootNode.extras = buffer.GetString(); + + merged.nodes.Replace(primaryRootNode); + } + } + + return merged; +} + +uint32_t GLTFLODUtils::NumberOfNodeLODLevels(const GLTFDocument& doc, const LODMap& lods) +{ + size_t maxLODLevel = 0; + for (auto node : doc.nodes.Elements()) + { + maxLODLevel = std::max(maxLODLevel, lods.at(node.id)->size()); + } + + return static_cast(maxLODLevel); +} + diff --git a/glTF-Toolkit/src/GLTFTextureCompressionUtils.cpp b/glTF-Toolkit/src/GLTFTextureCompressionUtils.cpp new file mode 100644 index 0000000..b39d396 --- /dev/null +++ b/glTF-Toolkit/src/GLTFTextureCompressionUtils.cpp @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" + +#include "GLTFTextureLoadingUtils.h" +#include "GLTFTexturePackingUtils.h" +#include "GLTFTextureCompressionUtils.h" +#include "DeviceResources.h" + +#include +#include +#include +#include +#include + +// Usings for ComPtr +using namespace ABI::Windows::Foundation; +using namespace Microsoft::WRL; + +// Usings for glTF +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +#include + +const char* Microsoft::glTF::Toolkit::EXTENSION_MSFT_TEXTURE_DDS = "MSFT_texture_dds"; + +GLTFDocument GLTFTextureCompressionUtils::CompressTextureAsDDS(const IStreamReader& streamReader, const GLTFDocument & doc, const Texture & texture, TextureCompression compression, const std::string& outputDirectory, size_t maxTextureSize, bool generateMipMaps, bool retainOriginalImage) +{ + GLTFDocument outputDoc(doc); + + // Early return cases: + // - No compression requested + // - This texture doesn't have an image associated + // - The texture already has a DDS extension + if (compression == TextureCompression::None || + texture.imageId.empty() || + texture.extensions.find(EXTENSION_MSFT_TEXTURE_DDS) != texture.extensions.end()) + { + // Return copy of document + return outputDoc; + } + + auto image = std::make_unique(GLTFTextureLoadingUtils::LoadTexture(streamReader, doc, texture.id)); + + // Resize + auto metadata = image->GetMetadata(); + if (maxTextureSize < metadata.width || maxTextureSize < metadata.height) + { + auto scaleFactor = static_cast(maxTextureSize) / std::max(metadata.width, metadata.height); + auto resizedWidth = static_cast(std::llround(metadata.width * scaleFactor)); + auto resizedHeight = static_cast(std::llround(metadata.height * scaleFactor)); + auto resized = std::make_unique(); + if (FAILED(DirectX::Resize(image->GetImages(), image->GetImageCount(), image->GetMetadata(), resizedWidth, resizedHeight, DirectX::TEX_FILTER_DEFAULT, *resized))) + { + throw GLTFException("Failed to resize image."); + } + + image = std::move(resized); + } + + if (generateMipMaps) + { + auto mipChain = std::make_unique(); + if (FAILED(DirectX::GenerateMipMaps(image->GetImages(), image->GetImageCount(), image->GetMetadata(), DirectX::TEX_FILTER_DEFAULT, 0, *mipChain))) + { + throw GLTFException("Failed to generate mip maps."); + } + + image = std::move(mipChain); + } + + CompressImage(*image, compression); + + // Save image to file + std::string outputImagePath = "texture_" + texture.id; + + if (!generateMipMaps) + { + // The default is to have mips, so note on the texture when it doesn't + outputImagePath += "_nomips"; + } + + switch (compression) + { + case TextureCompression::BC3: + outputImagePath += "_BC3"; + break; + case TextureCompression::BC5: + outputImagePath += "_BC5"; + break; + case TextureCompression::BC7: + outputImagePath += "_BC7"; + break; + default: + throw GLTFException("Invalid compression."); + break; + } + + outputImagePath += ".dds"; + std::wstring outputImagePathW(outputImagePath.begin(), outputImagePath.end()); + + wchar_t outputImageFullPath[MAX_PATH]; + + std::wstring outputDirectoryW(outputDirectory.begin(), outputDirectory.end()); + + if (FAILED(::PathCchCombine(outputImageFullPath, ARRAYSIZE(outputImageFullPath), outputDirectoryW.c_str(), outputImagePathW.c_str()))) + { + throw GLTFException("Failed to compose output file path."); + } + + if (FAILED(SaveToDDSFile(image->GetImages(), image->GetImageCount(), image->GetMetadata(), DirectX::DDS_FLAGS::DDS_FLAGS_NONE, outputImageFullPath))) + { + throw GLTFException("Failed to save image as DDS."); + } + + std::wstring outputImageFullPathW(outputImageFullPath); + std::string outputImageFullPathA(outputImageFullPathW.begin(), outputImageFullPathW.end()); + + // Add back to GLTF + std::string ddsImageId(texture.imageId); + + Image ddsImage(doc.images.Get(texture.imageId)); + ddsImage.mimeType = "image/vnd-ms.dds"; + ddsImage.uri = outputImageFullPathA; + + if (retainOriginalImage) + { + ddsImageId.assign(std::to_string(doc.images.Size())); + ddsImage.id = ddsImageId; + outputDoc.images.Append(std::move(ddsImage)); + } + else + { + outputDoc.images.Replace(ddsImage); + } + + Texture ddsTexture(texture); + + // Create the JSON for the DDS extension element + rapidjson::Document ddsExtensionJson; + ddsExtensionJson.SetObject(); + + ddsExtensionJson.AddMember("source", rapidjson::Value(std::stoi(ddsImageId)), ddsExtensionJson.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + ddsExtensionJson.Accept(writer); + + ddsTexture.extensions.insert(std::pair(EXTENSION_MSFT_TEXTURE_DDS, buffer.GetString())); + + outputDoc.textures.Replace(ddsTexture); + + outputDoc.extensionsUsed.insert(EXTENSION_MSFT_TEXTURE_DDS); + + if (!retainOriginalImage) + { + outputDoc.extensionsRequired.insert(EXTENSION_MSFT_TEXTURE_DDS); + } + + return outputDoc; +} + +GLTFDocument GLTFTextureCompressionUtils::CompressAllTexturesForWindowsMR(const IStreamReader& streamReader, const GLTFDocument & doc, const std::string& outputDirectory, size_t maxTextureSize, bool retainOriginalImages) +{ + GLTFDocument outputDoc(doc); + + for (auto material : doc.materials.Elements()) + { + auto compressIfNotEmpty = [&outputDoc, &streamReader, &outputDirectory, maxTextureSize, retainOriginalImages](const std::string& textureId, TextureCompression compression) + { + if (!textureId.empty()) + { + outputDoc = CompressTextureAsDDS(streamReader, outputDoc, outputDoc.textures.Get(textureId), compression, outputDirectory, maxTextureSize, true, retainOriginalImages); + } + }; + + // Compress base and emissive texture as BC7 + compressIfNotEmpty(material.metallicRoughness.baseColorTextureId, TextureCompression::BC7); + compressIfNotEmpty(material.emissiveTextureId, TextureCompression::BC7); + + // Get other textures from the MSFT_packing_occlusionRoughnessMetallic extension + if (material.extensions.find(EXTENSION_MSFT_PACKING_ORM) != material.extensions.end()) + { + rapidjson::Document packingOrmContents; + packingOrmContents.Parse(material.extensions[EXTENSION_MSFT_PACKING_ORM].c_str()); + + // Compress packed textures as BC7 + if (packingOrmContents.HasMember("roughnessMetallicOcclusionTexture")) + { + auto rmoTextureId = packingOrmContents["roughnessMetallicOcclusionTexture"]["index"].GetInt(); + compressIfNotEmpty(std::to_string(rmoTextureId), TextureCompression::BC7); + } + + if (packingOrmContents.HasMember("occlusionRoughnessMetallicTexture")) + { + auto ormTextureId = packingOrmContents["occlusionRoughnessMetallicTexture"]["index"].GetInt(); + compressIfNotEmpty(std::to_string(ormTextureId), TextureCompression::BC7); + } + + // Compress normal texture as BC5 + if (packingOrmContents.HasMember("normalTexture")) + { + auto normalTextureId = packingOrmContents["normalTexture"]["index"].GetInt(); + compressIfNotEmpty(std::to_string(normalTextureId), TextureCompression::BC5); + } + } + } + + return outputDoc; +} + +void GLTFTextureCompressionUtils::CompressImage(DirectX::ScratchImage& image, TextureCompression compression) +{ + if (compression == TextureCompression::None) + { + return; + } + + DXGI_FORMAT compressionFormat = DXGI_FORMAT_BC7_UNORM; + switch (compression) + { + case TextureCompression::BC3: + compressionFormat = DXGI_FORMAT_BC3_UNORM; + break; + case TextureCompression::BC5: + compressionFormat = DXGI_FORMAT_BC5_UNORM; + break; + case TextureCompression::BC7: + compressionFormat = DXGI_FORMAT_BC7_UNORM; + break; + default: + throw std::invalid_argument("Invalid compression specified."); + break; + } + + DX::DeviceResources deviceResources; + deviceResources.CreateDeviceResources(); + ComPtr device(deviceResources.GetD3DDevice()); + + DirectX::ScratchImage compressedImage; + + bool gpuCompressionSuccessful = false; + if (device != nullptr) + { + if (SUCCEEDED(DirectX::Compress(device.Get(), image.GetImages(), image.GetImageCount(), image.GetMetadata(), compressionFormat, DirectX::TEX_COMPRESS_DEFAULT, 0, compressedImage))) + { + gpuCompressionSuccessful = true; + } + } + + if (!gpuCompressionSuccessful) + { + // Try software compression + if (FAILED(DirectX::Compress(image.GetImages(), image.GetImageCount(), image.GetMetadata(), compressionFormat, DirectX::TEX_COMPRESS_DEFAULT, 0, compressedImage))) + { + throw GLTFException("Failed to compress data using software compression"); + } + } + + image = std::move(compressedImage); +} \ No newline at end of file diff --git a/glTF-Toolkit/src/GLTFTextureLoadingUtils.cpp b/glTF-Toolkit/src/GLTFTextureLoadingUtils.cpp new file mode 100644 index 0000000..3e176a1 --- /dev/null +++ b/glTF-Toolkit/src/GLTFTextureLoadingUtils.cpp @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" + +#include +#include + +#include "GLTFTextureLoadingUtils.h" + +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +namespace +{ +} + +DirectX::ScratchImage GLTFTextureLoadingUtils::LoadTexture(const IStreamReader& streamReader, const GLTFDocument& doc, const std::string& textureId) +{ + DirectX::ScratchImage output; + + const Texture& texture = doc.textures.Get(textureId); + + GLTFResourceReader gltfResourceReader(streamReader); + + const Image& image = doc.images.Get(texture.imageId); + + std::vector imageData = gltfResourceReader.ReadBinaryData(doc, image); + + auto data = std::make_unique(imageData.size()); + memcpy_s(data.get(), imageData.size(), imageData.data(), imageData.size()); + + DirectX::TexMetadata info; + if (FAILED(DirectX::LoadFromDDSMemory(data.get(), imageData.size(), DirectX::DDS_FLAGS_NONE, &info, output))) + { + // DDS failed, try WIC + // Note: try DDS first since WIC can load some DDS (but not all), so we wouldn't want to get + // a partial or invalid DDS loaded from WIC. + if (FAILED(DirectX::LoadFromWICMemory(data.get(), imageData.size(), DirectX::WIC_FLAGS_IGNORE_SRGB, &info, output))) + { + throw GLTFException("Failed to load image - Image could not be loaded as DDS or read by WIC."); + } + } + + if (info.format == DXGI_FORMAT_R32G32B32A32_FLOAT) + { + return output; + } + else + { + DirectX::ScratchImage converted; + if (FAILED(DirectX::Convert(*output.GetImage(0, 0, 0), DXGI_FORMAT_R32G32B32A32_FLOAT, DirectX::TEX_FILTER_DEFAULT, DirectX::TEX_THRESHOLD_DEFAULT, converted))) + { + throw GLTFException("Failed to convert texture to DXGI_FORMAT_R32G32B32A32_FLOAT for processing."); + } + + return converted; + } +} \ No newline at end of file diff --git a/glTF-Toolkit/src/GLTFTexturePackingUtils.cpp b/glTF-Toolkit/src/GLTFTexturePackingUtils.cpp new file mode 100644 index 0000000..b52ce54 --- /dev/null +++ b/glTF-Toolkit/src/GLTFTexturePackingUtils.cpp @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "GLTFTextureLoadingUtils.h" +#include "GLTFTexturePackingUtils.h" + +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +const char* Microsoft::glTF::Toolkit::EXTENSION_MSFT_PACKING_ORM = "MSFT_packing_occlusionRoughnessMetallic"; + +namespace +{ + enum Channel + { + Red = 0, + Green = 4, + Blue = 8, + Alpha = 12 + }; + + // Constants for the format DXGI_FORMAT_R32G32B32A32_FLOAT + const size_t DXGI_FORMAT_R32G32B32A32_FLOAT_STRIDE = 16; + + // Gets the value of channel `channel` in pixel index `offset` in image `imageData` + // assumed to be formatted as DXGI_FORMAT_R32G32B32A32_FLOAT + float* GetChannelValue(uint8_t * imageData, size_t offset, Channel channel) + { + return reinterpret_cast(imageData + offset * DXGI_FORMAT_R32G32B32A32_FLOAT_STRIDE + channel); + } + + std::string SaveAsPng(std::unique_ptr& image, const std::string& fileName, const std::string& directory) + { + wchar_t outputImageFullPath[MAX_PATH]; + auto fileNameW = std::wstring(fileName.begin(), fileName.end()); + auto directoryW = std::wstring(directory.begin(), directory.end()); + + if (FAILED(::PathCchCombine(outputImageFullPath, ARRAYSIZE(outputImageFullPath), directoryW.c_str(), fileNameW.c_str()))) + { + throw GLTFException("Failed to compose output file path."); + } + + const DirectX::Image* img = image->GetImage(0, 0, 0); + if (FAILED(SaveToWICFile(*img, DirectX::WIC_FLAGS::WIC_FLAGS_NONE, GUID_ContainerFormatPng, outputImageFullPath, &GUID_WICPixelFormat24bppBGR))) + { + throw GLTFException("Failed to save file."); + } + + std::wstring outputImageFullPathStr(outputImageFullPath); + return std::string(outputImageFullPathStr.begin(), outputImageFullPathStr.end()); + } + + std::string AddImageToDocument(GLTFDocument& doc, const std::string& imageUri) + { + Image image; + auto imageId = std::to_string(doc.images.Size()); + image.id = imageId; + image.uri = imageUri; + doc.images.Append(std::move(image)); + + return imageId; + } + + void AddTextureToOrmExtension(const std::string& imageId, TexturePacking packing, GLTFDocument& doc, rapidjson::Value& ormExtensionJson, rapidjson::MemoryPoolAllocator<>& a) + { + Texture ormTexture; + auto textureId = std::to_string(doc.textures.Size()); + ormTexture.id = textureId; + ormTexture.imageId = imageId; + doc.textures.Append(std::move(ormTexture)); + + rapidjson::Value ormTextureJson(rapidjson::kObjectType); + { + ormTextureJson.AddMember("index", rapidjson::Value(std::stoi(textureId)), a); + } + switch (packing) + { + case TexturePacking::OcclusionRoughnessMetallic: + ormExtensionJson.AddMember("occlusionRoughnessMetallicTexture", ormTextureJson, a); + break; + case TexturePacking::RoughnessMetallicOcclusion: + ormExtensionJson.AddMember("roughnessMetallicOcclusionTexture", ormTextureJson, a); + break; + default: + throw GLTFException("Invalid packing."); + } + } +} + +GLTFDocument GLTFTexturePackingUtils::PackMaterialForWindowsMR(const IStreamReader& streamReader, const GLTFDocument& doc, const Material& material, TexturePacking packing, const std::string& outputDirectory) +{ + GLTFDocument outputDoc(doc); + + // No packing requested, return copy of document + if (packing == TexturePacking::None) + { + return outputDoc; + } + + // Read images from material + auto metallicRoughness = material.metallicRoughness.metallicRoughnessTextureId; + auto normal = material.normalTexture.id; + auto occlusion = material.occlusionTexture.id; + + bool hasMR = !metallicRoughness.empty(); + bool hasNormal = !normal.empty(); + bool hasOcclusion = !occlusion.empty(); + + // Early return if there's nothing to pack + if (!hasMR && !hasOcclusion && !hasNormal) + { + // RM, O and Normal are empty, and the packing requires at least one of them + return outputDoc; + } + + // TODO: Optimization - If the texture pair (MR + O) has already been packed together with the + // current packing, point to that existing texture instead of creating a new one + + Material outputMaterial = outputDoc.materials.Get(material.id); + + // Create the JSON for the material extension element + rapidjson::Document ormExtensionJson; + ormExtensionJson.SetObject(); + rapidjson::MemoryPoolAllocator<>& allocator = ormExtensionJson.GetAllocator(); + + std::unique_ptr metallicRoughnessImage = nullptr; + uint8_t *mrPixels = nullptr; + if (hasMR) + { + try + { + metallicRoughnessImage = std::make_unique(GLTFTextureLoadingUtils::LoadTexture(streamReader, doc, metallicRoughness)); + mrPixels = metallicRoughnessImage->GetPixels(); + } + catch (GLTFException) + { + throw GLTFException("Failed to load metallic roughness texture."); + } + } + + std::unique_ptr occlusionImage = nullptr; + uint8_t *occlusionPixels = nullptr; + if (hasOcclusion) + { + try + { + occlusionImage = std::make_unique(GLTFTextureLoadingUtils::LoadTexture(streamReader, doc, occlusion)); + occlusionPixels = occlusionImage->GetPixels(); + } + catch (GLTFException) + { + throw GLTFException("Failed to load occlusion texture."); + } + } + + // Pack textures using DirectXTex + + if (packing & TexturePacking::OcclusionRoughnessMetallic) + { + std::string ormImageId; + + // If occlusion and metallic roughness are pointing to the same texture, + // according to the GLTF spec, that texture is already packed as ORM + // (occlusion = R, roughness = G, metalness = B) + if (occlusion == metallicRoughness && hasOcclusion) + { + ormImageId = metallicRoughness; + } + else + { + auto orm = std::make_unique(); + + auto sourceImage = hasMR ? *metallicRoughnessImage->GetImage(0, 0, 0) : *occlusionImage->GetImage(0, 0, 0); + if (FAILED(orm->Initialize2D(sourceImage.format, sourceImage.width, sourceImage.height, 1, 1))) + { + throw GLTFException("Failed to initialize from texture."); + } + + auto ormPixels = orm->GetPixels(); + auto metadata = orm->GetMetadata(); + + // TODO: resize? + + for (size_t i = 0; i < metadata.width * metadata.height; i += 1) + { + // Occlusion: Occ [R] -> ORM [R] + *GetChannelValue(ormPixels, i, Channel::Red) = hasOcclusion ? *GetChannelValue(occlusionPixels, i, Channel::Red) : 255.0f; + // Roughness: MR [G] -> ORM [G] + *GetChannelValue(ormPixels, i, Channel::Green) = hasMR ? *GetChannelValue(mrPixels, i, Channel::Green) : 255.0f; + // Metalness: MR [B] -> ORM [B] + *GetChannelValue(ormPixels, i, Channel::Blue) = hasMR ? *GetChannelValue(mrPixels, i, Channel::Blue) : 255.0f; + } + + auto imagePath = SaveAsPng(orm, "packing_orm_" + material.id + ".png", outputDirectory); + + ormImageId = AddImageToDocument(outputDoc, imagePath); + } + + AddTextureToOrmExtension(ormImageId, TexturePacking::OcclusionRoughnessMetallic, outputDoc, ormExtensionJson, allocator); + } + + if (packing & TexturePacking::RoughnessMetallicOcclusion) + { + auto rmo = std::make_unique(); + + // TODO: resize? + + auto sourceImage = hasMR ? *metallicRoughnessImage->GetImage(0, 0, 0) : *occlusionImage->GetImage(0, 0, 0); + if (FAILED(rmo->Initialize2D(sourceImage.format, sourceImage.width, sourceImage.height, 1, 1))) + { + throw GLTFException("Failed to initialize from texture."); + } + + auto rmoPixels = rmo->GetPixels(); + auto metadata = rmo->GetMetadata(); + + for (size_t i = 0; i < metadata.width * metadata.height; i += 1) + { + // Roughness: MR [G] -> RMO [R] + *GetChannelValue(rmoPixels, i, Channel::Red) = hasMR ? *GetChannelValue(mrPixels, i, Channel::Green) : 255.0f; + // Metalness: MR [B] -> RMO [G] + *GetChannelValue(rmoPixels, i, Channel::Green) = hasMR ? *GetChannelValue(mrPixels, i, Channel::Blue) : 255.0f; + // Occlusion: Occ [R] -> RMO [B] + *GetChannelValue(rmoPixels, i, Channel::Blue) = hasOcclusion ? *GetChannelValue(occlusionPixels, i, Channel::Red) : 255.0f; + } + + auto imagePath = SaveAsPng(rmo, "packing_rmo_" + material.id + ".png", outputDirectory); + + // Add back to GLTF + auto rmoImageId = AddImageToDocument(outputDoc, imagePath); + + AddTextureToOrmExtension(rmoImageId, TexturePacking::RoughnessMetallicOcclusion, outputDoc, ormExtensionJson, allocator); + } + + if (!normal.empty()) + { + rapidjson::Value ormNormalTextureJson(rapidjson::kObjectType); + { + ormNormalTextureJson.AddMember("index", rapidjson::Value(std::stoi(normal)), allocator); + } + ormExtensionJson.AddMember("normalTexture", ormNormalTextureJson, allocator); + } + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + ormExtensionJson.Accept(writer); + + outputMaterial.extensions.insert(std::pair(EXTENSION_MSFT_PACKING_ORM, buffer.GetString())); + + outputDoc.materials.Replace(outputMaterial); + + outputDoc.extensionsUsed.insert(EXTENSION_MSFT_PACKING_ORM); + + return outputDoc; +} + +GLTFDocument GLTFTexturePackingUtils::PackAllMaterialsForWindowsMR(const IStreamReader& streamReader, const GLTFDocument & doc, TexturePacking packing, const std::string& outputDirectory) +{ + GLTFDocument outputDoc(doc); + + // No packing requested, return copy of document + if (packing == TexturePacking::None) + { + return outputDoc; + } + + for (auto material : doc.materials.Elements()) + { + outputDoc = PackMaterialForWindowsMR(streamReader, outputDoc, material, packing, outputDirectory); + } + + return outputDoc; +} \ No newline at end of file diff --git a/glTF-Toolkit/src/SerializeBinary.cpp b/glTF-Toolkit/src/SerializeBinary.cpp new file mode 100644 index 0000000..b3a7f68 --- /dev/null +++ b/glTF-Toolkit/src/SerializeBinary.cpp @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" + +#include "AccessorUtils.h" +#include "SerializeBinary.h" + +#include "GLTFSDK/GLTF.h" +#include "GLTFSDK/GLTFDocument.h" +#include "GLTFSDK/GLBResourceReader.h" +#include "GLTFSDK/GLBResourceWriter2.h" +#include "GLTFSDK/Serialize.h" +#include "GLTFSDK/BufferBuilder.h" + +using namespace Microsoft::glTF; +using namespace Microsoft::glTF::Toolkit; + +namespace +{ + static std::string MimeTypeFromUri(const std::string& uri) + { + auto extension = uri.substr(uri.rfind('.') + 1, 3); + std::transform(extension.begin(), extension.end(), extension.begin(), [](char c) { return static_cast(::tolower(static_cast(c))); }); + + if (extension == FILE_EXT_DDS) + { + return MIMETYPE_DDS; + } + + if (extension == FILE_EXT_JPEG) + { + return MIMETYPE_JPEG; + } + + if (extension == FILE_EXT_PNG) + { + return MIMETYPE_PNG; + } + + return "text/plain"; + } + + template + void SaveAccessor(const Accessor& accessor, const std::vector accessorContents, BufferBuilder& builder) + { + auto min = accessor.min; + auto max = accessor.max; + if ((min.empty() || max.empty()) && !accessorContents.empty()) + { + auto minmax = AccessorUtils::CalculateMinMax(accessor, accessorContents); + min = minmax.first; + max = minmax.second; + } + + builder.AddAccessor(accessorContents, accessor.componentType, accessor.type, min, max); + } + + template + static std::vector vector_static_cast(const std::vector& original) + { + auto newData = std::vector(original.size()); + + std::transform(original.begin(), original.end(), newData.begin(), + [](const OriginalType& element) + { + return static_cast(element); + }); + + return newData; + } + + template + void ConvertAndSaveAccessor(const Accessor& accessor, const std::vector accessorContents, BufferBuilder& builder) + { + switch (accessor.componentType) + { + case COMPONENT_BYTE: + SaveAccessor(accessor, vector_static_cast(accessorContents), builder); + break; + case COMPONENT_UNSIGNED_BYTE: + SaveAccessor(accessor, vector_static_cast(accessorContents), builder); + break; + case COMPONENT_SHORT: + SaveAccessor(accessor, vector_static_cast(accessorContents), builder); + break; + case COMPONENT_UNSIGNED_SHORT: + SaveAccessor(accessor, vector_static_cast(accessorContents), builder); + break; + case COMPONENT_UNSIGNED_INT: + SaveAccessor(accessor, vector_static_cast(accessorContents), builder); + break; + case COMPONENT_FLOAT: + SaveAccessor(accessor, vector_static_cast(accessorContents), builder); + break; + default: + throw GLTFException("Unsupported accessor ComponentType"); + } + } + + template + void SerializeAccessor(const Accessor& accessor, const GLTFDocument& doc, const GLTFResourceReader& reader, BufferBuilder& builder, const AccessorConversionStrategy& accessorConversion) + { + builder.AddBufferView(doc.bufferViews.Get(accessor.bufferViewId).target); + const std::vector& accessorContents = reader.ReadBinaryData(doc, accessor); + + if (accessorConversion != nullptr && accessorConversion(accessor) != accessor.componentType) + { + Accessor updatedAccessor(accessor); + updatedAccessor.componentType = accessorConversion(accessor); + + // Force recalculation of min and max + updatedAccessor.min.clear(); + updatedAccessor.max.clear(); + + ConvertAndSaveAccessor(updatedAccessor, accessorContents, builder); + } + else + { + SaveAccessor(accessor, accessorContents, builder); + } + } + + void SerializeAccessor(const Accessor& accessor, const GLTFDocument& doc, const GLTFResourceReader& reader, BufferBuilder& builder, const AccessorConversionStrategy& accessorConversion) + { + switch (accessor.componentType) + { + case COMPONENT_BYTE: + SerializeAccessor(accessor, doc, reader, builder, accessorConversion); + break; + case COMPONENT_UNSIGNED_BYTE: + SerializeAccessor(accessor, doc, reader, builder, accessorConversion); + break; + case COMPONENT_SHORT: + SerializeAccessor(accessor, doc, reader, builder, accessorConversion); + break; + case COMPONENT_UNSIGNED_SHORT: + SerializeAccessor(accessor, doc, reader, builder, accessorConversion); + break; + case COMPONENT_UNSIGNED_INT: + SerializeAccessor(accessor, doc, reader, builder, accessorConversion); + break; + case COMPONENT_FLOAT: + SerializeAccessor(accessor, doc, reader, builder, accessorConversion); + break; + default: + throw GLTFException("Unsupported accessor ComponentType"); + } + } +} + +void Microsoft::glTF::Toolkit::SerializeBinary(const GLTFDocument& gltfDocument, const IStreamReader& inputStreamReader, std::unique_ptr& outputStreamFactory, const AccessorConversionStrategy& accessorConversion) +{ + auto writer = std::make_unique(std::move(outputStreamFactory), std::string()); + + GLTFDocument outputDoc(gltfDocument); + + outputDoc.buffers.Clear(); + outputDoc.bufferViews.Clear(); + outputDoc.accessors.Clear(); + + GLTFResourceReader gltfResourceReader(inputStreamReader); + + std::unique_ptr builder = std::make_unique(std::move(writer)); + + // GLB buffer + builder->AddBuffer(GLB_BUFFER_ID); + + // Serialize accessors + for (auto accessor : gltfDocument.accessors.Elements()) + { + SerializeAccessor(accessor, gltfDocument, gltfResourceReader, *builder, accessorConversion); + } + + // Serialize images + for (auto image : outputDoc.images.Elements()) + { + if (!image.uri.empty()) + { + Image newImage(image); + + auto data = gltfResourceReader.ReadBinaryData(gltfDocument, image); + + auto imageBufferView = builder->AddBufferView(data); + + newImage.bufferViewId = imageBufferView.id; + if (image.mimeType.empty()) + { + newImage.mimeType = MimeTypeFromUri(image.uri); + } + + newImage.uri.clear(); + + outputDoc.images.Replace(newImage); + } + } + + builder->Output(outputDoc); + + // Add extensions and extras to bufferViews, if any + for (auto bufferView : gltfDocument.bufferViews.Elements()) + { + auto fixedBufferView = outputDoc.bufferViews.Get(bufferView.id); + fixedBufferView.extensions = bufferView.extensions; + fixedBufferView.extras = bufferView.extras; + + outputDoc.bufferViews.Replace(fixedBufferView); + } + + auto manifest = Serialize(outputDoc); + + auto outputWriter = dynamic_cast(&builder->GetResourceWriter()); + if (outputWriter != nullptr) + { + outputWriter->Flush(manifest, std::string()); + } +} \ No newline at end of file diff --git a/glTF-Toolkit/src/pch.cpp b/glTF-Toolkit/src/pch.cpp new file mode 100644 index 0000000..0026eb6 --- /dev/null +++ b/glTF-Toolkit/src/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" \ No newline at end of file