You're breathtaking!
This commit is contained in:
468
Source/Engine/Content/Assets/Animation.cpp
Normal file
468
Source/Engine/Content/Assets/Animation.cpp
Normal file
@@ -0,0 +1,468 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Animation.h"
|
||||
#include "SkinnedModel.h"
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Profiler/ProfilerCPU.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
#include "Engine/Serialization/MemoryReadStream.h"
|
||||
#if USE_EDITOR
|
||||
#include "Engine/Serialization/MemoryWriteStream.h"
|
||||
#endif
|
||||
|
||||
REGISTER_BINARY_ASSET(Animation, "FlaxEngine.Animation", nullptr, false);
|
||||
|
||||
Animation::Animation(const SpawnParams& params, const AssetInfo* info)
|
||||
: BinaryAsset(params, info)
|
||||
{
|
||||
}
|
||||
|
||||
Animation::InfoData Animation::GetInfo() const
|
||||
{
|
||||
InfoData info;
|
||||
if (IsLoaded())
|
||||
{
|
||||
info.Length = Data.GetLength();
|
||||
info.FramesCount = (int32)Data.Duration;
|
||||
info.ChannelsCount = Data.Channels.Count();
|
||||
info.KeyframesCount = Data.GetKeyframesCount();
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Length = 0.0f;
|
||||
info.FramesCount = 0;
|
||||
info.ChannelsCount = 0;
|
||||
info.KeyframesCount = 0;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
void Animation::ClearCache()
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Unlink events
|
||||
for (auto i = MappingCache.Begin(); i.IsNotEnd(); ++i)
|
||||
{
|
||||
i->Key->OnUnloaded.Unbind<Animation, &Animation::OnSkinnedModelUnloaded>(this);
|
||||
i->Key->OnReloading.Unbind<Animation, &Animation::OnSkinnedModelUnloaded>(this);
|
||||
}
|
||||
|
||||
// Free memory
|
||||
MappingCache.Clear();
|
||||
MappingCache.Cleanup();
|
||||
}
|
||||
|
||||
const Animation::NodeToChannel* Animation::GetMapping(SkinnedModel* obj)
|
||||
{
|
||||
ASSERT(obj && obj->IsLoaded() && IsLoaded());
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Try quick lookup
|
||||
NodeToChannel* result = MappingCache.TryGet(obj);
|
||||
if (result == nullptr)
|
||||
{
|
||||
PROFILE_CPU();
|
||||
|
||||
// Add to cache
|
||||
NodeToChannel tmp;
|
||||
auto bucket = MappingCache.Add(obj, tmp);
|
||||
result = &bucket->Value;
|
||||
obj->OnUnloaded.Bind<Animation, &Animation::OnSkinnedModelUnloaded>(this);
|
||||
obj->OnReloading.Bind<Animation, &Animation::OnSkinnedModelUnloaded>(this);
|
||||
|
||||
// Initialize the mapping
|
||||
const auto& skeleton = obj->Skeleton;
|
||||
const int32 nodesCount = skeleton.Nodes.Count();
|
||||
result->Resize(nodesCount, false);
|
||||
result->SetAll(-1);
|
||||
for (int32 i = 0; i < Data.Channels.Count(); i++)
|
||||
{
|
||||
auto& nodeAnim = Data.Channels[i];
|
||||
|
||||
for (int32 j = 0; j < nodesCount; j++)
|
||||
{
|
||||
if (skeleton.Nodes[j].Name == nodeAnim.NodeName)
|
||||
{
|
||||
result->At(j) = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
void Animation::LoadTimeline(BytesContainer& result) const
|
||||
{
|
||||
result.Release();
|
||||
if (!IsLoaded())
|
||||
return;
|
||||
MemoryWriteStream stream(4096);
|
||||
|
||||
// Version
|
||||
stream.WriteInt32(3);
|
||||
|
||||
// Meta
|
||||
float fps = (float)Data.FramesPerSecond;
|
||||
const float fpsInv = 1.0f / fps;
|
||||
stream.WriteFloat(fps);
|
||||
stream.WriteInt32((int32)Data.Duration);
|
||||
int32 tracksCount = Data.Channels.Count();
|
||||
for (auto& channel : Data.Channels)
|
||||
tracksCount +=
|
||||
(channel.Position.GetKeyframes().HasItems() ? 1 : 0) +
|
||||
(channel.Rotation.GetKeyframes().HasItems() ? 1 : 0) +
|
||||
(channel.Scale.GetKeyframes().HasItems() ? 1 : 0);
|
||||
stream.WriteInt32(tracksCount);
|
||||
|
||||
// Tracks
|
||||
int32 trackIndex = 0;
|
||||
for (int32 i = 0; i < Data.Channels.Count(); i++)
|
||||
{
|
||||
auto& channel = Data.Channels[i];
|
||||
const int32 childrenCount =
|
||||
(channel.Position.GetKeyframes().HasItems() ? 1 : 0) +
|
||||
(channel.Rotation.GetKeyframes().HasItems() ? 1 : 0) +
|
||||
(channel.Scale.GetKeyframes().HasItems() ? 1 : 0);
|
||||
|
||||
// Animation Channel track
|
||||
stream.WriteByte(17); // Track Type
|
||||
stream.WriteByte(0); // Track Flags
|
||||
stream.WriteInt32(-1); // Parent Index
|
||||
stream.WriteInt32(childrenCount); // Children Count
|
||||
stream.WriteString(channel.NodeName, -13); // Name
|
||||
stream.Write(&Color32::White); // Color
|
||||
const int32 parentIndex = trackIndex++;
|
||||
|
||||
auto& position = channel.Position.GetKeyframes();
|
||||
if (position.HasItems())
|
||||
{
|
||||
// Animation Channel Data track (position)
|
||||
stream.WriteByte(18); // Track Type
|
||||
stream.WriteByte(0); // Track Flags
|
||||
stream.WriteInt32(parentIndex); // Parent Index
|
||||
stream.WriteInt32(0); // Children Count
|
||||
stream.WriteString(String::Format(TEXT("Track_{0}_Position"), i), -13); // Name
|
||||
stream.Write(&Color32::White); // Color
|
||||
stream.WriteByte(0); // Type
|
||||
stream.WriteInt32(position.Count()); // Keyframes Count
|
||||
for (auto& k : position)
|
||||
{
|
||||
stream.WriteFloat(k.Time * fpsInv);
|
||||
stream.Write(&k.Value);
|
||||
}
|
||||
trackIndex++;
|
||||
}
|
||||
|
||||
auto& rotation = channel.Rotation.GetKeyframes();
|
||||
if (rotation.HasItems())
|
||||
{
|
||||
// Animation Channel Data track (rotation)
|
||||
stream.WriteByte(18); // Track Type
|
||||
stream.WriteByte(0); // Track Flags
|
||||
stream.WriteInt32(parentIndex); // Parent Index
|
||||
stream.WriteInt32(0); // Children Count
|
||||
stream.WriteString(String::Format(TEXT("Track_{0}_Rotation"), i), -13); // Name
|
||||
stream.Write(&Color32::White); // Color
|
||||
stream.WriteByte(1); // Type
|
||||
stream.WriteInt32(rotation.Count()); // Keyframes Count
|
||||
for (auto& k : rotation)
|
||||
{
|
||||
stream.WriteFloat(k.Time * fpsInv);
|
||||
stream.Write(&k.Value);
|
||||
}
|
||||
trackIndex++;
|
||||
}
|
||||
|
||||
auto& scale = channel.Scale.GetKeyframes();
|
||||
if (scale.HasItems())
|
||||
{
|
||||
// Animation Channel Data track (scale)
|
||||
stream.WriteByte(18); // Track Type
|
||||
stream.WriteByte(0); // Track Flags
|
||||
stream.WriteInt32(parentIndex); // Parent Index
|
||||
stream.WriteInt32(0); // Children Count
|
||||
stream.WriteString(String::Format(TEXT("Track_{0}_Scale"), i), -13); // Name
|
||||
stream.Write(&Color32::White); // Color
|
||||
stream.WriteByte(2); // Type
|
||||
stream.WriteInt32(scale.Count()); // Keyframes Count
|
||||
for (auto& k : scale)
|
||||
{
|
||||
stream.WriteFloat(k.Time * fpsInv);
|
||||
stream.Write(&k.Value);
|
||||
}
|
||||
trackIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
result.Copy(stream.GetHandle(), stream.GetPosition());
|
||||
}
|
||||
|
||||
bool Animation::SaveTimeline(BytesContainer& data)
|
||||
{
|
||||
// Wait for asset to be loaded or don't if last load failed (eg. by shader source compilation error)
|
||||
if (LastLoadFailed())
|
||||
{
|
||||
LOG(Warning, "Saving asset that failed to load.");
|
||||
}
|
||||
else if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
MemoryReadStream stream(data.Get(), data.Length());
|
||||
|
||||
// Version
|
||||
int32 version;
|
||||
stream.ReadInt32(&version);
|
||||
if (version != 3)
|
||||
{
|
||||
LOG(Error, "Unknown timeline data version {0}.", version);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Meta
|
||||
float fps;
|
||||
stream.ReadFloat(&fps);
|
||||
Data.FramesPerSecond = static_cast<double>(fps);
|
||||
int32 duration;
|
||||
stream.ReadInt32(&duration);
|
||||
Data.Duration = static_cast<double>(duration);
|
||||
int32 tracksCount;
|
||||
stream.ReadInt32(&tracksCount);
|
||||
|
||||
// Tracks
|
||||
Data.Channels.Clear();
|
||||
Dictionary<int32, int32> animationChannelTrackIndexToChannelIndex;
|
||||
animationChannelTrackIndexToChannelIndex.EnsureCapacity(tracksCount * 3);
|
||||
for (int32 trackIndex = 0; trackIndex < tracksCount; trackIndex++)
|
||||
{
|
||||
const byte trackType = stream.ReadByte();
|
||||
const byte trackFlags = stream.ReadByte();
|
||||
int32 parentIndex, childrenCount;
|
||||
stream.ReadInt32(&parentIndex);
|
||||
stream.ReadInt32(&childrenCount);
|
||||
String name;
|
||||
stream.ReadString(&name, -13);
|
||||
Color32 color;
|
||||
stream.Read(&color);
|
||||
switch (trackType)
|
||||
{
|
||||
case 17:
|
||||
{
|
||||
// Animation Channel track
|
||||
const int32 channelIndex = Data.Channels.Count();
|
||||
animationChannelTrackIndexToChannelIndex[trackIndex] = channelIndex;
|
||||
auto& channel = Data.Channels.AddOne();
|
||||
channel.NodeName = name;
|
||||
break;
|
||||
}
|
||||
case 18:
|
||||
{
|
||||
// Animation Channel Data track
|
||||
const byte type = stream.ReadByte();
|
||||
int32 keyframesCount;
|
||||
stream.ReadInt32(&keyframesCount);
|
||||
int32 channelIndex;
|
||||
if (!animationChannelTrackIndexToChannelIndex.TryGet(parentIndex, channelIndex))
|
||||
{
|
||||
LOG(Error, "Invalid animation channel data track parent linkage.");
|
||||
return true;
|
||||
}
|
||||
auto& channel = Data.Channels[channelIndex];
|
||||
switch (type)
|
||||
{
|
||||
case 0:
|
||||
channel.Position.Resize(keyframesCount);
|
||||
for (int32 i = 0; i < keyframesCount; i++)
|
||||
{
|
||||
LinearCurveKeyframe<Vector3>& k = channel.Position.GetKeyframes()[i];
|
||||
stream.ReadFloat(&k.Time);
|
||||
k.Time *= fps;
|
||||
stream.Read(&k.Value);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
channel.Rotation.Resize(keyframesCount);
|
||||
for (int32 i = 0; i < keyframesCount; i++)
|
||||
{
|
||||
LinearCurveKeyframe<Quaternion>& k = channel.Rotation.GetKeyframes()[i];
|
||||
stream.ReadFloat(&k.Time);
|
||||
k.Time *= fps;
|
||||
stream.Read(&k.Value);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
channel.Scale.Resize(keyframesCount);
|
||||
for (int32 i = 0; i < keyframesCount; i++)
|
||||
{
|
||||
LinearCurveKeyframe<Vector3>& k = channel.Scale.GetKeyframes()[i];
|
||||
stream.ReadFloat(&k.Time);
|
||||
k.Time *= fps;
|
||||
stream.Read(&k.Value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG(Error, "Unsupported track type {0} for animation.", trackType);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (stream.GetLength() != stream.GetPosition())
|
||||
{
|
||||
LOG(Warning, "Invalid animation timeline data length.");
|
||||
}
|
||||
|
||||
return Save();
|
||||
}
|
||||
|
||||
bool Animation::Save(const StringView& path)
|
||||
{
|
||||
// Wait for asset to be loaded or don't if last load failed (eg. by shader source compilation error)
|
||||
if (LastLoadFailed())
|
||||
{
|
||||
LOG(Warning, "Saving asset that failed to load.");
|
||||
}
|
||||
else if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Serialize animation data to the stream
|
||||
{
|
||||
MemoryWriteStream stream(4096);
|
||||
|
||||
// Info
|
||||
stream.WriteInt32(100);
|
||||
stream.WriteDouble(Data.Duration);
|
||||
stream.WriteDouble(Data.FramesPerSecond);
|
||||
stream.WriteBool(Data.EnableRootMotion);
|
||||
stream.WriteString(Data.RootNodeName, 13);
|
||||
|
||||
// Animation channels
|
||||
stream.WriteInt32(Data.Channels.Count());
|
||||
for (int32 i = 0; i < Data.Channels.Count(); i++)
|
||||
{
|
||||
auto& anim = Data.Channels[i];
|
||||
stream.WriteString(anim.NodeName, 172);
|
||||
anim.Position.Serialize(stream);
|
||||
anim.Rotation.Serialize(stream);
|
||||
anim.Scale.Serialize(stream);
|
||||
}
|
||||
|
||||
// Set data to the chunk asset
|
||||
auto chunk0 = GetOrCreateChunk(0);
|
||||
ASSERT(chunk0 != nullptr);
|
||||
chunk0->Data.Copy(stream.GetHandle(), stream.GetPosition());
|
||||
}
|
||||
|
||||
// Save
|
||||
AssetInitData data;
|
||||
data.SerializedVersion = SerializedVersion;
|
||||
const bool saveResult = path.HasChars() ? SaveAsset(path, data) : SaveAsset(data, true);
|
||||
if (saveResult)
|
||||
{
|
||||
LOG(Error, "Cannot save \'{0}\'", ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void Animation::OnSkinnedModelUnloaded(Asset* obj)
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
const auto key = static_cast<SkinnedModel*>(obj);
|
||||
auto i = MappingCache.Find(key);
|
||||
ASSERT(i != MappingCache.End());
|
||||
|
||||
// Unlink event
|
||||
key->OnUnloaded.Unbind<Animation, &Animation::OnSkinnedModelUnloaded>(this);
|
||||
key->OnReloading.Unbind<Animation, &Animation::OnSkinnedModelUnloaded>(this);
|
||||
|
||||
// Clear cache
|
||||
i->Value.Resize(0, false);
|
||||
MappingCache.Remove(i);
|
||||
}
|
||||
|
||||
Asset::LoadResult Animation::load()
|
||||
{
|
||||
// Get stream with animations data
|
||||
const auto dataChunk = GetChunk(0);
|
||||
if (dataChunk == nullptr)
|
||||
return LoadResult::MissingDataChunk;
|
||||
MemoryReadStream stream(dataChunk->Get(), dataChunk->Size());
|
||||
|
||||
// Info
|
||||
int32 headerVersion = *(int32*)stream.GetPositionHandle();
|
||||
switch (headerVersion)
|
||||
{
|
||||
case 100:
|
||||
{
|
||||
stream.ReadInt32(&headerVersion);
|
||||
stream.ReadDouble(&Data.Duration);
|
||||
stream.ReadDouble(&Data.FramesPerSecond);
|
||||
Data.EnableRootMotion = stream.ReadBool();
|
||||
stream.ReadString(&Data.RootNodeName, 13);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
stream.ReadDouble(&Data.Duration);
|
||||
stream.ReadDouble(&Data.FramesPerSecond);
|
||||
break;
|
||||
}
|
||||
if (Data.Duration < ZeroTolerance || Data.FramesPerSecond < ZeroTolerance)
|
||||
{
|
||||
LOG(Warning, "Invalid animation info");
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
|
||||
// Animation channels
|
||||
int32 animationsCount;
|
||||
stream.ReadInt32(&animationsCount);
|
||||
Data.Channels.Resize(animationsCount, false);
|
||||
for (int32 i = 0; i < animationsCount; i++)
|
||||
{
|
||||
auto& anim = Data.Channels[i];
|
||||
|
||||
stream.ReadString(&anim.NodeName, 172);
|
||||
bool failed = anim.Position.Deserialize(stream);
|
||||
failed |= anim.Rotation.Deserialize(stream);
|
||||
failed |= anim.Scale.Deserialize(stream);
|
||||
|
||||
if (failed)
|
||||
{
|
||||
LOG(Warning, "Failed to deserialize the animation curve data.");
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
void Animation::unload(bool isReloading)
|
||||
{
|
||||
ClearCache();
|
||||
Data.Dispose();
|
||||
}
|
||||
|
||||
AssetChunksFlag Animation::getChunksToPreload() const
|
||||
{
|
||||
return GET_CHUNK_FLAG(0);
|
||||
}
|
||||
148
Source/Engine/Content/Assets/Animation.h
Normal file
148
Source/Engine/Content/Assets/Animation.h
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../BinaryAsset.h"
|
||||
#include "Engine/Core/Collections/Dictionary.h"
|
||||
#include "Engine/Animations/AnimationData.h"
|
||||
|
||||
class SkinnedModel;
|
||||
|
||||
/// <summary>
|
||||
/// Asset that contains an animation spline represented by a set of keyframes, each representing an endpoint of a linear curve.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API Animation : public BinaryAsset
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(Animation, 1);
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Contains basic information about the animation asset contents.
|
||||
/// </summary>
|
||||
API_STRUCT() struct InfoData
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_NO_SPAWN(InfoData);
|
||||
|
||||
/// <summary>
|
||||
/// Length of the animation in seconds.
|
||||
/// </summary>
|
||||
API_FIELD() float Length;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of animation frames (some curve tracks may use less keyframes).
|
||||
/// </summary>
|
||||
API_FIELD() int32 FramesCount;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of animation channel tracks.
|
||||
/// </summary>
|
||||
API_FIELD() int32 ChannelsCount;
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of keyframes in the animation tracks.
|
||||
/// </summary>
|
||||
API_FIELD() int32 KeyframesCount;
|
||||
};
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The animation data.
|
||||
/// </summary>
|
||||
AnimationData Data;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the mapping for every skeleton node to the animation data channels.
|
||||
/// Can be used for a simple lookup or to check if a given node is animated (unused nodes are using -1 index).
|
||||
/// </summary>
|
||||
typedef Array<int32> NodeToChannel;
|
||||
|
||||
/// <summary>
|
||||
/// The skeleton nodes to animation channel indices mapping cache. Use it as read-only. It's being maintained internally by the asset.
|
||||
/// </summary>
|
||||
Dictionary<SkinnedModel*, NodeToChannel> MappingCache;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length of the animation (in seconds).
|
||||
/// </summary>
|
||||
/// <returns>The length in seconds.</returns>
|
||||
API_PROPERTY() float GetLength() const
|
||||
{
|
||||
return IsLoaded() ? Data.GetLength() : 0.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duration of the animation (in frames).
|
||||
/// </summary>
|
||||
/// <returns>The duration in frames.</returns>
|
||||
API_PROPERTY() float GetDuration() const
|
||||
{
|
||||
return (float)Data.Duration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of the animation frames per second.
|
||||
/// </summary>
|
||||
/// <returns>The frames per second.</returns>
|
||||
API_PROPERTY() float GetFramesPerSecond() const
|
||||
{
|
||||
return (float)Data.FramesPerSecond;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the animation clip info.
|
||||
/// </summary>
|
||||
/// <returns>The animation info.</returns>
|
||||
API_PROPERTY() InfoData GetInfo() const;
|
||||
|
||||
/// <summary>
|
||||
/// Clears the skeleton mapping cache.
|
||||
/// </summary>
|
||||
void ClearCache();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the skeleton mapping cache.
|
||||
/// </summary>
|
||||
/// <param name="obj">The target skinned model to get mapping to its skeleton.</param>
|
||||
/// <returns>The cached node-to-channel mapping for the fast animation sampling for the skinned model skeleton nodes.</returns>
|
||||
const NodeToChannel* GetMapping(SkinnedModel* obj);
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Gets the animation as serialized timeline data. Used to show it in Editor.
|
||||
/// </summary>
|
||||
/// <param name="result">The output timeline data container. Empty if failed to load.</param>
|
||||
API_FUNCTION() void LoadTimeline(API_PARAM(Out) BytesContainer& result) const;
|
||||
|
||||
/// <summary>
|
||||
/// Saves the serialized timeline data to the asset as animation.
|
||||
/// </summary>
|
||||
/// <remarks>The cannot be used by virtual assets.</remarks>
|
||||
/// <param name="data">The timeline data container.</param>
|
||||
/// <returns><c>true</c> failed to save data; otherwise, <c>false</c>.</returns>
|
||||
API_FUNCTION() bool SaveTimeline(BytesContainer& data);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the animation data to the asset. Supported only in Editor.
|
||||
/// </summary>
|
||||
/// <remarks>The cannot be used by virtual assets.</remarks>
|
||||
/// <returns><c>true</c> failed to save data; otherwise, <c>false</c>.</returns>
|
||||
bool Save(const StringView& path = StringView::Empty);
|
||||
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
void OnSkinnedModelUnloaded(Asset* obj);
|
||||
|
||||
protected:
|
||||
|
||||
// [BinaryAsset]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
AssetChunksFlag getChunksToPreload() const override;
|
||||
};
|
||||
139
Source/Engine/Content/Assets/AnimationGraph.cpp
Normal file
139
Source/Engine/Content/Assets/AnimationGraph.cpp
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "AnimationGraph.h"
|
||||
#if USE_EDITOR
|
||||
#include "AnimationGraphFunction.h"
|
||||
#endif
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Core/Types/DataContainer.h"
|
||||
#include "Engine/Serialization/MemoryReadStream.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
|
||||
REGISTER_BINARY_ASSET(AnimationGraph, "FlaxEngine.AnimationGraph", nullptr, false);
|
||||
|
||||
AnimationGraph::AnimationGraph(const SpawnParams& params, const AssetInfo* info)
|
||||
: BinaryAsset(params, info)
|
||||
, Graph(this)
|
||||
, GraphExecutor(Graph)
|
||||
{
|
||||
}
|
||||
|
||||
Asset::LoadResult AnimationGraph::load()
|
||||
{
|
||||
// Get stream with graph data
|
||||
const auto surfaceChunk = GetChunk(0);
|
||||
if (surfaceChunk == nullptr)
|
||||
return LoadResult::MissingDataChunk;
|
||||
MemoryReadStream stream(surfaceChunk->Get(), surfaceChunk->Size());
|
||||
|
||||
// Load graph
|
||||
if (Graph.Load(&stream, USE_EDITOR))
|
||||
{
|
||||
LOG(Warning, "Failed to load animation graph \'{0}\'", ToString());
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
// Find asset dependencies to nested anim graph functions
|
||||
ClearDependencies();
|
||||
FindDependencies(&Graph);
|
||||
#endif
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
void AnimationGraph::unload(bool isReloading)
|
||||
{
|
||||
Graph.Clear();
|
||||
}
|
||||
|
||||
AssetChunksFlag AnimationGraph::getChunksToPreload() const
|
||||
{
|
||||
return GET_CHUNK_FLAG(0);
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
void AnimationGraph::OnDependencyModified(BinaryAsset* asset)
|
||||
{
|
||||
BinaryAsset::OnDependencyModified(asset);
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
BytesContainer AnimationGraph::LoadSurface()
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
if (!LoadChunks(GET_CHUNK_FLAG(0)))
|
||||
{
|
||||
const auto data = GetChunk(0);
|
||||
BytesContainer result;
|
||||
result.Copy(data->Data);
|
||||
return result;
|
||||
}
|
||||
|
||||
LOG(Warning, "Animation Graph \'{0}\' surface data is missing.", GetPath());
|
||||
return BytesContainer();
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
bool AnimationGraph::SaveSurface(BytesContainer& data)
|
||||
{
|
||||
// Wait for asset to be loaded or don't if last load failed
|
||||
if (LastLoadFailed())
|
||||
{
|
||||
LOG(Warning, "Saving asset that failed to load.");
|
||||
}
|
||||
else if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Release all chunks
|
||||
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
|
||||
ReleaseChunk(i);
|
||||
|
||||
// Set Visject Surface data
|
||||
auto visjectSurfaceChunk = GetOrCreateChunk(0);
|
||||
ASSERT(visjectSurfaceChunk != nullptr);
|
||||
visjectSurfaceChunk->Data.Copy(data);
|
||||
|
||||
// Save
|
||||
AssetInitData assetData;
|
||||
assetData.SerializedVersion = 1;
|
||||
if (SaveAsset(assetData))
|
||||
{
|
||||
LOG(Error, "Cannot save \'{0}\'", ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AnimationGraph::FindDependencies(AnimGraphBase* graph)
|
||||
{
|
||||
for (const auto& node : graph->Nodes)
|
||||
{
|
||||
if (node.Type == GRAPH_NODE_MAKE_TYPE(9, 24))
|
||||
{
|
||||
const auto function = node.Assets[0].As<AnimationGraphFunction>();
|
||||
if (function)
|
||||
{
|
||||
AddDependency(function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto* subGraph : graph->SubGraphs)
|
||||
{
|
||||
FindDependencies(subGraph);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
81
Source/Engine/Content/Assets/AnimationGraph.h
Normal file
81
Source/Engine/Content/Assets/AnimationGraph.h
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../BinaryAsset.h"
|
||||
#include "Engine/Animations/Graph/AnimGraph.h"
|
||||
|
||||
/// <summary>
|
||||
/// The Animation Graph is used to evaluate a final pose for the animated model for the current frame.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API AnimationGraph : public BinaryAsset
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(AnimationGraph, 1);
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The animation graph.
|
||||
/// </summary>
|
||||
AnimGraph Graph;
|
||||
|
||||
/// <summary>
|
||||
/// The animation graph runtime executor.
|
||||
/// </summary>
|
||||
AnimGraphExecutor GraphExecutor;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base model asset used for the animation preview and the skeleton layout source.
|
||||
/// </summary>
|
||||
/// <returns>The base model.</returns>
|
||||
API_PROPERTY() SkinnedModel* GetBaseModel() const
|
||||
{
|
||||
return Graph.BaseModel.Get();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load surface graph from the asset.
|
||||
/// </summary>
|
||||
/// <returns>The surface data or empty if failed to load it.</returns>
|
||||
API_FUNCTION() BytesContainer LoadSurface();
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Updates the animation graph surface (save new one, discard cached data, reload asset).
|
||||
/// </summary>
|
||||
/// <param name="data">Stream with graph data.</param>
|
||||
/// <returns>True if cannot save it, otherwise false.</returns>
|
||||
API_FUNCTION() bool SaveSurface(BytesContainer& data);
|
||||
|
||||
private:
|
||||
|
||||
void FindDependencies(AnimGraphBase* graph);
|
||||
|
||||
#endif
|
||||
|
||||
public:
|
||||
|
||||
// [BinaryAsset]
|
||||
#if USE_EDITOR
|
||||
void GetReferences(Array<Guid>& output) const override
|
||||
{
|
||||
// Base
|
||||
BinaryAsset::GetReferences(output);
|
||||
|
||||
Graph.GetReferences(output);
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
|
||||
// [BinaryAsset]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
AssetChunksFlag getChunksToPreload() const override;
|
||||
#if USE_EDITOR
|
||||
void OnDependencyModified(BinaryAsset* asset) override;
|
||||
#endif
|
||||
};
|
||||
178
Source/Engine/Content/Assets/AnimationGraphFunction.cpp
Normal file
178
Source/Engine/Content/Assets/AnimationGraphFunction.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "AnimationGraphFunction.h"
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Core/Types/DataContainer.h"
|
||||
#include "Engine/Serialization/MemoryReadStream.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
|
||||
REGISTER_BINARY_ASSET(AnimationGraphFunction, "FlaxEngine.AnimationGraphFunction", nullptr, false);
|
||||
|
||||
AnimationGraphFunction::AnimationGraphFunction(const SpawnParams& params, const AssetInfo* info)
|
||||
: BinaryAsset(params, info)
|
||||
{
|
||||
}
|
||||
|
||||
Asset::LoadResult AnimationGraphFunction::load()
|
||||
{
|
||||
// Get graph data from chunk
|
||||
const auto surfaceChunk = GetChunk(0);
|
||||
if (!surfaceChunk || !surfaceChunk->IsLoaded())
|
||||
return LoadResult::MissingDataChunk;
|
||||
GraphData.Swap(surfaceChunk->Data);
|
||||
|
||||
// Load graph
|
||||
MemoryReadStream stream(GraphData.Get(), GraphData.Length());
|
||||
AnimGraph graph(this, true);
|
||||
if (graph.Load(&stream, false))
|
||||
return LoadResult::Failed;
|
||||
|
||||
// Load function signature
|
||||
// Note: search also the nested state machines graphs (state output and transition rule)
|
||||
ProcessGraphForSignature(&graph, true, Array<int32, FixedAllocation<8>>());
|
||||
if (Inputs.Count() >= 16 || Outputs.Count() >= 16)
|
||||
{
|
||||
LOG(Error, "Too many function inputs/outputs in '{0}'. The limit is max 16 inputs and max 16 outputs.", ToString());
|
||||
}
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
void AnimationGraphFunction::unload(bool isReloading)
|
||||
{
|
||||
GraphData.Release();
|
||||
Inputs.Clear();
|
||||
Outputs.Clear();
|
||||
}
|
||||
|
||||
AssetChunksFlag AnimationGraphFunction::getChunksToPreload() const
|
||||
{
|
||||
return GET_CHUNK_FLAG(0);
|
||||
}
|
||||
|
||||
BytesContainer AnimationGraphFunction::LoadSurface() const
|
||||
{
|
||||
BytesContainer result;
|
||||
ScopeLock lock(Locker);
|
||||
result.Link(GraphData);
|
||||
return result;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
void AnimationGraphFunction::GetSignature(Array<StringView, FixedAllocation<32>>& types, Array<StringView, FixedAllocation<32>>& names)
|
||||
{
|
||||
types.Resize(32);
|
||||
names.Resize(32);
|
||||
for (int32 i = 0; i < Inputs.Count(); i++)
|
||||
{
|
||||
auto& input = Inputs[i];
|
||||
types[i] = input.Type;
|
||||
names[i] = input.Name;
|
||||
}
|
||||
for (int32 i = 0; i < Outputs.Count(); i++)
|
||||
{
|
||||
auto& output = Outputs[i];
|
||||
types[i + 16] = output.Type;
|
||||
names[i + 16] = output.Name;
|
||||
}
|
||||
}
|
||||
|
||||
bool AnimationGraphFunction::SaveSurface(BytesContainer& data)
|
||||
{
|
||||
// Wait for asset to be loaded or don't if last load failed
|
||||
if (LastLoadFailed())
|
||||
{
|
||||
LOG(Warning, "Saving asset that failed to load.");
|
||||
}
|
||||
else if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Set Visject Surface data
|
||||
auto surfaceChunk = GetOrCreateChunk(0);
|
||||
ASSERT(surfaceChunk != nullptr);
|
||||
surfaceChunk->Data.Copy(data);
|
||||
|
||||
// Save asset
|
||||
AssetInitData initData;
|
||||
initData.SerializedVersion = 1;
|
||||
if (SaveAsset(initData))
|
||||
{
|
||||
LOG(Error, "Cannot save \'{0}\'", ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void AnimationGraphFunction::ProcessGraphForSignature(AnimGraphBase* graph, bool canUseOutputs, const Array<int32, FixedAllocation<8>>& graphIndices)
|
||||
{
|
||||
for (int32 i = 0; i < graph->Nodes.Count(); i++)
|
||||
{
|
||||
auto& node = graph->Nodes[i];
|
||||
|
||||
if (node.Type == GRAPH_NODE_MAKE_TYPE(16, 1)) // Function Input
|
||||
{
|
||||
if (Inputs.Count() < 16)
|
||||
{
|
||||
auto& p = Inputs.AddOne();
|
||||
p.NodeIndex = i;
|
||||
p.GraphIndices = graphIndices;
|
||||
#if USE_EDITOR
|
||||
p.Type = GetGraphFunctionTypeName_Deprecated(node.Values[0]);
|
||||
p.Name = (StringView)node.Values[1];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else if (node.Type == GRAPH_NODE_MAKE_TYPE(16, 2)) // Function Output
|
||||
{
|
||||
if (Outputs.Count() < 16 && canUseOutputs)
|
||||
{
|
||||
auto& p = Outputs.AddOne();
|
||||
p.NodeIndex = i;
|
||||
p.GraphIndices = graphIndices;
|
||||
#if USE_EDITOR
|
||||
p.Type = GetGraphFunctionTypeName_Deprecated(node.Values[0]);
|
||||
p.Name = (StringView)node.Values[1];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else if (node.Type == GRAPH_NODE_MAKE_TYPE(9, 18)) // State Machine
|
||||
{
|
||||
if (node.Data.StateMachine.Graph)
|
||||
{
|
||||
auto subGraph = graphIndices;
|
||||
subGraph.Add(graph->SubGraphs.Find(node.Data.StateMachine.Graph));
|
||||
ASSERT(subGraph.Last() != -1);
|
||||
ProcessGraphForSignature(node.Data.StateMachine.Graph, false, subGraph);
|
||||
}
|
||||
}
|
||||
else if (node.Type == GRAPH_NODE_MAKE_TYPE(9, 20)) // State
|
||||
{
|
||||
if (node.Data.State.Graph)
|
||||
{
|
||||
auto subGraph = graphIndices;
|
||||
subGraph.Add(graph->SubGraphs.Find(node.Data.State.Graph));
|
||||
ASSERT(subGraph.Last() != -1);
|
||||
ProcessGraphForSignature(node.Data.State.Graph, false, subGraph);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto& stateTransition : graph->StateTransitions)
|
||||
{
|
||||
if (stateTransition.RuleGraph)
|
||||
{
|
||||
auto subGraph = graphIndices;
|
||||
subGraph.Add(graph->SubGraphs.Find(stateTransition.RuleGraph));
|
||||
ASSERT(subGraph.Last() != -1);
|
||||
ProcessGraphForSignature(stateTransition.RuleGraph, false, subGraph);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
Source/Engine/Content/Assets/AnimationGraphFunction.h
Normal file
71
Source/Engine/Content/Assets/AnimationGraphFunction.h
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../BinaryAsset.h"
|
||||
#include "Engine/Animations/Graph/AnimGraph.h"
|
||||
|
||||
/// <summary>
|
||||
/// Animation Graph function asset that contains reusable part of the anim graph.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API AnimationGraphFunction : public BinaryAsset
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(AnimationGraphFunction, 1);
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The loaded anim graph function graph data (serialized anim graph).
|
||||
/// </summary>
|
||||
BytesContainer GraphData;
|
||||
|
||||
struct FunctionParameter
|
||||
{
|
||||
int32 NodeIndex;
|
||||
Array<int32, FixedAllocation<8>> GraphIndices;
|
||||
#if USE_EDITOR
|
||||
String Type;
|
||||
String Name;
|
||||
#endif
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The input nodes.
|
||||
/// </summary>
|
||||
Array<FunctionParameter, FixedAllocation<16>> Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// The output nodes.
|
||||
/// </summary>
|
||||
Array<FunctionParameter, FixedAllocation<16>> Outputs;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load surface graph from the asset.
|
||||
/// </summary>
|
||||
/// <returns>The output surface data, or empty if failed to load.</returns>
|
||||
API_FUNCTION() BytesContainer LoadSurface() const;
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
// Gets the function signature for Visject Surface editor.
|
||||
API_FUNCTION() void GetSignature(API_PARAM(Out) Array<StringView, FixedAllocation<32>>& types, API_PARAM(Out) Array<StringView, FixedAllocation<32>>& names);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the anim graph surface (save new one, discards cached data, reloads asset).
|
||||
/// </summary>
|
||||
/// <param name="data">The surface graph data.</param>
|
||||
/// <returns>True if cannot save it, otherwise false.</returns>
|
||||
API_FUNCTION() bool SaveSurface(BytesContainer& data);
|
||||
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
void ProcessGraphForSignature(AnimGraphBase* graph, bool canUseOutputs, const Array<int32, FixedAllocation<8>>& graphIndices);
|
||||
|
||||
protected:
|
||||
|
||||
// [BinaryAsset]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
AssetChunksFlag getChunksToPreload() const override;
|
||||
};
|
||||
12
Source/Engine/Content/Assets/CubeTexture.cpp
Normal file
12
Source/Engine/Content/Assets/CubeTexture.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "CubeTexture.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
#include "Engine/Content/Upgraders/TextureAssetUpgrader.h"
|
||||
|
||||
REGISTER_BINARY_ASSET(CubeTexture, "FlaxEngine.CubeTexture", ::New<TextureAssetUpgrader>(), true);
|
||||
|
||||
CubeTexture::CubeTexture(const SpawnParams& params, const AssetInfo* info)
|
||||
: TextureBase(params, info)
|
||||
{
|
||||
}
|
||||
13
Source/Engine/Content/Assets/CubeTexture.h
Normal file
13
Source/Engine/Content/Assets/CubeTexture.h
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Graphics/Textures/TextureBase.h"
|
||||
|
||||
/// <summary>
|
||||
/// Cube texture asset contains 6 images that is usually stored on a GPU as a cube map (one slice per each axis direction).
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API CubeTexture : public TextureBase
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(CubeTexture, TexturesSerializedVersion);
|
||||
};
|
||||
27
Source/Engine/Content/Assets/IESProfile.cpp
Normal file
27
Source/Engine/Content/Assets/IESProfile.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "IESProfile.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
#include "Engine/Content/Upgraders/TextureAssetUpgrader.h"
|
||||
|
||||
REGISTER_BINARY_ASSET(IESProfile, "FlaxEngine.IESProfile", ::New<TextureAssetUpgrader>(), false);
|
||||
|
||||
IESProfile::IESProfile(const SpawnParams& params, const AssetInfo* info)
|
||||
: TextureBase(params, info)
|
||||
, Brightness(0)
|
||||
, TextureMultiplier(1)
|
||||
{
|
||||
}
|
||||
|
||||
bool IESProfile::init(AssetInitData& initData)
|
||||
{
|
||||
// Base
|
||||
if (TextureBase::init(initData))
|
||||
return true;
|
||||
|
||||
// Get settings from texture header mini-storage
|
||||
auto data = (CustomDataLayout*)_texture.GetHeader()->CustomData;
|
||||
Brightness = data->Brightness;
|
||||
TextureMultiplier = data->TextureMultiplier;
|
||||
return false;
|
||||
}
|
||||
38
Source/Engine/Content/Assets/IESProfile.h
Normal file
38
Source/Engine/Content/Assets/IESProfile.h
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Graphics/Textures/TextureBase.h"
|
||||
|
||||
/// <summary>
|
||||
/// Contains IES profile texture used by the lights to simulate real life bulb light emission.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API IESProfile : public TextureBase
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(IESProfile, TexturesSerializedVersion);
|
||||
|
||||
public:
|
||||
|
||||
struct CustomDataLayout
|
||||
{
|
||||
float Brightness;
|
||||
float TextureMultiplier;
|
||||
};
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The light brightness in Lumens, imported from IES profile.
|
||||
/// </summary>
|
||||
API_FIELD() float Brightness;
|
||||
|
||||
/// <summary>
|
||||
/// The multiplier to map texture value to result to integrate over the sphere to 1.
|
||||
/// </summary>
|
||||
API_FIELD() float TextureMultiplier;
|
||||
|
||||
protected:
|
||||
|
||||
// [BinaryAsset]
|
||||
bool init(AssetInitData& initData) override;
|
||||
};
|
||||
554
Source/Engine/Content/Assets/Material.cpp
Normal file
554
Source/Engine/Content/Assets/Material.cpp
Normal file
@@ -0,0 +1,554 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Material.h"
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Core/Types/DataContainer.h"
|
||||
#include "Engine/Content/Upgraders/ShaderAssetUpgrader.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
#include "Engine/Graphics/RenderTools.h"
|
||||
#include "Engine/Graphics/Materials/MaterialShader.h"
|
||||
#include "Engine/Graphics/Shaders/Cache/ShaderCacheManager.h"
|
||||
#include "Engine/Graphics/Shaders/Cache/ShaderStorage.h"
|
||||
#include "Engine/Serialization/MemoryReadStream.h"
|
||||
#if COMPILE_WITH_SHADER_COMPILER
|
||||
#include "MaterialFunction.h"
|
||||
#include "Engine/Serialization/MemoryWriteStream.h"
|
||||
#include "Engine/Utilities/Encryption.h"
|
||||
#include "Engine/Tools/MaterialGenerator/MaterialGenerator.h"
|
||||
#include "Engine/ShadersCompilation/Config.h"
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Enable/disable automatic material shader source code generation (if missing)
|
||||
/// </summary>
|
||||
#define MATERIAL_AUTO_GENERATE_MISSING_SOURCE (USE_EDITOR)
|
||||
|
||||
REGISTER_BINARY_ASSET(Material, "FlaxEngine.Material", ::New<ShaderAssetUpgrader>(), false);
|
||||
|
||||
Material::Material(const SpawnParams& params, const AssetInfo* info)
|
||||
: ShaderAssetTypeBase<MaterialBase>(params, info)
|
||||
{
|
||||
}
|
||||
|
||||
bool Material::IsMaterialInstance() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const MaterialInfo& Material::GetInfo() const
|
||||
{
|
||||
if (_materialShader)
|
||||
return _materialShader->GetInfo();
|
||||
|
||||
static MaterialInfo EmptyInfo;
|
||||
return EmptyInfo;
|
||||
}
|
||||
|
||||
bool Material::IsReady() const
|
||||
{
|
||||
return _materialShader && _materialShader->IsReady();
|
||||
}
|
||||
|
||||
DrawPass Material::GetDrawModes() const
|
||||
{
|
||||
if (_materialShader)
|
||||
return _materialShader->GetDrawModes();
|
||||
return DrawPass::None;
|
||||
}
|
||||
|
||||
bool Material::CanUseLightmap() const
|
||||
{
|
||||
return _materialShader && _materialShader->CanUseLightmap();
|
||||
}
|
||||
|
||||
bool Material::CanUseInstancing() const
|
||||
{
|
||||
return _materialShader && _materialShader->CanUseInstancing();
|
||||
}
|
||||
|
||||
void Material::Bind(BindParameters& params)
|
||||
{
|
||||
ASSERT(IsReady());
|
||||
|
||||
MaterialParamsLink* lastLink = params.ParamsLink;
|
||||
MaterialParamsLink link;
|
||||
link.This = &Params;
|
||||
if (lastLink)
|
||||
{
|
||||
while (lastLink->Down)
|
||||
lastLink = lastLink->Down;
|
||||
lastLink->Down = &link;
|
||||
}
|
||||
else
|
||||
{
|
||||
params.ParamsLink = &link;
|
||||
}
|
||||
link.Up = lastLink;
|
||||
link.Down = nullptr;
|
||||
|
||||
_materialShader->Bind(params);
|
||||
|
||||
if (lastLink)
|
||||
{
|
||||
lastLink->Down = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
params.ParamsLink = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
#if COMPILE_WITH_MATERIAL_GRAPH && COMPILE_WITH_SHADER_COMPILER
|
||||
|
||||
namespace
|
||||
{
|
||||
void OnGeneratorError(ShaderGraph<>::Node* node, ShaderGraphBox* box, const StringView& text)
|
||||
{
|
||||
LOG(Error, "Material error: {0} (Node:{1}:{2}, Box:{3})", text, node ? node->Type : -1, node ? node->ID : -1, box ? box->ID : -1);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Asset::LoadResult Material::load()
|
||||
{
|
||||
ASSERT(_materialShader == nullptr);
|
||||
FlaxChunk* materialParamsChunk;
|
||||
|
||||
// Special case for Null renderer
|
||||
if (GPUDevice::Instance->GetRendererType() == RendererType::Null)
|
||||
{
|
||||
// Hack loading
|
||||
MemoryReadStream shaderCacheStream(nullptr, 0);
|
||||
_materialShader = MaterialShader::CreateDummy(shaderCacheStream, _shaderHeader.Material.Info);
|
||||
if (_materialShader == nullptr)
|
||||
{
|
||||
LOG(Warning, "Cannot load material.");
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
materialParamsChunk = GetChunk(SHADER_FILE_CHUNK_MATERIAL_PARAMS);
|
||||
if (materialParamsChunk != nullptr && materialParamsChunk->IsLoaded())
|
||||
{
|
||||
MemoryReadStream materialParamsStream(materialParamsChunk->Get(), materialParamsChunk->Size());
|
||||
if (Params.Load(&materialParamsStream))
|
||||
{
|
||||
LOG(Warning, "Cannot load material parameters.");
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't use parameters
|
||||
Params.Dispose();
|
||||
}
|
||||
ParamsChanged();
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
// If engine was compiled with shaders compiling service:
|
||||
// - Material should be changed in need to convert it to the newer version (via Visject Surface)
|
||||
// Shader should be recompiled if shader source code has been modified
|
||||
// otherwise:
|
||||
// - If material version is not supported then material cannot be loaded
|
||||
#if COMPILE_WITH_SHADER_COMPILER
|
||||
|
||||
#if BUILD_DEBUG
|
||||
// Materials force reload!
|
||||
Globals::ConvertLoadedMaterialsByForce = false;
|
||||
#endif
|
||||
|
||||
// Check if current engine has different materials version or convert it by force or has no source generated at all
|
||||
if (_shaderHeader.Material.GraphVersion != MATERIAL_GRAPH_VERSION
|
||||
|| Globals::ConvertLoadedMaterialsByForce
|
||||
#if MATERIAL_AUTO_GENERATE_MISSING_SOURCE
|
||||
|| !HasChunk(SHADER_FILE_CHUNK_SOURCE)
|
||||
#endif
|
||||
|| HasDependenciesModified()
|
||||
)
|
||||
{
|
||||
// Prepare
|
||||
MaterialGenerator generator;
|
||||
generator.Error.Bind(&OnGeneratorError);
|
||||
if (_shaderHeader.Material.GraphVersion != MATERIAL_GRAPH_VERSION)
|
||||
LOG(Info, "Converting material \'{0}\', from version {1} to {2}...", ToString(), _shaderHeader.Material.GraphVersion, MATERIAL_GRAPH_VERSION);
|
||||
else
|
||||
LOG(Info, "Updating material \'{0}\'...", ToString());
|
||||
|
||||
// Load or create material surface
|
||||
MaterialLayer* layer;
|
||||
if (HasChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE))
|
||||
{
|
||||
// Load graph
|
||||
if (LoadChunks(GET_CHUNK_FLAG(SHADER_FILE_CHUNK_VISJECT_SURFACE)))
|
||||
{
|
||||
LOG(Warning, "Cannot load \'{0}\' data from chunk {1}.", ToString(), SHADER_FILE_CHUNK_VISJECT_SURFACE);
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
|
||||
// Get stream with graph data
|
||||
auto surfaceChunk = GetChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE);
|
||||
MemoryReadStream stream(surfaceChunk->Get(), surfaceChunk->Size());
|
||||
|
||||
// Load layer
|
||||
layer = MaterialLayer::Load(GetID(), &stream, _shaderHeader.Material.Info, ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create default layer
|
||||
layer = MaterialLayer::CreateDefault(GetID());
|
||||
|
||||
// Create surface chunk
|
||||
auto surfaceChunk = GetOrCreateChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE);
|
||||
if (surfaceChunk == nullptr)
|
||||
return LoadResult::MissingDataChunk;
|
||||
|
||||
// Save layer to the chunk data
|
||||
MemoryWriteStream stream(512);
|
||||
layer->Graph.Save(&stream, false);
|
||||
surfaceChunk->Data.Copy(stream.GetHandle(), stream.GetPosition());
|
||||
}
|
||||
generator.AddLayer(layer);
|
||||
|
||||
// Get chunk with material parameters
|
||||
materialParamsChunk = GetOrCreateChunk(SHADER_FILE_CHUNK_MATERIAL_PARAMS);
|
||||
if (materialParamsChunk == nullptr)
|
||||
return LoadResult::MissingDataChunk;
|
||||
materialParamsChunk->Data.Release();
|
||||
|
||||
// Generate material source code and metadata
|
||||
MemoryWriteStream newMaterialMeta(1024);
|
||||
MemoryWriteStream source(64 * 1024);
|
||||
MaterialInfo info = _shaderHeader.Material.Info;
|
||||
if (generator.Generate(source, info, materialParamsChunk->Data))
|
||||
{
|
||||
LOG(Error, "Cannot generate material source code for \'{0}\'. Please see log for more info.", ToString());
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
|
||||
// Update asset dependencies
|
||||
ClearDependencies();
|
||||
for (auto& asset : generator.Assets)
|
||||
{
|
||||
if (asset->Is<MaterialBase>() ||
|
||||
asset->Is<MaterialFunction>())
|
||||
AddDependency(asset.As<BinaryAsset>());
|
||||
}
|
||||
|
||||
#if BUILD_DEBUG && USE_EDITOR
|
||||
// Dump generated material source to the temporary file
|
||||
source.SaveToFile(Globals::ProjectCacheFolder / TEXT("material.txt"));
|
||||
#endif
|
||||
|
||||
// Encrypt source code
|
||||
Encryption::EncryptBytes((byte*)source.GetHandle(), source.GetPosition());
|
||||
|
||||
// Set new source code chunk
|
||||
SetChunk(SHADER_FILE_CHUNK_SOURCE, ToSpan(source.GetHandle(), source.GetPosition()));
|
||||
|
||||
// Clear shader cache
|
||||
for (int32 chunkIndex = 1; chunkIndex < 14; chunkIndex++)
|
||||
ReleaseChunk(chunkIndex);
|
||||
|
||||
// Setup shader header
|
||||
Platform::MemoryClear(&_shaderHeader, sizeof(_shaderHeader));
|
||||
_shaderHeader.Material.GraphVersion = MATERIAL_GRAPH_VERSION;
|
||||
_shaderHeader.Material.Info = info;
|
||||
|
||||
// Save to file
|
||||
#if USE_EDITOR
|
||||
if (Save())
|
||||
{
|
||||
LOG(Error, "Cannot save \'{0}\'", ToString());
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
#endif
|
||||
#if COMPILE_WITH_SHADER_CACHE_MANAGER
|
||||
// Invalidate shader cache
|
||||
ShaderCacheManager::RemoveCache(GetID());
|
||||
#endif
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// Ensure that material is in the current version (whole materials pipeline depends on that)
|
||||
if (_shaderHeader.Material.GraphVersion != MATERIAL_GRAPH_VERSION)
|
||||
{
|
||||
LOG(Fatal, "Unsupported material version: {0} in material \'{1}\'. Current is {2}.", _shaderHeader.Material.GraphVersion, ToString(), MATERIAL_GRAPH_VERSION);
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// Load shader cache (it may call compilation or gather cached data)
|
||||
ShaderCacheResult shaderCache;
|
||||
if (LoadShaderCache(shaderCache))
|
||||
{
|
||||
LOG(Error, "Cannot load \'{0}\' shader cache.", ToString());
|
||||
#if 1
|
||||
return LoadResult::Failed;
|
||||
#else
|
||||
// Custom path: don't fail asset loading but use dummy material instead (surface and parameters will be available for editing)
|
||||
LOG(Error, "Using dummy material shader for \'{0}\'.", ToString());
|
||||
MemoryWriteStream dummyShader(64);
|
||||
{
|
||||
dummyShader.WriteInt32(6);
|
||||
dummyShader.WriteInt32(0);
|
||||
dummyShader.WriteInt32(0);
|
||||
dummyShader.WriteByte(0);
|
||||
dummyShader.WriteByte(0);
|
||||
}
|
||||
MemoryReadStream shaderCacheStream(dummyShader.GetHandle(), dummyShader.GetPosition());
|
||||
_materialShader = MaterialShader::CreateDummy(shaderCacheStream, _shaderHeader.Material.Info);
|
||||
if (_materialShader == nullptr)
|
||||
{
|
||||
LOG(Warning, "Cannot load material.");
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
// Load material (load shader from cache, load params, setup pipeline stuff)
|
||||
MemoryReadStream shaderCacheStream(shaderCache.Data.Get(), shaderCache.Data.Length());
|
||||
_materialShader = MaterialShader::Create(GetPath(), shaderCacheStream, _shaderHeader.Material.Info);
|
||||
if (_materialShader == nullptr)
|
||||
{
|
||||
LOG(Warning, "Cannot load material.");
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
// Load material parameters
|
||||
materialParamsChunk = GetChunk(SHADER_FILE_CHUNK_MATERIAL_PARAMS);
|
||||
if (materialParamsChunk != nullptr && materialParamsChunk->IsLoaded())
|
||||
{
|
||||
MemoryReadStream materialParamsStream(materialParamsChunk->Get(), materialParamsChunk->Size());
|
||||
if (Params.Load(&materialParamsStream))
|
||||
{
|
||||
LOG(Warning, "Cannot load material parameters.");
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't use parameters
|
||||
Params.Dispose();
|
||||
}
|
||||
ParamsChanged();
|
||||
|
||||
#if COMPILE_WITH_SHADER_COMPILER
|
||||
RegisterForShaderReloads(this, shaderCache);
|
||||
#endif
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
void Material::unload(bool isReloading)
|
||||
{
|
||||
#if COMPILE_WITH_SHADER_COMPILER
|
||||
UnregisterForShaderReloads(this);
|
||||
#endif
|
||||
|
||||
if (_materialShader)
|
||||
{
|
||||
_materialShader->Unload();
|
||||
Delete(_materialShader);
|
||||
_materialShader = nullptr;
|
||||
}
|
||||
|
||||
Params.Dispose();
|
||||
}
|
||||
|
||||
AssetChunksFlag Material::getChunksToPreload() const
|
||||
{
|
||||
AssetChunksFlag result = ShaderAssetTypeBase<MaterialBase>::getChunksToPreload();
|
||||
result |= GET_CHUNK_FLAG(SHADER_FILE_CHUNK_MATERIAL_PARAMS);
|
||||
return result;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
void Material::OnDependencyModified(BinaryAsset* asset)
|
||||
{
|
||||
BinaryAsset::OnDependencyModified(asset);
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
void Material::InitCompilationOptions(ShaderCompilationOptions& options)
|
||||
{
|
||||
// Base
|
||||
ShaderAssetBase::InitCompilationOptions(options);
|
||||
|
||||
#if COMPILE_WITH_SHADER_COMPILER
|
||||
// Ensure that this call is valid (material features switches may depend on target compilation platform)
|
||||
ASSERT(options.Profile != ShaderProfile::Unknown);
|
||||
|
||||
// Prepare
|
||||
auto& info = _shaderHeader.Material.Info;
|
||||
const bool isSurfaceOrTerrain = info.Domain == MaterialDomain::Surface || info.Domain == MaterialDomain::Terrain;
|
||||
const bool useCustomData = info.ShadingModel == MaterialShadingModel::Subsurface || info.ShadingModel == MaterialShadingModel::Foliage;
|
||||
const bool useForward = (info.Domain == MaterialDomain::Surface && info.BlendMode != MaterialBlendMode::Opaque) || info.Domain == MaterialDomain::Particle;
|
||||
const bool useTess =
|
||||
info.TessellationMode != TessellationMethod::None &&
|
||||
RenderTools::CanSupportTessellation(options.Profile) && isSurfaceOrTerrain;
|
||||
const bool useDistortion =
|
||||
(info.Domain == MaterialDomain::Surface || info.Domain == MaterialDomain::Particle) &&
|
||||
info.BlendMode != MaterialBlendMode::Opaque &&
|
||||
(info.UsageFlags & MaterialUsageFlags::UseRefraction) != 0 &&
|
||||
(info.FeaturesFlags & MaterialFeaturesFlags::DisableDistortion) == 0;
|
||||
|
||||
// @formatter:off
|
||||
static const char* Numbers[] =
|
||||
{
|
||||
"0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55","56","57","58","59","60","61","62","63","64","65","66","67","68","69",
|
||||
};
|
||||
// @formatter:on
|
||||
|
||||
// Setup shader macros
|
||||
options.Macros.Add({ "MATERIAL_DOMAIN", Numbers[(int32)info.Domain] });
|
||||
options.Macros.Add({ "MATERIAL_BLEND", Numbers[(int32)info.BlendMode] });
|
||||
options.Macros.Add({ "MATERIAL_SHADING_MODEL", Numbers[(int32)info.ShadingModel] });
|
||||
options.Macros.Add({ "MATERIAL_MASKED", Numbers[info.UsageFlags & MaterialUsageFlags::UseMask ? 1 : 0] });
|
||||
options.Macros.Add({ "DECAL_BLEND_MODE", Numbers[(int32)info.DecalBlendingMode] });
|
||||
options.Macros.Add({ "USE_EMISSIVE", Numbers[info.UsageFlags & MaterialUsageFlags::UseEmissive ? 1 : 0] });
|
||||
options.Macros.Add({ "USE_NORMAL", Numbers[info.UsageFlags & MaterialUsageFlags::UseNormal ? 1 : 0] });
|
||||
options.Macros.Add({ "USE_POSITION_OFFSET", Numbers[info.UsageFlags & MaterialUsageFlags::UsePositionOffset ? 1 : 0] });
|
||||
options.Macros.Add({ "USE_VERTEX_COLOR", Numbers[info.UsageFlags & MaterialUsageFlags::UseVertexColor ? 1 : 0] });
|
||||
options.Macros.Add({ "USE_DISPLACEMENT", Numbers[info.UsageFlags & MaterialUsageFlags::UseDisplacement ? 1 : 0] });
|
||||
options.Macros.Add({ "USE_DITHERED_LOD_TRANSITION", Numbers[info.FeaturesFlags & MaterialFeaturesFlags::DitheredLODTransition ? 1 : 0] });
|
||||
options.Macros.Add({ "USE_GBUFFER_CUSTOM_DATA", Numbers[useCustomData ? 1 : 0] });
|
||||
options.Macros.Add({ "USE_REFLECTIONS", Numbers[info.FeaturesFlags & MaterialFeaturesFlags::DisableReflections ? 0 : 1] });
|
||||
options.Macros.Add({ "USE_FOG", Numbers[info.FeaturesFlags & MaterialFeaturesFlags::DisableFog ? 0 : 1] });
|
||||
if (useForward)
|
||||
options.Macros.Add({ "USE_PIXEL_NORMAL_OFFSET_REFRACTION", Numbers[info.FeaturesFlags & MaterialFeaturesFlags::PixelNormalOffsetRefraction ? 1 : 0] });
|
||||
|
||||
// TODO: don't compile VS_Depth for deferred/forward materials if material doesn't use position offset or masking
|
||||
|
||||
options.Macros.Add({ "USE_TESSELLATION", Numbers[useTess ? 1 : 0] });
|
||||
options.Macros.Add({ "TESSELLATION_IN_CONTROL_POINTS", "3" });
|
||||
if (useTess)
|
||||
{
|
||||
switch (info.TessellationMode)
|
||||
{
|
||||
case TessellationMethod::Flat:
|
||||
options.Macros.Add({ "MATERIAL_TESSELLATION", "MATERIAL_TESSELLATION_FLAT" });
|
||||
break;
|
||||
case TessellationMethod::PointNormal:
|
||||
options.Macros.Add({ "MATERIAL_TESSELLATION", "MATERIAL_TESSELLATION_PN" });
|
||||
break;
|
||||
case TessellationMethod::Phong:
|
||||
options.Macros.Add({ "MATERIAL_TESSELLATION", "MATERIAL_TESSELLATION_PHONG" });
|
||||
break;
|
||||
}
|
||||
options.Macros.Add({ "MAX_TESSELLATION_FACTOR", Numbers[info.MaxTessellationFactor] });
|
||||
}
|
||||
|
||||
// Helper macros (used by the parser)
|
||||
options.Macros.Add({ "IS_SURFACE", Numbers[info.Domain == MaterialDomain::Surface ? 1 : 0] });
|
||||
options.Macros.Add({ "IS_POST_FX", Numbers[info.Domain == MaterialDomain::PostProcess ? 1 : 0] });
|
||||
options.Macros.Add({ "IS_GUI", Numbers[info.Domain == MaterialDomain::GUI ? 1 : 0] });
|
||||
options.Macros.Add({ "IS_DECAL", Numbers[info.Domain == MaterialDomain::Decal ? 1 : 0] });
|
||||
options.Macros.Add({ "IS_TERRAIN", Numbers[info.Domain == MaterialDomain::Terrain ? 1 : 0] });
|
||||
options.Macros.Add({ "IS_PARTICLE", Numbers[info.Domain == MaterialDomain::Particle ? 1 : 0] });
|
||||
options.Macros.Add({ "USE_FORWARD", Numbers[useForward ? 1 : 0] });
|
||||
options.Macros.Add({ "USE_DEFERRED", Numbers[isSurfaceOrTerrain && info.BlendMode == MaterialBlendMode::Opaque ? 1 : 0] });
|
||||
options.Macros.Add({ "USE_DISTORTION", Numbers[useDistortion ? 1 : 0] });
|
||||
options.Macros.Add({ "CAN_USE_LIGHTMAP", Numbers[isSurfaceOrTerrain ? 1 : 0] });
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
BytesContainer Material::LoadSurface(bool createDefaultIfMissing)
|
||||
{
|
||||
BytesContainer result;
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Check if has that chunk
|
||||
if (HasChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE))
|
||||
{
|
||||
// Load graph
|
||||
if (!LoadChunks(GET_CHUNK_FLAG(SHADER_FILE_CHUNK_VISJECT_SURFACE)))
|
||||
{
|
||||
// Get stream with graph data
|
||||
const auto data = GetChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE);
|
||||
result.Copy(data->Data);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
LOG(Warning, "Material \'{0}\' surface data is missing.", GetPath());
|
||||
|
||||
#if COMPILE_WITH_MATERIAL_GRAPH
|
||||
|
||||
// Check if create default surface
|
||||
if (createDefaultIfMissing)
|
||||
{
|
||||
// Create default layer
|
||||
const auto layer = MaterialLayer::CreateDefault(GetID());
|
||||
|
||||
// Serialize layer to stream
|
||||
MemoryWriteStream stream(256);
|
||||
layer->Graph.Save(&stream, false);
|
||||
|
||||
// Set output data
|
||||
result.Copy(stream.GetHandle(), stream.GetPosition());
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
bool Material::SaveSurface(BytesContainer& data, const MaterialInfo& info)
|
||||
{
|
||||
// Wait for asset to be loaded or don't if last load failed (eg. by shader source compilation error)
|
||||
if (LastLoadFailed())
|
||||
{
|
||||
LOG(Warning, "Saving asset that failed to load.");
|
||||
}
|
||||
else if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Release all chunks
|
||||
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
|
||||
ReleaseChunk(i);
|
||||
|
||||
// Update material info
|
||||
Platform::MemoryClear(&_shaderHeader, sizeof(_shaderHeader));
|
||||
_shaderHeader.Material.GraphVersion = MATERIAL_GRAPH_VERSION;
|
||||
_shaderHeader.Material.Info = info;
|
||||
|
||||
// Set Visject Surface data
|
||||
auto visjectSurfaceChunk = GetOrCreateChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE);
|
||||
ASSERT(visjectSurfaceChunk != nullptr);
|
||||
visjectSurfaceChunk->Data.Copy(data);
|
||||
|
||||
if (Save())
|
||||
{
|
||||
LOG(Error, "Cannot save \'{0}\'", ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
#if COMPILE_WITH_SHADER_CACHE_MANAGER
|
||||
// Invalidate shader cache
|
||||
ShaderCacheManager::RemoveCache(GetID());
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
68
Source/Engine/Content/Assets/Material.h
Normal file
68
Source/Engine/Content/Assets/Material.h
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "MaterialBase.h"
|
||||
#include "Engine/Graphics/Shaders/Cache/ShaderAssetBase.h"
|
||||
|
||||
class MaterialShader;
|
||||
|
||||
/// <summary>
|
||||
/// Material asset that contains shader for rendering models on the GPU.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API Material : public ShaderAssetTypeBase<MaterialBase>
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(Material, ShadersSerializedVersion);
|
||||
private:
|
||||
|
||||
MaterialShader* _materialShader = nullptr;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load surface graph from the asset.
|
||||
/// </summary>
|
||||
/// <param name="createDefaultIfMissing">True if create default surface if missing.</param>
|
||||
/// <returns>The output surface data, or empty if failed to load.</returns>
|
||||
API_FUNCTION() BytesContainer LoadSurface(bool createDefaultIfMissing);
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Updates the material surface (save new one, discard cached data, reload asset).
|
||||
/// </summary>
|
||||
/// <param name="data">The surface graph data.</param>
|
||||
/// <param name="info">The material info structure.</param>
|
||||
/// <returns>True if cannot save it, otherwise false.</returns>
|
||||
API_FUNCTION() bool SaveSurface(BytesContainer& data, const MaterialInfo& info);
|
||||
|
||||
#endif
|
||||
|
||||
public:
|
||||
|
||||
// [MaterialBase]
|
||||
bool IsMaterialInstance() const override;
|
||||
|
||||
// [IMaterial]
|
||||
const MaterialInfo& GetInfo() const override;
|
||||
bool IsReady() const override;
|
||||
DrawPass GetDrawModes() const override;
|
||||
bool CanUseLightmap() const override;
|
||||
bool CanUseInstancing() const override;
|
||||
void Bind(BindParameters& params) override;
|
||||
|
||||
// [ShaderAssetBase]
|
||||
#if USE_EDITOR
|
||||
void InitCompilationOptions(ShaderCompilationOptions& options) override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
|
||||
// [MaterialBase]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
AssetChunksFlag getChunksToPreload() const override;
|
||||
#if USE_EDITOR
|
||||
void OnDependencyModified(BinaryAsset* asset) override;
|
||||
#endif
|
||||
};
|
||||
46
Source/Engine/Content/Assets/MaterialBase.cpp
Normal file
46
Source/Engine/Content/Assets/MaterialBase.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "MaterialBase.h"
|
||||
#include "MaterialInstance.h"
|
||||
#include "Engine/Core/Types/Variant.h"
|
||||
#include "Engine/Content/Content.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
|
||||
REGISTER_BINARY_ASSET_ABSTRACT(MaterialBase, "FlaxEngine.MaterialBase");
|
||||
|
||||
MaterialBase::MaterialBase(const SpawnParams& params, const AssetInfo* info)
|
||||
: BinaryAsset(params, info)
|
||||
{
|
||||
}
|
||||
|
||||
Variant MaterialBase::GetParameterValue(const StringView& name)
|
||||
{
|
||||
const auto param = Params.Get(name);
|
||||
if (param)
|
||||
{
|
||||
return param->GetValue();
|
||||
}
|
||||
LOG(Warning, "Missing material parameter '{0}' in material {1}", String(name), ToString());
|
||||
return Variant::Null;
|
||||
}
|
||||
|
||||
void MaterialBase::SetParameterValue(const StringView& name, const Variant& value, bool warnIfMissing)
|
||||
{
|
||||
const auto param = Params.Get(name);
|
||||
if (param)
|
||||
{
|
||||
param->SetValue(value);
|
||||
param->SetIsOverride(true);
|
||||
}
|
||||
else if (warnIfMissing)
|
||||
{
|
||||
LOG(Warning, "Missing material parameter '{0}' in material {1}", String(name), ToString());
|
||||
}
|
||||
}
|
||||
|
||||
MaterialInstance* MaterialBase::CreateVirtualInstance()
|
||||
{
|
||||
auto instance = Content::CreateVirtualAsset<MaterialInstance>();
|
||||
instance->SetBaseMaterial(this);
|
||||
return instance;
|
||||
}
|
||||
92
Source/Engine/Content/Assets/MaterialBase.h
Normal file
92
Source/Engine/Content/Assets/MaterialBase.h
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../BinaryAsset.h"
|
||||
#include "Engine/Graphics/Materials/IMaterial.h"
|
||||
#include "Engine/Graphics/Materials/MaterialParams.h"
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="Material"/> and <see cref="MaterialInstance"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEngine.BinaryAsset" />
|
||||
API_CLASS(Abstract, NoSpawn) class FLAXENGINE_API MaterialBase : public BinaryAsset, public IMaterial
|
||||
{
|
||||
DECLARE_ASSET_HEADER(MaterialBase);
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The material parameters collection.
|
||||
/// </summary>
|
||||
MaterialParams Params;
|
||||
|
||||
/// <summary>
|
||||
/// Event called when parameters collections gets modified.
|
||||
/// </summary>
|
||||
Action ParamsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if material is an material instance.
|
||||
/// </summary>
|
||||
/// <returns>True if it's a material instance, otherwise false.</returns>
|
||||
virtual bool IsMaterialInstance() const = 0;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets the material parameters collection.
|
||||
/// </summary>
|
||||
API_PROPERTY() const Array<MaterialParameter>& GetParameters() const
|
||||
{
|
||||
return Params;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the material info, structure which describes material surface.
|
||||
/// </summary>
|
||||
API_PROPERTY() FORCE_INLINE const MaterialInfo& Info() const
|
||||
{
|
||||
return GetInfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the material parameter.
|
||||
/// </summary>
|
||||
API_FUNCTION() FORCE_INLINE MaterialParameter* GetParameter(const StringView& name)
|
||||
{
|
||||
return Params.Get(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the material parameter value.
|
||||
/// </summary>
|
||||
/// <returns>The parameter value.</returns>
|
||||
API_FUNCTION() Variant GetParameterValue(const StringView& name);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the material parameter value (and sets IsOverride to true).
|
||||
/// </summary>
|
||||
/// <param name="name">The parameter name.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
/// <param name="warnIfMissing">True if warn if parameter is missing, otherwise will do nothing.</param>
|
||||
API_FUNCTION() void SetParameterValue(const StringView& name, const Variant& value, bool warnIfMissing = true);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the virtual material instance of this material which allows to override any material parameters.
|
||||
/// </summary>
|
||||
/// <returns>The created virtual material instance asset.</returns>
|
||||
API_FUNCTION() MaterialInstance* CreateVirtualInstance();
|
||||
|
||||
public:
|
||||
|
||||
// [BinaryAsset]
|
||||
#if USE_EDITOR
|
||||
void GetReferences(Array<Guid>& output) const override
|
||||
{
|
||||
// Base
|
||||
BinaryAsset::GetReferences(output);
|
||||
|
||||
Params.GetReferences(output);
|
||||
}
|
||||
#endif
|
||||
};
|
||||
158
Source/Engine/Content/Assets/MaterialFunction.cpp
Normal file
158
Source/Engine/Content/Assets/MaterialFunction.cpp
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "MaterialFunction.h"
|
||||
#if COMPILE_WITH_MATERIAL_GRAPH
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Core/Types/DataContainer.h"
|
||||
#include "Engine/Serialization/MemoryReadStream.h"
|
||||
#endif
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
|
||||
REGISTER_BINARY_ASSET(MaterialFunction, "FlaxEngine.MaterialFunction", nullptr, false);
|
||||
|
||||
MaterialFunction::MaterialFunction(const SpawnParams& params, const AssetInfo* info)
|
||||
: BinaryAsset(params, info)
|
||||
{
|
||||
}
|
||||
|
||||
Asset::LoadResult MaterialFunction::load()
|
||||
{
|
||||
#if COMPILE_WITH_MATERIAL_GRAPH
|
||||
// Load graph
|
||||
const auto surfaceChunk = GetChunk(0);
|
||||
if (!surfaceChunk || !surfaceChunk->IsLoaded())
|
||||
return LoadResult::MissingDataChunk;
|
||||
MemoryReadStream stream(surfaceChunk->Get(), surfaceChunk->Size());
|
||||
if (Graph.Load(&stream, false))
|
||||
return LoadResult::Failed;
|
||||
|
||||
// Cache input and output nodes
|
||||
bool tooManyInputsOutputs = false;
|
||||
for (int32 i = 0; i < Graph.Nodes.Count(); i++)
|
||||
{
|
||||
auto& node = Graph.Nodes[i];
|
||||
if (node.Type == GRAPH_NODE_MAKE_TYPE(16, 1))
|
||||
{
|
||||
if (Inputs.Count() < 16)
|
||||
Inputs.Add(i);
|
||||
else
|
||||
tooManyInputsOutputs = true;
|
||||
}
|
||||
else if (node.Type == GRAPH_NODE_MAKE_TYPE(16, 2))
|
||||
{
|
||||
if (Outputs.Count() < 16)
|
||||
Outputs.Add(i);
|
||||
else
|
||||
tooManyInputsOutputs = true;
|
||||
}
|
||||
}
|
||||
if (tooManyInputsOutputs)
|
||||
{
|
||||
LOG(Error, "Too many function inputs/outputs in '{0}'. The limit is max 16 inputs and max 16 outputs.", ToString());
|
||||
}
|
||||
#endif
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
void MaterialFunction::unload(bool isReloading)
|
||||
{
|
||||
#if COMPILE_WITH_MATERIAL_GRAPH
|
||||
Graph.Clear();
|
||||
Inputs.Clear();
|
||||
Outputs.Clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
AssetChunksFlag MaterialFunction::getChunksToPreload() const
|
||||
{
|
||||
return GET_CHUNK_FLAG(0);
|
||||
}
|
||||
|
||||
#if COMPILE_WITH_MATERIAL_GRAPH
|
||||
|
||||
BytesContainer MaterialFunction::LoadSurface()
|
||||
{
|
||||
BytesContainer result;
|
||||
ScopeLock lock(Locker);
|
||||
if (HasChunk(0))
|
||||
{
|
||||
if (!LoadChunks(GET_CHUNK_FLAG(0)))
|
||||
{
|
||||
const auto surfaceChunk = GetChunk(0);
|
||||
result.Copy(surfaceChunk->Data);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool MaterialFunction::LoadSurface(MaterialGraph& graph)
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
if (HasChunk(0))
|
||||
{
|
||||
if (!LoadChunks(GET_CHUNK_FLAG(0)))
|
||||
{
|
||||
const auto surfaceChunk = GetChunk(0);
|
||||
MemoryReadStream stream(surfaceChunk->Get(), surfaceChunk->Size());
|
||||
return graph.Load(&stream, false);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MaterialFunction::GetSignature(Array<StringView, FixedAllocation<32>>& types, Array<StringView, FixedAllocation<32>>& names)
|
||||
{
|
||||
types.Resize(32);
|
||||
names.Resize(32);
|
||||
for (int32 i = 0; i < Inputs.Count(); i++)
|
||||
{
|
||||
auto& node = Graph.Nodes[Inputs[i]];
|
||||
types[i] = GetGraphFunctionTypeName_Deprecated(node.Values[0]);
|
||||
names[i] = (StringView)node.Values[1];
|
||||
}
|
||||
for (int32 i = 0; i < Outputs.Count(); i++)
|
||||
{
|
||||
auto& node = Graph.Nodes[Outputs[i]];
|
||||
types[i + 16] = GetGraphFunctionTypeName_Deprecated(node.Values[0]);
|
||||
names[i + 16] = (StringView)node.Values[1];
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
bool MaterialFunction::SaveSurface(BytesContainer& data)
|
||||
{
|
||||
// Wait for asset to be loaded or don't if last load failed
|
||||
if (LastLoadFailed())
|
||||
{
|
||||
LOG(Warning, "Saving asset that failed to load.");
|
||||
}
|
||||
else if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Set Visject Surface data
|
||||
auto surfaceChunk = GetOrCreateChunk(0);
|
||||
ASSERT(surfaceChunk != nullptr);
|
||||
surfaceChunk->Data.Copy(data);
|
||||
|
||||
// Save asset
|
||||
AssetInitData initData;
|
||||
initData.SerializedVersion = 1;
|
||||
if (SaveAsset(initData))
|
||||
{
|
||||
LOG(Error, "Cannot save \'{0}\'", ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
68
Source/Engine/Content/Assets/MaterialFunction.h
Normal file
68
Source/Engine/Content/Assets/MaterialFunction.h
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../BinaryAsset.h"
|
||||
#include "Engine/Tools/MaterialGenerator/Types.h"
|
||||
|
||||
/// <summary>
|
||||
/// Material function graph asset that contains reusable part of the material graph.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API MaterialFunction : public BinaryAsset
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(MaterialFunction, 1);
|
||||
public:
|
||||
|
||||
#if COMPILE_WITH_MATERIAL_GRAPH
|
||||
|
||||
/// <summary>
|
||||
/// The loaded material function graph.
|
||||
/// </summary>
|
||||
MaterialGraph Graph;
|
||||
|
||||
/// <summary>
|
||||
/// The input nodes (indices).
|
||||
/// </summary>
|
||||
Array<int32, FixedAllocation<16>> Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// The output nodes (indices).
|
||||
/// </summary>
|
||||
Array<int32, FixedAllocation<16>> Outputs;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load surface graph from the asset.
|
||||
/// </summary>
|
||||
/// <returns>The output surface data, or empty if failed to load.</returns>
|
||||
API_FUNCTION() BytesContainer LoadSurface();
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load surface graph from the asset.
|
||||
/// </summary>
|
||||
/// <param name="graph">The graph to load.</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
bool LoadSurface(MaterialGraph& graph);
|
||||
|
||||
// Gets the function signature for Visject Surface editor.
|
||||
API_FUNCTION() void GetSignature(API_PARAM(Out) Array<StringView, FixedAllocation<32>>& types, API_PARAM(Out) Array<StringView, FixedAllocation<32>>& names);
|
||||
|
||||
#endif
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Updates the material graph surface (save new one, discards cached data, reloads asset).
|
||||
/// </summary>
|
||||
/// <param name="data">The surface graph data.</param>
|
||||
/// <returns>True if cannot save it, otherwise false.</returns>
|
||||
API_FUNCTION() bool SaveSurface(BytesContainer& data);
|
||||
|
||||
#endif
|
||||
|
||||
protected:
|
||||
|
||||
// [BinaryAsset]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
AssetChunksFlag getChunksToPreload() const override;
|
||||
};
|
||||
325
Source/Engine/Content/Assets/MaterialInstance.cpp
Normal file
325
Source/Engine/Content/Assets/MaterialInstance.cpp
Normal file
@@ -0,0 +1,325 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "MaterialInstance.h"
|
||||
#include "Engine/Core/Types/Variant.h"
|
||||
#include "Engine/Content/Content.h"
|
||||
#include "Engine/Content/Upgraders/MaterialInstanceUpgrader.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
#include "Engine/Serialization/MemoryReadStream.h"
|
||||
|
||||
REGISTER_BINARY_ASSET(MaterialInstance, "FlaxEngine.MaterialInstance", ::New<MaterialInstanceUpgrader>(), true);
|
||||
|
||||
MaterialInstance::MaterialInstance(const SpawnParams& params, const AssetInfo* info)
|
||||
: MaterialBase(params, info)
|
||||
{
|
||||
}
|
||||
|
||||
void MaterialInstance::OnBaseSet()
|
||||
{
|
||||
ScopeLock lock(_baseMaterial->Locker);
|
||||
ASSERT(_baseMaterial->IsLoaded());
|
||||
|
||||
_baseMaterial->AddReference();
|
||||
_baseMaterial->OnUnloaded.Bind<MaterialInstance, &MaterialInstance::OnBaseUnloaded>(this);
|
||||
_baseMaterial->ParamsChanged.Bind<MaterialInstance, &MaterialInstance::OnBaseParamsChanged>(this);
|
||||
|
||||
MaterialParams& baseParams = _baseMaterial->Params;
|
||||
bool validParams = Params.Count() == baseParams.Count();
|
||||
for (int32 i = 0; validParams && i < Params.Count(); i++)
|
||||
{
|
||||
MaterialParameter& param = Params[i];
|
||||
MaterialParameter& baseParam = baseParams[i];
|
||||
|
||||
if (param.GetID() != baseParam.GetID() || param.GetParameterType() != baseParam.GetParameterType())
|
||||
{
|
||||
validParams = false;
|
||||
break;
|
||||
}
|
||||
|
||||
param._isPublic = baseParam._isPublic;
|
||||
param._registerIndex = baseParam._registerIndex;
|
||||
param._offset = baseParam._offset;
|
||||
param._name = baseParam._name;
|
||||
}
|
||||
if (validParams)
|
||||
{
|
||||
Params._versionHash = baseParams._versionHash;
|
||||
ParamsChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnBaseParamsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void MaterialInstance::OnBaseUnset()
|
||||
{
|
||||
ScopeLock lock(_baseMaterial->Locker);
|
||||
|
||||
_baseMaterial->RemoveReference();
|
||||
_baseMaterial->OnUnloaded.Unbind<MaterialInstance, &MaterialInstance::OnBaseUnloaded>(this);
|
||||
_baseMaterial->ParamsChanged.Unbind<MaterialInstance, &MaterialInstance::OnBaseParamsChanged>(this);
|
||||
}
|
||||
|
||||
void MaterialInstance::OnBaseUnloaded(Asset* p)
|
||||
{
|
||||
SetBaseMaterial(nullptr);
|
||||
}
|
||||
|
||||
void MaterialInstance::OnBaseParamsChanged()
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Skip if version has not been changed and the hash is the same
|
||||
auto baseParams = &_baseMaterial->Params;
|
||||
if (Params.GetVersionHash() == baseParams->GetVersionHash())
|
||||
return;
|
||||
|
||||
//LOG(Info, "Updating material instance params \'{0}\' (base: \'{1}\')", ToString(), _baseMaterial->ToString());
|
||||
|
||||
// Cache previous parameters
|
||||
MaterialParams oldParams;
|
||||
Params.Clone(oldParams);
|
||||
|
||||
// Get the newest parameters
|
||||
baseParams->Clone(Params);
|
||||
|
||||
// Override all public parameters by default
|
||||
for (auto& param : Params)
|
||||
param.SetIsOverride(param.IsPublic());
|
||||
|
||||
// Copy previous parameters values
|
||||
for (int32 i = 0; i < oldParams.Count(); i++)
|
||||
{
|
||||
const MaterialParameter& oldParam = oldParams[i];
|
||||
MaterialParameter* param = Params.Get(oldParam.GetParameterID());
|
||||
if (param)
|
||||
{
|
||||
// Check type
|
||||
if (oldParam.GetParameterType() == param->GetParameterType())
|
||||
{
|
||||
// Restore value
|
||||
const Variant value = oldParam.GetValue();
|
||||
param->SetValue(value);
|
||||
param->SetIsOverride(oldParam.IsOverride());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(Info, "Param {0} changed type from {1}", param->ToString(), oldParam.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParamsChanged();
|
||||
}
|
||||
|
||||
void MaterialInstance::OnUnload()
|
||||
{
|
||||
if (_baseMaterial)
|
||||
{
|
||||
OnBaseUnset();
|
||||
_baseMaterial = nullptr;
|
||||
}
|
||||
Params.Dispose();
|
||||
}
|
||||
|
||||
bool MaterialInstance::IsMaterialInstance() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
void MaterialInstance::GetReferences(Array<Guid>& output) const
|
||||
{
|
||||
// Base
|
||||
MaterialBase::GetReferences(output);
|
||||
|
||||
if (_baseMaterial)
|
||||
output.Add(_baseMaterial->GetID());
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
const MaterialInfo& MaterialInstance::GetInfo() const
|
||||
{
|
||||
if (_baseMaterial)
|
||||
return _baseMaterial->GetInfo();
|
||||
|
||||
static MaterialInfo EmptyInfo;
|
||||
return EmptyInfo;
|
||||
}
|
||||
|
||||
bool MaterialInstance::IsReady() const
|
||||
{
|
||||
return IsLoaded() && _baseMaterial && _baseMaterial->IsReady();
|
||||
}
|
||||
|
||||
DrawPass MaterialInstance::GetDrawModes() const
|
||||
{
|
||||
if (_baseMaterial)
|
||||
return _baseMaterial->GetDrawModes();
|
||||
return DrawPass::None;
|
||||
}
|
||||
|
||||
bool MaterialInstance::CanUseLightmap() const
|
||||
{
|
||||
return _baseMaterial && _baseMaterial->CanUseLightmap();
|
||||
}
|
||||
|
||||
bool MaterialInstance::CanUseInstancing() const
|
||||
{
|
||||
return _baseMaterial && _baseMaterial->CanUseInstancing();
|
||||
}
|
||||
|
||||
void MaterialInstance::Bind(BindParameters& params)
|
||||
{
|
||||
//ASSERT(IsReady());
|
||||
//ASSERT(_baseMaterial->Params.GetVersionHash() == Params.GetVersionHash());
|
||||
|
||||
auto lastLink = params.ParamsLink;
|
||||
MaterialParamsLink link;
|
||||
link.This = &Params;
|
||||
if (lastLink)
|
||||
{
|
||||
while (lastLink->Down)
|
||||
lastLink = lastLink->Down;
|
||||
lastLink->Down = &link;
|
||||
}
|
||||
else
|
||||
{
|
||||
params.ParamsLink = &link;
|
||||
}
|
||||
link.Up = lastLink;
|
||||
link.Down = nullptr;
|
||||
|
||||
_baseMaterial->Bind(params);
|
||||
|
||||
if (lastLink)
|
||||
{
|
||||
lastLink->Down = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
params.ParamsLink = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Asset::LoadResult MaterialInstance::load()
|
||||
{
|
||||
ASSERT(_baseMaterial == nullptr);
|
||||
|
||||
// Get main chunk
|
||||
auto chunk0 = GetChunk(0);
|
||||
if (chunk0 == nullptr || chunk0->IsMissing())
|
||||
return LoadResult::MissingDataChunk;
|
||||
MemoryReadStream headerStream(chunk0->Get(), chunk0->Size());
|
||||
|
||||
// Load base material
|
||||
Guid baseMaterialId;
|
||||
headerStream.Read(&baseMaterialId);
|
||||
auto baseMaterial = Content::LoadAsync<MaterialBase>(baseMaterialId);
|
||||
|
||||
// Load parameters
|
||||
Params.Load(&headerStream);
|
||||
|
||||
if (baseMaterial && !baseMaterial->WaitForLoaded())
|
||||
{
|
||||
_baseMaterial = baseMaterial;
|
||||
OnBaseSet();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clear parameters if has no material loaded
|
||||
Params.Dispose();
|
||||
ParamsChanged();
|
||||
}
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
void MaterialInstance::unload(bool isReloading)
|
||||
{
|
||||
OnUnload();
|
||||
}
|
||||
|
||||
AssetChunksFlag MaterialInstance::getChunksToPreload() const
|
||||
{
|
||||
return GET_CHUNK_FLAG(0);
|
||||
}
|
||||
|
||||
void MaterialInstance::SetBaseMaterial(MaterialBase* baseMaterial)
|
||||
{
|
||||
if (baseMaterial != nullptr && baseMaterial->WaitForLoaded())
|
||||
{
|
||||
LOG(Warning, "Cannot set base material of {0} to {1} because it failed to load.", ToString(), baseMaterial->ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
if (baseMaterial == _baseMaterial)
|
||||
return;
|
||||
|
||||
// Release previous parameters
|
||||
Params.Dispose();
|
||||
|
||||
// Set new value
|
||||
if (_baseMaterial)
|
||||
{
|
||||
OnBaseUnset();
|
||||
}
|
||||
_baseMaterial = baseMaterial;
|
||||
if (baseMaterial)
|
||||
{
|
||||
_baseMaterial = baseMaterial;
|
||||
OnBaseSet();
|
||||
}
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
bool MaterialInstance::Save(const StringView& path)
|
||||
{
|
||||
// Validate state
|
||||
if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
if (IsVirtual() && path.IsEmpty())
|
||||
{
|
||||
LOG(Error, "To save virtual asset asset you need to specify the target asset path location.");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Save instance data
|
||||
MemoryWriteStream stream(512);
|
||||
{
|
||||
// Save base material ID
|
||||
const auto baseMaterialId = _baseMaterial ? _baseMaterial->GetID() : Guid::Empty;
|
||||
stream.Write(&baseMaterialId);
|
||||
|
||||
// Save parameters
|
||||
Params.Save(&stream);
|
||||
}
|
||||
SetChunk(0, ToSpan(stream.GetHandle(), stream.GetPosition()));
|
||||
|
||||
// Setup asset data
|
||||
AssetInitData data;
|
||||
data.SerializedVersion = 4;
|
||||
|
||||
// Save data
|
||||
const bool saveResult = path.HasChars() ? SaveAsset(path, data) : SaveAsset(data, true);
|
||||
if (saveResult)
|
||||
{
|
||||
LOG(Error, "Cannot save \'{0}\'", ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
76
Source/Engine/Content/Assets/MaterialInstance.h
Normal file
76
Source/Engine/Content/Assets/MaterialInstance.h
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "MaterialBase.h"
|
||||
|
||||
/// <summary>
|
||||
/// Instance of the <seealso cref="Material" /> with custom set of material parameter values.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API MaterialInstance : public MaterialBase
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(MaterialInstance, 4);
|
||||
private:
|
||||
|
||||
MaterialBase* _baseMaterial = nullptr;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base material. If value gets changed parameters collection is restored to the default values of the new material.
|
||||
/// </summary>
|
||||
/// <returns>The base material.</returns>
|
||||
API_PROPERTY() FORCE_INLINE MaterialBase* GetBaseMaterial() const
|
||||
{
|
||||
return _baseMaterial;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the base material. If value gets changed parameters collection is restored to the default values of the new material.
|
||||
/// </summary>
|
||||
/// <param name="baseMaterial">The base material.</param>
|
||||
API_PROPERTY() void SetBaseMaterial(MaterialBase* baseMaterial);
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Saves this asset to the file. Supported only in Editor.
|
||||
/// </summary>
|
||||
/// <remarks>If you use saving with the GPU mesh data then the call has to be provided from the thread other than the main game thread.</remarks>
|
||||
/// <param name="path">The custom asset path to use for the saving. Use empty value to save this asset to its own storage location. Can be used to duplicate asset. Must be specified when saving virtual asset.</param>
|
||||
/// <returns>True if cannot save data, otherwise false.</returns>
|
||||
API_FUNCTION() bool Save(const StringView& path = StringView::Empty);
|
||||
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
void OnBaseSet();
|
||||
void OnBaseUnset();
|
||||
void OnBaseUnloaded(Asset* p);
|
||||
void OnBaseParamsChanged();
|
||||
void OnUnload();
|
||||
|
||||
public:
|
||||
|
||||
// [MaterialBase]
|
||||
bool IsMaterialInstance() const override;
|
||||
#if USE_EDITOR
|
||||
void GetReferences(Array<Guid>& output) const override;
|
||||
#endif
|
||||
|
||||
// [IMaterial]
|
||||
const MaterialInfo& GetInfo() const override;
|
||||
bool IsReady() const override;
|
||||
DrawPass GetDrawModes() const override;
|
||||
bool CanUseLightmap() const override;
|
||||
bool CanUseInstancing() const override;
|
||||
void Bind(BindParameters& params) override;
|
||||
|
||||
protected:
|
||||
|
||||
// [MaterialBase]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
AssetChunksFlag getChunksToPreload() const override;
|
||||
};
|
||||
839
Source/Engine/Content/Assets/Model.cpp
Normal file
839
Source/Engine/Content/Assets/Model.cpp
Normal file
@@ -0,0 +1,839 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Model.h"
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Engine/Engine.h"
|
||||
#include "Engine/Serialization/MemoryReadStream.h"
|
||||
#include "Engine/Content/WeakAssetReference.h"
|
||||
#include "Engine/Content/Upgraders/ModelAssetUpgrader.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
#include "Engine/Graphics/RenderTools.h"
|
||||
#include "Engine/Graphics/Models/ModelInstanceEntry.h"
|
||||
#include "Engine/Streaming/StreamingGroup.h"
|
||||
#include "Engine/Debug/Exceptions/ArgumentOutOfRangeException.h"
|
||||
#if GPU_ENABLE_ASYNC_RESOURCES_CREATION
|
||||
#include "Engine/Threading/ThreadPoolTask.h"
|
||||
#define STREAM_TASK_BASE ThreadPoolTask
|
||||
#else
|
||||
#include "Engine/Threading/MainThreadTask.h"
|
||||
#define STREAM_TASK_BASE MainThreadTask
|
||||
#endif
|
||||
|
||||
#define CHECK_INVALID_BUFFER(buffer) \
|
||||
if (buffer->IsValidFor(this) == false) \
|
||||
{ \
|
||||
LOG(Warning, "Invalid Model Instance Buffer size {0} for Model {1}. It should be {2}. Manual update to proper size.", buffer->Count(), ToString(), MaterialSlots.Count()); \
|
||||
buffer->Setup(this); \
|
||||
}
|
||||
|
||||
REGISTER_BINARY_ASSET_ABSTRACT(ModelBase, "FlaxEngine.ModelBase");
|
||||
|
||||
/// <summary>
|
||||
/// Model LOD streaming task.
|
||||
/// </summary>
|
||||
class StreamModelLODTask : public STREAM_TASK_BASE
|
||||
{
|
||||
private:
|
||||
|
||||
WeakAssetReference<Model> _asset;
|
||||
int32 _lodIndex;
|
||||
FlaxStorage::LockData _dataLock;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Init
|
||||
/// </summary>
|
||||
/// <param name="model">Parent model</param>
|
||||
/// <param name="lodIndex">LOD to stream index</param>
|
||||
StreamModelLODTask(Model* model, int32 lodIndex)
|
||||
: _asset(model)
|
||||
, _lodIndex(lodIndex)
|
||||
, _dataLock(model->Storage->Lock())
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
// [ThreadPoolTask]
|
||||
bool HasReference(Object* resource) const override
|
||||
{
|
||||
return _asset == resource || (_asset && _asset == resource);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
// [ThreadPoolTask]
|
||||
bool Run() override
|
||||
{
|
||||
AssetReference<Model> model = _asset.Get();
|
||||
if (model == nullptr)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get data
|
||||
BytesContainer data;
|
||||
model->GetLODData(_lodIndex, data);
|
||||
if (data.IsInvalid())
|
||||
{
|
||||
LOG(Warning, "Missing data chunk");
|
||||
return true;
|
||||
}
|
||||
MemoryReadStream stream(data.Get(), data.Length());
|
||||
|
||||
// Note: this is running on thread pool task so we must be sure that updated LOD is not used at all (for rendering)
|
||||
|
||||
// Load model LOD (initialize vertex and index buffers)
|
||||
if (model->LODs[_lodIndex].Load(stream))
|
||||
{
|
||||
LOG(Warning, "Cannot load LOD{1} for model \'{0}\'", model->ToString(), _lodIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Update residency level
|
||||
model->_loadedLODs++;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnEnd() override
|
||||
{
|
||||
// Unlink
|
||||
if (_asset)
|
||||
{
|
||||
ASSERT(_asset->_streamingTask == this);
|
||||
_asset->_streamingTask = nullptr;
|
||||
_asset.Unlink();
|
||||
}
|
||||
_dataLock.Release();
|
||||
|
||||
// Base
|
||||
STREAM_TASK_BASE::OnEnd();
|
||||
}
|
||||
};
|
||||
|
||||
REGISTER_BINARY_ASSET(Model, "FlaxEngine.Model", ::New<ModelAssetUpgrader>(), true);
|
||||
|
||||
Model::Model(const SpawnParams& params, const AssetInfo* info)
|
||||
: ModelBase(params, info, StreamingGroups::Instance()->Models())
|
||||
{
|
||||
}
|
||||
|
||||
Model::~Model()
|
||||
{
|
||||
// Ensure to be fully disposed
|
||||
ASSERT(IsInitialized() == false);
|
||||
ASSERT(_streamingTask == nullptr);
|
||||
}
|
||||
|
||||
bool Model::Intersects(const Ray& ray, const Matrix& world, float& distance, Vector3& normal, Mesh** mesh, int32 lodIndex)
|
||||
{
|
||||
return LODs[lodIndex].Intersects(ray, world, distance, normal, mesh);
|
||||
}
|
||||
|
||||
BoundingBox Model::GetBox(const Matrix& world, int32 lodIndex) const
|
||||
{
|
||||
return LODs[lodIndex].GetBox(world);
|
||||
}
|
||||
|
||||
BoundingBox Model::GetBox(int32 lodIndex) const
|
||||
{
|
||||
return LODs[lodIndex].GetBox();
|
||||
}
|
||||
|
||||
void Model::Draw(const RenderContext& renderContext, MaterialBase* material, const Matrix& world, StaticFlags flags, bool receiveDecals) const
|
||||
{
|
||||
if (!CanBeRendered())
|
||||
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, sphere.Radius, renderContext);
|
||||
if (lodIndex == -1)
|
||||
return;
|
||||
lodIndex += renderContext.View.ModelLODBias;
|
||||
lodIndex = ClampLODIndex(lodIndex);
|
||||
|
||||
// Draw
|
||||
LODs[lodIndex].Draw(renderContext, material, world, flags, receiveDecals);
|
||||
}
|
||||
|
||||
void Model::Draw(const RenderContext& renderContext, const Mesh::DrawInfo& info)
|
||||
{
|
||||
ASSERT(info.Buffer);
|
||||
if (!CanBeRendered())
|
||||
return;
|
||||
const auto frame = Engine::FrameCount;
|
||||
const auto modelFrame = info.DrawState->PrevFrame + 1;
|
||||
CHECK_INVALID_BUFFER(info.Buffer);
|
||||
|
||||
// Select a proper LOD index (model may be culled)
|
||||
int32 lodIndex;
|
||||
if (info.ForcedLOD != -1)
|
||||
{
|
||||
lodIndex = info.ForcedLOD;
|
||||
}
|
||||
else
|
||||
{
|
||||
lodIndex = RenderTools::ComputeModelLOD(this, info.Bounds.Center, info.Bounds.Radius, renderContext);
|
||||
if (lodIndex == -1)
|
||||
{
|
||||
// Handling model fade-out transition
|
||||
if (modelFrame == frame && info.DrawState->PrevLOD != -1)
|
||||
{
|
||||
// Check if start transition
|
||||
if (info.DrawState->LODTransition == 255)
|
||||
{
|
||||
info.DrawState->LODTransition = 0;
|
||||
}
|
||||
|
||||
RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition);
|
||||
|
||||
// Check if end transition
|
||||
if (info.DrawState->LODTransition == 255)
|
||||
{
|
||||
info.DrawState->PrevLOD = lodIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto prevLOD = ClampLODIndex(info.DrawState->PrevLOD);
|
||||
const float normalizedProgress = static_cast<float>(info.DrawState->LODTransition) * (1.0f / 255.0f);
|
||||
LODs[prevLOD].Draw(renderContext, info, normalizedProgress);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
lodIndex += info.LODBias + renderContext.View.ModelLODBias;
|
||||
lodIndex = ClampLODIndex(lodIndex);
|
||||
|
||||
// Check if it's the new frame and could update the drawing state (note: model instance could be rendered many times per frame to different viewports)
|
||||
if (modelFrame == frame)
|
||||
{
|
||||
// Check if start transition
|
||||
if (info.DrawState->PrevLOD != lodIndex && info.DrawState->LODTransition == 255)
|
||||
{
|
||||
info.DrawState->LODTransition = 0;
|
||||
}
|
||||
|
||||
RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition);
|
||||
|
||||
// Check if end transition
|
||||
if (info.DrawState->LODTransition == 255)
|
||||
{
|
||||
info.DrawState->PrevLOD = lodIndex;
|
||||
}
|
||||
}
|
||||
// Check if there was a gap between frames in drawing this model instance
|
||||
else if (modelFrame < frame || info.DrawState->PrevLOD == -1)
|
||||
{
|
||||
// Reset state
|
||||
info.DrawState->PrevLOD = lodIndex;
|
||||
info.DrawState->LODTransition = 255;
|
||||
}
|
||||
|
||||
// Draw
|
||||
if (info.DrawState->PrevLOD == lodIndex)
|
||||
{
|
||||
LODs[lodIndex].Draw(renderContext, info, 0.0f);
|
||||
}
|
||||
else if (info.DrawState->PrevLOD == -1)
|
||||
{
|
||||
const float normalizedProgress = static_cast<float>(info.DrawState->LODTransition) * (1.0f / 255.0f);
|
||||
LODs[lodIndex].Draw(renderContext, info, 1.0f - normalizedProgress);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto prevLOD = ClampLODIndex(info.DrawState->PrevLOD);
|
||||
const float normalizedProgress = static_cast<float>(info.DrawState->LODTransition) * (1.0f / 255.0f);
|
||||
LODs[prevLOD].Draw(renderContext, info, normalizedProgress);
|
||||
LODs[lodIndex].Draw(renderContext, info, normalizedProgress - 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
bool Model::SetupLODs(const Span<int32>& meshesCountPerLod)
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Validate input and state
|
||||
if (!IsVirtual())
|
||||
{
|
||||
LOG(Error, "Only virtual models can be updated at runtime.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return Init(meshesCountPerLod);
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
bool Model::Save(bool withMeshDataFromGpu, const StringView& path)
|
||||
{
|
||||
// Validate state
|
||||
if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
if (IsVirtual() && path.IsEmpty())
|
||||
{
|
||||
LOG(Error, "To save virtual asset asset you need to specify the target asset path location.");
|
||||
return true;
|
||||
}
|
||||
if (withMeshDataFromGpu && IsInMainThread())
|
||||
{
|
||||
LOG(Error, "To save model with GPU mesh buffers it needs to be called from the other thread (not the main thread).");
|
||||
return true;
|
||||
}
|
||||
if (IsVirtual() && !withMeshDataFromGpu)
|
||||
{
|
||||
LOG(Error, "To save virtual model asset you need to specify 'withMeshDataFromGpu' (it has no other storage container to get data).");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Create model data header
|
||||
MemoryWriteStream headerStream(1024);
|
||||
MemoryWriteStream* stream = &headerStream;
|
||||
{
|
||||
// Min Screen Size
|
||||
stream->WriteFloat(MinScreenSize);
|
||||
|
||||
// Amount of material slots
|
||||
stream->WriteInt32(MaterialSlots.Count());
|
||||
|
||||
// For each material slot
|
||||
for (int32 materialSlotIndex = 0; materialSlotIndex < MaterialSlots.Count(); materialSlotIndex++)
|
||||
{
|
||||
auto& slot = MaterialSlots[materialSlotIndex];
|
||||
|
||||
const auto id = slot.Material.GetID();
|
||||
stream->Write(&id);
|
||||
stream->WriteByte(static_cast<byte>(slot.ShadowsMode));
|
||||
stream->WriteString(slot.Name, 11);
|
||||
}
|
||||
|
||||
// Amount of LODs
|
||||
const int32 lods = LODs.Count();
|
||||
stream->WriteByte(lods);
|
||||
|
||||
// For each LOD
|
||||
for (int32 lodIndex = 0; lodIndex < lods; lodIndex++)
|
||||
{
|
||||
auto& lod = LODs[lodIndex];
|
||||
|
||||
// Screen Size
|
||||
stream->WriteFloat(lod.ScreenSize);
|
||||
|
||||
// Amount of meshes
|
||||
const int32 meshes = lod.Meshes.Count();
|
||||
stream->WriteUint16(meshes);
|
||||
|
||||
// For each mesh
|
||||
for (int32 meshIndex = 0; meshIndex < meshes; meshIndex++)
|
||||
{
|
||||
const auto& mesh = lod.Meshes[meshIndex];
|
||||
|
||||
// Material Slot index
|
||||
stream->WriteInt32(mesh.GetMaterialSlotIndex());
|
||||
|
||||
// Box
|
||||
const auto box = mesh.GetBox();
|
||||
stream->Write(&box);
|
||||
|
||||
// Sphere
|
||||
const auto sphere = mesh.GetSphere();
|
||||
stream->Write(&sphere);
|
||||
|
||||
// Has Lightmap UVs
|
||||
stream->WriteBool(mesh.HasLightmapUVs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use a temporary chunks for data storage for virtual assets
|
||||
FlaxChunk* tmpChunks[ASSET_FILE_DATA_CHUNKS];
|
||||
Platform::MemoryClear(tmpChunks, sizeof(tmpChunks));
|
||||
Array<FlaxChunk> chunks;
|
||||
if (IsVirtual())
|
||||
chunks.Resize(ASSET_FILE_DATA_CHUNKS);
|
||||
#define GET_CHUNK(index) (IsVirtual() ? tmpChunks[index] = &chunks[index] : GetOrCreateChunk(index))
|
||||
|
||||
// Check if use data from drive or from GPU
|
||||
if (withMeshDataFromGpu)
|
||||
{
|
||||
// Download all meshes buffers
|
||||
Array<Task*> tasks;
|
||||
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
|
||||
{
|
||||
auto& lod = LODs[lodIndex];
|
||||
|
||||
const int32 meshesCount = lod.Meshes.Count();
|
||||
struct MeshData
|
||||
{
|
||||
BytesContainer VB0;
|
||||
BytesContainer VB1;
|
||||
BytesContainer VB2;
|
||||
BytesContainer IB;
|
||||
|
||||
uint32 DataSize() const
|
||||
{
|
||||
return VB0.Length() + VB1.Length() + VB2.Length() + IB.Length();
|
||||
}
|
||||
};
|
||||
Array<MeshData> meshesData;
|
||||
meshesData.Resize(meshesCount);
|
||||
tasks.EnsureCapacity(meshesCount * 4);
|
||||
|
||||
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
|
||||
{
|
||||
const auto& mesh = lod.Meshes[meshIndex];
|
||||
auto& meshData = meshesData[meshIndex];
|
||||
|
||||
// Vertex Buffer 0 (required)
|
||||
auto task = mesh.ExtractDataAsync(MeshBufferType::Vertex0, meshData.VB0);
|
||||
if (task == nullptr)
|
||||
return true;
|
||||
task->Start();
|
||||
tasks.Add(task);
|
||||
|
||||
// Vertex Buffer 1 (required)
|
||||
task = mesh.ExtractDataAsync(MeshBufferType::Vertex1, meshData.VB1);
|
||||
if (task == nullptr)
|
||||
return true;
|
||||
task->Start();
|
||||
tasks.Add(task);
|
||||
|
||||
// Vertex Buffer 2 (optional)
|
||||
task = mesh.ExtractDataAsync(MeshBufferType::Vertex2, meshData.VB2);
|
||||
if (task)
|
||||
{
|
||||
task->Start();
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
||||
// Index Buffer (required)
|
||||
task = mesh.ExtractDataAsync(MeshBufferType::Index, meshData.IB);
|
||||
if (task == nullptr)
|
||||
return true;
|
||||
task->Start();
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
||||
// Wait for all
|
||||
if (Task::WaitAll(tasks))
|
||||
return true;
|
||||
tasks.Clear();
|
||||
|
||||
// Create meshes data
|
||||
{
|
||||
int32 dataSize = meshesCount * (2 * sizeof(uint32) + sizeof(bool));
|
||||
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
|
||||
{
|
||||
dataSize += meshesData[meshIndex].DataSize();
|
||||
}
|
||||
|
||||
MemoryWriteStream meshesStream(dataSize);
|
||||
|
||||
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
|
||||
{
|
||||
const auto& mesh = lod.Meshes[meshIndex];
|
||||
const auto& meshData = meshesData[meshIndex];
|
||||
|
||||
uint32 vertices = mesh.GetVertexCount();
|
||||
uint32 triangles = mesh.GetTriangleCount();
|
||||
bool hasColors = meshData.VB2.IsValid();
|
||||
uint32 vb0Size = vertices * sizeof(VB0ElementType);
|
||||
uint32 vb1Size = vertices * sizeof(VB1ElementType);
|
||||
uint32 vb2Size = vertices * sizeof(VB2ElementType);
|
||||
uint32 indicesCount = triangles * 3;
|
||||
bool shouldUse16BitIndexBuffer = indicesCount <= MAX_uint16;
|
||||
bool use16BitIndexBuffer = mesh.Use16BitIndexBuffer();
|
||||
uint32 ibSize = indicesCount * (use16BitIndexBuffer ? sizeof(uint16) : sizeof(uint32));
|
||||
|
||||
if (vertices == 0 || triangles == 0)
|
||||
{
|
||||
LOG(Warning, "Cannot save model with empty meshes.");
|
||||
return true;
|
||||
}
|
||||
if ((uint32)meshData.VB0.Length() < vb0Size)
|
||||
{
|
||||
LOG(Warning, "Invalid vertex buffer 0 size.");
|
||||
return true;
|
||||
}
|
||||
if ((uint32)meshData.VB1.Length() < vb1Size)
|
||||
{
|
||||
LOG(Warning, "Invalid vertex buffer 1 size.");
|
||||
return true;
|
||||
}
|
||||
if (hasColors && (uint32)meshData.VB2.Length() < vb2Size)
|
||||
{
|
||||
LOG(Warning, "Invalid vertex buffer 2 size.");
|
||||
return true;
|
||||
}
|
||||
if ((uint32)meshData.IB.Length() < ibSize)
|
||||
{
|
||||
LOG(Warning, "Invalid index buffer size.");
|
||||
return true;
|
||||
}
|
||||
|
||||
meshesStream.WriteUint32(vertices);
|
||||
meshesStream.WriteUint32(triangles);
|
||||
|
||||
meshesStream.WriteBytes(meshData.VB0.Get(), vb0Size);
|
||||
meshesStream.WriteBytes(meshData.VB1.Get(), vb1Size);
|
||||
|
||||
meshesStream.WriteBool(hasColors);
|
||||
|
||||
if (hasColors)
|
||||
{
|
||||
meshesStream.WriteBytes(meshData.VB2.Get(), vb2Size);
|
||||
}
|
||||
|
||||
if (shouldUse16BitIndexBuffer == use16BitIndexBuffer)
|
||||
{
|
||||
meshesStream.WriteBytes(meshData.IB.Get(), ibSize);
|
||||
}
|
||||
else if (shouldUse16BitIndexBuffer)
|
||||
{
|
||||
auto ib = (const int32*)meshData.IB.Get();
|
||||
for (uint32 i = 0; i < indicesCount; i++)
|
||||
{
|
||||
meshesStream.WriteUint16(ib[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CRASH;
|
||||
}
|
||||
}
|
||||
|
||||
// Override LOD data chunk with the fetched GPU meshes memory
|
||||
auto lodChunk = GET_CHUNK(MODEL_LOD_TO_CHUNK_INDEX(lodIndex));
|
||||
if (lodChunk == nullptr)
|
||||
return true;
|
||||
lodChunk->Data.Copy(meshesStream.GetHandle(), meshesStream.GetPosition());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT(!IsVirtual());
|
||||
|
||||
// Load all chunks with a mesh data
|
||||
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
|
||||
{
|
||||
if (LoadChunk(MODEL_LOD_TO_CHUNK_INDEX(lodIndex)))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Set mesh header data
|
||||
auto headerChunk = GET_CHUNK(0);
|
||||
ASSERT(headerChunk != nullptr);
|
||||
headerChunk->Data.Copy(headerStream.GetHandle(), headerStream.GetPosition());
|
||||
|
||||
#undef GET_CHUNK
|
||||
|
||||
// Save
|
||||
AssetInitData data;
|
||||
data.SerializedVersion = SerializedVersion;
|
||||
if (IsVirtual())
|
||||
Platform::MemoryCopy(_header.Chunks, tmpChunks, sizeof(_header.Chunks));
|
||||
const bool saveResult = path.HasChars() ? SaveAsset(path, data) : SaveAsset(data, true);
|
||||
if (IsVirtual())
|
||||
Platform::MemoryClear(_header.Chunks, sizeof(_header.Chunks));
|
||||
if (saveResult)
|
||||
{
|
||||
LOG(Error, "Cannot save \'{0}\'", ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool Model::Init(const Span<int32>& meshesCountPerLod)
|
||||
{
|
||||
if (meshesCountPerLod.IsInvalid() || meshesCountPerLod.Length() > MODEL_MAX_LODS)
|
||||
{
|
||||
Log::ArgumentOutOfRangeException();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dispose previous data and disable streaming (will start data uploading tasks manually)
|
||||
stopStreaming();
|
||||
|
||||
// Setup
|
||||
MaterialSlots.Resize(1);
|
||||
MinScreenSize = 0.0f;
|
||||
|
||||
// Setup LODs
|
||||
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
|
||||
{
|
||||
LODs[lodIndex].Dispose();
|
||||
}
|
||||
LODs.Resize(meshesCountPerLod.Length());
|
||||
_loadedLODs = meshesCountPerLod.Length();
|
||||
|
||||
// Setup meshes
|
||||
for (int32 lodIndex = 0; lodIndex < meshesCountPerLod.Length(); lodIndex++)
|
||||
{
|
||||
auto& lod = LODs[lodIndex];
|
||||
lod._model = this;
|
||||
lod.ScreenSize = 1.0f;
|
||||
const int32 meshesCount = meshesCountPerLod[lodIndex];
|
||||
if (meshesCount <= 0 || meshesCount > MODEL_MAX_MESHES)
|
||||
return true;
|
||||
|
||||
lod.Meshes.Resize(meshesCount);
|
||||
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
|
||||
{
|
||||
lod.Meshes[meshIndex].Init(this, lodIndex, meshIndex, 0, BoundingBox::Zero, BoundingSphere::Empty, true);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Model::SetupMaterialSlots(int32 slotsCount)
|
||||
{
|
||||
ModelBase::SetupMaterialSlots(slotsCount);
|
||||
|
||||
// Adjust meshes indices for slots
|
||||
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
|
||||
{
|
||||
for (int32 meshIndex = 0; meshIndex < LODs[lodIndex].Meshes.Count(); meshIndex++)
|
||||
{
|
||||
auto& mesh = LODs[lodIndex].Meshes[meshIndex];
|
||||
if (mesh.GetMaterialSlotIndex() >= slotsCount)
|
||||
mesh.SetMaterialSlotIndex(slotsCount - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Model::InitAsVirtual()
|
||||
{
|
||||
// Init with a single LOD and one mesh
|
||||
int32 meshesCount = 1;
|
||||
Init(ToSpan(&meshesCount, 1));
|
||||
|
||||
// Base
|
||||
BinaryAsset::InitAsVirtual();
|
||||
}
|
||||
|
||||
int32 Model::GetMaxResidency() const
|
||||
{
|
||||
return LODs.Count();
|
||||
}
|
||||
|
||||
int32 Model::GetCurrentResidency() const
|
||||
{
|
||||
return _loadedLODs;
|
||||
}
|
||||
|
||||
int32 Model::GetAllocatedResidency() const
|
||||
{
|
||||
return LODs.Count();
|
||||
}
|
||||
|
||||
bool Model::CanBeUpdated() const
|
||||
{
|
||||
// Check if is ready and has no streaming tasks running
|
||||
return IsInitialized() && _streamingTask == nullptr;
|
||||
}
|
||||
|
||||
Task* Model::UpdateAllocation(int32 residency)
|
||||
{
|
||||
// Models are not using dynamic allocation feature
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Task* Model::CreateStreamingTask(int32 residency)
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
ASSERT(IsInitialized() && Math::IsInRange(residency, 0, LODs.Count()) && _streamingTask == nullptr);
|
||||
Task* result = nullptr;
|
||||
const int32 lodCount = residency - GetCurrentResidency();
|
||||
|
||||
// Switch if go up or down with residency
|
||||
if (lodCount > 0)
|
||||
{
|
||||
// Allow only to change LODs count by 1
|
||||
ASSERT(Math::Abs(lodCount) == 1);
|
||||
|
||||
int32 lodIndex = HighestResidentLODIndex() - 1;
|
||||
|
||||
// Request LOD data
|
||||
result = (Task*)RequestLODDataAsync(lodIndex);
|
||||
|
||||
// Add upload data task
|
||||
_streamingTask = New<StreamModelLODTask>(this, lodIndex);
|
||||
if (result)
|
||||
result->ContinueWith(_streamingTask);
|
||||
else
|
||||
result = _streamingTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT(IsInMainThread());
|
||||
|
||||
// Do the quick data release
|
||||
for (int32 i = HighestResidentLODIndex(); i < LODs.Count() - residency; i++)
|
||||
LODs[i].Unload();
|
||||
_loadedLODs = residency;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Asset::LoadResult Model::load()
|
||||
{
|
||||
// Get header chunk
|
||||
auto chunk0 = GetChunk(0);
|
||||
if (chunk0 == nullptr || chunk0->IsMissing())
|
||||
return LoadResult::MissingDataChunk;
|
||||
MemoryReadStream headerStream(chunk0->Get(), chunk0->Size());
|
||||
ReadStream* stream = &headerStream;
|
||||
|
||||
// Min Screen Size
|
||||
stream->ReadFloat(&MinScreenSize);
|
||||
|
||||
// Amount of material slots
|
||||
int32 materialSlotsCount;
|
||||
stream->ReadInt32(&materialSlotsCount);
|
||||
if (materialSlotsCount <= 0 || materialSlotsCount > 4096)
|
||||
return LoadResult::InvalidData;
|
||||
MaterialSlots.Resize(materialSlotsCount, false);
|
||||
|
||||
// For each material slot
|
||||
for (int32 materialSlotIndex = 0; materialSlotIndex < materialSlotsCount; materialSlotIndex++)
|
||||
{
|
||||
auto& slot = MaterialSlots[materialSlotIndex];
|
||||
|
||||
// Material
|
||||
Guid materialId;
|
||||
stream->Read(&materialId);
|
||||
slot.Material = materialId;
|
||||
|
||||
// Shadows Mode
|
||||
slot.ShadowsMode = static_cast<ShadowsCastingMode>(stream->ReadByte());
|
||||
|
||||
// Name
|
||||
stream->ReadString(&slot.Name, 11);
|
||||
}
|
||||
|
||||
// Amount of LODs
|
||||
byte lods;
|
||||
stream->ReadByte(&lods);
|
||||
if (lods == 0 || lods > MODEL_MAX_LODS)
|
||||
return LoadResult::InvalidData;
|
||||
LODs.Resize(lods);
|
||||
|
||||
// For each LOD
|
||||
for (int32 lodIndex = 0; lodIndex < lods; lodIndex++)
|
||||
{
|
||||
auto& lod = LODs[lodIndex];
|
||||
lod._model = this;
|
||||
|
||||
// Screen Size
|
||||
stream->ReadFloat(&lod.ScreenSize);
|
||||
|
||||
// Amount of meshes
|
||||
uint16 meshesCount;
|
||||
stream->ReadUint16(&meshesCount);
|
||||
if (meshesCount == 0 || meshesCount > MODEL_MAX_MESHES)
|
||||
return LoadResult::InvalidData;
|
||||
ASSERT(lodIndex == 0 || LODs[0].Meshes.Count() >= meshesCount);
|
||||
|
||||
// Allocate memory
|
||||
lod.Meshes.Resize(meshesCount, false);
|
||||
|
||||
// For each mesh
|
||||
for (uint16 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
|
||||
{
|
||||
// Material Slot index
|
||||
int32 materialSlotIndex;
|
||||
stream->ReadInt32(&materialSlotIndex);
|
||||
if (materialSlotIndex < 0 || materialSlotIndex >= materialSlotsCount)
|
||||
{
|
||||
LOG(Warning, "Invalid material slot index {0} for mesh {1}. Slots count: {2}.", materialSlotIndex, meshIndex, materialSlotsCount);
|
||||
return LoadResult::InvalidData;
|
||||
}
|
||||
|
||||
// Box
|
||||
BoundingBox box;
|
||||
stream->Read(&box);
|
||||
|
||||
// Sphere
|
||||
BoundingSphere sphere;
|
||||
stream->Read(&sphere);
|
||||
|
||||
// Has Lightmap UVs
|
||||
bool hasLightmapUVs = stream->ReadBool();
|
||||
|
||||
lod.Meshes[meshIndex].Init(this, lodIndex, meshIndex, materialSlotIndex, box, sphere, hasLightmapUVs);
|
||||
}
|
||||
}
|
||||
|
||||
#if BUILD_DEBUG || BUILD_DEVELOPMENT
|
||||
// Validate LODs
|
||||
for (int32 lodIndex = 1; lodIndex < LODs.Count(); lodIndex++)
|
||||
{
|
||||
const auto prevSS = LODs[lodIndex - 1].ScreenSize;
|
||||
const auto thisSS = LODs[lodIndex].ScreenSize;
|
||||
if (prevSS <= thisSS)
|
||||
{
|
||||
LOG(Warning, "Model LOD {0} has invalid screen size compared to LOD {1} (asset: {2})", lodIndex, lodIndex - 1, GetPath());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Request resource streaming
|
||||
startStreaming(true);
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
void Model::unload(bool isReloading)
|
||||
{
|
||||
// End streaming (if still active)
|
||||
if (_streamingTask != nullptr)
|
||||
{
|
||||
// Cancel streaming task
|
||||
_streamingTask->Cancel();
|
||||
_streamingTask = nullptr;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
MaterialSlots.Resize(0);
|
||||
for (int32 i = 0; i < LODs.Count(); i++)
|
||||
LODs[i].Dispose();
|
||||
LODs.Clear();
|
||||
_loadedLODs = 0;
|
||||
}
|
||||
|
||||
bool Model::init(AssetInitData& initData)
|
||||
{
|
||||
// Validate
|
||||
if (initData.SerializedVersion != SerializedVersion)
|
||||
{
|
||||
LOG(Error, "Invalid serialized model version.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
AssetChunksFlag Model::getChunksToPreload() const
|
||||
{
|
||||
// Note: we don't preload any LODs here because it's done by the Streaming Manager
|
||||
return GET_CHUNK_FLAG(0);
|
||||
}
|
||||
269
Source/Engine/Content/Assets/Model.h
Normal file
269
Source/Engine/Content/Assets/Model.h
Normal file
@@ -0,0 +1,269 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ModelBase.h"
|
||||
#include "Engine/Graphics/Models/ModelLOD.h"
|
||||
|
||||
// Note: we use the first chunk as a header, next is the highest quality lod and then lower ones
|
||||
//
|
||||
// Example:
|
||||
// Chunk 0: Header
|
||||
// Chunk 1: LOD0
|
||||
// Chunk 2: LOD1
|
||||
// ..
|
||||
//
|
||||
#define MODEL_LOD_TO_CHUNK_INDEX(lod) (lod + 1)
|
||||
|
||||
class Mesh;
|
||||
class StreamModelLODTask;
|
||||
|
||||
/// <summary>
|
||||
/// Model asset that contains model object made of meshes which can rendered on the GPU.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API Model : public ModelBase
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(Model, 25);
|
||||
friend Mesh;
|
||||
friend StreamModelLODTask;
|
||||
private:
|
||||
|
||||
int32 _loadedLODs = 0;
|
||||
StreamModelLODTask* _streamingTask = nullptr;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Model level of details. The first entry is the highest quality LOD0 followed by more optimized versions.
|
||||
/// </summary>
|
||||
API_FIELD(ReadOnly) Array<ModelLOD, FixedAllocation<MODEL_MAX_LODS>> LODs;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="Model"/> class.
|
||||
/// </summary>
|
||||
~Model();
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is initialized.
|
||||
/// </summary>
|
||||
FORCE_INLINE bool IsInitialized() const
|
||||
{
|
||||
return LODs.HasItems();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets amount of the level of details in the model
|
||||
/// </summary>
|
||||
/// <returns>Amount of the level of details in the model</returns>
|
||||
FORCE_INLINE int32 GetLODsCount() const
|
||||
{
|
||||
return LODs.Count();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of loaded model LODs.
|
||||
/// </summary>
|
||||
/// <returns>Loaded LODs count</returns>
|
||||
API_PROPERTY() FORCE_INLINE int32 GetLoadedLODs() const
|
||||
{
|
||||
return _loadedLODs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified index is a valid LOD index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <returns>True if the specified index is a valid LOD index, otherwise false.</returns>
|
||||
FORCE_INLINE bool IsValidLODIndex(int32 index) const
|
||||
{
|
||||
return Math::IsInRange(index, 0, LODs.Count() - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the index of the LOD to be valid for rendering (only loaded LODs).
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <returns>The resident LOD index.</returns>
|
||||
FORCE_INLINE int32 ClampLODIndex(int32 index) const
|
||||
{
|
||||
return Math::Clamp(index, HighestResidentLODIndex(), LODs.Count() - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets index of the highest resident LOD (may be equal to LODs.Count if no LOD has been uploaded). Note: LOD=0 is the highest (top quality)
|
||||
/// </summary>
|
||||
/// <returns>LOD index</returns>
|
||||
FORCE_INLINE int32 HighestResidentLODIndex() const
|
||||
{
|
||||
return GetLODsCount() - _loadedLODs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether any LOD has been initialized.
|
||||
/// </summary>
|
||||
/// <returns>True if any LOD has been initialized, otherwise false.</returns>
|
||||
FORCE_INLINE bool HasAnyLODInitialized() const
|
||||
{
|
||||
return LODs.HasItems() && LODs.Last().HasAnyMeshInitialized();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this model can be rendered.
|
||||
/// </summary>
|
||||
/// <returns>True if can render that model, otherwise false.</returns>
|
||||
FORCE_INLINE bool CanBeRendered() const
|
||||
{
|
||||
return _loadedLODs > 0;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Requests the LOD data asynchronously (creates task that will gather chunk data or null if already here).
|
||||
/// </summary>
|
||||
/// <param name="lodIndex">Index of the LOD.</param>
|
||||
/// <returns>Task that will gather chunk data or null if already here.</returns>
|
||||
ContentLoadTask* RequestLODDataAsync(int32 lodIndex)
|
||||
{
|
||||
const int32 chunkIndex = MODEL_LOD_TO_CHUNK_INDEX(lodIndex);
|
||||
return RequestChunkDataAsync(chunkIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model LOD data (links bytes).
|
||||
/// </summary>
|
||||
/// <param name="lodIndex">Index of the LOD.</param>
|
||||
/// <param name="data">The data (may be missing if failed to get it).</param>
|
||||
void GetLODData(int32 lodIndex, BytesContainer& data) const
|
||||
{
|
||||
const int32 chunkIndex = MODEL_LOD_TO_CHUNK_INDEX(lodIndex);
|
||||
GetChunkData(chunkIndex, data);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Determines if there is an intersection between the Model and a Ray in given world using given instance.
|
||||
/// </summary>
|
||||
/// <param name="ray">The ray to test</param>
|
||||
/// <param name="world">World to test</param>
|
||||
/// <param name="distance">When the method completes, contains the distance of the intersection (if any valid).</param>
|
||||
/// <param name="normal">When the method completes, contains the intersection surface normal vector (if any valid).</param>
|
||||
/// <param name="mesh">Mesh, or null</param>
|
||||
/// <param name="lodIndex">Level Of Detail index</param>
|
||||
/// <returns>True whether the two objects intersected</returns>
|
||||
bool Intersects(const Ray& ray, const Matrix& world, float& distance, Vector3& normal, Mesh** mesh, int32 lodIndex = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model bounding box in custom matrix world space.
|
||||
/// </summary>
|
||||
/// <param name="world">The transformation matrix.</param>
|
||||
/// <param name="lodIndex">The Level Of Detail index.</param>
|
||||
/// <returns>The bounding box.</returns>
|
||||
API_FUNCTION() BoundingBox GetBox(const Matrix& world, int32 lodIndex = 0) const;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model bounding box in local space.
|
||||
/// </summary>
|
||||
/// <param name="lodIndex">The Level Of Detail index.</param>
|
||||
/// <returns>The bounding box.</returns>
|
||||
API_FUNCTION() BoundingBox GetBox(int32 lodIndex = 0) const;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Draws the meshes. Binds vertex and index buffers and invokes the draw calls.
|
||||
/// </summary>
|
||||
/// <param name="context">GPU context to draw with.</param>
|
||||
/// <param name="lodIndex">The Level Of Detail index.</param>
|
||||
void Render(GPUContext* context, int32 lodIndex = 0)
|
||||
{
|
||||
LODs[lodIndex].Render(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the model.
|
||||
/// </summary>
|
||||
/// <param name="renderContext">The rendering context.</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>
|
||||
API_FUNCTION() void Draw(API_PARAM(Ref) const RenderContext& renderContext, MaterialBase* material, API_PARAM(Ref) const Matrix& world, StaticFlags flags = StaticFlags::None, bool receiveDecals = true) const;
|
||||
|
||||
/// <summary>
|
||||
/// Draws the model.
|
||||
/// </summary>
|
||||
/// <param name="renderContext">The rendering context.</param>
|
||||
/// <param name="info">The packed drawing info data.</param>
|
||||
void Draw(const RenderContext& renderContext, const Mesh::DrawInfo& info);
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Setups the model LODs collection including meshes creation.
|
||||
/// </summary>
|
||||
/// <param name="meshesCountPerLod">The meshes count per lod array (amount of meshes per LOD).</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() bool SetupLODs(const Span<int32>& meshesCountPerLod);
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Saves this asset to the file. Supported only in Editor.
|
||||
/// </summary>
|
||||
/// <remarks>If you use saving with the GPU mesh data then the call has to be provided from the thread other than the main game thread.</remarks>
|
||||
/// <param name="withMeshDataFromGpu">True if save also GPU mesh buffers, otherwise will keep data in storage unmodified. Valid only if saving the same asset to the same location and it's loaded.</param>
|
||||
/// <param name="path">The custom asset path to use for the saving. Use empty value to save this asset to its own storage location. Can be used to duplicate asset. Must be specified when saving virtual asset.</param>
|
||||
/// <returns>True if cannot save data, otherwise false.</returns>
|
||||
API_FUNCTION() bool Save(bool withMeshDataFromGpu = false, const StringView& path = StringView::Empty);
|
||||
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this model to an empty collection of LODs with meshes.
|
||||
/// </summary>
|
||||
/// <param name="meshesCountPerLod">The meshes count per lod array (amount of meshes per LOD).</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
bool Init(const Span<int32>& meshesCountPerLod);
|
||||
|
||||
public:
|
||||
|
||||
// [ModelBase]
|
||||
void SetupMaterialSlots(int32 slotsCount) override;
|
||||
void InitAsVirtual() override;
|
||||
#if USE_EDITOR
|
||||
void GetReferences(Array<Guid>& output) const override
|
||||
{
|
||||
// Base
|
||||
BinaryAsset::GetReferences(output);
|
||||
|
||||
for (int32 i = 0; i < MaterialSlots.Count(); i++)
|
||||
{
|
||||
output.Add(MaterialSlots[i].Material.GetID());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// [StreamableResource]
|
||||
int32 GetMaxResidency() const override;
|
||||
int32 GetCurrentResidency() const override;
|
||||
int32 GetAllocatedResidency() const override;
|
||||
bool CanBeUpdated() const override;
|
||||
Task* UpdateAllocation(int32 residency) override;
|
||||
Task* CreateStreamingTask(int32 residency) override;
|
||||
|
||||
protected:
|
||||
|
||||
// [ModelBase]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
bool init(AssetInitData& initData) override;
|
||||
AssetChunksFlag getChunksToPreload() const override;
|
||||
};
|
||||
81
Source/Engine/Content/Assets/ModelBase.h
Normal file
81
Source/Engine/Content/Assets/ModelBase.h
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../BinaryAsset.h"
|
||||
#include "Engine/Core/Collections/Array.h"
|
||||
#include "Engine/Graphics/Models/MaterialSlot.h"
|
||||
#include "Engine/Streaming/StreamableResource.h"
|
||||
|
||||
/// <summary>
|
||||
/// Base class for asset types that can contain a model resource.
|
||||
/// </summary>
|
||||
API_CLASS(Abstract, NoSpawn) class FLAXENGINE_API ModelBase : public BinaryAsset, public StreamableResource
|
||||
{
|
||||
DECLARE_ASSET_HEADER(ModelBase);
|
||||
protected:
|
||||
|
||||
explicit ModelBase(const SpawnParams& params, const AssetInfo* info, StreamingGroup* group)
|
||||
: BinaryAsset(params, info)
|
||||
, StreamableResource(group)
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The minimum screen size to draw this model (the bottom limit). Used to cull small models. Set to 0 to disable this feature.
|
||||
/// </summary>
|
||||
API_FIELD() float MinScreenSize = 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The list of material slots.
|
||||
/// </summary>
|
||||
API_FIELD(ReadOnly) Array<MaterialSlot> MaterialSlots;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of the material slots used by this model asset.
|
||||
/// </summary>
|
||||
API_PROPERTY() int32 GetMaterialSlotsCount() const
|
||||
{
|
||||
return MaterialSlots.Count();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes the material slots collection. Updates meshes that were using removed slots.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void SetupMaterialSlots(int32 slotsCount)
|
||||
{
|
||||
CHECK(slotsCount >= 0 && slotsCount < 4096);
|
||||
if (!IsVirtual() && WaitForLoaded())
|
||||
return;
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
const int32 prevCount = MaterialSlots.Count();
|
||||
MaterialSlots.Resize(slotsCount);
|
||||
|
||||
// Initialize slot names
|
||||
for (int32 i = prevCount; i < slotsCount; i++)
|
||||
MaterialSlots[i].Name = String::Format(TEXT("Material {0}"), i + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the material slot by the name.
|
||||
/// </summary>
|
||||
/// <param name="name">The slot name.</param>
|
||||
/// <returns>The material slot with the given name or null if cannot find it (asset may be not loaded yet).</returns>
|
||||
API_FUNCTION() MaterialSlot* GetSlot(const StringView& name)
|
||||
{
|
||||
MaterialSlot* result = nullptr;
|
||||
for (auto& slot : MaterialSlots)
|
||||
{
|
||||
if (slot.Name == name)
|
||||
{
|
||||
result = &slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
85
Source/Engine/Content/Assets/RawDataAsset.cpp
Normal file
85
Source/Engine/Content/Assets/RawDataAsset.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "RawDataAsset.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
#include "Engine/Platform/FileSystem.h"
|
||||
|
||||
REGISTER_BINARY_ASSET(RawDataAsset, "FlaxEngine.RawDataAsset", nullptr, true);
|
||||
|
||||
RawDataAsset::RawDataAsset(const SpawnParams& params, const AssetInfo* info)
|
||||
: BinaryAsset(params, info)
|
||||
{
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
bool RawDataAsset::Save(const StringView& path)
|
||||
{
|
||||
// Validate state
|
||||
if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
if (IsVirtual() && path.IsEmpty())
|
||||
{
|
||||
LOG(Error, "To save virtual asset asset you need to specify the target asset path location.");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
bool result;
|
||||
if (IsVirtual())
|
||||
{
|
||||
FlaxChunk* tmpChunks[ASSET_FILE_DATA_CHUNKS];
|
||||
Platform::MemoryClear(tmpChunks, sizeof(tmpChunks));
|
||||
FlaxChunk chunk;
|
||||
tmpChunks[0] = &chunk;
|
||||
tmpChunks[0]->Data.Link(Data);
|
||||
|
||||
AssetInitData initData;
|
||||
initData.SerializedVersion = SerializedVersion;
|
||||
Platform::MemoryCopy(_header.Chunks, tmpChunks, sizeof(_header.Chunks));
|
||||
result = path.HasChars() ? SaveAsset(path, initData) : SaveAsset(initData, true);
|
||||
Platform::MemoryClear(_header.Chunks, sizeof(_header.Chunks));
|
||||
}
|
||||
else
|
||||
{
|
||||
auto chunk0 = GetChunk(0);
|
||||
chunk0->Data.Link(Data);
|
||||
|
||||
AssetInitData initData;
|
||||
initData.SerializedVersion = SerializedVersion;
|
||||
result = path.HasChars() ? SaveAsset(path, initData) : SaveAsset(initData, true);
|
||||
|
||||
chunk0->Data.Unlink();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Asset::LoadResult RawDataAsset::load()
|
||||
{
|
||||
auto chunk0 = GetChunk(0);
|
||||
if (chunk0 == nullptr || chunk0->IsMissing())
|
||||
return LoadResult::MissingDataChunk;
|
||||
|
||||
Data.Clear();
|
||||
Data.EnsureCapacity(chunk0->Data.Length());
|
||||
Data.Add(chunk0->Data.Get(), chunk0->Data.Length());
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
void RawDataAsset::unload(bool isReloading)
|
||||
{
|
||||
Data.Resize(0);
|
||||
}
|
||||
|
||||
AssetChunksFlag RawDataAsset::getChunksToPreload() const
|
||||
{
|
||||
return GET_CHUNK_FLAG(0);
|
||||
}
|
||||
40
Source/Engine/Content/Assets/RawDataAsset.h
Normal file
40
Source/Engine/Content/Assets/RawDataAsset.h
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../BinaryAsset.h"
|
||||
|
||||
/// <summary>
|
||||
/// Raw bytes container asset.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API RawDataAsset : public BinaryAsset
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(RawDataAsset, 1);
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The bytes array stored in the asset.
|
||||
/// </summary>
|
||||
API_FIELD() Array<byte> Data;
|
||||
|
||||
public:
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Saves this asset to the file. Supported only in Editor.
|
||||
/// </summary>
|
||||
/// <param name="path">The custom asset path to use for the saving. Use empty value to save this asset to its own storage location. Can be used to duplicate asset. Must be specified when saving virtual asset.</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() bool Save(const StringView& path = StringView::Empty);
|
||||
|
||||
#endif
|
||||
|
||||
protected:
|
||||
|
||||
// [BinaryAsset]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
AssetChunksFlag getChunksToPreload() const override;
|
||||
};
|
||||
62
Source/Engine/Content/Assets/Shader.cpp
Normal file
62
Source/Engine/Content/Assets/Shader.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Shader.h"
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Content/Upgraders/ShaderAssetUpgrader.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
#include "Engine/Serialization/MemoryReadStream.h"
|
||||
|
||||
REGISTER_BINARY_ASSET(Shader, "FlaxEngine.Shader", ::New<ShaderAssetUpgrader>(), false);
|
||||
|
||||
Shader::Shader(const SpawnParams& params, const AssetInfo* info)
|
||||
: ShaderAssetTypeBase<BinaryAsset>(params, info)
|
||||
{
|
||||
ASSERT(GPUDevice::Instance);
|
||||
_shader = GPUDevice::Instance->CreateShader(info->Path);
|
||||
ASSERT(_shader);
|
||||
GPU = _shader;
|
||||
}
|
||||
|
||||
Shader::~Shader()
|
||||
{
|
||||
Delete(_shader);
|
||||
}
|
||||
|
||||
Asset::LoadResult Shader::load()
|
||||
{
|
||||
// Special case for Null renderer that doesn't need shaders
|
||||
if (GPUDevice::Instance->GetRendererType() == RendererType::Null)
|
||||
{
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
// Load shader cache (it may call compilation or gather cached data)
|
||||
ShaderCacheResult shaderCache;
|
||||
if (LoadShaderCache(shaderCache))
|
||||
{
|
||||
LOG(Error, "Cannot load \'{0}\' shader cache.", ToString());
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
MemoryReadStream shaderCacheStream(shaderCache.Data.Get(), shaderCache.Data.Length());
|
||||
|
||||
// Create shader from cache
|
||||
if (_shader->Create(shaderCacheStream))
|
||||
{
|
||||
LOG(Error, "Cannot load shader \'{0}\'", ToString());
|
||||
return LoadResult::Failed;
|
||||
}
|
||||
|
||||
#if COMPILE_WITH_SHADER_COMPILER
|
||||
RegisterForShaderReloads(this, shaderCache);
|
||||
#endif
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
void Shader::unload(bool isReloading)
|
||||
{
|
||||
#if COMPILE_WITH_SHADER_COMPILER
|
||||
UnregisterForShaderReloads(this);
|
||||
#endif
|
||||
|
||||
_shader->ReleaseGPU();
|
||||
}
|
||||
47
Source/Engine/Content/Assets/Shader.h
Normal file
47
Source/Engine/Content/Assets/Shader.h
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../BinaryAsset.h"
|
||||
#include "Engine/Graphics/Shaders/GPUShader.h"
|
||||
#include "Engine/Graphics/Shaders/Cache/ShaderAssetBase.h"
|
||||
|
||||
/// <summary>
|
||||
/// The shader asset. Contains a program that runs on the GPU and is able to perform rendering calculation using textures, vertices and other resources.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API Shader : public ShaderAssetTypeBase<BinaryAsset>
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(Shader, ShadersSerializedVersion);
|
||||
private:
|
||||
|
||||
GPUShader* _shader;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="Shader"/> class.
|
||||
/// </summary>
|
||||
~Shader();
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The GPU shader object (not null).
|
||||
/// </summary>
|
||||
API_FIELD(ReadOnly) GPUShader* GPU;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GPU shader object.
|
||||
/// </summary>
|
||||
/// <returns>The GPU shader object.</returns>
|
||||
FORCE_INLINE GPUShader* GetShader() const
|
||||
{
|
||||
return GPU;
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
// [BinaryAsset]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
};
|
||||
130
Source/Engine/Content/Assets/SkeletonMask.cpp
Normal file
130
Source/Engine/Content/Assets/SkeletonMask.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "SkeletonMask.h"
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Serialization/MemoryReadStream.h"
|
||||
#include "Engine/Serialization/MemoryWriteStream.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
#include "Engine/Content/Upgraders/SkeletonMaskUpgrader.h"
|
||||
|
||||
REGISTER_BINARY_ASSET(SkeletonMask, "FlaxEngine.SkeletonMask", ::New<SkeletonMaskUpgrader>(), true);
|
||||
|
||||
SkeletonMask::SkeletonMask(const SpawnParams& params, const AssetInfo* info)
|
||||
: BinaryAsset(params, info)
|
||||
{
|
||||
Skeleton.Unload.Bind<SkeletonMask, &SkeletonMask::OnSkeletonUnload>(this);
|
||||
}
|
||||
|
||||
Asset::LoadResult SkeletonMask::load()
|
||||
{
|
||||
const auto dataChunk = GetChunk(0);
|
||||
if (dataChunk == nullptr)
|
||||
return LoadResult::MissingDataChunk;
|
||||
MemoryReadStream stream(dataChunk->Get(), dataChunk->Size());
|
||||
|
||||
Guid skeletonId;
|
||||
stream.Read(&skeletonId);
|
||||
int32 maskedNodesCount;
|
||||
stream.ReadInt32(&maskedNodesCount);
|
||||
_maskedNodes.Resize(maskedNodesCount);
|
||||
for (auto& e : _maskedNodes)
|
||||
{
|
||||
stream.ReadString(&e, -13);
|
||||
}
|
||||
Skeleton = skeletonId;
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
void SkeletonMask::unload(bool isReloading)
|
||||
{
|
||||
Skeleton.Unlink();
|
||||
_maskedNodes.Resize(0);
|
||||
_mask.Resize(0);
|
||||
}
|
||||
|
||||
AssetChunksFlag SkeletonMask::getChunksToPreload() const
|
||||
{
|
||||
return GET_CHUNK_FLAG(0);
|
||||
}
|
||||
|
||||
const BitArray<>& SkeletonMask::GetNodesMask()
|
||||
{
|
||||
if (_mask.IsEmpty() && Skeleton && !Skeleton->WaitForLoaded())
|
||||
{
|
||||
_mask.Resize(Skeleton->Skeleton.Nodes.Count());
|
||||
for (int32 i = 0; i < _mask.Count(); i++)
|
||||
{
|
||||
_mask.Set(i, _maskedNodes.Contains(Skeleton->Skeleton.Nodes[i].Name));
|
||||
}
|
||||
}
|
||||
|
||||
return _mask;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
bool SkeletonMask::Save(const StringView& path)
|
||||
{
|
||||
// Validate state
|
||||
if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
if (IsVirtual() && path.IsEmpty())
|
||||
{
|
||||
LOG(Error, "To save virtual asset asset you need to specify the target asset path location.");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Write data
|
||||
MemoryWriteStream stream(4096);
|
||||
const auto skeletonId = Skeleton.GetID();
|
||||
stream.Write(&skeletonId);
|
||||
stream.WriteInt32(_maskedNodes.Count());
|
||||
for (auto& e : _maskedNodes)
|
||||
{
|
||||
stream.WriteString(e, -13);
|
||||
}
|
||||
|
||||
// Save
|
||||
bool result;
|
||||
if (IsVirtual())
|
||||
{
|
||||
FlaxChunk* tmpChunks[ASSET_FILE_DATA_CHUNKS];
|
||||
Platform::MemoryClear(tmpChunks, sizeof(tmpChunks));
|
||||
FlaxChunk chunk;
|
||||
tmpChunks[0] = &chunk;
|
||||
tmpChunks[0]->Data.Link(stream.GetHandle(), stream.GetPosition());
|
||||
|
||||
AssetInitData initData;
|
||||
initData.SerializedVersion = SerializedVersion;
|
||||
Platform::MemoryCopy(_header.Chunks, tmpChunks, sizeof(_header.Chunks));
|
||||
result = path.HasChars() ? SaveAsset(path, initData) : SaveAsset(initData, true);
|
||||
Platform::MemoryClear(_header.Chunks, sizeof(_header.Chunks));
|
||||
}
|
||||
else
|
||||
{
|
||||
auto chunk0 = GetChunk(0);
|
||||
chunk0->Data.Copy(stream.GetHandle(), stream.GetPosition());
|
||||
|
||||
AssetInitData initData;
|
||||
initData.SerializedVersion = SerializedVersion;
|
||||
result = path.HasChars() ? SaveAsset(path, initData) : SaveAsset(initData, true);
|
||||
|
||||
chunk0->Data.Unlink();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void SkeletonMask::OnSkeletonUnload()
|
||||
{
|
||||
// Clear cached mask
|
||||
_mask.Resize(0);
|
||||
}
|
||||
91
Source/Engine/Content/Assets/SkeletonMask.h
Normal file
91
Source/Engine/Content/Assets/SkeletonMask.h
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../BinaryAsset.h"
|
||||
#include "Engine/Content/Assets/SkinnedModel.h"
|
||||
#include "Engine/Core/Collections/BitArray.h"
|
||||
|
||||
class MemoryReadStream;
|
||||
class MemoryWriteStream;
|
||||
|
||||
/// <summary>
|
||||
/// The skinned model skeleton bones boolean masking data.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API SkeletonMask : public BinaryAsset
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(SkeletonMask, 2);
|
||||
private:
|
||||
|
||||
Array<String> _maskedNodes;
|
||||
BitArray<> _mask;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The referenced skinned model skeleton that defines the masked nodes hierarchy.
|
||||
/// </summary>
|
||||
API_FIELD() AssetReference<SkinnedModel> Skeleton;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the per-skeleton node mask (by name).
|
||||
/// </summary>
|
||||
/// <returns>The masked nodes names list.</returns>
|
||||
API_PROPERTY() const Array<String>& GetMaskedNodes() const
|
||||
{
|
||||
return _maskedNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the per-skeleton node mask (by name).
|
||||
/// </summary>
|
||||
/// <param name="value">The masked nodes names list.</param>
|
||||
API_PROPERTY() void SetMaskedNodes(const Array<String>& value)
|
||||
{
|
||||
_maskedNodes = value;
|
||||
_mask.Resize(0);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets the per-skeleton-node boolean mask (read-only).
|
||||
/// </summary>
|
||||
/// <returns>The constant reference to the skeleton nodes mask.</returns>
|
||||
API_PROPERTY() const BitArray<>& GetNodesMask();
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Saves this asset to the file. Supported only in Editor.
|
||||
/// </summary>
|
||||
/// <param name="path">The custom asset path to use for the saving. Use empty value to save this asset to its own storage location. Can be used to duplicate asset. Must be specified when saving virtual asset.</param>
|
||||
/// <returns>True if cannot save data, otherwise false.</returns>
|
||||
API_FUNCTION() bool Save(const StringView& path = StringView::Empty);
|
||||
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
void OnSkeletonUnload();
|
||||
|
||||
public:
|
||||
|
||||
// [BinaryAsset]
|
||||
#if USE_EDITOR
|
||||
void GetReferences(Array<Guid>& output) const override
|
||||
{
|
||||
// Base
|
||||
BinaryAsset::GetReferences(output);
|
||||
|
||||
output.Add(Skeleton.GetID());
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
|
||||
// [BinaryAsset]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
AssetChunksFlag getChunksToPreload() const override;
|
||||
};
|
||||
969
Source/Engine/Content/Assets/SkinnedModel.cpp
Normal file
969
Source/Engine/Content/Assets/SkinnedModel.cpp
Normal file
@@ -0,0 +1,969 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "SkinnedModel.h"
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Engine/Engine.h"
|
||||
#include "Engine/Serialization/MemoryReadStream.h"
|
||||
#include "Engine/Streaming/StreamingGroup.h"
|
||||
#include "Engine/Threading/ThreadPoolTask.h"
|
||||
#include "Engine/Graphics/RenderTools.h"
|
||||
#include "Engine/Graphics/Models/ModelInstanceEntry.h"
|
||||
#include "Engine/Graphics/Models/Config.h"
|
||||
#include "Engine/Content/WeakAssetReference.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
#include "Engine/Content/Upgraders/SkinnedModelAssetUpgrader.h"
|
||||
#include "Engine/Debug/Exceptions/ArgumentOutOfRangeException.h"
|
||||
|
||||
#define CHECK_INVALID_BUFFER(buffer) \
|
||||
if (buffer->IsValidFor(this) == false) \
|
||||
{ \
|
||||
LOG(Warning, "Invalid Skinned Model Instance Buffer size {0} for Skinned Model {1}. It should be {2}. Manual update to proper size.", buffer->Count(), ToString(), MaterialSlots.Count()); \
|
||||
buffer->Setup(this); \
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skinned model data streaming task
|
||||
/// </summary>
|
||||
class StreamSkinnedModelLODTask : public ThreadPoolTask
|
||||
{
|
||||
private:
|
||||
|
||||
WeakAssetReference<SkinnedModel> _asset;
|
||||
int32 _lodIndex;
|
||||
FlaxStorage::LockData _dataLock;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Init
|
||||
/// </summary>
|
||||
/// <param name="model">Parent model</param>
|
||||
/// <param name="lodIndex">LOD to stream index</param>
|
||||
StreamSkinnedModelLODTask(SkinnedModel* model, int32 lodIndex)
|
||||
: _asset(model)
|
||||
, _lodIndex(lodIndex)
|
||||
, _dataLock(model->Storage->Lock())
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
// [ThreadPoolTask]
|
||||
bool HasReference(Object* resource) const override
|
||||
{
|
||||
return _asset == resource || (_asset && _asset == resource);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
// [ThreadPoolTask]
|
||||
bool Run() override
|
||||
{
|
||||
AssetReference<SkinnedModel> model = _asset.Get();
|
||||
if (model == nullptr)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get data
|
||||
BytesContainer data;
|
||||
model->GetLODData(_lodIndex, data);
|
||||
if (data.IsInvalid())
|
||||
{
|
||||
LOG(Warning, "Missing data chunk");
|
||||
return true;
|
||||
}
|
||||
MemoryReadStream stream(data.Get(), data.Length());
|
||||
|
||||
// Note: this is running on thread pool task so we must be sure that updated LOD is not used at all (for rendering)
|
||||
|
||||
// Load model LOD (initialize vertex and index buffers)
|
||||
if (model->LODs[_lodIndex].Load(stream))
|
||||
{
|
||||
LOG(Warning, "Cannot load LOD{1} for model \'{0}\'", model->ToString(), _lodIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Update residency level
|
||||
model->_loadedLODs++;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnEnd() override
|
||||
{
|
||||
// Unlink
|
||||
if (_asset)
|
||||
{
|
||||
ASSERT(_asset->_streamingTask == this);
|
||||
_asset->_streamingTask = nullptr;
|
||||
_asset.Unlink();
|
||||
}
|
||||
_dataLock.Release();
|
||||
|
||||
// Base
|
||||
ThreadPoolTask::OnEnd();
|
||||
}
|
||||
};
|
||||
|
||||
REGISTER_BINARY_ASSET(SkinnedModel, "FlaxEngine.SkinnedModel", ::New<SkinnedModelAssetUpgrader>(), true);
|
||||
|
||||
SkinnedModel::SkinnedModel(const SpawnParams& params, const AssetInfo* info)
|
||||
: ModelBase(params, info, StreamingGroups::Instance()->SkinnedModels())
|
||||
{
|
||||
}
|
||||
|
||||
SkinnedModel::~SkinnedModel()
|
||||
{
|
||||
// Ensure to be fully disposed
|
||||
ASSERT(IsInitialized() == false);
|
||||
ASSERT(_streamingTask == nullptr);
|
||||
}
|
||||
|
||||
Array<String> SkinnedModel::GetBlendShapes()
|
||||
{
|
||||
Array<String> result;
|
||||
if (LODs.HasItems())
|
||||
{
|
||||
for (auto& mesh : LODs[0].Meshes)
|
||||
{
|
||||
for (auto& blendShape : mesh.BlendShapes)
|
||||
{
|
||||
if (!result.Contains(blendShape.Name))
|
||||
result.Add(blendShape.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool SkinnedModel::Intersects(const Ray& ray, const Matrix& world, float& distance, Vector3& normal, SkinnedMesh** mesh, int32 lodIndex)
|
||||
{
|
||||
return LODs[lodIndex].Intersects(ray, world, distance, normal, mesh);
|
||||
}
|
||||
|
||||
BoundingBox SkinnedModel::GetBox(const Matrix& world, int32 lodIndex) const
|
||||
{
|
||||
return LODs[lodIndex].GetBox(world);
|
||||
}
|
||||
|
||||
BoundingBox SkinnedModel::GetBox(int32 lodIndex) const
|
||||
{
|
||||
return LODs[lodIndex].GetBox();
|
||||
}
|
||||
|
||||
void SkinnedModel::Draw(RenderContext& renderContext, const SkinnedMesh::DrawInfo& info)
|
||||
{
|
||||
ASSERT(info.Buffer);
|
||||
if (!CanBeRendered())
|
||||
return;
|
||||
const auto frame = Engine::FrameCount;
|
||||
const auto modelFrame = info.DrawState->PrevFrame + 1;
|
||||
CHECK_INVALID_BUFFER(info.Buffer);
|
||||
|
||||
// Select a proper LOD index (model may be culled)
|
||||
int32 lodIndex;
|
||||
if (info.ForcedLOD != -1)
|
||||
{
|
||||
lodIndex = (int32)info.ForcedLOD;
|
||||
}
|
||||
else
|
||||
{
|
||||
lodIndex = RenderTools::ComputeSkinnedModelLOD(this, info.Bounds.Center, info.Bounds.Radius, renderContext);
|
||||
if (lodIndex == -1)
|
||||
{
|
||||
// Handling model fade-out transition
|
||||
if (modelFrame == frame && info.DrawState->PrevLOD != -1)
|
||||
{
|
||||
// Check if start transition
|
||||
if (info.DrawState->LODTransition == 255)
|
||||
{
|
||||
info.DrawState->LODTransition = 0;
|
||||
}
|
||||
|
||||
RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition);
|
||||
|
||||
// Check if end transition
|
||||
if (info.DrawState->LODTransition == 255)
|
||||
{
|
||||
info.DrawState->PrevLOD = lodIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto prevLOD = ClampLODIndex(info.DrawState->PrevLOD);
|
||||
const float normalizedProgress = static_cast<float>(info.DrawState->LODTransition) * (1.0f / 255.0f);
|
||||
LODs[prevLOD].Draw(renderContext, info, normalizedProgress);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
lodIndex += info.LODBias + renderContext.View.ModelLODBias;
|
||||
lodIndex = ClampLODIndex(lodIndex);
|
||||
|
||||
// Check if it's the new frame and could update the drawing state (note: model instance could be rendered many times per frame to different viewports)
|
||||
if (modelFrame == frame)
|
||||
{
|
||||
// Check if start transition
|
||||
if (info.DrawState->PrevLOD != lodIndex && info.DrawState->LODTransition == 255)
|
||||
{
|
||||
info.DrawState->LODTransition = 0;
|
||||
}
|
||||
|
||||
RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition);
|
||||
|
||||
// Check if end transition
|
||||
if (info.DrawState->LODTransition == 255)
|
||||
{
|
||||
info.DrawState->PrevLOD = lodIndex;
|
||||
}
|
||||
}
|
||||
// Check if there was a gap between frames in drawing this model instance
|
||||
else if (modelFrame < frame || info.DrawState->PrevLOD == -1)
|
||||
{
|
||||
// Reset state
|
||||
info.DrawState->PrevLOD = lodIndex;
|
||||
info.DrawState->LODTransition = 255;
|
||||
}
|
||||
|
||||
// Draw
|
||||
if (info.DrawState->PrevLOD == lodIndex)
|
||||
{
|
||||
LODs[lodIndex].Draw(renderContext, info, 0.0f);
|
||||
}
|
||||
else if (info.DrawState->PrevLOD == -1)
|
||||
{
|
||||
const float normalizedProgress = static_cast<float>(info.DrawState->LODTransition) * (1.0f / 255.0f);
|
||||
LODs[lodIndex].Draw(renderContext, info, 1.0f - normalizedProgress);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto prevLOD = ClampLODIndex(info.DrawState->PrevLOD);
|
||||
const float normalizedProgress = static_cast<float>(info.DrawState->LODTransition) * (1.0f / 255.0f);
|
||||
LODs[prevLOD].Draw(renderContext, info, normalizedProgress);
|
||||
LODs[lodIndex].Draw(renderContext, info, normalizedProgress - 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
bool SkinnedModel::SetupLODs(const Span<int32>& meshesCountPerLod)
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Validate input and state
|
||||
if (!IsVirtual())
|
||||
{
|
||||
LOG(Error, "Only virtual models can be updated at runtime.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return Init(meshesCountPerLod);;
|
||||
}
|
||||
|
||||
bool SkinnedModel::SetupSkeleton(const Array<SkeletonNode>& nodes)
|
||||
{
|
||||
// Validate input
|
||||
if (nodes.Count() <= 0 || nodes.Count() > MAX_uint16)
|
||||
return true;
|
||||
auto model = this;
|
||||
if (!model->IsVirtual())
|
||||
return true;
|
||||
|
||||
ScopeLock lock(model->Locker);
|
||||
|
||||
// Setup nodes
|
||||
model->Skeleton.Nodes = nodes;
|
||||
|
||||
// Setup bones
|
||||
model->Skeleton.Bones.Resize(nodes.Count());
|
||||
for (int32 i = 0; i < nodes.Count(); i++)
|
||||
{
|
||||
auto& node = model->Skeleton.Nodes[i];
|
||||
model->Skeleton.Bones[i].ParentIndex = node.ParentIndex;
|
||||
model->Skeleton.Bones[i].LocalTransform = node.LocalTransform;
|
||||
model->Skeleton.Bones[i].NodeIndex = i;
|
||||
}
|
||||
|
||||
// Calculate offset matrix (inverse bind pose transform) for every bone manually
|
||||
for (int32 i = 0; i < model->Skeleton.Bones.Count(); i++)
|
||||
{
|
||||
Matrix t = Matrix::Identity;
|
||||
int32 idx = model->Skeleton.Bones[i].NodeIndex;
|
||||
|
||||
do
|
||||
{
|
||||
t *= model->Skeleton.Nodes[idx].LocalTransform.GetWorld();
|
||||
idx = model->Skeleton.Nodes[idx].ParentIndex;
|
||||
} while (idx != -1);
|
||||
|
||||
t.Invert();
|
||||
|
||||
model->Skeleton.Bones[i].OffsetMatrix = t;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SkinnedModel::SetupSkeleton(const Array<SkeletonNode>& nodes, const Array<SkeletonBone>& bones, bool autoCalculateOffsetMatrix)
|
||||
{
|
||||
// Validate input
|
||||
if (nodes.Count() <= 0 || nodes.Count() > MAX_uint16)
|
||||
return true;
|
||||
if (bones.Count() <= 0 || bones.Count() > MAX_BONES_PER_MODEL)
|
||||
return true;
|
||||
auto model = this;
|
||||
if (!model->IsVirtual())
|
||||
return true;
|
||||
|
||||
ScopeLock lock(model->Locker);
|
||||
|
||||
// Setup nodes
|
||||
model->Skeleton.Nodes = nodes;
|
||||
|
||||
// Setup bones
|
||||
model->Skeleton.Bones = bones;
|
||||
|
||||
// Calculate offset matrix (inverse bind pose transform) for every bone manually
|
||||
if (autoCalculateOffsetMatrix)
|
||||
{
|
||||
for (int32 i = 0; i < model->Skeleton.Bones.Count(); i++)
|
||||
{
|
||||
Matrix t = Matrix::Identity;
|
||||
int32 idx = model->Skeleton.Bones[i].NodeIndex;
|
||||
|
||||
do
|
||||
{
|
||||
t *= model->Skeleton.Nodes[idx].LocalTransform.GetWorld();
|
||||
idx = model->Skeleton.Nodes[idx].ParentIndex;
|
||||
} while (idx != -1);
|
||||
|
||||
t.Invert();
|
||||
|
||||
model->Skeleton.Bones[i].OffsetMatrix = t;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
bool SkinnedModel::Save(bool withMeshDataFromGpu, const StringView& path)
|
||||
{
|
||||
// Validate state
|
||||
if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
if (IsVirtual() && path.IsEmpty())
|
||||
{
|
||||
LOG(Error, "To save virtual asset asset you need to specify the target asset path location.");
|
||||
return true;
|
||||
}
|
||||
if (withMeshDataFromGpu && IsInMainThread())
|
||||
{
|
||||
LOG(Error, "To save model with GPU mesh buffers it needs to be called from the other thread (not the main thread).");
|
||||
return true;
|
||||
}
|
||||
if (IsVirtual() && !withMeshDataFromGpu)
|
||||
{
|
||||
LOG(Error, "To save virtual model asset you need to specify 'withMeshDataFromGpu' (it has no other storage container to get data).");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Create model data header
|
||||
MemoryWriteStream headerStream(1024);
|
||||
MemoryWriteStream* stream = &headerStream;
|
||||
{
|
||||
// Min Screen Size
|
||||
stream->WriteFloat(MinScreenSize);
|
||||
|
||||
// Amount of material slots
|
||||
stream->WriteInt32(MaterialSlots.Count());
|
||||
|
||||
// For each material slot
|
||||
for (int32 materialSlotIndex = 0; materialSlotIndex < MaterialSlots.Count(); materialSlotIndex++)
|
||||
{
|
||||
auto& slot = MaterialSlots[materialSlotIndex];
|
||||
|
||||
const auto id =slot.Material.GetID();
|
||||
stream->Write(&id);
|
||||
stream->WriteByte(static_cast<byte>(slot.ShadowsMode));
|
||||
stream->WriteString(slot.Name, 11);
|
||||
}
|
||||
|
||||
// Amount of LODs
|
||||
const int32 lods = LODs.Count();
|
||||
stream->WriteByte(lods);
|
||||
|
||||
// For each LOD
|
||||
for (int32 lodIndex = 0; lodIndex < lods; lodIndex++)
|
||||
{
|
||||
auto& lod = LODs[lodIndex];
|
||||
|
||||
// Screen Size
|
||||
stream->WriteFloat(lod.ScreenSize);
|
||||
|
||||
// Amount of meshes
|
||||
const int32 meshes = lod.Meshes.Count();
|
||||
stream->WriteUint16(meshes);
|
||||
|
||||
// For each mesh
|
||||
for (int32 meshIndex = 0; meshIndex < meshes; meshIndex++)
|
||||
{
|
||||
const auto& mesh = lod.Meshes[meshIndex];
|
||||
|
||||
// Material Slot index
|
||||
stream->WriteInt32(mesh.GetMaterialSlotIndex());
|
||||
|
||||
// Box
|
||||
const auto box = mesh.GetBox();
|
||||
stream->Write(&box);
|
||||
|
||||
// Sphere
|
||||
const auto sphere = mesh.GetSphere();
|
||||
stream->Write(&sphere);
|
||||
|
||||
// Blend Shapes
|
||||
stream->WriteUint16(mesh.BlendShapes.Count());
|
||||
for (int32 blendShapeIndex = 0; blendShapeIndex < mesh.BlendShapes.Count(); 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->Write(&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->Write(&bone.LocalTransform);
|
||||
stream->Write(&bone.OffsetMatrix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use a temporary chunks for data storage for virtual assets
|
||||
FlaxChunk* tmpChunks[ASSET_FILE_DATA_CHUNKS];
|
||||
Platform::MemoryClear(tmpChunks, sizeof(tmpChunks));
|
||||
Array<FlaxChunk> chunks;
|
||||
if (IsVirtual())
|
||||
chunks.Resize(ASSET_FILE_DATA_CHUNKS);
|
||||
#define GET_CHUNK(index) (IsVirtual() ? tmpChunks[index] = &chunks[index] : GetOrCreateChunk(index))
|
||||
|
||||
// Check if use data from drive or from GPU
|
||||
if (withMeshDataFromGpu)
|
||||
{
|
||||
// Download all meshes buffers
|
||||
Array<Task*> tasks;
|
||||
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
|
||||
{
|
||||
auto& lod = LODs[lodIndex];
|
||||
|
||||
const int32 meshesCount = lod.Meshes.Count();
|
||||
struct MeshData
|
||||
{
|
||||
BytesContainer VB0;
|
||||
BytesContainer IB;
|
||||
|
||||
uint32 DataSize() const
|
||||
{
|
||||
return VB0.Length() + IB.Length();
|
||||
}
|
||||
};
|
||||
Array<MeshData> meshesData;
|
||||
meshesData.Resize(meshesCount);
|
||||
tasks.EnsureCapacity(meshesCount * 4);
|
||||
|
||||
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
|
||||
{
|
||||
const auto& mesh = lod.Meshes[meshIndex];
|
||||
auto& meshData = meshesData[meshIndex];
|
||||
|
||||
// Vertex Buffer 0 (required)
|
||||
auto task = mesh.DownloadDataAsyncGPU(MeshBufferType::Vertex0, meshData.VB0);
|
||||
if (task == nullptr)
|
||||
return true;
|
||||
task->Start();
|
||||
tasks.Add(task);
|
||||
|
||||
// Index Buffer (required)
|
||||
task = mesh.DownloadDataAsyncGPU(MeshBufferType::Index, meshData.IB);
|
||||
if (task == nullptr)
|
||||
return true;
|
||||
task->Start();
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
||||
// Wait for all
|
||||
if (Task::WaitAll(tasks))
|
||||
return true;
|
||||
tasks.Clear();
|
||||
|
||||
// Create meshes data
|
||||
{
|
||||
int32 dataSize = meshesCount * (2 * sizeof(uint32) + sizeof(bool));
|
||||
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
|
||||
{
|
||||
dataSize += meshesData[meshIndex].DataSize();
|
||||
}
|
||||
|
||||
MemoryWriteStream meshesStream(dataSize);
|
||||
|
||||
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
|
||||
{
|
||||
const auto& mesh = lod.Meshes[meshIndex];
|
||||
const auto& meshData = meshesData[meshIndex];
|
||||
|
||||
const uint32 vertices = mesh.GetVertexCount();
|
||||
const uint32 triangles = mesh.GetTriangleCount();
|
||||
const uint32 vb0Size = vertices * sizeof(VB0SkinnedElementType);
|
||||
const uint32 indicesCount = triangles * 3;
|
||||
const bool shouldUse16BitIndexBuffer = indicesCount <= MAX_uint16;
|
||||
const bool use16BitIndexBuffer = mesh.Use16BitIndexBuffer();
|
||||
const uint32 ibSize = indicesCount * (use16BitIndexBuffer ? sizeof(uint16) : sizeof(uint32));
|
||||
|
||||
if (vertices == 0 || triangles == 0)
|
||||
{
|
||||
LOG(Warning, "Cannot save model with empty meshes.");
|
||||
return true;
|
||||
}
|
||||
if ((uint32)meshData.VB0.Length() < vb0Size)
|
||||
{
|
||||
LOG(Warning, "Invalid vertex buffer 0 size.");
|
||||
return true;
|
||||
}
|
||||
if ((uint32)meshData.IB.Length() < ibSize)
|
||||
{
|
||||
LOG(Warning, "Invalid index buffer size.");
|
||||
return true;
|
||||
}
|
||||
|
||||
meshesStream.WriteUint32(vertices);
|
||||
meshesStream.WriteUint32(triangles);
|
||||
meshesStream.WriteUint16(mesh.BlendShapes.Count());
|
||||
|
||||
for (const auto& blendShape : mesh.BlendShapes)
|
||||
{
|
||||
meshesStream.WriteBool(blendShape.UseNormals);
|
||||
meshesStream.WriteUint32(blendShape.MinVertexIndex);
|
||||
meshesStream.WriteUint32(blendShape.MaxVertexIndex);
|
||||
meshesStream.WriteUint32(blendShape.Vertices.Count());
|
||||
meshesStream.WriteBytes(blendShape.Vertices.Get(), blendShape.Vertices.Count() * sizeof(BlendShapeVertex));
|
||||
}
|
||||
|
||||
meshesStream.WriteBytes(meshData.VB0.Get(), vb0Size);
|
||||
|
||||
if (shouldUse16BitIndexBuffer == use16BitIndexBuffer)
|
||||
{
|
||||
meshesStream.WriteBytes(meshData.IB.Get(), ibSize);
|
||||
}
|
||||
else if (shouldUse16BitIndexBuffer)
|
||||
{
|
||||
const auto ib = reinterpret_cast<const int32*>(meshData.IB.Get());
|
||||
for (uint32 i = 0; i < indicesCount; i++)
|
||||
{
|
||||
meshesStream.WriteUint16(ib[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CRASH;
|
||||
}
|
||||
}
|
||||
|
||||
// Override meshes data chunk with the fetched GPU meshes memory
|
||||
auto lodChunk = GET_CHUNK(SKINNED_MODEL_LOD_TO_CHUNK_INDEX(lodIndex));
|
||||
if (lodChunk == nullptr)
|
||||
return true;
|
||||
lodChunk->Data.Copy(meshesStream.GetHandle(), meshesStream.GetPosition());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT(!IsVirtual());
|
||||
|
||||
// Load all chunks with a mesh data
|
||||
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
|
||||
{
|
||||
if (LoadChunk(SKINNED_MODEL_LOD_TO_CHUNK_INDEX(lodIndex)))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Set mesh header data
|
||||
auto headerChunk = GET_CHUNK(0);
|
||||
ASSERT(headerChunk != nullptr);
|
||||
headerChunk->Data.Copy(headerStream.GetHandle(), headerStream.GetPosition());
|
||||
|
||||
#undef GET_CHUNK
|
||||
|
||||
// Save
|
||||
AssetInitData data;
|
||||
data.SerializedVersion = SerializedVersion;
|
||||
if (IsVirtual())
|
||||
Platform::MemoryCopy(_header.Chunks, tmpChunks, sizeof(_header.Chunks));
|
||||
const bool saveResult = path.HasChars() ? SaveAsset(path, data) : SaveAsset(data, true);
|
||||
if (IsVirtual())
|
||||
Platform::MemoryClear(_header.Chunks, sizeof(_header.Chunks));
|
||||
if (saveResult)
|
||||
{
|
||||
LOG(Error, "Cannot save \'{0}\'", ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool SkinnedModel::Init(const Span<int32>& meshesCountPerLod)
|
||||
{
|
||||
if (meshesCountPerLod.IsInvalid() || meshesCountPerLod.Length() > MODEL_MAX_LODS)
|
||||
{
|
||||
Log::ArgumentOutOfRangeException();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dispose previous data and disable streaming (will start data uploading tasks manually)
|
||||
stopStreaming();
|
||||
|
||||
// Setup
|
||||
MaterialSlots.Resize(1);
|
||||
MinScreenSize = 0.0f;
|
||||
|
||||
// Setup LODs
|
||||
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
|
||||
{
|
||||
LODs[lodIndex].Dispose();
|
||||
}
|
||||
LODs.Resize(meshesCountPerLod.Length());
|
||||
_loadedLODs = meshesCountPerLod.Length();
|
||||
|
||||
// Setup meshes
|
||||
for (int32 lodIndex = 0; lodIndex < meshesCountPerLod.Length(); lodIndex++)
|
||||
{
|
||||
auto& lod = LODs[lodIndex];
|
||||
lod._model = this;
|
||||
lod.ScreenSize = 1.0f;
|
||||
const int32 meshesCount = meshesCountPerLod[lodIndex];
|
||||
if (meshesCount <= 0 || meshesCount > MODEL_MAX_MESHES)
|
||||
return true;
|
||||
|
||||
lod.Meshes.Resize(meshesCount);
|
||||
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
|
||||
{
|
||||
lod.Meshes[meshIndex].Init(this, lodIndex, meshIndex, 0, BoundingBox::Zero, BoundingSphere::Empty);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void SkinnedModel::SetupMaterialSlots(int32 slotsCount)
|
||||
{
|
||||
ModelBase::SetupMaterialSlots(slotsCount);
|
||||
|
||||
// Adjust meshes indices for slots
|
||||
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
|
||||
{
|
||||
for (int32 meshIndex = 0; meshIndex < LODs[lodIndex].Meshes.Count(); meshIndex++)
|
||||
{
|
||||
auto& mesh = LODs[lodIndex].Meshes[meshIndex];
|
||||
if (mesh.GetMaterialSlotIndex() >= slotsCount)
|
||||
mesh.SetMaterialSlotIndex(slotsCount - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SkinnedModel::InitAsVirtual()
|
||||
{
|
||||
// Init with one mesh and single bone
|
||||
int32 meshesCount = 1;
|
||||
Init(ToSpan(&meshesCount, 1));
|
||||
Skeleton.Dispose();
|
||||
//
|
||||
Skeleton.Nodes.Resize(1);
|
||||
Skeleton.Nodes[0].Name = TEXT("Root");
|
||||
Skeleton.Nodes[0].LocalTransform = Transform::Identity;
|
||||
Skeleton.Nodes[0].ParentIndex = -1;
|
||||
//
|
||||
Skeleton.Bones.Resize(1);
|
||||
Skeleton.Bones[0].NodeIndex = 0;
|
||||
Skeleton.Bones[0].OffsetMatrix = Matrix::Identity;
|
||||
Skeleton.Bones[0].LocalTransform = Transform::Identity;
|
||||
Skeleton.Bones[0].ParentIndex = -1;
|
||||
|
||||
// Base
|
||||
BinaryAsset::InitAsVirtual();
|
||||
}
|
||||
|
||||
int32 SkinnedModel::GetMaxResidency() const
|
||||
{
|
||||
return LODs.Count();
|
||||
}
|
||||
|
||||
int32 SkinnedModel::GetCurrentResidency() const
|
||||
{
|
||||
return _loadedLODs;
|
||||
}
|
||||
|
||||
int32 SkinnedModel::GetAllocatedResidency() const
|
||||
{
|
||||
return LODs.Count();
|
||||
}
|
||||
|
||||
bool SkinnedModel::CanBeUpdated() const
|
||||
{
|
||||
// Check if is ready and has no streaming tasks running
|
||||
return IsInitialized() && _streamingTask == nullptr;
|
||||
}
|
||||
|
||||
Task* SkinnedModel::UpdateAllocation(int32 residency)
|
||||
{
|
||||
// SkinnedModels are not using dynamic allocation feature
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Task* SkinnedModel::CreateStreamingTask(int32 residency)
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
ASSERT(IsInitialized() && Math::IsInRange(residency, 0, LODs.Count()) && _streamingTask == nullptr);
|
||||
Task* result = nullptr;
|
||||
const int32 lodCount = residency - GetCurrentResidency();
|
||||
|
||||
// Switch if go up or down with residency
|
||||
if (lodCount > 0)
|
||||
{
|
||||
// Allow only to change LODs count by 1
|
||||
ASSERT(Math::Abs(lodCount) == 1);
|
||||
|
||||
int32 lodIndex = HighestResidentLODIndex() - 1;
|
||||
|
||||
// Request LOD data
|
||||
result = (Task*)RequestLODDataAsync(lodIndex);
|
||||
|
||||
// Add upload data task
|
||||
_streamingTask = New<StreamSkinnedModelLODTask>(this, lodIndex);
|
||||
if (result)
|
||||
result->ContinueWith(_streamingTask);
|
||||
else
|
||||
result = _streamingTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT(IsInMainThread());
|
||||
|
||||
// Do the quick data release
|
||||
for (int32 i = HighestResidentLODIndex(); i < LODs.Count() - residency; i++)
|
||||
LODs[i].Unload();
|
||||
_loadedLODs = residency;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Asset::LoadResult SkinnedModel::load()
|
||||
{
|
||||
// Get header chunk
|
||||
auto chunk0 = GetChunk(0);
|
||||
if (chunk0 == nullptr || chunk0->IsMissing())
|
||||
return LoadResult::MissingDataChunk;
|
||||
MemoryReadStream headerStream(chunk0->Get(), chunk0->Size());
|
||||
ReadStream* stream = &headerStream;
|
||||
|
||||
// Min Screen Size
|
||||
stream->ReadFloat(&MinScreenSize);
|
||||
|
||||
// Amount of material slots
|
||||
int32 materialSlotsCount;
|
||||
stream->ReadInt32(&materialSlotsCount);
|
||||
if (materialSlotsCount <= 0 || materialSlotsCount > 4096)
|
||||
return LoadResult::InvalidData;
|
||||
MaterialSlots.Resize(materialSlotsCount, false);
|
||||
|
||||
// For each material slot
|
||||
for (int32 materialSlotIndex = 0; materialSlotIndex < materialSlotsCount; materialSlotIndex++)
|
||||
{
|
||||
auto& slot = MaterialSlots[materialSlotIndex];
|
||||
|
||||
// Material
|
||||
Guid materialId;
|
||||
stream->Read(&materialId);
|
||||
slot.Material = materialId;
|
||||
|
||||
// Shadows Mode
|
||||
slot.ShadowsMode = static_cast<ShadowsCastingMode>(stream->ReadByte());
|
||||
|
||||
// Name
|
||||
stream->ReadString(&slot.Name, 11);
|
||||
}
|
||||
|
||||
// Amount of LODs
|
||||
byte lods;
|
||||
stream->ReadByte(&lods);
|
||||
if (lods == 0 || lods > MODEL_MAX_LODS)
|
||||
return LoadResult::InvalidData;
|
||||
LODs.Resize(lods);
|
||||
|
||||
// For each LOD
|
||||
for (int32 lodIndex = 0; lodIndex < lods; lodIndex++)
|
||||
{
|
||||
auto& lod = LODs[lodIndex];
|
||||
lod._model = this;
|
||||
|
||||
// Screen Size
|
||||
stream->ReadFloat(&lod.ScreenSize);
|
||||
|
||||
// Amount of meshes
|
||||
uint16 meshesCount;
|
||||
stream->ReadUint16(&meshesCount);
|
||||
if (meshesCount == 0 || meshesCount > MODEL_MAX_MESHES)
|
||||
return LoadResult::InvalidData;
|
||||
ASSERT(lodIndex == 0 || LODs[0].Meshes.Count() >= meshesCount);
|
||||
|
||||
// Allocate memory
|
||||
lod.Meshes.Resize(meshesCount, false);
|
||||
|
||||
// For each mesh
|
||||
for (uint16 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
|
||||
{
|
||||
auto& mesh = lod.Meshes[meshIndex];
|
||||
|
||||
// Material Slot index
|
||||
int32 materialSlotIndex;
|
||||
stream->ReadInt32(&materialSlotIndex);
|
||||
if (materialSlotIndex < 0 || materialSlotIndex >= materialSlotsCount)
|
||||
{
|
||||
LOG(Warning, "Invalid material slot index {0} for mesh {1}. Slots count: {2}.", materialSlotIndex, meshIndex, materialSlotsCount);
|
||||
return LoadResult::InvalidData;
|
||||
}
|
||||
|
||||
// Box
|
||||
BoundingBox box;
|
||||
stream->Read(&box);
|
||||
|
||||
// Sphere
|
||||
BoundingSphere sphere;
|
||||
stream->Read(&sphere);
|
||||
|
||||
// Create mesh object
|
||||
mesh.Init(this, lodIndex, meshIndex, materialSlotIndex, box, sphere);
|
||||
|
||||
// Blend Shapes
|
||||
uint16 blendShapes;
|
||||
stream->ReadUint16(&blendShapes);
|
||||
mesh.BlendShapes.Resize(blendShapes);
|
||||
for (int32 blendShapeIndex = 0; blendShapeIndex < blendShapes; blendShapeIndex++)
|
||||
{
|
||||
auto& blendShape = mesh.BlendShapes[blendShapeIndex];
|
||||
stream->ReadString(&blendShape.Name, 13);
|
||||
stream->ReadFloat(&blendShape.Weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skeleton
|
||||
{
|
||||
int32 nodesCount;
|
||||
stream->ReadInt32(&nodesCount);
|
||||
if (nodesCount <= 0)
|
||||
return LoadResult::InvalidData;
|
||||
Skeleton.Nodes.Resize(nodesCount, false);
|
||||
|
||||
// For each node
|
||||
for (int32 nodeIndex = 0; nodeIndex < nodesCount; nodeIndex++)
|
||||
{
|
||||
auto& node = Skeleton.Nodes[nodeIndex];
|
||||
|
||||
stream->Read(&node.ParentIndex);
|
||||
stream->Read(&node.LocalTransform);
|
||||
stream->ReadString(&node.Name, 71);
|
||||
}
|
||||
|
||||
int32 bonesCount;
|
||||
stream->ReadInt32(&bonesCount);
|
||||
if (bonesCount <= 0)
|
||||
return LoadResult::InvalidData;
|
||||
Skeleton.Bones.Resize(bonesCount, false);
|
||||
|
||||
// For each bone
|
||||
for (int32 boneIndex = 0; boneIndex < bonesCount; boneIndex++)
|
||||
{
|
||||
auto& bone = Skeleton.Bones[boneIndex];
|
||||
|
||||
stream->Read(&bone.ParentIndex);
|
||||
stream->Read(&bone.NodeIndex);
|
||||
stream->Read(&bone.LocalTransform);
|
||||
stream->Read(&bone.OffsetMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
// Request resource streaming
|
||||
startStreaming(true);
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
void SkinnedModel::unload(bool isReloading)
|
||||
{
|
||||
// End streaming (if still active)
|
||||
if (_streamingTask != nullptr)
|
||||
{
|
||||
// Cancel streaming task
|
||||
_streamingTask->Cancel();
|
||||
_streamingTask = nullptr;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
MaterialSlots.Resize(0);
|
||||
for (int32 i = 0; i < LODs.Count(); i++)
|
||||
LODs[i].Dispose();
|
||||
LODs.Clear();
|
||||
Skeleton.Dispose();
|
||||
_loadedLODs = 0;
|
||||
}
|
||||
|
||||
bool SkinnedModel::init(AssetInitData& initData)
|
||||
{
|
||||
// Validate
|
||||
if (initData.SerializedVersion != SerializedVersion)
|
||||
{
|
||||
LOG(Error, "Invalid serialized model version.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
AssetChunksFlag SkinnedModel::getChunksToPreload() const
|
||||
{
|
||||
// Note: we don't preload any meshes here because it's done by the Streaming Manager
|
||||
return GET_CHUNK_FLAG(0);
|
||||
}
|
||||
332
Source/Engine/Content/Assets/SkinnedModel.h
Normal file
332
Source/Engine/Content/Assets/SkinnedModel.h
Normal file
@@ -0,0 +1,332 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ModelBase.h"
|
||||
#include "Engine/Graphics/Models/Config.h"
|
||||
#include "Engine/Graphics/Models/SkeletonData.h"
|
||||
#include "Engine/Graphics/Models/SkinnedModelLOD.h"
|
||||
|
||||
class StreamSkinnedModelLODTask;
|
||||
|
||||
// Note: we use the first chunk as a header, next is the highest quality lod and then lower ones
|
||||
//
|
||||
// Example:
|
||||
// Chunk 0: Header
|
||||
// Chunk 1: LOD0
|
||||
// Chunk 2: LOD1
|
||||
// ..
|
||||
//
|
||||
#define SKINNED_MODEL_LOD_TO_CHUNK_INDEX(lod) (lod + 1)
|
||||
|
||||
/// <summary>
|
||||
/// Skinned model asset that contains model object made of meshes that can be rendered on the GPU using skeleton bones skinning.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API SkinnedModel : public ModelBase
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(SkinnedModel, 4);
|
||||
friend SkinnedMesh;
|
||||
friend StreamSkinnedModelLODTask;
|
||||
private:
|
||||
|
||||
int32 _loadedLODs = 0;
|
||||
StreamSkinnedModelLODTask* _streamingTask = nullptr;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Model level of details. The first entry is the highest quality LOD0 followed by more optimized versions.
|
||||
/// </summary>
|
||||
API_FIELD(ReadOnly) Array<SkinnedModelLOD, FixedAllocation<MODEL_MAX_LODS>> LODs;
|
||||
|
||||
/// <summary>
|
||||
/// The skeleton bones hierarchy.
|
||||
/// </summary>
|
||||
SkeletonData Skeleton;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="SkinnedModel"/> class.
|
||||
/// </summary>
|
||||
~SkinnedModel();
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is initialized.
|
||||
/// </summary>
|
||||
FORCE_INLINE bool IsInitialized() const
|
||||
{
|
||||
return LODs.HasItems();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets amount of the level of details in the model
|
||||
/// </summary>
|
||||
/// <returns>Amount of the level of details in the model</returns>
|
||||
FORCE_INLINE int32 GetLODsCount() const
|
||||
{
|
||||
return LODs.Count();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of loaded model LODs.
|
||||
/// </summary>
|
||||
/// <returns>Loaded LODs count</returns>
|
||||
API_PROPERTY() FORCE_INLINE int32 GetLoadedLODs() const
|
||||
{
|
||||
return _loadedLODs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified index is a valid LOD index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <returns>True if the specified index is a valid LOD index, otherwise false.</returns>
|
||||
FORCE_INLINE bool IsValidLODIndex(int32 index) const
|
||||
{
|
||||
return Math::IsInRange(index, 0, LODs.Count() - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the index of the LOD to be valid for rendering (only loaded LODs).
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <returns>The resident LOD index.</returns>
|
||||
FORCE_INLINE int32 ClampLODIndex(int32 index) const
|
||||
{
|
||||
return Math::Clamp(index, HighestResidentLODIndex(), LODs.Count() - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets index of the highest resident LOD (may be equal to LODs.Count if no LOD has been uploaded). Note: LOD=0 is the highest (top quality)
|
||||
/// </summary>
|
||||
/// <returns>LOD index</returns>
|
||||
FORCE_INLINE int32 HighestResidentLODIndex() const
|
||||
{
|
||||
return GetLODsCount() - _loadedLODs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether any LOD has been initialized.
|
||||
/// </summary>
|
||||
/// <returns>True if any LOD has been initialized, otherwise false.</returns>
|
||||
FORCE_INLINE bool HasAnyLODInitialized() const
|
||||
{
|
||||
return LODs.HasItems() && LODs.Last().HasAnyMeshInitialized();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this model can be rendered.
|
||||
/// </summary>
|
||||
/// <returns>True if can render that model, otherwise false.</returns>
|
||||
FORCE_INLINE bool CanBeRendered() const
|
||||
{
|
||||
return _loadedLODs > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the skeleton nodes hierarchy.
|
||||
/// </summary>
|
||||
API_PROPERTY() const Array<SkeletonNode>& GetNodes() const
|
||||
{
|
||||
return Skeleton.Nodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the skeleton bones hierarchy.
|
||||
/// </summary>
|
||||
API_PROPERTY() const Array<SkeletonBone>& GetBones() const
|
||||
{
|
||||
return Skeleton.Bones;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the node with the given name.
|
||||
/// </summary>
|
||||
/// <param name="name">Thr name of the node.</param>
|
||||
/// <returns>The index of the node or -1 if not found.</returns>
|
||||
API_FUNCTION() FORCE_INLINE int32 FindNode(const StringView& name)
|
||||
{
|
||||
return Skeleton.FindNode(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the bone with the given name.
|
||||
/// </summary>
|
||||
/// <param name="name">Thr name of the node used by the bone.</param>
|
||||
/// <returns>The index of the bone or -1 if not found.</returns>
|
||||
API_FUNCTION() FORCE_INLINE int32 FindBone(const StringView& name)
|
||||
{
|
||||
return FindBone(FindNode(name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the bone that is using a given node index.
|
||||
/// </summary>
|
||||
/// <param name="nodeIndex">The index of the node.</param>
|
||||
/// <returns>The index of the bone or -1 if not found.</returns>
|
||||
API_FUNCTION() FORCE_INLINE int32 FindBone(int32 nodeIndex)
|
||||
{
|
||||
return Skeleton.FindBone(nodeIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the blend shapes names used by the skinned model meshes (from LOD 0 only).
|
||||
/// </summary>
|
||||
API_PROPERTY() Array<String> GetBlendShapes();
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Requests the LOD data asynchronously (creates task that will gather chunk data or null if already here).
|
||||
/// </summary>
|
||||
/// <param name="lodIndex">Index of the LOD.</param>
|
||||
/// <returns>Task that will gather chunk data or null if already here.</returns>
|
||||
ContentLoadTask* RequestLODDataAsync(int32 lodIndex)
|
||||
{
|
||||
const int32 chunkIndex = SKINNED_MODEL_LOD_TO_CHUNK_INDEX(lodIndex);
|
||||
return RequestChunkDataAsync(chunkIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model LOD data (links bytes).
|
||||
/// </summary>
|
||||
/// <param name="lodIndex">Index of the LOD.</param>
|
||||
/// <param name="data">The data (may be missing if failed to get it).</param>
|
||||
void GetLODData(int32 lodIndex, BytesContainer& data) const
|
||||
{
|
||||
const int32 chunkIndex = SKINNED_MODEL_LOD_TO_CHUNK_INDEX(lodIndex);
|
||||
GetChunkData(chunkIndex, data);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Determines if there is an intersection between the SkinnedModel and a Ray in given world using given instance.
|
||||
/// </summary>
|
||||
/// <param name="ray">The ray to test</param>
|
||||
/// <param name="world">World to test</param>
|
||||
/// <param name="distance">When the method completes, contains the distance of the intersection (if any valid).</param>
|
||||
/// <param name="normal">When the method completes, contains the intersection surface normal vector (if any valid).</param>
|
||||
/// <param name="mesh">Mesh, or null</param>
|
||||
/// <param name="lodIndex">Level Of Detail index</param>
|
||||
/// <returns>True whether the two objects intersected</returns>
|
||||
bool Intersects(const Ray& ray, const Matrix& world, float& distance, Vector3& normal, SkinnedMesh** mesh, int32 lodIndex = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model bounding box in custom matrix world space (rig pose transformed by matrix, not animated).
|
||||
/// </summary>
|
||||
/// <param name="world">The transformation matrix.</param>
|
||||
/// <param name="lodIndex">The Level Of Detail index.</param>
|
||||
/// <returns>The bounding box.</returns>
|
||||
API_FUNCTION() BoundingBox GetBox(const Matrix& world, int32 lodIndex = 0) const;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model bounding box in local space (rig pose, not animated).
|
||||
/// </summary>
|
||||
/// <param name="lodIndex">The Level Of Detail index.</param>
|
||||
/// <returns>The bounding box.</returns>
|
||||
API_FUNCTION() BoundingBox GetBox(int32 lodIndex = 0) const;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Draws the meshes. Binds vertex and index buffers and invokes the draw calls.
|
||||
/// </summary>
|
||||
/// <param name="context">The GPU context to draw with.</param>
|
||||
/// <param name="lodIndex">The Level Of Detail index.</param>
|
||||
void Render(GPUContext* context, int32 lodIndex = 0)
|
||||
{
|
||||
LODs[lodIndex].Render(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the model.
|
||||
/// </summary>
|
||||
/// <param name="renderContext">The rendering context.</param>
|
||||
/// <param name="info">The packed drawing info data.</param>
|
||||
void Draw(RenderContext& renderContext, const SkinnedMesh::DrawInfo& info);
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Setups the model LODs collection including meshes creation.
|
||||
/// </summary>
|
||||
/// <param name="meshesCountPerLod">The meshes count per lod array (amount of meshes per LOD).</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() bool SetupLODs(const Span<int32>& meshesCountPerLod);
|
||||
|
||||
/// <summary>
|
||||
/// Setups the skinned model skeleton. Uses the same nodes layout for skeleton bones and calculates the offset matrix by auto.
|
||||
/// </summary>
|
||||
/// <param name="nodes">The nodes hierarchy. The first node must be a root one (with parent index equal -1).</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() bool SetupSkeleton(const Array<SkeletonNode>& nodes);
|
||||
|
||||
/// <summary>
|
||||
/// Setups the skinned model skeleton.
|
||||
/// </summary>
|
||||
/// <param name="nodes">The nodes hierarchy. The first node must be a root one (with parent index equal -1).</param>
|
||||
/// <param name="bones">The bones hierarchy.</param>
|
||||
/// <param name="autoCalculateOffsetMatrix">If true then the OffsetMatrix for each bone will be auto-calculated by the engine, otherwise the provided values will be used.</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
API_FUNCTION() bool SetupSkeleton(const Array<SkeletonNode>& nodes, const Array<SkeletonBone>& bones, bool autoCalculateOffsetMatrix);
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Saves this asset to the file. Supported only in Editor.
|
||||
/// </summary>
|
||||
/// <remarks>If you use saving with the GPU mesh data then the call has to be provided from the thread other than the main game thread.</remarks>
|
||||
/// <param name="withMeshDataFromGpu">True if save also GPU mesh buffers, otherwise will keep data in storage unmodified. Valid only if saving the same asset to the same location and it's loaded.</param>
|
||||
/// <param name="path">The custom asset path to use for the saving. Use empty value to save this asset to its own storage location. Can be used to duplicate asset. Must be specified when saving virtual asset.</param>
|
||||
/// <returns>True if cannot save data, otherwise false.</returns>
|
||||
API_FUNCTION() bool Save(bool withMeshDataFromGpu = false, const StringView& path = StringView::Empty);
|
||||
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this skinned model to an empty collection of meshes. Ensure to init SkeletonData manually after the call.
|
||||
/// </summary>
|
||||
/// <param name="meshesCountPerLod">The meshes count per lod array (amount of meshes per LOD).</param>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
bool Init(const Span<int32>& meshesCountPerLod);
|
||||
|
||||
public:
|
||||
|
||||
// [ModelBase]
|
||||
void SetupMaterialSlots(int32 slotsCount) override;
|
||||
void InitAsVirtual() override;
|
||||
#if USE_EDITOR
|
||||
void GetReferences(Array<Guid>& output) const override
|
||||
{
|
||||
// Base
|
||||
BinaryAsset::GetReferences(output);
|
||||
|
||||
for (int32 i = 0; i < MaterialSlots.Count(); i++)
|
||||
{
|
||||
output.Add(MaterialSlots[i].Material.GetID());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// [StreamableResource]
|
||||
int32 GetMaxResidency() const override;
|
||||
int32 GetCurrentResidency() const override;
|
||||
int32 GetAllocatedResidency() const override;
|
||||
bool CanBeUpdated() const override;
|
||||
Task* UpdateAllocation(int32 residency) override;
|
||||
Task* CreateStreamingTask(int32 residency) override;
|
||||
|
||||
protected:
|
||||
|
||||
// [ModelBase]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
bool init(AssetInitData& initData) override;
|
||||
AssetChunksFlag getChunksToPreload() const override;
|
||||
};
|
||||
209
Source/Engine/Content/Assets/Texture.cpp
Normal file
209
Source/Engine/Content/Assets/Texture.cpp
Normal file
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "Texture.h"
|
||||
#include "Engine/Content/Content.h"
|
||||
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
||||
#include "Engine/Content/Upgraders/TextureAssetUpgrader.h"
|
||||
#include "Engine/Platform/FileSystem.h"
|
||||
#include "Engine/Graphics/RenderTools.h"
|
||||
#include "Engine/Graphics/Textures/TextureData.h"
|
||||
#include "Engine/Scripting/MainThreadManagedInvokeAction.h"
|
||||
#include "Engine/Tools/TextureTool/TextureTool.h"
|
||||
|
||||
REGISTER_BINARY_ASSET(Texture, "FlaxEngine.Texture", ::New<TextureAssetUpgrader>(), true);
|
||||
|
||||
Texture::Texture(const SpawnParams& params, const AssetInfo* info)
|
||||
: TextureBase(params, info)
|
||||
{
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
bool Texture::Save(const StringView& path, const InitData* customData)
|
||||
{
|
||||
// Validate state
|
||||
if (WaitForLoaded())
|
||||
{
|
||||
LOG(Error, "Asset loading failed. Cannot save it.");
|
||||
return true;
|
||||
}
|
||||
if (IsVirtual() && path.IsEmpty())
|
||||
{
|
||||
LOG(Error, "To save virtual asset asset you need to specify the target asset path location.");
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
AssetInitData data;
|
||||
const auto texture = StreamingTexture();
|
||||
|
||||
// Use a temporary chunks for data storage for virtual assets
|
||||
FlaxChunk* tmpChunks[ASSET_FILE_DATA_CHUNKS];
|
||||
Platform::MemoryClear(tmpChunks, sizeof(tmpChunks));
|
||||
Array<FlaxChunk> chunks;
|
||||
if (IsVirtual())
|
||||
chunks.Resize(ASSET_FILE_DATA_CHUNKS);
|
||||
#define GET_CHUNK(index) (IsVirtual() ? tmpChunks[index] = &chunks[index] : GetOrCreateChunk(index))
|
||||
|
||||
// Save header
|
||||
data.CustomData.Copy(texture->GetHeader());
|
||||
|
||||
// Validate custom data
|
||||
if (customData == nullptr)
|
||||
customData = _customData;
|
||||
if (IsVirtual())
|
||||
{
|
||||
// Virtual asset must have a custom data provided
|
||||
if (!customData)
|
||||
{
|
||||
LOG(Error, "To save virtual texture you need to initialize it first with a valid data.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (customData)
|
||||
{
|
||||
// Normal asset if has custom data passed it must match the texture info
|
||||
if (customData->Mips.Count() != texture->TotalMipLevels() ||
|
||||
customData->ArraySize != texture->TotalArraySize() ||
|
||||
customData->Format != Format() ||
|
||||
customData->Width != Width() ||
|
||||
customData->Height != Height())
|
||||
{
|
||||
LOG(Error, "Invalid custom texture data to save.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Get texture data to chunks
|
||||
if (customData)
|
||||
{
|
||||
for (int32 mipIndex = 0; mipIndex < texture->TotalMipLevels(); mipIndex++)
|
||||
{
|
||||
auto chunk = GET_CHUNK(mipIndex);
|
||||
|
||||
uint32 rowPitch, slicePitch;
|
||||
const int32 mipWidth = Math::Max(1, Width() >> mipIndex);
|
||||
const int32 mipHeight = Math::Max(1, Height() >> mipIndex);
|
||||
RenderTools::ComputePitch(Format(), mipWidth, mipHeight, rowPitch, slicePitch);
|
||||
|
||||
chunk->Data.Allocate(slicePitch * texture->TotalArraySize());
|
||||
|
||||
auto& mipData = customData->Mips[mipIndex];
|
||||
if (mipData.Data.Length() != (int32)mipData.SlicePitch * texture->TotalArraySize())
|
||||
{
|
||||
LOG(Error, "Invalid custom texture data (slice pitch * array size is different than data bytes count).");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Faster path if source and destination data layout matches
|
||||
if (rowPitch == mipData.RowPitch && slicePitch == mipData.SlicePitch)
|
||||
{
|
||||
const auto src = mipData.Data.Get();
|
||||
const auto dst = chunk->Data.Get();
|
||||
|
||||
Platform::MemoryCopy(dst, src, slicePitch * texture->TotalArraySize());
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto copyRowSize = Math::Min(mipData.RowPitch, rowPitch);
|
||||
|
||||
for (int32 slice = 0; slice < texture->TotalArraySize(); slice++)
|
||||
{
|
||||
auto src = mipData.Data.Get() + slice * mipData.SlicePitch;
|
||||
auto dst = chunk->Data.Get() + slice * slicePitch;
|
||||
|
||||
for (int32 line = 0; line < texture->TotalHeight(); line++)
|
||||
{
|
||||
Platform::MemoryCopy(dst, src, copyRowSize);
|
||||
|
||||
src += mipData.RowPitch;
|
||||
dst += rowPitch;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Load all chunks
|
||||
if (LoadChunks(ALL_ASSET_CHUNKS))
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef GET_CHUNK
|
||||
|
||||
// Save
|
||||
data.SerializedVersion = SerializedVersion;
|
||||
if (IsVirtual())
|
||||
Platform::MemoryCopy(_header.Chunks, tmpChunks, sizeof(_header.Chunks));
|
||||
const bool saveResult = path.HasChars() ? SaveAsset(path, data) : SaveAsset(data, true);
|
||||
if (IsVirtual())
|
||||
Platform::MemoryClear(_header.Chunks, sizeof(_header.Chunks));
|
||||
if (saveResult)
|
||||
{
|
||||
LOG(Error, "Cannot save \'{0}\'", ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool Texture::LoadFile(const StringView& path, bool generateMips)
|
||||
{
|
||||
if (!IsVirtual())
|
||||
{
|
||||
LOG(Error, "Loading image from file is supported only for virtual textures.");
|
||||
return true;
|
||||
}
|
||||
|
||||
TextureData textureData;
|
||||
if (TextureTool::ImportTexture(path, textureData))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
auto initData = New<InitData>();
|
||||
|
||||
initData->Format = textureData.Format;
|
||||
initData->Width = textureData.Width;
|
||||
initData->Height = textureData.Height;
|
||||
initData->ArraySize = 1;
|
||||
if (generateMips)
|
||||
initData->Mips.Resize(MipLevelsCount(textureData.Width, textureData.Height));
|
||||
else
|
||||
initData->Mips.Resize(textureData.GetMipLevels());
|
||||
|
||||
for (int32 mipIndex = 0; mipIndex < textureData.GetMipLevels(); mipIndex++)
|
||||
{
|
||||
auto& mip = initData->Mips[mipIndex];
|
||||
auto& data = *textureData.GetData(0, mipIndex);
|
||||
|
||||
mip.Data.Copy(data.Data);
|
||||
mip.RowPitch = data.RowPitch;
|
||||
mip.SlicePitch = data.DepthPitch;
|
||||
}
|
||||
|
||||
if (generateMips)
|
||||
{
|
||||
for (int32 mipIndex = textureData.GetMipLevels(); mipIndex < initData->Mips.Count(); mipIndex++)
|
||||
{
|
||||
initData->GenerateMip(mipIndex, true);
|
||||
}
|
||||
}
|
||||
|
||||
return Init(initData);
|
||||
}
|
||||
|
||||
Texture* Texture::FromFile(const StringView& path, bool generateMips)
|
||||
{
|
||||
auto texture = Content::CreateVirtualAsset<Texture>();
|
||||
if (texture->LoadFile(path, generateMips))
|
||||
{
|
||||
texture->DeleteObject();
|
||||
texture = nullptr;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
71
Source/Engine/Content/Assets/Texture.h
Normal file
71
Source/Engine/Content/Assets/Texture.h
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Graphics/Textures/TextureBase.h"
|
||||
|
||||
/// <summary>
|
||||
/// Texture asset contains an image that is usually stored on a GPU and is used during rendering graphics.
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API Texture : public TextureBase
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(Texture, TexturesSerializedVersion);
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets the texture format type.
|
||||
/// </summary>
|
||||
FORCE_INLINE TextureFormatType GetFormatType() const
|
||||
{
|
||||
return _texture.GetFormatType();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if texture is a normal map.
|
||||
/// </summary>
|
||||
API_PROPERTY() FORCE_INLINE bool IsNormalMap() const
|
||||
{
|
||||
return GetFormatType() == TextureFormatType::NormalMap;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Saves this asset to the file. Supported only in Editor.
|
||||
/// </summary>
|
||||
/// <param name="path">The custom asset path to use for the saving. Use empty value to save this asset to its own storage location. Can be used to duplicate asset. Must be specified when saving virtual asset.</param>
|
||||
/// <returns>True if cannot save data, otherwise false.</returns>
|
||||
API_FUNCTION() bool Save(const StringView& path = StringView::Empty)
|
||||
{
|
||||
return Save(path, nullptr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves this asset to the file. Supported only in Editor.
|
||||
/// </summary>
|
||||
/// <param name="path">The custom asset path to use for the saving. Use empty value to save this asset to its own storage location. Can be used to duplicate asset. Must be specified when saving virtual asset.</param>
|
||||
/// <param name="customData">The custom texture data container. Can be used to override the data stored in the asset. Use null to ignore this argument.</param>
|
||||
/// <returns>True if cannot save data, otherwise false.</returns>
|
||||
bool Save(const StringView& path, const InitData* customData);
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Loads the texture from the image file. Supported file formats depend on a runtime platform. All platform support loading PNG, BMP, TGA, HDR and JPEG files.
|
||||
/// </summary>
|
||||
/// <remarks>Valid only for virtual assets.</remarks>
|
||||
/// <param name="path">The source image file path.</param>
|
||||
/// <param name="generateMips">True if generate mipmaps for the imported texture.</param>
|
||||
/// <returns>True if fails, otherwise false.</returns>
|
||||
API_FUNCTION() bool LoadFile(const StringView& path, bool generateMips = false);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the texture from the image file and creates the virtual texture asset for it. Supported file formats depend on a runtime platform. All platform support loading PNG, BMP, TGA, HDR and JPEG files.
|
||||
/// </summary>
|
||||
/// <param name="path">The source image file path.</param>
|
||||
/// <param name="generateMips">True if generate mipmaps for the imported texture.</param>
|
||||
/// <returns>The loaded texture (virtual asset) or null if fails.</returns>
|
||||
API_FUNCTION() static Texture* FromFile(const StringView& path, bool generateMips = false);
|
||||
};
|
||||
2136
Source/Engine/Content/Assets/VisualScript.cpp
Normal file
2136
Source/Engine/Content/Assets/VisualScript.cpp
Normal file
File diff suppressed because it is too large
Load Diff
407
Source/Engine/Content/Assets/VisualScript.h
Normal file
407
Source/Engine/Content/Assets/VisualScript.h
Normal file
@@ -0,0 +1,407 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../BinaryAsset.h"
|
||||
#include "Engine/Scripting/BinaryModule.h"
|
||||
#include "Engine/Visject/VisjectGraph.h"
|
||||
|
||||
#define VISUAL_SCRIPT_GRAPH_MAX_CALL_STACK 250
|
||||
#define VISUAL_SCRIPT_DEBUGGING USE_EDITOR
|
||||
|
||||
#define VisualScriptGraphNode VisjectGraphNode<>
|
||||
|
||||
class VisualScripting;
|
||||
class VisualScriptingBinaryModule;
|
||||
|
||||
/// <summary>
|
||||
/// The Visual Script graph data.
|
||||
/// </summary>
|
||||
class VisualScriptGraph : public VisjectGraph<VisualScriptGraphNode, VisjectGraphBox, VisjectGraphParameter>
|
||||
{
|
||||
public:
|
||||
bool onNodeLoaded(Node* n) override;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The Visual Script graph executor runtime.
|
||||
/// </summary>
|
||||
class VisualScriptExecutor : public VisjectExecutor
|
||||
{
|
||||
friend VisualScripting;
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VisualScriptExecutor"/> class.
|
||||
/// </summary>
|
||||
VisualScriptExecutor();
|
||||
|
||||
private:
|
||||
Value eatBox(Node* caller, Box* box) override;
|
||||
Graph* GetCurrentGraph() const override;
|
||||
|
||||
void ProcessGroupConstants(Box* box, Node* node, Value& value);
|
||||
void ProcessGroupPacking(Box* box, Node* node, Value& value);
|
||||
void ProcessGroupParameters(Box* box, Node* node, Value& value);
|
||||
void ProcessGroupTools(Box* box, Node* node, Value& value);
|
||||
void ProcessGroupFunction(Box* boxBase, Node* node, Value& value);
|
||||
void ProcessGroupFlow(Box* boxBase, Node* node, Value& value);
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The Visual Script asset. Contains a graph with functions and parameters for visual scripting.
|
||||
/// </summary>
|
||||
/// <seealso cref="BinaryAsset" />
|
||||
API_CLASS(NoSpawn, Sealed) class FLAXENGINE_API VisualScript : public BinaryAsset
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(VisualScript, 1);
|
||||
friend VisualScripting;
|
||||
friend VisualScriptExecutor;
|
||||
friend VisualScriptingBinaryModule;
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Visual Script flag types.
|
||||
/// </summary>
|
||||
API_ENUM(Attributes="Flags") enum class Flags
|
||||
{
|
||||
/// <summary>
|
||||
/// No flags.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Script is abstract and cannot be instantiated directly.
|
||||
/// </summary>
|
||||
Abstract = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Script is sealed and cannot be inherited by other scripts.
|
||||
/// </summary>
|
||||
Sealed = 2,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Visual Script metadata container.
|
||||
/// </summary>
|
||||
API_STRUCT() struct Metadata
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(Metadata);
|
||||
|
||||
/// <summary>
|
||||
/// The base class typename.
|
||||
/// </summary>
|
||||
API_FIELD() String BaseTypename;
|
||||
|
||||
/// <summary>
|
||||
/// The script flags.
|
||||
/// </summary>
|
||||
API_FIELD() Flags Flags;
|
||||
};
|
||||
|
||||
enum class MethodFlags
|
||||
{
|
||||
None = 0,
|
||||
Static = 1,
|
||||
Virtual = 2,
|
||||
Override = 4,
|
||||
};
|
||||
|
||||
struct Method
|
||||
{
|
||||
VisualScript* Script;
|
||||
VisualScriptGraphNode* Node;
|
||||
StringAnsi Name;
|
||||
MethodFlags MethodFlags;
|
||||
ScriptingTypeMethodSignature Signature;
|
||||
Array<StringAnsi, InlinedAllocation<16>> ParamNames;
|
||||
};
|
||||
|
||||
struct Field
|
||||
{
|
||||
VisualScript* Script;
|
||||
VisjectGraphParameter* Parameter;
|
||||
int32 Index;
|
||||
StringAnsi Name;
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
Dictionary<Guid, Array<Variant>> _instances;
|
||||
ScriptingTypeHandle _scriptingTypeHandle;
|
||||
ScriptingTypeHandle _scriptingTypeHandleCached;
|
||||
StringAnsiView _typename;
|
||||
char _typenameChars[33];
|
||||
Array<Method, InlinedAllocation<32>> _methods;
|
||||
Array<Field, InlinedAllocation<32>> _fields;
|
||||
#if USE_EDITOR
|
||||
Array<Guid> _oldParamsLayout;
|
||||
#endif
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The Visual Script graph.
|
||||
/// </summary>
|
||||
VisualScriptGraph Graph;
|
||||
|
||||
/// <summary>
|
||||
/// The script metadata.
|
||||
/// </summary>
|
||||
API_FIELD(ReadOnly) Metadata Meta;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets the typename of the Visual Script. Identifies it's scripting type.
|
||||
/// </summary>
|
||||
API_PROPERTY() FORCE_INLINE const StringAnsiView& GetScriptTypeName() const
|
||||
{
|
||||
return _typename;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of Visual Script parameters declared in this graph (excluding base types).
|
||||
/// </summary>
|
||||
API_PROPERTY() const Array<VisjectGraphParameter>& GetParameters() const
|
||||
{
|
||||
return Graph.Parameters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scripting type handle of this Visual Script.
|
||||
/// </summary>
|
||||
ScriptingTypeHandle GetScriptingType();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the Visual Script object.
|
||||
/// </summary>
|
||||
/// <returns>The created instance or null if failed.</returns>
|
||||
API_FUNCTION() ScriptingObject* CreateInstance();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the Visual Script parameter of the given instance.
|
||||
/// </summary>
|
||||
/// <param name="name">The parameter name.</param>
|
||||
/// <param name="instance">The object instance.</param>
|
||||
/// <returns>The property value.</returns>
|
||||
API_FUNCTION() Variant GetScriptInstanceParameterValue(const StringView& name, ScriptingObject* instance) const;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of the Visual Script parameter of the given instance.
|
||||
/// </summary>
|
||||
/// <param name="name">The parameter name.</param>
|
||||
/// <param name="instance">The object instance.</param>
|
||||
/// <param name="value">The property value to set.</param>
|
||||
API_FUNCTION() void SetScriptInstanceParameterValue(const StringView& name, ScriptingObject* instance, const Variant& value) const;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of the Visual Script parameter of the given instance.
|
||||
/// </summary>
|
||||
/// <param name="name">The parameter name.</param>
|
||||
/// <param name="instance">The object instance.</param>
|
||||
/// <param name="value">The property value to set.</param>
|
||||
void SetScriptInstanceParameterValue(const StringView& name, ScriptingObject* instance, Variant&& value) const;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find the method matching the given properties. Doesn't check base classes but just this script.
|
||||
/// </summary>
|
||||
/// <param name="name">The method name.</param>
|
||||
/// <param name="numParams">The method parameters count.</param>
|
||||
/// <returns>The method graph node entry for callback, null if not found.</returns>
|
||||
const Method* FindMethod(const StringAnsiView& name, int32 numParams = 0) const;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find the field matching the given name. Doesn't check base classes but just this script.
|
||||
/// </summary>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <returns>The field entry for access, null if not found.</returns>
|
||||
const Field* FindField(const StringAnsiView& name) const;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load surface graph from the asset.
|
||||
/// </summary>
|
||||
/// <returns>The surface data or empty if failed to load it.</returns>
|
||||
API_FUNCTION() BytesContainer LoadSurface();
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Updates the graph surface (save new one, discard cached data, reload asset).
|
||||
/// </summary>
|
||||
/// <param name="data">Stream with graph data.</param>
|
||||
/// <param name="meta">Script metadata.</param>
|
||||
/// <returns>True if cannot save it, otherwise false.</returns>
|
||||
API_FUNCTION() bool SaveSurface(BytesContainer& data, API_PARAM(Ref) const Metadata& meta);
|
||||
|
||||
// Returns the amount of methods in the script.
|
||||
API_FUNCTION() int32 GetMethodsCount() const
|
||||
{
|
||||
return _methods.Count();
|
||||
}
|
||||
|
||||
// Gets the signature data of the method.
|
||||
API_FUNCTION() void GetMethodSignature(int32 index, API_PARAM(Out) String& name, API_PARAM(Out) byte& flags, API_PARAM(Out) String& returnTypeName, API_PARAM(Out) Array<String>& paramNames, API_PARAM(Out) Array<String>& paramTypeNames, API_PARAM(Out) Array<bool>& paramOuts);
|
||||
|
||||
// Gets the metadata of the script surface.
|
||||
API_FUNCTION() Span<byte> GetMetaData(int32 typeID);
|
||||
|
||||
// Gets the metadata of the method.
|
||||
API_FUNCTION() Span<byte> GetMethodMetaData(int32 index, int32 typeID);
|
||||
|
||||
#endif
|
||||
|
||||
public:
|
||||
|
||||
// [BinaryAsset]
|
||||
#if USE_EDITOR
|
||||
void GetReferences(Array<Guid>& output) const override
|
||||
{
|
||||
// Base
|
||||
BinaryAsset::GetReferences(output);
|
||||
|
||||
Graph.GetReferences(output);
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
|
||||
// [BinaryAsset]
|
||||
LoadResult load() override;
|
||||
void unload(bool isReloading) override;
|
||||
AssetChunksFlag getChunksToPreload() const override;
|
||||
|
||||
private:
|
||||
|
||||
void CacheScriptingType();
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The visual scripts module for engine scripting integration.
|
||||
/// </summary>
|
||||
/// <seealso cref="BinaryModule" />
|
||||
class FLAXENGINE_API VisualScriptingBinaryModule : public BinaryModule
|
||||
{
|
||||
friend VisualScript;
|
||||
private:
|
||||
|
||||
StringAnsi _name;
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VisualScriptingBinaryModule"/> class.
|
||||
/// </summary>
|
||||
VisualScriptingBinaryModule();
|
||||
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// The visual script assets loaded into the module with exposes scripting types. Order matches the Types array.
|
||||
/// </summary>
|
||||
Array<AssetReference<VisualScript>> Scripts;
|
||||
|
||||
private:
|
||||
|
||||
static ScriptingObject* VisualScriptObjectSpawn(const ScriptingObjectSpawnParams& params);
|
||||
#if USE_EDITOR
|
||||
void OnScriptsReloading();
|
||||
#endif
|
||||
|
||||
public:
|
||||
|
||||
// [BinaryModule]
|
||||
const StringAnsi& GetName() const override;
|
||||
bool IsLoaded() const override;
|
||||
bool FindScriptingType(const StringAnsiView& typeName, int32& typeIndex) override;
|
||||
void* FindMethod(const ScriptingTypeHandle& typeHandle, const StringAnsiView& name, int32 numParams = 0) override;
|
||||
bool InvokeMethod(void* method, const Variant& instance, Span<Variant> paramValues, Variant& result) override;
|
||||
void GetMethodSignature(void* method, ScriptingTypeMethodSignature& methodSignature) override;
|
||||
void* FindField(const ScriptingTypeHandle& typeHandle, const StringAnsiView& name) override;
|
||||
void GetFieldSignature(void* field, ScriptingTypeFieldSignature& fieldSignature) override;
|
||||
bool GetFieldValue(void* field, const Variant& instance, Variant& result) override;
|
||||
bool SetFieldValue(void* field, const Variant& instance, Variant& value) override;
|
||||
void SerializeObject(JsonWriter& stream, ScriptingObject* object, const ScriptingObject* otherObj) override;
|
||||
void DeserializeObject(ISerializable::DeserializeStream& stream, ScriptingObject* object, ISerializeModifier* modifier) override;
|
||||
void OnObjectIdChanged(ScriptingObject* object, const Guid& oldId) override;
|
||||
void OnObjectDeleted(ScriptingObject* object) override;
|
||||
void Destroy(bool isReloading) override;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The visual scripting runtime service. Allows to execute visual script functions with recursion support and thread-safety.
|
||||
/// </summary>
|
||||
class FLAXENGINE_API VisualScripting
|
||||
{
|
||||
public:
|
||||
|
||||
struct NodeBoxValue
|
||||
{
|
||||
uint32 NodeId;
|
||||
uint32 BoxId;
|
||||
Variant Value;
|
||||
};
|
||||
|
||||
struct ScopeContext
|
||||
{
|
||||
// Method input parameters
|
||||
Span<Variant> Parameters;
|
||||
// Invoke methods returned values cached within the scope (includes output parameters values)
|
||||
Array<NodeBoxValue, InlinedAllocation<16>> ReturnedValues;
|
||||
// Function result to return
|
||||
Variant FunctionReturn;
|
||||
};
|
||||
|
||||
struct StackFrame
|
||||
{
|
||||
VisualScript* Script;
|
||||
VisualScriptGraphNode* Node;
|
||||
VisualScriptGraphNode::Box* Box;
|
||||
ScriptingObject* Instance;
|
||||
StackFrame* PreviousFrame;
|
||||
ScopeContext* Scope;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the top frame of the current thread Visual Script execution stack frame.
|
||||
/// </summary>
|
||||
static StackFrame* GetThreadStackTop();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current stack trace of the current thread Visual Script execution.
|
||||
/// </summary>
|
||||
/// <returns>The the stack trace string.</returns>
|
||||
static String GetStackTrace();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the binary module for the Visual Scripting.
|
||||
/// </summary>
|
||||
static VisualScriptingBinaryModule* GetBinaryModule();
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the specified Visual Script method.
|
||||
/// </summary>
|
||||
/// <param name="method">The method to call.</param>
|
||||
/// <param name="instance">The object instance. Null for static methods.</param>
|
||||
/// <param name="parameters">The input parameters array. Unused if method is parameter-less.</param>
|
||||
/// <returns>The returned value. Undefined if method is void.</returns>
|
||||
static Variant Invoke(VisualScript::Method* method, ScriptingObject* instance, Span<Variant> parameters = Span<Variant>());
|
||||
|
||||
#if VISUAL_SCRIPT_DEBUGGING
|
||||
|
||||
// Custom event that is called every time the Visual Script signal flows over the graph (including the data connections). Can be used to read nad visualize the Visual Script execution logic.
|
||||
static Action DebugFlow;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to evaluate a given script box value for the debugger (called usually during the breakpoint on the hanging thread).
|
||||
/// </summary>
|
||||
/// <param name="script">The script.</param>
|
||||
/// <param name="instance">The current object instance.</param>
|
||||
/// <param name="nodeId">The ID of the node in the script graph to evaluate it's box.</param>
|
||||
/// <param name="boxId">The ID of the box in the node to evaluate it's value.</param>
|
||||
/// <param name="result">The output value. Valid only if method returned true.</param>
|
||||
/// <returns>True if could fetch teh value, otherwise false.</returns>
|
||||
static bool Evaluate(VisualScript* script, ScriptingObject* instance, uint32 nodeId, uint32 boxId, Variant& result);
|
||||
|
||||
#endif
|
||||
};
|
||||
Reference in New Issue
Block a user