Skip to content

Commit 272ecfd

Browse files
Radient: add glTF morph target import
Merge per-primitive morph streams into mesh-wide target data, preserve target names and default weights, and attach per-node weight instances with node overrides during scene instantiation.
1 parent a3846ec commit 272ecfd

6 files changed

Lines changed: 476 additions & 35 deletions

File tree

‎Radient/include/Assets/RadientMeshAssetManager.hpp‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
#pragma once
2828

29+
#include "Assets/RadientMorphTargetData.hpp"
2930
#include "Render/RadientDrawableMesh.hpp"
3031
#include "RadientAssetCache.hpp"
3132
#include "RadientAssets.h"
@@ -103,11 +104,12 @@ class RadientMeshAssetManager final : public std::enable_shared_from_this<Radien
103104
// Creates a mesh handle and schedules mesh-view payload creation. When
104105
// vertex or index data is still loading, the view task depends on the
105106
// corresponding data tasks if they are still available.
106-
RADIENT_STATUS CreateMeshView(IThreadPool& ThreadPool,
107-
const RadientMeshGeometryData* pGeometryData,
108-
Uint32 GeometryCount,
109-
const RadientMeshViewCreateInfo& ViewCI,
110-
IRadientMeshAsset** ppMesh);
107+
RADIENT_STATUS CreateMeshView(IThreadPool& ThreadPool,
108+
const RadientMeshGeometryData* pGeometryData,
109+
Uint32 GeometryCount,
110+
const RadientMeshViewCreateInfo& ViewCI,
111+
IRadientMeshAsset** ppMesh,
112+
std::unique_ptr<RadientMorphTargetData> pMorphTargetData = nullptr);
111113

112114
// Returns drawable mesh data when the mesh asset is ready. A pending status
113115
// means that any mesh dependency may still be unresolved: source/view
@@ -135,6 +137,8 @@ class RadientMeshAssetManager final : public std::enable_shared_from_this<Radien
135137
static const MeshVertexDataPayloadImpl* GetMeshVertexDataPayload(IRadientMeshAsset* pMeshAsset, Uint32 GeometryIndex);
136138
static const IRadientMeshIndexData* GetMeshIndexData(IRadientMeshAsset* pMeshAsset);
137139
static const IRadientMeshVertexData* GetMeshVertexData(IRadientMeshAsset* pMeshAsset);
140+
// Returns packed morph deltas for a ready mesh asset.
141+
static const RadientMorphTargetData* GetMorphTargetData(IRadientMeshAsset* pMeshAsset);
138142

139143
private:
140144
explicit RadientMeshAssetManager(const CreateInfo& CI);

‎Radient/include/Import/RadientImportedScene.hpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ struct ImportedNode
5858

5959
RefCntAutoPtr<IRadientMeshAsset> pMesh;
6060

61+
/// Optional node-specific morph weights. An empty array uses the mesh
62+
/// morph-target defaults.
63+
std::vector<Float32> MorphWeights;
64+
6165
/// Index in ImportedDocument::Skins, or InvalidImportedSkinIndex when the
6266
/// node does not use skinning.
6367
Uint32 SkinIndex = InvalidImportedSkinIndex;

‎Radient/src/Assets/RadientGLTFLoader.cpp‎

Lines changed: 225 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "Assets/RadientMeshAssetManager.hpp"
3232
#include "Assets/RadientMeshVertexSource.hpp"
3333
#include "Assets/RadientMeshViewSource.hpp"
34+
#include "Assets/RadientMorphTargetData.hpp"
3435
#include "Assets/RadientTextureAssetManager.hpp"
3536
#include "Errors.hpp"
3637
#include "GLTFBuilder.hpp"
@@ -46,6 +47,9 @@
4647
#include "TinyGltfModelView.hpp"
4748

4849
#include <algorithm>
50+
#include <cmath>
51+
#include <cstring>
52+
#include <limits>
4953
#include <memory>
5054
#include <string>
5155
#include <unordered_map>
@@ -194,9 +198,10 @@ struct PlannedIndexData
194198

195199
struct PlannedPrimitive
196200
{
197-
Uint32 VertexDataIndex = ~0u;
198-
Uint32 IndexDataIndex = ~0u;
199-
int MaterialId = -1;
201+
Uint32 VertexDataIndex = ~0u;
202+
Uint32 IndexDataIndex = ~0u;
203+
Uint32 SourcePrimitiveIndex = ~0u;
204+
int MaterialId = -1;
200205
};
201206

202207
struct PlannedMesh
@@ -343,10 +348,11 @@ class RadientGLTFGeometryPlan
343348
if (RADIENT_FAILED(Status))
344349
return Status;
345350

