Files
UnrealEngine/Engine/Source/Runtime/IESFile/Private/IESLoader.cpp
2025-05-18 13:04:45 +08:00

680 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "IESLoader.h"
#include "IESConverter.h"
#include "Math/RandomStream.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_MODULE(FDefaultModuleImpl, IESFile);
enum EIESVersion
{
EIESV_1986, // IES LM-63-1986
EIESV_1991, // IES LM-63-1991
EIESV_1995, // IES LM-63-1995
EIESV_2002, // IES LM-63-2002
};
enum class EIESPhotometricType
{
TypeC = 1,
TypeB = 2,
TypeA = 3
};
DEFINE_LOG_CATEGORY_STATIC(LogIESLoader, Log, All);
// space and return
static void JumpOverWhiteSpace(const uint8*& BufferPos)
{
while(*BufferPos)
{
if(*BufferPos == 13 && *(BufferPos + 1) == 10)
{
BufferPos += 2;
continue;
}
else if(*BufferPos <= ' ')
{
// Skip tab, space and invisible characters
++BufferPos;
continue;
}
break;
}
}
static void GetLineContent(const uint8*& BufferPos, char Line[256], bool bStopOnWhitespace)
{
JumpOverWhiteSpace(BufferPos);
char* LinePtr = Line;
uint32 i;
for(i = 0; i < 255; ++i)
{
if(*BufferPos == 0)
{
break;
}
else if(*BufferPos == '\r' && *(BufferPos + 1) == '\n')
{
BufferPos += 2;
break;
}
else if(*BufferPos == '\n')
{
++BufferPos;
break;
}
else if(bStopOnWhitespace && (*BufferPos <= ' '))
{
// tab, space, invisible characters
++BufferPos;
break;
}
*LinePtr++ = *BufferPos++;
}
Line[i] = 0;
}
// @return success
static bool GetFloat(const uint8*& BufferPos, float& ret)
{
char Line[256];
GetLineContent(BufferPos, Line, true);
ret = FCStringAnsi::Atof(Line);
return true;
}
static bool GetInt(const uint8*& BufferPos, int32& ret)
{
char Line[256];
GetLineContent(BufferPos, Line, true);
ret = FCStringAnsi::Atoi(Line);
return true;
}
FIESLoader::FIESLoader(const uint8* Buffer, uint32 BufferLength)
: Brightness(0)
, CachedIntegral(MAX_FLT)
, PhotometricType(EIESPhotometricType::TypeC)
, Error(TEXT("No data loaded"))
{
check(!IsValid());
// Make sure BufferLength plus the terminator doesn't overflow the size type used by TArray (which is signed).
BufferLength = FMath::Min<uint32>(BufferLength, MAX_int32 - 1);
TArray<uint8> ASCIIFile;
// add 0 termination for easier parsing
{
ASCIIFile.AddUninitialized(BufferLength + 1);
FMemory::Memcpy(ASCIIFile.GetData(), Buffer, BufferLength);
ASCIIFile[BufferLength] = 0;
}
Load(ASCIIFile.GetData());
}
#define PARSE_FLOAT(x) float x; if(!GetFloat(BufferPos, x)) return
#define PARSE_INT(x) int32 x; if(!GetInt(BufferPos, x)) return
void FIESLoader::Load(const uint8* Buffer)
{
// file format as described here: http://www.ltblight.com/English.lproj/LTBLhelp/pages/iesformat.html
const uint8* BufferPos = Buffer;
EIESVersion Version = EIESV_1986;
{
Error = TEXT("VersionError");
char Line1[256];
GetLineContent(BufferPos, Line1, false);
if(FCStringAnsi::Stricmp(Line1, "IESNA:LM-63-1995") == 0)
{
Version = EIESV_1995;
}
else if(FCStringAnsi::Stricmp(Line1, "IESNA91") == 0)
{
Version = EIESV_1991;
}
else if(FCStringAnsi::Stricmp(Line1, "IESNA:LM-63-2002") == 0)
{
Version = EIESV_2002;
}
else
{
Version = EIESV_1986;
// Return buffer to start of file since line read was not the version
BufferPos = Buffer;
}
}
Error = TEXT("HeaderError");
while(*BufferPos)
{
char Line[256];
GetLineContent(BufferPos, Line, false);
if(FCStringAnsi::Strcmp(Line, "TILT=NONE") == 0)
{
// at the moment we don't support only profiles with TILT=NONE
break;
}
else if(FCStringAnsi::Strncmp(Line, "TILT=", 5) == 0)
{
// "TILT=NONE", "TILT=INCLUDE", and "TILT={filename}"
// not supported yet, seems rare
return;
}
}
Error = TEXT("HeaderParameterError");
PARSE_INT(LightCreationCount);
if(LightCreationCount < 1)
{
Error = TEXT("Light count needs to be positive.");
return;
}
// if there is any file with that - do we need to parse it differently?
check(LightCreationCount >= 1);
PARSE_FLOAT(LumensPerLamp);
PARSE_FLOAT(CandelaMult);
if(CandelaMult < 0)
{
Error = TEXT("CandelaMult is negative");
return;
}
PARSE_INT(VAnglesNum);
PARSE_INT(HAnglesNum);
if(VAnglesNum < 0)
{
Error = TEXT("VAnglesNum is not valid");
return;
}
if(HAnglesNum < 0)
{
Error = TEXT("HAnglesNum is not valid");
return;
}
PARSE_INT(PhotometricTypeTemp);
if ( PhotometricTypeTemp >= (int32)EIESPhotometricType::TypeC && PhotometricTypeTemp <= (int32)EIESPhotometricType::TypeA )
{
PhotometricType = EIESPhotometricType(PhotometricTypeTemp);
}
// 1:feet, 2:meter
PARSE_INT(UnitType);
PARSE_FLOAT(Width);
PARSE_FLOAT(Length);
PARSE_FLOAT(Height);
PARSE_FLOAT(BallastFactor);
PARSE_FLOAT(FutureUse);
// check(FutureUse == 1.0f);
PARSE_FLOAT(InputWatts);
Error = TEXT("ContentError");
{
float MinSoFar = -FLT_MAX;
VAngles.Empty(VAnglesNum);
for(uint32 y = 0; y < (uint32)VAnglesNum; ++y)
{
PARSE_FLOAT(Value);
if(Value < MinSoFar)
{
// binary search later relies on that
Error = TEXT("V Values are not in increasing order");
return;
}
MinSoFar = Value;
VAngles.Add(Value);
}
}
{
float MinSoFar = -FLT_MAX;
HAngles.Empty(HAnglesNum);
for(uint32 x = 0; x < (uint32)HAnglesNum; ++x)
{
PARSE_FLOAT(Value);
if(Value < MinSoFar)
{
// binary search later relies on that
Error = TEXT("H Values are not in increasing order");
return;
}
MinSoFar = Value;
HAngles.Add(Value);
}
}
CandelaValues.Empty(HAnglesNum * VAnglesNum);
for(uint32 y = 0; y < (uint32)HAnglesNum; ++y)
{
for(uint32 x = 0; x < (uint32)VAnglesNum; ++x)
{
PARSE_FLOAT(Value);
CandelaValues.Add(Value * CandelaMult);
}
}
Error = TEXT("Unexpected content after candela values.");
JumpOverWhiteSpace(BufferPos);
if(*BufferPos)
{
// some files are terminated with "END"
char Line[256];
GetLineContent(BufferPos, Line, true);
if(FCStringAnsi::Strcmp(Line, "END") == 0)
{
JumpOverWhiteSpace(BufferPos);
}
}
if(*BufferPos)
{
Error = TEXT("Unexpected content after END.");
return;
}
Error = nullptr;
Brightness = ComputeMax(); // Use max candela as the brightness
if(Brightness <= 0)
{
// some samples have -1, then the brightness comes from the samples
// Brightness = ComputeFullIntegral();
// use some reasonable value
Brightness = 1000;
}
}
#undef PARSE_FLOAT
#undef PARSE_INT
uint32 FIESLoader::GetWidth() const
{
return 256;
}
uint32 FIESLoader::GetHeight() const
{
return 256;
}
bool FIESLoader::IsValid() const
{
return Error.IsEmpty();
}
float FIESLoader::GetBrightness() const
{
return Brightness;
}
float FIESLoader::ExtractInRGBA16F(TArray<uint8>& OutData)
{
check(IsValid());
uint32 Width = GetWidth();
uint32 Height = GetHeight();
check(!OutData.Num());
OutData.AddZeroed(Width * Height * sizeof(FFloat16Color));
FFloat16Color* Out = (FFloat16Color*)OutData.GetData();
float InvWidth = 1.0f / Width;
float InvHeight = 1.0f / Height;
float MaxValue = ComputeMax();
float InvMaxValue= 1.0f / MaxValue;
for(uint32 y = 0; y < Height; ++y)
{
float HFraction = y * InvHeight;
for(uint32 x = 0; x < Width; ++x)
{
// 0..1
float VFraction = x * InvWidth;
// distort for better quality?
// Fraction = Square(Fraction);
float FloatValue = InvMaxValue * Interpolate2D(HFraction * 360.0f, VFraction * 180.0f);
{
FFloat16 HalfValue(FloatValue);
FFloat16Color HalfColor;
HalfColor.R = HalfValue;
HalfColor.G = HalfValue;
HalfColor.B = HalfValue;
HalfColor.A = HalfValue;
*Out++ = HalfColor;
}
}
}
return 1.f;
}
float FIESLoader::ComputeFullIntegral()
{
// compute only if needed
if(CachedIntegral == MAX_FLT)
{
// monte carlo integration
// if quality is a problem we can improve on this algorithm or increase SampleCount
// larger number costs more time but improves quality
uint32 SampleCount = 1000000;
FRandomStream RandomStream(0x1234);
double Sum = 0;
for(uint32 i = 0; i < SampleCount; ++i)
{
FVector Dir = RandomStream.GetUnitVector();
// http://en.wikipedia.org/wiki/Spherical_coordinate_system
// 0..180
float VAngle = FMath::Acos(Dir.Z) / PI * 180;
// 0..360
float HAngle = FMath::Atan2(Dir.Y, Dir.X) / PI * 180 + 180;
check(VAngle >= 0 && VAngle <= 180);
check(HAngle >= 0 && HAngle <= 360);
Sum += Interpolate2D(HAngle, VAngle);
}
CachedIntegral = Sum / SampleCount;
}
return CachedIntegral;
}
float FIESLoader::ComputeMax() const
{
float ret = 0.0f;
for(uint32 i = 0; i < (uint32)CandelaValues.Num(); ++i)
{
float Value = CandelaValues[i];
ret = FMath::Max(ret, Value);
}
return ret;
}
float FIESLoader::ComputeFilterPos(float Value, const TArray<float>& SortedValues)
{
check(SortedValues.Num());
uint32 StartPos = 0;
uint32 EndPos = SortedValues.Num() - 1;
if(Value < SortedValues[StartPos])
{
return 0.0f;
}
if(Value > SortedValues[EndPos])
{
return (float)EndPos;
}
// binary search
while(StartPos < EndPos)
{
uint32 TestPos = (StartPos + EndPos + 1) / 2;
float TestValue = SortedValues[TestPos];
if(Value >= TestValue)
{
// prevent endless loop
check(StartPos != TestPos);
StartPos = TestPos;
}
else
{
// prevent endless loop
check(EndPos != TestPos - 1);
EndPos = TestPos - 1;
}
}
float LeftValue = SortedValues[StartPos];
float Fraction = 0.0f;
if(StartPos + 1 < (uint32)SortedValues.Num())
{
// if not at right border
float RightValue = SortedValues[StartPos + 1];
float DeltaValue = RightValue - LeftValue;
if(DeltaValue > 0.0001f)
{
Fraction = (Value - LeftValue) / DeltaValue;
}
}
return StartPos + Fraction;
}
float FIESLoader::InterpolatePoint(int X, int Y) const
{
uint32 HAnglesNum = HAngles.Num();
uint32 VAnglesNum = VAngles.Num();
check(X >= 0);
check(Y >= 0);
X %= HAnglesNum;
Y %= VAnglesNum;
check(X < (int)HAnglesNum);
check(Y < (int)VAnglesNum);
return CandelaValues[Y + VAnglesNum * X];
}
float FIESLoader::InterpolateBilinear(float fX, float fY) const
{
int X = (int)fX;
int Y = (int)fY;
float fracX = fX - X;
float fracY = fY - Y;
float p00 = InterpolatePoint(X + 0, Y + 0);
float p10 = InterpolatePoint(X + 1, Y + 0);
float p01 = InterpolatePoint(X + 0, Y + 1);
float p11 = InterpolatePoint(X + 1, Y + 1);
float p0 = FMath::Lerp(p00, p01, fracY);
float p1 = FMath::Lerp(p10, p11, fracY);
return FMath::Lerp(p0, p1, fracX);
}
float FIESLoader::Interpolate2D(float HAngle, float VAngle) const
{
float u = 0.0f;
float v = 0.0f;
if (PhotometricType == EIESPhotometricType::TypeA)
{
float X = FMath::Cos(FMath::DegreesToRadians(VAngle));
float Y = FMath::Sin(FMath::DegreesToRadians(VAngle)) * FMath::Sin(FMath::DegreesToRadians(HAngle));
float Z = FMath::Sin(FMath::DegreesToRadians(VAngle)) * FMath::Cos(FMath::DegreesToRadians(HAngle));
float NewVAngle = FMath::RadiansToDegrees(FMath::Asin(Z));
float NewHAngle = -FMath::RadiansToDegrees(FMath::Atan2(Y, X));
u = ComputeFilterPos(NewHAngle, HAngles);
v = ComputeFilterPos(NewVAngle, VAngles);
}
else
{
// Support symmetry, per the IES format doc:
// 0 There is only one horizontal angle, implying that the luminaire is laterally symmetric in all photometric planes.
// 90 The luminaire is assumed to be symmetric in each quadrant.
// 180 The luminaire is assumed to be bilaterally symmetric about the 0-180 degree photometric plane.
// 360 The luminaire is assumed to exhibit no lateral symmetry.
//
// A luminaire that is bilaterally symmetric about the 90-270 degree
// photometric plane will have a first value of 90 degrees and a last value of 270 degrees.
if (HAngles.Num() > 0)
{
if (HAngles.Last() > 0.f && HAngle > HAngles.Last())
{
bool bFlipHAngle = false;
int32 HQuadrant = (int32)HAngle / HAngles.Last();
if ( HQuadrant == 1 || HQuadrant == 3 )
{
bFlipHAngle = true;
}
if ( bFlipHAngle )
{
HAngle = HAngles.Last() - FMath::Fmod(HAngle, HAngles.Last());
}
else
{
HAngle = FMath::Fmod(HAngle, HAngles.Last());
}
}
else if (HAngles[0] > 0.f && HAngle < HAngles[0])
{
HAngle = HAngles.Last() - HAngles[0] - HAngle;
}
}
u = ComputeFilterPos(HAngle, HAngles);
v = ComputeFilterPos(VAngle, VAngles);
}
return InterpolateBilinear(u, v);
}
float FIESLoader::Interpolate1D(float VAngle) const
{
float v = ComputeFilterPos(VAngle, VAngles);
float ret = 0.0f;
uint32 HAnglesNum = (uint32)HAngles.Num();
for(uint32 i = 0; i < HAnglesNum; ++i)
{
ret += InterpolateBilinear((float)i, v);
}
return ret / HAnglesNum;
}
FIESConverter::FIESConverter(const uint8* Buffer, uint32 BufferLength)
{
Impl = MakeShared<FIESLoader>(Buffer, BufferLength);
if (Impl->IsValid())
{
Multiplier = Impl->ExtractInRGBA16F(RawData);
}
}
bool FIESConverter::IsValid() const
{
return Impl->IsValid();
}
const TCHAR* FIESConverter::GetError() const
{
return Impl->GetError();
}
uint32 FIESConverter::GetWidth() const
{
return Impl->GetWidth();
}
uint32 FIESConverter::GetHeight() const
{
return Impl->GetHeight();
}
float FIESConverter::GetBrightness() const
{
return Impl->GetBrightness();
}