s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
link Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=brax.terrainconvert&take=20
Showing code results for query:
*
(4 total matches found)
Editor
library
using System;
using System.Linq;
using Sandbox;
namespace Editor.TerrainConvert;
/// <summary>
/// Which source channel to read the height value from.
/// </summary>
public enum HeightChannel
{
/// <summary> Red channel only. Standard for grayscale heightmaps. </summary>
Red,
Green,
Blue,
Alpha,
/// <summary> Rec.709 weighted luminance of RGB. </summary>
Luminance,
/// <summary> Average of the RGB channels. </summary>
Average,
/// <summary> The largest of the RGB channels. </summary>
Max,
}
/// <summary>
/// Bit depth of the output .raw file. s&box terrain stores heights as 16-bit,
/// so 16-bit is recommended for the best precision.
/// </summary>
public enum HeightBitDepth
{
/// <summary> 16-bit per height sample (recommended). 2 bytes per pixel. </summary>
Bit16,
/// <summary> 8-bit per height sample. 1 byte per pixel. </summary>
Bit8,
}
/// <summary>
/// How the source values are remapped into the 0..1 height range before quantizing.
/// </summary>
public enum HeightNormalize
{
/// <summary> Use values as-is, clamped to 0..1. </summary>
Clamp,
/// <summary> Stretch the min..max range of the image to fill 0..1 (good for HDR/EXR). </summary>
MinMaxStretch,
}
/// <summary>
/// Byte order of multi-byte (16-bit) samples in the output file.
/// </summary>
public enum HeightByteOrder
{
/// <summary> Little-endian. What the s&box terrain importer reads by default ("Windows"). </summary>
LittleEndian,
/// <summary> Big-endian ("Mac"). </summary>
BigEndian,
}
/// <summary>
/// Settings for converting an image into a terrain heightmap .raw file.
/// </summary>
public class HeightmapConvertSettings
{
/// <summary>
/// Which channel of the source image holds the height. Most heightmaps are
/// grayscale, where every channel is identical - <see cref="HeightChannel.Red"/> works for those.
/// </summary>
[Property]
public HeightChannel Channel { get; set; } = HeightChannel.Red;
/// <summary>
/// 16-bit gives the smoothest terrain and matches what s&box stores internally.
/// </summary>
[Property]
public HeightBitDepth BitDepth { get; set; } = HeightBitDepth.Bit16;
/// <summary>
/// Output heightmaps are always square. If 0, the smaller of the source
/// dimensions is used. The image is resampled to this resolution.
/// </summary>
[Property, Range( 0, 8192 )]
public int Resolution { get; set; } = 0;
/// <summary>
/// Round the output resolution down to the nearest power of two (e.g 1024, 2048).
/// The terrain system expects power-of-two heightmaps, so leave this on.
/// </summary>
[Property]
public bool PowerOfTwo { get; set; } = true;
/// <summary>
/// How source values are mapped to the height range. Use <see cref="HeightNormalize.MinMaxStretch"/>
/// for HDR images that don't already fill 0..1.
/// </summary>
[Property]
public HeightNormalize Normalize { get; set; } = HeightNormalize.Clamp;
/// <summary>
/// Flip the image vertically. Image and terrain coordinate origins often differ;
/// toggle this if your terrain comes out mirrored north/south.
/// </summary>
[Property]
public bool FlipVertical { get; set; } = false;
/// <summary>
/// Invert the heights (high becomes low). Useful for depth/inverted maps.
/// </summary>
[Property]
public bool Invert { get; set; } = false;
/// <summary>
/// Byte order of 16-bit samples. The s&box importer reads little-endian by default.
/// </summary>
[Property, ShowIf( nameof( BitDepth ), HeightBitDepth.Bit16 )]
public HeightByteOrder ByteOrder { get; set; } = HeightByteOrder.LittleEndian;
}
/// <summary>
/// Converts loaded images into raw single-channel heightmaps compatible with the
/// s&box terrain importer (square, 8 or 16 bit, raw samples with no header).
/// </summary>
public static class HeightmapConverter
{
/// <summary>
/// Convert a bitmap into a raw heightmap byte buffer.
/// </summary>
/// <param name="bitmap">Source image.</param>
/// <param name="settings">Conversion settings.</param>
/// <param name="resolution">The square resolution of the produced heightmap.</param>
/// <returns>Raw heightmap bytes ready to write to a .raw file.</returns>
public static byte[] Convert( Bitmap bitmap, HeightmapConvertSettings settings, out int resolution )
{
ArgumentNullException.ThrowIfNull( bitmap );
ArgumentNullException.ThrowIfNull( settings );
var pixels = bitmap.GetPixels();
int count = bitmap.Width * bitmap.Height;
// Extract the chosen channel as a float per pixel, then run the shared pipeline.
var values = new float[count];
for ( int i = 0; i < count; i++ )
values[i] = SampleChannel( pixels[i], settings.Channel );
return BuildRaw( values, bitmap.Width, bitmap.Height, settings, out resolution );
}
/// <summary>
/// Convert a decoded EXR into a raw heightmap byte buffer, reading height from the
/// chosen channel (falling back to Y/R/first channel if the exact one is absent).
/// </summary>
public static byte[] Convert( ExrImage exr, HeightmapConvertSettings settings, out int resolution )
{
ArgumentNullException.ThrowIfNull( exr );
ArgumentNullException.ThrowIfNull( settings );
var values = SampleChannel( exr, settings.Channel );
return BuildRaw( values, exr.Width, exr.Height, settings, out resolution );
}
/// <summary>
/// Shared pipeline: resample a single-channel float plane to a square power-of-two,
/// normalize into 0..1, optionally invert, and quantize to raw bytes.
/// </summary>
static byte[] BuildRaw( float[] values, int srcWidth, int srcHeight, HeightmapConvertSettings settings, out int resolution )
{
resolution = ResolveResolution( srcWidth, srcHeight, settings );
// Bilinear resample to a square of the target resolution, working entirely in float
// so we don't lose precision on high bit-depth / HDR sources. Resampling produces a
// fresh array; otherwise clone so the in-place normalize/invert never mutates a
// caller-owned buffer (e.g. an EXR's cached channel plane).
values = srcWidth != resolution || srcHeight != resolution
? ResampleBilinear( values, srcWidth, srcHeight, resolution, resolution )
: (float[])values.Clone();
ApplyNormalize( values, settings.Normalize );
if ( settings.Invert )
{
for ( int i = 0; i < values.Length; i++ )
values[i] = 1f - values[i];
}
return Quantize( values, resolution, settings );
}
static float[] ResampleBilinear( float[] src, int srcW, int srcH, int dstW, int dstH )
{
var dst = new float[dstW * dstH];
for ( int y = 0; y < dstH; y++ )
{
float fy = dstH > 1 ? (float)y / (dstH - 1) * (srcH - 1) : 0f;
int y0 = (int)fy;
int y1 = Math.Min( y0 + 1, srcH - 1 );
float ty = fy - y0;
for ( int x = 0; x < dstW; x++ )
{
float fx = dstW > 1 ? (float)x / (dstW - 1) * (srcW - 1) : 0f;
int x0 = (int)fx;
int x1 = Math.Min( x0 + 1, srcW - 1 );
float tx = fx - x0;
float top = MathX.Lerp( src[y0 * srcW + x0], src[y0 * srcW + x1], tx );
float bottom = MathX.Lerp( src[y1 * srcW + x0], src[y1 * srcW + x1], tx );
dst[y * dstW + x] = MathX.Lerp( top, bottom, ty );
}
}
return dst;
}
static float[] SampleChannel( ExrImage exr, HeightChannel channel )
{
// For multi-channel EXRs, blend RGB the same way the bitmap path does. For the
// common single-channel (Y) heightmap, every option resolves to that one plane.
switch ( channel )
{
case HeightChannel.Red: return exr.GetHeightChannel( "R" );
case HeightChannel.Green: return exr.GetHeightChannel( "G" );
case HeightChannel.Blue: return exr.GetHeightChannel( "B" );
case HeightChannel.Alpha: return exr.GetHeightChannel( "A" );
}
var r = exr.GetChannel( "R" );
var g = exr.GetChannel( "G" );
var b = exr.GetChannel( "B" );
// No RGB set - it's a luminance/single-channel image, use it directly.
if ( r is null || g is null || b is null )
return exr.GetHeightChannel();
var outv = new float[r.Length];
for ( int i = 0; i < outv.Length; i++ )
{
outv[i] = channel switch
{
HeightChannel.Luminance => 0.2126f * r[i] + 0.7152f * g[i] + 0.0722f * b[i],
HeightChannel.Average => (r[i] + g[i] + b[i]) / 3f,
HeightChannel.Max => Math.Max( r[i], Math.Max( g[i], b[i] ) ),
_ => r[i],
};
}
return outv;
}
static int ResolveResolution( int width, int height, HeightmapConvertSettings settings )
{
int res = settings.Resolution > 0 ? settings.Resolution : Math.Min( width, height );
if ( settings.PowerOfTwo )
res = RoundDownToPowerOfTwo( res );
return Math.Clamp( res, 4, 16384 );
}
static float SampleChannel( Color c, HeightChannel channel ) => channel switch
{
HeightChannel.Red => c.r,
HeightChannel.Green => c.g,
HeightChannel.Blue => c.b,
HeightChannel.Alpha => c.a,
HeightChannel.Luminance => 0.2126f * c.r + 0.7152f * c.g + 0.0722f * c.b,
HeightChannel.Average => (c.r + c.g + c.b) / 3f,
HeightChannel.Max => Math.Max( c.r, Math.Max( c.g, c.b ) ),
_ => c.r,
};
static void ApplyNormalize( float[] values, HeightNormalize mode )
{
if ( mode == HeightNormalize.MinMaxStretch )
{
float min = values.Min();
float max = values.Max();
float range = max - min;
if ( range > 1e-6f )
{
for ( int i = 0; i < values.Length; i++ )
values[i] = (values[i] - min) / range;
return;
}
// Flat image - fall through to clamp.
}
for ( int i = 0; i < values.Length; i++ )
values[i] = Math.Clamp( values[i], 0f, 1f );
}
static byte[] Quantize( float[] values, int resolution, HeightmapConvertSettings settings )
{
bool flip = settings.FlipVertical;
if ( settings.BitDepth == HeightBitDepth.Bit8 )
{
var bytes = new byte[resolution * resolution];
for ( int y = 0; y < resolution; y++ )
{
int srcY = flip ? resolution - 1 - y : y;
for ( int x = 0; x < resolution; x++ )
{
float v = Math.Clamp( values[srcY * resolution + x], 0f, 1f );
bytes[y * resolution + x] = (byte)MathF.Round( v * byte.MaxValue );
}
}
return bytes;
}
else
{
bool little = settings.ByteOrder == HeightByteOrder.LittleEndian;
var bytes = new byte[resolution * resolution * 2];
for ( int y = 0; y < resolution; y++ )
{
int srcY = flip ? resolution - 1 - y : y;
for ( int x = 0; x < resolution; x++ )
{
float v = Math.Clamp( values[srcY * resolution + x], 0f, 1f );
ushort h = (ushort)MathF.Round( v * ushort.MaxValue );
int o = (y * resolution + x) * 2;
if ( little )
{
bytes[o] = (byte)(h & 0xFF);
bytes[o + 1] = (byte)(h >> 8);
}
else
{
bytes[o] = (byte)(h >> 8);
bytes[o + 1] = (byte)(h & 0xFF);
}
}
}
return bytes;
}
}
/// <summary>
/// Rounds a value down to the nearest power of two.
/// </summary>
public static int RoundDownToPowerOfTwo( int value )
{
if ( value < 1 ) return 1;
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return value - (value >> 1);
}
}
Editor
library
using System;
using System.IO;
using Sandbox;
namespace Editor.TerrainConvert;
/// <summary>
/// Editor tool that converts an image (.png, .jpg, .tga, .tif, .psd, .exr ...) into a
/// raw single-channel heightmap (.raw) that the s&box terrain importer can load.
/// </summary>
[EditorApp( "Heightmap Converter", "terrain", "Convert an image into a terrain heightmap (.raw)" )]
public class HeightmapConvertWindow : Widget
{
HeightmapConvertSettings Settings { get; set; } = new();
string inputFile;
Bitmap loadedBitmap;
ExrImage loadedExr;
int srcWidth, srcHeight;
bool HasImage => loadedBitmap is not null || loadedExr is not null;
Label inputLabel;
Label outputInfoLabel;
Button convertButton;
public HeightmapConvertWindow() : this( null ) { }
public HeightmapConvertWindow( Widget parent ) : base( parent )
{
WindowFlags = WindowFlags.Dialog | WindowFlags.Customized | WindowFlags.WindowTitle | WindowFlags.CloseButton | WindowFlags.WindowSystemMenuHint;
DeleteOnClose = true;
WindowTitle = "Convert Image to Heightmap";
SetWindowIcon( "terrain" );
Layout = Layout.Column();
Layout.Spacing = 8;
Layout.Margin = 16;
var warning = new WarningBox(
"Converts an image into a raw heightmap (.raw) for the terrain system.\n" +
"Output is square, single-channel, 8 or 16 bit. Import it via the Terrain component's heightmap import.", this );
Layout.Add( warning );
// Input file row
{
var row = Layout.Row();
row.Spacing = 8;
inputLabel = new Label( "No image selected", this );
inputLabel.WordWrap = false;
row.Add( inputLabel, 1 );
var browse = new Button( "Browse Image...", "image", this );
browse.Clicked = PickInputFile;
row.Add( browse );
Layout.Add( row );
}
// Settings sheet
{
var so = EditorUtility.GetSerializedObject( Settings );
so.OnPropertyChanged += _ => UpdateInfo();
var sheet = new ControlSheet();
sheet.AddObject( so );
Layout.Add( sheet );
}
outputInfoLabel = new Label( "", this );
outputInfoLabel.Color = Theme.TextControl.WithAlpha( 0.6f );
Layout.Add( outputInfoLabel );
Layout.AddStretchCell();
// Bottom bar
{
var row = Layout.Row();
row.Margin = new Sandbox.UI.Margin( 0, 8, 0, 0 );
row.AddStretchCell();
convertButton = new Button.Primary( "Convert & Save...", "file_download", this );
convertButton.Clicked = ConvertAndSave;
convertButton.Enabled = false;
row.Add( convertButton );
Layout.Add( row );
}
Width = 440;
MinimumWidth = 380;
Height = 520;
UpdateInfo();
Show();
Focus();
}
void PickInputFile()
{
var fd = new FileDialog( null )
{
Title = "Select Source Image",
};
fd.SetFindFile();
fd.SetModeOpen();
fd.SetNameFilter( "Images (*.png *.jpg *.jpeg *.tga *.tif *.tiff *.psd *.exr *.bmp *.raw)" );
if ( !fd.Execute() )
return;
LoadInput( fd.SelectedFile );
}
void LoadInput( string path )
{
loadedBitmap?.Dispose();
loadedBitmap = null;
loadedExr = null;
inputFile = null;
try
{
var bytes = File.ReadAllBytes( path );
// EXR isn't supported by the Skia-backed Bitmap loader, so decode it ourselves.
bool isExr = path.EndsWith( ".exr", StringComparison.OrdinalIgnoreCase ) || ExrImage.IsExr( bytes );
if ( isExr )
{
loadedExr = ExrImage.Load( bytes );
srcWidth = loadedExr.Width;
srcHeight = loadedExr.Height;
inputFile = path;
}
else
{
var bitmap = Bitmap.CreateFromBytes( bytes );
if ( bitmap is null || !bitmap.IsValid )
{
EditorUtility.DisplayDialog( "Couldn't load image",
$"Failed to decode '{Path.GetFileName( path )}'.\nThis image format may not be supported." );
return;
}
loadedBitmap = bitmap;
srcWidth = bitmap.Width;
srcHeight = bitmap.Height;
inputFile = path;
}
}
catch ( Exception e )
{
EditorUtility.DisplayDialog( "Couldn't load image", e.Message );
return;
}
UpdateInfo();
}
void UpdateInfo()
{
if ( !HasImage )
{
inputLabel.Text = "No image selected";
outputInfoLabel.Text = "Pick an image to begin.";
convertButton.Enabled = false;
return;
}
string kind = loadedExr is not null ? "EXR" : "image";
inputLabel.Text = $"{Path.GetFileName( inputFile )} ({srcWidth}x{srcHeight}, {kind})";
convertButton.Enabled = true;
int res = PreviewResolution();
string depth = Settings.BitDepth == HeightBitDepth.Bit16 ? "16-bit" : "8-bit";
int bytes = res * res * (Settings.BitDepth == HeightBitDepth.Bit16 ? 2 : 1);
outputInfoLabel.Text = $"Output: {res}x{res}, {depth} → {bytes / 1024:n0} KB raw";
}
int PreviewResolution()
{
int res = Settings.Resolution > 0 ? Settings.Resolution : Math.Min( srcWidth, srcHeight );
if ( Settings.PowerOfTwo )
res = HeightmapConverter.RoundDownToPowerOfTwo( res );
return Math.Clamp( res, 4, 16384 );
}
void ConvertAndSave()
{
if ( !HasImage )
return;
var fd = new FileDialog( null )
{
Title = "Save Heightmap",
DefaultSuffix = ".raw",
};
fd.Directory = Path.GetDirectoryName( inputFile );
fd.SelectFile( $"{Path.GetFileNameWithoutExtension( inputFile )}.raw" );
fd.SetFindFile();
fd.SetModeSave();
fd.SetNameFilter( "Raw Heightmap (*.raw *.r16 *.r8)" );
if ( !fd.Execute() )
return;
try
{
int resolution;
var data = loadedExr is not null
? HeightmapConverter.Convert( loadedExr, Settings, out resolution )
: HeightmapConverter.Convert( loadedBitmap, Settings, out resolution );
File.WriteAllBytes( fd.SelectedFile, data );
EditorUtility.DisplayDialog( "Heightmap saved",
$"Wrote {resolution}x{resolution} heightmap to:\n{fd.SelectedFile}\n\n" +
"Import it from a Terrain component: open its heightmap import and choose this .raw file." );
}
catch ( Exception e )
{
EditorUtility.DisplayDialog( "Conversion failed", e.Message );
}
}
public override void OnDestroyed()
{
base.OnDestroyed();
loadedBitmap?.Dispose();
loadedBitmap = null;
loadedExr = null;
}
}
Game
library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "terrainconvert" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "terrainconvert" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "brax" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "brax.terrainconvert" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "26" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]
[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-06-26T23:15:58.0786236Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.205.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.205.0")]
Editor
library
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
namespace Editor.TerrainConvert;
/// <summary>
/// Minimal OpenEXR reader for scanline images. Source 2's own EXR loader is internal to
/// the engine and can't be called from library code, so this decodes the float data directly.
///
/// Supports: scanline (non-tiled) images, HALF/FLOAT/UINT channels, and NONE/ZIP/ZIPS/RLE
/// compression - which covers the vast majority of heightmap exports. Tiled, deep, and the
/// lossy/wavelet codecs (PIZ, PXR24, B44, DWAA/DWAB) are not supported and throw a clear error.
/// </summary>
public sealed class ExrImage
{
public int Width { get; private set; }
public int Height { get; private set; }
/// <summary> Channel name -> linear float plane (row-major, Width*Height). </summary>
readonly Dictionary<string, float[]> channels = new( StringComparer.Ordinal );
public IReadOnlyCollection<string> ChannelNames => channels.Keys;
public bool HasChannel( string name ) => channels.ContainsKey( name );
enum PixelType { Uint = 0, Half = 1, Float = 2 }
enum Compression { None = 0, Rle = 1, Zips = 2, Zip = 3, Piz = 4, Pxr24 = 5, B44 = 6, B44A = 7, Dwaa = 8, Dwab = 9 }
readonly record struct ChannelInfo( string Name, PixelType Type )
{
public int SampleSize => Type == PixelType.Half ? 2 : 4;
}
/// <summary>
/// Returns true if the bytes start with the OpenEXR magic number.
/// </summary>
public static bool IsExr( byte[] data )
=> data is { Length: >= 4 } && data[0] == 0x76 && data[1] == 0x2f && data[2] == 0x31 && data[3] == 0x01;
/// <summary>
/// Decode an EXR from a byte buffer.
/// </summary>
public static ExrImage Load( byte[] data )
{
if ( !IsExr( data ) )
throw new InvalidDataException( "Not an OpenEXR file (bad magic number)." );
var img = new ExrImage();
img.Parse( data );
return img;
}
/// <summary>
/// Get a channel as a float plane, or null if absent. Common heightmap names are "Y"
/// (luminance), "R", or a plain unnamed channel.
/// </summary>
public float[] GetChannel( string name ) => channels.TryGetValue( name, out var v ) ? v : null;
/// <summary>
/// Pick the most sensible single channel to treat as height: explicit preference first,
/// then Y / R / the first available channel.
/// </summary>
public float[] GetHeightChannel( string preferred = null )
{
if ( preferred is not null && channels.TryGetValue( preferred, out var p ) )
return p;
foreach ( var name in new[] { "Y", "R", "G", "B" } )
if ( channels.TryGetValue( name, out var v ) )
return v;
foreach ( var v in channels.Values )
return v;
return null;
}
void Parse( byte[] data )
{
int pos = 4; // skip magic
int version = ReadInt32( data, ref pos );
bool tiled = (version & 0x200) != 0;
bool deep = (version & 0x800) != 0;
bool multipart = (version & 0x1000) != 0;
if ( tiled ) throw new NotSupportedException( "Tiled EXR images are not supported - re-export as a scanline image." );
if ( deep ) throw new NotSupportedException( "Deep EXR images are not supported." );
if ( multipart ) throw new NotSupportedException( "Multi-part EXR images are not supported." );
var chans = new List<ChannelInfo>();
var compression = Compression.None;
int xMin = 0, yMin = 0, xMax = 0, yMax = 0;
// Header attributes, terminated by an empty name.
while ( true )
{
string name = ReadNullString( data, ref pos );
if ( name.Length == 0 ) break;
string type = ReadNullString( data, ref pos );
int size = ReadInt32( data, ref pos );
int valueStart = pos;
switch ( name )
{
case "channels":
ParseChannels( data, valueStart, chans );
break;
case "compression":
compression = (Compression)data[valueStart];
break;
case "dataWindow":
int p = valueStart;
xMin = ReadInt32( data, ref p );
yMin = ReadInt32( data, ref p );
xMax = ReadInt32( data, ref p );
yMax = ReadInt32( data, ref p );
break;
}
pos = valueStart + size; // skip to next attribute
}
if ( chans.Count == 0 )
throw new InvalidDataException( "EXR has no channels." );
Width = xMax - xMin + 1;
Height = yMax - yMin + 1;
if ( Width <= 0 || Height <= 0 )
throw new InvalidDataException( $"EXR has invalid data window ({Width}x{Height})." );
int linesPerBlock = compression switch
{
Compression.None or Compression.Rle or Compression.Zips => 1,
Compression.Zip or Compression.Pxr24 => 16,
Compression.Piz or Compression.B44 or Compression.B44A or Compression.Dwaa => 32,
Compression.Dwab => 256,
_ => 1,
};
if ( compression is Compression.Piz or Compression.Pxr24 or Compression.B44 or Compression.B44A or Compression.Dwaa or Compression.Dwab )
throw new NotSupportedException( $"EXR compression '{compression}' is not supported. Re-export the heightmap as Uncompressed, ZIP, or RLE." );
// Allocate channel planes.
foreach ( var c in chans )
channels[c.Name] = new float[Width * Height];
// Scanline offset table: one ulong per block.
int blockCount = (Height + linesPerBlock - 1) / linesPerBlock;
var offsets = new long[blockCount];
for ( int i = 0; i < blockCount; i++ )
offsets[i] = (long)ReadUInt64( data, ref pos );
int rowBytes = 0;
foreach ( var c in chans )
rowBytes += Width * c.SampleSize;
// Each block: int32 yStart, int32 dataSize, then (compressed) pixel data.
foreach ( var off in offsets )
{
int bp = (int)off;
int yStart = ReadInt32( data, ref bp );
int dataSize = ReadInt32( data, ref bp );
int lines = Math.Min( linesPerBlock, yMax - yStart + 1 );
int uncompressedSize = rowBytes * lines;
byte[] block;
if ( compression == Compression.None || dataSize >= uncompressedSize )
{
// Stored uncompressed (NONE, or a block that didn't compress smaller).
block = new byte[uncompressedSize];
Array.Copy( data, bp, block, 0, uncompressedSize );
}
else
{
block = Decompress( compression, data, bp, dataSize, uncompressedSize );
}
ScatterBlock( block, chans, rowBytes, Width, yStart - yMin, lines );
}
}
void ScatterBlock( byte[] block, List<ChannelInfo> chans, int rowBytes, int width, int rowOffset, int lines )
{
for ( int i = 0; i < lines; i++ )
{
int rowBase = i * rowBytes;
int channelOffset = 0;
int destRow = rowOffset + i;
foreach ( var c in chans )
{
int src = rowBase + channelOffset;
var plane = channels[c.Name];
int destBase = destRow * width;
for ( int x = 0; x < width; x++ )
{
plane[destBase + x] = ReadSample( block, src, c.Type );
src += c.SampleSize;
}
channelOffset += width * c.SampleSize;
}
}
}
static float ReadSample( byte[] b, int offset, PixelType type ) => type switch
{
PixelType.Half => (float)BitConverter.UInt16BitsToHalf( (ushort)(b[offset] | (b[offset + 1] << 8)) ),
PixelType.Float => BitConverter.Int32BitsToSingle( b[offset] | (b[offset + 1] << 8) | (b[offset + 2] << 16) | (b[offset + 3] << 24) ),
PixelType.Uint => (uint)(b[offset] | (b[offset + 1] << 8) | (b[offset + 2] << 16) | (b[offset + 3] << 24)),
_ => 0f,
};
static void ParseChannels( byte[] data, int pos, List<ChannelInfo> chans )
{
while ( true )
{
string name = ReadNullString( data, ref pos );
if ( name.Length == 0 ) break;
int ptype = ReadInt32( data, ref pos );
pos += 1; // pLinear
pos += 3; // reserved
ReadInt32( data, ref pos ); // xSampling
ReadInt32( data, ref pos ); // ySampling
chans.Add( new ChannelInfo( name, (PixelType)ptype ) );
}
}
static byte[] Decompress( Compression compression, byte[] data, int offset, int size, int uncompressedSize )
{
// Step 1: codec-specific decompression into a temp buffer.
byte[] tmp = compression switch
{
Compression.Zip or Compression.Zips => Inflate( data, offset, size, uncompressedSize ),
Compression.Rle => RleDecode( data, offset, size, uncompressedSize ),
_ => throw new NotSupportedException( $"EXR compression '{compression}' is not supported." ),
};
// Step 2: undo EXR's byte predictor + interleave (shared by ZIP and RLE).
Predictor( tmp );
return Interleave( tmp );
}
static byte[] Inflate( byte[] data, int offset, int size, int expected )
{
using var ms = new MemoryStream( data, offset, size );
using var z = new ZLibStream( ms, CompressionMode.Decompress );
var outBuf = new byte[expected];
int read = 0;
while ( read < expected )
{
int n = z.Read( outBuf, read, expected - read );
if ( n == 0 ) break;
read += n;
}
return outBuf;
}
static byte[] RleDecode( byte[] data, int offset, int size, int expected )
{
var outBuf = new byte[expected];
int o = 0;
int i = offset;
int end = offset + size;
while ( i < end && o < expected )
{
sbyte count = (sbyte)data[i++];
if ( count < 0 )
{
int n = -count;
while ( n-- > 0 && i < end && o < expected )
outBuf[o++] = data[i++];
}
else
{
int n = count + 1;
byte v = data[i++];
while ( n-- > 0 && o < expected )
outBuf[o++] = v;
}
}
return outBuf;
}
// EXR delta predictor: each byte is reconstructed from the running difference.
static void Predictor( byte[] b )
{
for ( int i = 1; i < b.Length; i++ )
{
int d = b[i - 1] + b[i] - 128;
b[i] = (byte)d;
}
}
// EXR de-interleave: data is split into two halves that must be zippered back together.
static byte[] Interleave( byte[] src )
{
int len = src.Length;
var outB = new byte[len];
int t1 = 0;
int t2 = (len + 1) / 2;
int s = 0;
while ( true )
{
if ( s < len ) outB[s++] = src[t1++]; else break;
if ( s < len ) outB[s++] = src[t2++]; else break;
}
return outB;
}
static int ReadInt32( byte[] b, ref int pos )
{
int v = b[pos] | (b[pos + 1] << 8) | (b[pos + 2] << 16) | (b[pos + 3] << 24);
pos += 4;
return v;
}
static ulong ReadUInt64( byte[] b, ref int pos )
{
ulong v = 0;
for ( int i = 0; i < 8; i++ )
v |= (ulong)b[pos + i] << (i * 8);
pos += 8;
return v;
}
static string ReadNullString( byte[] b, ref int pos )
{
int start = pos;
while ( pos < b.Length && b[pos] != 0 ) pos++;
string s = Encoding.ASCII.GetString( b, start, pos - start );
pos++; // skip null
return s;
}
}
Debug: View Raw JSON Response
{
"TotalCount": 4,
"Files": [
{
"Ident": "brax.terrainconvert",
"Path": "Editor/HeightmapConverter.cs",
"FileName": "HeightmapConverter.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 301868,
"IsPrivate": false,
"Code": "using System;\nusing System.Linq;\nusing Sandbox;\n\nnamespace Editor.TerrainConvert;\n\n/// <summary>\n/// Which source channel to read the height value from.\n/// </summary>\npublic enum HeightChannel\n{\n\t/// <summary> Red channel only. Standard for grayscale heightmaps. </summary>\n\tRed,\n\tGreen,\n\tBlue,\n\tAlpha,\n\t/// <summary> Rec.709 weighted luminance of RGB. </summary>\n\tLuminance,\n\t/// <summary> Average of the RGB channels. </summary>\n\tAverage,\n\t/// <summary> The largest of the RGB channels. </summary>\n\tMax,\n}\n\n/// <summary>\n/// Bit depth of the output .raw file. s&box terrain stores heights as 16-bit,\n/// so 16-bit is recommended for the best precision.\n/// </summary>\npublic enum HeightBitDepth\n{\n\t/// <summary> 16-bit per height sample (recommended). 2 bytes per pixel. </summary>\n\tBit16,\n\t/// <summary> 8-bit per height sample. 1 byte per pixel. </summary>\n\tBit8,\n}\n\n/// <summary>\n/// How the source values are remapped into the 0..1 height range before quantizing.\n/// </summary>\npublic enum HeightNormalize\n{\n\t/// <summary> Use values as-is, clamped to 0..1. </summary>\n\tClamp,\n\t/// <summary> Stretch the min..max range of the image to fill 0..1 (good for HDR/EXR). </summary>\n\tMinMaxStretch,\n}\n\n/// <summary>\n/// Byte order of multi-byte (16-bit) samples in the output file.\n/// </summary>\npublic enum HeightByteOrder\n{\n\t/// <summary> Little-endian. What the s&box terrain importer reads by default (\"Windows\"). </summary>\n\tLittleEndian,\n\t/// <summary> Big-endian (\"Mac\"). </summary>\n\tBigEndian,\n}\n\n/// <summary>\n/// Settings for converting an image into a terrain heightmap .raw file.\n/// </summary>\npublic class HeightmapConvertSettings\n{\n\t/// <summary>\n\t/// Which channel of the source image holds the height. Most heightmaps are\n\t/// grayscale, where every channel is identical - <see cref=\"HeightChannel.Red\"/> works for those.\n\t/// </summary>\n\t[Property]\n\tpublic HeightChannel Channel { get; set; } = HeightChannel.Red;\n\n\t/// <summary>\n\t/// 16-bit gives the smoothest terrain and matches what s&box stores internally.\n\t/// </summary>\n\t[Property]\n\tpublic HeightBitDepth BitDepth { get; set; } = HeightBitDepth.Bit16;\n\n\t/// <summary>\n\t/// Output heightmaps are always square. If 0, the smaller of the source\n\t/// dimensions is used. The image is resampled to this resolution.\n\t/// </summary>\n\t[Property, Range( 0, 8192 )]\n\tpublic int Resolution { get; set; } = 0;\n\n\t/// <summary>\n\t/// Round the output resolution down to the nearest power of two (e.g 1024, 2048).\n\t/// The terrain system expects power-of-two heightmaps, so leave this on.\n\t/// </summary>\n\t[Property]\n\tpublic bool PowerOfTwo { get; set; } = true;\n\n\t/// <summary>\n\t/// How source values are mapped to the height range. Use <see cref=\"HeightNormalize.MinMaxStretch\"/>\n\t/// for HDR images that don't already fill 0..1.\n\t/// </summary>\n\t[Property]\n\tpublic HeightNormalize Normalize { get; set; } = HeightNormalize.Clamp;\n\n\t/// <summary>\n\t/// Flip the image vertically. Image and terrain coordinate origins often differ;\n\t/// toggle this if your terrain comes out mirrored north/south.\n\t/// </summary>\n\t[Property]\n\tpublic bool FlipVertical { get; set; } = false;\n\n\t/// <summary>\n\t/// Invert the heights (high becomes low). Useful for depth/inverted maps.\n\t/// </summary>\n\t[Property]\n\tpublic bool Invert { get; set; } = false;\n\n\t/// <summary>\n\t/// Byte order of 16-bit samples. The s&box importer reads little-endian by default.\n\t/// </summary>\n\t[Property, ShowIf( nameof( BitDepth ), HeightBitDepth.Bit16 )]\n\tpublic HeightByteOrder ByteOrder { get; set; } = HeightByteOrder.LittleEndian;\n}\n\n/// <summary>\n/// Converts loaded images into raw single-channel heightmaps compatible with the\n/// s&box terrain importer (square, 8 or 16 bit, raw samples with no header).\n/// </summary>\npublic static class HeightmapConverter\n{\n\t/// <summary>\n\t/// Convert a bitmap into a raw heightmap byte buffer.\n\t/// </summary>\n\t/// <param name=\"bitmap\">Source image.</param>\n\t/// <param name=\"settings\">Conversion settings.</param>\n\t/// <param name=\"resolution\">The square resolution of the produced heightmap.</param>\n\t/// <returns>Raw heightmap bytes ready to write to a .raw file.</returns>\n\tpublic static byte[] Convert( Bitmap bitmap, HeightmapConvertSettings settings, out int resolution )\n\t{\n\t\tArgumentNullException.ThrowIfNull( bitmap );\n\t\tArgumentNullException.ThrowIfNull( settings );\n\n\t\tvar pixels = bitmap.GetPixels();\n\t\tint count = bitmap.Width * bitmap.Height;\n\n\t\t// Extract the chosen channel as a float per pixel, then run the shared pipeline.\n\t\tvar values = new float[count];\n\t\tfor ( int i = 0; i < count; i++ )\n\t\t\tvalues[i] = SampleChannel( pixels[i], settings.Channel );\n\n\t\treturn BuildRaw( values, bitmap.Width, bitmap.Height, settings, out resolution );\n\t}\n\n\t/// <summary>\n\t/// Convert a decoded EXR into a raw heightmap byte buffer, reading height from the\n\t/// chosen channel (falling back to Y/R/first channel if the exact one is absent).\n\t/// </summary>\n\tpublic static byte[] Convert( ExrImage exr, HeightmapConvertSettings settings, out int resolution )\n\t{\n\t\tArgumentNullException.ThrowIfNull( exr );\n\t\tArgumentNullException.ThrowIfNull( settings );\n\n\t\tvar values = SampleChannel( exr, settings.Channel );\n\t\treturn BuildRaw( values, exr.Width, exr.Height, settings, out resolution );\n\t}\n\n\t/// <summary>\n\t/// Shared pipeline: resample a single-channel float plane to a square power-of-two,\n\t/// normalize into 0..1, optionally invert, and quantize to raw bytes.\n\t/// </summary>\n\tstatic byte[] BuildRaw( float[] values, int srcWidth, int srcHeight, HeightmapConvertSettings settings, out int resolution )\n\t{\n\t\tresolution = ResolveResolution( srcWidth, srcHeight, settings );\n\n\t\t// Bilinear resample to a square of the target resolution, working entirely in float\n\t\t// so we don't lose precision on high bit-depth / HDR sources. Resampling produces a\n\t\t// fresh array; otherwise clone so the in-place normalize/invert never mutates a\n\t\t// caller-owned buffer (e.g. an EXR's cached channel plane).\n\t\tvalues = srcWidth != resolution || srcHeight != resolution\n\t\t\t? ResampleBilinear( values, srcWidth, srcHeight, resolution, resolution )\n\t\t\t: (float[])values.Clone();\n\n\t\tApplyNormalize( values, settings.Normalize );\n\n\t\tif ( settings.Invert )\n\t\t{\n\t\t\tfor ( int i = 0; i < values.Length; i++ )\n\t\t\t\tvalues[i] = 1f - values[i];\n\t\t}\n\n\t\treturn Quantize( values, resolution, settings );\n\t}\n\n\tstatic float[] ResampleBilinear( float[] src, int srcW, int srcH, int dstW, int dstH )\n\t{\n\t\tvar dst = new float[dstW * dstH];\n\n\t\tfor ( int y = 0; y < dstH; y++ )\n\t\t{\n\t\t\tfloat fy = dstH > 1 ? (float)y / (dstH - 1) * (srcH - 1) : 0f;\n\t\t\tint y0 = (int)fy;\n\t\t\tint y1 = Math.Min( y0 + 1, srcH - 1 );\n\t\t\tfloat ty = fy - y0;\n\n\t\t\tfor ( int x = 0; x < dstW; x++ )\n\t\t\t{\n\t\t\t\tfloat fx = dstW > 1 ? (float)x / (dstW - 1) * (srcW - 1) : 0f;\n\t\t\t\tint x0 = (int)fx;\n\t\t\t\tint x1 = Math.Min( x0 + 1, srcW - 1 );\n\t\t\t\tfloat tx = fx - x0;\n\n\t\t\t\tfloat top = MathX.Lerp( src[y0 * srcW + x0], src[y0 * srcW + x1], tx );\n\t\t\t\tfloat bottom = MathX.Lerp( src[y1 * srcW + x0], src[y1 * srcW + x1], tx );\n\t\t\t\tdst[y * dstW + x] = MathX.Lerp( top, bottom, ty );\n\t\t\t}\n\t\t}\n\n\t\treturn dst;\n\t}\n\n\tstatic float[] SampleChannel( ExrImage exr, HeightChannel channel )\n\t{\n\t\t// For multi-channel EXRs, blend RGB the same way the bitmap path does. For the\n\t\t// common single-channel (Y) heightmap, every option resolves to that one plane.\n\t\tswitch ( channel )\n\t\t{\n\t\t\tcase HeightChannel.Red: return exr.GetHeightChannel( \"R\" );\n\t\t\tcase HeightChannel.Green: return exr.GetHeightChannel( \"G\" );\n\t\t\tcase HeightChannel.Blue: return exr.GetHeightChannel( \"B\" );\n\t\t\tcase HeightChannel.Alpha: return exr.GetHeightChannel( \"A\" );\n\t\t}\n\n\t\tvar r = exr.GetChannel( \"R\" );\n\t\tvar g = exr.GetChannel( \"G\" );\n\t\tvar b = exr.GetChannel( \"B\" );\n\n\t\t// No RGB set - it's a luminance/single-channel image, use it directly.\n\t\tif ( r is null || g is null || b is null )\n\t\t\treturn exr.GetHeightChannel();\n\n\t\tvar outv = new float[r.Length];\n\t\tfor ( int i = 0; i < outv.Length; i++ )\n\t\t{\n\t\t\toutv[i] = channel switch\n\t\t\t{\n\t\t\t\tHeightChannel.Luminance => 0.2126f * r[i] + 0.7152f * g[i] + 0.0722f * b[i],\n\t\t\t\tHeightChannel.Average => (r[i] + g[i] + b[i]) / 3f,\n\t\t\t\tHeightChannel.Max => Math.Max( r[i], Math.Max( g[i], b[i] ) ),\n\t\t\t\t_ => r[i],\n\t\t\t};\n\t\t}\n\t\treturn outv;\n\t}\n\n\tstatic int ResolveResolution( int width, int height, HeightmapConvertSettings settings )\n\t{\n\t\tint res = settings.Resolution > 0 ? settings.Resolution : Math.Min( width, height );\n\n\t\tif ( settings.PowerOfTwo )\n\t\t\tres = RoundDownToPowerOfTwo( res );\n\n\t\treturn Math.Clamp( res, 4, 16384 );\n\t}\n\n\tstatic float SampleChannel( Color c, HeightChannel channel ) => channel switch\n\t{\n\t\tHeightChannel.Red => c.r,\n\t\tHeightChannel.Green => c.g,\n\t\tHeightChannel.Blue => c.b,\n\t\tHeightChannel.Alpha => c.a,\n\t\tHeightChannel.Luminance => 0.2126f * c.r + 0.7152f * c.g + 0.0722f * c.b,\n\t\tHeightChannel.Average => (c.r + c.g + c.b) / 3f,\n\t\tHeightChannel.Max => Math.Max( c.r, Math.Max( c.g, c.b ) ),\n\t\t_ => c.r,\n\t};\n\n\tstatic void ApplyNormalize( float[] values, HeightNormalize mode )\n\t{\n\t\tif ( mode == HeightNormalize.MinMaxStretch )\n\t\t{\n\t\t\tfloat min = values.Min();\n\t\t\tfloat max = values.Max();\n\t\t\tfloat range = max - min;\n\n\t\t\tif ( range > 1e-6f )\n\t\t\t{\n\t\t\t\tfor ( int i = 0; i < values.Length; i++ )\n\t\t\t\t\tvalues[i] = (values[i] - min) / range;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Flat image - fall through to clamp.\n\t\t}\n\n\t\tfor ( int i = 0; i < values.Length; i++ )\n\t\t\tvalues[i] = Math.Clamp( values[i], 0f, 1f );\n\t}\n\n\tstatic byte[] Quantize( float[] values, int resolution, HeightmapConvertSettings settings )\n\t{\n\t\tbool flip = settings.FlipVertical;\n\n\t\tif ( settings.BitDepth == HeightBitDepth.Bit8 )\n\t\t{\n\t\t\tvar bytes = new byte[resolution * resolution];\n\t\t\tfor ( int y = 0; y < resolution; y++ )\n\t\t\t{\n\t\t\t\tint srcY = flip ? resolution - 1 - y : y;\n\t\t\t\tfor ( int x = 0; x < resolution; x++ )\n\t\t\t\t{\n\t\t\t\t\tfloat v = Math.Clamp( values[srcY * resolution + x], 0f, 1f );\n\t\t\t\t\tbytes[y * resolution + x] = (byte)MathF.Round( v * byte.MaxValue );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn bytes;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tbool little = settings.ByteOrder == HeightByteOrder.LittleEndian;\n\t\t\tvar bytes = new byte[resolution * resolution * 2];\n\t\t\tfor ( int y = 0; y < resolution; y++ )\n\t\t\t{\n\t\t\t\tint srcY = flip ? resolution - 1 - y : y;\n\t\t\t\tfor ( int x = 0; x < resolution; x++ )\n\t\t\t\t{\n\t\t\t\t\tfloat v = Math.Clamp( values[srcY * resolution + x], 0f, 1f );\n\t\t\t\t\tushort h = (ushort)MathF.Round( v * ushort.MaxValue );\n\n\t\t\t\t\tint o = (y * resolution + x) * 2;\n\t\t\t\t\tif ( little )\n\t\t\t\t\t{\n\t\t\t\t\t\tbytes[o] = (byte)(h & 0xFF);\n\t\t\t\t\t\tbytes[o + 1] = (byte)(h >> 8);\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tbytes[o] = (byte)(h >> 8);\n\t\t\t\t\t\tbytes[o + 1] = (byte)(h & 0xFF);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn bytes;\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// Rounds a value down to the nearest power of two.\n\t/// </summary>\n\tpublic static int RoundDownToPowerOfTwo( int value )\n\t{\n\t\tif ( value < 1 ) return 1;\n\t\tvalue |= value >> 1;\n\t\tvalue |= value >> 2;\n\t\tvalue |= value >> 4;\n\t\tvalue |= value >> 8;\n\t\tvalue |= value >> 16;\n\t\treturn value - (value >> 1);\n\t}\n}\n"
},
{
"Ident": "brax.terrainconvert",
"Path": "Editor/HeightmapConvertWindow.cs",
"FileName": "HeightmapConvertWindow.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 301868,
"IsPrivate": false,
"Code": "using System;\nusing System.IO;\nusing Sandbox;\n\nnamespace Editor.TerrainConvert;\n\n/// <summary>\n/// Editor tool that converts an image (.png, .jpg, .tga, .tif, .psd, .exr ...) into a\n/// raw single-channel heightmap (.raw) that the s&box terrain importer can load.\n/// </summary>\n[EditorApp( \"Heightmap Converter\", \"terrain\", \"Convert an image into a terrain heightmap (.raw)\" )]\npublic class HeightmapConvertWindow : Widget\n{\n\tHeightmapConvertSettings Settings { get; set; } = new();\n\n\tstring inputFile;\n\tBitmap loadedBitmap;\n\tExrImage loadedExr;\n\tint srcWidth, srcHeight;\n\n\tbool HasImage => loadedBitmap is not null || loadedExr is not null;\n\n\tLabel inputLabel;\n\tLabel outputInfoLabel;\n\tButton convertButton;\n\n\tpublic HeightmapConvertWindow() : this( null ) { }\n\n\tpublic HeightmapConvertWindow( Widget parent ) : base( parent )\n\t{\n\t\tWindowFlags = WindowFlags.Dialog | WindowFlags.Customized | WindowFlags.WindowTitle | WindowFlags.CloseButton | WindowFlags.WindowSystemMenuHint;\n\t\tDeleteOnClose = true;\n\t\tWindowTitle = \"Convert Image to Heightmap\";\n\t\tSetWindowIcon( \"terrain\" );\n\n\t\tLayout = Layout.Column();\n\t\tLayout.Spacing = 8;\n\t\tLayout.Margin = 16;\n\n\t\tvar warning = new WarningBox(\n\t\t\t\"Converts an image into a raw heightmap (.raw) for the terrain system.\\n\" +\n\t\t\t\"Output is square, single-channel, 8 or 16 bit. Import it via the Terrain component's heightmap import.\", this );\n\t\tLayout.Add( warning );\n\n\t\t// Input file row\n\t\t{\n\t\t\tvar row = Layout.Row();\n\t\t\trow.Spacing = 8;\n\n\t\t\tinputLabel = new Label( \"No image selected\", this );\n\t\t\tinputLabel.WordWrap = false;\n\t\t\trow.Add( inputLabel, 1 );\n\n\t\t\tvar browse = new Button( \"Browse Image...\", \"image\", this );\n\t\t\tbrowse.Clicked = PickInputFile;\n\t\t\trow.Add( browse );\n\n\t\t\tLayout.Add( row );\n\t\t}\n\n\t\t// Settings sheet\n\t\t{\n\t\t\tvar so = EditorUtility.GetSerializedObject( Settings );\n\t\t\tso.OnPropertyChanged += _ => UpdateInfo();\n\n\t\t\tvar sheet = new ControlSheet();\n\t\t\tsheet.AddObject( so );\n\t\t\tLayout.Add( sheet );\n\t\t}\n\n\t\toutputInfoLabel = new Label( \"\", this );\n\t\toutputInfoLabel.Color = Theme.TextControl.WithAlpha( 0.6f );\n\t\tLayout.Add( outputInfoLabel );\n\n\t\tLayout.AddStretchCell();\n\n\t\t// Bottom bar\n\t\t{\n\t\t\tvar row = Layout.Row();\n\t\t\trow.Margin = new Sandbox.UI.Margin( 0, 8, 0, 0 );\n\t\t\trow.AddStretchCell();\n\n\t\t\tconvertButton = new Button.Primary( \"Convert & Save...\", \"file_download\", this );\n\t\t\tconvertButton.Clicked = ConvertAndSave;\n\t\t\tconvertButton.Enabled = false;\n\t\t\trow.Add( convertButton );\n\n\t\t\tLayout.Add( row );\n\t\t}\n\n\t\tWidth = 440;\n\t\tMinimumWidth = 380;\n\t\tHeight = 520;\n\n\t\tUpdateInfo();\n\n\t\tShow();\n\t\tFocus();\n\t}\n\n\tvoid PickInputFile()\n\t{\n\t\tvar fd = new FileDialog( null )\n\t\t{\n\t\t\tTitle = \"Select Source Image\",\n\t\t};\n\t\tfd.SetFindFile();\n\t\tfd.SetModeOpen();\n\t\tfd.SetNameFilter( \"Images (*.png *.jpg *.jpeg *.tga *.tif *.tiff *.psd *.exr *.bmp *.raw)\" );\n\n\t\tif ( !fd.Execute() )\n\t\t\treturn;\n\n\t\tLoadInput( fd.SelectedFile );\n\t}\n\n\tvoid LoadInput( string path )\n\t{\n\t\tloadedBitmap?.Dispose();\n\t\tloadedBitmap = null;\n\t\tloadedExr = null;\n\t\tinputFile = null;\n\n\t\ttry\n\t\t{\n\t\t\tvar bytes = File.ReadAllBytes( path );\n\n\t\t\t// EXR isn't supported by the Skia-backed Bitmap loader, so decode it ourselves.\n\t\t\tbool isExr = path.EndsWith( \".exr\", StringComparison.OrdinalIgnoreCase ) || ExrImage.IsExr( bytes );\n\t\t\tif ( isExr )\n\t\t\t{\n\t\t\t\tloadedExr = ExrImage.Load( bytes );\n\t\t\t\tsrcWidth = loadedExr.Width;\n\t\t\t\tsrcHeight = loadedExr.Height;\n\t\t\t\tinputFile = path;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tvar bitmap = Bitmap.CreateFromBytes( bytes );\n\t\t\t\tif ( bitmap is null || !bitmap.IsValid )\n\t\t\t\t{\n\t\t\t\t\tEditorUtility.DisplayDialog( \"Couldn't load image\",\n\t\t\t\t\t\t$\"Failed to decode '{Path.GetFileName( path )}'.\\nThis image format may not be supported.\" );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tloadedBitmap = bitmap;\n\t\t\t\tsrcWidth = bitmap.Width;\n\t\t\t\tsrcHeight = bitmap.Height;\n\t\t\t\tinputFile = path;\n\t\t\t}\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tEditorUtility.DisplayDialog( \"Couldn't load image\", e.Message );\n\t\t\treturn;\n\t\t}\n\n\t\tUpdateInfo();\n\t}\n\n\tvoid UpdateInfo()\n\t{\n\t\tif ( !HasImage )\n\t\t{\n\t\t\tinputLabel.Text = \"No image selected\";\n\t\t\toutputInfoLabel.Text = \"Pick an image to begin.\";\n\t\t\tconvertButton.Enabled = false;\n\t\t\treturn;\n\t\t}\n\n\t\tstring kind = loadedExr is not null ? \"EXR\" : \"image\";\n\t\tinputLabel.Text = $\"{Path.GetFileName( inputFile )} ({srcWidth}x{srcHeight}, {kind})\";\n\t\tconvertButton.Enabled = true;\n\n\t\tint res = PreviewResolution();\n\t\tstring depth = Settings.BitDepth == HeightBitDepth.Bit16 ? \"16-bit\" : \"8-bit\";\n\t\tint bytes = res * res * (Settings.BitDepth == HeightBitDepth.Bit16 ? 2 : 1);\n\t\toutputInfoLabel.Text = $\"Output: {res}x{res}, {depth} \u2192 {bytes / 1024:n0} KB raw\";\n\t}\n\n\tint PreviewResolution()\n\t{\n\t\tint res = Settings.Resolution > 0 ? Settings.Resolution : Math.Min( srcWidth, srcHeight );\n\t\tif ( Settings.PowerOfTwo )\n\t\t\tres = HeightmapConverter.RoundDownToPowerOfTwo( res );\n\t\treturn Math.Clamp( res, 4, 16384 );\n\t}\n\n\tvoid ConvertAndSave()\n\t{\n\t\tif ( !HasImage )\n\t\t\treturn;\n\n\t\tvar fd = new FileDialog( null )\n\t\t{\n\t\t\tTitle = \"Save Heightmap\",\n\t\t\tDefaultSuffix = \".raw\",\n\t\t};\n\t\tfd.Directory = Path.GetDirectoryName( inputFile );\n\t\tfd.SelectFile( $\"{Path.GetFileNameWithoutExtension( inputFile )}.raw\" );\n\t\tfd.SetFindFile();\n\t\tfd.SetModeSave();\n\t\tfd.SetNameFilter( \"Raw Heightmap (*.raw *.r16 *.r8)\" );\n\n\t\tif ( !fd.Execute() )\n\t\t\treturn;\n\n\t\ttry\n\t\t{\n\t\t\tint resolution;\n\t\t\tvar data = loadedExr is not null\n\t\t\t\t? HeightmapConverter.Convert( loadedExr, Settings, out resolution )\n\t\t\t\t: HeightmapConverter.Convert( loadedBitmap, Settings, out resolution );\n\t\t\tFile.WriteAllBytes( fd.SelectedFile, data );\n\n\t\t\tEditorUtility.DisplayDialog( \"Heightmap saved\",\n\t\t\t\t$\"Wrote {resolution}x{resolution} heightmap to:\\n{fd.SelectedFile}\\n\\n\" +\n\t\t\t\t\"Import it from a Terrain component: open its heightmap import and choose this .raw file.\" );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tEditorUtility.DisplayDialog( \"Conversion failed\", e.Message );\n\t\t}\n\t}\n\n\tpublic override void OnDestroyed()\n\t{\n\t\tbase.OnDestroyed();\n\t\tloadedBitmap?.Dispose();\n\t\tloadedBitmap = null;\n\t\tloadedExr = null;\n\t}\n}\n"
},
{
"Ident": "brax.terrainconvert",
"Path": ".obj/__compiler_extra.cs",
"FileName": "__compiler_extra.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 301868,
"IsPrivate": false,
"Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"terrainconvert\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"terrainconvert\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"brax\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"brax.terrainconvert\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"26\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-06-26T23:15:58.0786236Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.205.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.205.0\")]"
},
{
"Ident": "brax.terrainconvert",
"Path": "Editor/ExrImage.cs",
"FileName": "ExrImage.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 301868,
"IsPrivate": false,
"Code": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Text;\n\nnamespace Editor.TerrainConvert;\n\n/// <summary>\n/// Minimal OpenEXR reader for scanline images. Source 2's own EXR loader is internal to\n/// the engine and can't be called from library code, so this decodes the float data directly.\n///\n/// Supports: scanline (non-tiled) images, HALF/FLOAT/UINT channels, and NONE/ZIP/ZIPS/RLE\n/// compression - which covers the vast majority of heightmap exports. Tiled, deep, and the\n/// lossy/wavelet codecs (PIZ, PXR24, B44, DWAA/DWAB) are not supported and throw a clear error.\n/// </summary>\npublic sealed class ExrImage\n{\n\tpublic int Width { get; private set; }\n\tpublic int Height { get; private set; }\n\n\t/// <summary> Channel name -> linear float plane (row-major, Width*Height). </summary>\n\treadonly Dictionary<string, float[]> channels = new( StringComparer.Ordinal );\n\n\tpublic IReadOnlyCollection<string> ChannelNames => channels.Keys;\n\tpublic bool HasChannel( string name ) => channels.ContainsKey( name );\n\n\tenum PixelType { Uint = 0, Half = 1, Float = 2 }\n\n\tenum Compression { None = 0, Rle = 1, Zips = 2, Zip = 3, Piz = 4, Pxr24 = 5, B44 = 6, B44A = 7, Dwaa = 8, Dwab = 9 }\n\n\treadonly record struct ChannelInfo( string Name, PixelType Type )\n\t{\n\t\tpublic int SampleSize => Type == PixelType.Half ? 2 : 4;\n\t}\n\n\t/// <summary>\n\t/// Returns true if the bytes start with the OpenEXR magic number.\n\t/// </summary>\n\tpublic static bool IsExr( byte[] data )\n\t\t=> data is { Length: >= 4 } && data[0] == 0x76 && data[1] == 0x2f && data[2] == 0x31 && data[3] == 0x01;\n\n\t/// <summary>\n\t/// Decode an EXR from a byte buffer.\n\t/// </summary>\n\tpublic static ExrImage Load( byte[] data )\n\t{\n\t\tif ( !IsExr( data ) )\n\t\t\tthrow new InvalidDataException( \"Not an OpenEXR file (bad magic number).\" );\n\n\t\tvar img = new ExrImage();\n\t\timg.Parse( data );\n\t\treturn img;\n\t}\n\n\t/// <summary>\n\t/// Get a channel as a float plane, or null if absent. Common heightmap names are \"Y\"\n\t/// (luminance), \"R\", or a plain unnamed channel.\n\t/// </summary>\n\tpublic float[] GetChannel( string name ) => channels.TryGetValue( name, out var v ) ? v : null;\n\n\t/// <summary>\n\t/// Pick the most sensible single channel to treat as height: explicit preference first,\n\t/// then Y / R / the first available channel.\n\t/// </summary>\n\tpublic float[] GetHeightChannel( string preferred = null )\n\t{\n\t\tif ( preferred is not null && channels.TryGetValue( preferred, out var p ) )\n\t\t\treturn p;\n\n\t\tforeach ( var name in new[] { \"Y\", \"R\", \"G\", \"B\" } )\n\t\t\tif ( channels.TryGetValue( name, out var v ) )\n\t\t\t\treturn v;\n\n\t\tforeach ( var v in channels.Values )\n\t\t\treturn v;\n\n\t\treturn null;\n\t}\n\n\tvoid Parse( byte[] data )\n\t{\n\t\tint pos = 4; // skip magic\n\n\t\tint version = ReadInt32( data, ref pos );\n\t\tbool tiled = (version & 0x200) != 0;\n\t\tbool deep = (version & 0x800) != 0;\n\t\tbool multipart = (version & 0x1000) != 0;\n\n\t\tif ( tiled ) throw new NotSupportedException( \"Tiled EXR images are not supported - re-export as a scanline image.\" );\n\t\tif ( deep ) throw new NotSupportedException( \"Deep EXR images are not supported.\" );\n\t\tif ( multipart ) throw new NotSupportedException( \"Multi-part EXR images are not supported.\" );\n\n\t\tvar chans = new List<ChannelInfo>();\n\t\tvar compression = Compression.None;\n\t\tint xMin = 0, yMin = 0, xMax = 0, yMax = 0;\n\n\t\t// Header attributes, terminated by an empty name.\n\t\twhile ( true )\n\t\t{\n\t\t\tstring name = ReadNullString( data, ref pos );\n\t\t\tif ( name.Length == 0 ) break;\n\n\t\t\tstring type = ReadNullString( data, ref pos );\n\t\t\tint size = ReadInt32( data, ref pos );\n\t\t\tint valueStart = pos;\n\n\t\t\tswitch ( name )\n\t\t\t{\n\t\t\t\tcase \"channels\":\n\t\t\t\t\tParseChannels( data, valueStart, chans );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"compression\":\n\t\t\t\t\tcompression = (Compression)data[valueStart];\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"dataWindow\":\n\t\t\t\t\tint p = valueStart;\n\t\t\t\t\txMin = ReadInt32( data, ref p );\n\t\t\t\t\tyMin = ReadInt32( data, ref p );\n\t\t\t\t\txMax = ReadInt32( data, ref p );\n\t\t\t\t\tyMax = ReadInt32( data, ref p );\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tpos = valueStart + size; // skip to next attribute\n\t\t}\n\n\t\tif ( chans.Count == 0 )\n\t\t\tthrow new InvalidDataException( \"EXR has no channels.\" );\n\n\t\tWidth = xMax - xMin + 1;\n\t\tHeight = yMax - yMin + 1;\n\n\t\tif ( Width <= 0 || Height <= 0 )\n\t\t\tthrow new InvalidDataException( $\"EXR has invalid data window ({Width}x{Height}).\" );\n\n\t\tint linesPerBlock = compression switch\n\t\t{\n\t\t\tCompression.None or Compression.Rle or Compression.Zips => 1,\n\t\t\tCompression.Zip or Compression.Pxr24 => 16,\n\t\t\tCompression.Piz or Compression.B44 or Compression.B44A or Compression.Dwaa => 32,\n\t\t\tCompression.Dwab => 256,\n\t\t\t_ => 1,\n\t\t};\n\n\t\tif ( compression is Compression.Piz or Compression.Pxr24 or Compression.B44 or Compression.B44A or Compression.Dwaa or Compression.Dwab )\n\t\t\tthrow new NotSupportedException( $\"EXR compression '{compression}' is not supported. Re-export the heightmap as Uncompressed, ZIP, or RLE.\" );\n\n\t\t// Allocate channel planes.\n\t\tforeach ( var c in chans )\n\t\t\tchannels[c.Name] = new float[Width * Height];\n\n\t\t// Scanline offset table: one ulong per block.\n\t\tint blockCount = (Height + linesPerBlock - 1) / linesPerBlock;\n\t\tvar offsets = new long[blockCount];\n\t\tfor ( int i = 0; i < blockCount; i++ )\n\t\t\toffsets[i] = (long)ReadUInt64( data, ref pos );\n\n\t\tint rowBytes = 0;\n\t\tforeach ( var c in chans )\n\t\t\trowBytes += Width * c.SampleSize;\n\n\t\t// Each block: int32 yStart, int32 dataSize, then (compressed) pixel data.\n\t\tforeach ( var off in offsets )\n\t\t{\n\t\t\tint bp = (int)off;\n\t\t\tint yStart = ReadInt32( data, ref bp );\n\t\t\tint dataSize = ReadInt32( data, ref bp );\n\n\t\t\tint lines = Math.Min( linesPerBlock, yMax - yStart + 1 );\n\t\t\tint uncompressedSize = rowBytes * lines;\n\n\t\t\tbyte[] block;\n\t\t\tif ( compression == Compression.None || dataSize >= uncompressedSize )\n\t\t\t{\n\t\t\t\t// Stored uncompressed (NONE, or a block that didn't compress smaller).\n\t\t\t\tblock = new byte[uncompressedSize];\n\t\t\t\tArray.Copy( data, bp, block, 0, uncompressedSize );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tblock = Decompress( compression, data, bp, dataSize, uncompressedSize );\n\t\t\t}\n\n\t\t\tScatterBlock( block, chans, rowBytes, Width, yStart - yMin, lines );\n\t\t}\n\t}\n\n\tvoid ScatterBlock( byte[] block, List<ChannelInfo> chans, int rowBytes, int width, int rowOffset, int lines )\n\t{\n\t\tfor ( int i = 0; i < lines; i++ )\n\t\t{\n\t\t\tint rowBase = i * rowBytes;\n\t\t\tint channelOffset = 0;\n\t\t\tint destRow = rowOffset + i;\n\n\t\t\tforeach ( var c in chans )\n\t\t\t{\n\t\t\t\tint src = rowBase + channelOffset;\n\t\t\t\tvar plane = channels[c.Name];\n\t\t\t\tint destBase = destRow * width;\n\n\t\t\t\tfor ( int x = 0; x < width; x++ )\n\t\t\t\t{\n\t\t\t\t\tplane[destBase + x] = ReadSample( block, src, c.Type );\n\t\t\t\t\tsrc += c.SampleSize;\n\t\t\t\t}\n\n\t\t\t\tchannelOffset += width * c.SampleSize;\n\t\t\t}\n\t\t}\n\t}\n\n\tstatic float ReadSample( byte[] b, int offset, PixelType type ) => type switch\n\t{\n\t\tPixelType.Half => (float)BitConverter.UInt16BitsToHalf( (ushort)(b[offset] | (b[offset + 1] << 8)) ),\n\t\tPixelType.Float => BitConverter.Int32BitsToSingle( b[offset] | (b[offset + 1] << 8) | (b[offset + 2] << 16) | (b[offset + 3] << 24) ),\n\t\tPixelType.Uint => (uint)(b[offset] | (b[offset + 1] << 8) | (b[offset + 2] << 16) | (b[offset + 3] << 24)),\n\t\t_ => 0f,\n\t};\n\n\tstatic void ParseChannels( byte[] data, int pos, List<ChannelInfo> chans )\n\t{\n\t\twhile ( true )\n\t\t{\n\t\t\tstring name = ReadNullString( data, ref pos );\n\t\t\tif ( name.Length == 0 ) break;\n\n\t\t\tint ptype = ReadInt32( data, ref pos );\n\t\t\tpos += 1; // pLinear\n\t\t\tpos += 3; // reserved\n\t\t\tReadInt32( data, ref pos ); // xSampling\n\t\t\tReadInt32( data, ref pos ); // ySampling\n\n\t\t\tchans.Add( new ChannelInfo( name, (PixelType)ptype ) );\n\t\t}\n\t}\n\n\tstatic byte[] Decompress( Compression compression, byte[] data, int offset, int size, int uncompressedSize )\n\t{\n\t\t// Step 1: codec-specific decompression into a temp buffer.\n\t\tbyte[] tmp = compression switch\n\t\t{\n\t\t\tCompression.Zip or Compression.Zips => Inflate( data, offset, size, uncompressedSize ),\n\t\t\tCompression.Rle => RleDecode( data, offset, size, uncompressedSize ),\n\t\t\t_ => throw new NotSupportedException( $\"EXR compression '{compression}' is not supported.\" ),\n\t\t};\n\n\t\t// Step 2: undo EXR's byte predictor + interleave (shared by ZIP and RLE).\n\t\tPredictor( tmp );\n\t\treturn Interleave( tmp );\n\t}\n\n\tstatic byte[] Inflate( byte[] data, int offset, int size, int expected )\n\t{\n\t\tusing var ms = new MemoryStream( data, offset, size );\n\t\tusing var z = new ZLibStream( ms, CompressionMode.Decompress );\n\t\tvar outBuf = new byte[expected];\n\t\tint read = 0;\n\t\twhile ( read < expected )\n\t\t{\n\t\t\tint n = z.Read( outBuf, read, expected - read );\n\t\t\tif ( n == 0 ) break;\n\t\t\tread += n;\n\t\t}\n\t\treturn outBuf;\n\t}\n\n\tstatic byte[] RleDecode( byte[] data, int offset, int size, int expected )\n\t{\n\t\tvar outBuf = new byte[expected];\n\t\tint o = 0;\n\t\tint i = offset;\n\t\tint end = offset + size;\n\n\t\twhile ( i < end && o < expected )\n\t\t{\n\t\t\tsbyte count = (sbyte)data[i++];\n\t\t\tif ( count < 0 )\n\t\t\t{\n\t\t\t\tint n = -count;\n\t\t\t\twhile ( n-- > 0 && i < end && o < expected )\n\t\t\t\t\toutBuf[o++] = data[i++];\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tint n = count + 1;\n\t\t\t\tbyte v = data[i++];\n\t\t\t\twhile ( n-- > 0 && o < expected )\n\t\t\t\t\toutBuf[o++] = v;\n\t\t\t}\n\t\t}\n\t\treturn outBuf;\n\t}\n\n\t// EXR delta predictor: each byte is reconstructed from the running difference.\n\tstatic void Predictor( byte[] b )\n\t{\n\t\tfor ( int i = 1; i < b.Length; i++ )\n\t\t{\n\t\t\tint d = b[i - 1] + b[i] - 128;\n\t\t\tb[i] = (byte)d;\n\t\t}\n\t}\n\n\t// EXR de-interleave: data is split into two halves that must be zippered back together.\n\tstatic byte[] Interleave( byte[] src )\n\t{\n\t\tint len = src.Length;\n\t\tvar outB = new byte[len];\n\t\tint t1 = 0;\n\t\tint t2 = (len + 1) / 2;\n\t\tint s = 0;\n\n\t\twhile ( true )\n\t\t{\n\t\t\tif ( s < len ) outB[s++] = src[t1++]; else break;\n\t\t\tif ( s < len ) outB[s++] = src[t2++]; else break;\n\t\t}\n\t\treturn outB;\n\t}\n\n\tstatic int ReadInt32( byte[] b, ref int pos )\n\t{\n\t\tint v = b[pos] | (b[pos + 1] << 8) | (b[pos + 2] << 16) | (b[pos + 3] << 24);\n\t\tpos += 4;\n\t\treturn v;\n\t}\n\n\tstatic ulong ReadUInt64( byte[] b, ref int pos )\n\t{\n\t\tulong v = 0;\n\t\tfor ( int i = 0; i < 8; i++ )\n\t\t\tv |= (ulong)b[pos + i] << (i * 8);\n\t\tpos += 8;\n\t\treturn v;\n\t}\n\n\tstatic string ReadNullString( byte[] b, ref int pos )\n\t{\n\t\tint start = pos;\n\t\twhile ( pos < b.Length && b[pos] != 0 ) pos++;\n\t\tstring s = Encoding.ASCII.GetString( b, start, pos - start );\n\t\tpos++; // skip null\n\t\treturn s;\n\t}\n}\n"
}
]
}