346-
PlannedPrimitive& Primitive = Mesh.Primitives.emplace_back();
347-
Primitive.VertexDataIndex = VertexDataIndex;
348-
Primitive.IndexDataIndex = IndexDataIndex;
349-
Primitive.MaterialId = GltfPrimitive.GetMaterialId();
351+
PlannedPrimitive& Primitive = Mesh.Primitives.emplace_back();
352+
Primitive.VertexDataIndex = VertexDataIndex;
353+
Primitive.IndexDataIndex = IndexDataIndex;
354+
Primitive.SourcePrimitiveIndex = static_cast<Uint32>(PrimitiveIndex);
355+
Primitive.MaterialId = GltfPrimitive.GetMaterialId();
350356
}
351357

352358
return RADIENT_STATUS_OK;
@@ -433,6 +439,190 @@ class RadientGLTFGeometryPlan
433439
std::vector<bool> m_ScannedMeshes;
434440
};
435441

442+
struct MorphTargetAttributeBuildData
443+
{
444+
std::string Semantic;
445+
Uint32 ComponentCount = 0;
446+
std::vector<Float32> Deltas;
447+
};
448+
449+
struct MorphTargetBuildData
450+
{
451+
std::string Name;
452+
Float32 DefaultWeight = 0.f;
453+
std::vector<MorphTargetAttributeBuildData> Attributes;
454+
std::vector<RadientMorphTargetAttributeDesc> AttributeDescs;
455+
std::vector<RadientMorphTargetAttributeCreateInfo> AttributeCreateInfos;
456+
};
457+
458+
std::unique_ptr<RadientMorphTargetData> BuildMorphTargetData(const GLTF::Mesh& Mesh)
459+
{
460+
if (Mesh.Primitives.empty())
461+
return nullptr;
462+
463+
const size_t TargetCount = Mesh.Primitives.front().MorphTargets.size();
464+
Uint32 VertexCount = 0;
465+
for (size_t PrimitiveIndex = 0; PrimitiveIndex < Mesh.Primitives.size(); ++PrimitiveIndex)
466+
{
467+
const GLTF::Primitive& Primitive = Mesh.Primitives[PrimitiveIndex];
468+
if (Primitive.MorphTargets.size() != TargetCount)
469+
{
470+
LOG_WARNING_MESSAGE("Ignoring morph targets for GLTF mesh '", Mesh.Name,
471+
"' because primitive ", PrimitiveIndex, " has ", Primitive.MorphTargets.size(),
472+
" targets, while primitive 0 has ", TargetCount);
473+
return nullptr;
474+
}
475+
if (Primitive.VertexCount > (std::numeric_limits<Uint32>::max)() - VertexCount)
476+
{
477+
LOG_WARNING_MESSAGE("Ignoring morph targets for GLTF mesh '", Mesh.Name,
478+
"' because its combined vertex count exceeds the supported range");
479+
return nullptr;
480+
}
481+
VertexCount += Primitive.VertexCount;
482+
}
483+
if (TargetCount == 0)
484+
return nullptr;
485+
if (TargetCount > (std::numeric_limits<Uint32>::max)())
486+
{
487+
LOG_WARNING_MESSAGE("Ignoring morph targets for GLTF mesh '", Mesh.Name,
488+
"' because the target count exceeds the supported range");
489+
return nullptr;
490+
}
491+
492+
if (!Mesh.Weights.empty() && Mesh.Weights.size() != TargetCount)
493+
{
494+
LOG_WARNING_MESSAGE("GLTF mesh '", Mesh.Name, "' provides ", Mesh.Weights.size(),
495+
" default morph weights for ", TargetCount,
496+
" targets; missing defaults will be zero and extra defaults will be ignored");
497+
}
498+
499+
// Radient uses one logical vertex domain for each mesh. Concatenate the
500+
// primitive streams in stored primitive order and leave missing attribute
501+
// regions initialized to zero.
502+
try
503+
{
504+
std::vector<MorphTargetBuildData> Targets(TargetCount);
505+
for (size_t TargetIndex = 0; TargetIndex < TargetCount; ++TargetIndex)
506+
{
507+
MorphTargetBuildData& Target = Targets[TargetIndex];
508+
if (TargetIndex < Mesh.MorphTargetNames.size())
509+
Target.Name = Mesh.MorphTargetNames[TargetIndex];
510+
if (TargetIndex < Mesh.Weights.size())
511+
{
512+
if (!std::isfinite(Mesh.Weights[TargetIndex]))
513+
{
514+
LOG_WARNING_MESSAGE("Ignoring morph targets for GLTF mesh '", Mesh.Name,
515+
"' because default weight ", TargetIndex, " is not finite");
516+
return nullptr;
517+
}
518+
Target.DefaultWeight = Mesh.Weights[TargetIndex];
519+
}
520+
521+
for (const GLTF::Primitive& Primitive : Mesh.Primitives)
522+
{
523+
for (const GLTF::MorphTargetAttribute& SourceAttribute : Primitive.MorphTargets[TargetIndex].Attributes)
524+
{
525+
const auto ExistingAttribute = std::find_if(
526+
Target.Attributes.begin(), Target.Attributes.end(),
527+
[&SourceAttribute](const MorphTargetAttributeBuildData& Attribute) {
528+
return Attribute.Semantic == SourceAttribute.Semantic;
529+
});
530+
if (ExistingAttribute != Target.Attributes.end())
531+
{
532+
if (ExistingAttribute->ComponentCount != SourceAttribute.NumComponents)
533+
{
534+
LOG_WARNING_MESSAGE("Ignoring morph targets for GLTF mesh '", Mesh.Name,
535+
"' because target ", TargetIndex, " attribute '",
536+
SourceAttribute.Semantic,
537+
"' has inconsistent component counts between primitives");
538+
return nullptr;
539+
}
540+
continue;
541+
}
542+
543+
const bool IsStandardSemantic =
544+
SourceAttribute.Semantic == RadientMorphTargetPositionSemantic ||
545+
SourceAttribute.Semantic == RadientMorphTargetNormalSemantic ||
546+
SourceAttribute.Semantic == RadientMorphTargetTangentSemantic;
547+
if (SourceAttribute.NumComponents == 0 || SourceAttribute.NumComponents > 4 ||
548+
(IsStandardSemantic && SourceAttribute.NumComponents != 3) ||
549+
VertexCount > (std::numeric_limits<size_t>::max)() / SourceAttribute.NumComponents)
550+
{
551+
LOG_WARNING_MESSAGE("Ignoring morph targets for GLTF mesh '", Mesh.Name,
552+
"' because target ", TargetIndex, " attribute '",
553+
SourceAttribute.Semantic, "' has an unsupported size");
554+
return nullptr;
555+
}
556+
557+
MorphTargetAttributeBuildData& Attribute = Target.Attributes.emplace_back();
558+
Attribute.Semantic = SourceAttribute.Semantic;
559+
Attribute.ComponentCount = SourceAttribute.NumComponents;
560+
Attribute.Deltas.resize(size_t{VertexCount} * Attribute.ComponentCount, 0.f);
561+
}
562+
}
563+
564+
Uint32 FirstVertex = 0;
565+
for (const GLTF::Primitive& Primitive : Mesh.Primitives)
566+
{
567+
const GLTF::MorphTarget& SourceTarget = Primitive.MorphTargets[TargetIndex];
568+
for (MorphTargetAttributeBuildData& Attribute : Target.Attributes)
569+
{
570+
const GLTF::MorphTargetAttribute* const pSourceAttribute =
571+
SourceTarget.FindAttribute(Attribute.Semantic.c_str());
572+
if (pSourceAttribute == nullptr)
573+
continue;
574+
575+
const size_t ValueCount = size_t{Primitive.VertexCount} * Attribute.ComponentCount;
576+
if (pSourceAttribute->FirstValue > SourceTarget.Values.size() ||
577+
ValueCount > SourceTarget.Values.size() - pSourceAttribute->FirstValue)
578+
{
579+
LOG_WARNING_MESSAGE("Ignoring morph targets for GLTF mesh '", Mesh.Name,
580+
"' because target ", TargetIndex, " attribute '",
581+
Attribute.Semantic, "' has an invalid value range");
582+
return nullptr;
583+
}
584+
585+
std::memcpy(Attribute.Deltas.data() + size_t{FirstVertex} * Attribute.ComponentCount,
586+
SourceTarget.GetAttributeData(*pSourceAttribute),
587+
ValueCount * sizeof(Float32));
588+
}
589+
FirstVertex += Primitive.VertexCount;
590+
}
591+
}
592+
593+
std::vector<RadientMorphTargetCreateInfo> TargetCreateInfos(TargetCount);
594+
for (size_t TargetIndex = 0; TargetIndex < TargetCount; ++TargetIndex)
595+
{
596+
MorphTargetBuildData& Target = Targets[TargetIndex];
597+
Target.AttributeDescs.resize(Target.Attributes.size());
598+
Target.AttributeCreateInfos.resize(Target.Attributes.size());
599+
for (size_t AttributeIndex = 0; AttributeIndex < Target.Attributes.size(); ++AttributeIndex)
600+
{
601+
const MorphTargetAttributeBuildData& Attribute = Target.Attributes[AttributeIndex];
602+
Target.AttributeDescs[AttributeIndex].Semantic = Attribute.Semantic.c_str();
603+
Target.AttributeDescs[AttributeIndex].ComponentCount = Attribute.ComponentCount;
604+
Target.AttributeCreateInfos[AttributeIndex].pDeltas = Attribute.Deltas.data();
605+
}
606+
607+
RadientMorphTargetCreateInfo& TargetCI = TargetCreateInfos[TargetIndex];
608+
TargetCI.Desc.Name = Target.Name.c_str();
609+
TargetCI.Desc.pAttributes = Target.AttributeDescs.empty() ? nullptr : Target.AttributeDescs.data();
610+
TargetCI.Desc.AttributeCount = static_cast<Uint32>(Target.AttributeDescs.size());
611+
TargetCI.Desc.DefaultWeight = Target.DefaultWeight;
612+
TargetCI.pAttributeData = Target.AttributeCreateInfos.empty() ? nullptr : Target.AttributeCreateInfos.data();
613+
}
614+
615+
return std::make_unique<RadientMorphTargetData>(
616+
TargetCreateInfos.data(), static_cast<Uint32>(TargetCreateInfos.size()), VertexCount);
617+
}
618+
catch (const std::exception& Error)
619+
{
620+
LOG_WARNING_MESSAGE("Ignoring morph targets for GLTF mesh '", Mesh.Name,
621+
"' because their data could not be packed: ", Error.what());
622+
return nullptr;
623+
}
624+
}
625+
436626
class RadientMeshLoader
437627
{
438628
public:
@@ -494,6 +684,7 @@ class RadientMeshLoader
494684
Primitives.reserve(pPlannedMesh->Primitives.size());
495685
GeometryIndices.reserve(pPlannedMesh->Primitives.size());
496686
pNewMesh->Primitives.reserve(pPlannedMesh->Primitives.size());
687+
bool MorphTargetsLoaded = true;
497688

498689
std::vector<const PlannedPrimitive*> SortedPrimitives;
499690
SortedPrimitives.reserve(pPlannedMesh->Primitives.size());
@@ -548,13 +739,22 @@ class RadientMeshLoader
548739
static_cast<Uint32>(PlannedPrimitive.MaterialId) :
549740
0u;
550741

551-
pNewMesh->Primitives.emplace_back(0u,
552-
pIndexData->IndexCount,
553-
0u,
554-
pVertexData->VertexCount,
555-
MaterialSlot,
556-
pVertexData->BBMin,
557-
pVertexData->BBMax);
742+
GLTF::Primitive& NewPrimitive = pNewMesh->Primitives.emplace_back(0u,
743+
pIndexData->IndexCount,
744+
0u,
745+
pVertexData->VertexCount,
746+
MaterialSlot,
747+
pVertexData->BBMin,
748+
pVertexData->BBMax);
749+
750+
const auto GltfPrimitive = GltfMesh.GetPrimitive(PlannedPrimitive.SourcePrimitiveIndex);
751+
if (!GLTF::LoadMorphTargets(GltfModel, GltfPrimitive, pVertexData->VertexCount, NewPrimitive))
752+
{
753+
LOG_WARNING_MESSAGE("Failed to load morph targets for GLTF mesh '", pNewMesh->Name,
754+
"' primitive ", PlannedPrimitive.SourcePrimitiveIndex,
755+
"; loading the mesh without morph targets");
756+
MorphTargetsLoaded = false;
757+
}
558758

559759
RadientMeshPrimitiveCreateInfo& Primitive = Primitives.emplace_back();
560760
Primitive.FirstIndex = 0;
@@ -569,6 +769,15 @@ class RadientMeshLoader
569769

570770
pNewMesh->UpdateBoundingBox();
571771

772+
std::unique_ptr<RadientMorphTargetData> pMorphTargetData;
773+
if (MorphTargetsLoaded)
774+
pMorphTargetData = BuildMorphTargetData(*pNewMesh);
775+
else
776+
{
777+
for (GLTF::Primitive& Primitive : pNewMesh->Primitives)
778+
Primitive.MorphTargets.clear();
779+
}
780+
572781
RadientMeshViewCreateInfo ViewCI;
573782
ViewCI.pPrimitives = Primitives.data();
574783
ViewCI.PrimitiveCount = static_cast<Uint32>(Primitives.size());
@@ -580,7 +789,8 @@ class RadientMeshLoader
580789
MeshGeometryData.data(),
581790
static_cast<Uint32>(MeshGeometryData.size()),
582791
ViewCI,
583-
pMeshAsset.GetAddressOfEmpty());
792+
pMeshAsset.GetAddressOfEmpty(),
793+
std::move(pMorphTargetData));
584794
if (RADIENT_FAILED(Status) || pMeshAsset == nullptr)
585795
{
586796
m_Status = RADIENT_FAILED(Status) ? Status : RADIENT_STATUS_FAILED;

0 commit comments

Comments
 (0)