mirror of
https://github.com/FirestormViewer/phoenix-firestorm.git
synced 2026-08-14 00:48:30 +00:00
1172 lines
40 KiB
C++
1172 lines
40 KiB
C++
/**
|
|
* @file fslocalmeshimportgltf.cpp
|
|
* @author Beq Janus
|
|
* @brief Local Mesh glTF importer source
|
|
*
|
|
* $LicenseInfo:firstyear=2026&license=viewerlgpl$
|
|
* Phoenix Firestorm Viewer Source Code
|
|
* Copyright (C) 2026, Beq Janus.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation;
|
|
* version 2.1 of the License only.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*
|
|
* $/LicenseInfo$
|
|
*/
|
|
|
|
// precompiled headers
|
|
#include "llviewerprecompiledheaders.h"
|
|
|
|
// own header
|
|
#include "fslocalmeshimportgltf.h"
|
|
|
|
// linden headers
|
|
#include "llerror.h"
|
|
#include "llmodelloader.h"
|
|
#include "llvoavatarself.h"
|
|
#include "llmatrix4a.h"
|
|
#include "lljointdata.h"
|
|
#include "llviewercontrol.h"
|
|
|
|
// glTF headers
|
|
#include "gltf/asset.h"
|
|
#include "gltf/accessor.h"
|
|
|
|
// glm headers
|
|
#include <glm/gtc/matrix_inverse.hpp>
|
|
#include <glm/gtc/packing.hpp>
|
|
#include <glm/gtc/type_ptr.hpp>
|
|
#include <glm/gtx/matrix_decompose.hpp>
|
|
#include <glm/gtx/quaternion.hpp>
|
|
|
|
// STL headers
|
|
#include <algorithm>
|
|
#include <set>
|
|
#include <sstream>
|
|
|
|
namespace
|
|
{
|
|
using local_joint_map_t = std::map<std::string, std::string, std::less<>>;
|
|
|
|
// Premade rotation matrix, GLTF is Y-up while SL is Z-up
|
|
const glm::mat4 kCoordSystemRotation(
|
|
1.f, 0.f, 0.f, 0.f,
|
|
0.f, 0.f, 1.f, 0.f,
|
|
0.f, -1.f, 0.f, 0.f,
|
|
0.f, 0.f, 0.f, 1.f
|
|
);
|
|
|
|
const glm::mat4 kCoordSystemRotationXY(
|
|
0.f, 1.f, 0.f, 0.f,
|
|
-1.f, 0.f, 0.f, 0.f,
|
|
0.f, 0.f, 1.f, 0.f,
|
|
0.f, 0.f, 0.f, 1.f
|
|
);
|
|
|
|
glm::mat4 buildViewerBasisTransform(bool apply_xy_rotation)
|
|
{
|
|
glm::mat4 basis = kCoordSystemRotation;
|
|
if (apply_xy_rotation)
|
|
{
|
|
basis = kCoordSystemRotationXY * basis;
|
|
}
|
|
return basis;
|
|
}
|
|
|
|
glm::mat4 convertTransformToViewerBasis(const glm::mat4& transform, bool apply_xy_rotation)
|
|
{
|
|
const glm::mat4 basis = buildViewerBasisTransform(apply_xy_rotation);
|
|
return basis * transform * glm::inverse(basis);
|
|
}
|
|
|
|
std::vector<S32> buildParentMap(const LL::GLTF::Asset& asset)
|
|
{
|
|
std::vector<S32> parent_map(asset.mNodes.size(), -1);
|
|
for (size_t i = 0; i < asset.mNodes.size(); ++i)
|
|
{
|
|
for (S32 child_idx : asset.mNodes[i].mChildren)
|
|
{
|
|
if (child_idx >= 0 && child_idx < static_cast<S32>(parent_map.size()))
|
|
{
|
|
parent_map[child_idx] = static_cast<S32>(i);
|
|
}
|
|
}
|
|
}
|
|
return parent_map;
|
|
}
|
|
|
|
std::vector<bool> buildSkinJointMembership(const LL::GLTF::Asset& asset, const LL::GLTF::Skin& skin)
|
|
{
|
|
std::vector<bool> is_skin_joint(asset.mNodes.size(), false);
|
|
for (S32 joint_node_idx : skin.mJoints)
|
|
{
|
|
if (joint_node_idx >= 0 && joint_node_idx < static_cast<S32>(is_skin_joint.size()))
|
|
{
|
|
is_skin_joint[joint_node_idx] = true;
|
|
}
|
|
}
|
|
return is_skin_joint;
|
|
}
|
|
|
|
void collectMeshNodes(const LL::GLTF::Asset& asset, S32 node_idx, std::vector<S32>& mesh_nodes)
|
|
{
|
|
if (node_idx < 0 || node_idx >= static_cast<S32>(asset.mNodes.size()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
const LL::GLTF::Node& node = asset.mNodes[node_idx];
|
|
if (node.mMesh >= 0)
|
|
{
|
|
mesh_nodes.push_back(node_idx);
|
|
}
|
|
|
|
for (S32 child_idx : node.mChildren)
|
|
{
|
|
collectMeshNodes(asset, child_idx, mesh_nodes);
|
|
}
|
|
}
|
|
|
|
void computeCombinedNodeTransform(const LL::GLTF::Asset& asset,
|
|
const std::vector<S32>& parent_map,
|
|
S32 node_index,
|
|
glm::mat4& combined_transform)
|
|
{
|
|
if (node_index < 0 || node_index >= static_cast<S32>(asset.mNodes.size()))
|
|
{
|
|
combined_transform = glm::mat4(1.f);
|
|
return;
|
|
}
|
|
|
|
combined_transform = asset.mNodes[node_index].mMatrix;
|
|
S32 parent_idx = (node_index < static_cast<S32>(parent_map.size())) ? parent_map[node_index] : -1;
|
|
while (parent_idx >= 0 && parent_idx < static_cast<S32>(asset.mNodes.size()))
|
|
{
|
|
combined_transform = asset.mNodes[parent_idx].mMatrix * combined_transform;
|
|
parent_idx = (parent_idx < static_cast<S32>(parent_map.size())) ? parent_map[parent_idx] : -1;
|
|
}
|
|
}
|
|
|
|
std::string normalizeJointName(const local_joint_map_t& joint_map, const std::string& joint_name)
|
|
{
|
|
auto joint_it = joint_map.find(joint_name);
|
|
return joint_it != joint_map.end() ? joint_it->second : joint_name;
|
|
}
|
|
|
|
std::vector<S32> buildJointIndexRemap(const LL::GLTF::Asset& asset,
|
|
const LL::GLTF::Skin& skin,
|
|
const local_joint_map_t& joint_map,
|
|
LLPointer<LLMeshSkinInfo> canonical_skin)
|
|
{
|
|
std::vector<S32> joint_index_remap(skin.mJoints.size(), -1);
|
|
if (canonical_skin.isNull() || canonical_skin->mJointNames.empty())
|
|
{
|
|
for (size_t i = 0; i < joint_index_remap.size(); ++i)
|
|
{
|
|
joint_index_remap[i] = static_cast<S32>(i);
|
|
}
|
|
return joint_index_remap;
|
|
}
|
|
|
|
std::map<std::string, S32, std::less<>> canonical_indices;
|
|
for (size_t i = 0; i < canonical_skin->mJointNames.size(); ++i)
|
|
{
|
|
canonical_indices.emplace(canonical_skin->mJointNames[i], static_cast<S32>(i));
|
|
}
|
|
|
|
for (size_t i = 0; i < skin.mJoints.size(); ++i)
|
|
{
|
|
const S32 joint_node_idx = skin.mJoints[i];
|
|
if (joint_node_idx < 0 || joint_node_idx >= static_cast<S32>(asset.mNodes.size()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const std::string joint_name = normalizeJointName(joint_map, asset.mNodes[joint_node_idx].mName);
|
|
auto canonical_it = canonical_indices.find(joint_name);
|
|
if (canonical_it != canonical_indices.end())
|
|
{
|
|
joint_index_remap[i] = canonical_it->second;
|
|
}
|
|
}
|
|
|
|
return joint_index_remap;
|
|
}
|
|
|
|
bool buildTriangleIndexArray(const LL::GLTF::Primitive& prim, std::vector<U32>& indices_out, std::string& error_out)
|
|
{
|
|
indices_out.clear();
|
|
error_out.clear();
|
|
|
|
std::vector<U32> base_indices = prim.mIndexArray;
|
|
if (base_indices.empty())
|
|
{
|
|
base_indices.resize(prim.mPositions.size());
|
|
for (U32 i = 0; i < base_indices.size(); ++i)
|
|
{
|
|
base_indices[i] = i;
|
|
}
|
|
}
|
|
|
|
switch (prim.mMode)
|
|
{
|
|
case LL::GLTF::Primitive::Mode::TRIANGLES:
|
|
{
|
|
if (base_indices.size() % 3 != 0)
|
|
{
|
|
error_out = "Index count is not divisible by 3.";
|
|
return false;
|
|
}
|
|
indices_out = std::move(base_indices);
|
|
return true;
|
|
}
|
|
case LL::GLTF::Primitive::Mode::TRIANGLE_STRIP:
|
|
{
|
|
if (base_indices.size() < 3)
|
|
{
|
|
error_out = "Triangle strip has fewer than 3 indices.";
|
|
return false;
|
|
}
|
|
for (size_t i = 2; i < base_indices.size(); ++i)
|
|
{
|
|
U32 i0 = base_indices[i - 2];
|
|
U32 i1 = base_indices[i - 1];
|
|
U32 i2 = base_indices[i];
|
|
if (i % 2 == 1)
|
|
{
|
|
std::swap(i0, i1);
|
|
}
|
|
indices_out.push_back(i0);
|
|
indices_out.push_back(i1);
|
|
indices_out.push_back(i2);
|
|
}
|
|
return true;
|
|
}
|
|
case LL::GLTF::Primitive::Mode::TRIANGLE_FAN:
|
|
{
|
|
if (base_indices.size() < 3)
|
|
{
|
|
error_out = "Triangle fan has fewer than 3 indices.";
|
|
return false;
|
|
}
|
|
for (size_t i = 2; i < base_indices.size(); ++i)
|
|
{
|
|
indices_out.push_back(base_indices[0]);
|
|
indices_out.push_back(base_indices[i - 1]);
|
|
indices_out.push_back(base_indices[i]);
|
|
}
|
|
return true;
|
|
}
|
|
default:
|
|
{
|
|
error_out = "Unsupported primitive mode.";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
struct JointNodeData
|
|
{
|
|
JointNodeData()
|
|
: mNodeIdx(-1)
|
|
, mParentNodeIdx(-1)
|
|
, mIsValidViewerJoint(false)
|
|
, mIsParentValidViewerJoint(false)
|
|
, mIsOverrideValid(false)
|
|
{
|
|
}
|
|
|
|
S32 mNodeIdx;
|
|
S32 mParentNodeIdx;
|
|
glm::mat4 mGltfRestMatrix;
|
|
glm::mat4 mViewerRestMatrix;
|
|
glm::mat4 mOverrideRestMatrix;
|
|
glm::mat4 mGltfMatrix;
|
|
glm::mat4 mOverrideMatrix;
|
|
std::string mName;
|
|
bool mIsValidViewerJoint;
|
|
bool mIsParentValidViewerJoint;
|
|
bool mIsOverrideValid;
|
|
};
|
|
|
|
using joints_data_map_t = std::map<S32, JointNodeData>;
|
|
using joints_name_to_node_map_t = std::map<std::string, S32, std::less<>>;
|
|
|
|
glm::mat4 buildGltfRestMatrix(const LL::GLTF::Asset& asset,
|
|
const std::vector<S32>& parent_map,
|
|
const std::vector<bool>& is_skin_joint,
|
|
S32 joint_node_index)
|
|
{
|
|
if (joint_node_index < 0 || joint_node_index >= static_cast<S32>(asset.mNodes.size()))
|
|
{
|
|
return glm::mat4(1.0f);
|
|
}
|
|
|
|
glm::mat4 rest_matrix = asset.mNodes[joint_node_index].mMatrix;
|
|
S32 parent_idx = (joint_node_index < static_cast<S32>(parent_map.size())) ? parent_map[joint_node_index] : -1;
|
|
while (parent_idx >= 0
|
|
&& parent_idx < static_cast<S32>(asset.mNodes.size())
|
|
&& parent_idx < static_cast<S32>(is_skin_joint.size())
|
|
&& is_skin_joint[parent_idx])
|
|
{
|
|
rest_matrix = asset.mNodes[parent_idx].mMatrix * rest_matrix;
|
|
parent_idx = (parent_idx < static_cast<S32>(parent_map.size())) ? parent_map[parent_idx] : -1;
|
|
}
|
|
|
|
return rest_matrix;
|
|
}
|
|
|
|
bool checkForXYrotation(const LL::GLTF::Asset& asset,
|
|
const LL::GLTF::Skin& skin,
|
|
const std::vector<S32>& parent_map,
|
|
const local_joint_map_t& joint_map)
|
|
{
|
|
const std::vector<bool> is_skin_joint = buildSkinJointMembership(asset, skin);
|
|
constexpr char right_shoulder[] = "mShoulderRight";
|
|
constexpr char left_shoulder[] = "mShoulderLeft";
|
|
|
|
const S32 joint_count = static_cast<S32>(skin.mJoints.size());
|
|
S32 joints_found = 0;
|
|
for (S32 i = 0; i < joint_count; ++i)
|
|
{
|
|
if (i >= static_cast<S32>(skin.mInverseBindMatricesData.size()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const S32 joint_node_idx = skin.mJoints[i];
|
|
if (joint_node_idx < 0 || joint_node_idx >= static_cast<S32>(asset.mNodes.size()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
std::string joint_name = asset.mNodes[joint_node_idx].mName;
|
|
auto map_it = joint_map.find(joint_name);
|
|
if (map_it == joint_map.end())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
joint_name = map_it->second;
|
|
if (joint_name != right_shoulder && joint_name != left_shoulder)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
glm::mat4 gltf_joint_rest = buildGltfRestMatrix(asset, parent_map, is_skin_joint, joint_node_idx);
|
|
glm::mat4 test_mat = glm::inverse(gltf_joint_rest) * skin.mInverseBindMatricesData[i];
|
|
|
|
const bool is_xy_rotated =
|
|
(fabsf(test_mat[0][0]) < 0.5f) &&
|
|
(fabsf(test_mat[1][1]) < 0.5f) &&
|
|
(fabsf(test_mat[2][2]) < 0.5f);
|
|
|
|
if (!is_xy_rotated)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
++joints_found;
|
|
}
|
|
|
|
return joints_found == 2;
|
|
}
|
|
|
|
void buildOverrideMatrix(const LLJointData& viewer_data,
|
|
joints_data_map_t& gltf_nodes,
|
|
joints_name_to_node_map_t& names_to_nodes,
|
|
const glm::mat4& parent_rest,
|
|
const glm::mat4& parent_support_rest,
|
|
bool apply_xy_rotation)
|
|
{
|
|
glm::mat4 rest(1.f);
|
|
const auto found_node = names_to_nodes.find(viewer_data.mName);
|
|
if (found_node != names_to_nodes.end())
|
|
{
|
|
S32 gltf_node_idx = found_node->second;
|
|
JointNodeData& node = gltf_nodes[gltf_node_idx];
|
|
node.mIsOverrideValid = true;
|
|
node.mViewerRestMatrix = viewer_data.mRestMatrix;
|
|
|
|
glm::mat4 gltf_joint_rest_pose = convertTransformToViewerBasis(node.mGltfRestMatrix, apply_xy_rotation);
|
|
|
|
glm::mat4 translated_joint;
|
|
if (viewer_data.mIsJoint)
|
|
{
|
|
translated_joint = glm::inverse(parent_rest) * gltf_joint_rest_pose;
|
|
}
|
|
else
|
|
{
|
|
translated_joint = glm::inverse(parent_support_rest) * gltf_joint_rest_pose;
|
|
}
|
|
|
|
glm::vec3 translation_override(0.0f);
|
|
glm::vec3 skew;
|
|
glm::vec3 scale;
|
|
glm::vec4 perspective;
|
|
glm::quat rotation;
|
|
glm::decompose(translated_joint, scale, rotation, translation_override, skew, perspective);
|
|
|
|
node.mOverrideMatrix = glm::recompose(glm::vec3(1, 1, 1),
|
|
glm::identity<glm::quat>(),
|
|
translation_override,
|
|
glm::vec3(0, 0, 0),
|
|
glm::vec4(0, 0, 0, 1));
|
|
|
|
if (viewer_data.mIsJoint)
|
|
{
|
|
// Keep the full local TRS for bind/rest propagation. Using the
|
|
// translation-only override here causes parent rotations to be
|
|
// re-encoded as rotated child translations down the chain.
|
|
rest = parent_rest * translated_joint;
|
|
node.mOverrideRestMatrix = rest;
|
|
}
|
|
else
|
|
{
|
|
rest = parent_support_rest * translated_joint;
|
|
node.mOverrideRestMatrix = rest;
|
|
rest = node.mOverrideRestMatrix;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
rest = parent_rest * viewer_data.mJointMatrix;
|
|
}
|
|
|
|
glm::mat4 support_rest = parent_support_rest;
|
|
if (viewer_data.mSupport == LLJointData::SUPPORT_BASE)
|
|
{
|
|
support_rest = rest;
|
|
}
|
|
|
|
for (const LLJointData& child_data : viewer_data.mChildren)
|
|
{
|
|
buildOverrideMatrix(child_data, gltf_nodes, names_to_nodes, rest, support_rest, apply_xy_rotation);
|
|
}
|
|
}
|
|
|
|
glm::mat4 computeGltfToViewerSkeletonTransform(const joints_data_map_t& joints_data_map,
|
|
S32 gltf_node_index,
|
|
bool apply_xy_rotation)
|
|
{
|
|
const auto it = joints_data_map.find(gltf_node_index);
|
|
if (it == joints_data_map.end())
|
|
{
|
|
return glm::mat4(1.0f);
|
|
}
|
|
|
|
const JointNodeData& node_data = it->second;
|
|
if (!node_data.mIsOverrideValid)
|
|
{
|
|
return glm::mat4(1.0f);
|
|
}
|
|
|
|
glm::mat4 rest_pose = convertTransformToViewerBasis(node_data.mGltfRestMatrix, apply_xy_rotation);
|
|
return node_data.mOverrideRestMatrix * glm::inverse(rest_pose);
|
|
}
|
|
|
|
}
|
|
|
|
FSLocalMeshImportGLTF::FSLocalMeshImportGLTF()
|
|
{
|
|
mLogToInfo = false;
|
|
}
|
|
|
|
FSLocalMeshImportGLTF::loadFile_return FSLocalMeshImportGLTF::loadFile(const std::string& filename,
|
|
LLLocalMeshFileLOD lod,
|
|
std::vector<std::unique_ptr<LLLocalMeshObject>>& object_vector)
|
|
{
|
|
pushLog("GLTF Importer", "Starting");
|
|
LL_DEBUGS("LocalMesh") << "GLTF Importer: Starting" << LL_ENDL;
|
|
|
|
LL::GLTF::Asset asset;
|
|
setLod(lod);
|
|
|
|
if (!asset.load(filename, false))
|
|
{
|
|
pushLog("GLTF Importer", "Failed to load glTF asset: " + filename, true);
|
|
return loadFile_return(false, mLoadingLog);
|
|
}
|
|
|
|
for (auto& node : asset.mNodes)
|
|
{
|
|
node.makeMatrixValid();
|
|
}
|
|
|
|
mParentMap = buildParentMap(asset);
|
|
|
|
if (asset.mScenes.empty())
|
|
{
|
|
pushLog("GLTF Importer", "GLTF asset contains no scenes.");
|
|
return loadFile_return(false, mLoadingLog);
|
|
}
|
|
|
|
const S32 scene_idx = (asset.mScene >= 0 && asset.mScene < static_cast<S32>(asset.mScenes.size()))
|
|
? asset.mScene
|
|
: 0;
|
|
LL::GLTF::Scene& scene = asset.mScenes[scene_idx];
|
|
|
|
// Update transforms without uploading to GL
|
|
scene.updateTransforms(asset);
|
|
|
|
std::vector<S32> mesh_nodes;
|
|
for (S32 root_idx : scene.mNodes)
|
|
{
|
|
collectMeshNodes(asset, root_idx, mesh_nodes);
|
|
}
|
|
|
|
if (mesh_nodes.empty())
|
|
{
|
|
pushLog("GLTF Importer", "GLTF asset contains no mesh nodes.");
|
|
return loadFile_return(false, mLoadingLog);
|
|
}
|
|
|
|
for (size_t object_idx = 0; object_idx < mesh_nodes.size(); ++object_idx)
|
|
{
|
|
S32 node_idx = mesh_nodes[object_idx];
|
|
const LL::GLTF::Node& node = asset.mNodes[node_idx];
|
|
|
|
if (node.mMesh < 0 || node.mMesh >= static_cast<S32>(asset.mMeshes.size()))
|
|
{
|
|
pushLog("GLTF Importer", "Mesh index out of bounds for node " + std::to_string(node_idx) + ", skipping.");
|
|
continue;
|
|
}
|
|
|
|
std::string object_name = node.mName;
|
|
if (object_name.empty())
|
|
{
|
|
object_name = asset.mMeshes[node.mMesh].mName;
|
|
}
|
|
if (object_name.empty())
|
|
{
|
|
object_name = "node_" + std::to_string(node_idx);
|
|
}
|
|
|
|
// LOD3 loads objects, lower LODs reuse them
|
|
if (mLod == LLLocalMeshFileLOD::LOCAL_LOD_HIGH)
|
|
{
|
|
std::unique_ptr<LLLocalMeshObject> current_object = std::make_unique<LLLocalMeshObject>(object_name);
|
|
bool object_success = processNodeMesh(asset, node, current_object.get());
|
|
|
|
if (object_success)
|
|
{
|
|
pushLog("GLTF Importer", "Object loaded successfully.");
|
|
LLMatrix4 identity_transform;
|
|
identity_transform.setIdentity();
|
|
postProcessObject(*current_object, identity_transform, true);
|
|
object_vector.push_back(std::move(current_object));
|
|
}
|
|
else
|
|
{
|
|
pushLog("GLTF Importer", "Object loading failed, skipping.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (object_vector.size() <= object_idx)
|
|
{
|
|
pushLog("GLTF Importer", "LOD" + std::to_string(mLod) + " is requesting an object that LOD3 did not have or failed to load, skipping.");
|
|
continue;
|
|
}
|
|
|
|
auto current_object_ptr = object_vector[object_idx].get();
|
|
if (!current_object_ptr)
|
|
{
|
|
pushLog("GLTF Importer", "Bad object reference given, skipping.");
|
|
continue;
|
|
}
|
|
|
|
bool object_success = processNodeMesh(asset, node, current_object_ptr);
|
|
if (object_success)
|
|
{
|
|
pushLog("GLTF Importer", "Object loaded successfully.");
|
|
LLMatrix4 identity_transform;
|
|
identity_transform.setIdentity();
|
|
postProcessObject(*current_object_ptr, identity_transform, false);
|
|
}
|
|
else
|
|
{
|
|
pushLog("GLTF Importer", "Object loading failed.");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (object_vector.empty())
|
|
{
|
|
pushLog("GLTF Importer", "No objects have been successfully loaded, stopping.");
|
|
return loadFile_return(false, mLoadingLog);
|
|
}
|
|
|
|
if (mLod == LLLocalMeshFileLOD::LOCAL_LOD_HIGH)
|
|
{
|
|
for (const auto& object : object_vector)
|
|
{
|
|
finalizeSkinInfo(object.get());
|
|
}
|
|
}
|
|
|
|
pushLog("GLTF Importer", "Object and face parsing complete.");
|
|
return loadFile_return(true, mLoadingLog);
|
|
}
|
|
|
|
bool FSLocalMeshImportGLTF::processNodeMesh(const LL::GLTF::Asset& asset, const LL::GLTF::Node& node, LLLocalMeshObject* object)
|
|
{
|
|
if (!object)
|
|
{
|
|
pushLog("GLTF Importer", "LLLocalMeshObject pointer is null.");
|
|
return false;
|
|
}
|
|
|
|
if (node.mMesh < 0 || node.mMesh >= static_cast<S32>(asset.mMeshes.size()))
|
|
{
|
|
pushLog("GLTF Importer", "Invalid mesh index for node.");
|
|
return false;
|
|
}
|
|
|
|
const LL::GLTF::Mesh& mesh = asset.mMeshes[node.mMesh];
|
|
if (mesh.mPrimitives.empty())
|
|
{
|
|
pushLog("GLTF Importer", "Mesh has no primitives, skipping.");
|
|
return false;
|
|
}
|
|
|
|
auto& object_faces = object->getFaces(mLod);
|
|
object_faces.clear();
|
|
|
|
S32 skin_idx = node.mSkin;
|
|
bool apply_xy_rotation = false;
|
|
std::vector<S32> joint_index_remap;
|
|
std::vector<std::string> skin_joint_names;
|
|
if (skin_idx >= 0 && skin_idx < static_cast<S32>(asset.mSkins.size()))
|
|
{
|
|
const auto joint_map = FSLocalMeshImportBase::loadJointMap();
|
|
const LL::GLTF::Skin& skin = asset.mSkins[skin_idx];
|
|
|
|
apply_xy_rotation = checkForXYrotation(asset, skin, mParentMap, joint_map);
|
|
|
|
skin_joint_names.reserve(skin.mJoints.size());
|
|
for (S32 joint_node_idx : skin.mJoints)
|
|
{
|
|
std::string joint_name;
|
|
if (joint_node_idx >= 0 && joint_node_idx < static_cast<S32>(asset.mNodes.size()))
|
|
{
|
|
joint_name = asset.mNodes[joint_node_idx].mName;
|
|
}
|
|
skin_joint_names.push_back(normalizeJointName(joint_map, joint_name));
|
|
}
|
|
|
|
LLPointer<LLMeshSkinInfo> canonical_skin = object->getObjectMeshSkinInfo();
|
|
if (mLod == LLLocalMeshFileLOD::LOCAL_LOD_HIGH
|
|
|| canonical_skin.isNull()
|
|
|| canonical_skin->mJointNames.empty())
|
|
{
|
|
if (!initSkinInfo(asset, skin_idx, object))
|
|
{
|
|
skin_idx = -1;
|
|
}
|
|
canonical_skin = object->getObjectMeshSkinInfo();
|
|
}
|
|
|
|
if (skin_idx >= 0)
|
|
{
|
|
joint_index_remap = buildJointIndexRemap(asset, skin, joint_map, canonical_skin);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
skin_idx = -1;
|
|
}
|
|
|
|
const S32 node_idx = static_cast<S32>(&node - asset.mNodes.data());
|
|
glm::mat4 mesh_transform(1.f);
|
|
computeCombinedNodeTransform(asset, mParentMap, node_idx, mesh_transform);
|
|
mesh_transform = kCoordSystemRotation * mesh_transform;
|
|
if (apply_xy_rotation)
|
|
{
|
|
mesh_transform = kCoordSystemRotationXY * mesh_transform;
|
|
}
|
|
|
|
const bool flip_winding = glm::determinant(mesh_transform) < 0.f;
|
|
|
|
bool submesh_failure_found = false;
|
|
bool stop_loading_additional_faces = false;
|
|
|
|
for (size_t prim_idx = 0; prim_idx < mesh.mPrimitives.size(); ++prim_idx)
|
|
{
|
|
if (stop_loading_additional_faces)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (object_faces.size() >= LL_SCULPT_MESH_MAX_FACES)
|
|
{
|
|
pushLog("GLTF Importer", "NOTE: reached the limit of "
|
|
+ std::to_string(LL_SCULPT_MESH_MAX_FACES)
|
|
+ " faces per object, ignoring the rest.");
|
|
stop_loading_additional_faces = true;
|
|
break;
|
|
}
|
|
|
|
const LL::GLTF::Primitive& prim = mesh.mPrimitives[prim_idx];
|
|
bool face_ok = appendPrimitiveToObject(asset,
|
|
prim,
|
|
mesh_transform,
|
|
flip_winding,
|
|
joint_index_remap,
|
|
skin_joint_names,
|
|
object,
|
|
skin_idx);
|
|
if (!face_ok)
|
|
{
|
|
submesh_failure_found = true;
|
|
}
|
|
}
|
|
|
|
return !submesh_failure_found;
|
|
}
|
|
|
|
bool FSLocalMeshImportGLTF::appendPrimitiveToObject(const LL::GLTF::Asset& asset,
|
|
const LL::GLTF::Primitive& prim,
|
|
const glm::mat4& mesh_transform,
|
|
bool flip_winding,
|
|
const std::vector<S32>& joint_index_remap,
|
|
const std::vector<std::string>& skin_joint_names,
|
|
LLLocalMeshObject* object,
|
|
S32 skin_idx)
|
|
{
|
|
auto current_submesh = std::make_unique<LLLocalMeshFace>();
|
|
|
|
if (prim.mPositions.empty())
|
|
{
|
|
pushLog("GLTF Importer", "Primitive has no positions, skipping.");
|
|
return false;
|
|
}
|
|
|
|
std::vector<U32> triangle_indices;
|
|
std::string triangle_error;
|
|
if (!buildTriangleIndexArray(prim, triangle_indices, triangle_error))
|
|
{
|
|
pushLog("GLTF Importer", "Primitive triangulation failed: " + triangle_error);
|
|
return false;
|
|
}
|
|
|
|
for (U32 idx : triangle_indices)
|
|
{
|
|
if (idx >= prim.mPositions.size())
|
|
{
|
|
pushLog("GLTF Importer", "Primitive index out of bounds, skipping.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const glm::mat3 normal_transform = glm::transpose(glm::inverse(glm::mat3(mesh_transform)));
|
|
|
|
auto& list_positions = current_submesh->getPositions();
|
|
auto& list_normals = current_submesh->getNormals();
|
|
auto& list_uvs = current_submesh->getUVs();
|
|
auto& list_indices = current_submesh->getIndices();
|
|
|
|
list_positions.reserve(prim.mPositions.size());
|
|
list_normals.reserve(prim.mPositions.size());
|
|
list_uvs.reserve(prim.mPositions.size());
|
|
|
|
for (size_t vert_idx = 0; vert_idx < prim.mPositions.size(); ++vert_idx)
|
|
{
|
|
const LLVector4a& pos = prim.mPositions[vert_idx];
|
|
glm::vec4 transformed_pos = mesh_transform * glm::vec4(pos[0], pos[1], pos[2], 1.f);
|
|
|
|
LLVector4 llpos;
|
|
llpos.set(transformed_pos.x, transformed_pos.y, transformed_pos.z, 0.f);
|
|
list_positions.push_back(llpos);
|
|
|
|
if (vert_idx == 0)
|
|
{
|
|
current_submesh->setFaceBoundingBox(llpos, true);
|
|
}
|
|
else
|
|
{
|
|
current_submesh->setFaceBoundingBox(llpos);
|
|
}
|
|
|
|
if (!prim.mNormals.empty() && vert_idx < prim.mNormals.size())
|
|
{
|
|
const LLVector4a& norm = prim.mNormals[vert_idx];
|
|
glm::vec3 transformed_norm = normal_transform * glm::vec3(norm[0], norm[1], norm[2]);
|
|
LLVector4 llnorm;
|
|
llnorm.set(transformed_norm.x, transformed_norm.y, transformed_norm.z, 0.f);
|
|
list_normals.push_back(llnorm);
|
|
}
|
|
else
|
|
{
|
|
list_normals.emplace_back(0.f, 0.f, 1.f, 0.f);
|
|
}
|
|
|
|
if (!prim.mTexCoords0.empty() && vert_idx < prim.mTexCoords0.size())
|
|
{
|
|
const LLVector2& uv = prim.mTexCoords0[vert_idx];
|
|
list_uvs.emplace_back(uv.mV[0], -uv.mV[1]);
|
|
}
|
|
}
|
|
|
|
list_indices.reserve(triangle_indices.size());
|
|
for (size_t idx = 0; idx < triangle_indices.size(); idx += 3)
|
|
{
|
|
list_indices.push_back(static_cast<S32>(triangle_indices[idx]));
|
|
list_indices.push_back(static_cast<S32>(triangle_indices[idx + (flip_winding ? 2 : 1)]));
|
|
list_indices.push_back(static_cast<S32>(triangle_indices[idx + (flip_winding ? 1 : 2)]));
|
|
}
|
|
|
|
// weights and joints
|
|
if (skin_idx >= 0
|
|
&& skin_idx < static_cast<S32>(asset.mSkins.size())
|
|
&& !prim.mWeights.empty()
|
|
&& !prim.mJoints.empty()
|
|
&& prim.mWeights.size() == prim.mPositions.size()
|
|
&& prim.mJoints.size() == prim.mPositions.size())
|
|
{
|
|
const LL::GLTF::Skin& skin = asset.mSkins[skin_idx];
|
|
|
|
LL::GLTF::Accessor::ComponentType joint_component_type = LL::GLTF::Accessor::ComponentType::UNSIGNED_BYTE;
|
|
auto joint_attr_it = prim.mAttributes.find("JOINTS_0");
|
|
if (joint_attr_it != prim.mAttributes.end() && joint_attr_it->second >= 0)
|
|
{
|
|
joint_component_type = asset.mAccessors[joint_attr_it->second].mComponentType;
|
|
}
|
|
|
|
auto& list_skin = current_submesh->getSkin();
|
|
list_skin.reserve(prim.mWeights.size());
|
|
S32 dropped_weighted_influences = 0;
|
|
S32 affected_vertices = 0;
|
|
std::set<std::string, std::less<>> dropped_joint_names;
|
|
|
|
for (size_t vert_idx = 0; vert_idx < prim.mWeights.size(); ++vert_idx)
|
|
{
|
|
const LLVector4a& weight_vec = prim.mWeights[vert_idx];
|
|
const float* weights = weight_vec.getF32ptr();
|
|
|
|
std::array<int, 4> joint_indices{};
|
|
if (joint_component_type == LL::GLTF::Accessor::ComponentType::UNSIGNED_SHORT)
|
|
{
|
|
const glm::u16vec4 unpacked_joints = glm::unpackUint4x16(prim.mJoints[vert_idx]);
|
|
joint_indices = { static_cast<int>(unpacked_joints.x),
|
|
static_cast<int>(unpacked_joints.y),
|
|
static_cast<int>(unpacked_joints.z),
|
|
static_cast<int>(unpacked_joints.w) };
|
|
}
|
|
else
|
|
{
|
|
const glm::u8vec4 unpacked_joints = glm::unpackUint4x8(static_cast<U32>(prim.mJoints[vert_idx] & 0xFFFFFFFF));
|
|
joint_indices = { static_cast<int>(unpacked_joints.x),
|
|
static_cast<int>(unpacked_joints.y),
|
|
static_cast<int>(unpacked_joints.z),
|
|
static_cast<int>(unpacked_joints.w) };
|
|
}
|
|
|
|
float weight_values[4] = { weights[0], weights[1], weights[2], weights[3] };
|
|
|
|
// Normalize and clamp
|
|
float total = weight_values[0] + weight_values[1] + weight_values[2] + weight_values[3];
|
|
if (total > 0.f)
|
|
{
|
|
for (float& w : weight_values)
|
|
{
|
|
w /= total;
|
|
}
|
|
}
|
|
|
|
LLLocalMeshFace::LLLocalMeshSkinUnit unit{};
|
|
bool vertex_dropped_joint = false;
|
|
for (size_t j = 0; j < 4; ++j)
|
|
{
|
|
S32 remapped_joint = joint_indices[j];
|
|
if (!joint_index_remap.empty())
|
|
{
|
|
if (remapped_joint < 0 || remapped_joint >= static_cast<S32>(joint_index_remap.size()))
|
|
{
|
|
remapped_joint = -1;
|
|
}
|
|
else
|
|
{
|
|
remapped_joint = joint_index_remap[remapped_joint];
|
|
}
|
|
}
|
|
|
|
if (weight_values[j] > 0.f && remapped_joint < 0)
|
|
{
|
|
++dropped_weighted_influences;
|
|
vertex_dropped_joint = true;
|
|
if (joint_indices[j] >= 0 && joint_indices[j] < static_cast<S32>(skin_joint_names.size()))
|
|
{
|
|
dropped_joint_names.emplace(skin_joint_names[joint_indices[j]]);
|
|
}
|
|
}
|
|
|
|
if (remapped_joint < 0 || weight_values[j] <= 0.f)
|
|
{
|
|
unit.mJointIndices[j] = -1;
|
|
unit.mJointWeights[j] = 0.f;
|
|
}
|
|
else
|
|
{
|
|
unit.mJointIndices[j] = remapped_joint;
|
|
unit.mJointWeights[j] = llclamp(weight_values[j], 0.f, 0.999f);
|
|
}
|
|
}
|
|
|
|
if (vertex_dropped_joint)
|
|
{
|
|
++affected_vertices;
|
|
}
|
|
|
|
list_skin.emplace_back(unit);
|
|
}
|
|
|
|
if (dropped_weighted_influences > 0)
|
|
{
|
|
std::ostringstream joint_stream;
|
|
bool first_joint = true;
|
|
for (const auto& joint_name : dropped_joint_names)
|
|
{
|
|
if (!first_joint)
|
|
{
|
|
joint_stream << ", ";
|
|
}
|
|
joint_stream << joint_name;
|
|
first_joint = false;
|
|
}
|
|
|
|
std::string warning = "LOD" + std::to_string(mLod)
|
|
+ " object \"" + object->getObjectName()
|
|
+ "\" dropped " + std::to_string(dropped_weighted_influences)
|
|
+ " weighted joint influence(s) across " + std::to_string(affected_vertices)
|
|
+ " vertex/vertices while remapping to the high LOD skin";
|
|
|
|
if (!dropped_joint_names.empty())
|
|
{
|
|
warning += " [" + joint_stream.str() + "]";
|
|
}
|
|
|
|
pushLog("GLTF Importer", "WARNING: " + warning);
|
|
LL_WARNS("LocalMesh") << warning << LL_ENDL;
|
|
}
|
|
}
|
|
else if (skin_idx >= 0)
|
|
{
|
|
pushLog("GLTF Importer", "Skinning data missing or mismatched for primitive, skipping weights.");
|
|
}
|
|
|
|
object->getFaces(mLod).push_back(std::move(current_submesh));
|
|
return true;
|
|
}
|
|
|
|
bool FSLocalMeshImportGLTF::initSkinInfo(const LL::GLTF::Asset& asset, S32 skin_idx, LLLocalMeshObject* object)
|
|
{
|
|
if (!object || skin_idx < 0 || skin_idx >= static_cast<S32>(asset.mSkins.size()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
LLPointer<LLMeshSkinInfo> skininfop = object->getObjectMeshSkinInfo();
|
|
if (skininfop == nullptr)
|
|
{
|
|
LL_DEBUGS("LocalMesh") << "Object mesh skin info is nullptr. allocate a new skininfo." << LL_ENDL;
|
|
try
|
|
{
|
|
skininfop = new LLMeshSkinInfo();
|
|
}
|
|
catch (const std::bad_alloc& ex)
|
|
{
|
|
LL_WARNS() << "Failed to allocate skin info with exception: " << ex.what() << LL_ENDL;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
auto joint_map = FSLocalMeshImportBase::loadJointMap();
|
|
const LL::GLTF::Skin& skin = asset.mSkins[skin_idx];
|
|
const bool apply_xy_rotation = checkForXYrotation(asset, skin, mParentMap, joint_map);
|
|
U32 recognized_joint_count = 0;
|
|
const std::vector<bool> is_skin_joint = buildSkinJointMembership(asset, skin);
|
|
for (S32 joint_node_idx : skin.mJoints)
|
|
{
|
|
if (joint_node_idx >= 0
|
|
&& joint_node_idx < static_cast<S32>(asset.mNodes.size())
|
|
&& joint_map.find(asset.mNodes[joint_node_idx].mName) != joint_map.end())
|
|
{
|
|
++recognized_joint_count;
|
|
}
|
|
}
|
|
|
|
joints_data_map_t joints_data;
|
|
joints_name_to_node_map_t names_to_nodes;
|
|
std::vector<LLJointData> viewer_skeleton;
|
|
if (gAgentAvatarp)
|
|
{
|
|
gAgentAvatarp->getJointMatricesAndHierarhy(viewer_skeleton);
|
|
}
|
|
|
|
const bool can_build_overrides = !viewer_skeleton.empty();
|
|
if (can_build_overrides)
|
|
{
|
|
for (size_t i = 0; i < skin.mJoints.size(); ++i)
|
|
{
|
|
S32 joint_node_idx = skin.mJoints[i];
|
|
if (joint_node_idx < 0 || joint_node_idx >= static_cast<S32>(asset.mNodes.size()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const LL::GLTF::Node& joint_node = asset.mNodes[joint_node_idx];
|
|
JointNodeData& data = joints_data[joint_node_idx];
|
|
data.mNodeIdx = joint_node_idx;
|
|
data.mGltfRestMatrix = buildGltfRestMatrix(asset, mParentMap, is_skin_joint, joint_node_idx);
|
|
data.mGltfMatrix = joint_node.mMatrix;
|
|
data.mOverrideMatrix = glm::mat4(1.f);
|
|
|
|
auto name_it = joint_map.find(joint_node.mName);
|
|
if (name_it != joint_map.end())
|
|
{
|
|
data.mName = name_it->second;
|
|
data.mIsValidViewerJoint = true;
|
|
}
|
|
else
|
|
{
|
|
data.mName = joint_node.mName;
|
|
data.mIsValidViewerJoint = false;
|
|
}
|
|
|
|
names_to_nodes[data.mName] = joint_node_idx;
|
|
|
|
for (S32 child_idx : joint_node.mChildren)
|
|
{
|
|
JointNodeData& child_data = joints_data[child_idx];
|
|
child_data.mParentNodeIdx = joint_node_idx;
|
|
child_data.mIsParentValidViewerJoint = data.mIsValidViewerJoint;
|
|
}
|
|
}
|
|
|
|
glm::mat4 identity(1.0f);
|
|
for (const LLJointData& viewer_data : viewer_skeleton)
|
|
{
|
|
buildOverrideMatrix(viewer_data, joints_data, names_to_nodes, identity, identity, apply_xy_rotation);
|
|
}
|
|
}
|
|
|
|
if (!enforceRigJointLimit("GLTF Importer", *object, skininfop, recognized_joint_count))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Always reset bind shape before rebuilding skin data so reloads do not reuse stale matrices.
|
|
skininfop->mBindShapeMatrix = LLMatrix4a::identity();
|
|
skininfop->mBindPoseMatrix.clear();
|
|
|
|
skininfop->mJointNames.clear();
|
|
skininfop->mJointNums.clear();
|
|
skininfop->mInvBindMatrix.clear();
|
|
skininfop->mAlternateBindMatrix.clear();
|
|
skininfop->mInvalidJointsScrubbed = false;
|
|
skininfop->mJointNumsInitialized = false;
|
|
|
|
static LLCachedControl<bool> apply_joint_offsets(gSavedSettings, "FSLocalMeshApplyJointOffsets");
|
|
|
|
for (size_t i = 0; i < skin.mJoints.size(); ++i)
|
|
{
|
|
S32 joint_node_idx = skin.mJoints[i];
|
|
std::string joint_name;
|
|
if (joint_node_idx >= 0 && joint_node_idx < static_cast<S32>(asset.mNodes.size()))
|
|
{
|
|
joint_name = asset.mNodes[joint_node_idx].mName;
|
|
}
|
|
|
|
joint_name = normalizeJointName(joint_map, joint_name);
|
|
|
|
skininfop->mJointNames.push_back(joint_name);
|
|
skininfop->mJointNums.push_back(-1);
|
|
|
|
glm::mat4 original_bind = glm::mat4(1.0f);
|
|
if (i < skin.mInverseBindMatricesData.size())
|
|
{
|
|
original_bind = glm::inverse(skin.mInverseBindMatricesData[i]);
|
|
}
|
|
|
|
glm::mat4 rotated_bind = convertTransformToViewerBasis(original_bind, apply_xy_rotation);
|
|
glm::mat4 skeleton_transform = glm::mat4(1.0f);
|
|
if (can_build_overrides)
|
|
{
|
|
skeleton_transform = computeGltfToViewerSkeletonTransform(joints_data, joint_node_idx, apply_xy_rotation);
|
|
}
|
|
|
|
glm::mat4 translated_bind = skeleton_transform * rotated_bind;
|
|
glm::mat4 final_inverse_bind = glm::inverse(translated_bind);
|
|
|
|
LLMatrix4 inv_bind_ll(glm::value_ptr(final_inverse_bind));
|
|
skininfop->mInvBindMatrix.push_back(LLMatrix4a(inv_bind_ll));
|
|
|
|
if (apply_joint_offsets && can_build_overrides)
|
|
{
|
|
LLMatrix4 alternate_bind(inv_bind_ll);
|
|
auto joint_it = joints_data.find(joint_node_idx);
|
|
if (joint_it != joints_data.end())
|
|
{
|
|
LLMatrix4 override_transform(glm::value_ptr(joint_it->second.mOverrideMatrix));
|
|
alternate_bind.setTranslation(override_transform.getTranslation());
|
|
}
|
|
skininfop->mAlternateBindMatrix.push_back(LLMatrix4a(alternate_bind));
|
|
}
|
|
}
|
|
|
|
object->setObjectMeshSkinInfo(skininfop);
|
|
return true;
|
|
}
|
|
|
|
void FSLocalMeshImportGLTF::finalizeSkinInfo(LLLocalMeshObject* object) const
|
|
{
|
|
if (!object)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LLPointer<LLMeshSkinInfo> skininfop = object->getObjectMeshSkinInfo();
|
|
if (skininfop.isNull() || skininfop->mInvBindMatrix.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
LLMatrix4 normalized_transformation = FSLocalMeshImportBase::buildNormalizedTransformation(*object);
|
|
bool bind_shape_finite = true;
|
|
const F32* bind_shape_ptr = skininfop->mBindShapeMatrix.getF32ptr();
|
|
for (int i = 0; i < 16; ++i)
|
|
{
|
|
if (!llfinite(bind_shape_ptr[i]))
|
|
{
|
|
bind_shape_finite = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bind_shape_finite)
|
|
{
|
|
skininfop->mBindShapeMatrix = LLMatrix4a::identity();
|
|
}
|
|
|
|
// Local mesh vertices are normalized into object space before upload; bind shape
|
|
// must always carry the inverse normalization back into skin space.
|
|
LLMatrix4a transform{normalized_transformation};
|
|
matMul(transform, skininfop->mBindShapeMatrix, skininfop->mBindShapeMatrix);
|
|
|
|
FSLocalMeshImportBase::buildBindPoseMatrix(skininfop);
|
|
|
|
skininfop->updateHash();
|
|
object->setObjectMeshSkinInfo(skininfop);
|
|
}
|