You're breathtaking!
This commit is contained in:
84
Source/Engine/Tools/AudioTool/AudioDecoder.h
Normal file
84
Source/Engine/Tools/AudioTool/AudioDecoder.h
Normal 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;
|
||||
};
|
||||
33
Source/Engine/Tools/AudioTool/AudioEncoder.h
Normal file
33
Source/Engine/Tools/AudioTool/AudioEncoder.h
Normal 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;
|
||||
};
|
||||
30
Source/Engine/Tools/AudioTool/AudioTool.Build.cs
Normal file
30
Source/Engine/Tools/AudioTool/AudioTool.Build.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
263
Source/Engine/Tools/AudioTool/AudioTool.cpp
Normal file
263
Source/Engine/Tools/AudioTool/AudioTool.cpp
Normal 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++;
|
||||
}
|
||||
}
|
||||
61
Source/Engine/Tools/AudioTool/AudioTool.h
Normal file
61
Source/Engine/Tools/AudioTool/AudioTool.h
Normal 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);
|
||||
}
|
||||
};
|
||||
88
Source/Engine/Tools/AudioTool/MP3Decoder.cpp
Normal file
88
Source/Engine/Tools/AudioTool/MP3Decoder.cpp
Normal 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;
|
||||
}
|
||||
43
Source/Engine/Tools/AudioTool/MP3Decoder.h
Normal file
43
Source/Engine/Tools/AudioTool/MP3Decoder.h
Normal 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
|
||||
114
Source/Engine/Tools/AudioTool/OggVorbisDecoder.cpp
Normal file
114
Source/Engine/Tools/AudioTool/OggVorbisDecoder.cpp
Normal 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
|
||||
55
Source/Engine/Tools/AudioTool/OggVorbisDecoder.h
Normal file
55
Source/Engine/Tools/AudioTool/OggVorbisDecoder.h
Normal 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
|
||||
265
Source/Engine/Tools/AudioTool/OggVorbisEncoder.cpp
Normal file
265
Source/Engine/Tools/AudioTool/OggVorbisEncoder.cpp
Normal 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
|
||||
95
Source/Engine/Tools/AudioTool/OggVorbisEncoder.h
Normal file
95
Source/Engine/Tools/AudioTool/OggVorbisEncoder.h
Normal 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
|
||||
168
Source/Engine/Tools/AudioTool/WaveDecoder.cpp
Normal file
168
Source/Engine/Tools/AudioTool/WaveDecoder.cpp
Normal 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');
|
||||
}
|
||||
54
Source/Engine/Tools/AudioTool/WaveDecoder.h
Normal file
54
Source/Engine/Tools/AudioTool/WaveDecoder.h
Normal 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
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = ¶m;
|
||||
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 = ¶m;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -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, ¶m);
|
||||
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
|
||||
@@ -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, ¶m);
|
||||
}
|
||||
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, ¶m);
|
||||
}
|
||||
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, ¶m);
|
||||
}
|
||||
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, ¶m);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Scene Color
|
||||
case 7:
|
||||
{
|
||||
// Sample scene color texture
|
||||
auto param = findOrAddSceneTexture(MaterialSceneTextures::SceneColor);
|
||||
sampleTexture(node, value, box, ¶m);
|
||||
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
|
||||
@@ -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
|
||||
559
Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp
Normal file
559
Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp
Normal 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
|
||||
216
Source/Engine/Tools/MaterialGenerator/MaterialGenerator.h
Normal file
216
Source/Engine/Tools/MaterialGenerator/MaterialGenerator.h
Normal 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
|
||||
239
Source/Engine/Tools/MaterialGenerator/MaterialLayer.cpp
Normal file
239
Source/Engine/Tools/MaterialGenerator/MaterialLayer.cpp
Normal 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
|
||||
154
Source/Engine/Tools/MaterialGenerator/MaterialLayer.h
Normal file
154
Source/Engine/Tools/MaterialGenerator/MaterialLayer.h
Normal 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
|
||||
67
Source/Engine/Tools/MaterialGenerator/Types.h
Normal file
67
Source/Engine/Tools/MaterialGenerator/Types.h
Normal 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
|
||||
790
Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp
Normal file
790
Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp
Normal 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
|
||||
980
Source/Engine/Tools/ModelTool/ModelTool.AutodeskFbxSdk.cpp
Normal file
980
Source/Engine/Tools/ModelTool/ModelTool.AutodeskFbxSdk.cpp
Normal 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
|
||||
64
Source/Engine/Tools/ModelTool/ModelTool.Build.cs
Normal file
64
Source/Engine/Tools/ModelTool/ModelTool.Build.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
1267
Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp
Normal file
1267
Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp
Normal file
File diff suppressed because it is too large
Load Diff
103
Source/Engine/Tools/ModelTool/ModelTool.Options.cpp
Normal file
103
Source/Engine/Tools/ModelTool/ModelTool.Options.cpp
Normal 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
|
||||
1275
Source/Engine/Tools/ModelTool/ModelTool.cpp
Normal file
1275
Source/Engine/Tools/ModelTool/ModelTool.cpp
Normal file
File diff suppressed because it is too large
Load Diff
280
Source/Engine/Tools/ModelTool/ModelTool.h
Normal file
280
Source/Engine/Tools/ModelTool/ModelTool.h
Normal 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
|
||||
346
Source/Engine/Tools/ModelTool/SpatialSort.cpp
Normal file
346
Source/Engine/Tools/ModelTool/SpatialSort.cpp
Normal 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
|
||||
184
Source/Engine/Tools/ModelTool/SpatialSort.h
Normal file
184
Source/Engine/Tools/ModelTool/SpatialSort.h
Normal 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
|
||||
91
Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.cpp
Normal file
91
Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.cpp
Normal 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
|
||||
80
Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.h
Normal file
80
Source/Engine/Tools/ModelTool/VertexTriangleAdjacency.h
Normal 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
|
||||
60
Source/Engine/Tools/TextureTool/TextureTool.Build.cs
Normal file
60
Source/Engine/Tools/TextureTool/TextureTool.Build.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
1038
Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp
Normal file
1038
Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp
Normal file
File diff suppressed because it is too large
Load Diff
700
Source/Engine/Tools/TextureTool/TextureTool.cpp
Normal file
700
Source/Engine/Tools/TextureTool/TextureTool.cpp
Normal 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
|
||||
297
Source/Engine/Tools/TextureTool/TextureTool.h
Normal file
297
Source/Engine/Tools/TextureTool/TextureTool.h
Normal 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
|
||||
245
Source/Engine/Tools/TextureTool/TextureTool.stb.cpp
Normal file
245
Source/Engine/Tools/TextureTool/TextureTool.stb.cpp
Normal 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
|
||||
Reference in New Issue
Block a user