**Refactor meshes format to support custom vertex layouts and new flexible api to access mesh data**
#3044 #2667
This commit is contained in:
@@ -6,9 +6,6 @@
|
||||
#include "Engine/Core/Math/BoundingSphere.h"
|
||||
#include "Engine/Animations/CurveSerialization.h"
|
||||
#include "Engine/Serialization/WriteStream.h"
|
||||
#include "Engine/Debug/Exceptions/ArgumentNullException.h"
|
||||
#include "Engine/Debug/Exceptions/ArgumentOutOfRangeException.h"
|
||||
#include "Engine/Debug/Exceptions/InvalidOperationException.h"
|
||||
|
||||
void MeshData::Clear()
|
||||
{
|
||||
@@ -71,6 +68,7 @@ void MeshData::Release()
|
||||
BlendShapes.Resize(0);
|
||||
}
|
||||
|
||||
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
||||
void MeshData::InitFromModelVertices(ModelVertex19* vertices, uint32 verticesCount)
|
||||
{
|
||||
Positions.Resize(verticesCount, false);
|
||||
@@ -160,6 +158,7 @@ void MeshData::InitFromModelVertices(VB0ElementType18* vb0, VB1ElementType18* vb
|
||||
vb1++;
|
||||
}
|
||||
}
|
||||
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
||||
|
||||
void MeshData::SetIndexBuffer(void* data, uint32 indicesCount)
|
||||
{
|
||||
@@ -181,237 +180,52 @@ void MeshData::SetIndexBuffer(void* data, uint32 indicesCount)
|
||||
}
|
||||
}
|
||||
|
||||
bool MeshData::Pack2Model(WriteStream* stream) const
|
||||
{
|
||||
// Validate input
|
||||
if (stream == nullptr)
|
||||
{
|
||||
LOG(Error, "Invalid input.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cache size
|
||||
uint32 verticiecCount = Positions.Count();
|
||||
uint32 indicesCount = Indices.Count();
|
||||
uint32 trianglesCount = indicesCount / 3;
|
||||
bool use16Bit = indicesCount <= MAX_uint16;
|
||||
if (verticiecCount == 0 || trianglesCount == 0 || indicesCount % 3 != 0)
|
||||
{
|
||||
LOG(Error, "Empty mesh! Triangles: {0}, Verticies: {1}.", trianglesCount, verticiecCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate data structure
|
||||
bool hasUVs = UVs.HasItems();
|
||||
if (hasUVs && UVs.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT("UVs"));
|
||||
return true;
|
||||
}
|
||||
bool hasNormals = Normals.HasItems();
|
||||
if (hasNormals && Normals.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT("Normals"));
|
||||
return true;
|
||||
}
|
||||
bool hasTangents = Tangents.HasItems();
|
||||
if (hasTangents && Tangents.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT("Tangents"));
|
||||
return true;
|
||||
}
|
||||
bool hasBitangentSigns = BitangentSigns.HasItems();
|
||||
if (hasBitangentSigns && BitangentSigns.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT("BitangentSigns"));
|
||||
return true;
|
||||
}
|
||||
bool hasLightmapUVs = LightmapUVs.HasItems();
|
||||
if (hasLightmapUVs && LightmapUVs.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT("LightmapUVs"));
|
||||
return true;
|
||||
}
|
||||
bool hasVertexColors = Colors.HasItems();
|
||||
if (hasVertexColors && Colors.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT("Colors"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vertices
|
||||
stream->WriteUint32(verticiecCount);
|
||||
|
||||
// Triangles
|
||||
stream->WriteUint32(trianglesCount);
|
||||
|
||||
// Vertex Buffer 0
|
||||
stream->WriteBytes(Positions.Get(), sizeof(Float3) * verticiecCount);
|
||||
|
||||
// Vertex Buffer 1
|
||||
VB1ElementType vb1;
|
||||
for (uint32 i = 0; i < verticiecCount; i++)
|
||||
{
|
||||
// Get vertex components
|
||||
Float2 uv = hasUVs ? UVs[i] : Float2::Zero;
|
||||
Float3 normal = hasNormals ? Normals[i] : Float3::UnitZ;
|
||||
Float3 tangent = hasTangents ? Tangents[i] : Float3::UnitX;
|
||||
Float2 lightmapUV = hasLightmapUVs ? LightmapUVs[i] : Float2::Zero;
|
||||
Float3 bitangentSign = hasBitangentSigns ? BitangentSigns[i] : Float3::Dot(Float3::Cross(Float3::Normalize(Float3::Cross(normal, tangent)), normal), tangent);
|
||||
|
||||
// Write vertex
|
||||
vb1.TexCoord = Half2(uv);
|
||||
vb1.Normal = Float1010102(normal * 0.5f + 0.5f, 0);
|
||||
vb1.Tangent = Float1010102(tangent * 0.5f + 0.5f, static_cast<byte>(bitangentSign < 0 ? 1 : 0));
|
||||
vb1.LightmapUVs = Half2(lightmapUV);
|
||||
stream->WriteBytes(&vb1, sizeof(vb1));
|
||||
}
|
||||
|
||||
// Vertex Buffer 2
|
||||
stream->WriteBool(hasVertexColors);
|
||||
if (hasVertexColors)
|
||||
{
|
||||
VB2ElementType vb2;
|
||||
for (uint32 i = 0; i < verticiecCount; i++)
|
||||
{
|
||||
vb2.Color = Color32(Colors[i]);
|
||||
stream->WriteBytes(&vb2, sizeof(vb2));
|
||||
}
|
||||
}
|
||||
|
||||
// Index Buffer
|
||||
if (use16Bit)
|
||||
{
|
||||
for (uint32 i = 0; i < indicesCount; i++)
|
||||
stream->WriteUint16(Indices[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream->WriteBytes(Indices.Get(), sizeof(uint32) * indicesCount);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MeshData::Pack2SkinnedModel(WriteStream* stream) const
|
||||
{
|
||||
// Validate input
|
||||
if (stream == nullptr)
|
||||
{
|
||||
LOG(Error, "Invalid input.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cache size
|
||||
uint32 verticiecCount = Positions.Count();
|
||||
uint32 indicesCount = Indices.Count();
|
||||
uint32 trianglesCount = indicesCount / 3;
|
||||
bool use16Bit = indicesCount <= MAX_uint16;
|
||||
if (verticiecCount == 0 || trianglesCount == 0 || indicesCount % 3 != 0)
|
||||
{
|
||||
LOG(Error, "Empty mesh! Triangles: {0}, Verticies: {1}.", trianglesCount, verticiecCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate data structure
|
||||
bool hasUVs = UVs.HasItems();
|
||||
if (hasUVs && UVs.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT( "UVs"));
|
||||
return true;
|
||||
}
|
||||
bool hasNormals = Normals.HasItems();
|
||||
if (hasNormals && Normals.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT("Normals"));
|
||||
return true;
|
||||
}
|
||||
bool hasTangents = Tangents.HasItems();
|
||||
if (hasTangents && Tangents.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT("Tangents"));
|
||||
return true;
|
||||
}
|
||||
bool hasBitangentSigns = BitangentSigns.HasItems();
|
||||
if (hasBitangentSigns && BitangentSigns.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT("BitangentSigns"));
|
||||
return true;
|
||||
}
|
||||
if (BlendIndices.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT("BlendIndices"));
|
||||
return true;
|
||||
}
|
||||
if (BlendWeights.Count() != verticiecCount)
|
||||
{
|
||||
LOG(Error, "Invalid size of {0} stream.", TEXT("BlendWeights"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vertices
|
||||
stream->WriteUint32(verticiecCount);
|
||||
|
||||
// Triangles
|
||||
stream->WriteUint32(trianglesCount);
|
||||
|
||||
// Blend Shapes
|
||||
stream->WriteUint16(BlendShapes.Count());
|
||||
for (const auto& blendShape : BlendShapes)
|
||||
{
|
||||
stream->WriteBool(blendShape.UseNormals);
|
||||
stream->WriteUint32(blendShape.MinVertexIndex);
|
||||
stream->WriteUint32(blendShape.MaxVertexIndex);
|
||||
stream->WriteUint32(blendShape.Vertices.Count());
|
||||
stream->WriteBytes(blendShape.Vertices.Get(), blendShape.Vertices.Count() * sizeof(BlendShapeVertex));
|
||||
}
|
||||
|
||||
// Vertex Buffer
|
||||
VB0SkinnedElementType vb;
|
||||
for (uint32 i = 0; i < verticiecCount; i++)
|
||||
{
|
||||
// Get vertex components
|
||||
Float2 uv = hasUVs ? UVs[i] : Float2::Zero;
|
||||
Float3 normal = hasNormals ? Normals[i] : Float3::UnitZ;
|
||||
Float3 tangent = hasTangents ? Tangents[i] : Float3::UnitX;
|
||||
Float3 bitangentSign = hasBitangentSigns ? BitangentSigns[i] : Float3::Dot(Float3::Cross(Float3::Normalize(Float3::Cross(normal, tangent)), normal), tangent);
|
||||
Int4 blendIndices = BlendIndices[i];
|
||||
Float4 blendWeights = BlendWeights[i];
|
||||
|
||||
// Write vertex
|
||||
vb.Position = Positions[i];
|
||||
vb.TexCoord = Half2(uv);
|
||||
vb.Normal = Float1010102(normal * 0.5f + 0.5f, 0);
|
||||
vb.Tangent = Float1010102(tangent * 0.5f + 0.5f, static_cast<byte>(bitangentSign < 0 ? 1 : 0));
|
||||
vb.BlendIndices = Color32(blendIndices.X, blendIndices.Y, blendIndices.Z, blendIndices.W);
|
||||
vb.BlendWeights = Half4(blendWeights);
|
||||
stream->WriteBytes(&vb, sizeof(vb));
|
||||
}
|
||||
|
||||
// Index Buffer
|
||||
if (use16Bit)
|
||||
{
|
||||
for (uint32 i = 0; i < indicesCount; i++)
|
||||
stream->WriteUint16(Indices[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream->WriteBytes(Indices.Get(), sizeof(uint32) * indicesCount);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void MeshData::CalculateBox(BoundingBox& result) const
|
||||
{
|
||||
if (Positions.HasItems())
|
||||
BoundingBox::FromPoints(Positions.Get(), Positions.Count(), result);
|
||||
else
|
||||
result = BoundingBox::Zero;
|
||||
}
|
||||
|
||||
void MeshData::CalculateSphere(BoundingSphere& result) const
|
||||
{
|
||||
if (Positions.HasItems())
|
||||
BoundingSphere::FromPoints(Positions.Get(), Positions.Count(), result);
|
||||
else
|
||||
result = BoundingSphere::Empty;
|
||||
}
|
||||
|
||||
void MeshData::CalculateBounds(BoundingBox& box, BoundingSphere& sphere) const
|
||||
{
|
||||
if (Positions.HasItems())
|
||||
{
|
||||
// Merged code of BoundingBox::FromPoints and BoundingSphere::FromPoints within a single loop
|
||||
const Float3* points = Positions.Get();
|
||||
const int32 pointsCount = Positions.Count();
|
||||
Float3 min = points[0];
|
||||
Float3 max = min;
|
||||
Float3 center = min;
|
||||
for (int32 i = 1; i < pointsCount; i++)
|
||||
Float3::Add(points[i], center, center);
|
||||
center /= (float)pointsCount;
|
||||
float radiusSq = Float3::DistanceSquared(center, min);
|
||||
for (int32 i = 1; i < pointsCount; i++)
|
||||
{
|
||||
Float3::Min(min, points[i], min);
|
||||
Float3::Max(max, points[i], max);
|
||||
const float distance = Float3::DistanceSquared(center, points[i]);
|
||||
if (distance > radiusSq)
|
||||
radiusSq = distance;
|
||||
}
|
||||
box = BoundingBox(min, max);
|
||||
sphere = BoundingSphere(center, Math::Sqrt(radiusSq));
|
||||
}
|
||||
else
|
||||
{
|
||||
box = BoundingBox::Zero;
|
||||
sphere = BoundingSphere::Empty;
|
||||
}
|
||||
}
|
||||
|
||||
void MeshData::TransformBuffer(const Matrix& matrix)
|
||||
@@ -616,276 +430,3 @@ void ModelData::TransformBuffer(const Matrix& matrix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
bool ModelData::Pack2ModelHeader(WriteStream* stream) const
|
||||
{
|
||||
// Validate input
|
||||
if (stream == nullptr)
|
||||
{
|
||||
Log::ArgumentNullException();
|
||||
return true;
|
||||
}
|
||||
const int32 lodCount = GetLODsCount();
|
||||
if (lodCount == 0 || lodCount > MODEL_MAX_LODS)
|
||||
{
|
||||
Log::ArgumentOutOfRangeException();
|
||||
return true;
|
||||
}
|
||||
if (Materials.IsEmpty())
|
||||
{
|
||||
Log::ArgumentOutOfRangeException(TEXT("MaterialSlots"), TEXT("Material slots collection cannot be empty."));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Min Screen Size
|
||||
stream->WriteFloat(MinScreenSize);
|
||||
|
||||
// Amount of material slots
|
||||
stream->WriteInt32(Materials.Count());
|
||||
|
||||
// For each material slot
|
||||
for (int32 materialSlotIndex = 0; materialSlotIndex < Materials.Count(); materialSlotIndex++)
|
||||
{
|
||||
auto& slot = Materials[materialSlotIndex];
|
||||
|
||||
stream->Write(slot.AssetID);
|
||||
stream->WriteByte(static_cast<byte>(slot.ShadowsMode));
|
||||
stream->WriteString(slot.Name, 11);
|
||||
}
|
||||
|
||||
// Amount of LODs
|
||||
stream->WriteByte(lodCount);
|
||||
|
||||
// For each LOD
|
||||
for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
|
||||
{
|
||||
auto& lod = LODs[lodIndex];
|
||||
|
||||
// Screen Size
|
||||
stream->WriteFloat(lod.ScreenSize);
|
||||
|
||||
// Amount of meshes
|
||||
const int32 meshes = lod.Meshes.Count();
|
||||
if (meshes == 0)
|
||||
{
|
||||
LOG(Warning, "Empty LOD.");
|
||||
return true;
|
||||
}
|
||||
if (meshes > MODEL_MAX_MESHES)
|
||||
{
|
||||
LOG(Warning, "Too many meshes per LOD.");
|
||||
return true;
|
||||
}
|
||||
stream->WriteUint16(meshes);
|
||||
|
||||
// For each mesh
|
||||
for (int32 meshIndex = 0; meshIndex < meshes; meshIndex++)
|
||||
{
|
||||
auto& mesh = *lod.Meshes[meshIndex];
|
||||
|
||||
// Material Slot
|
||||
stream->WriteInt32(mesh.MaterialSlotIndex);
|
||||
|
||||
// Box
|
||||
BoundingBox box;
|
||||
mesh.CalculateBox(box);
|
||||
stream->WriteBoundingBox(box);
|
||||
|
||||
// Sphere
|
||||
BoundingSphere sphere;
|
||||
mesh.CalculateSphere(sphere);
|
||||
stream->WriteBoundingSphere(sphere);
|
||||
|
||||
// TODO: calculate Sphere and Box at once - make it faster using SSE
|
||||
|
||||
// Has Lightmap UVs
|
||||
stream->WriteBool(mesh.LightmapUVs.HasItems());
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ModelData::Pack2SkinnedModelHeader(WriteStream* stream) const
|
||||
{
|
||||
// Validate input
|
||||
if (stream == nullptr)
|
||||
{
|
||||
Log::ArgumentNullException();
|
||||
return true;
|
||||
}
|
||||
const int32 lodCount = GetLODsCount();
|
||||
if (lodCount > MODEL_MAX_LODS)
|
||||
{
|
||||
Log::ArgumentOutOfRangeException();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Version
|
||||
stream->WriteByte(1);
|
||||
|
||||
// Min Screen Size
|
||||
stream->WriteFloat(MinScreenSize);
|
||||
|
||||
// Amount of material slots
|
||||
stream->WriteInt32(Materials.Count());
|
||||
|
||||
// For each material slot
|
||||
for (int32 materialSlotIndex = 0; materialSlotIndex < Materials.Count(); materialSlotIndex++)
|
||||
{
|
||||
auto& slot = Materials[materialSlotIndex];
|
||||
stream->Write(slot.AssetID);
|
||||
stream->WriteByte(static_cast<byte>(slot.ShadowsMode));
|
||||
stream->WriteString(slot.Name, 11);
|
||||
}
|
||||
|
||||
// Amount of LODs
|
||||
stream->WriteByte(lodCount);
|
||||
|
||||
// For each LOD
|
||||
for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
|
||||
{
|
||||
auto& lod = LODs[lodIndex];
|
||||
|
||||
// Screen Size
|
||||
stream->WriteFloat(lod.ScreenSize);
|
||||
|
||||
// Amount of meshes
|
||||
const int32 meshes = lod.Meshes.Count();
|
||||
if (meshes > MODEL_MAX_MESHES)
|
||||
{
|
||||
LOG(Warning, "Too many meshes per LOD.");
|
||||
return true;
|
||||
}
|
||||
stream->WriteUint16(meshes);
|
||||
|
||||
// For each mesh
|
||||
for (int32 meshIndex = 0; meshIndex < meshes; meshIndex++)
|
||||
{
|
||||
auto& mesh = *lod.Meshes[meshIndex];
|
||||
|
||||
// Material Slot
|
||||
stream->WriteInt32(mesh.MaterialSlotIndex);
|
||||
|
||||
// Box
|
||||
BoundingBox box;
|
||||
mesh.CalculateBox(box);
|
||||
stream->WriteBoundingBox(box);
|
||||
|
||||
// Sphere
|
||||
BoundingSphere sphere;
|
||||
mesh.CalculateSphere(sphere);
|
||||
stream->WriteBoundingSphere(sphere);
|
||||
|
||||
// TODO: calculate Sphere and Box at once - make it faster using SSE
|
||||
|
||||
// Blend Shapes
|
||||
const int32 blendShapes = mesh.BlendShapes.Count();
|
||||
stream->WriteUint16(blendShapes);
|
||||
for (int32 blendShapeIndex = 0; blendShapeIndex < blendShapes; blendShapeIndex++)
|
||||
{
|
||||
auto& blendShape = mesh.BlendShapes[blendShapeIndex];
|
||||
stream->WriteString(blendShape.Name, 13);
|
||||
stream->WriteFloat(blendShape.Weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skeleton
|
||||
{
|
||||
stream->WriteInt32(Skeleton.Nodes.Count());
|
||||
|
||||
// For each node
|
||||
for (int32 nodeIndex = 0; nodeIndex < Skeleton.Nodes.Count(); nodeIndex++)
|
||||
{
|
||||
auto& node = Skeleton.Nodes[nodeIndex];
|
||||
|
||||
stream->Write(node.ParentIndex);
|
||||
stream->WriteTransform(node.LocalTransform);
|
||||
stream->WriteString(node.Name, 71);
|
||||
}
|
||||
|
||||
stream->WriteInt32(Skeleton.Bones.Count());
|
||||
|
||||
// For each bone
|
||||
for (int32 boneIndex = 0; boneIndex < Skeleton.Bones.Count(); boneIndex++)
|
||||
{
|
||||
auto& bone = Skeleton.Bones[boneIndex];
|
||||
|
||||
stream->Write(bone.ParentIndex);
|
||||
stream->Write(bone.NodeIndex);
|
||||
stream->WriteTransform(bone.LocalTransform);
|
||||
stream->Write(bone.OffsetMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
// Retargeting
|
||||
{
|
||||
stream->WriteInt32(0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ModelData::Pack2AnimationHeader(WriteStream* stream, int32 animIndex) const
|
||||
{
|
||||
// Validate input
|
||||
if (stream == nullptr || animIndex < 0 || animIndex >= Animations.Count())
|
||||
{
|
||||
Log::ArgumentNullException();
|
||||
return true;
|
||||
}
|
||||
auto& anim = Animations.Get()[animIndex];
|
||||
if (anim.Duration <= ZeroTolerance || anim.FramesPerSecond <= ZeroTolerance)
|
||||
{
|
||||
Log::InvalidOperationException(TEXT("Invalid animation duration."));
|
||||
return true;
|
||||
}
|
||||
if (anim.Channels.IsEmpty())
|
||||
{
|
||||
Log::ArgumentOutOfRangeException(TEXT("Channels"), TEXT("Animation channels collection cannot be empty."));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Info
|
||||
stream->WriteInt32(103); // Header version (for fast version upgrades without serialization format change)
|
||||
stream->WriteDouble(anim.Duration);
|
||||
stream->WriteDouble(anim.FramesPerSecond);
|
||||
stream->WriteByte((byte)anim.RootMotionFlags);
|
||||
stream->WriteString(anim.RootNodeName, 13);
|
||||
|
||||
// Animation channels
|
||||
stream->WriteInt32(anim.Channels.Count());
|
||||
for (int32 i = 0; i < anim.Channels.Count(); i++)
|
||||
{
|
||||
auto& channel = anim.Channels[i];
|
||||
stream->WriteString(channel.NodeName, 172);
|
||||
Serialization::Serialize(*stream, channel.Position);
|
||||
Serialization::Serialize(*stream, channel.Rotation);
|
||||
Serialization::Serialize(*stream, channel.Scale);
|
||||
}
|
||||
|
||||
// Animation events
|
||||
stream->WriteInt32(anim.Events.Count());
|
||||
for (auto& e : anim.Events)
|
||||
{
|
||||
stream->WriteString(e.First, 172);
|
||||
stream->WriteInt32(e.Second.GetKeyframes().Count());
|
||||
for (const auto& k : e.Second.GetKeyframes())
|
||||
{
|
||||
stream->WriteFloat(k.Time);
|
||||
stream->WriteFloat(k.Value.Duration);
|
||||
stream->WriteStringAnsi(k.Value.TypeName, 17);
|
||||
stream->WriteJson(k.Value.JsonData);
|
||||
}
|
||||
}
|
||||
|
||||
// Nested animations
|
||||
stream->WriteInt32(0);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user