Merge branch 'project/gltf_mesh_import' into geenz/develop-to-gltf-mesh

master
Jonathan "Geenz" Goodman 2025-06-27 17:18:00 -04:00
commit 6dd8a02ed1
26 changed files with 2507 additions and 736 deletions

View File

@ -218,8 +218,10 @@ jobs:
prefix=${ba[0]}
if [ "$prefix" == "project" ]; then
IFS='_' read -ra prj <<< "${ba[1]}"
prj_str="${prj[*]}"
# uppercase first letter of each word
export viewer_channel="Second Life Project ${prj[*]^}"
capitalized=$(echo "$prj_str" | awk '{for (i=1; i<=NF; i++) $i = toupper(substr($i,1,1)) substr($i,2); print}')
export viewer_channel="Second Life Project $capitalized"
elif [[ "$prefix" == "release" || "$prefix" == "main" ]];
then
export viewer_channel="Second Life Release"
@ -455,7 +457,6 @@ jobs:
prerelease: true
generate_release_notes: true
target_commitish: ${{ github.sha }}
previous_tag: release
append_body: true
fail_on_unmatched_files: true
files: |

View File

@ -14,6 +14,7 @@ set(llappearance_SOURCE_FILES
llavatarjoint.cpp
llavatarjointmesh.cpp
lldriverparam.cpp
lljointdata.h
lllocaltextureobject.cpp
llpolyskeletaldistortion.cpp
llpolymesh.cpp

View File

@ -29,16 +29,17 @@
#include "llavatarappearance.h"
#include "llavatarappearancedefines.h"
#include "llavatarjointmesh.h"
#include "lljointdata.h"
#include "llstl.h"
#include "lldir.h"
#include "llpolymorph.h"
#include "llpolymesh.h"
#include "llpolyskeletaldistortion.h"
#include "llstl.h"
#include "lltexglobalcolor.h"
#include "llwearabledata.h"
#include "boost/bind.hpp"
#include "boost/tokenizer.hpp"
#include "v4math.h"
using namespace LLAvatarAppearanceDefines;
@ -71,11 +72,13 @@ public:
mChildren.clear();
}
bool parseXml(LLXmlTreeNode* node);
glm::mat4 getJointMatrix();
private:
std::string mName;
std::string mSupport;
std::string mAliases;
std::string mGroup;
bool mIsJoint;
LLVector3 mPos;
LLVector3 mEnd;
@ -105,11 +108,17 @@ public:
S32 getNumBones() const { return mNumBones; }
S32 getNumCollisionVolumes() const { return mNumCollisionVolumes; }
private:
typedef std::vector<LLAvatarBoneInfo*> bone_info_list_t;
static void getJointMatricesAndHierarhy(
LLAvatarBoneInfo* bone_info,
LLJointData& data,
const glm::mat4& parent_mat);
private:
S32 mNumBones;
S32 mNumCollisionVolumes;
LLAvatarAppearance::joint_alias_map_t mJointAliasMap;
typedef std::vector<LLAvatarBoneInfo*> bone_info_list_t;
bone_info_list_t mBoneInfoList;
};
@ -1598,6 +1607,15 @@ bool LLAvatarBoneInfo::parseXml(LLXmlTreeNode* node)
mSupport = "base";
}
// Skeleton has 133 bones, but shader only allows 110 (LL_MAX_JOINTS_PER_MESH_OBJECT)
// Groups can be used by importer to cut out unused groups of joints
static LLStdStringHandle group_string = LLXmlTree::addAttributeString("group");
if (!node->getFastAttributeString(group_string, mGroup))
{
LL_WARNS() << "Bone without group " << mName << LL_ENDL;
mGroup = "global";
}
if (mIsJoint)
{
static LLStdStringHandle pivot_string = LLXmlTree::addAttributeString("pivot");
@ -1623,6 +1641,21 @@ bool LLAvatarBoneInfo::parseXml(LLXmlTreeNode* node)
return true;
}
glm::mat4 LLAvatarBoneInfo::getJointMatrix()
{
glm::mat4 mat(1.0f);
// 1. Scaling
mat = glm::scale(mat, glm::vec3(mScale[0], mScale[1], mScale[2]));
// 2. Rotation (Euler angles rad)
mat = glm::rotate(mat, mRot[0], glm::vec3(1, 0, 0));
mat = glm::rotate(mat, mRot[1], glm::vec3(0, 1, 0));
mat = glm::rotate(mat, mRot[2], glm::vec3(0, 0, 1));
// 3. Position
mat = glm::translate(mat, glm::vec3(mPos[0], mPos[1], mPos[2]));
return mat;
}
//-----------------------------------------------------------------------------
// LLAvatarSkeletonInfo::parseXml()
//-----------------------------------------------------------------------------
@ -1653,6 +1686,25 @@ bool LLAvatarSkeletonInfo::parseXml(LLXmlTreeNode* node)
return true;
}
void LLAvatarSkeletonInfo::getJointMatricesAndHierarhy(
LLAvatarBoneInfo* bone_info,
LLJointData& data,
const glm::mat4& parent_mat)
{
data.mName = bone_info->mName;
data.mJointMatrix = bone_info->getJointMatrix();
data.mScale = glm::vec3(bone_info->mScale[0], bone_info->mScale[1], bone_info->mScale[2]);
data.mRotation = bone_info->mRot;
data.mRestMatrix = parent_mat * data.mJointMatrix;
data.mIsJoint = bone_info->mIsJoint;
data.mGroup = bone_info->mGroup;
for (LLAvatarBoneInfo* child_info : bone_info->mChildren)
{
LLJointData& child_data = data.mChildren.emplace_back();
getJointMatricesAndHierarhy(child_info, child_data, data.mRestMatrix);
}
}
//Make aliases for joint and push to map.
void LLAvatarAppearance::makeJointAliases(LLAvatarBoneInfo *bone_info)
{
@ -1714,6 +1766,16 @@ const LLAvatarAppearance::joint_alias_map_t& LLAvatarAppearance::getJointAliases
return mJointAliasMap;
}
void LLAvatarAppearance::getJointMatricesAndHierarhy(std::vector<LLJointData> &data) const
{
glm::mat4 identity(1.f);
for (LLAvatarBoneInfo* bone_info : sAvatarSkeletonInfo->mBoneInfoList)
{
LLJointData& child_data = data.emplace_back();
LLAvatarSkeletonInfo::getJointMatricesAndHierarhy(bone_info, child_data, identity);
}
}
//-----------------------------------------------------------------------------
// parseXmlSkeletonNode(): parses <skeleton> nodes from XML tree

View File

@ -34,6 +34,7 @@
#include "lltexlayer.h"
#include "llviewervisualparam.h"
#include "llxmltree.h"
#include "v4math.h"
class LLTexLayerSet;
class LLTexGlobalColor;
@ -41,6 +42,7 @@ class LLTexGlobalColorInfo;
class LLWearableData;
class LLAvatarBoneInfo;
class LLAvatarSkeletonInfo;
class LLJointData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// LLAvatarAppearance
@ -153,7 +155,9 @@ public:
const avatar_joint_list_t& getSkeleton() { return mSkeleton; }
typedef std::map<std::string, std::string, std::less<>> joint_alias_map_t;
const joint_alias_map_t& getJointAliases();
typedef std::map<std::string, std::string> joint_parent_map_t; // matrix plus parent
typedef std::map<std::string, glm::mat4> joint_rest_map_t;
void getJointMatricesAndHierarhy(std::vector<LLJointData> &data) const;
protected:
static bool parseSkeletonFile(const std::string& filename, LLXmlTree& skeleton_xml_tree);

View File

