Updated ozz-animation to version 0.14.1 @35b2efd4
This commit is contained in:
+2
-2
@@ -1,7 +1,7 @@
|
||||
# Directory src is available from the implementation files.
|
||||
include_directories(./)
|
||||
|
||||
add_subdirectory(animation)
|
||||
add_subdirectory(geometry)
|
||||
add_subdirectory(base)
|
||||
add_subdirectory(options)
|
||||
add_subdirectory(animation)
|
||||
add_subdirectory(geometry)
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
add_subdirectory(offline)
|
||||
add_subdirectory(runtime)
|
||||
add_subdirectory(runtime)
|
||||
add_subdirectory(offline)
|
||||
@@ -1,4 +1,6 @@
|
||||
add_library(ozz_animation_offline STATIC
|
||||
|
||||
add_library(ozz_animation_offline
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/export.h
|
||||
decimate.h
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/raw_animation.h
|
||||
raw_animation.cc
|
||||
@@ -22,8 +24,10 @@ add_library(ozz_animation_offline STATIC
|
||||
track_builder.cc
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/track_optimizer.h
|
||||
track_optimizer.cc)
|
||||
target_link_libraries(ozz_animation_offline
|
||||
ozz_animation)
|
||||
|
||||
target_compile_definitions(ozz_animation_offline PRIVATE $<$<BOOL:${BUILD_SHARED_LIBS}>:OZZ_BUILD_ANIMOFFLINE_LIB>)
|
||||
|
||||
target_link_libraries(ozz_animation_offline ozz_animation)
|
||||
|
||||
set_target_properties(ozz_animation_offline PROPERTIES FOLDER "ozz")
|
||||
|
||||
@@ -31,7 +35,7 @@ install(TARGETS ozz_animation_offline DESTINATION lib)
|
||||
|
||||
fuse_target("ozz_animation_offline")
|
||||
|
||||
add_subdirectory(tools) # tools target need to be defined before other targets that uses them
|
||||
add_subdirectory(fbx)
|
||||
add_subdirectory(gltf)
|
||||
add_subdirectory(tools)
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ bool AdditiveAnimationBuilder::operator()(const RawAnimation& _input,
|
||||
}
|
||||
|
||||
// Rebuilds output animation.
|
||||
_output->name = _input.name;
|
||||
_output->duration = _input.duration;
|
||||
_output->tracks.resize(_input.tracks.size());
|
||||
|
||||
@@ -143,6 +144,7 @@ bool AdditiveAnimationBuilder::operator()(
|
||||
}
|
||||
|
||||
// Rebuilds output animation.
|
||||
_output->name = _input.name;
|
||||
_output->duration = _input.duration;
|
||||
_output->tracks.resize(_input.tracks.size());
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ void CompressQuat(const ozz::math::Quaternion& _src,
|
||||
ozz::animation::QuaternionKey* _dest) {
|
||||
// Finds the largest quaternion component.
|
||||
const float quat[4] = {_src.x, _src.y, _src.z, _src.w};
|
||||
const size_t largest = std::max_element(quat, quat + 4, LessAbs) - quat;
|
||||
const ptrdiff_t largest = std::max_element(quat, quat + 4, LessAbs) - quat;
|
||||
assert(largest <= 3);
|
||||
_dest->largest = largest & 0x3;
|
||||
|
||||
@@ -269,7 +269,7 @@ unique_ptr<Animation> AnimationBuilder::operator()(
|
||||
|
||||
// Declares and preallocates tracks to sort.
|
||||
size_t translations = 0, rotations = 0, scales = 0;
|
||||
for (int i = 0; i < num_tracks; ++i) {
|
||||
for (uint16_t i = 0; i < num_tracks; ++i) {
|
||||
const RawAnimation::JointTrack& raw_track = _input.tracks[i];
|
||||
translations += raw_track.translations.size() + 2; // +2 because worst case
|
||||
rotations += raw_track.rotations.size() + 2; // needs to add the
|
||||
|
||||
@@ -2,22 +2,20 @@ if(NOT ozz_build_fbx)
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_library(ozz_animation_fbx STATIC
|
||||
add_library(ozz_animation_fbx
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/fbx/export.h
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/fbx/fbx.h
|
||||
fbx.cc
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/fbx/fbx_animation.h
|
||||
fbx_animation.cc
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/fbx/fbx_skeleton.h
|
||||
fbx_skeleton.cc)
|
||||
target_include_directories(ozz_animation_fbx
|
||||
PUBLIC $<BUILD_INTERFACE:${FBX_INCLUDE_DIRS}>)
|
||||
|
||||
target_compile_definitions(ozz_animation_fbx PRIVATE $<$<BOOL:${BUILD_SHARED_LIBS}>:OZZ_BUILD_ANIMATIONFBX_LIB>)
|
||||
|
||||
target_link_libraries(ozz_animation_fbx
|
||||
ozz_animation_offline
|
||||
debug ${FBX_LIBRARIES_DEBUG}
|
||||
optimized ${FBX_LIBRARIES})
|
||||
target_compile_options(ozz_animation_fbx
|
||||
PUBLIC $<$<BOOL:${W_NULL_DEREFERENCE}>:-Wno-null-dereference>
|
||||
PUBLIC $<$<BOOL:${W_PRAGMA_PACK}>:-Wno-pragma-pack>)
|
||||
fbx::sdk)
|
||||
set_target_properties(ozz_animation_fbx PROPERTIES FOLDER "ozz/tools")
|
||||
|
||||
install(TARGETS ozz_animation_fbx DESTINATION lib)
|
||||
@@ -32,6 +30,7 @@ add_executable(fbx2ozz
|
||||
target_link_libraries(fbx2ozz
|
||||
ozz_animation_tools
|
||||
ozz_animation_fbx)
|
||||
target_copy_shared_libraries(fbx2ozz)
|
||||
set_target_properties(fbx2ozz
|
||||
PROPERTIES FOLDER "ozz/tools")
|
||||
|
||||
@@ -45,6 +44,7 @@ set(build_configurations
|
||||
"/fbx/pab/locomotions.fbx\;{\"skeleton\":{\"filename\":\"pab_skeleton.ozz\",\"import\":{\"enable\":false}},\"animations\":[{\"filename\":\"pab_*.ozz\"}]}\;output:pab_walk.ozz\;output:pab_jog.ozz\;output:pab_run.ozz\;depend:pab_skeleton.ozz"
|
||||
"/fbx/pab/crossarms.fbx\;{\"skeleton\":{\"filename\":\"pab_skeleton.ozz\",\"import\":{\"enable\":false}},\"animations\":[{\"filename\":\"pab_crossarms.ozz\"}]}\;output:pab_crossarms.ozz\;depend:pab_skeleton.ozz"
|
||||
"/fbx/pab/crackhead.fbx\;{\"skeleton\":{\"filename\":\"pab_skeleton.ozz\",\"import\":{\"enable\":false}},\"animations\":[{\"filename\":\"pab_crackhead.ozz\"},{\"filename\":\"pab_crackhead_additive.ozz\",\"additive\":true}]}\;output:pab_crackhead_additive.ozz\;output:pab_crackhead.ozz\;depend:pab_skeleton.ozz"
|
||||
"/fbx/pab/hand.fbx\;{\"skeleton\":{\"filename\":\"pab_skeleton.ozz\",\"import\":{\"enable\":false}},\"animations\":[{\"clip\":\"curl\",\"filename\":\"pab_curl_additive.ozz\",\"additive\":true,\"additive_reference\":\"skeleton\"},{\"clip\":\"splay\",\"filename\":\"pab_splay_additive.ozz\",\"additive\":true,\"additive_reference\":\"skeleton\"}]}\;output:pab_curl_additive.ozz\;output:pab_splay_additive.ozz\;depend:pab_skeleton.ozz"
|
||||
|
||||
# Robot user channels
|
||||
"/fbx/robot.fbx\;\;config_file:${PROJECT_SOURCE_DIR}/samples/user_channel/config.json\;output:robot_skeleton.ozz\;output:robot_animation.ozz\;output:robot_track_grasp.ozz"
|
||||
|
||||
@@ -28,15 +28,12 @@
|
||||
#include "ozz/animation/offline/fbx/fbx_animation.h"
|
||||
|
||||
#include "ozz/animation/offline/fbx/fbx.h"
|
||||
|
||||
#include "ozz/animation/offline/raw_animation.h"
|
||||
#include "ozz/animation/offline/raw_animation_utils.h"
|
||||
#include "ozz/animation/offline/raw_track.h"
|
||||
#include "ozz/animation/runtime/skeleton.h"
|
||||
#include "ozz/animation/runtime/skeleton_utils.h"
|
||||
|
||||
#include "ozz/base/log.h"
|
||||
|
||||
#include "ozz/base/maths/math_ex.h"
|
||||
#include "ozz/base/maths/transform.h"
|
||||
|
||||
@@ -157,16 +154,16 @@ bool ExtractAnimation(FbxSceneLoader& _scene_loader, const SamplingInfo& _info,
|
||||
if (node == nullptr) {
|
||||
ozz::log::LogV() << "No animation track found for joint \""
|
||||
<< _skeleton.joint_names()[i]
|
||||
<< "\". Using skeleton bind pose instead." << std::endl;
|
||||
<< "\". Using skeleton rest pose instead." << std::endl;
|
||||
|
||||
const math::Transform& bind_pose =
|
||||
ozz::animation::GetJointLocalBindPose(_skeleton, i);
|
||||
const math::Transform& rest_pose =
|
||||
ozz::animation::GetJointLocalRestPose(_skeleton, i);
|
||||
const math::SimdFloat4 t =
|
||||
math::simd_float4::Load3PtrU(&bind_pose.translation.x);
|
||||
math::simd_float4::Load3PtrU(&rest_pose.translation.x);
|
||||
const math::SimdFloat4 q =
|
||||
math::simd_float4::LoadPtrU(&bind_pose.rotation.x);
|
||||
math::simd_float4::LoadPtrU(&rest_pose.rotation.x);
|
||||
const math::SimdFloat4 s =
|
||||
math::simd_float4::Load3PtrU(&bind_pose.scale.x);
|
||||
math::simd_float4::Load3PtrU(&rest_pose.scale.x);
|
||||
const math::Float4x4 local_matrix = math::Float4x4::FromAffine(t, q, s);
|
||||
|
||||
ozz::vector<math::Float4x4>& node_matrices = world_matrices[i];
|
||||
@@ -201,7 +198,7 @@ bool ExtractAnimation(FbxSceneLoader& _scene_loader, const SamplingInfo& _info,
|
||||
|
||||
// Builds local space animation tracks.
|
||||
// Allocates all tracks with the same number of joints as the skeleton.
|
||||
// Tracks that would not be found will be set to skeleton bind-pose
|
||||
// Tracks that would not be found will be set to skeleton rest-pose
|
||||
// transformation.
|
||||
_animation->tracks.resize(_skeleton.num_joints());
|
||||
for (int i = 0; i < _skeleton.num_joints(); i++) {
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include "ozz/animation/offline/fbx/fbx_skeleton.h"
|
||||
|
||||
#include "ozz/animation/offline/raw_skeleton.h"
|
||||
|
||||
#include "ozz/base/log.h"
|
||||
|
||||
namespace ozz {
|
||||
@@ -78,9 +77,12 @@ bool IsTypeSelected(const OzzImporter::NodeType& _types,
|
||||
case FbxNodeAttribute::eLight:
|
||||
return _types.light;
|
||||
|
||||
// Null
|
||||
case FbxNodeAttribute::eNull:
|
||||
return _types.null;
|
||||
|
||||
// Others
|
||||
case FbxNodeAttribute::eUnknown:
|
||||
case FbxNodeAttribute::eNull:
|
||||
case FbxNodeAttribute::eCameraSwitcher:
|
||||
case FbxNodeAttribute::eOpticalReference:
|
||||
case FbxNodeAttribute::eOpticalMarker:
|
||||
@@ -116,7 +118,7 @@ RecurseReturn RecurseNode(FbxNode* _node, FbxSystemConverter* _converter,
|
||||
this_joint = &sibling->back(); // Will not be resized inside recursion.
|
||||
this_joint->name = _node->GetName();
|
||||
|
||||
// Extract bind pose.
|
||||
// Extract rest pose.
|
||||
const FbxAMatrix node_global = _node->EvaluateGlobalTransform();
|
||||
const FbxAMatrix node_local = _parent_global_inv * node_global;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ add_executable(gltf2ozz
|
||||
|
||||
target_link_libraries(gltf2ozz PRIVATE
|
||||
ozz_animation_tools)
|
||||
target_copy_shared_libraries(gltf2ozz)
|
||||
set_target_properties(gltf2ozz
|
||||
PROPERTIES FOLDER "ozz/tools")
|
||||
|
||||
|
||||
@@ -9745,7 +9745,7 @@ class binary_writer
|
||||
static CharType to_char_type(std::uint8_t x) noexcept
|
||||
{
|
||||
static_assert(sizeof(std::uint8_t) == sizeof(CharType), "size of CharType must be equal to std::uint8_t");
|
||||
static_assert(std::is_pod<CharType>::value, "CharType must be POD");
|
||||
static_assert(std::is_standard_layout<CharType>::value && std::is_trivial<CharType>::value, "CharType must be POD");
|
||||
CharType result;
|
||||
std::memcpy(&result, &x, sizeof(x));
|
||||
return result;
|
||||
|
||||
+37
-35
@@ -358,7 +358,7 @@ bool SampleChannel(const tinygltf::Model& _model,
|
||||
}
|
||||
|
||||
ozz::animation::offline::RawAnimation::TranslationKey
|
||||
CreateTranslationBindPoseKey(const tinygltf::Node& _node) {
|
||||
CreateTranslationRestPoseKey(const tinygltf::Node& _node) {
|
||||
ozz::animation::offline::RawAnimation::TranslationKey key;
|
||||
key.time = 0.0f;
|
||||
|
||||
@@ -373,7 +373,7 @@ CreateTranslationBindPoseKey(const tinygltf::Node& _node) {
|
||||
return key;
|
||||
}
|
||||
|
||||
ozz::animation::offline::RawAnimation::RotationKey CreateRotationBindPoseKey(
|
||||
ozz::animation::offline::RawAnimation::RotationKey CreateRotationRestPoseKey(
|
||||
const tinygltf::Node& _node) {
|
||||
ozz::animation::offline::RawAnimation::RotationKey key;
|
||||
key.time = 0.0f;
|
||||
@@ -389,7 +389,7 @@ ozz::animation::offline::RawAnimation::RotationKey CreateRotationBindPoseKey(
|
||||
return key;
|
||||
}
|
||||
|
||||
ozz::animation::offline::RawAnimation::ScaleKey CreateScaleBindPoseKey(
|
||||
ozz::animation::offline::RawAnimation::ScaleKey CreateScaleRestPoseKey(
|
||||
const tinygltf::Node& _node) {
|
||||
ozz::animation::offline::RawAnimation::ScaleKey key;
|
||||
key.time = 0.0f;
|
||||
@@ -430,8 +430,8 @@ bool CreateNodeTransform(const tinygltf::Node& _node,
|
||||
ozz::math::SimdFloat4 translation, rotation, scale;
|
||||
if (ToAffine(matrix, &translation, &rotation, &scale)) {
|
||||
ozz::math::Store3PtrU(translation, &_transform->translation.x);
|
||||
ozz::math::StorePtrU(translation, &_transform->rotation.x);
|
||||
ozz::math::Store3PtrU(translation, &_transform->scale.x);
|
||||
ozz::math::StorePtrU(rotation, &_transform->rotation.x);
|
||||
ozz::math::Store3PtrU(scale, &_transform->scale.x);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -521,30 +521,38 @@ class GltfImporter : public ozz::animation::offline::OzzImporter {
|
||||
return success;
|
||||
}
|
||||
|
||||
// Given a skin find which of its joints is the skeleton root and return it
|
||||
// returns -1 if the skin has no associated joints
|
||||
int FindSkinRootJointIndex(const tinygltf::Skin& skin) {
|
||||
if (skin.joints.empty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (skin.skeleton != -1) {
|
||||
return skin.skeleton;
|
||||
}
|
||||
|
||||
ozz::map<int, int> parents;
|
||||
for (int node : skin.joints) {
|
||||
// Find all unique root joints of skeletons used by given skins and add them
|
||||
// to `roots`
|
||||
void FindSkinRootJointIndices(const ozz::vector<tinygltf::Skin>& skins,
|
||||
ozz::vector<int>& roots) {
|
||||
static constexpr int no_parent = -1;
|
||||
static constexpr int visited = -2;
|
||||
ozz::vector<int> parents(m_model.nodes.size(), no_parent);
|
||||
for (int node = 0; node < static_cast<int>(m_model.nodes.size()); node++) {
|
||||
for (int child : m_model.nodes[node].children) {
|
||||
parents[child] = node;
|
||||
}
|
||||
}
|
||||
|
||||
int root = skin.joints[0];
|
||||
while (parents.find(root) != parents.end()) {
|
||||
root = parents[root];
|
||||
}
|
||||
for (const tinygltf::Skin& skin : skins) {
|
||||
if (skin.joints.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return root;
|
||||
if (skin.skeleton != -1) {
|
||||
parents[skin.skeleton] = visited;
|
||||
roots.push_back(skin.skeleton);
|
||||
continue;
|
||||
}
|
||||
|
||||
int root = skin.joints[0];
|
||||
while (root != visited && parents[root] != no_parent) {
|
||||
root = parents[root];
|
||||
}
|
||||
if (root != visited) {
|
||||
roots.push_back(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Import(ozz::animation::offline::RawSkeleton* _skeleton,
|
||||
@@ -591,14 +599,8 @@ class GltfImporter : public ozz::animation::offline::OzzImporter {
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
// Uses all skins root
|
||||
for (auto& skin : skins) {
|
||||
const int root = FindSkinRootJointIndex(skin);
|
||||
if (root == -1) {
|
||||
continue;
|
||||
}
|
||||
roots.push_back(root);
|
||||
}
|
||||
// Uses all skins roots.
|
||||
FindSkinRootJointIndices(skins, roots);
|
||||
}
|
||||
|
||||
// Remove nodes listed multiple times.
|
||||
@@ -745,16 +747,16 @@ class GltfImporter : public ozz::animation::offline::OzzImporter {
|
||||
const tinygltf::Node* node = FindNodeByName(joint_names[i]);
|
||||
assert(node != nullptr);
|
||||
|
||||
// Pads the bind pose transform for any joints which do not have an
|
||||
// Pads the rest pose transform for any joints which do not have an
|
||||
// associated channel for this animation
|
||||
if (track.translations.empty()) {
|
||||
track.translations.push_back(CreateTranslationBindPoseKey(*node));
|
||||
track.translations.push_back(CreateTranslationRestPoseKey(*node));
|
||||
}
|
||||
if (track.rotations.empty()) {
|
||||
track.rotations.push_back(CreateRotationBindPoseKey(*node));
|
||||
track.rotations.push_back(CreateRotationRestPoseKey(*node));
|
||||
}
|
||||
if (track.scales.empty()) {
|
||||
track.scales.push_back(CreateScaleBindPoseKey(*node));
|
||||
track.scales.push_back(CreateScaleRestPoseKey(*node));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,9 +144,9 @@ unique_ptr<ozz::animation::Skeleton> SkeletonBuilder::operator()(
|
||||
}
|
||||
// Fills the SoaTransform structure.
|
||||
math::Transpose4x3(translations,
|
||||
&skeleton->joint_bind_poses_[i].translation.x);
|
||||
math::Transpose4x4(rotations, &skeleton->joint_bind_poses_[i].rotation.x);
|
||||
math::Transpose4x3(scales, &skeleton->joint_bind_poses_[i].scale.x);
|
||||
&skeleton->joint_rest_poses_[i].translation.x);
|
||||
math::Transpose4x4(rotations, &skeleton->joint_rest_poses_[i].rotation.x);
|
||||
math::Transpose4x3(scales, &skeleton->joint_rest_poses_[i].scale.x);
|
||||
}
|
||||
|
||||
return skeleton; // Success.
|
||||
|
||||
@@ -4,7 +4,8 @@ endif()
|
||||
|
||||
add_subdirectory(${PROJECT_SOURCE_DIR}/extern/jsoncpp json)
|
||||
|
||||
add_library(ozz_animation_tools STATIC
|
||||
add_library(ozz_animation_tools
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/tools/export.h
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/tools/import2ozz.h
|
||||
import2ozz.cc
|
||||
import2ozz_anim.h
|
||||
@@ -15,10 +16,14 @@ add_library(ozz_animation_tools STATIC
|
||||
import2ozz_skel.cc
|
||||
import2ozz_track.h
|
||||
import2ozz_track.cc)
|
||||
|
||||
target_compile_definitions(ozz_animation_tools PRIVATE $<$<BOOL:${BUILD_SHARED_LIBS}>:OZZ_BUILD_ANIMATIONTOOLS_LIB>)
|
||||
|
||||
target_link_libraries(ozz_animation_tools
|
||||
ozz_animation_offline
|
||||
ozz_options
|
||||
json)
|
||||
|
||||
set_target_properties(ozz_animation_tools
|
||||
PROPERTIES FOLDER "ozz/tools")
|
||||
|
||||
@@ -32,6 +37,7 @@ if(NOT EMSCRIPTEN)
|
||||
target_link_libraries(dump2ozz
|
||||
ozz_animation_tools
|
||||
ozz_options)
|
||||
target_copy_shared_libraries(dump2ozz)
|
||||
|
||||
# Uses a fake import file to dump reference and default configurations.
|
||||
add_custom_command(TARGET dump2ozz POST_BUILD
|
||||
@@ -39,4 +45,5 @@ if(NOT EMSCRIPTEN)
|
||||
|
||||
set_target_properties(dump2ozz
|
||||
PROPERTIES FOLDER "ozz/tools")
|
||||
|
||||
endif()
|
||||
@@ -137,11 +137,11 @@ unique_ptr<ozz::animation::Skeleton> LoadSkeleton(const char* _path) {
|
||||
return skeleton;
|
||||
}
|
||||
|
||||
vector<math::Transform> SkeletonBindPoseSoAToAoS(const Skeleton& _skeleton) {
|
||||
// Copy skeleton bind pose to AoS form.
|
||||
vector<math::Transform> SkeletonRestPoseSoAToAoS(const Skeleton& _skeleton) {
|
||||
// Copy skeleton rest pose to AoS form.
|
||||
vector<math::Transform> transforms(_skeleton.num_joints());
|
||||
for (int i = 0; i < _skeleton.num_soa_joints(); ++i) {
|
||||
const math::SoaTransform& soa_transform = _skeleton.joint_bind_poses()[i];
|
||||
const math::SoaTransform& soa_transform = _skeleton.joint_rest_poses()[i];
|
||||
math::SimdFloat4 translation[4];
|
||||
math::SimdFloat4 rotation[4];
|
||||
math::SimdFloat4 scale[4];
|
||||
@@ -248,7 +248,7 @@ bool Export(OzzImporter& _importer, const RawAnimation& _input_animation,
|
||||
bool succeeded = false;
|
||||
if (enum_found && reference == AdditiveReferenceEnum::kSkeleton) {
|
||||
const vector<math::Transform> transforms =
|
||||
SkeletonBindPoseSoAToAoS(_skeleton);
|
||||
SkeletonRestPoseSoAToAoS(_skeleton);
|
||||
succeeded =
|
||||
additive_builder(raw_animation, make_span(transforms), &raw_additive);
|
||||
} else {
|
||||
@@ -371,9 +371,12 @@ bool ImportAnimations(const Json::Value& _config, OzzImporter* _importer,
|
||||
LoadSkeleton(skeleton_config["filename"].asCString()));
|
||||
success &= skeleton.get() != nullptr;
|
||||
|
||||
if (!success)
|
||||
return false;
|
||||
|
||||
// Loop though all existing animations, and export those who match
|
||||
// configuration.
|
||||
for (Json::ArrayIndex i = 0; success && i < animations_config.size(); ++i) {
|
||||
for (Json::ArrayIndex i = 0; i < animations_config.size(); ++i) {
|
||||
const Json::Value& animation_config = animations_config[i];
|
||||
const char* clip_match = animation_config["clip"].asCString();
|
||||
|
||||
@@ -384,28 +387,45 @@ bool ImportAnimations(const Json::Value& _config, OzzImporter* _importer,
|
||||
continue;
|
||||
}
|
||||
|
||||
bool matched = false;
|
||||
for (size_t j = 0; success && j < import_animation_names.size(); ++j) {
|
||||
size_t num_not_clip_animation = 0, num_valid_animation = 0;
|
||||
for (size_t j = 0; j < import_animation_names.size(); ++j) {
|
||||
const char* animation_name = import_animation_names[j].c_str();
|
||||
if (!strmatch(animation_name, clip_match)) {
|
||||
continue;
|
||||
}
|
||||
++num_not_clip_animation;
|
||||
const bool anisuccess = ProcessAnimation(*_importer, animation_name, *skeleton,
|
||||
animation_config, _endianness);
|
||||
if(anisuccess){
|
||||
++num_valid_animation;
|
||||
}
|
||||
|
||||
matched = true;
|
||||
success = ProcessAnimation(*_importer, animation_name, *skeleton,
|
||||
animation_config, _endianness);
|
||||
|
||||
size_t num_valid_track = 0;
|
||||
const Json::Value& tracks_config = animation_config["tracks"];
|
||||
for (Json::ArrayIndex t = 0; success && t < tracks_config.size(); ++t) {
|
||||
success = ProcessTracks(*_importer, animation_name, *skeleton,
|
||||
tracks_config[t], _endianness);
|
||||
for (Json::ArrayIndex t = 0; t < tracks_config.size(); ++t) {
|
||||
if (ProcessTracks(*_importer, animation_name, *skeleton,
|
||||
tracks_config[t], _endianness)){
|
||||
++num_valid_track;
|
||||
}
|
||||
}
|
||||
|
||||
if (num_valid_track != tracks_config.size()){
|
||||
ozz::log::Log() << "One of track failed when import: \"" << animation_name
|
||||
<< "\"" << std::endl;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
// Don't display any message if no animation is supposed to be imported.
|
||||
if (!matched && *clip_match != 0) {
|
||||
if (0 == num_not_clip_animation && *clip_match != 0) {
|
||||
ozz::log::Log() << "No matching animation found for \"" << clip_match
|
||||
<< "\"." << std::endl;
|
||||
}
|
||||
|
||||
if (num_valid_animation != num_not_clip_animation){
|
||||
ozz::log::Log() << "One of animation failed when import, animation index: \"" << i
|
||||
<< "\"" << std::endl;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#ifndef OZZ_ANIMATION_OFFLINE_TOOLS_IMPORT2OZZ_ANIM_H_
|
||||
#define OZZ_ANIMATION_OFFLINE_TOOLS_IMPORT2OZZ_ANIM_H_
|
||||
|
||||
#include "ozz/animation/offline/tools/export.h"
|
||||
#include "ozz/base/endianness.h"
|
||||
#include "ozz/base/platform.h"
|
||||
|
||||
@@ -43,14 +44,15 @@ namespace animation {
|
||||
namespace offline {
|
||||
|
||||
class OzzImporter;
|
||||
bool ImportAnimations(const Json::Value& _config, OzzImporter* _importer,
|
||||
OZZ_ANIMTOOLS_DLL bool ImportAnimations(const Json::Value& _config,
|
||||
OzzImporter* _importer,
|
||||
const ozz::Endianness _endianness);
|
||||
|
||||
// Additive reference enum to config string conversions.
|
||||
struct AdditiveReferenceEnum {
|
||||
enum Value { kAnimation, kSkeleton };
|
||||
};
|
||||
struct AdditiveReference
|
||||
struct OZZ_ANIMTOOLS_DLL AdditiveReference
|
||||
: JsonEnum<AdditiveReference, AdditiveReferenceEnum::Value> {
|
||||
static EnumNames GetNames();
|
||||
};
|
||||
|
||||
@@ -27,24 +27,21 @@
|
||||
|
||||
#include "animation/offline/tools/import2ozz_config.h"
|
||||
|
||||
#include <json/json.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "animation/offline/tools/import2ozz_anim.h"
|
||||
#include "animation/offline/tools/import2ozz_track.h"
|
||||
#include "ozz/animation/offline/tools/import2ozz.h"
|
||||
|
||||
#include "ozz/animation/offline/animation_optimizer.h"
|
||||
#include "ozz/animation/offline/tools/import2ozz.h"
|
||||
#include "ozz/animation/offline/track_optimizer.h"
|
||||
|
||||
#include "ozz/base/containers/string.h"
|
||||
#include "ozz/base/log.h"
|
||||
|
||||
#include "ozz/options/options.h"
|
||||
|
||||
#include <json/json.h>
|
||||
|
||||
bool ValidateExclusiveConfigOption(const ozz::options::Option& _option,
|
||||
int _argc);
|
||||
OZZ_OPTIONS_DECLARE_STRING_FN(
|
||||
@@ -365,14 +362,14 @@ bool SanitizeAnimation(Json::Value& _root, bool _all_options) {
|
||||
MakeDefault(_root, "additive_reference", "animation",
|
||||
"Select reference pose to use to build additive/delta animation. "
|
||||
"Can be \"animation\" to use the 1st animation keyframe as "
|
||||
"reference, or \"skeleton\" to use skeleton bind pose.");
|
||||
"reference, or \"skeleton\" to use skeleton rest pose.");
|
||||
|
||||
if (!AdditiveReference::IsValidEnumName(
|
||||
_root["additive_reference"].asCString())) {
|
||||
ozz::log::Err() << "Invalid additive reference pose \""
|
||||
<< _root["additive_reference"].asCString() << "\". \""
|
||||
<< "Can be \"animation\" to use the 1st animation keyframe "
|
||||
"as reference, or \"skeleton\" to use skeleton bind "
|
||||
"as reference, or \"skeleton\" to use skeleton rest "
|
||||
"pose."
|
||||
<< std::endl;
|
||||
return false;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#ifndef OZZ_ANIMATION_OFFLINE_TOOLS_IMPORT2OZZ_CONFIG_H_
|
||||
#define OZZ_ANIMATION_OFFLINE_TOOLS_IMPORT2OZZ_CONFIG_H_
|
||||
|
||||
#include "ozz/animation/offline/tools/export.h"
|
||||
#include "ozz/base/platform.h"
|
||||
|
||||
#include <json/json-forwards.h>
|
||||
@@ -37,10 +38,10 @@ namespace animation {
|
||||
namespace offline {
|
||||
|
||||
// Get the sanitized (all members are set, with the right types) configuration.
|
||||
bool ProcessConfiguration(Json::Value* _config);
|
||||
OZZ_ANIMTOOLS_DLL bool ProcessConfiguration(Json::Value* _config);
|
||||
|
||||
// Internal function used to compare enum names.
|
||||
bool CompareName(const char* _a, const char* _b);
|
||||
OZZ_ANIMTOOLS_DLL bool CompareName(const char* _a, const char* _b);
|
||||
|
||||
// Struct allowing inheriting class to provide enum names.
|
||||
template <typename _Type, typename _Enum>
|
||||
|
||||
@@ -122,12 +122,13 @@ bool ImportSkeleton(const Json::Value& _config, OzzImporter* _importer,
|
||||
|
||||
// Setup node types import properties.
|
||||
const Json::Value& types_config = import_config["types"];
|
||||
OzzImporter::NodeType types = {0};
|
||||
OzzImporter::NodeType types = {};
|
||||
types.skeleton = types_config["skeleton"].asBool();
|
||||
types.marker = types_config["marker"].asBool();
|
||||
types.camera = types_config["camera"].asBool();
|
||||
types.geometry = types_config["geometry"].asBool();
|
||||
types.light = types_config["light"].asBool();
|
||||
types.null = types_config["null"].asBool();
|
||||
types.any = types_config["any"].asBool();
|
||||
|
||||
RawSkeleton raw_skeleton;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#ifndef OZZ_ANIMATION_OFFLINE_TOOLS_IMPORT2OZZ_SKEL_H_
|
||||
#define OZZ_ANIMATION_OFFLINE_TOOLS_IMPORT2OZZ_SKEL_H_
|
||||
|
||||
#include "ozz/animation/offline/tools/export.h"
|
||||
#include "ozz/base/endianness.h"
|
||||
#include "ozz/base/platform.h"
|
||||
|
||||
@@ -41,8 +42,9 @@ namespace offline {
|
||||
|
||||
class OzzImporter;
|
||||
|
||||
bool ImportSkeleton(const Json::Value& _config, OzzImporter* _importer,
|
||||
const ozz::Endianness _endianness);
|
||||
OZZ_ANIMTOOLS_DLL bool ImportSkeleton(const Json::Value& _config,
|
||||
OzzImporter* _importer,
|
||||
const ozz::Endianness _endianness);
|
||||
|
||||
} // namespace offline
|
||||
} // namespace animation
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
#ifndef OZZ_ANIMATION_OFFLINE_TOOLS_IMPORT2OZZ_TRACK_H_
|
||||
#define OZZ_ANIMATION_OFFLINE_TOOLS_IMPORT2OZZ_TRACK_H_
|
||||
|
||||
#include "animation/offline/tools/import2ozz_config.h"
|
||||
#include "ozz/animation/offline/tools/export.h"
|
||||
#include "ozz/animation/offline/tools/import2ozz.h"
|
||||
#include "ozz/base/endianness.h"
|
||||
#include "ozz/base/platform.h"
|
||||
|
||||
#include "animation/offline/tools/import2ozz_config.h"
|
||||
#include "ozz/animation/offline/tools/import2ozz.h"
|
||||
|
||||
namespace Json {
|
||||
class Value;
|
||||
}
|
||||
@@ -44,12 +44,14 @@ class Skeleton;
|
||||
namespace offline {
|
||||
|
||||
class OzzImporter;
|
||||
bool ProcessTracks(OzzImporter& _importer, const char* _animation_name,
|
||||
const Skeleton& _skeleton, const Json::Value& _config,
|
||||
const ozz::Endianness _endianness);
|
||||
OZZ_ANIMTOOLS_DLL bool ProcessTracks(OzzImporter& _importer,
|
||||
const char* _animation_name,
|
||||
const Skeleton& _skeleton,
|
||||
const Json::Value& _config,
|
||||
const ozz::Endianness _endianness);
|
||||
|
||||
// Property type enum to config string conversions.
|
||||
struct PropertyTypeConfig
|
||||
struct OZZ_ANIMTOOLS_DLL PropertyTypeConfig
|
||||
: JsonEnum<PropertyTypeConfig, OzzImporter::NodeProperty::Type> {
|
||||
static EnumNames GetNames();
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"filename" : "*.ozz", // Specifies animation output filename. Use a '*' character to specify part(s) of the filename that should be replaced by the clip name.
|
||||
"raw" : false, // Outputs raw animation.
|
||||
"additive" : false, // Creates a delta animation that can be used for additive blending.
|
||||
"additive_reference" : "animation", // Select reference pose to use to build additive/delta animation. Can be "animation" to use the 1st animation keyframe as reference, or "skeleton" to use skeleton bind pose.
|
||||
"additive_reference" : "animation", // Select reference pose to use to build additive/delta animation. Can be "animation" to use the 1st animation keyframe as reference, or "skeleton" to use skeleton rest pose.
|
||||
"sampling_rate" : 0, // Selects animation sampling rate in hertz. Set a value <= 0 to use imported scene default frame rate.
|
||||
"optimize" : true, // Activates keyframes reduction optimization.
|
||||
"optimization_settings" :
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
add_library(ozz_animation STATIC
|
||||
|
||||
add_library(ozz_animation
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/runtime/export.h
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/runtime/animation.h
|
||||
animation.cc
|
||||
animation_keyframe.h
|
||||
@@ -25,7 +27,11 @@ add_library(ozz_animation STATIC
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/runtime/track_triggering_job.h
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/animation/runtime/track_triggering_job_trait.h
|
||||
track_triggering_job.cc)
|
||||
|
||||
target_compile_definitions(ozz_animation PRIVATE $<$<BOOL:${BUILD_SHARED_LIBS}>:OZZ_BUILD_ANIMATION_LIB>)
|
||||
|
||||
target_link_libraries(ozz_animation
|
||||
PUBLIC
|
||||
ozz_base)
|
||||
|
||||
set_target_properties(ozz_animation
|
||||
|
||||
+14
-1
@@ -46,6 +46,19 @@ namespace animation {
|
||||
|
||||
Animation::Animation() : duration_(0.f), num_tracks_(0), name_(nullptr) {}
|
||||
|
||||
Animation::Animation(Animation&& _other) { *this = std::move(_other); }
|
||||
|
||||
Animation& Animation::operator=(Animation&& _other) {
|
||||
std::swap(duration_, _other.duration_);
|
||||
std::swap(num_tracks_, _other.num_tracks_);
|
||||
std::swap(name_, _other.name_);
|
||||
std::swap(translations_, _other.translations_);
|
||||
std::swap(rotations_, _other.rotations_);
|
||||
std::swap(scales_, _other.scales_);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
Animation::~Animation() { Deallocate(); }
|
||||
|
||||
void Animation::Allocate(size_t _name_len, size_t _translation_count,
|
||||
@@ -65,7 +78,7 @@ void Animation::Allocate(size_t _name_len, size_t _translation_count,
|
||||
_translation_count * sizeof(Float3Key) +
|
||||
_rotation_count * sizeof(QuaternionKey) +
|
||||
_scale_count * sizeof(Float3Key);
|
||||
span<char> buffer = {static_cast<char*>(memory::default_allocator()->Allocate(
|
||||
span<byte> buffer = {static_cast<byte*>(memory::default_allocator()->Allocate(
|
||||
buffer_size, alignof(Float3Key))),
|
||||
buffer_size};
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
#ifndef OZZ_ANIMATION_RUNTIME_ANIMATION_KEYFRAME_H_
|
||||
#define OZZ_ANIMATION_RUNTIME_ANIMATION_KEYFRAME_H_
|
||||
|
||||
#include "ozz/animation/runtime/export.h"
|
||||
#include "ozz/base/platform.h"
|
||||
|
||||
#ifndef OZZ_INCLUDE_PRIVATE_HEADER
|
||||
#error "This header is private, it cannot be included from public headers."
|
||||
#endif // OZZ_INCLUDE_PRIVATE_HEADER
|
||||
@@ -46,7 +48,7 @@ namespace animation {
|
||||
// Defines the float3 key frame type, used for translations and scales.
|
||||
// Translation values are stored as half precision floats with 16 bits per
|
||||
// component.
|
||||
struct Float3Key {
|
||||
struct OZZ_ANIMATION_DLL Float3Key {
|
||||
float ratio;
|
||||
uint16_t track;
|
||||
uint16_t value[3];
|
||||
@@ -67,7 +69,7 @@ struct Float3Key {
|
||||
// Quantization could be reduced to 11-11-10 bits as often used for animation
|
||||
// key frames, but in this case RotationKey structure would induce 16 bits of
|
||||
// padding.
|
||||
struct QuaternionKey {
|
||||
struct OZZ_ANIMATION_DLL QuaternionKey {
|
||||
float ratio;
|
||||
uint16_t track : 13; // The track this key frame belongs to.
|
||||
uint16_t largest : 2; // The largest component of the quaternion.
|
||||
|
||||
+15
-15
@@ -69,12 +69,12 @@ bool BlendingJob::Validate() const {
|
||||
|
||||
// Test for nullptr begin pointers.
|
||||
// Blending layers are mandatory, additive aren't.
|
||||
valid &= !bind_pose.empty();
|
||||
valid &= !rest_pose.empty();
|
||||
valid &= !output.empty();
|
||||
|
||||
// The bind pose size defines the ranges of transforms to blend, so all
|
||||
// The rest pose size defines the ranges of transforms to blend, so all
|
||||
// other buffers should be bigger.
|
||||
const size_t min_range = bind_pose.size();
|
||||
const size_t min_range = rest_pose.size();
|
||||
valid &= output.size() >= min_range;
|
||||
|
||||
// Validates layers.
|
||||
@@ -160,7 +160,7 @@ namespace {
|
||||
struct ProcessArgs {
|
||||
ProcessArgs(const BlendingJob& _job)
|
||||
: job(_job),
|
||||
num_soa_joints(_job.bind_pose.size()),
|
||||
num_soa_joints(_job.rest_pose.size()),
|
||||
num_passes(0),
|
||||
num_partial_passes(0),
|
||||
accumulated_weight(0.f) {
|
||||
@@ -182,7 +182,7 @@ struct ProcessArgs {
|
||||
// The job to process.
|
||||
const BlendingJob& job;
|
||||
|
||||
// The number of transforms to process as defined by the size of the bind
|
||||
// The number of transforms to process as defined by the size of the rest
|
||||
// pose.
|
||||
size_t num_soa_joints;
|
||||
|
||||
@@ -271,24 +271,24 @@ void BlendLayers(ProcessArgs* _args) {
|
||||
}
|
||||
}
|
||||
|
||||
// Blends bind pose to the output if accumulated weight is less than the
|
||||
// Blends rest pose to the output if accumulated weight is less than the
|
||||
// threshold value.
|
||||
void BlendBindPose(ProcessArgs* _args) {
|
||||
void BlendRestPose(ProcessArgs* _args) {
|
||||
assert(_args);
|
||||
|
||||
// Asserts buffer sizes, which must never fail as it has been validated.
|
||||
assert(_args->job.bind_pose.size() >= _args->num_soa_joints);
|
||||
assert(_args->job.rest_pose.size() >= _args->num_soa_joints);
|
||||
|
||||
if (_args->num_partial_passes == 0) {
|
||||
// No partial blending pass detected, threshold can be tested globally.
|
||||
const float bp_weight = _args->job.threshold - _args->accumulated_weight;
|
||||
|
||||
if (bp_weight > 0.f) { // The bind-pose is needed if it has a weight.
|
||||
if (bp_weight > 0.f) { // The rest-pose is needed if it has a weight.
|
||||
if (_args->num_passes == 0) {
|
||||
// Strictly copying bind-pose.
|
||||
// Strictly copying rest-pose.
|
||||
_args->accumulated_weight = 1.f;
|
||||
for (size_t i = 0; i < _args->num_soa_joints; ++i) {
|
||||
_args->job.output[i] = _args->job.bind_pose[i];
|
||||
_args->job.output[i] = _args->job.rest_pose[i];
|
||||
}
|
||||
} else {
|
||||
// Updates global accumulated weight, but not per-joint weight any more
|
||||
@@ -299,7 +299,7 @@ void BlendBindPose(ProcessArgs* _args) {
|
||||
math::simd_float4::Load1(bp_weight);
|
||||
|
||||
for (size_t i = 0; i < _args->num_soa_joints; ++i) {
|
||||
const math::SoaTransform& src = _args->job.bind_pose[i];
|
||||
const math::SoaTransform& src = _args->job.rest_pose[i];
|
||||
math::SoaTransform* dest = _args->job.output.begin() + i;
|
||||
OZZ_BLEND_N_PASS(src, simd_bp_weight, dest);
|
||||
}
|
||||
@@ -315,7 +315,7 @@ void BlendBindPose(ProcessArgs* _args) {
|
||||
assert(_args->num_passes != 0);
|
||||
|
||||
for (size_t i = 0; i < _args->num_soa_joints; ++i) {
|
||||
const math::SoaTransform& src = _args->job.bind_pose[i];
|
||||
const math::SoaTransform& src = _args->job.rest_pose[i];
|
||||
math::SoaTransform* dest = _args->job.output.begin() + i;
|
||||
const math::SimdFloat4 bp_weight =
|
||||
math::Max0(threshold - _args->accumulated_weights[i]);
|
||||
@@ -442,8 +442,8 @@ bool BlendingJob::Run() const {
|
||||
// Blends all layers to the job output buffers.
|
||||
BlendLayers(&process_args);
|
||||
|
||||
// Applies bind pose.
|
||||
BlendBindPose(&process_args);
|
||||
// Applies rest pose.
|
||||
BlendRestPose(&process_args);
|
||||
|
||||
// Normalizes output.
|
||||
Normalize(&process_args);
|
||||
|
||||
+40
-36
@@ -60,22 +60,21 @@ bool SamplingJob::Validate() const {
|
||||
bool valid = true;
|
||||
|
||||
// Test for nullptr pointers.
|
||||
if (!animation || !cache) {
|
||||
if (!animation || !context) {
|
||||
return false;
|
||||
}
|
||||
valid &= !output.empty();
|
||||
|
||||
const int num_soa_tracks = animation->num_soa_tracks();
|
||||
valid &= output.size() >= static_cast<size_t>(num_soa_tracks);
|
||||
|
||||
// Tests cache size.
|
||||
valid &= cache->max_soa_tracks() >= num_soa_tracks;
|
||||
// Tests context size.
|
||||
valid &= context->max_soa_tracks() >= num_soa_tracks;
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Loops through the sorted key frames and update cache structure.
|
||||
// Loops through the sorted key frames and update context structure.
|
||||
template <typename _Key>
|
||||
void UpdateCacheCursor(float _ratio, int _num_soa_tracks,
|
||||
const ozz::span<const _Key>& _keys, int* _cursor,
|
||||
@@ -118,16 +117,16 @@ void UpdateCacheCursor(float _ratio, int _num_soa_tracks,
|
||||
}
|
||||
|
||||
// Search for the keys that matches _ratio.
|
||||
// Iterates while the cache is not updated with left and right keys required
|
||||
// Iterates while the context is not updated with left and right keys required
|
||||
// for interpolation at time ratio _ratio, for all tracks. Thanks to the
|
||||
// keyframe sorting, the loop can end as soon as it finds a key greater that
|
||||
// _ratio. It will mean that all the keys lower than _ratio have been
|
||||
// processed, meaning all cache entries are up to date.
|
||||
// processed, meaning all context entries are up to date.
|
||||
while (cursor < _keys.end() &&
|
||||
_keys[_cache[cursor->track * 2 + 1]].ratio <= _ratio) {
|
||||
// Flag this soa entry as outdated.
|
||||
_outdated[cursor->track / 32] |= (1 << ((cursor->track & 0x1f) / 4));
|
||||
// Updates cache.
|
||||
// Updates context.
|
||||
const int base = cursor->track * 2;
|
||||
_cache[base] = _cache[base + 1];
|
||||
_cache[base + 1] = static_cast<int>(cursor - _keys.begin());
|
||||
@@ -293,7 +292,7 @@ void Interpolates(float _anim_ratio, int _num_soa_tracks,
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SamplingJob::SamplingJob() : ratio(0.f), animation(nullptr), cache(nullptr) {}
|
||||
SamplingJob::SamplingJob() : ratio(0.f), animation(nullptr), context(nullptr) {}
|
||||
|
||||
bool SamplingJob::Run() const {
|
||||
if (!Validate()) {
|
||||
@@ -308,60 +307,64 @@ bool SamplingJob::Run() const {
|
||||
// Clamps ratio in range [0,duration].
|
||||
const float anim_ratio = math::Clamp(0.f, ratio, 1.f);
|
||||
|
||||
// Step the cache to this potentially new animation and ratio.
|
||||
assert(cache->max_soa_tracks() >= num_soa_tracks);
|
||||
cache->Step(*animation, anim_ratio);
|
||||
// Step the context to this potentially new animation and ratio.
|
||||
assert(context->max_soa_tracks() >= num_soa_tracks);
|
||||
context->Step(*animation, anim_ratio);
|
||||
|
||||
// Fetch key frames from the animation to the cache a r = anim_ratio.
|
||||
// Fetch key frames from the animation to the context at r = anim_ratio.
|
||||
// Then updates outdated soa hot values.
|
||||
UpdateCacheCursor(anim_ratio, num_soa_tracks, animation->translations(),
|
||||
&cache->translation_cursor_, cache->translation_keys_,
|
||||
cache->outdated_translations_);
|
||||
&context->translation_cursor_, context->translation_keys_,
|
||||
context->outdated_translations_);
|
||||
UpdateInterpKeyframes(num_soa_tracks, animation->translations(),
|
||||
cache->translation_keys_, cache->outdated_translations_,
|
||||
cache->soa_translations_, &DecompressFloat3);
|
||||
context->translation_keys_,
|
||||
context->outdated_translations_,
|
||||
context->soa_translations_, &DecompressFloat3);
|
||||
|
||||
UpdateCacheCursor(anim_ratio, num_soa_tracks, animation->rotations(),
|
||||
&cache->rotation_cursor_, cache->rotation_keys_,
|
||||
cache->outdated_rotations_);
|
||||
&context->rotation_cursor_, context->rotation_keys_,
|
||||
context->outdated_rotations_);
|
||||
UpdateInterpKeyframes(num_soa_tracks, animation->rotations(),
|
||||
cache->rotation_keys_, cache->outdated_rotations_,
|
||||
cache->soa_rotations_, &DecompressQuaternion);
|
||||
context->rotation_keys_, context->outdated_rotations_,
|
||||
context->soa_rotations_, &DecompressQuaternion);
|
||||
|
||||
UpdateCacheCursor(anim_ratio, num_soa_tracks, animation->scales(),
|
||||
&cache->scale_cursor_, cache->scale_keys_,
|
||||
cache->outdated_scales_);
|
||||
UpdateInterpKeyframes(num_soa_tracks, animation->scales(), cache->scale_keys_,
|
||||
cache->outdated_scales_, cache->soa_scales_,
|
||||
&DecompressFloat3);
|
||||
&context->scale_cursor_, context->scale_keys_,
|
||||
context->outdated_scales_);
|
||||
UpdateInterpKeyframes(num_soa_tracks, animation->scales(),
|
||||
context->scale_keys_, context->outdated_scales_,
|
||||
context->soa_scales_, &DecompressFloat3);
|
||||
|
||||
// only interp as much as we have output for.
|
||||
const int num_soa_interp_tracks = math::Min(static_cast< int >(output.size()), num_soa_tracks);
|
||||
|
||||
// Interpolates soa hot data.
|
||||
Interpolates(anim_ratio, num_soa_tracks, cache->soa_translations_,
|
||||
cache->soa_rotations_, cache->soa_scales_, output.begin());
|
||||
Interpolates(anim_ratio, num_soa_interp_tracks, context->soa_translations_,
|
||||
context->soa_rotations_, context->soa_scales_, output.begin());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SamplingCache::SamplingCache()
|
||||
SamplingJob::Context::Context()
|
||||
: max_soa_tracks_(0),
|
||||
soa_translations_(
|
||||
nullptr) { // soa_translations_ is the allocation pointer.
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
SamplingCache::SamplingCache(int _max_tracks)
|
||||
SamplingJob::Context::Context(int _max_tracks)
|
||||
: max_soa_tracks_(0),
|
||||
soa_translations_(
|
||||
nullptr) { // soa_translations_ is the allocation pointer.
|
||||
Resize(_max_tracks);
|
||||
}
|
||||
|
||||
SamplingCache::~SamplingCache() {
|
||||
SamplingJob::Context::~Context() {
|
||||
// Deallocates everything at once.
|
||||
memory::default_allocator()->Deallocate(soa_translations_);
|
||||
}
|
||||
|
||||
void SamplingCache::Resize(int _max_tracks) {
|
||||
void SamplingJob::Context::Resize(int _max_tracks) {
|
||||
using internal::InterpSoaFloat3;
|
||||
using internal::InterpSoaQuaternion;
|
||||
|
||||
@@ -372,7 +375,7 @@ void SamplingCache::Resize(int _max_tracks) {
|
||||
// Updates maximum supported soa tracks.
|
||||
max_soa_tracks_ = (_max_tracks + 3) / 4;
|
||||
|
||||
// Allocate all cache data at once in a single allocation.
|
||||
// Allocate all context data at once in a single allocation.
|
||||
// Alignment is guaranteed because memory is dispatch from the highest
|
||||
// alignment requirement (Soa data: SimdFloat4) to the lowest (outdated
|
||||
// flag: unsigned char).
|
||||
@@ -430,8 +433,9 @@ void SamplingCache::Resize(int _max_tracks) {
|
||||
assert(alloc_cursor == alloc_begin + size);
|
||||
}
|
||||
|
||||
void SamplingCache::Step(const Animation& _animation, float _ratio) {
|
||||
// The cache is invalidated if animation has changed or if it is being rewind.
|
||||
void SamplingJob::Context::Step(const Animation& _animation, float _ratio) {
|
||||
// The context is invalidated if animation has changed or if it is being
|
||||
// rewind.
|
||||
if (animation_ != &_animation || _ratio < ratio_) {
|
||||
animation_ = &_animation;
|
||||
translation_cursor_ = 0;
|
||||
@@ -441,7 +445,7 @@ void SamplingCache::Step(const Animation& _animation, float _ratio) {
|
||||
ratio_ = _ratio;
|
||||
}
|
||||
|
||||
void SamplingCache::Invalidate() {
|
||||
void SamplingJob::Context::Invalidate() {
|
||||
animation_ = nullptr;
|
||||
ratio_ = 0.f;
|
||||
translation_cursor_ = 0;
|
||||
|
||||
+23
-12
@@ -41,6 +41,16 @@ namespace animation {
|
||||
|
||||
Skeleton::Skeleton() {}
|
||||
|
||||
Skeleton::Skeleton(Skeleton&& _other) { *this = std::move(_other); }
|
||||
|
||||
Skeleton& Skeleton::operator=(Skeleton&& _other) {
|
||||
std::swap(joint_rest_poses_, _other.joint_rest_poses_);
|
||||
std::swap(joint_parents_, _other.joint_parents_);
|
||||
std::swap(joint_names_, _other.joint_names_);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
Skeleton::~Skeleton() { Deallocate(); }
|
||||
|
||||
char* Skeleton::Allocate(size_t _chars_size, size_t _num_joints) {
|
||||
@@ -51,7 +61,7 @@ char* Skeleton::Allocate(size_t _chars_size, size_t _num_joints) {
|
||||
alignof(int16_t) >= alignof(char),
|
||||
"Must serve larger alignment values first)");
|
||||
|
||||
assert(joint_bind_poses_.size() == 0 && joint_names_.size() == 0 &&
|
||||
assert(joint_rest_poses_.size() == 0 && joint_names_.size() == 0 &&
|
||||
joint_parents_.size() == 0);
|
||||
|
||||
// Early out if no joint.
|
||||
@@ -59,23 +69,23 @@ char* Skeleton::Allocate(size_t _chars_size, size_t _num_joints) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Bind poses have SoA format
|
||||
// Rest poses have SoA format
|
||||
const size_t num_soa_joints = (_num_joints + 3) / 4;
|
||||
const size_t joint_bind_poses_size =
|
||||
const size_t joint_rest_poses_size =
|
||||
num_soa_joints * sizeof(math::SoaTransform);
|
||||
const size_t names_size = _num_joints * sizeof(char*);
|
||||
const size_t joint_parents_size = _num_joints * sizeof(int16_t);
|
||||
const size_t buffer_size =
|
||||
names_size + _chars_size + joint_parents_size + joint_bind_poses_size;
|
||||
names_size + _chars_size + joint_parents_size + joint_rest_poses_size;
|
||||
|
||||
// Allocates whole buffer.
|
||||
span<char> buffer = {static_cast<char*>(memory::default_allocator()->Allocate(
|
||||
span<byte> buffer = {static_cast<byte*>(memory::default_allocator()->Allocate(
|
||||
buffer_size, alignof(math::SoaTransform))),
|
||||
buffer_size};
|
||||
|
||||
// Serves larger alignment values first.
|
||||
// Bind pose first, biggest alignment.
|
||||
joint_bind_poses_ = fill_span<math::SoaTransform>(buffer, num_soa_joints);
|
||||
// Rest pose first, biggest alignment.
|
||||
joint_rest_poses_ = fill_span<math::SoaTransform>(buffer, num_soa_joints);
|
||||
|
||||
// Then names array, second biggest alignment.
|
||||
joint_names_ = fill_span<char*>(buffer, _num_joints);
|
||||
@@ -86,12 +96,13 @@ char* Skeleton::Allocate(size_t _chars_size, size_t _num_joints) {
|
||||
// Remaning buffer will be used to store joint names.
|
||||
assert(buffer.size_bytes() == _chars_size &&
|
||||
"Whole buffer should be consumned");
|
||||
return buffer.data();
|
||||
return reinterpret_cast<char*>(buffer.data());
|
||||
}
|
||||
|
||||
void Skeleton::Deallocate() {
|
||||
memory::default_allocator()->Deallocate(as_writable_bytes(joint_bind_poses_).data());
|
||||
joint_bind_poses_ = {};
|
||||
memory::default_allocator()->Deallocate(
|
||||
as_writable_bytes(joint_rest_poses_).data());
|
||||
joint_rest_poses_ = {};
|
||||
joint_names_ = {};
|
||||
joint_parents_ = {};
|
||||
}
|
||||
@@ -114,7 +125,7 @@ void Skeleton::Save(ozz::io::OArchive& _archive) const {
|
||||
_archive << static_cast<int32_t>(chars_count);
|
||||
_archive << ozz::io::MakeArray(joint_names_[0], chars_count);
|
||||
_archive << ozz::io::MakeArray(joint_parents_);
|
||||
_archive << ozz::io::MakeArray(joint_bind_poses_);
|
||||
_archive << ozz::io::MakeArray(joint_rest_poses_);
|
||||
}
|
||||
|
||||
void Skeleton::Load(ozz::io::IArchive& _archive, uint32_t _version) {
|
||||
@@ -155,7 +166,7 @@ void Skeleton::Load(ozz::io::IArchive& _archive, uint32_t _version) {
|
||||
joint_names_[num_joints - 1] = cursor;
|
||||
|
||||
_archive >> ozz::io::MakeArray(joint_parents_);
|
||||
_archive >> ozz::io::MakeArray(joint_bind_poses_);
|
||||
_archive >> ozz::io::MakeArray(joint_rest_poses_);
|
||||
}
|
||||
} // namespace animation
|
||||
} // namespace ozz
|
||||
|
||||
+22
-10
@@ -27,21 +27,33 @@
|
||||
|
||||
#include "ozz/animation/runtime/skeleton_utils.h"
|
||||
|
||||
#include "ozz/base/maths/soa_transform.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "ozz/base/maths/soa_transform.h"
|
||||
|
||||
namespace ozz {
|
||||
namespace animation {
|
||||
|
||||
// Unpacks skeleton bind pose stored in soa format by the skeleton.
|
||||
ozz::math::Transform GetJointLocalBindPose(const Skeleton& _skeleton,
|
||||
int FindJoint(const Skeleton& _skeleton, const char* _name) {
|
||||
const auto& names = _skeleton.joint_names();
|
||||
for (size_t i = 0; i < names.size(); ++i) {
|
||||
if (std::strcmp(names[i], _name) == 0) {
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Unpacks skeleton rest pose stored in soa format by the skeleton.
|
||||
ozz::math::Transform GetJointLocalRestPose(const Skeleton& _skeleton,
|
||||
int _joint) {
|
||||
assert(_joint >= 0 && _joint < _skeleton.num_joints() &&
|
||||
"Joint index out of range.");
|
||||
|
||||
const ozz::math::SoaTransform& soa_transform =
|
||||
_skeleton.joint_bind_poses()[_joint / 4];
|
||||
_skeleton.joint_rest_poses()[_joint / 4];
|
||||
|
||||
// Transpose SoA data to AoS.
|
||||
ozz::math::SimdFloat4 translations[4];
|
||||
@@ -52,13 +64,13 @@ ozz::math::Transform GetJointLocalBindPose(const Skeleton& _skeleton,
|
||||
ozz::math::Transpose3x4(&soa_transform.scale.x, scales);
|
||||
|
||||
// Stores to the Transform object.
|
||||
math::Transform bind_pose;
|
||||
math::Transform rest_pose;
|
||||
const int offset = _joint % 4;
|
||||
ozz::math::Store3PtrU(translations[offset], &bind_pose.translation.x);
|
||||
ozz::math::StorePtrU(rotations[offset], &bind_pose.rotation.x);
|
||||
ozz::math::Store3PtrU(scales[offset], &bind_pose.scale.x);
|
||||
ozz::math::Store3PtrU(translations[offset], &rest_pose.translation.x);
|
||||
ozz::math::StorePtrU(rotations[offset], &rest_pose.rotation.x);
|
||||
ozz::math::Store3PtrU(scales[offset], &rest_pose.scale.x);
|
||||
|
||||
return bind_pose;
|
||||
return rest_pose;
|
||||
}
|
||||
} // namespace animation
|
||||
} // namespace ozz
|
||||
|
||||
+20
-6
@@ -43,6 +43,20 @@ namespace internal {
|
||||
template <typename _ValueType>
|
||||
Track<_ValueType>::Track() : name_(nullptr) {}
|
||||
|
||||
template <typename _ValueType>
|
||||
Track<_ValueType>::Track(Track<_ValueType>&& _other) {
|
||||
*this = std::move(_other);
|
||||
}
|
||||
|
||||
template <typename _ValueType>
|
||||
Track<_ValueType>& Track<_ValueType>::operator=(Track<_ValueType>&& _other) {
|
||||
std::swap(ratios_, _other.ratios_);
|
||||
std::swap(values_, _other.values_);
|
||||
std::swap(steps_, _other.steps_);
|
||||
std::swap(name_, _other.name_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename _ValueType>
|
||||
Track<_ValueType>::~Track() {
|
||||
Deallocate();
|
||||
@@ -63,7 +77,7 @@ void Track<_ValueType>::Allocate(size_t _keys_count, size_t _name_len) {
|
||||
_keys_count * sizeof(float) + // ratios
|
||||
(_keys_count + 7) * sizeof(uint8_t) / 8 + // steps
|
||||
(_name_len > 0 ? _name_len + 1 : 0);
|
||||
span<char> buffer = {static_cast<char*>(memory::default_allocator()->Allocate(
|
||||
span<byte> buffer = {static_cast<byte*>(memory::default_allocator()->Allocate(
|
||||
buffer_size, alignof(_ValueType))),
|
||||
buffer_size};
|
||||
|
||||
@@ -142,11 +156,11 @@ void Track<_ValueType>::Load(ozz::io::IArchive& _archive, uint32_t _version) {
|
||||
}
|
||||
|
||||
// Explicitly instantiate supported tracks.
|
||||
template class Track<float>;
|
||||
template class Track<math::Float2>;
|
||||
template class Track<math::Float3>;
|
||||
template class Track<math::Float4>;
|
||||
template class Track<math::Quaternion>;
|
||||
template class OZZ_ANIMATION_DLL Track<float>;
|
||||
template class OZZ_ANIMATION_DLL Track<math::Float2>;
|
||||
template class OZZ_ANIMATION_DLL Track<math::Float3>;
|
||||
template class OZZ_ANIMATION_DLL Track<math::Float4>;
|
||||
template class OZZ_ANIMATION_DLL Track<math::Quaternion>;
|
||||
|
||||
} // namespace internal
|
||||
} // namespace animation
|
||||
|
||||
+11
-1
@@ -1,4 +1,6 @@
|
||||
add_library(ozz_base STATIC
|
||||
|
||||
add_library(ozz_base
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/base/export.h
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/base/endianness.h
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/base/gtest_helper.h
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/base/memory/allocator.h
|
||||
@@ -54,11 +56,19 @@ add_library(ozz_base STATIC
|
||||
maths/soa_math_archive.cc
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/base/maths/simd_math_archive.h
|
||||
maths/simd_math_archive.cc)
|
||||
|
||||
target_compile_definitions(ozz_base
|
||||
PUBLIC $<$<BOOL:${BUILD_SHARED_LIBS}>:OZZ_USE_DYNAMIC_LINKING>
|
||||
PRIVATE $<$<BOOL:${BUILD_SHARED_LIBS}>:OZZ_BUILD_BASE_LIB>)
|
||||
|
||||
target_compile_options(ozz_base PUBLIC $<$<CXX_COMPILER_ID:MSVC>:/wd4251>)
|
||||
target_include_directories(ozz_base PUBLIC
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:$<INSTALL_PREFIX>/include>)
|
||||
|
||||
set_target_properties(ozz_base PROPERTIES FOLDER "ozz")
|
||||
|
||||
install(TARGETS ozz_base DESTINATION lib)
|
||||
|
||||
fuse_target("ozz_base")
|
||||
|
||||
|
||||
+5
-3
@@ -182,7 +182,7 @@ int MemoryStream::Seek(int _offset, Origin _origin) {
|
||||
|
||||
// Exit if seeking before file begin or beyond max file size.
|
||||
if (origin < -_offset ||
|
||||
(_offset > 0 && origin > static_cast<int>(kMaxSize - _offset))) {
|
||||
(_offset > 0 && origin > static_cast<int>(kMaxSize) - _offset)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -204,9 +204,11 @@ bool MemoryStream::Resize(size_t _size) {
|
||||
(MemoryStream::kBufferSizeIncrement & (kBufferSizeIncrement - 1)) == 0,
|
||||
"kBufferSizeIncrement must be a power of 2");
|
||||
const size_t new_size = ozz::Align(_size, kBufferSizeIncrement);
|
||||
char* new_buffer = reinterpret_cast<char*>(
|
||||
byte* new_buffer = reinterpret_cast<byte*>(
|
||||
ozz::memory::default_allocator()->Allocate(new_size, 16));
|
||||
std::memcpy(new_buffer, buffer_, alloc_size_);
|
||||
if (buffer_ != nullptr) {
|
||||
std::memcpy(new_buffer, buffer_, alloc_size_);
|
||||
}
|
||||
ozz::memory::default_allocator()->Deallocate(buffer_);
|
||||
buffer_ = new_buffer;
|
||||
alloc_size_ = new_size;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
add_library(ozz_geometry STATIC
|
||||
add_library(ozz_geometry
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/geometry/runtime/export.h
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/geometry/runtime/skinning_job.h
|
||||
skinning_job.cc)
|
||||
target_link_libraries(ozz_geometry
|
||||
ozz_base)
|
||||
set_target_properties(ozz_geometry
|
||||
PROPERTIES FOLDER "ozz")
|
||||
skinning_job.cc)
|
||||
target_compile_definitions(ozz_geometry PRIVATE $<$<BOOL:${BUILD_SHARED_LIBS}>:OZZ_BUILD_GEOMETRY_LIB>)
|
||||
|
||||
target_link_libraries(ozz_geometry ozz_base)
|
||||
set_target_properties(ozz_geometry PROPERTIES FOLDER "ozz")
|
||||
|
||||
install(TARGETS ozz_geometry DESTINATION lib)
|
||||
|
||||
|
||||
+7
-5
@@ -1,9 +1,11 @@
|
||||
add_library(ozz_options STATIC
|
||||
../../include/ozz/options/options.h
|
||||
add_library(ozz_options
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/options/export.h
|
||||
${PROJECT_SOURCE_DIR}/include/ozz/options/options.h
|
||||
options.cc)
|
||||
target_include_directories(ozz_options PUBLIC
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:$<INSTALL_PREFIX>/include>)
|
||||
target_link_libraries(ozz_options ozz_base)
|
||||
|
||||
target_compile_definitions(ozz_options PRIVATE $<$<BOOL:${BUILD_SHARED_LIBS}>:OZZ_BUILD_OPTIONS_LIB>)
|
||||
|
||||
set_target_properties(ozz_options PROPERTIES FOLDER "ozz")
|
||||
|
||||
install(TARGETS ozz_options DESTINATION lib)
|
||||
|
||||
+10
-10
@@ -107,10 +107,10 @@ Registrer<_Option>::~Registrer() {
|
||||
}
|
||||
|
||||
// Explicit instantiation of all supported types of Registrer.
|
||||
template class Registrer<TypedOption<bool>>;
|
||||
template class Registrer<TypedOption<int>>;
|
||||
template class Registrer<TypedOption<float>>;
|
||||
template class Registrer<TypedOption<const char*>>;
|
||||
template class OZZ_OPTIONS_DLL Registrer<TypedOption<bool>>;
|
||||
template class OZZ_OPTIONS_DLL Registrer<TypedOption<int>>;
|
||||
template class OZZ_OPTIONS_DLL Registrer<TypedOption<float>>;
|
||||
template class OZZ_OPTIONS_DLL Registrer<TypedOption<const char*>>;
|
||||
} // namespace internal
|
||||
|
||||
// Construct the parser if no option is registered.
|
||||
@@ -130,10 +130,10 @@ ParseResult ParseCommandLine(int _argc, const char* const* _argv,
|
||||
|
||||
// A nullptr parser means that no option is registered and that ParseCommandLine
|
||||
// has not been called.
|
||||
std::string ParsedExecutablePath() {
|
||||
ozz::string ParsedExecutablePath() {
|
||||
Parser* parser = internal::g_global_registrer.parser();
|
||||
if (!parser) {
|
||||
return std::string();
|
||||
return {};
|
||||
}
|
||||
return parser->executable_path();
|
||||
}
|
||||
@@ -368,10 +368,10 @@ bool TypedOption<_Type>::ParseImpl(const char* _argv) {
|
||||
}
|
||||
|
||||
template <typename _Type>
|
||||
std::string TypedOption<_Type>::FormatDefault() const {
|
||||
ozz::string TypedOption<_Type>::FormatDefault() const {
|
||||
std::stringstream str;
|
||||
str << "\"" << std::boolalpha << default_ << "\"";
|
||||
return str.str();
|
||||
return str.str().c_str();
|
||||
}
|
||||
|
||||
template <typename _Type>
|
||||
@@ -648,8 +648,8 @@ const char* Parser::usage() const { return usage_; }
|
||||
|
||||
const char* Parser::version() const { return version_; }
|
||||
|
||||
std::string Parser::executable_path() const {
|
||||
return std::string(executable_path_begin_, executable_path_end_);
|
||||
ozz::string Parser::executable_path() const {
|
||||
return ozz::string(executable_path_begin_, executable_path_end_);
|
||||
}
|
||||
|
||||
const char* Parser::executable_name() const { return executable_name_; }
|
||||
|
||||
Reference in New Issue
Block a user