/
Metallist64
/
rbfx
Обзор
Документация
Войти
/
Metallist64
/
rbfx
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
Source/Editor/Assets/ModelImporter.cpp
569 строк
21 KB
Eugene Kozlov
Generate skins only if none are present.
26 дек 2025, 00:34
26 дек 2025, 00:34
63ae887
Код
Авторство
О чём код?
// // Copyright (c) 2017-2020 the rbfx project. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // #include "../Assets/ModelImporter.h" #include <Urho3D/Core/ProcessUtils.h> #include <Urho3D/Graphics/Animation.h> #include <Urho3D/IO/ArchiveSerialization.h> #include <Urho3D/IO/FileSystem.h> #include <Urho3D/Resource/JSONFile.h> #if URHO3D_GLOW #include <Urho3D/Glow/LightmapUVGenerator.h> #endif #include <EASTL/finally.h> namespace Urho3D { namespace { const StringVector DefaultSkipTags{"[skip]"}; bool IsFileNameGLTF(const ea::string& fileName, bool strict = true) { if (!strict && (fileName.ends_with(".gltf_", false) || fileName.ends_with(".glb_", false))) return true; return fileName.ends_with(".gltf", false) || fileName.ends_with(".glb", false); } bool IsFileNameFBX(const ea::string& fileName, bool strict = true) { if (!strict && fileName.ends_with(".fbx_", false)) return true; return fileName.ends_with(".fbx", false); } bool IsFileNameBlend(const ea::string& fileName, bool strict = true) { if (!strict && fileName.ends_with(".blend_", false)) return true; return fileName.ends_with(".blend", false); } bool IsAnimationLooped(const Animation& animation) { for (const auto& [_, track] : animation.GetTracks()) { if (!track.IsLooped()) return false; } for (const auto& [_, track] : animation.GetVariantTracks()) { if (!track.IsLooped()) return false; } return true; } template <class T> ea::optional<float> GetTrackStep(const T& track) { const unsigned numFrames = track.keyFrames_.size(); if (numFrames <= 1) return ea::nullopt; return track.keyFrames_[numFrames - 1].time_ - track.keyFrames_[numFrames - 2].time_; } ea::optional<float> GetFrameStep(const Animation& animation) { ea::optional<float> frameStep; for (const auto& [_, track] : animation.GetTracks()) { if (const auto trackStep = GetTrackStep(track)) frameStep = ea::min(frameStep.value_or(M_LARGE_VALUE), *trackStep); } for (const auto& [_, track] : animation.GetVariantTracks()) { if (const auto trackStep = GetTrackStep(track)) frameStep = ea::min(frameStep.value_or(M_LARGE_VALUE), *trackStep); } return frameStep; } AnimationTrack* GetRootAnimationTrack(Animation& animation) { const StringVariantMap& parentTracks = animation.GetMetadata(AnimationMetadata::ParentTracks).GetStringVariantMap(); StringVector rootTracks; for (const auto& [name, parent] : parentTracks) { if (parent.GetString().empty()) rootTracks.push_back(name); } return rootTracks.size() == 1 ? animation.GetTrack(rootTracks.front()) : nullptr; } } // namespace void Assets_ModelImporter(Context* context, Project* project) { if (!context->IsReflected<ModelImporter>()) ModelImporter::RegisterObject(context); } void ModelImporter::ResetRootMotionInfo::SerializeInBlock(Archive& archive) { SerializeOptionalValue(archive, "factor", factor_); SerializeOptionalValue(archive, "positionWeight", positionWeight_); SerializeOptionalValue(archive, "rotationSwingWeight", rotationSwingWeight_); SerializeOptionalValue(archive, "rotationTwistWeight", rotationTwistWeight_); SerializeOptionalValue(archive, "scaleWeight", scaleWeight_); } void ModelImporter::TransformerParams::SerializeInBlock(Archive& archive) { GLTFImporterSettings::SerializeInBlock(archive); SerializeOptionalValue(archive, "repairLooping", repairLooping_); SerializeOptionalValue(archive, "blenderApplyModifiers", blenderApplyModifiers_); SerializeOptionalValue(archive, "blenderDeformingBonesOnly", blenderDeformingBonesOnly_); SerializeOptionalValue(archive, "lightmapUVGenerate", lightmapUVGenerate_); SerializeOptionalValue(archive, "lightmapUVTexelsPerUnit", lightmapUVTexelsPerUnit_); SerializeOptionalValue(archive, "lightmapUVChannel", lightmapUVChannel_); SerializeOptionalValue(archive, "appendFiles", appendFiles_); SerializeOptionalValue(archive, "resetRootMotion", resetRootMotion_); SerializeOptionalValue(archive, "resourceMetadata", resourceMetadata_); SerializeOptionalValue(archive, "artificialSkinNodes", artificialSkinNodes_); } ModelImporter::ModelImporter(Context* context) : AssetTransformer(context) { defaultParams_.skipTags_ = DefaultSkipTags; } void ModelImporter::RegisterObject(Context* context) { context->AddFactoryReflection<ModelImporter>(Category_Transformer); // clang-format off URHO3D_ATTRIBUTE("Mirror X", bool, defaultParams_.mirrorX_, TransformerParams{}.mirrorX_, AM_DEFAULT); URHO3D_ATTRIBUTE("Scale", float, defaultParams_.scale_, TransformerParams{}.scale_, AM_DEFAULT); URHO3D_ATTRIBUTE("Rotation", Quaternion, defaultParams_.rotation_, TransformerParams{}.rotation_, AM_DEFAULT); URHO3D_ATTRIBUTE("Fade Transparency", bool, defaultParams_.fadeTransparency_, TransformerParams{}.fadeTransparency_, AM_DEFAULT); URHO3D_ATTRIBUTE("Cleanup Bone Names", bool, defaultParams_.cleanupBoneNames_, TransformerParams{}.cleanupBoneNames_, AM_DEFAULT); URHO3D_ATTRIBUTE("Cleanup Root Nodes", bool, defaultParams_.cleanupRootNodes_, TransformerParams{}.cleanupRootNodes_, AM_DEFAULT); URHO3D_ATTRIBUTE("Combine LODs", bool, defaultParams_.combineLODs_, TransformerParams{}.combineLODs_, AM_DEFAULT); URHO3D_ATTRIBUTE("Skip Tags", StringVector, defaultParams_.skipTags_, DefaultSkipTags, AM_DEFAULT); URHO3D_ATTRIBUTE("Keep Names On Merge", bool, defaultParams_.keepNamesOnMerge_, TransformerParams{}.keepNamesOnMerge_, AM_DEFAULT); URHO3D_ATTRIBUTE("Add Empty Nodes To Skeleton", bool, defaultParams_.addEmptyNodesToSkeleton_, TransformerParams{}.addEmptyNodesToSkeleton_, AM_DEFAULT); URHO3D_ATTRIBUTE("Repair Looping", bool, defaultParams_.repairLooping_, TransformerParams{}.repairLooping_, AM_DEFAULT); URHO3D_ATTRIBUTE("Artificial Skin Nodes", StringVector, defaultParams_.artificialSkinNodes_, TransformerParams{}.artificialSkinNodes_, AM_DEFAULT); URHO3D_ATTRIBUTE("Blender: Apply Modifiers", bool, defaultParams_.blenderApplyModifiers_, TransformerParams{}.blenderApplyModifiers_, AM_DEFAULT); URHO3D_ATTRIBUTE("Blender: Deforming Bones Only", bool, defaultParams_.blenderDeformingBonesOnly_, TransformerParams{}.blenderDeformingBonesOnly_, AM_DEFAULT); URHO3D_ATTRIBUTE("LightMap UV: Generate", bool, defaultParams_.lightmapUVGenerate_, TransformerParams{}.lightmapUVGenerate_, AM_DEFAULT); URHO3D_ATTRIBUTE("LightMap UV: Texels per Unit", float, defaultParams_.lightmapUVTexelsPerUnit_, TransformerParams{}.lightmapUVTexelsPerUnit_, AM_DEFAULT); URHO3D_ATTRIBUTE("LightMap UV: Channel", unsigned, defaultParams_.lightmapUVChannel_, TransformerParams{}.lightmapUVChannel_, AM_DEFAULT); // clang-format on } ToolManager* ModelImporter::GetToolManager() const { auto project = GetSubsystem<Project>(); return project->GetToolManager(); } bool ModelImporter::IsApplicable(const AssetTransformerInput& input) { const auto toolManager = GetToolManager(); if (IsFileNameGLTF(input.resourceName_)) return true; if (IsFileNameFBX(input.resourceName_)) { if (!toolManager->HasFBX2glTF()) { static bool logged = false; if (!logged) URHO3D_LOGERROR("FBX2glTF is not found, cannot import FBX files. See Settings/Editor/ExternalTools."); logged = true; return false; } return true; } if (IsFileNameBlend(input.resourceName_)) { if (!toolManager->HasBlender()) { static bool logged = false; if (!logged) URHO3D_LOGERROR( "Blender is not found, cannot import Blender files. See Settings/Editor/ExternalTools."); logged = true; return false; } return true; } return false; } bool ModelImporter::Execute( const AssetTransformerInput& input, AssetTransformerOutput& output, const AssetTransformerVector& transformers) { const ea::string paramsFileName = GetParametersFileName(input.inputFileName_); TransformerParams params; if (LoadParameters(params, paramsFileName)) AddDependency(input, output, paramsFileName); params.assetName_ = GetFileName(input.originalInputFileName_); const GLTFFileHandle handle = LoadData(input.inputFileName_, params, input.tempPath_); if (!handle) return false; return ImportGLTF(handle, params, input, output, transformers); } bool ModelImporter::ImportGLTF(GLTFFileHandle fileHandle, const TransformerParams& params, const AssetTransformerInput& input, AssetTransformerOutput& output, const AssetTransformerVector& transformers) { currentParams_ = ¶ms; const auto metadataGuard = ea::make_finally([&] { currentParams_ = nullptr; }); auto importer = MakeShared<GLTFImporter>(context_, params); const ea::string outputPath = AddTrailingSlash(input.outputFileName_); const ea::string resourceNamePrefix = AddTrailingSlash(input.outputResourceName_); if (!importer->LoadFile(fileHandle->fileName_)) { URHO3D_LOGERROR("Failed to load asset {} as GLTF model", input.resourceName_); return false; } for (const ea::string& secondaryFileName : params.appendFiles_) { const ea::string secondaryFilePath = GetPath(input.originalInputFileName_) + secondaryFileName; const GLTFFileHandle secondaryFileHandle = LoadData(secondaryFilePath, params, input.tempPath_); if (!secondaryFileHandle) { URHO3D_LOGWARNING("Failed to load secondary file {} for asset {}", secondaryFilePath, input.resourceName_); continue; } AddDependency(input, output, secondaryFilePath); if (!importer->MergeFile(secondaryFileHandle->fileName_, GetFileName(secondaryFilePath))) { URHO3D_LOGWARNING( "Failed to merge secondary file {} into asset {}", secondaryFilePath, input.resourceName_); continue; } } if (!importer->Process(outputPath, resourceNamePrefix, this)) { URHO3D_LOGERROR("Failed to process asset {}", input.resourceName_); return false; } auto fs = GetSubsystem<FileSystem>(); fs->RemoveDir(input.outputFileName_, true); fs->Delete(input.outputFileName_); if (!importer->SaveResources()) { URHO3D_LOGERROR("Failed to save output files for asset {}", input.resourceName_); return false; } for (const auto& [resourceName, fileName] : importer->GetSavedResources()) { AssetTransformerOutput nestedOutput; AssetTransformerInput nestedInput = input; nestedInput.resourceName_ = resourceName; nestedInput.inputFileName_ = fileName; nestedInput.outputFileName_ = fileName; if (!AssetTransformer::ExecuteTransformers(nestedInput, nestedOutput, transformers, true)) { URHO3D_LOGERROR("Failed to apply nested transformer for asset {}", resourceName); return false; } output.appliedTransformers_.insert( nestedOutput.appliedTransformers_.begin(), nestedOutput.appliedTransformers_.end()); } return true; } void ModelImporter::OnModelLoaded(ModelView& modelView) { if (currentParams_->lightmapUVGenerate_) { #if URHO3D_GLOW LightmapUVGenerationSettings settings; settings.texelPerUnit_ = currentParams_->lightmapUVTexelsPerUnit_; settings.uvChannel_ = currentParams_->lightmapUVChannel_; if (!GenerateLightmapUV(modelView, settings)) throw RuntimeException("Failed to generate lightmap UVs"); #else throw RuntimeException("Glow must be enabled to generate lightmap UVs"); #endif } } void ModelImporter::OnAnimationLoaded(Animation& animation) { AppendResourceMetadata(animation); const auto resetRootMotionIter = currentParams_->resetRootMotion_.find(animation.GetAnimationName()); if (resetRootMotionIter != currentParams_->resetRootMotion_.end()) ResetRootMotion(animation, resetRootMotionIter->second); const bool isAnimationLooped = IsAnimationLooped(animation); const auto frameStep = GetFrameStep(animation); // TODO: It would be better to add a keyframe at the end of the animation if (!isAnimationLooped && currentParams_->repairLooping_ && frameStep) { animation.SetLength(animation.GetLength() + *frameStep); animation.AddMetadata(AnimationMetadata::Looped, true); } else { animation.AddMetadata(AnimationMetadata::Looped, isAnimationLooped); } if (frameStep) { const int frameRate = RoundToInt(1.0f / *frameStep); const float frameRateError = Abs(frameRate - 1.0f / *frameStep); animation.AddMetadata(AnimationMetadata::FrameStep, *frameStep); if (frameRateError < 0.01f) animation.AddMetadata(AnimationMetadata::FrameRate, frameRate); } } ea::vector<ea::string> ModelImporter::GetArtificialSkinNodes() { return currentParams_->artificialSkinNodes_; } void ModelImporter::ResetRootMotion(Animation& animation, const ResetRootMotionInfo& info) { AnimationTrack* rootTrack = GetRootAnimationTrack(animation); if (!rootTrack) return; const AnimationKeyFrame& firstFrame = rootTrack->keyFrames_.front(); const AnimationKeyFrame& lastFrame = rootTrack->keyFrames_.back(); if (firstFrame.time_ == lastFrame.time_) return; // Copy the track const ea::string originalTrackName = rootTrack->name_ + "_Original"; if (animation.GetTrack(originalTrackName)) { URHO3D_LOGWARNING("Cannot create backup track '{}' for root motion, skipping", originalTrackName); return; } animation.AddMetadata(AnimationMetadata::RootTrack, rootTrack->name_); animation.AddMetadata(AnimationMetadata::OriginalRootTrack, originalTrackName); AnimationTrack* originalRootTrack = animation.CreateTrack(originalTrackName); originalRootTrack->channelMask_ = rootTrack->channelMask_; originalRootTrack->keyFrames_ = rootTrack->keyFrames_; // Calculate approximate velocity const Vector3 positionDelta = lastFrame.position_ - firstFrame.position_; const Quaternion rotationDelta = lastFrame.rotation_ * firstFrame.rotation_.Inverse(); const Vector3 scaleDelta = lastFrame.scale_ - firstFrame.scale_; const Vector3 linearVelocity = positionDelta / (lastFrame.time_ - firstFrame.time_); const Vector3 angularVelocity = rotationDelta.AngularVelocity() / (lastFrame.time_ - firstFrame.time_); const Vector3 scaleVelocity = scaleDelta / (lastFrame.time_ - firstFrame.time_); animation.AddMetadata(AnimationMetadata::RootLinearVelocity, linearVelocity); animation.AddMetadata(AnimationMetadata::RootAngularVelocity, angularVelocity); animation.AddMetadata(AnimationMetadata::RootScaleVelocity, scaleVelocity); // Calculate the reset transform const float resetTime = Lerp(firstFrame.time_, lastFrame.time_, info.factor_); unsigned frameIndex{}; Transform resetTransform; rootTrack->Sample(resetTime, lastFrame.time_, false, frameIndex, resetTransform); for (AnimationKeyFrame& frame : rootTrack->keyFrames_) { const Transform interpolatedTransform = firstFrame.Lerp(lastFrame, frame.time_); const Transform deltaTransform = frame * interpolatedTransform.Inverse(); const Transform filteredTransform = deltaTransform * resetTransform; const auto [deltaSwing, deltaTwist] = filteredTransform.rotation_.ToSwingTwist(rotationDelta.Axis()); frame.position_ = VectorLerp(resetTransform.position_, filteredTransform.position_, info.positionWeight_); frame.rotation_ = Quaternion::IDENTITY.Slerp(deltaSwing, info.rotationSwingWeight_) * Quaternion::IDENTITY.Slerp(deltaTwist, info.rotationTwistWeight_) * resetTransform.rotation_; frame.scale_ = Lerp(resetTransform.scale_, filteredTransform.scale_, info.scaleWeight_); } } void ModelImporter::AppendResourceMetadata(ResourceWithMetadata& resource) const { const auto metadataIter = currentParams_->resourceMetadata_.find(GetFileName(resource.GetName())); if (metadataIter == currentParams_->resourceMetadata_.end()) return; for (const auto& [name, value] : metadataIter->second) resource.AddMetadata(name, value); } ea::string ModelImporter::GetParametersFileName(const ea::string& fileName) const { return fileName + ".d/ModelImporter.json"; } bool ModelImporter::LoadParameters(TransformerParams& params, const ea::string& paramsFileName) const { JSONFile baseFile{context_}; baseFile.SaveObject("params", defaultParams_); JSONFile overrideFile{context_}; if (overrideFile.LoadFile(paramsFileName)) { baseFile.GetRoot().ReplaceRecursively(overrideFile.GetRoot()); if (baseFile.LoadObject("params", params)) return true; } params = defaultParams_; return false; } ModelImporter::GLTFFileHandle ModelImporter::LoadData( const ea::string& fileName, const TransformerParams& params, const ea::string& tempPath) const { if (IsFileNameGLTF(fileName, false)) { return LoadDataNative(fileName, params); } else if (IsFileNameFBX(fileName, false)) { return LoadDataFromFBX(fileName, params, tempPath); } else if (IsFileNameBlend(fileName, false)) { return LoadDataFromBlend(fileName, params, tempPath); } return nullptr; } ModelImporter::GLTFFileHandle ModelImporter::LoadDataNative( const ea::string& fileName, const TransformerParams& params) const { auto fs = context_->GetSubsystem<FileSystem>(); if (!fs->FileExists(fileName)) return nullptr; return ea::make_shared<GLTFFileInfo>(GLTFFileInfo{fileName}); } ModelImporter::GLTFFileHandle ModelImporter::LoadDataFromFBX( const ea::string& fileName, const TransformerParams& params, const ea::string& tempPath) const { auto fs = context_->GetSubsystem<FileSystem>(); const auto toolManager = GetToolManager(); if (!fs->FileExists(fileName)) return nullptr; // fbx2gltf generates extra files that we don't want to copy, create another temporary directory for those const TemporaryDir fbxTempFolderHolder{context_, RemoveTrailingSlash(tempPath) + ".2"}; const ea::string tempGltfFile = Format("{}{}.glb", tempPath, GenerateUUID()); const StringVector arguments{ "--binary", "--input", fileName, "--output", tempGltfFile, "--fbx-temp-dir", fbxTempFolderHolder.GetPath()}; ea::string commandOutput; if (fs->SystemRun(toolManager->GetFBX2glTF(), arguments, commandOutput) != 0) { URHO3D_LOGERROR("{}", commandOutput); return nullptr; } URHO3D_LOGDEBUG("FBX2glTF output:\n{}", commandOutput); const auto deleter = [fs](GLTFFileInfo* handle) { fs->Delete(handle->fileName_); delete handle; }; return ea::shared_ptr<GLTFFileInfo>(new GLTFFileInfo{tempGltfFile}, deleter); } ModelImporter::GLTFFileHandle ModelImporter::LoadDataFromBlend( const ea::string& fileName, const TransformerParams& params, const ea::string& tempPath) const { auto fs = context_->GetSubsystem<FileSystem>(); const auto toolManager = GetToolManager(); if (!fs->FileExists(fileName)) return nullptr; const ea::string tempGltfFile = tempPath + "model.glb"; // This script is passed as command line argument, so it must be a single line and use single quotes const ea::string script = Format( "import bpy;" "bpy.ops.export_scene.gltf(" " filepath='{}', " " export_format='GLB', " " export_apply={}, " " export_def_bones={}" ");", tempGltfFile, params.blenderApplyModifiers_ ? "True" : "False", params.blenderDeformingBonesOnly_ ? "True" : "False"); const StringVector arguments{"-b", fileName, "--python-expr", script}; ea::string commandOutput; if (fs->SystemRun(toolManager->GetBlender(), arguments, commandOutput) != 0) { URHO3D_LOGERROR("{}", commandOutput); return nullptr; } URHO3D_LOGDEBUG("Blender output:\n{}", commandOutput); const auto deleter = [fs](GLTFFileInfo* handle) { fs->Delete(handle->fileName_); delete handle; }; return ea::shared_ptr<GLTFFileInfo>(new GLTFFileInfo{tempGltfFile}, deleter); } } // namespace Urho3D