{"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/// \u003Csummary\u003E\n/// Which source channel to read the height value from.\n/// \u003C/summary\u003E\npublic enum HeightChannel\n{\n\t/// \u003Csummary\u003E Red channel only. Standard for grayscale heightmaps. \u003C/summary\u003E\n\tRed,\n\tGreen,\n\tBlue,\n\tAlpha,\n\t/// \u003Csummary\u003E Rec.709 weighted luminance of RGB. \u003C/summary\u003E\n\tLuminance,\n\t/// \u003Csummary\u003E Average of the RGB channels. \u003C/summary\u003E\n\tAverage,\n\t/// \u003Csummary\u003E The largest of the RGB channels. \u003C/summary\u003E\n\tMax,\n}\n\n/// \u003Csummary\u003E\n/// Bit depth of the output .raw file. s\u0026box terrain stores heights as 16-bit,\n/// so 16-bit is recommended for the best precision.\n/// \u003C/summary\u003E\npublic enum HeightBitDepth\n{\n\t/// \u003Csummary\u003E 16-bit per height sample (recommended). 2 bytes per pixel. \u003C/summary\u003E\n\tBit16,\n\t/// \u003Csummary\u003E 8-bit per height sample. 1 byte per pixel. \u003C/summary\u003E\n\tBit8,\n}\n\n/// \u003Csummary\u003E\n/// How the source values are remapped into the 0..1 height range before quantizing.\n/// \u003C/summary\u003E\npublic enum HeightNormalize\n{\n\t/// \u003Csummary\u003E Use values as-is, clamped to 0..1. \u003C/summary\u003E\n\tClamp,\n\t/// \u003Csummary\u003E Stretch the min..max range of the image to fill 0..1 (good for HDR/EXR). \u003C/summary\u003E\n\tMinMaxStretch,\n}\n\n/// \u003Csummary\u003E\n/// Byte order of multi-byte (16-bit) samples in the output file.\n/// \u003C/summary\u003E\npublic enum HeightByteOrder\n{\n\t/// \u003Csummary\u003E Little-endian. What the s\u0026box terrain importer reads by default (\u0022Windows\u0022). \u003C/summary\u003E\n\tLittleEndian,\n\t/// \u003Csummary\u003E Big-endian (\u0022Mac\u0022). \u003C/summary\u003E\n\tBigEndian,\n}\n\n/// \u003Csummary\u003E\n/// Settings for converting an image into a terrain heightmap .raw file.\n/// \u003C/summary\u003E\npublic class HeightmapConvertSettings\n{\n\t/// \u003Csummary\u003E\n\t/// Which channel of the source image holds the height. Most heightmaps are\n\t/// grayscale, where every channel is identical - \u003Csee cref=\u0022HeightChannel.Red\u0022/\u003E works for those.\n\t/// \u003C/summary\u003E\n\t[Property]\n\tpublic HeightChannel Channel { get; set; } = HeightChannel.Red;\n\n\t/// \u003Csummary\u003E\n\t/// 16-bit gives the smoothest terrain and matches what s\u0026box stores internally.\n\t/// \u003C/summary\u003E\n\t[Property]\n\tpublic HeightBitDepth BitDepth { get; set; } = HeightBitDepth.Bit16;\n\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\n\t[Property, Range( 0, 8192 )]\n\tpublic int Resolution { get; set; } = 0;\n\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\n\t[Property]\n\tpublic bool PowerOfTwo { get; set; } = true;\n\n\t/// \u003Csummary\u003E\n\t/// How source values are mapped to the height range. Use \u003Csee cref=\u0022HeightNormalize.MinMaxStretch\u0022/\u003E\n\t/// for HDR images that don\u0027t already fill 0..1.\n\t/// \u003C/summary\u003E\n\t[Property]\n\tpublic HeightNormalize Normalize { get; set; } = HeightNormalize.Clamp;\n\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\n\t[Property]\n\tpublic bool FlipVertical { get; set; } = false;\n\n\t/// \u003Csummary\u003E\n\t/// Invert the heights (high becomes low). Useful for depth/inverted maps.\n\t/// \u003C/summary\u003E\n\t[Property]\n\tpublic bool Invert { get; set; } = false;\n\n\t/// \u003Csummary\u003E\n\t/// Byte order of 16-bit samples. The s\u0026box importer reads little-endian by default.\n\t/// \u003C/summary\u003E\n\t[Property, ShowIf( nameof( BitDepth ), HeightBitDepth.Bit16 )]\n\tpublic HeightByteOrder ByteOrder { get; set; } = HeightByteOrder.LittleEndian;\n}\n\n/// \u003Csummary\u003E\n/// Converts loaded images into raw single-channel heightmaps compatible with the\n/// s\u0026box terrain importer (square, 8 or 16 bit, raw samples with no header).\n/// \u003C/summary\u003E\npublic static class HeightmapConverter\n{\n\t/// \u003Csummary\u003E\n\t/// Convert a bitmap into a raw heightmap byte buffer.\n\t/// \u003C/summary\u003E\n\t/// \u003Cparam name=\u0022bitmap\u0022\u003ESource image.\u003C/param\u003E\n\t/// \u003Cparam name=\u0022settings\u0022\u003EConversion settings.\u003C/param\u003E\n\t/// \u003Cparam name=\u0022resolution\u0022\u003EThe square resolution of the produced heightmap.\u003C/param\u003E\n\t/// \u003Creturns\u003ERaw heightmap bytes ready to write to a .raw file.\u003C/returns\u003E\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 \u003C count; i\u002B\u002B )\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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\u0027t 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\u0027s 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 \u003C values.Length; i\u002B\u002B )\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 \u003C dstH; y\u002B\u002B )\n\t\t{\n\t\t\tfloat fy = dstH \u003E 1 ? (float)y / (dstH - 1) * (srcH - 1) : 0f;\n\t\t\tint y0 = (int)fy;\n\t\t\tint y1 = Math.Min( y0 \u002B 1, srcH - 1 );\n\t\t\tfloat ty = fy - y0;\n\n\t\t\tfor ( int x = 0; x \u003C dstW; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tfloat fx = dstW \u003E 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 \u002B 1, srcW - 1 );\n\t\t\t\tfloat tx = fx - x0;\n\n\t\t\t\tfloat top = MathX.Lerp( src[y0 * srcW \u002B x0], src[y0 * srcW \u002B x1], tx );\n\t\t\t\tfloat bottom = MathX.Lerp( src[y1 * srcW \u002B x0], src[y1 * srcW \u002B x1], tx );\n\t\t\t\tdst[y * dstW \u002B 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( \u0022R\u0022 );\n\t\t\tcase HeightChannel.Green: return exr.GetHeightChannel( \u0022G\u0022 );\n\t\t\tcase HeightChannel.Blue: return exr.GetHeightChannel( \u0022B\u0022 );\n\t\t\tcase HeightChannel.Alpha: return exr.GetHeightChannel( \u0022A\u0022 );\n\t\t}\n\n\t\tvar r = exr.GetChannel( \u0022R\u0022 );\n\t\tvar g = exr.GetChannel( \u0022G\u0022 );\n\t\tvar b = exr.GetChannel( \u0022B\u0022 );\n\n\t\t// No RGB set - it\u0027s 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 \u003C outv.Length; i\u002B\u002B )\n\t\t{\n\t\t\toutv[i] = channel switch\n\t\t\t{\n\t\t\t\tHeightChannel.Luminance =\u003E 0.2126f * r[i] \u002B 0.7152f * g[i] \u002B 0.0722f * b[i],\n\t\t\t\tHeightChannel.Average =\u003E (r[i] \u002B g[i] \u002B b[i]) / 3f,\n\t\t\t\tHeightChannel.Max =\u003E Math.Max( r[i], Math.Max( g[i], b[i] ) ),\n\t\t\t\t_ =\u003E 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 \u003E 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 ) =\u003E channel switch\n\t{\n\t\tHeightChannel.Red =\u003E c.r,\n\t\tHeightChannel.Green =\u003E c.g,\n\t\tHeightChannel.Blue =\u003E c.b,\n\t\tHeightChannel.Alpha =\u003E c.a,\n\t\tHeightChannel.Luminance =\u003E 0.2126f * c.r \u002B 0.7152f * c.g \u002B 0.0722f * c.b,\n\t\tHeightChannel.Average =\u003E (c.r \u002B c.g \u002B c.b) / 3f,\n\t\tHeightChannel.Max =\u003E Math.Max( c.r, Math.Max( c.g, c.b ) ),\n\t\t_ =\u003E 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 \u003E 1e-6f )\n\t\t\t{\n\t\t\t\tfor ( int i = 0; i \u003C values.Length; i\u002B\u002B )\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 \u003C values.Length; i\u002B\u002B )\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 \u003C resolution; y\u002B\u002B )\n\t\t\t{\n\t\t\t\tint srcY = flip ? resolution - 1 - y : y;\n\t\t\t\tfor ( int x = 0; x \u003C resolution; x\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tfloat v = Math.Clamp( values[srcY * resolution \u002B x], 0f, 1f );\n\t\t\t\t\tbytes[y * resolution \u002B 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 \u003C resolution; y\u002B\u002B )\n\t\t\t{\n\t\t\t\tint srcY = flip ? resolution - 1 - y : y;\n\t\t\t\tfor ( int x = 0; x \u003C resolution; x\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tfloat v = Math.Clamp( values[srcY * resolution \u002B 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 \u002B 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 \u0026 0xFF);\n\t\t\t\t\t\tbytes[o \u002B 1] = (byte)(h \u003E\u003E 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 \u003E\u003E 8);\n\t\t\t\t\t\tbytes[o \u002B 1] = (byte)(h \u0026 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/// \u003Csummary\u003E\n\t/// Rounds a value down to the nearest power of two.\n\t/// \u003C/summary\u003E\n\tpublic static int RoundDownToPowerOfTwo( int value )\n\t{\n\t\tif ( value \u003C 1 ) return 1;\n\t\tvalue |= value \u003E\u003E 1;\n\t\tvalue |= value \u003E\u003E 2;\n\t\tvalue |= value \u003E\u003E 4;\n\t\tvalue |= value \u003E\u003E 8;\n\t\tvalue |= value \u003E\u003E 16;\n\t\treturn value - (value \u003E\u003E 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/// \u003Csummary\u003E\n/// Editor tool that converts an image (.png, .jpg, .tga, .tif, .psd, .exr ...) into a\n/// raw single-channel heightmap (.raw) that the s\u0026box terrain importer can load.\n/// \u003C/summary\u003E\n[EditorApp( \u0022Heightmap Converter\u0022, \u0022terrain\u0022, \u0022Convert an image into a terrain heightmap (.raw)\u0022 )]\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 =\u003E 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 = \u0022Convert Image to Heightmap\u0022;\n\t\tSetWindowIcon( \u0022terrain\u0022 );\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\u0022Converts an image into a raw heightmap (.raw) for the terrain system.\\n\u0022 \u002B\n\t\t\t\u0022Output is square, single-channel, 8 or 16 bit. Import it via the Terrain component\u0027s heightmap import.\u0022, 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( \u0022No image selected\u0022, this );\n\t\t\tinputLabel.WordWrap = false;\n\t\t\trow.Add( inputLabel, 1 );\n\n\t\t\tvar browse = new Button( \u0022Browse Image...\u0022, \u0022image\u0022, 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 \u002B= _ =\u003E 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( \u0022\u0022, 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( \u0022Convert \u0026 Save...\u0022, \u0022file_download\u0022, 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 = \u0022Select Source Image\u0022,\n\t\t};\n\t\tfd.SetFindFile();\n\t\tfd.SetModeOpen();\n\t\tfd.SetNameFilter( \u0022Images (*.png *.jpg *.jpeg *.tga *.tif *.tiff *.psd *.exr *.bmp *.raw)\u0022 );\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\u0027t supported by the Skia-backed Bitmap loader, so decode it ourselves.\n\t\t\tbool isExr = path.EndsWith( \u0022.exr\u0022, 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( \u0022Couldn\u0027t load image\u0022,\n\t\t\t\t\t\t$\u0022Failed to decode \u0027{Path.GetFileName( path )}\u0027.\\nThis image format may not be supported.\u0022 );\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( \u0022Couldn\u0027t load image\u0022, 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 = \u0022No image selected\u0022;\n\t\t\toutputInfoLabel.Text = \u0022Pick an image to begin.\u0022;\n\t\t\tconvertButton.Enabled = false;\n\t\t\treturn;\n\t\t}\n\n\t\tstring kind = loadedExr is not null ? \u0022EXR\u0022 : \u0022image\u0022;\n\t\tinputLabel.Text = $\u0022{Path.GetFileName( inputFile )}  ({srcWidth}x{srcHeight}, {kind})\u0022;\n\t\tconvertButton.Enabled = true;\n\n\t\tint res = PreviewResolution();\n\t\tstring depth = Settings.BitDepth == HeightBitDepth.Bit16 ? \u002216-bit\u0022 : \u00228-bit\u0022;\n\t\tint bytes = res * res * (Settings.BitDepth == HeightBitDepth.Bit16 ? 2 : 1);\n\t\toutputInfoLabel.Text = $\u0022Output: {res}x{res}, {depth} \u2192 {bytes / 1024:n0} KB raw\u0022;\n\t}\n\n\tint PreviewResolution()\n\t{\n\t\tint res = Settings.Resolution \u003E 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 = \u0022Save Heightmap\u0022,\n\t\t\tDefaultSuffix = \u0022.raw\u0022,\n\t\t};\n\t\tfd.Directory = Path.GetDirectoryName( inputFile );\n\t\tfd.SelectFile( $\u0022{Path.GetFileNameWithoutExtension( inputFile )}.raw\u0022 );\n\t\tfd.SetFindFile();\n\t\tfd.SetModeSave();\n\t\tfd.SetNameFilter( \u0022Raw Heightmap (*.raw *.r16 *.r8)\u0022 );\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( \u0022Heightmap saved\u0022,\n\t\t\t\t$\u0022Wrote {resolution}x{resolution} heightmap to:\\n{fd.SelectedFile}\\n\\n\u0022 \u002B\n\t\t\t\t\u0022Import it from a Terrain component: open its heightmap import and choose this .raw file.\u0022 );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tEditorUtility.DisplayDialog( \u0022Conversion failed\u0022, 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( \u0022AddonTitle\u0022, \u0022terrainconvert\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022terrainconvert\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022brax\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022brax.terrainconvert\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002226\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-06-26T23:15:58.0786236Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.205.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.205.0\u0022)]"},{"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/// \u003Csummary\u003E\n/// Minimal OpenEXR reader for scanline images. Source 2\u0027s own EXR loader is internal to\n/// the engine and can\u0027t 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/// \u003C/summary\u003E\npublic sealed class ExrImage\n{\n\tpublic int Width { get; private set; }\n\tpublic int Height { get; private set; }\n\n\t/// \u003Csummary\u003E Channel name -\u003E linear float plane (row-major, Width*Height). \u003C/summary\u003E\n\treadonly Dictionary\u003Cstring, float[]\u003E channels = new( StringComparer.Ordinal );\n\n\tpublic IReadOnlyCollection\u003Cstring\u003E ChannelNames =\u003E channels.Keys;\n\tpublic bool HasChannel( string name ) =\u003E 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 =\u003E Type == PixelType.Half ? 2 : 4;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Returns true if the bytes start with the OpenEXR magic number.\n\t/// \u003C/summary\u003E\n\tpublic static bool IsExr( byte[] data )\n\t\t=\u003E data is { Length: \u003E= 4 } \u0026\u0026 data[0] == 0x76 \u0026\u0026 data[1] == 0x2f \u0026\u0026 data[2] == 0x31 \u0026\u0026 data[3] == 0x01;\n\n\t/// \u003Csummary\u003E\n\t/// Decode an EXR from a byte buffer.\n\t/// \u003C/summary\u003E\n\tpublic static ExrImage Load( byte[] data )\n\t{\n\t\tif ( !IsExr( data ) )\n\t\t\tthrow new InvalidDataException( \u0022Not an OpenEXR file (bad magic number).\u0022 );\n\n\t\tvar img = new ExrImage();\n\t\timg.Parse( data );\n\t\treturn img;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Get a channel as a float plane, or null if absent. Common heightmap names are \u0022Y\u0022\n\t/// (luminance), \u0022R\u0022, or a plain unnamed channel.\n\t/// \u003C/summary\u003E\n\tpublic float[] GetChannel( string name ) =\u003E channels.TryGetValue( name, out var v ) ? v : null;\n\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\n\tpublic float[] GetHeightChannel( string preferred = null )\n\t{\n\t\tif ( preferred is not null \u0026\u0026 channels.TryGetValue( preferred, out var p ) )\n\t\t\treturn p;\n\n\t\tforeach ( var name in new[] { \u0022Y\u0022, \u0022R\u0022, \u0022G\u0022, \u0022B\u0022 } )\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 \u0026 0x200) != 0;\n\t\tbool deep = (version \u0026 0x800) != 0;\n\t\tbool multipart = (version \u0026 0x1000) != 0;\n\n\t\tif ( tiled ) throw new NotSupportedException( \u0022Tiled EXR images are not supported - re-export as a scanline image.\u0022 );\n\t\tif ( deep ) throw new NotSupportedException( \u0022Deep EXR images are not supported.\u0022 );\n\t\tif ( multipart ) throw new NotSupportedException( \u0022Multi-part EXR images are not supported.\u0022 );\n\n\t\tvar chans = new List\u003CChannelInfo\u003E();\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 \u0022channels\u0022:\n\t\t\t\t\tParseChannels( data, valueStart, chans );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \u0022compression\u0022:\n\t\t\t\t\tcompression = (Compression)data[valueStart];\n\t\t\t\t\tbreak;\n\t\t\t\tcase \u0022dataWindow\u0022:\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 \u002B size; // skip to next attribute\n\t\t}\n\n\t\tif ( chans.Count == 0 )\n\t\t\tthrow new InvalidDataException( \u0022EXR has no channels.\u0022 );\n\n\t\tWidth = xMax - xMin \u002B 1;\n\t\tHeight = yMax - yMin \u002B 1;\n\n\t\tif ( Width \u003C= 0 || Height \u003C= 0 )\n\t\t\tthrow new InvalidDataException( $\u0022EXR has invalid data window ({Width}x{Height}).\u0022 );\n\n\t\tint linesPerBlock = compression switch\n\t\t{\n\t\t\tCompression.None or Compression.Rle or Compression.Zips =\u003E 1,\n\t\t\tCompression.Zip or Compression.Pxr24 =\u003E 16,\n\t\t\tCompression.Piz or Compression.B44 or Compression.B44A or Compression.Dwaa =\u003E 32,\n\t\t\tCompression.Dwab =\u003E 256,\n\t\t\t_ =\u003E 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( $\u0022EXR compression \u0027{compression}\u0027 is not supported. Re-export the heightmap as Uncompressed, ZIP, or RLE.\u0022 );\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 \u002B linesPerBlock - 1) / linesPerBlock;\n\t\tvar offsets = new long[blockCount];\n\t\tfor ( int i = 0; i \u003C blockCount; i\u002B\u002B )\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 \u002B= 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 \u002B 1 );\n\t\t\tint uncompressedSize = rowBytes * lines;\n\n\t\t\tbyte[] block;\n\t\t\tif ( compression == Compression.None || dataSize \u003E= uncompressedSize )\n\t\t\t{\n\t\t\t\t// Stored uncompressed (NONE, or a block that didn\u0027t 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\u003CChannelInfo\u003E chans, int rowBytes, int width, int rowOffset, int lines )\n\t{\n\t\tfor ( int i = 0; i \u003C lines; i\u002B\u002B )\n\t\t{\n\t\t\tint rowBase = i * rowBytes;\n\t\t\tint channelOffset = 0;\n\t\t\tint destRow = rowOffset \u002B i;\n\n\t\t\tforeach ( var c in chans )\n\t\t\t{\n\t\t\t\tint src = rowBase \u002B 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 \u003C width; x\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tplane[destBase \u002B x] = ReadSample( block, src, c.Type );\n\t\t\t\t\tsrc \u002B= c.SampleSize;\n\t\t\t\t}\n\n\t\t\t\tchannelOffset \u002B= width * c.SampleSize;\n\t\t\t}\n\t\t}\n\t}\n\n\tstatic float ReadSample( byte[] b, int offset, PixelType type ) =\u003E type switch\n\t{\n\t\tPixelType.Half =\u003E (float)BitConverter.UInt16BitsToHalf( (ushort)(b[offset] | (b[offset \u002B 1] \u003C\u003C 8)) ),\n\t\tPixelType.Float =\u003E BitConverter.Int32BitsToSingle( b[offset] | (b[offset \u002B 1] \u003C\u003C 8) | (b[offset \u002B 2] \u003C\u003C 16) | (b[offset \u002B 3] \u003C\u003C 24) ),\n\t\tPixelType.Uint =\u003E (uint)(b[offset] | (b[offset \u002B 1] \u003C\u003C 8) | (b[offset \u002B 2] \u003C\u003C 16) | (b[offset \u002B 3] \u003C\u003C 24)),\n\t\t_ =\u003E 0f,\n\t};\n\n\tstatic void ParseChannels( byte[] data, int pos, List\u003CChannelInfo\u003E 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 \u002B= 1;      // pLinear\n\t\t\tpos \u002B= 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 =\u003E Inflate( data, offset, size, uncompressedSize ),\n\t\t\tCompression.Rle =\u003E RleDecode( data, offset, size, uncompressedSize ),\n\t\t\t_ =\u003E throw new NotSupportedException( $\u0022EXR compression \u0027{compression}\u0027 is not supported.\u0022 ),\n\t\t};\n\n\t\t// Step 2: undo EXR\u0027s byte predictor \u002B 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 \u003C 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 \u002B= 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 \u002B size;\n\n\t\twhile ( i \u003C end \u0026\u0026 o \u003C expected )\n\t\t{\n\t\t\tsbyte count = (sbyte)data[i\u002B\u002B];\n\t\t\tif ( count \u003C 0 )\n\t\t\t{\n\t\t\t\tint n = -count;\n\t\t\t\twhile ( n-- \u003E 0 \u0026\u0026 i \u003C end \u0026\u0026 o \u003C expected )\n\t\t\t\t\toutBuf[o\u002B\u002B] = data[i\u002B\u002B];\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tint n = count \u002B 1;\n\t\t\t\tbyte v = data[i\u002B\u002B];\n\t\t\t\twhile ( n-- \u003E 0 \u0026\u0026 o \u003C expected )\n\t\t\t\t\toutBuf[o\u002B\u002B] = 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 \u003C b.Length; i\u002B\u002B )\n\t\t{\n\t\t\tint d = b[i - 1] \u002B 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 \u002B 1) / 2;\n\t\tint s = 0;\n\n\t\twhile ( true )\n\t\t{\n\t\t\tif ( s \u003C len ) outB[s\u002B\u002B] = src[t1\u002B\u002B]; else break;\n\t\t\tif ( s \u003C len ) outB[s\u002B\u002B] = src[t2\u002B\u002B]; 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 \u002B 1] \u003C\u003C 8) | (b[pos \u002B 2] \u003C\u003C 16) | (b[pos \u002B 3] \u003C\u003C 24);\n\t\tpos \u002B= 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 \u003C 8; i\u002B\u002B )\n\t\t\tv |= (ulong)b[pos \u002B i] \u003C\u003C (i * 8);\n\t\tpos \u002B= 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 \u003C b.Length \u0026\u0026 b[pos] != 0 ) pos\u002B\u002B;\n\t\tstring s = Encoding.ASCII.GetString( b, start, pos - start );\n\t\tpos\u002B\u002B; // skip null\n\t\treturn s;\n\t}\n}\n"}]}