You're breathtaking!

This commit is contained in:
Wojtek Figat
2020-12-07 23:40:54 +01:00
commit 6fb9eee74c
5143 changed files with 1153594 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Audio/Types.h"
#include "Engine/Core/Collections/Array.h"
class ReadStream;
/// <summary>
/// Interface used for implementations that parse audio formats into a set of PCM samples.
/// </summary>
class AudioDecoder
{
public:
/// <summary>
/// Finalizes an instance of the <see cref="AudioDecoder"/> class.
/// </summary>
virtual ~AudioDecoder()
{
}
public:
/// <summary>
/// Tries to open the specified stream with audio data and loads the whole audio data.
/// </summary>
/// <param name="stream">The data stream audio data is stored in. Must be valid until decoder usage end. Decoder may cache this pointer for the later usage.</param>
/// <param name="info">The output information describing meta-data of the audio in the stream.</param>
/// <param name="result">The output data.</param>
/// <param name="offset">The offset.</param>
/// <returns>True if the data is invalid or conversion failed, otherwise false.</returns>
virtual bool Convert(ReadStream* stream, AudioDataInfo& info, Array<byte>& result, uint32 offset = 0)
{
if (!IsValid(stream, offset))
return true;
if (!Open(stream, info, offset))
return true;
// Load the whole audio data
const int32 bytesPerSample = info.BitDepth / 8;
const int32 bufferSize = info.NumSamples * bytesPerSample;
result.Resize(bufferSize);
Read(result.Get(), info.NumSamples);
return false;
}
public:
/// <summary>
/// Tries to open the specified stream with audio data. Must be called before any reads or seeks.
/// </summary>
/// <param name="stream">The data stream audio data is stored in. Must be valid until decoder usage end. Decoder may cache this pointer for the later usage.</param>
/// <param name="info">The output information describing meta-data of the audio in the stream.</param>
/// <param name="offset">The offset.</param>
/// <returns>True if the data is invalid, otherwise false.</returns>
virtual bool Open(ReadStream* stream, AudioDataInfo& info, uint32 offset = 0) = 0;
/// <summary>
/// Moves the read pointer to the specified offset. Any further Read() calls will read from this location. User must ensure not to seek past the end of the data.
/// </summary>
/// <param name="offset">The offset to move the pointer in. In number of samples.</param>
virtual void Seek(uint32 offset) = 0;
/// <summary>
/// Reads a set of samples from the audio data.
/// </summary>
/// <remarks>
/// All values are returned as signed values.
/// </remarks>
/// <param name="samples">Pre-allocated buffer to store the samples in.</param>
/// <param name="numSamples">The number of samples to read.</param>
virtual void Read(byte* samples, uint32 numSamples) = 0;
/// <summary>
/// Checks if the data in the provided stream valid audio data for the current format. You should check this before calling Open().
/// </summary>
/// <param name="stream">The stream to check.</param>
/// <param name="offset">The offset at which audio data in the stream begins, in bytes.</param>
/// <returns>True if the data is valid, otherwise false.</returns>
virtual bool IsValid(ReadStream* stream, uint32 offset = 0) = 0;
};

View File

@@ -0,0 +1,33 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Audio/Types.h"
#include "Engine/Core/Types/DataContainer.h"
/// <summary>
/// Interface used for implementations that encodes set of PCM samples into a target audio format.
/// </summary>
class AudioEncoder
{
public:
/// <summary>
/// Finalizes an instance of the <see cref="AudioEncoder"/> class.
/// </summary>
virtual ~AudioEncoder()
{
}
public:
/// <summary>
/// Converts the input PCM samples buffer into the encoder audio format.
/// </summary>
/// <param name="samples">The buffer containing samples in PCM format. All samples should be in signed integer format.</param>
/// <param name="info">The input information describing meta-data of the audio in the samples buffer.</param>
/// <param name="result">The output data.</param>
/// <param name="quality">The output data compression quality (normalized in range [0;1]).</param>
/// <returns>True if the data is invalid or conversion failed, otherwise false.</returns>
virtual bool Convert(byte* samples, AudioDataInfo& info, BytesContainer& result, float quality = 0.5f) = 0;
};

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using Flax.Build;
using Flax.Build.NativeCpp;
/// <summary>
/// Audio data utilities module.
/// </summary>
public class AudioTool : EngineModule
{
/// <inheritdoc />
public override void Setup(BuildOptions options)
{
base.Setup(options);
// TODO: convert into private deps
options.PublicDependencies.Add("minimp3");
options.PublicDependencies.Add("ogg");
options.PublicDependencies.Add("vorbis");
options.PublicDefinitions.Add("COMPILE_WITH_AUDIO_TOOL");
options.PublicDefinitions.Add("COMPILE_WITH_OGG_VORBIS");
}
/// <inheritdoc />
public override void GetFilesToDeploy(List<string> files)
{
}
}

View File

@@ -0,0 +1,263 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "AudioTool.h"
#include "Engine/Core/Core.h"
#include "Engine/Core/Memory/Allocation.h"
void ConvertToMono8(const int8* input, uint8* output, uint32 numSamples, uint32 numChannels)
{
for (uint32 i = 0; i < numSamples; i++)
{
int16 sum = 0;
for (uint32 j = 0; j < numChannels; j++)
{
sum += *input;
++input;
}
*output = sum / numChannels;
++output;
}
}
void ConvertToMono16(const int16* input, int16* output, uint32 numSamples, uint32 numChannels)
{
for (uint32 i = 0; i < numSamples; i++)
{
int32 sum = 0;
for (uint32 j = 0; j < numChannels; j++)
{
sum += *input;
++input;
}
*output = sum / numChannels;
++output;
}
}
void Convert32To24Bits(const int32 input, uint8* output)
{
const uint32 valToEncode = *(uint32*)&input;
output[0] = (valToEncode >> 8) & 0x000000FF;
output[1] = (valToEncode >> 16) & 0x000000FF;
output[2] = (valToEncode >> 24) & 0x000000FF;
}
void ConvertToMono24(const uint8* input, uint8* output, uint32 numSamples, uint32 numChannels)
{
for (uint32 i = 0; i < numSamples; i++)
{
int64 sum = 0;
for (uint32 j = 0; j < numChannels; j++)
{
sum += AudioTool::Convert24To32Bits(input);
input += 3;
}
const int32 avg = (int32)(sum / numChannels);
Convert32To24Bits(avg, output);
output += 3;
}
}
void ConvertToMono32(const int32* input, int32* output, uint32 numSamples, uint32 numChannels)
{
for (uint32 i = 0; i < numSamples; i++)
{
int64 sum = 0;
for (uint32 j = 0; j < numChannels; j++)
{
sum += *input;
++input;
}
*output = (int32)(sum / numChannels);
++output;
}
}
void Convert8To32Bits(const int8* input, int32* output, uint32 numSamples)
{
for (uint32 i = 0; i < numSamples; i++)
{
const int8 val = input[i];
output[i] = val << 24;
}
}
void Convert16To32Bits(const int16* input, int32* output, uint32 numSamples)
{
for (uint32 i = 0; i < numSamples; i++)
output[i] = input[i] << 16;
}
void Convert24To32Bits(const uint8* input, int32* output, uint32 numSamples)
{
for (uint32 i = 0; i < numSamples; i++)
{
output[i] = AudioTool::Convert24To32Bits(input);
input += 3;
}
}
void Convert32To8Bits(const int32* input, uint8* output, uint32 numSamples)
{
for (uint32 i = 0; i < numSamples; i++)
output[i] = (int8)(input[i] >> 24);
}
void Convert32To16Bits(const int32* input, int16* output, uint32 numSamples)
{
for (uint32 i = 0; i < numSamples; i++)
output[i] = (int16)(input[i] >> 16);
}
void Convert32To24Bits(const int32* input, uint8* output, uint32 numSamples)
{
for (uint32 i = 0; i < numSamples; i++)
{
Convert32To24Bits(input[i], output);
output += 3;
}
}
void AudioTool::ConvertToMono(const byte* input, byte* output, uint32 bitDepth, uint32 numSamples, uint32 numChannels)
{
switch (bitDepth)
{
case 8:
ConvertToMono8((int8*)input, output, numSamples, numChannels);
break;
case 16:
ConvertToMono16((int16*)input, (int16*)output, numSamples, numChannels);
break;
case 24:
ConvertToMono24(input, output, numSamples, numChannels);
break;
case 32:
ConvertToMono32((int32*)input, (int32*)output, numSamples, numChannels);
break;
default:
CRASH;
break;
}
}
void AudioTool::ConvertBitDepth(const byte* input, uint32 inBitDepth, byte* output, uint32 outBitDepth, uint32 numSamples)
{
int32* srcBuffer = nullptr;
const bool needTempBuffer = inBitDepth != 32;
if (needTempBuffer)
srcBuffer = (int32*)Allocator::Allocate(numSamples * sizeof(int32));
else
srcBuffer = (int32*)input;
// Convert it to a temporary 32-bit buffer and then use that to convert to actual requested bit depth.
// It could be more efficient to convert directly from source to requested depth without a temporary buffer,
// at the cost of additional complexity. If this method ever becomes a performance issue consider that.
switch (inBitDepth)
{
case 8:
Convert8To32Bits((int8*)input, srcBuffer, numSamples);
break;
case 16:
Convert16To32Bits((int16*)input, srcBuffer, numSamples);
break;
case 24:
::Convert24To32Bits(input, srcBuffer, numSamples);
break;
case 32:
// Do nothing
break;
default:
CRASH;
break;
}
switch (outBitDepth)
{
case 8:
Convert32To8Bits(srcBuffer, output, numSamples);
break;
case 16:
Convert32To16Bits(srcBuffer, (int16*)output, numSamples);
break;
case 24:
Convert32To24Bits(srcBuffer, output, numSamples);
break;
case 32:
Platform::MemoryCopy(output, srcBuffer, numSamples * sizeof(int32));
break;
default:
CRASH;
break;
}
if (needTempBuffer)
{
Allocator::Free(srcBuffer);
srcBuffer = nullptr;
}
}
void AudioTool::ConvertToFloat(const byte* input, uint32 inBitDepth, float* output, uint32 numSamples)
{
if (inBitDepth == 8)
{
for (uint32 i = 0; i < numSamples; i++)
{
const int8 sample = *(int8*)input;
output[i] = sample / 127.0f;
input++;
}
}
else if (inBitDepth == 16)
{
for (uint32 i = 0; i < numSamples; i++)
{
const int16 sample = *(int16*)input;
output[i] = sample / 32767.0f;
input += 2;
}
}
else if (inBitDepth == 24)
{
for (uint32 i = 0; i < numSamples; i++)
{
const int32 sample = Convert24To32Bits(input);
output[i] = sample / 2147483647.0f;
input += 3;
}
}
else if (inBitDepth == 32)
{
for (uint32 i = 0; i < numSamples; i++)
{
const int32 sample = *(int32*)input;
output[i] = sample / 2147483647.0f;
input += 4;
}
}
else
{
CRASH;
}
}
void AudioTool::ConvertFromFloat(const float* input, int32* output, uint32 numSamples)
{
for (uint32 i = 0; i < numSamples; i++)
{
const float sample = *(float*)input;
output[i] = static_cast<int32>(sample * 2147483647.0f);
input++;
}
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Config.h"
#include "Engine/Core/Types/BaseTypes.h"
/// <summary>
/// Audio data importing and processing utilities.
/// </summary>
class FLAXENGINE_API AudioTool
{
public:
/// <summary>
/// Converts a set of audio samples using multiple channels into a set of mono samples.
/// </summary>
/// <param name="input">A set of input samples. Per-channels samples should be interleaved. Size of each sample is determined by bitDepth. Total size of the buffer should be (numSamples * numChannels * bitDepth / 8).</param>
/// <param name="output">The pre-allocated buffer to store the mono samples. Should be of (numSamples * bitDepth / 8) size.</param>
/// <param name="bitDepth">The size of a single sample in bits.</param>
/// <param name="numSamples">The number of samples per a single channel.</param>
/// <param name="numChannels">The number of channels in the input data.</param>
static void ConvertToMono(const byte* input, byte* output, uint32 bitDepth, uint32 numSamples, uint32 numChannels);
/// <summary>
/// Converts a set of audio samples of a certain bit depth to a new bit depth.
/// </summary>
/// <param name="input">A set of input samples. Total size of the buffer should be *numSamples * inBitDepth / 8).</param>
/// <param name="inBitDepth">The size of a single sample in the input array, in bits.</param>
/// <param name="output">The pre-allocated buffer to store the output samples in. Total size of the buffer should be (numSamples * outBitDepth / 8).</param>
/// <param name="outBitDepth">Size of a single sample in the output array, in bits.</param>
/// <param name="numSamples">The total number of samples to process.</param>
static void ConvertBitDepth(const byte* input, uint32 inBitDepth, byte* output, uint32 outBitDepth, uint32 numSamples);
/// <summary>
/// Converts a set of audio samples of a certain bit depth to a set of floating point samples in range [-1, 1].
/// </summary>
/// <param name="input">A set of input samples. Total size of the buffer should be (numSamples * inBitDepth / 8). All input samples should be signed integers.</param>
/// <param name="inBitDepth">The size of a single sample in the input array, in bits.</param>
/// <param name="output">The pre-allocated buffer to store the output samples in. Total size of the buffer should be numSamples * sizeof(float).</param>
/// <param name="numSamples">The total number of samples to process.</param>
static void ConvertToFloat(const byte* input, uint32 inBitDepth, float* output, uint32 numSamples);
/// <summary>
/// Converts a set of audio samples of floating point samples in range [-1, 1] to a 32-bit depth PCM data.
/// </summary>
/// <param name="input">A set of input samples. Total size of the buffer should be (numSamples * sizeof(float)). All input samples should be in range [-1, 1].</param>
/// <param name="output">The pre-allocated buffer to store the output samples in. Total size of the buffer should be numSamples * sizeof(float).</param>
/// <param name="numSamples">The total number of samples to process.</param>
static void ConvertFromFloat(const float* input, int32* output, uint32 numSamples);
/// <summary>
/// Converts a 24-bit signed integer into a 32-bit signed integer.
/// </summary>
/// <param name="input">The 24-bit signed integer as an array of 3 bytes.</param>
/// <returns>The 32-bit signed integer.</returns>
FORCE_INLINE static int32 Convert24To32Bits(const byte* input)
{
return (input[2] << 24) | (input[1] << 16) | (input[0] << 8);
}
};

View File

@@ -0,0 +1,88 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "MP3Decoder.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Serialization/MemoryWriteStream.h"
#define MINIMP3_IMPLEMENTATION
#include <minimp3/minimp3.h>
bool MP3Decoder::Convert(ReadStream* stream, AudioDataInfo& info, Array<byte>& result, uint32 offset)
{
ASSERT(stream);
mStream = stream;
mStream->SetPosition(offset);
int32 dataSize = mStream->GetLength() - offset;
Array<byte> dataBytes;
dataBytes.Resize(dataSize);
byte* data = dataBytes.Get();
mStream->ReadBytes(data, dataSize);
info.NumSamples = 0;
info.SampleRate = 0;
info.NumChannels = 0;
info.BitDepth = 16;
mp3dec_frame_info_t mp3Info;
short pcm[MINIMP3_MAX_SAMPLES_PER_FRAME];
MemoryWriteStream output(Math::RoundUpToPowerOf2(dataSize));
do
{
const int32 samples = mp3dec_decode_frame(&mp3d, data, dataSize, pcm, &mp3Info);
if (samples)
{
info.NumSamples += samples * mp3Info.channels;
output.WriteBytes(pcm, samples * 2 * mp3Info.channels);
if (!info.SampleRate)
info.SampleRate = mp3Info.hz;
if (!info.NumChannels)
info.NumChannels = mp3Info.channels;
if (info.SampleRate != mp3Info.hz || info.NumChannels != mp3Info.channels)
break;
}
data += mp3Info.frame_bytes;
dataSize -= mp3Info.frame_bytes;
} while (mp3Info.frame_bytes);
if (info.SampleRate == 0)
return true;
// Load the whole audio data
const int32 bytesPerSample = info.BitDepth / 8;
const int32 bufferSize = info.NumSamples * bytesPerSample;
result.Set(output.GetHandle(), bufferSize);
return false;
}
bool MP3Decoder::Open(ReadStream* stream, AudioDataInfo& info, uint32 offset)
{
CRASH;
// TODO: open MP3
return true;
}
void MP3Decoder::Seek(uint32 offset)
{
// TODO: seek MP3
CRASH;
}
void MP3Decoder::Read(byte* samples, uint32 numSamples)
{
// TODO: load MP3 format
CRASH;
}
bool MP3Decoder::IsValid(ReadStream* stream, uint32 offset)
{
// TODO: check MP3 format
CRASH;
return false;
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#if COMPILE_WITH_AUDIO_TOOL
#include "AudioDecoder.h"
#include "Engine/Serialization/ReadStream.h"
#include <minimp3/minimp3.h>
/// <summary>
/// Decodes .mp3 audio data into raw PCM format.
/// </summary>
/// <seealso cref="AudioDecoder" />
class MP3Decoder : public AudioDecoder
{
private:
ReadStream* mStream;
mp3dec_t mp3d;
public:
/// <summary>
/// Initializes a new instance of the <see cref="MP3Decoder"/> class.
/// </summary>
MP3Decoder()
{
mStream = nullptr;
mp3dec_init(&mp3d);
}
public:
// [AudioDecoder]
bool Convert(ReadStream* stream, AudioDataInfo& info, Array<byte>& result, uint32 offset = 0) override;
bool Open(ReadStream* stream, AudioDataInfo& info, uint32 offset = 0) override;
void Seek(uint32 offset) override;
void Read(byte* samples, uint32 numSamples) override;
bool IsValid(ReadStream* stream, uint32 offset = 0) override;
};
#endif

View File

@@ -0,0 +1,114 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_OGG_VORBIS
#include "OggVorbisDecoder.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include <vorbis/codec.h>
size_t oggRead(void* ptr, size_t size, size_t nmemb, void* data)
{
OggVorbisDecoder* decoderData = static_cast<OggVorbisDecoder*>(data);
const auto len = Math::Min<uint32>(static_cast<uint32>(size * nmemb), decoderData->Stream->GetLength() - decoderData->Stream->GetPosition());
decoderData->Stream->ReadBytes(ptr, len);
return static_cast<std::size_t>(len);
}
int oggSeek(void* data, ogg_int64_t offset, int whence)
{
OggVorbisDecoder* decoderData = static_cast<OggVorbisDecoder*>(data);
switch (whence)
{
case SEEK_SET:
offset += decoderData->Offset;
break;
case SEEK_CUR:
offset += decoderData->Stream->GetPosition();
break;
case SEEK_END:
offset = Math::Max<ogg_int64_t>(0, decoderData->Stream->GetLength() - 1);
break;
}
decoderData->Stream->SetPosition(static_cast<uint32>(offset));
return static_cast<int>(decoderData->Stream->GetPosition() - decoderData->Offset);
}
long oggTell(void* data)
{
OggVorbisDecoder* decoderData = static_cast<OggVorbisDecoder*>(data);
return static_cast<long>(decoderData->Stream->GetPosition() - decoderData->Offset);
}
bool OggVorbisDecoder::Open(ReadStream* stream, AudioDataInfo& info, uint32 offset)
{
if (stream == nullptr)
return false;
stream->SetPosition(offset);
Stream = stream;
Offset = offset;
const ov_callbacks callbacks = { &oggRead, &oggSeek, nullptr, &oggTell };
const int status = ov_open_callbacks(this, &OggVorbisFile, nullptr, 0, callbacks);
if (status < 0)
{
LOG(Warning, "Failed to open Ogg Vorbis file.");
return false;
}
vorbis_info* vorbisInfo = ov_info(&OggVorbisFile, -1);
info.NumChannels = vorbisInfo->channels;
info.SampleRate = vorbisInfo->rate;
info.NumSamples = static_cast<uint32>(ov_pcm_total(&OggVorbisFile, -1) * vorbisInfo->channels);
info.BitDepth = 16;
ChannelCount = info.NumChannels;
return true;
}
void OggVorbisDecoder::Seek(uint32 offset)
{
ov_pcm_seek(&OggVorbisFile, offset / ChannelCount);
}
void OggVorbisDecoder::Read(byte* samples, uint32 numSamples)
{
uint32 numReadSamples = 0;
while (numReadSamples < numSamples)
{
const int32 bytesToRead = static_cast<int32>(numSamples - numReadSamples) * sizeof(int16);
const uint32 bytesRead = ov_read(&OggVorbisFile, (char*)samples, bytesToRead, 0, 2, 1, nullptr);
if (bytesRead > 0)
{
const uint32 samplesRead = bytesRead / sizeof(int16);
numReadSamples += samplesRead;
samples += samplesRead * sizeof(int16);
}
else
{
break;
}
}
}
bool OggVorbisDecoder::IsValid(ReadStream* stream, uint32 offset)
{
stream->SetPosition(offset);
Stream = stream;
Offset = offset;
OggVorbis_File file;
const ov_callbacks callbacks = { &oggRead, &oggSeek, nullptr, &oggTell };
if (ov_test_callbacks(this, &file, nullptr, 0, callbacks) == 0)
{
ov_clear(&file);
return true;
}
return false;
}
#endif

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#if COMPILE_WITH_OGG_VORBIS
#include "AudioDecoder.h"
#include "Engine/Serialization/ReadStream.h"
#include <ThirdParty/vorbis/vorbisfile.h>
/// <summary>
/// Decodes .ogg audio data into raw PCM format.
/// </summary>
/// <seealso cref="AudioDecoder" />
class OggVorbisDecoder : public AudioDecoder
{
public:
ReadStream* Stream;
uint32 Offset;
uint32 ChannelCount;
OggVorbis_File OggVorbisFile;
public:
/// <summary>
/// Initializes a new instance of the <see cref="OggVorbisDecoder"/> class.
/// </summary>
OggVorbisDecoder()
{
Stream = nullptr;
Offset = 0;
ChannelCount = 0;
OggVorbisFile.datasource = nullptr;
}
/// <summary>
/// Finalizes an instance of the <see cref="OggVorbisDecoder"/> class.
/// </summary>
~OggVorbisDecoder()
{
if (OggVorbisFile.datasource != nullptr)
ov_clear(&OggVorbisFile);
}
public:
// [AudioDecoder]
bool Open(ReadStream* stream, AudioDataInfo& info, uint32 offset = 0) override;
void Seek(uint32 offset) override;
void Read(byte* samples, uint32 numSamples) override;
bool IsValid(ReadStream* stream, uint32 offset = 0) override;
};
#endif

View File

@@ -0,0 +1,265 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_OGG_VORBIS
#include "OggVorbisEncoder.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Math.h"
#include "AudioTool.h"
#include <ThirdParty/vorbis/vorbisenc.h>
// Writes to the internal cached buffer and flushes it if needed
#define WRITE_TO_BUFFER(data, length) \
if ((_bufferOffset + length) > BUFFER_SIZE) \
Flush(); \
if(length > BUFFER_SIZE) \
_writeCallback(data, length, _userData); \
else \
{ \
Platform::MemoryCopy(_buffer + _bufferOffset, data, length); \
_bufferOffset += length; \
}
OggVorbisEncoder::OggVorbisEncoder()
: _bufferOffset(0)
, _numChannels(0)
, _bitDepth(0)
, _closed(true)
{
}
OggVorbisEncoder::~OggVorbisEncoder()
{
Close();
}
bool OggVorbisEncoder::Open(WriteCallback writeCallback, uint32 sampleRate, uint32 bitDepth, uint32 numChannels, float quality, void* userData)
{
_numChannels = numChannels;
_bitDepth = bitDepth;
_writeCallback = writeCallback;
_userData = userData;
_closed = false;
ogg_stream_init(&_oggState, rand());
vorbis_info_init(&_vorbisInfo);
int32 status = vorbis_encode_init_vbr(&_vorbisInfo, numChannels, sampleRate, quality);
if (status != 0)
{
LOG(Warning, "Failed to write Ogg Vorbis file.");
Close();
return true;
}
vorbis_analysis_init(&_vorbisState, &_vorbisInfo);
vorbis_block_init(&_vorbisState, &_vorbisBlock);
// Generate header
vorbis_comment comment;
vorbis_comment_init(&comment);
ogg_packet headerPacket, commentPacket, codePacket;
status = vorbis_analysis_headerout(&_vorbisState, &comment, &headerPacket, &commentPacket, &codePacket);
vorbis_comment_clear(&comment);
if (status != 0)
{
LOG(Warning, "Failed to write Ogg Vorbis file.");
Close();
return true;
}
// Write header
ogg_stream_packetin(&_oggState, &headerPacket);
ogg_stream_packetin(&_oggState, &commentPacket);
ogg_stream_packetin(&_oggState, &codePacket);
ogg_page page;
while (ogg_stream_flush(&_oggState, &page) > 0)
{
WRITE_TO_BUFFER(page.header, page.header_len);
WRITE_TO_BUFFER(page.body, page.body_len);
}
return false;
}
void OggVorbisEncoder::Write(uint8* samples, uint32 numSamples)
{
static const uint32 WRITE_LENGTH = 1024;
uint32 numFrames = numSamples / _numChannels;
while (numFrames > 0)
{
const uint32 numFramesToWrite = Math::Min(numFrames, WRITE_LENGTH);
float** buffer = vorbis_analysis_buffer(&_vorbisState, numFramesToWrite);
if (_bitDepth == 8)
{
for (uint32 i = 0; i < numFramesToWrite; i++)
{
for (uint32 j = 0; j < _numChannels; j++)
{
const int8 sample = *(int8*)samples;
const float encodedSample = sample / 127.0f;
buffer[j][i] = encodedSample;
samples++;
}
}
}
else if (_bitDepth == 16)
{
for (uint32 i = 0; i < numFramesToWrite; i++)
{
for (uint32 j = 0; j < _numChannels; j++)
{
const int16 sample = *(int16*)samples;
const float encodedSample = sample / 32767.0f;
buffer[j][i] = encodedSample;
samples += 2;
}
}
}
else if (_bitDepth == 24)
{
for (uint32 i = 0; i < numFramesToWrite; i++)
{
for (uint32 j = 0; j < _numChannels; j++)
{
const int32 sample = AudioTool::Convert24To32Bits(samples);
const float encodedSample = sample / 2147483647.0f;
buffer[j][i] = encodedSample;
samples += 3;
}
}
}
else if (_bitDepth == 32)
{
for (uint32 i = 0; i < numFramesToWrite; i++)
{
for (uint32 j = 0; j < _numChannels; j++)
{
const int32 sample = *(int32*)samples;
const float encodedSample = sample / 2147483647.0f;
buffer[j][i] = encodedSample;
samples += 4;
}
}
}
else
{
CRASH;
}
// Signal how many frames were written
vorbis_analysis_wrote(&_vorbisState, numFramesToWrite);
WriteBlocks();
numFrames -= numFramesToWrite;
}
}
void OggVorbisEncoder::WriteBlocks()
{
while (vorbis_analysis_blockout(&_vorbisState, &_vorbisBlock) == 1)
{
// Analyze and determine optimal bit rate
vorbis_analysis(&_vorbisBlock, nullptr);
vorbis_bitrate_addblock(&_vorbisBlock);
// Write block into ogg packets
ogg_packet packet;
while (vorbis_bitrate_flushpacket(&_vorbisState, &packet))
{
ogg_stream_packetin(&_oggState, &packet);
// If new page, write it to the internal buffer
ogg_page page;
while (ogg_stream_flush(&_oggState, &page) > 0)
{
WRITE_TO_BUFFER(page.header, page.header_len);
WRITE_TO_BUFFER(page.body, page.body_len);
}
}
}
}
struct EncodedBlock
{
uint8* data;
uint32 size;
};
struct ConvertWriteCallbackData
{
Array<EncodedBlock> Blocks;
uint32 TotalEncodedSize = 0;
};
void ConvertWriteCallback(uint8* buffer, uint32 size, void* userData)
{
EncodedBlock newBlock;
newBlock.data = (uint8*)Allocator::Allocate(size);
newBlock.size = size;
Platform::MemoryCopy(newBlock.data, buffer, size);
auto data = (ConvertWriteCallbackData*)userData;
data->Blocks.Add(newBlock);
data->TotalEncodedSize += size;
};
bool OggVorbisEncoder::Convert(byte* samples, AudioDataInfo& info, BytesContainer& result, float quality)
{
ConvertWriteCallbackData data;
if (Open(ConvertWriteCallback, info.SampleRate, info.BitDepth, info.NumChannels, quality, &data))
return true;
Write(samples, info.NumSamples);
Close();
result.Allocate(data.TotalEncodedSize);
uint32 offset = 0;
for (auto& block : data.Blocks)
{
Platform::MemoryCopy(result.Get() + offset, block.data, block.size);
offset += block.size;
Allocator::Free(block.data);
}
return false;
}
void OggVorbisEncoder::Flush()
{
if (_bufferOffset > 0 && _writeCallback != nullptr)
_writeCallback(_buffer, _bufferOffset, _userData);
_bufferOffset = 0;
}
void OggVorbisEncoder::Close()
{
if (_closed)
return;
// Mark end of data and flush any remaining data in the buffers
vorbis_analysis_wrote(&_vorbisState, 0);
WriteBlocks();
Flush();
ogg_stream_clear(&_oggState);
vorbis_block_clear(&_vorbisBlock);
vorbis_dsp_clear(&_vorbisState);
vorbis_info_clear(&_vorbisInfo);
_closed = true;
}
#endif

View File

@@ -0,0 +1,95 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "AudioEncoder.h"
#include "Engine/Audio/Config.h"
#if COMPILE_WITH_AUDIO_TOOL && COMPILE_WITH_OGG_VORBIS
#include <ThirdParty/vorbis/vorbisfile.h>
/// <summary>
/// Raw PCM data encoder to Ogg Vorbis audio format.
/// </summary>
/// <seealso cref="AudioEncoder" />
class OggVorbisEncoder : public AudioEncoder
{
public:
typedef void (*WriteCallback)(byte*, uint32, void*);
private:
static const uint32 BUFFER_SIZE = 4096;
WriteCallback _writeCallback;
void* _userData;
byte _buffer[BUFFER_SIZE];
uint32 _bufferOffset;
uint32 _numChannels;
uint32 _bitDepth;
bool _closed;
ogg_stream_state _oggState;
vorbis_info _vorbisInfo;
vorbis_dsp_state _vorbisState;
vorbis_block _vorbisBlock;
public:
/// <summary>
/// Initializes a new instance of the <see cref="OggVorbisEncoder"/> class.
/// </summary>
OggVorbisEncoder();
/// <summary>
/// Finalizes an instance of the <see cref="OggVorbisEncoder"/> class.
/// </summary>
~OggVorbisEncoder();
public:
/// <summary>
/// Sets up the writer. Should be called before calling Write().
/// </summary>
/// <param name="writeCallback">Callback that will be triggered when the writer is ready to output some data. The callback should copy the provided data into its own buffer.</param>
/// <param name="sampleRate">Determines how many samples per second the written data will have.</param>
/// <param name="bitDepth">Determines the size of a single sample, in bits.</param>
/// <param name="numChannels">Determines the number of audio channels. Channel data will be output interleaved in the output buffer.</param>
/// <param name="quality">The output data compression quality (normalized in range [0;1]).</param>
/// <param name="userData">The custom used data passed to the write callback.</param>
/// <returns>True if failed to open the data, otherwise false.</returns>
bool Open(WriteCallback writeCallback, uint32 sampleRate, uint32 bitDepth, uint32 numChannels, float quality, void* userData = nullptr);
/// <summary>
/// Writes a new set of samples and converts them to Ogg Vorbis.
/// </summary>
/// <param name="samples">The samples in PCM format. 8-bit samples should be unsigned, but higher bit depths signed. Each sample is assumed to be the bit depth that was provided to the Open() method.</param>
/// <param name="numSamples">The number of samples to encode.</param>
void Write(byte* samples, uint32 numSamples);
/// <summary>
/// Flushes the last of the data into the write buffer (triggers the write callback). This is called automatically when the writer is closed or goes out of scope.
/// </summary>
void Flush();
/// <summary>
/// Closes the encoder and flushes the last of the data into the write buffer (triggers the write callback). This is called automatically when the writer goes out of scope.
/// </summary>
void Close();
private:
/// <summary>
/// Writes Vorbis blocks into Ogg packets.
/// </summary>
void WriteBlocks();
public:
// [AudioEncoder]
bool Convert(byte* samples, AudioDataInfo& info, BytesContainer& result, float quality = 0.5f) override;
};
#endif

View File

@@ -0,0 +1,168 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "WaveDecoder.h"
#include "Engine/Core/Log.h"
#include "AudioTool.h"
#define WAVE_FORMAT_PCM 0x0001
#define WAVE_FORMAT_IEEE_FLOAT 0x0003
#define WAVE_FORMAT_ALAW 0x0006
#define WAVE_FORMAT_MULAW 0x0007
#define WAVE_FORMAT_EXTENDED 0xFFFE
#define MAIN_CHUNK_SIZE 12
bool WaveDecoder::ParseHeader(AudioDataInfo& info)
{
bool foundData = false;
while (!foundData)
{
// Get sub-chunk ID and size
uint8 subChunkId[4];
mStream->Read(subChunkId, sizeof(subChunkId));
uint32 subChunkSize = 0;
mStream->ReadUint32(&subChunkSize);
// FMT chunk
if (subChunkId[0] == 'f' && subChunkId[1] == 'm' && subChunkId[2] == 't' && subChunkId[3] == ' ')
{
uint16 format;
mStream->ReadUint16(&format);
if (format != WAVE_FORMAT_PCM && format != WAVE_FORMAT_IEEE_FLOAT && format != WAVE_FORMAT_EXTENDED)
{
LOG(Warning, "Wave file doesn't contain raw PCM data. Not supported.");
return false;
}
uint16 numChannels = 0;
mStream->ReadUint16(&numChannels);
uint32 sampleRate = 0;
mStream->ReadUint32(&sampleRate);
uint32 byteRate = 0;
mStream->ReadUint32(&byteRate);
uint16 blockAlign = 0;
mStream->ReadUint16(&blockAlign);
uint16 bitDepth = 0;
mStream->ReadUint16(&bitDepth);
if (bitDepth != 8 && bitDepth != 16 && bitDepth != 24 && bitDepth != 32)
{
LOG(Warning, "Unsupported number of bits per sample: {0}", bitDepth);
return false;
}
info.NumChannels = numChannels;
info.SampleRate = sampleRate;
info.BitDepth = bitDepth;
// Read extension data, and get the actual format
if (format == WAVE_FORMAT_EXTENDED)
{
uint16 extensionSize = 0;
mStream->ReadUint16(&extensionSize);
if (extensionSize != 22)
{
LOG(Warning, "Wave file doesn't contain raw PCM data. Not supported.");
return false;
}
uint16 validBitDepth = 0;
mStream->ReadUint16(&validBitDepth);
uint32 channelMask = 0;
mStream->ReadUint32(&channelMask);
uint8 subFormat[16];
mStream->Read(subFormat, sizeof(subFormat));
Platform::MemoryCopy(&format, subFormat, sizeof(format));
if (format != WAVE_FORMAT_PCM)
{
LOG(Warning, "Wave file doesn't contain raw PCM data. Not supported.");
return false;
}
}
mBytesPerSample = bitDepth / 8;
mFormat = format;
}
// DATA chunk
else if (subChunkId[0] == 'd' && subChunkId[1] == 'a' && subChunkId[2] == 't' && subChunkId[3] == 'a')
{
info.NumSamples = subChunkSize / mBytesPerSample;
mDataOffset = (uint32)mStream->GetPosition();
foundData = true;
}
// Unsupported chunk type
else
{
if (mStream->GetPosition() + subChunkSize >= mStream->GetLength())
return false;
mStream->SetPosition(mStream->GetPosition() + subChunkSize);
}
}
return true;
}
bool WaveDecoder::Open(ReadStream* stream, AudioDataInfo& info, uint32 offset)
{
ASSERT(stream);
mStream = stream;
mStream->SetPosition(offset + MAIN_CHUNK_SIZE);
if (!ParseHeader(info))
{
LOG(Warning, "Provided file is not a valid WAVE file.");
return false;
}
return true;
}
void WaveDecoder::Seek(uint32 offset)
{
mStream->SetPosition(mDataOffset + offset * mBytesPerSample);
}
void WaveDecoder::Read(byte* samples, uint32 numSamples)
{
const uint32 numRead = numSamples * mBytesPerSample;
mStream->Read(samples, numRead);
// 8-bit samples are stored as unsigned, but engine convention is to store all bit depths as signed
if (mBytesPerSample == 1)
{
for (uint32 i = 0; i < numRead; i++)
{
int8 val = samples[i] - 128;
samples[i] = *((uint8*)&val);
}
}
// IEEE float need to be converted into signed PCM data
else if (mFormat == WAVE_FORMAT_IEEE_FLOAT)
{
AudioTool::ConvertFromFloat((const float*)samples, (int32*)samples, numSamples);
}
}
bool WaveDecoder::IsValid(ReadStream* stream, uint32 offset)
{
ASSERT(stream);
stream->SetPosition(offset);
byte header[MAIN_CHUNK_SIZE];
stream->ReadBytes(header, sizeof(header));
return (header[0] == 'R') && (header[1] == 'I') && (header[2] == 'F') && (header[3] == 'F')
&& (header[8] == 'W') && (header[9] == 'A') && (header[10] == 'V') && (header[11] == 'E');
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#if COMPILE_WITH_AUDIO_TOOL
#include "AudioDecoder.h"
#include "Engine/Serialization/ReadStream.h"
/// <summary>
/// Decodes .wav audio data into raw PCM format.
/// </summary>
/// <seealso cref="AudioDecoder" />
class WaveDecoder : public AudioDecoder
{
private:
ReadStream* mStream;
uint16 mFormat;
uint32 mDataOffset;
uint32 mBytesPerSample;
public:
/// <summary>
/// Initializes a new instance of the <see cref="WaveDecoder"/> class.
/// </summary>
WaveDecoder()
{
mStream = nullptr;
mFormat = 0;
mDataOffset = 0;
mBytesPerSample = 0;
}
private:
/// <summary>
/// Parses the WAVE header and output audio file meta-data. Returns false if the header is not valid.
/// </summary>
/// <param name="info">The output information.</param>
/// <returns>True if header is valid, otherwise false.</returns>
bool ParseHeader(AudioDataInfo& info);
public:
// [AudioDecoder]
bool Open(ReadStream* stream, AudioDataInfo& info, uint32 offset = 0) override;
void Seek(uint32 offset) override;
void Read(byte* samples, uint32 numSamples) override;
bool IsValid(ReadStream* stream, uint32 offset = 0) override;
};
#endif

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using Flax.Build;
using Flax.Build.NativeCpp;
/// <summary>
/// Materials shader code generation utilities module.
/// </summary>
public class MaterialGenerator : EngineModule
{
/// <inheritdoc />
public override void Setup(BuildOptions options)
{
base.Setup(options);
options.PublicDefinitions.Add("COMPILE_WITH_MATERIAL_GRAPH");
options.PublicDependencies.Add("Visject");
}
/// <inheritdoc />
public override void GetFilesToDeploy(List<string> files)
{
}
}

View File

@@ -0,0 +1,317 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MATERIAL_GRAPH
#include "MaterialGenerator.h"
#include "Engine/Content/Assets/MaterialInstance.h"
#include "Engine/Content/Assets/Material.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Engine/Engine.h"
MaterialGenerator::MaterialGraphBoxesMapping MaterialGenerator::MaterialGraphBoxesMappings[] =
{
{ 0, nullptr, MaterialTreeType::PixelShader, MaterialValue::Zero },
{ 1, TEXT("Color"), MaterialTreeType::PixelShader, MaterialValue::InitForZero(VariantType::Vector3) },
{ 2, TEXT("Mask"), MaterialTreeType::PixelShader, MaterialValue::InitForOne(VariantType::Float) },
{ 3, TEXT("Emissive"), MaterialTreeType::PixelShader, MaterialValue::InitForZero(VariantType::Vector3) },
{ 4, TEXT("Metalness"), MaterialTreeType::PixelShader, MaterialValue::InitForZero(VariantType::Float) },
{ 5, TEXT("Specular"), MaterialTreeType::PixelShader, MaterialValue::InitForHalf(VariantType::Float) },
{ 6, TEXT("Roughness"), MaterialTreeType::PixelShader, MaterialValue::InitForHalf(VariantType::Float) },
{ 7, TEXT("AO"), MaterialTreeType::PixelShader, MaterialValue::InitForOne(VariantType::Float) },
{ 8, TEXT("TangentNormal"), MaterialTreeType::PixelShader, MaterialValue(VariantType::Vector3, TEXT("float3(0, 0, 1.0)")) },
{ 9, TEXT("Opacity"), MaterialTreeType::PixelShader, MaterialValue::InitForOne(VariantType::Float) },
{ 10, TEXT("Refraction"), MaterialTreeType::PixelShader, MaterialValue::InitForOne(VariantType::Float) },
{ 11, TEXT("PositionOffset"), MaterialTreeType::VertexShader, MaterialValue::InitForZero(VariantType::Vector3) },
{ 12, TEXT("TessellationMultiplier"), MaterialTreeType::VertexShader, MaterialValue(VariantType::Float, TEXT("4.0f")) },
{ 13, TEXT("WorldDisplacement"), MaterialTreeType::DomainShader, MaterialValue::InitForZero(VariantType::Vector3) },
{ 14, TEXT("SubsurfaceColor"), MaterialTreeType::PixelShader, MaterialValue::InitForZero(VariantType::Vector3) },
};
const MaterialGenerator::MaterialGraphBoxesMapping& MaterialGenerator::GetMaterialRootNodeBox(MaterialGraphBoxes box)
{
return MaterialGraphBoxesMappings[static_cast<int32>(box)];
}
void MaterialGenerator::AddLayer(MaterialLayer* layer)
{
_layers.Add(layer);
}
MaterialLayer* MaterialGenerator::GetLayer(const Guid& id, Node* caller)
{
// Find layer first
for (int32 i = 0; i < _layers.Count(); i++)
{
if (_layers[i]->ID == id)
{
// Found
return _layers[i];
}
}
// Load asset
Asset* asset = Assets.LoadAsync<MaterialBase>(id);
if (asset == nullptr || asset->WaitForLoaded(30000))
{
OnError(caller, nullptr, TEXT("Failed to load material asset."));
return nullptr;
}
// Special case for engine exit event
if (Engine::ShouldExit())
{
// End
return nullptr;
}
// Check if load failed
if (!asset->IsLoaded())
{
OnError(caller, nullptr, TEXT("Failed to load material asset."));
return nullptr;
}
// Check if it's a material instance
Material* material = nullptr;
Asset* iterator = asset;
while (material == nullptr)
{
// Wait for material to be loaded
if (iterator->WaitForLoaded())
{
OnError(caller, nullptr, TEXT("Material asset load failed."));
return nullptr;
}
if (iterator->GetTypeName() == MaterialInstance::TypeName)
{
auto instance = ((MaterialInstance*)iterator);
// Check if base material has been assigned
if (instance->GetBaseMaterial() == nullptr)
{
OnError(caller, nullptr, TEXT("Material instance has missing base material."));
return nullptr;
}
iterator = instance->GetBaseMaterial();
}
else
{
material = ((Material*)iterator);
// Check if instanced material is not instancing current material
ASSERT(GetRootLayer());
if (material->GetID() == GetRootLayer()->ID)
{
OnError(caller, nullptr, TEXT("Cannot use instance of the current material as layer."));
return nullptr;
}
}
}
ASSERT(material);
// Get surface data
BytesContainer surfaceData = material->LoadSurface(true);
if (surfaceData.IsInvalid())
{
OnError(caller, nullptr, TEXT("Cannot load surface data."));
return nullptr;
}
MemoryReadStream surfaceDataStream(surfaceData.Get(), surfaceData.Length());
// Load layer
auto layer = MaterialLayer::Load(id, &surfaceDataStream, material->GetInfo(), material->ToString());
// Validate layer info
if (layer == nullptr)
{
OnError(caller, nullptr, TEXT("Cannot load layer."));
return nullptr;
}
#if 0 // allow mixing material domains because material generator uses the base layer for features usage checks and sub layers produce only Material structure for blending or extracting
if (layer->Domain != GetRootLayer()->Domain) // TODO: maybe allow solid on transparent?
{
OnError(caller, nullptr, TEXT("Cannot mix materials with different Domains."));
return nullptr;
}
#endif
// Override parameters values if using material instance
if (asset->GetTypeName() == MaterialInstance::TypeName)
{
auto instance = ((MaterialInstance*)asset);
auto instanceParams = &instance->Params;
// Clone overriden parameters values
for (auto& param : layer->Graph.Parameters)
{
const auto instanceParam = instanceParams->Get(param.Identifier);
if (instanceParam && instanceParam->IsOverride())
{
param.Value = instanceParam->GetValue();
// Fold object references to their ids
if (param.Value.Type == VariantType::Object)
param.Value = param.Value.AsObject ? param.Value.AsObject->GetID() : Guid::Empty;
else if (param.Value.Type == VariantType::Asset)
param.Value = param.Value.AsObject ? param.Value.AsObject->GetID() : Guid::Empty;
}
}
}
// Prepare layer
layer->Prepare();
const bool allowPublicParameters = true;
prepareLayer(layer, allowPublicParameters);
AddLayer(layer);
return layer;
}
MaterialLayer* MaterialGenerator::GetRootLayer() const
{
return _layers.Count() > 0 ? _layers[0] : nullptr;
}
void MaterialGenerator::prepareLayer(MaterialLayer* layer, bool allowVisibleParams)
{
LayerParamMapping m;
// Ensure that layer hasn't been used
ASSERT(layer->HasAnyVariableName() == false);
// Add all parameters to be saved in the result material parameters collection (perform merge)
bool isRooLayer = GetRootLayer() == layer;
for (int32 j = 0; j < layer->Graph.Parameters.Count(); j++)
{
const auto param = &layer->Graph.Parameters[j];
// For all not root layers (sub-layers) we won't to change theirs ID in order to prevent duplicated ID)
m.SrcId = param->Identifier;
if (isRooLayer)
{
// Use the same ID (so we can edit it)
m.DstId = param->Identifier;
}
else
{
// Generate new ID
m.DstId = param->Identifier;
m.DstId.A += _parameters.Count() * 17 + 13;
}
layer->ParamIdsMappings.Add(m);
SerializedMaterialParam& mp = _parameters.AddOne();
mp.ID = m.DstId;
mp.IsPublic = param->IsPublic && allowVisibleParams;
mp.Override = true;
mp.Name = param->Name;
mp.ShaderName = getParamName(_parameters.Count());
mp.Type = MaterialParameterType::Bool;
mp.AsBool = false;
switch (param->Type.Type)
{
case VariantType::Bool:
mp.Type = MaterialParameterType::Bool;
mp.AsBool = param->Value.AsBool;
break;
case VariantType::Int:
mp.Type = MaterialParameterType::Integer;
mp.AsInteger = param->Value.AsInt;
break;
case VariantType::Int64:
mp.Type = MaterialParameterType::Integer;
mp.AsInteger = (int32)param->Value.AsInt64;
break;
case VariantType::Uint:
mp.Type = MaterialParameterType::Integer;
mp.AsInteger = (int32)param->Value.AsUint;
break;
case VariantType::Uint64:
mp.Type = MaterialParameterType::Integer;
mp.AsInteger = (int32)param->Value.AsUint64;
break;
case VariantType::Float:
mp.Type = MaterialParameterType::Float;
mp.AsFloat = param->Value.AsFloat;
break;
case VariantType::Double:
mp.Type = MaterialParameterType::Float;
mp.AsFloat = (float)param->Value.AsDouble;
break;
case VariantType::Vector2:
mp.Type = MaterialParameterType::Vector2;
mp.AsVector2 = param->Value.AsVector2();
break;
case VariantType::Vector3:
mp.Type = MaterialParameterType::Vector3;
mp.AsVector3 = param->Value.AsVector3();
break;
case VariantType::Vector4:
mp.Type = MaterialParameterType::Vector4;
mp.AsVector4 = param->Value.AsVector4();
break;
case VariantType::Color:
mp.Type = MaterialParameterType::Color;
mp.AsColor = param->Value.AsColor();
break;
case VariantType::Matrix:
mp.Type = MaterialParameterType::Matrix;
mp.AsMatrix = *(Matrix*)param->Value.AsBlob.Data;
break;
case VariantType::Asset:
if (!param->Type.TypeName)
{
OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
break;
}
if (StringUtils::Compare(param->Type.TypeName, "FlaxEngine.Texture") == 0)
{
mp.Type = MaterialParameterType::Texture;
// Special case for Normal Maps
auto asset = Content::LoadAsync<Texture>((Guid)param->Value);
if (asset && !asset->WaitForLoaded() && asset->IsNormalMap())
mp.Type = MaterialParameterType::NormalMap;
}
else if (StringUtils::Compare(param->Type.TypeName, "FlaxEngine.CubeTexture") == 0)
mp.Type = MaterialParameterType::CubeTexture;
else
OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
mp.AsGuid = (Guid)param->Value;
break;
case VariantType::Object:
if (!param->Type.TypeName)
{
OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
break;
}
if (StringUtils::Compare(param->Type.TypeName, "FlaxEngine.GPUTexture") == 0)
mp.Type = MaterialParameterType::GPUTexture;
else
OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
mp.AsGuid = (Guid)param->Value;
break;
case VariantType::Enum:
if (!param->Type.TypeName)
{
OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
break;
}
if (StringUtils::Compare(param->Type.TypeName, "FlaxEngine.MaterialSceneTextures") == 0)
mp.Type = MaterialParameterType::SceneTexture;
else if (StringUtils::Compare(param->Type.TypeName, "FlaxEngine.ChannelMask") == 0)
mp.Type = MaterialParameterType::ChannelMask;
else
OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
mp.AsInteger = (int32)param->Value.AsUint64;
break;
default:
OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
break;
}
}
}
#endif

View File

@@ -0,0 +1,347 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MATERIAL_GRAPH
#include "MaterialGenerator.h"
void MaterialGenerator::ProcessGroupLayers(Box* box, Node* node, Value& value)
{
switch (node->TypeID)
{
// Sample Layer
case 1:
{
Guid id = (Guid)node->Values[0];
if (!id.IsValid())
{
OnError(node, box, TEXT("Missing material."));
break;
}
ASSERT(GetRootLayer() != nullptr && GetRootLayer()->ID.IsValid());
if (id == GetRootLayer()->ID)
{
OnError(node, box, TEXT("Cannot use current material as layer."));
break;
}
// Load material layer
auto layer = GetLayer(id, node);
if (layer == nullptr)
{
OnError(node, box, TEXT("Cannot load material."));
break;
}
ASSERT(_layers.Contains(layer));
// Peek material variable name (may be empty if not used before)
auto uvsBox = node->GetBox(0);
String& varName = layer->GetVariableName(uvsBox->HasConnection() ? uvsBox->Connections[0] : nullptr);
// Check if layer has no generated data
if (varName.IsEmpty())
{
// Create material variable
MaterialValue defaultValue = MaterialValue::InitForZero(VariantType::Void);
varName = writeLocal(defaultValue, node).Value;
// Check if use custom UVs
String customUVs;
String orginalUVs;
if (uvsBox->HasConnection())
{
// Sample custom UVs
MaterialValue v = eatBox(uvsBox->GetParent<Node>(), uvsBox->FirstConnection());
customUVs = v.Value;
// TODO: better idea would to be to use variable for current UVs, by default=input.TexCoord.xy could be modified when sampling layers
// Cache original pixel UVs
orginalUVs = writeLocal(VariantType::Vector2, TEXT("input.TexCoord.xy"), node).Value;
// Modify current pixel UVs
_writer.Write(*String::Format(TEXT("\tinput.TexCoord.xy = {0};\n"), customUVs));
}
// Cache current layer and tree type
auto callingLayerVarName = _treeLayerVarName;
auto callingLayer = _treeLayer;
auto treeType = _treeType;
_treeLayer = layer;
_graphStack.Push(&_treeLayer->Graph);
_treeLayerVarName = varName;
// Sample layer
const MaterialGraphBox* layerInputBox = layer->Root->GetBox(0);
if (layerInputBox->HasConnection())
{
MaterialValue subLayer = eatBox(layer->Root, layerInputBox->FirstConnection());
_writer.Write(TEXT("\t{0} = {1};\n"), varName, subLayer.Value);
}
else
{
#define EAT_BOX(type) eatMaterialGraphBox(layer, MaterialGraphBoxes::type)
switch (_treeType)
{
case MaterialTreeType::VertexShader:
{
EAT_BOX(PositionOffset);
EAT_BOX(TessellationMultiplier);
break;
}
case MaterialTreeType::DomainShader:
{
EAT_BOX(WorldDisplacement);
break;
}
case MaterialTreeType::PixelShader:
{
EAT_BOX(Normal);
EAT_BOX(Color);
EAT_BOX(Metalness);
EAT_BOX(Specular);
EAT_BOX(Roughness);
EAT_BOX(AmbientOcclusion);
EAT_BOX(Opacity);
EAT_BOX(Refraction);
EAT_BOX(Mask);
EAT_BOX(Emissive);
EAT_BOX(SubsurfaceColor);
if ((GetRootLayer()->FeaturesFlags & MaterialFeaturesFlags::InputWorldSpaceNormal) != (layer->FeaturesFlags & MaterialFeaturesFlags::InputWorldSpaceNormal))
{
// TODO convert normal vector to match the output layer properties
LOG(Warning, "TODO: convert normal vector to match the output layer properties");
}
break;
}
default:
break;
}
#undef EAT_BOX
}
// Mix usage flags
callingLayer->UsageFlags |= _treeLayer->UsageFlags;
// Restore calling tree and layer
_treeLayerVarName = callingLayerVarName;
_treeLayer = callingLayer;
_graphStack.Pop();
_treeType = treeType;
// Check was using custom UVs for sampling
if (uvsBox->HasConnection())
{
// Restore current pixel UVs
_writer.Write(*String::Format(TEXT("\tinput.TexCoord.xy = {0};\n"), orginalUVs));
}
}
ASSERT(varName.HasChars());
// Use value
value = MaterialValue(VariantType::Void, varName);
break;
}
// Blend Linear
case 2:
case 5:
case 8:
{
const Value defaultValue = MaterialValue::InitForZero(VariantType::Void);
const Value alpha = tryGetValue(node->GetBox(2), 0, Value::Zero);
if (alpha.IsZero())
{
// Bottom-only
value = tryGetValue(node->GetBox(0), defaultValue);
return;
}
if (alpha.IsOne())
{
// Top-only
value = tryGetValue(node->GetBox(1), defaultValue);
return;
}
// Sample layers
const MaterialValue bottom = tryGetValue(node->GetBox(0), defaultValue);
const MaterialValue top = tryGetValue(node->GetBox(1), defaultValue);
// Create new layer
value = writeLocal(defaultValue, node);
// Blend layers
if (node->TypeID == 8)
{
// Height Layer Blend
auto bottomHeight = tryGetValue(node->GetBox(4), Value::Zero);
auto topHeight = tryGetValue(node->GetBox(5), Value::Zero);
auto bottomHeightScaled = writeLocal(VariantType::Float, String::Format(TEXT("{0} * (1.0 - {1})"), bottomHeight.Value, alpha.Value), node);
auto topHeightScaled = writeLocal(VariantType::Float, String::Format(TEXT("{0} * {1}"), topHeight.Value, alpha.Value), node);
auto heightStart = writeLocal(VariantType::Float, String::Format(TEXT("max({0}, {1}) - 0.05"), bottomHeightScaled.Value, topHeightScaled.Value), node);
auto bottomLevel = writeLocal(VariantType::Float, String::Format(TEXT("max({0} - {1}, 0.0001)"), topHeightScaled.Value, heightStart.Value), node);
_writer.Write(TEXT("\t{0} = {1} / (max({2} - {3}, 0) + {4});\n"), alpha.Value, bottomLevel.Value, bottomHeightScaled.Value, heightStart.Value, bottomLevel.Value);
}
#define EAT_BOX(type) writeBlending(MaterialGraphBoxes::type, value, bottom, top, alpha)
switch (_treeType)
{
case MaterialTreeType::VertexShader:
{
EAT_BOX(PositionOffset);
EAT_BOX(TessellationMultiplier);
break;
}
case MaterialTreeType::DomainShader:
{
EAT_BOX(WorldDisplacement);
break;
}
case MaterialTreeType::PixelShader:
{
EAT_BOX(Normal);
EAT_BOX(Color);
EAT_BOX(Metalness);
EAT_BOX(Specular);
EAT_BOX(Roughness);
EAT_BOX(AmbientOcclusion);
EAT_BOX(Opacity);
EAT_BOX(Refraction);
EAT_BOX(Mask);
EAT_BOX(Emissive);
EAT_BOX(SubsurfaceColor);
break;
}
default:
break;
}
#undef EAT_BOX
break;
}
// Pack Material Layer (old: without TessellationMultiplier, SubsurfaceColor and WorldDisplacement support)
// [Deprecated on 2018.10.01, expires on 2019.10.01]
case 3:
{
// Create new layer
const MaterialValue defaultValue = MaterialValue::InitForZero(VariantType::Void);
value = writeLocal(defaultValue, node);
// Sample layer
#define CHECK_MATERIAL_FEATURE(type, feature) if (node->GetBox(static_cast<int32>(MaterialGraphBoxes::type))->HasConnection()) _treeLayer->UsageFlags |= MaterialUsageFlags::feature
#define EAT_BOX(type) eatMaterialGraphBox(value.Value, node->GetBox((int32)MaterialGraphBoxes::type), MaterialGraphBoxes::type)
switch (_treeType)
{
case MaterialTreeType::VertexShader:
{
EAT_BOX(PositionOffset);
CHECK_MATERIAL_FEATURE(PositionOffset, UsePositionOffset);
break;
}
case MaterialTreeType::PixelShader:
{
EAT_BOX(Normal);
EAT_BOX(Color);
EAT_BOX(Metalness);
EAT_BOX(Specular);
EAT_BOX(Roughness);
EAT_BOX(AmbientOcclusion);
EAT_BOX(Opacity);
EAT_BOX(Refraction);
EAT_BOX(Mask);
EAT_BOX(Emissive);
CHECK_MATERIAL_FEATURE(Emissive, UseEmissive);
CHECK_MATERIAL_FEATURE(Normal, UseNormal);
CHECK_MATERIAL_FEATURE(Mask, UseMask);
break;
}
default:
break;
}
#undef CHECK_MATERIAL_FEATURE
#undef EAT_BOX
break;
}
// Unpack Material Layer
// Node type 4 -> [Deprecated on 2018.10.01, expires on 2019.10.01]
case 4:
case 7:
{
// Get layer
MaterialValue defaultValue = MaterialValue::InitForZero(VariantType::Void);
MaterialValue layer = tryGetValue(node->GetBox(0), defaultValue);
// Extract component or use default value if cannot use that box in the current tree
auto& nodeMapping = MaterialGraphBoxesMappings[box->ID];
if (nodeMapping.TreeType == _treeType)
{
value = MaterialValue(nodeMapping.DefaultValue.Type, layer.Value + TEXT(".") + nodeMapping.SubName);
}
else
{
value = nodeMapping.DefaultValue;
}
break;
}
// Pack Material Layer
case 6:
{
// Create new layer
const MaterialValue defaultValue = MaterialValue::InitForZero(VariantType::Void);
value = writeLocal(defaultValue, node);
// Sample layer
#define CHECK_MATERIAL_FEATURE(type, feature) if (node->GetBox(static_cast<int32>(MaterialGraphBoxes::type))->HasConnection()) _treeLayer->UsageFlags |= MaterialUsageFlags::feature
#define EAT_BOX(type) eatMaterialGraphBox(value.Value, node->GetBox((int32)MaterialGraphBoxes::type), MaterialGraphBoxes::type)
switch (_treeType)
{
case MaterialTreeType::VertexShader:
{
EAT_BOX(PositionOffset);
EAT_BOX(TessellationMultiplier);
CHECK_MATERIAL_FEATURE(PositionOffset, UsePositionOffset);
break;
}
case MaterialTreeType::DomainShader:
{
EAT_BOX(WorldDisplacement);
CHECK_MATERIAL_FEATURE(WorldDisplacement, UseDisplacement);
break;
}
case MaterialTreeType::PixelShader:
{
EAT_BOX(Normal);
EAT_BOX(Color);
EAT_BOX(Metalness);
EAT_BOX(Specular);
EAT_BOX(Roughness);
EAT_BOX(AmbientOcclusion);
EAT_BOX(Opacity);
EAT_BOX(Refraction);
EAT_BOX(Mask);
EAT_BOX(Emissive);
EAT_BOX(SubsurfaceColor);
CHECK_MATERIAL_FEATURE(Emissive, UseEmissive);
CHECK_MATERIAL_FEATURE(Normal, UseNormal);
CHECK_MATERIAL_FEATURE(Mask, UseMask);
CHECK_MATERIAL_FEATURE(Refraction, UseRefraction);
break;
}
default:
break;
}
#undef CHECK_MATERIAL_FEATURE
#undef EAT_BOX
break;
}
}
}
#endif

View File

@@ -0,0 +1,414 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MATERIAL_GRAPH
#include "MaterialGenerator.h"
#include "Engine/Content/Assets/MaterialFunction.h"
void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value)
{
switch (node->TypeID)
{
// World Position
case 2:
value = Value(VariantType::Vector3, TEXT("input.WorldPosition.xyz"));
break;
// View
case 3:
{
switch (box->ID)
{
// Position
case 0:
value = Value(VariantType::Vector3, TEXT("ViewPos"));
break;
// Direction
case 1:
value = Value(VariantType::Vector3, TEXT("ViewDir"));
break;
// Far Plane
case 2:
value = Value(VariantType::Float, TEXT("ViewFar"));
break;
default: CRASH;
}
break;
}
// Normal
case 4:
value = getNormal;
break;
// Camera Vector
case 5:
value = getCameraVector(node);
break;
// Screen Position
case 6:
{
// Position
if (box->ID == 0)
value = Value(VariantType::Vector2, TEXT("input.SvPosition.xy"));
// Texcoord
else if (box->ID == 1)
value = writeLocal(VariantType::Vector2, TEXT("input.SvPosition.xy * ScreenSize.zw"), node);
break;
}
// Screen Size
case 7:
{
value = Value(VariantType::Vector2, box->ID == 0 ? TEXT("ScreenSize.xy") : TEXT("ScreenSize.zw"));
break;
}
// Custom code
case 8:
{
// Skip if has no code
if (((StringView)node->Values[0]).IsEmpty())
{
value = Value::Zero;
break;
}
const int32 InputsMax = 8;
const int32 OutputsMax = 4;
const int32 Input0BoxID = 0;
const int32 Output0BoxID = 8;
// Create output variables
Value values[OutputsMax];
for (int32 i = 0; i < OutputsMax; i++)
{
const auto outputBox = node->GetBox(Output0BoxID + i);
if (outputBox && outputBox->HasConnection())
{
values[i] = writeLocal(VariantType::Vector4, node);
}
}
// Process custom code (inject inputs and outputs)
String code;
code = (StringView)node->Values[0];
for (int32 i = 0; i < InputsMax; i++)
{
auto inputName = TEXT("Input") + StringUtils::ToString(i);
const auto inputBox = node->GetBox(Input0BoxID + i);
if (inputBox && inputBox->HasConnection())
{
auto inputValue = tryGetValue(inputBox, Value::Zero);
if (inputValue.Type != VariantType::Vector4)
inputValue = inputValue.Cast(VariantType::Vector4);
code.Replace(*inputName, *inputValue.Value, StringSearchCase::CaseSensitive);
}
}
for (int32 i = 0; i < OutputsMax; i++)
{
auto outputName = TEXT("Output") + StringUtils::ToString(i);
const auto outputBox = node->GetBox(Output0BoxID + i);
if (outputBox && outputBox->HasConnection())
{
code.Replace(*outputName, *values[i].Value, StringSearchCase::CaseSensitive);
}
}
// Write code
_writer.Write(TEXT("{\n"));
_writer.Write(*code);
_writer.Write(TEXT("}\n"));
// Link output values to boxes
for (int32 i = 0; i < OutputsMax; i++)
{
const auto outputBox = node->GetBox(Output0BoxID + i);
if (outputBox && outputBox->HasConnection())
{
outputBox->Cache = values[i];
}
}
value = box->Cache;
break;
}
// Object Position
case 9:
value = Value(VariantType::Vector3, TEXT("GetObjectPosition(input)"));
break;
// Two Sided Sign
case 10:
value = Value(VariantType::Float, TEXT("input.TwoSidedSign"));
break;
// Camera Depth Fade
case 11:
{
auto faeLength = tryGetValue(node->GetBox(0), node->Values[0]).AsFloat();
auto fadeOffset = tryGetValue(node->GetBox(1), node->Values[1]).AsFloat();
// TODO: for pixel shader it could calc PixelDepth = mul(float4(WorldPos.xyz, 1), ViewProjMatrix).w and use it
auto x1 = writeLocal(VariantType::Vector3, TEXT("ViewPos - input.WorldPosition"), node);
auto x2 = writeLocal(VariantType::Vector3, TEXT("TransformViewVectorToWorld(input, float3(0, 0, -1))"), node);
auto x3 = writeLocal(VariantType::Float, String::Format(TEXT("dot(normalize({0}), {1}) * length({0})"), x1.Value, x2.Value), node);
auto x4 = writeLocal(VariantType::Float, String::Format(TEXT("{0} - {1}"), x3.Value, fadeOffset.Value), node);
auto x5 = writeLocal(VariantType::Float, String::Format(TEXT("saturate({0} / {1})"), x4.Value, faeLength.Value), node);
value = x5;
break;
}
// Vertex Color
case 12:
value = getVertexColor;
_treeLayer->UsageFlags |= MaterialUsageFlags::UseVertexColor;
break;
// Pre-skinned Local Position
case 13:
value = _treeType == MaterialTreeType::VertexShader ? Value(VariantType::Vector3, TEXT("input.PreSkinnedPosition")) : Value::Zero;
break;
// Pre-skinned Local Normal
case 14:
value = _treeType == MaterialTreeType::VertexShader ? Value(VariantType::Vector3, TEXT("input.PreSkinnedNormal")) : Value::Zero;
break;
// Depth
case 15:
value = writeLocal(VariantType::Float, TEXT("distance(ViewPos, input.WorldPosition)"), node);
break;
// Tangent
case 16:
value = Value(VariantType::Vector3, TEXT("input.TBN[0]"));
break;
// Bitangent
case 17:
value = Value(VariantType::Vector3, TEXT("input.TBN[1]"));
break;
// Camera Position
case 18:
value = Value(VariantType::Vector3, TEXT("ViewPos"));
break;
// Per Instance Random
case 19:
value = Value(VariantType::Float, TEXT("GetPerInstanceRandom(input)"));
break;
// Interpolate VS To PS
case 20:
{
const auto input = node->GetBox(0);
// If used in VS then pass the value from the input box
if (_treeType == MaterialTreeType::VertexShader)
{
value = tryGetValue(input, Value::Zero).AsVector4();
break;
}
// Check if can use more interpolants
if (_vsToPsInterpolants.Count() == 16)
{
OnError(node, box, TEXT("Too many VS to PS interpolants used."));
value = Value::Zero;
break;
}
// Check if can use interpolants
const auto layer = GetRootLayer();
if (!layer || layer->Domain == MaterialDomain::Decal || layer->Domain == MaterialDomain::PostProcess)
{
OnError(node, box, TEXT("VS to PS interpolants are not supported in Decal or Post Process materials."));
value = Value::Zero;
break;
}
// Indicate the interpolator slot usage
value = Value(VariantType::Vector4, String::Format(TEXT("input.CustomVSToPS[{0}]"), _vsToPsInterpolants.Count()));
_vsToPsInterpolants.Add(input);
break;
}
// Terrain Holes Mask
case 21:
{
MaterialLayer* baseLayer = GetRootLayer();
if (baseLayer->Domain == MaterialDomain::Terrain)
value = Value(VariantType::Float, TEXT("input.HolesMask"));
else
value = Value::One;
break;
}
// Terrain Layer Weight
case 22:
{
MaterialLayer* baseLayer = GetRootLayer();
if (baseLayer->Domain != MaterialDomain::Terrain)
{
value = Value::One;
break;
}
const int32 layer = node->Values[0].AsInt;
if (layer < 0 || layer > 7)
{
value = Value::One;
OnError(node, box, TEXT("Invalid terrain layer index."));
break;
}
const int32 slotIndex = layer / 4;
const int32 componentIndex = layer % 4;
value = Value(VariantType::Float, String::Format(TEXT("input.Layers[{0}][{1}]"), slotIndex, componentIndex));
break;
}
// Depth Fade
case 23:
{
// Calculate screen-space UVs
auto screenUVs = writeLocal(VariantType::Vector2, TEXT("input.SvPosition.xy * ScreenSize.zw"), node);
// Sample scene depth buffer
auto sceneDepthTexture = findOrAddSceneTexture(MaterialSceneTextures::SceneDepth);
auto depthSample = writeLocal(VariantType::Float, String::Format(TEXT("{0}.SampleLevel(SamplerLinearClamp, {1}, 0).x"), sceneDepthTexture.ShaderName, screenUVs.Value), node);
// Linearize raw device depth
Value sceneDepth;
linearizeSceneDepth(node, depthSample, sceneDepth);
// Calculate pixel depth
auto posVS = writeLocal(VariantType::Float, TEXT("mul(float4(input.WorldPosition.xyz, 1), ViewMatrix).z"), node);
// Compute depth difference
auto depthDiff = writeLocal(VariantType::Float, String::Format(TEXT("{0} * ViewFar - {1}"), sceneDepth.Value, posVS.Value), node);
// Apply smoothing factor and clamp the result
value = writeLocal(VariantType::Float, String::Format(TEXT("saturate({0} / {1})"), depthDiff.Value, node->Values[0].AsFloat), node);
break;
}
// Material Function
case 24:
{
// Load function asset
const auto function = Assets.LoadAsync<MaterialFunction>((Guid)node->Values[0]);
if (!function || function->WaitForLoaded())
{
OnError(node, box, TEXT("Missing or invalid function."));
value = Value::Zero;
break;
}
#if 0
// Prevent recursive calls
for (int32 i = _callStack.Count() - 1; i >= 0; i--)
{
if (_callStack[i]->Type == GRAPH_NODE_MAKE_TYPE(1, 24))
{
const auto callFunc = Assets.LoadAsync<MaterialFunction>((Guid)_callStack[i]->Values[0]);
if (callFunc == function)
{
OnError(node, box, String::Format(TEXT("Recursive call to function '{0}'!"), function->ToString()));
value = Value::Zero;
return;
}
}
}
#endif
// Create a instanced version of the function graph
Graph* graph;
if (!_functions.TryGet(node, graph))
{
graph = New<MaterialGraph>();
function->LoadSurface((MaterialGraph&)*graph);
_functions.Add(node, graph);
}
// Peek the function output (function->Outputs maps the functions outputs to output nodes indices)
const int32 outputIndex = box->ID - 16;
if (outputIndex < 0 || outputIndex >= function->Outputs.Count())
{
OnError(node, box, TEXT("Invalid function output box."));
value = Value::Zero;
break;
}
Node* functionOutputNode = &graph->Nodes[function->Outputs[outputIndex]];
Box* functionOutputBox = functionOutputNode->TryGetBox(0);
// Evaluate the function output
_graphStack.Push(graph);
value = functionOutputBox && functionOutputBox->HasConnection() ? eatBox(node, functionOutputBox->FirstConnection()) : Value::Zero;
_graphStack.Pop();
break;
}
// Object Size
case 25:
value = Value(VariantType::Vector3, TEXT("GetObjectSize(input)"));
break;
default:
break;
}
}
void MaterialGenerator::ProcessGroupFunction(Box* box, Node* node, Value& value)
{
switch (node->TypeID)
{
// Function Input
case 1:
{
// Find the function call
Node* functionCallNode = nullptr;
ASSERT(_graphStack.Count() >= 2);
Graph* graph;
for (int32 i = _callStack.Count() - 1; i >= 0; i--)
{
if (_callStack[i]->Type == GRAPH_NODE_MAKE_TYPE(1, 24) && _functions.TryGet(_callStack[i], graph) && _graphStack[_graphStack.Count() - 1] == graph)
{
functionCallNode = _callStack[i];
break;
}
}
if (!functionCallNode)
{
OnError(node, box, TEXT("Missing calling function node."));
value = Value::Zero;
break;
}
const auto function = Assets.LoadAsync<MaterialFunction>((Guid)functionCallNode->Values[0]);
if (!_functions.TryGet(functionCallNode, graph) || !function)
{
OnError(node, box, TEXT("Missing calling function graph."));
value = Value::Zero;
break;
}
// Peek the input box to use
int32 inputIndex = -1;
for (int32 i = 0; i < function->Inputs.Count(); i++)
{
if (node->ID == graph->Nodes[function->Inputs[i]].ID)
{
inputIndex = i;
break;
}
}
if (inputIndex < 0 || inputIndex >= function->Inputs.Count())
{
OnError(node, box, TEXT("Invalid function input box."));
value = Value::Zero;
break;
}
Box* functionCallBox = functionCallNode->TryGetBox(inputIndex);
if (functionCallBox->HasConnection())
{
// Use provided input value from the function call
_graphStack.Pop();
value = eatBox(node, functionCallBox->FirstConnection());
_graphStack.Push(graph);
}
else
{
// Use the default value from the function graph
value = tryGetValue(node->TryGetBox(1), Value::Zero);
}
break;
}
default:
break;
}
}
#endif

View File

@@ -0,0 +1,96 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MATERIAL_GRAPH
#include "MaterialGenerator.h"
void MaterialGenerator::ProcessGroupParameters(Box* box, Node* node, Value& value)
{
switch (node->TypeID)
{
// Get
case 1:
{
// Get parameter
const auto param = findParam((Guid)node->Values[0], _treeLayer);
if (param)
{
switch (param->Type)
{
case MaterialParameterType::Bool:
value = Value(VariantType::Bool, param->ShaderName);
break;
case MaterialParameterType::Integer:
case MaterialParameterType::SceneTexture:
value = Value(VariantType::Int, param->ShaderName);
break;
case MaterialParameterType::Float:
value = Value(VariantType::Float, param->ShaderName);
break;
case MaterialParameterType::Vector2:
case MaterialParameterType::Vector3:
case MaterialParameterType::Vector4:
case MaterialParameterType::Color:
{
// Set result values based on box ID
const Value sample(box->Type.Type, param->ShaderName);
switch (box->ID)
{
case 0:
value = sample;
break;
case 1:
value.Value = sample.Value + _subs[0];
break;
case 2:
value.Value = sample.Value + _subs[1];
break;
case 3:
value.Value = sample.Value + _subs[2];
break;
case 4:
value.Value = sample.Value + _subs[3];
break;
default: CRASH;
break;
}
value.Type = box->Type.Type;
break;
}
case MaterialParameterType::Matrix:
{
value = Value(box->Type.Type, String::Format(TEXT("{0}[{1}]"), param->ShaderName, box->ID));
break;
}
case MaterialParameterType::ChannelMask:
{
const auto input = tryGetValue(node->GetBox(0), Value::Zero);
value = writeLocal(VariantType::Float, String::Format(TEXT("dot({0}, {1})"), input.Value, param->ShaderName), node);
break;
}
case MaterialParameterType::CubeTexture:
case MaterialParameterType::NormalMap:
case MaterialParameterType::Texture:
case MaterialParameterType::GPUTextureArray:
case MaterialParameterType::GPUTextureCube:
case MaterialParameterType::GPUTextureVolume:
case MaterialParameterType::GPUTexture:
sampleTexture(node, value, box, param);
break;
default: CRASH;
break;
}
}
else
{
OnError(node, box, String::Format(TEXT("Missing graph parameter {0}."), node->Values[0]));
value = Value::Zero;
}
break;
}
default:
break;
}
}
#endif

View File

@@ -0,0 +1,166 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MATERIAL_GRAPH
#include "MaterialGenerator.h"
MaterialValue MaterialGenerator::AccessParticleAttribute(Node* caller, const StringView& name, ParticleAttributeValueTypes valueType, const Char* index)
{
// TODO: cache the attribute value during material tree execution (eg. reuse the same Particle Color read for many nodes in graph)
String mappingName = TEXT("Particle.");
mappingName += name;
SerializedMaterialParam* attributeMapping = nullptr;
// Find if this attribute has been already accessed
for (int32 i = 0; i < _parameters.Count(); i++)
{
SerializedMaterialParam& param = _parameters[i];
if (!param.IsPublic && param.Type == MaterialParameterType::Integer && param.Name == mappingName)
{
// Reuse attribute mapping
attributeMapping = &param;
break;
}
}
if (!attributeMapping)
{
// Create
SerializedMaterialParam& param = _parameters.AddOne();
param.Type = MaterialParameterType::Integer;
param.IsPublic = false;
param.Override = true;
param.Name = mappingName;
param.ShaderName = getParamName(_parameters.Count());
param.AsInteger = 0;
param.ID = Guid::New();
attributeMapping = &param;
}
// Read particle data from the buffer
VariantType::Types type;
const Char* format;
switch (valueType)
{
case ParticleAttributeValueTypes::Float:
type = VariantType::Float;
format = TEXT("GetParticleFloat({1}, {0})");
break;
case ParticleAttributeValueTypes::Vector2:
type = VariantType::Vector2;
format = TEXT("GetParticleVec2({1}, {0})");
break;
case ParticleAttributeValueTypes::Vector3:
type = VariantType::Vector3;
format = TEXT("GetParticleVec3({1}, {0})");
break;
case ParticleAttributeValueTypes::Vector4:
type = VariantType::Vector4;
format = TEXT("GetParticleVec4({1}, {0})");
break;
case ParticleAttributeValueTypes::Int:
type = VariantType::Int;
format = TEXT("GetParticleInt({1}, {0})");
break;
case ParticleAttributeValueTypes::Uint:
type = VariantType::Uint;
format = TEXT("GetParticleUint({1}, {0})");
break;
default:
return MaterialValue::Zero;
}
return writeLocal(type, String::Format(format, attributeMapping->ShaderName, index ? index : TEXT("input.ParticleIndex")), caller);
}
void MaterialGenerator::ProcessGroupParticles(Box* box, Node* node, Value& value)
{
// Only particle shaders can access particles data
if (GetRootLayer()->Domain != MaterialDomain::Particle)
{
value = MaterialValue::Zero;
return;
}
switch (node->TypeID)
{
// Particle Attribute
case 100:
{
value = AccessParticleAttribute(node, (StringView)node->Values[0], static_cast<ParticleAttributeValueTypes>(node->Values[1].AsInt));
break;
}
// Particle Attribute (by index)
case 303:
{
const auto particleIndex = Value::Cast(tryGetValue(node->GetBox(1), Value(VariantType::Uint, TEXT("input.ParticleIndex"))), VariantType::Uint);
value = AccessParticleAttribute(node, (StringView)node->Values[0], static_cast<ParticleAttributeValueTypes>(node->Values[1].AsInt), particleIndex.Value.Get());
break;
}
// Particle Position
case 101:
{
value = AccessParticleAttribute(node, TEXT("Position"), ParticleAttributeValueTypes::Vector3);
break;
}
// Particle Lifetime
case 102:
{
value = AccessParticleAttribute(node, TEXT("Lifetime"), ParticleAttributeValueTypes::Float);
break;
}
// Particle Age
case 103:
{
value = AccessParticleAttribute(node, TEXT("Age"), ParticleAttributeValueTypes::Float);
break;
}
// Particle Color
case 104:
{
value = AccessParticleAttribute(node, TEXT("Color"), ParticleAttributeValueTypes::Vector4);
break;
}
// Particle Velocity
case 105:
{
value = AccessParticleAttribute(node, TEXT("Velocity"), ParticleAttributeValueTypes::Vector3);
break;
}
// Particle Sprite Size
case 106:
{
value = AccessParticleAttribute(node, TEXT("SpriteSize"), ParticleAttributeValueTypes::Vector2);
break;
}
// Particle Mass
case 107:
{
value = AccessParticleAttribute(node, TEXT("Mass"), ParticleAttributeValueTypes::Float);
break;
}
// Particle Rotation
case 108:
{
value = AccessParticleAttribute(node, TEXT("Rotation"), ParticleAttributeValueTypes::Vector3);
break;
}
// Particle Angular Velocity
case 109:
{
value = AccessParticleAttribute(node, TEXT("AngularVelocity"), ParticleAttributeValueTypes::Vector3);
break;
}
// Particle Normalized Age
case 110:
{
const auto age = AccessParticleAttribute(node, TEXT("Age"), ParticleAttributeValueTypes::Float);
const auto lifetime = AccessParticleAttribute(node, TEXT("Lifetime"), ParticleAttributeValueTypes::Float);
value = writeOperation2(node, age, lifetime, '/');
break;
}
default:
break;
}
}
#endif

View File

@@ -0,0 +1,189 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MATERIAL_GRAPH
#include "MaterialGenerator.h"
MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, Box* box, SerializedMaterialParam* texture)
{
ASSERT(texture && box);
// Cache data
const auto parent = box->GetParent<MaterialGraphNode>();
const bool isCubemap = texture->Type == MaterialParameterType::CubeTexture;
const bool isArray = texture->Type == MaterialParameterType::GPUTextureArray;
const bool isVolume = texture->Type == MaterialParameterType::GPUTextureVolume;
const bool isNormalMap = texture->Type == MaterialParameterType::NormalMap;
const bool canUseSample = CanUseSample(_treeType);
MaterialGraphBox* valueBox = parent->GetBox(1);
// Check if has variable assigned
if (texture->Type != MaterialParameterType::Texture
&& texture->Type != MaterialParameterType::NormalMap
&& texture->Type != MaterialParameterType::SceneTexture
&& texture->Type != MaterialParameterType::GPUTexture
&& texture->Type != MaterialParameterType::GPUTextureVolume
&& texture->Type != MaterialParameterType::GPUTextureCube
&& texture->Type != MaterialParameterType::GPUTextureArray
&& texture->Type != MaterialParameterType::CubeTexture)
{
OnError(caller, box, TEXT("No parameter for texture sample node."));
return nullptr;
}
// Check if it's 'Object' box that is using only texture object without sampling
if (box->ID == 6)
{
// Return texture object
value.Value = texture->ShaderName;
value.Type = VariantType::Object;
return nullptr;
}
// Check if hasn't been sampled during that tree eating
if (valueBox->Cache.IsInvalid())
{
// Check if use custom UVs
String uv;
MaterialGraphBox* uvBox = parent->GetBox(0);
bool useCustomUVs = uvBox->HasConnection();
bool use3dUVs = isCubemap || isArray || isVolume;
if (useCustomUVs)
{
// Get custom UVs
auto textureParamId = texture->ID;
ASSERT(textureParamId.IsValid());
MaterialValue v = tryGetValue(uvBox, getUVs);
uv = MaterialValue::Cast(v, use3dUVs ? VariantType::Vector3 : VariantType::Vector2).Value;
// Restore texture (during tryGetValue pointer could go invalid)
texture = findParam(textureParamId);
ASSERT(texture);
}
else
{
// Use default UVs
uv = use3dUVs ? TEXT("float3(input.TexCoord.xy, 0)") : TEXT("input.TexCoord.xy");
}
// Select sampler
// TODO: add option for texture groups and per texture options like wrap mode etc.
// TODO: changing texture sampler option
const Char* sampler = TEXT("SamplerLinearWrap");
// Sample texture
if (isNormalMap)
{
const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, 0).xyz");
// Sample encoded normal map
const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv);
const auto normalVector = writeLocal(VariantType::Vector3, sampledValue, parent);
// Decode normal vector
_writer.Write(TEXT("\t{0}.xy = {0}.xy * 2.0 - 1.0;\n"), normalVector.Value);
_writer.Write(TEXT("\t{0}.z = sqrt(saturate(1.0 - dot({0}.xy, {0}.xy)));\n"), normalVector.Value);
valueBox->Cache = normalVector;
}
else
{
// Select format string based on texture type
const Char* format;
/*if (isCubemap)
{
MISSING_CODE("sampling cube maps and 3d texture in material generator");
//format = TEXT("SAMPLE_CUBEMAP({0}, {1})");
}
else*/
{
/*if (useCustomUVs)
{
createGradients(writer, parent);
format = TEXT("SAMPLE_TEXTURE_GRAD({0}, {1}, {2}, {3})");
}
else*/
{
format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, 0)");
}
}
// Sample texture
String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, _ddx.Value, _ddy.Value);
valueBox->Cache = writeLocal(VariantType::Vector4, sampledValue, parent);
}
}
return &valueBox->Cache;
}
void MaterialGenerator::sampleTexture(Node* caller, Value& value, Box* box, SerializedMaterialParam* texture)
{
const auto sample = sampleTextureRaw(caller, value, box, texture);
if (sample == nullptr)
return;
// Set result values based on box ID
switch (box->ID)
{
case 1:
value = *sample;
break;
case 2:
value.Value = sample->Value + _subs[0];
break;
case 3:
value.Value = sample->Value + _subs[1];
break;
case 4:
value.Value = sample->Value + _subs[2];
break;
case 5:
value.Value = sample->Value + _subs[3];
break;
default: CRASH;
break;
}
value.Type = box->Type.Type;
}
void MaterialGenerator::sampleSceneDepth(Node* caller, Value& value, Box* box)
{
// Sample depth buffer
auto param = findOrAddSceneTexture(MaterialSceneTextures::SceneDepth);
const auto depthSample = sampleTextureRaw(caller, value, box, &param);
if (depthSample == nullptr)
return;
// Linearize raw device depth
linearizeSceneDepth(caller, *depthSample, value);
}
void MaterialGenerator::linearizeSceneDepth(Node* caller, const Value& depth, Value& value)
{
value = writeLocal(VariantType::Float, String::Format(TEXT("ViewInfo.w / ({0}.x - ViewInfo.z)"), depth.Value), caller);
}
byte MaterialGenerator::getStartSrvRegister(MaterialLayer* baseLayer)
{
// Note: this must match material templates
switch (baseLayer->Domain)
{
case MaterialDomain::Surface:
return baseLayer->BlendMode == MaterialBlendMode::Transparent ? 3 : 3;
case MaterialDomain::PostProcess:
return 0;
case MaterialDomain::Decal:
return 1;
case MaterialDomain::GUI:
return 0;
case MaterialDomain::Terrain:
return 6;
case MaterialDomain::Particle:
return 5;
default:
CRASH;
return 0;
}
}
#endif

View File

@@ -0,0 +1,417 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MATERIAL_GRAPH
#include "MaterialGenerator.h"
void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
{
switch (node->TypeID)
{
// Texture
case 1:
{
// Check if texture has been selected
Guid textureId = (Guid)node->Values[0];
if (textureId.IsValid())
{
// Get or create parameter for that texture
auto param = findOrAddTexture(textureId);
// Sample texture
sampleTexture(node, value, box, &param);
}
else
{
// Use default value
value = Value::Zero;
}
break;
}
// TexCoord
case 2:
value = getUVs;
break;
// Cube Texture
case 3:
{
// Check if texture has been selected
Guid textureId = (Guid)node->Values[0];
if (textureId.IsValid())
{
// Get or create parameter for that cube texture
auto param = findOrAddCubeTexture(textureId);
// Sample texture
sampleTexture(node, value, box, &param);
}
else
{
// Use default value
value = Value::Zero;
}
break;
}
// Normal Map
case 4:
{
// Check if texture has been selected
Guid textureId = (Guid)node->Values[0];
if (textureId.IsValid())
{
// Get or create parameter for that texture
auto param = findOrAddNormalMap(textureId);
// Sample texture
sampleTexture(node, value, box, &param);
}
else
{
// Use default value
value = Value::Zero;
}
break;
}
// Parallax Occlusion Mapping
case 5:
{
auto heightTextureBox = node->GetBox(4);
if (!heightTextureBox->HasConnection())
{
value = Value::Zero;
// TODO: handle missing texture error
//OnError("No Variable Entry for height texture.", node);
break;
}
auto heightTexture = eatBox(heightTextureBox->GetParent<Node>(), heightTextureBox->FirstConnection());
if (heightTexture.Type != VariantType::Object)
{
value = Value::Zero;
// TODO: handle invalid connection data error
//OnError("No Variable Entry for height texture.", node);
break;
}
Value uvs = tryGetValue(node->GetBox(0), getUVs).AsVector2();
if (_treeType != MaterialTreeType::PixelShader)
{
// Required ddx/ddy instructions are only supported in Pixel Shader
value = uvs;
break;
}
Value scale = tryGetValue(node->GetBox(1), node->Values[0]);
Value minSteps = tryGetValue(node->GetBox(2), node->Values[1]);
Value maxSteps = tryGetValue(node->GetBox(3), node->Values[2]);
Value result = writeLocal(VariantType::Vector2, uvs.Value, node);
createGradients(node);
ASSERT(node->Values[3].Type == VariantType::Int && Math::IsInRange(node->Values[3].AsInt, 0, 3));
auto channel = _subs[node->Values[3].AsInt];
Value cameraVectorWS = getCameraVector(node);
Value cameraVectorTS = writeLocal(VariantType::Vector3, String::Format(TEXT("TransformWorldVectorToTangent(input, {0})"), cameraVectorWS.Value), node);
auto code = String::Format(TEXT(
" {{\n"
" float vLength = length({8}.rg);\n"
" float coeff0 = vLength / {8}.b;\n"
" float coeff1 = coeff0 * (-({4}));\n"
" float2 vNorm = {8}.rg / vLength;\n"
" float2 maxOffset = (vNorm * coeff1);\n"
" float numSamples = lerp({0}, {3}, saturate(dot({9}, input.TBN[2])));\n"
" float stepSize = 1.0 / numSamples;\n"
" float2 currOffset = 0;\n"
" float2 lastOffset = 0;\n"
" float currRayHeight = 1.0;\n"
" float lastSampledHeight = 1;\n"
" int currSample = 0;\n"
" while (currSample < (int)numSamples)\n"
" {{\n"
" float currSampledHeight = {1}.SampleGrad(SamplerLinearWrap, {10} + currOffset, {5}, {6}){7};\n"
" if (currSampledHeight > currRayHeight)\n"
" {{\n"
" float delta1 = currSampledHeight - currRayHeight;\n"
" float delta2 = (currRayHeight + stepSize) - lastSampledHeight;\n"
" float ratio = delta1 / max(delta1 + delta2, 0.00001f);\n"
" currOffset = ratio * lastOffset + (1.0 - ratio) * currOffset;\n"
" break;\n"
" }}\n"
" currRayHeight -= stepSize;\n"
" lastOffset = currOffset;\n"
" currOffset += stepSize * maxOffset;\n"
" lastSampledHeight = currSampledHeight;\n"
" currSample++;\n"
" }}\n"
" {2} = {10} + currOffset;\n"
" }}\n"
),
minSteps.Value, // {0}
heightTexture.Value, // {1}
result.Value, // {2}
maxSteps.Value, // {3}
scale.Value, // {4}
_ddx.Value, // {5}
_ddy.Value, // {6}
channel, // {7}
cameraVectorTS.Value, // {8}
cameraVectorWS.Value, // {9}
uvs.Value // {10}
);
_writer.Write(*code);
value = result;
break;
}
// Scene Texture
case 6:
{
// Get texture type
auto type = (MaterialSceneTextures)(int32)node->Values[0];
// Some types need more logic
switch (type)
{
case MaterialSceneTextures::SceneDepth:
{
sampleSceneDepth(node, value, box);
break;
}
case MaterialSceneTextures::DiffuseColor:
{
auto gBuffer0Param = findOrAddSceneTexture(MaterialSceneTextures::BaseColor);
auto gBuffer2Param = findOrAddSceneTexture(MaterialSceneTextures::Metalness);
auto gBuffer0Sample = sampleTextureRaw(node, value, box, &gBuffer0Param);
if (gBuffer0Sample == nullptr)
break;
auto gBuffer2Sample = sampleTextureRaw(node, value, box, &gBuffer2Param);
if (gBuffer2Sample == nullptr)
break;
value = writeLocal(VariantType::Vector3, String::Format(TEXT("GetDiffuseColor({0}.rgb, {1}.g)"), gBuffer0Sample->Value, gBuffer2Sample->Value), node);
break;
}
case MaterialSceneTextures::SpecularColor:
{
auto gBuffer0Param = findOrAddSceneTexture(MaterialSceneTextures::BaseColor);
auto gBuffer2Param = findOrAddSceneTexture(MaterialSceneTextures::Metalness);
auto gBuffer0Sample = sampleTextureRaw(node, value, box, &gBuffer0Param);
if (gBuffer0Sample == nullptr)
break;
auto gBuffer2Sample = sampleTextureRaw(node, value, box, &gBuffer2Param);
if (gBuffer2Sample == nullptr)
break;
value = writeLocal(VariantType::Vector3, String::Format(TEXT("GetSpecularColor({0}.rgb, {1}.b, {1}.g)"), gBuffer0Sample->Value, gBuffer2Sample->Value), node);
break;
}
case MaterialSceneTextures::WorldNormal:
{
auto gBuffer1Param = findOrAddSceneTexture(MaterialSceneTextures::WorldNormal);
auto gBuffer1Sample = sampleTextureRaw(node, value, box, &gBuffer1Param);
if (gBuffer1Sample == nullptr)
break;
value = writeLocal(VariantType::Vector3, String::Format(TEXT("DecodeNormal({0}.rgb)"), gBuffer1Sample->Value), node);
break;
}
case MaterialSceneTextures::AmbientOcclusion:
{
auto gBuffer2Param = findOrAddSceneTexture(MaterialSceneTextures::AmbientOcclusion);
auto gBuffer2Sample = sampleTextureRaw(node, value, box, &gBuffer2Param);
if (gBuffer2Sample == nullptr)
break;
value = writeLocal(VariantType::Float, String::Format(TEXT("{0}.a"), gBuffer2Sample->Value), node);
break;
}
case MaterialSceneTextures::Metalness:
{
auto gBuffer2Param = findOrAddSceneTexture(MaterialSceneTextures::Metalness);
auto gBuffer2Sample = sampleTextureRaw(node, value, box, &gBuffer2Param);
if (gBuffer2Sample == nullptr)
break;
value = writeLocal(VariantType::Float, String::Format(TEXT("{0}.g"), gBuffer2Sample->Value), node);
break;
}
case MaterialSceneTextures::Roughness:
{
auto gBuffer0Param = findOrAddSceneTexture(MaterialSceneTextures::Roughness);
auto gBuffer0Sample = sampleTextureRaw(node, value, box, &gBuffer0Param);
if (gBuffer0Sample == nullptr)
break;
value = writeLocal(VariantType::Float, String::Format(TEXT("{0}.r"), gBuffer0Sample->Value), node);
break;
}
case MaterialSceneTextures::Specular:
{
auto gBuffer2Param = findOrAddSceneTexture(MaterialSceneTextures::Specular);
auto gBuffer2Sample = sampleTextureRaw(node, value, box, &gBuffer2Param);
if (gBuffer2Sample == nullptr)
break;
value = writeLocal(VariantType::Float, String::Format(TEXT("{0}.b"), gBuffer2Sample->Value), node);
break;
}
case MaterialSceneTextures::ShadingModel:
{
auto gBuffer1Param = findOrAddSceneTexture(MaterialSceneTextures::WorldNormal);
auto gBuffer1Sample = sampleTextureRaw(node, value, box, &gBuffer1Param);
if (gBuffer1Sample == nullptr)
break;
value = writeLocal(VariantType::Int, String::Format(TEXT("(int)({0}.a * 3.999)"), gBuffer1Sample->Value), node);
break;
}
default:
{
// Sample single texture
auto param = findOrAddSceneTexture(type);
sampleTexture(node, value, box, &param);
break;
}
}
break;
}
// Scene Color
case 7:
{
// Sample scene color texture
auto param = findOrAddSceneTexture(MaterialSceneTextures::SceneColor);
sampleTexture(node, value, box, &param);
break;
}
// Scene Depth
case 8:
{
sampleSceneDepth(node, value, box);
break;
}
// Sample Texture
case 9:
{
enum CommonSamplerType
{
LinearClamp = 0,
PointClamp = 1,
LinearWrap = 2,
PointWrap = 3,
};
const Char* SamplerNames[]
{
TEXT("SamplerLinearClamp"),
TEXT("SamplerPointClamp"),
TEXT("SamplerLinearWrap"),
TEXT("SamplerPointWrap"),
};
// Get input boxes
auto textureBox = node->GetBox(0);
auto uvsBox = node->GetBox(1);
auto levelBox = node->GetBox(2);
auto offsetBox = node->GetBox(3);
if (!textureBox->HasConnection())
{
// No texture to sample
value = Value::Zero;
break;
}
const bool canUseSample = CanUseSample(_treeType);
const auto texture = eatBox(textureBox->GetParent<Node>(), textureBox->FirstConnection());
// Get UVs
Value uvs;
const bool useCustomUVs = uvsBox->HasConnection();
if (useCustomUVs)
{
// Get custom UVs
uvs = eatBox(uvsBox->GetParent<Node>(), uvsBox->FirstConnection());
}
else
{
// Use default UVs
uvs = getUVs;
}
const auto textureParam = findParam(texture.Value);
if (!textureParam)
{
// Missing texture
value = Value::Zero;
break;
}
const bool isCubemap = textureParam->Type == MaterialParameterType::CubeTexture || textureParam->Type == MaterialParameterType::GPUTextureCube;
const bool isArray = textureParam->Type == MaterialParameterType::GPUTextureArray;
const bool isVolume = textureParam->Type == MaterialParameterType::GPUTextureVolume;
const bool isNormalMap = textureParam->Type == MaterialParameterType::NormalMap;
const bool use3dUVs = isCubemap || isArray || isVolume;
uvs = Value::Cast(uvs, use3dUVs ? VariantType::Vector3 : VariantType::Vector2);
// Get other inputs
const auto level = tryGetValue(levelBox, node->Values[1]);
const bool useLevel = levelBox->HasConnection() || (int32)node->Values[1] != -1;
const bool useOffset = offsetBox->HasConnection();
const auto offset = useOffset ? eatBox(offsetBox->GetParent<Node>(), offsetBox->FirstConnection()) : Value::Zero;
const int32 samplerIndex = node->Values[0].AsInt;
if (samplerIndex < 0 || samplerIndex >= ARRAY_COUNT(SamplerNames))
{
OnError(node, box, TEXT("Invalid texture sampler."));
return;
}
// Pick a property format string
const Char* format;
if (useLevel || !canUseSample)
{
if (useOffset)
format = TEXT("{0}.SampleLevel({1}, {2}, {3}, {4})");
else
format = TEXT("{0}.SampleLevel({1}, {2}, {3})");
}
else
{
if (useOffset)
format = TEXT("{0}.Sample({1}, {2}, {4})");
else
format = TEXT("{0}.Sample({1}, {2})");
}
// Sample texture
const String sampledValue = String::Format(format,
texture.Value, // {0}
SamplerNames[samplerIndex], // {1}
uvs.Value, // {2}
level.Value, // {3}
offset.Value // {4}
);
textureBox->Cache = writeLocal(VariantType::Vector4, sampledValue, node);
// Decode normal map vector
if (isNormalMap)
{
// TODO: maybe we could use helper function for UnpackNormalTexture() and unify unpacking?
_writer.Write(TEXT("\t{0}.xy = {0}.xy * 2.0 - 1.0;\n"), textureBox->Cache.Value);
_writer.Write(TEXT("\t{0}.z = sqrt(saturate(1.0 - dot({0}.xy, {0}.xy)));\n"), textureBox->Cache.Value);
}
value = textureBox->Cache;
break;
}
// Flipbook
case 10:
{
// Get input values
auto uv = Value::Cast(tryGetValue(node->GetBox(0), getUVs), VariantType::Vector2);
auto frame = Value::Cast(tryGetValue(node->GetBox(1), node->Values[0]), VariantType::Float);
auto framesXY = Value::Cast(tryGetValue(node->GetBox(2), node->Values[1]), VariantType::Vector2);
auto invertX = Value::Cast(tryGetValue(node->GetBox(3), node->Values[2]), VariantType::Float);
auto invertY = Value::Cast(tryGetValue(node->GetBox(4), node->Values[3]), VariantType::Float);
// Write operations
auto framesCount = writeLocal(VariantType::Float, String::Format(TEXT("{0}.x * {1}.y"), framesXY.Value, framesXY.Value), node);
frame = writeLocal(VariantType::Float, String::Format(TEXT("fmod(floor({0}), {1})"), frame.Value, framesCount.Value), node);
auto framesXYInv = writeOperation2(node, Value::One, framesXY, '/');
auto frameY = writeLocal(VariantType::Float, String::Format(TEXT("abs({0} * {1}.y - (floor({2} * {3}.x) + {0} * 1))"), invertY.Value, framesXY.Value, frame.Value, framesXYInv.Value), node);
auto frameX = writeLocal(VariantType::Float, String::Format(TEXT("abs({0} * {1}.x - (({2} - {1}.x * floor({2} * {3}.x)) + {0} * 1))"), invertX.Value, framesXY.Value, frame.Value, framesXYInv.Value), node);
value = writeLocal(VariantType::Vector2, String::Format(TEXT("({3} + float2({0}, {1})) * {2}"), frameX.Value, frameY.Value, framesXYInv.Value, uv.Value), node);
break;
}
default:
break;
}
}
#endif

View File

@@ -0,0 +1,87 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MATERIAL_GRAPH
#include "MaterialGenerator.h"
void MaterialGenerator::ProcessGroupTools(Box* box, Node* node, Value& value)
{
switch (node->TypeID)
{
// Fresnel
case 1:
case 4:
{
// Gets constants
auto cameraVector = getCameraVector(node);
// Get inputs
Value exponent = tryGetValue(node->GetBox(0), 0, Value(5.0f)).AsFloat();
Value fraction = tryGetValue(node->GetBox(1), 1, Value(0.04f)).AsFloat();
Value normal = tryGetValue(node->GetBox(2), getNormal).AsVector3();
// Write operations
auto local1 = writeFunction2(node, normal, cameraVector, TEXT("dot"), VariantType::Float);
auto local2 = writeFunction2(node, Value::Zero, local1, TEXT("max"));
auto local3 = writeOperation2(node, Value::One, local2, '-');
auto local4 = writeFunction2(node, local3, exponent, TEXT("ClampedPow"), VariantType::Float);
auto local5 = writeLocal(VariantType::Float, String::Format(TEXT("{0} * (1.0 - {1})"), local4.Value, fraction.Value), node);
auto local6 = writeOperation2(node, local5, fraction, '+');
_includes.Add(TEXT("./Flax/Math.hlsl"));
// Gets value
value = local6;
break;
}
// Desaturation
case 2:
{
// Get inputs
Value input = tryGetValue(node->GetBox(0), Value::Zero).AsVector3();
Value scale = tryGetValue(node->GetBox(1), Value::Zero).AsFloat();
Value luminanceFactors = Value(node->Values[0].AsVector3());
// Write operations
auto dot = writeFunction2(node, input, luminanceFactors, TEXT("dot"), VariantType::Float);
value = writeFunction3(node, input, dot, scale, TEXT("lerp"), VariantType::Vector3);
break;
}
// Time
case 3:
{
value = getTime;
break;
}
// Panner
case 6:
{
// Get inputs
const Value uv = tryGetValue(node->GetBox(0), getUVs).AsVector2();
const Value time = tryGetValue(node->GetBox(1), getTime).AsFloat();
const Value speed = tryGetValue(node->GetBox(2), Value::One).AsVector2();
const bool useFractionalPart = (bool)node->Values[0];
// Write operations
auto local1 = writeOperation2(node, speed, time, '*');
if (useFractionalPart)
local1 = writeFunction1(node, local1, TEXT("frac"));
value = writeOperation2(node, uv, local1, '+');
break;
}
// Linearize Depth
case 7:
{
// Get input
const Value depth = tryGetValue(node->GetBox(0), Value::Zero).AsFloat();
// Linearize raw device depth
linearizeSceneDepth(node, depth, value);
break;
}
default:
ShaderGenerator::ProcessGroupTools(box, node, value);
break;
}
}
#endif

View File

@@ -0,0 +1,559 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MATERIAL_GRAPH
#include "MaterialGenerator.h"
#include "Engine/Visject/ShaderGraphUtilities.h"
#include "Engine/Graphics/Materials/MaterialShader.h"
/// <summary>
/// Material shader source code template has special marks for generated code.
/// Each starts with '@' char and index of the mapped string.
/// </summary>
enum MaterialTemplateInputsMapping
{
In_VersionNumber = 0,
In_Constants = 1,
In_ShaderResources = 2,
In_Defines = 3,
In_GetMaterialPS = 4,
In_GetMaterialVS = 5,
In_GetMaterialDS = 6,
In_Includes = 7,
In_MAX
};
MaterialValue MaterialGenerator::getUVs(VariantType::Vector2, TEXT("input.TexCoord"));
MaterialValue MaterialGenerator::getTime(VariantType::Float, TEXT("TimeParam"));
MaterialValue MaterialGenerator::getNormal(VariantType::Vector3, TEXT("input.TBN[2]"));
MaterialValue MaterialGenerator::getVertexColor(VariantType::Vector4, TEXT("GetVertexColor(input)"));
MaterialGenerator::MaterialGenerator()
{
// Register per group type processing events
// Note: index must match group id
_perGroupProcessCall[1].Bind<MaterialGenerator, &MaterialGenerator::ProcessGroupMaterial>(this);
_perGroupProcessCall[3].Bind<MaterialGenerator, &MaterialGenerator::ProcessGroupMath>(this);
_perGroupProcessCall[5].Bind<MaterialGenerator, &MaterialGenerator::ProcessGroupTextures>(this);
_perGroupProcessCall[6].Bind<MaterialGenerator, &MaterialGenerator::ProcessGroupParameters>(this);
_perGroupProcessCall[7].Bind<MaterialGenerator, &MaterialGenerator::ProcessGroupTools>(this);
_perGroupProcessCall[8].Bind<MaterialGenerator, &MaterialGenerator::ProcessGroupLayers>(this);
_perGroupProcessCall[14].Bind<MaterialGenerator, &MaterialGenerator::ProcessGroupParticles>(this);
_perGroupProcessCall[16].Bind<MaterialGenerator, &MaterialGenerator::ProcessGroupFunction>(this);
}
MaterialGenerator::~MaterialGenerator()
{
_layers.ClearDelete();
}
bool MaterialGenerator::Generate(WriteStream& source, MaterialInfo& materialInfo, BytesContainer& parametersData)
{
ASSERT_LOW_LAYER(_layers.Count() > 0);
String inputs[In_MAX];
// Setup and prepare layers
_writer.Clear();
_includes.Clear();
_callStack.Clear();
_parameters.Clear();
_localIndex = 0;
_vsToPsInterpolants.Clear();
_treeLayer = nullptr;
_graphStack.Clear();
for (int32 i = 0; i < _layers.Count(); i++)
{
auto layer = _layers[i];
// Prepare
layer->Prepare();
prepareLayer(_layers[i], true);
// Assign layer variable name for initial layers
layer->Usage[0].VarName = TEXT("material");
if (i != 0)
layer->Usage[0].VarName += StringUtils::ToString(i);
}
inputs[In_VersionNumber] = StringUtils::ToString(MATERIAL_GRAPH_VERSION);
// Cache data
MaterialLayer* baseLayer = GetRootLayer();
MaterialGraphNode* baseNode = baseLayer->Root;
_treeLayerVarName = baseLayer->GetVariableName(nullptr);
_treeLayer = baseLayer;
_graphStack.Add(&_treeLayer->Graph);
const MaterialGraphBox* layerInputBox = baseLayer->Root->GetBox(0);
const bool isLayered = layerInputBox->HasConnection();
// Check if material is using special features and update the metadata flags
if (!isLayered)
{
baseLayer->UpdateFeaturesFlags();
}
// Pixel Shader
_treeType = MaterialTreeType::PixelShader;
Value materialVarPS;
if (isLayered)
{
materialVarPS = eatBox(baseNode, layerInputBox->FirstConnection());
}
else
{
materialVarPS = Value(VariantType::Void, baseLayer->GetVariableName(nullptr));
_writer.Write(TEXT("\tMaterial {0} = (Material)0;\n"), materialVarPS.Value);
if (baseLayer->Domain == MaterialDomain::Surface || baseLayer->Domain == MaterialDomain::Terrain || baseLayer->Domain == MaterialDomain::Particle)
{
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Emissive);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Normal);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Color);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Metalness);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Specular);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::AmbientOcclusion);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Roughness);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Opacity);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Refraction);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::SubsurfaceColor);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Mask);
}
else if (baseLayer->Domain == MaterialDomain::Decal)
{
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Emissive);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Normal);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Color);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Metalness);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Specular);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Roughness);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Opacity);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Mask);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::AmbientOcclusion);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Refraction);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::SubsurfaceColor);
}
else if (baseLayer->Domain == MaterialDomain::PostProcess)
{
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Emissive);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Opacity);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Normal);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Color);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Metalness);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Specular);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::AmbientOcclusion);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Roughness);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Refraction);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Mask);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::SubsurfaceColor);
}
else if (baseLayer->Domain == MaterialDomain::GUI)
{
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Emissive);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Opacity);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Mask);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Normal);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Color);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Metalness);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Specular);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::AmbientOcclusion);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Roughness);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Refraction);
eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::SubsurfaceColor);
}
else
{
CRASH;
}
}
{
// Flip normal for inverted triangles (used by two sided materials)
_writer.Write(TEXT("\t{0}.TangentNormal *= input.TwoSidedSign;\n"), materialVarPS.Value);
// Normalize and transform to world space if need to
_writer.Write(TEXT("\t{0}.TangentNormal = normalize({0}.TangentNormal);\n"), materialVarPS.Value);
if ((baseLayer->FeaturesFlags & MaterialFeaturesFlags::InputWorldSpaceNormal) == 0)
{
_writer.Write(TEXT("\t{0}.WorldNormal = normalize(TransformTangentVectorToWorld(input, {0}.TangentNormal));\n"), materialVarPS.Value);
}
else
{
_writer.Write(TEXT("\t{0}.WorldNormal = {0}.TangentNormal;\n"), materialVarPS.Value);
_writer.Write(TEXT("\t{0}.TangentNormal = normalize(TransformWorldVectorToTangent(input, {0}.WorldNormal));\n"), materialVarPS.Value);
}
// Clamp values
_writer.Write(TEXT("\t{0}.Metalness = saturate({0}.Metalness);\n"), materialVarPS.Value);
_writer.Write(TEXT("\t{0}.Roughness = max(0.04, {0}.Roughness);\n"), materialVarPS.Value);
_writer.Write(TEXT("\t{0}.AO = saturate({0}.AO);\n"), materialVarPS.Value);
_writer.Write(TEXT("\t{0}.Opacity = saturate({0}.Opacity);\n"), materialVarPS.Value);
// Return result
_writer.Write(TEXT("\treturn {0};"), materialVarPS.Value);
}
inputs[In_GetMaterialPS] = _writer.ToString();
_writer.Clear();
clearCache();
// Domain Shader
_treeType = MaterialTreeType::DomainShader;
if (isLayered)
{
const Value layer = eatBox(baseNode, layerInputBox->FirstConnection());
_writer.Write(TEXT("\treturn {0};"), layer.Value);
}
else
{
_writer.Write(TEXT("\tMaterial material = (Material)0;\n"));
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::WorldDisplacement);
_writer.Write(TEXT("\treturn material;"));
}
inputs[In_GetMaterialDS] = _writer.ToString();
_writer.Clear();
clearCache();
// Vertex Shader
_treeType = MaterialTreeType::VertexShader;
if (isLayered)
{
const Value layer = eatBox(baseNode, layerInputBox->FirstConnection());
_writer.Write(TEXT("\treturn {0};"), layer.Value);
}
else
{
_writer.Write(TEXT("\tMaterial material = (Material)0;\n"));
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::PositionOffset);
eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::TessellationMultiplier);
for (int32 i = 0; i < _vsToPsInterpolants.Count(); i++)
{
const auto value = tryGetValue(_vsToPsInterpolants[i], Value::Zero).AsVector4().Value;
_writer.Write(TEXT("\tmaterial.CustomVSToPS[{0}] = {1};\n"), i, value);
}
_writer.Write(TEXT("\treturn material;"));
}
inputs[In_GetMaterialVS] = _writer.ToString();
_writer.Clear();
clearCache();
// Update material usage based on material generator outputs
materialInfo.UsageFlags = baseLayer->UsageFlags;
// Defines
{
_writer.Write(TEXT("#define MATERIAL_MASK_THRESHOLD ({0})\n"), baseLayer->MaskThreshold);
_writer.Write(TEXT("#define CUSTOM_VERTEX_INTERPOLATORS_COUNT ({0})\n"), _vsToPsInterpolants.Count());
_writer.Write(TEXT("#define MATERIAL_OPACITY_THRESHOLD ({0})"), baseLayer->OpacityThreshold);
inputs[In_Defines] = _writer.ToString();
_writer.Clear();
}
// Includes
{
for (auto& include : _includes)
{
_writer.Write(TEXT("#include \"{0}\"\n"), include.Item);
}
inputs[In_Includes] = _writer.ToString();
_writer.Clear();
}
// Check if material is using any parameters
if (_parameters.HasItems())
{
ShaderGraphUtilities::GenerateShaderConstantBuffer(_writer, _parameters);
inputs[In_Constants] = _writer.ToString();
_writer.Clear();
const int32 startRegister = getStartSrvRegister(baseLayer);
const auto error = ShaderGraphUtilities::GenerateShaderResources(_writer, _parameters, startRegister);
if (error)
{
OnError(nullptr, nullptr, error);
return true;
}
inputs[In_ShaderResources] = _writer.ToString();
_writer.Clear();
}
// Save material parameters data
if (_parameters.HasItems())
MaterialParams::Save(parametersData, &_parameters);
else
parametersData.Release();
_parameters.Clear();
// Create source code
{
// Open template file
String path = Globals::EngineContentFolder / TEXT("Editor/MaterialTemplates/");
switch (materialInfo.Domain)
{
case MaterialDomain::Surface:
if (materialInfo.BlendMode == MaterialBlendMode::Opaque)
path /= TEXT("SurfaceDeferred.shader");
else
path /= TEXT("SurfaceForward.shader");
break;
case MaterialDomain::PostProcess:
path /= TEXT("PostProcess.shader");
break;
case MaterialDomain::GUI:
path /= TEXT("GUI.shader");
break;
case MaterialDomain::Decal:
path /= TEXT("Decal.shader");
break;
case MaterialDomain::Terrain:
path /= TEXT("Terrain.shader");
break;
case MaterialDomain::Particle:
path /= TEXT("Particle.shader");
break;
default:
LOG(Warning, "Unknown material domain.");
return true;
}
auto file = FileReadStream::Open(path);
if (file == nullptr)
{
LOG(Warning, "Cannot load material base source code.");
return true;
}
// Format template
uint32 length = file->GetLength();
Array<char> tmp;
for (uint32 i = 0; i < length; i++)
{
char c = file->ReadByte();
if (c != '@')
{
source.WriteByte(c);
}
else
{
i++;
int32 inIndex = file->ReadByte() - '0';
ASSERT_LOW_LAYER(Math::IsInRange(inIndex, 0, In_MAX - 1));
const String& in = inputs[inIndex];
if (in.Length() > 0)
{
tmp.EnsureCapacity(in.Length() + 1, false);
StringUtils::ConvertUTF162ANSI(*in, tmp.Get(), in.Length());
source.WriteBytes(tmp.Get(), in.Length());
}
}
}
// Close file
Delete(file);
// Ensure to have null-terminated source code
source.WriteByte(0);
}
return false;
}
void MaterialGenerator::clearCache()
{
for (int32 i = 0; i < _layers.Count(); i++)
_layers[i]->ClearCache();
for (auto& e : _functions)
{
for (auto& node : e.Value->Nodes)
{
for (int32 j = 0; j < node.Boxes.Count(); j++)
node.Boxes[j].Cache.Clear();
}
}
_ddx = Value();
_ddy = Value();
_cameraVector = Value();
}
void MaterialGenerator::writeBlending(MaterialGraphBoxes box, Value& result, const Value& bottom, const Value& top, const Value& alpha)
{
const auto& boxInfo = GetMaterialRootNodeBox(box);
_writer.Write(TEXT("\t{0}.{1} = lerp({2}.{1}, {3}.{1}, {4});\n"), result.Value, boxInfo.SubName, bottom.Value, top.Value, alpha.Value);
if (box == MaterialGraphBoxes::Normal)
{
_writer.Write(TEXT("\t{0}.{1} = normalize({0}.{1});\n"), result.Value, boxInfo.SubName);
}
}
SerializedMaterialParam* MaterialGenerator::findParam(const Guid& id, MaterialLayer* layer)
{
// Use per material layer params mapping
return findParam(layer->GetMappedParamId(id));
}
MaterialGraphParameter* MaterialGenerator::findGraphParam(const Guid& id)
{
MaterialGraphParameter* result = nullptr;
for (int32 i = 0; i < _layers.Count(); i++)
{
result = _layers[i]->Graph.GetParameter(id);
if (result)
break;
}
return result;
}
void MaterialGenerator::createGradients(Node* caller)
{
if (_ddx.IsInvalid())
_ddx = writeLocal(VariantType::Vector2, TEXT("ddx(input.TexCoord.xy)"), caller);
if (_ddy.IsInvalid())
_ddy = writeLocal(VariantType::Vector2, TEXT("ddy(input.TexCoord.xy)"), caller);
}
MaterialGenerator::Value MaterialGenerator::getCameraVector(Node* caller)
{
if (_cameraVector.IsInvalid())
_cameraVector = writeLocal(VariantType::Vector3, TEXT("normalize(ViewPos.xyz - input.WorldPosition.xyz)"), caller);
return _cameraVector;
}
void MaterialGenerator::eatMaterialGraphBox(String& layerVarName, MaterialGraphBox* nodeBox, MaterialGraphBoxes box)
{
// Cache data
auto& boxInfo = GetMaterialRootNodeBox(box);
// Get value
const auto value = Value::Cast(tryGetValue(nodeBox, boxInfo.DefaultValue), boxInfo.DefaultValue.Type);
// Write formatted value
_writer.WriteLine(TEXT("\t{0}.{1} = {2};"), layerVarName, boxInfo.SubName, value.Value);
}
void MaterialGenerator::eatMaterialGraphBox(MaterialLayer* layer, MaterialGraphBoxes box)
{
auto& boxInfo = GetMaterialRootNodeBox(box);
const auto nodeBox = layer->Root->GetBox(boxInfo.ID);
eatMaterialGraphBox(_treeLayerVarName, nodeBox, box);
}
void MaterialGenerator::eatMaterialGraphBoxWithDefault(MaterialLayer* layer, MaterialGraphBoxes box)
{
auto& boxInfo = GetMaterialRootNodeBox(box);
_writer.WriteLine(TEXT("\t{0}.{1} = {2};"), _treeLayerVarName, boxInfo.SubName, boxInfo.DefaultValue.Value);
}
void MaterialGenerator::ProcessGroupMath(Box* box, Node* node, Value& value)
{
switch (node->TypeID)
{
// Vector Transform
case 30:
{
// Get input vector
auto v = tryGetValue(node->GetBox(0), Value::InitForZero(VariantType::Vector3));
// Select transformation spaces
ASSERT(node->Values[0].Type == VariantType::Int && node->Values[1].Type == VariantType::Int);
ASSERT(Math::IsInRange(node->Values[0].AsInt, 0, (int32)TransformCoordinateSystem::MAX - 1));
ASSERT(Math::IsInRange(node->Values[1].AsInt, 0, (int32)TransformCoordinateSystem::MAX - 1));
auto inputType = static_cast<TransformCoordinateSystem>(node->Values[0].AsInt);
auto outputType = static_cast<TransformCoordinateSystem>(node->Values[1].AsInt);
if (inputType == outputType)
{
// No space change at all
value = v;
}
else
{
// Switch by source space type
const Char* format = nullptr;
switch (inputType)
{
case TransformCoordinateSystem::Tangent:
switch (outputType)
{
case TransformCoordinateSystem::Tangent:
format = TEXT("{0}");
break;
case TransformCoordinateSystem::World:
format = TEXT("TransformTangentVectorToWorld(input, {0})");
break;
case TransformCoordinateSystem::View:
format = TEXT("TransformWorldVectorToView(input, TransformTangentVectorToWorld(input, {0}))");
break;
case TransformCoordinateSystem::Local:
format = TEXT("TransformWorldVectorToLocal(input, TransformTangentVectorToWorld(input, {0}))");
break;
}
break;
case TransformCoordinateSystem::World:
switch (outputType)
{
case TransformCoordinateSystem::Tangent:
format = TEXT("TransformWorldVectorToTangent(input, {0})");
break;
case TransformCoordinateSystem::World:
format = TEXT("{0}");
break;
case TransformCoordinateSystem::View:
format = TEXT("TransformWorldVectorToView(input, {0})");
break;
case TransformCoordinateSystem::Local:
format = TEXT("TransformWorldVectorToLocal(input, {0})");
break;
}
break;
case TransformCoordinateSystem::View:
switch (outputType)
{
case TransformCoordinateSystem::Tangent:
format = TEXT("TransformWorldVectorToTangent(input, TransformViewVectorToWorld(input, {0}))");
break;
case TransformCoordinateSystem::World:
format = TEXT("TransformViewVectorToWorld(input, {0})");
break;
case TransformCoordinateSystem::View:
format = TEXT("{0}");
break;
case TransformCoordinateSystem::Local:
format = TEXT("TransformWorldVectorToLocal(input, TransformViewVectorToWorld(input, {0}))");
break;
}
break;
case TransformCoordinateSystem::Local:
switch (outputType)
{
case TransformCoordinateSystem::Tangent:
format = TEXT("TransformWorldVectorToTangent(input, TransformLocalVectorToWorld(input, {0}))");
break;
case TransformCoordinateSystem::World:
format = TEXT("TransformLocalVectorToWorld(input, {0})");
break;
case TransformCoordinateSystem::View:
format = TEXT("TransformWorldVectorToView(input, TransformLocalVectorToWorld(input, {0}))");
break;
case TransformCoordinateSystem::Local:
format = TEXT("{0}");
break;
}
break;
}
ASSERT(format != nullptr);
// Write operation
value = writeLocal(VariantType::Vector3, String::Format(format, v.Value), node);
}
break;
}
default:
ShaderGenerator::ProcessGroupMath(box, node, value);
break;
}
}
#endif

View File

@@ -0,0 +1,216 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#if COMPILE_WITH_MATERIAL_GRAPH
#include "Engine/Graphics/Materials/MaterialInfo.h"
#include "Engine/Graphics/Materials/MaterialParams.h"
#include "Engine/Content/Utilities/AssetsContainer.h"
#include "MaterialLayer.h"
#include "Types.h"
/// <summary>
/// Material node input boxes (each enum item value maps to box ID).
/// </summary>
enum class MaterialGraphBoxes
{
/// <summary>
/// The layer input.
/// </summary>
Layer = 0,
/// <summary>
/// The color input.
/// </summary>
Color = 1,
/// <summary>
/// The mask input.
/// </summary>
Mask = 2,
/// <summary>
/// The emissive input.
/// </summary>
Emissive = 3,
/// <summary>
/// The metalness input.
/// </summary>
Metalness = 4,
/// <summary>
/// The specular input.
/// </summary>
Specular = 5,
/// <summary>
/// The roughness input.
/// </summary>
Roughness = 6,
/// <summary>
/// The ambient occlusion input.
/// </summary>
AmbientOcclusion = 7,
/// <summary>
/// The normal input.
/// </summary>
Normal = 8,
/// <summary>
/// The opacity input.
/// </summary>
Opacity = 9,
/// <summary>
/// The refraction input.
/// </summary>
Refraction = 10,
/// <summary>
/// The position offset input.
/// </summary>
PositionOffset = 11,
/// <summary>
/// The tessellation multiplier input.
/// </summary>
TessellationMultiplier = 12,
/// <summary>
/// The world displacement input.
/// </summary>
WorldDisplacement = 13,
/// <summary>
/// The subsurface color input.
/// </summary>
SubsurfaceColor = 14,
/// <summary>
/// The amount of input boxes.
/// </summary>
MAX
};
/// <summary>
/// Material shaders generator from graphs.
/// </summary>
class MaterialGenerator : public ShaderGenerator
{
public:
struct MaterialGraphBoxesMapping
{
byte ID;
const Char* SubName;
MaterialTreeType TreeType;
MaterialValue DefaultValue;
};
private:
Array<MaterialLayer*> _layers;
Array<MaterialGraphBox*, FixedAllocation<16>> _vsToPsInterpolants;
MaterialTreeType _treeType;
MaterialLayer* _treeLayer = nullptr;
String _treeLayerVarName;
MaterialValue _ddx, _ddy, _cameraVector;
public:
MaterialGenerator();
~MaterialGenerator();
public:
/// <summary>
/// Gets material root layer
/// </summary>
/// <returns>Base layer</returns>
MaterialLayer* GetRootLayer() const;
/// <summary>
/// Add new layer to the generator data (will be deleted after usage)
/// </summary>
/// <param name="layer">Layer to add</param>
void AddLayer(MaterialLayer* layer);
/// <summary>
/// Gets layer that has given ID, if not loaded tries to load it
/// </summary>
/// <param name="id">Layer ID</param>
/// <param name="caller">Calling node</param>
/// <returns>Material layer or null if cannot do that</returns>
MaterialLayer* GetLayer(const Guid& id, Node* caller);
/// <summary>
/// Generate material source code (first layer should be the base one)
/// </summary>
/// <param name="source">Output source code</param>
/// <param name="materialInfo">Material info structure (will contain output data)</param>
/// <param name="parametersData">Output material parameters data</param>
/// <returns>True if cannot generate code, otherwise false</returns>
bool Generate(WriteStream& source, MaterialInfo& materialInfo, BytesContainer& parametersData);
private:
void clearCache();
void createGradients(Node* caller);
Value getCameraVector(Node* caller);
void eatMaterialGraphBox(String& layerVarName, MaterialGraphBox* nodeBox, MaterialGraphBoxes box);
void eatMaterialGraphBox(MaterialLayer* layer, MaterialGraphBoxes box);
void eatMaterialGraphBoxWithDefault(MaterialLayer* layer, MaterialGraphBoxes box);
void ProcessGroupLayers(Box* box, Node* node, Value& value);
void ProcessGroupMaterial(Box* box, Node* node, Value& value);
void ProcessGroupMath(Box* box, Node* node, Value& value);
void ProcessGroupParameters(Box* box, Node* node, Value& value);
void ProcessGroupTextures(Box* box, Node* node, Value& value);
void ProcessGroupTools(Box* box, Node* node, Value& value);
void ProcessGroupParticles(Box* box, Node* node, Value& value);
void ProcessGroupFunction(Box* box, Node* node, Value& value);
void writeBlending(MaterialGraphBoxes box, Value& result, const Value& bottom, const Value& top, const Value& alpha);
using ShaderGenerator::findParam;
SerializedMaterialParam* findParam(const Guid& id, MaterialLayer* layer);
MaterialGraphParameter* findGraphParam(const Guid& id);
MaterialValue* sampleTextureRaw(Node* caller, Value& value, Box* box, SerializedMaterialParam* texture);
void sampleTexture(Node* caller, Value& value, Box* box, SerializedMaterialParam* texture);
void sampleSceneDepth(Node* caller, Value& value, Box* box);
void linearizeSceneDepth(Node* caller, const Value& depth, Value& value);
// This must match ParticleAttribute::ValueTypes enum in Particles Module
enum class ParticleAttributeValueTypes
{
Float,
Vector2,
Vector3,
Vector4,
Int,
Uint,
};
MaterialValue AccessParticleAttribute(Node* caller, const StringView& name, ParticleAttributeValueTypes valueType, const Char* index = nullptr);
void prepareLayer(MaterialLayer* layer, bool allowVisibleParams);
public:
static MaterialValue getUVs;
static MaterialValue getTime;
static MaterialValue getNormal;
static MaterialValue getVertexColor;
static MaterialGraphBoxesMapping MaterialGraphBoxesMappings[];
static const MaterialGraphBoxesMapping& GetMaterialRootNodeBox(MaterialGraphBoxes box);
static byte getStartSrvRegister(MaterialLayer* baseLayer);
};
#endif

View File

@@ -0,0 +1,239 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MATERIAL_GRAPH
#include "MaterialLayer.h"
#include "MaterialGenerator.h"
MaterialLayer::MaterialLayer(const Guid& id)
: ID(id)
, Root(nullptr)
, FeaturesFlags(MaterialFeaturesFlags::None)
, UsageFlags(MaterialUsageFlags::None)
, Domain(MaterialDomain::Surface)
, BlendMode(MaterialBlendMode::Opaque)
, ShadingModel(MaterialShadingModel::Lit)
, MaskThreshold(0.3f)
, OpacityThreshold(0.12f)
, ParamIdsMappings(8)
{
ASSERT(ID.IsValid());
}
void MaterialLayer::ClearCache()
{
for (int32 i = 0; i < Graph.Nodes.Count(); i++)
{
auto& node = Graph.Nodes[i];
for (int32 j = 0; j < node.Boxes.Count(); j++)
{
node.Boxes[j].Cache.Clear();
}
}
for (int32 i = 0; i < ARRAY_COUNT(Usage); i++)
{
Usage[i].VarName.Clear();
Usage[i].Hint = nullptr;
}
}
void MaterialLayer::Prepare()
{
// Clear cached data
ClearCache();
// Ensure to have root node set
if (Root == nullptr)
{
for (int32 i = 0; i < Graph.Nodes.Count(); i++)
{
const auto node = &Graph.Nodes[i];
if (node->Type == ROOT_NODE_TYPE)
{
Root = (MaterialGraphNode*)node;
break;
}
}
if (Root == nullptr)
{
// Ensure to have root node
createRootNode();
}
}
}
Guid MaterialLayer::GetMappedParamId(const Guid& id)
{
// TODO: test ParamIdsMappings using Dictionary. will performance change? mamybe we don't wont to allocate too much memory
for (int32 i = 0; i < ParamIdsMappings.Count(); i++)
{
if (ParamIdsMappings[i].SrcId == id)
return ParamIdsMappings[i].DstId;
}
return Guid::Empty;
}
String& MaterialLayer::GetVariableName(void* hint)
{
if (hint == nullptr)
{
return Usage[0].VarName;
}
for (int32 i = 1; i < ARRAY_COUNT(Usage); i++)
{
if (Usage[i].Hint == nullptr)
{
Usage[i].Hint = hint;
}
if (Usage[i].Hint == hint)
{
return Usage[i].VarName;
}
}
LOG(Error, "Too many layer samples per material! Layer {0}", ID);
return Usage[0].VarName;
}
bool MaterialLayer::HasAnyVariableName()
{
for (int32 i = 1; i < ARRAY_COUNT(Usage); i++)
{
if (Usage[i].Hint || Usage[i].VarName.HasChars())
{
return true;
}
}
return false;
}
void MaterialLayer::UpdateFeaturesFlags()
{
ASSERT(Root);
// Gather the material features usage
UsageFlags = MaterialUsageFlags::None;
#define CHECK_BOX_AS_FEATURE(box, feature) \
if (Root->GetBox(static_cast<int32>(MaterialGraphBoxes::box))->HasConnection()) \
UsageFlags |= MaterialUsageFlags::feature; \
else \
UsageFlags &= ~MaterialUsageFlags::feature;
CHECK_BOX_AS_FEATURE(Emissive, UseEmissive);
CHECK_BOX_AS_FEATURE(Normal, UseNormal);
CHECK_BOX_AS_FEATURE(Mask, UseMask);
CHECK_BOX_AS_FEATURE(PositionOffset, UsePositionOffset);
CHECK_BOX_AS_FEATURE(WorldDisplacement, UseDisplacement);
CHECK_BOX_AS_FEATURE(Refraction, UseRefraction);
#undef CHECK_BOX_AS_FEATURE
}
MaterialLayer* MaterialLayer::CreateDefault(const Guid& id)
{
// Create new layer object
auto layer = New<MaterialLayer>(id);
// Create default root node
layer->createRootNode();
return layer;
}
MaterialLayer* MaterialLayer::Load(const Guid& id, ReadStream* graphData, const MaterialInfo& info, const String& caller)
{
// Create new layer object
auto layer = New<MaterialLayer>(id);
// Load graph
if (layer->Graph.Load(graphData, false))
{
LOG(Warning, "Cannot load graph '{0}'.", caller);
}
// Find root node
for (int32 i = 0; i < layer->Graph.Nodes.Count(); i++)
{
if (layer->Graph.Nodes[i].Type == ROOT_NODE_TYPE)
{
layer->Root = (MaterialGraphNode*)&layer->Graph.Nodes[i];
break;
}
}
// Ensure to have root node
if (layer->Root == nullptr)
{
LOG(Warning, "Missing root node in '{0}'.", caller);
layer->createRootNode();
}
// Ensure to have valid root node
else if (layer->Root->Boxes.Count() != static_cast<int32>(MaterialGraphBoxes::MAX))
{
#define ADD_BOX(type, valueType) \
if(layer->Root->Boxes.Count() <= static_cast<int32>(MaterialGraphBoxes::type)) \
layer->Root->Boxes.Add(MaterialGraphBox(layer->Root, static_cast<int32>(MaterialGraphBoxes::type), VariantType::valueType))
ADD_BOX(TessellationMultiplier, Float);
ADD_BOX(WorldDisplacement, Vector3);
ADD_BOX(SubsurfaceColor, Vector3);
static_assert(static_cast<int32>(MaterialGraphBoxes::MAX) == 15, "Invalid amount of boxes added for root node. Please update the code above");
ASSERT(layer->Root->Boxes.Count() == static_cast<int32>(MaterialGraphBoxes::MAX));
#if BUILD_DEBUG
// Test for valid pointers after node upgrade
for (int32 i = 0; i < layer->Root->Boxes.Count(); i++)
{
if (layer->Root->Boxes[i].Parent != layer->Root)
{
CRASH;
}
}
#endif
#undef ADD_BOX
}
// Setup
layer->FeaturesFlags = info.FeaturesFlags;
layer->UsageFlags = info.UsageFlags;
layer->Domain = info.Domain;
layer->BlendMode = info.BlendMode;
layer->ShadingModel = info.ShadingModel;
layer->MaskThreshold = info.MaskThreshold;
layer->OpacityThreshold = info.OpacityThreshold;
return layer;
}
void MaterialLayer::createRootNode()
{
// Create node
auto& rootNode = Graph.Nodes.AddOne();
rootNode.ID = 1;
rootNode.Type = ROOT_NODE_TYPE;
rootNode.Boxes.Resize(static_cast<int32>(MaterialGraphBoxes::MAX));
#define INIT_BOX(type, valueType) rootNode.Boxes[static_cast<int32>(MaterialGraphBoxes::type)] = MaterialGraphBox(&rootNode, static_cast<int32>(MaterialGraphBoxes::type), VariantType::valueType)
INIT_BOX(Layer, Void);
INIT_BOX(Color, Vector3);
INIT_BOX(Mask, Float);
INIT_BOX(Emissive, Vector3);
INIT_BOX(Metalness, Float);
INIT_BOX(Specular, Float);
INIT_BOX(Roughness, Float);
INIT_BOX(AmbientOcclusion, Float);
INIT_BOX(Normal, Vector3);
INIT_BOX(Opacity, Float);
INIT_BOX(Refraction, Float);
INIT_BOX(PositionOffset, Vector3);
INIT_BOX(TessellationMultiplier, Float);
INIT_BOX(WorldDisplacement, Vector3);
INIT_BOX(SubsurfaceColor, Vector3);
static_assert(static_cast<int32>(MaterialGraphBoxes::MAX) == 15, "Invalid amount of boxes created for root node. Please update the code above");
#undef INIT_BOX
// Mark as root
Root = (MaterialGraphNode*)&rootNode;
}
#endif

View File

@@ -0,0 +1,154 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#if COMPILE_WITH_MATERIAL_GRAPH
#include "Types.h"
#include "Engine/Graphics/Materials/MaterialInfo.h"
/// <summary>
/// We use them to map layer's params ID.
/// Used in layered materials to change param ID for each layer (two or more layers may have the same params ID from the same base material)
/// </summary>
struct LayerParamMapping
{
Guid SrcId;
Guid DstId;
};
/// <summary>
/// Single material layer
/// </summary>
class MaterialLayer
{
public:
struct LayerUsage
{
String VarName;
void* Hint;
LayerUsage()
{
Hint = nullptr;
}
};
public:
/// <summary>
/// Layer ID
/// </summary>
Guid ID;
/// <summary>
/// Graph data
/// </summary>
MaterialGraph Graph;
/// <summary>
/// Root node
/// </summary>
MaterialGraphNode* Root;
/// <summary>
/// Material structure variable name (different for every layer sampling with different UVs, default UVs are a first index)
/// </summary>
LayerUsage Usage[4];
/// <summary>
/// Layer features flags
/// </summary>
MaterialFeaturesFlags FeaturesFlags;
/// <summary>
/// Layer usage flags
/// </summary>
MaterialUsageFlags UsageFlags;
/// <summary>
/// Domain
/// </summary>
MaterialDomain Domain;
/// <summary>
/// Blending mode
/// </summary>
MaterialBlendMode BlendMode;
/// <summary>
/// The shading model.
/// </summary>
MaterialShadingModel ShadingModel;
/// <summary>
/// The opacity threshold value (masked materials pixels clipping).
/// </summary>
float MaskThreshold;
/// <summary>
/// The opacity threshold value (transparent materials shadow pass though clipping).
/// </summary>
float OpacityThreshold;
/// <summary>
/// Helper array with original layer parameters Ids mappings into new Ids
/// Note: during sampling different materials layers we have to change their parameters Ids due to possible Ids collisions
/// Collisions may occur in duplicated materials so we want to resolve them.
/// </summary>
Array<LayerParamMapping> ParamIdsMappings;
public:
/// <summary>
/// Initializes a new instance of the <see cref="MaterialLayer"/> class.
/// </summary>
/// <param name="id">The layer asset identifier.</param>
MaterialLayer(const Guid& id);
public:
/// <summary>
/// Clear all cached values
/// </summary>
void ClearCache();
/// <summary>
/// Prepare layer for the material compilation process
/// </summary>
void Prepare();
Guid GetMappedParamId(const Guid& id);
String& GetVariableName(void* hint);
bool HasAnyVariableName();
void UpdateFeaturesFlags();
public:
/// <summary>
/// Create default empty layer
/// </summary>
/// <param name="id">Layer id</param>
/// <returns>Layer</returns>
static MaterialLayer* CreateDefault(const Guid& id);
/// <summary>
/// Load layer data
/// </summary>
/// <param name="id">Layer id</param>
/// <param name="graphData">Stream with saved graph object</param>
/// <param name="info">Material info structure</param>
/// <param name="caller">Calling object name</param>
/// <returns>Layer</returns>
static MaterialLayer* Load(const Guid& id, ReadStream* graphData, const MaterialInfo& info, const String& caller);
private:
void createRootNode();
};
#endif

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#if COMPILE_WITH_MATERIAL_GRAPH
#include "Engine/Core/Enums.h"
#include "Engine/Visject/ShaderGraph.h"
class MaterialGraphNode : public ShaderGraphNode<>
{
};
class MaterialGraph : public ShaderGraph<>
{
};
typedef ShaderGraphBox MaterialGraphBox;
typedef ShaderGraphParameter MaterialGraphParameter;
typedef ShaderGraphValue MaterialValue;
/// <summary>
/// Material function generate tree type
/// </summary>
DECLARE_ENUM_3(MaterialTreeType, VertexShader, DomainShader, PixelShader);
/// <summary>
/// Vector transformation coordinate systems.
/// </summary>
enum class TransformCoordinateSystem
{
/// <summary>
/// The world space. It's absolute world space coordinate system.
/// </summary>
World = 0,
/// <summary>
/// The tangent space. It's relative to the surface (tangent frame defined by normal, tangent and bitangent vectors).
/// </summary>
Tangent = 1,
/// <summary>
/// The view space. It's relative to the current rendered viewport orientation.
/// </summary>
View = 2,
/// <summary>
/// The local space. It's relative to the rendered object (aka object space).
/// </summary>
Local = 3,
/// <summary>
/// The count of the items in the enum.
/// </summary>
MAX
};
#define ROOT_NODE_TYPE GRAPH_NODE_MAKE_TYPE(1, 1)
class MaterialLayer;
inline bool CanUseSample(MaterialTreeType treeType)
{
return treeType == MaterialTreeType::PixelShader;
}
#endif

View File

@@ -0,0 +1,790 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MODEL_TOOL && USE_ASSIMP
#include "ModelTool.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Tools/TextureTool/TextureTool.h"
// Import Assimp library
// Source: https://github.com/assimp/assimp
#define ASSIMP_BUILD_NO_EXPORT
#include <ThirdParty/assimp/Importer.hpp>
#include <ThirdParty/assimp/types.h>
#include <ThirdParty/assimp/config.h>
#include <ThirdParty/assimp/scene.h>
#include <ThirdParty/assimp/version.h>
#include <ThirdParty/assimp/postprocess.h>
#include <ThirdParty/assimp/LogStream.hpp>
#include <ThirdParty/assimp/DefaultLogger.hpp>
#include <ThirdParty/assimp/Logger.hpp>
using namespace Assimp;
class AssimpLogStream : public LogStream
{
public:
AssimpLogStream()
{
DefaultLogger::create("");
DefaultLogger::get()->attachStream(this);
}
~AssimpLogStream()
{
DefaultLogger::get()->detatchStream(this);
DefaultLogger::kill();
}
public:
void write(const char* message) override
{
String s(message);
s.Replace('\n', ' ');
LOG(Info, "[Assimp]: {0}", s);
}
};
Vector2 ToVector2(const aiVector2D& v)
{
return Vector2(v.x, v.y);
}
Vector2 ToVector2(const aiVector3D& v)
{
return Vector2(v.x, v.y);
}
Vector3 ToVector3(const aiVector3D& v)
{
return Vector3(v.x, v.y, v.z);
}
Color ToColor(const aiColor3D& v)
{
return Color(v.r, v.g, v.b, 1.0f);
}
Color ToColor(const aiColor4D& v)
{
return Color(v.r, v.g, v.b, v.a);
}
Quaternion ToQuaternion(const aiQuaternion& v)
{
return Quaternion(v.x, v.y, v.z, v.w);
}
Matrix ToMatrix(const aiMatrix4x4& mat)
{
return Matrix(mat.a1, mat.b1, mat.c1, mat.d1,
mat.a2, mat.b2, mat.c2, mat.d2,
mat.a3, mat.b3, mat.c3, mat.d3,
mat.a4, mat.b4, mat.c4, mat.d4);
}
struct AssimpNode
{
/// <summary>
/// The parent index. The root node uses value -1.
/// </summary>
int32 ParentIndex;
/// <summary>
/// The local transformation of the bone, relative to parent bone.
/// </summary>
Transform LocalTransform;
/// <summary>
/// The name of this bone.
/// </summary>
String Name;
/// <summary>
/// The LOD index of the data in this node (used to separate meshes across different level of details).
/// </summary>
int32 LodIndex;
};
struct AssimpBone
{
/// <summary>
/// The index of the related node.
/// </summary>
int32 NodeIndex;
/// <summary>
/// The parent bone index. The root bone uses value -1.
/// </summary>
int32 ParentBoneIndex;
/// <summary>
/// The name of this bone.
/// </summary>
String Name;
/// <summary>
/// The matrix that transforms from mesh space to bone space in bind pose.
/// </summary>
Matrix OffsetMatrix;
bool operator<(const AssimpBone& other) const
{
return NodeIndex < other.NodeIndex;
}
};
struct AssimpImporterData
{
ImportedModelData& Model;
const String Path;
const aiScene* Scene;
const ModelTool::Options& Options;
Array<AssimpNode> Nodes;
Array<AssimpBone> Bones;
Dictionary<int32, Array<int32>> MeshIndexToNodeIndex;
AssimpImporterData(const char* path, ImportedModelData& model, const ModelTool::Options& options, const aiScene* scene)
: Model(model)
, Path(path)
, Scene(scene)
, Options(options)
, Nodes(static_cast<int32>(scene->mNumMeshes * 4.0f))
, MeshIndexToNodeIndex(static_cast<int32>(scene->mNumMeshes * 8.0f))
{
}
int32 FindNode(const String& name, StringSearchCase caseSensitivity = StringSearchCase::CaseSensitive)
{
for (int32 i = 0; i < Nodes.Count(); i++)
{
if (Nodes[i].Name.Compare(name, caseSensitivity) == 0)
return i;
}
return -1;
}
int32 FindBone(const String& name, StringSearchCase caseSensitivity = StringSearchCase::CaseSensitive)
{
for (int32 i = 0; i < Bones.Count(); i++)
{
if (Bones[i].Name.Compare(name, caseSensitivity) == 0)
return i;
}
return -1;
}
int32 FindBone(const int32 nodeIndex)
{
for (int32 i = 0; i < Bones.Count(); i++)
{
if (Bones[i].NodeIndex == nodeIndex)
return i;
}
return -1;
}
};
void ProcessNodes(AssimpImporterData& data, aiNode* aNode, int32 parentIndex)
{
const int32 nodeIndex = data.Nodes.Count();
// Assign the index of the node to the index of the mesh
for (unsigned i = 0; i < aNode->mNumMeshes; i++)
{
int meshIndex = aNode->mMeshes[i];
data.MeshIndexToNodeIndex[meshIndex].Add(nodeIndex);
}
// Create node
AssimpNode node;
node.ParentIndex = parentIndex;
node.Name = aNode->mName.C_Str();
// Pick node LOD index
if (parentIndex == -1 || !data.Options.ImportLODs)
{
node.LodIndex = 0;
}
else
{
node.LodIndex = data.Nodes[parentIndex].LodIndex;
if (node.LodIndex == 0)
{
node.LodIndex = ModelTool::DetectLodIndex(node.Name);
}
ASSERT(Math::IsInRange(node.LodIndex, 0, MODEL_MAX_LODS - 1));
}
Matrix transform = ToMatrix(aNode->mTransformation);
transform.Decompose(node.LocalTransform);
data.Nodes.Add(node);
// Process the children
for (unsigned i = 0; i < aNode->mNumChildren; i++)
{
ProcessNodes(data, aNode->mChildren[i], nodeIndex);
}
}
bool ProcessMesh(AssimpImporterData& data, const aiMesh* aMesh, MeshData& mesh, String& errorMsg)
{
// Properties
mesh.Name = aMesh->mName.C_Str();
mesh.MaterialSlotIndex = aMesh->mMaterialIndex;
// Vertex positions
mesh.Positions.Set((const Vector3*)aMesh->mVertices, aMesh->mNumVertices);
// Texture coordinates
if (aMesh->mTextureCoords[0])
{
mesh.UVs.Resize(aMesh->mNumVertices, false);
aiVector3D* a = aMesh->mTextureCoords[0];
for (uint32 v = 0; v < aMesh->mNumVertices; v++)
{
mesh.UVs[v] = *(Vector2*)a;
a++;
}
}
// Normals
if (aMesh->mNormals)
{
mesh.Normals.Set((const Vector3*)aMesh->mNormals, aMesh->mNumVertices);
}
// Tangents
if (aMesh->mTangents)
{
mesh.Tangents.Set((const Vector3*)aMesh->mTangents, aMesh->mNumVertices);
}
// Indices
const int32 indicesCount = aMesh->mNumFaces * 3;
mesh.Indices.Resize(indicesCount, false);
for (unsigned faceIndex = 0, i = 0; faceIndex < aMesh->mNumFaces; faceIndex++)
{
const auto face = &aMesh->mFaces[faceIndex];
if (face->mNumIndices != 3)
{
errorMsg = TEXT("All faces in a mesh must be trangles!");
return true;
}
mesh.Indices[i++] = face->mIndices[0];
mesh.Indices[i++] = face->mIndices[1];
mesh.Indices[i++] = face->mIndices[2];
}
// Lightmap UVs
if (data.Options.LightmapUVsSource == ModelLightmapUVsSource::Disable)
{
// No lightmap UVs
}
else if (data.Options.LightmapUVsSource == ModelLightmapUVsSource::Generate)
{
// Generate lightmap UVs
if (mesh.GenerateLightmapUVs())
{
LOG(Error, "Failed to generate lightmap uvs");
}
}
else
{
// Select input channel index
int32 inputChannelIndex;
switch (data.Options.LightmapUVsSource)
{
case ModelLightmapUVsSource::Channel0:
inputChannelIndex = 0;
break;
case ModelLightmapUVsSource::Channel1:
inputChannelIndex = 1;
break;
case ModelLightmapUVsSource::Channel2:
inputChannelIndex = 2;
break;
case ModelLightmapUVsSource::Channel3:
inputChannelIndex = 3;
break;
default:
inputChannelIndex = INVALID_INDEX;
break;
}
// Check if has that channel texcoords
if (inputChannelIndex >= 0 && inputChannelIndex < AI_MAX_NUMBER_OF_TEXTURECOORDS && aMesh->mTextureCoords[inputChannelIndex])
{
mesh.LightmapUVs.Resize(aMesh->mNumVertices, false);
aiVector3D* a = aMesh->mTextureCoords[inputChannelIndex];
for (uint32 v = 0; v < aMesh->mNumVertices; v++)
{
mesh.LightmapUVs[v] = *(Vector2*)a;
a++;
}
}
else
{
LOG(Warning, "Cannot import model lightmap uvs. Missing texcoords channel {0}.", inputChannelIndex);
}
}
// Vertex Colors
if (data.Options.ImportVertexColors && aMesh->mColors[0])
{
mesh.Colors.Resize(aMesh->mNumVertices, false);
aiColor4D* a = aMesh->mColors[0];
for (uint32 v = 0; v < aMesh->mNumVertices; v++)
{
mesh.Colors[v] = *(Color*)a;
a++;
}
}
// Blend Indices and Blend Weights
if (aMesh->mNumBones > 0 && aMesh->mBones && data.Model.Types & ImportDataTypes::Skeleton)
{
const int32 vertexCount = mesh.Positions.Count();
mesh.BlendIndices.Resize(vertexCount);
mesh.BlendWeights.Resize(vertexCount);
mesh.BlendIndices.SetAll(Int4::Zero);
mesh.BlendWeights.SetAll(Vector4::Zero);
// Build skinning clusters and fill controls points data stutcture
for (unsigned boneId = 0; boneId < aMesh->mNumBones; boneId++)
{
const auto aBone = aMesh->mBones[boneId];
// Find the node where the bone is mapped - based on the name
const String boneName(aBone->mName.C_Str());
const int32 nodeIndex = data.FindNode(boneName);
if (nodeIndex == -1)
{
LOG(Warning, "Invalid mesh bone linkage. Mesh: {0}, bone: {1}. Skipping...", mesh.Name, boneName);
continue;
}
// Create bone if missing
int32 boneIndex = data.FindBone(boneName);
if (boneIndex == -1)
{
// Find the parent bone
int32 parentBoneIndex = -1;
for (int32 i = nodeIndex; i != -1; i = data.Nodes[i].ParentIndex)
{
parentBoneIndex = data.FindBone(i);
if (parentBoneIndex != -1)
break;
}
// Add bone
boneIndex = data.Bones.Count();
data.Bones.EnsureCapacity(Math::Max(128, boneIndex + 16));
data.Bones.Resize(boneIndex + 1);
auto& bone = data.Bones[boneIndex];
// Setup bone
bone.Name = boneName;
bone.NodeIndex = nodeIndex;
bone.ParentBoneIndex = parentBoneIndex;
bone.OffsetMatrix = ToMatrix(aBone->mOffsetMatrix);
}
// Apply the bone influences
for (unsigned vtxWeightId = 0; vtxWeightId < aBone->mNumWeights; vtxWeightId++)
{
const auto vtxWeight = aBone->mWeights[vtxWeightId];
if (vtxWeight.mWeight <= 0 || vtxWeight.mVertexId >= (unsigned)vertexCount)
continue;
auto& indices = mesh.BlendIndices[vtxWeight.mVertexId];
auto& weights = mesh.BlendWeights[vtxWeight.mVertexId];
for (int32 k = 0; k < 4; k++)
{
if (vtxWeight.mWeight >= weights.Raw[k])
{
for (int32 l = 2; l >= k; l--)
{
indices.Raw[l + 1] = indices.Raw[l];
weights.Raw[l + 1] = weights.Raw[l];
}
indices.Raw[k] = boneIndex;
weights.Raw[k] = vtxWeight.mWeight;
break;
}
}
}
}
mesh.NormalizeBlendWeights();
}
// Blend Shapes
if (aMesh->mNumAnimMeshes > 0 && data.Model.Types & ImportDataTypes::Skeleton && data.Options.ImportBlendShapes)
{
mesh.BlendShapes.EnsureCapacity(aMesh->mNumAnimMeshes);
for (unsigned int animMeshIndex = 0; animMeshIndex < aMesh->mNumAnimMeshes; animMeshIndex++)
{
const aiAnimMesh* aAnimMesh = aMesh->mAnimMeshes[animMeshIndex];
BlendShape& blendShapeData = mesh.BlendShapes.AddOne();
blendShapeData.Name = aAnimMesh->mName.C_Str();
if (blendShapeData.Name.IsEmpty())
blendShapeData.Name = mesh.Name + TEXT("_blend_shape_") + StringUtils::ToString(animMeshIndex);
blendShapeData.Weight = aAnimMesh->mWeight;
blendShapeData.Vertices.Resize(aAnimMesh->mNumVertices);
for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
blendShapeData.Vertices[i].VertexIndex = i;
const aiVector3D* shapeVertices = aAnimMesh->mVertices;
if (shapeVertices)
{
for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
blendShapeData.Vertices[i].PositionDelta = ToVector3(shapeVertices[i]) - mesh.Positions[i];
}
else
{
for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
blendShapeData.Vertices[i].PositionDelta = Vector3::Zero;
}
const aiVector3D* shapeNormals = aAnimMesh->mNormals;
if (shapeNormals)
{
for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
blendShapeData.Vertices[i].NormalDelta = ToVector3(shapeNormals[i]) - mesh.Normals[i];
}
else
{
for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
blendShapeData.Vertices[i].NormalDelta = Vector3::Zero;
}
}
}
return false;
}
bool ImportTexture(AssimpImporterData& data, aiString& aFilename, int32& textureIndex, TextureEntry::TypeHint type)
{
// Find texture file path
const String filename = String(aFilename.C_Str()).TrimTrailing();
String path;
if (ModelTool::FindTexture(data.Path, filename, path))
return true;
// Check if already used
textureIndex = 0;
while (textureIndex < data.Model.Textures.Count())
{
if (data.Model.Textures[textureIndex].FilePath == path)
return true;
textureIndex++;
}
// Import texture
auto& texture = data.Model.Textures.AddOne();
texture.FilePath = path;
texture.Type = type;
texture.AssetID = Guid::Empty;
return true;
}
bool ImportMaterialTexture(AssimpImporterData& data, const aiMaterial* aMaterial, aiTextureType aTextureType, int32& textureIndex, TextureEntry::TypeHint type)
{
aiString aFilename;
return aMaterial->GetTexture(aTextureType, 0, &aFilename, nullptr, nullptr, nullptr, nullptr) == AI_SUCCESS &&
ImportTexture(data, aFilename, textureIndex, type);
}
bool ImportMaterials(AssimpImporterData& data, String& errorMsg)
{
const uint32 materialsCount = (uint32)data.Scene->mNumMaterials;
data.Model.Materials.Resize(materialsCount, false);
for (uint32 i = 0; i < materialsCount; i++)
{
auto& materialSlot = data.Model.Materials[i];
const aiMaterial* aMaterial = data.Scene->mMaterials[i];
aiString aName;
if (aMaterial->Get(AI_MATKEY_NAME, aName) == AI_SUCCESS)
materialSlot.Name = String(aName.C_Str()).TrimTrailing();
materialSlot.AssetID = Guid::Empty;
aiColor3D aColor;
if (aMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, aColor) == AI_SUCCESS)
materialSlot.Diffuse.Color = ToColor(aColor);
bool aBoolean;
if (aMaterial->Get(AI_MATKEY_TWOSIDED, aBoolean) == AI_SUCCESS)
materialSlot.TwoSided = aBoolean;
bool aFloat;
if (aMaterial->Get(AI_MATKEY_OPACITY, aFloat) == AI_SUCCESS)
materialSlot.Opacity.Value = aFloat;
if (data.Model.Types & ImportDataTypes::Textures)
{
ImportMaterialTexture(data, aMaterial, aiTextureType_DIFFUSE, materialSlot.Diffuse.TextureIndex, TextureEntry::TypeHint::ColorRGB);
ImportMaterialTexture(data, aMaterial, aiTextureType_EMISSIVE, materialSlot.Emissive.TextureIndex, TextureEntry::TypeHint::ColorRGB);
ImportMaterialTexture(data, aMaterial, aiTextureType_NORMALS, materialSlot.Normals.TextureIndex, TextureEntry::TypeHint::Normals);
ImportMaterialTexture(data, aMaterial, aiTextureType_OPACITY, materialSlot.Opacity.TextureIndex, TextureEntry::TypeHint::ColorRGBA);
if (materialSlot.Diffuse.TextureIndex != -1)
{
// Detect using alpha mask in diffuse texture
materialSlot.Diffuse.HasAlphaMask = TextureTool::HasAlpha(data.Model.Textures[materialSlot.Diffuse.TextureIndex].FilePath);
if (materialSlot.Diffuse.HasAlphaMask)
data.Model.Textures[materialSlot.Diffuse.TextureIndex].Type = TextureEntry::TypeHint::ColorRGBA;
}
}
}
return false;
}
bool ImportMeshes(AssimpImporterData& data, String& errorMsg)
{
for (unsigned i = 0; i < data.Scene->mNumMeshes; i++)
{
const auto aMesh = data.Scene->mMeshes[i];
// Skip invalid meshes
if (aMesh->mPrimitiveTypes != aiPrimitiveType_TRIANGLE || aMesh->mNumVertices == 0 || aMesh->mNumFaces == 0 || aMesh->mFaces[0].mNumIndices != 3)
continue;
// Skip unused meshes
if (!data.MeshIndexToNodeIndex.ContainsKey(i))
continue;
// Import mesh data
MeshData* meshData = New<MeshData>();
if (ProcessMesh(data, aMesh, *meshData, errorMsg))
return true;
auto& nodesWithMesh = data.MeshIndexToNodeIndex[i];
for (int32 j = 0; j < nodesWithMesh.Count(); j++)
{
const auto nodeIndex = nodesWithMesh[j];
auto& node = data.Nodes[nodeIndex];
const int32 lodIndex = node.LodIndex;
// The first mesh instance uses meshData directly while others have to clone it
if (j != 0)
{
meshData = New<MeshData>(*meshData);
}
// Link mesh
meshData->NodeIndex = nodeIndex;
if (data.Model.LODs.Count() <= lodIndex)
data.Model.LODs.Resize(lodIndex + 1);
data.Model.LODs[lodIndex].Meshes.Add(meshData);
}
}
return false;
}
void ImportCurve(aiVectorKey* keys, uint32 keysCount, LinearCurve<Vector3>& curve)
{
if (keys == nullptr || keysCount == 0)
return;
const auto keyframes = curve.Resize(keysCount);
for (uint32 i = 0; i < keysCount; i++)
{
auto& aKey = keys[i];
auto& key = keyframes[i];
key.Time = (float)aKey.mTime;
key.Value = ToVector3(aKey.mValue);
}
}
void ImportCurve(aiQuatKey* keys, uint32 keysCount, LinearCurve<Quaternion>& curve)
{
if (keys == nullptr || keysCount == 0)
return;
const auto keyframes = curve.Resize(keysCount);
for (uint32 i = 0; i < keysCount; i++)
{
auto& aKey = keys[i];
auto& key = keyframes[i];
key.Time = (float)aKey.mTime;
key.Value = ToQuaternion(aKey.mValue);
}
}
static bool AssimpInited = false;
bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, const Options& options, String& errorMsg)
{
// Prepare
if (!AssimpInited)
{
AssimpInited = true;
// Log Assimp version
LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision());
}
Importer importer;
AssimpLogStream assimpLogStream;
bool importMeshes = (data.Types & ImportDataTypes::Geometry) != 0;
bool importAnimations = (data.Types & ImportDataTypes::Animations) != 0;
// Setup import flags
unsigned int flags =
aiProcess_JoinIdenticalVertices |
aiProcess_LimitBoneWeights |
aiProcess_Triangulate |
aiProcess_GenUVCoords |
aiProcess_FindDegenerates |
aiProcess_FindInvalidData |
//aiProcess_ValidateDataStructure |
aiProcess_ConvertToLeftHanded;
if (importMeshes)
{
if (options.CalculateNormals)
flags |= aiProcess_FixInfacingNormals | aiProcess_GenSmoothNormals;
if (options.CalculateTangents)
flags |= aiProcess_CalcTangentSpace;
if (options.OptimizeMeshes)
flags |= aiProcess_OptimizeMeshes | aiProcess_SplitLargeMeshes | aiProcess_ImproveCacheLocality;
if (options.MergeMeshes)
flags |= aiProcess_RemoveRedundantMaterials;
}
// Setup import options
importer.SetPropertyFloat(AI_CONFIG_PP_GSN_MAX_SMOOTHING_ANGLE, options.SmoothingNormalsAngle);
importer.SetPropertyFloat(AI_CONFIG_PP_CT_MAX_SMOOTHING_ANGLE, options.SmoothingTangentsAngle);
//importer.SetPropertyInteger(AI_CONFIG_PP_SLM_TRIANGLE_LIMIT, MAX_uint16);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, false);
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, importAnimations);
//importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); // TODO: optimize pivots when https://github.com/assimp/assimp/issues/1068 gets fixed
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true);
// Import file
const auto scene = importer.ReadFile(path, flags);
if (scene == nullptr)
{
LOG_STR(Warning, String(importer.GetErrorString()));
LOG_STR(Warning, String(path));
LOG_STR(Warning, StringUtils::ToString(flags));
errorMsg = importer.GetErrorString();
return true;
}
// Process imported scene nodes
AssimpImporterData assimpData(path, data, options, scene);
ProcessNodes(assimpData, scene->mRootNode, -1);
// Import materials
if (data.Types & ImportDataTypes::Materials)
{
if (ImportMaterials(assimpData, errorMsg))
{
LOG(Warning, "Failed to import materials.");
return true;
}
}
// Import geometry
if (data.Types & ImportDataTypes::Geometry)
{
if (ImportMeshes(assimpData, errorMsg))
{
LOG(Warning, "Failed to import meshes.");
return true;
}
}
// Import skeleton
if (data.Types & ImportDataTypes::Skeleton)
{
data.Skeleton.Nodes.Resize(assimpData.Nodes.Count(), false);
for (int32 i = 0; i < assimpData.Nodes.Count(); i++)
{
auto& node = data.Skeleton.Nodes[i];
auto& aNode = assimpData.Nodes[i];
node.Name = aNode.Name;
node.ParentIndex = aNode.ParentIndex;
node.LocalTransform = aNode.LocalTransform;
}
data.Skeleton.Bones.Resize(assimpData.Bones.Count(), false);
for (int32 i = 0; i < assimpData.Bones.Count(); i++)
{
auto& bone = data.Skeleton.Bones[i];
auto& aBone = assimpData.Bones[i];
const auto boneNodeIndex = aBone.NodeIndex;
const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : assimpData.Bones[aBone.ParentBoneIndex].NodeIndex;
bone.ParentIndex = aBone.ParentBoneIndex;
bone.NodeIndex = aBone.NodeIndex;
bone.LocalTransform = CombineTransformsFromNodeIndices(assimpData.Nodes, parentBoneNodeIndex, boneNodeIndex);
bone.OffsetMatrix = aBone.OffsetMatrix;
}
}
// Import animations
if (data.Types & ImportDataTypes::Animations)
{
if (scene->HasAnimations())
{
const auto animIndex = Math::Clamp<int32>(options.AnimationIndex, 0, scene->mNumAnimations - 1);
const auto animations = scene->mAnimations[animIndex];
data.Animation.Channels.Resize(animations->mNumChannels, false);
data.Animation.Duration = animations->mDuration;
data.Animation.FramesPerSecond = animations->mTicksPerSecond != 0.0 ? animations->mTicksPerSecond : 25.0;
for (unsigned i = 0; i < animations->mNumChannels; i++)
{
const auto aAnim = animations->mChannels[i];
auto& anim = data.Animation.Channels[i];
anim.NodeName = aAnim->mNodeName.C_Str();
ImportCurve(aAnim->mPositionKeys, aAnim->mNumPositionKeys, anim.Position);
ImportCurve(aAnim->mRotationKeys, aAnim->mNumRotationKeys, anim.Rotation);
ImportCurve(aAnim->mScalingKeys, aAnim->mNumScalingKeys, anim.Scale);
}
}
else
{
LOG(Warning, "Loaded scene has no animations");
}
}
// Import nodes
if (data.Types & ImportDataTypes::Nodes)
{
data.Nodes.Resize(assimpData.Nodes.Count());
for (int32 i = 0; i < assimpData.Nodes.Count(); i++)
{
auto& node = data.Nodes[i];
auto& aNode = assimpData.Nodes[i];
node.Name = aNode.Name;
node.ParentIndex = aNode.ParentIndex;
node.LocalTransform = aNode.LocalTransform;
}
}
return false;
}
#endif

View File

@@ -0,0 +1,980 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MODEL_TOOL && USE_AUTODESK_FBX_SDK
#include "ModelTool.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Threading/Threading.h"
// Import Autodesk FBX SDK
#define FBXSDK_NEW_API
#include <fbxsdk.h>
class FbxSdkManager
{
public:
static FbxManager* Manager;
static CriticalSection Locker;
static void Init()
{
if (Manager == nullptr)
{
LOG_STR(Info, String("Autodesk FBX SDK " FBXSDK_VERSION_STRING_FULL));
Manager = FbxManager::Create();
if (Manager == nullptr)
{
LOG(Fatal, "Autodesk FBX SDK failed to initialize.");
return;
}
FbxIOSettings* ios = FbxIOSettings::Create(Manager, IOSROOT);
ios->SetBoolProp(IMP_FBX_TEXTURE, false);
ios->SetBoolProp(IMP_FBX_GOBO, false);
Manager->SetIOSettings(ios);
}
}
};
FbxManager* FbxSdkManager::Manager = nullptr;
CriticalSection FbxSdkManager::Locker;
Matrix ToFlaxType(const FbxAMatrix& value)
{
Matrix native;
for (int32 row = 0; row < 4; row++)
for (int32 col = 0; col < 4; col++)
native.Values[row][col] = (float)value[col][row];
return native;
}
Vector3 ToFlaxType(const FbxVector4& value)
{
Vector3 native;
native.X = (float)value[0];
native.Y = (float)value[1];
native.Z = (float)value[2];
return native;
}
Vector3 ToFlaxType(const FbxDouble3& value)
{
Vector3 native;
native.X = (float)value[0];
native.Y = (float)value[1];
native.Z = (float)value[2];
return native;
}
Vector2 ToFlaxType(const FbxVector2& value)
{
Vector2 native;
native.X = (float)value[0];
native.Y = 1 - (float)value[1];
return native;
}
Color ToFlaxType(const FbxColor& value)
{
Color native;
native.R = (float)value[0];
native.G = (float)value[1];
native.B = (float)value[2];
native.A = (float)value[3];
return native;
}
int ToFlaxType(const int& value)
{
return value;
}
/// <summary>
/// Represents a single node in the FBX transform hierarchy.
/// </summary>
struct Node
{
/// <summary>
/// The parent index. The root node uses value -1.
/// </summary>
int32 ParentIndex;
/// <summary>
/// The local transformation of the node, relative to parent node.
/// </summary>
Transform LocalTransform;
/// <summary>
/// The name of this node.
/// </summary>
String Name;
/// <summary>
/// The LOD index of the data in this node (used to separate meshes across different level of details).
/// </summary>
int32 LodIndex;
Matrix GeomTransform;
Matrix WorldTransform;
FbxNode* FbxNode;
};
struct Bone
{
/// <summary>
/// The index of the related node.
/// </summary>
int32 NodeIndex;
/// <summary>
/// The parent bone index. The root bone uses value -1.
/// </summary>
int32 ParentBoneIndex;
/// <summary>
/// The name of this bone.
/// </summary>
String Name;
/// <summary>
/// The matrix that transforms from mesh space to bone space in bind pose.
/// </summary>
Matrix OffsetMatrix;
};
struct ImporterData
{
ImportedModelData& Model;
const FbxScene* Scene;
const ModelTool::Options& Options;
Array<Node> Nodes;
Array<Bone> Bones;
Dictionary<FbxMesh*, MeshData*> Meshes;
Array<FbxSurfaceMaterial*> Materials;
ImporterData(ImportedModelData& model, const ModelTool::Options& options, const FbxScene* scene)
: Model(model)
, Scene(scene)
, Options(options)
, Nodes(256)
, Meshes(256)
, Materials(64)
{
}
int32 FindNode(FbxNode* fbxNode)
{
for (int32 i = 0; i < Nodes.Count(); i++)
{
if (Nodes[i].FbxNode == fbxNode)
return i;
}
return -1;
}
int32 FindNode(const String& name, StringSearchCase caseSensitivity = StringSearchCase::CaseSensitive)
{
for (int32 i = 0; i < Nodes.Count(); i++)
{
if (Nodes[i].Name.Compare(name, caseSensitivity) == 0)
return i;
}
return -1;
}
int32 FindBone(const String& name, StringSearchCase caseSensitivity = StringSearchCase::CaseSensitive)
{
for (int32 i = 0; i < Bones.Count(); i++)
{
if (Bones[i].Name.Compare(name, caseSensitivity) == 0)
return i;
}
return -1;
}
int32 FindBone(const int32 nodeIndex)
{
for (int32 i = 0; i < Bones.Count(); i++)
{
if (Bones[i].NodeIndex == nodeIndex)
return i;
}
return -1;
}
};
void ProcessNodes(ImporterData& data, FbxNode* fbxNode, int32 parentIndex)
{
const int32 nodeIndex = data.Nodes.Count();
Vector3 translation = ToFlaxType(fbxNode->EvaluateLocalTranslation(FbxTime(0)));
Vector3 rotationEuler = ToFlaxType(fbxNode->EvaluateLocalRotation(FbxTime(0)));
Vector3 scale = ToFlaxType(fbxNode->EvaluateLocalScaling(FbxTime(0)));
Quaternion rotation = Quaternion::Euler(rotationEuler);
// Create node
Node node;
node.ParentIndex = parentIndex;
node.Name = String(fbxNode->GetNameWithoutNameSpacePrefix().Buffer());
node.LocalTransform = Transform(translation, rotation, scale);
node.FbxNode = fbxNode;
// Geometry transform is applied to geometry (mesh data) only, it is not inherited by children, so we store it separately
Vector3 geomTrans = ToFlaxType(fbxNode->GeometricTranslation.Get());
Vector3 geomRotEuler = ToFlaxType(fbxNode->GeometricRotation.Get());
Vector3 geomScale = ToFlaxType(fbxNode->GeometricScaling.Get());
Quaternion geomRotation = Quaternion::Euler(geomRotEuler);
Transform(geomTrans, geomRotation, geomScale).GetWorld(node.GeomTransform);
// Pick node LOD index
if (parentIndex == -1 || !data.Options.ImportLODs)
{
node.LodIndex = 0;
}
else
{
node.LodIndex = data.Nodes[parentIndex].LodIndex;
if (node.LodIndex == 0)
{
node.LodIndex = ModelTool::DetectLodIndex(node.Name);
}
ASSERT(Math::IsInRange(node.LodIndex, 0, MODEL_MAX_LODS - 1));
}
if (parentIndex == -1)
{
node.LocalTransform.GetWorld(node.WorldTransform);
}
else
{
node.WorldTransform = data.Nodes[parentIndex].WorldTransform * node.LocalTransform.GetWorld();
}
data.Nodes.Add(node);
// Process the children
for (int i = 0; i < fbxNode->GetChildCount(); i++)
{
ProcessNodes(data, fbxNode->GetChild(i), nodeIndex);
}
}
template<class TFBX, class TNative>
void ReadLayerData(FbxMesh* fbxMesh, FbxLayerElementTemplate<TFBX>& layer, Array<TNative>& output)
{
if (layer.GetDirectArray().GetCount() == 0)
return;
int32 vertexCount = fbxMesh->GetControlPointsCount();
int32 triangleCount = fbxMesh->GetPolygonCount();
output.Resize(vertexCount);
switch (layer.GetMappingMode())
{
case FbxLayerElement::eByControlPoint:
{
for (int vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++)
{
int index = 0;
if (layer.GetReferenceMode() == FbxGeometryElement::eDirect)
index = vertexIndex;
else if (layer.GetReferenceMode() == FbxGeometryElement::eIndexToDirect)
index = layer.GetIndexArray().GetAt(vertexIndex);
output[vertexIndex] = ToFlaxType(layer.GetDirectArray().GetAt(index));
}
}
break;
case FbxLayerElement::eByPolygonVertex:
{
int indexByPolygonVertex = 0;
for (int polygonIndex = 0; polygonIndex < triangleCount; polygonIndex++)
{
const int polygonSize = fbxMesh->GetPolygonSize(polygonIndex);
for (int i = 0; i < polygonSize; i++)
{
int index = 0;
if (layer.GetReferenceMode() == FbxGeometryElement::eDirect)
index = indexByPolygonVertex;
else if (layer.GetReferenceMode() == FbxGeometryElement::eIndexToDirect)
index = layer.GetIndexArray().GetAt(indexByPolygonVertex);
int vertexIndex = fbxMesh->GetPolygonVertex(polygonIndex, i);
output[vertexIndex] = ToFlaxType(layer.GetDirectArray().GetAt(index));
indexByPolygonVertex++;
}
}
}
break;
case FbxLayerElement::eAllSame:
{
output[0] = ToFlaxType(layer.GetDirectArray().GetAt(0));
for (int vertexIndex = 1; vertexIndex < vertexCount; vertexIndex++)
{
output[vertexIndex] = output[0];
}
}
break;
default:
LOG(Warning, "Unsupported layer mapping mode.");
break;
}
}
bool IsGroupMappingModeByEdge(FbxLayerElement* layerElement)
{
return layerElement->GetMappingMode() == FbxLayerElement::eByEdge;
}
bool ProcessMesh(ImporterData& data, FbxMesh* fbxMesh, MeshData& mesh, String& errorMsg)
{
// Properties
mesh.Name = fbxMesh->GetName();
mesh.MaterialSlotIndex = -1;
if (fbxMesh->GetElementMaterial())
{
const auto materialIndices = &(fbxMesh->GetElementMaterial()->GetIndexArray());
if (materialIndices)
{
mesh.MaterialSlotIndex = materialIndices->GetAt(0);
}
}
int32 vertexCount = fbxMesh->GetControlPointsCount();
int32 triangleCount = fbxMesh->GetPolygonCount();
FbxVector4* controlPoints = fbxMesh->GetControlPoints();
FbxGeometryElementNormal* normalElement = fbxMesh->GetElementNormal();
FbxGeometryElementTangent* tangentElement = fbxMesh->GetElementTangent();
// Regenerate data if necessary
if (normalElement == nullptr || data.Options.CalculateNormals)
{
fbxMesh->GenerateNormals(true, false, false);
normalElement = fbxMesh->GetElementNormal();
}
if (tangentElement == nullptr || data.Options.CalculateTangents)
{
fbxMesh->GenerateTangentsData(0, true);
tangentElement = fbxMesh->GetElementTangent();
}
bool needEdgeIndexing = false;
if (normalElement)
needEdgeIndexing |= IsGroupMappingModeByEdge(normalElement);
// Vertex positions
mesh.Positions.Resize(vertexCount, false);
for (int32 i = 0; i < vertexCount; i++)
{
mesh.Positions[i] = ToFlaxType(controlPoints[i]);
}
// Indices
const int32 indexCount = triangleCount * 3;
mesh.Indices.Resize(indexCount, false);
int* fbxIndices = fbxMesh->GetPolygonVertices();
for (int32 i = 0; i < indexCount; i++)
{
mesh.Indices[i] = fbxIndices[i];
}
// Texture coordinates
FbxGeometryElementUV* texcoords = fbxMesh->GetElementUV(0);
if (texcoords)
{
ReadLayerData(fbxMesh, *texcoords, mesh.UVs);
}
// Normals
if (normalElement)
{
ReadLayerData(fbxMesh, *normalElement, mesh.Normals);
}
// Tangents
if (tangentElement)
{
ReadLayerData(fbxMesh, *tangentElement, mesh.Tangents);
}
// Lightmap UVs
if (data.Options.LightmapUVsSource == ModelLightmapUVsSource::Disable)
{
// No lightmap UVs
}
else if (data.Options.LightmapUVsSource == ModelLightmapUVsSource::Generate)
{
// Generate lightmap UVs
if (mesh.GenerateLightmapUVs())
{
// TODO: we could propagate this message to Debug Console in editor? or create interface to gather some msgs from importing service
LOG(Warning, "Failed to generate lightmap uvs");
}
}
else
{
// Select input channel index
int32 inputChannelIndex;
switch (data.Options.LightmapUVsSource)
{
case ModelLightmapUVsSource::Channel0:
inputChannelIndex = 0;
break;
case ModelLightmapUVsSource::Channel1:
inputChannelIndex = 1;
break;
case ModelLightmapUVsSource::Channel2:
inputChannelIndex = 2;
break;
case ModelLightmapUVsSource::Channel3:
inputChannelIndex = 3;
break;
default:
inputChannelIndex = INVALID_INDEX;
break;
}
// Check if has that channel texcoords
if (inputChannelIndex >= 0 && inputChannelIndex < fbxMesh->GetElementUVCount() && fbxMesh->GetElementUV(inputChannelIndex))
{
ReadLayerData(fbxMesh, *fbxMesh->GetElementUV(inputChannelIndex), mesh.LightmapUVs);
}
else
{
// TODO: we could propagate this message to Debug Console in editor? or create interface to gather some msgs from importing service
LOG(Warning, "Cannot import model lightmap uvs. Missing texcoords channel {0}.", inputChannelIndex);
}
}
// Vertex Colors
if (data.Options.ImportVertexColors && fbxMesh->GetElementVertexColorCount() > 0)
{
auto vertexColorElement = fbxMesh->GetElementVertexColor(0);
ReadLayerData(fbxMesh, *vertexColorElement, mesh.Colors);
}
// Blend Indices and Blend Weights
const int skinDeformerCount = fbxMesh->GetDeformerCount(FbxDeformer::eSkin);
if (skinDeformerCount > 0)
{
const int32 vertexCount = mesh.Positions.Count();
mesh.BlendIndices.Resize(vertexCount);
mesh.BlendWeights.Resize(vertexCount);
mesh.BlendIndices.SetAll(Int4::Zero);
mesh.BlendWeights.SetAll(Vector4::Zero);
for (int deformerIndex = 0; deformerIndex < skinDeformerCount; deformerIndex++)
{
FbxSkin* skin = FbxCast<FbxSkin>(fbxMesh->GetDeformer(deformerIndex, FbxDeformer::eSkin));
int totalClusterCount = skin->GetClusterCount();
for (int clusterIndex = 0; clusterIndex < totalClusterCount; ++clusterIndex)
{
FbxCluster* cluster = skin->GetCluster(clusterIndex);
int indexCount = cluster->GetControlPointIndicesCount();
if (indexCount == 0)
continue;
FbxNode* link = cluster->GetLink();
const String boneName(link->GetName());
// Find the node where the bone is mapped - based on the name
const int32 nodeIndex = data.FindNode(link);
if (nodeIndex == -1)
{
LOG(Warning, "Invalid mesh bone linkage. Mesh: {0}, bone: {1}. Skipping...", mesh.Name, boneName);
continue;
}
// Create bone if missing
int32 boneIndex = data.FindBone(boneName);
if (boneIndex == -1)
{
// Find the parent bone
int32 parentBoneIndex = -1;
for (int32 i = nodeIndex; i != -1; i = data.Nodes[i].ParentIndex)
{
parentBoneIndex = data.FindBone(i);
if (parentBoneIndex != -1)
break;
}
// Add bone
boneIndex = data.Bones.Count();
data.Bones.EnsureCapacity(Math::Max(128, boneIndex + 16));
data.Bones.Resize(boneIndex + 1);
auto& bone = data.Bones[boneIndex];
FbxAMatrix transformMatrix;
FbxAMatrix transformLinkMatrix;
cluster->GetTransformMatrix(transformMatrix);
cluster->GetTransformLinkMatrix(transformLinkMatrix);
const auto globalBindposeInverseMatrix = transformLinkMatrix.Inverse() * transformMatrix;
// Setup bone
bone.Name = boneName;
bone.NodeIndex = nodeIndex;
bone.ParentBoneIndex = parentBoneIndex;
bone.OffsetMatrix = ToFlaxType(globalBindposeInverseMatrix);
}
// Apply the bone influences
int* cluserIndices = cluster->GetControlPointIndices();
double* cluserWeights = cluster->GetControlPointWeights();
for (int j = 0; j < indexCount; j++)
{
const int vtxWeightId = cluserIndices[j];
if (vtxWeightId >= vertexCount)
continue;
const auto vtxWeight = (float)cluserWeights[j];
if (vtxWeight <= 0 || isnan(vtxWeight) || isinf(vtxWeight))
continue;
auto& indices = mesh.BlendIndices[vtxWeightId];
auto& weights = mesh.BlendWeights[vtxWeightId];
for (int32 k = 0; k < 4; k++)
{
if (vtxWeight >= weights.Raw[k])
{
for (int32 l = 2; l >= k; l--)
{
indices.Raw[l + 1] = indices.Raw[l];
weights.Raw[l + 1] = weights.Raw[l];
}
indices.Raw[k] = boneIndex;
weights.Raw[k] = vtxWeight;
break;
}
}
}
}
}
mesh.NormalizeBlendWeights();
}
// Blend Shapes
const int blendShapeDeformerCount = fbxMesh->GetDeformerCount(FbxDeformer::eBlendShape);
if (blendShapeDeformerCount > 0 && data.Model.Types & ImportDataTypes::Skeleton && data.Options.ImportBlendShapes)
{
mesh.BlendShapes.EnsureCapacity(blendShapeDeformerCount);
for (int deformerIndex = 0; deformerIndex < skinDeformerCount; deformerIndex++)
{
FbxBlendShape* blendShape = FbxCast<FbxBlendShape>(fbxMesh->GetDeformer(deformerIndex, FbxDeformer::eBlendShape));
const int blendShapeChannelCount = blendShape->GetBlendShapeChannelCount();
for (int32 channelIndex = 0; channelIndex < blendShapeChannelCount; channelIndex++)
{
FbxBlendShapeChannel* blendShapeChannel = blendShape->GetBlendShapeChannel(channelIndex);
// Use last shape
const int shapeCount = blendShapeChannel->GetTargetShapeCount();
if (shapeCount == 0)
continue;
FbxShape* shape = blendShapeChannel->GetTargetShape(shapeCount - 1);
int shapeControlPointsCount = shape->GetControlPointsCount();
if (shapeControlPointsCount != vertexCount)
continue;
BlendShape& blendShapeData = mesh.BlendShapes.AddOne();
blendShapeData.Name = blendShapeChannel->GetName();
const auto dotPos = blendShapeData.Name.Find('.');
if (dotPos != -1)
blendShapeData.Name = blendShapeData.Name.Substring(dotPos + 1);
blendShapeData.Weight = blendShapeChannel->GetTargetShapeCount() > 1 ? (float)(blendShapeChannel->DeformPercent.Get() / 100.0) : 1.0f;
FbxVector4* shapeControlPoints = shape->GetControlPoints();
blendShapeData.Vertices.Resize(shapeControlPointsCount);
for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
blendShapeData.Vertices[i].VertexIndex = i;
for (int i = 0; i < blendShapeData.Vertices.Count(); i++)
blendShapeData.Vertices[i].PositionDelta = ToFlaxType(shapeControlPoints[i] - controlPoints[i]);
// TODO: support importing normals from blend shape
for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
blendShapeData.Vertices[i].NormalDelta = Vector3::Zero;
}
}
}
// Flip the Y in texcoords
for (int32 i = 0; i < mesh.UVs.Count(); i++)
mesh.UVs[i].Y = 1.0f - mesh.UVs[i].Y;
for (int32 i = 0; i < mesh.LightmapUVs.Count(); i++)
mesh.LightmapUVs[i].Y = 1.0f - mesh.LightmapUVs[i].Y;
// Handle missing material case (could never happen but it's better to be sure it will work)
if (mesh.MaterialSlotIndex == -1)
{
mesh.MaterialSlotIndex = 0;
LOG(Warning, "Mesh \'{0}\' has missing material slot.", mesh.Name);
}
return false;
}
bool ImportMesh(ImporterData& data, int32 nodeIndex, FbxMesh* fbxMesh, String& errorMsg)
{
auto& model = data.Model;
// Skip invalid meshes
if (!fbxMesh->IsTriangleMesh() || fbxMesh->GetControlPointsCount() == 0 || fbxMesh->GetPolygonCount() == 0)
return false;
// Check if that mesh has been already imported (instanced geometry)
MeshData* meshData = nullptr;
if (data.Meshes.TryGet(fbxMesh, meshData) && meshData)
{
// Clone mesh
meshData = New<MeshData>(*meshData);
}
else
{
// Import mesh data
meshData = New<MeshData>();
if (ProcessMesh(data, fbxMesh, *meshData, errorMsg))
return true;
}
// Link mesh
meshData->NodeIndex = nodeIndex;
auto& node = data.Nodes[nodeIndex];
const auto lodIndex = node.LodIndex;
if (model.LODs.Count() <= lodIndex)
model.LODs.Resize(lodIndex + 1);
model.LODs[lodIndex].Meshes.Add(meshData);
return false;
}
bool ImportMesh(ImporterData& data, int32 nodeIndex, String& errorMsg)
{
auto fbxNode = data.Nodes[nodeIndex].FbxNode;
// Process the node's attributes
for (int i = 0; i < fbxNode->GetNodeAttributeCount(); i++)
{
auto attribute = fbxNode->GetNodeAttributeByIndex(i);
if (!attribute)
continue;
switch (attribute->GetAttributeType())
{
case FbxNodeAttribute::eNurbs:
case FbxNodeAttribute::eNurbsSurface:
case FbxNodeAttribute::ePatch:
{
FbxGeometryConverter geomConverter(FbxSdkManager::Manager);
attribute = geomConverter.Triangulate(attribute, true);
if (attribute->GetAttributeType() == FbxNodeAttribute::eMesh)
{
FbxMesh* mesh = static_cast<FbxMesh*>(attribute);
mesh->RemoveBadPolygons();
if (ImportMesh(data, nodeIndex, mesh, errorMsg))
return true;
}
}
break;
case FbxNodeAttribute::eMesh:
{
FbxMesh* mesh = static_cast<FbxMesh*>(attribute);
mesh->RemoveBadPolygons();
if (!mesh->IsTriangleMesh())
{
FbxGeometryConverter geomConverter(FbxSdkManager::Manager);
geomConverter.Triangulate(mesh, true);
attribute = fbxNode->GetNodeAttribute();
mesh = static_cast<FbxMesh*>(attribute);
}
if (ImportMesh(data, nodeIndex, mesh, errorMsg))
return true;
}
break;
default:
break;
}
}
return false;
}
bool ImportMeshes(ImporterData& data, String& errorMsg)
{
for (int32 i = 0; i < data.Nodes.Count(); i++)
{
if (ImportMesh(data, i, errorMsg))
return true;
}
return false;
}
/*
void ImportCurve(aiVectorKey* keys, uint32 keysCount, LinearCurve<Vector3>& curve)
{
if (keys == nullptr || keysCount == 0)
return;
const auto keyframes = curve.Resize(keysCount);
for (uint32 i = 0; i < keysCount; i++)
{
auto& aKey = keys[i];
auto& key = keyframes[i];
key.Time = (float)aKey.mTime;
key.Value = ToVector3(aKey.mValue);
}
}
void ImportCurve(aiQuatKey* keys, uint32 keysCount, LinearCurve<Quaternion>& curve)
{
if (keys == nullptr || keysCount == 0)
return;
const auto keyframes = curve.Resize(keysCount);
for (uint32 i = 0; i < keysCount; i++)
{
auto& aKey = keys[i];
auto& key = keyframes[i];
key.Time = (float)aKey.mTime;
key.Value = ToQuaternion(aKey.mValue);
}
}
*/
/// <summary>
/// Bakes the node transformations.
/// </summary>
/// <remarks>
/// FBX stores transforms in a more complex way than just translation-rotation-scale as used by Flax Engine.
/// Instead they also support rotations offsets and pivots, scaling pivots and more. We wish to bake all this data
/// into a standard transform so we can access it using node's local TRS properties (e.g. FbxNode::LclTranslation).
/// </remarks>
/// <param name="scene">The FBX scene.</param>
void BakeTransforms(FbxScene* scene)
{
double frameRate = FbxTime::GetFrameRate(scene->GetGlobalSettings().GetTimeMode());
Array<FbxNode*> todo;
todo.Push(scene->GetRootNode());
while (todo.HasItems())
{
FbxNode* node = todo.Pop();
FbxVector4 zero(0, 0, 0);
FbxVector4 one(1, 1, 1);
// Activate pivot converting
node->SetPivotState(FbxNode::eSourcePivot, FbxNode::ePivotActive);
node->SetPivotState(FbxNode::eDestinationPivot, FbxNode::ePivotActive);
// We want to set all these to 0 (1 for scale) and bake them into the transforms
node->SetPostRotation(FbxNode::eDestinationPivot, zero);
node->SetPreRotation(FbxNode::eDestinationPivot, zero);
node->SetRotationOffset(FbxNode::eDestinationPivot, zero);
node->SetScalingOffset(FbxNode::eDestinationPivot, zero);
node->SetRotationPivot(FbxNode::eDestinationPivot, zero);
node->SetScalingPivot(FbxNode::eDestinationPivot, zero);
// We account for geometric properties separately during node traversal
node->SetGeometricTranslation(FbxNode::eDestinationPivot, node->GetGeometricTranslation(FbxNode::eSourcePivot));
node->SetGeometricRotation(FbxNode::eDestinationPivot, node->GetGeometricRotation(FbxNode::eSourcePivot));
node->SetGeometricScaling(FbxNode::eDestinationPivot, node->GetGeometricScaling(FbxNode::eSourcePivot));
// Flax assumes euler angles are in YXZ order
node->SetRotationOrder(FbxNode::eDestinationPivot, FbxEuler::eOrderYXZ);
// Keep interpolation as is
node->SetQuaternionInterpolation(FbxNode::eDestinationPivot, node->GetQuaternionInterpolation(FbxNode::eSourcePivot));
for (int i = 0; i < node->GetChildCount(); i++)
{
FbxNode* childNode = node->GetChild(i);
todo.Push(childNode);
}
}
scene->GetRootNode()->ConvertPivotAnimationRecursive(nullptr, FbxNode::eDestinationPivot, frameRate, false);
}
bool ModelTool::ImportDataAutodeskFbxSdk(const char* path, ImportedModelData& data, const Options& options, String& errorMsg)
{
ScopeLock lock(FbxSdkManager::Locker);
// Initialize
FbxSdkManager::Init();
auto scene = FbxScene::Create(FbxSdkManager::Manager, "Scene");
if (scene == nullptr)
{
errorMsg = TEXT("Failed to create FBX scene");
return false;
}
// Import file
bool importMeshes = (data.Types & ImportDataTypes::Geometry) != 0;
bool importAnimations = (data.Types & ImportDataTypes::Animations) != 0;
FbxImporter* importer = FbxImporter::Create(FbxSdkManager::Manager, "");
auto ios = FbxSdkManager::Manager->GetIOSettings();
ios->SetBoolProp(IMP_FBX_MODEL, importMeshes);
ios->SetBoolProp(IMP_FBX_ANIMATION, importAnimations);
if (!importer->Initialize(path, -1, ios))
{
errorMsg = String::Format(TEXT("Failed to initialize FBX importer. {0}"), String(importer->GetStatus().GetErrorString()));
return false;
}
if (!importer->Import(scene))
{
errorMsg = String::Format(TEXT("Failed to import FBX scene. {0}"), String(importer->GetStatus().GetErrorString()));
importer->Destroy();
return false;
}
{
const FbxAxisSystem fileCoordSystem = scene->GetGlobalSettings().GetAxisSystem();
FbxAxisSystem bsCoordSystem(FbxAxisSystem::eDirectX);
if (fileCoordSystem != bsCoordSystem)
bsCoordSystem.ConvertScene(scene);
}
importer->Destroy();
importer = nullptr;
BakeTransforms(scene);
// TODO: optimizeMeshes
// Process imported scene nodes
ImporterData importerData(data, options, scene);
ProcessNodes(importerData, scene->GetRootNode(), -1);
// Add all materials
for (int i = 0; i < scene->GetMaterialCount(); i++)
{
importerData.Materials.Add(scene->GetMaterial(i));
}
// Import geometry (meshes and materials)
if (data.Types & ImportDataTypes::Geometry)
{
if (ImportMeshes(importerData, errorMsg))
{
LOG(Warning, "Failed to import meshes.");
return true;
}
}
// TODO: remove unused materials if meshes merging is disabled
// Import skeleton
if (data.Types & ImportDataTypes::Skeleton)
{
data.Skeleton.Nodes.Resize(importerData.Nodes.Count(), false);
for (int32 i = 0; i < importerData.Nodes.Count(); i++)
{
auto& node = data.Skeleton.Nodes[i];
auto& fbxNode = importerData.Nodes[i];
node.Name = fbxNode.Name;
node.ParentIndex = fbxNode.ParentIndex;
node.LocalTransform = fbxNode.LocalTransform;
}
data.Skeleton.Bones.Resize(importerData.Bones.Count(), false);
for (int32 i = 0; i < importerData.Bones.Count(); i++)
{
auto& bone = data.Skeleton.Bones[i];
auto& fbxBone = importerData.Bones[i];
const auto boneNodeIndex = fbxBone.NodeIndex;
const auto parentBoneNodeIndex = fbxBone.ParentBoneIndex == -1 ? -1 : importerData.Bones[fbxBone.ParentBoneIndex].NodeIndex;
bone.ParentIndex = fbxBone.ParentBoneIndex;
bone.NodeIndex = fbxBone.NodeIndex;
bone.LocalTransform = CombineTransformsFromNodeIndices(importerData.Nodes, parentBoneNodeIndex, boneNodeIndex);
bone.OffsetMatrix = fbxBone.OffsetMatrix;
}
}
/*
// Import animations
if (data.Types & ImportDataTypes::Animations)
{
if (scene->HasAnimations())
{
const auto animations = scene->mAnimations[0];
data.Animation.Channels.Resize(animations->mNumChannels, false);
data.Animation.Duration = animations->mDuration;
data.Animation.FramesPerSecond = animations->mTicksPerSecond != 0.0 ? animations->mTicksPerSecond : 25.0;
for (unsigned i = 0; i < animations->mNumChannels; i++)
{
const auto aAnim = animations->mChannels[i];
auto& anim = data.Animation.Channels[i];
anim.NodeName = aAnim->mNodeName.C_Str();
ImportCurve(aAnim->mPositionKeys, aAnim->mNumPositionKeys, anim.Position);
ImportCurve(aAnim->mRotationKeys, aAnim->mNumRotationKeys, anim.Rotation);
ImportCurve(aAnim->mScalingKeys, aAnim->mNumScalingKeys, anim.Scale);
}
}
else
{
LOG(Warning, "Loaded scene has no animations");
}
}
*/
// Import nodes
if (data.Types & ImportDataTypes::Nodes)
{
data.Nodes.Resize(importerData.Nodes.Count());
for (int32 i = 0; i < importerData.Nodes.Count(); i++)
{
auto& node = data.Nodes[i];
auto& aNode = importerData.Nodes[i];
node.Name = aNode.Name;
node.ParentIndex = aNode.ParentIndex;
node.LocalTransform = aNode.LocalTransform;
}
}
// Export materials info
const int32 materialsCount = importerData.Materials.Count();
data.Materials.Resize(materialsCount, false);
for (int32 i = 0; i < importerData.Materials.Count(); i++)
{
auto& material = data.Materials[i];
const auto fbxMaterial = importerData.Materials[i];
material.Name = String(fbxMaterial->GetName()).TrimTrailing();
material.MaterialID = Guid::Empty;
}
scene->Clear();
return false;
}
#endif

View File

@@ -0,0 +1,64 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using System.IO;
using Flax.Build;
using Flax.Build.NativeCpp;
/// <summary>
/// Model data utilities module.
/// </summary>
public class ModelTool : EngineModule
{
/// <inheritdoc />
public override void Setup(BuildOptions options)
{
base.Setup(options);
bool useAssimp = true;
bool useAutodeskFbxSdk = false;
bool useOpenFBX = true;
if (useAssimp)
{
options.PrivateDependencies.Add("assimp");
options.PrivateDefinitions.Add("USE_ASSIMP");
}
if (useAutodeskFbxSdk)
{
options.PrivateDefinitions.Add("USE_AUTODESK_FBX_SDK");
// FBX SDK 2020.0.1 VS2015
// TODO: convert this into AutodeskFbxSdk and implement proper SDK lookup with multiple versions support
// TODO: link against dll with delay loading
var sdkRoot = @"C:\Program Files\Autodesk\FBX\FBX SDK\2020.0.1";
var libSubDir = "lib\\vs2015\\x64\\release";
options.PrivateIncludePaths.Add(Path.Combine(sdkRoot, "include"));
options.OutputFiles.Add(Path.Combine(sdkRoot, libSubDir, "libfbxsdk-md.lib"));
options.OutputFiles.Add(Path.Combine(sdkRoot, libSubDir, "zlib-md.lib"));
options.OutputFiles.Add(Path.Combine(sdkRoot, libSubDir, "libxml2-md.lib"));
}
if (useOpenFBX)
{
options.PrivateDependencies.Add("OpenFBX");
options.PrivateDefinitions.Add("USE_OPEN_FBX");
}
options.PrivateDependencies.Add("TextureTool");
options.PrivateDefinitions.Add("COMPILE_WITH_ASSETS_IMPORTER");
options.PrivateDependencies.Add("DirectXMesh");
options.PrivateDependencies.Add("UVAtlas");
options.PrivateDependencies.Add("meshoptimizer");
options.PublicDefinitions.Add("COMPILE_WITH_MODEL_TOOL");
}
/// <inheritdoc />
public override void GetFilesToDeploy(List<string> files)
{
files.Add(Path.Combine(FolderPath, "ModelTool.h"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MODEL_TOOL
#include "ModelTool.h"
#include "Engine/Core/Log.h"
#include "Engine/Serialization/Serialization.h"
BoundingBox ImportedModelData::LOD::GetBox() const
{
if (Meshes.IsEmpty())
return BoundingBox::Empty;
BoundingBox box;
Meshes[0]->CalculateBox(box);
for (int32 i = 1; i < Meshes.Count(); i++)
{
if (Meshes[i]->Positions.HasItems())
{
BoundingBox t;
Meshes[i]->CalculateBox(t);
BoundingBox::Merge(box, t, box);
}
}
return box;
}
void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj)
{
SERIALIZE_GET_OTHER_OBJ(ModelTool::Options);
SERIALIZE(Type);
SERIALIZE(CalculateNormals);
SERIALIZE(SmoothingNormalsAngle);
SERIALIZE(FlipNormals);
SERIALIZE(CalculateTangents);
SERIALIZE(SmoothingTangentsAngle);
SERIALIZE(OptimizeMeshes);
SERIALIZE(MergeMeshes);
SERIALIZE(ImportLODs);
SERIALIZE(ImportVertexColors);
SERIALIZE(ImportBlendShapes);
SERIALIZE(LightmapUVsSource);
SERIALIZE(Scale);
SERIALIZE(Rotation);
SERIALIZE(Translation);
SERIALIZE(CenterGeometry);
SERIALIZE(Duration);
SERIALIZE(FramesRange);
SERIALIZE(DefaultFrameRate);
SERIALIZE(SamplingRate);
SERIALIZE(SkipEmptyCurves);
SERIALIZE(OptimizeKeyframes);
SERIALIZE(EnableRootMotion);
SERIALIZE(RootNodeName);
SERIALIZE(AnimationIndex);
SERIALIZE(GenerateLODs);
SERIALIZE(BaseLOD);
SERIALIZE(LODCount);
SERIALIZE(TriangleReduction);
SERIALIZE(ImportMaterials);
SERIALIZE(ImportTextures);
SERIALIZE(RestoreMaterialsOnReimport);
}
void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
DESERIALIZE(Type);
DESERIALIZE(CalculateNormals);
DESERIALIZE(SmoothingNormalsAngle);
DESERIALIZE(FlipNormals);
DESERIALIZE(CalculateTangents);
DESERIALIZE(SmoothingTangentsAngle);
DESERIALIZE(OptimizeMeshes);
DESERIALIZE(MergeMeshes);
DESERIALIZE(ImportLODs);
DESERIALIZE(ImportVertexColors);
DESERIALIZE(ImportBlendShapes);
DESERIALIZE(LightmapUVsSource);
DESERIALIZE(Scale);
DESERIALIZE(Rotation);
DESERIALIZE(Translation);
DESERIALIZE(CenterGeometry);
DESERIALIZE(Duration);
DESERIALIZE(FramesRange);
DESERIALIZE(DefaultFrameRate);
DESERIALIZE(SamplingRate);
DESERIALIZE(SkipEmptyCurves);
DESERIALIZE(OptimizeKeyframes);
DESERIALIZE(EnableRootMotion);
DESERIALIZE(RootNodeName);
DESERIALIZE(AnimationIndex);
DESERIALIZE(GenerateLODs);
DESERIALIZE(BaseLOD);
DESERIALIZE(LODCount);
DESERIALIZE(TriangleReduction);
DESERIALIZE(ImportMaterials);
DESERIALIZE(ImportTextures);
DESERIALIZE(RestoreMaterialsOnReimport);
}
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,280 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#if COMPILE_WITH_MODEL_TOOL
#include "Engine/Core/Config.h"
#include "Engine/Serialization/ISerializable.h"
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Graphics/Models/SkeletonData.h"
#include "Engine/Animations/AnimationData.h"
class JsonWriter;
/// <summary>
/// The model file import data types (used as flags).
/// </summary>
enum class ImportDataTypes : int32
{
/// <summary>
/// Imports materials and meshes.
/// </summary>
Geometry = 1 << 0,
/// <summary>
/// Imports the skeleton bones hierarchy.
/// </summary>
Skeleton = 1 << 1,
/// <summary>
/// Imports the animations.
/// </summary>
Animations = 1 << 2,
/// <summary>
/// Imports the scene nodes hierarchy.
/// </summary>
Nodes = 1 << 3,
/// <summary>
/// Imports the materials.
/// </summary>
Materials = 1 << 4,
/// <summary>
/// Imports the textures.
/// </summary>
Textures = 1 << 5,
};
DECLARE_ENUM_OPERATORS(ImportDataTypes);
/// <summary>
/// Imported model data container. Represents unified model source file data (meshes, animations, skeleton, materials).
/// </summary>
class ImportedModelData
{
public:
struct LOD
{
Array<MeshData*> Meshes;
BoundingBox GetBox() const;
};
struct Node
{
/// <summary>
/// The parent node index. The root node uses value -1.
/// </summary>
int32 ParentIndex;
/// <summary>
/// The local transformation of the node, relative to the parent node.
/// </summary>
Transform LocalTransform;
/// <summary>
/// The name of this node.
/// </summary>
String Name;
};
public:
/// <summary>
/// The import data types types.
/// </summary>
ImportDataTypes Types;
/// <summary>
/// The textures slots.
/// </summary>
Array<TextureEntry> Textures;
/// <summary>
/// The material slots.
/// </summary>
Array<MaterialSlotEntry> Materials;
/// <summary>
/// The level of details data.
/// </summary>
Array<LOD> LODs;
/// <summary>
/// The skeleton data.
/// </summary>
SkeletonData Skeleton;
/// <summary>
/// The scene nodes.
/// </summary>
Array<Node> Nodes;
/// <summary>
/// The node animations.
/// </summary>
AnimationData Animation;
public:
/// <summary>
/// Initializes a new instance of the <see cref="ImportedModelData"/> class.
/// </summary>
/// <param name="types">The types.</param>
ImportedModelData(ImportDataTypes types)
{
Types = types;
}
/// <summary>
/// Finalizes an instance of the <see cref="ImportedModelData"/> class.
/// </summary>
~ImportedModelData()
{
// Ensure to cleanup data
for (int32 i = 0; i < LODs.Count(); i++)
LODs[i].Meshes.ClearDelete();
}
};
/// <summary>
/// Import models and animations helper.
/// </summary>
class FLAXENGINE_API ModelTool
{
public:
/// <summary>
/// Declares the imported data type.
/// </summary>
DECLARE_ENUM_EX_3(ModelType, int32, 0, Model, SkinnedModel, Animation);
/// <summary>
/// Declares the imported animation clip duration.
/// </summary>
DECLARE_ENUM_EX_2(AnimationDuration, int32, 0, Imported, Custom);
/// <summary>
/// Importing model options
/// </summary>
struct Options : public ISerializable
{
ModelType Type = ModelType::Model;
// Geometry
bool CalculateNormals = false;
float SmoothingNormalsAngle = 175.0f;
bool FlipNormals = false;
float SmoothingTangentsAngle = 45.0f;
bool CalculateTangents = true;
bool OptimizeMeshes = true;
bool MergeMeshes = true;
bool ImportLODs = true;
bool ImportVertexColors = true;
bool ImportBlendShapes = false;
ModelLightmapUVsSource LightmapUVsSource = ModelLightmapUVsSource::Disable;
// Transform
float Scale = 1.0f;
Quaternion Rotation = Quaternion::Identity;
Vector3 Translation = Vector3::Zero;
bool CenterGeometry = false;
// Animation
AnimationDuration Duration = AnimationDuration::Imported;
Vector2 FramesRange = Vector2::Zero;
float DefaultFrameRate = 0.0f;
float SamplingRate = 0.0f;
bool SkipEmptyCurves = true;
bool OptimizeKeyframes = true;
bool EnableRootMotion = false;
String RootNodeName;
int32 AnimationIndex = -1;
// Level Of Detail
bool GenerateLODs = false;
int32 BaseLOD = 0;
int32 LODCount = 4;
float TriangleReduction = 0.5f;
// Materials
bool ImportMaterials = true;
bool ImportTextures = true;
bool RestoreMaterialsOnReimport = true;
public:
// [ISerializable]
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
};
public:
/// <summary>
/// Imports the model source file data.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="data">The output data.</param>
/// <param name="options">The import options.</param>
/// <param name="errorMsg">The error message container.</param>
/// <returns>True if fails, otherwise false.</returns>
static bool ImportData(const String& path, ImportedModelData& data, Options options, String& errorMsg);
/// <summary>
/// Imports the model.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="meshData">The output data.</param>
/// <param name="options">The import options.</param>
/// <param name="errorMsg">The error message container.</param>
/// <param name="autoImportOutput">The output folder for the additional imported data - optional. Used to auto-import textures and material assets.</param>
/// <returns>True if fails, otherwise false.</returns>
static bool ImportModel(const String& path, ModelData& meshData, Options options, String& errorMsg, const String& autoImportOutput = String::Empty);
public:
static int32 DetectLodIndex(const String& nodeName);
static bool FindTexture(const String& sourcePath, const String& file, String& path);
/// <summary>
/// Gets the local transformations to go from rootIndex to index.
/// </summary>
/// <param name="nodes">The nodes containing the local transformations.</param>
/// <param name="rootIndex">The root index.</param>
/// <param name="index">The current index.</param>
/// <returns>The transformation at this index.</returns>
template<typename Node>
static Transform CombineTransformsFromNodeIndices(Array<Node>& nodes, int32 rootIndex, int32 index)
{
if (index == -1 || index == rootIndex)
return Transform::Identity;
auto result = nodes[index].LocalTransform;
if (index != rootIndex)
{
const auto parentTransform = CombineTransformsFromNodeIndices(nodes, rootIndex, nodes[index].ParentIndex);
result = parentTransform.LocalToWorld(result);
}
return result;
}
private:
#if USE_ASSIMP
static bool ImportDataAssimp(const char* path, ImportedModelData& data, const Options& options, String& errorMsg);
#endif
#if USE_AUTODESK_FBX_SDK
static bool ImportDataAutodeskFbxSdk(const char* path, ImportedModelData& data, const Options& options, String& errorMsg);
#endif
#if USE_OPEN_FBX
static bool ImportDataOpenFBX(const char* path, ImportedModelData& data, const Options& options, String& errorMsg);
#endif
};
#endif

View File

@@ -0,0 +1,346 @@
/*
---------------------------------------------------------------------------
Open Asset Import Library (assimp)
---------------------------------------------------------------------------
Copyright (c) 2006-2018, assimp team
All rights reserved.
Redistribution and use of this software in source and binary forms,
with or without modification, are permitted provided that the following
conditions are met:
* Redistributions of source code must retain the above
copyright notice, this list of conditions and the
following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the
following disclaimer in the documentation and/or other
materials provided with the distribution.
* Neither the name of the assimp team, nor the names of its
contributors may be used to endorse or promote products
derived from this software without specific prior
written permission of the assimp team.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---------------------------------------------------------------------------
*/
/** @file Implementation of the helper class to quickly find vertices close to a given position */
#include "SpatialSort.h"
#if COMPILE_WITH_MODEL_TOOL
#include <assimp/ai_assert.h>
using namespace Assimp;
// CHAR_BIT seems to be defined under MVSC, but not under GCC. Pray that the correct value is 8.
#ifndef CHAR_BIT
# define CHAR_BIT 8
#endif
// ------------------------------------------------------------------------------------------------
// Constructs a spatially sorted representation from the given position array.
SpatialSort::SpatialSort(const aiVector3D* pPositions, unsigned int pNumPositions,
unsigned int pElementOffset)
// define the reference plane. We choose some arbitrary vector away from all basic axises
// in the hope that no model spreads all its vertices along this plane.
: mPlaneNormal(0.8523f, 0.34321f, 0.5736f)
{
mPlaneNormal.Normalize();
Fill(pPositions, pNumPositions, pElementOffset);
}
// ------------------------------------------------------------------------------------------------
SpatialSort::SpatialSort()
: mPlaneNormal(0.8523f, 0.34321f, 0.5736f)
{
mPlaneNormal.Normalize();
}
// ------------------------------------------------------------------------------------------------
// Destructor
SpatialSort::~SpatialSort()
{
// nothing to do here, everything destructs automatically
}
// ------------------------------------------------------------------------------------------------
void SpatialSort::Fill(const aiVector3D* pPositions, unsigned int pNumPositions,
unsigned int pElementOffset,
bool pFinalize /*= true */)
{
mPositions.clear();
Append(pPositions, pNumPositions, pElementOffset, pFinalize);
}
// ------------------------------------------------------------------------------------------------
void SpatialSort::Finalize()
{
std::sort(mPositions.begin(), mPositions.end());
}
// ------------------------------------------------------------------------------------------------
void SpatialSort::Append(const aiVector3D* pPositions, unsigned int pNumPositions,
unsigned int pElementOffset,
bool pFinalize /*= true */)
{
// store references to all given positions along with their distance to the reference plane
const size_t initial = mPositions.size();
mPositions.reserve(initial + (pFinalize ? pNumPositions : pNumPositions * 2));
for (unsigned int a = 0; a < pNumPositions; a++)
{
const char* tempPointer = reinterpret_cast<const char*>(pPositions);
const aiVector3D* vec = reinterpret_cast<const aiVector3D*>(tempPointer + a * pElementOffset);
// store position by index and distance
ai_real distance = *vec * mPlaneNormal;
mPositions.push_back(Entry(static_cast<unsigned int>(a + initial), *vec, distance));
}
if (pFinalize)
{
// now sort the array ascending by distance.
Finalize();
}
}
// ------------------------------------------------------------------------------------------------
// Returns an iterator for all positions close to the given position.
void SpatialSort::FindPositions(const aiVector3D& pPosition,
ai_real pRadius, std::vector<unsigned int>& poResults) const
{
const ai_real dist = pPosition * mPlaneNormal;
const ai_real minDist = dist - pRadius, maxDist = dist + pRadius;
// clear the array
poResults.clear();
// quick check for positions outside the range
if (mPositions.size() == 0)
return;
if (maxDist < mPositions.front().mDistance)
return;
if (minDist > mPositions.back().mDistance)
return;
// do a binary search for the minimal distance to start the iteration there
unsigned int index = (unsigned int)mPositions.size() / 2;
unsigned int binaryStepSize = (unsigned int)mPositions.size() / 4;
while (binaryStepSize > 1)
{
if (mPositions[index].mDistance < minDist)
index += binaryStepSize;
else
index -= binaryStepSize;
binaryStepSize /= 2;
}
// depending on the direction of the last step we need to single step a bit back or forth
// to find the actual beginning element of the range
while (index > 0 && mPositions[index].mDistance > minDist)
index--;
while (index < (mPositions.size() - 1) && mPositions[index].mDistance < minDist)
index++;
// Mow start iterating from there until the first position lays outside of the distance range.
// Add all positions inside the distance range within the given radius to the result aray
std::vector<Entry>::const_iterator it = mPositions.begin() + index;
const ai_real pSquared = pRadius * pRadius;
while (it->mDistance < maxDist)
{
if ((it->mPosition - pPosition).SquareLength() < pSquared)
poResults.push_back(it->mIndex);
++it;
if (it == mPositions.end())
break;
}
// that's it
}
namespace
{
// Binary, signed-integer representation of a single-precision floating-point value.
// IEEE 754 says: "If two floating-point numbers in the same format are ordered then they are
// ordered the same way when their bits are reinterpreted as sign-magnitude integers."
// This allows us to convert all floating-point numbers to signed integers of arbitrary size
// and then use them to work with ULPs (Units in the Last Place, for high-precision
// computations) or to compare them (integer comparisons are faster than floating-point
// comparisons on many platforms).
typedef ai_int BinFloat;
// --------------------------------------------------------------------------------------------
// Converts the bit pattern of a floating-point number to its signed integer representation.
BinFloat ToBinary(const ai_real& pValue)
{
// If this assertion fails, signed int is not big enough to store a float on your platform.
// Please correct the declaration of BinFloat a few lines above - but do it in a portable,
// #ifdef'd manner!
static_assert( sizeof(BinFloat) >= sizeof(ai_real), "sizeof(BinFloat) >= sizeof(ai_real)");
#if defined( _MSC_VER)
// If this assertion fails, Visual C++ has finally moved to ILP64. This means that this
// code has just become legacy code! Find out the current value of _MSC_VER and modify
// the #if above so it evaluates false on the current and all upcoming VC versions (or
// on the current platform, if LP64 or LLP64 are still used on other platforms).
static_assert( sizeof(BinFloat) == sizeof(ai_real), "sizeof(BinFloat) == sizeof(ai_real)");
// This works best on Visual C++, but other compilers have their problems with it.
const BinFloat binValue = reinterpret_cast<BinFloat const &>(pValue);
#else
// On many compilers, reinterpreting a float address as an integer causes aliasing
// problems. This is an ugly but more or less safe way of doing it.
union {
ai_real asFloat;
BinFloat asBin;
} conversion;
conversion.asBin = 0; // zero empty space in case sizeof(BinFloat) > sizeof(float)
conversion.asFloat = pValue;
const BinFloat binValue = conversion.asBin;
#endif
// floating-point numbers are of sign-magnitude format, so find out what signed number
// representation we must convert negative values to.
// See http://en.wikipedia.org/wiki/Signed_number_representations.
// Two's complement?
if ((-42 == (~42 + 1)) && (binValue & 0x80000000))
return BinFloat(1 << (CHAR_BIT * sizeof(BinFloat) - 1)) - binValue;
// One's complement?
else if ((-42 == ~42) && (binValue & 0x80000000))
return BinFloat(-0) - binValue;
// Sign-magnitude?
else if ((-42 == (42 | (-0))) && (binValue & 0x80000000)) // -0 = 1000... binary
return binValue;
else
return binValue;
}
} // namespace
// ------------------------------------------------------------------------------------------------
// Fills an array with indices of all positions identical to the given position. In opposite to
// FindPositions(), not an epsilon is used but a (very low) tolerance of four floating-point units.
void SpatialSort::FindIdenticalPositions(const aiVector3D& pPosition,
std::vector<unsigned int>& poResults) const
{
// Epsilons have a huge disadvantage: they are of constant precision, while floating-point
// values are of log2 precision. If you apply e=0.01 to 100, the epsilon is rather small, but
// if you apply it to 0.001, it is enormous.
// The best way to overcome this is the unit in the last place (ULP). A precision of 2 ULPs
// tells us that a float does not differ more than 2 bits from the "real" value. ULPs are of
// logarithmic precision - around 1, they are 1*(2^24) and around 10000, they are 0.00125.
// For standard C math, we can assume a precision of 0.5 ULPs according to IEEE 754. The
// incoming vertex positions might have already been transformed, probably using rather
// inaccurate SSE instructions, so we assume a tolerance of 4 ULPs to safely identify
// identical vertex positions.
static const int toleranceInULPs = 4;
// An interesting point is that the inaccuracy grows linear with the number of operations:
// multiplying to numbers, each inaccurate to four ULPs, results in an inaccuracy of four ULPs
// plus 0.5 ULPs for the multiplication.
// To compute the distance to the plane, a dot product is needed - that is a multiplication and
// an addition on each number.
static const int distanceToleranceInULPs = toleranceInULPs + 1;
// The squared distance between two 3D vectors is computed the same way, but with an additional
// subtraction.
static const int distance3DToleranceInULPs = distanceToleranceInULPs + 1;
// Convert the plane distance to its signed integer representation so the ULPs tolerance can be
// applied. For some reason, VC won't optimize two calls of the bit pattern conversion.
const BinFloat minDistBinary = ToBinary(pPosition * mPlaneNormal) - distanceToleranceInULPs;
const BinFloat maxDistBinary = minDistBinary + 2 * distanceToleranceInULPs;
// clear the array in this strange fashion because a simple clear() would also deallocate
// the array which we want to avoid
poResults.resize(0);
// do a binary search for the minimal distance to start the iteration there
unsigned int index = (unsigned int)mPositions.size() / 2;
unsigned int binaryStepSize = (unsigned int)mPositions.size() / 4;
while (binaryStepSize > 1)
{
// Ugly, but conditional jumps are faster with integers than with floats
if (minDistBinary > ToBinary(mPositions[index].mDistance))
index += binaryStepSize;
else
index -= binaryStepSize;
binaryStepSize /= 2;
}
// depending on the direction of the last step we need to single step a bit back or forth
// to find the actual beginning element of the range
while (index > 0 && minDistBinary < ToBinary(mPositions[index].mDistance))
index--;
while (index < (mPositions.size() - 1) && minDistBinary > ToBinary(mPositions[index].mDistance))
index++;
// Now start iterating from there until the first position lays outside of the distance range.
// Add all positions inside the distance range within the tolerance to the result array
std::vector<Entry>::const_iterator it = mPositions.begin() + index;
while (ToBinary(it->mDistance) < maxDistBinary)
{
if (distance3DToleranceInULPs >= ToBinary((it->mPosition - pPosition).SquareLength()))
poResults.push_back(it->mIndex);
++it;
if (it == mPositions.end())
break;
}
// that's it
}
// ------------------------------------------------------------------------------------------------
unsigned int SpatialSort::GenerateMappingTable(std::vector<unsigned int>& fill, ai_real pRadius) const
{
fill.resize(mPositions.size(),UINT_MAX);
ai_real dist, maxDist;
unsigned int t = 0;
const ai_real pSquared = pRadius * pRadius;
for (size_t i = 0; i < mPositions.size();)
{
dist = mPositions[i].mPosition * mPlaneNormal;
maxDist = dist + pRadius;
fill[mPositions[i].mIndex] = t;
const aiVector3D& oldPos = mPositions[i].mPosition;
for (++i; i < fill.size() && mPositions[i].mDistance < maxDist
&& (mPositions[i].mPosition - oldPos).SquareLength() < pSquared; ++i)
{
fill[mPositions[i].mIndex] = t;
}
++t;
}
#ifdef ASSIMP_BUILD_DEBUG
// debug invariant: mPositions[i].mIndex values must range from 0 to mPositions.size()-1
for (size_t i = 0; i < fill.size(); ++i) {
ai_assert(fill[i]<mPositions.size());
}
#endif
return t;
}
#endif

View File

@@ -0,0 +1,184 @@
/*
Open Asset Import Library (assimp)
----------------------------------------------------------------------
Copyright (c) 2006-2018, assimp team
All rights reserved.
Redistribution and use of this software in source and binary forms,
with or without modification, are permitted provided that the
following conditions are met:
* Redistributions of source code must retain the above
copyright notice, this list of conditions and the
following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the
following disclaimer in the documentation and/or other
materials provided with the distribution.
* Neither the name of the assimp team, nor the names of its
contributors may be used to endorse or promote products
derived from this software without specific prior
written permission of the assimp team.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----------------------------------------------------------------------
*/
#pragma once
/** Small helper classes to optimise finding vertizes close to a given location */
#ifndef AI_SPATIALSORT_H_INC
#define AI_SPATIALSORT_H_INC
#include <vector>
#include <assimp/types.h>
namespace Assimp
{
// ------------------------------------------------------------------------------------------------
/** A little helper class to quickly find all vertices in the epsilon environment of a given
* position. Construct an instance with an array of positions. The class stores the given positions
* by their indices and sorts them by their distance to an arbitrary chosen plane.
* You can then query the instance for all vertices close to a given position in an average O(log n)
* time, with O(n) worst case complexity when all vertices lay on the plane. The plane is chosen
* so that it avoids common planes in usual data sets. */
// ------------------------------------------------------------------------------------------------
class ASSIMP_API SpatialSort
{
public:
SpatialSort();
// ------------------------------------------------------------------------------------
/** Constructs a spatially sorted representation from the given position array.
* Supply the positions in its layout in memory, the class will only refer to them
* by index.
* @param pPositions Pointer to the first position vector of the array.
* @param pNumPositions Number of vectors to expect in that array.
* @param pElementOffset Offset in bytes from the beginning of one vector in memory
* to the beginning of the next vector. */
SpatialSort(const aiVector3D* pPositions, unsigned int pNumPositions,
unsigned int pElementOffset);
/** Destructor */
~SpatialSort();
public:
// ------------------------------------------------------------------------------------
/** Sets the input data for the SpatialSort. This replaces existing data, if any.
* The new data receives new indices in ascending order.
*
* @param pPositions Pointer to the first position vector of the array.
* @param pNumPositions Number of vectors to expect in that array.
* @param pElementOffset Offset in bytes from the beginning of one vector in memory
* to the beginning of the next vector.
* @param pFinalize Specifies whether the SpatialSort's internal representation
* is finalized after the new data has been added. Finalization is
* required in order to use #FindPosition() or #GenerateMappingTable().
* If you don't finalize yet, you can use #Append() to add data from
* other sources.*/
void Fill(const aiVector3D* pPositions, unsigned int pNumPositions,
unsigned int pElementOffset,
bool pFinalize = true);
// ------------------------------------------------------------------------------------
/** Same as #Fill(), except the method appends to existing data in the #SpatialSort. */
void Append(const aiVector3D* pPositions, unsigned int pNumPositions,
unsigned int pElementOffset,
bool pFinalize = true);
// ------------------------------------------------------------------------------------
/** Finalize the spatial hash data structure. This can be useful after
* multiple calls to #Append() with the pFinalize parameter set to false.
* This is finally required before one of #FindPositions() and #GenerateMappingTable()
* can be called to query the spatial sort.*/
void Finalize();
// ------------------------------------------------------------------------------------
/** Returns an iterator for all positions close to the given position.
* @param pPosition The position to look for vertices.
* @param pRadius Maximal distance from the position a vertex may have to be counted in.
* @param poResults The container to store the indices of the found positions.
* Will be emptied by the call so it may contain anything.
* @return An iterator to iterate over all vertices in the given area.*/
void FindPositions(const aiVector3D& pPosition, ai_real pRadius,
std::vector<unsigned int>& poResults) const;
// ------------------------------------------------------------------------------------
/** Fills an array with indices of all positions identical to the given position. In
* opposite to FindPositions(), not an epsilon is used but a (very low) tolerance of
* four floating-point units.
* @param pPosition The position to look for vertices.
* @param poResults The container to store the indices of the found positions.
* Will be emptied by the call so it may contain anything.*/
void FindIdenticalPositions(const aiVector3D& pPosition,
std::vector<unsigned int>& poResults) const;
// ------------------------------------------------------------------------------------
/** Compute a table that maps each vertex ID referring to a spatially close
* enough position to the same output ID. Output IDs are assigned in ascending order
* from 0...n.
* @param fill Will be filled with numPositions entries.
* @param pRadius Maximal distance from the position a vertex may have to
* be counted in.
* @return Number of unique vertices (n). */
unsigned int GenerateMappingTable(std::vector<unsigned int>& fill,
ai_real pRadius) const;
protected:
/** Normal of the sorting plane, normalized. The center is always at (0, 0, 0) */
aiVector3D mPlaneNormal;
/** An entry in a spatially sorted position array. Consists of a vertex index,
* its position and its pre-calculated distance from the reference plane */
struct Entry
{
unsigned int mIndex; ///< The vertex referred by this entry
aiVector3D mPosition; ///< Position
ai_real mDistance; ///< Distance of this vertex to the sorting plane
Entry()
: mIndex(999999999)
, mPosition()
, mDistance(99999.)
{
// empty
}
Entry(unsigned int pIndex, const aiVector3D& pPosition, ai_real pDistance)
: mIndex(pIndex)
, mPosition(pPosition)
, mDistance(pDistance)
{
// empty
}
bool operator <(const Entry& e) const
{
return mDistance < e.mDistance;
}
};
// all positions, sorted by distance to the sorting plane
std::vector<Entry> mPositions;
};
} // end of namespace Assimp
#endif // AI_SPATIALSORT_H_INC

View File

@@ -0,0 +1,91 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_MODEL_TOOL
#include "VertexTriangleAdjacency.h"
#include "Engine/Core/Math/Math.h"
VertexTriangleAdjacency::VertexTriangleAdjacency(uint32* indices, int32 indicesCount, uint32 vertexCount, bool computeNumTriangles)
{
// Compute the number of referenced vertices if it wasn't specified by the caller
const uint32* const indicesEnd = indices + indicesCount;
if (vertexCount == 0)
{
for (uint32* triangle = indices; triangle != indicesEnd; triangle += 3)
{
ASSERT(nullptr != triangle);
vertexCount = Math::Max(vertexCount, triangle[0]);
vertexCount = Math::Max(vertexCount, triangle[1]);
vertexCount = Math::Max(vertexCount, triangle[2]);
}
}
NumVertices = vertexCount;
uint32* pi;
// Allocate storage
if (computeNumTriangles)
{
pi = LiveTriangles = new uint32[vertexCount + 1];
Platform::MemoryClear(LiveTriangles, sizeof(uint32) * (vertexCount + 1));
OffsetTable = new uint32[vertexCount + 2] + 1;
}
else
{
pi = OffsetTable = new uint32[vertexCount + 2] + 1;
Platform::MemoryClear(OffsetTable, sizeof(uint32) * (vertexCount + 1));
LiveTriangles = nullptr; // Important, otherwise the d'tor would crash
}
// Get a pointer to the end of the buffer
uint32* piEnd = pi + vertexCount;
*piEnd++ = 0u;
// First pass: compute the number of faces referencing each vertex
for (uint32* triangle = indices; triangle != indicesEnd; triangle += 3)
{
pi[triangle[0]]++;
pi[triangle[1]]++;
pi[triangle[2]]++;
}
// Second pass: compute the final offset table
int32 iSum = 0;
uint32* piCurOut = OffsetTable;
for (uint32* piCur = pi; piCur != piEnd; ++piCur, piCurOut++)
{
const int32 iLastSum = iSum;
iSum += *piCur;
*piCurOut = iLastSum;
}
pi = this->OffsetTable;
// Third pass: compute the final table
AdjacencyTable = new uint32[iSum];
iSum = 0;
for (uint32* triangle = indices; triangle != indicesEnd; triangle += 3, iSum++)
{
uint32 idx = triangle[0];
AdjacencyTable[pi[idx]++] = iSum;
idx = triangle[1];
AdjacencyTable[pi[idx]++] = iSum;
idx = triangle[2];
AdjacencyTable[pi[idx]++] = iSum;
}
// Fourth pass: undo the offset computations made during the third pass
// We could do this in a separate buffer, but this would be TIMES slower.
OffsetTable--;
*OffsetTable = 0u;
}
VertexTriangleAdjacency::~VertexTriangleAdjacency()
{
delete[] OffsetTable;
delete[] AdjacencyTable;
delete[] LiveTriangles;
}
#endif

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#if COMPILE_WITH_MODEL_TOOL
#include "Engine/Core/Config.h"
#include "Engine/Core/Types/BaseTypes.h"
#include "Engine/Platform/Platform.h"
/// <summary>
/// The VertexTriangleAdjacency class computes a vertex-triangle adjacency map from a given index buffer.
/// </summary>
class VertexTriangleAdjacency
{
public:
/// <summary>
/// Construction from an existing index buffer
/// </summary>
/// <param name="indices">The index buffer.</param>
/// <param name="indicesCount">The number of triangles in the buffer.</param>
/// <param name="vertexCount">The number of referenced vertices. This value is computed automatically if 0 is specified.</param>
/// <param name="computeNumTriangles">If you want the class to compute a list containing the number of referenced triangles per vertex per vertex - pass true.</param>
VertexTriangleAdjacency(uint32* indices, int32 indicesCount, uint32 vertexCount = 0, bool computeNumTriangles = true);
/// <summary>
/// Destructor
/// </summary>
~VertexTriangleAdjacency();
public:
/// <summary>
/// The offset table
/// </summary>
uint32* OffsetTable;
/// <summary>
/// The adjacency table.
/// </summary>
uint32* AdjacencyTable;
/// <summary>
/// The table containing the number of referenced triangles per vertex.
/// </summary>
uint32* LiveTriangles;
/// <summary>
/// The total number of referenced vertices.
/// </summary>
uint32 NumVertices;
public:
/// <summary>
/// Gets all triangles adjacent to a vertex.
/// </summary>
/// <param name="vertexIndex">The index of the vertex.</param>
/// <returns>A pointer to the adjacency list.</returns>
uint32* GetAdjacentTriangles(uint32 vertexIndex) const
{
ASSERT(vertexIndex >= 0 && vertexIndex < NumVertices);
return &AdjacencyTable[OffsetTable[vertexIndex]];
}
/// <summary>
/// Gets the number of triangles that are referenced by a vertex. This function returns a reference that can be modified.
/// </summary>
/// <param name="vertexIndex">The index of the vertex.</param>
/// <returns>The number of referenced triangles</returns>
uint32& GetNumTrianglesPtr(uint32 vertexIndex) const
{
ASSERT(vertexIndex >= 0 && vertexIndex < NumVertices);
ASSERT(nullptr != LiveTriangles);
return LiveTriangles[vertexIndex];
}
};
#endif

View File

@@ -0,0 +1,60 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using System.IO;
using Flax.Build;
using Flax.Build.NativeCpp;
/// <summary>
/// Texture utilities module.
/// </summary>
public class TextureTool : EngineModule
{
/// <inheritdoc />
public override void Setup(BuildOptions options)
{
base.Setup(options);
options.SourcePaths.Clear();
options.SourceFiles.Add(Path.Combine(FolderPath, "TextureTool.cpp"));
options.SourceFiles.Add(Path.Combine(FolderPath, "TextureTool.h"));
bool useDirectXTex = false;
bool useStb = false;
switch (options.Platform.Target)
{
case TargetPlatform.Windows:
case TargetPlatform.UWP:
case TargetPlatform.XboxOne:
case TargetPlatform.XboxScarlett:
useDirectXTex = true;
break;
case TargetPlatform.Linux:
case TargetPlatform.PS4:
case TargetPlatform.Android:
useStb = true;
break;
default: throw new InvalidPlatformException(options.Platform.Target);
}
if (useDirectXTex)
{
options.PrivateDependencies.Add("DirectXTex");
options.SourceFiles.Add(Path.Combine(FolderPath, "TextureTool.DirectXTex.cpp"));
}
if (useStb)
{
options.PrivateDependencies.Add("stb");
options.SourceFiles.Add(Path.Combine(FolderPath, "TextureTool.stb.cpp"));
}
options.PublicDefinitions.Add("COMPILE_WITH_TEXTURE_TOOL");
}
/// <inheritdoc />
public override void GetFilesToDeploy(List<string> files)
{
files.Add(Path.Combine(FolderPath, "TextureTool.h"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,700 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_TEXTURE_TOOL
#include "TextureTool.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Types/DateTime.h"
#include "Engine/Core/Math/Packed.h"
#include "Engine/Core/Math/Color32.h"
#include "Engine/Core/Math/VectorInt.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Serialization/JsonWriter.h"
#include "Engine/Serialization/JsonTools.h"
#include "Engine/Graphics/Textures/TextureData.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#if USE_EDITOR
namespace
{
Dictionary<String, bool> TexturesHasAlphaCache;
}
#endif
TextureTool::Options::Options()
{
Type = TextureFormatType::ColorRGB;
IsAtlas = false;
NeverStream = false;
Compress = true;
IndependentChannels = false;
sRGB = false;
GenerateMipMaps = true;
FlipY = false;
Resize = false;
PreserveAlphaCoverage = false;
PreserveAlphaCoverageReference = 0.5f;
Scale = 1.0f;
SizeX = 1024;
SizeY = 1024;
MaxSize = GPU_MAX_TEXTURE_SIZE;
}
String TextureTool::Options::ToString() const
{
return String::Format(TEXT("Type: {}, IsAtlas: {}, NeverStream: {}, IndependentChannels: {}, sRGB: {}, GenerateMipMaps: {}, FlipY: {}, Scale: {}, MaxSize: {}, Resize: {}, PreserveAlphaCoverage: {}, PreserveAlphaCoverageReference: {}, SizeX: {}, SizeY: {}"),
::ToString(Type),
IsAtlas,
NeverStream,
IndependentChannels,
sRGB,
GenerateMipMaps,
FlipY,
Scale,
MaxSize,
MaxSize,
Resize,
PreserveAlphaCoverage,
PreserveAlphaCoverageReference,
SizeX,
SizeY
);
}
void TextureTool::Options::Serialize(SerializeStream& stream, const void* otherObj)
{
stream.JKEY("Type");
stream.Enum(Type);
stream.JKEY("IsAtlas");
stream.Bool(IsAtlas);
stream.JKEY("NeverStream");
stream.Bool(NeverStream);
stream.JKEY("Compress");
stream.Bool(Compress);
stream.JKEY("IndependentChannels");
stream.Bool(IndependentChannels);
stream.JKEY("sRGB");
stream.Bool(sRGB);
stream.JKEY("GenerateMipMaps");
stream.Bool(GenerateMipMaps);
stream.JKEY("FlipY");
stream.Bool(FlipY);
stream.JKEY("Resize");
stream.Bool(Resize);
stream.JKEY("PreserveAlphaCoverage");
stream.Bool(PreserveAlphaCoverage);
stream.JKEY("PreserveAlphaCoverageReference");
stream.Float(PreserveAlphaCoverageReference);
stream.JKEY("Scale");
stream.Float(Scale);
stream.JKEY("MaxSize");
stream.Int(MaxSize);
stream.JKEY("SizeX");
stream.Int(SizeX);
stream.JKEY("SizeY");
stream.Int(SizeY);
stream.JKEY("Sprites");
stream.StartArray();
for (int32 i = 0; i < Sprites.Count(); i++)
{
auto& s = Sprites[i];
stream.StartObject();
stream.JKEY("Position");
stream.Vector2(s.Area.Location);
stream.JKEY("Size");
stream.Vector2(s.Area.Size);
stream.JKEY("Name");
stream.String(s.Name);
stream.EndObject();
}
stream.EndArray();
}
void TextureTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
// Restore general import options
Type = JsonTools::GetEnum(stream, "Type", Type);
IsAtlas = JsonTools::GetBool(stream, "IsAtlas", IsAtlas);
NeverStream = JsonTools::GetBool(stream, "NeverStream", NeverStream);
Compress = JsonTools::GetBool(stream, "Compress", Compress);
IndependentChannels = JsonTools::GetBool(stream, "IndependentChannels", IndependentChannels);
sRGB = JsonTools::GetBool(stream, "sRGB", sRGB);
GenerateMipMaps = JsonTools::GetBool(stream, "GenerateMipMaps", GenerateMipMaps);
FlipY = JsonTools::GetBool(stream, "FlipY", FlipY);
Resize = JsonTools::GetBool(stream, "Resize", Resize);
PreserveAlphaCoverage = JsonTools::GetBool(stream, "PreserveAlphaCoverage", PreserveAlphaCoverage);
PreserveAlphaCoverageReference = JsonTools::GetFloat(stream, "PreserveAlphaCoverageReference", PreserveAlphaCoverageReference);
Scale = JsonTools::GetFloat(stream, "Scale", Scale);
SizeX = JsonTools::GetInt(stream, "SizeX", SizeX);
SizeY = JsonTools::GetInt(stream, "SizeY", SizeY);
MaxSize = JsonTools::GetInt(stream, "MaxSize", MaxSize);
// Load sprites
// Note: we use it if no sprites in texture header has been loaded earlier
auto* spritesMember = stream.FindMember("Sprites");
if (spritesMember != stream.MemberEnd() && Sprites.IsEmpty())
{
auto& spritesArray = spritesMember->value;
ASSERT(spritesArray.IsArray());
Sprites.EnsureCapacity(spritesArray.Size());
for (uint32 i = 0; i < spritesArray.Size(); i++)
{
Sprite s;
auto& stData = spritesArray[i];
s.Area.Location = JsonTools::GetVector2(stData, "Position", Vector2::Zero);
s.Area.Size = JsonTools::GetVector2(stData, "Size", Vector2::One);
s.Name = JsonTools::GetString(stData, "Name");
Sprites.Add(s);
}
}
}
#if USE_EDITOR
bool TextureTool::HasAlpha(const StringView& path)
{
// Try to hit the cache (eg. if texture was already imported before)
if (!TexturesHasAlphaCache.ContainsKey(path))
{
TextureData textureData;
if (ImportTexture(path, textureData))
return false;
}
return TexturesHasAlphaCache[path];
}
#endif
bool TextureTool::ImportTexture(const StringView& path, TextureData& textureData)
{
LOG(Info, "Importing texture from \'{0}\'", path);
const auto startTime = DateTime::NowUTC();
// Detect texture format type
ImageType type;
if (GetImageType(path, type))
return true;
// Import
bool hasAlpha = false;
#if COMPILE_WITH_DIRECTXTEX
const auto failed = ImportTextureDirectXTex(type, path, textureData, hasAlpha);
#elif COMPILE_WITH_STB
const auto failed = ImportTextureStb(type, path, textureData, hasAlpha);
#else
const auto failed = true;
LOG(Warning, "Importing textures is not supported on this platform.");
#endif
if (failed)
{
LOG(Warning, "Importing texture failed.");
}
else
{
#if USE_EDITOR
TexturesHasAlphaCache[path] = hasAlpha;
#endif
LOG(Info, "Texture imported in {0} ms", static_cast<int32>((DateTime::NowUTC() - startTime).GetTotalMilliseconds()));
}
return failed;
}
bool TextureTool::ImportTexture(const StringView& path, TextureData& textureData, Options options, String& errorMsg)
{
LOG(Info, "Importing texture from \'{0}\'. Options: {1}", path, options.ToString());
const auto startTime = DateTime::NowUTC();
// Detect texture format type
ImageType type;
if (options.InternalLoad.IsBinded())
{
type = ImageType::Internal;
}
else
{
if (GetImageType(path, type))
return true;
}
// Clamp values
options.MaxSize = Math::Clamp(options.MaxSize, 1, GPU_MAX_TEXTURE_SIZE);
options.SizeX = Math::Clamp(options.SizeX, 1, GPU_MAX_TEXTURE_SIZE);
options.SizeY = Math::Clamp(options.SizeY, 1, GPU_MAX_TEXTURE_SIZE);
// Import
bool hasAlpha = false;
#if COMPILE_WITH_DIRECTXTEX
const auto failed = ImportTextureDirectXTex(type, path, textureData, options, errorMsg, hasAlpha);
#else
const auto failed = true;
LOG(Warning, "Importing textures is not supported on this platform.");
#endif
if (failed)
{
LOG(Warning, "Importing texture failed. {0}", errorMsg);
}
else
{
#if USE_EDITOR
TexturesHasAlphaCache[path] = hasAlpha;
#endif
LOG(Info, "Texture imported in {0} ms", static_cast<int32>((DateTime::NowUTC() - startTime).GetTotalMilliseconds()));
}
return failed;
}
bool TextureTool::ExportTexture(const StringView& path, const TextureData& textureData)
{
LOG(Info, "Exporting texture to \'{0}\'.", path);
const auto startTime = DateTime::NowUTC();
ImageType type;
if (GetImageType(path, type))
return true;
if (textureData.Items.IsEmpty())
{
LOG(Warning, "Missing texture data.");
return true;
}
// Export
#if COMPILE_WITH_DIRECTXTEX
const auto failed = ExportTextureDirectXTex(type, path, textureData);
#elif COMPILE_WITH_STB
const auto failed = ExportTextureStb(type, path, textureData);
#else
const auto failed = true;
LOG(Warning, "Exporting textures is not supported on this platform.");
#endif
if (failed)
{
LOG(Warning, "Exporting failed.");
}
else
{
LOG(Info, "Texture exported in {0} ms", static_cast<int32>((DateTime::NowUTC() - startTime).GetTotalMilliseconds()));
}
return failed;
}
bool TextureTool::Convert(TextureData& dst, const TextureData& src, const PixelFormat dstFormat)
{
// Validate input
if (src.GetMipLevels() == 0)
{
LOG(Warning, "Missing source data.");
return true;
}
if (src.Format == dstFormat)
{
LOG(Warning, "Soure data and destination format are the same. Cannot perform conversion.");
return true;
}
if (src.Depth != 1)
{
LOG(Warning, "Converting volume texture data is not supported.");
return true;
}
#if COMPILE_WITH_DIRECTXTEX
return ConvertDirectXTex(dst, src, dstFormat);
#else
LOG(Warning, "Converting textures is not supported on this platform.");
return true;
#endif
}
bool TextureTool::Resize(TextureData& dst, const TextureData& src, int32 dstWidth, int32 dstHeight)
{
// Validate input
if (src.GetMipLevels() == 0)
{
LOG(Warning, "Missing source data.");
return true;
}
if (src.Width == dstWidth && src.Height == dstHeight)
{
LOG(Warning, "Soure data and destination dimensions are the same. Cannot perform resizing.");
return true;
}
if (src.Depth != 1)
{
LOG(Warning, "Resizing volume texture data is not supported.");
return true;
}
#if COMPILE_WITH_DIRECTXTEX
return ResizeDirectXTex(dst, src, dstWidth, dstHeight);
#else
LOG(Warning, "Resizing textures is not supported on this platform.");
return true;
#endif
}
TextureTool::PixelFormatSampler PixelFormatSamplers[] =
{
{
PixelFormat::R32G32B32A32_Float,
sizeof(Vector4),
[](const void* ptr)
{
return Color(*(Vector4*)ptr);
},
[](const void* ptr, const Color& color)
{
*(Vector4*)ptr = color.ToVector4();
},
},
{
PixelFormat::R32G32B32_Float,
sizeof(Vector3),
[](const void* ptr)
{
return Color(*(Vector3*)ptr, 1.0f);
},
[](const void* ptr, const Color& color)
{
*(Vector3*)ptr = color.ToVector3();
},
},
{
PixelFormat::R16G16B16A16_Float,
sizeof(Half4),
[](const void* ptr)
{
return Color(((Half4*)ptr)->ToVector4());
},
[](const void* ptr, const Color& color)
{
*(Half4*)ptr = Half4(color.R, color.G, color.B, color.A);
},
},
{
PixelFormat::R16G16B16A16_UNorm,
sizeof(RGBA16UNorm),
[](const void* ptr)
{
return Color(((RGBA16UNorm*)ptr)->ToVector4());
},
[](const void* ptr, const Color& color)
{
*(RGBA16UNorm*)ptr = RGBA16UNorm(color.R, color.G, color.B, color.A);
}
},
{
PixelFormat::R32G32_Float,
sizeof(Vector2),
[](const void* ptr)
{
return Color(((Vector2*)ptr)->X, ((Vector2*)ptr)->Y, 1.0f);
},
[](const void* ptr, const Color& color)
{
*(Vector2*)ptr = Vector2(color.R, color.G);
},
},
{
PixelFormat::R8G8B8A8_UNorm,
sizeof(Color32),
[](const void* ptr)
{
return Color(*(Color32*)ptr);
},
[](const void* ptr, const Color& color)
{
*(Color32*)ptr = Color32(color);
},
},
{
PixelFormat::R8G8B8A8_UNorm_sRGB,
sizeof(Color32),
[](const void* ptr)
{
return Color(*(Color32*)ptr);
},
[](const void* ptr, const Color& color)
{
*(Color32*)ptr = Color32(color);
},
},
{
PixelFormat::R16G16_Float,
sizeof(Half2),
[](const void* ptr)
{
const Vector2 rg = ((Half2*)ptr)->ToVector2();
return Color(rg.X, rg.Y, 0, 1);
},
[](const void* ptr, const Color& color)
{
*(Half2*)ptr = Half2(color.R, color.G);
},
},
{
PixelFormat::R16G16_UNorm,
sizeof(RG16UNorm),
[](const void* ptr)
{
const Vector2 rg = ((RG16UNorm*)ptr)->ToVector2();
return Color(rg.X, rg.Y, 0, 1);
},
[](const void* ptr, const Color& color)
{
*(RG16UNorm*)ptr = RG16UNorm(color.R, color.G);
},
},
{
PixelFormat::R32_Float,
sizeof(float),
[](const void* ptr)
{
return Color(*(float*)ptr, 0, 0, 1);
},
[](const void* ptr, const Color& color)
{
*(float*)ptr = color.R;
},
},
{
PixelFormat::R16_Float,
sizeof(Half),
[](const void* ptr)
{
return Color(ConvertHalfToFloat(*(Half*)ptr), 0, 0, 1);
},
[](const void* ptr, const Color& color)
{
*(Half*)ptr = ConvertFloatToHalf(color.R);
},
},
{
PixelFormat::R16_UNorm,
sizeof(uint16),
[](const void* ptr)
{
return Color((float)*(uint16*)ptr / MAX_uint16, 0, 0, 1);
},
[](const void* ptr, const Color& color)
{
*(uint16*)ptr = (uint16)(color.R * MAX_uint16);
},
},
{
PixelFormat::R8_UNorm,
sizeof(uint8),
[](const void* ptr)
{
return Color((float)*(byte*)ptr / MAX_uint8, 0, 0, 1);
},
[](const void* ptr, const Color& color)
{
*(byte*)ptr = (byte)(color.R * MAX_uint8);
},
},
{
PixelFormat::A8_UNorm,
sizeof(uint8),
[](const void* ptr)
{
return Color(0, 0, 0, (float)*(byte*)ptr / MAX_uint8);
},
[](const void* ptr, const Color& color)
{
*(byte*)ptr = (byte)(color.A * MAX_uint8);
},
},
{
PixelFormat::B8G8R8A8_UNorm,
sizeof(Color32),
[](const void* ptr)
{
const Color32 bgra = *(Color32*)ptr;
return Color(Color32(bgra.B, bgra.G, bgra.R, bgra.A));
},
[](const void* ptr, const Color& color)
{
*(Color32*)ptr = Color32(byte(color.B * MAX_uint8), byte(color.G * MAX_uint8), byte(color.R * MAX_uint8), byte(color.A * MAX_uint8));
},
},
{
PixelFormat::B8G8R8A8_UNorm_sRGB,
sizeof(Color32),
[](const void* ptr)
{
const Color32 bgra = *(Color32*)ptr;
return Color(Color32(bgra.B, bgra.G, bgra.R, bgra.A));
},
[](const void* ptr, const Color& color)
{
*(Color32*)ptr = Color32(byte(color.B * MAX_uint8), byte(color.G * MAX_uint8), byte(color.R * MAX_uint8), byte(color.A * MAX_uint8));
},
},
{
PixelFormat::B8G8R8X8_UNorm,
sizeof(Color32),
[](const void* ptr)
{
const Color32 bgra = *(Color32*)ptr;
return Color(Color32(bgra.B, bgra.G, bgra.R, MAX_uint8));
},
[](const void* ptr, const Color& color)
{
*(Color32*)ptr = Color32(byte(color.B * MAX_uint8), byte(color.G * MAX_uint8), byte(color.R * MAX_uint8), MAX_uint8);
},
},
{
PixelFormat::B8G8R8X8_UNorm_sRGB,
sizeof(Color32),
[](const void* ptr)
{
const Color32 bgra = *(Color32*)ptr;
return Color(Color32(bgra.B, bgra.G, bgra.R, MAX_uint8));
},
[](const void* ptr, const Color& color)
{
*(Color32*)ptr = Color32(byte(color.B * MAX_uint8), byte(color.G * MAX_uint8), byte(color.R * MAX_uint8), MAX_uint8);
},
},
{
PixelFormat::R11G11B10_Float,
sizeof(FloatR11G11B10),
[](const void* ptr)
{
const Vector3 rgb = ((FloatR11G11B10*)ptr)->ToVector3();
return Color(rgb.X, rgb.Y, rgb.Z);
},
[](const void* ptr, const Color& color)
{
*(FloatR11G11B10*)ptr = Float1010102(color.R, color.G, color.B, color.A);
},
},
};
const TextureTool::PixelFormatSampler* TextureTool::GetSampler(PixelFormat format)
{
format = PixelFormatExtensions::MakeTypelessFloat(format);
for (auto& sampler : PixelFormatSamplers)
{
if (sampler.Format == format)
return &sampler;
}
return nullptr;
}
void TextureTool::Store(const PixelFormatSampler* sampler, int32 x, int32 y, const void* data, int32 rowPitch, const Color& color)
{
ASSERT_LOW_LAYER(sampler);
sampler->Store((byte*)data + rowPitch * y + sampler->PixelSize * x, color);
}
Color TextureTool::SamplePoint(const PixelFormatSampler* sampler, const Vector2& uv, const void* data, const Int2& size, int32 rowPitch)
{
ASSERT_LOW_LAYER(sampler);
const Int2 end = size - 1;
const Int2 uvFloor(Math::Min(Math::FloorToInt(uv.X * size.X), end.X), Math::Min(Math::FloorToInt(uv.Y * size.Y), end.Y));
return sampler->Sample((byte*)data + rowPitch * uvFloor.Y + sampler->PixelSize * uvFloor.X);
}
Color TextureTool::SamplePoint(const PixelFormatSampler* sampler, int32 x, int32 y, const void* data, int32 rowPitch)
{
ASSERT_LOW_LAYER(sampler);
return sampler->Sample((byte*)data + rowPitch * y + sampler->PixelSize * x);
}
Color TextureTool::SampleLinear(const PixelFormatSampler* sampler, const Vector2& uv, const void* data, const Int2& size, int32 rowPitch)
{
ASSERT_LOW_LAYER(sampler);
const Int2 end = size - 1;
const Int2 uvFloor(Math::Min(Math::FloorToInt(uv.X * size.X), end.X), Math::Min(Math::FloorToInt(uv.Y * size.Y), end.Y));
const Int2 uvNext(Math::Min(uvFloor.X + 1, end.X), Math::Min(uvFloor.Y + 1, end.Y));
const Vector2 uvFraction(uv.X * size.Y - uvFloor.X, uv.Y * size.Y - uvFloor.Y);
const Color v00 = sampler->Sample((byte*)data + rowPitch * uvFloor.Y + sampler->PixelSize * uvFloor.X);
const Color v01 = sampler->Sample((byte*)data + rowPitch * uvFloor.Y + sampler->PixelSize * uvNext.X);
const Color v10 = sampler->Sample((byte*)data + rowPitch * uvNext.Y + sampler->PixelSize * uvFloor.X);
const Color v11 = sampler->Sample((byte*)data + rowPitch * uvNext.Y + sampler->PixelSize * uvNext.X);
return Color::Lerp(Color::Lerp(v00, v01, uvFraction.X), Color::Lerp(v10, v11, uvFraction.X), uvFraction.Y);
}
bool TextureTool::GetImageType(const StringView& path, ImageType& type)
{
const auto extension = FileSystem::GetExtension(path).ToLower();
if (extension == TEXT("tga"))
{
type = ImageType::TGA;
}
else if (extension == TEXT("dds"))
{
type = ImageType::DDS;
}
else if (extension == TEXT("png"))
{
type = ImageType::PNG;
}
else if (extension == TEXT("bmp"))
{
type = ImageType::BMP;
}
else if (extension == TEXT("gif"))
{
type = ImageType::GIF;
}
else if (extension == TEXT("tiff") || extension == TEXT("tif"))
{
type = ImageType::TIFF;
}
else if (extension == TEXT("hdr"))
{
type = ImageType::HDR;
}
else if (extension == TEXT("jpeg") || extension == TEXT("jpg"))
{
type = ImageType::JPEG;
}
else if (extension == TEXT("raw"))
{
type = ImageType::RAW;
}
else
{
LOG(Warning, "Unknown file type.");
return true;
}
return false;
}
#endif

View File

@@ -0,0 +1,297 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#if COMPILE_WITH_TEXTURE_TOOL
#include "Engine/Render2D/SpriteAtlas.h"
#include "Engine/Graphics/Textures/Types.h"
#include "Engine/Graphics/Textures/GPUTexture.h"
#include "Engine/Serialization/ISerializable.h"
class JsonWriter;
/// <summary>
/// Textures importing, processing and exporting utilities.
/// </summary>
class FLAXENGINE_API TextureTool
{
public:
/// <summary>
/// Importing texture options
/// </summary>
struct Options : public ISerializable
{
/// <summary>
/// Texture format type
/// </summary>
TextureFormatType Type;
/// <summary>
/// True if texture should be imported as a texture atlas resource
/// </summary>
bool IsAtlas;
/// <summary>
/// True if disable dynamic texture streaming
/// </summary>
bool NeverStream;
/// <summary>
/// Enables/disables texture data compression.
/// </summary>
bool Compress;
/// <summary>
/// True if texture channels have independent data
/// </summary>
bool IndependentChannels;
/// <summary>
/// True if use sRGB format for texture data. Recommended for color maps and diffuse color textures.
/// </summary>
bool sRGB;
/// <summary>
/// True if generate mip maps chain for the texture.
/// </summary>
bool GenerateMipMaps;
/// <summary>
/// True if flip Y coordinate of the texture.
/// </summary>
bool FlipY;
/// <summary>
/// True if resize the texture.
/// </summary>
bool Resize;
/// <summary>
/// True if preserve alpha coverage in generated mips for alpha test reference. Scales mipmap alpha values to preserve alpha coverage based on an alpha test reference value.
/// </summary>
bool PreserveAlphaCoverage;
/// <summary>
/// The reference value for the alpha coverage preserving.
/// </summary>
float PreserveAlphaCoverageReference;
/// <summary>
/// The import texture scale.
/// </summary>
float Scale;
/// <summary>
/// Custom texture size X, use only if Resize texture flag is set.
/// </summary>
int32 SizeX;
/// <summary>
/// Custom texture size Y, use only if Resize texture flag is set.
/// </summary>
int32 SizeY;
/// <summary>
/// Maximum size of the texture (for both width and height).
/// Higher resolution textures will be resized during importing process.
/// </summary>
int32 MaxSize;
/// <summary>
/// Function used for fast importing textures used by internal parts of the engine
/// </summary>
Function<bool(TextureData&)> InternalLoad;
/// <summary>
/// The sprites for the sprite sheet import mode.
/// </summary>
Array<Sprite> Sprites;
public:
/// <summary>
/// Init
/// </summary>
Options();
/// <summary>
/// Gets string that contains information about options
/// </summary>
/// <returns>String</returns>
String ToString() const;
public:
// [ISerializable]
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
};
public:
#if USE_EDITOR
/// <summary>
/// Checks whenever the given texture file contains alpha channel data with values different than solid fill of 1 (non fully opaque).
/// </summary>
/// <param name="path">The file path.</param>
/// <returns>True if has alpha channel, otherwise false.</returns>
static bool HasAlpha(const StringView& path);
#endif
/// <summary>
/// Imports the texture.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="textureData">The output data.</param>
/// <returns>True if fails, otherwise false.</returns>
static bool ImportTexture(const StringView& path, TextureData& textureData);
/// <summary>
/// Imports the texture.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="textureData">The output data.</param>
/// <param name="options">The import options.</param>
/// <param name="errorMsg">The error message container.</param>
/// <returns>True if fails, otherwise false.</returns>
static bool ImportTexture(const StringView& path, TextureData& textureData, Options options, String& errorMsg);
/// <summary>
/// Exports the texture.
/// </summary>
/// <param name="path">The output file path.</param>
/// <param name="textureData">The output data.</param>
/// <returns>True if fails, otherwise false.</returns>
static bool ExportTexture(const StringView& path, const TextureData& textureData);
/// <summary>
/// Converts the specified source texture data into an another format.
/// </summary>
/// <param name="dst">The destination data.</param>
/// <param name="src">The source data.</param>
/// <param name="dstFormat">The destination data format. Must be other than source data type.</param>
/// <returns>True if fails, otherwise false.</returns>
static bool Convert(TextureData& dst, const TextureData& src, const PixelFormat dstFormat);
/// <summary>
/// Resizes the specified source texture data into an another dimensions.
/// </summary>
/// <param name="dst">The destination data.</param>
/// <param name="src">The source data.</param>
/// <param name="dstWidth">The destination data width.</param>
/// <param name="dstHeight">The destination data height.</param>
/// <returns>True if fails, otherwise false.</returns>
static bool Resize(TextureData& dst, const TextureData& src, int32 dstWidth, int32 dstHeight);
public:
typedef Color (*ReadPixel)(const void*);
typedef void (*WritePixel)(const void*, const Color&);
struct PixelFormatSampler
{
PixelFormat Format;
int32 PixelSize;
ReadPixel Sample;
WritePixel Store;
};
/// <summary>
/// Determines whether this tool can sample the specified format to read texture samples and returns the sampler object.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>The pointer to the sampler object or null if cannot sample the given format.</returns>
static const PixelFormatSampler* GetSampler(PixelFormat format);
/// <summary>
/// Stores the color into the specified texture data (uses no interpolation).
/// </summary>
/// <remarks>
/// Use GetSampler for the texture sampler.
/// </remarks>
/// <param name="sampler">The texture data sampler.</param>
/// <param name="x">The X texture coordinates (normalized to range 0-width).</param>
/// <param name="y">The Y texture coordinates (normalized to range 0-height).</param>
/// <param name="data">The data pointer for the texture slice (1D or 2D image).</param>
/// <param name="rowPitch">The row pitch (in bytes). The offset between each image rows.</param>
/// <param name="color">The color to store.</param>
static void Store(const PixelFormatSampler* sampler, int32 x, int32 y, const void* data, int32 rowPitch, const Color& color);
/// <summary>
/// Samples the specified texture data (uses no interpolation).
/// </summary>
/// <remarks>
/// Use GetSampler for the texture sampler.
/// </remarks>
/// <param name="sampler">The texture data sampler.</param>
/// <param name="uv">The texture coordinates (normalized to range 0-1).</param>
/// <param name="data">The data pointer for the texture slice (1D or 2D image).</param>
/// <param name="size">The size of the input texture (in pixels).</param>
/// <param name="rowPitch">The row pitch (in bytes). The offset between each image rows.</param>
/// <returns>The sampled color.</returns>
static Color SamplePoint(const PixelFormatSampler* sampler, const Vector2& uv, const void* data, const Int2& size, int32 rowPitch);
/// <summary>
/// Samples the specified texture data (uses no interpolation).
/// </summary>
/// <remarks>
/// Use GetSampler for the texture sampler.
/// </remarks>
/// <param name="sampler">The texture data sampler.</param>
/// <param name="x">The X texture coordinates (normalized to range 0-width).</param>
/// <param name="y">The Y texture coordinates (normalized to range 0-height).</param>
/// <param name="data">The data pointer for the texture slice (1D or 2D image).</param>
/// <param name="rowPitch">The row pitch (in bytes). The offset between each image rows.</param>
/// <returns>The sampled color.</returns>
static Color SamplePoint(const PixelFormatSampler* sampler, int32 x, int32 y, const void* data, int32 rowPitch);
/// <summary>
/// Samples the specified texture data (uses linear interpolation).
/// </summary>
/// <remarks>
/// Use GetSampler for the texture sampler.
/// </remarks>
/// <param name="sampler">The texture data sampler.</param>
/// <param name="uv">The texture coordinates (normalized to range 0-1).</param>
/// <param name="data">The data pointer for the texture slice (1D or 2D image).</param>
/// <param name="size">The size of the input texture (in pixels).</param>
/// <param name="rowPitch">The row pitch (in bytes). The offset between each image rows.</param>
/// <returns>The sampled color.</returns>
static Color SampleLinear(const PixelFormatSampler* sampler, const Vector2& uv, const void* data, const Int2& size, int32 rowPitch);
private:
enum class ImageType
{
DDS,
TGA,
PNG,
BMP,
GIF,
TIFF,
JPEG,
HDR,
RAW,
Internal,
};
static bool GetImageType(const StringView& path, ImageType& type);
#if COMPILE_WITH_DIRECTXTEX
static bool ExportTextureDirectXTex(ImageType type, const StringView& path, const TextureData& textureData);
static bool ImportTextureDirectXTex(ImageType type, const StringView& path, TextureData& textureData, bool& hasAlpha);
static bool ImportTextureDirectXTex(ImageType type, const StringView& path, TextureData& textureData, const Options& options, String& errorMsg, bool& hasAlpha);
static bool ConvertDirectXTex(TextureData& dst, const TextureData& src, const PixelFormat dstFormat);
static bool ResizeDirectXTex(TextureData& dst, const TextureData& src, int32 dstWidth, int32 dstHeight);
#endif
#if COMPILE_WITH_STB
static bool ExportTextureStb(ImageType type, const StringView& path, const TextureData& textureData);
static bool ImportTextureStb(ImageType type, const StringView& path, TextureData& textureData, bool& hasAlpha);
#endif
};
#endif

View File

@@ -0,0 +1,245 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#if COMPILE_WITH_TEXTURE_TOOL && COMPILE_WITH_STB
#include "TextureTool.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Color32.h"
#include "Engine/Serialization/FileWriteStream.h"
#include "Engine/Graphics/Textures/TextureData.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Platform/File.h"
#define STBI_ASSERT(x) ASSERT(x)
#define STBI_MALLOC(sz) Allocator::Allocate(sz)
#define STBI_REALLOC(p, newsz) AllocatorExt::Realloc(p, newsz)
#define STBI_REALLOC_SIZED(p, oldsz, newsz) AllocatorExt::Realloc(p, oldsz, newsz)
#define STBI_FREE(p) Allocator::Free(p)
#define STBI_WRITE_NO_STDIO
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <ThirdParty/stb/stb_image_write.h>
#define STBI_NO_PSD
#define STBI_NO_PIC
#define STBI_NO_PNM
#define STBI_NO_FAILURE_STRINGS
#define STBI_NO_STDIO
#define STB_IMAGE_IMPLEMENTATION
#include <ThirdParty/stb/stb_image.h>
static void stbWrite(void* context, void* data, int size)
{
auto file = (FileWriteStream*)context;
file->WriteBytes(data, (uint32)size);
}
bool TextureTool::ExportTextureStb(ImageType type, const StringView& path, const TextureData& textureData)
{
if (textureData.GetArraySize() != 1)
{
LOG(Warning, "Exporting texture arrays and cubemaps is not supported by stb library.");
}
// Convert into RGBA8
const auto sampler = GetSampler(textureData.Format);
if (sampler == nullptr)
{
LOG(Warning, "Texture data format {0} is not supported by stb library.", (int32)textureData.Format);
return true;
}
const auto srcData = textureData.GetData(0, 0);
const int comp = 4;
Array<byte> data;
bool sRGB = PixelFormatExtensions::IsSRGB(textureData.Format);
if (type == ImageType::HDR)
{
data.Resize(sizeof(float) * comp * textureData.Width * textureData.Height);
auto ptr = (Vector4*)data.Get();
for (int32 y = 0; y < textureData.Height; y++)
{
for (int32 x = 0; x < textureData.Width; x++)
{
Color color = SamplePoint(sampler, x, y, srcData->Data.Get(), srcData->RowPitch);
if (sRGB)
color = Color::SrgbToLinear(color);
*(ptr + x + y * textureData.Width) = color.ToVector4();
}
}
}
else
{
data.Resize(sizeof(Color32) * comp * textureData.Width * textureData.Height);
auto ptr = (Color32*)data.Get();
for (int32 y = 0; y < textureData.Height; y++)
{
for (int32 x = 0; x < textureData.Width; x++)
{
Color color = SamplePoint(sampler, x, y, srcData->Data.Get(), srcData->RowPitch);
if (sRGB)
color = Color::SrgbToLinear(color);
*(ptr + x + y * textureData.Width) = Color32(color);
}
}
}
const auto file = FileWriteStream::Open(path);
if (!file)
{
LOG(Warning, "Failed to open file.");
return true;
}
stbi__write_context s;
s.func = stbWrite;
s.context = file;
int32 result = 99;
switch (type)
{
case ImageType::BMP:
result = stbi_write_bmp_core(&s, textureData.Width, textureData.Height, comp, data.Get());
break;
case ImageType::JPEG:
result = stbi_write_jpg_core(&s, textureData.Width, textureData.Height, comp, data.Get(), 90);
break;
case ImageType::TGA:
result = stbi_write_tga_core(&s, textureData.Width, textureData.Height, comp, data.Get());
break;
case ImageType::HDR:
result = stbi_write_hdr_core(&s, textureData.Width, textureData.Height, comp, (float*)data.Get());
break;
case ImageType::PNG:
{
int32 ptrSize = 0;
const auto ptr = stbi_write_png_to_mem(data.Get(), 0, textureData.Width, textureData.Height, comp, &ptrSize);
if (ptr)
{
file->WriteBytes(ptr, ptrSize);
result = 0;
}
else
{
result = 99;
}
break;
}
case ImageType::GIF:
LOG(Warning, "GIF format is not supported by stb library.");
break;
case ImageType::TIFF:
LOG(Warning, "GIF format is not supported by stb library.");
break;
case ImageType::DDS:
LOG(Warning, "DDS format is not supported by stb library.");
break;
case ImageType::RAW:
LOG(Warning, "RAW format is not supported by stb library.");
break;
default:
LOG(Warning, "Unknown format.");
break;
}
if (result != 0)
{
LOG(Warning, "Saving texture failed. Error from stb library: {0}", result);
}
file->Close();
Delete(file);
return result != 0;
}
bool TextureTool::ImportTextureStb(ImageType type, const StringView& path, TextureData& textureData, bool& hasAlpha)
{
Array<byte> fileData;
if (File::ReadAllBytes(path, fileData))
{
LOG(Warning, "Failed to read data from file.");
return true;
}
switch (type)
{
case ImageType::PNG:
case ImageType::BMP:
case ImageType::GIF:
case ImageType::JPEG:
case ImageType::HDR:
case ImageType::TGA:
{
int x, y, comp;
stbi_uc* stbData = stbi_load_from_memory(fileData.Get(), fileData.Count(), &x, &y, &comp, 4);
if (!stbData)
{
LOG(Warning, "Failed to load image.");
return false;
}
// Setup texture data
textureData.Width = x;
textureData.Height = y;
textureData.Depth = 1;
textureData.Format = PixelFormat::R8G8B8A8_UNorm;
textureData.Items.Resize(1);
textureData.Items[0].Mips.Resize(1);
auto& mip = textureData.Items[0].Mips[0];
mip.RowPitch = sizeof(Color32) * x;
mip.DepthPitch = mip.RowPitch * y;
mip.Lines = y;
mip.Data.Copy(stbData, mip.DepthPitch);
#if USE_EDITOR
// TODO: detect hasAlpha
#endif
stbi_image_free(stbData);
break;
}
case ImageType::RAW:
{
// Assume 16-bit, grayscale .RAW file in little-endian byte order
// Check size
const auto size = (int32)Math::Sqrt(fileData.Count() / 2.0f);
if (fileData.Count() != size * size * 2)
{
LOG(Warning, "Invalid RAW file data size or format. Use 16-bit .RAW file in little-endian byte order (square dimensions).");
return true;
}
// Setup texture data
textureData.Width = size;
textureData.Height = size;
textureData.Depth = 1;
textureData.Format = PixelFormat::R16_UNorm;
textureData.Items.Resize(1);
textureData.Items[0].Mips.Resize(1);
auto& mip = textureData.Items[0].Mips[0];
mip.RowPitch = fileData.Count() / size;
mip.DepthPitch = fileData.Count();
mip.Lines = size;
mip.Data.Copy(fileData);
break;
}
case ImageType::DDS:
LOG(Warning, "DDS format is not supported by stb library.");
break;
case ImageType::TIFF:
LOG(Warning, "TIFF format is not supported by stb library.");
break;
default:
LOG(Warning, "Unknown format.");
return true;
}
return false;
}
#endif