@ -0,0 +1,66 @@
/**
* @file lljointdata.h
* @brief LLJointData class for holding individual joint data and skeleton
*
* $LicenseInfo:firstyear=2025&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2025, Linden Research, Inc.
*
* 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
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#ifndef LL_LLJOINTDATA_H
#define LL_LLJOINTDATA_H
#include "v4math.h"
// may be just move LLAvatarBoneInfo
class LLJointData
{
public:
std::string mName;
std::string mGroup;
glm::mat4 mJointMatrix;
glm::mat4 mRestMatrix;
glm::vec3 mScale;
LLVector3 mRotation;
typedef std::vector<LLJointData> bones_t;
bones_t mChildren;
bool mIsJoint; // if not, collision_volume
enum SupportCategory
{
SUPPORT_BASE,
SUPPORT_EXTENDED
};
SupportCategory mSupport;
void setSupport(const std::string& support)
{
if (support == "extended")
{
mSupport = SUPPORT_EXTENDED;
}
else
{
mSupport = SUPPORT_BASE;
}
}
};
#endif //LL_LLJOINTDATA_H

View File

@ -12,7 +12,6 @@ include(TinyGLTF)
set(llprimitive_SOURCE_FILES
lldaeloader.cpp
llgltfloader.cpp
llgltfmaterial.cpp
llmaterialid.cpp
llmaterial.cpp
@ -32,7 +31,6 @@ set(llprimitive_SOURCE_FILES
set(llprimitive_HEADER_FILES
CMakeLists.txt
lldaeloader.h
llgltfloader.h
llgltfmaterial.h
llgltfmaterial_templates.h
legacy_object_types.h

View File

@ -1,404 +0,0 @@
/**
* @file LLGLTFLoader.cpp
* @brief LLGLTFLoader class implementation
*
* $LicenseInfo:firstyear=2022&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2022, Linden Research, Inc.
*
* 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
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#include "llgltfloader.h"
// Import & define single-header gltf import/export lib
#define TINYGLTF_IMPLEMENTATION
#define TINYGLTF_USE_CPP14 // default is C++ 11
// tinygltf by default loads image files using STB
#define STB_IMAGE_IMPLEMENTATION
// to use our own image loading:
// 1. replace this definition with TINYGLTF_NO_STB_IMAGE
// 2. provide image loader callback with TinyGLTF::SetImageLoader(LoadimageDataFunction LoadImageData, void *user_data)
// tinygltf saves image files using STB
#define STB_IMAGE_WRITE_IMPLEMENTATION
// similarly, can override with TINYGLTF_NO_STB_IMAGE_WRITE and TinyGLTF::SetImageWriter(fxn, data)
// Additionally, disable inclusion of STB header files entirely with
// TINYGLTF_NO_INCLUDE_STB_IMAGE
// TINYGLTF_NO_INCLUDE_STB_IMAGE_WRITE
#include "tinygltf/tiny_gltf.h"
// TODO: includes inherited from dae loader. Validate / prune
#include "llsdserialize.h"
#include "lljoint.h"
#include "llmatrix4a.h"
#include <boost/regex.hpp>
#include <boost/algorithm/string/replace.hpp>
static const std::string lod_suffix[LLModel::NUM_LODS] =
{
"_LOD0",
"_LOD1",
"_LOD2",
"",
"_PHYS",
};
LLGLTFLoader::LLGLTFLoader(std::string filename,
S32 lod,
LLModelLoader::load_callback_t load_cb,
LLModelLoader::joint_lookup_func_t joint_lookup_func,
LLModelLoader::texture_load_func_t texture_load_func,
LLModelLoader::state_callback_t state_cb,
void * opaque_userdata,
JointTransformMap & jointTransformMap,
JointNameSet & jointsFromNodes,
std::map<std::string, std::string, std::less<>> & jointAliasMap,
U32 maxJointsPerMesh,
U32 modelLimit) //,
//bool preprocess)
: LLModelLoader( filename,
lod,
load_cb,
joint_lookup_func,
texture_load_func,
state_cb,
opaque_userdata,
jointTransformMap,
jointsFromNodes,
jointAliasMap,
maxJointsPerMesh ),
//mPreprocessGLTF(preprocess),
mMeshesLoaded(false),
mMaterialsLoaded(false)
{
}
LLGLTFLoader::~LLGLTFLoader() {}
bool LLGLTFLoader::OpenFile(const std::string &filename)
{
tinygltf::TinyGLTF loader;
std::string error_msg;
std::string warn_msg;
std::string filename_lc(filename);
LLStringUtil::toLower(filename_lc);
// Load a tinygltf model fom a file. Assumes that the input filename has already been
// been sanitized to one of (.gltf , .glb) extensions, so does a simple find to distinguish.
if (std::string::npos == filename_lc.rfind(".gltf"))
{ // file is binary
mGltfLoaded = loader.LoadBinaryFromFile(&mGltfModel, &error_msg, &warn_msg, filename);
}
else
{ // file is ascii
mGltfLoaded = loader.LoadASCIIFromFile(&mGltfModel, &error_msg, &warn_msg, filename);
}
if (!mGltfLoaded)
{
if (!warn_msg.empty())
LL_WARNS("GLTF_IMPORT") << "gltf load warning: " << warn_msg.c_str() << LL_ENDL;
if (!error_msg.empty())
LL_WARNS("GLTF_IMPORT") << "gltf load error: " << error_msg.c_str() << LL_ENDL;
return false;
}
mMeshesLoaded = parseMeshes();
if (mMeshesLoaded) uploadMeshes();
mMaterialsLoaded = parseMaterials();
if (mMaterialsLoaded) uploadMaterials();
return (mMeshesLoaded || mMaterialsLoaded);
}
bool LLGLTFLoader::parseMeshes()
{
if (!mGltfLoaded) return false;
// 2022-04 DJH Volume params from dae example. TODO understand PCODE
LLVolumeParams volume_params;
volume_params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE);
for (tinygltf::Mesh mesh : mGltfModel.meshes)
{
LLModel *pModel = new LLModel(volume_params, 0.f);
if (populateModelFromMesh(pModel, mesh) &&
(LLModel::NO_ERRORS == pModel->getStatus()) &&
validate_model(pModel))
{
mModelList.push_back(pModel);
}
else
{
setLoadState(ERROR_MODEL + pModel->getStatus());
delete(pModel);
return false;
}
}
return true;
}
bool LLGLTFLoader::populateModelFromMesh(LLModel* pModel, const tinygltf::Mesh &mesh)
{
pModel->mLabel = mesh.name;
int pos_idx;
tinygltf::Accessor indices_a, positions_a, normals_a, uv0_a, color0_a;
auto prims = mesh.primitives;
for (auto prim : prims)
{
if (prim.indices >= 0) indices_a = mGltfModel.accessors[prim.indices];
pos_idx = (prim.attributes.count("POSITION") > 0) ? prim.attributes.at("POSITION") : -1;
if (pos_idx >= 0)
{
positions_a = mGltfModel.accessors[pos_idx];
if (TINYGLTF_COMPONENT_TYPE_FLOAT != positions_a.componentType)
continue;
auto positions_bv = mGltfModel.bufferViews[positions_a.bufferView];
auto positions_buf = mGltfModel.buffers[positions_bv.buffer];
//auto type = positions_vb.
//if (positions_buf.name
}
#if 0
int norm_idx, tan_idx, uv0_idx, uv1_idx, color0_idx, color1_idx;
norm_idx = (prim.attributes.count("NORMAL") > 0) ? prim.attributes.at("NORMAL") : -1;
tan_idx = (prim.attributes.count("TANGENT") > 0) ? prim.attributes.at("TANGENT") : -1;
uv0_idx = (prim.attributes.count("TEXCOORDS_0") > 0) ? prim.attributes.at("TEXCOORDS_0") : -1;
uv1_idx = (prim.attributes.count("TEXCOORDS_1") > 0) ? prim.attributes.at("TEXCOORDS_1") : -1;
color0_idx = (prim.attributes.count("COLOR_0") > 0) ? prim.attributes.at("COLOR_0") : -1;
color1_idx = (prim.attributes.count("COLOR_1") > 0) ? prim.attributes.at("COLOR_1") : -1;
#endif
if (prim.mode == TINYGLTF_MODE_TRIANGLES)
{
//auto pos = mesh. TODO resume here DJH 2022-04
}
}
//pModel->addFace()
return false;
}
bool LLGLTFLoader::parseMaterials()
{
if (!mGltfLoaded) return false;
// fill local texture data structures
mSamplers.clear();
for (auto in_sampler : mGltfModel.samplers)
{
gltf_sampler sampler;
sampler.magFilter = in_sampler.magFilter > 0 ? in_sampler.magFilter : GL_LINEAR;
sampler.minFilter = in_sampler.minFilter > 0 ? in_sampler.minFilter : GL_LINEAR;;
sampler.wrapS = in_sampler.wrapS;
sampler.wrapT = in_sampler.wrapT;
sampler.name = in_sampler.name; // unused
mSamplers.push_back(sampler);
}
mImages.clear();
for (auto in_image : mGltfModel.images)
{
gltf_image image;
image.numChannels = in_image.component;
image.bytesPerChannel = in_image.bits >> 3; // Convert bits to bytes
image.pixelType = in_image.pixel_type; // Maps exactly, i.e. TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE == GL_UNSIGNED_BYTE, etc
image.size = static_cast<U32>(in_image.image.size());
image.height = in_image.height;
image.width = in_image.width;
image.data = in_image.image.data();
if (in_image.as_is)
{
LL_WARNS("GLTF_IMPORT") << "Unsupported image encoding" << LL_ENDL;
return false;
}
if (image.size != image.height * image.width * image.numChannels * image.bytesPerChannel)
{
LL_WARNS("GLTF_IMPORT") << "Image size error" << LL_ENDL;
return false;
}
mImages.push_back(image);
}
mTextures.clear();
for (auto in_tex : mGltfModel.textures)
{
gltf_texture tex;
tex.imageIdx = in_tex.source;
tex.samplerIdx = in_tex.sampler;
tex.imageUuid.setNull();
if (tex.imageIdx >= mImages.size() || tex.samplerIdx >= mSamplers.size())
{
LL_WARNS("GLTF_IMPORT") << "Texture sampler/image index error" << LL_ENDL;
return false;
}
mTextures.push_back(tex);
}
// parse each material
for (tinygltf::Material gltf_material : mGltfModel.materials)
{
gltf_render_material mat;
mat.name = gltf_material.name;
tinygltf::PbrMetallicRoughness& pbr = gltf_material.pbrMetallicRoughness;
mat.hasPBR = true; // Always true, for now
mat.baseColor.set(pbr.baseColorFactor.data());
mat.hasBaseTex = pbr.baseColorTexture.index >= 0;
mat.baseColorTexIdx = pbr.baseColorTexture.index;
mat.baseColorTexCoords = pbr.baseColorTexture.texCoord;
mat.metalness = pbr.metallicFactor;
mat.roughness = pbr.roughnessFactor;
mat.hasMRTex = pbr.metallicRoughnessTexture.index >= 0;
mat.metalRoughTexIdx = pbr.metallicRoughnessTexture.index;
mat.metalRoughTexCoords = pbr.metallicRoughnessTexture.texCoord;
mat.normalScale = gltf_material.normalTexture.scale;
mat.hasNormalTex = gltf_material.normalTexture.index >= 0;
mat.normalTexIdx = gltf_material.normalTexture.index;
mat.normalTexCoords = gltf_material.normalTexture.texCoord;
mat.occlusionScale = gltf_material.occlusionTexture.strength;
mat.hasOcclusionTex = gltf_material.occlusionTexture.index >= 0;
mat.occlusionTexIdx = gltf_material.occlusionTexture.index;
mat.occlusionTexCoords = gltf_material.occlusionTexture.texCoord;
mat.emissiveColor.set(gltf_material.emissiveFactor.data());
mat.hasEmissiveTex = gltf_material.emissiveTexture.index >= 0;
mat.emissiveTexIdx = gltf_material.emissiveTexture.index;
mat.emissiveTexCoords = gltf_material.emissiveTexture.texCoord;
mat.alphaMode = gltf_material.alphaMode;
mat.alphaMask = gltf_material.alphaCutoff;
if ((mat.hasNormalTex && (mat.normalTexIdx >= mTextures.size())) ||
(mat.hasOcclusionTex && (mat.occlusionTexIdx >= mTextures.size())) ||
(mat.hasEmissiveTex && (mat.emissiveTexIdx >= mTextures.size())) ||
(mat.hasBaseTex && (mat.baseColorTexIdx >= mTextures.size())) ||
(mat.hasMRTex && (mat.metalRoughTexIdx >= mTextures.size())))
{
LL_WARNS("GLTF_IMPORT") << "Texture resource index error" << LL_ENDL;
return false;
}
if ((mat.hasNormalTex && (mat.normalTexCoords > 2)) || // mesh can have up to 3 sets of UV
(mat.hasOcclusionTex && (mat.occlusionTexCoords > 2)) ||
(mat.hasEmissiveTex && (mat.emissiveTexCoords > 2)) ||
(mat.hasBaseTex && (mat.baseColorTexCoords > 2)) ||
(mat.hasMRTex && (mat.metalRoughTexCoords > 2)))
{
LL_WARNS("GLTF_IMPORT") << "Image texcoord index error" << LL_ENDL;
return false;
}
mMaterials.push_back(mat);
}
return true;
}
// TODO: convert raw vertex buffers to UUIDs
void LLGLTFLoader::uploadMeshes()
{
llassert(0);
}
// convert raw image buffers to texture UUIDs & assemble into a render material
void LLGLTFLoader::uploadMaterials()
{
for (gltf_render_material mat : mMaterials) // Initially 1 material per gltf file, but design for multiple
{
if (mat.hasBaseTex)
{
gltf_texture& gtex = mTextures[mat.baseColorTexIdx];
if (gtex.imageUuid.isNull())
{
gtex.imageUuid = imageBufferToTextureUUID(gtex);
}
}
if (mat.hasMRTex)
{
gltf_texture& gtex = mTextures[mat.metalRoughTexIdx];
if (gtex.imageUuid.isNull())
{
gtex.imageUuid = imageBufferToTextureUUID(gtex);
}
}
if (mat.hasNormalTex)
{
gltf_texture& gtex = mTextures[mat.normalTexIdx];
if (gtex.imageUuid.isNull())
{
gtex.imageUuid = imageBufferToTextureUUID(gtex);
}
}
if (mat.hasOcclusionTex)
{
gltf_texture& gtex = mTextures[mat.occlusionTexIdx];
if (gtex.imageUuid.isNull())
{
gtex.imageUuid = imageBufferToTextureUUID(gtex);
}
}
if (mat.hasEmissiveTex)
{
gltf_texture& gtex = mTextures[mat.emissiveTexIdx];
if (gtex.imageUuid.isNull())
{
gtex.imageUuid = imageBufferToTextureUUID(gtex);
}
}
}
}
LLUUID LLGLTFLoader::imageBufferToTextureUUID(const gltf_texture& tex)
{
//gltf_image& image = mImages[tex.imageIdx];
//gltf_sampler& sampler = mSamplers[tex.samplerIdx];
// fill an LLSD container with image+sampler data
// upload texture
// retrieve UUID
return LLUUID::null;
}

View File

@ -1,206 +0,0 @@
/**
* @file LLGLTFLoader.h
* @brief LLGLTFLoader class definition
*
* $LicenseInfo:firstyear=2022&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2022, Linden Research, Inc.
*
* 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
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#ifndef LL_LLGLTFLoader_H
#define LL_LLGLTFLoader_H
#include "tinygltf/tiny_gltf.h"
#include "llglheaders.h"
#include "llmodelloader.h"
// gltf_* structs are temporary, used to organize the subset of data that eventually goes into the material LLSD
class gltf_sampler
{
public:
// Uses GL enums
S32 minFilter; // GL_NEAREST, GL_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR or GL_LINEAR_MIPMAP_LINEAR
S32 magFilter; // GL_NEAREST or GL_LINEAR
S32 wrapS; // GL_CLAMP_TO_EDGE, GL_MIRRORED_REPEAT or GL_REPEAT
S32 wrapT; // GL_CLAMP_TO_EDGE, GL_MIRRORED_REPEAT or GL_REPEAT
//S32 wrapR; // Found in some sample files, but not part of glTF 2.0 spec. Ignored.
std::string name; // optional, currently unused
// extensions and extras are sampler optional fields that we don't support - at least initially
};
class gltf_image
{
public:// Note that glTF images are defined with row 0 at the top (opposite of OpenGL)
U8* data; // ptr to decoded image data
U32 size; // in bytes, regardless of channel width
U32 width;
U32 height;
U32 numChannels; // range 1..4
U32 bytesPerChannel; // converted from gltf "bits", expects only 8, 16 or 32 as input
U32 pixelType; // one of (TINYGLTF_COMPONENT_TYPE)_UNSIGNED_BYTE, _UNSIGNED_SHORT, _UNSIGNED_INT, or _FLOAT
};
class gltf_texture
{
public:
U32 imageIdx;
U32 samplerIdx;
LLUUID imageUuid = LLUUID::null;
};
class gltf_render_material
{
public:
std::string name;
// scalar values
LLColor4 baseColor; // linear encoding. Multiplied with vertex color, if present.
double metalness;
double roughness;
double normalScale; // scale applies only to X,Y components of normal
double occlusionScale; // strength multiplier for occlusion
LLColor4 emissiveColor; // emissive mulitiplier, assumed linear encoding (spec 2.0 is silent)
std::string alphaMode; // "OPAQUE", "MASK" or "BLEND"
double alphaMask; // alpha cut-off
// textures
U32 baseColorTexIdx; // always sRGB encoded
U32 metalRoughTexIdx; // always linear, roughness in G channel, metalness in B channel
U32 normalTexIdx; // linear, valid range R[0-1], G[0-1], B[0.5-1]. Normal = texel * 2 - vec3(1.0)
U32 occlusionTexIdx; // linear, occlusion in R channel, 0 meaning fully occluded, 1 meaning not occluded
U32 emissiveTexIdx; // always stored as sRGB, in nits (candela / meter^2)
// texture coordinates
U32 baseColorTexCoords;
U32 metalRoughTexCoords;
U32 normalTexCoords;
U32 occlusionTexCoords;
U32 emissiveTexCoords;
// TODO: Add traditional (diffuse, normal, specular) UUIDs here, or add this struct to LL_TextureEntry??
bool hasPBR;
bool hasBaseTex, hasMRTex, hasNormalTex, hasOcclusionTex, hasEmissiveTex;
// This field is populated after upload
LLUUID material_uuid = LLUUID::null;
};
class gltf_mesh
{
public:
std::string name;
// TODO add mesh import DJH 2022-04
};
class LLGLTFLoader : public LLModelLoader
{
public:
typedef std::map<std::string, LLImportMaterial> material_map;
LLGLTFLoader(std::string filename,
S32 lod,
LLModelLoader::load_callback_t load_cb,
LLModelLoader::joint_lookup_func_t joint_lookup_func,
LLModelLoader::texture_load_func_t texture_load_func,
LLModelLoader::state_callback_t state_cb,
void * opaque_userdata,
JointTransformMap & jointTransformMap,
JointNameSet & jointsFromNodes,
std::map<std::string, std::string,std::less<>> &jointAliasMap,
U32 maxJointsPerMesh,
U32 modelLimit); //,
//bool preprocess );
virtual ~LLGLTFLoader();
virtual bool OpenFile(const std::string &filename);
protected:
tinygltf::Model mGltfModel;
bool mGltfLoaded;
bool mMeshesLoaded;
bool mMaterialsLoaded;
std::vector<gltf_mesh> mMeshes;
std::vector<gltf_render_material> mMaterials;
std::vector<gltf_texture> mTextures;
std::vector<gltf_image> mImages;
std::vector<gltf_sampler> mSamplers;
private:
bool parseMeshes();
void uploadMeshes();
bool parseMaterials();
void uploadMaterials();
bool populateModelFromMesh(LLModel* pModel, const tinygltf::Mesh &mesh);
LLUUID imageBufferToTextureUUID(const gltf_texture& tex);
// bool mPreprocessGLTF;
/* Below inherited from dae loader - unknown if/how useful here
void processElement(gltfElement *element, bool &badElement, GLTF *gltf);
void processGltfModel(LLModel *model, GLTF *gltf, gltfElement *pRoot, gltfMesh *mesh, gltfSkin *skin);
material_map getMaterials(LLModel *model, gltfInstance_geometry *instance_geo, GLTF *gltf);
LLImportMaterial profileToMaterial(gltfProfile_COMMON *material, GLTF *gltf);
LLColor4 getGltfColor(gltfElement *element);
gltfElement *getChildFromElement(gltfElement *pElement, std::string const &name);
bool isNodeAJoint(gltfNode *pNode);
void processJointNode(gltfNode *pNode, std::map<std::string, LLMatrix4> &jointTransforms);
void extractTranslation(gltfTranslate *pTranslate, LLMatrix4 &transform);
void extractTranslationViaElement(gltfElement *pTranslateElement, LLMatrix4 &transform);
void extractTranslationViaSID(gltfElement *pElement, LLMatrix4 &transform);
void buildJointToNodeMappingFromScene(gltfElement *pRoot);
void processJointToNodeMapping(gltfNode *pNode);
void processChildJoints(gltfNode *pParentNode);
bool verifyCount(int expected, int result);
// Verify that a controller matches vertex counts
bool verifyController(gltfController *pController);
static bool addVolumeFacesFromGltfMesh(LLModel *model, gltfMesh *mesh, LLSD &log_msg);
static bool createVolumeFacesFromGltfMesh(LLModel *model, gltfMesh *mesh);
static LLModel *loadModelFromGltfMesh(gltfMesh *mesh);
// Loads a mesh breaking it into one or more models as necessary
// to get around volume face limitations while retaining >8 materials
//
bool loadModelsFromGltfMesh(gltfMesh *mesh, std::vector<LLModel *> &models_out, U32 submodel_limit);
static std::string getElementLabel(gltfElement *element);
static size_t getSuffixPosition(std::string label);
static std::string getLodlessLabel(gltfElement *element);
static std::string preprocessGLTF(std::string filename);
*/
};
#endif // LL_LLGLTFLLOADER_H

View File

@ -334,6 +334,162 @@ void LLModel::normalizeVolumeFaces()
}
}
void LLModel::normalizeVolumeFacesAndWeights()
{
if (!mVolumeFaces.empty())
{
LLVector4a min, max;
// For all of the volume faces
// in the model, loop over
// them and see what the extents
// of the volume along each axis.
min = mVolumeFaces[0].mExtents[0];
max = mVolumeFaces[0].mExtents[1];
for (U32 i = 1; i < mVolumeFaces.size(); ++i)
{
LLVolumeFace& face = mVolumeFaces[i];
update_min_max(min, max, face.mExtents[0]);
update_min_max(min, max, face.mExtents[1]);
if (face.mTexCoords)
{
LLVector2& min_tc = face.mTexCoordExtents[0];
LLVector2& max_tc = face.mTexCoordExtents[1];
min_tc = face.mTexCoords[0];
max_tc = face.mTexCoords[0];
for (S32 j = 1; j < face.mNumVertices; ++j)
{
update_min_max(min_tc, max_tc, face.mTexCoords[j]);
}
}
else
{
face.mTexCoordExtents[0].set(0, 0);
face.mTexCoordExtents[1].set(1, 1);
}
}
// Now that we have the extents of the model
// we can compute the offset needed to center
// the model at the origin.
// Compute center of the model
// and make it negative to get translation
// needed to center at origin.
LLVector4a trans;
trans.setAdd(min, max);
trans.mul(-0.5f);
// Compute the total size along all
// axes of the model.
LLVector4a size;
size.setSub(max, min);
// Prevent division by zero.
F32 x = size[0];
F32 y = size[1];
F32 z = size[2];
F32 w = size[3];
if (fabs(x) < F_APPROXIMATELY_ZERO)
{
x = 1.0;
}
if (fabs(y) < F_APPROXIMATELY_ZERO)
{
y = 1.0;
}
if (fabs(z) < F_APPROXIMATELY_ZERO)
{
z = 1.0;
}
size.set(x, y, z, w);
// Compute scale as reciprocal of size
LLVector4a scale;
scale.splat(1.f);
scale.div(size);
LLVector4a inv_scale(1.f);
inv_scale.div(scale);
for (U32 i = 0; i < mVolumeFaces.size(); ++i)
{
LLVolumeFace& face = mVolumeFaces[i];
// We shrink the extents so
// that they fall within
// the unit cube.
// VFExtents change
face.mExtents[0].add(trans);
face.mExtents[0].mul(scale);
face.mExtents[1].add(trans);
face.mExtents[1].mul(scale);
// For all the positions, we scale
// the positions to fit within the unit cube.
LLVector4a* pos = (LLVector4a*)face.mPositions;
LLVector4a* norm = (LLVector4a*)face.mNormals;
LLVector4a* t = (LLVector4a*)face.mTangents;
for (S32 j = 0; j < face.mNumVertices; ++j)
{
pos[j].add(trans);
pos[j].mul(scale);
if (norm && !norm[j].equals3(LLVector4a::getZero()))
{
norm[j].mul(inv_scale);
norm[j].normalize3();
}
if (t)
{
F32 w = t[j].getF32ptr()[3];
t[j].mul(inv_scale);
t[j].normalize3();
t[j].getF32ptr()[3] = w;
}
}
}
weight_map old_weights = mSkinWeights;
mSkinWeights.clear();
mPosition.clear();
for (auto& weights : old_weights)
{
LLVector4a pos(weights.first.mV[VX], weights.first.mV[VY], weights.first.mV[VZ]);
pos.add(trans);
pos.mul(scale);
LLVector3 scaled_pos(pos.getF32ptr());
mPosition.push_back(scaled_pos);
mSkinWeights[scaled_pos] = weights.second;
}
// mNormalizedScale is the scale at which
// we would need to multiply the model
// by to get the original size of the
// model instead of the normalized size.
LLVector4a normalized_scale;
normalized_scale.splat(1.f);
normalized_scale.div(scale);
mNormalizedScale.set(normalized_scale.getF32ptr());
mNormalizedTranslation.set(trans.getF32ptr());
mNormalizedTranslation *= -1.f;
// remember normalized scale so original dimensions can be recovered for mesh processing (i.e. tangent generation)
for (auto& face : mVolumeFaces)
{
face.mNormalizedScale = mNormalizedScale;
}
}
}
void LLModel::getNormalizedScaleTranslation(LLVector3& scale_out, LLVector3& translation_out) const
{
scale_out = mNormalizedScale;
@ -1561,11 +1717,21 @@ LLSD LLMeshSkinInfo::asLLSD(bool include_joints, bool lock_scale_if_joint_positi
{
ret["joint_names"][i] = mJointNames[i];
// For model to work at all there must be a matching bind matrix,
// so supply an indentity one if it isn't true
// Note: can build an actual bind matrix from joints
const LLMatrix4a& inv_bind = mInvBindMatrix.size() > i ? mInvBindMatrix[i] : LLMatrix4a::identity();
if (i >= mInvBindMatrix.size())
{
LL_WARNS("MESHSKININFO") << "Joint index " << i << " (" << mJointNames[i] << ") exceeds inverse bind matrix size "
<< mInvBindMatrix.size() << LL_ENDL;
}
for (U32 j = 0; j < 4; j++)
{
for (U32 k = 0; k < 4; k++)
{
ret["inverse_bind_matrix"][i][j*4+k] = mInvBindMatrix[i].mMatrix[j][k];
ret["inverse_bind_matrix"][i][j * 4 + k] = inv_bind.mMatrix[j][k];
}
}
}
@ -1578,15 +1744,25 @@ LLSD LLMeshSkinInfo::asLLSD(bool include_joints, bool lock_scale_if_joint_positi
}
}
if ( include_joints && mAlternateBindMatrix.size() > 0 )
// optional 'joint overrides'
if (include_joints && mAlternateBindMatrix.size() > 0)
{
for (U32 i = 0; i < mJointNames.size(); ++i)
{
// If there is not enough to match mJointNames,
// either supply no alternate matrixes at all or supply
// replacements
const LLMatrix4a& alt_bind = mAlternateBindMatrix.size() > i ? mAlternateBindMatrix[i] : LLMatrix4a::identity();
if (i >= mAlternateBindMatrix.size())
{
LL_WARNS("MESHSKININFO") << "Joint index " << i << " (" << mJointNames[i] << ") exceeds alternate bind matrix size "
<< mAlternateBindMatrix.size() << LL_ENDL;
}
for (U32 j = 0; j < 4; j++)
{
for (U32 k = 0; k < 4; k++)
{
ret["alt_inverse_bind_matrix"][i][j*4+k] = mAlternateBindMatrix[i].mMatrix[j][k];
ret["alt_inverse_bind_matrix"][i][j * 4 + k] = alt_bind.mMatrix[j][k];
}
}
}

View File

@ -202,6 +202,7 @@ public:
void sortVolumeFacesByMaterialName();
void normalizeVolumeFaces();
void normalizeVolumeFacesAndWeights();
void trimVolumeFacesToSize(U32 new_count = LL_SCULPT_MESH_MAX_FACES, LLVolume::face_list_t* remainder = NULL);
void remapVolumeFaces();
void optimizeVolumeFaces();

View File

@ -150,6 +150,8 @@ void LLModelLoader::run()
{
mWarningsArray.clear();
doLoadModel();
// todo: we are inside of a thread, push this into main thread worker,
// not into doOnIdleOneTime that laks tread safety
doOnIdleOneTime(boost::bind(&LLModelLoader::loadModelCallback,this));
}
@ -466,6 +468,58 @@ bool LLModelLoader::isRigSuitableForJointPositionUpload( const std::vector<std::
return true;
}
void LLModelLoader::dumpDebugData()
{
std::string log_file = mFilename + "_importer.txt";
LLStringUtil::toLower(log_file);
llofstream file;
file.open(log_file.c_str());
if (!file)
{
LL_WARNS() << "dumpDebugData failed to open file " << log_file << LL_ENDL;
return;
}
file << "Importing: " << mFilename << "\n";
std::map<std::string, LLMatrix4a> inv_bind;
std::map<std::string, LLMatrix4a> alt_bind;
for (LLPointer<LLModel>& mdl : mModelList)
{
file << "Model name: " << mdl->mLabel << "\n";
const LLMeshSkinInfo& skin_info = mdl->mSkinInfo;
file << "Shape Bind matrix: " << skin_info.mBindShapeMatrix << "\n";
file << "Skin Weights count: " << (S32)mdl->mSkinWeights.size() << "\n";
// some objects might have individual bind matrices,
// but for now it isn't accounted for
size_t joint_count = skin_info.mJointNames.size();
for (size_t i = 0; i< joint_count;i++)
{
const std::string& joint = skin_info.mJointNames[i];
if (skin_info.mInvBindMatrix.size() > i)
{
inv_bind[joint] = skin_info.mInvBindMatrix[i];
}
if (skin_info.mAlternateBindMatrix.size() > i)
{
alt_bind[joint] = skin_info.mAlternateBindMatrix[i];
}
}
}
file << "Inv Bind matrices.\n";
for (auto& bind : inv_bind)
{
file << "Joint: " << bind.first << " Matrix: " << bind.second << "\n";
}
file << "Alt Bind matrices.\n";
for (auto& bind : alt_bind)
{
file << "Joint: " << bind.first << " Matrix: " << bind.second << "\n";
}
}
//called in the main thread
void LLModelLoader::loadTextures()

View File

@ -111,6 +111,7 @@ public:
bool mCacheOnlyHitIfRigged; // ignore cached SLM if it does not contain rig info (and we want rig info)
model_list mModelList;
// The scene is pretty much what ends up getting loaded for upload. Basically assign things to this guy if you want something uploaded.
scene mScene;
typedef std::queue<LLPointer<LLModel> > model_queue;
@ -119,9 +120,14 @@ public:
model_queue mPhysicsQ;
//map of avatar joints as named in COLLADA assets to internal joint names
// Do not use this for anything other than looking up the name of a joint. This is populated elsewhere.
JointMap mJointMap;
// The joint list is what you want to use to actually setup the specific joint transformations.
JointTransformMap& mJointList;
JointNameSet& mJointsFromNode;
U32 mMaxJointsPerMesh;
LLModelLoader(
@ -192,6 +198,7 @@ public:
const LLSD logOut() const { return mWarningsArray; }
void clearLog() { mWarningsArray.clear(); }
void dumpDebugData();
protected:

View File

@ -76,6 +76,7 @@ set(viewer_SOURCE_FILES
gltf/accessor.cpp
gltf/primitive.cpp
gltf/animation.cpp
gltf/llgltfloader.cpp
groupchatlistener.cpp
llaccountingcostmanager.cpp
llaisapi.cpp
@ -746,6 +747,7 @@ set(viewer_HEADER_FILES
gltf/buffer_util.h
gltf/primitive.h
gltf/animation.h
gltf/llgltfloader.h
llaccountingcost.h
llaccountingcostmanager.h
llaisapi.h

View File

@ -50,6 +50,10 @@ namespace LL
"KHR_texture_transform"
};
static std::unordered_set<std::string> ExtensionsIgnored = {
"KHR_materials_pbrSpecularGlossiness"
};
Material::AlphaMode gltf_alpha_mode_to_enum(const std::string& alpha_mode)
{
if (alpha_mode == "OPAQUE")
@ -472,11 +476,14 @@ void Asset::update()
for (auto& image : mImages)
{
if (image.mTexture.notNull())
{ // HACK - force texture to be loaded full rez
// TODO: calculate actual vsize
image.mTexture->addTextureStats(2048.f * 2048.f);
image.mTexture->setBoostLevel(LLViewerTexture::BOOST_HIGH);
if (image.mLoadIntoTexturePipe)
{
if (image.mTexture.notNull())
{ // HACK - force texture to be loaded full rez
// TODO: calculate actual vsize
image.mTexture->addTextureStats(2048.f * 2048.f);
image.mTexture->setBoostLevel(LLViewerTexture::BOOST_HIGH);
}
}
}
}
@ -486,18 +493,23 @@ void Asset::update()
bool Asset::prep()
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF;
// check required extensions and fail if not supported
bool unsupported = false;
// check required extensions
for (auto& extension : mExtensionsRequired)
{
if (ExtensionsSupported.find(extension) == ExtensionsSupported.end())
{
LL_WARNS() << "Unsupported extension: " << extension << LL_ENDL;
unsupported = true;
if (ExtensionsIgnored.find(extension) == ExtensionsIgnored.end())
{
LL_WARNS() << "Unsupported extension: " << extension << LL_ENDL;
mUnsupportedExtensions.push_back(extension);
}
else
{
mIgnoredExtensions.push_back(extension);
}
}
}
if (unsupported)
if (mUnsupportedExtensions.size() > 0)
{
return false;
}
@ -513,7 +525,7 @@ bool Asset::prep()
for (auto& image : mImages)
{
if (!image.prep(*this))
if (!image.prep(*this, mLoadIntoVRAM))
{
return false;
}
@ -542,102 +554,106 @@ bool Asset::prep()
return false;
}
}
// prepare vertex buffers
// material count is number of materials + 1 for default material
U32 mat_count = (U32) mMaterials.size() + 1;
if (LLGLSLShader::sCurBoundShaderPtr == nullptr)
{ // make sure a shader is bound to satisfy mVertexBuffer->setBuffer
gDebugProgram.bind();
}
for (S32 double_sided = 0; double_sided < 2; ++double_sided)
if (mLoadIntoVRAM)
{
RenderData& rd = mRenderData[double_sided];
for (U32 i = 0; i < LLGLSLShader::NUM_GLTF_VARIANTS; ++i)
{
rd.mBatches[i].resize(mat_count);
// prepare vertex buffers
// material count is number of materials + 1 for default material
U32 mat_count = (U32) mMaterials.size() + 1;
if (LLGLSLShader::sCurBoundShaderPtr == nullptr)
{ // make sure a shader is bound to satisfy mVertexBuffer->setBuffer
gDebugProgram.bind();
}
// for each material
for (S32 mat_id = -1; mat_id < (S32)mMaterials.size(); ++mat_id)
for (S32 double_sided = 0; double_sided < 2; ++double_sided)
{
// for each shader variant
U32 vertex_count[LLGLSLShader::NUM_GLTF_VARIANTS] = { 0 };
U32 index_count[LLGLSLShader::NUM_GLTF_VARIANTS] = { 0 };
S32 ds_mat = mat_id == -1 ? 0 : mMaterials[mat_id].mDoubleSided;
if (ds_mat != double_sided)
RenderData& rd = mRenderData[double_sided];
for (U32 i = 0; i < LLGLSLShader::NUM_GLTF_VARIANTS; ++i)
{
continue;
rd.mBatches[i].resize(mat_count);
}
for (U32 variant = 0; variant < LLGLSLShader::NUM_GLTF_VARIANTS; ++variant)
// for each material
for (S32 mat_id = -1; mat_id < (S32)mMaterials.size(); ++mat_id)
{
U32 attribute_mask = 0;
// for each mesh
for (auto& mesh : mMeshes)
// for each shader variant
U32 vertex_count[LLGLSLShader::NUM_GLTF_VARIANTS] = { 0 };
U32 index_count[LLGLSLShader::NUM_GLTF_VARIANTS] = { 0 };
S32 ds_mat = mat_id == -1 ? 0 : mMaterials[mat_id].mDoubleSided;
if (ds_mat != double_sided)
{
// for each primitive
for (auto& primitive : mesh.mPrimitives)
{
if (primitive.mMaterial == mat_id && primitive.mShaderVariant == variant)
{
// accumulate vertex and index counts
primitive.mVertexOffset = vertex_count[variant];
primitive.mIndexOffset = index_count[variant];
vertex_count[variant] += primitive.getVertexCount();
index_count[variant] += primitive.getIndexCount();
// all primitives of a given variant and material should all have the same attribute mask
llassert(attribute_mask == 0 || primitive.mAttributeMask == attribute_mask);
attribute_mask |= primitive.mAttributeMask;
}
}
continue;
}
// allocate vertex buffer and pack it
if (vertex_count[variant] > 0)
for (U32 variant = 0; variant < LLGLSLShader::NUM_GLTF_VARIANTS; ++variant)
{
U32 mat_idx = mat_id + 1;
LLVertexBuffer* vb = new LLVertexBuffer(attribute_mask);
rd.mBatches[variant][mat_idx].mVertexBuffer = vb;
vb->allocateBuffer(vertex_count[variant],
index_count[variant] * 2); // hack double index count... TODO: find a better way to indicate 32-bit indices will be used
vb->setBuffer();
U32 attribute_mask = 0;
// for each mesh
for (auto& mesh : mMeshes)
{
// for each primitive
for (auto& primitive : mesh.mPrimitives)
{
if (primitive.mMaterial == mat_id && primitive.mShaderVariant == variant)
{
primitive.upload(vb);
// accumulate vertex and index counts
primitive.mVertexOffset = vertex_count[variant];
primitive.mIndexOffset = index_count[variant];
vertex_count[variant] += primitive.getVertexCount();
index_count[variant] += primitive.getIndexCount();
// all primitives of a given variant and material should all have the same attribute mask
llassert(attribute_mask == 0 || primitive.mAttributeMask == attribute_mask);
attribute_mask |= primitive.mAttributeMask;
}
}
}
vb->unmapBuffer();
// allocate vertex buffer and pack it
if (vertex_count[variant] > 0)
{
U32 mat_idx = mat_id + 1;
#if 0
LLVertexBuffer* vb = new LLVertexBuffer(attribute_mask);
vb->unbind();
rd.mBatches[variant][mat_idx].mVertexBuffer = vb;
vb->allocateBuffer(vertex_count[variant],
index_count[variant] * 2); // hack double index count... TODO: find a better way to indicate 32-bit indices will be used
vb->setBuffer();
for (auto& mesh : mMeshes)
{
for (auto& primitive : mesh.mPrimitives)
{
if (primitive.mMaterial == mat_id && primitive.mShaderVariant == variant)
{
primitive.upload(vb);
}
}
}
vb->unmapBuffer();
vb->unbind();
#endif
}
}
}
}
}
// sanity check that all primitives have a vertex buffer
for (auto& mesh : mMeshes)
{
for (auto& primitive : mesh.mPrimitives)
// sanity check that all primitives have a vertex buffer
for (auto& mesh : mMeshes)
{
llassert(primitive.mVertexBuffer.notNull());
for (auto& primitive : mesh.mPrimitives)
{
//llassert(primitive.mVertexBuffer.notNull());
}
}
}
#if 0
// build render batches
for (S32 node_id = 0; node_id < mNodes.size(); ++node_id)
{
@ -664,6 +680,7 @@ bool Asset::prep()
}
}
}
#endif
return true;
}
@ -672,9 +689,10 @@ Asset::Asset(const Value& src)
*this = src;
}
bool Asset::load(std::string_view filename)
bool Asset::load(std::string_view filename, bool loadIntoVRAM)
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF;
mLoadIntoVRAM = loadIntoVRAM;
mFilename = filename;
std::string ext = gDirUtilp->getExtension(mFilename);
@ -692,7 +710,7 @@ bool Asset::load(std::string_view filename)
}
else if (ext == "glb")
{
return loadBinary(str);
return loadBinary(str, mLoadIntoVRAM);
}
else
{
@ -709,8 +727,9 @@ bool Asset::load(std::string_view filename)
return false;
}
bool Asset::loadBinary(const std::string& data)
bool Asset::loadBinary(const std::string& data, bool loadIntoVRAM)
{
mLoadIntoVRAM = loadIntoVRAM;
// load from binary gltf
const U8* ptr = (const U8*)data.data();
const U8* end = ptr + data.size();
@ -935,8 +954,9 @@ void Asset::eraseBufferView(S32 bufferView)
LLViewerFetchedTexture* fetch_texture(const LLUUID& id);
bool Image::prep(Asset& asset)
bool Image::prep(Asset& asset, bool loadIntoVRAM)
{
mLoadIntoTexturePipe = loadIntoVRAM;
LLUUID id;
if (mUri.size() == UUID_STR_SIZE && LLUUID::parseUUID(mUri, &id) && id.notNull())
{ // loaded from an asset, fetch the texture from the asset system
@ -951,12 +971,12 @@ bool Image::prep(Asset& asset)
{ // embedded in a buffer, load the texture from the buffer
BufferView& bufferView = asset.mBufferViews[mBufferView];
Buffer& buffer = asset.mBuffers[bufferView.mBuffer];
U8* data = buffer.mData.data() + bufferView.mByteOffset;
mTexture = LLViewerTextureManager::getFetchedTextureFromMemory(data, bufferView.mByteLength, mMimeType);
if (mTexture.isNull())
if (mLoadIntoTexturePipe)
{
U8* data = buffer.mData.data() + bufferView.mByteOffset;
mTexture = LLViewerTextureManager::getFetchedTextureFromMemory(data, bufferView.mByteLength, mMimeType);
}
else if (mTexture.isNull() && mLoadIntoTexturePipe)
{
LL_WARNS("GLTF") << "Failed to load image from buffer:" << LL_ENDL;
LL_WARNS("GLTF") << " image: " << mName << LL_ENDL;
@ -971,12 +991,12 @@ bool Image::prep(Asset& asset)
std::string img_file = dir + gDirUtilp->getDirDelimiter() + mUri;
LLUUID tracking_id = LLLocalBitmapMgr::getInstance()->addUnit(img_file);
if (tracking_id.notNull())
if (tracking_id.notNull() && mLoadIntoTexturePipe)
{
LLUUID world_id = LLLocalBitmapMgr::getInstance()->getWorldID(tracking_id);
mTexture = LLViewerTextureManager::getFetchedTexture(world_id);
}
else
else if (mLoadIntoTexturePipe)
{
LL_WARNS("GLTF") << "Failed to load image from file:" << LL_ENDL;
LL_WARNS("GLTF") << " image: " << mName << LL_ENDL;
@ -991,7 +1011,7 @@ bool Image::prep(Asset& asset)
return false;
}
if (!asset.mFilename.empty())
if (!asset.mFilename.empty() && mLoadIntoTexturePipe)
{ // local preview, boost image so it doesn't discard and force to save raw image in case we save out or upload
mTexture->setBoostLevel(LLViewerTexture::BOOST_PREVIEW);
mTexture->forceToSaveRawImage(0, F32_MAX);

View File

@ -286,6 +286,7 @@ namespace LL
void serialize(boost::json::object& dst) const;
};
// Image is for images that we want to load for the given asset. This acts as an interface into the viewer's texture pipe.
class Image
{
public:
@ -301,6 +302,8 @@ namespace LL
S32 mBits = -1;
S32 mPixelType = -1;
bool mLoadIntoTexturePipe = false;
LLPointer<LLViewerFetchedTexture> mTexture;
const Image& operator=(const Value& src);
@ -316,7 +319,7 @@ namespace LL
// preserve only uri and name
void clearData(Asset& asset);
bool prep(Asset& asset);
bool prep(Asset& asset, bool loadIntoVRAM);
};
// Render Batch -- vertex buffer and list of primitives to render using
@ -391,6 +394,10 @@ namespace LL
// UBO for storing material data
U32 mMaterialsUBO = 0;
bool mLoadIntoVRAM = false;
std::vector<std::string> mUnsupportedExtensions;
std::vector<std::string> mIgnoredExtensions;
// prepare for first time use
bool prep();
@ -428,12 +435,12 @@ namespace LL
// accepts .gltf and .glb files
// Any existing data will be lost
// returns result of prep() on success
bool load(std::string_view filename);
bool load(std::string_view filename, bool loadIntoVRAM);
// load .glb contents from memory
// data - binary contents of .glb file
// returns result of prep() on success
bool loadBinary(const std::string& data);
bool loadBinary(const std::string& data, bool loadIntoVRAM);
const Asset& operator=(const Value& src);
void serialize(boost::json::object& dst) const;

View File

@ -158,6 +158,12 @@ namespace LL
dst.load3(src);
}
template<>
inline void copyVec3<F32, LLColor4U>(F32* src, LLColor4U& dst)
{
dst.set((U8)(src[0] * 255.f), (U8)(src[1] * 255.f), (U8)(src[2] * 255.f), 255);
}
template<>
inline void copyVec3<U16, LLColor4U>(U16* src, LLColor4U& dst)
{
@ -369,6 +375,11 @@ namespace LL
template<class T>
inline void copy(Asset& asset, Accessor& accessor, LLStrider<T>& dst)
{
if (accessor.mBufferView == INVALID_INDEX)
{
LL_WARNS("GLTF") << "Invalid buffer" << LL_ENDL;
return;
}
const BufferView& bufferView = asset.mBufferViews[accessor.mBufferView];
const Buffer& buffer = asset.mBuffers[bufferView.mBuffer];
const U8* src = buffer.mData.data() + bufferView.mByteOffset + accessor.mByteOffset;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,203 @@
/**
* @file LLGLTFLoader.h
* @brief LLGLTFLoader class definition
*
* $LicenseInfo:firstyear=2022&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2022, Linden Research, Inc.
*
* 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
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#ifndef LL_LLGLTFLoader_H
#define LL_LLGLTFLoader_H
#include "tinygltf/tiny_gltf.h"
#include "asset.h"
#include "llglheaders.h"
#include "lljointdata.h"
#include "llmodelloader.h"
class LLGLTFLoader : public LLModelLoader
{
public:
typedef std::map<std::string, LLImportMaterial> material_map;
typedef std::map<std::string, std::string> joint_viewer_parent_map_t;
typedef std::map<std::string, glm::mat4> joint_viewer_rest_map_t;
typedef std::map<S32, glm::mat4> joint_node_mat4_map_t;
struct JointNodeData
{
JointNodeData()
: mJointListIdx(-1)
, mNodeIdx(-1)
, mParentNodeIdx(-1)
, mIsValidViewerJoint(false)
, mIsParentValidViewerJoint(false)
, mIsOverrideValid(false)
{
}
S32 mJointListIdx;
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;
};
typedef std::map <S32, JointNodeData> joints_data_map_t;
typedef std::map <std::string, S32> joints_name_to_node_map_t;
LLGLTFLoader(std::string filename,
S32 lod,
LLModelLoader::load_callback_t load_cb,
LLModelLoader::joint_lookup_func_t joint_lookup_func,
LLModelLoader::texture_load_func_t texture_load_func,
LLModelLoader::state_callback_t state_cb,
void * opaque_userdata,
JointTransformMap & jointTransformMap,
JointNameSet & jointsFromNodes,
std::map<std::string, std::string> &jointAliasMap,
U32 maxJointsPerMesh,
U32 modelLimit,
std::vector<LLJointData> viewer_skeleton); //,
//bool preprocess );
virtual ~LLGLTFLoader();
virtual bool OpenFile(const std::string &filename);
struct GLTFVertex
{
glm::vec3 position;
glm::vec3 normal;
glm::vec2 uv0;
glm::u16vec4 joints;
glm::vec4 weights;
};
protected:
LL::GLTF::Asset mGLTFAsset;
tinygltf::Model mGltfModel;
bool mGltfLoaded;
bool mApplyXYRotation = false;
U32 mGeneratedModelLimit;
// GLTF isn't aware of viewer's skeleton and uses it's own,
// so need to take viewer's joints and use them to
// recalculate iverse bind matrices
std::vector<LLJointData> mViewerJointData;
// vector of vectors because of a posibility of having more than one skin
typedef std::vector<LLMeshSkinInfo::matrix_list_t> bind_matrices_t;
typedef std::vector<std::vector<std::string> > joint_names_t;
bind_matrices_t mInverseBindMatrices;
bind_matrices_t mAlternateBindMatrices;
joint_names_t mJointNames; // empty string when no legal name for a given idx
// what group a joint belongs to.
// For purpose of stripping unused groups when joints are over limit.
struct JointGroups
{
std::string mGroup;
std::string mParentGroup;
};
typedef std::map<std::string, JointGroups> joint_to_group_map_t;
joint_to_group_map_t mJointGroups;
// per skin joint count, needs to be tracked for the sake of limits check.
std::vector<S32> mValidJointsCount;
private:
bool parseMeshes();
void computeCombinedNodeTransform(const LL::GLTF::Asset& asset, S32 node_index, glm::mat4& combined_transform) const;
void processNodeHierarchy(S32 node_idx, std::map<std::string, S32>& mesh_name_counts, U32 submodel_limit, const LLVolumeParams& volume_params);
bool addJointToModelSkin(LLMeshSkinInfo& skin_info, S32 gltf_skin_idx, size_t gltf_joint_idx) const;
bool populateModelFromMesh(LLModel* pModel, const LL::GLTF::Mesh &mesh, const LL::GLTF::Node &node, material_map& mats, S32 instance_count);
void populateJointsFromSkin(S32 skin_idx);
void populateJointGroups();
void addModelToScene(LLModel* pModel, U32 submodel_limit, const LLMatrix4& transformation, const LLVolumeParams& volume_params, const material_map& mats);
S32 findClosestValidJoint(S32 source_joint, const LL::GLTF::Skin& gltf_skin) const;
S32 findValidRootJointNode(S32 source_joint_node, const LL::GLTF::Skin& gltf_skin) const;
S32 findGLTFRootJointNode(const LL::GLTF::Skin& gltf_skin) const; // if there are multiple roots, gltf stores them under one commor joint
S32 findParentNode(S32 node) const;
void buildJointGroup(LLJointData& viewer_data, const std::string& parent_group);
void buildOverrideMatrix(LLJointData& data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest, glm::mat4& support_rest) const;
glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const LL::GLTF::Skin& gltf_skin) const;
glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const joints_data_map_t& joint_data) const;
glm::mat4 computeGltfToViewerSkeletonTransform(const joints_data_map_t& joints_data_map, S32 gltf_node_index, const std::string& joint_name) const;
bool checkForXYrotation(const LL::GLTF::Skin& gltf_skin, S32 joint_idx, S32 bind_indx);
void checkForXYrotation(const LL::GLTF::Skin& gltf_skin);
std::string extractTextureToTempFile(S32 textureIndex, const std::string& texture_type);
void notifyUnsupportedExtension(bool unsupported);
// bool mPreprocessGLTF;
/* Below inherited from dae loader - unknown if/how useful here
void processElement(gltfElement *element, bool &badElement, GLTF *gltf);
void processGltfModel(LLModel *model, GLTF *gltf, gltfElement *pRoot, gltfMesh *mesh, gltfSkin *skin);
material_map getMaterials(LLModel *model, gltfInstance_geometry *instance_geo, GLTF *gltf);
LLImportMaterial profileToMaterial(gltfProfile_COMMON *material, GLTF *gltf);
LLColor4 getGltfColor(gltfElement *element);
gltfElement *getChildFromElement(gltfElement *pElement, std::string const &name);
bool isNodeAJoint(gltfNode *pNode);
void processJointNode(gltfNode *pNode, std::map<std::string, LLMatrix4> &jointTransforms);
void extractTranslation(gltfTranslate *pTranslate, LLMatrix4 &transform);
void extractTranslationViaElement(gltfElement *pTranslateElement, LLMatrix4 &transform);
void extractTranslationViaSID(gltfElement *pElement, LLMatrix4 &transform);
void buildJointToNodeMappingFromScene(gltfElement *pRoot);
void processJointToNodeMapping(gltfNode *pNode);
void processChildJoints(gltfNode *pParentNode);
bool verifyCount(int expected, int result);
// Verify that a controller matches vertex counts
bool verifyController(gltfController *pController);
static bool addVolumeFacesFromGltfMesh(LLModel *model, gltfMesh *mesh, LLSD &log_msg);
static bool createVolumeFacesFromGltfMesh(LLModel *model, gltfMesh *mesh);
static LLModel *loadModelFromGltfMesh(gltfMesh *mesh);
// Loads a mesh breaking it into one or more models as necessary
// to get around volume face limitations while retaining >8 materials
//
bool loadModelsFromGltfMesh(gltfMesh *mesh, std::vector<LLModel *> &models_out, U32 submodel_limit);
static std::string getElementLabel(gltfElement *element);
static size_t getSuffixPosition(std::string label);
static std::string getLodlessLabel(gltfElement *element);
static std::string preprocessGLTF(std::string filename);
*/
};
#endif // LL_LLGLTFLLOADER_H

View File

@ -317,7 +317,7 @@ void GLTFSceneManager::load(const std::string& filename)
{
std::shared_ptr<Asset> asset = std::make_shared<Asset>();
if (asset->load(filename))
if (asset->load(filename, true))
{
gDebugProgram.bind(); // bind a shader to satisfy LLVertexBuffer assertions
asset->updateTransforms();

View File

@ -59,7 +59,7 @@ LLFilePicker LLFilePicker::sInstance;
#define XML_FILTER L"XML files (*.xml)\0*.xml\0"
#define SLOBJECT_FILTER L"Objects (*.slobject)\0*.slobject\0"
#define RAW_FILTER L"RAW files (*.raw)\0*.raw\0"
#define MODEL_FILTER L"Model files (*.dae)\0*.dae\0"
#define MODEL_FILTER L"Model files (*.dae, *.gltf, *.glb)\0*.dae;*.gltf;*.glb\0"
#define MATERIAL_FILTER L"GLTF Files (*.gltf; *.glb)\0*.gltf;*.glb\0"
#define HDRI_FILTER L"HDRI Files (*.exr)\0*.exr\0"
#define MATERIAL_TEXTURES_FILTER L"GLTF Import (*.gltf; *.glb; *.tga; *.bmp; *.jpg; *.jpeg; *.png)\0*.gltf;*.glb;*.tga;*.bmp;*.jpg;*.jpeg;*.png\0"
@ -217,6 +217,8 @@ bool LLFilePicker::setupFilter(ELoadFilter filter)
break;
case FFLOAD_MODEL:
mOFN.lpstrFilter = MODEL_FILTER \
COLLADA_FILTER \
MATERIAL_FILTER \
L"\0";
break;
case FFLOAD_MATERIAL:
@ -671,6 +673,8 @@ std::unique_ptr<std::vector<std::string>> LLFilePicker::navOpenFilterProc(ELoadF
case FFLOAD_HDRI:
allowedv->push_back("exr");
case FFLOAD_MODEL:
allowedv->push_back("gltf");
allowedv->push_back("glb");
case FFLOAD_COLLADA:
allowedv->push_back("dae");
break;

View File

@ -64,6 +64,7 @@
#include "llcallbacklist.h"
#include "llviewertexteditor.h"
#include "llviewernetwork.h"
#include "llmaterialeditor.h"
//static
@ -619,11 +620,9 @@ void LLFloaterModelPreview::onJointListSelection()
LLPanel *panel = mTabContainer->getPanelByName("rigging_panel");
LLScrollListCtrl *joints_list = panel->getChild<LLScrollListCtrl>("joints_list");
LLScrollListCtrl *joints_pos = panel->getChild<LLScrollListCtrl>("pos_overrides_list");
LLScrollListCtrl *joints_scale = panel->getChild<LLScrollListCtrl>("scale_overrides_list");
LLTextBox *joint_pos_descr = panel->getChild<LLTextBox>("pos_overrides_descr");
joints_pos->deleteAllItems();
joints_scale->deleteAllItems();
LLScrollListItem *selected = joints_list->getFirstSelected();
if (selected)

View File

@ -137,7 +137,8 @@ LLFloaterComboOptions* LLFloaterComboOptions::showUI(
{
combo_picker->mComboOptions->addSimpleElement(*iter);
}
combo_picker->mComboOptions->selectFirstItem();
// select 'Bulk Upload All' option
combo_picker->mComboOptions->selectNthItem((S32)options.size() - 1);
combo_picker->openFloater(LLSD(title));
combo_picker->setFocus(true);

View File

@ -30,7 +30,7 @@
#include "llmodelloader.h"
#include "lldaeloader.h"
#include "llgltfloader.h"
#include "gltf/llgltfloader.h"
#include "llfloatermodelpreview.h"
#include "llagent.h"
@ -40,6 +40,7 @@
#include "lldrawable.h"
#include "llface.h"
#include "lliconctrl.h"
#include "lljointdata.h"
#include "llmatrix4a.h"
#include "llmeshrepository.h"
#include "llmeshoptimizer.h"
@ -810,6 +811,9 @@ void LLModelPreview::loadModel(std::string filename, S32 lod, bool force_disable
}
else
{
LLVOAvatar* av = getPreviewAvatar();
std::vector<LLJointData> viewer_skeleton;
av->getJointMatricesAndHierarhy(viewer_skeleton);
mModelLoader = new LLGLTFLoader(
filename,
lod,
@ -822,7 +826,8 @@ void LLModelPreview::loadModel(std::string filename, S32 lod, bool force_disable
mJointsFromNode,
joint_alias_map,
LLSkinningUtil::getMaxJointCount(),
gSavedSettings.getU32("ImporterModelLimit"));
gSavedSettings.getU32("ImporterModelLimit"),
viewer_skeleton);
}
if (force_disable_slm)
@ -3090,25 +3095,48 @@ void LLModelPreview::lookupLODModelFiles(S32 lod)
S32 next_lod = (lod - 1 >= LLModel::LOD_IMPOSTOR) ? lod - 1 : LLModel::LOD_PHYSICS;
std::string lod_filename = mLODFile[LLModel::LOD_HIGH];
std::string ext = ".dae";
std::string lod_filename_lower(lod_filename);
LLStringUtil::toLower(lod_filename_lower);
std::string::size_type i = lod_filename_lower.rfind(ext);
if (i != std::string::npos)
// Check for each supported file extension
std::vector<std::string> supported_exts = { ".dae", ".gltf", ".glb" };
std::string found_ext;
std::string::size_type ext_pos = std::string::npos;
for (const auto& ext : supported_exts)
{
lod_filename.replace(i, lod_filename.size() - ext.size(), getLodSuffix(next_lod) + ext);
}
if (gDirUtilp->fileExists(lod_filename))
{
LLFloaterModelPreview* fmp = LLFloaterModelPreview::sInstance;
if (fmp)
std::string::size_type i = lod_filename_lower.rfind(ext);
if (i != std::string::npos)
{
fmp->setCtrlLoadFromFile(next_lod);
ext_pos = i;
found_ext = ext;
break;
}
}
if (ext_pos != std::string::npos)
{
// Replace extension with LOD suffix + original extension
std::string lod_file_to_check = lod_filename;
lod_file_to_check.replace(ext_pos, found_ext.size(), getLodSuffix(next_lod) + found_ext);
if (gDirUtilp->fileExists(lod_file_to_check))
{
LLFloaterModelPreview* fmp = LLFloaterModelPreview::sInstance;
if (fmp)
{
fmp->setCtrlLoadFromFile(next_lod);
}
loadModel(lod_file_to_check, next_lod);
}
else
{
lookupLODModelFiles(next_lod);
}
loadModel(lod_filename, next_lod);
}
else
{
// No recognized extension found, continue with next LOD
lookupLODModelFiles(next_lod);
}
}

View File

@ -135,6 +135,12 @@ void LLSkinningUtil::initSkinningMatrixPalette(
initJointNums(const_cast<LLMeshSkinInfo*>(skin), avatar);
if (skin->mInvBindMatrix.size() < count )
{
// faulty model? mInvBindMatrix.size() should have matched mJointNames.size()
return;
}
LLMatrix4a world[LL_CHARACTER_MAX_ANIMATED_JOINTS];
for (S32 j = 0; j < count; ++j)
@ -354,7 +360,8 @@ void LLSkinningUtil::updateRiggingInfo(const LLMeshSkinInfo* skin, LLVOAvatar *a
{
rig_info_tab[joint_num].setIsRiggedTo(true);
const LLMatrix4a& mat = skin->mBindPoseMatrix[joint_index];
size_t bind_poses_size = skin->mBindPoseMatrix.size();
const LLMatrix4a& mat = bind_poses_size > joint_index ? skin->mBindPoseMatrix[joint_index] : LLMatrix4a::identity();
LLVector4a pos_joint_space;
mat.affineTransform(pos, pos_joint_space);

View File

@ -14,7 +14,7 @@
legacy_header_height="25">
<string name="status_idle"></string>
<string name="status_parse_error">Error: Dae parsing issue - see log for details.</string>
<string name="status_parse_error">Error: Model parsing issue - see log for details.</string>
<string name="status_bind_shape_orientation">Warning: bind shape matrix is not in standard X-forward orientation.</string>
<string name="status_material_mismatch">Error: Material of model is not a subset of reference model.</string>
<string name="status_reading_file">Loading...</string>
@ -45,6 +45,7 @@
<string name="UnrecognizedJoint">Rigged to unrecognized joint name [NAME]</string>
<string name="UnknownJoints">Skinning disabled due to [COUNT] unknown joints</string>
<string name="ModelLoaded">Model [MODEL_NAME] loaded</string>
<string name="InvBindCountMismatch">Bind matrices count mismatch joints count</string>
<string name="IncompleteTC">Texture coordinates data is not complete.</string>
<string name="PositionNaN">Found NaN while loading position data from DAE-Model, invalid model.</string>
@ -61,6 +62,22 @@
<string name="ParsingErrorNoScene">Document has no visual_scene</string>
<string name="ParsingErrorPositionInvalidModel">Unable to process mesh without position data. Invalid model.</string>
<!-- GLTF specific messages -->
<string name="NoScenesFound">No scenes defined in GLTF file</string>
<string name="InvalidMeshReference">Node [NODE_NAME] references invalid mesh [MESH_INDEX] (total meshes: [TOTAL_MESHES])</string>
<string name="InvalidGeometryNonTriangulated">Mesh [MESH_NAME] primitive [PRIMITIVE_INDEX]: Invalid geometry with [INDEX_COUNT] indices (must be triangulated)</string>
<string name="EmptyVertexArray">Mesh [MESH_NAME] primitive [PRIMITIVE_INDEX]: Empty vertex array</string>
<string name="ErrorIndexLimit">Unable to process mesh [MESH_NAME] due to 65,534 vertex limit. Vertex count: [VERTEX_COUNT]</string>
<string name="TextureFound">Found texture: [TEXTURE_NAME] for material: [MATERIAL_NAME]</string>
<string name="IgnoredExtension">Model uses unsupported extension: [EXT], related material properties are ignored</string>
<string name="UnsupportedExtension">Unable to load model, unsupported extension: [EXT]</string>
<string name="TooManyMeshParts">Model contains [PART_COUNT] mesh parts. Maximum allowed: [LIMIT]</string>
<string name="FailedToCreateTempFile">Failed to create temporary file for embedded [TEXTURE_TYPE] texture [TEXTURE_INDEX]: [TEMP_FILE]</string>
<string name="SkinJointsOverLimit">Skin [SKIN_INDEX] defines [JOINT_COUNT] compatible joints, maximum is: [MAX]. Unused joints will be stripped on per model basis.</string>
<string name="ModelTooManyJoint">Model [MODEL_NAME] uses [JOINT_COUNT], maximum: [MAX], upload might fail</string>
<string name="ParsingErrorMissingBuffer">Buffer is either missing or empty [BUFFER_NAME].</string>
<string name="ParsingErrorMissingBufferBin">Buffer is either missing or empty. Check presence of [BUFFER_URI] file.</string>
<panel
follows="top|left"
height="595"
@ -1706,7 +1723,6 @@ Analysed:
height="408"/>
<panel
follows="right|bottom"
can_resize="false"
height="140"
layout="topleft"
name="right_panel"

View File

@ -9453,8 +9453,11 @@ Unable to upload texture: &apos;[NAME]&apos;
icon="alertmodal.tga"
name="CannotUploadMaterial"
type="alertmodal">
There was a problem uploading the file
Unable to upload material file. The file may be corrupted, in an unsupported format, or contain invalid data. Please check that you're using a valid GLTF/GLB file with proper material definitions.
<tag>fail</tag>
<usetemplate
name="okbutton"
yestext="OK"/>
</notification>
<notification