Files
sbox-public/engine/Sandbox.Engine/Resources/Textures/Bitmap/Bitmap.Loading.Ies.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

244 lines
5.8 KiB
C#

using SkiaSharp;
using System.IO;
using System.Text;
namespace Sandbox;
public partial class Bitmap
{
public unsafe static Bitmap CreateFromIesBytes( byte[] data )
{
if ( !IsIes( data ) )
return default;
const int width = 512;
const int height = 512;
var imageData = new Half[width * height * 4];
var bitmap = new SKBitmap( width, height, SKColorType.RgbaF16, SKAlphaType.Unpremul );
var ies = new IES();
try
{
using var stream = new MemoryStream( data );
ies.ParseIESInfo( stream );
}
catch ( Exception ex )
{
Log.Warning( $"Failed to parse IES file: {ex.Message}" );
return default;
}
if ( ies.CandelasMax <= 0 )
{
Log.Warning( "IES file has invalid maximum candela value." );
return default;
}
var brightness = 1.0f / ies.CandelasMax;
for ( var y = 0; y < height; y++ )
{
for ( var x = 0; x < width; x++ )
{
var px = (x / (float)width) * 2 - 1;
var py = (y / (float)height) * 2 - 1;
var radius = MathF.Sqrt( px * px + py * py );
var angle = MathF.Atan2( py, px ) * 180 / MathF.PI;
var candela = ies.Sample2D( radius, angle );
candela = MathF.Max( 0, candela * brightness );
candela = MathF.Pow( candela, 2.2f );
var idx = (y * width + x) * 4;
imageData[idx + 0] = (Half)candela;
imageData[idx + 1] = (Half)candela;
imageData[idx + 2] = (Half)candela;
imageData[idx + 3] = (Half)1.0f;
}
}
unsafe
{
fixed ( Half* ptr = imageData )
{
bitmap.SetPixels( (IntPtr)ptr );
}
}
return new Bitmap( bitmap );
}
private class IES
{
public int NumLamps;
public float LumensPerLamp;
public float CandelaMultiplier;
public int NumHorizontal;
public int NumVertical;
public int PhotometricType;
public int UnitsType;
public float Width;
public float Length;
public float Height;
public float BallastFactor;
public float Unused;
public float InputWatts;
public List<float> VerticalAngles;
public List<float> HorizontalAngles;
public List<float> Candelas;
public float CandelasMax;
public void ParseIESInfo( Stream stream )
{
var reader = new StreamReader( stream );
_ = reader.ReadLine();
// Read until tilt information, we don't care the metadata that's behind
while ( !reader.EndOfStream )
{
var line = reader.ReadLine();
if ( line.StartsWith( "TILT=" ) )
{
var tilt = line.Substring( 5 );
if ( tilt != "NONE" )
{
throw new Exception( "Tilted IES files are not supported" );
}
break;
}
}
if ( reader.EndOfStream )
{
throw new Exception( "IES file is missing TILT information" );
}
// We get the light info on this line
var lightInfo = reader.ReadToEnd().Split( new string[] { " ", "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries );
NumLamps = int.Parse( lightInfo[0] );
LumensPerLamp = float.Parse( lightInfo[1] );
CandelaMultiplier = float.Parse( lightInfo[2] );
NumVertical = int.Parse( lightInfo[3] );
NumHorizontal = int.Parse( lightInfo[4] );
PhotometricType = int.Parse( lightInfo[5] );
UnitsType = int.Parse( lightInfo[6] );
Width = float.Parse( lightInfo[7] );
Length = float.Parse( lightInfo[8] );
Height = float.Parse( lightInfo[9] );
BallastFactor = float.Parse( lightInfo[10] );
Unused = float.Parse( lightInfo[11] );
InputWatts = float.Parse( lightInfo[12] );
VerticalAngles = new List<float>();
HorizontalAngles = new List<float>();
Candelas = new List<float>();
int i = 13;
for ( int y = 0; y < NumVertical; y++, i++ )
{
VerticalAngles.Add( float.Parse( lightInfo[i] ) );
}
for ( int x = 0; x < NumHorizontal; x++, i++ )
{
HorizontalAngles.Add( float.Parse( lightInfo[i] ) );
}
for ( int x = 0; x < NumHorizontal; x++ )
{
for ( int y = 0; y < NumVertical; y++, i++ )
{
Candelas.Add( float.Parse( lightInfo[i] ) );
}
}
CandelasMax = Candelas.Max();
}
public float Sample2D( float radius, float angleDeg )
{
// Early out if radius is beyond 1
if ( radius > 1.0f ) return 0;
// Convert angle to 0-360 range
angleDeg = ((angleDeg % 360) + 360) % 360;
// Mirror angles above 180 degrees back to 0-180 range
if ( angleDeg > 180.0f )
{
angleDeg = 360.0f - angleDeg;
}
// Find horizontal angle indices
int h1 = 0, h2 = 0;
float hBlend = 0;
for ( int i = 0; i < NumHorizontal - 1; i++ )
{
if ( angleDeg >= HorizontalAngles[i] && angleDeg <= HorizontalAngles[i + 1] )
{
h1 = i;
h2 = i + 1;
hBlend = (angleDeg - HorizontalAngles[i]) / (HorizontalAngles[i + 1] - HorizontalAngles[i]);
break;
}
}
// Convert radius to vertical angle (0 = straight down, 1 = horizontal)
float vertAngle = 90.0f * radius;
// Find vertical angle indices
int v1 = 0, v2 = 0;
float vBlend = 0;
for ( int i = 0; i < NumVertical - 1; i++ )
{
if ( vertAngle >= VerticalAngles[i] && vertAngle <= VerticalAngles[i + 1] )
{
v1 = i;
v2 = i + 1;
vBlend = (vertAngle - VerticalAngles[i]) / (VerticalAngles[i + 1] - VerticalAngles[i]);
break;
}
}
// Bilinear interpolation of candela values
float c11 = Candelas[h1 * NumVertical + v1];
float c12 = Candelas[h1 * NumVertical + v2];
float c21 = Candelas[h2 * NumVertical + v1];
float c22 = Candelas[h2 * NumVertical + v2];
float c1 = c11 * (1 - vBlend) + c12 * vBlend;
float c2 = c21 * (1 - vBlend) + c22 * vBlend;
float candela = c1 * (1 - hBlend) + c2 * hBlend;
// Apply candela multiplier
return candela * CandelaMultiplier;
}
}
/// <summary>
/// Return true if this data is a Ies file
/// </summary>
public static bool IsIes( byte[] data )
{
if ( data == null || data.Length < 10 )
return false;
var header = Encoding.ASCII.GetString( data, 0, Math.Min( 20, data.Length ) ).Trim();
return header.StartsWith( "IESNA" ) || header.StartsWith( "IESNA:LM-63" );
}
}