Merge pull request #11346 from birarda/feat/draco-in-baking

Add mesh compression to baking library and FBXReader
This commit is contained in:
Clément Brisset 2017-09-14 15:32:58 -07:00 committed by GitHub
commit cbe61871dc
31 changed files with 1243 additions and 1570 deletions

40
cmake/externals/draco/CMakeLists.txt vendored Normal file
View 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")

View 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)

View file

@ -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()

View file

@ -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})

View file

@ -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;
}

View file

@ -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;

View file

@ -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() {

View file

@ -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();

View file

@ -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
View 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
View 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_

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;

View 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;
}
}
}
}

View 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

View file

@ -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)

View file

@ -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());
}
}

View file

@ -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

View file

@ -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();
}
}
}

View file

@ -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

View file

@ -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.

View file

@ -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;

View file

@ -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();
}

View file

@ -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

View file

@ -14,7 +14,7 @@
#include <QtWidgets/QWidget>
#include "../Baker.h"
#include <Baker.h>
class BakeWidget : public QWidget {
Q_OBJECT

View file

@ -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");

View file

@ -16,7 +16,7 @@
#include <SettingHandle.h>
#include "../FBXBaker.h"
#include <FBXBaker.h>
#include "BakeWidget.h"

View file

@ -16,7 +16,7 @@
#include <SettingHandle.h>
#include "../TextureBaker.h"
#include <TextureBaker.h>
#include "BakeWidget.h"