mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-08-09 15:58:35 +02:00
Merge pull request #11346 from birarda/feat/draco-in-baking
Add mesh compression to baking library and FBXReader
This commit is contained in:
commit
cbe61871dc
31 changed files with 1243 additions and 1570 deletions
40
cmake/externals/draco/CMakeLists.txt
vendored
Normal file
40
cmake/externals/draco/CMakeLists.txt
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
set(EXTERNAL_NAME draco)
|
||||||
|
|
||||||
|
if (ANDROID)
|
||||||
|
set(ANDROID_CMAKE_ARGS "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" "-DANDROID_NATIVE_API_LEVEL=19")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (APPLE)
|
||||||
|
set(EXTRA_CMAKE_FLAGS -DCMAKE_CXX_FLAGS=-stdlib=libc++ -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
include(ExternalProject)
|
||||||
|
ExternalProject_Add(
|
||||||
|
${EXTERNAL_NAME}
|
||||||
|
URL http://hifi-public.s3.amazonaws.com/dependencies/draco-1.1.0.zip
|
||||||
|
URL_MD5 208f8b04c91d5f1c73d731a3ea37c5bb
|
||||||
|
CONFIGURE_COMMAND CMAKE_ARGS ${ANDROID_CMAKE_ARGS} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> ${EXTRA_CMAKE_FLAGS}
|
||||||
|
LOG_DOWNLOAD 1
|
||||||
|
LOG_CONFIGURE 1
|
||||||
|
LOG_BUILD 1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Hide this external target (for ide users)
|
||||||
|
set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")
|
||||||
|
|
||||||
|
ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR)
|
||||||
|
|
||||||
|
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
|
||||||
|
|
||||||
|
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE PATH "List of Draco include directories")
|
||||||
|
|
||||||
|
if (UNIX)
|
||||||
|
set(LIB_PREFIX "lib")
|
||||||
|
set(LIB_EXT "a")
|
||||||
|
elseif (WIN32)
|
||||||
|
set(LIB_EXT "lib")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(${EXTERNAL_NAME_UPPER}_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}draco.${LIB_EXT} CACHE FILEPATH "Path to Draco release library")
|
||||||
|
set(${EXTERNAL_NAME_UPPER}_ENCODER_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}dracoenc.${LIB_EXT} CACHE FILEPATH "Path to Draco encoder release library")
|
||||||
|
set(${EXTERNAL_NAME_UPPER}_DECODER_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}dracodec.${LIB_EXT} CACHE FILEPATH "Path to Draco decoder release library")
|
30
cmake/modules/FindDraco.cmake
Normal file
30
cmake/modules/FindDraco.cmake
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#
|
||||||
|
# FindDraco.cmake
|
||||||
|
#
|
||||||
|
# Try to find Draco libraries and include path.
|
||||||
|
# Once done this will define
|
||||||
|
#
|
||||||
|
# DRACO_FOUND
|
||||||
|
# DRACO_INCLUDE_DIRS
|
||||||
|
# DRACO_LIBRARY
|
||||||
|
# DRACO_ENCODER_LIBRARY
|
||||||
|
# DRACO_DECODER_LIBRARY
|
||||||
|
#
|
||||||
|
# Created on 8/8/2017 by Stephen Birarda
|
||||||
|
# Copyright 2017 High Fidelity, Inc.
|
||||||
|
#
|
||||||
|
# Distributed under the Apache License, Version 2.0.
|
||||||
|
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
#
|
||||||
|
|
||||||
|
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
|
||||||
|
hifi_library_search_hints("draco")
|
||||||
|
|
||||||
|
find_path(DRACO_INCLUDE_DIRS draco/core/draco_types.h PATH_SUFFIXES include/draco/src include HINTS ${DRACO_SEARCH_DIRS})
|
||||||
|
|
||||||
|
find_library(DRACO_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS})
|
||||||
|
find_library(DRACO_ENCODER_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS})
|
||||||
|
find_library(DRACO_DECODER_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS})
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(DRACO DEFAULT_MSG DRACO_INCLUDE_DIRS DRACO_LIBRARY DRACO_ENCODER_LIBRARY DRACO_DECODER_LIBRARY)
|
|
@ -1,111 +0,0 @@
|
||||||
# Locate the FBX SDK
|
|
||||||
#
|
|
||||||
# Defines the following variables:
|
|
||||||
#
|
|
||||||
# FBX_FOUND - Found the FBX SDK
|
|
||||||
# FBX_VERSION - Version number
|
|
||||||
# FBX_INCLUDE_DIRS - Include directories
|
|
||||||
# FBX_LIBRARIES - The libraries to link to
|
|
||||||
#
|
|
||||||
# Accepts the following variables as input:
|
|
||||||
#
|
|
||||||
# FBX_VERSION - as a CMake variable, e.g. 2017.0.1
|
|
||||||
# FBX_ROOT - (as a CMake or environment variable)
|
|
||||||
# The root directory of the FBX SDK install
|
|
||||||
|
|
||||||
# adapted from https://github.com/ufz-vislab/VtkFbxConverter/blob/master/FindFBX.cmake
|
|
||||||
# which uses the MIT license (https://github.com/ufz-vislab/VtkFbxConverter/blob/master/LICENSE.txt)
|
|
||||||
|
|
||||||
if (NOT FBX_VERSION)
|
|
||||||
set(FBX_VERSION 2017.1)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
string(REGEX REPLACE "^([0-9]+).*$" "\\1" FBX_VERSION_MAJOR "${FBX_VERSION}")
|
|
||||||
string(REGEX REPLACE "^[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_MINOR "${FBX_VERSION}")
|
|
||||||
string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_PATCH "${FBX_VERSION}")
|
|
||||||
|
|
||||||
set(FBX_MAC_LOCATIONS "/Applications/Autodesk/FBX\ SDK/${FBX_VERSION}")
|
|
||||||
set(FBX_LINUX_LOCATIONS "/usr/local/fbxsdk")
|
|
||||||
|
|
||||||
if (WIN32)
|
|
||||||
string(REGEX REPLACE "\\\\" "/" WIN_PROGRAM_FILES_X64_DIRECTORY $ENV{ProgramW6432})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(FBX_WIN_LOCATIONS "${WIN_PROGRAM_FILES_X64_DIRECTORY}/Autodesk/FBX/FBX SDK/${FBX_VERSION}")
|
|
||||||
|
|
||||||
set(FBX_SEARCH_LOCATIONS $ENV{FBX_ROOT} ${FBX_ROOT} ${FBX_MAC_LOCATIONS} ${FBX_WIN_LOCATIONS} ${FBX_LINUX_LOCATIONS})
|
|
||||||
|
|
||||||
function(_fbx_append_debugs _endvar _library)
|
|
||||||
if (${_library} AND ${_library}_DEBUG)
|
|
||||||
set(_output optimized ${${_library}} debug ${${_library}_DEBUG})
|
|
||||||
else()
|
|
||||||
set(_output ${${_library}})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(${_endvar} ${_output} PARENT_SCOPE)
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang")
|
|
||||||
set(fbx_compiler clang)
|
|
||||||
elseif (${CMAKE_CXX_COMPILER_ID} MATCHES "GNU")
|
|
||||||
set(fbx_compiler gcc4)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
function(_fbx_find_library _name _lib _suffix)
|
|
||||||
if (MSVC_VERSION EQUAL 1910)
|
|
||||||
set(VS_PREFIX vs2015)
|
|
||||||
elseif (MSVC_VERSION EQUAL 1900)
|
|
||||||
set(VS_PREFIX vs2015)
|
|
||||||
elseif (MSVC_VERSION EQUAL 1800)
|
|
||||||
set(VS_PREFIX vs2013)
|
|
||||||
elseif (MSVC_VERSION EQUAL 1700)
|
|
||||||
set(VS_PREFIX vs2012)
|
|
||||||
elseif (MSVC_VERSION EQUAL 1600)
|
|
||||||
set(VS_PREFIX vs2010)
|
|
||||||
elseif (MSVC_VERSION EQUAL 1500)
|
|
||||||
set(VS_PREFIX vs2008)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
find_library(${_name}
|
|
||||||
NAMES ${_lib}
|
|
||||||
HINTS ${FBX_SEARCH_LOCATIONS}
|
|
||||||
PATH_SUFFIXES lib/${fbx_compiler}/${_suffix} lib/${fbx_compiler}/x64/${_suffix} lib/${fbx_compiler}/ub/${_suffix} lib/${VS_PREFIX}/x64/${_suffix}
|
|
||||||
)
|
|
||||||
|
|
||||||
mark_as_advanced(${_name})
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
find_path(FBX_INCLUDE_DIR fbxsdk.h
|
|
||||||
PATHS ${FBX_SEARCH_LOCATIONS}
|
|
||||||
PATH_SUFFIXES include
|
|
||||||
)
|
|
||||||
mark_as_advanced(FBX_INCLUDE_DIR)
|
|
||||||
|
|
||||||
if (WIN32)
|
|
||||||
_fbx_find_library(FBX_LIBRARY libfbxsdk-md release)
|
|
||||||
_fbx_find_library(FBX_LIBRARY_DEBUG libfbxsdk-md debug)
|
|
||||||
elseif (APPLE)
|
|
||||||
find_library(CARBON NAMES Carbon)
|
|
||||||
find_library(SYSTEM_CONFIGURATION NAMES SystemConfiguration)
|
|
||||||
_fbx_find_library(FBX_LIBRARY libfbxsdk.a release)
|
|
||||||
_fbx_find_library(FBX_LIBRARY_DEBUG libfbxsdk.a debug)
|
|
||||||
else ()
|
|
||||||
_fbx_find_library(FBX_LIBRARY libfbxsdk.a release)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
include(FindPackageHandleStandardArgs)
|
|
||||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(FBX DEFAULT_MSG FBX_LIBRARY FBX_INCLUDE_DIR)
|
|
||||||
|
|
||||||
if (FBX_FOUND)
|
|
||||||
set(FBX_INCLUDE_DIRS ${FBX_INCLUDE_DIR})
|
|
||||||
_fbx_append_debugs(FBX_LIBRARIES FBX_LIBRARY)
|
|
||||||
add_definitions(-DFBXSDK_NEW_API)
|
|
||||||
|
|
||||||
if (WIN32)
|
|
||||||
add_definitions(-DK_PLUGIN -DK_FBXSDK -DK_NODLL)
|
|
||||||
set(CMAKE_EXE_LINKER_FLAGS /NODEFAULTLIB:\"LIBCMT\")
|
|
||||||
set(FBX_LIBRARIES ${FBX_LIBRARIES} Wininet.lib)
|
|
||||||
elseif (APPLE)
|
|
||||||
set(FBX_LIBRARIES ${FBX_LIBRARIES} ${CARBON} ${SYSTEM_CONFIGURATION})
|
|
||||||
endif()
|
|
||||||
endif()
|
|
|
@ -1,19 +1,10 @@
|
||||||
set(TARGET_NAME baking)
|
set(TARGET_NAME baking)
|
||||||
setup_hifi_library(Concurrent)
|
setup_hifi_library(Concurrent)
|
||||||
|
|
||||||
find_package(FBX)
|
link_hifi_libraries(shared model networking ktx image fbx)
|
||||||
if (FBX_FOUND)
|
|
||||||
if (CMAKE_THREAD_LIBS_INIT)
|
|
||||||
target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES} "${CMAKE_THREAD_LIBS_INIT}")
|
|
||||||
else ()
|
|
||||||
target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES})
|
|
||||||
endif ()
|
|
||||||
target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${FBX_INCLUDE_DIR})
|
|
||||||
|
|
||||||
if (UNIX)
|
|
||||||
target_link_libraries(${TARGET_NAME} ${CMAKE_DL_LIBS})
|
|
||||||
endif (UNIX)
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
link_hifi_libraries(shared model networking ktx image)
|
|
||||||
include_hifi_library_headers(gpu)
|
include_hifi_library_headers(gpu)
|
||||||
|
|
||||||
|
add_dependency_external_projects(draco)
|
||||||
|
find_package(Draco REQUIRED)
|
||||||
|
target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${DRACO_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(${TARGET_NAME} ${DRACO_LIBRARY} ${DRACO_ENCODER_LIBRARY})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// FBXBaker.cpp
|
// FBXBaker.cpp
|
||||||
// tools/oven/src
|
// tools/baking/src
|
||||||
//
|
//
|
||||||
// Created by Stephen Birarda on 3/30/17.
|
// Created by Stephen Birarda on 3/30/17.
|
||||||
// Copyright 2017 High Fidelity, Inc.
|
// Copyright 2017 High Fidelity, Inc.
|
||||||
|
@ -11,8 +11,6 @@
|
||||||
|
|
||||||
#include <cmath> // need this include so we don't get an error looking for std::isnan
|
#include <cmath> // need this include so we don't get an error looking for std::isnan
|
||||||
|
|
||||||
#include <fbxsdk.h>
|
|
||||||
|
|
||||||
#include <QtConcurrent>
|
#include <QtConcurrent>
|
||||||
#include <QtCore/QCoreApplication>
|
#include <QtCore/QCoreApplication>
|
||||||
#include <QtCore/QDir>
|
#include <QtCore/QDir>
|
||||||
|
@ -27,13 +25,26 @@
|
||||||
|
|
||||||
#include <PathUtils.h>
|
#include <PathUtils.h>
|
||||||
|
|
||||||
|
#include <FBXReader.h>
|
||||||
|
#include <FBXWriter.h>
|
||||||
|
|
||||||
#include "ModelBakingLoggingCategory.h"
|
#include "ModelBakingLoggingCategory.h"
|
||||||
#include "TextureBaker.h"
|
#include "TextureBaker.h"
|
||||||
|
|
||||||
#include "FBXBaker.h"
|
#include "FBXBaker.h"
|
||||||
|
|
||||||
std::once_flag onceFlag;
|
#ifdef _WIN32
|
||||||
FBXSDKManagerUniquePointer FBXBaker::_sdkManager { nullptr };
|
#pragma warning( push )
|
||||||
|
#pragma warning( disable : 4267 )
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <draco/mesh/triangle_soup_mesh_builder.h>
|
||||||
|
#include <draco/compression/encode.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#pragma warning( pop )
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
|
FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
|
||||||
const QString& bakedOutputDir, const QString& originalOutputDir) :
|
const QString& bakedOutputDir, const QString& originalOutputDir) :
|
||||||
|
@ -42,12 +53,7 @@ FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGet
|
||||||
_originalOutputDir(originalOutputDir),
|
_originalOutputDir(originalOutputDir),
|
||||||
_textureThreadGetter(textureThreadGetter)
|
_textureThreadGetter(textureThreadGetter)
|
||||||
{
|
{
|
||||||
std::call_once(onceFlag, [](){
|
|
||||||
// create the static FBX SDK manager
|
|
||||||
_sdkManager = FBXSDKManagerUniquePointer(FbxManager::Create(), [](FbxManager* manager){
|
|
||||||
manager->Destroy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FBXBaker::bake() {
|
void FBXBaker::bake() {
|
||||||
|
@ -85,7 +91,8 @@ void FBXBaker::bakeSourceCopy() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// enumerate the textures found in the scene and start a bake for them
|
// enumerate the models and textures found in the scene and start a bake for them
|
||||||
|
rewriteAndBakeSceneModels();
|
||||||
rewriteAndBakeSceneTextures();
|
rewriteAndBakeSceneTextures();
|
||||||
|
|
||||||
if (hasErrors()) {
|
if (hasErrors()) {
|
||||||
|
@ -205,29 +212,20 @@ void FBXBaker::handleFBXNetworkReply() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void FBXBaker::importScene() {
|
void FBXBaker::importScene() {
|
||||||
// create an FBX SDK importer
|
|
||||||
FbxImporter* importer = FbxImporter::Create(_sdkManager.get(), "");
|
|
||||||
|
|
||||||
qDebug() << "file path: " << _originalFBXFilePath.toLocal8Bit().data() << QDir(_originalFBXFilePath).exists();
|
qDebug() << "file path: " << _originalFBXFilePath.toLocal8Bit().data() << QDir(_originalFBXFilePath).exists();
|
||||||
// import the copy of the original FBX file
|
|
||||||
bool importStatus = importer->Initialize(_originalFBXFilePath.toLocal8Bit().data());
|
|
||||||
|
|
||||||
if (!importStatus) {
|
QFile fbxFile(_originalFBXFilePath);
|
||||||
// failed to initialize importer, print an error and return
|
if (!fbxFile.open(QIODevice::ReadOnly)) {
|
||||||
handleError("Failed to import " + _fbxURL.toString() + " - " + importer->GetStatus().GetErrorString());
|
handleError("Error opening " + _originalFBXFilePath + " for reading");
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
qCDebug(model_baking) << "Imported" << _fbxURL << "to FbxScene";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup a new scene to hold the imported file
|
FBXReader reader;
|
||||||
_scene = FbxScene::Create(_sdkManager.get(), "bakeScene");
|
|
||||||
|
|
||||||
// import the file to the created scene
|
qCDebug(model_baking) << "Parsing" << _fbxURL;
|
||||||
importer->Import(_scene);
|
_rootNode = reader._rootNode = reader.parseFBX(&fbxFile);
|
||||||
|
_geometry = reader.extractFBXGeometry({}, _fbxURL.toString());
|
||||||
// destroy the importer that is no longer needed
|
_textureContent = reader._textureContent;
|
||||||
importer->Destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) {
|
QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) {
|
||||||
|
@ -264,7 +262,7 @@ QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) {
|
||||||
return bakedTextureFileName;
|
return bakedTextureFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* fileTexture) {
|
QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName) {
|
||||||
QUrl urlToTexture;
|
QUrl urlToTexture;
|
||||||
|
|
||||||
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
|
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
|
||||||
|
@ -274,7 +272,6 @@ QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* f
|
||||||
// external texture that we'll need to download or find
|
// external texture that we'll need to download or find
|
||||||
|
|
||||||
// first check if it the RelativePath to the texture in the FBX was relative
|
// first check if it the RelativePath to the texture in the FBX was relative
|
||||||
QString relativeFileName = fileTexture->GetRelativeFileName();
|
|
||||||
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
|
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
|
||||||
|
|
||||||
// this is a relative file path which will require different handling
|
// this is a relative file path which will require different handling
|
||||||
|
@ -293,84 +290,237 @@ QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* f
|
||||||
return urlToTexture;
|
return urlToTexture;
|
||||||
}
|
}
|
||||||
|
|
||||||
image::TextureUsage::Type textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMaterial* material) {
|
void FBXBaker::rewriteAndBakeSceneModels() {
|
||||||
using namespace image::TextureUsage;
|
unsigned int meshIndex = 0;
|
||||||
|
for (FBXNode& rootChild : _rootNode.children) {
|
||||||
// this is a property we know has a texture, we need to match it to a High Fidelity known texture type
|
if (rootChild.name == "Objects") {
|
||||||
// since that information is passed to the baking process
|
for (FBXNode& objectChild : rootChild.children) {
|
||||||
|
if (objectChild.name == "Geometry") {
|
||||||
|
|
||||||
// grab the hierarchical name for this property and lowercase it for case-insensitive compare
|
// TODO Pull this out of _geometry instead so we don't have to reprocess it
|
||||||
auto propertyName = QString(property.GetHierarchicalName()).toLower();
|
auto extractedMesh = FBXReader::extractMesh(objectChild, meshIndex);
|
||||||
|
auto& mesh = extractedMesh.mesh;
|
||||||
|
|
||||||
// figure out the type of the property based on what known value string it matches
|
Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size());
|
||||||
if ((propertyName.contains("diffuse") && !propertyName.contains("tex_global_diffuse"))
|
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
|
||||||
|| propertyName.contains("tex_color_map")) {
|
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
|
||||||
return ALBEDO_TEXTURE;
|
|
||||||
} else if (propertyName.contains("transparentcolor") || propertyName.contains("transparencyfactor")) {
|
|
||||||
return ALBEDO_TEXTURE;
|
|
||||||
} else if (propertyName.contains("bump")) {
|
|
||||||
return BUMP_TEXTURE;
|
|
||||||
} else if (propertyName.contains("normal")) {
|
|
||||||
return NORMAL_TEXTURE;
|
|
||||||
} else if ((propertyName.contains("specular") && !propertyName.contains("tex_global_specular"))
|
|
||||||
|| propertyName.contains("reflection")) {
|
|
||||||
return SPECULAR_TEXTURE;
|
|
||||||
} else if (propertyName.contains("tex_metallic_map")) {
|
|
||||||
return METALLIC_TEXTURE;
|
|
||||||
} else if (propertyName.contains("shininess")) {
|
|
||||||
return GLOSS_TEXTURE;
|
|
||||||
} else if (propertyName.contains("tex_roughness_map")) {
|
|
||||||
return ROUGHNESS_TEXTURE;
|
|
||||||
} else if (propertyName.contains("emissive")) {
|
|
||||||
return EMISSIVE_TEXTURE;
|
|
||||||
} else if (propertyName.contains("ambientcolor")) {
|
|
||||||
return LIGHTMAP_TEXTURE;
|
|
||||||
} else if (propertyName.contains("ambientfactor")) {
|
|
||||||
// we need to check what the ambient factor is, since that tells Interface to process this texture
|
|
||||||
// either as an occlusion texture or a light map
|
|
||||||
auto lambertMaterial = FbxCast<FbxSurfaceLambert>(material);
|
|
||||||
|
|
||||||
if (lambertMaterial->AmbientFactor == 0) {
|
int64_t numTriangles { 0 };
|
||||||
return LIGHTMAP_TEXTURE;
|
for (auto& part : mesh.parts) {
|
||||||
} else if (lambertMaterial->AmbientFactor > 0) {
|
if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) {
|
||||||
return OCCLUSION_TEXTURE;
|
handleWarning("Found a mesh part with invalid index data, skipping");
|
||||||
} else {
|
continue;
|
||||||
return UNUSED_TEXTURE;
|
}
|
||||||
|
numTriangles += part.quadTrianglesIndices.size() / 3;
|
||||||
|
numTriangles += part.triangleIndices.size() / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numTriangles == 0) {
|
||||||
|
handleWarning("Skipping compression of mesh because no triangles were found");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
draco::TriangleSoupMeshBuilder meshBuilder;
|
||||||
|
|
||||||
|
meshBuilder.Start(numTriangles);
|
||||||
|
|
||||||
|
bool hasNormals { mesh.normals.size() > 0 };
|
||||||
|
bool hasColors { mesh.colors.size() > 0 };
|
||||||
|
bool hasTexCoords { mesh.texCoords.size() > 0 };
|
||||||
|
bool hasTexCoords1 { mesh.texCoords1.size() > 0 };
|
||||||
|
bool hasPerFaceMaterials { mesh.parts.size() > 1 };
|
||||||
|
|
||||||
|
int normalsAttributeID { -1 };
|
||||||
|
int colorsAttributeID { -1 };
|
||||||
|
int texCoordsAttributeID { -1 };
|
||||||
|
int texCoords1AttributeID { -1 };
|
||||||
|
int faceMaterialAttributeID { -1 };
|
||||||
|
|
||||||
|
const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION,
|
||||||
|
3, draco::DT_FLOAT32);
|
||||||
|
|
||||||
|
if (hasNormals) {
|
||||||
|
normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL,
|
||||||
|
3, draco::DT_FLOAT32);
|
||||||
|
}
|
||||||
|
if (hasColors) {
|
||||||
|
colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR,
|
||||||
|
3, draco::DT_FLOAT32);
|
||||||
|
}
|
||||||
|
if (hasTexCoords) {
|
||||||
|
texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD,
|
||||||
|
2, draco::DT_FLOAT32);
|
||||||
|
}
|
||||||
|
if (hasTexCoords1) {
|
||||||
|
texCoords1AttributeID = meshBuilder.AddAttribute(
|
||||||
|
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1,
|
||||||
|
2, draco::DT_FLOAT32);
|
||||||
|
}
|
||||||
|
if (hasPerFaceMaterials) {
|
||||||
|
faceMaterialAttributeID = meshBuilder.AddAttribute(
|
||||||
|
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID,
|
||||||
|
1, draco::DT_UINT16);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
auto partIndex = 0;
|
||||||
|
draco::FaceIndex face;
|
||||||
|
for (auto& part : mesh.parts) {
|
||||||
|
const auto& matTex = extractedMesh.partMaterialTextures[partIndex];
|
||||||
|
|
||||||
|
auto addFace = [&](QVector<int>& indices, int index, draco::FaceIndex face) {
|
||||||
|
auto idx0 = indices[index];
|
||||||
|
auto idx1 = indices[index + 1];
|
||||||
|
auto idx2 = indices[index + 2];
|
||||||
|
|
||||||
|
if (hasPerFaceMaterials) {
|
||||||
|
uint16_t materialID = matTex.first;
|
||||||
|
meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID);
|
||||||
|
}
|
||||||
|
|
||||||
|
meshBuilder.SetAttributeValuesForFace(positionAttributeID, face,
|
||||||
|
&mesh.vertices[idx0], &mesh.vertices[idx1],
|
||||||
|
&mesh.vertices[idx2]);
|
||||||
|
|
||||||
|
if (hasNormals) {
|
||||||
|
meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face,
|
||||||
|
&mesh.normals[idx0], &mesh.normals[idx1],
|
||||||
|
&mesh.normals[idx2]);
|
||||||
|
}
|
||||||
|
if (hasColors) {
|
||||||
|
meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face,
|
||||||
|
&mesh.colors[idx0], &mesh.colors[idx1],
|
||||||
|
&mesh.colors[idx2]);
|
||||||
|
}
|
||||||
|
if (hasTexCoords) {
|
||||||
|
meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face,
|
||||||
|
&mesh.texCoords[idx0], &mesh.texCoords[idx1],
|
||||||
|
&mesh.texCoords[idx2]);
|
||||||
|
}
|
||||||
|
if (hasTexCoords1) {
|
||||||
|
meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face,
|
||||||
|
&mesh.texCoords1[idx0], &mesh.texCoords1[idx1],
|
||||||
|
&mesh.texCoords1[idx2]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) {
|
||||||
|
addFace(part.quadTrianglesIndices, i, face++);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) {
|
||||||
|
addFace(part.triangleIndices, i, face++);
|
||||||
|
}
|
||||||
|
|
||||||
|
partIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto dracoMesh = meshBuilder.Finalize();
|
||||||
|
|
||||||
|
if (!dracoMesh) {
|
||||||
|
handleWarning("Failed to finalize the baking of a draco Geometry node");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to modify unique attribute IDs for custom attributes
|
||||||
|
// so the attributes are easily retrievable on the other side
|
||||||
|
if (hasPerFaceMaterials) {
|
||||||
|
dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTexCoords1) {
|
||||||
|
dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1);
|
||||||
|
}
|
||||||
|
|
||||||
|
draco::Encoder encoder;
|
||||||
|
|
||||||
|
encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14);
|
||||||
|
encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12);
|
||||||
|
encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10);
|
||||||
|
encoder.SetSpeedOptions(0, 5);
|
||||||
|
|
||||||
|
draco::EncoderBuffer buffer;
|
||||||
|
encoder.EncodeMeshToBuffer(*dracoMesh, &buffer);
|
||||||
|
|
||||||
|
FBXNode dracoMeshNode;
|
||||||
|
dracoMeshNode.name = "DracoMesh";
|
||||||
|
auto value = QVariant::fromValue(QByteArray(buffer.data(), (int) buffer.size()));
|
||||||
|
dracoMeshNode.properties.append(value);
|
||||||
|
|
||||||
|
objectChild.children.push_back(dracoMeshNode);
|
||||||
|
|
||||||
|
static const std::vector<QString> nodeNamesToDelete {
|
||||||
|
// Node data that is packed into the draco mesh
|
||||||
|
"Vertices",
|
||||||
|
"PolygonVertexIndex",
|
||||||
|
"LayerElementNormal",
|
||||||
|
"LayerElementColor",
|
||||||
|
"LayerElementUV",
|
||||||
|
"LayerElementMaterial",
|
||||||
|
"LayerElementTexture",
|
||||||
|
|
||||||
|
// Node data that we don't support
|
||||||
|
"Edges",
|
||||||
|
"LayerElementTangent",
|
||||||
|
"LayerElementBinormal",
|
||||||
|
"LayerElementSmoothing"
|
||||||
|
};
|
||||||
|
auto& children = objectChild.children;
|
||||||
|
auto it = children.begin();
|
||||||
|
while (it != children.end()) {
|
||||||
|
auto begin = nodeNamesToDelete.begin();
|
||||||
|
auto end = nodeNamesToDelete.end();
|
||||||
|
if (find(begin, end, it->name) != end) {
|
||||||
|
it = children.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (propertyName.contains("tex_ao_map")) {
|
|
||||||
return OCCLUSION_TEXTURE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return UNUSED_TEXTURE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FBXBaker::rewriteAndBakeSceneTextures() {
|
void FBXBaker::rewriteAndBakeSceneTextures() {
|
||||||
|
using namespace image::TextureUsage;
|
||||||
|
QHash<QString, image::TextureUsage::Type> textureTypes;
|
||||||
|
|
||||||
// enumerate the surface materials to find the textures used in the scene
|
// enumerate the materials in the extracted geometry so we can determine the texture type for each texture ID
|
||||||
int numMaterials = _scene->GetMaterialCount();
|
for (const auto& material : _geometry->materials) {
|
||||||
for (int i = 0; i < numMaterials; i++) {
|
if (material.normalTexture.isBumpmap) {
|
||||||
FbxSurfaceMaterial* material = _scene->GetMaterial(i);
|
textureTypes[material.normalTexture.id] = BUMP_TEXTURE;
|
||||||
|
} else {
|
||||||
|
textureTypes[material.normalTexture.id] = NORMAL_TEXTURE;
|
||||||
|
}
|
||||||
|
|
||||||
if (material) {
|
textureTypes[material.albedoTexture.id] = ALBEDO_TEXTURE;
|
||||||
// enumerate the properties of this material to see what texture channels it might have
|
textureTypes[material.glossTexture.id] = GLOSS_TEXTURE;
|
||||||
FbxProperty property = material->GetFirstProperty();
|
textureTypes[material.roughnessTexture.id] = ROUGHNESS_TEXTURE;
|
||||||
|
textureTypes[material.specularTexture.id] = SPECULAR_TEXTURE;
|
||||||
|
textureTypes[material.metallicTexture.id] = METALLIC_TEXTURE;
|
||||||
|
textureTypes[material.emissiveTexture.id] = EMISSIVE_TEXTURE;
|
||||||
|
textureTypes[material.occlusionTexture.id] = OCCLUSION_TEXTURE;
|
||||||
|
textureTypes[material.lightmapTexture.id] = LIGHTMAP_TEXTURE;
|
||||||
|
}
|
||||||
|
|
||||||
while (property.IsValid()) {
|
// enumerate the children of the root node
|
||||||
// first check if this property has connected textures, if not we don't need to bother with it here
|
for (FBXNode& rootChild : _rootNode.children) {
|
||||||
if (property.GetSrcObjectCount<FbxTexture>() > 0) {
|
|
||||||
|
|
||||||
// figure out the type of texture from the material property
|
if (rootChild.name == "Objects") {
|
||||||
auto textureType = textureTypeForMaterialProperty(property, material);
|
|
||||||
|
|
||||||
if (textureType != image::TextureUsage::UNUSED_TEXTURE) {
|
// enumerate the objects
|
||||||
int numTextures = property.GetSrcObjectCount<FbxFileTexture>();
|
auto object = rootChild.children.begin();
|
||||||
|
while (object != rootChild.children.end()) {
|
||||||
|
if (object->name == "Texture") {
|
||||||
|
|
||||||
for (int j = 0; j < numTextures; j++) {
|
// enumerate the texture children
|
||||||
FbxFileTexture* fileTexture = property.GetSrcObject<FbxFileTexture>(j);
|
for (FBXNode& textureChild : object->children) {
|
||||||
|
|
||||||
|
if (textureChild.name == "RelativeFilename") {
|
||||||
|
|
||||||
// use QFileInfo to easily split up the existing texture filename into its components
|
// use QFileInfo to easily split up the existing texture filename into its components
|
||||||
QString fbxTextureFileName { fileTexture->GetFileName() };
|
QString fbxTextureFileName { textureChild.properties.at(0).toByteArray() };
|
||||||
QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") };
|
QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") };
|
||||||
|
|
||||||
// make sure this texture points to something and isn't one we've already re-mapped
|
// make sure this texture points to something and isn't one we've already re-mapped
|
||||||
|
@ -393,38 +543,50 @@ void FBXBaker::rewriteAndBakeSceneTextures() {
|
||||||
};
|
};
|
||||||
_outputFiles.push_back(bakedTextureFilePath);
|
_outputFiles.push_back(bakedTextureFilePath);
|
||||||
|
|
||||||
qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName()
|
qCDebug(model_baking).noquote() << "Re-mapping" << fbxTextureFileName
|
||||||
<< "to" << bakedTextureFilePath;
|
<< "to" << bakedTextureFileName;
|
||||||
|
|
||||||
// figure out the URL to this texture, embedded or external
|
// figure out the URL to this texture, embedded or external
|
||||||
auto urlToTexture = getTextureURL(textureFileInfo, fileTexture);
|
auto urlToTexture = getTextureURL(textureFileInfo, fbxTextureFileName);
|
||||||
|
|
||||||
// write the new filename into the FBX scene
|
// write the new filename into the FBX scene
|
||||||
fileTexture->SetFileName(bakedTextureFilePath.toUtf8().data());
|
textureChild.properties[0] = bakedTextureFileName.toLocal8Bit();
|
||||||
|
|
||||||
// write the relative filename to be the baked texture file name since it will
|
|
||||||
// be right beside the FBX
|
|
||||||
fileTexture->SetRelativeFileName(bakedTextureFileName.toLocal8Bit().constData());
|
|
||||||
|
|
||||||
if (!_bakingTextures.contains(urlToTexture)) {
|
if (!_bakingTextures.contains(urlToTexture)) {
|
||||||
|
|
||||||
|
// grab the ID for this texture so we can figure out the
|
||||||
|
// texture type from the loaded materials
|
||||||
|
QString textureID { object->properties[0].toByteArray() };
|
||||||
|
auto textureType = textureTypes[textureID];
|
||||||
|
|
||||||
|
// check if this was an embedded texture we have already have in-memory content for
|
||||||
|
auto textureContent = _textureContent.value(fbxTextureFileName.toLocal8Bit());
|
||||||
|
|
||||||
// bake this texture asynchronously
|
// bake this texture asynchronously
|
||||||
bakeTexture(urlToTexture, textureType, _bakedOutputDir);
|
bakeTexture(urlToTexture, textureType, _bakedOutputDir, textureContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
property = material->GetNextProperty(property);
|
++object;
|
||||||
|
|
||||||
|
} else if (object->name == "Video") {
|
||||||
|
// this is an embedded texture, we need to remove it from the FBX
|
||||||
|
object = rootChild.children.erase(object);
|
||||||
|
} else {
|
||||||
|
++object;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir) {
|
void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType,
|
||||||
|
const QDir& outputDir, const QByteArray& textureContent) {
|
||||||
// start a bake for this texture and add it to our list to keep track of
|
// start a bake for this texture and add it to our list to keep track of
|
||||||
QSharedPointer<TextureBaker> bakingTexture {
|
QSharedPointer<TextureBaker> bakingTexture {
|
||||||
new TextureBaker(textureURL, textureType, outputDir),
|
new TextureBaker(textureURL, textureType, outputDir, textureContent),
|
||||||
&TextureBaker::deleteLater
|
&TextureBaker::deleteLater
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -474,7 +636,7 @@ void FBXBaker::handleBakedTexture() {
|
||||||
|
|
||||||
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
|
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
|
||||||
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
|
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
|
||||||
<< "for" << _fbxURL;
|
<< "for" << _fbxURL;
|
||||||
} else {
|
} else {
|
||||||
handleError("Could not save original external texture " + originalTextureFile.fileName()
|
handleError("Could not save original external texture " + originalTextureFile.fileName()
|
||||||
+ " for " + _fbxURL.toString());
|
+ " for " + _fbxURL.toString());
|
||||||
|
@ -491,13 +653,13 @@ void FBXBaker::handleBakedTexture() {
|
||||||
} else {
|
} else {
|
||||||
// there was an error baking this texture - add it to our list of errors
|
// there was an error baking this texture - add it to our list of errors
|
||||||
_errorList.append(bakedTexture->getErrors());
|
_errorList.append(bakedTexture->getErrors());
|
||||||
|
|
||||||
// we don't emit finished yet so that the other textures can finish baking first
|
// we don't emit finished yet so that the other textures can finish baking first
|
||||||
_pendingErrorEmission = true;
|
_pendingErrorEmission = true;
|
||||||
|
|
||||||
// now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list
|
// now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list
|
||||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
_bakingTextures.remove(bakedTexture->getTextureURL());
|
||||||
|
|
||||||
checkIfTexturesFinished();
|
checkIfTexturesFinished();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -512,29 +674,25 @@ void FBXBaker::handleBakedTexture() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void FBXBaker::exportScene() {
|
void FBXBaker::exportScene() {
|
||||||
// setup the exporter
|
|
||||||
FbxExporter* exporter = FbxExporter::Create(_sdkManager.get(), "");
|
|
||||||
|
|
||||||
// save the relative path to this FBX inside our passed output folder
|
// save the relative path to this FBX inside our passed output folder
|
||||||
|
|
||||||
auto fileName = _fbxURL.fileName();
|
auto fileName = _fbxURL.fileName();
|
||||||
auto baseName = fileName.left(fileName.lastIndexOf('.'));
|
auto baseName = fileName.left(fileName.lastIndexOf('.'));
|
||||||
auto bakedFilename = baseName + BAKED_FBX_EXTENSION;
|
auto bakedFilename = baseName + BAKED_FBX_EXTENSION;
|
||||||
|
|
||||||
_bakedFBXFilePath = _bakedOutputDir + "/" + bakedFilename;
|
_bakedFBXFilePath = _bakedOutputDir + "/" + bakedFilename;
|
||||||
|
|
||||||
bool exportStatus = exporter->Initialize(_bakedFBXFilePath.toLocal8Bit().data());
|
auto fbxData = FBXWriter::encodeFBX(_rootNode);
|
||||||
|
|
||||||
if (!exportStatus) {
|
QFile bakedFile(_bakedFBXFilePath);
|
||||||
// failed to initialize exporter, print an error and return
|
|
||||||
handleError("Failed to export FBX file at " + _fbxURL.toString() + " to " + _bakedFBXFilePath
|
if (!bakedFile.open(QIODevice::WriteOnly)) {
|
||||||
+ "- error: " + exporter->GetStatus().GetErrorString());
|
handleError("Error opening " + _bakedFBXFilePath + " for writing");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_outputFiles.push_back(_bakedFBXFilePath);
|
bakedFile.write(fbxData);
|
||||||
|
|
||||||
// export the scene
|
_outputFiles.push_back(_bakedFBXFilePath);
|
||||||
exporter->Export(_scene);
|
|
||||||
|
|
||||||
qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath;
|
qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// FBXBaker.h
|
// FBXBaker.h
|
||||||
// tools/oven/src
|
// tools/baking/src
|
||||||
//
|
//
|
||||||
// Created by Stephen Birarda on 3/30/17.
|
// Created by Stephen Birarda on 3/30/17.
|
||||||
// Copyright 2017 High Fidelity, Inc.
|
// Copyright 2017 High Fidelity, Inc.
|
||||||
|
@ -24,15 +24,9 @@
|
||||||
|
|
||||||
#include <gpu/Texture.h>
|
#include <gpu/Texture.h>
|
||||||
|
|
||||||
namespace fbxsdk {
|
#include <FBX.h>
|
||||||
class FbxManager;
|
|
||||||
class FbxProperty;
|
|
||||||
class FbxScene;
|
|
||||||
class FbxFileTexture;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const QString BAKED_FBX_EXTENSION = ".baked.fbx";
|
static const QString BAKED_FBX_EXTENSION = ".baked.fbx";
|
||||||
using FBXSDKManagerUniquePointer = std::unique_ptr<fbxsdk::FbxManager, std::function<void (fbxsdk::FbxManager *)>>;
|
|
||||||
|
|
||||||
using TextureBakerThreadGetter = std::function<QThread*()>;
|
using TextureBakerThreadGetter = std::function<QThread*()>;
|
||||||
|
|
||||||
|
@ -64,6 +58,7 @@ private:
|
||||||
void loadSourceFBX();
|
void loadSourceFBX();
|
||||||
|
|
||||||
void importScene();
|
void importScene();
|
||||||
|
void rewriteAndBakeSceneModels();
|
||||||
void rewriteAndBakeSceneTextures();
|
void rewriteAndBakeSceneTextures();
|
||||||
void exportScene();
|
void exportScene();
|
||||||
void removeEmbeddedMediaFolder();
|
void removeEmbeddedMediaFolder();
|
||||||
|
@ -71,11 +66,16 @@ private:
|
||||||
void checkIfTexturesFinished();
|
void checkIfTexturesFinished();
|
||||||
|
|
||||||
QString createBakedTextureFileName(const QFileInfo& textureFileInfo);
|
QString createBakedTextureFileName(const QFileInfo& textureFileInfo);
|
||||||
QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture);
|
QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName);
|
||||||
|
|
||||||
void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir);
|
void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir,
|
||||||
|
const QByteArray& textureContent = QByteArray());
|
||||||
|
|
||||||
QUrl _fbxURL;
|
QUrl _fbxURL;
|
||||||
|
|
||||||
|
FBXNode _rootNode;
|
||||||
|
FBXGeometry* _geometry;
|
||||||
|
QHash<QByteArray, QByteArray> _textureContent;
|
||||||
|
|
||||||
QString _bakedFBXFilePath;
|
QString _bakedFBXFilePath;
|
||||||
|
|
||||||
|
@ -87,9 +87,6 @@ private:
|
||||||
QDir _tempDir;
|
QDir _tempDir;
|
||||||
QString _originalFBXFilePath;
|
QString _originalFBXFilePath;
|
||||||
|
|
||||||
static FBXSDKManagerUniquePointer _sdkManager;
|
|
||||||
fbxsdk::FbxScene* _scene { nullptr };
|
|
||||||
|
|
||||||
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
|
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
|
||||||
QHash<QString, int> _textureNameMatchCount;
|
QHash<QString, int> _textureNameMatchCount;
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,10 @@
|
||||||
|
|
||||||
const QString BAKED_TEXTURE_EXT = ".ktx";
|
const QString BAKED_TEXTURE_EXT = ".ktx";
|
||||||
|
|
||||||
TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory) :
|
TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType,
|
||||||
|
const QDir& outputDirectory, const QByteArray& textureContent) :
|
||||||
_textureURL(textureURL),
|
_textureURL(textureURL),
|
||||||
|
_originalTexture(textureContent),
|
||||||
_textureType(textureType),
|
_textureType(textureType),
|
||||||
_outputDirectory(outputDirectory)
|
_outputDirectory(outputDirectory)
|
||||||
{
|
{
|
||||||
|
@ -39,8 +41,13 @@ void TextureBaker::bake() {
|
||||||
// once our texture is loaded, kick off a the processing
|
// once our texture is loaded, kick off a the processing
|
||||||
connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture);
|
connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture);
|
||||||
|
|
||||||
// first load the texture (either locally or remotely)
|
if (_originalTexture.isEmpty()) {
|
||||||
loadTexture();
|
// first load the texture (either locally or remotely)
|
||||||
|
loadTexture();
|
||||||
|
} else {
|
||||||
|
// we already have a texture passed to us, use that
|
||||||
|
emit originalTextureLoaded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const QStringList TextureBaker::getSupportedFormats() {
|
const QStringList TextureBaker::getSupportedFormats() {
|
||||||
|
|
|
@ -27,7 +27,8 @@ class TextureBaker : public Baker {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory);
|
TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType,
|
||||||
|
const QDir& outputDirectory, const QByteArray& textureContent = QByteArray());
|
||||||
|
|
||||||
static const QStringList getSupportedFormats();
|
static const QStringList getSupportedFormats();
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
set(TARGET_NAME fbx)
|
set(TARGET_NAME fbx)
|
||||||
setup_hifi_library()
|
setup_hifi_library()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
link_hifi_libraries(shared model networking image)
|
link_hifi_libraries(shared model networking image)
|
||||||
include_hifi_library_headers(gpu image)
|
include_hifi_library_headers(gpu image)
|
||||||
|
|
||||||
|
add_dependency_external_projects(draco)
|
||||||
|
find_package(Draco REQUIRED)
|
||||||
|
target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${DRACO_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(${TARGET_NAME} ${DRACO_LIBRARY} ${DRACO_ENCODER_LIBRARY})
|
||||||
|
|
10
libraries/fbx/src/FBX.cpp
Normal file
10
libraries/fbx/src/FBX.cpp
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
//
|
||||||
|
// FBX.cpp
|
||||||
|
// libraries/fbx/src
|
||||||
|
//
|
||||||
|
// Created by Ryan Huffman on 9/5/17.
|
||||||
|
// Copyright 2017 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
341
libraries/fbx/src/FBX.h
Normal file
341
libraries/fbx/src/FBX.h
Normal file
|
@ -0,0 +1,341 @@
|
||||||
|
//
|
||||||
|
// FBX.h
|
||||||
|
// libraries/fbx/src
|
||||||
|
//
|
||||||
|
// Created by Ryan Huffman on 9/5/17.
|
||||||
|
// Copyright 2017 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef hifi_FBX_h_
|
||||||
|
#define hifi_FBX_h_
|
||||||
|
|
||||||
|
#include <QMetaType>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QVarLengthArray>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
|
#include <Extents.h>
|
||||||
|
#include <Transform.h>
|
||||||
|
|
||||||
|
#include <model/Geometry.h>
|
||||||
|
#include <model/Material.h>
|
||||||
|
|
||||||
|
static const QByteArray FBX_BINARY_PROLOG = "Kaydara FBX Binary ";
|
||||||
|
static const int FBX_HEADER_BYTES_BEFORE_VERSION = 23;
|
||||||
|
static const quint32 FBX_VERSION_2016 = 7500;
|
||||||
|
|
||||||
|
|
||||||
|
// TODO Convert to GeometryAttribute type
|
||||||
|
static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000;
|
||||||
|
static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES;
|
||||||
|
static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1;
|
||||||
|
|
||||||
|
|
||||||
|
class FBXNode;
|
||||||
|
using FBXNodeList = QList<FBXNode>;
|
||||||
|
|
||||||
|
|
||||||
|
/// A node within an FBX document.
|
||||||
|
class FBXNode {
|
||||||
|
public:
|
||||||
|
QByteArray name;
|
||||||
|
QVariantList properties;
|
||||||
|
FBXNodeList children;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/// A single blendshape extracted from an FBX document.
|
||||||
|
class FBXBlendshape {
|
||||||
|
public:
|
||||||
|
QVector<int> indices;
|
||||||
|
QVector<glm::vec3> vertices;
|
||||||
|
QVector<glm::vec3> normals;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FBXJointShapeInfo {
|
||||||
|
// same units and frame as FBXJoint.translation
|
||||||
|
glm::vec3 avgPoint;
|
||||||
|
std::vector<float> dots;
|
||||||
|
std::vector<glm::vec3> points;
|
||||||
|
std::vector<glm::vec3> debugLines;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A single joint (transformation node) extracted from an FBX document.
|
||||||
|
class FBXJoint {
|
||||||
|
public:
|
||||||
|
|
||||||
|
FBXJointShapeInfo shapeInfo;
|
||||||
|
QVector<int> freeLineage;
|
||||||
|
bool isFree;
|
||||||
|
int parentIndex;
|
||||||
|
float distanceToParent;
|
||||||
|
|
||||||
|
// http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/SDKRef/a00209.html
|
||||||
|
|
||||||
|
glm::vec3 translation; // T
|
||||||
|
glm::mat4 preTransform; // Roff * Rp
|
||||||
|
glm::quat preRotation; // Rpre
|
||||||
|
glm::quat rotation; // R
|
||||||
|
glm::quat postRotation; // Rpost
|
||||||
|
glm::mat4 postTransform; // Rp-1 * Soff * Sp * S * Sp-1
|
||||||
|
|
||||||
|
// World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1)
|
||||||
|
|
||||||
|
glm::mat4 transform;
|
||||||
|
glm::vec3 rotationMin; // radians
|
||||||
|
glm::vec3 rotationMax; // radians
|
||||||
|
glm::quat inverseDefaultRotation;
|
||||||
|
glm::quat inverseBindRotation;
|
||||||
|
glm::mat4 bindTransform;
|
||||||
|
QString name;
|
||||||
|
bool isSkeletonJoint;
|
||||||
|
bool bindTransformFoundInCluster;
|
||||||
|
|
||||||
|
// geometric offset is applied in local space but does NOT affect children.
|
||||||
|
bool hasGeometricOffset;
|
||||||
|
glm::vec3 geometricTranslation;
|
||||||
|
glm::quat geometricRotation;
|
||||||
|
glm::vec3 geometricScaling;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/// A single binding to a joint in an FBX document.
|
||||||
|
class FBXCluster {
|
||||||
|
public:
|
||||||
|
|
||||||
|
int jointIndex;
|
||||||
|
glm::mat4 inverseBindMatrix;
|
||||||
|
};
|
||||||
|
|
||||||
|
const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048;
|
||||||
|
|
||||||
|
/// A texture map in an FBX document.
|
||||||
|
class FBXTexture {
|
||||||
|
public:
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QByteArray filename;
|
||||||
|
QByteArray content;
|
||||||
|
|
||||||
|
Transform transform;
|
||||||
|
int maxNumPixels { MAX_NUM_PIXELS_FOR_FBX_TEXTURE };
|
||||||
|
int texcoordSet;
|
||||||
|
QString texcoordSetName;
|
||||||
|
|
||||||
|
bool isBumpmap{ false };
|
||||||
|
|
||||||
|
bool isNull() const { return name.isEmpty() && filename.isEmpty() && content.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A single part of a mesh (with the same material).
|
||||||
|
class FBXMeshPart {
|
||||||
|
public:
|
||||||
|
|
||||||
|
QVector<int> quadIndices; // original indices from the FBX mesh
|
||||||
|
QVector<int> quadTrianglesIndices; // original indices from the FBX mesh of the quad converted as triangles
|
||||||
|
QVector<int> triangleIndices; // original indices from the FBX mesh
|
||||||
|
|
||||||
|
QString materialID;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FBXMaterial {
|
||||||
|
public:
|
||||||
|
FBXMaterial() {};
|
||||||
|
FBXMaterial(const glm::vec3& diffuseColor, const glm::vec3& specularColor, const glm::vec3& emissiveColor,
|
||||||
|
float shininess, float opacity) :
|
||||||
|
diffuseColor(diffuseColor),
|
||||||
|
specularColor(specularColor),
|
||||||
|
emissiveColor(emissiveColor),
|
||||||
|
shininess(shininess),
|
||||||
|
opacity(opacity) {}
|
||||||
|
|
||||||
|
void getTextureNames(QSet<QString>& textureList) const;
|
||||||
|
void setMaxNumPixelsPerTexture(int maxNumPixels);
|
||||||
|
|
||||||
|
glm::vec3 diffuseColor{ 1.0f };
|
||||||
|
float diffuseFactor{ 1.0f };
|
||||||
|
glm::vec3 specularColor{ 0.02f };
|
||||||
|
float specularFactor{ 1.0f };
|
||||||
|
|
||||||
|
glm::vec3 emissiveColor{ 0.0f };
|
||||||
|
float emissiveFactor{ 0.0f };
|
||||||
|
|
||||||
|
float shininess{ 23.0f };
|
||||||
|
float opacity{ 1.0f };
|
||||||
|
|
||||||
|
float metallic{ 0.0f };
|
||||||
|
float roughness{ 1.0f };
|
||||||
|
float emissiveIntensity{ 1.0f };
|
||||||
|
float ambientFactor{ 1.0f };
|
||||||
|
|
||||||
|
QString materialID;
|
||||||
|
QString name;
|
||||||
|
QString shadingModel;
|
||||||
|
model::MaterialPointer _material;
|
||||||
|
|
||||||
|
FBXTexture normalTexture;
|
||||||
|
FBXTexture albedoTexture;
|
||||||
|
FBXTexture opacityTexture;
|
||||||
|
FBXTexture glossTexture;
|
||||||
|
FBXTexture roughnessTexture;
|
||||||
|
FBXTexture specularTexture;
|
||||||
|
FBXTexture metallicTexture;
|
||||||
|
FBXTexture emissiveTexture;
|
||||||
|
FBXTexture occlusionTexture;
|
||||||
|
FBXTexture scatteringTexture;
|
||||||
|
FBXTexture lightmapTexture;
|
||||||
|
glm::vec2 lightmapParams{ 0.0f, 1.0f };
|
||||||
|
|
||||||
|
|
||||||
|
bool isPBSMaterial{ false };
|
||||||
|
// THe use XXXMap are not really used to drive which map are going or not, debug only
|
||||||
|
bool useNormalMap{ false };
|
||||||
|
bool useAlbedoMap{ false };
|
||||||
|
bool useOpacityMap{ false };
|
||||||
|
bool useRoughnessMap{ false };
|
||||||
|
bool useSpecularMap{ false };
|
||||||
|
bool useMetallicMap{ false };
|
||||||
|
bool useEmissiveMap{ false };
|
||||||
|
bool useOcclusionMap{ false };
|
||||||
|
|
||||||
|
bool needTangentSpace() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A single mesh (with optional blendshapes) extracted from an FBX document.
|
||||||
|
class FBXMesh {
|
||||||
|
public:
|
||||||
|
|
||||||
|
QVector<FBXMeshPart> parts;
|
||||||
|
|
||||||
|
QVector<glm::vec3> vertices;
|
||||||
|
QVector<glm::vec3> normals;
|
||||||
|
QVector<glm::vec3> tangents;
|
||||||
|
QVector<glm::vec3> colors;
|
||||||
|
QVector<glm::vec2> texCoords;
|
||||||
|
QVector<glm::vec2> texCoords1;
|
||||||
|
QVector<uint16_t> clusterIndices;
|
||||||
|
QVector<uint8_t> clusterWeights;
|
||||||
|
|
||||||
|
QVector<FBXCluster> clusters;
|
||||||
|
|
||||||
|
Extents meshExtents;
|
||||||
|
glm::mat4 modelTransform;
|
||||||
|
|
||||||
|
QVector<FBXBlendshape> blendshapes;
|
||||||
|
|
||||||
|
unsigned int meshIndex; // the order the meshes appeared in the object file
|
||||||
|
|
||||||
|
model::MeshPointer _mesh;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExtractedMesh {
|
||||||
|
public:
|
||||||
|
FBXMesh mesh;
|
||||||
|
QMultiHash<int, int> newIndices;
|
||||||
|
QVector<QHash<int, int> > blendshapeIndexMaps;
|
||||||
|
QVector<QPair<int, int> > partMaterialTextures;
|
||||||
|
QHash<QString, size_t> texcoordSetMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A single animation frame extracted from an FBX document.
|
||||||
|
class FBXAnimationFrame {
|
||||||
|
public:
|
||||||
|
QVector<glm::quat> rotations;
|
||||||
|
QVector<glm::vec3> translations;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A light in an FBX document.
|
||||||
|
class FBXLight {
|
||||||
|
public:
|
||||||
|
QString name;
|
||||||
|
Transform transform;
|
||||||
|
float intensity;
|
||||||
|
float fogValue;
|
||||||
|
glm::vec3 color;
|
||||||
|
|
||||||
|
FBXLight() :
|
||||||
|
name(),
|
||||||
|
transform(),
|
||||||
|
intensity(1.0f),
|
||||||
|
fogValue(0.0f),
|
||||||
|
color(1.0f)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(FBXAnimationFrame)
|
||||||
|
Q_DECLARE_METATYPE(QVector<FBXAnimationFrame>)
|
||||||
|
|
||||||
|
/// A set of meshes extracted from an FBX document.
|
||||||
|
class FBXGeometry {
|
||||||
|
public:
|
||||||
|
using Pointer = std::shared_ptr<FBXGeometry>;
|
||||||
|
|
||||||
|
QString originalURL;
|
||||||
|
QString author;
|
||||||
|
QString applicationName; ///< the name of the application that generated the model
|
||||||
|
|
||||||
|
QVector<FBXJoint> joints;
|
||||||
|
QHash<QString, int> jointIndices; ///< 1-based, so as to more easily detect missing indices
|
||||||
|
bool hasSkeletonJoints;
|
||||||
|
|
||||||
|
QVector<FBXMesh> meshes;
|
||||||
|
|
||||||
|
QHash<QString, FBXMaterial> materials;
|
||||||
|
|
||||||
|
glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file
|
||||||
|
|
||||||
|
int leftEyeJointIndex = -1;
|
||||||
|
int rightEyeJointIndex = -1;
|
||||||
|
int neckJointIndex = -1;
|
||||||
|
int rootJointIndex = -1;
|
||||||
|
int leanJointIndex = -1;
|
||||||
|
int headJointIndex = -1;
|
||||||
|
int leftHandJointIndex = -1;
|
||||||
|
int rightHandJointIndex = -1;
|
||||||
|
int leftToeJointIndex = -1;
|
||||||
|
int rightToeJointIndex = -1;
|
||||||
|
|
||||||
|
float leftEyeSize = 0.0f; // Maximum mesh extents dimension
|
||||||
|
float rightEyeSize = 0.0f;
|
||||||
|
|
||||||
|
QVector<int> humanIKJointIndices;
|
||||||
|
|
||||||
|
glm::vec3 palmDirection;
|
||||||
|
|
||||||
|
glm::vec3 neckPivot;
|
||||||
|
|
||||||
|
Extents bindExtents;
|
||||||
|
Extents meshExtents;
|
||||||
|
|
||||||
|
QVector<FBXAnimationFrame> animationFrames;
|
||||||
|
|
||||||
|
int getJointIndex(const QString& name) const { return jointIndices.value(name) - 1; }
|
||||||
|
QStringList getJointNames() const;
|
||||||
|
|
||||||
|
bool hasBlendedMeshes() const;
|
||||||
|
|
||||||
|
/// Returns the unscaled extents of the model's mesh
|
||||||
|
Extents getUnscaledMeshExtents() const;
|
||||||
|
|
||||||
|
bool convexHullContains(const glm::vec3& point) const;
|
||||||
|
|
||||||
|
QHash<int, QString> meshIndicesToModelNames;
|
||||||
|
|
||||||
|
/// given a meshIndex this will return the name of the model that mesh belongs to if known
|
||||||
|
QString getModelNameOfMesh(int meshIndex) const;
|
||||||
|
|
||||||
|
QList<QString> blendshapeChannelNames;
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(FBXGeometry)
|
||||||
|
Q_DECLARE_METATYPE(FBXGeometry::Pointer)
|
||||||
|
|
||||||
|
#endif // hifi_FBX_h_
|
|
@ -168,7 +168,8 @@ QString getID(const QVariantList& properties, int index = 0) {
|
||||||
return processID(properties.at(index).toString());
|
return processID(properties.at(index).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* HUMANIK_JOINTS[] = {
|
/// The names of the joints in the Maya HumanIK rig
|
||||||
|
static const std::array<const char*, 16> HUMANIK_JOINTS = {{
|
||||||
"RightHand",
|
"RightHand",
|
||||||
"RightForeArm",
|
"RightForeArm",
|
||||||
"RightArm",
|
"RightArm",
|
||||||
|
@ -184,9 +185,8 @@ const char* HUMANIK_JOINTS[] = {
|
||||||
"RightLeg",
|
"RightLeg",
|
||||||
"LeftLeg",
|
"LeftLeg",
|
||||||
"RightFoot",
|
"RightFoot",
|
||||||
"LeftFoot",
|
"LeftFoot"
|
||||||
""
|
}};
|
||||||
};
|
|
||||||
|
|
||||||
class FBXModel {
|
class FBXModel {
|
||||||
public:
|
public:
|
||||||
|
@ -468,7 +468,7 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) {
|
||||||
}
|
}
|
||||||
|
|
||||||
FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) {
|
FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) {
|
||||||
const FBXNode& node = _fbxNode;
|
const FBXNode& node = _rootNode;
|
||||||
QMap<QString, ExtractedMesh> meshes;
|
QMap<QString, ExtractedMesh> meshes;
|
||||||
QHash<QString, QString> modelIDsToNames;
|
QHash<QString, QString> modelIDsToNames;
|
||||||
QHash<QString, int> meshIDsToMeshIndices;
|
QHash<QString, int> meshIDsToMeshIndices;
|
||||||
|
@ -512,11 +512,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS
|
||||||
|
|
||||||
|
|
||||||
QVector<QString> humanIKJointNames;
|
QVector<QString> humanIKJointNames;
|
||||||
for (int i = 0;; i++) {
|
for (int i = 0; i < (int) HUMANIK_JOINTS.size(); i++) {
|
||||||
QByteArray jointName = HUMANIK_JOINTS[i];
|
QByteArray jointName = HUMANIK_JOINTS[i];
|
||||||
if (jointName.isEmpty()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
humanIKJointNames.append(processID(getString(joints.value(jointName, jointName))));
|
humanIKJointNames.append(processID(getString(joints.value(jointName, jointName))));
|
||||||
}
|
}
|
||||||
QVector<QString> humanIKJointIDs(humanIKJointNames.size());
|
QVector<QString> humanIKJointIDs(humanIKJointNames.size());
|
||||||
|
@ -942,7 +939,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS
|
||||||
QByteArray content;
|
QByteArray content;
|
||||||
foreach (const FBXNode& subobject, object.children) {
|
foreach (const FBXNode& subobject, object.children) {
|
||||||
if (subobject.name == "RelativeFilename") {
|
if (subobject.name == "RelativeFilename") {
|
||||||
filepath= subobject.properties.at(0).toByteArray();
|
filepath = subobject.properties.at(0).toByteArray();
|
||||||
filepath = filepath.replace('\\', '/');
|
filepath = filepath.replace('\\', '/');
|
||||||
|
|
||||||
} else if (subobject.name == "Content" && !subobject.properties.isEmpty()) {
|
} else if (subobject.name == "Content" && !subobject.properties.isEmpty()) {
|
||||||
|
@ -1842,7 +1839,7 @@ FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const
|
||||||
|
|
||||||
FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) {
|
FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) {
|
||||||
FBXReader reader;
|
FBXReader reader;
|
||||||
reader._fbxNode = FBXReader::parseFBX(device);
|
reader._rootNode = FBXReader::parseFBX(device);
|
||||||
reader._loadLightmaps = loadLightmaps;
|
reader._loadLightmaps = loadLightmaps;
|
||||||
reader._lightmapLevel = lightmapLevel;
|
reader._lightmapLevel = lightmapLevel;
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
#ifndef hifi_FBXReader_h
|
#ifndef hifi_FBXReader_h
|
||||||
#define hifi_FBXReader_h
|
#define hifi_FBXReader_h
|
||||||
|
|
||||||
|
#include "FBX.h"
|
||||||
|
|
||||||
#include <QMetaType>
|
#include <QMetaType>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
@ -31,305 +33,6 @@
|
||||||
class QIODevice;
|
class QIODevice;
|
||||||
class FBXNode;
|
class FBXNode;
|
||||||
|
|
||||||
typedef QList<FBXNode> FBXNodeList;
|
|
||||||
|
|
||||||
/// The names of the joints in the Maya HumanIK rig, terminated with an empty string.
|
|
||||||
extern const char* HUMANIK_JOINTS[];
|
|
||||||
|
|
||||||
/// A node within an FBX document.
|
|
||||||
class FBXNode {
|
|
||||||
public:
|
|
||||||
|
|
||||||
QByteArray name;
|
|
||||||
QVariantList properties;
|
|
||||||
FBXNodeList children;
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A single blendshape extracted from an FBX document.
|
|
||||||
class FBXBlendshape {
|
|
||||||
public:
|
|
||||||
|
|
||||||
QVector<int> indices;
|
|
||||||
QVector<glm::vec3> vertices;
|
|
||||||
QVector<glm::vec3> normals;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FBXJointShapeInfo {
|
|
||||||
// same units and frame as FBXJoint.translation
|
|
||||||
glm::vec3 avgPoint;
|
|
||||||
std::vector<float> dots;
|
|
||||||
std::vector<glm::vec3> points;
|
|
||||||
std::vector<glm::vec3> debugLines;
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A single joint (transformation node) extracted from an FBX document.
|
|
||||||
class FBXJoint {
|
|
||||||
public:
|
|
||||||
|
|
||||||
FBXJointShapeInfo shapeInfo;
|
|
||||||
QVector<int> freeLineage;
|
|
||||||
bool isFree;
|
|
||||||
int parentIndex;
|
|
||||||
float distanceToParent;
|
|
||||||
|
|
||||||
// http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/SDKRef/a00209.html
|
|
||||||
|
|
||||||
glm::vec3 translation; // T
|
|
||||||
glm::mat4 preTransform; // Roff * Rp
|
|
||||||
glm::quat preRotation; // Rpre
|
|
||||||
glm::quat rotation; // R
|
|
||||||
glm::quat postRotation; // Rpost
|
|
||||||
glm::mat4 postTransform; // Rp-1 * Soff * Sp * S * Sp-1
|
|
||||||
|
|
||||||
// World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1)
|
|
||||||
|
|
||||||
glm::mat4 transform;
|
|
||||||
glm::vec3 rotationMin; // radians
|
|
||||||
glm::vec3 rotationMax; // radians
|
|
||||||
glm::quat inverseDefaultRotation;
|
|
||||||
glm::quat inverseBindRotation;
|
|
||||||
glm::mat4 bindTransform;
|
|
||||||
QString name;
|
|
||||||
bool isSkeletonJoint;
|
|
||||||
bool bindTransformFoundInCluster;
|
|
||||||
|
|
||||||
// geometric offset is applied in local space but does NOT affect children.
|
|
||||||
bool hasGeometricOffset;
|
|
||||||
glm::vec3 geometricTranslation;
|
|
||||||
glm::quat geometricRotation;
|
|
||||||
glm::vec3 geometricScaling;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/// A single binding to a joint in an FBX document.
|
|
||||||
class FBXCluster {
|
|
||||||
public:
|
|
||||||
|
|
||||||
int jointIndex;
|
|
||||||
glm::mat4 inverseBindMatrix;
|
|
||||||
};
|
|
||||||
|
|
||||||
const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048;
|
|
||||||
|
|
||||||
/// A texture map in an FBX document.
|
|
||||||
class FBXTexture {
|
|
||||||
public:
|
|
||||||
QString name;
|
|
||||||
QByteArray filename;
|
|
||||||
QByteArray content;
|
|
||||||
|
|
||||||
Transform transform;
|
|
||||||
int maxNumPixels { MAX_NUM_PIXELS_FOR_FBX_TEXTURE };
|
|
||||||
int texcoordSet;
|
|
||||||
QString texcoordSetName;
|
|
||||||
|
|
||||||
bool isBumpmap{ false };
|
|
||||||
|
|
||||||
bool isNull() const { return name.isEmpty() && filename.isEmpty() && content.isEmpty(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A single part of a mesh (with the same material).
|
|
||||||
class FBXMeshPart {
|
|
||||||
public:
|
|
||||||
|
|
||||||
QVector<int> quadIndices; // original indices from the FBX mesh
|
|
||||||
QVector<int> quadTrianglesIndices; // original indices from the FBX mesh of the quad converted as triangles
|
|
||||||
QVector<int> triangleIndices; // original indices from the FBX mesh
|
|
||||||
|
|
||||||
QString materialID;
|
|
||||||
};
|
|
||||||
|
|
||||||
class FBXMaterial {
|
|
||||||
public:
|
|
||||||
FBXMaterial() {};
|
|
||||||
FBXMaterial(const glm::vec3& diffuseColor, const glm::vec3& specularColor, const glm::vec3& emissiveColor,
|
|
||||||
float shininess, float opacity) :
|
|
||||||
diffuseColor(diffuseColor),
|
|
||||||
specularColor(specularColor),
|
|
||||||
emissiveColor(emissiveColor),
|
|
||||||
shininess(shininess),
|
|
||||||
opacity(opacity) {}
|
|
||||||
|
|
||||||
void getTextureNames(QSet<QString>& textureList) const;
|
|
||||||
void setMaxNumPixelsPerTexture(int maxNumPixels);
|
|
||||||
|
|
||||||
glm::vec3 diffuseColor{ 1.0f };
|
|
||||||
float diffuseFactor{ 1.0f };
|
|
||||||
glm::vec3 specularColor{ 0.02f };
|
|
||||||
float specularFactor{ 1.0f };
|
|
||||||
|
|
||||||
glm::vec3 emissiveColor{ 0.0f };
|
|
||||||
float emissiveFactor{ 0.0f };
|
|
||||||
|
|
||||||
float shininess{ 23.0f };
|
|
||||||
float opacity{ 1.0f };
|
|
||||||
|
|
||||||
float metallic{ 0.0f };
|
|
||||||
float roughness{ 1.0f };
|
|
||||||
float emissiveIntensity{ 1.0f };
|
|
||||||
float ambientFactor{ 1.0f };
|
|
||||||
|
|
||||||
QString materialID;
|
|
||||||
QString name;
|
|
||||||
QString shadingModel;
|
|
||||||
model::MaterialPointer _material;
|
|
||||||
|
|
||||||
FBXTexture normalTexture;
|
|
||||||
FBXTexture albedoTexture;
|
|
||||||
FBXTexture opacityTexture;
|
|
||||||
FBXTexture glossTexture;
|
|
||||||
FBXTexture roughnessTexture;
|
|
||||||
FBXTexture specularTexture;
|
|
||||||
FBXTexture metallicTexture;
|
|
||||||
FBXTexture emissiveTexture;
|
|
||||||
FBXTexture occlusionTexture;
|
|
||||||
FBXTexture scatteringTexture;
|
|
||||||
FBXTexture lightmapTexture;
|
|
||||||
glm::vec2 lightmapParams{ 0.0f, 1.0f };
|
|
||||||
|
|
||||||
|
|
||||||
bool isPBSMaterial{ false };
|
|
||||||
// THe use XXXMap are not really used to drive which map are going or not, debug only
|
|
||||||
bool useNormalMap{ false };
|
|
||||||
bool useAlbedoMap{ false };
|
|
||||||
bool useOpacityMap{ false };
|
|
||||||
bool useRoughnessMap{ false };
|
|
||||||
bool useSpecularMap{ false };
|
|
||||||
bool useMetallicMap{ false };
|
|
||||||
bool useEmissiveMap{ false };
|
|
||||||
bool useOcclusionMap{ false };
|
|
||||||
|
|
||||||
bool needTangentSpace() const;
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A single mesh (with optional blendshapes) extracted from an FBX document.
|
|
||||||
class FBXMesh {
|
|
||||||
public:
|
|
||||||
|
|
||||||
QVector<FBXMeshPart> parts;
|
|
||||||
|
|
||||||
QVector<glm::vec3> vertices;
|
|
||||||
QVector<glm::vec3> normals;
|
|
||||||
QVector<glm::vec3> tangents;
|
|
||||||
QVector<glm::vec3> colors;
|
|
||||||
QVector<glm::vec2> texCoords;
|
|
||||||
QVector<glm::vec2> texCoords1;
|
|
||||||
QVector<uint16_t> clusterIndices;
|
|
||||||
QVector<uint8_t> clusterWeights;
|
|
||||||
|
|
||||||
QVector<FBXCluster> clusters;
|
|
||||||
|
|
||||||
Extents meshExtents;
|
|
||||||
glm::mat4 modelTransform;
|
|
||||||
|
|
||||||
QVector<FBXBlendshape> blendshapes;
|
|
||||||
|
|
||||||
unsigned int meshIndex; // the order the meshes appeared in the object file
|
|
||||||
|
|
||||||
model::MeshPointer _mesh;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ExtractedMesh {
|
|
||||||
public:
|
|
||||||
FBXMesh mesh;
|
|
||||||
QMultiHash<int, int> newIndices;
|
|
||||||
QVector<QHash<int, int> > blendshapeIndexMaps;
|
|
||||||
QVector<QPair<int, int> > partMaterialTextures;
|
|
||||||
QHash<QString, size_t> texcoordSetMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A single animation frame extracted from an FBX document.
|
|
||||||
class FBXAnimationFrame {
|
|
||||||
public:
|
|
||||||
QVector<glm::quat> rotations;
|
|
||||||
QVector<glm::vec3> translations;
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A light in an FBX document.
|
|
||||||
class FBXLight {
|
|
||||||
public:
|
|
||||||
QString name;
|
|
||||||
Transform transform;
|
|
||||||
float intensity;
|
|
||||||
float fogValue;
|
|
||||||
glm::vec3 color;
|
|
||||||
|
|
||||||
FBXLight() :
|
|
||||||
name(),
|
|
||||||
transform(),
|
|
||||||
intensity(1.0f),
|
|
||||||
fogValue(0.0f),
|
|
||||||
color(1.0f)
|
|
||||||
{}
|
|
||||||
};
|
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(FBXAnimationFrame)
|
|
||||||
Q_DECLARE_METATYPE(QVector<FBXAnimationFrame>)
|
|
||||||
|
|
||||||
/// A set of meshes extracted from an FBX document.
|
|
||||||
class FBXGeometry {
|
|
||||||
public:
|
|
||||||
using Pointer = std::shared_ptr<FBXGeometry>;
|
|
||||||
|
|
||||||
QString originalURL;
|
|
||||||
QString author;
|
|
||||||
QString applicationName; ///< the name of the application that generated the model
|
|
||||||
|
|
||||||
QVector<FBXJoint> joints;
|
|
||||||
QHash<QString, int> jointIndices; ///< 1-based, so as to more easily detect missing indices
|
|
||||||
bool hasSkeletonJoints;
|
|
||||||
|
|
||||||
QVector<FBXMesh> meshes;
|
|
||||||
|
|
||||||
QHash<QString, FBXMaterial> materials;
|
|
||||||
|
|
||||||
glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file
|
|
||||||
|
|
||||||
int leftEyeJointIndex = -1;
|
|
||||||
int rightEyeJointIndex = -1;
|
|
||||||
int neckJointIndex = -1;
|
|
||||||
int rootJointIndex = -1;
|
|
||||||
int leanJointIndex = -1;
|
|
||||||
int headJointIndex = -1;
|
|
||||||
int leftHandJointIndex = -1;
|
|
||||||
int rightHandJointIndex = -1;
|
|
||||||
int leftToeJointIndex = -1;
|
|
||||||
int rightToeJointIndex = -1;
|
|
||||||
|
|
||||||
float leftEyeSize = 0.0f; // Maximum mesh extents dimension
|
|
||||||
float rightEyeSize = 0.0f;
|
|
||||||
|
|
||||||
QVector<int> humanIKJointIndices;
|
|
||||||
|
|
||||||
glm::vec3 palmDirection;
|
|
||||||
|
|
||||||
glm::vec3 neckPivot;
|
|
||||||
|
|
||||||
Extents bindExtents;
|
|
||||||
Extents meshExtents;
|
|
||||||
|
|
||||||
QVector<FBXAnimationFrame> animationFrames;
|
|
||||||
|
|
||||||
int getJointIndex(const QString& name) const { return jointIndices.value(name) - 1; }
|
|
||||||
QStringList getJointNames() const;
|
|
||||||
|
|
||||||
bool hasBlendedMeshes() const;
|
|
||||||
|
|
||||||
/// Returns the unscaled extents of the model's mesh
|
|
||||||
Extents getUnscaledMeshExtents() const;
|
|
||||||
|
|
||||||
bool convexHullContains(const glm::vec3& point) const;
|
|
||||||
|
|
||||||
QHash<int, QString> meshIndicesToModelNames;
|
|
||||||
|
|
||||||
/// given a meshIndex this will return the name of the model that mesh belongs to if known
|
|
||||||
QString getModelNameOfMesh(int meshIndex) const;
|
|
||||||
|
|
||||||
QList<QString> blendshapeChannelNames;
|
|
||||||
};
|
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(FBXGeometry)
|
|
||||||
Q_DECLARE_METATYPE(FBXGeometry::Pointer)
|
|
||||||
|
|
||||||
/// Reads FBX geometry from the supplied model and mapping data.
|
/// Reads FBX geometry from the supplied model and mapping data.
|
||||||
/// \exception QString if an error occurs in parsing
|
/// \exception QString if an error occurs in parsing
|
||||||
|
@ -402,12 +105,12 @@ class FBXReader {
|
||||||
public:
|
public:
|
||||||
FBXGeometry* _fbxGeometry;
|
FBXGeometry* _fbxGeometry;
|
||||||
|
|
||||||
FBXNode _fbxNode;
|
FBXNode _rootNode;
|
||||||
static FBXNode parseFBX(QIODevice* device);
|
static FBXNode parseFBX(QIODevice* device);
|
||||||
|
|
||||||
FBXGeometry* extractFBXGeometry(const QVariantHash& mapping, const QString& url);
|
FBXGeometry* extractFBXGeometry(const QVariantHash& mapping, const QString& url);
|
||||||
|
|
||||||
ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex);
|
static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex);
|
||||||
QHash<QString, ExtractedMesh> meshes;
|
QHash<QString, ExtractedMesh> meshes;
|
||||||
static void buildModelMesh(FBXMesh& extractedMesh, const QString& url);
|
static void buildModelMesh(FBXMesh& extractedMesh, const QString& url);
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,7 @@ FBXTexture FBXReader::getTexture(const QString& textureID) {
|
||||||
texture.filename = filepath;
|
texture.filename = filepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
texture.id = textureID;
|
||||||
texture.name = _textureNames.value(textureID);
|
texture.name = _textureNames.value(textureID);
|
||||||
texture.transform.setIdentity();
|
texture.transform.setIdentity();
|
||||||
texture.texcoordSet = 0;
|
texture.texcoordSet = 0;
|
||||||
|
|
|
@ -9,6 +9,17 @@
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#pragma warning( push )
|
||||||
|
#pragma warning( disable : 4267 )
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <draco/compression/decode.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#pragma warning( pop )
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QDataStream>
|
#include <QDataStream>
|
||||||
|
@ -168,11 +179,17 @@ void appendIndex(MeshData& data, QVector<int>& indices, int index) {
|
||||||
ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIndex) {
|
ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIndex) {
|
||||||
MeshData data;
|
MeshData data;
|
||||||
data.extracted.mesh.meshIndex = meshIndex++;
|
data.extracted.mesh.meshIndex = meshIndex++;
|
||||||
|
|
||||||
QVector<int> materials;
|
QVector<int> materials;
|
||||||
QVector<int> textures;
|
QVector<int> textures;
|
||||||
|
|
||||||
bool isMaterialPerPolygon = false;
|
bool isMaterialPerPolygon = false;
|
||||||
|
|
||||||
static const QVariant BY_VERTICE = QByteArray("ByVertice");
|
static const QVariant BY_VERTICE = QByteArray("ByVertice");
|
||||||
static const QVariant INDEX_TO_DIRECT = QByteArray("IndexToDirect");
|
static const QVariant INDEX_TO_DIRECT = QByteArray("IndexToDirect");
|
||||||
|
|
||||||
|
bool isDracoMesh = false;
|
||||||
|
|
||||||
foreach (const FBXNode& child, object.children) {
|
foreach (const FBXNode& child, object.children) {
|
||||||
if (child.name == "Vertices") {
|
if (child.name == "Vertices") {
|
||||||
data.vertices = createVec3Vector(getDoubleVector(child));
|
data.vertices = createVec3Vector(getDoubleVector(child));
|
||||||
|
@ -304,8 +321,9 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn
|
||||||
if (subdata.name == "Materials") {
|
if (subdata.name == "Materials") {
|
||||||
materials = getIntVector(subdata);
|
materials = getIntVector(subdata);
|
||||||
} else if (subdata.name == "MappingInformationType") {
|
} else if (subdata.name == "MappingInformationType") {
|
||||||
if (subdata.properties.at(0) == BY_POLYGON)
|
if (subdata.properties.at(0) == BY_POLYGON) {
|
||||||
isMaterialPerPolygon = true;
|
isMaterialPerPolygon = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
isMaterialPerPolygon = false;
|
isMaterialPerPolygon = false;
|
||||||
}
|
}
|
||||||
|
@ -318,70 +336,194 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn
|
||||||
textures = getIntVector(subdata);
|
textures = getIntVector(subdata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (child.name == "DracoMesh") {
|
||||||
}
|
isDracoMesh = true;
|
||||||
|
|
||||||
bool isMultiMaterial = false;
|
// load the draco mesh from the FBX and create a draco::Mesh
|
||||||
if (isMaterialPerPolygon) {
|
draco::Decoder decoder;
|
||||||
isMultiMaterial = true;
|
draco::DecoderBuffer decodedBuffer;
|
||||||
}
|
QByteArray dracoArray = child.properties.at(0).value<QByteArray>();
|
||||||
// TODO: make excellent use of isMultiMaterial
|
decodedBuffer.Init(dracoArray.data(), dracoArray.size());
|
||||||
Q_UNUSED(isMultiMaterial);
|
|
||||||
|
|
||||||
// convert the polygons to quads and triangles
|
std::unique_ptr<draco::Mesh> dracoMesh(new draco::Mesh());
|
||||||
int polygonIndex = 0;
|
decoder.DecodeBufferToGeometry(&decodedBuffer, dracoMesh.get());
|
||||||
QHash<QPair<int, int>, int> materialTextureParts;
|
|
||||||
for (int beginIndex = 0; beginIndex < data.polygonIndices.size(); polygonIndex++) {
|
|
||||||
int endIndex = beginIndex;
|
|
||||||
while (endIndex < data.polygonIndices.size() && data.polygonIndices.at(endIndex++) >= 0);
|
|
||||||
|
|
||||||
QPair<int, int> materialTexture((polygonIndex < materials.size()) ? materials.at(polygonIndex) : 0,
|
// prepare attributes for this mesh
|
||||||
(polygonIndex < textures.size()) ? textures.at(polygonIndex) : 0);
|
auto positionAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::POSITION);
|
||||||
int& partIndex = materialTextureParts[materialTexture];
|
auto normalAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::NORMAL);
|
||||||
if (partIndex == 0) {
|
auto texCoordAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::TEX_COORD);
|
||||||
data.extracted.partMaterialTextures.append(materialTexture);
|
auto extraTexCoordAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_TEX_COORD_1);
|
||||||
data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1);
|
auto colorAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::COLOR);
|
||||||
partIndex = data.extracted.mesh.parts.size();
|
auto matTexAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_MATERIAL_ID);
|
||||||
}
|
|
||||||
FBXMeshPart& part = data.extracted.mesh.parts[partIndex - 1];
|
|
||||||
|
|
||||||
if (endIndex - beginIndex == 4) {
|
|
||||||
appendIndex(data, part.quadIndices, beginIndex++);
|
|
||||||
appendIndex(data, part.quadIndices, beginIndex++);
|
|
||||||
appendIndex(data, part.quadIndices, beginIndex++);
|
|
||||||
appendIndex(data, part.quadIndices, beginIndex++);
|
|
||||||
|
|
||||||
int quadStartIndex = part.quadIndices.size() - 4;
|
// setup extracted mesh data structures given number of points
|
||||||
int i0 = part.quadIndices[quadStartIndex + 0];
|
auto numVertices = dracoMesh->num_points();
|
||||||
int i1 = part.quadIndices[quadStartIndex + 1];
|
|
||||||
int i2 = part.quadIndices[quadStartIndex + 2];
|
|
||||||
int i3 = part.quadIndices[quadStartIndex + 3];
|
|
||||||
|
|
||||||
// Sam's recommended triangle slices
|
QHash<QPair<int, int>, int> materialTextureParts;
|
||||||
// Triangle tri1 = { v0, v1, v3 };
|
|
||||||
// Triangle tri2 = { v1, v2, v3 };
|
|
||||||
// NOTE: Random guy on the internet's recommended triangle slices
|
|
||||||
// Triangle tri1 = { v0, v1, v2 };
|
|
||||||
// Triangle tri2 = { v2, v3, v0 };
|
|
||||||
|
|
||||||
part.quadTrianglesIndices.append(i0);
|
data.extracted.mesh.vertices.resize(numVertices);
|
||||||
part.quadTrianglesIndices.append(i1);
|
|
||||||
part.quadTrianglesIndices.append(i3);
|
|
||||||
|
|
||||||
part.quadTrianglesIndices.append(i1);
|
if (normalAttribute) {
|
||||||
part.quadTrianglesIndices.append(i2);
|
data.extracted.mesh.normals.resize(numVertices);
|
||||||
part.quadTrianglesIndices.append(i3);
|
}
|
||||||
|
|
||||||
} else {
|
if (texCoordAttribute) {
|
||||||
for (int nextIndex = beginIndex + 1;; ) {
|
data.extracted.mesh.texCoords.resize(numVertices);
|
||||||
appendIndex(data, part.triangleIndices, beginIndex);
|
}
|
||||||
appendIndex(data, part.triangleIndices, nextIndex++);
|
|
||||||
appendIndex(data, part.triangleIndices, nextIndex);
|
if (extraTexCoordAttribute) {
|
||||||
if (nextIndex >= data.polygonIndices.size() || data.polygonIndices.at(nextIndex) < 0) {
|
data.extracted.mesh.texCoords1.resize(numVertices);
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
|
if (colorAttribute) {
|
||||||
|
data.extracted.mesh.colors.resize(numVertices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// enumerate the vertices and construct the extracted mesh
|
||||||
|
for (int i = 0; i < numVertices; ++i) {
|
||||||
|
draco::PointIndex vertexIndex(i);
|
||||||
|
|
||||||
|
if (positionAttribute) {
|
||||||
|
// read position from draco mesh to extracted mesh
|
||||||
|
auto mappedIndex = positionAttribute->mapped_index(vertexIndex);
|
||||||
|
|
||||||
|
positionAttribute->ConvertValue<float, 3>(mappedIndex,
|
||||||
|
reinterpret_cast<float*>(&data.extracted.mesh.vertices[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalAttribute) {
|
||||||
|
// read normals from draco mesh to extracted mesh
|
||||||
|
auto mappedIndex = normalAttribute->mapped_index(vertexIndex);
|
||||||
|
|
||||||
|
normalAttribute->ConvertValue<float, 3>(mappedIndex,
|
||||||
|
reinterpret_cast<float*>(&data.extracted.mesh.normals[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (texCoordAttribute) {
|
||||||
|
// read UVs from draco mesh to extracted mesh
|
||||||
|
auto mappedIndex = texCoordAttribute->mapped_index(vertexIndex);
|
||||||
|
|
||||||
|
texCoordAttribute->ConvertValue<float, 2>(mappedIndex,
|
||||||
|
reinterpret_cast<float*>(&data.extracted.mesh.texCoords[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraTexCoordAttribute) {
|
||||||
|
// some meshes have a second set of UVs, read those to extracted mesh
|
||||||
|
auto mappedIndex = extraTexCoordAttribute->mapped_index(vertexIndex);
|
||||||
|
|
||||||
|
extraTexCoordAttribute->ConvertValue<float, 2>(mappedIndex,
|
||||||
|
reinterpret_cast<float*>(&data.extracted.mesh.texCoords1[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorAttribute) {
|
||||||
|
// read vertex colors from draco mesh to extracted mesh
|
||||||
|
auto mappedIndex = colorAttribute->mapped_index(vertexIndex);
|
||||||
|
|
||||||
|
colorAttribute->ConvertValue<float, 3>(mappedIndex,
|
||||||
|
reinterpret_cast<float*>(&data.extracted.mesh.colors[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
data.extracted.newIndices.insert(i, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < dracoMesh->num_faces(); ++i) {
|
||||||
|
// grab the material ID and texture ID for this face, if we have it
|
||||||
|
auto& dracoFace = dracoMesh->face(draco::FaceIndex(i));
|
||||||
|
auto& firstCorner = dracoFace[0];
|
||||||
|
|
||||||
|
uint16_t materialID { 0 };
|
||||||
|
|
||||||
|
if (matTexAttribute) {
|
||||||
|
// read material ID and texture ID mappings into materials and texture vectors
|
||||||
|
auto mappedIndex = matTexAttribute->mapped_index(firstCorner);
|
||||||
|
|
||||||
|
matTexAttribute->ConvertValue<uint16_t, 1>(mappedIndex, &materialID);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPair<int, int> materialTexture(materialID, 0);
|
||||||
|
|
||||||
|
// grab or setup the FBXMeshPart for the part this face belongs to
|
||||||
|
int& partIndexPlusOne = materialTextureParts[materialTexture];
|
||||||
|
if (partIndexPlusOne == 0) {
|
||||||
|
data.extracted.partMaterialTextures.append(materialTexture);
|
||||||
|
data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1);
|
||||||
|
partIndexPlusOne = data.extracted.mesh.parts.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// give the mesh part this index
|
||||||
|
FBXMeshPart& part = data.extracted.mesh.parts[partIndexPlusOne - 1];
|
||||||
|
part.triangleIndices.append(firstCorner.value());
|
||||||
|
part.triangleIndices.append(dracoFace[1].value());
|
||||||
|
part.triangleIndices.append(dracoFace[2].value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when we have a draco mesh, we've already built the extracted mesh, so we don't need to do the
|
||||||
|
// processing we do for normal meshes below
|
||||||
|
if (!isDracoMesh) {
|
||||||
|
bool isMultiMaterial = false;
|
||||||
|
if (isMaterialPerPolygon) {
|
||||||
|
isMultiMaterial = true;
|
||||||
|
}
|
||||||
|
// TODO: make excellent use of isMultiMaterial
|
||||||
|
Q_UNUSED(isMultiMaterial);
|
||||||
|
|
||||||
|
// convert the polygons to quads and triangles
|
||||||
|
int polygonIndex = 0;
|
||||||
|
QHash<QPair<int, int>, int> materialTextureParts;
|
||||||
|
for (int beginIndex = 0; beginIndex < data.polygonIndices.size(); polygonIndex++) {
|
||||||
|
int endIndex = beginIndex;
|
||||||
|
while (endIndex < data.polygonIndices.size() && data.polygonIndices.at(endIndex++) >= 0);
|
||||||
|
|
||||||
|
QPair<int, int> materialTexture((polygonIndex < materials.size()) ? materials.at(polygonIndex) : 0,
|
||||||
|
(polygonIndex < textures.size()) ? textures.at(polygonIndex) : 0);
|
||||||
|
int& partIndex = materialTextureParts[materialTexture];
|
||||||
|
if (partIndex == 0) {
|
||||||
|
data.extracted.partMaterialTextures.append(materialTexture);
|
||||||
|
data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1);
|
||||||
|
partIndex = data.extracted.mesh.parts.size();
|
||||||
|
}
|
||||||
|
FBXMeshPart& part = data.extracted.mesh.parts[partIndex - 1];
|
||||||
|
|
||||||
|
if (endIndex - beginIndex == 4) {
|
||||||
|
appendIndex(data, part.quadIndices, beginIndex++);
|
||||||
|
appendIndex(data, part.quadIndices, beginIndex++);
|
||||||
|
appendIndex(data, part.quadIndices, beginIndex++);
|
||||||
|
appendIndex(data, part.quadIndices, beginIndex++);
|
||||||
|
|
||||||
|
int quadStartIndex = part.quadIndices.size() - 4;
|
||||||
|
int i0 = part.quadIndices[quadStartIndex + 0];
|
||||||
|
int i1 = part.quadIndices[quadStartIndex + 1];
|
||||||
|
int i2 = part.quadIndices[quadStartIndex + 2];
|
||||||
|
int i3 = part.quadIndices[quadStartIndex + 3];
|
||||||
|
|
||||||
|
// Sam's recommended triangle slices
|
||||||
|
// Triangle tri1 = { v0, v1, v3 };
|
||||||
|
// Triangle tri2 = { v1, v2, v3 };
|
||||||
|
// NOTE: Random guy on the internet's recommended triangle slices
|
||||||
|
// Triangle tri1 = { v0, v1, v2 };
|
||||||
|
// Triangle tri2 = { v2, v3, v0 };
|
||||||
|
|
||||||
|
part.quadTrianglesIndices.append(i0);
|
||||||
|
part.quadTrianglesIndices.append(i1);
|
||||||
|
part.quadTrianglesIndices.append(i3);
|
||||||
|
|
||||||
|
part.quadTrianglesIndices.append(i1);
|
||||||
|
part.quadTrianglesIndices.append(i2);
|
||||||
|
part.quadTrianglesIndices.append(i3);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
for (int nextIndex = beginIndex + 1;; ) {
|
||||||
|
appendIndex(data, part.triangleIndices, beginIndex);
|
||||||
|
appendIndex(data, part.triangleIndices, nextIndex++);
|
||||||
|
appendIndex(data, part.triangleIndices, nextIndex);
|
||||||
|
if (nextIndex >= data.polygonIndices.size() || data.polygonIndices.at(nextIndex) < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
beginIndex = endIndex;
|
||||||
}
|
}
|
||||||
beginIndex = endIndex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,15 +24,18 @@
|
||||||
#include <shared/NsightHelpers.h>
|
#include <shared/NsightHelpers.h>
|
||||||
#include "ModelFormatLogging.h"
|
#include "ModelFormatLogging.h"
|
||||||
|
|
||||||
template<class T> int streamSize() {
|
template<class T>
|
||||||
|
int streamSize() {
|
||||||
return sizeof(T);
|
return sizeof(T);
|
||||||
}
|
}
|
||||||
|
|
||||||
template<bool> int streamSize() {
|
template<bool>
|
||||||
|
int streamSize() {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<class T> QVariant readBinaryArray(QDataStream& in, int& position) {
|
template<class T>
|
||||||
|
QVariant readBinaryArray(QDataStream& in, int& position) {
|
||||||
quint32 arrayLength;
|
quint32 arrayLength;
|
||||||
quint32 encoding;
|
quint32 encoding;
|
||||||
quint32 compressedLength;
|
quint32 compressedLength;
|
||||||
|
@ -350,8 +353,7 @@ FBXNode parseTextFBXNode(Tokenizer& tokenizer) {
|
||||||
FBXNode FBXReader::parseFBX(QIODevice* device) {
|
FBXNode FBXReader::parseFBX(QIODevice* device) {
|
||||||
PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xff0000ff, device);
|
PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xff0000ff, device);
|
||||||
// verify the prolog
|
// verify the prolog
|
||||||
const QByteArray BINARY_PROLOG = "Kaydara FBX Binary ";
|
if (device->peek(FBX_BINARY_PROLOG.size()) != FBX_BINARY_PROLOG) {
|
||||||
if (device->peek(BINARY_PROLOG.size()) != BINARY_PROLOG) {
|
|
||||||
// parse as a text file
|
// parse as a text file
|
||||||
FBXNode top;
|
FBXNode top;
|
||||||
Tokenizer tokenizer(device);
|
Tokenizer tokenizer(device);
|
||||||
|
@ -377,15 +379,13 @@ FBXNode FBXReader::parseFBX(QIODevice* device) {
|
||||||
// Bytes 0 - 20: Kaydara FBX Binary \x00(file - magic, with 2 spaces at the end, then a NULL terminator).
|
// Bytes 0 - 20: Kaydara FBX Binary \x00(file - magic, with 2 spaces at the end, then a NULL terminator).
|
||||||
// Bytes 21 - 22: [0x1A, 0x00](unknown but all observed files show these bytes).
|
// Bytes 21 - 22: [0x1A, 0x00](unknown but all observed files show these bytes).
|
||||||
// Bytes 23 - 26 : unsigned int, the version number. 7300 for version 7.3 for example.
|
// Bytes 23 - 26 : unsigned int, the version number. 7300 for version 7.3 for example.
|
||||||
const int HEADER_BEFORE_VERSION = 23;
|
in.skipRawData(FBX_HEADER_BYTES_BEFORE_VERSION);
|
||||||
const quint32 VERSION_FBX2016 = 7500;
|
int position = FBX_HEADER_BYTES_BEFORE_VERSION;
|
||||||
in.skipRawData(HEADER_BEFORE_VERSION);
|
|
||||||
int position = HEADER_BEFORE_VERSION;
|
|
||||||
quint32 fileVersion;
|
quint32 fileVersion;
|
||||||
in >> fileVersion;
|
in >> fileVersion;
|
||||||
position += sizeof(fileVersion);
|
position += sizeof(fileVersion);
|
||||||
qCDebug(modelformat) << "fileVersion:" << fileVersion;
|
qCDebug(modelformat) << "fileVersion:" << fileVersion;
|
||||||
bool has64BitPositions = (fileVersion >= VERSION_FBX2016);
|
bool has64BitPositions = (fileVersion >= FBX_VERSION_2016);
|
||||||
|
|
||||||
// parse the top-level node
|
// parse the top-level node
|
||||||
FBXNode top;
|
FBXNode top;
|
||||||
|
|
222
libraries/fbx/src/FBXWriter.cpp
Normal file
222
libraries/fbx/src/FBXWriter.cpp
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
//
|
||||||
|
// FBXWriter.cpp
|
||||||
|
// libraries/fbx/src
|
||||||
|
//
|
||||||
|
// Created by Ryan Huffman on 9/5/17.
|
||||||
|
// Copyright 2017 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "FBXWriter.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void writeVector(QDataStream& out, char ch, QVector<T> list) {
|
||||||
|
out.device()->write(&ch, 1);
|
||||||
|
out << (int32_t)list.length();
|
||||||
|
out << (int32_t)0;
|
||||||
|
out << (int32_t)0;
|
||||||
|
|
||||||
|
out.writeBytes(reinterpret_cast<const char*>(list.constData()), list.length() * sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
QByteArray FBXWriter::encodeFBX(const FBXNode& root) {
|
||||||
|
QByteArray data;
|
||||||
|
QDataStream out(&data, QIODevice::WriteOnly);
|
||||||
|
out.setByteOrder(QDataStream::LittleEndian);
|
||||||
|
out.setVersion(QDataStream::Qt_4_5);
|
||||||
|
|
||||||
|
out.writeRawData(FBX_BINARY_PROLOG, FBX_BINARY_PROLOG.size());
|
||||||
|
auto bytes = QByteArray(FBX_HEADER_BYTES_BEFORE_VERSION - FBX_BINARY_PROLOG.size(), '\0');
|
||||||
|
out.writeRawData(bytes, bytes.size());
|
||||||
|
|
||||||
|
out << FBX_VERSION_2016;
|
||||||
|
|
||||||
|
for (auto& child : root.children) {
|
||||||
|
encodeNode(out, child);
|
||||||
|
}
|
||||||
|
encodeNode(out, FBXNode());
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FBXWriter::encodeNode(QDataStream& out, const FBXNode& node) {
|
||||||
|
qDebug() << "Encoding " << node.name;
|
||||||
|
|
||||||
|
auto device = out.device();
|
||||||
|
auto nodeStartPos = device->pos();
|
||||||
|
|
||||||
|
// endOffset (temporary, updated later)
|
||||||
|
out << (qint64)0;
|
||||||
|
|
||||||
|
// Property count
|
||||||
|
out << (quint64)node.properties.size();
|
||||||
|
|
||||||
|
// Property list length (temporary, updated later)
|
||||||
|
out << (quint64)0;
|
||||||
|
|
||||||
|
out << (quint8)node.name.size();
|
||||||
|
out.writeRawData(node.name, node.name.size());
|
||||||
|
|
||||||
|
auto nodePropertiesStartPos = device->pos();
|
||||||
|
|
||||||
|
for (const auto& prop : node.properties) {
|
||||||
|
encodeFBXProperty(out, prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go back and write property list length
|
||||||
|
auto nodePropertiesEndPos = device->pos();
|
||||||
|
device->seek(nodeStartPos + sizeof(qint64) + sizeof(quint64));
|
||||||
|
out << (quint64)(nodePropertiesEndPos - nodePropertiesStartPos);
|
||||||
|
|
||||||
|
device->seek(nodePropertiesEndPos);
|
||||||
|
|
||||||
|
for (auto& child : node.children) {
|
||||||
|
encodeNode(out, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children.length() > 0) {
|
||||||
|
encodeNode(out, FBXNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go back and write actual endOffset
|
||||||
|
auto nodeEndPos = device->pos();
|
||||||
|
device->seek(nodeStartPos);
|
||||||
|
out << (qint64)(nodeEndPos);
|
||||||
|
|
||||||
|
device->seek(nodeEndPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FBXWriter::encodeFBXProperty(QDataStream& out, const QVariant& prop) {
|
||||||
|
auto type = prop.userType();
|
||||||
|
switch (type) {
|
||||||
|
case QVariant::Type::Bool:
|
||||||
|
|
||||||
|
out.device()->write("C", 1);
|
||||||
|
out << prop.toBool();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QMetaType::Int:
|
||||||
|
out.device()->write("I", 1);
|
||||||
|
out << prop.toInt();
|
||||||
|
break;
|
||||||
|
|
||||||
|
encodeNode(out, FBXNode());
|
||||||
|
case QMetaType::Float:
|
||||||
|
out.device()->write("F", 1);
|
||||||
|
out << prop.toFloat();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QMetaType::Double:
|
||||||
|
out.device()->write("D", 1);
|
||||||
|
out << prop.toDouble();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QMetaType::LongLong:
|
||||||
|
out.device()->write("L", 1);
|
||||||
|
out << prop.toLongLong();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QMetaType::QString:
|
||||||
|
{
|
||||||
|
auto bytes = prop.toString().toUtf8();
|
||||||
|
out << 'S';
|
||||||
|
out << bytes.length();
|
||||||
|
out << bytes;
|
||||||
|
out << (int32_t)bytes.size();
|
||||||
|
out.writeRawData(bytes, bytes.size());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case QMetaType::QByteArray:
|
||||||
|
{
|
||||||
|
auto bytes = prop.toByteArray();
|
||||||
|
out.device()->write("S", 1);
|
||||||
|
out << (int32_t)bytes.size();
|
||||||
|
out.writeRawData(bytes, bytes.size());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Delete? Do we ever use QList instead of QVector?
|
||||||
|
case QVariant::Type::List:
|
||||||
|
{
|
||||||
|
auto list = prop.toList();
|
||||||
|
auto listType = prop.userType();
|
||||||
|
|
||||||
|
switch (listType) {
|
||||||
|
case QMetaType::Float:
|
||||||
|
out.device()->write("f", 1);
|
||||||
|
out << (int32_t)list.length();
|
||||||
|
out << (int32_t)0;
|
||||||
|
out << (int32_t)0;
|
||||||
|
for (auto& innerProp : list) {
|
||||||
|
out << innerProp.toFloat();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QMetaType::Double:
|
||||||
|
out.device()->write("d", 1);
|
||||||
|
out << (int32_t)list.length();
|
||||||
|
out << (int32_t)0;
|
||||||
|
out << (int32_t)0;
|
||||||
|
for (auto& innerProp : list) {
|
||||||
|
out << innerProp.toDouble();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QMetaType::LongLong:
|
||||||
|
out.device()->write("l", 1);
|
||||||
|
out << (int32_t)list.length();
|
||||||
|
out << (int32_t)0;
|
||||||
|
out << (int32_t)0;
|
||||||
|
for (auto& innerProp : list) {
|
||||||
|
out << innerProp.toLongLong();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QMetaType::Int:
|
||||||
|
out.device()->write("i", 1);
|
||||||
|
out << (int32_t)list.length();
|
||||||
|
out << (int32_t)0;
|
||||||
|
out << (int32_t)0;
|
||||||
|
for (auto& innerProp : list) {
|
||||||
|
out << innerProp.toInt();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QMetaType::Bool:
|
||||||
|
out.device()->write("b", 1);
|
||||||
|
out << (int32_t)list.length();
|
||||||
|
out << (int32_t)0;
|
||||||
|
out << (int32_t)0;
|
||||||
|
for (auto& innerProp : list) {
|
||||||
|
out << innerProp.toBool();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
if (prop.canConvert<QVector<float>>()) {
|
||||||
|
writeVector(out, 'f', prop.value<QVector<float>>());
|
||||||
|
} else if (prop.canConvert<QVector<double>>()) {
|
||||||
|
writeVector(out, 'd', prop.value<QVector<double>>());
|
||||||
|
} else if (prop.canConvert<QVector<qint64>>()) {
|
||||||
|
writeVector(out, 'l', prop.value<QVector<qint64>>());
|
||||||
|
} else if (prop.canConvert<QVector<qint32>>()) {
|
||||||
|
writeVector(out, 'i', prop.value<QVector<qint32>>());
|
||||||
|
} else if (prop.canConvert<QVector<bool>>()) {
|
||||||
|
writeVector(out, 'b', prop.value<QVector<bool>>());
|
||||||
|
} else {
|
||||||
|
qDebug() << "Unsupported property type in FBXWriter::encodeNode: " << type << prop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
28
libraries/fbx/src/FBXWriter.h
Normal file
28
libraries/fbx/src/FBXWriter.h
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// FBXWriter.h
|
||||||
|
// libraries/fbx/src
|
||||||
|
//
|
||||||
|
// Created by Ryan Huffman on 9/5/17.
|
||||||
|
// Copyright 2017 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef hifi_FBXWriter_h
|
||||||
|
#define hifi_FBXWriter_h
|
||||||
|
|
||||||
|
#include "FBX.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QDataStream>
|
||||||
|
|
||||||
|
class FBXWriter {
|
||||||
|
public:
|
||||||
|
static QByteArray encodeFBX(const FBXNode& root);
|
||||||
|
|
||||||
|
static void encodeNode(QDataStream& out, const FBXNode& node);
|
||||||
|
static void encodeFBXProperty(QDataStream& out, const QVariant& property);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // hifi_FBXWriter_h
|
|
@ -2,7 +2,7 @@ set(TARGET_NAME oven)
|
||||||
|
|
||||||
setup_hifi_project(Widgets Gui Concurrent)
|
setup_hifi_project(Widgets Gui Concurrent)
|
||||||
|
|
||||||
link_hifi_libraries(networking shared image gpu ktx fbx baking)
|
link_hifi_libraries(networking shared image gpu ktx fbx baking model)
|
||||||
|
|
||||||
setup_memory_debugger()
|
setup_memory_debugger()
|
||||||
|
|
||||||
|
@ -17,16 +17,4 @@ if (UNIX)
|
||||||
endif()
|
endif()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
# try to find the FBX SDK but fail silently if we don't
|
|
||||||
# because this tool is not built by default
|
|
||||||
find_package(FBX)
|
|
||||||
if (FBX_FOUND)
|
|
||||||
if (CMAKE_THREAD_LIBS_INIT)
|
|
||||||
target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES} "${CMAKE_THREAD_LIBS_INIT}")
|
|
||||||
else ()
|
|
||||||
target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES})
|
|
||||||
endif ()
|
|
||||||
target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${FBX_INCLUDE_DIR})
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE)
|
set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE)
|
||||||
|
|
|
@ -42,7 +42,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString outputPath) {
|
||||||
// create our appropiate baker
|
// create our appropiate baker
|
||||||
if (isFBX) {
|
if (isFBX) {
|
||||||
_baker = std::unique_ptr<Baker> { new FBXBaker(inputUrl, []() -> QThread* { return qApp->getNextWorkerThread(); }, outputPath) };
|
_baker = std::unique_ptr<Baker> { new FBXBaker(inputUrl, []() -> QThread* { return qApp->getNextWorkerThread(); }, outputPath) };
|
||||||
_baker->moveToThread(qApp->getFBXBakerThread());
|
_baker->moveToThread(qApp->getNextWorkerThread());
|
||||||
} else if (isSupportedImage) {
|
} else if (isSupportedImage) {
|
||||||
_baker = std::unique_ptr<Baker> { new TextureBaker(inputUrl, image::TextureUsage::CUBE_TEXTURE, outputPath) };
|
_baker = std::unique_ptr<Baker> { new TextureBaker(inputUrl, image::TextureUsage::CUBE_TEXTURE, outputPath) };
|
||||||
_baker->moveToThread(qApp->getNextWorkerThread());
|
_baker->moveToThread(qApp->getNextWorkerThread());
|
||||||
|
@ -61,4 +61,4 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString outputPath) {
|
||||||
void BakerCLI::handleFinishedBaker() {
|
void BakerCLI::handleFinishedBaker() {
|
||||||
qCDebug(model_baking) << "Finished baking file.";
|
qCDebug(model_baking) << "Finished baking file.";
|
||||||
QApplication::exit(_baker.get()->hasErrors());
|
QApplication::exit(_baker.get()->hasErrors());
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,7 +214,7 @@ void DomainBaker::enumerateEntities() {
|
||||||
|
|
||||||
// move the baker to the baker thread
|
// move the baker to the baker thread
|
||||||
// and kickoff the bake
|
// and kickoff the bake
|
||||||
baker->moveToThread(qApp->getFBXBakerThread());
|
baker->moveToThread(qApp->getNextWorkerThread());
|
||||||
QMetaObject::invokeMethod(baker.data(), "bake");
|
QMetaObject::invokeMethod(baker.data(), "bake");
|
||||||
|
|
||||||
// keep track of the total number of baking entities
|
// keep track of the total number of baking entities
|
||||||
|
|
|
@ -1,568 +0,0 @@
|
||||||
//
|
|
||||||
// FBXBaker.cpp
|
|
||||||
// tools/oven/src
|
|
||||||
//
|
|
||||||
// Created by Stephen Birarda on 3/30/17.
|
|
||||||
// Copyright 2017 High Fidelity, Inc.
|
|
||||||
//
|
|
||||||
// Distributed under the Apache License, Version 2.0.
|
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
||||||
//
|
|
||||||
|
|
||||||
#include <cmath> // need this include so we don't get an error looking for std::isnan
|
|
||||||
|
|
||||||
#include <fbxsdk.h>
|
|
||||||
|
|
||||||
#include <QtConcurrent>
|
|
||||||
#include <QtCore/QCoreApplication>
|
|
||||||
#include <QtCore/QDir>
|
|
||||||
#include <QtCore/QEventLoop>
|
|
||||||
#include <QtCore/QFileInfo>
|
|
||||||
#include <QtCore/QThread>
|
|
||||||
|
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
#include <NetworkAccessManager.h>
|
|
||||||
#include <SharedUtil.h>
|
|
||||||
|
|
||||||
#include <PathUtils.h>
|
|
||||||
|
|
||||||
#include "ModelBakingLoggingCategory.h"
|
|
||||||
#include "TextureBaker.h"
|
|
||||||
|
|
||||||
#include "FBXBaker.h"
|
|
||||||
|
|
||||||
std::once_flag onceFlag;
|
|
||||||
FBXSDKManagerUniquePointer FBXBaker::_sdkManager { nullptr };
|
|
||||||
|
|
||||||
FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
|
|
||||||
const QString& bakedOutputDir, const QString& originalOutputDir) :
|
|
||||||
_fbxURL(fbxURL),
|
|
||||||
_bakedOutputDir(bakedOutputDir),
|
|
||||||
_originalOutputDir(originalOutputDir),
|
|
||||||
_textureThreadGetter(textureThreadGetter)
|
|
||||||
{
|
|
||||||
std::call_once(onceFlag, [](){
|
|
||||||
// create the static FBX SDK manager
|
|
||||||
_sdkManager = FBXSDKManagerUniquePointer(FbxManager::Create(), [](FbxManager* manager){
|
|
||||||
manager->Destroy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void FBXBaker::bake() {
|
|
||||||
auto tempDir = PathUtils::generateTemporaryDir();
|
|
||||||
|
|
||||||
if (tempDir.isEmpty()) {
|
|
||||||
handleError("Failed to create a temporary directory.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_tempDir = tempDir;
|
|
||||||
|
|
||||||
_originalFBXFilePath = _tempDir.filePath(_fbxURL.fileName());
|
|
||||||
qDebug() << "Made temporary dir " << _tempDir;
|
|
||||||
qDebug() << "Origin file path: " << _originalFBXFilePath;
|
|
||||||
|
|
||||||
// setup the output folder for the results of this bake
|
|
||||||
setupOutputFolder();
|
|
||||||
|
|
||||||
if (hasErrors()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
connect(this, &FBXBaker::sourceCopyReadyToLoad, this, &FBXBaker::bakeSourceCopy);
|
|
||||||
|
|
||||||
// make a local copy of the FBX file
|
|
||||||
loadSourceFBX();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FBXBaker::bakeSourceCopy() {
|
|
||||||
// load the scene from the FBX file
|
|
||||||
importScene();
|
|
||||||
|
|
||||||
if (hasErrors()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// enumerate the textures found in the scene and start a bake for them
|
|
||||||
rewriteAndBakeSceneTextures();
|
|
||||||
|
|
||||||
if (hasErrors()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// export the FBX with re-written texture references
|
|
||||||
exportScene();
|
|
||||||
|
|
||||||
if (hasErrors()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if we're already done with textures (in case we had none to re-write)
|
|
||||||
checkIfTexturesFinished();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FBXBaker::setupOutputFolder() {
|
|
||||||
// make sure there isn't already an output directory using the same name
|
|
||||||
int iteration = 0;
|
|
||||||
|
|
||||||
if (QDir(_bakedOutputDir).exists()) {
|
|
||||||
qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing.";
|
|
||||||
//_bakedOutputDir = _baseOutputPath + "/" + _fbxName + "-" + QString::number(++iteration) + "/";
|
|
||||||
} else {
|
|
||||||
qCDebug(model_baking) << "Creating FBX output folder" << _bakedOutputDir;
|
|
||||||
|
|
||||||
// attempt to make the output folder
|
|
||||||
if (!QDir().mkpath(_bakedOutputDir)) {
|
|
||||||
handleError("Failed to create FBX output folder " + _bakedOutputDir);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// attempt to make the output folder
|
|
||||||
if (!QDir().mkpath(_originalOutputDir)) {
|
|
||||||
handleError("Failed to create FBX output folder " + _bakedOutputDir);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FBXBaker::loadSourceFBX() {
|
|
||||||
// check if the FBX is local or first needs to be downloaded
|
|
||||||
if (_fbxURL.isLocalFile()) {
|
|
||||||
// load up the local file
|
|
||||||
QFile localFBX { _fbxURL.toLocalFile() };
|
|
||||||
|
|
||||||
qDebug() << "Local file url: " << _fbxURL << _fbxURL.toString() << _fbxURL.toLocalFile() << ", copying to: " << _originalFBXFilePath;
|
|
||||||
|
|
||||||
if (!localFBX.exists()) {
|
|
||||||
//QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), "");
|
|
||||||
handleError("Could not find " + _fbxURL.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// make a copy in the output folder
|
|
||||||
if (!_originalOutputDir.isEmpty()) {
|
|
||||||
qDebug() << "Copying to: " << _originalOutputDir << "/" << _fbxURL.fileName();
|
|
||||||
localFBX.copy(_originalOutputDir + "/" + _fbxURL.fileName());
|
|
||||||
}
|
|
||||||
|
|
||||||
localFBX.copy(_originalFBXFilePath);
|
|
||||||
|
|
||||||
// emit our signal to start the import of the FBX source copy
|
|
||||||
emit sourceCopyReadyToLoad();
|
|
||||||
} else {
|
|
||||||
// remote file, kick off a download
|
|
||||||
auto& networkAccessManager = NetworkAccessManager::getInstance();
|
|
||||||
|
|
||||||
QNetworkRequest networkRequest;
|
|
||||||
|
|
||||||
// setup the request to follow re-directs and always hit the network
|
|
||||||
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
|
||||||
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
|
|
||||||
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
|
|
||||||
|
|
||||||
|
|
||||||
networkRequest.setUrl(_fbxURL);
|
|
||||||
|
|
||||||
qCDebug(model_baking) << "Downloading" << _fbxURL;
|
|
||||||
auto networkReply = networkAccessManager.get(networkRequest);
|
|
||||||
|
|
||||||
connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FBXBaker::handleFBXNetworkReply() {
|
|
||||||
auto requestReply = qobject_cast<QNetworkReply*>(sender());
|
|
||||||
|
|
||||||
if (requestReply->error() == QNetworkReply::NoError) {
|
|
||||||
qCDebug(model_baking) << "Downloaded" << _fbxURL;
|
|
||||||
|
|
||||||
// grab the contents of the reply and make a copy in the output folder
|
|
||||||
QFile copyOfOriginal(_originalFBXFilePath);
|
|
||||||
|
|
||||||
qDebug(model_baking) << "Writing copy of original FBX to" << _originalFBXFilePath << copyOfOriginal.fileName();
|
|
||||||
|
|
||||||
if (!copyOfOriginal.open(QIODevice::WriteOnly)) {
|
|
||||||
// add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made
|
|
||||||
handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to open " + _originalFBXFilePath + ")");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (copyOfOriginal.write(requestReply->readAll()) == -1) {
|
|
||||||
handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to write)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// close that file now that we are done writing to it
|
|
||||||
copyOfOriginal.close();
|
|
||||||
|
|
||||||
if (!_originalOutputDir.isEmpty()) {
|
|
||||||
copyOfOriginal.copy(_originalOutputDir + "/" + _fbxURL.fileName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// emit our signal to start the import of the FBX source copy
|
|
||||||
emit sourceCopyReadyToLoad();
|
|
||||||
} else {
|
|
||||||
// add an error to our list stating that the FBX could not be downloaded
|
|
||||||
handleError("Failed to download " + _fbxURL.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FBXBaker::importScene() {
|
|
||||||
// create an FBX SDK importer
|
|
||||||
FbxImporter* importer = FbxImporter::Create(_sdkManager.get(), "");
|
|
||||||
|
|
||||||
qDebug() << "file path: " << _originalFBXFilePath.toLocal8Bit().data() << QDir(_originalFBXFilePath).exists();
|
|
||||||
// import the copy of the original FBX file
|
|
||||||
bool importStatus = importer->Initialize(_originalFBXFilePath.toLocal8Bit().data());
|
|
||||||
|
|
||||||
if (!importStatus) {
|
|
||||||
// failed to initialize importer, print an error and return
|
|
||||||
handleError("Failed to import " + _fbxURL.toString() + " - " + importer->GetStatus().GetErrorString());
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
qCDebug(model_baking) << "Imported" << _fbxURL << "to FbxScene";
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup a new scene to hold the imported file
|
|
||||||
_scene = FbxScene::Create(_sdkManager.get(), "bakeScene");
|
|
||||||
|
|
||||||
// import the file to the created scene
|
|
||||||
importer->Import(_scene);
|
|
||||||
|
|
||||||
// destroy the importer that is no longer needed
|
|
||||||
importer->Destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) {
|
|
||||||
auto fbxPath = fbxURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
|
|
||||||
auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
|
|
||||||
|
|
||||||
if (texturePath.startsWith(fbxPath)) {
|
|
||||||
// texture path is a child of the FBX path, return the texture path without the fbx path
|
|
||||||
return texturePath.mid(fbxPath.length());
|
|
||||||
} else {
|
|
||||||
// the texture path was not a child of the FBX path, return the empty string
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) {
|
|
||||||
// first make sure we have a unique base name for this texture
|
|
||||||
// in case another texture referenced by this model has the same base name
|
|
||||||
auto nameMatches = _textureNameMatchCount[textureFileInfo.baseName()];
|
|
||||||
|
|
||||||
QString bakedTextureFileName { textureFileInfo.completeBaseName() };
|
|
||||||
|
|
||||||
if (nameMatches > 0) {
|
|
||||||
// there are already nameMatches texture with this name
|
|
||||||
// append - and that number to our baked texture file name so that it is unique
|
|
||||||
bakedTextureFileName += "-" + QString::number(nameMatches);
|
|
||||||
}
|
|
||||||
|
|
||||||
bakedTextureFileName += BAKED_TEXTURE_EXT;
|
|
||||||
|
|
||||||
// increment the number of name matches
|
|
||||||
++nameMatches;
|
|
||||||
|
|
||||||
return bakedTextureFileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* fileTexture) {
|
|
||||||
QUrl urlToTexture;
|
|
||||||
|
|
||||||
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
|
|
||||||
// set the texture URL to the local texture that we have confirmed exists
|
|
||||||
urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath());
|
|
||||||
} else {
|
|
||||||
// external texture that we'll need to download or find
|
|
||||||
|
|
||||||
// first check if it the RelativePath to the texture in the FBX was relative
|
|
||||||
QString relativeFileName = fileTexture->GetRelativeFileName();
|
|
||||||
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
|
|
||||||
|
|
||||||
// this is a relative file path which will require different handling
|
|
||||||
// depending on the location of the original FBX
|
|
||||||
if (_fbxURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) {
|
|
||||||
// the absolute path we ran into for the texture in the FBX exists on this machine
|
|
||||||
// so use that file
|
|
||||||
urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath());
|
|
||||||
} else {
|
|
||||||
// we didn't find the texture on this machine at the absolute path
|
|
||||||
// so assume that it is right beside the FBX to match the behaviour of interface
|
|
||||||
urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return urlToTexture;
|
|
||||||
}
|
|
||||||
|
|
||||||
image::TextureUsage::Type textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMaterial* material) {
|
|
||||||
using namespace image::TextureUsage;
|
|
||||||
|
|
||||||
// this is a property we know has a texture, we need to match it to a High Fidelity known texture type
|
|
||||||
// since that information is passed to the baking process
|
|
||||||
|
|
||||||
// grab the hierarchical name for this property and lowercase it for case-insensitive compare
|
|
||||||
auto propertyName = QString(property.GetHierarchicalName()).toLower();
|
|
||||||
|
|
||||||
// figure out the type of the property based on what known value string it matches
|
|
||||||
if ((propertyName.contains("diffuse") && !propertyName.contains("tex_global_diffuse"))
|
|
||||||
|| propertyName.contains("tex_color_map")) {
|
|
||||||
return ALBEDO_TEXTURE;
|
|
||||||
} else if (propertyName.contains("transparentcolor") || propertyName.contains("transparencyfactor")) {
|
|
||||||
return ALBEDO_TEXTURE;
|
|
||||||
} else if (propertyName.contains("bump")) {
|
|
||||||
return BUMP_TEXTURE;
|
|
||||||
} else if (propertyName.contains("normal")) {
|
|
||||||
return NORMAL_TEXTURE;
|
|
||||||
} else if ((propertyName.contains("specular") && !propertyName.contains("tex_global_specular"))
|
|
||||||
|| propertyName.contains("reflection")) {
|
|
||||||
return SPECULAR_TEXTURE;
|
|
||||||
} else if (propertyName.contains("tex_metallic_map")) {
|
|
||||||
return METALLIC_TEXTURE;
|
|
||||||
} else if (propertyName.contains("shininess")) {
|
|
||||||
return GLOSS_TEXTURE;
|
|
||||||
} else if (propertyName.contains("tex_roughness_map")) {
|
|
||||||
return ROUGHNESS_TEXTURE;
|
|
||||||
} else if (propertyName.contains("emissive")) {
|
|
||||||
return EMISSIVE_TEXTURE;
|
|
||||||
} else if (propertyName.contains("ambientcolor")) {
|
|
||||||
return LIGHTMAP_TEXTURE;
|
|
||||||
} else if (propertyName.contains("ambientfactor")) {
|
|
||||||
// we need to check what the ambient factor is, since that tells Interface to process this texture
|
|
||||||
// either as an occlusion texture or a light map
|
|
||||||
auto lambertMaterial = FbxCast<FbxSurfaceLambert>(material);
|
|
||||||
|
|
||||||
if (lambertMaterial->AmbientFactor == 0) {
|
|
||||||
return LIGHTMAP_TEXTURE;
|
|
||||||
} else if (lambertMaterial->AmbientFactor > 0) {
|
|
||||||
return OCCLUSION_TEXTURE;
|
|
||||||
} else {
|
|
||||||
return UNUSED_TEXTURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (propertyName.contains("tex_ao_map")) {
|
|
||||||
return OCCLUSION_TEXTURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return UNUSED_TEXTURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FBXBaker::rewriteAndBakeSceneTextures() {
|
|
||||||
|
|
||||||
// enumerate the surface materials to find the textures used in the scene
|
|
||||||
int numMaterials = _scene->GetMaterialCount();
|
|
||||||
for (int i = 0; i < numMaterials; i++) {
|
|
||||||
FbxSurfaceMaterial* material = _scene->GetMaterial(i);
|
|
||||||
|
|
||||||
if (material) {
|
|
||||||
// enumerate the properties of this material to see what texture channels it might have
|
|
||||||
FbxProperty property = material->GetFirstProperty();
|
|
||||||
|
|
||||||
while (property.IsValid()) {
|
|
||||||
// first check if this property has connected textures, if not we don't need to bother with it here
|
|
||||||
if (property.GetSrcObjectCount<FbxTexture>() > 0) {
|
|
||||||
|
|
||||||
// figure out the type of texture from the material property
|
|
||||||
auto textureType = textureTypeForMaterialProperty(property, material);
|
|
||||||
|
|
||||||
if (textureType != image::TextureUsage::UNUSED_TEXTURE) {
|
|
||||||
int numTextures = property.GetSrcObjectCount<FbxFileTexture>();
|
|
||||||
|
|
||||||
for (int j = 0; j < numTextures; j++) {
|
|
||||||
FbxFileTexture* fileTexture = property.GetSrcObject<FbxFileTexture>(j);
|
|
||||||
|
|
||||||
// use QFileInfo to easily split up the existing texture filename into its components
|
|
||||||
QString fbxTextureFileName { fileTexture->GetFileName() };
|
|
||||||
QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") };
|
|
||||||
|
|
||||||
// make sure this texture points to something and isn't one we've already re-mapped
|
|
||||||
if (!textureFileInfo.filePath().isEmpty()
|
|
||||||
&& textureFileInfo.suffix() != BAKED_TEXTURE_EXT.mid(1)) {
|
|
||||||
|
|
||||||
// construct the new baked texture file name and file path
|
|
||||||
// ensuring that the baked texture will have a unique name
|
|
||||||
// even if there was another texture with the same name at a different path
|
|
||||||
auto bakedTextureFileName = createBakedTextureFileName(textureFileInfo);
|
|
||||||
QString bakedTextureFilePath {
|
|
||||||
_bakedOutputDir + "/" + bakedTextureFileName
|
|
||||||
};
|
|
||||||
_outputFiles.push_back(bakedTextureFilePath);
|
|
||||||
|
|
||||||
qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName()
|
|
||||||
<< "to" << bakedTextureFilePath;
|
|
||||||
|
|
||||||
// figure out the URL to this texture, embedded or external
|
|
||||||
auto urlToTexture = getTextureURL(textureFileInfo, fileTexture);
|
|
||||||
|
|
||||||
// write the new filename into the FBX scene
|
|
||||||
fileTexture->SetFileName(bakedTextureFilePath.toUtf8().data());
|
|
||||||
|
|
||||||
// write the relative filename to be the baked texture file name since it will
|
|
||||||
// be right beside the FBX
|
|
||||||
fileTexture->SetRelativeFileName(bakedTextureFileName.toLocal8Bit().constData());
|
|
||||||
|
|
||||||
if (!_bakingTextures.contains(urlToTexture)) {
|
|
||||||
// bake this texture asynchronously
|
|
||||||
bakeTexture(urlToTexture, textureType, _bakedOutputDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property = material->GetNextProperty(property);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir) {
|
|
||||||
// start a bake for this texture and add it to our list to keep track of
|
|
||||||
QSharedPointer<TextureBaker> bakingTexture {
|
|
||||||
new TextureBaker(textureURL, textureType, outputDir),
|
|
||||||
&TextureBaker::deleteLater
|
|
||||||
};
|
|
||||||
|
|
||||||
// make sure we hear when the baking texture is done
|
|
||||||
connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture);
|
|
||||||
|
|
||||||
// keep a shared pointer to the baking texture
|
|
||||||
_bakingTextures.insert(textureURL, bakingTexture);
|
|
||||||
|
|
||||||
// start baking the texture on one of our available worker threads
|
|
||||||
bakingTexture->moveToThread(_textureThreadGetter());
|
|
||||||
QMetaObject::invokeMethod(bakingTexture.data(), "bake");
|
|
||||||
}
|
|
||||||
|
|
||||||
void FBXBaker::handleBakedTexture() {
|
|
||||||
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
|
|
||||||
|
|
||||||
// make sure we haven't already run into errors, and that this is a valid texture
|
|
||||||
if (bakedTexture) {
|
|
||||||
if (!hasErrors()) {
|
|
||||||
if (!bakedTexture->hasErrors()) {
|
|
||||||
if (!_originalOutputDir.isEmpty()) {
|
|
||||||
// we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture
|
|
||||||
|
|
||||||
// use the path to the texture being baked to determine if this was an embedded or a linked texture
|
|
||||||
|
|
||||||
// it is embeddded if the texure being baked was inside the original output folder
|
|
||||||
// since that is where the FBX SDK places the .fbm folder it generates when importing the FBX
|
|
||||||
|
|
||||||
auto originalOutputFolder = QUrl::fromLocalFile(_originalOutputDir);
|
|
||||||
|
|
||||||
if (!originalOutputFolder.isParentOf(bakedTexture->getTextureURL())) {
|
|
||||||
// for linked textures we want to save a copy of original texture beside the original FBX
|
|
||||||
|
|
||||||
qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL();
|
|
||||||
|
|
||||||
// check if we have a relative path to use for the texture
|
|
||||||
auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL());
|
|
||||||
|
|
||||||
QFile originalTextureFile {
|
|
||||||
_originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (relativeTexturePath.length() > 0) {
|
|
||||||
// make the folders needed by the relative path
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
|
|
||||||
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
|
|
||||||
<< "for" << _fbxURL;
|
|
||||||
} else {
|
|
||||||
handleError("Could not save original external texture " + originalTextureFile.fileName()
|
|
||||||
+ " for " + _fbxURL.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// now that this texture has been baked and handled, we can remove that TextureBaker from our hash
|
|
||||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
|
||||||
|
|
||||||
checkIfTexturesFinished();
|
|
||||||
} else {
|
|
||||||
// there was an error baking this texture - add it to our list of errors
|
|
||||||
_errorList.append(bakedTexture->getErrors());
|
|
||||||
|
|
||||||
// we don't emit finished yet so that the other textures can finish baking first
|
|
||||||
_pendingErrorEmission = true;
|
|
||||||
|
|
||||||
// now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list
|
|
||||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
|
||||||
|
|
||||||
checkIfTexturesFinished();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// we have errors to attend to, so we don't do extra processing for this texture
|
|
||||||
// but we do need to remove that TextureBaker from our list
|
|
||||||
// and then check if we're done with all textures
|
|
||||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
|
||||||
|
|
||||||
checkIfTexturesFinished();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FBXBaker::exportScene() {
|
|
||||||
// setup the exporter
|
|
||||||
FbxExporter* exporter = FbxExporter::Create(_sdkManager.get(), "");
|
|
||||||
|
|
||||||
// save the relative path to this FBX inside our passed output folder
|
|
||||||
|
|
||||||
auto fileName = _fbxURL.fileName();
|
|
||||||
auto baseName = fileName.left(fileName.lastIndexOf('.'));
|
|
||||||
auto bakedFilename = baseName + BAKED_FBX_EXTENSION;
|
|
||||||
|
|
||||||
_bakedFBXFilePath = _bakedOutputDir + "/" + bakedFilename;
|
|
||||||
|
|
||||||
bool exportStatus = exporter->Initialize(_bakedFBXFilePath.toLocal8Bit().data());
|
|
||||||
|
|
||||||
if (!exportStatus) {
|
|
||||||
// failed to initialize exporter, print an error and return
|
|
||||||
handleError("Failed to export FBX file at " + _fbxURL.toString() + " to " + _bakedFBXFilePath
|
|
||||||
+ "- error: " + exporter->GetStatus().GetErrorString());
|
|
||||||
}
|
|
||||||
|
|
||||||
_outputFiles.push_back(_bakedFBXFilePath);
|
|
||||||
|
|
||||||
// export the scene
|
|
||||||
exporter->Export(_scene);
|
|
||||||
|
|
||||||
qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void FBXBaker::removeEmbeddedMediaFolder() {
|
|
||||||
// now that the bake is complete, remove the embedded media folder produced by the FBX SDK when it imports an FBX
|
|
||||||
//auto embeddedMediaFolderName = _fbxURL.fileName().replace(".fbx", ".fbm");
|
|
||||||
//QDir(_bakedOutputDir + ORIGINAL_OUTPUT_SUBFOLDER + embeddedMediaFolderName).removeRecursively();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FBXBaker::checkIfTexturesFinished() {
|
|
||||||
// check if we're done everything we need to do for this FBX
|
|
||||||
// and emit our finished signal if we're done
|
|
||||||
|
|
||||||
if (_bakingTextures.isEmpty()) {
|
|
||||||
// remove the embedded media folder that the FBX SDK produces when reading the original
|
|
||||||
removeEmbeddedMediaFolder();
|
|
||||||
|
|
||||||
if (hasErrors()) {
|
|
||||||
// if we're checking for completion but we have errors
|
|
||||||
// that means one or more of our texture baking operations failed
|
|
||||||
|
|
||||||
if (_pendingErrorEmission) {
|
|
||||||
emit finished();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
qCDebug(model_baking) << "Finished baking" << _fbxURL;
|
|
||||||
|
|
||||||
emit finished();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
//
|
|
||||||
// FBXBaker.h
|
|
||||||
// tools/oven/src
|
|
||||||
//
|
|
||||||
// Created by Stephen Birarda on 3/30/17.
|
|
||||||
// Copyright 2017 High Fidelity, Inc.
|
|
||||||
//
|
|
||||||
// Distributed under the Apache License, Version 2.0.
|
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef hifi_FBXBaker_h
|
|
||||||
#define hifi_FBXBaker_h
|
|
||||||
|
|
||||||
#include <QtCore/QFutureSynchronizer>
|
|
||||||
#include <QtCore/QDir>
|
|
||||||
#include <QtCore/QUrl>
|
|
||||||
#include <QtNetwork/QNetworkReply>
|
|
||||||
|
|
||||||
#include "Baker.h"
|
|
||||||
#include "TextureBaker.h"
|
|
||||||
|
|
||||||
#include <gpu/Texture.h>
|
|
||||||
|
|
||||||
namespace fbxsdk {
|
|
||||||
class FbxManager;
|
|
||||||
class FbxProperty;
|
|
||||||
class FbxScene;
|
|
||||||
class FbxFileTexture;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const QString BAKED_FBX_EXTENSION = ".baked.fbx";
|
|
||||||
using FBXSDKManagerUniquePointer = std::unique_ptr<fbxsdk::FbxManager, std::function<void (fbxsdk::FbxManager *)>>;
|
|
||||||
|
|
||||||
using TextureBakerThreadGetter = std::function<QThread*()>;
|
|
||||||
|
|
||||||
class FBXBaker : public Baker {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
|
|
||||||
const QString& bakedOutputDir, const QString& originalOutputDir = "");
|
|
||||||
|
|
||||||
QUrl getFBXUrl() const { return _fbxURL; }
|
|
||||||
QString getBakedFBXFilePath() const { return _bakedFBXFilePath; }
|
|
||||||
std::vector<QString> getOutputFiles() const { return _outputFiles; }
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
// all calls to FBXBaker::bake for FBXBaker instances must be from the same thread
|
|
||||||
// because the Autodesk SDK will cause a crash if it is called from multiple threads
|
|
||||||
virtual void bake() override;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void sourceCopyReadyToLoad();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void bakeSourceCopy();
|
|
||||||
void handleFBXNetworkReply();
|
|
||||||
void handleBakedTexture();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void setupOutputFolder();
|
|
||||||
|
|
||||||
void loadSourceFBX();
|
|
||||||
|
|
||||||
void importScene();
|
|
||||||
void rewriteAndBakeSceneTextures();
|
|
||||||
void exportScene();
|
|
||||||
void removeEmbeddedMediaFolder();
|
|
||||||
|
|
||||||
void checkIfTexturesFinished();
|
|
||||||
|
|
||||||
QString createBakedTextureFileName(const QFileInfo& textureFileInfo);
|
|
||||||
QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture);
|
|
||||||
|
|
||||||
void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir);
|
|
||||||
|
|
||||||
QUrl _fbxURL;
|
|
||||||
|
|
||||||
QString _bakedFBXFilePath;
|
|
||||||
|
|
||||||
QString _bakedOutputDir;
|
|
||||||
|
|
||||||
// If set, the original FBX and textures will also be copied here
|
|
||||||
QString _originalOutputDir;
|
|
||||||
|
|
||||||
QDir _tempDir;
|
|
||||||
QString _originalFBXFilePath;
|
|
||||||
|
|
||||||
// List of baked output files, includes the FBX and textures
|
|
||||||
std::vector<QString> _outputFiles;
|
|
||||||
|
|
||||||
static FBXSDKManagerUniquePointer _sdkManager;
|
|
||||||
fbxsdk::FbxScene* _scene { nullptr };
|
|
||||||
|
|
||||||
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
|
|
||||||
QHash<QString, int> _textureNameMatchCount;
|
|
||||||
|
|
||||||
TextureBakerThreadGetter _textureThreadGetter;
|
|
||||||
|
|
||||||
bool _pendingErrorEmission { false };
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // hifi_FBXBaker_h
|
|
|
@ -51,11 +51,7 @@ Oven::Oven(int argc, char* argv[]) :
|
||||||
image::setCubeTexturesCompressionEnabled(true);
|
image::setCubeTexturesCompressionEnabled(true);
|
||||||
|
|
||||||
// setup our worker threads
|
// setup our worker threads
|
||||||
setupWorkerThreads(QThread::idealThreadCount() - 1);
|
setupWorkerThreads(QThread::idealThreadCount());
|
||||||
|
|
||||||
// Autodesk's SDK means that we need a single thread for all FBX importing/exporting in the same process
|
|
||||||
// setup the FBX Baker thread
|
|
||||||
setupFBXBakerThread();
|
|
||||||
|
|
||||||
// check if we were passed any command line arguments that would tell us just to run without the GUI
|
// check if we were passed any command line arguments that would tell us just to run without the GUI
|
||||||
if (parser.isSet(CLI_INPUT_PARAMETER) || parser.isSet(CLI_OUTPUT_PARAMETER)) {
|
if (parser.isSet(CLI_INPUT_PARAMETER) || parser.isSet(CLI_OUTPUT_PARAMETER)) {
|
||||||
|
@ -81,10 +77,6 @@ Oven::~Oven() {
|
||||||
_workerThreads[i]->quit();
|
_workerThreads[i]->quit();
|
||||||
_workerThreads[i]->wait();
|
_workerThreads[i]->wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup the FBX Baker thread
|
|
||||||
_fbxBakerThread->quit();
|
|
||||||
_fbxBakerThread->wait();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Oven::setupWorkerThreads(int numWorkerThreads) {
|
void Oven::setupWorkerThreads(int numWorkerThreads) {
|
||||||
|
@ -97,22 +89,6 @@ void Oven::setupWorkerThreads(int numWorkerThreads) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Oven::setupFBXBakerThread() {
|
|
||||||
// we're being asked for the FBX baker thread, but we don't have one yet
|
|
||||||
// so set that up now
|
|
||||||
_fbxBakerThread = new QThread(this);
|
|
||||||
_fbxBakerThread->setObjectName("Oven FBX Baker Thread");
|
|
||||||
}
|
|
||||||
|
|
||||||
QThread* Oven::getFBXBakerThread() {
|
|
||||||
if (!_fbxBakerThread->isRunning()) {
|
|
||||||
// start the FBX baker thread if it isn't running yet
|
|
||||||
_fbxBakerThread->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _fbxBakerThread;
|
|
||||||
}
|
|
||||||
|
|
||||||
QThread* Oven::getNextWorkerThread() {
|
QThread* Oven::getNextWorkerThread() {
|
||||||
// Here we replicate some of the functionality of QThreadPool by giving callers an available worker thread to use.
|
// Here we replicate some of the functionality of QThreadPool by giving callers an available worker thread to use.
|
||||||
// We can't use QThreadPool because we want to put QObjects with signals/slots on these threads.
|
// We can't use QThreadPool because we want to put QObjects with signals/slots on these threads.
|
||||||
|
|
|
@ -34,7 +34,6 @@ public:
|
||||||
|
|
||||||
OvenMainWindow* getMainWindow() const { return _mainWindow; }
|
OvenMainWindow* getMainWindow() const { return _mainWindow; }
|
||||||
|
|
||||||
QThread* getFBXBakerThread();
|
|
||||||
QThread* getNextWorkerThread();
|
QThread* getNextWorkerThread();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -42,7 +41,6 @@ private:
|
||||||
void setupFBXBakerThread();
|
void setupFBXBakerThread();
|
||||||
|
|
||||||
OvenMainWindow* _mainWindow;
|
OvenMainWindow* _mainWindow;
|
||||||
QThread* _fbxBakerThread;
|
|
||||||
QList<QThread*> _workerThreads;
|
QList<QThread*> _workerThreads;
|
||||||
|
|
||||||
std::atomic<uint> _nextWorkerThreadIndex;
|
std::atomic<uint> _nextWorkerThreadIndex;
|
||||||
|
|
|
@ -1,131 +0,0 @@
|
||||||
//
|
|
||||||
// TextureBaker.cpp
|
|
||||||
// tools/oven/src
|
|
||||||
//
|
|
||||||
// Created by Stephen Birarda on 4/5/17.
|
|
||||||
// Copyright 2017 High Fidelity, Inc.
|
|
||||||
//
|
|
||||||
// Distributed under the Apache License, Version 2.0.
|
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
||||||
//
|
|
||||||
|
|
||||||
#include <QtCore/QDir>
|
|
||||||
#include <QtCore/QEventLoop>
|
|
||||||
#include <QtCore/QFile>
|
|
||||||
#include <QtNetwork/QNetworkReply>
|
|
||||||
|
|
||||||
#include <image/Image.h>
|
|
||||||
#include <ktx/KTX.h>
|
|
||||||
#include <NetworkAccessManager.h>
|
|
||||||
#include <SharedUtil.h>
|
|
||||||
|
|
||||||
#include "ModelBakingLoggingCategory.h"
|
|
||||||
|
|
||||||
#include "TextureBaker.h"
|
|
||||||
|
|
||||||
const QString BAKED_TEXTURE_EXT = ".ktx";
|
|
||||||
|
|
||||||
TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory) :
|
|
||||||
_textureURL(textureURL),
|
|
||||||
_textureType(textureType),
|
|
||||||
_outputDirectory(outputDirectory)
|
|
||||||
{
|
|
||||||
// figure out the baked texture filename
|
|
||||||
auto originalFilename = textureURL.fileName();
|
|
||||||
_bakedTextureFileName = originalFilename.left(originalFilename.lastIndexOf('.')) + BAKED_TEXTURE_EXT;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureBaker::bake() {
|
|
||||||
// once our texture is loaded, kick off a the processing
|
|
||||||
connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture);
|
|
||||||
|
|
||||||
// first load the texture (either locally or remotely)
|
|
||||||
loadTexture();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureBaker::loadTexture() {
|
|
||||||
// check if the texture is local or first needs to be downloaded
|
|
||||||
if (_textureURL.isLocalFile()) {
|
|
||||||
// load up the local file
|
|
||||||
QFile localTexture { _textureURL.toLocalFile() };
|
|
||||||
|
|
||||||
if (!localTexture.open(QIODevice::ReadOnly)) {
|
|
||||||
handleError("Unable to open texture " + _textureURL.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_originalTexture = localTexture.readAll();
|
|
||||||
|
|
||||||
emit originalTextureLoaded();
|
|
||||||
} else {
|
|
||||||
// remote file, kick off a download
|
|
||||||
auto& networkAccessManager = NetworkAccessManager::getInstance();
|
|
||||||
|
|
||||||
QNetworkRequest networkRequest;
|
|
||||||
|
|
||||||
// setup the request to follow re-directs and always hit the network
|
|
||||||
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
|
||||||
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
|
|
||||||
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
|
|
||||||
|
|
||||||
networkRequest.setUrl(_textureURL);
|
|
||||||
|
|
||||||
qCDebug(model_baking) << "Downloading" << _textureURL;
|
|
||||||
|
|
||||||
// kickoff the download, wait for slot to tell us it is done
|
|
||||||
auto networkReply = networkAccessManager.get(networkRequest);
|
|
||||||
connect(networkReply, &QNetworkReply::finished, this, &TextureBaker::handleTextureNetworkReply);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureBaker::handleTextureNetworkReply() {
|
|
||||||
auto requestReply = qobject_cast<QNetworkReply*>(sender());
|
|
||||||
|
|
||||||
if (requestReply->error() == QNetworkReply::NoError) {
|
|
||||||
qCDebug(model_baking) << "Downloaded texture" << _textureURL;
|
|
||||||
|
|
||||||
// store the original texture so it can be passed along for the bake
|
|
||||||
_originalTexture = requestReply->readAll();
|
|
||||||
|
|
||||||
emit originalTextureLoaded();
|
|
||||||
} else {
|
|
||||||
// add an error to our list stating that this texture could not be downloaded
|
|
||||||
handleError("Error downloading " + _textureURL.toString() + " - " + requestReply->errorString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureBaker::processTexture() {
|
|
||||||
auto processedTexture = image::processImage(_originalTexture, _textureURL.toString().toStdString(),
|
|
||||||
ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType);
|
|
||||||
|
|
||||||
if (!processedTexture) {
|
|
||||||
handleError("Could not process texture " + _textureURL.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// the baked textures need to have the source hash added for cache checks in Interface
|
|
||||||
// so we add that to the processed texture before handling it off to be serialized
|
|
||||||
auto hashData = QCryptographicHash::hash(_originalTexture, QCryptographicHash::Md5);
|
|
||||||
std::string hash = hashData.toHex().toStdString();
|
|
||||||
processedTexture->setSourceHash(hash);
|
|
||||||
|
|
||||||
auto memKTX = gpu::Texture::serialize(*processedTexture);
|
|
||||||
|
|
||||||
if (!memKTX) {
|
|
||||||
handleError("Could not serialize " + _textureURL.toString() + " to KTX");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* data = reinterpret_cast<const char*>(memKTX->_storage->data());
|
|
||||||
const size_t length = memKTX->_storage->size();
|
|
||||||
|
|
||||||
// attempt to write the baked texture to the destination file path
|
|
||||||
QFile bakedTextureFile { _outputDirectory.absoluteFilePath(_bakedTextureFileName) };
|
|
||||||
|
|
||||||
if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) {
|
|
||||||
handleError("Could not write baked texture for " + _textureURL.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(model_baking) << "Baked texture" << _textureURL;
|
|
||||||
emit finished();
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
//
|
|
||||||
// TextureBaker.h
|
|
||||||
// tools/oven/src
|
|
||||||
//
|
|
||||||
// Created by Stephen Birarda on 4/5/17.
|
|
||||||
// Copyright 2017 High Fidelity, Inc.
|
|
||||||
//
|
|
||||||
// Distributed under the Apache License, Version 2.0.
|
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef hifi_TextureBaker_h
|
|
||||||
#define hifi_TextureBaker_h
|
|
||||||
|
|
||||||
#include <QtCore/QObject>
|
|
||||||
#include <QtCore/QUrl>
|
|
||||||
#include <QtCore/QRunnable>
|
|
||||||
|
|
||||||
#include <image/Image.h>
|
|
||||||
|
|
||||||
#include "Baker.h"
|
|
||||||
|
|
||||||
extern const QString BAKED_TEXTURE_EXT;
|
|
||||||
|
|
||||||
class TextureBaker : public Baker {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory);
|
|
||||||
|
|
||||||
const QByteArray& getOriginalTexture() const { return _originalTexture; }
|
|
||||||
|
|
||||||
QUrl getTextureURL() const { return _textureURL; }
|
|
||||||
|
|
||||||
QString getDestinationFilePath() const { return _outputDirectory.absoluteFilePath(_bakedTextureFileName); }
|
|
||||||
QString getBakedTextureFileName() const { return _bakedTextureFileName; }
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
virtual void bake() override;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void originalTextureLoaded();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void processTexture();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void loadTexture();
|
|
||||||
void handleTextureNetworkReply();
|
|
||||||
|
|
||||||
QUrl _textureURL;
|
|
||||||
QByteArray _originalTexture;
|
|
||||||
image::TextureUsage::Type _textureType;
|
|
||||||
|
|
||||||
QDir _outputDirectory;
|
|
||||||
QString _bakedTextureFileName;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // hifi_TextureBaker_h
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
#include <QtWidgets/QWidget>
|
#include <QtWidgets/QWidget>
|
||||||
|
|
||||||
#include "../Baker.h"
|
#include <Baker.h>
|
||||||
|
|
||||||
class BakeWidget : public QWidget {
|
class BakeWidget : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
|
@ -156,22 +156,6 @@ void ModelBakeWidget::outputDirectoryChanged(const QString& newDirectory) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModelBakeWidget::bakeButtonClicked() {
|
void ModelBakeWidget::bakeButtonClicked() {
|
||||||
// make sure we have a valid output directory
|
|
||||||
QDir outputDirectory(_outputDirLineEdit->text());
|
|
||||||
|
|
||||||
outputDirectory.mkdir(".");
|
|
||||||
if (!outputDirectory.exists()) {
|
|
||||||
QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDir bakedOutputDirectory = outputDirectory.absoluteFilePath("baked");
|
|
||||||
QDir originalOutputDirectory = outputDirectory.absoluteFilePath("original");
|
|
||||||
|
|
||||||
bakedOutputDirectory.mkdir(".");
|
|
||||||
originalOutputDirectory.mkdir(".");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// make sure we have a non empty URL to a model to bake
|
// make sure we have a non empty URL to a model to bake
|
||||||
if (_modelLineEdit->text().isEmpty()) {
|
if (_modelLineEdit->text().isEmpty()) {
|
||||||
|
@ -193,6 +177,34 @@ void ModelBakeWidget::bakeButtonClicked() {
|
||||||
qDebug() << "New url: " << modelToBakeURL;
|
qDebug() << "New url: " << modelToBakeURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto modelName = modelToBakeURL.fileName().left(modelToBakeURL.fileName().lastIndexOf('.'));
|
||||||
|
|
||||||
|
// make sure we have a valid output directory
|
||||||
|
QDir outputDirectory(_outputDirLineEdit->text());
|
||||||
|
QString subFolderName = modelName + "/";
|
||||||
|
|
||||||
|
// output in a sub-folder with the name of the fbx, potentially suffixed by a number to make it unique
|
||||||
|
int iteration = 0;
|
||||||
|
|
||||||
|
while (outputDirectory.exists(subFolderName)) {
|
||||||
|
subFolderName = modelName + "-" + QString::number(++iteration) + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
outputDirectory.mkdir(subFolderName);
|
||||||
|
|
||||||
|
if (!outputDirectory.exists()) {
|
||||||
|
QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputDirectory.cd(subFolderName);
|
||||||
|
|
||||||
|
QDir bakedOutputDirectory = outputDirectory.absoluteFilePath("baked");
|
||||||
|
QDir originalOutputDirectory = outputDirectory.absoluteFilePath("original");
|
||||||
|
|
||||||
|
bakedOutputDirectory.mkdir(".");
|
||||||
|
originalOutputDirectory.mkdir(".");
|
||||||
|
|
||||||
// everything seems to be in place, kick off a bake for this model now
|
// everything seems to be in place, kick off a bake for this model now
|
||||||
auto baker = std::unique_ptr<FBXBaker> {
|
auto baker = std::unique_ptr<FBXBaker> {
|
||||||
new FBXBaker(modelToBakeURL, []() -> QThread* {
|
new FBXBaker(modelToBakeURL, []() -> QThread* {
|
||||||
|
@ -201,7 +213,7 @@ void ModelBakeWidget::bakeButtonClicked() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// move the baker to the FBX baker thread
|
// move the baker to the FBX baker thread
|
||||||
baker->moveToThread(qApp->getFBXBakerThread());
|
baker->moveToThread(qApp->getNextWorkerThread());
|
||||||
|
|
||||||
// invoke the bake method on the baker thread
|
// invoke the bake method on the baker thread
|
||||||
QMetaObject::invokeMethod(baker.get(), "bake");
|
QMetaObject::invokeMethod(baker.get(), "bake");
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
#include <SettingHandle.h>
|
#include <SettingHandle.h>
|
||||||
|
|
||||||
#include "../FBXBaker.h"
|
#include <FBXBaker.h>
|
||||||
|
|
||||||
#include "BakeWidget.h"
|
#include "BakeWidget.h"
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
#include <SettingHandle.h>
|
#include <SettingHandle.h>
|
||||||
|
|
||||||
#include "../TextureBaker.h"
|
#include <TextureBaker.h>
|
||||||
|
|
||||||
#include "BakeWidget.h"
|
#include "BakeWidget.h"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue