2019-12-26 15:33:43 -05:00
|
|
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
2014-03-14 14:13:41 -04:00
|
|
|
|
|
|
|
|
#include "NormalMapIdentification.h"
|
2021-08-26 10:49:21 -04:00
|
|
|
|
|
|
|
|
#if WITH_EDITOR
|
|
|
|
|
|
Copying //UE4/Dev-Build to //UE4/Dev-Main (Source: //UE4/Dev-Build @ 3209340)
#lockdown Nick.Penwarden
#rb none
==========================
MAJOR FEATURES + CHANGES
==========================
Change 3209340 on 2016/11/23 by Ben.Marsh
Convert UE4 codebase to an "include what you use" model - where every header just includes the dependencies it needs, rather than every source file including large monolithic headers like Engine.h and UnrealEd.h.
Measured full rebuild times around 2x faster using XGE on Windows, and improvements of 25% or more for incremental builds and full rebuilds on most other platforms.
* Every header now includes everything it needs to compile.
* There's a CoreMinimal.h header that gets you a set of ubiquitous types from Core (eg. FString, FName, TArray, FVector, etc...). Most headers now include this first.
* There's a CoreTypes.h header that sets up primitive UE4 types and build macros (int32, PLATFORM_WIN64, etc...). All headers in Core include this first, as does CoreMinimal.h.
* Every .cpp file includes its matching .h file first.
* This helps validate that each header is including everything it needs to compile.
* No engine code includes a monolithic header such as Engine.h or UnrealEd.h any more.
* You will get a warning if you try to include one of these from the engine. They still exist for compatibility with game projects and do not produce warnings when included there.
* There have only been minor changes to our internal games down to accommodate these changes. The intent is for this to be as seamless as possible.
* No engine code explicitly includes a precompiled header any more.
* We still use PCHs, but they're force-included on the compiler command line by UnrealBuildTool instead. This lets us tune what they contain without breaking any existing include dependencies.
* PCHs are generated by a tool to get a statistical amount of coverage for the source files using it, and I've seeded the new shared PCHs to contain any header included by > 15% of source files.
Tool used to generate this transform is at Engine\Source\Programs\IncludeTool.
[CL 3209342 by Ben Marsh in Main branch]
2016-11-23 15:48:37 -05:00
|
|
|
#include "Engine/Texture2D.h"
|
|
|
|
|
#include "Framework/Notifications/NotificationManager.h"
|
2022-12-12 08:24:56 -05:00
|
|
|
#include "TextureCompiler.h"
|
Copying //UE4/Dev-Build to //UE4/Dev-Main (Source: //UE4/Dev-Build @ 3209340)
#lockdown Nick.Penwarden
#rb none
==========================
MAJOR FEATURES + CHANGES
==========================
Change 3209340 on 2016/11/23 by Ben.Marsh
Convert UE4 codebase to an "include what you use" model - where every header just includes the dependencies it needs, rather than every source file including large monolithic headers like Engine.h and UnrealEd.h.
Measured full rebuild times around 2x faster using XGE on Windows, and improvements of 25% or more for incremental builds and full rebuilds on most other platforms.
* Every header now includes everything it needs to compile.
* There's a CoreMinimal.h header that gets you a set of ubiquitous types from Core (eg. FString, FName, TArray, FVector, etc...). Most headers now include this first.
* There's a CoreTypes.h header that sets up primitive UE4 types and build macros (int32, PLATFORM_WIN64, etc...). All headers in Core include this first, as does CoreMinimal.h.
* Every .cpp file includes its matching .h file first.
* This helps validate that each header is including everything it needs to compile.
* No engine code includes a monolithic header such as Engine.h or UnrealEd.h any more.
* You will get a warning if you try to include one of these from the engine. They still exist for compatibility with game projects and do not produce warnings when included there.
* There have only been minor changes to our internal games down to accommodate these changes. The intent is for this to be as seamless as possible.
* No engine code explicitly includes a precompiled header any more.
* We still use PCHs, but they're force-included on the compiler command line by UnrealBuildTool instead. This lets us tune what they contain without breaking any existing include dependencies.
* PCHs are generated by a tool to get a statistical amount of coverage for the source files using it, and I've seeded the new shared PCHs to contain any header included by > 15% of source files.
Tool used to generate this transform is at Engine\Source\Programs\IncludeTool.
[CL 3209342 by Ben Marsh in Main branch]
2016-11-23 15:48:37 -05:00
|
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
2014-03-14 14:13:41 -04:00
|
|
|
|
2015-04-16 12:29:07 -04:00
|
|
|
#define NORMALMAP_IDENTIFICATION_TIMING (0)
|
2014-03-14 14:13:41 -04:00
|
|
|
|
|
|
|
|
#define LOCTEXT_NAMESPACE "NormalMapIdentification"
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
// Constant values
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
// These values may need tuning, but results so far have been good
|
|
|
|
|
|
|
|
|
|
// These values are the threshold values for the average vector's
|
|
|
|
|
// length to be considered within limits as a normal map normal
|
|
|
|
|
const float NormalMapMinLengthConfidenceThreshold = 0.55f;
|
|
|
|
|
const float NormalMapMaxLengthConfidenceThreshold = 1.1f;
|
|
|
|
|
|
|
|
|
|
// This value is the threshold value for the average vector to be considered
|
|
|
|
|
// to be going in the correct direction.
|
|
|
|
|
const float NormalMapDeviationThreshold = 0.8f;
|
|
|
|
|
|
|
|
|
|
// Samples from the texture will be taken in blocks of this size^2
|
|
|
|
|
const int32 SampleTileEdgeLength = 4;
|
|
|
|
|
|
|
|
|
|
// We sample up to this many tiles in each axis. Sampling more tiles
|
|
|
|
|
// will likely be more accurate, but will take longer.
|
2015-05-07 05:33:50 -04:00
|
|
|
const int32 MaxTilesPerAxis = 16;
|
2014-03-14 14:13:41 -04:00
|
|
|
|
|
|
|
|
// This is used in the comparison with "mid-gray"
|
|
|
|
|
const float ColorComponentNearlyZeroThreshold = (2.0f / 255.0f);
|
|
|
|
|
|
2014-04-30 12:38:40 -04:00
|
|
|
// This is used when comparing alpha to zero to avoid picking up sprites
|
|
|
|
|
const float AlphaComponentNearlyZeroThreshold = (1.0f / 255.0f);
|
|
|
|
|
|
2014-03-14 14:13:41 -04:00
|
|
|
// These values are chosen to make the threshold colors (from uint8 textures)
|
|
|
|
|
// discard the top most and bottom most two values, i.e. 0, 1, 254 and 255 on
|
|
|
|
|
// the assumption that these are likely invalid values for a general normal map
|
|
|
|
|
const float ColorComponentMinVectorThreshold = (2.0f / 255.0f) * 2.0f - 1.0f;
|
|
|
|
|
const float ColorComponentMaxVectorThreshold = (253.0f/255.0f) * 2.0f - 1.0f;
|
2015-05-07 05:33:50 -04:00
|
|
|
|
|
|
|
|
// This is the threshold delta length for a vector to be considered as a unit vector
|
|
|
|
|
const float NormalVectorUnitLengthDeltaThreshold = 0.45f;
|
|
|
|
|
|
|
|
|
|
// Rejected to taken sample ratio threshold.
|
|
|
|
|
const float RejectedToTakenRatioThreshold = 0.33f;
|
2014-03-14 14:13:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
// Texture sampler classes
|
|
|
|
|
class NormalMapSamplerBase
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
NormalMapSamplerBase()
|
|
|
|
|
: SourceTexture(NULL)
|
|
|
|
|
, TextureSizeX(0)
|
|
|
|
|
, TextureSizeY(0)
|
|
|
|
|
, SourceTextureData(NULL)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~NormalMapSamplerBase()
|
|
|
|
|
{
|
2022-09-26 15:24:17 -04:00
|
|
|
if ( SourceTextureData != NULL )
|
2014-03-14 14:13:41 -04:00
|
|
|
{
|
|
|
|
|
SourceTexture->Source.UnlockMip(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-26 15:24:17 -04:00
|
|
|
bool SetSourceTexture( UTexture* Texture )
|
2014-03-14 14:13:41 -04:00
|
|
|
{
|
|
|
|
|
SourceTexture = Texture;
|
|
|
|
|
TextureSizeX = Texture->Source.GetSizeX();
|
|
|
|
|
TextureSizeY = Texture->Source.GetSizeY();
|
Allow imported jpegs to continue to be stored as jpeg data in .uassets until the data is actually edited and needs to be re-compressed. This can reduce megascan assets by up to 40%. The feature is disabled by default and can be enabled for a project by setting the editor config value [TextureImporter]RetainJpegFormat=True.
* Texture Work
- When importing a jpeg we will now check the config file to see if we should try and keep the data in compressed JPEG format and decompress it as it is requested rather than returning the data in raw uncompressed format.
- If the data is kept in jpeg compressed format we can initialize the texture via the new method InitWithCompressedSourceData instead. (note that we can probably merge ::InitWithCompressedSourceData and ::Init but that introduces further risk, this way we know that ONLY textures that were originally jpeg can be going through ::InitWithCompressedSourceData for now)
- We now record the compression format of the bulkdata in FTextureSource via 'CompressionFormat' an enum rather than a single bool 'bPNGCompressed' which only wqorked for png format.
- We should deprecate bPNGCompressed at some point and loading existing textures off disk will not have 'bPNGCompressed' and 'CompressionFormat' in sync, fixing this is out of scope for EA work.
- Note that when PNG compression is used we can decompress the data, use that, then recompress it without loss in quality, the same is not to be said for JPEG so if the data is modified by a call to ::LockMip/::Unlock mip the data will end up falling back to being stored as a compressed PNG or raw data. If this occurs we will log a warning.
- To reduce the number of places where this happens a new ::LockMipReadOnly method has been added, locking the texture source as read only will not replace the stored data when unlocked (as read only means no changes)
- Attempting to lock a texture for a different mode than it is currently locked for is considered a programming error and will raise an assert.
- Updated the 'SourceCompression' asset registry tag. This can be seen when hovering the mouse over the asset in the content browser and will show which compression is being used on the source data, png, jpeg or none.
* NormalMapIdentification/PaperAtlasTextureHelpers
- Switched to use LockMipReadOnly as we never modify the locked mip data.
- Every other remaining place in code that I could find that used LockMip() is actually calling that to edit the data.
* JPEG Decompression
- Worth noting that in early access the jpeg compression is slower than decompressing the same data if it was stored in compressed png format.
- On main the jpeg decompressor has been upgraded and the data ends up being smaller AND faster to decode, so wins all around.
- For early access we are willing to take the increase decoding cost to reduce the risk of the submit.
#rb Mark.Lintott, PJ.Kack, Danny.Couture
#[fyi] Jonathan.Bard
#lockdown Nick.Whiting
#jira UE-113796
#ushell-cherrypick of 16051246 by paul.chipchase
#preflight 607dcf0de7a5ac0001985d44
[CL 16054157 by paul chipchase in ue5-main branch]
2021-04-19 15:36:03 -04:00
|
|
|
SourceTextureData = Texture->Source.LockMipReadOnly(0);
|
2022-09-26 15:24:17 -04:00
|
|
|
return SourceTextureData != nullptr;
|
2014-03-14 14:13:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UTexture* SourceTexture;
|
2022-03-30 12:05:31 -04:00
|
|
|
int64 TextureSizeX;
|
|
|
|
|
int64 TextureSizeY;
|
Allow imported jpegs to continue to be stored as jpeg data in .uassets until the data is actually edited and needs to be re-compressed. This can reduce megascan assets by up to 40%. The feature is disabled by default and can be enabled for a project by setting the editor config value [TextureImporter]RetainJpegFormat=True.
* Texture Work
- When importing a jpeg we will now check the config file to see if we should try and keep the data in compressed JPEG format and decompress it as it is requested rather than returning the data in raw uncompressed format.
- If the data is kept in jpeg compressed format we can initialize the texture via the new method InitWithCompressedSourceData instead. (note that we can probably merge ::InitWithCompressedSourceData and ::Init but that introduces further risk, this way we know that ONLY textures that were originally jpeg can be going through ::InitWithCompressedSourceData for now)
- We now record the compression format of the bulkdata in FTextureSource via 'CompressionFormat' an enum rather than a single bool 'bPNGCompressed' which only wqorked for png format.
- We should deprecate bPNGCompressed at some point and loading existing textures off disk will not have 'bPNGCompressed' and 'CompressionFormat' in sync, fixing this is out of scope for EA work.
- Note that when PNG compression is used we can decompress the data, use that, then recompress it without loss in quality, the same is not to be said for JPEG so if the data is modified by a call to ::LockMip/::Unlock mip the data will end up falling back to being stored as a compressed PNG or raw data. If this occurs we will log a warning.
- To reduce the number of places where this happens a new ::LockMipReadOnly method has been added, locking the texture source as read only will not replace the stored data when unlocked (as read only means no changes)
- Attempting to lock a texture for a different mode than it is currently locked for is considered a programming error and will raise an assert.
- Updated the 'SourceCompression' asset registry tag. This can be seen when hovering the mouse over the asset in the content browser and will show which compression is being used on the source data, png, jpeg or none.
* NormalMapIdentification/PaperAtlasTextureHelpers
- Switched to use LockMipReadOnly as we never modify the locked mip data.
- Every other remaining place in code that I could find that used LockMip() is actually calling that to edit the data.
* JPEG Decompression
- Worth noting that in early access the jpeg compression is slower than decompressing the same data if it was stored in compressed png format.
- On main the jpeg decompressor has been upgraded and the data ends up being smaller AND faster to decode, so wins all around.
- For early access we are willing to take the increase decoding cost to reduce the risk of the submit.
#rb Mark.Lintott, PJ.Kack, Danny.Couture
#[fyi] Jonathan.Bard
#lockdown Nick.Whiting
#jira UE-113796
#ushell-cherrypick of 16051246 by paul.chipchase
#preflight 607dcf0de7a5ac0001985d44
[CL 16054157 by paul chipchase in ue5-main branch]
2021-04-19 15:36:03 -04:00
|
|
|
const uint8* SourceTextureData;
|
2014-03-14 14:13:41 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template<int RIdx, int GIdx, int BIdx, int AIdx> class SampleNormalMapPixel8 : public NormalMapSamplerBase
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
SampleNormalMapPixel8() {}
|
|
|
|
|
~SampleNormalMapPixel8() {}
|
|
|
|
|
|
|
|
|
|
FLinearColor DoSampleColor( int32 X, int32 Y )
|
|
|
|
|
{
|
|
|
|
|
FLinearColor Result;
|
Allow imported jpegs to continue to be stored as jpeg data in .uassets until the data is actually edited and needs to be re-compressed. This can reduce megascan assets by up to 40%. The feature is disabled by default and can be enabled for a project by setting the editor config value [TextureImporter]RetainJpegFormat=True.
* Texture Work
- When importing a jpeg we will now check the config file to see if we should try and keep the data in compressed JPEG format and decompress it as it is requested rather than returning the data in raw uncompressed format.
- If the data is kept in jpeg compressed format we can initialize the texture via the new method InitWithCompressedSourceData instead. (note that we can probably merge ::InitWithCompressedSourceData and ::Init but that introduces further risk, this way we know that ONLY textures that were originally jpeg can be going through ::InitWithCompressedSourceData for now)
- We now record the compression format of the bulkdata in FTextureSource via 'CompressionFormat' an enum rather than a single bool 'bPNGCompressed' which only wqorked for png format.
- We should deprecate bPNGCompressed at some point and loading existing textures off disk will not have 'bPNGCompressed' and 'CompressionFormat' in sync, fixing this is out of scope for EA work.
- Note that when PNG compression is used we can decompress the data, use that, then recompress it without loss in quality, the same is not to be said for JPEG so if the data is modified by a call to ::LockMip/::Unlock mip the data will end up falling back to being stored as a compressed PNG or raw data. If this occurs we will log a warning.
- To reduce the number of places where this happens a new ::LockMipReadOnly method has been added, locking the texture source as read only will not replace the stored data when unlocked (as read only means no changes)
- Attempting to lock a texture for a different mode than it is currently locked for is considered a programming error and will raise an assert.
- Updated the 'SourceCompression' asset registry tag. This can be seen when hovering the mouse over the asset in the content browser and will show which compression is being used on the source data, png, jpeg or none.
* NormalMapIdentification/PaperAtlasTextureHelpers
- Switched to use LockMipReadOnly as we never modify the locked mip data.
- Every other remaining place in code that I could find that used LockMip() is actually calling that to edit the data.
* JPEG Decompression
- Worth noting that in early access the jpeg compression is slower than decompressing the same data if it was stored in compressed png format.
- On main the jpeg decompressor has been upgraded and the data ends up being smaller AND faster to decode, so wins all around.
- For early access we are willing to take the increase decoding cost to reduce the risk of the submit.
#rb Mark.Lintott, PJ.Kack, Danny.Couture
#[fyi] Jonathan.Bard
#lockdown Nick.Whiting
#jira UE-113796
#ushell-cherrypick of 16051246 by paul.chipchase
#preflight 607dcf0de7a5ac0001985d44
[CL 16054157 by paul chipchase in ue5-main branch]
2021-04-19 15:36:03 -04:00
|
|
|
const uint8* PixelToSample = SourceTextureData + ((Y * TextureSizeX + X) * 4);
|
2014-03-14 14:13:41 -04:00
|
|
|
|
|
|
|
|
const float OneOver255 = 1.0f / 255.0f;
|
|
|
|
|
|
|
|
|
|
Result.B = (float)PixelToSample[BIdx] * OneOver255;
|
|
|
|
|
Result.G = (float)PixelToSample[GIdx] * OneOver255;
|
|
|
|
|
Result.R = (float)PixelToSample[RIdx] * OneOver255;
|
|
|
|
|
Result.A = (float)PixelToSample[AIdx] * OneOver255;
|
|
|
|
|
|
|
|
|
|
return Result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float ScaleAndBiasComponent( float Value ) const
|
|
|
|
|
{
|
|
|
|
|
return Value * 2.0f - 1.0f;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
typedef SampleNormalMapPixel8<2, 1, 0, 3> SampleNormalMapPixelBGRA8;
|
|
|
|
|
typedef SampleNormalMapPixel8<0, 1, 2, 3> SampleNormalMapPixelRGBA8;
|
|
|
|
|
|
2015-05-21 10:47:22 -04:00
|
|
|
class SampleNormalMapPixelRGBA16 : public NormalMapSamplerBase
|
2014-03-14 14:13:41 -04:00
|
|
|
{
|
|
|
|
|
public:
|
2015-05-21 10:47:22 -04:00
|
|
|
SampleNormalMapPixelRGBA16() {}
|
|
|
|
|
~SampleNormalMapPixelRGBA16() {}
|
2014-03-14 14:13:41 -04:00
|
|
|
|
|
|
|
|
FLinearColor DoSampleColor( int32 X, int32 Y )
|
|
|
|
|
{
|
|
|
|
|
FLinearColor Result;
|
Allow imported jpegs to continue to be stored as jpeg data in .uassets until the data is actually edited and needs to be re-compressed. This can reduce megascan assets by up to 40%. The feature is disabled by default and can be enabled for a project by setting the editor config value [TextureImporter]RetainJpegFormat=True.
* Texture Work
- When importing a jpeg we will now check the config file to see if we should try and keep the data in compressed JPEG format and decompress it as it is requested rather than returning the data in raw uncompressed format.
- If the data is kept in jpeg compressed format we can initialize the texture via the new method InitWithCompressedSourceData instead. (note that we can probably merge ::InitWithCompressedSourceData and ::Init but that introduces further risk, this way we know that ONLY textures that were originally jpeg can be going through ::InitWithCompressedSourceData for now)
- We now record the compression format of the bulkdata in FTextureSource via 'CompressionFormat' an enum rather than a single bool 'bPNGCompressed' which only wqorked for png format.
- We should deprecate bPNGCompressed at some point and loading existing textures off disk will not have 'bPNGCompressed' and 'CompressionFormat' in sync, fixing this is out of scope for EA work.
- Note that when PNG compression is used we can decompress the data, use that, then recompress it without loss in quality, the same is not to be said for JPEG so if the data is modified by a call to ::LockMip/::Unlock mip the data will end up falling back to being stored as a compressed PNG or raw data. If this occurs we will log a warning.
- To reduce the number of places where this happens a new ::LockMipReadOnly method has been added, locking the texture source as read only will not replace the stored data when unlocked (as read only means no changes)
- Attempting to lock a texture for a different mode than it is currently locked for is considered a programming error and will raise an assert.
- Updated the 'SourceCompression' asset registry tag. This can be seen when hovering the mouse over the asset in the content browser and will show which compression is being used on the source data, png, jpeg or none.
* NormalMapIdentification/PaperAtlasTextureHelpers
- Switched to use LockMipReadOnly as we never modify the locked mip data.
- Every other remaining place in code that I could find that used LockMip() is actually calling that to edit the data.
* JPEG Decompression
- Worth noting that in early access the jpeg compression is slower than decompressing the same data if it was stored in compressed png format.
- On main the jpeg decompressor has been upgraded and the data ends up being smaller AND faster to decode, so wins all around.
- For early access we are willing to take the increase decoding cost to reduce the risk of the submit.
#rb Mark.Lintott, PJ.Kack, Danny.Couture
#[fyi] Jonathan.Bard
#lockdown Nick.Whiting
#jira UE-113796
#ushell-cherrypick of 16051246 by paul.chipchase
#preflight 607dcf0de7a5ac0001985d44
[CL 16054157 by paul chipchase in ue5-main branch]
2021-04-19 15:36:03 -04:00
|
|
|
const uint8* PixelToSample = SourceTextureData + ((Y * TextureSizeX + X) * 8);
|
2014-03-14 14:13:41 -04:00
|
|
|
|
|
|
|
|
const float OneOver65535 = 1.0f / 65535.0f;
|
|
|
|
|
|
2015-05-21 10:47:22 -04:00
|
|
|
Result.R = (float)((uint16*)PixelToSample)[0] * OneOver65535;
|
|
|
|
|
Result.G = (float)((uint16*)PixelToSample)[1] * OneOver65535;
|
|
|
|
|
Result.B = (float)((uint16*)PixelToSample)[2] * OneOver65535;
|
|
|
|
|
Result.A = (float)((uint16*)PixelToSample)[3] * OneOver65535;
|
2014-03-14 14:13:41 -04:00
|
|
|
|
|
|
|
|
return Result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float ScaleAndBiasComponent( float Value ) const
|
|
|
|
|
{
|
|
|
|
|
return Value * 2.0f - 1.0f;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
class SampleNormalMapPixelF16 : public NormalMapSamplerBase
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
SampleNormalMapPixelF16() {}
|
|
|
|
|
~SampleNormalMapPixelF16() {}
|
|
|
|
|
|
|
|
|
|
FLinearColor DoSampleColor( int32 X, int32 Y )
|
|
|
|
|
{
|
2022-03-15 18:29:37 -04:00
|
|
|
const uint8* PixelToSample = SourceTextureData + ((Y * TextureSizeX + X) * sizeof(FFloat16Color));
|
2014-03-14 14:13:41 -04:00
|
|
|
|
2015-05-21 11:02:01 -04:00
|
|
|
// this assume the normal map to be in linear (not the case if Photoshop converts a 8bit normalmap to float and saves it as 16bit dds)
|
2022-03-15 18:29:37 -04:00
|
|
|
|
|
|
|
|
return ((const FFloat16Color*)PixelToSample)->GetFloats();
|
|
|
|
|
}
|
2014-03-14 14:13:41 -04:00
|
|
|
|
2022-03-15 18:29:37 -04:00
|
|
|
float ScaleAndBiasComponent( float Value ) const
|
|
|
|
|
{
|
|
|
|
|
// no need to scale and bias floating point components.
|
|
|
|
|
return Value;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
class SampleNormalMapPixelF32 : public NormalMapSamplerBase
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
SampleNormalMapPixelF32() {}
|
|
|
|
|
~SampleNormalMapPixelF32() {}
|
|
|
|
|
|
|
|
|
|
FLinearColor DoSampleColor( int32 X, int32 Y )
|
|
|
|
|
{
|
|
|
|
|
const uint8* PixelToSample = SourceTextureData + ((Y * TextureSizeX + X) * sizeof(FLinearColor));
|
|
|
|
|
return *((const FLinearColor*)PixelToSample);
|
2014-03-14 14:13:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float ScaleAndBiasComponent( float Value ) const
|
|
|
|
|
{
|
|
|
|
|
// no need to scale and bias floating point components.
|
|
|
|
|
return Value;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
template<class SamplerClass> class TNormalMapAnalyzer
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
TNormalMapAnalyzer()
|
|
|
|
|
: NumSamplesTaken(0)
|
2015-05-07 05:33:50 -04:00
|
|
|
, NumSamplesRejected(0)
|
2014-03-14 14:13:41 -04:00
|
|
|
, NumSamplesThreshold(0)
|
|
|
|
|
, AverageColor(0.0f,0.0f,0.0f,0.0f)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* EvaluateSubBlock
|
|
|
|
|
* Iterates over all pixels in the specified rectangle, if the resulting pixel
|
|
|
|
|
* isn't black, mid grey or would result in X or Y being -1 or +1 then it is
|
|
|
|
|
* added to the average color and the number of samples count is incremented.
|
|
|
|
|
*/
|
|
|
|
|
void EvaluateSubBlock( int32 Left, int32 Top, int32 Width, int32 Height )
|
|
|
|
|
{
|
|
|
|
|
for ( int32 Y=Top; Y != (Top+Height); Y++ )
|
|
|
|
|
{
|
|
|
|
|
for ( int32 X=Left; X != (Left+Width); X++ )
|
|
|
|
|
{
|
|
|
|
|
FLinearColor ColorSample = Sampler.DoSampleColor( X, Y );
|
2015-05-07 05:33:50 -04:00
|
|
|
|
|
|
|
|
// Nearly black or transparent pixels don't contribute to the calculation
|
|
|
|
|
if (FMath::IsNearlyZero(ColorSample.A, AlphaComponentNearlyZeroThreshold) ||
|
|
|
|
|
ColorSample.IsAlmostBlack())
|
2014-03-14 14:13:41 -04:00
|
|
|
{
|
2015-05-07 05:33:50 -04:00
|
|
|
continue;
|
2014-03-14 14:13:41 -04:00
|
|
|
}
|
2015-05-07 05:33:50 -04:00
|
|
|
|
|
|
|
|
// Scale and bias, if required, to get a signed vector
|
|
|
|
|
float Vx = Sampler.ScaleAndBiasComponent( ColorSample.R );
|
|
|
|
|
float Vy = Sampler.ScaleAndBiasComponent( ColorSample.G );
|
|
|
|
|
float Vz = Sampler.ScaleAndBiasComponent( ColorSample.B );
|
|
|
|
|
|
|
|
|
|
const float Length = FMath::Sqrt(Vx * Vx + Vy * Vy + Vz * Vz);
|
|
|
|
|
if (Length < ColorComponentNearlyZeroThreshold)
|
|
|
|
|
{
|
|
|
|
|
// mid-grey pixels representing (0,0,0) are also not considered as they may be used to denote unused areas
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the vector is sufficiently different in length from a unit vector, consider it invalid.
|
|
|
|
|
if (FMath::Abs(Length - 1.0f) > NormalVectorUnitLengthDeltaThreshold)
|
|
|
|
|
{
|
|
|
|
|
NumSamplesRejected++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the vector is pointing backwards then it is an invalid sample, so consider it invalid
|
|
|
|
|
if (Vz < 0.0f)
|
|
|
|
|
{
|
|
|
|
|
NumSamplesRejected++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AverageColor += ColorSample;
|
|
|
|
|
NumSamplesTaken++;
|
2014-03-14 14:13:41 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* DoesTextureLookLikelyToBeANormalMap
|
|
|
|
|
*
|
|
|
|
|
* Makes a best guess as to whether a texture represents a normal map or not.
|
|
|
|
|
* Will not be 100% accurate, but aims to be as good as it can without usage
|
|
|
|
|
* information or relying on naming conventions.
|
|
|
|
|
*
|
|
|
|
|
* The heuristic takes samples in small blocks across the texture (if the texture
|
|
|
|
|
* is large enough). The assumption is that if the texture represents a normal map
|
|
|
|
|
* then the average direction of the resulting vector should be somewhere near {0,0,1}.
|
|
|
|
|
* It samples in a number of blocks spread out to decrease the chance of hitting a
|
|
|
|
|
* single unused/blank area of texture, which could happen depending on uv layout.
|
|
|
|
|
*
|
|
|
|
|
* Any pixels that are black, mid-gray or have a red or green value resulting in X or Y
|
|
|
|
|
* being -1 or +1 are ignored on the grounds that they are invalid values. Artists
|
|
|
|
|
* sometimes fill the unused areas of normal maps with color being the {0,0,1} vector,
|
|
|
|
|
* but that cannot be relied on - those areas are often black or gray instead.
|
|
|
|
|
*
|
|
|
|
|
* If the heuristic manages to sample enough valid pixels, the threshold being based
|
|
|
|
|
* on the total number of samples it will be looking at, then it takes the average
|
|
|
|
|
* vector of all the sampled pixels and checks to see if the length and direction are
|
|
|
|
|
* within a specific tolerance. See the namespace at the top of the file for tolerance
|
|
|
|
|
* value specifications. If the vector satisfies those tolerances then the texture is
|
|
|
|
|
* considered to be a normal map.
|
|
|
|
|
*/
|
|
|
|
|
bool DoesTextureLookLikelyToBeANormalMap( UTexture* Texture )
|
|
|
|
|
{
|
|
|
|
|
int32 TextureSizeX = Texture->Source.GetSizeX();
|
|
|
|
|
int32 TextureSizeY = Texture->Source.GetSizeY();
|
|
|
|
|
|
|
|
|
|
// Calculate the number of tiles in each axis, but limit the number
|
|
|
|
|
// we interact with to a maximum of 16 tiles (4x4)
|
|
|
|
|
int32 NumTilesX = FMath::Min( TextureSizeX / SampleTileEdgeLength, MaxTilesPerAxis );
|
|
|
|
|
int32 NumTilesY = FMath::Min( TextureSizeY / SampleTileEdgeLength, MaxTilesPerAxis );
|
|
|
|
|
|
2022-09-26 15:24:17 -04:00
|
|
|
if ( ! Sampler.SetSourceTexture( Texture ) )
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2014-03-14 14:13:41 -04:00
|
|
|
|
|
|
|
|
if (( NumTilesX > 0 ) &&
|
|
|
|
|
( NumTilesY > 0 ))
|
|
|
|
|
{
|
|
|
|
|
// If texture is large enough then take samples spread out across the image
|
|
|
|
|
NumSamplesThreshold = (NumTilesX * NumTilesY) * 4; // on average 4 samples per tile need to be valid...
|
|
|
|
|
|
|
|
|
|
for ( int32 TileY = 0; TileY < NumTilesY; TileY++ )
|
|
|
|
|
{
|
|
|
|
|
int Top = (TextureSizeY / NumTilesY) * TileY;
|
|
|
|
|
|
|
|
|
|
for ( int32 TileX = 0; TileX < NumTilesX; TileX++ )
|
|
|
|
|
{
|
|
|
|
|
int Left = (TextureSizeX / NumTilesX) * TileX;
|
|
|
|
|
|
|
|
|
|
EvaluateSubBlock( Left, Top, SampleTileEdgeLength, SampleTileEdgeLength );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
NumSamplesThreshold = (TextureSizeX * TextureSizeY) / 4;
|
|
|
|
|
|
|
|
|
|
// Texture is small enough to sample all texels
|
|
|
|
|
EvaluateSubBlock( 0, 0, TextureSizeX, TextureSizeY );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if we managed to take a reasonable number of samples then we can evaluate the result
|
|
|
|
|
if ( NumSamplesTaken >= NumSamplesThreshold )
|
|
|
|
|
{
|
2015-05-07 05:33:50 -04:00
|
|
|
const float RejectedToTakenRatio = static_cast<float>(NumSamplesRejected) / static_cast<float>(NumSamplesTaken);
|
|
|
|
|
if ( RejectedToTakenRatio >= RejectedToTakenRatioThreshold )
|
|
|
|
|
{
|
|
|
|
|
// Too many invalid samples, probably not a normal map
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-14 14:13:41 -04:00
|
|
|
AverageColor /= (float)NumSamplesTaken;
|
|
|
|
|
|
|
|
|
|
// See if the resulting vector lies anywhere near the {0,0,1} vector
|
|
|
|
|
float Vx = Sampler.ScaleAndBiasComponent( AverageColor.R );
|
|
|
|
|
float Vy = Sampler.ScaleAndBiasComponent( AverageColor.G );
|
|
|
|
|
float Vz = Sampler.ScaleAndBiasComponent( AverageColor.B );
|
|
|
|
|
|
|
|
|
|
float Magnitude = FMath::Sqrt( Vx*Vx + Vy*Vy + Vz*Vz );
|
|
|
|
|
|
|
|
|
|
// The normalized value of the Z component tells us how close to {0,0,1} the average vector is
|
|
|
|
|
float NormalizedZ = Vz / Magnitude;
|
|
|
|
|
|
|
|
|
|
// if the average vector is longer than or equal to the min length, shorter than the max length
|
|
|
|
|
// and the normalized Z value means that the vector is close enough to {0,0,1} then we consider
|
|
|
|
|
// this a normal map
|
|
|
|
|
return ((Magnitude >= NormalMapMinLengthConfidenceThreshold) &&
|
|
|
|
|
(Magnitude < NormalMapMaxLengthConfidenceThreshold) &&
|
|
|
|
|
(NormalizedZ >= NormalMapDeviationThreshold));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Not enough samples, don't trust the result at all
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int32 NumSamplesTaken;
|
2015-05-07 05:33:50 -04:00
|
|
|
int32 NumSamplesRejected;
|
2014-03-14 14:13:41 -04:00
|
|
|
int32 NumSamplesThreshold;
|
|
|
|
|
FLinearColor AverageColor;
|
|
|
|
|
|
|
|
|
|
SamplerClass Sampler;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Attempts to evaluate the pixels in the texture to see if it is a normal map
|
|
|
|
|
*
|
|
|
|
|
* @param Texture The texture to examine
|
|
|
|
|
*
|
|
|
|
|
* @return bool true if the texture is likely a normal map (although it's not necessarily guaranteed)
|
|
|
|
|
*/
|
|
|
|
|
static bool IsTextureANormalMap( UTexture* Texture )
|
|
|
|
|
{
|
2022-03-15 18:29:37 -04:00
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(IsTextureANormalMap);
|
|
|
|
|
|
2014-03-14 14:13:41 -04:00
|
|
|
#if NORMALMAP_IDENTIFICATION_TIMING
|
|
|
|
|
double StartSeconds = FPlatformTime::Seconds();
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// Analyze the source texture to try and figure out if it's a normal map.
|
|
|
|
|
// First check is to make sure it's an appropriate surface format.
|
|
|
|
|
ETextureSourceFormat SourceFormat = Texture->Source.GetFormat();
|
|
|
|
|
|
|
|
|
|
bool bIsNormalMap = false;
|
|
|
|
|
switch ( SourceFormat )
|
|
|
|
|
{
|
|
|
|
|
// The texture could be a normal map if it's one of these formats
|
|
|
|
|
case TSF_BGRA8:
|
|
|
|
|
{
|
|
|
|
|
TNormalMapAnalyzer<SampleNormalMapPixelBGRA8> Analyzer;
|
|
|
|
|
bIsNormalMap = Analyzer.DoesTextureLookLikelyToBeANormalMap( Texture );
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case TSF_RGBA16:
|
|
|
|
|
{
|
2015-05-21 10:47:22 -04:00
|
|
|
TNormalMapAnalyzer<SampleNormalMapPixelRGBA16> Analyzer;
|
2014-03-14 14:13:41 -04:00
|
|
|
bIsNormalMap = Analyzer.DoesTextureLookLikelyToBeANormalMap( Texture );
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case TSF_RGBA16F:
|
|
|
|
|
{
|
|
|
|
|
TNormalMapAnalyzer<SampleNormalMapPixelF16> Analyzer;
|
|
|
|
|
bIsNormalMap = Analyzer.DoesTextureLookLikelyToBeANormalMap( Texture );
|
|
|
|
|
}
|
|
|
|
|
break;
|
2022-03-15 18:29:37 -04:00
|
|
|
case TSF_RGBA32F:
|
2014-03-14 14:13:41 -04:00
|
|
|
{
|
2022-03-15 18:29:37 -04:00
|
|
|
TNormalMapAnalyzer<SampleNormalMapPixelF32> Analyzer;
|
2014-03-14 14:13:41 -04:00
|
|
|
bIsNormalMap = Analyzer.DoesTextureLookLikelyToBeANormalMap( Texture );
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// assume the texture is not a normal map
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if NORMALMAP_IDENTIFICATION_TIMING
|
|
|
|
|
double EndSeconds = FPlatformTime::Seconds();
|
|
|
|
|
|
2021-08-26 10:49:21 -04:00
|
|
|
FString Msg = FString::Printf( TEXT("NormalMapIdentification took %f seconds to analyze %s"), (EndSeconds-StartSeconds), *Texture->GetFullName() );
|
2014-03-14 14:13:41 -04:00
|
|
|
|
|
|
|
|
GLog->Log(Msg);
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
return bIsNormalMap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Class to handle callbacks from notifications informing the user a texture was imported as a normal map */
|
|
|
|
|
class NormalMapImportNotificationHandler : public TSharedFromThis<NormalMapImportNotificationHandler>
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
NormalMapImportNotificationHandler() :
|
|
|
|
|
Texture(NULL)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
~NormalMapImportNotificationHandler()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** This method is invoked when the user clicks the "OK" button on the notification */
|
|
|
|
|
void OKSetting(TSharedPtr<NormalMapImportNotificationHandler>)
|
|
|
|
|
{
|
|
|
|
|
if ( Notification.IsValid() )
|
|
|
|
|
{
|
|
|
|
|
Notification.Pin()->SetCompletionState(SNotificationItem::ECompletionState::CS_Success);
|
|
|
|
|
Notification.Pin()->Fadeout();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* This method is invoked when the user clicked the "Revert" button on the notification */
|
|
|
|
|
void RevertSetting(TSharedPtr<NormalMapImportNotificationHandler>)
|
|
|
|
|
{
|
|
|
|
|
UTexture2D* Texture2D = Texture.IsValid() ? Cast<UTexture2D>(Texture.Get()) : NULL;
|
|
|
|
|
if ( Texture2D )
|
|
|
|
|
{
|
2022-12-12 08:24:56 -05:00
|
|
|
if (FTextureCompilingManager::Get().IsCompilingTexture(Texture2D))
|
|
|
|
|
{
|
|
|
|
|
// Block until compile is done
|
|
|
|
|
TArray<UTexture*> TextureArray;
|
|
|
|
|
TextureArray.Add(Texture2D);
|
|
|
|
|
FTextureCompilingManager::Get().FinishCompilation(TextureArray);
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-14 14:13:41 -04:00
|
|
|
if ( Texture2D->CompressionSettings == TC_Normalmap )
|
|
|
|
|
{
|
2020-09-24 00:43:27 -04:00
|
|
|
// Must wait until the texture is done with previous operations before changing settings and getting it to rebuild.
|
|
|
|
|
Texture2D->WaitForPendingInitOrStreaming();
|
|
|
|
|
|
|
|
|
|
Texture2D->SetFlags(RF_Transactional);
|
2022-10-21 08:50:30 -04:00
|
|
|
// Modify calls FinishCachePlatformData to wait on any async build of this texture
|
2020-09-24 00:43:27 -04:00
|
|
|
Texture2D->Modify();
|
|
|
|
|
Texture2D->PreEditChange(NULL);
|
2014-03-14 14:13:41 -04:00
|
|
|
{
|
2020-09-24 00:43:27 -04:00
|
|
|
Texture2D->CompressionSettings = TC_Default;
|
|
|
|
|
Texture2D->SRGB = true;
|
|
|
|
|
Texture2D->LODGroup = TEXTUREGROUP_World;
|
2014-03-14 14:13:41 -04:00
|
|
|
}
|
2020-09-24 00:43:27 -04:00
|
|
|
Texture2D->PostEditChange();
|
2014-03-14 14:13:41 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( Notification.IsValid() )
|
|
|
|
|
{
|
|
|
|
|
Notification.Pin()->SetCompletionState(SNotificationItem::ECompletionState::CS_Success);
|
|
|
|
|
Notification.Pin()->Fadeout();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TWeakObjectPtr<UTexture> Texture;
|
|
|
|
|
TWeakPtr<SNotificationItem> Notification;
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-26 10:49:21 -04:00
|
|
|
bool UE::NormalMapIdentification::HandleAssetPostImport( UTexture* Texture )
|
2014-03-14 14:13:41 -04:00
|
|
|
{
|
2021-08-26 10:49:21 -04:00
|
|
|
if( Texture != NULL)
|
2014-03-14 14:13:41 -04:00
|
|
|
{
|
|
|
|
|
// Try to automatically identify a normal map
|
2022-10-21 08:50:30 -04:00
|
|
|
// this only reads Texture->Source
|
2021-08-26 10:49:21 -04:00
|
|
|
if ( IsTextureANormalMap( Texture ) )
|
2014-03-14 14:13:41 -04:00
|
|
|
{
|
|
|
|
|
// Set the compression settings and no gamma correction for a normal map
|
|
|
|
|
{
|
|
|
|
|
Texture->SetFlags(RF_Transactional);
|
|
|
|
|
Texture->CompressionSettings = TC_Normalmap;
|
|
|
|
|
Texture->SRGB = false;
|
|
|
|
|
Texture->LODGroup = TEXTUREGROUP_WorldNormalMap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show the user a notification indicating that this texture will be imported as a normal map.
|
|
|
|
|
// Offer two options to the user, "OK" dismisses the notification early, "Revert" reverts the settings to that of a diffuse map.
|
2022-10-21 08:50:30 -04:00
|
|
|
// ?? Guess?? this has to be done from main thread only??
|
2014-03-14 14:13:41 -04:00
|
|
|
TSharedPtr<NormalMapImportNotificationHandler> NormalMapNotificationDelegate(new NormalMapImportNotificationHandler);
|
|
|
|
|
{
|
|
|
|
|
NormalMapNotificationDelegate->Texture = Texture;
|
|
|
|
|
|
|
|
|
|
// this is a cheat to make sure the notification keeps the callback thing alive while it's active...
|
|
|
|
|
FText OKText = LOCTEXT("ImportTexture_OKNormalMapSettings", "OK");
|
|
|
|
|
FText OKTooltipText = LOCTEXT("ImportTexture_OKTooltip", "Accept normal map settings");
|
|
|
|
|
FText RevertText = LOCTEXT("ImportTexture_RevertNormalMapSettings", "Revert");
|
|
|
|
|
FText RevertTooltipText = LOCTEXT("ImportTexture_RevertTooltip", "Revert to diffuse map settings");
|
|
|
|
|
|
|
|
|
|
FFormatNamedArguments Args;
|
|
|
|
|
Args.Add( TEXT("TextureName"), FText::FromName(Texture->GetFName()) );
|
|
|
|
|
FNotificationInfo NormalMapNotification( FText::Format(LOCTEXT("ImportTexture_IsNormalMap", "Texture {TextureName} was imported as a normal map"), Args ) );
|
|
|
|
|
NormalMapNotification.ButtonDetails.Add(FNotificationButtonInfo(OKText, OKTooltipText, FSimpleDelegate::CreateSP(NormalMapNotificationDelegate.Get(), &NormalMapImportNotificationHandler::OKSetting, NormalMapNotificationDelegate)));
|
|
|
|
|
NormalMapNotification.ButtonDetails.Add(FNotificationButtonInfo(RevertText, RevertTooltipText, FSimpleDelegate::CreateSP(NormalMapNotificationDelegate.Get(), &NormalMapImportNotificationHandler::RevertSetting, NormalMapNotificationDelegate)));
|
|
|
|
|
NormalMapNotification.bFireAndForget = true;
|
|
|
|
|
NormalMapNotification.bUseLargeFont = false;
|
|
|
|
|
NormalMapNotification.bUseSuccessFailIcons = false;
|
|
|
|
|
NormalMapNotification.bUseThrobber = false;
|
|
|
|
|
NormalMapNotification.ExpireDuration = 10.0f;
|
|
|
|
|
|
|
|
|
|
NormalMapNotificationDelegate->Notification = FSlateNotificationManager::Get().AddNotification(NormalMapNotification);
|
|
|
|
|
if ( NormalMapNotificationDelegate->Notification.IsValid() )
|
|
|
|
|
{
|
|
|
|
|
NormalMapNotificationDelegate->Notification.Pin()->SetCompletionState(SNotificationItem::CS_Pending);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-08-26 10:49:21 -04:00
|
|
|
|
|
|
|
|
return true;
|
2014-03-14 14:13:41 -04:00
|
|
|
}
|
|
|
|
|
}
|
2021-08-26 10:49:21 -04:00
|
|
|
|
|
|
|
|
return false;
|
2014-03-14 14:13:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#undef LOCTEXT_NAMESPACE
|
2021-08-26 10:49:21 -04:00
|
|
|
|
|
|
|
|
#endif //WITH_EDITOR
|