diff --git a/.gitmodules b/.gitmodules index 2a2427c..9ddd49f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "GLTFKit"] - path = GLTFKit - url = https://github.com/warrenm/GLTFKit.git [submodule "tinygltf"] path = tinygltf url = https://github.com/syoyo/tinygltf.git diff --git a/GLTFKit b/GLTFKit deleted file mode 160000 index 0f4b7cc..0000000 --- a/GLTFKit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0f4b7ccd2c9004f1b37bb29a1dca5d331f36b4a7 diff --git a/README.md b/README.md index 4389479..c044c80 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,35 @@ ![status](https://img.shields.io/badge/glTF-2%2E0-green.svg?style=flat) [![License](http://img.shields.io/:license-mit-blue.svg)](https://github.com/toshiks/glTF-quicklook/blob/master/LICENSE) -Simple QuickLook plugin for previewing gltf-files on macOS +Simple QuickLook plugin for previewing gltf-files on macOS. ## Status -v1.0 release(3 Dec, 2018) +v1.1 release(14 Sep, 2019) + +## Features +* glTF specification v2.0.0 +* Draco compression format +* Animations (not for Draco compression format) +* Textures ## System Requirements - macOS 10.13 (High Sierra) or later +- installed [Draco compression library](https://github.com/google/draco) ## Install ### Manually -1. Download **glTF-qucklook_vX.X.zip** from [Releases](https://github.com/toshiks/glTF-quicklook/releases/latest). -2. Put **glTF-qucklook.qlgenerator** from zip file into +1. In terminal run command: brew install draco@1.3.5 +2. Download **glTF-qucklook_vX.X.zip** from [Releases](https://github.com/toshiks/glTF-quicklook/releases/latest). +3. Put **glTF-qucklook.qlgenerator** from zip file into 1. `/Library/QuickLook` - for all users; 2. `~/Library/QuickLook` - only for the logged-in user. -3. Run `qlmanage -r` command to reload QuickLook plugins. +4. Run `qlmanage -r` command to reload QuickLook plugins. + + ## Licenses diff --git a/glTF-quicklook.xcodeproj/project.pbxproj b/glTF-quicklook.xcodeproj/project.pbxproj index ab64362..3bab40e 100644 --- a/glTF-quicklook.xcodeproj/project.pbxproj +++ b/glTF-quicklook.xcodeproj/project.pbxproj @@ -21,33 +21,15 @@ 2744673121B1F39100E8338E /* GenerateThumbnailForURL.m in Sources */ = {isa = PBXBuildFile; fileRef = 2744673021B1F39100E8338E /* GenerateThumbnailForURL.m */; }; 2744673321B1F39100E8338E /* GeneratePreviewForURL.m in Sources */ = {isa = PBXBuildFile; fileRef = 2744673221B1F39100E8338E /* GeneratePreviewForURL.m */; }; 2744673521B1F39100E8338E /* main.c in Sources */ = {isa = PBXBuildFile; fileRef = 2744673421B1F39100E8338E /* main.c */; }; - 2744674C21B1F95A00E8338E /* GLTF.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2744674521B1F94300E8338E /* GLTF.framework */; }; - 2744674D21B1F95F00E8338E /* GLTFSCN.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2744674B21B1F95100E8338E /* GLTFSCN.framework */; }; - 2744674F21B1FA1900E8338E /* GLTFSCN.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 2744674B21B1F95100E8338E /* GLTFSCN.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 2744675021B1FA1900E8338E /* GLTF.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 2744674521B1F94300E8338E /* GLTF.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 27C37BC421B367FE008CF713 /* GLTFErrorCheckerByJSON.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27C37BC221B367FE008CF713 /* GLTFErrorCheckerByJSON.mm */; }; - 27C37BC521B367FE008CF713 /* GLTFErrorCheckerByJSON.h in Headers */ = {isa = PBXBuildFile; fileRef = 27C37BC321B367FE008CF713 /* GLTFErrorCheckerByJSON.h */; }; - 27C37BD421B38688008CF713 /* GLTFErrorCheckerByScenes.h in Headers */ = {isa = PBXBuildFile; fileRef = 27C37BD221B38688008CF713 /* GLTFErrorCheckerByScenes.h */; }; - 27C37BD521B38688008CF713 /* GLTFErrorCheckerByScenes.m in Sources */ = {isa = PBXBuildFile; fileRef = 27C37BD321B38688008CF713 /* GLTFErrorCheckerByScenes.m */; }; + 27BDD5C7232D26F400D0A2F1 /* TinyGLTFSCN.h in Headers */ = {isa = PBXBuildFile; fileRef = 27BDD5C5232D26F400D0A2F1 /* TinyGLTFSCN.h */; }; + 27BDD5C8232D26F400D0A2F1 /* TinyGLTFSCN.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27BDD5C6232D26F400D0A2F1 /* TinyGLTFSCN.mm */; }; + 27BDD5CC232D292800D0A2F1 /* libdracodec.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27BDD5C9232D292700D0A2F1 /* libdracodec.a */; }; + 27BDD5CD232D292800D0A2F1 /* libdraco.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27BDD5CA232D292700D0A2F1 /* libdraco.a */; }; + 27BDD5CE232D292800D0A2F1 /* libdracoenc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27BDD5CB232D292800D0A2F1 /* libdracoenc.a */; }; + 27BDD5D1232D31E000D0A2F1 /* GLTFSCNAnimationTargetPair.h in Headers */ = {isa = PBXBuildFile; fileRef = 27BDD5CF232D31E000D0A2F1 /* GLTFSCNAnimationTargetPair.h */; }; + 27BDD5D4232D6EB900D0A2F1 /* GLTFSCNAnimationTargetPair.m in Sources */ = {isa = PBXBuildFile; fileRef = 27BDD5D3232D6EB900D0A2F1 /* GLTFSCNAnimationTargetPair.m */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 2744674421B1F94300E8338E /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 2744674021B1F94300E8338E /* GLTF.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 83D6FF481F48BB3A00F71E0C; - remoteInfo = GLTF; - }; - 2744674A21B1F95100E8338E /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 2744674621B1F95100E8338E /* GLTFSCN.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 83A2D6E91F9961AC00FB8874; - remoteInfo = GLTFSCN; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 2744674E21B1FA1100E8338E /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -55,8 +37,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 2744674F21B1FA1900E8338E /* GLTFSCN.framework in CopyFiles */, - 2744675021B1FA1900E8338E /* GLTF.framework in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -79,12 +59,13 @@ 2744673221B1F39100E8338E /* GeneratePreviewForURL.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GeneratePreviewForURL.m; sourceTree = ""; }; 2744673421B1F39100E8338E /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = ""; }; 2744673621B1F39100E8338E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 2744674021B1F94300E8338E /* GLTF.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GLTF.xcodeproj; path = GLTFKit/Framework/GLTF/GLTF.xcodeproj; sourceTree = ""; }; - 2744674621B1F95100E8338E /* GLTFSCN.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GLTFSCN.xcodeproj; path = GLTFKit/Framework/GLTFSCN/GLTFSCN.xcodeproj; sourceTree = ""; }; - 27C37BC221B367FE008CF713 /* GLTFErrorCheckerByJSON.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = GLTFErrorCheckerByJSON.mm; sourceTree = ""; }; - 27C37BC321B367FE008CF713 /* GLTFErrorCheckerByJSON.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GLTFErrorCheckerByJSON.h; sourceTree = ""; }; - 27C37BD221B38688008CF713 /* GLTFErrorCheckerByScenes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GLTFErrorCheckerByScenes.h; sourceTree = ""; }; - 27C37BD321B38688008CF713 /* GLTFErrorCheckerByScenes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GLTFErrorCheckerByScenes.m; sourceTree = ""; }; + 27BDD5C5232D26F400D0A2F1 /* TinyGLTFSCN.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TinyGLTFSCN.h; sourceTree = ""; }; + 27BDD5C6232D26F400D0A2F1 /* TinyGLTFSCN.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = TinyGLTFSCN.mm; sourceTree = ""; }; + 27BDD5C9232D292700D0A2F1 /* libdracodec.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libdracodec.a; path = ../../../../usr/local/Cellar/draco/1.3.5/lib/libdracodec.a; sourceTree = ""; }; + 27BDD5CA232D292700D0A2F1 /* libdraco.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libdraco.a; path = ../../../../usr/local/Cellar/draco/1.3.5/lib/libdraco.a; sourceTree = ""; }; + 27BDD5CB232D292800D0A2F1 /* libdracoenc.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libdracoenc.a; path = ../../../../usr/local/Cellar/draco/1.3.5/lib/libdracoenc.a; sourceTree = ""; }; + 27BDD5CF232D31E000D0A2F1 /* GLTFSCNAnimationTargetPair.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GLTFSCNAnimationTargetPair.h; sourceTree = ""; }; + 27BDD5D3232D6EB900D0A2F1 /* GLTFSCNAnimationTargetPair.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GLTFSCNAnimationTargetPair.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -92,12 +73,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 27BDD5CC232D292800D0A2F1 /* libdracodec.a in Frameworks */, + 27BDD5CD232D292800D0A2F1 /* libdraco.a in Frameworks */, + 27BDD5CE232D292800D0A2F1 /* libdracoenc.a in Frameworks */, 2707800821B5C9140081C27D /* json.hpp in Frameworks */, 2707800921B5C9140081C27D /* stb_image.h in Frameworks */, 2707800A21B5C9140081C27D /* stb_image_write.h in Frameworks */, 2707800B21B5C9140081C27D /* tiny_gltf.h in Frameworks */, - 2744674D21B1F95F00E8338E /* GLTFSCN.framework in Frameworks */, - 2744674C21B1F95A00E8338E /* GLTF.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -133,14 +115,11 @@ 2744672F21B1F39100E8338E /* glTF-quicklook */ = { isa = PBXGroup; children = ( + 27BDD5C2232D26AA00D0A2F1 /* TinyGLTFSCN */, 2744673021B1F39100E8338E /* GenerateThumbnailForURL.m */, 2744673221B1F39100E8338E /* GeneratePreviewForURL.m */, 2744673421B1F39100E8338E /* main.c */, 2744673621B1F39100E8338E /* Info.plist */, - 27C37BC221B367FE008CF713 /* GLTFErrorCheckerByJSON.mm */, - 27C37BC321B367FE008CF713 /* GLTFErrorCheckerByJSON.h */, - 27C37BD221B38688008CF713 /* GLTFErrorCheckerByScenes.h */, - 27C37BD321B38688008CF713 /* GLTFErrorCheckerByScenes.m */, 27077F9321B58C0D0081C27D /* SceneGenerator.h */, 27077F9421B58C0D0081C27D /* SceneGenerator.m */, 27077FCF21B5AE330081C27D /* ThumbnailGenerator.h */, @@ -154,30 +133,26 @@ 2744673F21B1F94300E8338E /* Frameworks */ = { isa = PBXGroup; children = ( + 27BDD5CA232D292700D0A2F1 /* libdraco.a */, + 27BDD5C9232D292700D0A2F1 /* libdracodec.a */, + 27BDD5CB232D292800D0A2F1 /* libdracoenc.a */, 2707800421B5C9140081C27D /* json.hpp */, 2707800621B5C9140081C27D /* stb_image_write.h */, 2707800521B5C9140081C27D /* stb_image.h */, 2707800721B5C9140081C27D /* tiny_gltf.h */, - 2744674621B1F95100E8338E /* GLTFSCN.xcodeproj */, - 2744674021B1F94300E8338E /* GLTF.xcodeproj */, ); name = Frameworks; sourceTree = ""; }; - 2744674121B1F94300E8338E /* Products */ = { - isa = PBXGroup; - children = ( - 2744674521B1F94300E8338E /* GLTF.framework */, - ); - name = Products; - sourceTree = ""; - }; - 2744674721B1F95100E8338E /* Products */ = { + 27BDD5C2232D26AA00D0A2F1 /* TinyGLTFSCN */ = { isa = PBXGroup; children = ( - 2744674B21B1F95100E8338E /* GLTFSCN.framework */, + 27BDD5C5232D26F400D0A2F1 /* TinyGLTFSCN.h */, + 27BDD5C6232D26F400D0A2F1 /* TinyGLTFSCN.mm */, + 27BDD5CF232D31E000D0A2F1 /* GLTFSCNAnimationTargetPair.h */, + 27BDD5D3232D6EB900D0A2F1 /* GLTFSCNAnimationTargetPair.m */, ); - name = Products; + path = TinyGLTFSCN; sourceTree = ""; }; /* End PBXGroup section */ @@ -187,10 +162,10 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 27C37BD421B38688008CF713 /* GLTFErrorCheckerByScenes.h in Headers */, 27077FD121B5AE330081C27D /* ThumbnailGenerator.h in Headers */, - 27C37BC521B367FE008CF713 /* GLTFErrorCheckerByJSON.h in Headers */, + 27BDD5D1232D31E000D0A2F1 /* GLTFSCNAnimationTargetPair.h in Headers */, 27077FCD21B5A20C0081C27D /* SceneGenerator.h in Headers */, + 27BDD5C7232D26F400D0A2F1 /* TinyGLTFSCN.h in Headers */, 27077FD521B5B24B0081C27D /* SCNScene+BoundingBox.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -242,16 +217,6 @@ mainGroup = 2744672321B1F39100E8338E; productRefGroup = 2744672E21B1F39100E8338E /* Products */; projectDirPath = ""; - projectReferences = ( - { - ProductGroup = 2744674121B1F94300E8338E /* Products */; - ProjectRef = 2744674021B1F94300E8338E /* GLTF.xcodeproj */; - }, - { - ProductGroup = 2744674721B1F95100E8338E /* Products */; - ProjectRef = 2744674621B1F95100E8338E /* GLTFSCN.xcodeproj */; - }, - ); projectRoot = ""; targets = ( 2744672C21B1F39100E8338E /* glTF-quicklook */, @@ -259,23 +224,6 @@ }; /* End PBXProject section */ -/* Begin PBXReferenceProxy section */ - 2744674521B1F94300E8338E /* GLTF.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = GLTF.framework; - remoteRef = 2744674421B1F94300E8338E /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 2744674B21B1F95100E8338E /* GLTFSCN.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = GLTFSCN.framework; - remoteRef = 2744674A21B1F95100E8338E /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; -/* End PBXReferenceProxy section */ - /* Begin PBXResourcesBuildPhase section */ 2744672B21B1F39100E8338E /* Resources */ = { isa = PBXResourcesBuildPhase; @@ -295,11 +243,11 @@ 27077FCE21B5A20C0081C27D /* SceneGenerator.m in Sources */, 2744673121B1F39100E8338E /* GenerateThumbnailForURL.m in Sources */, 27077FD221B5AE330081C27D /* ThumbnailGenerator.m in Sources */, - 27C37BC421B367FE008CF713 /* GLTFErrorCheckerByJSON.mm in Sources */, + 27BDD5D4232D6EB900D0A2F1 /* GLTFSCNAnimationTargetPair.m in Sources */, 2744673321B1F39100E8338E /* GeneratePreviewForURL.m in Sources */, 27077FD621B5B24B0081C27D /* SCNScene+BoundingBox.m in Sources */, 2744673521B1F39100E8338E /* main.c in Sources */, - 27C37BD521B38688008CF713 /* GLTFErrorCheckerByScenes.m in Sources */, + 27BDD5C8232D26F400D0A2F1 /* TinyGLTFSCN.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -428,13 +376,18 @@ CLANG_MODULES_DISABLE_PRIVATE_WARNING = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 2G28BM99HR; + DEVELOPMENT_TEAM = 9K99AL2M82; EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_INPUT_FILETYPE = automatic; + HEADER_SEARCH_PATHS = /usr/local/Cellar/draco/1.3.5/include; INFOPLIST_FILE = "$(SRCROOT)/glTF-quicklook/Info.plist"; INSTALL_PATH = /Library/QuickLook; LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks/"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + /usr/local/Cellar/draco/1.3.5/lib, + ); ONLY_ACTIVE_ARCH = YES; OTHER_CPLUSPLUSFLAGS = "$(OTHER_CFLAGS)"; PRODUCT_BUNDLE_IDENTIFIER = "com.ITS.glTF-quicklook"; @@ -452,13 +405,18 @@ CLANG_MODULES_DISABLE_PRIVATE_WARNING = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 2G28BM99HR; + DEVELOPMENT_TEAM = 9K99AL2M82; EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_INPUT_FILETYPE = automatic; + HEADER_SEARCH_PATHS = /usr/local/Cellar/draco/1.3.5/include; INFOPLIST_FILE = "$(SRCROOT)/glTF-quicklook/Info.plist"; INSTALL_PATH = /Library/QuickLook; LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks/"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + /usr/local/Cellar/draco/1.3.5/lib, + ); OTHER_CPLUSPLUSFLAGS = "$(OTHER_CFLAGS)"; PRODUCT_BUNDLE_IDENTIFIER = "com.ITS.glTF-quicklook"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/glTF-quicklook.xcodeproj/xcshareddata/xcschemes/glTF-qucklook.xcscheme b/glTF-quicklook.xcodeproj/xcshareddata/xcschemes/glTF-qucklook.xcscheme index 4335625..29a7991 100644 --- a/glTF-quicklook.xcodeproj/xcshareddata/xcschemes/glTF-qucklook.xcscheme +++ b/glTF-quicklook.xcodeproj/xcshareddata/xcschemes/glTF-qucklook.xcscheme @@ -33,7 +33,7 @@ - -NS_ASSUME_NONNULL_BEGIN - -@interface GR : NSObject -+ (bool) isGoodGLTFByName:(const char*) name; -@end - -NS_ASSUME_NONNULL_END - - diff --git a/glTF-quicklook/GLTFErrorCheckerByJSON.mm b/glTF-quicklook/GLTFErrorCheckerByJSON.mm deleted file mode 100644 index a50263d..0000000 --- a/glTF-quicklook/GLTFErrorCheckerByJSON.mm +++ /dev/null @@ -1,78 +0,0 @@ -// -// GLTFErrorChecker.cpp -// glTF-qucklook -// -// Created by Klochkov Anton on 02/12/2018. -// Copyright © 2018 Klochkov Anton. All rights reserved. -// - - -#include "GLTFErrorCheckerByJSON.h" - -#define TINYGLTF_IMPLEMENTATION -#define STB_IMAGE_IMPLEMENTATION -#define TINYGLTF_NO_STB_IMAGE_WRITE - -#import "stb_image.h" -#import "tiny_gltf.h" - -using namespace tinygltf; - -@interface GR () - -bool privateIsGoodGLTF(const char *name); - -@end - - -@implementation GR - -+ (bool)isGoodGLTFByName:(const char *)name { - return privateIsGoodGLTF(name); -} - - -bool privateIsGoodGLTF(const char *name) { - Model model; - TinyGLTF loader; - std::string err; - std::string warn; - - std::string nameS(name); - bool ret; - - - try { - if (nameS[nameS.size() - 1] == 'b') { - ret = loader.LoadBinaryFromFile(&model, &err, &warn, nameS); // for binary glTF(.glb) - } else { - ret = loader.LoadASCIIFromFile(&model, &err, &warn, nameS); - } - } catch (...) { - return false; - } - - if (!warn.empty()) { -#if DEBUG - NSLog(@"Warn: %s\n", warn.c_str()); -#endif - } - - if (!err.empty()) { -#if DEBUG - NSLog(@"Err: %s\n", err.c_str()); -#endif - return false; - } - - if (!ret) { -#if DEBUG - NSLog(@"Failed to parse glTF\n"); -#endif - return false; - } - - return true; -} - -@end diff --git a/glTF-quicklook/GLTFErrorCheckerByScenes.h b/glTF-quicklook/GLTFErrorCheckerByScenes.h deleted file mode 100644 index 3638c2c..0000000 --- a/glTF-quicklook/GLTFErrorCheckerByScenes.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// GLTFErrorCheckerByScenes.h -// glTF-qucklook -// -// Created by Klochkov Anton on 02/12/2018. -// Copyright © 2018 Klochkov Anton. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface GLTFErrorCheckerByScenes : NSObject -+ (bool) isGoodGLTFByScenes:(NSArray *) scenes; -@end - -NS_ASSUME_NONNULL_END diff --git a/glTF-quicklook/GLTFErrorCheckerByScenes.m b/glTF-quicklook/GLTFErrorCheckerByScenes.m deleted file mode 100644 index 90ac5d2..0000000 --- a/glTF-quicklook/GLTFErrorCheckerByScenes.m +++ /dev/null @@ -1,68 +0,0 @@ -// -// GLTFErrorCheckerByScenes.m -// glTF-qucklook -// -// Created by Klochkov Anton on 02/12/2018. -// Copyright © 2018 Klochkov Anton. All rights reserved. -// - -#import "GLTFErrorCheckerByScenes.h" - -@import GLTF; -@import GLTFSCN; -@import SceneKit; - -@implementation GLTFErrorCheckerByScenes - -+ (bool)isGoodGLTFByScenes:(NSArray *)scenes { - for (GLTFScene *scene in scenes){ - if (![GLTFErrorCheckerByScenes checkNodes:scene.nodes]) - return false; - } - return true; -} - -+ (bool) checkNodes:(NSArray*) nodes { - if (nodes == nil){ - return true; - } - - if (nodes.count == 0) { - return true; - } - - for (GLTFNode *node in nodes){ - for (GLTFSubmesh *mesh in node.mesh.submeshes) { - for (NSString *nameAttrib in mesh.accessorsForAttributes.allKeys) { - - GLTFAccessor *accessor = mesh.accessorsForAttributes[nameAttrib]; - if (accessor == nil) { - return false; - } - - GLTFBufferView *bufferView = accessor.bufferView; - if (bufferView == nil) { - return false; - } - - id buffer = bufferView.buffer; - if (buffer == nil) { - return false; - } - - if (buffer.length == 0) { - return false; - } - } - } - - if (![GLTFErrorCheckerByScenes checkNodes:node.children]){ - return false; - } - } - - return true; -} - - -@end diff --git a/glTF-quicklook/Info.plist b/glTF-quicklook/Info.plist index 9ac86da..e1e46eb 100644 --- a/glTF-quicklook/Info.plist +++ b/glTF-quicklook/Info.plist @@ -25,9 +25,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.0 + 1.2 CFBundleVersion - 1 + 3 CFPlugInDynamicRegisterFunction CFPlugInDynamicRegistration @@ -47,7 +47,7 @@ CFPlugInUnloadFunction NSHumanReadableCopyright - Copyright © 2018 Klochkov Anton. All rights reserved. + Copyright © 2019 Klochkov Anton. All rights reserved. QLNeedsToBeRunInMainThread QLPreviewHeight diff --git a/glTF-quicklook/SceneGenerator.m b/glTF-quicklook/SceneGenerator.m index 23d2588..0397855 100644 --- a/glTF-quicklook/SceneGenerator.m +++ b/glTF-quicklook/SceneGenerator.m @@ -8,11 +8,8 @@ #import "SceneGenerator.h" -#import "GLTFErrorCheckerByJSON.h" -#import "GLTFErrorCheckerByScenes.h" - -@import GLTF; -@import GLTFSCN; +#import "TinyGLTFSCN/TinyGLTFSCN.h" +#import "TinyGLTFSCN/GLTFSCNAnimationTargetPair.h" @implementation SceneGenerator @@ -25,22 +22,14 @@ + (SCNScene *)sceneByURL:(CFURLRef)url { } + (SCNScene *)sceneByNSURL:(NSURL*)url { - SCNScene *scene = nil; - - if (![GR isGoodGLTFByName:url.path.UTF8String]) { - return [SceneGenerator errorScene]; - } + TinyGLTFSCN *loader = [[TinyGLTFSCN alloc] init]; - id bufferAllocator = [[GLTFDefaultBufferAllocator alloc] init]; - GLTFAsset *asset = [[GLTFAsset alloc] initWithURL:url bufferAllocator:bufferAllocator]; - - if (asset == nil || ![GLTFErrorCheckerByScenes isGoodGLTFByScenes:asset.scenes]) { + if (![loader loadModel:url]) { return [SceneGenerator errorScene]; } - GLTFSCNAsset *scnAsset = [SCNScene assetFromGLTFAsset:asset options:@{}]; - scene = scnAsset.defaultScene; - [SceneGenerator setAnimationsToScene:scene scnAsset:scnAsset]; + SCNScene *scene = loader.scenes[0]; + [SceneGenerator enableAnimationsToScene:loader.animations]; return scene; } @@ -60,20 +49,20 @@ + (SCNScene *) errorScene { return [SCNScene sceneWithURL:urlFile options:nil error:nil]; } -+ (void) setAnimationsToScene: (SCNScene*) scene scnAsset:(GLTFSCNAsset *)scnAsset { - NSDictionary *animations = scnAsset.animations; ++ (void) enableAnimationsToScene:(NSDictionary *)animations { + if (animations.count == 0) { + return; + } - if (animations.count != 0) { - NSString *name = scnAsset.animations.allKeys.firstObject; + NSString *name = animations.allKeys.firstObject; #if DEBUG - NSLog(@"Animation name: %@", name); + NSLog(@"Animation name: %@", name); #endif - - [animations[name] enumerateObjectsUsingBlock:^(GLTFSCNAnimationTargetPair *pair, NSUInteger index, BOOL *stop) { - pair.animation.usesSceneTimeBase = NO; - [pair.target addAnimation:pair.animation forKey:nil]; - }]; - } + + [animations[name] enumerateObjectsUsingBlock:^(GLTFSCNAnimationTargetPair *pair, NSUInteger index, BOOL *stop) { + pair.animation.usesSceneTimeBase = NO; + [pair.target addAnimation:pair.animation forKey:nil]; + }]; } diff --git a/glTF-quicklook/TinyGLTFSCN/GLTFSCNAnimationTargetPair.h b/glTF-quicklook/TinyGLTFSCN/GLTFSCNAnimationTargetPair.h new file mode 100644 index 0000000..8cea5e9 --- /dev/null +++ b/glTF-quicklook/TinyGLTFSCN/GLTFSCNAnimationTargetPair.h @@ -0,0 +1,19 @@ +// +// GLTFSCNAnimationTargetPair.h +// glTF-quicklook +// +// Created by Klochkov Anton on 14/09/2019. +// Copyright © 2019 Klochkov Anton. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GLTFSCNAnimationTargetPair : NSObject +@property (nonatomic, strong) CAAnimation *animation; +@property (nonatomic, strong) SCNNode *target; +@end + +NS_ASSUME_NONNULL_END diff --git a/glTF-quicklook/TinyGLTFSCN/GLTFSCNAnimationTargetPair.m b/glTF-quicklook/TinyGLTFSCN/GLTFSCNAnimationTargetPair.m new file mode 100644 index 0000000..238b703 --- /dev/null +++ b/glTF-quicklook/TinyGLTFSCN/GLTFSCNAnimationTargetPair.m @@ -0,0 +1,14 @@ +// +// GLTFSCNAnimationTargetPair.m +// glTF-quicklook +// +// Created by Klochkov Anton on 14/09/2019. +// Copyright © 2019 Klochkov Anton. All rights reserved. +// + +#import + + +@implementation GLTFSCNAnimationTargetPair + +@end diff --git a/glTF-quicklook/TinyGLTFSCN/TinyGLTFSCN.h b/glTF-quicklook/TinyGLTFSCN/TinyGLTFSCN.h new file mode 100644 index 0000000..66e27bb --- /dev/null +++ b/glTF-quicklook/TinyGLTFSCN/TinyGLTFSCN.h @@ -0,0 +1,23 @@ +// +// TinyGLTFSCN.h +// glTF-quicklook +// +// Created by Klochkov Anton on 12/06/2019. +// Copyright © 2019 Klochkov Anton. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TinyGLTFSCN : NSObject + +- (BOOL) loadModel: (NSURL *) modelURL; + +@property (nonatomic, readonly) NSArray *scenes; +@property (nonatomic, readonly) NSDictionary *animations; + +@end + +NS_ASSUME_NONNULL_END diff --git a/glTF-quicklook/TinyGLTFSCN/TinyGLTFSCN.mm b/glTF-quicklook/TinyGLTFSCN/TinyGLTFSCN.mm new file mode 100644 index 0000000..9268d35 --- /dev/null +++ b/glTF-quicklook/TinyGLTFSCN/TinyGLTFSCN.mm @@ -0,0 +1,958 @@ +// +// TinyGLTFSCN.m +// glTF-quicklook +// +// Created by Klochkov Anton on 12/06/2019. +// Copyright © 2019 Klochkov Anton. All rights reserved. +// + +#define TINYGLTF_IMPLEMENTATION +#define STB_IMAGE_IMPLEMENTATION +#define TINYGLTF_NO_STB_IMAGE_WRITE +#define TINYGLTF_ENABLE_DRACO +#define TINYGLTF_USE_CPP14 +#define TINYGLTF_NO_EXTERNAL_IMAGE + +#import "TinyGLTFSCN.h" +#import "GLTFSCNAnimationTargetPair.h" +#include "tiny_gltf.h" + +typedef struct __attribute__((packed)) { + float x, y, z; +} GLTFVector3; + +typedef struct __attribute__((packed)) { + float x, y, z, w; +} GLTFVector4; + +typedef struct __attribute__((packed)) { + GLTFVector4 columns[4]; +} GLTFMatrix4; + +static SCNMatrix4 GLTFSCNMatrix4FromFloat4x4(GLTFMatrix4 m) { + SCNMatrix4 mOut = (SCNMatrix4) { + m.columns[0].x, m.columns[0].y, m.columns[0].z, m.columns[0].w, + m.columns[1].x, m.columns[1].y, m.columns[1].z, m.columns[1].w, + m.columns[2].x, m.columns[2].y, m.columns[2].z, m.columns[2].w, + m.columns[3].x, m.columns[3].y, m.columns[3].z, m.columns[3].w + }; + return mOut; +} + +const std::string GLTFAttributeSemanticPosition = "POSITION"; +const std::string GLTFAttributeSemanticTangent = "TANGENT"; +const std::string GLTFAttributeSemanticNormal = "NORMAL"; +const std::string GLTFAttributeSemanticTexCoord0 = "TEXCOORD_0"; +const std::string GLTFAttributeSemanticTexCoord1 = "TEXCOORD_1"; +const std::string GLTFAttributeSemanticColor0 = "COLOR_0"; +const std::string GLTFAttributeSemanticJoints0 = "JOINTS_0"; +const std::string GLTFAttributeSemanticJoints1 = "JOINTS_1"; +const std::string GLTFAttributeSemanticWeights0 = "WEIGHTS_0"; +const std::string GLTFAttributeSemanticWeights1 = "WEIGHTS_1"; +const std::string GLTFAttributeSemanticRoughness = "ROUGHNESS"; +const std::string GLTFAttributeSemanticMetallic = "METALLIC"; + +typedef NS_ENUM(NSInteger, TinyImageChannel) { + TinyImageChannelRed, + TinyImageChannelGreen, + TinyImageChannelBlue, + TinyImageChannelAlpha, + TinyImageChannelAll = 255 +}; + + +typedef NS_ENUM(NSInteger, GLTF_TYPE) { + GLTF_TYPE_GLTF, + GLTF_TYPE_GLB +}; + + +static SCNGeometryPrimitiveType TinyGLTFSCNGeometryPrimitiveTypeForPrimitiveType(NSInteger primitiveType) { + switch (primitiveType) { + case TINYGLTF_MODE_POINTS: + return SCNGeometryPrimitiveTypePoint; + case TINYGLTF_MODE_LINE: + return SCNGeometryPrimitiveTypeLine; + case TINYGLTF_MODE_TRIANGLES: + return SCNGeometryPrimitiveTypeTriangles; + case TINYGLTF_MODE_TRIANGLE_STRIP: + return SCNGeometryPrimitiveTypeTriangleStrip; + default: + // Unsupported: line loop, line strip, triangle fan + return SCNGeometryPrimitiveTypePolygon; + } +} + +static NSInteger TinyGLTFPrimitiveCountForIndexCount(NSInteger indexCount, SCNGeometryPrimitiveType primitiveType) { + switch (primitiveType) { + case SCNGeometryPrimitiveTypePoint: + return indexCount; + case SCNGeometryPrimitiveTypeLine: + return indexCount / 2; + case SCNGeometryPrimitiveTypeTriangles: + return indexCount / 3; + case SCNGeometryPrimitiveTypeTriangleStrip: + return indexCount - 2; + case SCNGeometryPrimitiveTypePolygon: + return 1; + default: + return 0; + } +} + +static SCNWrapMode GLTFSCNWrapModeForAddressMode(int mode) { + switch (mode) { + case TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE: + return SCNWrapModeClamp; + case TINYGLTF_TEXTURE_WRAP_MIRRORED_REPEAT: + return SCNWrapModeMirror; + case TINYGLTF_TEXTURE_WRAP_REPEAT: + default: + return SCNWrapModeRepeat; + } +} + +size_t TinyGLTFSizeOfComponentTypeWithDimension(NSInteger baseType, NSInteger dimension) +{ + switch (baseType) { + case TINYGLTF_COMPONENT_TYPE_BYTE: + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + switch (dimension) { + case TINYGLTF_TYPE_VEC2: + return 2; + case TINYGLTF_TYPE_VEC3: + return 3; + case TINYGLTF_TYPE_VEC4: + return 4; + default: + break; + } + case TINYGLTF_COMPONENT_TYPE_SHORT: + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + switch (dimension) { + case TINYGLTF_TYPE_VEC2: + return 4; + case TINYGLTF_TYPE_VEC3: + return 6; + case TINYGLTF_TYPE_VEC4: + return 8; + default: + break; + } + case TINYGLTF_COMPONENT_TYPE_INT: + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + case TINYGLTF_COMPONENT_TYPE_FLOAT: + switch (dimension) { + case TINYGLTF_TYPE_SCALAR: + return 4; + case TINYGLTF_TYPE_VEC2: + return 8; + case TINYGLTF_TYPE_VEC3: + return 12; + case TINYGLTF_TYPE_VEC4: + case TINYGLTF_TYPE_MAT2: + return 16; + case TINYGLTF_TYPE_MAT3: + return 36; + case TINYGLTF_TYPE_MAT4: + return 64; + default: + break; + } + default: + break; + } + return 0; +} + +NSInteger TinyGLTFComponentCountForDimension(NSInteger dimension) { + switch (dimension) { + case TINYGLTF_TYPE_SCALAR: + return 1; + case TINYGLTF_TYPE_VEC2: + return 2; + case TINYGLTF_TYPE_VEC3: + return 3; + case TINYGLTF_TYPE_VEC4: + return 4; + case TINYGLTF_TYPE_MAT2: + return 4; + case TINYGLTF_TYPE_MAT3: + return 9; + case TINYGLTF_TYPE_MAT4: + return 16; + default: + return 0; + } +} + + +@interface TinyGLTFSCN () +@property (nonatomic, strong) NSMutableDictionary *scnNodesForTinyNodes; +@property (nonatomic, strong) NSMutableDictionary *> *inverseBindMatricesForSkins; +@property (nonatomic, strong) NSMutableDictionary *cgImagesForImagesAndChannels; +@property (nonatomic, assign) NSInteger namelessAnimationIndex; +@property (nonatomic, strong) NSURL *gltfPath; +@end + + +@implementation TinyGLTFSCN + +- (GLTF_TYPE) typeOfFileByPath: (NSURL *) url { + return [[url pathExtension].lowercaseString isEqualToString:@"glb"] ? GLTF_TYPE_GLB : GLTF_TYPE_GLTF; +} + +- (BOOL) loadModel: (NSURL *) modelURL { + self.scnNodesForTinyNodes = [NSMutableDictionary dictionary]; + self.inverseBindMatricesForSkins = [NSMutableDictionary dictionary]; + self.cgImagesForImagesAndChannels = [NSMutableDictionary dictionary]; + self.namelessAnimationIndex = 0; + self.gltfPath = [modelURL URLByDeletingLastPathComponent]; + + GLTF_TYPE typeOfFile = [self typeOfFileByPath:modelURL]; + + tinygltf::Model model; + tinygltf::TinyGLTF loader; + std::string err; + std::string warn; + bool ret; + + if (typeOfFile == GLTF_TYPE_GLTF) { + ret = loader.LoadASCIIFromFile(&model, &err, &warn, std::string(modelURL.path.UTF8String)); + } else { + ret = loader.LoadBinaryFromFile(&model, &err, &warn, std::string(modelURL.path.UTF8String)); + } + + if (!warn.empty()) { +#if DEBUG + NSLog(@"Warn: %s\n", warn.c_str()); +#endif + } + + if (!err.empty()) { +#if DEBUG + NSLog(@"Err: %s\n", err.c_str()); +#endif + return NO; + } + + if (!ret) { +#if DEBUG + NSLog(@"Failed to parse glTF\n"); +#endif + return NO; + } + + [self loadScenes:model]; + [self loadAnimations:model]; + + return YES; +} + +- (void) loadScenes:(const tinygltf::Model &)model { + NSMutableArray *scenes = [NSMutableArray array]; + + for (const auto& scene: model.scenes) { + SCNScene *scnScene = [SCNScene scene]; + + for (const auto& node: scene.nodes) { + [self recursiveAddNodeWithId:node toSCNNode:scnScene.rootNode fromModel:model]; + } + + [scenes addObject:scnScene]; + } + + _scenes = scenes; +} + +- (void) loadAnimations:(const tinygltf::Model &)model { + NSMutableDictionary *animations = [NSMutableDictionary dictionary]; + for (const auto &animation: model.animations) { + NSString *name = animation.name.size() != 0 ? [[NSString alloc] initWithFormat:@"%s", animation.name.c_str()]: [self _nextAnonymousAnimationName]; + NSMutableArray *pairs = [NSMutableArray array]; + for (size_t i = 0; i < animation.channels.size(); ++i) { + const auto& channel = animation.channels[i]; + const auto& sampler = animation.samplers[channel.sampler]; + + + const auto& inputAccessor = model.accessors[sampler.input]; + const auto& outputAccessor = model.accessors[sampler.output]; + + + CAKeyframeAnimation *keyframeAnimation = nil; + NSString *targetPath = [[NSString alloc] initWithFormat:@"%s", channel.target_path.c_str()]; + + if ([targetPath isEqualToString:@"rotation"]) { + keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"orientation"]; + keyframeAnimation.values = [self arrayFromQuaternionAccessor:outputAccessor fromModel:model]; + } else if ([targetPath isEqualToString:@"translation"]) { + keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"translation"]; + keyframeAnimation.values = [self vectorArrayFromAccessor:outputAccessor fromModel:model]; + } else if ([targetPath isEqualToString:@"scale"]) { + keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"scale"]; + keyframeAnimation.values = [self vectorArrayFromScalarAccessor:outputAccessor fromModel:model]; + } else { + continue; + } + + NSTimeInterval startTime = [self startTime:inputAccessor fromModel:model]; + NSTimeInterval endTime = [self endTime:inputAccessor fromModel:model keyFrameCount:(int)inputAccessor.count]; + + + keyframeAnimation.keyTimes = [self normalizedArrayFromFloatAccessor:inputAccessor fromModel:model minimumValue:startTime maximumValue:endTime]; + keyframeAnimation.beginTime = startTime; + keyframeAnimation.duration = endTime - startTime; + keyframeAnimation.repeatDuration = FLT_MAX; + NSNumber *targetNodeID = [NSNumber numberWithInteger:channel.target_node]; + + SCNNode *scnNode = self.scnNodesForTinyNodes[targetNodeID]; + if (scnNode != nil) { + GLTFSCNAnimationTargetPair *pair = [GLTFSCNAnimationTargetPair new]; + pair.animation = keyframeAnimation; + pair.target = scnNode; + [pairs addObject:pair]; + } else { +#if DEBUG + NSLog(@"WARNING: Could not find node for channel target node identifier %@", targetNodeID); +#endif + } + } + + animations[name] = [pairs copy]; + } + + _animations = animations; + +} + +- (simd_float4x4)computeTransformForTinyNode: (const tinygltf::Node &)node { + simd_quatf _rotationQuaternion = simd_quaternion(0.f, 0.f, 0.f, 1.f); + simd_float3 _scale = vector3(1.0f, 1.0f, 1.0f); + simd_float3 _translation = vector3(0.0f, 0.0f, 0.0f); + + const auto& rotation = node.rotation; + const auto& scale = node.scale; + const auto& translation = node.translation; + + if (rotation.size() == 4) { + _rotationQuaternion = simd_quaternion(float(rotation[0]), float(rotation[1]), float(rotation[2]), float(rotation[3])); + } + + if (scale.size() == 3) { + _scale = vector3(float(scale[0]), float(scale[1]), float(scale[2])); + } + + if (translation.size() == 3) { + _translation = vector3(float(translation[0]), float(translation[1]), float(translation[2])); + } + + simd_float4x4 translationMatrix = matrix_identity_float4x4; + translationMatrix.columns[3][0] = _translation[0]; + translationMatrix.columns[3][1] = _translation[1]; + translationMatrix.columns[3][2] = _translation[2]; + + simd_float4x4 rotationMatrix = simd_matrix4x4(_rotationQuaternion); + + simd_float4x4 scaleMatrix = matrix_identity_float4x4; + scaleMatrix.columns[0][0] = _scale[0]; + scaleMatrix.columns[1][1] = _scale[1]; + scaleMatrix.columns[2][2] = _scale[2]; + + return matrix_multiply(matrix_multiply(translationMatrix, rotationMatrix), scaleMatrix); +} + +- (void) recursiveAddNodeWithId: (NSInteger)nodeID toSCNNode:(SCNNode *)node fromModel: (const tinygltf::Model &)model { + const auto &tinyNode = model.nodes[nodeID]; + SCNNode *scnNode = [self makeSCNNodeForTinyNodeByID:nodeID]; + + if (@available(iOS 11.0, *)) { + scnNode.simdTransform = [self computeTransformForTinyNode:tinyNode]; + } else { + scnNode.transform = SCNMatrix4FromMat4([self computeTransformForTinyNode:tinyNode]); + } + scnNode.name = node.name; + + [node addChildNode:scnNode]; + + NSArray *meshNodes = [self nodesForTinyMesh:tinyNode.mesh withSkin:tinyNode.skin fromModel:model]; + for (SCNNode *meshNode in meshNodes) { + [scnNode addChildNode:meshNode]; + } + + for (const auto &child: tinyNode.children){ + [self recursiveAddNodeWithId:child toSCNNode:scnNode fromModel:model]; + } +} + +- (NSArray *)nodesForTinyMesh: (NSInteger)meshID withSkin:(NSInteger)skinID fromModel:(const tinygltf::Model &)model { + if (meshID == -1){ + return nil; + } + + const auto& mesh = model.meshes[meshID]; + + NSMutableArray *nodes = [NSMutableArray array]; + + NSArray *bones = [self bonesForTinySkin:skinID fromModel:model]; + NSArray *inverseBindMatrices = [self inverseBindMatricesForTinySkin:skinID fromModel:model]; + + for (size_t i = 0; i < mesh.primitives.size(); ++i) { + const auto& submesh = mesh.primitives[i]; + + NSMutableArray *sources = [NSMutableArray array]; + NSMutableArray *elements = [NSMutableArray array]; + + auto addAttribute = [&](const std::string& attribute, SCNGeometrySourceSemantic semantic) { + const auto &attributeIter = submesh.attributes.find(attribute); + + if (attributeIter == submesh.attributes.end()){ + return; + } + + SCNGeometrySource *attributeSource = [self geometrySourceWithSemantic:semantic accessorID:attributeIter->second fromModel:model]; + if (attributeSource != nil) { + [sources addObject:attributeSource]; + } + + return; + }; + + addAttribute(GLTFAttributeSemanticPosition, SCNGeometrySourceSemanticVertex); + addAttribute(GLTFAttributeSemanticNormal, SCNGeometrySourceSemanticNormal); + addAttribute(GLTFAttributeSemanticTangent, SCNGeometrySourceSemanticTangent); + addAttribute(GLTFAttributeSemanticTexCoord0, SCNGeometrySourceSemanticTexcoord); + addAttribute(GLTFAttributeSemanticColor0, SCNGeometrySourceSemanticColor); + + const auto& indexAccessor = model.accessors[submesh.indices]; + const auto& indexBufferView = model.bufferViews[indexAccessor.bufferView]; + const auto& indexBuffer = model.buffers[indexBufferView.buffer]; + + SCNGeometryPrimitiveType primitiveType = TinyGLTFSCNGeometryPrimitiveTypeForPrimitiveType(submesh.mode); + NSInteger bytesPerIndex = (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) ? sizeof(uint16_t) : sizeof(uint32_t); + NSData *indexData = [NSData dataWithBytes:indexBuffer.data.data() + indexBufferView.byteOffset + indexAccessor.byteOffset length:indexAccessor.count * bytesPerIndex]; + NSInteger indexCount = indexAccessor.count; + NSInteger primitiveCount = TinyGLTFPrimitiveCountForIndexCount(indexCount, primitiveType); + SCNGeometryElement *geometryElement = [SCNGeometryElement geometryElementWithData:indexData + primitiveType:primitiveType + primitiveCount:primitiveCount + bytesPerIndex:bytesPerIndex]; + [elements addObject:geometryElement]; + + SCNGeometry *geometry = [SCNGeometry geometryWithSources:sources elements:elements]; + + + SCNMaterial *material = [self materialForTinyMaterial:submesh.material fromModel:model]; + if (material != nil) { + geometry.materials = @[material]; + } + + SCNNode *node = [SCNNode node]; + node.geometry = geometry; + + const auto& boneWeightsIter = submesh.attributes.find(GLTFAttributeSemanticWeights0); + const auto& boneIndicesIter = submesh.attributes.find(GLTFAttributeSemanticJoints0); + + if (boneIndicesIter != submesh.attributes.end() && boneWeightsIter != submesh.attributes.end()) { + SCNGeometrySource *boneWeights = [self geometrySourceWithSemantic:SCNGeometrySourceSemanticVertex accessorID:boneWeightsIter->second fromModel:model]; + SCNGeometrySource *boneIndices = [self geometrySourceWithSemantic:SCNGeometrySourceSemanticVertex accessorID:boneIndicesIter->second fromModel:model]; + + if (boneWeights != nil && boneIndices != nil) { + SCNSkinner *skinner = [SCNSkinner skinnerWithBaseGeometry:geometry + bones:bones + boneInverseBindTransforms:inverseBindMatrices + boneWeights:boneWeights + boneIndices:boneIndices]; + node.skinner = skinner; + } + } + + [nodes addObject:node]; + } + + return nodes; +} + +- (SCNMaterial *)materialForTinyMaterial:(NSInteger)materialID fromModel:(const tinygltf::Model &)model { + if (materialID == -1) { + return nil; + } + + const auto& material = model.materials[materialID]; + + SCNMaterial *scnMaterial = [SCNMaterial material]; + + scnMaterial.name = [NSString stringWithUTF8String:material.name.c_str()]; + + scnMaterial.lightingModelName = SCNLightingModelPhysicallyBased; + scnMaterial.doubleSided = material.doubleSided; + + if (material.pbrMetallicRoughness.baseColorTexture.index != -1) { + const auto& baseColorTexture = model.textures[material.pbrMetallicRoughness.baseColorTexture.index]; + + if (baseColorTexture.source != -1) { + scnMaterial.diffuse.contents = (__bridge id)[self cgImageForTinyImage:baseColorTexture.source channelMask:TinyImageChannel::TinyImageChannelAll fromModel:model]; + } + + if (baseColorTexture.sampler != -1) { + const auto &baseColorTextureSampler = model.samplers[baseColorTexture.sampler]; + scnMaterial.diffuse.wrapS = GLTFSCNWrapModeForAddressMode(baseColorTextureSampler.wrapS); + scnMaterial.diffuse.wrapT = GLTFSCNWrapModeForAddressMode(baseColorTextureSampler.wrapT); + } + } + + if (scnMaterial.diffuse.contents == nil || material.pbrMetallicRoughness.baseColorTexture.index == -1) { + const auto& colorFactor = material.pbrMetallicRoughness.baseColorFactor; + + if (colorFactor.size() >= 4) { + scnMaterial.diffuse.contents = (__bridge_transfer id)[self newCGColorForFloat4:simd_make_float4(colorFactor[0], colorFactor[1], colorFactor[2], colorFactor[3])]; + } + } + + scnMaterial.diffuse.mappingChannel = material.pbrMetallicRoughness.baseColorTexture.texCoord; + + if (material.pbrMetallicRoughness.metallicRoughnessTexture.index != -1) { + const auto& metallicRoughnessTexture = model.textures[material.pbrMetallicRoughness.metallicRoughnessTexture.index]; + + if (metallicRoughnessTexture.source != -1) { + scnMaterial.metalness.contents = (__bridge id)[self cgImageForTinyImage:metallicRoughnessTexture.source channelMask:TinyImageChannel::TinyImageChannelBlue fromModel:model]; + + scnMaterial.roughness.contents = (__bridge id)[self cgImageForTinyImage:metallicRoughnessTexture.source channelMask:TinyImageChannel::TinyImageChannelGreen fromModel:model]; + } + + if (metallicRoughnessTexture.sampler != -1) { + const auto &metallicRoughnessTextureSampler = model.samplers[metallicRoughnessTexture.sampler]; + scnMaterial.metalness.wrapS = GLTFSCNWrapModeForAddressMode(metallicRoughnessTextureSampler.wrapS); + scnMaterial.metalness.wrapT = GLTFSCNWrapModeForAddressMode(metallicRoughnessTextureSampler.wrapT); + scnMaterial.roughness.wrapS = GLTFSCNWrapModeForAddressMode(metallicRoughnessTextureSampler.wrapS); + scnMaterial.roughness.wrapT = GLTFSCNWrapModeForAddressMode(metallicRoughnessTextureSampler.wrapT); + } + } + + if (scnMaterial.metalness.contents == nil || material.pbrMetallicRoughness.metallicRoughnessTexture.index == -1) { + scnMaterial.metalness.contents = @(material.pbrMetallicRoughness.metallicFactor); + } + + if (scnMaterial.roughness.contents == nil || material.pbrMetallicRoughness.metallicRoughnessTexture.index == -1) { + scnMaterial.metalness.contents = @(material.pbrMetallicRoughness.roughnessFactor); + } + + scnMaterial.metalness.mappingChannel = material.pbrMetallicRoughness.metallicRoughnessTexture.texCoord; + scnMaterial.roughness.mappingChannel = material.pbrMetallicRoughness.metallicRoughnessTexture.texCoord; + + + + if (material.normalTexture.index != -1) { + const auto& normalTexture = model.textures[material.normalTexture.index]; + + if (normalTexture.source != -1) { + scnMaterial.normal.contents = (__bridge id)[self cgImageForTinyImage:normalTexture.source channelMask:TinyImageChannel::TinyImageChannelAll fromModel:model]; + } + + if (normalTexture.sampler != -1) { + const auto &normalTextureSampler = model.samplers[normalTexture.sampler]; + scnMaterial.normal.wrapS = GLTFSCNWrapModeForAddressMode(normalTextureSampler.wrapS); + scnMaterial.normal.wrapT = GLTFSCNWrapModeForAddressMode(normalTextureSampler.wrapT); + } + } + + scnMaterial.normal.mappingChannel = material.normalTexture.texCoord; + + if (material.occlusionTexture.index != -1) { + const auto& occlusionTexture = model.textures[material.occlusionTexture.index]; + + if (occlusionTexture.source != -1) { + scnMaterial.ambientOcclusion.contents = (__bridge id)[self cgImageForTinyImage:occlusionTexture.source channelMask:TinyImageChannel::TinyImageChannelRed fromModel:model]; + } + + if (occlusionTexture.sampler != -1) { + const auto &occlusionTextureSampler = model.samplers[occlusionTexture.sampler]; + scnMaterial.ambientOcclusion.wrapS = GLTFSCNWrapModeForAddressMode(occlusionTextureSampler.wrapS); + scnMaterial.ambientOcclusion.wrapT = GLTFSCNWrapModeForAddressMode(occlusionTextureSampler.wrapT); + } + } + + scnMaterial.ambientOcclusion.mappingChannel = material.occlusionTexture.texCoord; + + if (material.emissiveTexture.index != -1) { + const auto& emissiveTexture = model.textures[material.emissiveTexture.index]; + + if (emissiveTexture.source != -1) { + scnMaterial.emission.contents = (__bridge id)[self cgImageForTinyImage:emissiveTexture.source channelMask:TinyImageChannel::TinyImageChannelAll fromModel:model]; + } + + if (emissiveTexture.sampler != -1) { + const auto &emissiveTextureSampler = model.samplers[emissiveTexture.sampler]; + scnMaterial.emission.wrapS = GLTFSCNWrapModeForAddressMode(emissiveTextureSampler.wrapS); + scnMaterial.emission.wrapT = GLTFSCNWrapModeForAddressMode(emissiveTextureSampler.wrapT); + } + } + + if (scnMaterial.emission.contents == nil || material.emissiveTexture.index == -1) { + const auto& emissiveFactor = material.emissiveFactor; + + if (emissiveFactor.size() >= 3) { + scnMaterial.emission.contents = (__bridge_transfer id)[self newCGColorForFloat3:simd_make_float3(emissiveFactor[0], emissiveFactor[1], emissiveFactor[2])]; + } + } + + scnMaterial.emission.mappingChannel = material.emissiveTexture.texCoord; + + return scnMaterial; +} + +- (CGImageRef)newCGImageByExtractingChannel:(NSInteger)channelIndex fromCGImage:(const tinygltf::Image *)sourceImage { + if (sourceImage == NULL) { + return NULL; + } + + NSData *imageData = [[NSData alloc] initWithBytes:sourceImage->image.data() length:sourceImage->image.size()]; + + CGDataProviderRef imgDataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef) imageData); + CGImageRef imageRef = CGImageCreateWithJPEGDataProvider(imgDataProvider, NULL, true, kCGRenderingIntentDefault); + + size_t width = sourceImage->width; + size_t height = sourceImage->height; + size_t bpc = 8; + size_t Bpr = width * 4; + + uint8_t *pixels = (uint8_t *)malloc(Bpr * height); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGContextRef context = CGBitmapContextCreate(pixels, width, height, bpc, Bpr, colorSpace, kCGImageAlphaPremultipliedLast); + CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); + + for (int i = 0; i < width * height; ++i) { + uint8_t components[4] = { pixels[i * 4 + 0], pixels[i * 4 + 1], pixels[i * 4 + 2], pixels[i * 4 + 3] }; // RGBA + pixels[i] = components[channelIndex]; + } + + CGColorSpaceRef monoColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericGrayGamma2_2); + CGContextRef monoContext = CGBitmapContextCreate(pixels, width, height, bpc, width, monoColorSpace, kCGImageAlphaNone); + + CGImageRef channelImage = CGBitmapContextCreateImage(monoContext); + + CGColorSpaceRelease(monoColorSpace); + CGContextRelease(monoContext); + CGColorSpaceRelease(colorSpace); + CGContextRelease(context); + free(pixels); + // CGImageRelease(imageRef); + + return channelImage; +} + +- (CGImage *)cgImageForTinyImage:(NSInteger)imageID channelMask:(TinyImageChannel)channelMask fromModel:(const tinygltf::Model &)model { + if (imageID == -1) { + return nil; + } + + const auto& image = model.images[imageID]; + + NSString *imageName = nil; + if (image.name.size() == 0) { + imageName = [NSString stringWithFormat:@"%ld", (long)imageID]; + } else { + imageName = [NSString stringWithFormat:@"%s", image.name.c_str()]; + } + + NSString *maskedIdentifier = [NSString stringWithFormat:@"%@/%d", imageName, (int)channelMask]; + + // Check the cache to see if we already have an exact match for the requested image and channel subset + CGImageRef exactCachedImage = (__bridge CGImageRef)self.cgImagesForImagesAndChannels[maskedIdentifier]; + if (exactCachedImage != nil) { + return exactCachedImage; + } + + // If we don't have an exact match for the image+channel pair, we may still have the original image cached + NSString *unmaskedIdentifier = [NSString stringWithFormat:@"%@/%d", imageName, (int)TinyImageChannel::TinyImageChannelAll]; + CGImageRef originalImage = (__bridge CGImageRef)self.cgImagesForImagesAndChannels[unmaskedIdentifier]; + + if (originalImage == NULL) { + // We got unlucky, so we need to load and cache the original + if (image.uri.size() != 0) { + + NSURL *url = [[NSURL alloc] initFileURLWithPath:[[NSString alloc] initWithFormat:@"%s", image.uri.c_str()] relativeToURL:self.gltfPath]; + CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)url, nil); + originalImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil); + if (imageSource) { + CFRelease(imageSource); + } + } else if ( image.bufferView != -1) { + + const auto& bufferView = model.bufferViews[image.bufferView]; + const auto& buffer = model.buffers[bufferView.buffer]; + + NSData *imageData = [NSData dataWithBytes:buffer.data.data() + bufferView.byteOffset length:bufferView.byteLength]; + CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, nil); + originalImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil); + if (imageSource) { + CFRelease(imageSource); + } + } + + self.cgImagesForImagesAndChannels[unmaskedIdentifier] = (__bridge id)originalImage; + CGImageRelease(originalImage); + } + + // Now that we have the original, we may need to extract the requisite channel and cache the result + if (channelMask != TinyImageChannel::TinyImageChannelAll) { + CGImageRef extractedImage = [self newCGImageByExtractingChannel:(int)channelMask fromCGImage:&image]; + self.cgImagesForImagesAndChannels[maskedIdentifier] = (__bridge id)extractedImage; + CGImageRelease(extractedImage); + return extractedImage; + } + + return originalImage; +} + +- (SCNGeometrySource *)geometrySourceWithSemantic:(SCNGeometrySourceSemantic)semantic accessorID:(NSInteger)accessorID fromModel: (const tinygltf::Model &)model{ + if (accessorID == -1) { + return nil; + } + + const auto& accessor = model.accessors[accessorID]; + + const auto& bufferView = model.bufferViews[accessor.bufferView]; + const auto& buffer = model.buffers[bufferView.buffer]; + + NSInteger bytesPerElement = TinyGLTFSizeOfComponentTypeWithDimension(accessor.componentType, accessor.type); + BOOL componentsAreFloat = TINYGLTF_COMPONENT_TYPE_FLOAT == accessor.componentType; + NSInteger componentsPerElement = TinyGLTFComponentCountForDimension(accessor.type); + NSInteger bytesPerComponent = bytesPerElement / componentsPerElement; + NSInteger dataOffset = 0; + NSInteger dataStride = bufferView.byteStride; + if (dataStride == 0) { + dataStride = bytesPerElement; + } + + const char *dataBase = ((char *)(buffer.data.data()) + bufferView.byteOffset + accessor.byteOffset); + + if ([semantic isEqualToString:SCNGeometrySourceSemanticBoneWeights]) + { + for (int i = 0; i < accessor.count; ++i) { + float *weights = (float *)(dataBase + i * dataStride); + float sum = weights[0] + weights[1] + weights[2] + weights[3]; + if (sum != 1.0f) { + weights[0] /= sum; + weights[1] /= sum; + weights[2] /= sum; + weights[3] /= sum; + } + } + } + + NSData *data = [NSData dataWithBytes:dataBase length:accessor.count * dataStride]; + + SCNGeometrySource *source = [SCNGeometrySource geometrySourceWithData:data + semantic:semantic + vectorCount:accessor.count + floatComponents:componentsAreFloat + componentsPerVector:componentsPerElement + bytesPerComponent:bytesPerComponent + dataOffset:dataOffset + dataStride:dataStride]; + return source; +} + +- (NSArray *)inverseBindMatricesForTinySkin: (NSInteger) skinID fromModel: (const tinygltf::Model &)model { + if (skinID == -1){ + return @[]; + } + + NSNumber *objectSkinID = [NSNumber numberWithInteger:skinID]; + const auto& skin = model.skins[skinID]; + + NSArray *inverseBindMatrices = self.inverseBindMatricesForSkins[objectSkinID]; + + if (inverseBindMatrices != nil) { + return inverseBindMatrices; + } + + NSMutableArray *matrices = [NSMutableArray array]; + + const auto& accessor = model.accessors[skin.inverseBindMatrices]; + const auto& bufferView = model.bufferViews[accessor.bufferView]; + + const unsigned char *buffer = model.buffers[bufferView.buffer].data.data(); + GLTFMatrix4 *ibms = (GLTFMatrix4 *)(buffer + bufferView.byteOffset + accessor.byteOffset); + + for (int i = 0; i < accessor.count; ++i) { + SCNMatrix4 ibm = GLTFSCNMatrix4FromFloat4x4(ibms[i]); + NSValue *ibmValue = [NSValue valueWithSCNMatrix4:ibm]; + [matrices addObject:ibmValue]; + } + matrices = [matrices copy]; + self.inverseBindMatricesForSkins[objectSkinID] = matrices; + + return matrices; +} + +- (NSArray *) bonesForTinySkin: (NSInteger) skinID fromModel: (const tinygltf::Model &)model { + if (skinID == -1) { + return @[]; + } + + const auto& skin = model.skins[skinID]; + + NSMutableArray *bones = [NSMutableArray array]; + for (const auto& jointNode: skin.joints) { + SCNNode *boneNode = [self makeSCNNodeForTinyNodeByID:jointNode]; + if (boneNode != nil) { + [bones addObject:boneNode]; + } else { +#if DEBUG + NSLog(@"WARNING: Did not find node for joint with identifier %d", jointNode); +#endif + } + } + + if (bones.count == skin.joints.size()) { + return [bones copy]; + } else { +#if DEBUG + NSLog(@"WARNING: Bone count for skinner does not match joint node count for skin with identifier %ld", (long)skinID); +#endif + } + + return @[]; +} + +- (SCNNode *) makeSCNNodeForTinyNodeByID: (NSInteger)nodeID { + NSNumber *objectNodeID = [NSNumber numberWithInteger:nodeID]; + SCNNode *scnNode = self.scnNodesForTinyNodes[objectNodeID]; + + if (scnNode == nil) { + scnNode = [SCNNode node]; + self.scnNodesForTinyNodes[objectNodeID] = scnNode; + } + + return scnNode; +} + +- (CGColorRef)newCGColorForFloat4:(simd_float4)v { + CGFloat components[] = { v.x, v.y, v.z, v.w }; + CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGColorRef color = CGColorCreate(colorSpace, &components[0]); + CGColorSpaceRelease(colorSpace); + return color; +} + +- (CGColorRef)newCGColorForFloat3:(simd_float3)v { + CGFloat components[] = { v.x, v.y, v.z, 1 }; + CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGColorRef color = CGColorCreate(colorSpace, &components[0]); + CGColorSpaceRelease(colorSpace); + return color; +} + +- (NSArray *)arrayFromQuaternionAccessor:(const tinygltf::Accessor &)accessor fromModel: (const tinygltf::Model &)model { + + const auto& buffetView = model.bufferViews[accessor.bufferView]; + const auto& buffer = model.buffers[buffetView.buffer]; + + NSMutableArray *values = [NSMutableArray array]; + + const GLTFVector4 *quaternions = (const GLTFVector4 *)((const char *)buffer.data.data() + buffetView.byteOffset + accessor.byteOffset); + NSInteger count = accessor.count; + for (NSInteger i = 0; i < count; ++i) { + SCNVector4 quat = SCNVector4Zero; + if (quaternions != nullptr) { + quat = (SCNVector4){ quaternions[i].x, quaternions[i].y, quaternions[i].z, quaternions[i].w }; + } + NSValue *value = [NSValue valueWithSCNVector4:quat]; + [values addObject:value]; + } + return [values copy]; +} + +- (NSArray *)vectorArrayFromAccessor:(const tinygltf::Accessor &)accessor fromModel: (const tinygltf::Model &)model { + const auto& buffetView = model.bufferViews[accessor.bufferView]; + const auto& buffer = model.buffers[buffetView.buffer]; + + NSMutableArray *values = [NSMutableArray array]; + const GLTFVector3 *vectors = (const GLTFVector3 *)((const char *)buffer.data.data() + buffetView.byteOffset + accessor.byteOffset); + NSInteger count = accessor.count; + for (NSInteger i = 0; i < count; ++i) { + SCNVector3 scnVec = SCNVector3Zero; + if (vectors != nullptr) { + GLTFVector3 vec = vectors[i]; + scnVec = (SCNVector3){ vec.x, vec.y, vec.z }; + } + + NSValue *value = [NSValue valueWithSCNVector3:scnVec]; + [values addObject:value]; + } + return [values copy]; +} + +- (NSArray *)vectorArrayFromScalarAccessor:(const tinygltf::Accessor &)accessor fromModel: (const tinygltf::Model &)model { + const auto& buffetView = model.bufferViews[accessor.bufferView]; + const auto& buffer = model.buffers[buffetView.buffer]; + + NSMutableArray *values = [NSMutableArray array]; + const float *floats = (const float *)((const char *)buffer.data.data() + buffetView.byteOffset + accessor.byteOffset); + NSInteger count = accessor.count; + for (NSInteger i = 0; i < count; ++i) { + SCNVector3 scnVec = SCNVector3Zero; + if (floats != nullptr) { + scnVec = (SCNVector3){ floats[i], floats[i], floats[i] }; + } + NSValue *value = [NSValue valueWithSCNVector3:scnVec]; + [values addObject:value]; + } + return [values copy]; +} + +- (NSArray *)normalizedArrayFromFloatAccessor:(const tinygltf::Accessor &)accessor fromModel: (const tinygltf::Model &)model minimumValue:(float)minimumValue maximumValue:(float)maximumValue { + const auto& buffetView = model.bufferViews[accessor.bufferView]; + const auto& buffer = model.buffers[buffetView.buffer]; + + NSMutableArray *values = [NSMutableArray array]; + const float *floats = (const float *)((const char *)buffer.data.data() + buffetView.byteOffset + accessor.byteOffset); + NSInteger count = accessor.count; + for (NSInteger i = 0; i < count; ++i) { + float f = 0; + if (floats != nullptr) { + f = floats[i]; + } + + f = fmin(fmax(0, (f - minimumValue) / (maximumValue - minimumValue)), 1); + NSValue *value = [NSNumber numberWithFloat:f]; + [values addObject:value]; + } + return [values copy]; +} + +- (NSTimeInterval)startTime:(const tinygltf::Accessor &)inputAccessor fromModel: (const tinygltf::Model &)model { + const auto& buffetView = model.bufferViews[inputAccessor.bufferView]; + const auto& buffer = model.buffers[buffetView.buffer]; + + const float *timeValues = (const float *)((const char *)buffer.data.data() + buffetView.byteOffset + inputAccessor.byteOffset); + + float startTime = 0; + if (timeValues != nullptr) { + startTime = timeValues[0]; + } + return startTime; +} + +- (NSTimeInterval)endTime:(const tinygltf::Accessor &)outputAccessor fromModel: (const tinygltf::Model &)model keyFrameCount:(int)keyFrameCount { + const auto& buffetView = model.bufferViews[outputAccessor.bufferView]; + const auto& buffer = model.buffers[buffetView.buffer]; + + const float *timeValues = (const float *)((const char *)buffer.data.data() + buffetView.byteOffset + outputAccessor.byteOffset); + float endTime = 0; + if (timeValues != nullptr) { + endTime = timeValues[keyFrameCount - 1]; + } + + return endTime; +} + +- (NSString *)_nextAnonymousAnimationName { + NSString *name = [NSString stringWithFormat:@"UNNAMED_%d", (int)self.namelessAnimationIndex]; + self.namelessAnimationIndex = self.namelessAnimationIndex + 1; + return name; +} + +@end diff --git a/tinygltf b/tinygltf index d80f0f9..7e00904 160000 --- a/tinygltf +++ b/tinygltf @@ -1 +1 @@ -Subproject commit d80f0f9a6c89947f011cad033db61877ec683d04 +Subproject commit 7e009041e35b999fd1e47c0f0e42cadcf8f5c31c