Add api to draw Skinned Model from code with Animated Model pose

https://forum.flaxengine.com/t/how-to-render-outline-for-an-animated-model/2598
This commit is contained in:
2026-06-16 11:19:34 +02:00
parent ef35f01c17
commit 888dfbe014
10 changed files with 154 additions and 10 deletions
@@ -14,14 +14,15 @@
#include "Engine/Level/Actors/Skybox.h"
#include "Engine/Level/Actors/Decal.h"
#include "Engine/Level/Actors/ExponentialHeightFog.h"
#include "Engine/Audio/AudioListener.h"
#include "Engine/Audio/AudioSource.h"
#include "Engine/Particles/ParticleEffect.h"
#include "Engine/Animations/SceneAnimations/SceneAnimationPlayer.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Level/Actors/Sky.h"
#include "Engine/Level/Actors/SkyLight.h"
#include "Engine/Level/Actors/SpotLight.h"
#include "Engine/Audio/AudioListener.h"
#include "Engine/Audio/AudioSource.h"
#include "Engine/Particles/ParticleEffect.h"
#include "Engine/Graphics/Models/ModelInstanceEntry.h"
#include "Engine/Animations/SceneAnimations/SceneAnimationPlayer.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Video/VideoPlayer.h"
enum class IconTypes
@@ -235,6 +235,25 @@ BoundingBox SkinnedModel::GetBox(int32 lodIndex) const
return LODs[lodIndex].GetBox();
}
void SkinnedModel::Draw(const RenderContext& renderContext, const SkinnedMeshBones& pose, MaterialBase* material, const Matrix& world, StaticFlags flags, bool receiveDecals, int8 sortOrder) const
{
if (!CanBeRendered() || !pose)
return;
// Select a proper LOD index (model may be culled)
const BoundingBox box = GetBox(world);
BoundingSphere sphere;
BoundingSphere::FromBox(box, sphere);
int32 lodIndex = RenderTools::ComputeModelLOD(this, sphere.Center - renderContext.View.Origin, (float)sphere.Radius, renderContext);
if (lodIndex == -1)
return;
lodIndex += renderContext.View.ModelLODBias;
lodIndex = ClampLODIndex(lodIndex);
// Draw
LODs[lodIndex].Draw(renderContext, pose, material, world, flags, receiveDecals, DrawPass::Default, 0, sortOrder);
}
void SkinnedModel::Draw(const RenderContext& renderContext, const SkinnedMesh::DrawInfo& info)
{
ModelDraw(this, renderContext, renderContext, info);
@@ -46,6 +46,24 @@ public:
/// <returns>True whether the two objects intersected</returns>
bool Intersects(const Ray& ray, const Transform& transform, Real& distance, Vector3& normal, SkinnedMesh** mesh);
/// <summary>
/// Draws the meshes from the model LOD.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="pose">The Animated Model to use its skeleton bones pose for rendering skinned mesh.</param>
/// <param name="material">The material to use for rendering.</param>
/// <param name="world">The world transformation of the model.</param>
/// <param name="flags">The object static flags.</param>
/// <param name="receiveDecals">True if rendered geometry can receive decals, otherwise false.</param>
/// <param name="drawModes">The draw passes to use for rendering this object.</param>
/// <param name="perInstanceRandom">The random per-instance value (normalized to range 0-1).</param>
/// <param name="sortOrder">Object sorting key.</param>
API_FUNCTION() void Draw(API_PARAM(Ref) const RenderContext& renderContext, const SkinnedMeshBones& pose, MaterialBase* material, API_PARAM(Ref) const Matrix& world, StaticFlags flags = StaticFlags::None, bool receiveDecals = true, DrawPass drawModes = DrawPass::Default, float perInstanceRandom = 0.0f, int8 sortOrder = 0) const
{
for (int32 i = 0; i < Meshes.Count(); i++)
Meshes.Get()[i].Draw(renderContext, pose, material, world, flags, receiveDecals, drawModes, perInstanceRandom, sortOrder);
}
/// <summary>
/// Draws the meshes. Binds vertex and index buffers and invokes the draw calls.
/// </summary>
@@ -249,6 +267,18 @@ public:
LODs[lodIndex].Render(context);
}
/// <summary>
/// Draws the model.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="pose">The skeleton bones pose to use for rendering skinned mesh.</param>
/// <param name="material">The material to use for rendering.</param>
/// <param name="world">The world transformation of the model.</param>
/// <param name="flags">The object static flags.</param>
/// <param name="receiveDecals">True if rendered geometry can receive decals, otherwise false.</param>
/// <param name="sortOrder">Object sorting key.</param>
API_FUNCTION() void Draw(API_PARAM(Ref) const RenderContext& renderContext, const SkinnedMeshBones& pose, MaterialBase* material, API_PARAM(Ref) const Matrix& world, StaticFlags flags = StaticFlags::None, bool receiveDecals = true, int8 sortOrder = 0) const;
/// <summary>
/// Draws the model.
/// </summary>
+2 -1
View File
@@ -3,9 +3,10 @@
#pragma once
#include "Config.h"
#include "Engine/Core/ISerializable.h"
#include "Engine/Core/Collections/ChunkedArray.h"
#include "Engine/Content/Assets/Model.h"
#include "Engine/Core/ISerializable.h"
#include "Engine/Graphics/Models/ModelInstanceEntry.h"
/// <summary>
/// The foliage instances scaling modes.
-3
View File
@@ -3,12 +3,9 @@
#pragma once
#include "MeshBase.h"
#include "ModelInstanceEntry.h"
#include "Config.h"
#include "Types.h"
class Lightmap;
/// <summary>
/// Represents part of the model that is made of vertices and can be rendered using custom material and transformation.
/// </summary>
+1
View File
@@ -19,6 +19,7 @@ struct RenderContext;
struct RenderContextBatch;
class Task;
class ModelBase;
class MaterialBase;
class Lightmap;
class GPUBuffer;
class SkinnedMeshDrawData;
+31 -1
View File
@@ -309,6 +309,37 @@ bool SkinnedMesh::UpdateMesh(uint32 vertexCount, uint32 triangleCount, const Flo
return ::UpdateMesh(this, vertexCount, triangleCount, PixelFormat::R16_UInt, vertices, triangles, blendIndices, blendWeights, normals, tangents, uvs, colors);
}
void SkinnedMesh::Draw(const RenderContext& renderContext, const SkinnedMeshBones& pose, MaterialBase* material, const Matrix& world, StaticFlags flags, bool receiveDecals, DrawPass drawModes, float perInstanceRandom, int8 sortOrder, uint8 stencilValue) const
{
if (!material || !material->IsSurface() || !IsInitialized() || !pose)
return;
drawModes &= material->GetDrawModes();
if (drawModes == DrawPass::None)
return;
// Setup draw call
DrawCall drawCall;
drawCall.Geometry.IndexBuffer = _indexBuffer;
drawCall.Geometry.VertexBuffers[0] = _vertexBuffers[0];
drawCall.Draw.IndicesCount = _triangles * 3;
drawCall.InstanceCount = 1;
drawCall.Material = material;
drawCall.World = world;
drawCall.ObjectPosition = drawCall.World.GetTranslation();
drawCall.ObjectRadius = (float)_sphere.Radius * drawCall.World.GetScaleVector().GetAbsolute().MaxValue();
drawCall.Surface.GeometrySize = _box.GetSize();
drawCall.Surface.PrevWorld = world;
drawCall.Surface.Skinning = pose.PrevBonesOffset != 0 ? DrawCall::SkinningMode::WithPrevBones : DrawCall::SkinningMode::Active;
drawCall.Surface.SkinningBones = pose.BoneMatrices;
drawCall.Surface.SkinningBonesOffset = pose.BoneOffset;
drawCall.Surface.PrevBonesOffset = pose.PrevBonesOffset;
drawCall.PerInstanceRandom = perInstanceRandom;
drawCall.StencilValue = stencilValue;
// Push draw call to the render list
renderContext.List->AddDrawCall(renderContext, drawModes, flags, drawCall, receiveDecals, sortOrder);
}
void SkinnedMesh::Draw(const RenderContext& renderContext, const DrawInfo& info, float lodDitherFactor) const
{
const auto& entry = info.Buffer->At(_materialSlotIndex);
@@ -340,7 +371,6 @@ void SkinnedMesh::Draw(const RenderContext& renderContext, const DrawInfo& info,
drawCall.Geometry.VertexBuffers[0] = _vertexBuffers[0];
if (info.Deformation)
info.Deformation->RunDeformers(this, MeshBufferType::Vertex0, drawCall.Geometry.VertexBuffers[0]);
drawCall.Draw.StartIndex = 0;
drawCall.Draw.IndicesCount = _triangles * 3;
drawCall.InstanceCount = 1;
drawCall.Material = material;
@@ -5,6 +5,26 @@
#include "MeshBase.h"
#include "BlendShape.h"
/// <summary>
/// Contains skeletal bones pose data for skinned mesh rendering.
/// </summary>
API_STRUCT(NoDefault) struct FLAXENGINE_API SkinnedMeshBones
{
DECLARE_SCRIPTING_TYPE_MINIMAL(SkinnedMeshBones);
// Buffer with skinned mesh bones data (array of Matrix3x4, one per bone). Used for GPU skinning in the vertex shader.
API_FIELD() GPUBuffer* BoneMatrices = nullptr;
// Offset in the bones buffer where data starts (in Matrix3x4s).
API_FIELD() uint32 BoneOffset = 0;
// Offset in the bones buffer where previous-frame bones data starts (in Matrix3x4s). Can be negative, value 0 means it's unsued (single-frame data provided).
API_FIELD() int16 PrevBonesOffset = 0;
FORCE_INLINE operator bool() const
{
return BoneMatrices != nullptr;
}
};
/// <summary>
/// Represents part of the skinned model that is made of vertices and can be rendered using custom material, transformation and skeleton bones hierarchy.
/// </summary>
@@ -124,6 +144,21 @@ public:
bool UpdateMesh(uint32 vertexCount, uint32 triangleCount, const Float3* vertices, const uint32* triangles, const Int4* blendIndices, const Float4* blendWeights, const Float3* normals = nullptr, const Float3* tangents = nullptr, const Float2* uvs = nullptr, const Color32* colors = nullptr);
public:
/// <summary>
/// Draws the mesh.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="pose">The skeleton bones pose to use for rendering skinned mesh.</param>
/// <param name="material">The material to use for rendering.</param>
/// <param name="world">The world transformation of the model.</param>
/// <param name="flags">The object static flags.</param>
/// <param name="receiveDecals">True if rendered geometry can receive decals, otherwise false.</param>
/// <param name="drawModes">The draw passes to use for rendering this object.</param>
/// <param name="perInstanceRandom">The random per-instance value (normalized to range 0-1).</param>
/// <param name="sortOrder">Object sorting key.</param>
/// <param name="stencilValue">Object stencil value.</param>
API_FUNCTION() void Draw(API_PARAM(Ref) const RenderContext& renderContext, const SkinnedMeshBones& pose, MaterialBase* material, API_PARAM(Ref) const Matrix& world, StaticFlags flags = StaticFlags::None, bool receiveDecals = true, DrawPass drawModes = DrawPass::Default, float perInstanceRandom = 0.0f, int8 sortOrder = 0, uint8 stencilValue = 0) const;
/// <summary>
/// Draws the mesh.
/// </summary>
@@ -357,6 +357,31 @@ void AnimatedModel::PreInitSkinningData()
UpdateSockets();
}
SkinnedMeshBones AnimatedModel::GetSkinnedMeshBones() const
{
SkinnedMeshBones result;
if (_bones.IsAllocated)
{
// Flush skinning data with GPU
// TODO: what if it's called outside the rendering?outside PreDraw/PostDraw on AnimatedModelRenderListExtension?
if (_bones.IsDirty && GPUDevice::Instance->IsRendering())
const_cast<SkinnedBones&>(_bones).Flush();
result.BoneMatrices = RenderListExtension.GlobalBuffer;
result.BoneOffset = _bones.GlobalBufferOffset / sizeof(Matrix3x4);
if (_bones.HasPrevBones && _bones.IsPrevFlushed)
{
result.PrevBonesOffset = _bones.BonesCount;
if (_bones.IsPrevBones)
{
result.BoneOffset += result.PrevBonesOffset;
result.PrevBonesOffset = -result.PrevBonesOffset;
}
}
}
return result;
}
void AnimatedModel::GetCurrentPose(Array<Matrix>& nodesTransformation, bool worldSpace) const
{
if (GraphInstance.NodesPose.IsEmpty())
@@ -241,6 +241,11 @@ public:
/// </summary>
API_FUNCTION() void PreInitSkinningData();
/// <summary>
/// Gets the skeleton bones data used for skinning. It contains the reference to GPU buffer with final transformations of the skeleton nodes (bones) that are used for skinning the mesh inside vertex shader.
/// </summary>
API_PROPERTY() SkinnedMeshBones GetSkinnedMeshBones() const;
/// <summary>
/// Gets the per-node final transformations (skeleton pose).
/// </summary>