mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-08-04 12:35:21 +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)
|
||||
setup_hifi_library(Concurrent)
|
||||
|
||||
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})
|
||||
|
||||
if (UNIX)
|
||||
target_link_libraries(${TARGET_NAME} ${CMAKE_DL_LIBS})
|
||||
endif (UNIX)
|
||||
endif ()
|
||||
|
||||
link_hifi_libraries(shared model networking ktx image)
|
||||
link_hifi_libraries(shared model networking ktx image fbx)
|
||||
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
|
||||
// tools/oven/src
|
||||
// tools/baking/src
|
||||
//
|
||||
// Created by Stephen Birarda on 3/30/17.
|
||||
// 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 <fbxsdk.h>
|
||||
|
||||
#include <QtConcurrent>
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QDir>
|
||||
|
@ -27,13 +25,26 @@
|
|||
|
||||
#include <PathUtils.h>
|
||||
|
||||
#include <FBXReader.h>
|
||||
#include <FBXWriter.h>
|
||||
|
||||
#include "ModelBakingLoggingCategory.h"
|
||||
#include "TextureBaker.h"
|
||||
|
||||
#include "FBXBaker.h"
|
||||
|
||||
std::once_flag onceFlag;
|
||||
FBXSDKManagerUniquePointer FBXBaker::_sdkManager { nullptr };
|
||||
#ifdef _WIN32
|
||||
#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,
|
||||
const QString& bakedOutputDir, const QString& originalOutputDir) :
|
||||
|
@ -42,12 +53,7 @@ FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGet
|
|||
_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() {
|
||||
|
@ -85,7 +91,8 @@ void FBXBaker::bakeSourceCopy() {
|
|||
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();
|
||||
|
||||
if (hasErrors()) {
|
||||
|
@ -205,29 +212,20 @@ void FBXBaker::handleFBXNetworkReply() {
|
|||
}
|
||||
|
||||
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());
|
||||
QFile fbxFile(_originalFBXFilePath);
|
||||
if (!fbxFile.open(QIODevice::ReadOnly)) {
|
||||
handleError("Error opening " + _originalFBXFilePath + " for reading");
|
||||
return;
|
||||
} else {
|
||||
qCDebug(model_baking) << "Imported" << _fbxURL << "to FbxScene";
|
||||
}
|
||||
|
||||
// setup a new scene to hold the imported file
|
||||
_scene = FbxScene::Create(_sdkManager.get(), "bakeScene");
|
||||
FBXReader reader;
|
||||
|
||||
// import the file to the created scene
|
||||
importer->Import(_scene);
|
||||
|
||||
// destroy the importer that is no longer needed
|
||||
importer->Destroy();
|
||||
qCDebug(model_baking) << "Parsing" << _fbxURL;
|
||||
_rootNode = reader._rootNode = reader.parseFBX(&fbxFile);
|
||||
_geometry = reader.extractFBXGeometry({}, _fbxURL.toString());
|
||||
_textureContent = reader._textureContent;
|
||||
}
|
||||
|
||||
QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) {
|
||||
|
@ -264,7 +262,7 @@ QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) {
|
|||
return bakedTextureFileName;
|
||||
}
|
||||
|
||||
QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* fileTexture) {
|
||||
QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName) {
|
||||
QUrl urlToTexture;
|
||||
|
||||
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
|
||||
|
||||
// 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
|
||||
|
@ -293,84 +290,237 @@ QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* f
|
|||
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
|
||||
void FBXBaker::rewriteAndBakeSceneModels() {
|
||||
unsigned int meshIndex = 0;
|
||||
for (FBXNode& rootChild : _rootNode.children) {
|
||||
if (rootChild.name == "Objects") {
|
||||
for (FBXNode& objectChild : rootChild.children) {
|
||||
if (objectChild.name == "Geometry") {
|
||||
|
||||
// grab the hierarchical name for this property and lowercase it for case-insensitive compare
|
||||
auto propertyName = QString(property.GetHierarchicalName()).toLower();
|
||||
// TODO Pull this out of _geometry instead so we don't have to reprocess it
|
||||
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
|
||||
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);
|
||||
Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size());
|
||||
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
|
||||
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
|
||||
|
||||
if (lambertMaterial->AmbientFactor == 0) {
|
||||
return LIGHTMAP_TEXTURE;
|
||||
} else if (lambertMaterial->AmbientFactor > 0) {
|
||||
return OCCLUSION_TEXTURE;
|
||||
} else {
|
||||
return UNUSED_TEXTURE;
|
||||
int64_t numTriangles { 0 };
|
||||
for (auto& part : mesh.parts) {
|
||||
if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) {
|
||||
handleWarning("Found a mesh part with invalid index data, skipping");
|
||||
continue;
|
||||
}
|
||||
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() {
|
||||
using namespace image::TextureUsage;
|
||||
QHash<QString, image::TextureUsage::Type> textureTypes;
|
||||
|
||||
// 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);
|
||||
// enumerate the materials in the extracted geometry so we can determine the texture type for each texture ID
|
||||
for (const auto& material : _geometry->materials) {
|
||||
if (material.normalTexture.isBumpmap) {
|
||||
textureTypes[material.normalTexture.id] = BUMP_TEXTURE;
|
||||
} else {
|
||||
textureTypes[material.normalTexture.id] = NORMAL_TEXTURE;
|
||||
}
|
||||
|
||||
if (material) {
|
||||
// enumerate the properties of this material to see what texture channels it might have
|
||||
FbxProperty property = material->GetFirstProperty();
|
||||
textureTypes[material.albedoTexture.id] = ALBEDO_TEXTURE;
|
||||
textureTypes[material.glossTexture.id] = GLOSS_TEXTURE;
|
||||
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()) {
|
||||
// first check if this property has connected textures, if not we don't need to bother with it here
|
||||
if (property.GetSrcObjectCount<FbxTexture>() > 0) {
|
||||
// enumerate the children of the root node
|
||||
for (FBXNode& rootChild : _rootNode.children) {
|
||||
|
||||
// figure out the type of texture from the material property
|
||||
auto textureType = textureTypeForMaterialProperty(property, material);
|
||||
if (rootChild.name == "Objects") {
|
||||
|
||||
if (textureType != image::TextureUsage::UNUSED_TEXTURE) {
|
||||
int numTextures = property.GetSrcObjectCount<FbxFileTexture>();
|
||||
// enumerate the objects
|
||||
auto object = rootChild.children.begin();
|
||||
while (object != rootChild.children.end()) {
|
||||
if (object->name == "Texture") {
|
||||
|
||||
for (int j = 0; j < numTextures; j++) {
|
||||
FbxFileTexture* fileTexture = property.GetSrcObject<FbxFileTexture>(j);
|
||||
// enumerate the texture children
|
||||
for (FBXNode& textureChild : object->children) {
|
||||
|
||||
if (textureChild.name == "RelativeFilename") {
|
||||
|
||||
// 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("\\", "/") };
|
||||
|
||||
// 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);
|
||||
|
||||
qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName()
|
||||
<< "to" << bakedTextureFilePath;
|
||||
qCDebug(model_baking).noquote() << "Re-mapping" << fbxTextureFileName
|
||||
<< "to" << bakedTextureFileName;
|
||||
|
||||
// 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
|
||||
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());
|
||||
textureChild.properties[0] = bakedTextureFileName.toLocal8Bit();
|
||||
|
||||
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
|
||||
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
|
||||
QSharedPointer<TextureBaker> bakingTexture {
|
||||
new TextureBaker(textureURL, textureType, outputDir),
|
||||
new TextureBaker(textureURL, textureType, outputDir, textureContent),
|
||||
&TextureBaker::deleteLater
|
||||
};
|
||||
|
||||
|
@ -474,7 +636,7 @@ void FBXBaker::handleBakedTexture() {
|
|||
|
||||
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
|
||||
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
|
||||
<< "for" << _fbxURL;
|
||||
<< "for" << _fbxURL;
|
||||
} else {
|
||||
handleError("Could not save original external texture " + originalTextureFile.fileName()
|
||||
+ " for " + _fbxURL.toString());
|
||||
|
@ -491,13 +653,13 @@ void FBXBaker::handleBakedTexture() {
|
|||
} 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 {
|
||||
|
@ -512,29 +674,25 @@ void FBXBaker::handleBakedTexture() {
|
|||
}
|
||||
|
||||
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());
|
||||
auto fbxData = FBXWriter::encodeFBX(_rootNode);
|
||||
|
||||
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());
|
||||
QFile bakedFile(_bakedFBXFilePath);
|
||||
|
||||
if (!bakedFile.open(QIODevice::WriteOnly)) {
|
||||
handleError("Error opening " + _bakedFBXFilePath + " for writing");
|
||||
return;
|
||||
}
|
||||
|
||||
_outputFiles.push_back(_bakedFBXFilePath);
|
||||
bakedFile.write(fbxData);
|
||||
|
||||
// export the scene
|
||||
exporter->Export(_scene);
|
||||
_outputFiles.push_back(_bakedFBXFilePath);
|
||||
|
||||
qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// FBXBaker.h
|
||||
// tools/oven/src
|
||||
// tools/baking/src
|
||||
//
|
||||
// Created by Stephen Birarda on 3/30/17.
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
|
@ -24,15 +24,9 @@
|
|||
|
||||
#include <gpu/Texture.h>
|
||||
|
||||
namespace fbxsdk {
|
||||
class FbxManager;
|
||||
class FbxProperty;
|
||||
class FbxScene;
|
||||
class FbxFileTexture;
|
||||
}
|
||||
#include <FBX.h>
|
||||
|
||||
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*()>;
|
||||
|
||||
|
@ -64,6 +58,7 @@ private:
|
|||
void loadSourceFBX();
|
||||
|
||||
void importScene();
|
||||
void rewriteAndBakeSceneModels();
|
||||
void rewriteAndBakeSceneTextures();
|
||||
void exportScene();
|
||||
void removeEmbeddedMediaFolder();
|
||||
|
@ -71,11 +66,16 @@ private:
|
|||
void checkIfTexturesFinished();
|
||||
|
||||
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;
|
||||
|
||||
FBXNode _rootNode;
|
||||
FBXGeometry* _geometry;
|
||||
QHash<QByteArray, QByteArray> _textureContent;
|
||||
|
||||
QString _bakedFBXFilePath;
|
||||
|
||||
|
@ -87,9 +87,6 @@ private:
|
|||
QDir _tempDir;
|
||||
QString _originalFBXFilePath;
|
||||
|
||||
static FBXSDKManagerUniquePointer _sdkManager;
|
||||
fbxsdk::FbxScene* _scene { nullptr };
|
||||
|
||||
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
|
||||
QHash<QString, int> _textureNameMatchCount;
|
||||
|
||||
|
|
|
@ -25,8 +25,10 @@
|
|||
|
||||
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),
|
||||
_originalTexture(textureContent),
|
||||
_textureType(textureType),
|
||||
_outputDirectory(outputDirectory)
|
||||
{
|
||||
|
@ -39,8 +41,13 @@ 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();
|
||||
if (_originalTexture.isEmpty()) {
|
||||
// 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() {
|
||||
|
|
|
@ -27,7 +27,8 @@ class TextureBaker : public Baker {
|
|||
Q_OBJECT
|
||||
|
||||
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();
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
set(TARGET_NAME fbx)
|
||||
setup_hifi_library()
|
||||
|
||||
|
||||
|
||||
link_hifi_libraries(shared model networking 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());
|
||||
}
|
||||
|
||||
const char* HUMANIK_JOINTS[] = {
|
||||
/// The names of the joints in the Maya HumanIK rig
|
||||
static const std::array<const char*, 16> HUMANIK_JOINTS = {{
|
||||
"RightHand",
|
||||
"RightForeArm",
|
||||
"RightArm",
|
||||
|
@ -184,9 +185,8 @@ const char* HUMANIK_JOINTS[] = {
|
|||
"RightLeg",
|
||||
"LeftLeg",
|
||||
"RightFoot",
|
||||
"LeftFoot",
|
||||
""
|
||||
};
|
||||
"LeftFoot"
|
||||
}};
|
||||
|
||||
class FBXModel {
|
||||
public:
|
||||
|
@ -468,7 +468,7 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) {
|
|||
}
|
||||
|
||||
FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) {
|
||||
const FBXNode& node = _fbxNode;
|
||||
const FBXNode& node = _rootNode;
|
||||
QMap<QString, ExtractedMesh> meshes;
|
||||
QHash<QString, QString> modelIDsToNames;
|
||||
QHash<QString, int> meshIDsToMeshIndices;
|
||||
|
@ -512,11 +512,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS
|
|||
|
||||
|
||||
QVector<QString> humanIKJointNames;
|
||||
for (int i = 0;; i++) {
|
||||
for (int i = 0; i < (int) HUMANIK_JOINTS.size(); i++) {
|
||||
QByteArray jointName = HUMANIK_JOINTS[i];
|
||||
if (jointName.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
humanIKJointNames.append(processID(getString(joints.value(jointName, jointName))));
|
||||
}
|
||||
QVector<QString> humanIKJointIDs(humanIKJointNames.size());
|
||||
|
@ -942,7 +939,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS
|
|||
QByteArray content;
|
||||
foreach (const FBXNode& subobject, object.children) {
|
||||
if (subobject.name == "RelativeFilename") {
|
||||
filepath= subobject.properties.at(0).toByteArray();
|
||||
filepath = subobject.properties.at(0).toByteArray();
|
||||
filepath = filepath.replace('\\', '/');
|
||||
|
||||
} 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) {
|
||||
FBXReader reader;
|
||||
reader._fbxNode = FBXReader::parseFBX(device);
|
||||
reader._rootNode = FBXReader::parseFBX(device);
|
||||
reader._loadLightmaps = loadLightmaps;
|
||||
reader._lightmapLevel = lightmapLevel;
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
#ifndef hifi_FBXReader_h
|
||||
#define hifi_FBXReader_h
|
||||
|
||||
#include "FBX.h"
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QSet>
|
||||
#include <QUrl>
|
||||
|
@ -31,305 +33,6 @@
|
|||
class QIODevice;
|
||||
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.
|
||||
/// \exception QString if an error occurs in parsing
|
||||
|
@ -402,12 +105,12 @@ class FBXReader {
|
|||
public:
|
||||
FBXGeometry* _fbxGeometry;
|
||||
|
||||
FBXNode _fbxNode;
|
||||
FBXNode _rootNode;
|
||||
static FBXNode parseFBX(QIODevice* device);
|
||||
|
||||
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;
|
||||
static void buildModelMesh(FBXMesh& extractedMesh, const QString& url);
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@ FBXTexture FBXReader::getTexture(const QString& textureID) {
|
|||
texture.filename = filepath;
|
||||
}
|
||||
|
||||
texture.id = textureID;
|
||||
texture.name = _textureNames.value(textureID);
|
||||
texture.transform.setIdentity();
|
||||
texture.texcoordSet = 0;
|
||||
|
|
|
@ -9,6 +9,17 @@
|
|||
// 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 <QBuffer>
|
||||
#include <QDataStream>
|
||||
|
@ -168,11 +179,17 @@ void appendIndex(MeshData& data, QVector<int>& indices, int index) {
|
|||
ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIndex) {
|
||||
MeshData data;
|
||||
data.extracted.mesh.meshIndex = meshIndex++;
|
||||
|
||||
QVector<int> materials;
|
||||
QVector<int> textures;
|
||||
|
||||
bool isMaterialPerPolygon = false;
|
||||
|
||||
static const QVariant BY_VERTICE = QByteArray("ByVertice");
|
||||
static const QVariant INDEX_TO_DIRECT = QByteArray("IndexToDirect");
|
||||
|
||||
bool isDracoMesh = false;
|
||||
|
||||
foreach (const FBXNode& child, object.children) {
|
||||
if (child.name == "Vertices") {
|
||||
data.vertices = createVec3Vector(getDoubleVector(child));
|
||||
|
@ -304,8 +321,9 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn
|
|||
if (subdata.name == "Materials") {
|
||||
materials = getIntVector(subdata);
|
||||
} else if (subdata.name == "MappingInformationType") {
|
||||
if (subdata.properties.at(0) == BY_POLYGON)
|
||||
if (subdata.properties.at(0) == BY_POLYGON) {
|
||||
isMaterialPerPolygon = true;
|
||||
}
|
||||
} else {
|
||||
isMaterialPerPolygon = false;
|
||||
}
|
||||
|
@ -318,70 +336,194 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn
|
|||
textures = getIntVector(subdata);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (child.name == "DracoMesh") {
|
||||
isDracoMesh = true;
|
||||
|
||||
bool isMultiMaterial = false;
|
||||
if (isMaterialPerPolygon) {
|
||||
isMultiMaterial = true;
|
||||
}
|
||||
// TODO: make excellent use of isMultiMaterial
|
||||
Q_UNUSED(isMultiMaterial);
|
||||
// load the draco mesh from the FBX and create a draco::Mesh
|
||||
draco::Decoder decoder;
|
||||
draco::DecoderBuffer decodedBuffer;
|
||||
QByteArray dracoArray = child.properties.at(0).value<QByteArray>();
|
||||
decodedBuffer.Init(dracoArray.data(), dracoArray.size());
|
||||
|
||||
// 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);
|
||||
std::unique_ptr<draco::Mesh> dracoMesh(new draco::Mesh());
|
||||
decoder.DecodeBufferToGeometry(&decodedBuffer, dracoMesh.get());
|
||||
|
||||
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++);
|
||||
// prepare attributes for this mesh
|
||||
auto positionAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::POSITION);
|
||||
auto normalAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::NORMAL);
|
||||
auto texCoordAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::TEX_COORD);
|
||||
auto extraTexCoordAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_TEX_COORD_1);
|
||||
auto colorAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::COLOR);
|
||||
auto matTexAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_MATERIAL_ID);
|
||||
|
||||
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];
|
||||
// setup extracted mesh data structures given number of points
|
||||
auto numVertices = dracoMesh->num_points();
|
||||
|
||||
// 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 };
|
||||
QHash<QPair<int, int>, int> materialTextureParts;
|
||||
|
||||
part.quadTrianglesIndices.append(i0);
|
||||
part.quadTrianglesIndices.append(i1);
|
||||
part.quadTrianglesIndices.append(i3);
|
||||
data.extracted.mesh.vertices.resize(numVertices);
|
||||
|
||||
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;
|
||||
}
|
||||
if (normalAttribute) {
|
||||
data.extracted.mesh.normals.resize(numVertices);
|
||||
}
|
||||
|
||||
if (texCoordAttribute) {
|
||||
data.extracted.mesh.texCoords.resize(numVertices);
|
||||
}
|
||||
|
||||
if (extraTexCoordAttribute) {
|
||||
data.extracted.mesh.texCoords1.resize(numVertices);
|
||||
}
|
||||
|
||||
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 "ModelFormatLogging.h"
|
||||
|
||||
template<class T> int streamSize() {
|
||||
template<class T>
|
||||
int streamSize() {
|
||||
return sizeof(T);
|
||||
}
|
||||
|
||||
template<bool> int streamSize() {
|
||||
template<bool>
|
||||
int streamSize() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
template<class T> QVariant readBinaryArray(QDataStream& in, int& position) {
|
||||
template<class T>
|
||||
QVariant readBinaryArray(QDataStream& in, int& position) {
|
||||
quint32 arrayLength;
|
||||
quint32 encoding;
|
||||
quint32 compressedLength;
|
||||
|
@ -350,8 +353,7 @@ FBXNode parseTextFBXNode(Tokenizer& tokenizer) {
|
|||
FBXNode FBXReader::parseFBX(QIODevice* device) {
|
||||
PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xff0000ff, device);
|
||||
// verify the prolog
|
||||
const QByteArray BINARY_PROLOG = "Kaydara FBX Binary ";
|
||||
if (device->peek(BINARY_PROLOG.size()) != BINARY_PROLOG) {
|
||||
if (device->peek(FBX_BINARY_PROLOG.size()) != FBX_BINARY_PROLOG) {
|
||||
// parse as a text file
|
||||
FBXNode top;
|
||||
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 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.
|
||||
const int HEADER_BEFORE_VERSION = 23;
|
||||
const quint32 VERSION_FBX2016 = 7500;
|
||||
in.skipRawData(HEADER_BEFORE_VERSION);
|
||||
int position = HEADER_BEFORE_VERSION;
|
||||
in.skipRawData(FBX_HEADER_BYTES_BEFORE_VERSION);
|
||||
int position = FBX_HEADER_BYTES_BEFORE_VERSION;
|
||||
quint32 fileVersion;
|
||||
in >> fileVersion;
|
||||
position += sizeof(fileVersion);
|
||||
qCDebug(modelformat) << "fileVersion:" << fileVersion;
|
||||
bool has64BitPositions = (fileVersion >= VERSION_FBX2016);
|
||||
bool has64BitPositions = (fileVersion >= FBX_VERSION_2016);
|
||||
|
||||
// parse the top-level node
|
||||
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)
|
||||
|
||||
link_hifi_libraries(networking shared image gpu ktx fbx baking)
|
||||
link_hifi_libraries(networking shared image gpu ktx fbx baking model)
|
||||
|
||||
setup_memory_debugger()
|
||||
|
||||
|
@ -17,16 +17,4 @@ if (UNIX)
|
|||
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)
|
||||
|
|
|
@ -42,7 +42,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString outputPath) {
|
|||
// create our appropiate baker
|
||||
if (isFBX) {
|
||||
_baker = std::unique_ptr<Baker> { new FBXBaker(inputUrl, []() -> QThread* { return qApp->getNextWorkerThread(); }, outputPath) };
|
||||
_baker->moveToThread(qApp->getFBXBakerThread());
|
||||
_baker->moveToThread(qApp->getNextWorkerThread());
|
||||
} else if (isSupportedImage) {
|
||||
_baker = std::unique_ptr<Baker> { new TextureBaker(inputUrl, image::TextureUsage::CUBE_TEXTURE, outputPath) };
|
||||
_baker->moveToThread(qApp->getNextWorkerThread());
|
||||
|
@ -61,4 +61,4 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString outputPath) {
|
|||
void BakerCLI::handleFinishedBaker() {
|
||||
qCDebug(model_baking) << "Finished baking file.";
|
||||
QApplication::exit(_baker.get()->hasErrors());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -214,7 +214,7 @@ void DomainBaker::enumerateEntities() {
|
|||
|
||||
// move the baker to the baker thread
|
||||
// and kickoff the bake
|
||||
baker->moveToThread(qApp->getFBXBakerThread());
|
||||
baker->moveToThread(qApp->getNextWorkerThread());
|
||||
QMetaObject::invokeMethod(baker.data(), "bake");
|
||||
|
||||
// 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);
|
||||
|
||||
// setup our worker threads
|
||||
setupWorkerThreads(QThread::idealThreadCount() - 1);
|
||||
|
||||
// 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();
|
||||
setupWorkerThreads(QThread::idealThreadCount());
|
||||
|
||||
// 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)) {
|
||||
|
@ -81,10 +77,6 @@ Oven::~Oven() {
|
|||
_workerThreads[i]->quit();
|
||||
_workerThreads[i]->wait();
|
||||
}
|
||||
|
||||
// cleanup the FBX Baker thread
|
||||
_fbxBakerThread->quit();
|
||||
_fbxBakerThread->wait();
|
||||
}
|
||||
|
||||
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() {
|
||||
// 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.
|
||||
|
|
|
@ -34,7 +34,6 @@ public:
|
|||
|
||||
OvenMainWindow* getMainWindow() const { return _mainWindow; }
|
||||
|
||||
QThread* getFBXBakerThread();
|
||||
QThread* getNextWorkerThread();
|
||||
|
||||
private:
|
||||
|
@ -42,7 +41,6 @@ private:
|
|||
void setupFBXBakerThread();
|
||||
|
||||
OvenMainWindow* _mainWindow;
|
||||
QThread* _fbxBakerThread;
|
||||
QList<QThread*> _workerThreads;
|
||||
|
||||
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 "../Baker.h"
|
||||
#include <Baker.h>
|
||||
|
||||
class BakeWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
|
|
@ -156,22 +156,6 @@ void ModelBakeWidget::outputDirectoryChanged(const QString& newDirectory) {
|
|||
}
|
||||
|
||||
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
|
||||
if (_modelLineEdit->text().isEmpty()) {
|
||||
|
@ -193,6 +177,34 @@ void ModelBakeWidget::bakeButtonClicked() {
|
|||
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
|
||||
auto baker = std::unique_ptr<FBXBaker> {
|
||||
new FBXBaker(modelToBakeURL, []() -> QThread* {
|
||||
|
@ -201,7 +213,7 @@ void ModelBakeWidget::bakeButtonClicked() {
|
|||
};
|
||||
|
||||
// move the baker to the FBX baker thread
|
||||
baker->moveToThread(qApp->getFBXBakerThread());
|
||||
baker->moveToThread(qApp->getNextWorkerThread());
|
||||
|
||||
// invoke the bake method on the baker thread
|
||||
QMetaObject::invokeMethod(baker.get(), "bake");
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
#include <SettingHandle.h>
|
||||
|
||||
#include "../FBXBaker.h"
|
||||
#include <FBXBaker.h>
|
||||
|
||||
#include "BakeWidget.h"
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
#include <SettingHandle.h>
|
||||
|
||||
#include "../TextureBaker.h"
|
||||
#include <TextureBaker.h>
|
||||
|
||||
#include "BakeWidget.h"
|
||||
|
||||
|
|
Loading…
Reference in a new issue