mirror of
https://github.com/overte-org/overte.git
synced 2025-04-23 14:14:07 +02:00
Merged with master with new abotProcessing in image
This commit is contained in:
commit
23c77d528a
122 changed files with 6112 additions and 1958 deletions
assignment-client
CMakeLists.txt
src/assets
cmake
interface
libraries
baking
entities-renderer/src
EntityTreeRenderer.cppEntityTreeRenderer.hRenderableEntityItem.cppRenderableEntityItem.hRenderableModelEntityItem.cppRenderableModelEntityItem.hRenderableTextEntityItem.cppRenderableWebEntityItem.cppRenderableWebEntityItem.h
fbx
gpu/src/gpu
image/src/image
model-networking/src/model-networking
networking/src
AssetClient.cppAssetClient.hAssetRequest.hAssetResourceRequest.cppAssetUtils.cppAssetUtils.hMappingRequest.cppMappingRequest.hResourceCache.cppResourceCache.hResourceRequest.cppResourceRequest.h
udt
physics/src
render-utils/src
render/src/render
script-engine/src
AssetScriptingInterface.cppAssetScriptingInterface.hRecordingScriptingInterface.cppRecordingScriptingInterface.h
shared/src
ui/src/ui
scripts
developer/tests
system
|
@ -13,7 +13,7 @@ setup_memory_debugger()
|
|||
link_hifi_libraries(
|
||||
audio avatars octree gpu model fbx entities
|
||||
networking animation recording shared script-engine embedded-webserver
|
||||
controllers physics plugins midi
|
||||
controllers physics plugins midi baking image
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,17 +14,39 @@
|
|||
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QThreadPool>
|
||||
#include <QRunnable>
|
||||
|
||||
#include <ThreadedAssignment.h>
|
||||
|
||||
#include "AssetUtils.h"
|
||||
#include "ReceivedMessage.h"
|
||||
|
||||
|
||||
namespace std {
|
||||
template <>
|
||||
struct hash<QString> {
|
||||
size_t operator()(const QString& v) const { return qHash(v); }
|
||||
};
|
||||
}
|
||||
|
||||
struct AssetMeta {
|
||||
AssetMeta() {
|
||||
}
|
||||
|
||||
int bakeVersion { 0 };
|
||||
bool failedLastBake { false };
|
||||
QString lastBakeErrors;
|
||||
};
|
||||
|
||||
class BakeAssetTask;
|
||||
|
||||
class AssetServer : public ThreadedAssignment {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AssetServer(ReceivedMessage& message);
|
||||
|
||||
void aboutToFinish() override;
|
||||
|
||||
public slots:
|
||||
void run() override;
|
||||
|
||||
|
@ -39,13 +61,14 @@ private slots:
|
|||
void sendStatsPacket() override;
|
||||
|
||||
private:
|
||||
using Mappings = QVariantHash;
|
||||
using Mappings = std::unordered_map<QString, QString>;
|
||||
|
||||
void handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleSetBakingEnabledOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
|
||||
// Mapping file operations must be called from main assignment thread only
|
||||
bool loadMappingsFromFile();
|
||||
|
@ -55,19 +78,54 @@ private:
|
|||
bool setMapping(AssetPath path, AssetHash hash);
|
||||
|
||||
/// Delete mapping `path`. Returns `true` if deletion of mappings succeeds, else `false`.
|
||||
bool deleteMappings(AssetPathList& paths);
|
||||
bool deleteMappings(const AssetPathList& paths);
|
||||
|
||||
/// Rename mapping from `oldPath` to `newPath`. Returns true if successful
|
||||
bool renameMapping(AssetPath oldPath, AssetPath newPath);
|
||||
|
||||
// deletes any unmapped files from the local asset directory
|
||||
bool setBakingEnabled(const AssetPathList& paths, bool enabled);
|
||||
|
||||
/// Delete any unmapped files from the local asset directory
|
||||
void cleanupUnmappedFiles();
|
||||
|
||||
QString getPathToAssetHash(const AssetHash& assetHash);
|
||||
|
||||
std::pair<BakingStatus, QString> getAssetStatus(const AssetPath& path, const AssetHash& hash);
|
||||
|
||||
void bakeAssets();
|
||||
void maybeBake(const AssetPath& path, const AssetHash& hash);
|
||||
void createEmptyMetaFile(const AssetHash& hash);
|
||||
bool hasMetaFile(const AssetHash& hash);
|
||||
bool needsToBeBaked(const AssetPath& path, const AssetHash& assetHash);
|
||||
void bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath);
|
||||
|
||||
/// Move baked content for asset to baked directory and update baked status
|
||||
void handleCompletedBake(QString originalAssetHash, QString assetPath, QVector<QString> bakedFilePaths);
|
||||
void handleFailedBake(QString originalAssetHash, QString assetPath, QString errors);
|
||||
void handleAbortedBake(QString originalAssetHash, QString assetPath);
|
||||
|
||||
/// Create meta file to describe baked content for original asset
|
||||
std::pair<bool, AssetMeta> readMetaFile(AssetHash hash);
|
||||
bool writeMetaFile(AssetHash originalAssetHash, const AssetMeta& meta = AssetMeta());
|
||||
|
||||
/// Remove baked paths when the original asset is deleteds
|
||||
void removeBakedPathsForDeletedAsset(AssetHash originalAssetHash);
|
||||
|
||||
Mappings _fileMappings;
|
||||
|
||||
QDir _resourcesDirectory;
|
||||
QDir _filesDirectory;
|
||||
QThreadPool _taskPool;
|
||||
|
||||
/// Task pool for handling uploads and downloads of assets
|
||||
QThreadPool _transferTaskPool;
|
||||
|
||||
QHash<AssetHash, std::shared_ptr<BakeAssetTask>> _pendingBakes;
|
||||
QThreadPool _bakingTaskPool;
|
||||
|
||||
bool _wasColorTextureCompressionEnabled { false };
|
||||
bool _wasGrayscaleTextureCompressionEnabled { false };
|
||||
bool _wasNormalTextureCompressionEnabled { false };
|
||||
bool _wasCubeTextureCompressionEnabled { false };
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
14
assignment-client/src/assets/AssetServerLogging.cpp
Normal file
14
assignment-client/src/assets/AssetServerLogging.cpp
Normal file
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// AssetServerLogging.cpp
|
||||
// assignment-client/src/assets
|
||||
//
|
||||
// Created by Clement Brisset on 8/9/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 "AssetServerLogging.h"
|
||||
|
||||
Q_LOGGING_CATEGORY(asset_server, "hifi.asset-server")
|
19
assignment-client/src/assets/AssetServerLogging.h
Normal file
19
assignment-client/src/assets/AssetServerLogging.h
Normal file
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// AssetServerLogging.h
|
||||
// assignment-client/src/assets
|
||||
//
|
||||
// Created by Clement Brisset on 8/9/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_AssetServerLogging_h
|
||||
#define hifi_AssetServerLogging_h
|
||||
|
||||
#include <QLoggingCategory>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(asset_server)
|
||||
|
||||
#endif // hifi_AssetServerLogging_h
|
79
assignment-client/src/assets/BakeAssetTask.cpp
Normal file
79
assignment-client/src/assets/BakeAssetTask.cpp
Normal file
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// BakeAssetTask.cpp
|
||||
// assignment-client/src/assets
|
||||
//
|
||||
// Created by Stephen Birarda on 9/18/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 "BakeAssetTask.h"
|
||||
|
||||
#include <QtCore/QThread>
|
||||
|
||||
#include <FBXBaker.h>
|
||||
#include <PathUtils.h>
|
||||
|
||||
BakeAssetTask::BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) :
|
||||
_assetHash(assetHash),
|
||||
_assetPath(assetPath),
|
||||
_filePath(filePath)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void BakeAssetTask::run() {
|
||||
_isBaking.store(true);
|
||||
|
||||
qRegisterMetaType<QVector<QString> >("QVector<QString>");
|
||||
TextureBakerThreadGetter fn = []() -> QThread* { return QThread::currentThread(); };
|
||||
|
||||
if (_assetPath.endsWith(".fbx")) {
|
||||
_baker = std::unique_ptr<FBXBaker> {
|
||||
new FBXBaker(QUrl("file:///" + _filePath), fn, PathUtils::generateTemporaryDir())
|
||||
};
|
||||
} else {
|
||||
_baker = std::unique_ptr<TextureBaker> {
|
||||
new TextureBaker(QUrl("file:///" + _filePath), image::TextureUsage::CUBE_TEXTURE,
|
||||
PathUtils::generateTemporaryDir())
|
||||
};
|
||||
}
|
||||
|
||||
QEventLoop loop;
|
||||
connect(_baker.get(), &Baker::finished, &loop, &QEventLoop::quit);
|
||||
connect(_baker.get(), &Baker::aborted, &loop, &QEventLoop::quit);
|
||||
QMetaObject::invokeMethod(_baker.get(), "bake", Qt::QueuedConnection);
|
||||
loop.exec();
|
||||
|
||||
if (_baker->wasAborted()) {
|
||||
qDebug() << "Aborted baking: " << _assetHash << _assetPath;
|
||||
|
||||
_wasAborted.store(true);
|
||||
|
||||
emit bakeAborted(_assetHash, _assetPath);
|
||||
} else if (_baker->hasErrors()) {
|
||||
qDebug() << "Failed to bake: " << _assetHash << _assetPath << _baker->getErrors();
|
||||
|
||||
auto errors = _baker->getErrors().join('\n'); // Join error list into a single string for convenience
|
||||
|
||||
_didFinish.store(true);
|
||||
|
||||
emit bakeFailed(_assetHash, _assetPath, errors);
|
||||
} else {
|
||||
auto vectorOutputFiles = QVector<QString>::fromStdVector(_baker->getOutputFiles());
|
||||
|
||||
qDebug() << "Finished baking: " << _assetHash << _assetPath << vectorOutputFiles;
|
||||
|
||||
_didFinish.store(true);
|
||||
|
||||
emit bakeComplete(_assetHash, _assetPath, vectorOutputFiles);
|
||||
}
|
||||
}
|
||||
|
||||
void BakeAssetTask::abort() {
|
||||
if (_baker) {
|
||||
_baker->abort();
|
||||
}
|
||||
}
|
52
assignment-client/src/assets/BakeAssetTask.h
Normal file
52
assignment-client/src/assets/BakeAssetTask.h
Normal file
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// BakeAssetTask.h
|
||||
// assignment-client/src/assets
|
||||
//
|
||||
// Created by Stephen Birarda on 9/18/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_BakeAssetTask_h
|
||||
#define hifi_BakeAssetTask_h
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QRunnable>
|
||||
|
||||
#include <AssetUtils.h>
|
||||
#include <Baker.h>
|
||||
|
||||
class BakeAssetTask : public QObject, public QRunnable {
|
||||
Q_OBJECT
|
||||
public:
|
||||
BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath);
|
||||
|
||||
bool isBaking() { return _isBaking.load(); }
|
||||
|
||||
void run() override;
|
||||
|
||||
void abort();
|
||||
bool wasAborted() const { return _wasAborted.load(); }
|
||||
bool didFinish() const { return _didFinish.load(); }
|
||||
|
||||
signals:
|
||||
void bakeComplete(QString assetHash, QString assetPath, QVector<QString> outputFiles);
|
||||
void bakeFailed(QString assetHash, QString assetPath, QString errors);
|
||||
void bakeAborted(QString assetHash, QString assetPath);
|
||||
|
||||
private:
|
||||
std::atomic<bool> _isBaking { false };
|
||||
AssetHash _assetHash;
|
||||
AssetPath _assetPath;
|
||||
QString _filePath;
|
||||
std::unique_ptr<Baker> _baker;
|
||||
std::atomic<bool> _wasAborted { false };
|
||||
std::atomic<bool> _didFinish { false };
|
||||
};
|
||||
|
||||
#endif // hifi_BakeAssetTask_h
|
40
cmake/externals/draco/CMakeLists.txt
vendored
Normal file
40
cmake/externals/draco/CMakeLists.txt
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
set(EXTERNAL_NAME draco)
|
||||
|
||||
if (ANDROID)
|
||||
set(ANDROID_CMAKE_ARGS "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" "-DANDROID_NATIVE_API_LEVEL=19")
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
set(EXTRA_CMAKE_FLAGS -DCMAKE_CXX_FLAGS=-stdlib=libc++ -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++)
|
||||
endif ()
|
||||
|
||||
include(ExternalProject)
|
||||
ExternalProject_Add(
|
||||
${EXTERNAL_NAME}
|
||||
URL http://hifi-public.s3.amazonaws.com/dependencies/draco-1.1.0.zip
|
||||
URL_MD5 208f8b04c91d5f1c73d731a3ea37c5bb
|
||||
CONFIGURE_COMMAND CMAKE_ARGS ${ANDROID_CMAKE_ARGS} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> ${EXTRA_CMAKE_FLAGS}
|
||||
LOG_DOWNLOAD 1
|
||||
LOG_CONFIGURE 1
|
||||
LOG_BUILD 1
|
||||
)
|
||||
|
||||
# Hide this external target (for ide users)
|
||||
set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")
|
||||
|
||||
ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR)
|
||||
|
||||
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
|
||||
|
||||
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE PATH "List of Draco include directories")
|
||||
|
||||
if (UNIX)
|
||||
set(LIB_PREFIX "lib")
|
||||
set(LIB_EXT "a")
|
||||
elseif (WIN32)
|
||||
set(LIB_EXT "lib")
|
||||
endif ()
|
||||
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}draco.${LIB_EXT} CACHE FILEPATH "Path to Draco release library")
|
||||
set(${EXTERNAL_NAME_UPPER}_ENCODER_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}dracoenc.${LIB_EXT} CACHE FILEPATH "Path to Draco encoder release library")
|
||||
set(${EXTERNAL_NAME_UPPER}_DECODER_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}dracodec.${LIB_EXT} CACHE FILEPATH "Path to Draco decoder release library")
|
30
cmake/modules/FindDraco.cmake
Normal file
30
cmake/modules/FindDraco.cmake
Normal file
|
@ -0,0 +1,30 @@
|
|||
#
|
||||
# FindDraco.cmake
|
||||
#
|
||||
# Try to find Draco libraries and include path.
|
||||
# Once done this will define
|
||||
#
|
||||
# DRACO_FOUND
|
||||
# DRACO_INCLUDE_DIRS
|
||||
# DRACO_LIBRARY
|
||||
# DRACO_ENCODER_LIBRARY
|
||||
# DRACO_DECODER_LIBRARY
|
||||
#
|
||||
# Created on 8/8/2017 by Stephen Birarda
|
||||
# Copyright 2017 High Fidelity, Inc.
|
||||
#
|
||||
# Distributed under the Apache License, Version 2.0.
|
||||
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
#
|
||||
|
||||
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
|
||||
hifi_library_search_hints("draco")
|
||||
|
||||
find_path(DRACO_INCLUDE_DIRS draco/core/draco_types.h PATH_SUFFIXES include/draco/src include HINTS ${DRACO_SEARCH_DIRS})
|
||||
|
||||
find_library(DRACO_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS})
|
||||
find_library(DRACO_ENCODER_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS})
|
||||
find_library(DRACO_DECODER_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(DRACO DEFAULT_MSG DRACO_INCLUDE_DIRS DRACO_LIBRARY DRACO_ENCODER_LIBRARY DRACO_DECODER_LIBRARY)
|
|
@ -1,115 +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)
|
||||
if (WIN32)
|
||||
set(FBX_VERSION 2017.1)
|
||||
else()
|
||||
set(FBX_VERSION 2017.0.1)
|
||||
endif()
|
||||
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/lib/gcc4/x64/debug/")
|
||||
|
||||
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 vs2017)
|
||||
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}/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()
|
|
@ -836,6 +836,9 @@ Section "-Core installation"
|
|||
Delete "$INSTDIR\ui_resources_200_percent.pak"
|
||||
Delete "$INSTDIR\vccorlib120.dll"
|
||||
Delete "$INSTDIR\version"
|
||||
Delete "$INSTDIR\msvcr140.dll"
|
||||
Delete "$INSTDIR\msvcp140.dll"
|
||||
Delete "$INSTDIR\vcruntime140.dll"
|
||||
Delete "$INSTDIR\xinput1_3.dll"
|
||||
|
||||
; Delete old desktop shortcuts before they were renamed during Sandbox rename
|
||||
|
|
Binary file not shown.
1056
interface/resources/fonts/hifi-glyphs/icons-reference.html
Normal file
1056
interface/resources/fonts/hifi-glyphs/icons-reference.html
Normal file
File diff suppressed because it is too large
Load diff
421
interface/resources/fonts/hifi-glyphs/styles.css
Normal file
421
interface/resources/fonts/hifi-glyphs/styles.css
Normal file
|
@ -0,0 +1,421 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
@font-face {
|
||||
font-family: "hifi-glyphs";
|
||||
src:url("fonts/hifi-glyphs.eot");
|
||||
src:url("fonts/hifi-glyphs.eot?#iefix") format("embedded-opentype"),
|
||||
url("fonts/hifi-glyphs.woff") format("woff"),
|
||||
url("fonts/hifi-glyphs.ttf") format("truetype"),
|
||||
url("fonts/hifi-glyphs.svg#hifi-glyphs") format("svg");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
||||
[data-icon]:before {
|
||||
font-family: "hifi-glyphs" !important;
|
||||
content: attr(data-icon);
|
||||
font-style: normal !important;
|
||||
font-weight: normal !important;
|
||||
font-variant: normal !important;
|
||||
text-transform: none !important;
|
||||
speak: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
[class^="icon-"]:before,
|
||||
[class*=" icon-"]:before {
|
||||
font-family: "hifi-glyphs" !important;
|
||||
font-style: normal !important;
|
||||
font-weight: normal !important;
|
||||
font-variant: normal !important;
|
||||
text-transform: none !important;
|
||||
speak: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-hmd:before {
|
||||
content: "\62";
|
||||
}
|
||||
.icon-2d-screen:before {
|
||||
content: "\63";
|
||||
}
|
||||
.icon-keyboard:before {
|
||||
content: "\64";
|
||||
}
|
||||
.icon-hand-controllers:before {
|
||||
content: "\65";
|
||||
}
|
||||
.icon-headphones-mic:before {
|
||||
content: "\66";
|
||||
}
|
||||
.icon-gamepad:before {
|
||||
content: "\67";
|
||||
}
|
||||
.icon-headphones:before {
|
||||
content: "\68";
|
||||
}
|
||||
.icon-mic:before {
|
||||
content: "\69";
|
||||
}
|
||||
.icon-upload:before {
|
||||
content: "\6a";
|
||||
}
|
||||
.icon-script:before {
|
||||
content: "\6b";
|
||||
}
|
||||
.icon-text:before {
|
||||
content: "\6c";
|
||||
}
|
||||
.icon-cube:before {
|
||||
content: "\6d";
|
||||
}
|
||||
.icon-sphere:before {
|
||||
content: "\6e";
|
||||
}
|
||||
.icon-zone:before {
|
||||
content: "\6f";
|
||||
}
|
||||
.icon-light:before {
|
||||
content: "\70";
|
||||
}
|
||||
.icon-web:before {
|
||||
content: "\71";
|
||||
}
|
||||
.icon-web-2:before {
|
||||
content: "\72";
|
||||
}
|
||||
.icon-edit:before {
|
||||
content: "\73";
|
||||
}
|
||||
.icon-market:before {
|
||||
content: "\74";
|
||||
}
|
||||
.icon-directory:before {
|
||||
content: "\75";
|
||||
}
|
||||
.icon-menu:before {
|
||||
content: "\76";
|
||||
}
|
||||
.icon-close:before {
|
||||
content: "\77";
|
||||
}
|
||||
.icon-close-inverted:before {
|
||||
content: "\78";
|
||||
}
|
||||
.icon-pin:before {
|
||||
content: "\79";
|
||||
}
|
||||
.icon-pin-inverted:before {
|
||||
content: "\7a";
|
||||
}
|
||||
.icon-resize-handle:before {
|
||||
content: "\41";
|
||||
}
|
||||
.icon-diclosure-expand:before {
|
||||
content: "\42";
|
||||
}
|
||||
.icon-reload-small:before {
|
||||
content: "\61";
|
||||
}
|
||||
.icon-close-small:before {
|
||||
content: "\43";
|
||||
}
|
||||
.icon-backward:before {
|
||||
content: "\45";
|
||||
}
|
||||
.icon-reload:before {
|
||||
content: "\46";
|
||||
}
|
||||
.icon-minimize:before {
|
||||
content: "\49";
|
||||
}
|
||||
.icon-maximize:before {
|
||||
content: "\4a";
|
||||
}
|
||||
.icon-maximize-inverted:before {
|
||||
content: "\4b";
|
||||
}
|
||||
.icon-disclosure-button-expand:before {
|
||||
content: "\4c";
|
||||
}
|
||||
.icon-disclosure-button-collapse:before {
|
||||
content: "\4d";
|
||||
}
|
||||
.icon-script-stop:before {
|
||||
content: "\4e";
|
||||
}
|
||||
.icon-script-reload:before {
|
||||
content: "\4f";
|
||||
}
|
||||
.icon-script-run:before {
|
||||
content: "\50";
|
||||
}
|
||||
.icon-script-new:before {
|
||||
content: "\51";
|
||||
}
|
||||
.icon-hifi-forum:before {
|
||||
content: "\32";
|
||||
}
|
||||
.icon-hifi-logo-small:before {
|
||||
content: "\53";
|
||||
}
|
||||
.icon-avatar-1:before {
|
||||
content: "\54";
|
||||
}
|
||||
.icon-placemark:before {
|
||||
content: "\55";
|
||||
}
|
||||
.icon-box:before {
|
||||
content: "\56";
|
||||
}
|
||||
.icon-community:before {
|
||||
content: "\30";
|
||||
}
|
||||
.icon-grab-handle:before {
|
||||
content: "\58";
|
||||
}
|
||||
.icon-search:before {
|
||||
content: "\59";
|
||||
}
|
||||
.icon-disclosure-collapse:before {
|
||||
content: "\5a";
|
||||
}
|
||||
.icon-script-upload:before {
|
||||
content: "\52";
|
||||
}
|
||||
.icon-code:before {
|
||||
content: "\57";
|
||||
}
|
||||
.icon-avatar:before {
|
||||
content: "\3c";
|
||||
}
|
||||
.icon-arrows-h:before {
|
||||
content: "\3a";
|
||||
}
|
||||
.icon-arrows-v:before {
|
||||
content: "\3b";
|
||||
}
|
||||
.icon-arrows:before {
|
||||
content: "\60";
|
||||
}
|
||||
.icon-compress:before {
|
||||
content: "\21";
|
||||
}
|
||||
.icon-expand:before {
|
||||
content: "\22";
|
||||
}
|
||||
.icon-placemark-1:before {
|
||||
content: "\23";
|
||||
}
|
||||
.icon-circle:before {
|
||||
content: "\24";
|
||||
}
|
||||
.icon-hand-pointer:before {
|
||||
content: "\39";
|
||||
}
|
||||
.icon-plus-square-o:before {
|
||||
content: "\25";
|
||||
}
|
||||
.icon-square:before {
|
||||
content: "\27";
|
||||
}
|
||||
.icon-align-center:before {
|
||||
content: "\38";
|
||||
}
|
||||
.icon-align-justify:before {
|
||||
content: "\29";
|
||||
}
|
||||
.icon-align-left:before {
|
||||
content: "\2a";
|
||||
}
|
||||
.icon-align-right:before {
|
||||
content: "\5e";
|
||||
}
|
||||
.icon-bars:before {
|
||||
content: "\37";
|
||||
}
|
||||
.icon-circle-slash:before {
|
||||
content: "\2c";
|
||||
}
|
||||
.icon-sync:before {
|
||||
content: "\28";
|
||||
}
|
||||
.icon-key:before {
|
||||
content: "\2d";
|
||||
}
|
||||
.icon-link:before {
|
||||
content: "\2e";
|
||||
}
|
||||
.icon-location:before {
|
||||
content: "\2f";
|
||||
}
|
||||
.icon-carat-r:before {
|
||||
content: "\33";
|
||||
}
|
||||
.icon-carat-l:before {
|
||||
content: "\34";
|
||||
}
|
||||
.icon-folder-lg:before {
|
||||
content: "\3e";
|
||||
}
|
||||
.icon-folder-sm:before {
|
||||
content: "\3f";
|
||||
}
|
||||
.icon-level-up:before {
|
||||
content: "\31";
|
||||
}
|
||||
.icon-info:before {
|
||||
content: "\5b";
|
||||
}
|
||||
.icon-question:before {
|
||||
content: "\5d";
|
||||
}
|
||||
.icon-alert:before {
|
||||
content: "\2b";
|
||||
}
|
||||
.icon-home:before {
|
||||
content: "\5f";
|
||||
}
|
||||
.icon-error:before {
|
||||
content: "\3d";
|
||||
}
|
||||
.icon-settings:before {
|
||||
content: "\40";
|
||||
}
|
||||
.icon-trash:before {
|
||||
content: "\7b";
|
||||
}
|
||||
.icon-object-group:before {
|
||||
content: "\e000";
|
||||
}
|
||||
.icon-cm:before {
|
||||
content: "\7d";
|
||||
}
|
||||
.icon-msvg:before {
|
||||
content: "\7e";
|
||||
}
|
||||
.icon-deg:before {
|
||||
content: "\5c";
|
||||
}
|
||||
.icon-px:before {
|
||||
content: "\7c";
|
||||
}
|
||||
.icon-m-sq:before {
|
||||
content: "\e001";
|
||||
}
|
||||
.icon-m-cubed:before {
|
||||
content: "\e002";
|
||||
}
|
||||
.icon-acceleration:before {
|
||||
content: "\e003";
|
||||
}
|
||||
.icon-particles:before {
|
||||
content: "\e004";
|
||||
}
|
||||
.icon-voxels:before {
|
||||
content: "\e005";
|
||||
}
|
||||
.icon-lock:before {
|
||||
content: "\e006";
|
||||
}
|
||||
.icon-visible:before {
|
||||
content: "\e007";
|
||||
}
|
||||
.icon-model:before {
|
||||
content: "\e008";
|
||||
}
|
||||
.icon-forward:before {
|
||||
content: "\44";
|
||||
}
|
||||
.icon-avatar-2:before {
|
||||
content: "\e009";
|
||||
}
|
||||
.icon-arrow-dn:before {
|
||||
content: "\35";
|
||||
}
|
||||
.icon-arrow-up:before {
|
||||
content: "\36";
|
||||
}
|
||||
.icon-time:before {
|
||||
content: "\e00a";
|
||||
}
|
||||
.icon-transparency:before {
|
||||
content: "\e00b";
|
||||
}
|
||||
.icon-unmuted:before {
|
||||
content: "\47";
|
||||
}
|
||||
.icon-user:before {
|
||||
content: "\e00c";
|
||||
}
|
||||
.icon-edit-pencil:before {
|
||||
content: "\e00d";
|
||||
}
|
||||
.icon-muted:before {
|
||||
content: "\48";
|
||||
}
|
||||
.icon-vol-0:before {
|
||||
content: "\e00e";
|
||||
}
|
||||
.icon-vol-1:before {
|
||||
content: "\e00f";
|
||||
}
|
||||
.icon-vol-2:before {
|
||||
content: "\e010";
|
||||
}
|
||||
.icon-vol-3:before {
|
||||
content: "\e011";
|
||||
}
|
||||
.icon-vol-4:before {
|
||||
content: "\e012";
|
||||
}
|
||||
.icon-vol-x-0:before {
|
||||
content: "\e013";
|
||||
}
|
||||
.icon-vol-x-1:before {
|
||||
content: "\e014";
|
||||
}
|
||||
.icon-vol-x-2:before {
|
||||
content: "\e015";
|
||||
}
|
||||
.icon-vol-x-3:before {
|
||||
content: "\e016";
|
||||
}
|
||||
.icon-vol-x-4:before {
|
||||
content: "\e017";
|
||||
}
|
||||
.icon-share-ext:before {
|
||||
content: "\e018";
|
||||
}
|
||||
.icon-ellipsis:before {
|
||||
content: "\e019";
|
||||
}
|
||||
.icon-check:before {
|
||||
content: "\e01a";
|
||||
}
|
||||
.icon-sliders:before {
|
||||
content: "\26";
|
||||
}
|
||||
.icon-polyline:before {
|
||||
content: "\e01b";
|
||||
}
|
||||
.icon-source:before {
|
||||
content: "\e01c";
|
||||
}
|
||||
.icon-playback-play:before {
|
||||
content: "\e01d";
|
||||
}
|
||||
.icon-stop-square:before {
|
||||
content: "\e01e";
|
||||
}
|
||||
.icon-avatar-t-pose:before {
|
||||
content: "\e01f";
|
||||
}
|
||||
.icon-check-2-01:before {
|
||||
content: "\e020";
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
import QtQuick.Dialogs 1.2 as OriginalDialogs
|
||||
import Qt.labs.settings 1.0
|
||||
|
||||
|
@ -48,8 +49,14 @@ ScrollingWindow {
|
|||
Component.onCompleted: {
|
||||
ApplicationInterface.uploadRequest.connect(uploadClicked);
|
||||
assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError);
|
||||
assetMappingsModel.autoRefreshEnabled = true;
|
||||
|
||||
reload();
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
assetMappingsModel.autoRefreshEnabled = false;
|
||||
}
|
||||
|
||||
function doDeleteFile(path) {
|
||||
console.log("Deleting " + path);
|
||||
|
@ -70,11 +77,11 @@ ScrollingWindow {
|
|||
|
||||
function doRenameFile(oldPath, newPath) {
|
||||
|
||||
if (newPath[0] != "/") {
|
||||
if (newPath[0] !== "/") {
|
||||
newPath = "/" + newPath;
|
||||
}
|
||||
|
||||
if (oldPath[oldPath.length - 1] == '/' && newPath[newPath.length - 1] != '/') {
|
||||
if (oldPath[oldPath.length - 1] === '/' && newPath[newPath.length - 1] != '/') {
|
||||
// this is a folder rename but the user neglected to add a trailing slash when providing a new path
|
||||
newPath = newPath + "/";
|
||||
}
|
||||
|
@ -144,7 +151,6 @@ ScrollingWindow {
|
|||
|
||||
function reload() {
|
||||
Assets.mappingModel.refresh();
|
||||
treeView.selection.clear();
|
||||
}
|
||||
|
||||
function handleGetMappingsError(errorString) {
|
||||
|
@ -300,7 +306,7 @@ ScrollingWindow {
|
|||
object.selected.connect(function(destinationPath) {
|
||||
destinationPath = destinationPath.trim();
|
||||
|
||||
if (path == destinationPath) {
|
||||
if (path === destinationPath) {
|
||||
return;
|
||||
}
|
||||
if (fileExists(destinationPath)) {
|
||||
|
@ -361,7 +367,7 @@ ScrollingWindow {
|
|||
running: false
|
||||
}
|
||||
|
||||
property var uploadOpen: false;
|
||||
property bool uploadOpen: false;
|
||||
Timer {
|
||||
id: timer
|
||||
}
|
||||
|
@ -464,20 +470,10 @@ ScrollingWindow {
|
|||
HifiControls.VerticalSpacer {}
|
||||
|
||||
Row {
|
||||
id: buttonRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: hifi.dimensions.contentSpacing.x
|
||||
|
||||
HifiControls.GlyphButton {
|
||||
glyph: hifi.glyphs.reload
|
||||
color: hifi.buttons.black
|
||||
colorScheme: root.colorScheme
|
||||
width: hifi.dimensions.controlLineHeight
|
||||
|
||||
onClicked: root.reload()
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
text: "Add To World"
|
||||
color: hifi.buttons.black
|
||||
|
@ -511,7 +507,182 @@ ScrollingWindow {
|
|||
enabled: treeView.selection.hasSelection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Tree {
|
||||
id: treeView
|
||||
anchors.top: assetDirectory.bottom
|
||||
anchors.bottom: infoRow.top
|
||||
anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
treeModel: assetProxyModel
|
||||
selectionMode: SelectionMode.ExtendedSelection
|
||||
headerVisible: true
|
||||
sortIndicatorVisible: true
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
modifyEl: renameEl
|
||||
|
||||
TableViewColumn {
|
||||
id: nameColumn
|
||||
title: "Name:"
|
||||
role: "name"
|
||||
width: treeView.width - bakedColumn.width;
|
||||
}
|
||||
TableViewColumn {
|
||||
id: bakedColumn
|
||||
title: "Use Baked?"
|
||||
role: "baked"
|
||||
width: 100
|
||||
}
|
||||
|
||||
itemDelegate: Loader {
|
||||
id: itemDelegateLoader
|
||||
|
||||
anchors {
|
||||
left: parent ? parent.left : undefined
|
||||
leftMargin: (styleData.column === 0 ? (2 + styleData.depth) : 1) * hifi.dimensions.tablePadding
|
||||
right: parent ? parent.right : undefined
|
||||
rightMargin: hifi.dimensions.tablePadding
|
||||
verticalCenter: parent ? parent.verticalCenter : undefined
|
||||
}
|
||||
|
||||
function convertToGlyph(text) {
|
||||
switch (text) {
|
||||
case "Not Baked":
|
||||
return hifi.glyphs.circleSlash;
|
||||
case "Baked":
|
||||
return hifi.glyphs.check_2_01;
|
||||
case "Error":
|
||||
return hifi.glyphs.alert;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function getComponent() {
|
||||
if ((styleData.column === 0) && styleData.selected) {
|
||||
return textFieldComponent;
|
||||
} else if (convertToGlyph(styleData.value) != "") {
|
||||
return glyphComponent;
|
||||
} else {
|
||||
return labelComponent;
|
||||
}
|
||||
|
||||
}
|
||||
sourceComponent: getComponent()
|
||||
|
||||
Component {
|
||||
id: labelComponent
|
||||
FiraSansSemiBold {
|
||||
text: styleData.value
|
||||
size: hifi.fontSizes.tableText
|
||||
color: colorScheme == hifi.colorSchemes.light
|
||||
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
|
||||
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
|
||||
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: styleData.column === 1 ? TextInput.AlignHCenter : TextInput.AlignLeft
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: glyphComponent
|
||||
|
||||
HiFiGlyphs {
|
||||
text: convertToGlyph(styleData.value)
|
||||
size: hifi.dimensions.frameIconSize
|
||||
color: colorScheme == hifi.colorSchemes.light
|
||||
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
|
||||
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
|
||||
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: TextInput.AlignHCenter
|
||||
|
||||
HifiControls.ToolTip {
|
||||
anchors.fill: parent
|
||||
|
||||
visible: styleData.value === "Error"
|
||||
|
||||
toolTip: assetProxyModel.data(styleData.index, 0x106)
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: textFieldComponent
|
||||
|
||||
TextField {
|
||||
id: textField
|
||||
readOnly: !activeFocus
|
||||
|
||||
text: styleData.value
|
||||
|
||||
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
|
||||
font.family: firaSansSemiBold.name
|
||||
font.pixelSize: hifi.fontSizes.textFieldInput
|
||||
height: hifi.dimensions.tableRowHeight
|
||||
|
||||
style: TextFieldStyle {
|
||||
textColor: readOnly
|
||||
? hifi.colors.black
|
||||
: (treeView.isLightColorScheme ? hifi.colors.black : hifi.colors.white)
|
||||
background: Rectangle {
|
||||
visible: !readOnly
|
||||
color: treeView.isLightColorScheme ? hifi.colors.white : hifi.colors.black
|
||||
border.color: hifi.colors.primaryHighlight
|
||||
border.width: 1
|
||||
}
|
||||
selectedTextColor: hifi.colors.black
|
||||
selectionColor: hifi.colors.primaryHighlight
|
||||
padding.left: readOnly ? 0 : hifi.dimensions.textPadding
|
||||
padding.right: readOnly ? 0 : hifi.dimensions.textPadding
|
||||
}
|
||||
|
||||
validator: RegExpValidator {
|
||||
regExp: /[^/]+/
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key == Qt.Key_Escape) {
|
||||
text = styleData.value;
|
||||
unfocusHelper.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
if (acceptableInput && styleData.selected) {
|
||||
if (!treeView.modifyEl(treeView.selection.currentIndex, text)) {
|
||||
text = styleData.value;
|
||||
}
|
||||
unfocusHelper.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
onReadOnlyChanged: {
|
||||
// Have to explicily set keyboardRaised because automatic setting fails because readOnly is true at the time.
|
||||
keyboardRaised = activeFocus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MouseArea {
|
||||
propagateComposedEvents: true
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
if (!HMD.active) { // Popup only displays properly on desktop
|
||||
var index = treeView.indexAt(mouse.x, mouse.y);
|
||||
treeView.selection.setCurrentIndex(index, 0x0002);
|
||||
contextMenu.currentIndex = index;
|
||||
contextMenu.popup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: contextMenu
|
||||
title: "Edit"
|
||||
|
@ -541,36 +712,49 @@ ScrollingWindow {
|
|||
}
|
||||
}
|
||||
|
||||
HifiControls.Tree {
|
||||
id: treeView
|
||||
anchors.top: assetDirectory.bottom
|
||||
Row {
|
||||
id: infoRow
|
||||
anchors.left: treeView.left
|
||||
anchors.right: treeView.right
|
||||
anchors.bottom: uploadSection.top
|
||||
anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
treeModel: assetProxyModel
|
||||
canEdit: true
|
||||
colorScheme: root.colorScheme
|
||||
selectionMode: SelectionMode.ExtendedSelection
|
||||
|
||||
modifyEl: renameEl
|
||||
|
||||
MouseArea {
|
||||
propagateComposedEvents: true
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
if (!HMD.active) { // Popup only displays properly on desktop
|
||||
var index = treeView.indexAt(mouse.x, mouse.y);
|
||||
treeView.selection.setCurrentIndex(index, 0x0002);
|
||||
contextMenu.currentIndex = index;
|
||||
contextMenu.popup();
|
||||
}
|
||||
}
|
||||
anchors.bottomMargin: hifi.dimensions.contentSpacing.y
|
||||
spacing: hifi.dimensions.contentSpacing.x
|
||||
|
||||
RalewayRegular {
|
||||
size: hifi.fontSizes.sectionName
|
||||
font.capitalization: Font.AllUppercase
|
||||
text: selectedItems + " items selected"
|
||||
color: hifi.colors.lightGrayText
|
||||
}
|
||||
|
||||
HifiControls.CheckBox {
|
||||
function isChecked() {
|
||||
var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105);
|
||||
var bakingDisabled = (status === "Not Baked" || status === "--");
|
||||
return selectedItems === 1 && !bakingDisabled;
|
||||
}
|
||||
|
||||
text: "Use baked (optimized) versions"
|
||||
colorScheme: root.colorScheme
|
||||
enabled: selectedItems === 1 && assetProxyModel.data(treeView.selection.currentIndex, 0x105) !== "--"
|
||||
checked: isChecked()
|
||||
onClicked: {
|
||||
var mappings = [];
|
||||
for (var i in treeView.selection.selectedIndexes) {
|
||||
var index = treeView.selection.selectedIndexes[i];
|
||||
var path = assetProxyModel.data(index, 0x100);
|
||||
mappings.push(path);
|
||||
}
|
||||
print("Setting baking enabled:" + mappings + " " + checked);
|
||||
Assets.setBakingEnabled(mappings, checked, function() {
|
||||
reload();
|
||||
});
|
||||
|
||||
checked = Qt.binding(isChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.ContentSection {
|
||||
id: uploadSection
|
||||
name: "Upload A File"
|
||||
|
|
|
@ -34,7 +34,10 @@ Item {
|
|||
|
||||
onClicked: {
|
||||
mouse.accepted = true;
|
||||
|
||||
webEntity.synthesizeKeyPress(glyph);
|
||||
webEntity.synthesizeKeyPress(glyph, mirrorText);
|
||||
|
||||
if (toggle) {
|
||||
toggled = !toggled;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
//
|
||||
|
||||
import QtQuick 2.0
|
||||
import "."
|
||||
|
||||
Item {
|
||||
id: keyboardBase
|
||||
|
@ -16,9 +17,15 @@ Item {
|
|||
property bool raised: false
|
||||
property bool numeric: false
|
||||
|
||||
readonly property int keyboardRowHeight: 50
|
||||
readonly property int keyboardWidth: 480
|
||||
|
||||
readonly property int mirrorTextHeight: keyboardRowHeight
|
||||
|
||||
property bool showMirrorText: true
|
||||
readonly property int raisedHeight: 200
|
||||
|
||||
height: enabled && raised ? raisedHeight : 0
|
||||
height: enabled && raised ? raisedHeight + (showMirrorText ? keyboardRowHeight : 0) : 0
|
||||
visible: enabled && raised
|
||||
|
||||
property bool shiftMode: false
|
||||
|
@ -93,24 +100,35 @@ Item {
|
|||
}
|
||||
|
||||
Rectangle {
|
||||
id: leftRect
|
||||
y: 0
|
||||
height: 200
|
||||
x: 0
|
||||
height: showMirrorText ? mirrorTextHeight : 0
|
||||
width: keyboardWidth
|
||||
color: "#252525"
|
||||
anchors.right: keyboardRect.left
|
||||
anchors.rightMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
|
||||
TextEdit {
|
||||
id: mirrorText
|
||||
visible: showMirrorText
|
||||
size: 13.5
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "#FFFFFF";
|
||||
anchors.fill: parent
|
||||
wrapMode: Text.WordWrap
|
||||
readOnly: false // we need to leave this property read-only to allow control to accept QKeyEvent
|
||||
selectByMouse: false
|
||||
}
|
||||
|
||||
MouseArea { // ... and we need this mouse area to prevent mirrorText from getting mouse events to ensure it will never get focus
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: keyboardRect
|
||||
x: 206
|
||||
y: 0
|
||||
width: 480
|
||||
height: 200
|
||||
x: 0
|
||||
y: showMirrorText ? mirrorTextHeight : 0
|
||||
width: keyboardWidth
|
||||
height: raisedHeight
|
||||
color: "#252525"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
|
@ -118,13 +136,13 @@ Item {
|
|||
|
||||
Column {
|
||||
id: columnAlpha
|
||||
width: 480
|
||||
height: 200
|
||||
width: keyboardWidth
|
||||
height: raisedHeight
|
||||
visible: !numeric
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
width: keyboardWidth
|
||||
height: keyboardRowHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
|
@ -142,8 +160,8 @@ Item {
|
|||
}
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
width: keyboardWidth
|
||||
height: keyboardRowHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 20
|
||||
|
||||
|
@ -160,8 +178,8 @@ Item {
|
|||
}
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
width: keyboardWidth
|
||||
height: keyboardRowHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
|
@ -185,8 +203,8 @@ Item {
|
|||
}
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
width: keyboardWidth
|
||||
height: keyboardRowHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
|
@ -205,13 +223,13 @@ Item {
|
|||
|
||||
Column {
|
||||
id: columnNumeric
|
||||
width: 480
|
||||
height: 200
|
||||
width: keyboardWidth
|
||||
height: raisedHeight
|
||||
visible: numeric
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
width: keyboardWidth
|
||||
height: keyboardRowHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
|
@ -229,8 +247,8 @@ Item {
|
|||
}
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
width: keyboardWidth
|
||||
height: keyboardRowHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
|
@ -248,8 +266,8 @@ Item {
|
|||
}
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
width: keyboardWidth
|
||||
height: keyboardRowHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
|
@ -273,8 +291,8 @@ Item {
|
|||
}
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
width: keyboardWidth
|
||||
height: keyboardRowHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
|
@ -291,31 +309,4 @@ Item {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rightRect
|
||||
y: 280
|
||||
height: 200
|
||||
color: "#252525"
|
||||
border.width: 0
|
||||
anchors.left: keyboardRect.right
|
||||
anchors.leftMargin: 0
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rectangle1
|
||||
color: "#ffffff"
|
||||
anchors.bottom: keyboardRect.top
|
||||
anchors.bottomMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 0
|
||||
}
|
||||
}
|
||||
|
|
51
interface/resources/qml/controls-uit/ToolTip.qml
Normal file
51
interface/resources/qml/controls-uit/ToolTip.qml
Normal file
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// ToolTip.qml
|
||||
//
|
||||
// Created by Clement on 9/12/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
|
||||
//
|
||||
|
||||
import QtQuick 2.5
|
||||
|
||||
Item {
|
||||
property string toolTip
|
||||
property bool showToolTip: false
|
||||
|
||||
Rectangle {
|
||||
id: toolTipRectangle
|
||||
anchors.right: parent.right
|
||||
|
||||
width: toolTipText.width + 4
|
||||
height: toolTipText.height + 4
|
||||
opacity: (toolTip != "" && showToolTip) ? 1 : 0
|
||||
color: "#ffffaa"
|
||||
border.color: "#0a0a0a"
|
||||
Text {
|
||||
id: toolTipText
|
||||
text: toolTip
|
||||
color: "black"
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
Behavior on opacity {
|
||||
PropertyAnimation {
|
||||
easing.type: Easing.InOutQuad
|
||||
duration: 250
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
onEntered: showTimer.start()
|
||||
onExited: { showToolTip = false; showTimer.stop(); }
|
||||
hoverEnabled: true
|
||||
}
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: 250
|
||||
onTriggered: { showToolTip = true; }
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ TreeView {
|
|||
id: treeView
|
||||
|
||||
property var treeModel: ListModel { }
|
||||
property var canEdit: false
|
||||
property bool centerHeaderText: false
|
||||
property int colorScheme: hifi.colorSchemes.light
|
||||
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
|
||||
|
||||
|
@ -31,14 +31,9 @@ TreeView {
|
|||
model: treeModel
|
||||
}
|
||||
|
||||
TableViewColumn {
|
||||
role: "display";
|
||||
}
|
||||
|
||||
anchors { left: parent.left; right: parent.right }
|
||||
|
||||
|
||||
headerVisible: false
|
||||
headerDelegate: Item { } // Fix OSX QML bug that displays scrollbar starting too low.
|
||||
|
||||
// Use rectangle to draw border with rounded corners.
|
||||
frameVisible: false
|
||||
|
@ -61,6 +56,64 @@ TreeView {
|
|||
// Needed in order for rows to keep displaying rows after end of table entries.
|
||||
backgroundColor: parent.isLightColorScheme ? hifi.colors.tableRowLightEven : hifi.colors.tableRowDarkEven
|
||||
alternateBackgroundColor: parent.isLightColorScheme ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd
|
||||
|
||||
headerDelegate: Rectangle {
|
||||
height: hifi.dimensions.tableHeaderHeight
|
||||
color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark
|
||||
|
||||
RalewayRegular {
|
||||
id: titleText
|
||||
text: styleData.value
|
||||
size: hifi.fontSizes.tableHeading
|
||||
font.capitalization: Font.AllUppercase
|
||||
color: hifi.colors.baseGrayHighlight
|
||||
horizontalAlignment: (centerHeaderText ? Text.AlignHCenter : Text.AlignLeft)
|
||||
elide: Text.ElideRight
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: hifi.dimensions.tablePadding
|
||||
right: sortIndicatorVisible && sortIndicatorColumn === styleData.column ? titleSort.left : parent.right
|
||||
rightMargin: hifi.dimensions.tablePadding
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
HiFiGlyphs {
|
||||
id: titleSort
|
||||
text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn
|
||||
color: isLightColorScheme ? hifi.colors.darkGray : hifi.colors.baseGrayHighlight
|
||||
opacity: 0.6;
|
||||
size: hifi.fontSizes.tableHeadingIcon
|
||||
anchors {
|
||||
right: parent.right
|
||||
verticalCenter: titleText.verticalCenter
|
||||
}
|
||||
visible: sortIndicatorVisible && sortIndicatorColumn === styleData.column
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
topMargin: 1
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 2
|
||||
}
|
||||
color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight
|
||||
visible: styleData.column > 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight
|
||||
}
|
||||
}
|
||||
|
||||
branchDelegate: HiFiGlyphs {
|
||||
text: styleData.isExpanded ? hifi.glyphs.caratDn : hifi.glyphs.caratR
|
||||
|
@ -76,28 +129,53 @@ TreeView {
|
|||
|
||||
handle: Item {
|
||||
id: scrollbarHandle
|
||||
implicitWidth: 6
|
||||
implicitWidth: hifi.dimensions.scrollbarHandleWidth
|
||||
Rectangle {
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: 2 // Move it right
|
||||
rightMargin: -2 // ""
|
||||
topMargin: 3 // Shrink vertically
|
||||
topMargin: treeView.headerVisible ? hifi.dimensions.tableHeaderHeight + 3 : 3
|
||||
bottomMargin: 3 // ""
|
||||
leftMargin: 1 // Move it right
|
||||
rightMargin: -1 // ""
|
||||
}
|
||||
radius: 3
|
||||
color: hifi.colors.tableScrollHandleDark
|
||||
radius: hifi.dimensions.scrollbarHandleWidth / 2
|
||||
color: treeView.isLightColorScheme ? hifi.colors.tableScrollHandleLight : hifi.colors.tableScrollHandleDark
|
||||
}
|
||||
}
|
||||
|
||||
scrollBarBackground: Item {
|
||||
implicitWidth: 9
|
||||
implicitWidth: hifi.dimensions.scrollbarBackgroundWidth
|
||||
Rectangle {
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: treeView.headerVisible ? hifi.dimensions.tableHeaderHeight - 1 : -1
|
||||
margins: -1 // Expand
|
||||
}
|
||||
color: hifi.colors.tableBackgroundDark
|
||||
color: treeView.isLightColorScheme ? hifi.colors.tableScrollBackgroundLight : hifi.colors.tableScrollBackgroundDark
|
||||
|
||||
// Extend header color above scrollbar background
|
||||
Rectangle {
|
||||
anchors {
|
||||
top: parent.top
|
||||
topMargin: -hifi.dimensions.tableHeaderHeight
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
height: hifi.dimensions.tableHeaderHeight
|
||||
color: treeView.isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark
|
||||
visible: treeView.headerVisible
|
||||
}
|
||||
Rectangle {
|
||||
// Extend header bottom border
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
height: 1
|
||||
color: treeView.isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight
|
||||
visible: treeView.headerVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,9 +197,7 @@ TreeView {
|
|||
: (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd)
|
||||
}
|
||||
|
||||
itemDelegate: Loader {
|
||||
id: itemDelegateLoader
|
||||
|
||||
itemDelegate: FiraSansSemiBold {
|
||||
anchors {
|
||||
left: parent ? parent.left : undefined
|
||||
leftMargin: (2 + styleData.depth) * hifi.dimensions.tablePadding
|
||||
|
@ -130,83 +206,13 @@ TreeView {
|
|||
verticalCenter: parent ? parent.verticalCenter : undefined
|
||||
}
|
||||
|
||||
function getComponent() {
|
||||
if (treeView.canEdit && styleData.selected) {
|
||||
return textFieldComponent;
|
||||
} else {
|
||||
return labelComponent;
|
||||
}
|
||||
|
||||
}
|
||||
sourceComponent: getComponent()
|
||||
|
||||
Component {
|
||||
id: labelComponent
|
||||
FiraSansSemiBold {
|
||||
|
||||
text: styleData.value
|
||||
size: hifi.fontSizes.tableText
|
||||
color: colorScheme == hifi.colorSchemes.light
|
||||
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
|
||||
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: textFieldComponent
|
||||
|
||||
TextField {
|
||||
id: textField
|
||||
readOnly: !activeFocus
|
||||
|
||||
text: styleData.value
|
||||
|
||||
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
|
||||
font.family: firaSansSemiBold.name
|
||||
font.pixelSize: hifi.fontSizes.textFieldInput
|
||||
height: hifi.dimensions.tableRowHeight
|
||||
|
||||
style: TextFieldStyle {
|
||||
textColor: readOnly
|
||||
? hifi.colors.black
|
||||
: (treeView.isLightColorScheme ? hifi.colors.black : hifi.colors.white)
|
||||
background: Rectangle {
|
||||
visible: !readOnly
|
||||
color: treeView.isLightColorScheme ? hifi.colors.white : hifi.colors.black
|
||||
border.color: hifi.colors.primaryHighlight
|
||||
border.width: 1
|
||||
}
|
||||
selectedTextColor: hifi.colors.black
|
||||
selectionColor: hifi.colors.primaryHighlight
|
||||
padding.left: readOnly ? 0 : hifi.dimensions.textPadding
|
||||
padding.right: readOnly ? 0 : hifi.dimensions.textPadding
|
||||
}
|
||||
|
||||
validator: RegExpValidator {
|
||||
regExp: /[^/]+/
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key == Qt.Key_Escape) {
|
||||
text = styleData.value;
|
||||
unfocusHelper.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
if (acceptableInput && styleData.selected) {
|
||||
if (!modifyEl(selection.currentIndex, text)) {
|
||||
text = styleData.value;
|
||||
}
|
||||
unfocusHelper.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
onReadOnlyChanged: {
|
||||
// Have to explicily set keyboardRaised because automatic setting fails because readOnly is true at the time.
|
||||
keyboardRaised = activeFocus;
|
||||
}
|
||||
}
|
||||
}
|
||||
text: styleData.value
|
||||
size: hifi.fontSizes.tableText
|
||||
color: colorScheme == hifi.colorSchemes.light
|
||||
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
|
||||
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
|
||||
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Item {
|
||||
|
@ -219,11 +225,4 @@ TreeView {
|
|||
onClicked: {
|
||||
selectionModel.setCurrentIndex(index, ItemSelectionModel.ClearAndSelect);
|
||||
}
|
||||
|
||||
onActivated: {
|
||||
var path = scriptsModel.data(index, 0x100)
|
||||
if (path) {
|
||||
loadScript(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -371,6 +371,17 @@ ScrollingWindow {
|
|||
colorScheme: hifi.colorSchemes.dark
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
TableViewColumn {
|
||||
role: "display";
|
||||
}
|
||||
|
||||
onActivated: {
|
||||
var path = scriptsModel.data(index, 0x100)
|
||||
if (path) {
|
||||
loadScript(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.VerticalSpacer {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
import QtQuick.Dialogs 1.2 as OriginalDialogs
|
||||
import Qt.labs.settings 1.0
|
||||
|
||||
|
@ -48,9 +49,15 @@ Rectangle {
|
|||
isHMD = HMD.active;
|
||||
ApplicationInterface.uploadRequest.connect(uploadClicked);
|
||||
assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError);
|
||||
assetMappingsModel.autoRefreshEnabled = true;
|
||||
|
||||
reload();
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
assetMappingsModel.autoRefreshEnabled = false;
|
||||
}
|
||||
|
||||
function doDeleteFile(path) {
|
||||
console.log("Deleting " + path);
|
||||
|
||||
|
@ -105,12 +112,12 @@ Rectangle {
|
|||
|
||||
function askForOverwrite(path, callback) {
|
||||
var object = tabletRoot.messageBox({
|
||||
icon: hifi.icons.question,
|
||||
buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No,
|
||||
defaultButton: OriginalDialogs.StandardButton.No,
|
||||
title: "Overwrite File",
|
||||
text: path + "\n" + "This file already exists. Do you want to overwrite it?"
|
||||
});
|
||||
icon: hifi.icons.question,
|
||||
buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No,
|
||||
defaultButton: OriginalDialogs.StandardButton.No,
|
||||
title: "Overwrite File",
|
||||
text: path + "\n" + "This file already exists. Do you want to overwrite it?"
|
||||
});
|
||||
object.selected.connect(function(button) {
|
||||
if (button === OriginalDialogs.StandardButton.Yes) {
|
||||
callback();
|
||||
|
@ -144,14 +151,13 @@ Rectangle {
|
|||
|
||||
function reload() {
|
||||
Assets.mappingModel.refresh();
|
||||
treeView.selection.clear();
|
||||
}
|
||||
|
||||
function handleGetMappingsError(errorString) {
|
||||
errorMessageBox(
|
||||
"There was a problem retreiving the list of assets from your Asset Server.\n"
|
||||
+ errorString
|
||||
);
|
||||
"There was a problem retreiving the list of assets from your Asset Server.\n"
|
||||
+ errorString
|
||||
);
|
||||
}
|
||||
|
||||
function addToWorld() {
|
||||
|
@ -179,25 +185,25 @@ Rectangle {
|
|||
var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH;
|
||||
var DYNAMIC_DEFAULT = false;
|
||||
var prompt = tabletRoot.customInputDialog({
|
||||
textInput: {
|
||||
label: "Model URL",
|
||||
text: defaultURL
|
||||
},
|
||||
comboBox: {
|
||||
label: "Automatic Collisions",
|
||||
index: SHAPE_TYPE_DEFAULT,
|
||||
items: SHAPE_TYPES
|
||||
},
|
||||
checkBox: {
|
||||
label: "Dynamic",
|
||||
checked: DYNAMIC_DEFAULT,
|
||||
disableForItems: [
|
||||
SHAPE_TYPE_STATIC_MESH
|
||||
],
|
||||
checkStateOnDisable: false,
|
||||
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors"
|
||||
}
|
||||
});
|
||||
textInput: {
|
||||
label: "Model URL",
|
||||
text: defaultURL
|
||||
},
|
||||
comboBox: {
|
||||
label: "Automatic Collisions",
|
||||
index: SHAPE_TYPE_DEFAULT,
|
||||
items: SHAPE_TYPES
|
||||
},
|
||||
checkBox: {
|
||||
label: "Dynamic",
|
||||
checked: DYNAMIC_DEFAULT,
|
||||
disableForItems: [
|
||||
SHAPE_TYPE_STATIC_MESH
|
||||
],
|
||||
checkStateOnDisable: false,
|
||||
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors"
|
||||
}
|
||||
});
|
||||
|
||||
prompt.selected.connect(function (jsonResult) {
|
||||
if (jsonResult) {
|
||||
|
@ -205,23 +211,23 @@ Rectangle {
|
|||
var url = result.textInput.trim();
|
||||
var shapeType;
|
||||
switch (result.comboBox) {
|
||||
case SHAPE_TYPE_SIMPLE_HULL:
|
||||
shapeType = "simple-hull";
|
||||
break;
|
||||
case SHAPE_TYPE_SIMPLE_COMPOUND:
|
||||
shapeType = "simple-compound";
|
||||
break;
|
||||
case SHAPE_TYPE_STATIC_MESH:
|
||||
shapeType = "static-mesh";
|
||||
break;
|
||||
case SHAPE_TYPE_BOX:
|
||||
shapeType = "box";
|
||||
break;
|
||||
case SHAPE_TYPE_SPHERE:
|
||||
shapeType = "sphere";
|
||||
break;
|
||||
default:
|
||||
shapeType = "none";
|
||||
case SHAPE_TYPE_SIMPLE_HULL:
|
||||
shapeType = "simple-hull";
|
||||
break;
|
||||
case SHAPE_TYPE_SIMPLE_COMPOUND:
|
||||
shapeType = "simple-compound";
|
||||
break;
|
||||
case SHAPE_TYPE_STATIC_MESH:
|
||||
shapeType = "static-mesh";
|
||||
break;
|
||||
case SHAPE_TYPE_BOX:
|
||||
shapeType = "box";
|
||||
break;
|
||||
case SHAPE_TYPE_SPHERE:
|
||||
shapeType = "sphere";
|
||||
break;
|
||||
default:
|
||||
shapeType = "none";
|
||||
}
|
||||
|
||||
var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT;
|
||||
|
@ -230,7 +236,7 @@ Rectangle {
|
|||
print("Error: model cannot be both static mesh and dynamic. This should never happen.");
|
||||
} else if (url) {
|
||||
var name = assetProxyModel.data(treeView.selection.currentIndex);
|
||||
var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(MyAvatar.orientation)));
|
||||
var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)));
|
||||
var gravity;
|
||||
if (dynamic) {
|
||||
// Create a vector <0, -10, 0>. { x: 0, y: -10, z: 0 } won't work because Qt is dumb and this is a
|
||||
|
@ -293,10 +299,10 @@ Rectangle {
|
|||
}
|
||||
|
||||
var object = tabletRoot.inputDialog({
|
||||
label: "Enter new path:",
|
||||
current: path,
|
||||
placeholderText: "Enter path here"
|
||||
});
|
||||
label: "Enter new path:",
|
||||
current: path,
|
||||
placeholderText: "Enter path here"
|
||||
});
|
||||
object.selected.connect(function(destinationPath) {
|
||||
destinationPath = destinationPath.trim();
|
||||
|
||||
|
@ -339,12 +345,12 @@ Rectangle {
|
|||
}
|
||||
|
||||
var object = tabletRoot.messageBox({
|
||||
icon: hifi.icons.question,
|
||||
buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No,
|
||||
defaultButton: OriginalDialogs.StandardButton.Yes,
|
||||
title: "Delete",
|
||||
text: modalMessage
|
||||
});
|
||||
icon: hifi.icons.question,
|
||||
buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No,
|
||||
defaultButton: OriginalDialogs.StandardButton.Yes,
|
||||
title: "Delete",
|
||||
text: modalMessage
|
||||
});
|
||||
object.selected.connect(function(button) {
|
||||
if (button === OriginalDialogs.StandardButton.Yes) {
|
||||
doDeleteFile(path);
|
||||
|
@ -379,38 +385,38 @@ Rectangle {
|
|||
var filename = fileUrl.slice(fileUrl.lastIndexOf('/') + 1);
|
||||
|
||||
Assets.uploadFile(fileUrl, directory + filename,
|
||||
function() {
|
||||
// Upload started
|
||||
uploadSpinner.visible = true;
|
||||
uploadButton.enabled = false;
|
||||
uploadProgressLabel.text = "In progress...";
|
||||
},
|
||||
function(err, path) {
|
||||
print(err, path);
|
||||
if (err === "") {
|
||||
uploadProgressLabel.text = "Upload Complete";
|
||||
timer.interval = 1000;
|
||||
timer.repeat = false;
|
||||
timer.triggered.connect(function() {
|
||||
uploadSpinner.visible = false;
|
||||
uploadButton.enabled = true;
|
||||
uploadOpen = false;
|
||||
});
|
||||
timer.start();
|
||||
console.log("Asset Browser - finished uploading: ", fileUrl);
|
||||
reload();
|
||||
} else {
|
||||
uploadSpinner.visible = false;
|
||||
uploadButton.enabled = true;
|
||||
uploadOpen = false;
|
||||
function() {
|
||||
// Upload started
|
||||
uploadSpinner.visible = true;
|
||||
uploadButton.enabled = false;
|
||||
uploadProgressLabel.text = "In progress...";
|
||||
},
|
||||
function(err, path) {
|
||||
print(err, path);
|
||||
if (err === "") {
|
||||
uploadProgressLabel.text = "Upload Complete";
|
||||
timer.interval = 1000;
|
||||
timer.repeat = false;
|
||||
timer.triggered.connect(function() {
|
||||
uploadSpinner.visible = false;
|
||||
uploadButton.enabled = true;
|
||||
uploadOpen = false;
|
||||
});
|
||||
timer.start();
|
||||
console.log("Asset Browser - finished uploading: ", fileUrl);
|
||||
reload();
|
||||
} else {
|
||||
uploadSpinner.visible = false;
|
||||
uploadButton.enabled = true;
|
||||
uploadOpen = false;
|
||||
|
||||
if (err !== -1) {
|
||||
console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err);
|
||||
var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + err);
|
||||
box.selected.connect(reload);
|
||||
}
|
||||
}
|
||||
}, dropping);
|
||||
if (err !== -1) {
|
||||
console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err);
|
||||
var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + err);
|
||||
box.selected.connect(reload);
|
||||
}
|
||||
}
|
||||
}, dropping);
|
||||
}
|
||||
|
||||
function initiateUpload(url) {
|
||||
|
@ -421,9 +427,9 @@ Rectangle {
|
|||
doUpload(fileUrl, true);
|
||||
} else {
|
||||
var browser = tabletRoot.fileDialog({
|
||||
selectDirectory: false,
|
||||
dir: currentDirectory
|
||||
});
|
||||
selectDirectory: false,
|
||||
dir: currentDirectory
|
||||
});
|
||||
|
||||
browser.canceled.connect(function() {
|
||||
uploadOpen = false;
|
||||
|
@ -445,11 +451,11 @@ Rectangle {
|
|||
|
||||
function errorMessageBox(message) {
|
||||
return tabletRoot.messageBox({
|
||||
icon: hifi.icons.warning,
|
||||
defaultButton: OriginalDialogs.StandardButton.Ok,
|
||||
title: "Error",
|
||||
text: message
|
||||
});
|
||||
icon: hifi.icons.warning,
|
||||
defaultButton: OriginalDialogs.StandardButton.Ok,
|
||||
title: "Error",
|
||||
text: message
|
||||
});
|
||||
}
|
||||
|
||||
Column {
|
||||
|
@ -469,15 +475,6 @@ Rectangle {
|
|||
height: 30
|
||||
spacing: hifi.dimensions.contentSpacing.x
|
||||
|
||||
HifiControls.GlyphButton {
|
||||
glyph: hifi.glyphs.reload
|
||||
color: hifi.buttons.black
|
||||
colorScheme: root.colorScheme
|
||||
width: hifi.dimensions.controlLineHeight
|
||||
|
||||
onClicked: root.reload()
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
text: "Add To World"
|
||||
color: hifi.buttons.black
|
||||
|
@ -511,7 +508,180 @@ Rectangle {
|
|||
enabled: treeView.selection.hasSelection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Tree {
|
||||
id: treeView
|
||||
anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
treeModel: assetProxyModel
|
||||
selectionMode: SelectionMode.ExtendedSelection
|
||||
headerVisible: true
|
||||
sortIndicatorVisible: true
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
modifyEl: renameEl
|
||||
|
||||
TableViewColumn {
|
||||
id: nameColumn
|
||||
title: "Name:"
|
||||
role: "name"
|
||||
width: treeView.width - bakedColumn.width;
|
||||
}
|
||||
TableViewColumn {
|
||||
id: bakedColumn
|
||||
title: "Use Baked?"
|
||||
role: "baked"
|
||||
width: 100
|
||||
}
|
||||
|
||||
itemDelegate: Loader {
|
||||
id: itemDelegateLoader
|
||||
|
||||
anchors {
|
||||
left: parent ? parent.left : undefined
|
||||
leftMargin: (styleData.column === 0 ? (2 + styleData.depth) : 1) * hifi.dimensions.tablePadding
|
||||
right: parent ? parent.right : undefined
|
||||
rightMargin: hifi.dimensions.tablePadding
|
||||
verticalCenter: parent ? parent.verticalCenter : undefined
|
||||
}
|
||||
|
||||
function convertToGlyph(text) {
|
||||
switch (text) {
|
||||
case "Not Baked":
|
||||
return hifi.glyphs.circleSlash;
|
||||
case "Baked":
|
||||
return hifi.glyphs.check_2_01;
|
||||
case "Error":
|
||||
return hifi.glyphs.alert;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function getComponent() {
|
||||
if ((styleData.column === 0) && styleData.selected) {
|
||||
return textFieldComponent;
|
||||
} else if (convertToGlyph(styleData.value) != "") {
|
||||
return glyphComponent;
|
||||
} else {
|
||||
return labelComponent;
|
||||
}
|
||||
|
||||
}
|
||||
sourceComponent: getComponent()
|
||||
|
||||
Component {
|
||||
id: labelComponent
|
||||
FiraSansSemiBold {
|
||||
text: styleData.value
|
||||
size: hifi.fontSizes.tableText
|
||||
color: colorScheme == hifi.colorSchemes.light
|
||||
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
|
||||
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
|
||||
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: styleData.column === 1 ? TextInput.AlignHCenter : TextInput.AlignLeft
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: glyphComponent
|
||||
|
||||
HiFiGlyphs {
|
||||
text: convertToGlyph(styleData.value)
|
||||
size: hifi.dimensions.frameIconSize
|
||||
color: colorScheme == hifi.colorSchemes.light
|
||||
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
|
||||
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
|
||||
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: TextInput.AlignHCenter
|
||||
|
||||
HifiControls.ToolTip {
|
||||
anchors.fill: parent
|
||||
|
||||
visible: styleData.value === "Error"
|
||||
|
||||
toolTip: assetProxyModel.data(styleData.index, 0x106)
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: textFieldComponent
|
||||
|
||||
TextField {
|
||||
id: textField
|
||||
readOnly: !activeFocus
|
||||
|
||||
text: styleData.value
|
||||
|
||||
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
|
||||
font.family: firaSansSemiBold.name
|
||||
font.pixelSize: hifi.fontSizes.textFieldInput
|
||||
height: hifi.dimensions.tableRowHeight
|
||||
|
||||
style: TextFieldStyle {
|
||||
textColor: readOnly
|
||||
? hifi.colors.black
|
||||
: (treeView.isLightColorScheme ? hifi.colors.black : hifi.colors.white)
|
||||
background: Rectangle {
|
||||
visible: !readOnly
|
||||
color: treeView.isLightColorScheme ? hifi.colors.white : hifi.colors.black
|
||||
border.color: hifi.colors.primaryHighlight
|
||||
border.width: 1
|
||||
}
|
||||
selectedTextColor: hifi.colors.black
|
||||
selectionColor: hifi.colors.primaryHighlight
|
||||
padding.left: readOnly ? 0 : hifi.dimensions.textPadding
|
||||
padding.right: readOnly ? 0 : hifi.dimensions.textPadding
|
||||
}
|
||||
|
||||
validator: RegExpValidator {
|
||||
regExp: /[^/]+/
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key == Qt.Key_Escape) {
|
||||
text = styleData.value;
|
||||
unfocusHelper.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
if (acceptableInput && styleData.selected) {
|
||||
if (!treeView.modifyEl(treeView.selection.currentIndex, text)) {
|
||||
text = styleData.value;
|
||||
}
|
||||
unfocusHelper.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
onReadOnlyChanged: {
|
||||
// Have to explicily set keyboardRaised because automatic setting fails because readOnly is true at the time.
|
||||
keyboardRaised = activeFocus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MouseArea {
|
||||
propagateComposedEvents: true
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
if (!HMD.active) { // Popup only displays properly on desktop
|
||||
var index = treeView.indexAt(mouse.x, mouse.y);
|
||||
treeView.selection.setCurrentIndex(index, 0x0002);
|
||||
contextMenu.currentIndex = index;
|
||||
contextMenu.popup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: contextMenu
|
||||
title: "Edit"
|
||||
|
@ -539,39 +709,50 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
HifiControls.Tree {
|
||||
id: treeView
|
||||
height: 290
|
||||
anchors.leftMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border
|
||||
anchors.rightMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
treeModel: assetProxyModel
|
||||
canEdit: true
|
||||
colorScheme: root.colorScheme
|
||||
selectionMode: SelectionMode.ExtendedSelection
|
||||
Row {
|
||||
id: infoRow
|
||||
anchors.left: treeView.left
|
||||
anchors.right: treeView.right
|
||||
anchors.bottomMargin: hifi.dimensions.contentSpacing.y
|
||||
spacing: hifi.dimensions.contentSpacing.x
|
||||
|
||||
RalewayRegular {
|
||||
size: hifi.fontSizes.sectionName
|
||||
font.capitalization: Font.AllUppercase
|
||||
text: selectedItems + " items selected"
|
||||
color: hifi.colors.lightGrayText
|
||||
}
|
||||
|
||||
modifyEl: renameEl
|
||||
HifiControls.CheckBox {
|
||||
function isChecked() {
|
||||
var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105);
|
||||
var bakingDisabled = (status === "Not Baked" || status === "--");
|
||||
return selectedItems === 1 && !bakingDisabled;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
propagateComposedEvents: true
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
text: "Use baked (optimized) versions"
|
||||
colorScheme: root.colorScheme
|
||||
enabled: selectedItems === 1 && assetProxyModel.data(treeView.selection.currentIndex, 0x105) !== "--"
|
||||
checked: isChecked()
|
||||
onClicked: {
|
||||
if (!HMD.active) { // Popup only displays properly on desktop
|
||||
var index = treeView.indexAt(mouse.x, mouse.y);
|
||||
treeView.selection.setCurrentIndex(index, 0x0002);
|
||||
contextMenu.currentIndex = index;
|
||||
contextMenu.popup();
|
||||
var mappings = [];
|
||||
for (var i in treeView.selection.selectedIndexes) {
|
||||
var index = treeView.selection.selectedIndexes[i];
|
||||
var path = assetProxyModel.data(index, 0x100);
|
||||
mappings.push(path);
|
||||
}
|
||||
print("Setting baking enabled:" + mappings + " " + checked);
|
||||
Assets.setBakingEnabled(mappings, checked, function() {
|
||||
reload();
|
||||
});
|
||||
|
||||
checked = Qt.binding(isChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
HifiControls.TabletContentSection {
|
||||
id: uploadSection
|
||||
name: "Upload A File"
|
||||
|
|
|
@ -400,6 +400,17 @@ Rectangle {
|
|||
colorScheme: hifi.colorSchemes.dark
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
TableViewColumn {
|
||||
role: "display";
|
||||
}
|
||||
|
||||
onActivated: {
|
||||
var path = scriptsModel.data(index, 0x100)
|
||||
if (path) {
|
||||
loadScript(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.VerticalSpacer {
|
||||
|
|
|
@ -338,5 +338,6 @@ Item {
|
|||
readonly property string stop_square: "\ue01e"
|
||||
readonly property string avatarTPose: "\ue01f"
|
||||
readonly property string lock: "\ue006"
|
||||
readonly property string check_2_01: "\ue020"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2196,6 +2196,14 @@ float MyAvatar::getDomainMaxScale() {
|
|||
return _domainMaximumScale;
|
||||
}
|
||||
|
||||
void MyAvatar::setGravity(float gravity) {
|
||||
_characterController.setGravity(gravity);
|
||||
}
|
||||
|
||||
float MyAvatar::getGravity() {
|
||||
return _characterController.getGravity();
|
||||
}
|
||||
|
||||
void MyAvatar::increaseSize() {
|
||||
// make sure we're starting from an allowable scale
|
||||
clampTargetScaleToDomainLimits();
|
||||
|
|
|
@ -152,7 +152,7 @@ class MyAvatar : public Avatar {
|
|||
|
||||
Q_PROPERTY(float userHeight READ getUserHeight WRITE setUserHeight)
|
||||
Q_PROPERTY(float userEyeHeight READ getUserEyeHeight)
|
||||
|
||||
|
||||
const QString DOMINANT_LEFT_HAND = "left";
|
||||
const QString DOMINANT_RIGHT_HAND = "right";
|
||||
|
||||
|
@ -551,6 +551,9 @@ public slots:
|
|||
float getDomainMinScale();
|
||||
float getDomainMaxScale();
|
||||
|
||||
void setGravity(float gravity);
|
||||
float getGravity();
|
||||
|
||||
void goToLocation(const glm::vec3& newPosition,
|
||||
bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(),
|
||||
bool shouldFaceLocation = false);
|
||||
|
|
|
@ -19,20 +19,12 @@
|
|||
#include <AssetUpload.h>
|
||||
#include <MappingRequest.h>
|
||||
#include <NetworkLogging.h>
|
||||
#include <NodeList.h>
|
||||
#include <OffscreenUi.h>
|
||||
|
||||
void AssetMappingModel::clear() {
|
||||
// make sure we are on the same thread before we touch the hash
|
||||
if (thread() != QThread::currentThread()) {
|
||||
QMetaObject::invokeMethod(this, "clear");
|
||||
return;
|
||||
}
|
||||
static const int AUTO_REFRESH_INTERVAL = 1000;
|
||||
|
||||
qDebug() << "Clearing loaded asset mappings for Asset Browser";
|
||||
|
||||
_pathToItemMap.clear();
|
||||
QStandardItemModel::clear();
|
||||
}
|
||||
int assetMappingModelMetatypeId = qRegisterMetaType<AssetMappingModel*>("AssetMappingModel*");
|
||||
|
||||
AssetMappingsScriptingInterface::AssetMappingsScriptingInterface() {
|
||||
_proxyModel.setSourceModel(&_assetMappingModel);
|
||||
|
@ -154,7 +146,7 @@ void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) {
|
|||
auto map = callback.engine()->newObject();
|
||||
|
||||
for (auto& kv : mappings ) {
|
||||
map.setProperty(kv.first, kv.second);
|
||||
map.setProperty(kv.first, kv.second.hash);
|
||||
}
|
||||
|
||||
if (callback.isCallable()) {
|
||||
|
@ -174,7 +166,7 @@ void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString new
|
|||
|
||||
connect(request, &RenameMappingRequest::finished, this, [this, callback](RenameMappingRequest* request) mutable {
|
||||
if (callback.isCallable()) {
|
||||
QJSValueList args { request->getErrorString() };
|
||||
QJSValueList args{ request->getErrorString() };
|
||||
callback.call(args);
|
||||
}
|
||||
|
||||
|
@ -184,6 +176,49 @@ void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString new
|
|||
request->start();
|
||||
}
|
||||
|
||||
void AssetMappingsScriptingInterface::setBakingEnabled(QStringList paths, bool enabled, QJSValue callback) {
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto request = assetClient->createSetBakingEnabledRequest(paths, enabled);
|
||||
|
||||
connect(request, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable {
|
||||
if (callback.isCallable()) {
|
||||
QJSValueList args{ request->getErrorString() };
|
||||
callback.call(args);
|
||||
}
|
||||
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->start();
|
||||
}
|
||||
|
||||
AssetMappingModel::AssetMappingModel() {
|
||||
setupRoles();
|
||||
|
||||
connect(&_autoRefreshTimer, &QTimer::timeout, this, [this] {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
auto assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
|
||||
if (assetServer) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
_autoRefreshTimer.setInterval(AUTO_REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
bool AssetMappingModel::isAutoRefreshEnabled() {
|
||||
return _autoRefreshTimer.isActive();
|
||||
}
|
||||
|
||||
void AssetMappingModel::setAutoRefreshEnabled(bool enabled) {
|
||||
if (enabled != _autoRefreshTimer.isActive()) {
|
||||
if (enabled) {
|
||||
_autoRefreshTimer.start();
|
||||
} else {
|
||||
_autoRefreshTimer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetMappingModel::isKnownFolder(QString path) const {
|
||||
if (!path.endsWith("/")) {
|
||||
return false;
|
||||
|
@ -198,10 +233,7 @@ bool AssetMappingModel::isKnownFolder(QString path) const {
|
|||
return false;
|
||||
}
|
||||
|
||||
int assetMappingModelMetatypeId = qRegisterMetaType<AssetMappingModel*>("AssetMappingModel*");
|
||||
|
||||
void AssetMappingModel::refresh() {
|
||||
qDebug() << "Refreshing asset mapping model";
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto request = assetClient->createGetAllMappingsRequest();
|
||||
|
||||
|
@ -211,6 +243,12 @@ void AssetMappingModel::refresh() {
|
|||
auto existingPaths = _pathToItemMap.keys();
|
||||
for (auto& mapping : mappings) {
|
||||
auto& path = mapping.first;
|
||||
|
||||
if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) {
|
||||
// Hide baked mappings
|
||||
continue;
|
||||
}
|
||||
|
||||
auto parts = path.split("/");
|
||||
auto length = parts.length();
|
||||
|
||||
|
@ -223,27 +261,32 @@ void AssetMappingModel::refresh() {
|
|||
// start index at 1 to avoid empty string from leading slash
|
||||
for (int i = 1; i < length; ++i) {
|
||||
fullPath += (i == 1 ? "" : "/") + parts[i];
|
||||
bool isFolder = i < length - 1;
|
||||
|
||||
auto it = _pathToItemMap.find(fullPath);
|
||||
if (it == _pathToItemMap.end()) {
|
||||
auto item = new QStandardItem(parts[i]);
|
||||
bool isFolder = i < length - 1;
|
||||
item->setData(isFolder ? fullPath + "/" : fullPath, Qt::UserRole);
|
||||
item->setData(isFolder, Qt::UserRole + 1);
|
||||
item->setData(parts[i], Qt::UserRole + 2);
|
||||
item->setData("atp:" + fullPath, Qt::UserRole + 3);
|
||||
item->setData(fullPath, Qt::UserRole + 4);
|
||||
|
||||
if (lastItem) {
|
||||
lastItem->setChild(lastItem->rowCount(), 0, item);
|
||||
lastItem->appendRow(item);
|
||||
} else {
|
||||
appendRow(item);
|
||||
}
|
||||
|
||||
lastItem = item;
|
||||
_pathToItemMap[fullPath] = lastItem;
|
||||
} else {
|
||||
lastItem = it.value();
|
||||
}
|
||||
|
||||
// update status
|
||||
auto statusString = isFolder ? "--" : bakingStatusToString(mapping.second.status);
|
||||
lastItem->setData(statusString, Qt::UserRole + 5);
|
||||
lastItem->setData(mapping.second.bakingErrors, Qt::UserRole + 6);
|
||||
}
|
||||
|
||||
Q_ASSERT(fullPath == path);
|
||||
|
@ -295,8 +338,30 @@ void AssetMappingModel::refresh() {
|
|||
emit errorGettingMappings(request->getErrorString());
|
||||
}
|
||||
|
||||
emit updated();
|
||||
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->start();
|
||||
}
|
||||
|
||||
void AssetMappingModel::clear() {
|
||||
// make sure we are on the same thread before we touch the hash
|
||||
if (thread() != QThread::currentThread()) {
|
||||
QMetaObject::invokeMethod(this, "clear");
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Clearing loaded asset mappings for Asset Browser";
|
||||
|
||||
_pathToItemMap.clear();
|
||||
QStandardItemModel::clear();
|
||||
}
|
||||
|
||||
void AssetMappingModel::setupRoles() {
|
||||
QHash<int, QByteArray> roleNames;
|
||||
roleNames[Qt::DisplayRole] = "name";
|
||||
roleNames[Qt::UserRole + 5] = "baked";
|
||||
setItemRoleNames(roleNames);
|
||||
}
|
|
@ -25,9 +25,16 @@
|
|||
|
||||
class AssetMappingModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool autoRefreshEnabled READ isAutoRefreshEnabled WRITE setAutoRefreshEnabled)
|
||||
|
||||
public:
|
||||
AssetMappingModel();
|
||||
|
||||
Q_INVOKABLE void refresh();
|
||||
|
||||
bool isAutoRefreshEnabled();
|
||||
void setAutoRefreshEnabled(bool enabled);
|
||||
|
||||
bool isKnownMapping(QString path) const { return _pathToItemMap.contains(path); }
|
||||
bool isKnownFolder(QString path) const;
|
||||
|
||||
|
@ -36,9 +43,13 @@ public slots:
|
|||
|
||||
signals:
|
||||
void errorGettingMappings(QString errorString);
|
||||
void updated();
|
||||
|
||||
private:
|
||||
void setupRoles();
|
||||
|
||||
QHash<QString, QStandardItem*> _pathToItemMap;
|
||||
QTimer _autoRefreshTimer;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(AssetMappingModel*)
|
||||
|
@ -61,10 +72,11 @@ public:
|
|||
Q_INVOKABLE void setMapping(QString path, QString hash, QJSValue callback = QJSValue());
|
||||
Q_INVOKABLE void getMapping(QString path, QJSValue callback = QJSValue());
|
||||
Q_INVOKABLE void uploadFile(QString path, QString mapping, QJSValue startedCallback = QJSValue(), QJSValue completedCallback = QJSValue(), bool dropEvent = false);
|
||||
Q_INVOKABLE void deleteMappings(QStringList paths, QJSValue callback);
|
||||
Q_INVOKABLE void deleteMappings(QStringList paths, QJSValue callback = QJSValue());
|
||||
Q_INVOKABLE void deleteMapping(QString path, QJSValue callback) { deleteMappings(QStringList(path), callback = QJSValue()); }
|
||||
Q_INVOKABLE void getAllMappings(QJSValue callback = QJSValue());
|
||||
Q_INVOKABLE void renameMapping(QString oldPath, QString newPath, QJSValue callback = QJSValue());
|
||||
Q_INVOKABLE void setBakingEnabled(QStringList paths, bool enabled, QJSValue callback = QJSValue());
|
||||
|
||||
protected:
|
||||
QSet<AssetRequest*> _pendingRequests;
|
||||
|
|
|
@ -343,30 +343,6 @@ void setupPreferences() {
|
|||
preferences->addPreference(preference);
|
||||
}
|
||||
}
|
||||
{
|
||||
auto getter = []()->bool { return image::isColorTexturesCompressionEnabled(); };
|
||||
auto setter = [](bool value) { return image::setColorTexturesCompressionEnabled(value); };
|
||||
auto preference = new CheckPreference(RENDER, "Compress Color Textures", getter, setter);
|
||||
preferences->addPreference(preference);
|
||||
}
|
||||
{
|
||||
auto getter = []()->bool { return image::isNormalTexturesCompressionEnabled(); };
|
||||
auto setter = [](bool value) { return image::setNormalTexturesCompressionEnabled(value); };
|
||||
auto preference = new CheckPreference(RENDER, "Compress Normal Textures", getter, setter);
|
||||
preferences->addPreference(preference);
|
||||
}
|
||||
{
|
||||
auto getter = []()->bool { return image::isGrayscaleTexturesCompressionEnabled(); };
|
||||
auto setter = [](bool value) { return image::setGrayscaleTexturesCompressionEnabled(value); };
|
||||
auto preference = new CheckPreference(RENDER, "Compress Grayscale Textures", getter, setter);
|
||||
preferences->addPreference(preference);
|
||||
}
|
||||
{
|
||||
auto getter = []()->bool { return image::isCubeTexturesCompressionEnabled(); };
|
||||
auto setter = [](bool value) { return image::setCubeTexturesCompressionEnabled(value); };
|
||||
auto preference = new CheckPreference(RENDER, "Compress Cube Textures", getter, setter);
|
||||
preferences->addPreference(preference);
|
||||
}
|
||||
}
|
||||
{
|
||||
static const QString RENDER("Networking");
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
#include <EntityTreeRenderer.h>
|
||||
#include <NetworkingConstants.h>
|
||||
|
||||
#ifndef MIN
|
||||
#define MIN(a,b) ((a) < (b) ? (a) : (b))
|
||||
#endif
|
||||
|
||||
static const float CONTEXT_OVERLAY_TABLET_OFFSET = 30.0f; // Degrees
|
||||
static const float CONTEXT_OVERLAY_TABLET_ORIENTATION = 210.0f; // Degrees
|
||||
static const float CONTEXT_OVERLAY_TABLET_DISTANCE = 0.65F; // Meters
|
||||
|
@ -38,11 +42,6 @@ ContextOverlayInterface::ContextOverlayInterface() {
|
|||
_entityPropertyFlags += PROP_DIMENSIONS;
|
||||
_entityPropertyFlags += PROP_REGISTRATION_POINT;
|
||||
|
||||
// initially, set _enabled to match the switch. Later we enable/disable via the getter/setters
|
||||
// if we are in edit or pal (for instance). Note this is temporary, as we expect to enable this all
|
||||
// the time after getting edge highlighting, etc...
|
||||
_enabled = _settingSwitch.get();
|
||||
|
||||
auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>().data();
|
||||
connect(entityTreeRenderer, SIGNAL(mousePressOnEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(createOrDestroyContextOverlay(const EntityItemID&, const PointerEvent&)));
|
||||
connect(entityTreeRenderer, SIGNAL(hoverEnterEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(contextOverlays_hoverEnterEntity(const EntityItemID&, const PointerEvent&)));
|
||||
|
@ -65,40 +64,36 @@ ContextOverlayInterface::ContextOverlayInterface() {
|
|||
connect(_selectionScriptingInterface.data(), &SelectionScriptingInterface::selectedItemsListChanged, &_selectionToSceneHandler, &SelectionToSceneHandler::selectedItemsListChanged);
|
||||
}
|
||||
|
||||
static const uint32_t MOUSE_HW_ID = 0;
|
||||
static const uint32_t LEFT_HAND_HW_ID = 1;
|
||||
static const xColor CONTEXT_OVERLAY_COLOR = { 255, 255, 255 };
|
||||
static const float CONTEXT_OVERLAY_INSIDE_DISTANCE = 1.0f; // in meters
|
||||
static const float CONTEXT_OVERLAY_CLOSE_DISTANCE = 1.5f; // in meters
|
||||
static const float CONTEXT_OVERLAY_CLOSE_SIZE = 0.12f; // in meters, same x and y dims
|
||||
static const float CONTEXT_OVERLAY_FAR_SIZE = 0.08f; // in meters, same x and y dims
|
||||
static const float CONTEXT_OVERLAY_CLOSE_OFFSET_ANGLE = 20.0f;
|
||||
static const float CONTEXT_OVERLAY_SIZE = 0.09f; // in meters, same x and y dims
|
||||
static const float CONTEXT_OVERLAY_OFFSET_DISTANCE = 0.1f;
|
||||
static const float CONTEXT_OVERLAY_OFFSET_ANGLE = 5.0f;
|
||||
static const float CONTEXT_OVERLAY_UNHOVERED_ALPHA = 0.85f;
|
||||
static const float CONTEXT_OVERLAY_HOVERED_ALPHA = 1.0f;
|
||||
static const float CONTEXT_OVERLAY_UNHOVERED_PULSEMIN = 0.6f;
|
||||
static const float CONTEXT_OVERLAY_UNHOVERED_PULSEMAX = 1.0f;
|
||||
static const float CONTEXT_OVERLAY_UNHOVERED_PULSEPERIOD = 1.0f;
|
||||
static const float CONTEXT_OVERLAY_UNHOVERED_COLORPULSE = 1.0f;
|
||||
static const float CONTEXT_OVERLAY_FAR_OFFSET = 0.1f;
|
||||
|
||||
void ContextOverlayInterface::setEnabled(bool enabled) {
|
||||
// only enable/disable if the setting in 'on'. If it is 'off',
|
||||
// make sure _enabled is always false.
|
||||
if (_settingSwitch.get()) {
|
||||
_enabled = enabled;
|
||||
} else {
|
||||
_enabled = false;
|
||||
}
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event) {
|
||||
if (_enabled && event.getButton() == PointerEvent::SecondaryButton) {
|
||||
if (contextOverlayFilterPassed(entityItemID)) {
|
||||
if (event.getID() == MOUSE_HW_ID) {
|
||||
enableEntityHighlight(entityItemID);
|
||||
}
|
||||
|
||||
qCDebug(context_overlay) << "Creating Context Overlay on top of entity with ID: " << entityItemID;
|
||||
|
||||
// Add all necessary variables to the stack
|
||||
EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityItemID, _entityPropertyFlags);
|
||||
glm::vec3 cameraPosition = qApp->getCamera().getPosition();
|
||||
float distanceFromCameraToEntity = glm::distance(entityProperties.getPosition(), cameraPosition);
|
||||
glm::vec3 entityDimensions = entityProperties.getDimensions();
|
||||
glm::vec3 entityPosition = entityProperties.getPosition();
|
||||
glm::vec3 contextOverlayPosition = entityProperties.getPosition();
|
||||
|
@ -131,27 +126,22 @@ bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID&
|
|||
// If the camera is inside the box...
|
||||
// ...position the Context Overlay 1 meter in front of the camera.
|
||||
contextOverlayPosition = cameraPosition + CONTEXT_OVERLAY_INSIDE_DISTANCE * (qApp->getCamera().getOrientation() * Vectors::FRONT);
|
||||
contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_CLOSE_SIZE, CONTEXT_OVERLAY_CLOSE_SIZE) * glm::distance(contextOverlayPosition, cameraPosition);
|
||||
} else if (distanceFromCameraToEntity < CONTEXT_OVERLAY_CLOSE_DISTANCE) {
|
||||
// Else if the entity is too close to the camera...
|
||||
// ...rotate the Context Overlay to the right of the entity.
|
||||
// This makes it easy to inspect things you're holding.
|
||||
float offsetAngle = -CONTEXT_OVERLAY_CLOSE_OFFSET_ANGLE;
|
||||
if (event.getID() == LEFT_HAND_HW_ID) {
|
||||
offsetAngle *= -1;
|
||||
}
|
||||
contextOverlayPosition = (glm::quat(glm::radians(glm::vec3(0.0f, offsetAngle, 0.0f))) * (entityPosition - cameraPosition)) + cameraPosition;
|
||||
contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_CLOSE_SIZE, CONTEXT_OVERLAY_CLOSE_SIZE) * glm::distance(contextOverlayPosition, cameraPosition);
|
||||
contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_SIZE, CONTEXT_OVERLAY_SIZE) * glm::distance(contextOverlayPosition, cameraPosition);
|
||||
} else {
|
||||
// Else, place the Context Overlay some offset away from the entity's bounding
|
||||
// box in the direction of the camera.
|
||||
// Rotate the Context Overlay some number of degrees offset from the entity
|
||||
// along the line cast from your head to the entity's bounding box.
|
||||
glm::vec3 direction = glm::normalize(entityPosition - cameraPosition);
|
||||
float distance;
|
||||
BoxFace face;
|
||||
glm::vec3 normal;
|
||||
boundingBox.findRayIntersection(cameraPosition, direction, distance, face, normal);
|
||||
contextOverlayPosition = (cameraPosition + direction * distance) - direction * CONTEXT_OVERLAY_FAR_OFFSET;
|
||||
contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_FAR_SIZE, CONTEXT_OVERLAY_FAR_SIZE) * glm::distance(contextOverlayPosition, cameraPosition);
|
||||
float offsetAngle = -CONTEXT_OVERLAY_OFFSET_ANGLE;
|
||||
if (event.getID() == LEFT_HAND_HW_ID) {
|
||||
offsetAngle *= -1;
|
||||
}
|
||||
contextOverlayPosition = (glm::quat(glm::radians(glm::vec3(0.0f, offsetAngle, 0.0f)))) *
|
||||
((cameraPosition + direction * (distance - CONTEXT_OVERLAY_OFFSET_DISTANCE)));
|
||||
contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_SIZE, CONTEXT_OVERLAY_SIZE) * glm::distance(contextOverlayPosition, cameraPosition);
|
||||
}
|
||||
|
||||
// Finally, setup and draw the Context Overlay
|
||||
|
@ -176,6 +166,7 @@ bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID&
|
|||
}
|
||||
} else {
|
||||
if (!_currentEntityWithContextOverlay.isNull()) {
|
||||
disableEntityHighlight(_currentEntityWithContextOverlay);
|
||||
return destroyContextOverlay(_currentEntityWithContextOverlay, event);
|
||||
}
|
||||
return false;
|
||||
|
@ -237,13 +228,13 @@ void ContextOverlayInterface::contextOverlays_hoverLeaveOverlay(const OverlayID&
|
|||
}
|
||||
|
||||
void ContextOverlayInterface::contextOverlays_hoverEnterEntity(const EntityItemID& entityID, const PointerEvent& event) {
|
||||
if (contextOverlayFilterPassed(entityID)) {
|
||||
if (contextOverlayFilterPassed(entityID) && _enabled && event.getID() != MOUSE_HW_ID) {
|
||||
enableEntityHighlight(entityID);
|
||||
}
|
||||
}
|
||||
|
||||
void ContextOverlayInterface::contextOverlays_hoverLeaveEntity(const EntityItemID& entityID, const PointerEvent& event) {
|
||||
if (_currentEntityWithContextOverlay != entityID) {
|
||||
if (_currentEntityWithContextOverlay != entityID && _enabled && event.getID() != MOUSE_HW_ID) {
|
||||
disableEntityHighlight(entityID);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,8 +76,6 @@ private:
|
|||
|
||||
bool _isInMarketplaceInspectionMode { false };
|
||||
|
||||
Setting::Handle<bool> _settingSwitch { "inspectionMode", false };
|
||||
|
||||
void openMarketplace();
|
||||
void enableEntityHighlight(const EntityItemID& entityItemID);
|
||||
void disableEntityHighlight(const EntityItemID& entityItemID);
|
||||
|
|
10
libraries/baking/CMakeLists.txt
Normal file
10
libraries/baking/CMakeLists.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
set(TARGET_NAME baking)
|
||||
setup_hifi_library(Concurrent)
|
||||
|
||||
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})
|
|
@ -13,20 +13,49 @@
|
|||
|
||||
#include "Baker.h"
|
||||
|
||||
bool Baker::shouldStop() {
|
||||
if (_shouldAbort) {
|
||||
setWasAborted(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasErrors()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Baker::handleError(const QString& error) {
|
||||
qCCritical(model_baking).noquote() << error;
|
||||
_errorList.append(error);
|
||||
emit finished();
|
||||
setIsFinished(true);
|
||||
}
|
||||
|
||||
void Baker::handleErrors(const QStringList& errors) {
|
||||
// we're appending errors, presumably from a baking operation we called
|
||||
// add those to our list and emit that we are finished
|
||||
_errorList.append(errors);
|
||||
emit finished();
|
||||
setIsFinished(true);
|
||||
}
|
||||
|
||||
void Baker::handleWarning(const QString& warning) {
|
||||
qCWarning(model_baking).noquote() << warning;
|
||||
_warningList.append(warning);
|
||||
}
|
||||
|
||||
void Baker::setIsFinished(bool isFinished) {
|
||||
_isFinished.store(isFinished);
|
||||
|
||||
if (isFinished) {
|
||||
emit finished();
|
||||
}
|
||||
}
|
||||
|
||||
void Baker::setWasAborted(bool wasAborted) {
|
||||
_wasAborted.store(wasAborted);
|
||||
|
||||
if (wasAborted) {
|
||||
emit aborted();
|
||||
}
|
||||
}
|
|
@ -18,17 +18,30 @@ class Baker : public QObject {
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
bool shouldStop();
|
||||
|
||||
bool hasErrors() const { return !_errorList.isEmpty(); }
|
||||
QStringList getErrors() const { return _errorList; }
|
||||
|
||||
bool hasWarnings() const { return !_warningList.isEmpty(); }
|
||||
QStringList getWarnings() const { return _warningList; }
|
||||
|
||||
std::vector<QString> getOutputFiles() const { return _outputFiles; }
|
||||
|
||||
virtual void setIsFinished(bool isFinished);
|
||||
bool isFinished() const { return _isFinished.load(); }
|
||||
|
||||
virtual void setWasAborted(bool wasAborted);
|
||||
|
||||
bool wasAborted() const { return _wasAborted.load(); }
|
||||
|
||||
public slots:
|
||||
virtual void bake() = 0;
|
||||
virtual void abort() { _shouldAbort.store(true); }
|
||||
|
||||
signals:
|
||||
void finished();
|
||||
void aborted();
|
||||
|
||||
protected:
|
||||
void handleError(const QString& error);
|
||||
|
@ -36,8 +49,17 @@ protected:
|
|||
|
||||
void handleErrors(const QStringList& errors);
|
||||
|
||||
// List of baked output files. For instance, for an FBX this would
|
||||
// include the .fbx and all of its texture files.
|
||||
std::vector<QString> _outputFiles;
|
||||
|
||||
QStringList _errorList;
|
||||
QStringList _warningList;
|
||||
|
||||
std::atomic<bool> _isFinished { false };
|
||||
|
||||
std::atomic<bool> _shouldAbort { false };
|
||||
std::atomic<bool> _wasAborted { false };
|
||||
};
|
||||
|
||||
#endif // hifi_Baker_h
|
821
libraries/baking/src/FBXBaker.cpp
Normal file
821
libraries/baking/src/FBXBaker.cpp
Normal file
|
@ -0,0 +1,821 @@
|
|||
//
|
||||
// FBXBaker.cpp
|
||||
// tools/baking/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 <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 <FBXReader.h>
|
||||
#include <FBXWriter.h>
|
||||
|
||||
#include "ModelBakingLoggingCategory.h"
|
||||
#include "TextureBaker.h"
|
||||
|
||||
#include "FBXBaker.h"
|
||||
|
||||
#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) :
|
||||
_fbxURL(fbxURL),
|
||||
_bakedOutputDir(bakedOutputDir),
|
||||
_originalOutputDir(originalOutputDir),
|
||||
_textureThreadGetter(textureThreadGetter)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void FBXBaker::abort() {
|
||||
Baker::abort();
|
||||
|
||||
// tell our underlying TextureBaker instances to abort
|
||||
// the FBXBaker will wait until all are aborted before emitting its own abort signal
|
||||
for (auto& textureBaker : _bakingTextures) {
|
||||
textureBaker->abort();
|
||||
}
|
||||
}
|
||||
|
||||
void FBXBaker::bake() {
|
||||
qDebug() << "FBXBaker" << _fbxURL << "bake starting";
|
||||
|
||||
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 (shouldStop()) {
|
||||
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 (shouldStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// enumerate the models and textures found in the scene and start a bake for them
|
||||
rewriteAndBakeSceneTextures();
|
||||
|
||||
if (shouldStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
rewriteAndBakeSceneModels();
|
||||
|
||||
if (shouldStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// export the FBX with re-written texture references
|
||||
exportScene();
|
||||
|
||||
if (shouldStop()) {
|
||||
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
|
||||
if (QDir(_bakedOutputDir).exists()) {
|
||||
qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing.";
|
||||
} 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() {
|
||||
qDebug() << "file path: " << _originalFBXFilePath.toLocal8Bit().data() << QDir(_originalFBXFilePath).exists();
|
||||
|
||||
QFile fbxFile(_originalFBXFilePath);
|
||||
if (!fbxFile.open(QIODevice::ReadOnly)) {
|
||||
handleError("Error opening " + _originalFBXFilePath + " for reading");
|
||||
return;
|
||||
}
|
||||
|
||||
FBXReader reader;
|
||||
|
||||
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) {
|
||||
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, QString relativeFileName, bool isEmbedded) {
|
||||
|
||||
QUrl urlToTexture;
|
||||
|
||||
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
|
||||
|
||||
if (isEmbedded) {
|
||||
urlToTexture = _fbxURL.toString() + "/" + apparentRelativePath.filePath();
|
||||
} else {
|
||||
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
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
void FBXBaker::rewriteAndBakeSceneModels() {
|
||||
unsigned int meshIndex = 0;
|
||||
bool hasDeformers { false };
|
||||
for (FBXNode& rootChild : _rootNode.children) {
|
||||
if (rootChild.name == "Objects") {
|
||||
for (FBXNode& objectChild : rootChild.children) {
|
||||
if (objectChild.name == "Deformer") {
|
||||
hasDeformers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasDeformers) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (FBXNode& rootChild : _rootNode.children) {
|
||||
if (rootChild.name == "Objects") {
|
||||
for (FBXNode& objectChild : rootChild.children) {
|
||||
if (objectChild.name == "Geometry") {
|
||||
|
||||
// TODO Pull this out of _geometry instead so we don't have to reprocess it
|
||||
auto extractedMesh = FBXReader::extractMesh(objectChild, meshIndex, false);
|
||||
auto& mesh = extractedMesh.mesh;
|
||||
|
||||
if (mesh.wasCompressed) {
|
||||
handleError("Cannot re-bake a file that contains compressed mesh");
|
||||
return;
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
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) {
|
||||
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 };
|
||||
bool needsOriginalIndices { hasDeformers };
|
||||
|
||||
int normalsAttributeID { -1 };
|
||||
int colorsAttributeID { -1 };
|
||||
int texCoordsAttributeID { -1 };
|
||||
int texCoords1AttributeID { -1 };
|
||||
int faceMaterialAttributeID { -1 };
|
||||
int originalIndexAttributeID { -1 };
|
||||
|
||||
const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION,
|
||||
3, draco::DT_FLOAT32);
|
||||
if (needsOriginalIndices) {
|
||||
originalIndexAttributeID = meshBuilder.AddAttribute(
|
||||
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX,
|
||||
1, draco::DT_INT32);
|
||||
}
|
||||
|
||||
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];
|
||||
uint16_t materialID = matTex.first;
|
||||
|
||||
auto addFace = [&](QVector<int>& indices, int index, draco::FaceIndex face) {
|
||||
int32_t idx0 = indices[index];
|
||||
int32_t idx1 = indices[index + 1];
|
||||
int32_t idx2 = indices[index + 2];
|
||||
|
||||
if (hasPerFaceMaterials) {
|
||||
meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID);
|
||||
}
|
||||
|
||||
meshBuilder.SetAttributeValuesForFace(positionAttributeID, face,
|
||||
&mesh.vertices[idx0], &mesh.vertices[idx1],
|
||||
&mesh.vertices[idx2]);
|
||||
|
||||
if (needsOriginalIndices) {
|
||||
meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face,
|
||||
&mesh.originalIndices[idx0],
|
||||
&mesh.originalIndices[idx1],
|
||||
&mesh.originalIndices[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);
|
||||
}
|
||||
|
||||
if (needsOriginalIndices) {
|
||||
dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FBXBaker::rewriteAndBakeSceneTextures() {
|
||||
using namespace image::TextureUsage;
|
||||
QHash<QString, image::TextureUsage::Type> textureTypes;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// enumerate the children of the root node
|
||||
for (FBXNode& rootChild : _rootNode.children) {
|
||||
|
||||
if (rootChild.name == "Objects") {
|
||||
|
||||
// enumerate the objects
|
||||
auto object = rootChild.children.begin();
|
||||
while (object != rootChild.children.end()) {
|
||||
if (object->name == "Texture") {
|
||||
|
||||
// double check that we didn't get an abort while baking the last texture
|
||||
if (shouldStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 { textureChild.properties.at(0).toByteArray() };
|
||||
QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") };
|
||||
|
||||
if (textureFileInfo.suffix() == BAKED_TEXTURE_EXT.mid(1)) {
|
||||
// re-baking an FBX that already references baked textures is a fail
|
||||
// so we add an error and return from here
|
||||
handleError("Cannot re-bake a file that references compressed textures");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// make sure this texture points to something and isn't one we've already re-mapped
|
||||
if (!textureFileInfo.filePath().isEmpty()) {
|
||||
// check if this was an embedded texture we have already have in-memory content for
|
||||
auto textureContent = _textureContent.value(fbxTextureFileName.toLocal8Bit());
|
||||
|
||||
// figure out the URL to this texture, embedded or external
|
||||
auto urlToTexture = getTextureURL(textureFileInfo, fbxTextureFileName,
|
||||
!textureContent.isNull());
|
||||
|
||||
QString bakedTextureFileName;
|
||||
if (_remappedTexturePaths.contains(urlToTexture)) {
|
||||
bakedTextureFileName = _remappedTexturePaths[urlToTexture];
|
||||
} else {
|
||||
// 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
|
||||
bakedTextureFileName = createBakedTextureFileName(textureFileInfo);
|
||||
_remappedTexturePaths[urlToTexture] = bakedTextureFileName;
|
||||
}
|
||||
|
||||
qCDebug(model_baking).noquote() << "Re-mapping" << fbxTextureFileName
|
||||
<< "to" << bakedTextureFileName;
|
||||
|
||||
QString bakedTextureFilePath {
|
||||
_bakedOutputDir + "/" + bakedTextureFileName
|
||||
};
|
||||
|
||||
// write the new filename into the FBX scene
|
||||
textureChild.properties[0] = bakedTextureFileName.toLocal8Bit();
|
||||
|
||||
if (!_bakingTextures.contains(urlToTexture)) {
|
||||
_outputFiles.push_back(bakedTextureFilePath);
|
||||
|
||||
// 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];
|
||||
|
||||
// bake this texture asynchronously
|
||||
bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
++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, const QString& bakedFilename, 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, bakedFilename, textureContent),
|
||||
&TextureBaker::deleteLater
|
||||
};
|
||||
|
||||
// make sure we hear when the baking texture is done or aborted
|
||||
connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture);
|
||||
connect(bakingTexture.data(), &TextureBaker::aborted, this, &FBXBaker::handleAbortedTexture);
|
||||
|
||||
// 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 (!shouldStop()) {
|
||||
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 a folder with the name of the FBX
|
||||
// since that is the fake URL we provide when baking external textures
|
||||
|
||||
if (!_fbxURL.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());
|
||||
|
||||
// abort any other ongoing texture bakes since we know we'll end up failing
|
||||
for (auto& bakingTexture : _bakingTextures) {
|
||||
bakingTexture->abort();
|
||||
}
|
||||
|
||||
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::handleAbortedTexture() {
|
||||
// grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore
|
||||
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
|
||||
|
||||
if (bakedTexture) {
|
||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
||||
}
|
||||
|
||||
// since a texture we were baking aborted, our status is also aborted
|
||||
_shouldAbort.store(true);
|
||||
|
||||
// abort any other ongoing texture bakes since we know we'll end up failing
|
||||
for (auto& bakingTexture : _bakingTextures) {
|
||||
bakingTexture->abort();
|
||||
}
|
||||
|
||||
checkIfTexturesFinished();
|
||||
}
|
||||
|
||||
void FBXBaker::exportScene() {
|
||||
// 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;
|
||||
|
||||
auto fbxData = FBXWriter::encodeFBX(_rootNode);
|
||||
|
||||
QFile bakedFile(_bakedFBXFilePath);
|
||||
|
||||
if (!bakedFile.open(QIODevice::WriteOnly)) {
|
||||
handleError("Error opening " + _bakedFBXFilePath + " for writing");
|
||||
return;
|
||||
}
|
||||
|
||||
bakedFile.write(fbxData);
|
||||
|
||||
_outputFiles.push_back(_bakedFBXFilePath);
|
||||
|
||||
qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath;
|
||||
}
|
||||
|
||||
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()) {
|
||||
if (shouldStop()) {
|
||||
// if we're checking for completion but we have errors
|
||||
// that means one or more of our texture baking operations failed
|
||||
|
||||
if (_pendingErrorEmission) {
|
||||
setIsFinished(true);
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
qCDebug(model_baking) << "Finished baking, emitting finsihed" << _fbxURL;
|
||||
|
||||
setIsFinished(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FBXBaker::setWasAborted(bool wasAborted) {
|
||||
if (wasAborted != _wasAborted.load()) {
|
||||
Baker::setWasAborted(wasAborted);
|
||||
|
||||
if (wasAborted) {
|
||||
qCDebug(model_baking) << "Aborted baking" << _fbxURL;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
@ -20,33 +20,30 @@
|
|||
#include "Baker.h"
|
||||
#include "TextureBaker.h"
|
||||
|
||||
#include "ModelBakingLoggingCategory.h"
|
||||
|
||||
#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*()>;
|
||||
|
||||
class FBXBaker : public Baker {
|
||||
Q_OBJECT
|
||||
public:
|
||||
FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath,
|
||||
TextureBakerThreadGetter textureThreadGetter, bool copyOriginals = true);
|
||||
FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
|
||||
const QString& bakedOutputDir, const QString& originalOutputDir = "");
|
||||
|
||||
QUrl getFBXUrl() const { return _fbxURL; }
|
||||
QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; }
|
||||
QString getBakedFBXFilePath() const { return _bakedFBXFilePath; }
|
||||
|
||||
virtual void setWasAborted(bool wasAborted) override;
|
||||
|
||||
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;
|
||||
virtual void abort() override;
|
||||
|
||||
signals:
|
||||
void sourceCopyReadyToLoad();
|
||||
|
@ -55,46 +52,49 @@ private slots:
|
|||
void bakeSourceCopy();
|
||||
void handleFBXNetworkReply();
|
||||
void handleBakedTexture();
|
||||
void handleAbortedTexture();
|
||||
|
||||
private:
|
||||
void setupOutputFolder();
|
||||
|
||||
void loadSourceFBX();
|
||||
|
||||
void bakeCopiedFBX();
|
||||
|
||||
void importScene();
|
||||
void rewriteAndBakeSceneModels();
|
||||
void rewriteAndBakeSceneTextures();
|
||||
void exportScene();
|
||||
void removeEmbeddedMediaFolder();
|
||||
void possiblyCleanupOriginals();
|
||||
|
||||
void checkIfTexturesFinished();
|
||||
|
||||
QString createBakedTextureFileName(const QFileInfo& textureFileInfo);
|
||||
QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture);
|
||||
QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false);
|
||||
|
||||
void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir);
|
||||
|
||||
QString pathToCopyOfOriginal() const;
|
||||
void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir,
|
||||
const QString& bakedFilename, const QByteArray& textureContent = QByteArray());
|
||||
|
||||
QUrl _fbxURL;
|
||||
QString _fbxName;
|
||||
|
||||
QString _baseOutputPath;
|
||||
QString _uniqueOutputPath;
|
||||
QString _bakedFBXRelativePath;
|
||||
|
||||
static FBXSDKManagerUniquePointer _sdkManager;
|
||||
fbxsdk::FbxScene* _scene { nullptr };
|
||||
FBXNode _rootNode;
|
||||
FBXGeometry* _geometry;
|
||||
QHash<QByteArray, QByteArray> _textureContent;
|
||||
|
||||
QString _bakedFBXFilePath;
|
||||
|
||||
QString _bakedOutputDir;
|
||||
|
||||
// If set, the original FBX and textures will also be copied here
|
||||
QString _originalOutputDir;
|
||||
|
||||
QDir _tempDir;
|
||||
QString _originalFBXFilePath;
|
||||
|
||||
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
|
||||
QHash<QString, int> _textureNameMatchCount;
|
||||
QHash<QUrl, QString> _remappedTexturePaths;
|
||||
|
||||
TextureBakerThreadGetter _textureThreadGetter;
|
||||
|
||||
bool _copyOriginals { true };
|
||||
|
||||
bool _pendingErrorEmission { false };
|
||||
};
|
||||
|
|
@ -25,22 +25,48 @@
|
|||
|
||||
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 QString& bakedFilename,
|
||||
const QByteArray& textureContent) :
|
||||
_textureURL(textureURL),
|
||||
_originalTexture(textureContent),
|
||||
_textureType(textureType),
|
||||
_outputDirectory(outputDirectory)
|
||||
_outputDirectory(outputDirectory),
|
||||
_bakedTextureFileName(bakedFilename)
|
||||
{
|
||||
// figure out the baked texture filename
|
||||
auto originalFilename = textureURL.fileName();
|
||||
_bakedTextureFileName = originalFilename.left(originalFilename.lastIndexOf('.')) + BAKED_TEXTURE_EXT;
|
||||
if (bakedFilename.isEmpty()) {
|
||||
// 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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void TextureBaker::abort() {
|
||||
Baker::abort();
|
||||
|
||||
// flip our atomic bool so any ongoing texture processing is stopped
|
||||
_abortProcessing.store(true);
|
||||
}
|
||||
|
||||
const QStringList TextureBaker::getSupportedFormats() {
|
||||
auto formats = QImageReader::supportedImageFormats();
|
||||
QStringList stringFormats;
|
||||
std::transform(formats.begin(), formats.end(), std::back_inserter(stringFormats),
|
||||
[](QByteArray& format) -> QString { return format; });
|
||||
return stringFormats;
|
||||
}
|
||||
|
||||
void TextureBaker::loadTexture() {
|
||||
|
@ -96,7 +122,11 @@ void TextureBaker::handleTextureNetworkReply() {
|
|||
|
||||
void TextureBaker::processTexture() {
|
||||
auto processedTexture = image::processImage(_originalTexture, _textureURL.toString().toStdString(),
|
||||
ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType);
|
||||
ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, _abortProcessing);
|
||||
|
||||
if (shouldStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!processedTexture) {
|
||||
handleError("Could not process texture " + _textureURL.toString());
|
||||
|
@ -120,12 +150,21 @@ void TextureBaker::processTexture() {
|
|||
const size_t length = memKTX->_storage->size();
|
||||
|
||||
// attempt to write the baked texture to the destination file path
|
||||
QFile bakedTextureFile { _outputDirectory.absoluteFilePath(_bakedTextureFileName) };
|
||||
auto filePath = _outputDirectory.absoluteFilePath(_bakedTextureFileName);
|
||||
QFile bakedTextureFile { filePath };
|
||||
|
||||
if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) {
|
||||
handleError("Could not write baked texture for " + _textureURL.toString());
|
||||
} else {
|
||||
_outputFiles.push_back(filePath);
|
||||
}
|
||||
|
||||
qCDebug(model_baking) << "Baked texture" << _textureURL;
|
||||
emit finished();
|
||||
setIsFinished(true);
|
||||
}
|
||||
|
||||
void TextureBaker::setWasAborted(bool wasAborted) {
|
||||
Baker::setWasAborted(wasAborted);
|
||||
|
||||
qCDebug(model_baking) << "Aborted baking" << _textureURL;
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
#include <QtCore/QObject>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QRunnable>
|
||||
#include <QImageReader>
|
||||
|
||||
#include <image/Image.h>
|
||||
|
||||
|
@ -26,7 +27,11 @@ 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 QString& bakedFilename = QString(),
|
||||
const QByteArray& textureContent = QByteArray());
|
||||
|
||||
static const QStringList getSupportedFormats();
|
||||
|
||||
const QByteArray& getOriginalTexture() const { return _originalTexture; }
|
||||
|
||||
|
@ -35,8 +40,11 @@ public:
|
|||
QString getDestinationFilePath() const { return _outputDirectory.absoluteFilePath(_bakedTextureFileName); }
|
||||
QString getBakedTextureFileName() const { return _bakedTextureFileName; }
|
||||
|
||||
virtual void setWasAborted(bool wasAborted) override;
|
||||
|
||||
public slots:
|
||||
virtual void bake() override;
|
||||
virtual void abort() override;
|
||||
|
||||
signals:
|
||||
void originalTextureLoaded();
|
||||
|
@ -54,6 +62,8 @@ private:
|
|||
|
||||
QDir _outputDirectory;
|
||||
QString _bakedTextureFileName;
|
||||
|
||||
std::atomic<bool> _abortProcessing { false };
|
||||
};
|
||||
|
||||
#endif // hifi_TextureBaker_h
|
|
@ -159,6 +159,88 @@ void EntityTreeRenderer::shutdown() {
|
|||
clear(); // always clear() on shutdown
|
||||
}
|
||||
|
||||
void EntityTreeRenderer::addPendingEntities(const render::ScenePointer& scene, render::Transaction& transaction) {
|
||||
// Clear any expired entities
|
||||
// FIXME should be able to use std::remove_if, but it fails due to some
|
||||
// weird compilation error related to EntityItemID assignment operators
|
||||
for (auto itr = _entitiesToAdd.begin(); _entitiesToAdd.end() != itr; ) {
|
||||
if (itr->second.expired()) {
|
||||
_entitiesToAdd.erase(itr++);
|
||||
} else {
|
||||
++itr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_entitiesToAdd.empty()) {
|
||||
std::unordered_set<EntityItemID> processedIds;
|
||||
for (const auto& entry : _entitiesToAdd) {
|
||||
auto entity = entry.second.lock();
|
||||
if (!entity) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Path to the parent transforms is not valid,
|
||||
// don't add to the scene graph yet
|
||||
if (!entity->isParentPathComplete()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto entityID = entity->getEntityItemID();
|
||||
processedIds.insert(entityID);
|
||||
auto renderable = EntityRenderer::addToScene(*this, entity, scene, transaction);
|
||||
if (renderable) {
|
||||
_entitiesInScene.insert({ entityID, renderable });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!processedIds.empty()) {
|
||||
for (const auto& processedId : processedIds) {
|
||||
_entitiesToAdd.erase(processedId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction) {
|
||||
std::unordered_set<EntityItemID> changedEntities;
|
||||
_changedEntitiesGuard.withWriteLock([&] {
|
||||
#if 0
|
||||
// FIXME Weird build failure in latest VC update that fails to compile when using std::swap
|
||||
changedEntities.swap(_changedEntities);
|
||||
#else
|
||||
changedEntities.insert(_changedEntities.begin(), _changedEntities.end());
|
||||
_changedEntities.clear();
|
||||
#endif
|
||||
});
|
||||
|
||||
for (const auto& entityId : changedEntities) {
|
||||
auto renderable = renderableForEntityId(entityId);
|
||||
if (!renderable) {
|
||||
continue;
|
||||
}
|
||||
_renderablesToUpdate.insert({ entityId, renderable });
|
||||
}
|
||||
|
||||
// NOTE: Looping over all the entity renderers is likely to be a bottleneck in the future
|
||||
// Currently, this is necessary because the model entity loading logic requires constant polling
|
||||
// This was working fine because the entity server used to send repeated updates as your view changed,
|
||||
// but with the improved entity server logic (PR 11141), updateInScene (below) would not be triggered enough
|
||||
for (const auto& entry : _entitiesInScene) {
|
||||
const auto& renderable = entry.second;
|
||||
if (renderable) {
|
||||
renderable->update(scene, transaction);
|
||||
}
|
||||
}
|
||||
if (!_renderablesToUpdate.empty()) {
|
||||
for (const auto& entry : _renderablesToUpdate) {
|
||||
const auto& renderable = entry.second;
|
||||
renderable->updateInScene(scene, transaction);
|
||||
}
|
||||
_renderablesToUpdate.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void EntityTreeRenderer::update(bool simulate) {
|
||||
PerformanceTimer perfTimer("ETRupdate");
|
||||
if (_tree && !_shuttingDown) {
|
||||
|
@ -178,30 +260,11 @@ void EntityTreeRenderer::update(bool simulate) {
|
|||
}
|
||||
}
|
||||
|
||||
std::unordered_set<EntityItemID> changedEntities;
|
||||
// FIXME Weird build failure in latest VC update that fails to compile when using std::swap
|
||||
_changedEntitiesGuard.withWriteLock([&] {
|
||||
changedEntities.insert(_changedEntities.begin(), _changedEntities.end());
|
||||
_changedEntities.clear();
|
||||
});
|
||||
|
||||
for (const auto& entityId : changedEntities) {
|
||||
auto renderable = renderableForEntityId(entityId);
|
||||
if (!renderable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_renderablesToUpdate.insert({ entityId, renderable });
|
||||
}
|
||||
|
||||
auto scene = _viewState->getMain3DScene();
|
||||
if (scene && !_renderablesToUpdate.empty()) {
|
||||
if (scene) {
|
||||
render::Transaction transaction;
|
||||
for (const auto& entry : _renderablesToUpdate) {
|
||||
const auto& renderable = entry.second;
|
||||
renderable->updateInScene(scene, transaction);
|
||||
}
|
||||
_renderablesToUpdate.clear();
|
||||
addPendingEntities(scene, transaction);
|
||||
updateChangedEntities(scene, transaction);
|
||||
scene->enqueueTransaction(transaction);
|
||||
}
|
||||
}
|
||||
|
@ -689,8 +752,12 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) {
|
|||
}
|
||||
|
||||
void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) {
|
||||
// If it's in the pending queue, remove it
|
||||
_entitiesToAdd.erase(entityID);
|
||||
|
||||
auto itr = _entitiesInScene.find(entityID);
|
||||
if (_entitiesInScene.end() == itr) {
|
||||
// Not in the scene, and no longer potentially in the pending queue, we're done
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -725,25 +792,10 @@ void EntityTreeRenderer::addingEntity(const EntityItemID& entityID) {
|
|||
checkAndCallPreload(entityID);
|
||||
auto entity = std::static_pointer_cast<EntityTree>(_tree)->findEntityByID(entityID);
|
||||
if (entity) {
|
||||
addEntityToScene(entity);
|
||||
_entitiesToAdd.insert({ entity->getEntityItemID(), entity });
|
||||
}
|
||||
}
|
||||
|
||||
void EntityTreeRenderer::addEntityToScene(const EntityItemPointer& entity) {
|
||||
// here's where we add the entity payload to the scene
|
||||
auto scene = _viewState->getMain3DScene();
|
||||
if (!scene) {
|
||||
qCWarning(entitiesrenderer) << "EntityTreeRenderer::addEntityToScene(), Unexpected null scene, possibly during application shutdown";
|
||||
return;
|
||||
}
|
||||
|
||||
auto renderable = EntityRenderer::addToScene(*this, entity, scene);
|
||||
if (renderable) {
|
||||
_entitiesInScene[entity->getEntityItemID()] = renderable;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void EntityTreeRenderer::entityScriptChanging(const EntityItemID& entityID, bool reload) {
|
||||
checkAndCallPreload(entityID, reload, true);
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ namespace render { namespace entities {
|
|||
class EntityRenderer;
|
||||
using EntityRendererPointer = std::shared_ptr<EntityRenderer>;
|
||||
using EntityRendererWeakPointer = std::weak_ptr<EntityRenderer>;
|
||||
|
||||
} }
|
||||
|
||||
// Allow the use of std::unordered_map with QUuid keys
|
||||
|
@ -157,12 +158,13 @@ protected:
|
|||
}
|
||||
|
||||
private:
|
||||
void addPendingEntities(const render::ScenePointer& scene, render::Transaction& transaction);
|
||||
void updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction);
|
||||
EntityRendererPointer renderableForEntity(const EntityItemPointer& entity) const { return renderableForEntityId(entity->getID()); }
|
||||
render::ItemID renderableIdForEntity(const EntityItemPointer& entity) const { return renderableIdForEntityId(entity->getID()); }
|
||||
|
||||
void resetEntitiesScriptEngine();
|
||||
|
||||
void addEntityToScene(const EntityItemPointer& entity);
|
||||
bool findBestZoneAndMaybeContainingEntities(QVector<EntityItemID>* entitiesContainingAvatar = nullptr);
|
||||
|
||||
bool applyLayeredZones();
|
||||
|
@ -260,6 +262,7 @@ private:
|
|||
|
||||
std::unordered_map<EntityItemID, EntityRendererPointer> _renderablesToUpdate;
|
||||
std::unordered_map<EntityItemID, EntityRendererPointer> _entitiesInScene;
|
||||
std::unordered_map<EntityItemID, EntityItemWeakPointer> _entitiesToAdd;
|
||||
// For Scene.shouldRenderEntities
|
||||
QList<EntityItemID> _entityIDsLastInScene;
|
||||
|
||||
|
|
|
@ -187,7 +187,7 @@ void EntityRenderer::render(RenderArgs* args) {
|
|||
// Methods called by the EntityTreeRenderer
|
||||
//
|
||||
|
||||
EntityRenderer::Pointer EntityRenderer::addToScene(EntityTreeRenderer& renderer, const EntityItemPointer& entity, const ScenePointer& scene) {
|
||||
EntityRenderer::Pointer EntityRenderer::addToScene(EntityTreeRenderer& renderer, const EntityItemPointer& entity, const ScenePointer& scene, Transaction& transaction) {
|
||||
EntityRenderer::Pointer result;
|
||||
if (!entity) {
|
||||
return result;
|
||||
|
@ -245,9 +245,7 @@ EntityRenderer::Pointer EntityRenderer::addToScene(EntityTreeRenderer& renderer,
|
|||
}
|
||||
|
||||
if (result) {
|
||||
Transaction transaction;
|
||||
result->addToScene(scene, transaction);
|
||||
scene->enqueueTransaction(transaction);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -293,6 +291,18 @@ void EntityRenderer::updateInScene(const ScenePointer& scene, Transaction& trans
|
|||
});
|
||||
}
|
||||
|
||||
void EntityRenderer::update(const ScenePointer& scene, Transaction& transaction) {
|
||||
if (!isValidRenderItem()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!needsUpdate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
doUpdate(scene, transaction, _entity);
|
||||
}
|
||||
|
||||
//
|
||||
// Internal methods
|
||||
//
|
||||
|
@ -306,6 +316,11 @@ bool EntityRenderer::needsRenderUpdate() const {
|
|||
return needsRenderUpdateFromEntity(_entity);
|
||||
}
|
||||
|
||||
// Returns true if the item needs to have update called
|
||||
bool EntityRenderer::needsUpdate() const {
|
||||
return needsUpdateFromEntity(_entity);
|
||||
}
|
||||
|
||||
// Returns true if the item in question needs to have updateInScene called because of changes in the entity
|
||||
bool EntityRenderer::needsRenderUpdateFromEntity(const EntityItemPointer& entity) const {
|
||||
bool success = false;
|
||||
|
|
|
@ -30,7 +30,7 @@ class EntityRenderer : public QObject, public std::enable_shared_from_this<Entit
|
|||
|
||||
public:
|
||||
static void initEntityRenderers();
|
||||
static Pointer addToScene(EntityTreeRenderer& renderer, const EntityItemPointer& entity, const ScenePointer& scene);
|
||||
static Pointer addToScene(EntityTreeRenderer& renderer, const EntityItemPointer& entity, const ScenePointer& scene, Transaction& transaction);
|
||||
|
||||
// Allow classes to override this to interact with the user
|
||||
virtual bool wantsHandControllerPointerEvents() const { return false; }
|
||||
|
@ -49,6 +49,8 @@ public:
|
|||
virtual bool addToScene(const ScenePointer& scene, Transaction& transaction) final;
|
||||
virtual void removeFromScene(const ScenePointer& scene, Transaction& transaction);
|
||||
|
||||
virtual void update(const ScenePointer& scene, Transaction& transaction);
|
||||
|
||||
protected:
|
||||
virtual bool needsRenderUpdateFromEntity() const final { return needsRenderUpdateFromEntity(_entity); }
|
||||
virtual void onAddToScene(const EntityItemPointer& entity);
|
||||
|
@ -71,6 +73,12 @@ protected:
|
|||
// Returns true if the item in question needs to have updateInScene called because of changes in the entity
|
||||
virtual bool needsRenderUpdateFromEntity(const EntityItemPointer& entity) const;
|
||||
|
||||
// Returns true if the item in question needs to have update called
|
||||
virtual bool needsUpdate() const;
|
||||
|
||||
// Returns true if the item in question needs to have update called because of changes in the entity
|
||||
virtual bool needsUpdateFromEntity(const EntityItemPointer& entity) const { return false; }
|
||||
|
||||
// Will be called on the main thread from updateInScene. This can be used to fetch things like
|
||||
// network textures or model geometry from resource caches
|
||||
virtual void doRenderUpdateSynchronous(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) { }
|
||||
|
@ -80,6 +88,8 @@ protected:
|
|||
// data in this method if using multi-threaded rendering
|
||||
virtual void doRenderUpdateAsynchronous(const EntityItemPointer& entity);
|
||||
|
||||
virtual void doUpdate(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) { }
|
||||
|
||||
// Called by the `render` method after `needsRenderUpdate`
|
||||
virtual void doRender(RenderArgs* args) = 0;
|
||||
|
||||
|
@ -148,6 +158,15 @@ protected:
|
|||
onRemoveFromSceneTyped(_typedEntity);
|
||||
}
|
||||
|
||||
using Parent::needsUpdateFromEntity;
|
||||
// Returns true if the item in question needs to have update called because of changes in the entity
|
||||
virtual bool needsUpdateFromEntity(const EntityItemPointer& entity) const override final {
|
||||
if (Parent::needsUpdateFromEntity(entity)) {
|
||||
return true;
|
||||
}
|
||||
return needsUpdateFromTypedEntity(_typedEntity);
|
||||
}
|
||||
|
||||
using Parent::needsRenderUpdateFromEntity;
|
||||
// Returns true if the item in question needs to have updateInScene called because of changes in the entity
|
||||
virtual bool needsRenderUpdateFromEntity(const EntityItemPointer& entity) const override final {
|
||||
|
@ -162,6 +181,11 @@ protected:
|
|||
doRenderUpdateSynchronousTyped(scene, transaction, _typedEntity);
|
||||
}
|
||||
|
||||
virtual void doUpdate(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) override final {
|
||||
Parent::doUpdate(scene, transaction, entity);
|
||||
doUpdateTyped(scene, transaction, _typedEntity);
|
||||
}
|
||||
|
||||
virtual void doRenderUpdateAsynchronous(const EntityItemPointer& entity) override final {
|
||||
Parent::doRenderUpdateAsynchronous(entity);
|
||||
doRenderUpdateAsynchronousTyped(_typedEntity);
|
||||
|
@ -170,6 +194,8 @@ protected:
|
|||
virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { return false; }
|
||||
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { }
|
||||
virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { }
|
||||
virtual bool needsUpdateFromTypedEntity(const TypedEntityPointer& entity) const { return false; }
|
||||
virtual void doUpdateTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { }
|
||||
virtual void onAddToSceneTyped(const TypedEntityPointer& entity) { }
|
||||
virtual void onRemoveFromSceneTyped(const TypedEntityPointer& entity) { }
|
||||
|
||||
|
|
|
@ -904,7 +904,7 @@ using namespace render;
|
|||
using namespace render::entities;
|
||||
|
||||
ItemKey ModelEntityRenderer::getKey() {
|
||||
return ItemKey::Builder::opaqueShape().withTypeMeta();
|
||||
return ItemKey::Builder().withTypeMeta();
|
||||
}
|
||||
|
||||
uint32_t ModelEntityRenderer::metaFetchMetaSubItems(ItemIDs& subItems) {
|
||||
|
@ -1026,12 +1026,16 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) {
|
|||
entity->copyAnimationJointDataToModel();
|
||||
}
|
||||
|
||||
bool ModelEntityRenderer::needsRenderUpdate() const {
|
||||
bool ModelEntityRenderer::needsUpdate() const {
|
||||
ModelPointer model;
|
||||
withReadLock([&] {
|
||||
model = _model;
|
||||
});
|
||||
|
||||
if (_modelJustLoaded) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (model) {
|
||||
if (_needsJointSimulation || _moving || _animating) {
|
||||
return true;
|
||||
|
@ -1057,10 +1061,10 @@ bool ModelEntityRenderer::needsRenderUpdate() const {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
return Parent::needsRenderUpdate();
|
||||
return Parent::needsUpdate();
|
||||
}
|
||||
|
||||
bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const {
|
||||
bool ModelEntityRenderer::needsUpdateFromTypedEntity(const TypedEntityPointer& entity) const {
|
||||
if (resultWithReadLock<bool>([&] {
|
||||
if (entity->hasModel() != _hasModel) {
|
||||
return true;
|
||||
|
@ -1122,7 +1126,7 @@ bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPoin
|
|||
return false;
|
||||
}
|
||||
|
||||
void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
|
||||
void ModelEntityRenderer::doUpdateTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
|
||||
if (_hasModel != entity->hasModel()) {
|
||||
_hasModel = entity->hasModel();
|
||||
}
|
||||
|
@ -1148,9 +1152,11 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
|
|||
return;
|
||||
}
|
||||
|
||||
_modelJustLoaded = false;
|
||||
// Check for addition
|
||||
if (_hasModel && !(bool)_model) {
|
||||
model = std::make_shared<Model>(nullptr, entity.get());
|
||||
connect(model.get(), &Model::setURLFinished, this, &ModelEntityRenderer::handleModelLoaded);
|
||||
model->setLoadingPriority(EntityTreeRenderer::getEntityLoadingPriority(*entity));
|
||||
model->init();
|
||||
entity->setModel(model);
|
||||
|
@ -1175,8 +1181,8 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
|
|||
properties.setLastEdited(usecTimestampNow()); // we must set the edit time since we're editing it
|
||||
auto extents = model->getMeshExtents();
|
||||
properties.setDimensions(extents.maximum - extents.minimum);
|
||||
qCDebug(entitiesrenderer) << "Autoresizing"
|
||||
<< (!entity->getName().isEmpty() ? entity->getName() : entity->getModelURL())
|
||||
qCDebug(entitiesrenderer) << "Autoresizing"
|
||||
<< (!entity->getName().isEmpty() ? entity->getName() : entity->getModelURL())
|
||||
<< "from mesh extents";
|
||||
|
||||
QMetaObject::invokeMethod(DependencyManager::get<EntityScriptingInterface>().data(), "editEntity",
|
||||
|
@ -1203,7 +1209,6 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
|
|||
entity->updateModelBounds();
|
||||
}
|
||||
|
||||
|
||||
if (model->isVisible() != _visible) {
|
||||
// FIXME: this seems like it could be optimized if we tracked our last known visible state in
|
||||
// the renderable item. As it stands now the model checks it's visible/invisible state
|
||||
|
@ -1234,7 +1239,6 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
if (_animating) {
|
||||
if (!jointsMapped()) {
|
||||
mapJoints(entity, model->getJointNames());
|
||||
|
@ -1243,6 +1247,12 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
|
|||
}
|
||||
}
|
||||
|
||||
void ModelEntityRenderer::handleModelLoaded(bool success) {
|
||||
if (success) {
|
||||
_modelJustLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: this only renders the "meta" portion of the Model, namely it renders debugging items
|
||||
void ModelEntityRenderer::doRender(RenderArgs* args) {
|
||||
PROFILE_RANGE(render_detail, "MetaModelRender");
|
||||
|
|
|
@ -138,10 +138,10 @@ protected:
|
|||
virtual ItemKey getKey() override;
|
||||
virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) override;
|
||||
|
||||
virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
|
||||
virtual bool needsRenderUpdate() const override;
|
||||
virtual bool needsUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
|
||||
virtual bool needsUpdate() const override;
|
||||
virtual void doRender(RenderArgs* args) override;
|
||||
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
|
||||
virtual void doUpdateTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
|
||||
|
||||
private:
|
||||
void animate(const TypedEntityPointer& entity);
|
||||
|
@ -151,6 +151,7 @@ private:
|
|||
// Transparency is handled in ModelMeshPartPayload
|
||||
virtual bool isTransparent() const override { return false; }
|
||||
|
||||
bool _modelJustLoaded { false };
|
||||
bool _hasModel { false };
|
||||
::ModelPointer _model;
|
||||
GeometryResource::Pointer _compoundShapeResource;
|
||||
|
@ -178,6 +179,9 @@ private:
|
|||
bool _animating { false };
|
||||
uint64_t _lastAnimated { 0 };
|
||||
float _currentFrame { 0 };
|
||||
|
||||
private slots:
|
||||
void handleModelLoaded(bool success);
|
||||
};
|
||||
|
||||
} } // namespace
|
||||
|
|
|
@ -102,7 +102,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) {
|
|||
glm::quat orientation = glm::quat(glm::vec3(0.0f, yawRotation, 0.0f));
|
||||
transformToTopLeft.setRotation(orientation);
|
||||
}
|
||||
transformToTopLeft.postTranslate(glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left
|
||||
transformToTopLeft.postTranslate(dimensions * glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left
|
||||
transformToTopLeft.setScale(1.0f); // Use a scale of one so that the text is not deformed
|
||||
|
||||
batch.setModelTransform(transformToTopLeft);
|
||||
|
|
|
@ -140,6 +140,11 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene
|
|||
_webSurface->resize(QSize(windowSize.x, windowSize.y));
|
||||
}
|
||||
|
||||
void WebEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) {
|
||||
Parent::doRenderUpdateAsynchronousTyped(entity);
|
||||
_modelTransform.postScale(entity->getDimensions());
|
||||
}
|
||||
|
||||
void WebEntityRenderer::doRender(RenderArgs* args) {
|
||||
withWriteLock([&] {
|
||||
_lastRenderTime = usecTimestampNow();
|
||||
|
|
|
@ -29,6 +29,7 @@ protected:
|
|||
virtual bool needsRenderUpdate() const override;
|
||||
virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
|
||||
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
|
||||
virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override;
|
||||
virtual void doRender(RenderArgs* args) override;
|
||||
virtual bool isTransparent() const override;
|
||||
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
set(TARGET_NAME fbx)
|
||||
setup_hifi_library()
|
||||
link_hifi_libraries(shared model networking)
|
||||
include_hifi_library_headers(gpu)
|
||||
|
||||
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})
|
||||
|
|
345
libraries/fbx/src/FBX.h
Normal file
345
libraries/fbx/src/FBX.h
Normal file
|
@ -0,0 +1,345 @@
|
|||
//
|
||||
// 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_2015 = 7400;
|
||||
static const quint32 FBX_VERSION_2016 = 7500;
|
||||
|
||||
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;
|
||||
static const int DRACO_ATTRIBUTE_ORIGINAL_INDEX = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 2;
|
||||
|
||||
static const int32_t FBX_PROPERTY_UNCOMPRESSED_FLAG = 0;
|
||||
static const int32_t FBX_PROPERTY_COMPRESSED_FLAG = 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<int32_t> originalIndices;
|
||||
|
||||
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;
|
||||
bool wasCompressed { false };
|
||||
};
|
||||
|
||||
class ExtractedMesh {
|
||||
public:
|
||||
FBXMesh mesh;
|
||||
QMultiHash<int, int> newIndices;
|
||||
QVector<QHash<int, int> > blendshapeIndexMaps;
|
||||
QVector<QPair<int, int> > partMaterialTextures;
|
||||
QHash<QString, size_t> texcoordSetMap;
|
||||
};
|
||||
|
||||
/// A single animation frame extracted from an FBX document.
|
||||
class FBXAnimationFrame {
|
||||
public:
|
||||
QVector<glm::quat> rotations;
|
||||
QVector<glm::vec3> translations;
|
||||
};
|
||||
|
||||
/// A light in an FBX document.
|
||||
class FBXLight {
|
||||
public:
|
||||
QString name;
|
||||
Transform transform;
|
||||
float intensity;
|
||||
float fogValue;
|
||||
glm::vec3 color;
|
||||
|
||||
FBXLight() :
|
||||
name(),
|
||||
transform(),
|
||||
intensity(1.0f),
|
||||
fogValue(0.0f),
|
||||
color(1.0f)
|
||||
{}
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(FBXAnimationFrame)
|
||||
Q_DECLARE_METATYPE(QVector<FBXAnimationFrame>)
|
||||
|
||||
/// A set of meshes extracted from an FBX document.
|
||||
class FBXGeometry {
|
||||
public:
|
||||
using Pointer = std::shared_ptr<FBXGeometry>;
|
||||
|
||||
QString originalURL;
|
||||
QString author;
|
||||
QString applicationName; ///< the name of the application that generated the model
|
||||
|
||||
QVector<FBXJoint> joints;
|
||||
QHash<QString, int> jointIndices; ///< 1-based, so as to more easily detect missing indices
|
||||
bool hasSkeletonJoints;
|
||||
|
||||
QVector<FBXMesh> meshes;
|
||||
|
||||
QHash<QString, FBXMaterial> materials;
|
||||
|
||||
glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file
|
||||
|
||||
int leftEyeJointIndex = -1;
|
||||
int rightEyeJointIndex = -1;
|
||||
int neckJointIndex = -1;
|
||||
int rootJointIndex = -1;
|
||||
int leanJointIndex = -1;
|
||||
int headJointIndex = -1;
|
||||
int leftHandJointIndex = -1;
|
||||
int rightHandJointIndex = -1;
|
||||
int leftToeJointIndex = -1;
|
||||
int rightToeJointIndex = -1;
|
||||
|
||||
float leftEyeSize = 0.0f; // Maximum mesh extents dimension
|
||||
float rightEyeSize = 0.0f;
|
||||
|
||||
QVector<int> humanIKJointIndices;
|
||||
|
||||
glm::vec3 palmDirection;
|
||||
|
||||
glm::vec3 neckPivot;
|
||||
|
||||
Extents bindExtents;
|
||||
Extents meshExtents;
|
||||
|
||||
QVector<FBXAnimationFrame> animationFrames;
|
||||
|
||||
int getJointIndex(const QString& name) const { return jointIndices.value(name) - 1; }
|
||||
QStringList getJointNames() const;
|
||||
|
||||
bool hasBlendedMeshes() const;
|
||||
|
||||
/// Returns the unscaled extents of the model's mesh
|
||||
Extents getUnscaledMeshExtents() const;
|
||||
|
||||
bool convexHullContains(const glm::vec3& point) const;
|
||||
|
||||
QHash<int, QString> meshIndicesToModelNames;
|
||||
|
||||
/// given a meshIndex this will return the name of the model that mesh belongs to if known
|
||||
QString getModelNameOfMesh(int meshIndex) const;
|
||||
|
||||
QList<QString> blendshapeChannelNames;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(FBXGeometry)
|
||||
Q_DECLARE_METATYPE(FBXGeometry::Pointer)
|
||||
|
||||
#endif // hifi_FBX_h_
|
|
@ -168,7 +168,8 @@ QString getID(const QVariantList& properties, int index = 0) {
|
|||
return processID(properties.at(index).toString());
|
||||
}
|
||||
|
||||
const char* HUMANIK_JOINTS[] = {
|
||||
/// The names of the joints in the Maya HumanIK rig
|
||||
static const std::array<const char*, 16> HUMANIK_JOINTS = {{
|
||||
"RightHand",
|
||||
"RightForeArm",
|
||||
"RightArm",
|
||||
|
@ -184,9 +185,8 @@ const char* HUMANIK_JOINTS[] = {
|
|||
"RightLeg",
|
||||
"LeftLeg",
|
||||
"RightFoot",
|
||||
"LeftFoot",
|
||||
""
|
||||
};
|
||||
"LeftFoot"
|
||||
}};
|
||||
|
||||
class FBXModel {
|
||||
public:
|
||||
|
@ -468,7 +468,7 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) {
|
|||
}
|
||||
|
||||
FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) {
|
||||
const FBXNode& node = _fbxNode;
|
||||
const FBXNode& node = _rootNode;
|
||||
QMap<QString, ExtractedMesh> meshes;
|
||||
QHash<QString, QString> modelIDsToNames;
|
||||
QHash<QString, int> meshIDsToMeshIndices;
|
||||
|
@ -512,11 +512,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS
|
|||
|
||||
|
||||
QVector<QString> humanIKJointNames;
|
||||
for (int i = 0;; i++) {
|
||||
for (int i = 0; i < (int) HUMANIK_JOINTS.size(); i++) {
|
||||
QByteArray jointName = HUMANIK_JOINTS[i];
|
||||
if (jointName.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
humanIKJointNames.append(processID(getString(joints.value(jointName, jointName))));
|
||||
}
|
||||
QVector<QString> humanIKJointIDs(humanIKJointNames.size());
|
||||
|
@ -846,12 +843,14 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS
|
|||
QByteArray filename = subobject.properties.at(0).toByteArray();
|
||||
QByteArray filepath = filename.replace('\\', '/');
|
||||
filename = fileOnUrl(filepath, url);
|
||||
qDebug() << "Filename" << filepath << filename;
|
||||
_textureFilepaths.insert(getID(object.properties), filepath);
|
||||
_textureFilenames.insert(getID(object.properties), filename);
|
||||
} else if (subobject.name == "TextureName" && subobject.properties.length() >= TEXTURE_NAME_MIN_SIZE) {
|
||||
// trim the name from the timestamp
|
||||
QString name = QString(subobject.properties.at(0).toByteArray());
|
||||
name = name.left(name.indexOf('['));
|
||||
qDebug() << "Filename" << name;
|
||||
_textureNames.insert(getID(object.properties), name);
|
||||
} else if (subobject.name == "Texture_Alpha_Source" && subobject.properties.length() >= TEXTURE_ALPHA_SOURCE_MIN_SIZE) {
|
||||
tex.assign<uint8_t>(tex.alphaSource, subobject.properties.at(0).value<int>());
|
||||
|
@ -940,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()) {
|
||||
|
@ -1840,9 +1839,11 @@ 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;
|
||||
|
||||
qDebug() << "Reading FBX: " << url;
|
||||
|
||||
return reader.extractFBXGeometry(mapping, url);
|
||||
}
|
||||
|
|
|
@ -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, bool deduplicate = true);
|
||||
QHash<QString, ExtractedMesh> meshes;
|
||||
static void buildModelMesh(FBXMesh& extractedMesh, const QString& url);
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@ FBXTexture FBXReader::getTexture(const QString& textureID) {
|
|||
texture.filename = filepath;
|
||||
}
|
||||
|
||||
texture.id = textureID;
|
||||
texture.name = _textureNames.value(textureID);
|
||||
texture.transform.setIdentity();
|
||||
texture.texcoordSet = 0;
|
||||
|
|
|
@ -9,6 +9,17 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma warning( push )
|
||||
#pragma warning( disable : 4267 )
|
||||
#endif
|
||||
|
||||
#include <draco/compression/decode.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma warning( pop )
|
||||
#endif
|
||||
|
||||
#include <iostream>
|
||||
#include <QBuffer>
|
||||
#include <QDataStream>
|
||||
|
@ -73,7 +84,7 @@ public:
|
|||
};
|
||||
|
||||
|
||||
void appendIndex(MeshData& data, QVector<int>& indices, int index) {
|
||||
void appendIndex(MeshData& data, QVector<int>& indices, int index, bool deduplicate) {
|
||||
if (index >= data.polygonIndices.size()) {
|
||||
return;
|
||||
}
|
||||
|
@ -145,12 +156,13 @@ void appendIndex(MeshData& data, QVector<int>& indices, int index) {
|
|||
}
|
||||
|
||||
QHash<Vertex, int>::const_iterator it = data.indices.find(vertex);
|
||||
if (it == data.indices.constEnd()) {
|
||||
if (!deduplicate || it == data.indices.constEnd()) {
|
||||
int newIndex = data.extracted.mesh.vertices.size();
|
||||
indices.append(newIndex);
|
||||
data.indices.insert(vertex, newIndex);
|
||||
data.extracted.newIndices.insert(vertexIndex, newIndex);
|
||||
data.extracted.mesh.vertices.append(position);
|
||||
data.extracted.mesh.originalIndices.append(vertexIndex);
|
||||
data.extracted.mesh.normals.append(normal);
|
||||
data.extracted.mesh.texCoords.append(vertex.texCoord);
|
||||
if (hasColors) {
|
||||
|
@ -165,14 +177,20 @@ void appendIndex(MeshData& data, QVector<int>& indices, int index) {
|
|||
}
|
||||
}
|
||||
|
||||
ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIndex) {
|
||||
ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate) {
|
||||
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 +322,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 +337,206 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn
|
|||
textures = getIntVector(subdata);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (child.name == "DracoMesh") {
|
||||
isDracoMesh = true;
|
||||
data.extracted.mesh.wasCompressed = 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 materialIDAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_MATERIAL_ID);
|
||||
auto originalIndexAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
|
||||
|
||||
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);
|
||||
if (normalAttribute) {
|
||||
data.extracted.mesh.normals.resize(numVertices);
|
||||
}
|
||||
|
||||
} 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 (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]));
|
||||
}
|
||||
|
||||
if (originalIndexAttribute) {
|
||||
auto mappedIndex = originalIndexAttribute->mapped_index(vertexIndex);
|
||||
|
||||
int32_t originalIndex;
|
||||
|
||||
originalIndexAttribute->ConvertValue<int32_t, 1>(mappedIndex, &originalIndex);
|
||||
|
||||
data.extracted.newIndices.insert(originalIndex, i);
|
||||
} else {
|
||||
data.extracted.newIndices.insert(i, i);
|
||||
}
|
||||
}
|
||||
beginIndex = endIndex;
|
||||
|
||||
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 (materialIDAttribute) {
|
||||
// read material ID and texture ID mappings into materials and texture vectors
|
||||
auto mappedIndex = materialIDAttribute->mapped_index(firstCorner);
|
||||
|
||||
materialIDAttribute->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++, deduplicate);
|
||||
appendIndex(data, part.quadIndices, beginIndex++, deduplicate);
|
||||
appendIndex(data, part.quadIndices, beginIndex++, deduplicate);
|
||||
appendIndex(data, part.quadIndices, beginIndex++, deduplicate);
|
||||
|
||||
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, deduplicate);
|
||||
appendIndex(data, part.triangleIndices, nextIndex++, deduplicate);
|
||||
appendIndex(data, part.triangleIndices, nextIndex, deduplicate);
|
||||
if (nextIndex >= data.polygonIndices.size() || data.polygonIndices.at(nextIndex) < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
beginIndex = endIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,15 +24,18 @@
|
|||
#include <shared/NsightHelpers.h>
|
||||
#include "ModelFormatLogging.h"
|
||||
|
||||
template<class T> int streamSize() {
|
||||
template<class T>
|
||||
int streamSize() {
|
||||
return sizeof(T);
|
||||
}
|
||||
|
||||
template<bool> int streamSize() {
|
||||
template<bool>
|
||||
int streamSize() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
template<class T> QVariant readBinaryArray(QDataStream& in, int& position) {
|
||||
template<class T>
|
||||
QVariant readBinaryArray(QDataStream& in, int& position) {
|
||||
quint32 arrayLength;
|
||||
quint32 encoding;
|
||||
quint32 compressedLength;
|
||||
|
@ -45,9 +48,8 @@ template<class T> QVariant readBinaryArray(QDataStream& in, int& position) {
|
|||
QVector<T> values;
|
||||
if ((int)QSysInfo::ByteOrder == (int)in.byteOrder()) {
|
||||
values.resize(arrayLength);
|
||||
const unsigned int DEFLATE_ENCODING = 1;
|
||||
QByteArray arrayData;
|
||||
if (encoding == DEFLATE_ENCODING) {
|
||||
if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) {
|
||||
// preface encoded data with uncompressed length
|
||||
QByteArray compressed(sizeof(quint32) + compressedLength, 0);
|
||||
*((quint32*)compressed.data()) = qToBigEndian<quint32>(arrayLength * sizeof(T));
|
||||
|
@ -69,8 +71,7 @@ template<class T> QVariant readBinaryArray(QDataStream& in, int& position) {
|
|||
}
|
||||
} else {
|
||||
values.reserve(arrayLength);
|
||||
const unsigned int DEFLATE_ENCODING = 1;
|
||||
if (encoding == DEFLATE_ENCODING) {
|
||||
if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) {
|
||||
// preface encoded data with uncompressed length
|
||||
QByteArray compressed(sizeof(quint32) + compressedLength, 0);
|
||||
*((quint32*)compressed.data()) = qToBigEndian<quint32>(arrayLength * sizeof(T));
|
||||
|
@ -350,8 +351,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 +377,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;
|
||||
|
|
200
libraries/fbx/src/FBXWriter.cpp
Normal file
200
libraries/fbx/src/FBXWriter.cpp
Normal file
|
@ -0,0 +1,200 @@
|
|||
//
|
||||
// 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>
|
||||
|
||||
#ifdef USE_FBX_2016_FORMAT
|
||||
using FBXEndOffset = int64_t;
|
||||
using FBXPropertyCount = uint64_t;
|
||||
using FBXListLength = uint64_t;
|
||||
#else
|
||||
using FBXEndOffset = int32_t;
|
||||
using FBXPropertyCount = uint32_t;
|
||||
using FBXListLength = uint32_t;
|
||||
#endif
|
||||
|
||||
template <typename T>
|
||||
void writeVector(QDataStream& out, char ch, QVector<T> vec) {
|
||||
// Minimum number of bytes to consider compressing
|
||||
const int ATTEMPT_COMPRESSION_THRESHOLD_BYTES = 2000;
|
||||
|
||||
out.device()->write(&ch, 1);
|
||||
out << (int32_t)vec.length();
|
||||
|
||||
auto data { QByteArray::fromRawData((const char*)vec.constData(), vec.length() * sizeof(T)) };
|
||||
|
||||
if (data.size() >= ATTEMPT_COMPRESSION_THRESHOLD_BYTES) {
|
||||
auto compressedDataWithLength { qCompress(data) };
|
||||
|
||||
// qCompress packs a length uint32 at the beginning of the buffer, but the FBX format
|
||||
// does not expect it. This removes it.
|
||||
auto compressedData = QByteArray::fromRawData(
|
||||
compressedDataWithLength.constData() + sizeof(uint32_t), compressedDataWithLength.size() - sizeof(uint32_t));
|
||||
|
||||
if (compressedData.size() < data.size()) {
|
||||
out << FBX_PROPERTY_COMPRESSED_FLAG;
|
||||
out << (int32_t)compressedData.size();
|
||||
out.writeRawData(compressedData.constData(), compressedData.size());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
out << FBX_PROPERTY_UNCOMPRESSED_FLAG;
|
||||
out << (int32_t)0;
|
||||
out.writeRawData(data.constData(), data.size());
|
||||
}
|
||||
|
||||
|
||||
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());
|
||||
|
||||
#ifdef USE_FBX_2016_FORMAT
|
||||
out << FBX_VERSION_2016;
|
||||
#else
|
||||
out << FBX_VERSION_2015;
|
||||
#endif
|
||||
|
||||
for (auto& child : root.children) {
|
||||
encodeNode(out, child);
|
||||
}
|
||||
encodeNode(out, FBXNode());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
void FBXWriter::encodeNode(QDataStream& out, const FBXNode& node) {
|
||||
auto device = out.device();
|
||||
auto nodeStartPos = device->pos();
|
||||
|
||||
// endOffset (temporary, updated later)
|
||||
out << (FBXEndOffset)0;
|
||||
|
||||
// Property count
|
||||
out << (FBXPropertyCount)node.properties.size();
|
||||
|
||||
// Property list length (temporary, updated later)
|
||||
out << (FBXListLength)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(FBXEndOffset) + sizeof(FBXPropertyCount));
|
||||
out << (FBXListLength)(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 << (FBXEndOffset)(nodeEndPos);
|
||||
|
||||
device->seek(nodeEndPos);
|
||||
}
|
||||
|
||||
void FBXWriter::encodeFBXProperty(QDataStream& out, const QVariant& prop) {
|
||||
auto type = prop.userType();
|
||||
switch (type) {
|
||||
case QMetaType::Short:
|
||||
out.device()->write("Y", 1);
|
||||
out << prop.value<int16_t>();
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
throw("Unsupported property type in FBXWriter::encodeNode: " + QString::number(type) + " " + prop.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
30
libraries/fbx/src/FBXWriter.h
Normal file
30
libraries/fbx/src/FBXWriter.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// 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>
|
||||
|
||||
//#define USE_FBX_2016_FORMAT
|
||||
|
||||
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
|
|
@ -515,20 +515,18 @@ TexturePointer Texture::build(const ktx::KTXDescriptor& descriptor) {
|
|||
return texture;
|
||||
}
|
||||
|
||||
|
||||
|
||||
TexturePointer Texture::unserialize(const cache::FilePointer& cacheEntry) {
|
||||
std::unique_ptr<ktx::KTX> ktxPointer = ktx::KTX::create(std::make_shared<storage::FileStorage>(cacheEntry->getFilepath().c_str()));
|
||||
if (!ktxPointer) {
|
||||
return nullptr;
|
||||
}
|
||||
std::unique_ptr<ktx::KTX> ktxPointer = ktx::KTX::create(std::make_shared<storage::FileStorage>(cacheEntry->getFilepath().c_str()));
|
||||
if (!ktxPointer) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto texture = build(ktxPointer->toDescriptor());
|
||||
if (texture) {
|
||||
texture->setKtxBacking(cacheEntry);
|
||||
}
|
||||
auto texture = build(ktxPointer->toDescriptor());
|
||||
if (texture) {
|
||||
texture->setKtxBacking(cacheEntry);
|
||||
}
|
||||
|
||||
return texture;
|
||||
return texture;
|
||||
}
|
||||
|
||||
TexturePointer Texture::unserialize(const std::string& ktxfile) {
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
#include <Profile.h>
|
||||
#include <StatTracker.h>
|
||||
#include <GLMHelpers.h>
|
||||
#include <SettingHandle.h>
|
||||
|
||||
#include "ImageLogging.h"
|
||||
|
||||
|
@ -31,12 +30,6 @@ using namespace gpu;
|
|||
|
||||
#define CPU_MIPMAPS 1
|
||||
|
||||
static std::mutex settingsMutex;
|
||||
static Setting::Handle<bool> compressColorTextures("hifi.graphics.compressColorTextures", false);
|
||||
static Setting::Handle<bool> compressNormalTextures("hifi.graphics.compressNormalTextures", false);
|
||||
static Setting::Handle<bool> compressGrayscaleTextures("hifi.graphics.compressGrayscaleTextures", false);
|
||||
static Setting::Handle<bool> compressCubeTextures("hifi.graphics.compressCubeTextures", false);
|
||||
|
||||
static const glm::uvec2 SPARSE_PAGE_SIZE(128);
|
||||
static const glm::uvec2 MAX_TEXTURE_SIZE(4096);
|
||||
bool DEV_DECIMATE_TEXTURES = false;
|
||||
|
@ -45,6 +38,11 @@ std::atomic<size_t> RECTIFIED_TEXTURE_COUNT{ 0 };
|
|||
|
||||
static const auto HDR_FORMAT = gpu::Element::COLOR_R11G11B10;
|
||||
|
||||
static std::atomic<bool> compressColorTextures { false };
|
||||
static std::atomic<bool> compressNormalTextures { false };
|
||||
static std::atomic<bool> compressGrayscaleTextures { false };
|
||||
static std::atomic<bool> compressCubeTextures { false };
|
||||
|
||||
bool needsSparseRectification(const glm::uvec2& size) {
|
||||
// Don't attempt to rectify small textures (textures less than the sparse page size in any dimension)
|
||||
if (glm::any(glm::lessThan(size, SPARSE_PAGE_SIZE))) {
|
||||
|
@ -106,59 +104,70 @@ TextureUsage::TextureLoader TextureUsage::getTextureLoaderForType(Type type, con
|
|||
}
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::createStrict2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return process2DTextureColorFromImage(srcImage, srcImageName, true);
|
||||
gpu::TexturePointer TextureUsage::createStrict2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return process2DTextureColorFromImage(srcImage, srcImageName, true, abortProcessing);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return process2DTextureColorFromImage(srcImage, srcImageName, false);
|
||||
gpu::TexturePointer TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return process2DTextureColorFromImage(srcImage, srcImageName, false, abortProcessing);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::createAlbedoTextureFromImage(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return process2DTextureColorFromImage(srcImage, srcImageName, false);
|
||||
gpu::TexturePointer TextureUsage::createAlbedoTextureFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return process2DTextureColorFromImage(srcImage, srcImageName, false, abortProcessing);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::createEmissiveTextureFromImage(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return process2DTextureColorFromImage(srcImage, srcImageName, false);
|
||||
gpu::TexturePointer TextureUsage::createEmissiveTextureFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return process2DTextureColorFromImage(srcImage, srcImageName, false, abortProcessing);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::createLightmapTextureFromImage(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return process2DTextureColorFromImage(srcImage, srcImageName, false);
|
||||
gpu::TexturePointer TextureUsage::createLightmapTextureFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return process2DTextureColorFromImage(srcImage, srcImageName, false, abortProcessing);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::createNormalTextureFromNormalImage(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return process2DTextureNormalMapFromImage(srcImage, srcImageName, false);
|
||||
gpu::TexturePointer TextureUsage::createNormalTextureFromNormalImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return process2DTextureNormalMapFromImage(srcImage, srcImageName, false, abortProcessing);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::createNormalTextureFromBumpImage(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return process2DTextureNormalMapFromImage(srcImage, srcImageName, true);
|
||||
gpu::TexturePointer TextureUsage::createNormalTextureFromBumpImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return process2DTextureNormalMapFromImage(srcImage, srcImageName, true, abortProcessing);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::createRoughnessTextureFromImage(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return process2DTextureGrayscaleFromImage(srcImage, srcImageName, false);
|
||||
gpu::TexturePointer TextureUsage::createRoughnessTextureFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return process2DTextureGrayscaleFromImage(srcImage, srcImageName, false, abortProcessing);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::createRoughnessTextureFromGlossImage(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return process2DTextureGrayscaleFromImage(srcImage, srcImageName, true);
|
||||
gpu::TexturePointer TextureUsage::createRoughnessTextureFromGlossImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return process2DTextureGrayscaleFromImage(srcImage, srcImageName, true, abortProcessing);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::createMetallicTextureFromImage(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return process2DTextureGrayscaleFromImage(srcImage, srcImageName, false);
|
||||
gpu::TexturePointer TextureUsage::createMetallicTextureFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return process2DTextureGrayscaleFromImage(srcImage, srcImageName, false, abortProcessing);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return processCubeTextureColorFromImage(srcImage, srcImageName, true);
|
||||
gpu::TexturePointer TextureUsage::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return processCubeTextureColorFromImage(srcImage, srcImageName, true, abortProcessing);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::createCubeTextureFromImageWithoutIrradiance(const QImage& srcImage, const std::string& srcImageName) {
|
||||
return processCubeTextureColorFromImage(srcImage, srcImageName, false);
|
||||
gpu::TexturePointer TextureUsage::createCubeTextureFromImageWithoutIrradiance(const QImage& srcImage, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
return processCubeTextureColorFromImage(srcImage, srcImageName, false, abortProcessing);
|
||||
}
|
||||
|
||||
|
||||
bool isColorTexturesCompressionEnabled() {
|
||||
#if CPU_MIPMAPS
|
||||
std::lock_guard<std::mutex> guard(settingsMutex);
|
||||
return compressColorTextures.get();
|
||||
return compressColorTextures.load();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
|
@ -166,8 +175,7 @@ bool isColorTexturesCompressionEnabled() {
|
|||
|
||||
bool isNormalTexturesCompressionEnabled() {
|
||||
#if CPU_MIPMAPS
|
||||
std::lock_guard<std::mutex> guard(settingsMutex);
|
||||
return compressNormalTextures.get();
|
||||
return compressNormalTextures.load();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
|
@ -175,8 +183,7 @@ bool isNormalTexturesCompressionEnabled() {
|
|||
|
||||
bool isGrayscaleTexturesCompressionEnabled() {
|
||||
#if CPU_MIPMAPS
|
||||
std::lock_guard<std::mutex> guard(settingsMutex);
|
||||
return compressGrayscaleTextures.get();
|
||||
return compressGrayscaleTextures.load();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
|
@ -184,31 +191,26 @@ bool isGrayscaleTexturesCompressionEnabled() {
|
|||
|
||||
bool isCubeTexturesCompressionEnabled() {
|
||||
#if CPU_MIPMAPS
|
||||
std::lock_guard<std::mutex> guard(settingsMutex);
|
||||
return compressCubeTextures.get();
|
||||
return compressCubeTextures.load();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void setColorTexturesCompressionEnabled(bool enabled) {
|
||||
std::lock_guard<std::mutex> guard(settingsMutex);
|
||||
compressColorTextures.set(enabled);
|
||||
compressColorTextures.store(enabled);
|
||||
}
|
||||
|
||||
void setNormalTexturesCompressionEnabled(bool enabled) {
|
||||
std::lock_guard<std::mutex> guard(settingsMutex);
|
||||
compressNormalTextures.set(enabled);
|
||||
compressNormalTextures.store(enabled);
|
||||
}
|
||||
|
||||
void setGrayscaleTexturesCompressionEnabled(bool enabled) {
|
||||
std::lock_guard<std::mutex> guard(settingsMutex);
|
||||
compressGrayscaleTextures.set(enabled);
|
||||
compressGrayscaleTextures.store(enabled);
|
||||
}
|
||||
|
||||
void setCubeTexturesCompressionEnabled(bool enabled) {
|
||||
std::lock_guard<std::mutex> guard(settingsMutex);
|
||||
compressCubeTextures.set(enabled);
|
||||
compressCubeTextures.store(enabled);
|
||||
}
|
||||
|
||||
static float denormalize(float value, const float minValue) {
|
||||
|
@ -230,7 +232,9 @@ uint32 packR11G11B10F(const glm::vec3& color) {
|
|||
return glm::packF2x11_1x10(ucolor);
|
||||
}
|
||||
|
||||
gpu::TexturePointer processImage(const QByteArray& content, const std::string& filename, int maxNumPixels, TextureUsage::Type textureType) {
|
||||
gpu::TexturePointer processImage(const QByteArray& content, const std::string& filename,
|
||||
int maxNumPixels, TextureUsage::Type textureType,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
// Help the QImage loader by extracting the image file format from the url filename ext.
|
||||
// Some tga are not created properly without it.
|
||||
auto filenameExtension = filename.substr(filename.find_last_of('.') + 1);
|
||||
|
@ -280,7 +284,7 @@ gpu::TexturePointer processImage(const QByteArray& content, const std::string& f
|
|||
}
|
||||
|
||||
auto loader = TextureUsage::getTextureLoaderForType(textureType);
|
||||
auto texture = loader(image, filename);
|
||||
auto texture = loader(image, filename, abortProcessing);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
@ -401,7 +405,24 @@ struct MyErrorHandler : public nvtt::ErrorHandler {
|
|||
}
|
||||
};
|
||||
|
||||
void generateHDRMips(gpu::Texture* texture, const QImage& image, int face) {
|
||||
class SequentialTaskDispatcher : public nvtt::TaskDispatcher {
|
||||
public:
|
||||
SequentialTaskDispatcher(const std::atomic<bool>& abortProcessing) : _abortProcessing(abortProcessing) {};
|
||||
|
||||
const std::atomic<bool>& _abortProcessing;
|
||||
|
||||
virtual void dispatch(nvtt::Task* task, void* context, int count) override {
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (!_abortProcessing.load()) {
|
||||
task(context, i);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void generateHDRMips(gpu::Texture* texture, const QImage& image, const std::atomic<bool>& abortProcessing, int face) {
|
||||
assert(image.format() == QIMAGE_HDR_FORMAT);
|
||||
|
||||
const int width = image.width(), height = image.height();
|
||||
|
@ -481,14 +502,18 @@ void generateHDRMips(gpu::Texture* texture, const QImage& image, int face) {
|
|||
surface.setAlphaMode(alphaMode);
|
||||
surface.setWrapMode(wrapMode);
|
||||
|
||||
SequentialTaskDispatcher dispatcher(abortProcessing);
|
||||
nvtt::Compressor compressor;
|
||||
context.setTaskDispatcher(&dispatcher);
|
||||
|
||||
context.compress(surface, face, mipLevel++, compressionOptions, outputOptions);
|
||||
while (surface.canMakeNextMipmap()) {
|
||||
while (surface.canMakeNextMipmap() && !abortProcessing.load()) {
|
||||
surface.buildNextMipmap(nvtt::MipmapFilter_Box);
|
||||
context.compress(surface, face, mipLevel++, compressionOptions, outputOptions);
|
||||
}
|
||||
}
|
||||
|
||||
void generateLDRMips(gpu::Texture* texture, QImage& image, int face) {
|
||||
void generateLDRMips(gpu::Texture* texture, QImage& image, const std::atomic<bool>& abortProcessing, int face) {
|
||||
if (image.format() != QImage::Format_ARGB32) {
|
||||
image = image.convertToFormat(QImage::Format_ARGB32);
|
||||
}
|
||||
|
@ -601,18 +626,20 @@ void generateLDRMips(gpu::Texture* texture, QImage& image, int face) {
|
|||
MyErrorHandler errorHandler;
|
||||
outputOptions.setErrorHandler(&errorHandler);
|
||||
|
||||
SequentialTaskDispatcher dispatcher(abortProcessing);
|
||||
nvtt::Compressor compressor;
|
||||
compressor.setTaskDispatcher(&dispatcher);
|
||||
compressor.process(inputOptions, compressionOptions, outputOptions);
|
||||
}
|
||||
|
||||
void generateMips(gpu::Texture* texture, QImage& image, int face = -1) {
|
||||
void generateMips(gpu::Texture* texture, QImage& image, const std::atomic<bool>& abortProcessing = false, int face = -1) {
|
||||
#if CPU_MIPMAPS
|
||||
PROFILE_RANGE(resource_parse, "generateMips");
|
||||
|
||||
if (image.format() == QIMAGE_HDR_FORMAT) {
|
||||
generateHDRMips(texture, image, face);
|
||||
generateHDRMips(texture, image, abortProcessing, face);
|
||||
} else {
|
||||
generateLDRMips(texture, image, face);
|
||||
generateLDRMips(texture, image, abortProcessing, face);
|
||||
}
|
||||
#else
|
||||
texture->autoGenerateMips(-1);
|
||||
|
@ -646,7 +673,8 @@ void processTextureAlpha(const QImage& srcImage, bool& validAlpha, bool& alphaAs
|
|||
validAlpha = (numOpaques != NUM_PIXELS);
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isStrict) {
|
||||
gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
bool isStrict, const std::atomic<bool>& abortProcessing) {
|
||||
PROFILE_RANGE(resource_parse, "process2DTextureColorFromImage");
|
||||
QImage image = processSourceImage(srcImage, false);
|
||||
bool validAlpha = image.hasAlphaChannel();
|
||||
|
@ -694,7 +722,7 @@ gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(const QImage& s
|
|||
}
|
||||
theTexture->setUsage(usage.build());
|
||||
theTexture->setStoredMipFormat(formatMip);
|
||||
generateMips(theTexture.get(), image);
|
||||
generateMips(theTexture.get(), image, abortProcessing);
|
||||
}
|
||||
|
||||
return theTexture;
|
||||
|
@ -770,7 +798,8 @@ QImage processBumpMap(QImage& image) {
|
|||
|
||||
return result;
|
||||
}
|
||||
gpu::TexturePointer TextureUsage::process2DTextureNormalMapFromImage(const QImage& srcImage, const std::string& srcImageName, bool isBumpMap) {
|
||||
gpu::TexturePointer TextureUsage::process2DTextureNormalMapFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
bool isBumpMap, const std::atomic<bool>& abortProcessing) {
|
||||
PROFILE_RANGE(resource_parse, "process2DTextureNormalMapFromImage");
|
||||
QImage image = processSourceImage(srcImage, false);
|
||||
|
||||
|
@ -795,13 +824,15 @@ gpu::TexturePointer TextureUsage::process2DTextureNormalMapFromImage(const QImag
|
|||
theTexture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR));
|
||||
theTexture->setSource(srcImageName);
|
||||
theTexture->setStoredMipFormat(formatMip);
|
||||
generateMips(theTexture.get(), image);
|
||||
generateMips(theTexture.get(), image, abortProcessing);
|
||||
}
|
||||
|
||||
return theTexture;
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::process2DTextureGrayscaleFromImage(const QImage& srcImage, const std::string& srcImageName, bool isInvertedPixels) {
|
||||
gpu::TexturePointer TextureUsage::process2DTextureGrayscaleFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
bool isInvertedPixels,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
PROFILE_RANGE(resource_parse, "process2DTextureGrayscaleFromImage");
|
||||
QImage image = processSourceImage(srcImage, false);
|
||||
|
||||
|
@ -829,7 +860,7 @@ gpu::TexturePointer TextureUsage::process2DTextureGrayscaleFromImage(const QImag
|
|||
theTexture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR));
|
||||
theTexture->setSource(srcImageName);
|
||||
theTexture->setStoredMipFormat(formatMip);
|
||||
generateMips(theTexture.get(), image);
|
||||
generateMips(theTexture.get(), image, abortProcessing);
|
||||
}
|
||||
|
||||
return theTexture;
|
||||
|
@ -1147,7 +1178,9 @@ QImage convertToHDRFormat(QImage srcImage, gpu::Element format) {
|
|||
return hdrImage;
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool generateIrradiance) {
|
||||
gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName,
|
||||
bool generateIrradiance,
|
||||
const std::atomic<bool>& abortProcessing) {
|
||||
PROFILE_RANGE(resource_parse, "processCubeTextureColorFromImage");
|
||||
|
||||
gpu::TexturePointer theTexture = nullptr;
|
||||
|
@ -1207,7 +1240,7 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(const QImage&
|
|||
theTexture->setStoredMipFormat(formatMip);
|
||||
|
||||
for (uint8 face = 0; face < faces.size(); ++face) {
|
||||
generateMips(theTexture.get(), faces[face], face);
|
||||
generateMips(theTexture.get(), faces[face], abortProcessing, face);
|
||||
}
|
||||
|
||||
// Generate irradiance while we are at it
|
||||
|
|
|
@ -41,26 +41,42 @@ enum Type {
|
|||
UNUSED_TEXTURE
|
||||
};
|
||||
|
||||
using TextureLoader = std::function<gpu::TexturePointer(const QImage&, const std::string&)>;
|
||||
using TextureLoader = std::function<gpu::TexturePointer(const QImage&, const std::string&, const std::atomic<bool>&)>;
|
||||
TextureLoader getTextureLoaderForType(Type type, const QVariantMap& options = QVariantMap());
|
||||
|
||||
gpu::TexturePointer create2DTextureFromImage(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer createStrict2DTextureFromImage(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer createAlbedoTextureFromImage(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer createEmissiveTextureFromImage(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer createNormalTextureFromNormalImage(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer createNormalTextureFromBumpImage(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer createRoughnessTextureFromImage(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer createRoughnessTextureFromGlossImage(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer createMetallicTextureFromImage(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer createCubeTextureFromImage(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer createCubeTextureFromImageWithoutIrradiance(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer createLightmapTextureFromImage(const QImage& image, const std::string& srcImageName);
|
||||
gpu::TexturePointer create2DTextureFromImage(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer createStrict2DTextureFromImage(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer createAlbedoTextureFromImage(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer createEmissiveTextureFromImage(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer createNormalTextureFromNormalImage(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer createNormalTextureFromBumpImage(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer createRoughnessTextureFromImage(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer createRoughnessTextureFromGlossImage(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer createMetallicTextureFromImage(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer createCubeTextureFromImage(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer createCubeTextureFromImageWithoutIrradiance(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer createLightmapTextureFromImage(const QImage& image, const std::string& srcImageName,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
|
||||
gpu::TexturePointer process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isStrict);
|
||||
gpu::TexturePointer process2DTextureNormalMapFromImage(const QImage& srcImage, const std::string& srcImageName, bool isBumpMap);
|
||||
gpu::TexturePointer process2DTextureGrayscaleFromImage(const QImage& srcImage, const std::string& srcImageName, bool isInvertedPixels);
|
||||
gpu::TexturePointer processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool generateIrradiance);
|
||||
gpu::TexturePointer process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isStrict,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer process2DTextureNormalMapFromImage(const QImage& srcImage, const std::string& srcImageName, bool isBumpMap,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer process2DTextureGrayscaleFromImage(const QImage& srcImage, const std::string& srcImageName, bool isInvertedPixels,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
gpu::TexturePointer processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool generateIrradiance,
|
||||
const std::atomic<bool>& abortProcessing);
|
||||
|
||||
} // namespace TextureUsage
|
||||
|
||||
|
@ -74,7 +90,9 @@ void setNormalTexturesCompressionEnabled(bool enabled);
|
|||
void setGrayscaleTexturesCompressionEnabled(bool enabled);
|
||||
void setCubeTexturesCompressionEnabled(bool enabled);
|
||||
|
||||
gpu::TexturePointer processImage(const QByteArray& content, const std::string& url, int maxNumPixels, TextureUsage::Type textureType);
|
||||
gpu::TexturePointer processImage(const QByteArray& content, const std::string& url,
|
||||
int maxNumPixels, TextureUsage::Type textureType,
|
||||
const std::atomic<bool>& abortProcessing = false);
|
||||
|
||||
} // namespace image
|
||||
|
||||
|
|
|
@ -77,6 +77,8 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) {
|
|||
texdir += '/';
|
||||
}
|
||||
_textureBaseUrl = resolveTextureBaseUrl(url, _url.resolved(texdir));
|
||||
} else {
|
||||
_textureBaseUrl = _effectiveBaseURL;
|
||||
}
|
||||
|
||||
auto animGraphVariant = mapping.value("animGraphUrl");
|
||||
|
@ -239,7 +241,10 @@ private:
|
|||
};
|
||||
|
||||
void GeometryDefinitionResource::downloadFinished(const QByteArray& data) {
|
||||
QThreadPool::globalInstance()->start(new GeometryReader(_self, _url, _mapping, data, _combineParts));
|
||||
qDebug() << "Processing geometry: " << _effectiveBaseURL;
|
||||
_url = _effectiveBaseURL;
|
||||
_textureBaseUrl = _effectiveBaseURL;
|
||||
QThreadPool::globalInstance()->start(new GeometryReader(_self, _effectiveBaseURL, _mapping, data, _combineParts));
|
||||
}
|
||||
|
||||
void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxGeometry) {
|
||||
|
@ -250,6 +255,7 @@ void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxG
|
|||
QHash<QString, size_t> materialIDAtlas;
|
||||
for (const FBXMaterial& material : _fbxGeometry->materials) {
|
||||
materialIDAtlas[material.materialID] = _materials.size();
|
||||
qDebug() << "setGeometryDefinition() " << _textureBaseUrl;
|
||||
_materials.push_back(std::make_shared<NetworkMaterial>(material, _textureBaseUrl));
|
||||
}
|
||||
|
||||
|
@ -342,6 +348,7 @@ Geometry::Geometry(const Geometry& geometry) {
|
|||
|
||||
_materials.reserve(geometry._materials.size());
|
||||
for (const auto& material : geometry._materials) {
|
||||
qDebug() << "Geometry() no base url...";
|
||||
_materials.push_back(std::make_shared<NetworkMaterial>(*material));
|
||||
}
|
||||
|
||||
|
@ -427,6 +434,7 @@ void GeometryResource::deleter() {
|
|||
void GeometryResource::setTextures() {
|
||||
if (_fbxGeometry) {
|
||||
for (const FBXMaterial& material : _fbxGeometry->materials) {
|
||||
qDebug() << "setTextures() " << _textureBaseUrl;
|
||||
_materials.push_back(std::make_shared<NetworkMaterial>(material, _textureBaseUrl));
|
||||
}
|
||||
}
|
||||
|
@ -456,7 +464,7 @@ void GeometryResourceWatcher::setResource(GeometryResource::Pointer resource) {
|
|||
_resource = resource;
|
||||
if (_resource) {
|
||||
if (_resource->isLoaded()) {
|
||||
_geometryRef = std::make_shared<Geometry>(*_resource);
|
||||
resourceFinished(true);
|
||||
} else {
|
||||
startWatching();
|
||||
}
|
||||
|
@ -528,6 +536,7 @@ model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, image
|
|||
NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl) :
|
||||
model::Material(*material._material)
|
||||
{
|
||||
qDebug() << "Created network material with base url: " << textureBaseUrl;
|
||||
_textures = Textures(MapChannel::NUM_MAP_CHANNELS);
|
||||
if (!material.albedoTexture.filename.isEmpty()) {
|
||||
auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP);
|
||||
|
|
|
@ -131,6 +131,7 @@ private:
|
|||
Geometry::Pointer& _geometryRef;
|
||||
};
|
||||
|
||||
|
||||
/// Stores cached model geometries.
|
||||
class ModelCache : public ResourceCache, public Dependency {
|
||||
Q_OBJECT
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include <QThreadPool>
|
||||
#include <QNetworkReply>
|
||||
#include <QPainter>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#if DEBUG_DUMP_TEXTURE_LOADS
|
||||
#include <QtCore/QFile>
|
||||
|
@ -189,8 +190,14 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUs
|
|||
if (url.scheme() == RESOURCE_SCHEME) {
|
||||
return getResourceTexture(url);
|
||||
}
|
||||
auto modifiedUrl = url;
|
||||
if (type == image::TextureUsage::CUBE_TEXTURE) {
|
||||
QUrlQuery query { url.query() };
|
||||
query.addQueryItem("skybox", "");
|
||||
modifiedUrl.setQuery(query.toString());
|
||||
}
|
||||
TextureExtra extra = { type, content, maxNumPixels };
|
||||
return ResourceCache::getResource(url, QUrl(), &extra).staticCast<NetworkTexture>();
|
||||
return ResourceCache::getResource(modifiedUrl, QUrl(), &extra).staticCast<NetworkTexture>();
|
||||
}
|
||||
|
||||
gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) {
|
||||
|
@ -257,7 +264,7 @@ gpu::TexturePointer getFallbackTextureForType(image::TextureUsage::Type type) {
|
|||
gpu::TexturePointer TextureCache::getImageTexture(const QString& path, image::TextureUsage::Type type, QVariantMap options) {
|
||||
QImage image = QImage(path);
|
||||
auto loader = image::TextureUsage::getTextureLoaderForType(type, options);
|
||||
return gpu::TexturePointer(loader(image, QUrl::fromLocalFile(path).fileName().toStdString()));
|
||||
return gpu::TexturePointer(loader(image, QUrl::fromLocalFile(path).fileName().toStdString(), false));
|
||||
}
|
||||
|
||||
QSharedPointer<Resource> TextureCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
|
||||
|
@ -291,6 +298,8 @@ NetworkTexture::NetworkTexture(const QUrl& url, image::TextureUsage::Type type,
|
|||
_textureSource = std::make_shared<gpu::TextureSource>();
|
||||
_lowestRequestedMipLevel = 0;
|
||||
|
||||
_shouldFailOnRedirect = !_sourceIsKTX;
|
||||
|
||||
if (type == image::TextureUsage::CUBE_TEXTURE) {
|
||||
setLoadPriority(this, SKYBOX_LOAD_PRIORITY);
|
||||
} else if (_sourceIsKTX) {
|
||||
|
@ -420,6 +429,21 @@ void NetworkTexture::makeRequest() {
|
|||
|
||||
}
|
||||
|
||||
bool NetworkTexture::handleFailedRequest(ResourceRequest::Result result) {
|
||||
if (!_sourceIsKTX && result == ResourceRequest::Result::RedirectFail) {
|
||||
auto newPath = _request->getRelativePathUrl();
|
||||
if (newPath.fileName().endsWith(".ktx")) {
|
||||
qDebug() << "Redirecting to" << newPath << "from" << _url;
|
||||
_sourceIsKTX = true;
|
||||
_activeUrl = newPath;
|
||||
_shouldFailOnRedirect = false;
|
||||
makeRequest();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return Resource::handleFailedRequest(result);
|
||||
}
|
||||
|
||||
void NetworkTexture::startRequestForNextMipLevel() {
|
||||
auto self = _self.lock();
|
||||
if (!self) {
|
||||
|
@ -519,7 +543,7 @@ void NetworkTexture::ktxInitialDataRequestFinished() {
|
|||
_ktxHighMipData = _ktxMipRequest->getData();
|
||||
handleFinishedInitialLoad();
|
||||
} else {
|
||||
if (handleFailedRequest(result)) {
|
||||
if (Resource::handleFailedRequest(result)) {
|
||||
_ktxResourceState = PENDING_INITIAL_LOAD;
|
||||
} else {
|
||||
_ktxResourceState = FAILED_TO_LOAD;
|
||||
|
@ -608,7 +632,7 @@ void NetworkTexture::ktxMipRequestFinished() {
|
|||
finishedLoading(false);
|
||||
}
|
||||
} else {
|
||||
if (handleFailedRequest(result)) {
|
||||
if (Resource::handleFailedRequest(result)) {
|
||||
_ktxResourceState = PENDING_MIP_REQUEST;
|
||||
} else {
|
||||
_ktxResourceState = FAILED_TO_LOAD;
|
||||
|
|
|
@ -75,6 +75,8 @@ protected:
|
|||
|
||||
virtual void downloadFinished(const QByteArray& data) override;
|
||||
|
||||
bool handleFailedRequest(ResourceRequest::Result result) override;
|
||||
|
||||
Q_INVOKABLE void loadContent(const QByteArray& content);
|
||||
Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight);
|
||||
|
||||
|
|
|
@ -185,6 +185,14 @@ RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetPath& o
|
|||
return request;
|
||||
}
|
||||
|
||||
SetBakingEnabledRequest* AssetClient::createSetBakingEnabledRequest(const AssetPathList& path, bool enabled) {
|
||||
auto bakingEnabledRequest = new SetBakingEnabledRequest(path, enabled);
|
||||
|
||||
bakingEnabledRequest->moveToThread(thread());
|
||||
|
||||
return bakingEnabledRequest;
|
||||
}
|
||||
|
||||
AssetRequest* AssetClient::createRequest(const AssetHash& hash, const ByteRange& byteRange) {
|
||||
auto request = new AssetRequest(hash, byteRange);
|
||||
|
||||
|
@ -585,6 +593,37 @@ MessageID AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetP
|
|||
return INVALID_MESSAGE_ID;
|
||||
}
|
||||
|
||||
MessageID AssetClient::setBakingEnabled(const AssetPathList& paths, bool enabled, MappingOperationCallback callback) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
|
||||
|
||||
if (assetServer) {
|
||||
auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true);
|
||||
|
||||
auto messageID = ++_currentID;
|
||||
packetList->writePrimitive(messageID);
|
||||
|
||||
packetList->writePrimitive(AssetMappingOperationType::SetBakingEnabled);
|
||||
|
||||
packetList->writePrimitive(enabled);
|
||||
|
||||
packetList->writePrimitive(int(paths.size()));
|
||||
|
||||
for (auto& path : paths) {
|
||||
packetList->writeString(path);
|
||||
}
|
||||
|
||||
if (nodeList->sendPacketList(std::move(packetList), *assetServer) != -1) {
|
||||
_pendingMappingRequests[assetServer][messageID] = callback;
|
||||
|
||||
return messageID;
|
||||
}
|
||||
}
|
||||
|
||||
callback(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
|
||||
return INVALID_MESSAGE_ID;
|
||||
}
|
||||
|
||||
bool AssetClient::cancelMappingRequest(MessageID id) {
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ class SetMappingRequest;
|
|||
class GetAllMappingsRequest;
|
||||
class DeleteMappingsRequest;
|
||||
class RenameMappingRequest;
|
||||
class SetBakingEnabledRequest;
|
||||
class AssetRequest;
|
||||
class AssetUpload;
|
||||
|
||||
|
@ -56,6 +57,7 @@ public:
|
|||
Q_INVOKABLE DeleteMappingsRequest* createDeleteMappingsRequest(const AssetPathList& paths);
|
||||
Q_INVOKABLE SetMappingRequest* createSetMappingRequest(const AssetPath& path, const AssetHash& hash);
|
||||
Q_INVOKABLE RenameMappingRequest* createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath);
|
||||
Q_INVOKABLE SetBakingEnabledRequest* createSetBakingEnabledRequest(const AssetPathList& path, bool enabled);
|
||||
Q_INVOKABLE AssetRequest* createRequest(const AssetHash& hash, const ByteRange& byteRange = ByteRange());
|
||||
Q_INVOKABLE AssetUpload* createUpload(const QString& filename);
|
||||
Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data);
|
||||
|
@ -81,6 +83,7 @@ private:
|
|||
MessageID setAssetMapping(const QString& path, const AssetHash& hash, MappingOperationCallback callback);
|
||||
MessageID deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback);
|
||||
MessageID renameAssetMapping(const AssetPath& oldPath, const AssetPath& newPath, MappingOperationCallback callback);
|
||||
MessageID setBakingEnabled(const AssetPathList& paths, bool enabled, MappingOperationCallback callback);
|
||||
|
||||
MessageID getAssetInfo(const QString& hash, GetInfoCallback callback);
|
||||
MessageID getAsset(const QString& hash, DataOffset start, DataOffset end,
|
||||
|
@ -119,6 +122,7 @@ private:
|
|||
friend class SetMappingRequest;
|
||||
friend class DeleteMappingsRequest;
|
||||
friend class RenameMappingRequest;
|
||||
friend class SetBakingEnabledRequest;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
#include "ByteRange.h"
|
||||
|
||||
const QString ATP_SCHEME { "atp:" };
|
||||
|
||||
class AssetRequest : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
|
|
@ -63,7 +63,7 @@ void AssetResourceRequest::doSend() {
|
|||
// This is an ATP path, we'll need to figure out what the mapping is.
|
||||
// This may incur a roundtrip to the asset-server, or it may return immediately from the cache in AssetClient.
|
||||
|
||||
auto path = _url.path();
|
||||
auto path = _url.path() + (_url.hasQuery() ? "?" + _url.query() : "");
|
||||
requestMappingForPath(path);
|
||||
}
|
||||
}
|
||||
|
@ -82,15 +82,27 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) {
|
|||
Q_ASSERT(_state == InProgress);
|
||||
Q_ASSERT(request == _assetMappingRequest);
|
||||
|
||||
bool failed = false;
|
||||
|
||||
switch (request->getError()) {
|
||||
case MappingRequest::NoError:
|
||||
// we have no error, we should have a resulting hash - use that to send of a request for that asset
|
||||
qCDebug(networking) << "Got mapping for:" << path << "=>" << request->getHash();
|
||||
|
||||
requestHash(request->getHash());
|
||||
|
||||
statTracker->incrementStat(STAT_ATP_MAPPING_REQUEST_SUCCESS);
|
||||
|
||||
// if we got a redirected path we need to store that with the resource request as relative path URL
|
||||
if (request->wasRedirected()) {
|
||||
_relativePathURL = ATP_SCHEME + request->getRedirectedPath();
|
||||
}
|
||||
|
||||
if (request->wasRedirected() && _failOnRedirect) {
|
||||
_result = RedirectFail;
|
||||
failed = true;
|
||||
} else {
|
||||
requestHash(request->getHash());
|
||||
}
|
||||
|
||||
break;
|
||||
default: {
|
||||
switch (request->getError()) {
|
||||
|
@ -107,17 +119,20 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) {
|
|||
break;
|
||||
}
|
||||
|
||||
// since we've failed we know we are finished
|
||||
_state = Finished;
|
||||
emit finished();
|
||||
|
||||
statTracker->incrementStat(STAT_ATP_MAPPING_REQUEST_FAILED);
|
||||
statTracker->incrementStat(STAT_ATP_REQUEST_FAILED);
|
||||
failed = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
_state = Finished;
|
||||
emit finished();
|
||||
|
||||
statTracker->incrementStat(STAT_ATP_MAPPING_REQUEST_FAILED);
|
||||
statTracker->incrementStat(STAT_ATP_REQUEST_FAILED);
|
||||
}
|
||||
|
||||
_assetMappingRequest->deleteLater();
|
||||
_assetMappingRequest = nullptr;
|
||||
});
|
||||
|
|
|
@ -84,3 +84,20 @@ bool isValidHash(const AssetHash& hash) {
|
|||
QRegExp hashRegex { ASSET_HASH_REGEX_STRING };
|
||||
return hashRegex.exactMatch(hash);
|
||||
}
|
||||
|
||||
QString bakingStatusToString(BakingStatus status) {
|
||||
switch (status) {
|
||||
case NotBaked:
|
||||
return "Not Baked";
|
||||
case Pending:
|
||||
return "Pending";
|
||||
case Baking:
|
||||
return "Baking";
|
||||
case Baked:
|
||||
return "Baked";
|
||||
case Error:
|
||||
return "Error";
|
||||
default:
|
||||
return "--";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ using DataOffset = int64_t;
|
|||
|
||||
using AssetPath = QString;
|
||||
using AssetHash = QString;
|
||||
using AssetMapping = std::map<AssetPath, AssetHash>;
|
||||
using AssetPathList = QStringList;
|
||||
|
||||
const size_t SHA256_HASH_LENGTH = 32;
|
||||
|
@ -34,6 +33,8 @@ const QString ASSET_FILE_PATH_REGEX_STRING = "^(\\/[^\\/\\0]+)+$";
|
|||
const QString ASSET_PATH_REGEX_STRING = "^\\/([^\\/\\0]+(\\/)?)+$";
|
||||
const QString ASSET_HASH_REGEX_STRING = QString("^[a-fA-F0-9]{%1}$").arg(SHA256_HASH_HEX_LENGTH);
|
||||
|
||||
const QString HIDDEN_BAKED_CONTENT_FOLDER = "/.baked/";
|
||||
|
||||
enum AssetServerError : uint8_t {
|
||||
NoError = 0,
|
||||
AssetNotFound,
|
||||
|
@ -49,9 +50,27 @@ enum AssetMappingOperationType : uint8_t {
|
|||
GetAll,
|
||||
Set,
|
||||
Delete,
|
||||
Rename
|
||||
Rename,
|
||||
SetBakingEnabled
|
||||
};
|
||||
|
||||
enum BakingStatus {
|
||||
Irrelevant,
|
||||
NotBaked,
|
||||
Pending,
|
||||
Baking,
|
||||
Baked,
|
||||
Error
|
||||
};
|
||||
|
||||
struct MappingInfo {
|
||||
AssetHash hash;
|
||||
BakingStatus status;
|
||||
QString bakingErrors;
|
||||
};
|
||||
|
||||
using AssetMapping = std::map<AssetPath, MappingInfo>;
|
||||
|
||||
QUrl getATPUrl(const QString& hash);
|
||||
|
||||
QByteArray hashData(const QByteArray& data);
|
||||
|
@ -63,4 +82,6 @@ bool isValidFilePath(const AssetPath& path);
|
|||
bool isValidPath(const AssetPath& path);
|
||||
bool isValidHash(const QString& hashString);
|
||||
|
||||
QString bakingStatusToString(BakingStatus status);
|
||||
|
||||
#endif // hifi_AssetUtils_h
|
||||
|
|
|
@ -87,6 +87,20 @@ void GetMappingRequest::doStart() {
|
|||
|
||||
if (!_error) {
|
||||
_hash = message->read(SHA256_HASH_LENGTH).toHex();
|
||||
|
||||
// check the boolean to see if this request got re-directed
|
||||
quint8 wasRedirected;
|
||||
message->readPrimitive(&wasRedirected);
|
||||
_wasRedirected = wasRedirected;
|
||||
|
||||
// if it did grab that re-directed path
|
||||
if (_wasRedirected) {
|
||||
_redirectedPath = message->readString();
|
||||
qDebug() << "Got redirected from " << _path << " to " << _redirectedPath;
|
||||
} else {
|
||||
qDebug() << "Not redirected: " << _path;
|
||||
}
|
||||
|
||||
}
|
||||
emit finished(this);
|
||||
});
|
||||
|
@ -117,12 +131,18 @@ void GetAllMappingsRequest::doStart() {
|
|||
|
||||
|
||||
if (!_error) {
|
||||
int numberOfMappings;
|
||||
uint32_t numberOfMappings;
|
||||
message->readPrimitive(&numberOfMappings);
|
||||
for (auto i = 0; i < numberOfMappings; ++i) {
|
||||
for (uint32_t i = 0; i < numberOfMappings; ++i) {
|
||||
auto path = message->readString();
|
||||
auto hash = message->read(SHA256_HASH_LENGTH).toHex();
|
||||
_mappings[path] = hash;
|
||||
BakingStatus status;
|
||||
QString lastBakeErrors;
|
||||
message->readPrimitive(&status);
|
||||
if (status == BakingStatus::Error) {
|
||||
lastBakeErrors = message->readString();
|
||||
}
|
||||
_mappings[path] = { hash, status, lastBakeErrors };
|
||||
}
|
||||
}
|
||||
emit finished(this);
|
||||
|
@ -257,3 +277,46 @@ void RenameMappingRequest::doStart() {
|
|||
emit finished(this);
|
||||
});
|
||||
}
|
||||
|
||||
SetBakingEnabledRequest::SetBakingEnabledRequest(const AssetPathList& paths, bool enabled) : _paths(paths), _enabled(enabled) {
|
||||
for (auto& path : _paths) {
|
||||
path = path.trimmed();
|
||||
}
|
||||
};
|
||||
|
||||
void SetBakingEnabledRequest::doStart() {
|
||||
|
||||
// short circuit the request if any of the paths are invalid
|
||||
for (auto& path : _paths) {
|
||||
if (!isValidPath(path)) {
|
||||
_error = MappingRequest::InvalidPath;
|
||||
emit finished(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
|
||||
_mappingRequestID = assetClient->setBakingEnabled(_paths, _enabled,
|
||||
[this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer<ReceivedMessage> message) {
|
||||
|
||||
_mappingRequestID = INVALID_MESSAGE_ID;
|
||||
if (!responseReceived) {
|
||||
_error = NetworkError;
|
||||
} else {
|
||||
switch (error) {
|
||||
case AssetServerError::NoError:
|
||||
_error = NoError;
|
||||
break;
|
||||
case AssetServerError::PermissionDenied:
|
||||
_error = PermissionDenied;
|
||||
break;
|
||||
default:
|
||||
_error = UnknownError;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit finished(this);
|
||||
});
|
||||
};
|
|
@ -53,6 +53,8 @@ public:
|
|||
GetMappingRequest(const AssetPath& path);
|
||||
|
||||
AssetHash getHash() const { return _hash; }
|
||||
AssetPath getRedirectedPath() const { return _redirectedPath; }
|
||||
bool wasRedirected() const { return _wasRedirected; }
|
||||
|
||||
signals:
|
||||
void finished(GetMappingRequest* thisRequest);
|
||||
|
@ -62,6 +64,10 @@ private:
|
|||
|
||||
AssetPath _path;
|
||||
AssetHash _hash;
|
||||
|
||||
|
||||
AssetPath _redirectedPath;
|
||||
bool _wasRedirected { false };
|
||||
};
|
||||
|
||||
class SetMappingRequest : public MappingRequest {
|
||||
|
@ -124,7 +130,22 @@ signals:
|
|||
private:
|
||||
virtual void doStart() override;
|
||||
|
||||
std::map<AssetPath, AssetHash> _mappings;
|
||||
AssetMapping _mappings;
|
||||
};
|
||||
|
||||
class SetBakingEnabledRequest : public MappingRequest {
|
||||
Q_OBJECT
|
||||
public:
|
||||
SetBakingEnabledRequest(const AssetPathList& path, bool enabled);
|
||||
|
||||
signals:
|
||||
void finished(SetBakingEnabledRequest* thisRequest);
|
||||
|
||||
private:
|
||||
virtual void doStart() override;
|
||||
|
||||
AssetPathList _paths;
|
||||
bool _enabled;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -687,8 +687,9 @@ void Resource::makeRequest() {
|
|||
PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_request->setByteRange(_requestByteRange);
|
||||
_request->setFailOnRedirect(_shouldFailOnRedirect);
|
||||
|
||||
qCDebug(resourceLog).noquote() << "Starting request for:" << _url.toDisplayString();
|
||||
emit loading();
|
||||
|
@ -731,6 +732,11 @@ void Resource::handleReplyFinished() {
|
|||
if (result == ResourceRequest::Success) {
|
||||
auto extraInfo = _url == _activeUrl ? "" : QString(", %1").arg(_activeUrl.toDisplayString());
|
||||
qCDebug(networking).noquote() << QString("Request finished for %1%2").arg(_url.toDisplayString(), extraInfo);
|
||||
|
||||
auto relativePathURL = _request->getRelativePathUrl();
|
||||
if (!relativePathURL.isEmpty()) {
|
||||
_effectiveBaseURL = relativePathURL;
|
||||
}
|
||||
|
||||
auto data = _request->getData();
|
||||
emit loaded(data);
|
||||
|
|
|
@ -449,11 +449,13 @@ protected:
|
|||
Q_INVOKABLE void allReferencesCleared();
|
||||
|
||||
/// Return true if the resource will be retried
|
||||
bool handleFailedRequest(ResourceRequest::Result result);
|
||||
virtual bool handleFailedRequest(ResourceRequest::Result result);
|
||||
|
||||
QUrl _url;
|
||||
QUrl _effectiveBaseURL{ _url };
|
||||
QUrl _activeUrl;
|
||||
ByteRange _requestByteRange;
|
||||
bool _shouldFailOnRedirect { false };
|
||||
|
||||
// _loaded == true means we are in a loaded and usable state. It is possible that there may still be
|
||||
// active requests/loading while in this state. Example: Progressive KTX downloads, where higher resolution
|
||||
|
|
|
@ -36,6 +36,7 @@ QString ResourceRequest::getResultString() const {
|
|||
case AccessDenied: return "Access Denied";
|
||||
case InvalidURL: return "Invalid URL";
|
||||
case NotFound: return "Not Found";
|
||||
case RedirectFail: return "Redirect Fail";
|
||||
default: return "Unspecified Error";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,8 @@ public:
|
|||
AccessDenied,
|
||||
InvalidByteRange,
|
||||
InvalidURL,
|
||||
NotFound
|
||||
NotFound,
|
||||
RedirectFail
|
||||
};
|
||||
Q_ENUM(Result)
|
||||
|
||||
|
@ -66,9 +67,11 @@ public:
|
|||
Result getResult() const { return _result; }
|
||||
QString getResultString() const;
|
||||
QUrl getUrl() const { return _url; }
|
||||
QUrl getRelativePathUrl() const { return _relativePathURL; }
|
||||
bool loadedFromCache() const { return _loadedFromCache; }
|
||||
bool getRangeRequestSuccessful() const { return _rangeRequestSuccessful; }
|
||||
bool getTotalSizeOfResource() const { return _totalSizeOfResource; }
|
||||
void setFailOnRedirect(bool failOnRedirect) { _failOnRedirect = failOnRedirect; }
|
||||
|
||||
void setCacheEnabled(bool value) { _cacheEnabled = value; }
|
||||
void setByteRange(ByteRange byteRange) { _byteRange = byteRange; }
|
||||
|
@ -84,9 +87,11 @@ protected:
|
|||
virtual void doSend() = 0;
|
||||
|
||||
QUrl _url;
|
||||
QUrl _relativePathURL;
|
||||
State _state { NotStarted };
|
||||
Result _result;
|
||||
QByteArray _data;
|
||||
bool _failOnRedirect { false };
|
||||
bool _cacheEnabled { true };
|
||||
bool _loadedFromCache { false };
|
||||
ByteRange _byteRange;
|
||||
|
|
|
@ -42,6 +42,9 @@ PacketVersion versionForPacketType(PacketType packetType) {
|
|||
return static_cast<PacketVersion>(MessageDataVersion::TextOrBinaryData);
|
||||
case PacketType::ICEServerHeartbeat:
|
||||
return 18; // ICE Server Heartbeat signing
|
||||
case PacketType::AssetMappingOperation:
|
||||
case PacketType::AssetMappingOperationReply:
|
||||
return static_cast<PacketVersion>(AssetServerPacketVersion::RedirectedMappings);
|
||||
case PacketType::AssetGetInfo:
|
||||
case PacketType::AssetGet:
|
||||
case PacketType::AssetUpload:
|
||||
|
@ -65,7 +68,6 @@ PacketVersion versionForPacketType(PacketType packetType) {
|
|||
case PacketType::MicrophoneAudioWithEcho:
|
||||
case PacketType::AudioStreamStats:
|
||||
return static_cast<PacketVersion>(AudioVersion::HighDynamicRangeVolume);
|
||||
|
||||
default:
|
||||
return 17;
|
||||
}
|
||||
|
|
|
@ -140,11 +140,16 @@ public:
|
|||
|
||||
const static QSet<PacketTypeEnum::Value> getNonVerifiedPackets() {
|
||||
const static QSet<PacketTypeEnum::Value> NON_VERIFIED_PACKETS = QSet<PacketTypeEnum::Value>()
|
||||
<< PacketTypeEnum::Value::NodeJsonStats << PacketTypeEnum::Value::EntityQuery
|
||||
<< PacketTypeEnum::Value::OctreeDataNack << PacketTypeEnum::Value::EntityEditNack
|
||||
<< PacketTypeEnum::Value::DomainListRequest << PacketTypeEnum::Value::StopNode
|
||||
<< PacketTypeEnum::Value::DomainDisconnectRequest << PacketTypeEnum::Value::UsernameFromIDRequest
|
||||
<< PacketTypeEnum::Value::NodeKickRequest << PacketTypeEnum::Value::NodeMuteRequest;
|
||||
<< PacketTypeEnum::Value::NodeJsonStats
|
||||
<< PacketTypeEnum::Value::EntityQuery
|
||||
<< PacketTypeEnum::Value::OctreeDataNack
|
||||
<< PacketTypeEnum::Value::EntityEditNack
|
||||
<< PacketTypeEnum::Value::DomainListRequest
|
||||
<< PacketTypeEnum::Value::StopNode
|
||||
<< PacketTypeEnum::Value::DomainDisconnectRequest
|
||||
<< PacketTypeEnum::Value::UsernameFromIDRequest
|
||||
<< PacketTypeEnum::Value::NodeKickRequest
|
||||
<< PacketTypeEnum::Value::NodeMuteRequest;
|
||||
return NON_VERIFIED_PACKETS;
|
||||
}
|
||||
|
||||
|
@ -269,7 +274,8 @@ enum class EntityQueryPacketVersion: PacketVersion {
|
|||
|
||||
enum class AssetServerPacketVersion: PacketVersion {
|
||||
VegasCongestionControl = 19,
|
||||
RangeRequestSupport
|
||||
RangeRequestSupport,
|
||||
RedirectedMappings
|
||||
};
|
||||
|
||||
enum class AvatarMixerPacketVersion : PacketVersion {
|
||||
|
|
|
@ -120,7 +120,7 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
|
|||
_dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR);
|
||||
_dynamicsWorld->addAction(this);
|
||||
// restore gravity settings because adding an object to the world overwrites its gravity setting
|
||||
_rigidBody->setGravity(_gravity * _currentUp);
|
||||
_rigidBody->setGravity(_currentGravity * _currentUp);
|
||||
btCollisionShape* shape = _rigidBody->getCollisionShape();
|
||||
assert(shape && shape->getShapeType() == CONVEX_HULL_SHAPE_PROXYTYPE);
|
||||
_ghost.setCharacterShape(static_cast<btConvexHullShape*>(shape));
|
||||
|
@ -302,7 +302,7 @@ void CharacterController::playerStep(btCollisionWorld* collisionWorld, btScalar
|
|||
// add minimum velocity to counteract gravity's displacement during one step
|
||||
// Note: the 0.5 factor comes from the fact that we really want the
|
||||
// average velocity contribution from gravity during the step
|
||||
stepUpSpeed -= 0.5f * _gravity * timeToStep; // remember: _gravity is negative scalar
|
||||
stepUpSpeed -= 0.5f * _currentGravity * timeToStep; // remember: _gravity is negative scalar
|
||||
|
||||
btScalar vDotUp = velocity.dot(_currentUp);
|
||||
if (vDotUp < stepUpSpeed) {
|
||||
|
@ -351,6 +351,28 @@ static const char* stateToStr(CharacterController::State state) {
|
|||
}
|
||||
#endif // #ifdef DEBUG_STATE_CHANGE
|
||||
|
||||
void CharacterController::updateCurrentGravity() {
|
||||
int16_t collisionGroup = computeCollisionGroup();
|
||||
if (_state == State::Hover || collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS) {
|
||||
_currentGravity = 0.0f;
|
||||
} else {
|
||||
_currentGravity = _gravity;
|
||||
}
|
||||
if (_rigidBody) {
|
||||
_rigidBody->setGravity(_currentGravity * _currentUp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void CharacterController::setGravity(float gravity) {
|
||||
_gravity = gravity;
|
||||
updateCurrentGravity();
|
||||
}
|
||||
|
||||
float CharacterController::getGravity() {
|
||||
return _gravity;
|
||||
}
|
||||
|
||||
#ifdef DEBUG_STATE_CHANGE
|
||||
void CharacterController::setState(State desiredState, const char* reason) {
|
||||
#else
|
||||
|
@ -365,19 +387,7 @@ void CharacterController::setState(State desiredState) {
|
|||
qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason;
|
||||
#endif
|
||||
_state = desiredState;
|
||||
updateGravity();
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterController::updateGravity() {
|
||||
int16_t collisionGroup = computeCollisionGroup();
|
||||
if (_state == State::Hover || collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS) {
|
||||
_gravity = 0.0f;
|
||||
} else {
|
||||
_gravity = DEFAULT_AVATAR_GRAVITY;
|
||||
}
|
||||
if (_rigidBody) {
|
||||
_rigidBody->setGravity(_gravity * _currentUp);
|
||||
updateCurrentGravity();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -436,14 +446,14 @@ void CharacterController::handleChangedCollisionGroup() {
|
|||
_dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR);
|
||||
}
|
||||
_pendingFlags &= ~PENDING_FLAG_UPDATE_COLLISION_GROUP;
|
||||
updateGravity();
|
||||
updateCurrentGravity();
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterController::updateUpAxis(const glm::quat& rotation) {
|
||||
_currentUp = quatRotate(glmToBullet(rotation), LOCAL_UP_AXIS);
|
||||
if (_rigidBody) {
|
||||
_rigidBody->setGravity(_gravity * _currentUp);
|
||||
_rigidBody->setGravity(_currentGravity * _currentUp);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
#include "BulletUtil.h"
|
||||
#include "CharacterGhostObject.h"
|
||||
#include "AvatarConstants.h"
|
||||
|
||||
const uint32_t PENDING_FLAG_ADD_TO_SIMULATION = 1U << 0;
|
||||
const uint32_t PENDING_FLAG_REMOVE_FROM_SIMULATION = 1U << 1;
|
||||
|
@ -42,15 +43,18 @@ const btScalar MAX_CHARACTER_MOTOR_TIMESCALE = 60.0f; // one minute
|
|||
const btScalar MIN_CHARACTER_MOTOR_TIMESCALE = 0.05f;
|
||||
|
||||
class CharacterController : public btCharacterControllerInterface {
|
||||
|
||||
public:
|
||||
CharacterController();
|
||||
virtual ~CharacterController();
|
||||
|
||||
bool needsRemoval() const;
|
||||
bool needsAddition() const;
|
||||
virtual void setDynamicsWorld(btDynamicsWorld* world);
|
||||
btCollisionObject* getCollisionObject() { return _rigidBody; }
|
||||
|
||||
void setGravity(float gravity);
|
||||
float getGravity();
|
||||
|
||||
virtual void updateShapeIfNecessary() = 0;
|
||||
|
||||
// overrides from btCharacterControllerInterface
|
||||
|
@ -131,7 +135,7 @@ protected:
|
|||
#endif
|
||||
|
||||
virtual void updateMassProperties() = 0;
|
||||
void updateGravity();
|
||||
void updateCurrentGravity();
|
||||
void updateUpAxis(const glm::quat& rotation);
|
||||
bool checkForSupport(btCollisionWorld* collisionWorld);
|
||||
|
||||
|
@ -184,7 +188,8 @@ protected:
|
|||
bool _stepUpEnabled { true };
|
||||
bool _hasSupport;
|
||||
|
||||
btScalar _gravity { 0.0f };
|
||||
btScalar _currentGravity { 0.0f };
|
||||
btScalar _gravity { DEFAULT_AVATAR_GRAVITY };
|
||||
|
||||
btScalar _followTime;
|
||||
btVector3 _followLinearDisplacement;
|
||||
|
|
|
@ -321,17 +321,25 @@ template <> void payloadRender(const ModelMeshPartPayload::Pointer& payload, Ren
|
|||
|
||||
}
|
||||
|
||||
ModelMeshPartPayload::ModelMeshPartPayload(ModelPointer model, int _meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform) :
|
||||
_meshIndex(_meshIndex),
|
||||
ModelMeshPartPayload::ModelMeshPartPayload(ModelPointer model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform) :
|
||||
_meshIndex(meshIndex),
|
||||
_shapeID(shapeIndex) {
|
||||
|
||||
assert(model && model->isLoaded());
|
||||
_model = model;
|
||||
auto& modelMesh = model->getGeometry()->getMeshes().at(_meshIndex);
|
||||
const Model::MeshState& state = model->getMeshState(_meshIndex);
|
||||
|
||||
updateMeshPart(modelMesh, partIndex);
|
||||
computeAdjustedLocalBound(state.clusterMatrices);
|
||||
|
||||
updateTransform(transform, offsetTransform);
|
||||
Transform renderTransform = transform;
|
||||
if (state.clusterMatrices.size() == 1) {
|
||||
renderTransform = transform.worldTransform(Transform(state.clusterMatrices[0]));
|
||||
}
|
||||
updateTransformForSkinnedMesh(renderTransform, transform, state.clusterBuffer);
|
||||
|
||||
initCache();
|
||||
}
|
||||
|
||||
|
|
|
@ -209,11 +209,6 @@ void Model::updateRenderItems() {
|
|||
return;
|
||||
}
|
||||
|
||||
glm::vec3 scale = getScale();
|
||||
if (_collisionGeometry) {
|
||||
// _collisionGeometry is already scaled
|
||||
scale = glm::vec3(1.0f);
|
||||
}
|
||||
_needsUpdateClusterMatrices = true;
|
||||
_renderItemsNeedUpdate = false;
|
||||
|
||||
|
@ -221,7 +216,7 @@ void Model::updateRenderItems() {
|
|||
// the application will ensure only the last lambda is actually invoked.
|
||||
void* key = (void*)this;
|
||||
std::weak_ptr<Model> weakSelf = shared_from_this();
|
||||
AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [weakSelf, scale]() {
|
||||
AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [weakSelf]() {
|
||||
|
||||
// do nothing, if the model has already been destroyed.
|
||||
auto self = weakSelf.lock();
|
||||
|
@ -1219,6 +1214,7 @@ const render::ItemIDs& Model::fetchRenderItemIDs() const {
|
|||
}
|
||||
|
||||
void Model::createRenderItemSet() {
|
||||
updateClusterMatrices();
|
||||
if (_collisionGeometry) {
|
||||
if (_collisionRenderItems.empty()) {
|
||||
createCollisionRenderItemSet();
|
||||
|
@ -1269,7 +1265,6 @@ void Model::createVisibleRenderItemSet() {
|
|||
shapeID++;
|
||||
}
|
||||
}
|
||||
computeMeshPartLocalBounds();
|
||||
}
|
||||
|
||||
void Model::createCollisionRenderItemSet() {
|
||||
|
|
|
@ -81,10 +81,10 @@ public:
|
|||
float getColorB() const { return color.b; }
|
||||
|
||||
glm::vec3 color{ 1.f, 0.7f, 0.2f };
|
||||
float width{ 3.f };
|
||||
float intensity{ 1.f };
|
||||
float fillOpacityUnoccluded{ 0.35f };
|
||||
float fillOpacityOccluded{ 0.1f };
|
||||
float width{ 2.0f };
|
||||
float intensity{ 0.9f };
|
||||
float fillOpacityUnoccluded{ 0.0f };
|
||||
float fillOpacityOccluded{ 0.0f };
|
||||
bool glow{ false };
|
||||
|
||||
signals:
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
layout(location = 0) in vec4 inColor;
|
||||
in vec4 _color;
|
||||
in float distanceFromCenter;
|
||||
|
||||
out vec4 _fragColor;
|
||||
|
||||
|
@ -17,10 +18,10 @@ void main(void) {
|
|||
// The incoming value actually ranges from -1 to 1, so modify it
|
||||
// so that it goes from 0 -> 1 -> 0 with the solid alpha being at
|
||||
// the center of the line
|
||||
float alpha = 1.0 - abs(inColor.a);
|
||||
float alpha = 1.0 - abs(distanceFromCenter);
|
||||
|
||||
// Convert from a linear alpha curve to a sharp peaked one
|
||||
alpha = pow(alpha, 10);
|
||||
alpha = _color.a * pow(alpha, 10);
|
||||
|
||||
// Drop everything where the curve falls off to nearly nothing
|
||||
if (alpha <= 0.05) {
|
||||
|
@ -28,6 +29,5 @@ void main(void) {
|
|||
}
|
||||
|
||||
// Emit the color
|
||||
_fragColor = vec4(inColor.rgb, alpha);
|
||||
return;
|
||||
_fragColor = vec4(_color.rgb, alpha);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,9 @@ layout(std140) uniform lineData {
|
|||
vec4 color;
|
||||
};
|
||||
|
||||
layout(location = 0) out vec4 _color;
|
||||
out vec4 _color;
|
||||
// the distance from the center in 'quad space'
|
||||
out float distanceFromCenter;
|
||||
|
||||
void main(void) {
|
||||
_color = color;
|
||||
|
@ -45,11 +47,10 @@ void main(void) {
|
|||
// Add or subtract the orthogonal vector based on a different vertex ID
|
||||
// calculation
|
||||
if (gl_VertexID < 2) {
|
||||
// Use the alpha channel to store the distance from the center in 'quad space'
|
||||
_color.a = -1.0;
|
||||
distanceFromCenter = -1.0;
|
||||
eye.xyz -= orthogonal;
|
||||
} else {
|
||||
_color.a = 1.0;
|
||||
distanceFromCenter = 1.0;
|
||||
eye.xyz += orthogonal;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ namespace render {
|
|||
class Scene;
|
||||
using ScenePointer = std::shared_ptr<Scene>;
|
||||
class ShapePipeline;
|
||||
class Transaction;
|
||||
}
|
||||
|
||||
using RenderArgs = render::Args;
|
||||
|
|
|
@ -55,7 +55,6 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu
|
|||
|
||||
|
||||
void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) {
|
||||
const QString ATP_SCHEME { "atp:" };
|
||||
|
||||
if (!urlString.startsWith(ATP_SCHEME)) {
|
||||
return;
|
||||
|
@ -89,6 +88,20 @@ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callb
|
|||
assetRequest->start();
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::setBakingEnabled(QString path, bool enabled, QScriptValue callback) {
|
||||
auto setBakingEnabledRequest = DependencyManager::get<AssetClient>()->createSetBakingEnabledRequest({ path }, enabled);
|
||||
|
||||
QObject::connect(setBakingEnabledRequest, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable {
|
||||
if (callback.isFunction()) {
|
||||
QString error = request->getErrorString();
|
||||
QScriptValueList args{ error };
|
||||
callback.call(_engine->currentContext()->thisObject(), args);
|
||||
}
|
||||
request->deleteLater();
|
||||
});
|
||||
setBakingEnabledRequest->start();
|
||||
}
|
||||
|
||||
#if (PR_BUILD || DEV_BUILD)
|
||||
void AssetScriptingInterface::sendFakedHandshake() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
|
|
@ -75,6 +75,8 @@ public:
|
|||
* @param {string} error
|
||||
*/
|
||||
Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback);
|
||||
|
||||
Q_INVOKABLE void setBakingEnabled(QString path, bool enabled, QScriptValue callback);
|
||||
|
||||
#if (PR_BUILD || DEV_BUILD)
|
||||
Q_INVOKABLE void sendFakedHandshake();
|
||||
|
|
|
@ -54,9 +54,24 @@ float RecordingScriptingInterface::playerLength() const {
|
|||
return _player->length();
|
||||
}
|
||||
|
||||
void RecordingScriptingInterface::playClip(NetworkClipLoaderPointer clipLoader, const QString& url, QScriptValue callback) {
|
||||
_player->queueClip(clipLoader->getClip());
|
||||
|
||||
if (callback.isFunction()) {
|
||||
QScriptValueList args { true, url };
|
||||
callback.call(_scriptEngine->globalObject(), args);
|
||||
}
|
||||
}
|
||||
|
||||
void RecordingScriptingInterface::loadRecording(const QString& url, QScriptValue callback) {
|
||||
auto clipLoader = DependencyManager::get<recording::ClipCache>()->getClipLoader(url);
|
||||
|
||||
if (clipLoader->isLoaded()) {
|
||||
qCDebug(scriptengine) << "Recording already loaded from" << url;
|
||||
playClip(clipLoader, url, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
// hold a strong pointer to the loading clip so that it has a chance to load
|
||||
_clipLoaders.insert(clipLoader);
|
||||
|
||||
|
@ -69,12 +84,7 @@ void RecordingScriptingInterface::loadRecording(const QString& url, QScriptValue
|
|||
if (auto clipLoader = weakClipLoader.toStrongRef()) {
|
||||
qCDebug(scriptengine) << "Loaded recording from" << url;
|
||||
|
||||
_player->queueClip(clipLoader->getClip());
|
||||
|
||||
if (callback.isFunction()) {
|
||||
QScriptValueList args { true, url };
|
||||
callback.call(_scriptEngine->globalObject(), args);
|
||||
}
|
||||
playClip(clipLoader, url, callback);
|
||||
|
||||
// drop our strong pointer to this clip so it is cleaned up
|
||||
_clipLoaders.remove(clipLoader);
|
||||
|
|
|
@ -88,6 +88,9 @@ protected:
|
|||
|
||||
QSharedPointer<BaseScriptEngine> _scriptEngine;
|
||||
QSet<recording::NetworkClipLoaderPointer> _clipLoaders;
|
||||
|
||||
private:
|
||||
void playClip(recording::NetworkClipLoaderPointer clipLoader, const QString& url, QScriptValue callback);
|
||||
};
|
||||
|
||||
#endif // hifi_RecordingScriptingInterface_h
|
||||
|
|
|
@ -54,6 +54,19 @@ QString PathUtils::getAppLocalDataFilePath(const QString& filename) {
|
|||
return QDir(getAppLocalDataPath()).absoluteFilePath(filename);
|
||||
}
|
||||
|
||||
QString PathUtils::generateTemporaryDir() {
|
||||
QDir rootTempDir = QDir::tempPath();
|
||||
QString appName = qApp->applicationName();
|
||||
for (auto i = 0; i < 64; ++i) {
|
||||
auto now = std::chrono::system_clock::now().time_since_epoch().count();
|
||||
QDir tempDir = rootTempDir.filePath(appName + "-" + QString::number(now));
|
||||
if (tempDir.mkpath(".")) {
|
||||
return tempDir.absolutePath();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
QString fileNameWithoutExtension(const QString& fileName, const QVector<QString> possibleExtensions) {
|
||||
QString fileNameLowered = fileName.toLower();
|
||||
foreach (const QString possibleExtension, possibleExtensions) {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QDir>
|
||||
|
||||
#include "DependencyManager.h"
|
||||
|
||||
|
@ -36,6 +37,8 @@ public:
|
|||
static QString getAppDataFilePath(const QString& filename);
|
||||
static QString getAppLocalDataFilePath(const QString& filename);
|
||||
|
||||
static QString generateTemporaryDir();
|
||||
|
||||
static Qt::CaseSensitivity getFSCaseSensitivity();
|
||||
static QString stripFilename(const QUrl& url);
|
||||
// note: this is FS-case-sensitive version of parentURL.isParentOf(childURL)
|
||||
|
|
|
@ -1182,3 +1182,19 @@ void SpatiallyNestable::dump(const QString& prefix) const {
|
|||
parent->dump(prefix + " ");
|
||||
}
|
||||
}
|
||||
|
||||
bool SpatiallyNestable::isParentPathComplete() const {
|
||||
static const QUuid IDENTITY;
|
||||
QUuid parentID = getParentID();
|
||||
if (parentID.isNull() || parentID == IDENTITY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
SpatiallyNestablePointer parent = getParentPointer(success);
|
||||
if (!success || !parent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent->isParentPathComplete();
|
||||
}
|
||||
|
|
|
@ -66,6 +66,10 @@ public:
|
|||
|
||||
static QString nestableTypeToString(NestableType nestableType);
|
||||
|
||||
|
||||
virtual bool isParentPathComplete() const;
|
||||
|
||||
|
||||
// world frame
|
||||
virtual const Transform getTransform(bool& success, int depth = 0) const;
|
||||
virtual const Transform getTransform() const;
|
||||
|
|
|
@ -211,12 +211,12 @@ class UrlHandler : public QObject {
|
|||
public:
|
||||
Q_INVOKABLE bool canHandleUrl(const QString& url) {
|
||||
static auto handler = dynamic_cast<AbstractUriHandler*>(qApp);
|
||||
return handler->canAcceptURL(url);
|
||||
return handler && handler->canAcceptURL(url);
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool handleUrl(const QString& url) {
|
||||
static auto handler = dynamic_cast<AbstractUriHandler*>(qApp);
|
||||
return handler->acceptURL(url);
|
||||
return handler && handler->acceptURL(url);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -987,8 +987,8 @@ static bool equals(const QByteArray& byteArray, const uint8_t* ptr) {
|
|||
return ptr[i] == 0x00;
|
||||
}
|
||||
|
||||
void OffscreenQmlSurface::synthesizeKeyPress(QString key) {
|
||||
auto eventHandler = getEventHandler();
|
||||
void OffscreenQmlSurface::synthesizeKeyPress(QString key, QObject* targetOverride) {
|
||||
auto eventHandler = targetOverride ? targetOverride : getEventHandler();
|
||||
if (eventHandler) {
|
||||
auto utf8Key = key.toUtf8();
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ public:
|
|||
bool eventFilter(QObject* originalDestination, QEvent* event) override;
|
||||
|
||||
void setKeyboardRaised(QObject* object, bool raised, bool numeric = false);
|
||||
Q_INVOKABLE void synthesizeKeyPress(QString key);
|
||||
Q_INVOKABLE void synthesizeKeyPress(QString key, QObject* targetOverride = nullptr);
|
||||
|
||||
using TextureAndFence = std::pair<uint32_t, void*>;
|
||||
// Checks to see if a new texture is available. If one is, the function returns true and
|
||||
|
|
|
@ -61,7 +61,7 @@ void RequestFilters::interceptHFWebEngineRequest(QWebEngineUrlRequestInfo& info)
|
|||
|
||||
// During the period in which we have HFC commerce in the system, but not applied everywhere:
|
||||
const QString tokenStringCommerce{ "Chrome/48.0 (HighFidelityInterface WithHFC)" };
|
||||
static Setting::Handle<bool> _settingSwitch{ "inspectionMode", false };
|
||||
static Setting::Handle<bool> _settingSwitch{ "commerce", false };
|
||||
bool isMoney = _settingSwitch.get();
|
||||
|
||||
const QString tokenString = !isAuthable ? tokenStringMobile : (isMoney ? tokenStringCommerce : tokenStringMetaverse);
|
||||
|
|
45
scripts/developer/tests/gravityScript.js
Normal file
45
scripts/developer/tests/gravityScript.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// Gravity Script 1.0
|
||||
// ************
|
||||
//
|
||||
// Created by Cain Kilgore on 9/14/2017
|
||||
|
||||
// Javascript for the Gravity Modifier Implementation to test
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
|
||||
function menuParameters(menuNameSelection, menuItemNameSelection) {
|
||||
Menu.addMenuItem({
|
||||
menuName: menuNameSelection,
|
||||
menuItemName: menuItemNameSelection,
|
||||
isCheckable: false
|
||||
});
|
||||
}
|
||||
|
||||
function setupMenu() {
|
||||
if (!Menu.menuExists("Gravity")) {
|
||||
Menu.addMenu("Gravity");
|
||||
for (var i = -5; i <= 5; i++) {
|
||||
menuParameters("Gravity", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function menuItemEvent(menuItem) {
|
||||
for (var i = -5; i <= 5; i++) {
|
||||
if (menuItem == i) {
|
||||
MyAvatar.setGravity(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onScriptEnding() {
|
||||
Menu.removeMenu("Gravity");
|
||||
}
|
||||
|
||||
setupMenu();
|
||||
Menu.menuItemEvent.connect(menuItemEvent);
|
||||
Script.scriptEnding.connect(onScriptEnding);
|
|
@ -131,7 +131,7 @@
|
|||
var button;
|
||||
var buttonName = "WALLET";
|
||||
var tablet = null;
|
||||
var walletEnabled = Settings.getValue("inspectionMode", false);
|
||||
var walletEnabled = Settings.getValue("commerce", false);
|
||||
function startup() {
|
||||
if (walletEnabled) {
|
||||
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
||||
|
|
|
@ -148,7 +148,9 @@ Script.include("/~/system/libraries/controllers.js");
|
|||
if (mode === "full") {
|
||||
var fullEndToEdit = PICK_WITH_HAND_RAY ? this.fullEnd : fullEnd;
|
||||
fullEndToEdit.dimensions = dim;
|
||||
LaserPointers.editRenderState(laserPointerID, mode, {path: fullPath, end: fullEndToEdit});
|
||||
LaserPointers.editRenderState(laserPointerID, mode, { path: fullPath, end: fullEndToEdit });
|
||||
this.contextOverlayTimer = false;
|
||||
this.destroyContextOverlay();
|
||||
} else if (mode === "half") {
|
||||
var halfEndToEdit = PICK_WITH_HAND_RAY ? this.halfEnd : halfEnd;
|
||||
halfEndToEdit.dimensions = dim;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue