{"TotalCount":22,"Files":[{"Ident":"anakin.flatpack","Path":"FlatpackSystem.cs","FileName":"FlatpackSystem.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace Flatpack;\r\n\r\n/// \u003Csummary\u003E\r\n/// How hard Flatpack works, for a game\u0027s graphics settings: high quality presets turn it off, low ones lean on it.\r\n/// Set \u003Csee cref=\u0022FlatpackSystem.Level\u0022/\u003E; every Flatpack Renderer follows it within a few frames, nothing re-bakes.\r\n/// \u003C/summary\u003E\r\npublic enum FlatpackLevel\r\n{\r\n\t/// \u003Csummary\u003EReal models everywhere; cards never show (a game\u0027s High or Ultra preset).\u003C/summary\u003E\r\n\tOff,\r\n\r\n\t/// \u003Csummary\u003ESwap where the bake was measured to look like the model (the default; a game\u0027s Medium).\u003C/summary\u003E\r\n\tBalanced,\r\n\r\n\t/// \u003Csummary\u003ESwap at 0.6 times the distance, from 3 bounding radii (a game\u0027s Low).\u003C/summary\u003E\r\n\tPerformance,\r\n\r\n\t/// \u003Csummary\u003ESwap at 0.35 times the distance, from 1.5 bounding radii (a game\u0027s lowest preset or weak hardware).\u003C/summary\u003E\r\n\tMaximum,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Runs the model/card distance check for every \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E in the scene.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// One loop per frame instead of one Update per renderer. The loop is time-sliced: each frame checks at most\r\n/// \u003Csee cref=\u0022RenderersPerFrame\u0022/\u003E renderers, round-robin, so a forest of 5000 trees spreads its checks over a few\r\n/// frames. Renderers in the middle of a cross-fade are ticked every frame until they finish.\r\n/// \u003C/remarks\u003E\r\npublic sealed class FlatpackSystem : GameObjectSystem\u003CFlatpackSystem\u003E\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Global level for every Flatpack Renderer, meant to be driven by a game\u0027s graphics settings, for example\r\n\t/// \u003Cc\u003EFlatpackSystem.Level = FlatpackLevel.Performance;\u003C/c\u003E on the Low preset. Applies to automatic and manual swap\r\n\t/// distances alike; Force Model and Force Card modes ignore it.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static FlatpackLevel Level { get; set; } = FlatpackLevel.Balanced;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// What \u003Csee cref=\u0022Level\u0022/\u003E does to a swap distance: a multiplier, and the closest swap allowed in bounding radii\r\n\t/// (0 = the renderer\u0027s own minimum). \u003Csee cref=\u0022FlatpackLevel.Off\u0022/\u003E pushes the swap out to infinity.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static (float Scale, float MinimumRadii) LevelScaling =\u003E Level switch\r\n\t{\r\n\t\tFlatpackLevel.Off =\u003E (float.PositiveInfinity, 0.0f),\r\n\t\tFlatpackLevel.Performance =\u003E (0.6f, 3.0f),\r\n\t\tFlatpackLevel.Maximum =\u003E (0.35f, 1.5f),\r\n\t\t_ =\u003E (1.0f, 0.0f),\r\n\t};\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Distance checks per frame. Lower spreads the work thinner but makes swaps lag behind a fast camera.\r\n\t/// \u003C/summary\u003E\r\n\tpublic int RenderersPerFrame { get; set; } = 2048;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Number of registered renderers.\r\n\t/// \u003C/summary\u003E\r\n\tpublic int Count =\u003E _renderers.Count;\r\n\r\n\treadonly List\u003CFlatpackRenderer\u003E _renderers = new();\r\n\treadonly List\u003CFlatpackRenderer\u003E _fading = new();\r\n\treadonly Dictionary\u003CFlatpackImpostor, int\u003E _visibleCards = new();\r\n\tint _cursor;\r\n\r\n\tpublic FlatpackSystem( Scene scene ) : base( scene )\r\n\t{\r\n\t\t// After the scene update, so this frame\u0027s camera and object movement are already in.\r\n\t\tListen( Stage.FinishUpdate, 100, Tick, \u0022Flatpack.DistancePass\u0022 );\r\n\t}\r\n\r\n\tinternal void Register( FlatpackRenderer renderer )\r\n\t{\r\n\t\tif ( renderer.SystemIndex \u003E= 0 )\r\n\t\t\treturn;\r\n\r\n\t\trenderer.SystemIndex = _renderers.Count;\r\n\t\t_renderers.Add( renderer );\r\n\t}\r\n\r\n\tinternal void Unregister( FlatpackRenderer renderer )\r\n\t{\r\n\t\tvar index = renderer.SystemIndex;\r\n\t\tif ( index \u003C 0 || index \u003E= _renderers.Count || _renderers[index] != renderer )\r\n\t\t\treturn;\r\n\r\n\t\t// Swap-remove keeps this O(1); the moved renderer takes over the freed slot.\r\n\t\tvar last = _renderers[^1];\r\n\t\t_renderers[index] = last;\r\n\t\tlast.SystemIndex = index;\r\n\t\t_renderers.RemoveAt( _renderers.Count - 1 );\r\n\r\n\t\trenderer.SystemIndex = -1;\r\n\t\t_fading.Remove( renderer );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Reference count of visible cards per impostor, kept by \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E, so the tick can keep\r\n\t/// those atlases streamed in without walking every renderer.\r\n\t/// \u003C/summary\u003E\r\n\tinternal void CardVisibilityChanged( FlatpackImpostor impostor, bool visible )\r\n\t{\r\n\t\tif ( impostor is null )\r\n\t\t\treturn;\r\n\r\n\t\t_visibleCards.TryGetValue( impostor, out var count );\r\n\t\tcount \u002B= visible ? 1 : -1;\r\n\r\n\t\tif ( count \u003E 0 )\r\n\t\t\t_visibleCards[impostor] = count;\r\n\t\telse\r\n\t\t\t_visibleCards.Remove( impostor );\r\n\t}\r\n\r\n\tvoid Tick()\r\n\t{\r\n\t\tforeach ( var impostor in _visibleCards.Keys )\r\n\t\t\timpostor.MarkAtlasesUsed();\r\n\r\n\t\tif ( _renderers.Count == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tvar camera = Scene.Camera;\r\n\t\tif ( !camera.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar cameraPosition = camera.WorldPosition;\r\n\t\tvar count = Math.Min( RenderersPerFrame, _renderers.Count );\r\n\r\n\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _cursor \u003E= _renderers.Count )\r\n\t\t\t\t_cursor = 0;\r\n\r\n\t\t\tvar renderer = _renderers[_cursor\u002B\u002B];\r\n\t\t\tvar distanceSquared = cameraPosition.DistanceSquared( renderer.WorldPosition );\r\n\r\n\t\t\tif ( renderer.Evaluate( distanceSquared ) \u0026\u0026 !_fading.Contains( renderer ) )\r\n\t\t\t\t_fading.Add( renderer );\r\n\t\t}\r\n\r\n\t\tif ( _fading.Count == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tvar delta = Time.Delta;\r\n\t\tfor ( var i = _fading.Count - 1; i \u003E= 0; i-- )\r\n\t\t{\r\n\t\t\tif ( !_fading[i].TickFade( delta ) )\r\n\t\t\t\t_fading.RemoveAt( i );\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"anakin.flatpack","Path":"Editor/FlatpackPfm.cs","FileName":"FlatpackPfm.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":429259,"IsPrivate":false,"Code":"using System.IO;\nusing System.Text;\n\nnamespace Flatpack.EditorTools;\n\n/// \u003Csummary\u003E\n/// Writes greyscale PFM (portable float map) files. The baker stores the 16-bit depth atlas this way: s\u0026amp;box\u0027s texture\n/// compiler reads PNGs at 8 bits whatever their bit depth, but keeps a float source\u0027s precision when it builds an R16\n/// texture (measured in iteration 8: every value of a 16-bit ramp came back exact).\n/// \u003C/summary\u003E\nstatic class FlatpackPfm\n{\n\t/// \u003Csummary\u003E\n\t/// Write \u003Cparamref name=\u0022pixels\u0022/\u003E (row by row, top row first) as a \u003Cparamref name=\u0022width\u0022/\u003E x\n\t/// \u003Cparamref name=\u0022height\u0022/\u003E single-channel float image.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// The PFM format stores rows bottom first, but the s\u0026amp;box texture compiler reads the first row as the top, so rows\n\t/// go out in image order.\n\t/// \u003C/remarks\u003E\n\tpublic static void WriteGray( string path, float[] pixels, int width, int height )\n\t{\n\t\tusing var file = File.Create( path );\n\t\tusing var writer = new BinaryWriter( file );\n\n\t\t// \u0022Pf\u0022 is one channel; a negative scale means little-endian floats.\n\t\twriter.Write( Encoding.ASCII.GetBytes( $\u0022Pf\\n{width} {height}\\n-1.0\\n\u0022 ) );\n\t\tfor ( var i = 0; i \u003C width * height; i\u002B\u002B )\n\t\t\twriter.Write( pixels[i] );\n\t}\n}\n"},{"Ident":"anakin.flatpack","Path":"Editor/FlatpackBaker.cs","FileName":"FlatpackBaker.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Editor;\nusing Sandbox;\n\nnamespace Flatpack.EditorTools;\n\n/// \u003Csummary\u003E\n/// Bakes a model into a Flatpack impostor: albedo, normal, depth and roughness \u002B AO atlases, a frame lookup texture, a\n/// material using \u003Cc\u003Eflatpack_impostor.shader\u003C/c\u003E, and a \u003Csee cref=\u0022FlatpackImpostor\u0022/\u003E resource that ties them together.\n/// \u003C/summary\u003E\n/// \u003Cremarks\u003E\n/// \u003Cpara\u003E\n/// The model is rendered alone in a throwaway scene with an orthographic camera, once per frame of the\n/// N x N grid. Each camera is fitted to that view\u0027s outline and depth range (stored in the frame lookup texture), so the\n/// object fills its frame. Each pass uses one of the engine\u0027s camera debug views, which write material data straight to\n/// the target with no lighting, exposure or tonemapping:\n/// \u003C/para\u003E\n/// \u003Clist type=\u0022bullet\u0022\u003E\n/// \u003Citem\u003EAlbedo: the colour texture as sRGB bytes, alpha = coverage.\u003C/item\u003E\n/// \u003Citem\u003ENormalMap: the normal-mapped world normal packed as n * 0.5 \u002B 0.5. The model sits at the origin with no\n/// rotation, so world space is object space.\u003C/item\u003E\n/// \u003Citem\u003ERoughness, AmbientOcclusion: linear values as bytes.\u003C/item\u003E\n/// \u003Citem\u003EDepth: there is no depth view, so \u003Cc\u003Eflatpack_bake_depth.shader\u003C/c\u003E goes on as a material override and\n/// writes linear depth as albedo (16 bits split over red and green), read back through the Albedo view.\u003C/item\u003E\n/// \u003C/list\u003E\n/// \u003C/remarks\u003E\npublic static class FlatpackBaker\n{\n\t/// \u003Csummary\u003E\n\t/// The runtime impostor shader the generated material uses.\n\t/// \u003C/summary\u003E\n\tpublic const string ImpostorShader = \u0022shaders/flatpack_impostor.shader\u0022;\n\n\tconst string DepthShader = \u0022shaders/flatpack_bake_depth.shader\u0022;\n\n\t/// \u003Csummary\u003EOpacity at which a supersample counts as covered: the usual alpha-test cut of s\u0026amp;box materials.\u003C/summary\u003E\n\tconst byte AlphaTestCut = 128;\n\n\t/// \u003Csummary\u003E\n\t/// Bakes whose covered texels are more than this share partly covered are see-through detail. Measured on the test\n\t/// set: fence 0.75, weed tree 0.50, everything solid or leafy 0.06 to 0.24.\n\t/// \u003C/summary\u003E\n\tpublic const float SeeThroughShare = 0.4f;\n\n\t/// \u003Csummary\u003E\n\t/// Framed bakes whose covered texels are more than this share partly covered are fine wire mesh and get baked again\n\t/// unframed. Measured framed in iteration 8: fence 0.59, weed tree 0.42, solid and leafy models 0.10 or less.\n\t/// \u003C/summary\u003E\n\tconst float FineMeshShare = 0.5f;\n\n\t/// \u003Csummary\u003E\n\t/// What a bake wrote.\n\t/// \u003C/summary\u003E\n\tpublic sealed class Result\n\t{\n\t\t/// \u003Csummary\u003EThe \u003Cc\u003E.fpack\u003C/c\u003E resource, loaded.\u003C/summary\u003E\n\t\tpublic FlatpackImpostor Impostor { get; init; }\n\n\t\t/// \u003Csummary\u003EAsset-relative path of the \u003Cc\u003E.fpack\u003C/c\u003E.\u003C/summary\u003E\n\t\tpublic string ImpostorPath { get; init; }\n\n\t\t/// \u003Csummary\u003EAsset-relative path of the generated \u003Cc\u003E.vmat\u003C/c\u003E.\u003C/summary\u003E\n\t\tpublic string MaterialPath { get; init; }\n\n\t\t/// \u003Csummary\u003EAbsolute paths of every file written.\u003C/summary\u003E\n\t\tpublic IReadOnlyList\u003Cstring\u003E Files { get; init; }\n\n\t\t/// \u003Csummary\u003EWall-clock bake time.\u003C/summary\u003E\n\t\tpublic TimeSpan Duration { get; init; }\n\n\t\t/// \u003Csummary\u003EThe quality level the bake ended up at (Auto picks one).\u003C/summary\u003E\n\t\tpublic FlatpackQuality Level { get; init; }\n\n\t\t/// \u003Csummary\u003EHow closely the card matches the model by distance, when the bake was measured.\u003C/summary\u003E\n\t\tpublic FlatpackQualityCheck.Report Measured { get; init; }\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Bake one model at the settings\u0027 quality level, or for \u003Csee cref=\u0022FlatpackQuality.Auto\u0022/\u003E at the cheapest level that\n\t/// matches the model from \u003Csee cref=\u0022FlatpackBakeSettings.AutoTargetRadii\u0022/\u003E outward. Measures the result when\n\t/// \u003Csee cref=\u0022FlatpackBakeSettings.MeasureQuality\u0022/\u003E is on (always for Auto) and stores where it holds up.\n\t/// \u003C/summary\u003E\n\tpublic static async Task\u003CResult\u003E BakeAsync( Model model, FlatpackBakeSettings settings, Action\u003Cfloat, string\u003E progress = null, CancellationToken cancel = default )\n\t{\n\t\tif ( settings.Quality != FlatpackQuality.Auto )\n\t\t{\n\t\t\tvar result = await BakeLevelAsync( model, settings, progress, cancel );\n\t\t\treturn settings.MeasureQuality ? await MeasureAsync( result, progress, cancel ) : result;\n\t\t}\n\n\t\t// Auto: climb the levels until one holds up at the target distance. Each bake overwrites the last.\n\t\tvar timer = Stopwatch.StartNew();\n\t\tvar levels = new[] { FlatpackQuality.Low, FlatpackQuality.Medium, FlatpackQuality.High, FlatpackQuality.Ultra };\n\t\tvar best = FlatpackQuality.Ultra;\n\t\tvar bestScore = float.MaxValue;\n\t\tResult last = null;\n\n\t\tfor ( var i = 0; i \u003C levels.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar level = levels[i];\n\t\t\tvar step = i;\n\t\t\tvar baked = await BakeLevelAsync( model, settings.WithQuality( level ),\n\t\t\t\t( f, text ) =\u003E progress?.Invoke( (step \u002B f * 0.8f) / levels.Length, $\u0022{level}: {text}\u0022 ), cancel );\n\t\t\tlast = await MeasureAsync( baked, ( f, text ) =\u003E progress?.Invoke( (step \u002B 0.8f \u002B f * 0.2f) / levels.Length, $\u0022{level}: {text}\u0022 ), cancel );\n\n\t\t\t// See-through detail goes straight to Ultra. At Medium a wire grid has about one texel per wire, so its frames\n\t\t\t// line up badly and the panel doubles when seen at an angle, even where the bake check passes it (iteration 7).\n\t\t\tif ( last.Impostor.SeeThrough \u003E SeeThroughShare \u0026\u0026 level != FlatpackQuality.Ultra )\n\t\t\t{\n\t\t\t\ti = levels.Length - 2;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Holds up from the target (judged out to 16 radii; past that every level matches as well as it ever will).\n\t\t\tif ( last.Measured.HoldsUpFrom \u003C= MathF.Min( settings.AutoTargetRadii, 16.0f ) )\n\t\t\t\treturn Finish( last, timer );\n\n\t\t\tvar score = last.Measured.HoldsUpFrom;\n\n\t\t\tif ( score \u003C bestScore )\n\t\t\t{\n\t\t\t\tbestScore = score;\n\t\t\t\tbest = level;\n\t\t\t}\n\t\t}\n\n\t\t// Nothing held up (fine wire mesh, for example). Keep whichever level held up from closest.\n\t\tif ( best != FlatpackQuality.Ultra )\n\t\t{\n\t\t\tvar baked = await BakeLevelAsync( model, settings.WithQuality( best ), progress, cancel );\n\t\t\tlast = await MeasureAsync( baked, progress, cancel );\n\t\t}\n\n\t\treturn Finish( last, timer );\n\t}\n\n\tstatic Result Finish( Result r, Stopwatch timer ) =\u003E new()\n\t{\n\t\tImpostor = r.Impostor, ImpostorPath = r.ImpostorPath, MaterialPath = r.MaterialPath, Files = r.Files,\n\t\tLevel = r.Level, Measured = r.Measured, Duration = timer.Elapsed,\n\t};\n\n\t/// \u003Csummary\u003E\n\t/// Measure a finished bake and store where it holds up on its \u003Cc\u003E.fpack\u003C/c\u003E.\n\t/// \u003C/summary\u003E\n\tstatic async Task\u003CResult\u003E MeasureAsync( Result baked, Action\u003Cfloat, string\u003E progress, CancellationToken cancel )\n\t{\n\t\tif ( baked.Impostor.SeeThrough \u003E SeeThroughShare )\n\t\t{\n\t\t\tprogress?.Invoke( 0.0f, \u0022Fitting see-through coverage to the model\u0022 );\n\t\t\tvar (soft, growth) = await FlatpackQualityCheck.FitSoftCoverageAsync( baked.Impostor, cancel );\n\t\t\tWriteMaterialValue( baked.MaterialPath, \u0022g_flFlatpackSoftCoverage\u0022, soft );\n\t\t\tWriteMaterialValue( baked.MaterialPath, \u0022g_flFlatpackSoftGrowth\u0022, growth );\n\t\t}\n\n\t\tprogress?.Invoke( 0.0f, \u0022Measuring against the model\u0022 );\n\t\tvar report = await FlatpackQualityCheck.MeasureAsync( baked.Impostor, cancel: cancel );\n\n\t\tvar impostor = baked.Impostor;\n\t\timpostor.HoldsUpFrom = float.IsPositiveInfinity( report.HoldsUpFrom ) ? -1.0f : report.HoldsUpFrom;\n\t\tAssetSystem.FindByPath( baked.ImpostorPath )?.SaveToDisk( impostor );\n\t\tprogress?.Invoke( 1.0f, report.Summary );\n\n\t\treturn new Result\n\t\t{\n\t\t\tImpostor = impostor, ImpostorPath = baked.ImpostorPath, MaterialPath = baked.MaterialPath, Files = baked.Files,\n\t\t\tDuration = baked.Duration, Level = baked.Level, Measured = report,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Share of covered texels (alpha above 2 of 255) that are only partly covered (below 250).\n\t/// \u003C/summary\u003E\n\tstatic float PartialCoverageShare( Color32[] albedo )\n\t{\n\t\tlong covered = 0, partial = 0;\n\t\tforeach ( var c in albedo )\n\t\t{\n\t\t\tif ( c.a \u003C= 2 ) continue;\n\t\t\tcovered\u002B\u002B;\n\t\t\tif ( c.a \u003C 250 ) partial\u002B\u002B;\n\t\t}\n\t\treturn covered \u003E 0 ? partial / (float)covered : 0.0f;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Set one value in a generated \u003Cc\u003E.vmat\u003C/c\u003E (adding the line if it\u0027s missing) and recompile it.\n\t/// \u003C/summary\u003E\n\tstatic void WriteMaterialValue( string materialPath, string key, float value )\n\t{\n\t\tvar asset = AssetSystem.FindByPath( materialPath );\n\t\tif ( asset is null || !File.Exists( asset.AbsolutePath ) )\n\t\t\treturn;\n\n\t\tvar line = $\u0022\\t{key} \\\u0022{value.ToString( \u00220.000000\u0022, CultureInfo.InvariantCulture )}\\\u0022\u0022;\n\t\tvar lines = File.ReadAllLines( asset.AbsolutePath ).ToList();\n\t\tvar index = lines.FindIndex( l =\u003E l.TrimStart().StartsWith( key \u002B \u0022 \u0022 ) );\n\t\tif ( index \u003E= 0 )\n\t\t\tlines[index] = line;\n\t\telse\n\t\t\tlines.Insert( Math.Max( 0, lines.FindLastIndex( l =\u003E l.Trim() == \u0022}\u0022 ) ), line );\n\n\t\tFile.WriteAllLines( asset.AbsolutePath, lines );\n\t\tasset.Compile( true );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Bake one model at one quality level. Runs on the main thread but yields between frames, so the editor keeps\n\t/// drawing; cancel with \u003Cparamref name=\u0022cancel\u0022/\u003E. \u003Cparamref name=\u0022progress\u0022/\u003E gets a 0..1 fraction and a status line.\n\t/// \u003C/summary\u003E\n\tstatic async Task\u003CResult\u003E BakeLevelAsync( Model model, FlatpackBakeSettings settings, Action\u003Cfloat, string\u003E progress, CancellationToken cancel )\n\t{\n\t\tif ( model is null || model.IsError )\n\t\t\tthrow new ArgumentException( \u0022Pick a valid model to bake.\u0022 );\n\n\t\tvar timer = Stopwatch.StartNew();\n\t\tvar (grid, atlasSize, searchSteps) = settings.Resolve();\n\t\tvar n = Math.Clamp( grid, 2, 32 );\n\t\t// The shader addresses frame (x, y) at (x \u002B u) / n of the atlas, so the atlas must be exactly n frames wide.\n\t\t// 2048 / 12 isn\u0027t whole; round the frame down (to keep the atlas a multiple of 4 for block compression) and\n\t\t// shrink the atlas to fit, rather than let the frames drift out of place across the atlas.\n\t\tvar cell = (int)atlasSize / n;\n\t\twhile ( (cell * n) % 4 != 0 )\n\t\t\tcell--;\n\t\tvar atlas = cell * n;\n\t\tvar layout = settings.Layout;\n\n\t\tvar bounds = model.RenderBounds;\n\t\tif ( bounds.Size.Length \u003C 0.001f )\n\t\t\tbounds = model.Bounds;\n\n\t\t// A sphere fitted to the vertices is usually much smaller than the box\u0027s half diagonal, so the object fills more\n\t\t// of every frame. Fall back to the box for models without readable vertices.\n\t\tvar vertices = model.GetVertices();\n\t\tvar points = vertices is { Length: \u003E 0 } ? vertices.Select( v =\u003E v.Position ).ToArray() : null;\n\t\tvar (center, radius) = points is not null\n\t\t\t? FlatpackMath.BoundingSphere( points )\n\t\t\t: FlatpackMath.BoundingSphere( bounds );\n\n\t\tvar (folder, assetsRoot) = ResolveOutputFolder( model, settings );\n\t\tvar name = SafeName( model );\n\n\t\tvar fitted = settings.FitEachView \u0026\u0026 points is not null;\n\t\tvar fits = FitFrames( points, center, radius, n, cell, layout, fitted );\n\n\t\tvar albedo = new Color32[atlas * atlas];\n\t\tvar normal = new Color32[atlas * atlas];\n\t\tvar depth = new float[atlas * atlas];\n\t\tvar rao = settings.BakeRoughnessAo ? new Color32[atlas * atlas] : null;\n\t\tvar supersample = Math.Clamp( settings.Supersample, 1, 4 );\n\n\t\tprogress?.Invoke( 0.0f, $\u0022Baking {name}\u0022 );\n\t\tvar bakedMetal = await CaptureFramesAsync( model, layout, n, cell, atlas, center, radius, fits, albedo, normal, depth, rao, settings.BakeSelfShadow, supersample, progress, cancel );\n\t\tvar seeThrough = PartialCoverageShare( albedo );\n\n\t\t// Fine wire mesh does worse framed: a fence panel seen edge-on squeezes into a sliver of texels, and its card came\n\t\t// out sparser than the real fence from 16 radii (iteration 8: 64 to 68% wrong pixels against 45 unframed). Bake\n\t\t// those views sphere-sized again. The weed tree\u0027s twigs (0.42 framed) keep the framing, which helped them a lot.\n\t\tif ( fitted \u0026\u0026 seeThrough \u003E FineMeshShare )\n\t\t{\n\t\t\tprogress?.Invoke( 0.0f, $\u0022Baking {name} again, unframed (fine mesh)\u0022 );\n\t\t\tfits = FitFrames( points, center, radius, n, cell, layout, false );\n\t\t\tbakedMetal = await CaptureFramesAsync( model, layout, n, cell, atlas, center, radius, fits, albedo, normal, depth, rao, settings.BakeSelfShadow, supersample, progress, cancel );\n\t\t\tseeThrough = PartialCoverageShare( albedo );\n\t\t}\n\n\t\tcancel.ThrowIfCancellationRequested();\n\t\tprogress?.Invoke( 0.85f, \u0022Padding edges\u0022 );\n\t\tawait Task.Delay( 1 );\n\n\t\tDilateIntoFrames( albedo, normal, depth, rao, atlas, cell, settings.EdgePadding );\n\t\tvar mipAlphaScales = MipAlphaScales( albedo, atlas );\n\n\t\tcancel.ThrowIfCancellationRequested();\n\t\tprogress?.Invoke( 0.9f, \u0022Writing files\u0022 );\n\t\tawait Task.Delay( 1 );\n\n\t\tDirectory.CreateDirectory( folder );\n\t\tvar files = new List\u003Cstring\u003E();\n\n\t\tvar depthBits = settings.DepthBits \u003E= 12 ? 16 : 8;\n\t\tvar albedoFile = WritePng( Path.Combine( folder, $\u0022{name}_fp_albedo.png\u0022 ), albedo, atlas, files );\n\t\tvar normalFile = WritePng( Path.Combine( folder, $\u0022{name}_fp_normal.png\u0022 ), normal, atlas, files );\n\t\tvar depthFile = depthBits == 16\n\t\t\t? WritePfm( Path.Combine( folder, $\u0022{name}_fp_depth.pfm\u0022 ), depth, atlas, files )\n\t\t\t: WritePng( Path.Combine( folder, $\u0022{name}_fp_depth.png\u0022 ), DepthBytes( depth ), atlas, files );\n\t\tvar framesFile = WriteFrames( Path.Combine( folder, $\u0022{name}_fp_frames.png\u0022 ), fits, n, files );\n\n\t\t// The coarse map must never read deeper than the depth texture it summarises, or the search would skip a block\n\t\t// it should have searched: BC4 rounds by a few steps, R16 by half of one.\n\t\tvar coarse = CoarseDepth( depth, atlas, cell, n, depthBits == 16 ? 1.0f / 65535.0f : 4.0f / 255.0f, out var coarseSize );\n\t\tvar coarseFile = Path.Combine( folder, $\u0022{name}_fp_depthmin.pfm\u0022 );\n\t\tFlatpackPfm.WriteGray( coarseFile, coarse, coarseSize, coarseSize );\n\t\tfiles.Add( coarseFile );\n\t\tvar raoFile = rao is null ? null : WritePng( Path.Combine( folder, $\u0022{name}_fp_rao.png\u0022 ), rao, atlas, files );\n\n\t\tvar zLocked = settings.ZLockedCard || layout == FlatpackLayout.Cylinder;\n\t\tvar materialFile = Path.Combine( folder, $\u0022{name}_fp.vmat\u0022 );\n\t\tFile.WriteAllText( materialFile, BuildMaterial(\n\t\t\tRelative( assetsRoot, albedoFile ), Relative( assetsRoot, normalFile ), Relative( assetsRoot, depthFile ), depthBits,\n\t\t\tRelative( assetsRoot, framesFile ), Relative( assetsRoot, coarseFile ), raoFile is null ? null : Relative( assetsRoot, raoFile ),\n\t\t\tn, layout, center, radius, zLocked, mipAlphaScales, searchSteps, seeThrough \u003E SeeThroughShare, bakedMetal ) );\n\t\tfiles.Add( materialFile );\n\n\t\tprogress?.Invoke( 0.95f, \u0022Compiling material\u0022 );\n\n\t\t// Textures first, so the material compile finds them already built.\n\t\tforeach ( var file in files )\n\t\t{\n\t\t\tvar asset = AssetSystem.RegisterFile( file );\n\t\t\tif ( asset is not null )\n\t\t\t\tawait asset.CompileIfNeededAsync( 120.0f );\n\t\t}\n\n\t\tvar materialPath = Relative( assetsRoot, materialFile );\n\t\tvar material = Material.Load( materialPath );\n\n\t\t// The resource: create it once, then overwrite its values on re-bakes so references to it survive.\n\t\tvar impostorFile = Path.Combine( folder, $\u0022{name}.fpack\u0022 );\n\t\tvar impostorPath = Relative( assetsRoot, impostorFile );\n\t\tvar impostorAsset = AssetSystem.FindByPath( impostorPath );\n\t\tif ( impostorAsset is null )\n\t\t{\n\t\t\timpostorAsset = AssetSystem.CreateResource( \u0022fpack\u0022, impostorFile );\n\t\t\tawait impostorAsset.CompileIfNeededAsync();\n\t\t}\n\n\t\tif ( !impostorAsset.TryLoadResource\u003CFlatpackImpostor\u003E( out var impostor ) )\n\t\t\tthrow new InvalidOperationException( $\u0022Couldn\u0027t create {impostorPath}.\u0022 );\n\n\t\timpostor.SourceModel = model;\n\t\timpostor.Material = material;\n\t\timpostor.Layout = layout;\n\t\timpostor.GridSize = n;\n\t\timpostor.AtlasSize = atlas;\n\t\timpostor.Center = center;\n\t\timpostor.Radius = radius;\n\t\timpostor.Bounds = bounds;\n\t\timpostor.ThinDetail = mipAlphaScales[1];\n\t\timpostor.QualityLevel = settings.Quality.ToString();\n\t\timpostor.HoldsUpFrom = 0.0f;\n\t\timpostor.SeeThrough = seeThrough;\n\t\timpostorAsset.SaveToDisk( impostor );\n\t\tfiles.Add( impostorFile );\n\n\t\tprogress?.Invoke( 1.0f, $\u0022Baked {name}\u0022 );\n\n\t\treturn new Result\n\t\t{\n\t\t\tImpostor = impostor,\n\t\t\tImpostorPath = impostorPath,\n\t\t\tMaterialPath = materialPath,\n\t\t\tFiles = files,\n\t\t\tDuration = timer.Elapsed,\n\t\t\tLevel = settings.Quality,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Each view\u0027s bounds: its outline and depth range from the vertices when \u003Cparamref name=\u0022fitted\u0022/\u003E, else the whole\n\t/// sphere. Padded by two texels so bilinear filtering at the outline stays inside the frame.\n\t/// \u003C/summary\u003E\n\tstatic FlatpackMath.FrameFit[] FitFrames( Vector3[] points, Vector3 center, float radius, int n, int cell, FlatpackLayout layout, bool fitted )\n\t{\n\t\tvar fits = new FlatpackMath.FrameFit[n * n];\n\t\tfor ( var y = 0; y \u003C n; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( var x = 0; x \u003C n; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tfits[y * n \u002B x] = fitted\n\t\t\t\t\t? FlatpackMath.FitFrame( points, center, radius, FlatpackMath.FrameDirection( x, y, n, layout ), cell, 2 )\n\t\t\t\t\t: FlatpackMath.DecodeFrameFit( FlatpackMath.EncodeFrameFit( FlatpackMath.FrameFit.Sphere ) );\n\t\t\t}\n\t\t}\n\t\treturn fits;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Render every frame and copy it into the CPU atlases. Returns whether metalness went into the roughness/AO atlas.\n\t/// \u003C/summary\u003E\n\tstatic async Task\u003Cbool\u003E CaptureFramesAsync( Model model, FlatpackLayout layout, int n, int cell, int atlas, Vector3 center, float radius, FlatpackMath.FrameFit[] fits,\n\t\tColor32[] albedo, Color32[] normal, float[] depth, Color32[] rao, bool selfShadow, int supersample, Action\u003Cfloat, string\u003E progress, CancellationToken cancel )\n\t{\n\t\tvar depthMaterial = Material.Create( \u0022flatpack_bake_depth\u0022, DepthShader, true );\n\t\tselfShadow \u0026= rao is not null;\n\n\t\tvar scene = Scene.CreateEditorScene();\n\t\tscene.Name = \u0022Flatpack Bake\u0022;\n\n\t\ttry\n\t\t{\n\t\t\tModelRenderer renderer;\n\t\t\tCameraComponent camera;\n\t\t\tDirectionalLight sun = null;\n\t\t\tMetalPass metal = null;\n\n\t\t\tusing ( scene.Push() )\n\t\t\t{\n\t\t\t\tvar source = new GameObject( true, \u0022source\u0022 );\n\t\t\t\trenderer = source.Components.Create\u003CModelRenderer\u003E();\n\t\t\t\trenderer.Model = model;\n\n\t\t\t\t// Bake from the most detailed LOD, whatever the frame\u0027s screen size would pick.\n\t\t\t\trenderer.LodOverride = 0;\n\n\t\t\t\tvar eye = new GameObject( true, \u0022camera\u0022 );\n\t\t\t\tcamera = eye.Components.Create\u003CCameraComponent\u003E();\n\t\t\t\tcamera.BackgroundColor = Color.Transparent;\n\t\t\t\tcamera.Orthographic = true;\n\n\t\t\t\tif ( selfShadow )\n\t\t\t\t{\n\t\t\t\t\tvar light = new GameObject( true, \u0022sky light\u0022 );\n\t\t\t\t\tsun = light.Components.Create\u003CDirectionalLight\u003E();\n\t\t\t\t\tsun.LightColor = Color.White;\n\t\t\t\t\tsun.Shadows = true;\n\t\t\t\t}\n\n\t\t\t\tif ( rao is not null )\n\t\t\t\t\tmetal = MetalPass.Create( renderer );\n\t\t\t}\n\n\t\t\tvar frame = new Bitmap( cell * supersample, cell * supersample, false );\n\t\t\tvar total = n * n;\n\t\t\tvar sliceTimer = Stopwatch.StartNew();\n\n\t\t\tfor ( var y = 0; y \u003C n; y\u002B\u002B )\n\t\t\t{\n\t\t\t\tfor ( var x = 0; x \u003C n; x\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tcancel.ThrowIfCancellationRequested();\n\n\t\t\t\t\tvar dir = FlatpackMath.FrameDirection( x, y, n, layout );\n\t\t\t\t\tvar fit = fits[y * n \u002B x];\n\n\t\t\t\t\tColor32[] colorPixels, normalPixels, roughPixels = null, aoPixels = null;\n\t\t\t\t\tfloat[] depthPixels;\n\t\t\t\t\tbyte[] visibility = null, metalness = null;\n\n\t\t\t\t\tusing ( scene.Push() )\n\t\t\t\t\t{\n\t\t\t\t\t\tFrameCamera( camera, center, radius, dir, fit );\n\t\t\t\t\t\tscene.EditorTick( 0.0f, 0.0f );\n\n\t\t\t\t\t\tcolorPixels = Capture( camera, renderer, frame, SceneCameraDebugMode.Albedo, null );\n\t\t\t\t\t\tnormalPixels = Capture( camera, renderer, frame, SceneCameraDebugMode.NormalMap, null );\n\t\t\t\t\t\tdepthPixels = DecodeDepth( Capture( camera, renderer, frame, SceneCameraDebugMode.Albedo, depthMaterial ) );\n\n\t\t\t\t\t\tif ( rao is not null )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\troughPixels = Capture( camera, renderer, frame, SceneCameraDebugMode.Roughness, null );\n\t\t\t\t\t\t\taoPixels = Capture( camera, renderer, frame, SceneCameraDebugMode.AmbientOcclusion, null );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( selfShadow )\n\t\t\t\t\t\t\tvisibility = CaptureSkyVisibility( scene, camera, renderer, sun, frame, normalPixels );\n\n\t\t\t\t\t\tif ( metal is not null )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif ( sun is not null ) sun.Enabled = false;\n\t\t\t\t\t\t\tmetalness = metal.Capture( scene, camera, renderer, frame );\n\t\t\t\t\t\t\tif ( sun is not null ) sun.Enabled = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( supersample \u003E 1 )\n\t\t\t\t\t\tDownsample( cell, supersample, ref colorPixels, ref normalPixels, ref depthPixels, ref roughPixels, ref aoPixels, ref visibility, ref metalness );\n\n\t\t\t\t\tBlitFrame( x, y, cell, atlas, colorPixels, normalPixels, depthPixels, roughPixels, aoPixels, visibility, metalness, albedo, normal, depth, rao );\n\n\t\t\t\t\tvar done = y * n \u002B x \u002B 1;\n\t\t\t\t\tprogress?.Invoke( done / (float)total * 0.85f, $\u0022Frame {done} of {total}\u0022 );\n\n\t\t\t\t\t// Hand the main thread back now and then so the editor stays responsive.\n\t\t\t\t\tif ( sliceTimer.ElapsedMilliseconds \u003E 30 )\n\t\t\t\t\t{\n\t\t\t\t\t\tawait Task.Delay( 1 );\n\t\t\t\t\t\tsliceTimer.Restart();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn metal is not null;\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\tscene.Destroy();\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Directions the sky-visibility pass lights the model from: one overhead and two rings, spread over the upper\n\t/// hemisphere. Fixed in object space, so every frame sees the same shadows and blending between frames stays seamless.\n\t/// \u003C/summary\u003E\n\tstatic readonly Angles[] SkyLights =\n\t{\n\t\tnew( 85, 0, 0 ),\n\t\tnew( 60, 0, 0 ), new( 60, 120, 0 ), new( 60, 240, 0 ),\n\t\tnew( 30, 45, 0 ), new( 30, 135, 0 ), new( 30, 225, 0 ), new( 30, 315, 0 ),\n\t};\n\n\t/// \u003Csummary\u003E\n\t/// How much of the sky each texel sees, 0..255: diffuse light with shadows over diffuse light without, both summed over\n\t/// \u003Csee cref=\u0022SkyLights\u0022/\u003E. This is the self-shadowing a dense model (a tree\u0027s inner branches) has and a card can\u0027t\n\t/// work out live; the shader uses it to darken ambient light and part of the albedo.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// Eight lights average out to soft occlusion; with only a few, a surface facing one light gets that light\u0027s hard\n\t/// shadow and turns black. Ratios are taken in linear light.\n\t/// \u003C/remarks\u003E\n\tstatic byte[] CaptureSkyVisibility( Scene scene, CameraComponent camera, ModelRenderer renderer, DirectionalLight sun, Bitmap target, Color32[] normals )\n\t{\n\t\tvar count = target.Width * target.Height;\n\t\tvar lit = new float[count];\n\t\tvar open = new float[count];\n\n\t\tforeach ( var angles in SkyLights )\n\t\t{\n\t\t\tsun.WorldRotation = angles;\n\n\t\t\tforeach ( var shadowed in new[] { true, false } )\n\t\t\t{\n\t\t\t\tsun.Shadows = shadowed;\n\t\t\t\tscene.EditorTick( 0.0f, 0.0f );\n\n\t\t\t\t// Render twice: the first render after moving or toggling the light can still use the old shadow map.\n\t\t\t\tCapture( camera, renderer, target, SceneCameraDebugMode.Diffuse, null );\n\t\t\t\tvar pixels = Capture( camera, renderer, target, SceneCameraDebugMode.Diffuse, null );\n\n\t\t\t\tvar sum = shadowed ? lit : open;\n\t\t\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\n\t\t\t\t\tsum[i] \u002B= Linear( pixels[i].r );\n\t\t\t}\n\t\t}\n\n\t\t// Shadow-map bias and terminator softness keep even fully open texels a little under 1, and that shifts from frame\n\t\t// to frame. Normalise: the well-lit texels of this frame (95th percentile) count as fully visible. Capped, so a\n\t\t// frame that really is mostly in shadow isn\u0027t brightened.\n\t\tvar ratio = new float[count];\n\t\tvar samples = new List\u003Cfloat\u003E( count );\n\t\tvar openMax = 0.0f;\n\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\n\t\t\topenMax = MathF.Max( openMax, open[i] );\n\n\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\n\t\t{\n\t\t\tratio[i] = open[i] \u003E 0.0f ? Math.Clamp( lit[i] / open[i], 0.0f, 1.0f ) : 1.0f;\n\t\t\tif ( normals[i].a \u003E 0 \u0026\u0026 open[i] \u003E openMax * 0.2f )\n\t\t\t\tsamples.Add( ratio[i] );\n\t\t}\n\n\t\tsamples.Sort();\n\t\tvar reference = samples.Count \u003E 16 ? samples[(int)(samples.Count * 0.95f)] : 1.0f;\n\t\treference = Math.Clamp( reference, 0.75f, 1.0f );\n\n\t\tvar visibility = new byte[count];\n\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\n\t\t{\n\t\t\t// Texels the sky barely reaches (undersides) carry little information; fade them toward fully visible.\n\t\t\tvar known = openMax \u003E 0.0f ? Math.Clamp( open[i] / (openMax * 0.2f), 0.0f, 1.0f ) : 0.0f;\n\t\t\tvar v = Math.Clamp( ratio[i] / reference, 0.0f, 1.0f );\n\t\t\tvisibility[i] = (byte)Math.Clamp( (int)MathF.Round( (v * known \u002B (1.0f - known)) * 255.0f ), 0, 255 );\n\t\t}\n\n\t\treturn visibility;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The diffuse lighting view writes light through an sRGB target; ratios only make sense in linear light.\n\t/// \u003C/summary\u003E\n\tstatic float Linear( byte value )\n\t{\n\t\tvar c = value / 255.0f;\n\t\treturn c \u003C= 0.04045f ? c / 12.92f : MathF.Pow( (c \u002B 0.055f) / 1.055f, 2.4f );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Point the bake camera at one frame: orthographic, framing exactly the frame\u0027s fitted image rectangle (wider or\n\t/// taller than square when the object is), with the near plane on its nearest surface.\n\t/// \u003C/summary\u003E\n\tstatic void FrameCamera( CameraComponent camera, Vector3 center, float radius, Vector3 dir, FlatpackMath.FrameFit fit )\n\t{\n\t\tFlatpackMath.FrameBasis( dir, out var right, out var up );\n\n\t\tvar size = (fit.Max - fit.Min) * (2.0f * radius);\n\t\tvar mid = (fit.Min \u002B fit.Max) * 0.5f - new Vector2( 0.5f, 0.5f );\n\n\t\t// Image v runs down, so the rectangle\u0027s centre is up by -v. The camera stays two radii out, outside the sphere.\n\t\tvar distance = radius * 2.0f;\n\t\tcamera.WorldPosition = center \u002B right * (mid.x * 2.0f * radius) - up * (mid.y * 2.0f * radius) \u002B dir * distance;\n\t\tcamera.WorldRotation = FlatpackMath.FrameRotation( dir );\n\n\t\t// Depth 0..1 spans one bounding diameter back from the nearest surface, not just to the furthest one: the background\n\t\t// then sits at least a diameter behind the object, where the shader\u0027s search never reaches, even in mips that\n\t\t// average an outline with it. 16 bits over a diameter is still 256 times finer than the 8 bits it replaces.\n\t\tvar zNear = distance - fit.Near * radius;\n\t\tvar zFar = zNear \u002B 2.0f * radius;\n\t\tcamera.OrthographicHeight = size.y;\n\t\tcamera.ZNear = zNear;\n\t\tcamera.ZFar = zFar;\n\n\t\t// The engine\u0027s own orthographic camera is square for a square target; this is the same matrix (checked against\n\t\t// CameraComponent.ProjectionMatrix in the editor) with its own width, so a tall object fills the frame both ways.\n\t\tcamera.CustomProjectionMatrix = new Matrix(\n\t\t\t2.0f / size.x, 0, 0, 0,\n\t\t\t0, 2.0f / size.y, 0, 0,\n\t\t\t0, 0, -1.0f / (zFar - zNear), -zNear / (zFar - zNear),\n\t\t\t0, 0, 0, 1 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The depth bake\u0027s red and green bytes back to one 0..1 depth: red is whole 1/255 steps, green the fraction of a step.\n\t/// \u003C/summary\u003E\n\tstatic float[] DecodeDepth( Color32[] pixels )\n\t{\n\t\tvar depth = new float[pixels.Length];\n\t\tfor ( var i = 0; i \u003C pixels.Length; i\u002B\u002B )\n\t\t\tdepth[i] = Math.Clamp( (pixels[i].r \u002B pixels[i].g / 255.0f) / 255.0f, 0.0f, 1.0f );\n\t\treturn depth;\n\t}\n\n\tstatic Color32[] Capture( CameraComponent camera, ModelRenderer renderer, Bitmap target, SceneCameraDebugMode mode, Material overrideMaterial )\n\t{\n\t\tcamera.DebugMode = mode;\n\t\trenderer.MaterialOverride = overrideMaterial;\n\t\tcamera.RenderToBitmap( target );\n\t\treturn target.GetPixels32();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Bakes metalness. s\u0026amp;box has no metalness debug view and materials store it in different ways (a texture, a\n\t/// constant, packed channels), so this solves for it instead of reading it: the Reflect view of the real materials\n\t/// against copies forced to metalness 0 and 1, all lit by one light from the camera. Per texel,\n\t/// metal = (real - dielectric) / (metal - dielectric).\n\t/// \u003C/summary\u003E\n\tsealed class MetalPass\n\t{\n\t\tMaterial[] _original, _dielectric, _metal;\n\t\tDirectionalLight _light;\n\n\t\t/// \u003Csummary\u003EA pass for \u003Cparamref name=\u0022renderer\u0022/\u003E\u0027s model, or null when none of its materials can be metal.\u003C/summary\u003E\n\t\tpublic static MetalPass Create( ModelRenderer renderer )\n\t\t{\n\t\t\tvar count = renderer.Materials.Count;\n\t\t\tvar pass = new MetalPass { _original = new Material[count], _dielectric = new Material[count], _metal = new Material[count] };\n\t\t\tvar any = false;\n\t\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar source = renderer.Materials.GetOriginal( i );\n\t\t\t\tpass._original[i] = source;\n\t\t\t\tif ( source is null )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tany |= source.GetFeature( \u0022F_METALNESS_TEXTURE\u0022 ) != 0 || source.GetVector4( \u0022g_flMetalness\u0022 ).x \u003E 0.0f;\n\t\t\t\tpass._dielectric[i] = Forced( source, 0.0f, i );\n\t\t\t\tpass._metal[i] = Forced( source, 1.0f, i );\n\t\t\t}\n\n\t\t\tif ( !any )\n\t\t\t\treturn null;\n\n\t\t\tpass._light = new GameObject( true, \u0022metal light\u0022 ).Components.Create\u003CDirectionalLight\u003E();\n\t\t\tpass._light.LightColor = Color.White;\n\t\t\tpass._light.Shadows = false;\n\t\t\tpass._light.Enabled = false;\n\t\t\treturn pass;\n\t\t}\n\n\t\tstatic Material Forced( Material source, float metalness, int slot )\n\t\t{\n\t\t\tvar copy = source.CreateCopy( $\u0022flatpack_metal_{metalness:0}_{slot}\u0022 );\n\t\t\tcopy.SetFeature( \u0022F_METALNESS_TEXTURE\u0022, 0 );\n\t\t\tcopy.Set( \u0022g_flMetalness\u0022, metalness );\n\t\t\treturn copy;\n\t\t}\n\n\t\t/// \u003Csummary\u003EMetalness per pixel of \u003Cparamref name=\u0022target\u0022/\u003E, 0..255. Call inside the bake scene.\u003C/summary\u003E\n\t\tpublic byte[] Capture( Scene scene, CameraComponent camera, ModelRenderer renderer, Bitmap target )\n\t\t{\n\t\t\t_light.WorldRotation = camera.WorldRotation;\n\t\t\t_light.Enabled = true;\n\t\t\tvar real = Render( scene, camera, renderer, target, _original );\n\t\t\tvar low = Render( scene, camera, renderer, target, _dielectric );\n\t\t\tvar high = Render( scene, camera, renderer, target, _metal );\n\t\t\t_light.Enabled = false;\n\t\t\trenderer.ClearMaterialOverrides();\n\n\t\t\tvar w = target.Width;\n\t\t\tvar h = target.Height;\n\t\t\tvar count = real.Length;\n\t\t\tvar value = new float[count];\n\t\t\tvar known = new bool[count];\n\t\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( real[i].a \u003C 128 )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tfloat r = Brightness( real[i] ), d = Brightness( low[i] ), m = Brightness( high[i] );\n\n\t\t\t\t// Where metal and dielectric reflect alike (dark colours, grazing angles) the solve has nothing to go on;\n\t\t\t\t// metalness barely changes the look there anyway. Those texels borrow their neighbours\u0027 value below.\n\t\t\t\tif ( MathF.Abs( m - d ) \u003C 6.0f )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvalue[i] = Math.Clamp( (r - d) / (m - d), 0.0f, 1.0f );\n\t\t\t\tknown[i] = true;\n\t\t\t}\n\n\t\t\t// A few rounds of filling unknown texels from known neighbours; anything still unknown is non-metal.\n\t\t\tfor ( var round = 0; round \u003C 6; round\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar next = (bool[])known.Clone();\n\t\t\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tif ( known[i] || real[i].a \u003C 128 )\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\tint x = i % w, y = i / w, n = 0;\n\t\t\t\t\tvar sum = 0.0f;\n\t\t\t\t\tfor ( var dy = -1; dy \u003C= 1; dy\u002B\u002B )\n\t\t\t\t\t{\n\t\t\t\t\t\tfor ( var dx = -1; dx \u003C= 1; dx\u002B\u002B )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tint nx = x \u002B dx, ny = y \u002B dy;\n\t\t\t\t\t\t\tif ( nx \u003C 0 || ny \u003C 0 || nx \u003E= w || ny \u003E= h ) continue;\n\t\t\t\t\t\t\tvar j = ny * w \u002B nx;\n\t\t\t\t\t\t\tif ( !known[j] ) continue;\n\t\t\t\t\t\t\tsum \u002B= value[j];\n\t\t\t\t\t\t\tn\u002B\u002B;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( n == 0 )\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\tvalue[i] = sum / n;\n\t\t\t\t\tnext[i] = true;\n\t\t\t\t}\n\t\t\t\tknown = next;\n\t\t\t}\n\n\t\t\tvar bytes = new byte[count];\n\t\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\n\t\t\t\tbytes[i] = (byte)Math.Clamp( (int)MathF.Round( value[i] * 255.0f ), 0, 255 );\n\t\t\treturn bytes;\n\t\t}\n\n\t\tstatic Color32[] Render( Scene scene, CameraComponent camera, ModelRenderer renderer, Bitmap target, Material[] materials )\n\t\t{\n\t\t\tfor ( var i = 0; i \u003C materials.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( materials[i] is not null )\n\t\t\t\t\trenderer.Materials.SetOverride( i, materials[i] );\n\t\t\t}\n\t\t\trenderer.Materials.Apply();\n\t\t\tscene.EditorTick( 0.0f, 0.0f );\n\t\t\treturn FlatpackBaker.Capture( camera, renderer, target, SceneCameraDebugMode.Reflect, null );\n\t\t}\n\n\t\tstatic float Brightness( Color32 c ) =\u003E (c.r \u002B c.g \u002B c.b) / 3.0f;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Average a supersampled frame down to \u003Cparamref name=\u0022cell\u0022/\u003E texels a side. Alpha becomes the share of covered\n\t/// samples; colour, normal, depth and the material channels average over the covered samples only, so the empty\n\t/// background doesn\u0027t darken thin parts.\n\t/// \u003C/summary\u003E\n\tstatic void Downsample( int cell, int ss, ref Color32[] color, ref Color32[] normal, ref float[] depth, ref Color32[] rough, ref Color32[] ao, ref byte[] visibility, ref byte[] metal )\n\t{\n\t\tvar size = cell * ss;\n\t\tvar outColor = new Color32[cell * cell];\n\t\tvar outNormal = new Color32[cell * cell];\n\t\tvar outDepth = new float[cell * cell];\n\t\tvar outRough = rough is null ? null : new Color32[cell * cell];\n\t\tvar outAo = ao is null ? null : new Color32[cell * cell];\n\t\tvar outVis = visibility is null ? null : new byte[cell * cell];\n\t\tvar outMetal = metal is null ? null : new byte[cell * cell];\n\n\t\tfor ( var y = 0; y \u003C cell; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( var x = 0; x \u003C cell; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tfloat r = 0, g = 0, b = 0, a = 0, nx = 0, ny = 0, nz = 0, d = 0, ro = 0, oc = 0, vi = 0, me = 0;\n\t\t\t\tvar covered = 0;\n\n\t\t\t\tfor ( var sy = 0; sy \u003C ss; sy\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tfor ( var sx = 0; sx \u003C ss; sx\u002B\u002B )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar i = (y * ss \u002B sy) * size \u002B x * ss \u002B sx;\n\t\t\t\t\t\tvar c = color[i];\n\n\t\t\t\t\t\t// The albedo view writes the material\u0027s raw opacity. Alpha-tested textures keep their small mips\n\t\t\t\t\t\t// above the cut on purpose, so averaging that raw value overstates thin detail (a fence panel that\u0027s\n\t\t\t\t\t\t// 12% wire read as 44%). Cut each sample the way the game cuts the real model, then average.\n\t\t\t\t\t\tif ( c.a \u003C AlphaTestCut )\n\t\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t\ta \u002B= 255;\n\t\t\t\t\t\tcovered\u002B\u002B;\n\t\t\t\t\t\tr \u002B= c.r; g \u002B= c.g; b \u002B= c.b;\n\t\t\t\t\t\tnx \u002B= normal[i].r / 127.5f - 1.0f; ny \u002B= normal[i].g / 127.5f - 1.0f; nz \u002B= normal[i].b / 127.5f - 1.0f;\n\t\t\t\t\t\td \u002B= depth[i];\n\t\t\t\t\t\tif ( rough is not null ) ro \u002B= rough[i].r;\n\t\t\t\t\t\tif ( ao is not null ) oc \u002B= ao[i].r;\n\t\t\t\t\t\tif ( visibility is not null ) vi \u002B= visibility[i];\n\t\t\t\t\t\tif ( metal is not null ) me \u002B= metal[i];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tvar o = y * cell \u002B x;\n\t\t\t\tif ( covered == 0 )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tbyte B( float v ) =\u003E (byte)Math.Clamp( (int)MathF.Round( v ), 0, 255 );\n\t\t\t\tvar alpha = B( a / (ss * ss) );\n\t\t\t\toutColor[o] = new Color32( B( r / covered ), B( g / covered ), B( b / covered ), Math.Max( alpha, (byte)1 ) );\n\n\t\t\t\tvar len = MathF.Sqrt( nx * nx \u002B ny * ny \u002B nz * nz );\n\t\t\t\tif ( len \u003C 1e-4f ) len = 1.0f;\n\t\t\t\toutNormal[o] = new Color32( B( (nx / len \u002B 1.0f) * 127.5f ), B( (ny / len \u002B 1.0f) * 127.5f ), B( (nz / len \u002B 1.0f) * 127.5f ), 255 );\n\n\t\t\t\toutDepth[o] = d / covered;\n\t\t\t\tif ( outRough is not null ) { var v = B( ro / covered ); outRough[o] = new Color32( v, v, v, 255 ); }\n\t\t\t\tif ( outAo is not null ) { var v = B( oc / covered ); outAo[o] = new Color32( v, v, v, 255 ); }\n\t\t\t\tif ( outVis is not null ) outVis[o] = B( vi / covered );\n\t\t\t\tif ( outMetal is not null ) outMetal[o] = B( me / covered );\n\t\t\t}\n\t\t}\n\n\t\tcolor = outColor; normal = outNormal; depth = outDepth; rough = outRough; ao = outAo; visibility = outVis; metal = outMetal;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Copy one frame\u0027s passes into the atlases. Uncovered texels stay fully clear (depth at the far plane) for the\n\t/// dilation pass.\n\t/// \u003C/summary\u003E\n\tstatic void BlitFrame( int fx, int fy, int cell, int atlas, Color32[] color, Color32[] normal, float[] depth, Color32[] rough, Color32[] ao,\n\t\tbyte[] visibility, byte[] metal, Color32[] albedoAtlas, Color32[] normalAtlas, float[] depthAtlas, Color32[] raoAtlas )\n\t{\n\t\tfor ( var py = 0; py \u003C cell; py\u002B\u002B )\n\t\t{\n\t\t\tvar row = (fy * cell \u002B py) * atlas \u002B fx * cell;\n\n\t\t\tfor ( var px = 0; px \u003C cell; px\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar src = py * cell \u002B px;\n\t\t\t\tvar dst = row \u002B px;\n\t\t\t\tvar c = color[src];\n\n\t\t\t\tif ( c.a == 0 )\n\t\t\t\t{\n\t\t\t\t\talbedoAtlas[dst] = default;\n\t\t\t\t\tnormalAtlas[dst] = new Color32( 128, 128, 255, 255 );\n\t\t\t\t\tdepthAtlas[dst] = 1.0f;\n\t\t\t\t\tif ( raoAtlas is not null ) raoAtlas[dst] = default;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\talbedoAtlas[dst] = c;\n\t\t\t\tnormalAtlas[dst] = new Color32( normal[src].r, normal[src].g, normal[src].b, 255 );\n\t\t\t\tdepthAtlas[dst] = depth[src];\n\n\t\t\t\t// Metalness rides in alpha as 1 \u002B metal x 254 (never 0, which would wipe the colour; see the shader).\n\t\t\t\tif ( raoAtlas is not null )\n\t\t\t\t\traoAtlas[dst] = new Color32( rough[src].r, ao[src].r, visibility?[src] ?? 255, metal is null ? (byte)255 : (byte)(1 \u002B metal[src] * 254 / 255) );\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Grow every frame\u0027s colours outward from its silhouette, staying inside the frame, so bilinear filtering and\n\t/// mips near the edge read the object\u0027s own colour instead of the empty background.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// One multi-source breadth-first fill over the whole atlas: each empty texel takes the values of the nearest\n\t/// covered texel (by steps) in its own frame. Albedo alpha on grown texels is 1/255, far below the alpha clip,\n\t/// but non-zero so the colour survives the PNG round trip.\n\t/// \u003C/remarks\u003E\n\tstatic void DilateIntoFrames( Color32[] albedo, Color32[] normal, float[] depth, Color32[] rao, int atlas, int cell, int padding )\n\t{\n\t\tvar maxSteps = padding \u003C= 0 ? int.MaxValue : padding;\n\t\tvar source = new int[albedo.Length];\n\t\tvar steps = new int[albedo.Length];\n\t\tvar queue = new Queue\u003Cint\u003E( albedo.Length / 4 );\n\n\t\tfor ( var i = 0; i \u003C albedo.Length; i\u002B\u002B )\n\t\t{\n\t\t\tif ( albedo[i].a \u003E 0 )\n\t\t\t{\n\t\t\t\tsource[i] = i;\n\t\t\t\tqueue.Enqueue( i );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tsource[i] = -1;\n\t\t\t}\n\t\t}\n\n\t\twhile ( queue.Count \u003E 0 )\n\t\t{\n\t\t\tvar i = queue.Dequeue();\n\t\t\tif ( steps[i] \u003E= maxSteps )\n\t\t\t\tcontinue;\n\n\t\t\tvar x = i % atlas;\n\t\t\tvar y = i / atlas;\n\n\t\t\t// Neighbours in the same frame only, never across a frame border.\n\t\t\tif ( x % cell \u003E 0 ) Grow( i, i - 1 );\n\t\t\tif ( x % cell \u003C cell - 1 ) Grow( i, i \u002B 1 );\n\t\t\tif ( y % cell \u003E 0 ) Grow( i, i - atlas );\n\t\t\tif ( y % cell \u003C cell - 1 ) Grow( i, i \u002B atlas );\n\t\t}\n\n\t\tvoid Grow( int from, int to )\n\t\t{\n\t\t\tif ( source[to] \u003E= 0 )\n\t\t\t\treturn;\n\n\t\t\tvar s = source[from];\n\t\t\tsource[to] = s;\n\t\t\tsteps[to] = steps[from] \u002B 1;\n\t\t\tqueue.Enqueue( to );\n\n\t\t\tvar c = albedo[s];\n\t\t\talbedo[to] = new Color32( c.r, c.g, c.b, 1 );\n\n\t\t\t// Grown texels keep the neighbour\u0027s normal but sit at the far plane, so the shader\u0027s depth search passes\n\t\t\t// straight through the background instead of stopping on borrowed depth and drawing a ghost.\n\t\t\tnormal[to] = normal[s];\n\t\t\tdepth[to] = 1.0f;\n\t\t\tif ( rao is not null ) rao[to] = rao[s];\n\t\t}\n\t}\n\n\tstatic string WritePng( string path, Color32[] pixels, int size, List\u003Cstring\u003E files ) =\u003E WritePng( path, pixels, size, size, files );\n\n\tstatic string WritePng( string path, Color32[] pixels, int width, int height, List\u003Cstring\u003E files )\n\t{\n\t\tusing var bitmap = new Bitmap( width, height, false );\n\t\tvar colors = new Color[pixels.Length];\n\t\tfor ( var i = 0; i \u003C pixels.Length; i\u002B\u002B )\n\t\t\tcolors[i] = pixels[i];\n\n\t\tbitmap.SetPixels( colors );\n\t\tFile.WriteAllBytes( path, bitmap.ToPng() );\n\t\tfiles.Add( path );\n\t\treturn path;\n\t}\n\n\t/// \u003Csummary\u003EThe 16-bit depth atlas, as a float image the texture compiler turns into R16.\u003C/summary\u003E\n\tstatic string WritePfm( string path, float[] depth, int size, List\u003Cstring\u003E files )\n\t{\n\t\tFlatpackPfm.WriteGray( path, depth, size, size );\n\t\tfiles.Add( path );\n\t\treturn path;\n\t}\n\n\t/// \u003Csummary\u003EDepth rounded to bytes, for the 8-bit (BC4) depth texture.\u003C/summary\u003E\n\tstatic Color32[] DepthBytes( float[] depth )\n\t{\n\t\tvar bytes = new Color32[depth.Length];\n\t\tfor ( var i = 0; i \u003C depth.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar v = (byte)Math.Clamp( (int)MathF.Round( depth[i] * 255.0f ), 0, 255 );\n\t\t\tbytes[i] = new Color32( v, v, v, 255 );\n\t\t}\n\t\treturn bytes;\n\t}\n\n\t/// \u003Csummary\u003ETexels per side of one coarse depth block (written into the material as \u003Cc\u003Eg_flFlatpackCoarseBlock\u003C/c\u003E).\u003C/summary\u003E\n\tpublic const int CoarseBlock = 16;\n\n\t/// \u003Csummary\u003E\n\t/// The coarse depth map: for each \u003Csee cref=\u0022CoarseBlock\u0022/\u003E x \u003Csee cref=\u0022CoarseBlock\u0022/\u003E texel block of each frame, the\n\t/// nearest depth in it (the smallest value) less \u003Cparamref name=\u0022margin\u0022/\u003E, laid out per frame like the atlas\n\t/// (frame x, y at blocks x, y of a \u003Cparamref name=\u0022size\u0022/\u003E-wide square). The shader\u0027s search skips every block whose\n\t/// nearest surface the ray never gets down to, and marches the rest a texel at a time.\n\t/// \u003C/summary\u003E\n\tstatic float[] CoarseDepth( float[] depth, int atlas, int cell, int n, float margin, out int size )\n\t{\n\t\tvar blocks = (cell \u002B CoarseBlock - 1) / CoarseBlock;\n\t\tsize = blocks * n;\n\t\tvar coarse = new float[size * size];\n\n\t\tfor ( var fy = 0; fy \u003C n; fy\u002B\u002B )\n\t\tfor ( var fx = 0; fx \u003C n; fx\u002B\u002B )\n\t\tfor ( var by = 0; by \u003C blocks; by\u002B\u002B )\n\t\tfor ( var bx = 0; bx \u003C blocks; bx\u002B\u002B )\n\t\t{\n\t\t\tvar nearest = 1.0f;\n\t\t\tfor ( var y = by * CoarseBlock; y \u003C Math.Min( (by \u002B 1) * CoarseBlock, cell ); y\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar row = (fy * cell \u002B y) * atlas \u002B fx * cell;\n\t\t\t\tfor ( var x = bx * CoarseBlock; x \u003C Math.Min( (bx \u002B 1) * CoarseBlock, cell ); x\u002B\u002B )\n\t\t\t\t\tnearest = MathF.Min( nearest, depth[row \u002B x] );\n\t\t\t}\n\n\t\t\tcoarse[(fy * blocks \u002B by) * size \u002B fx * blocks \u002B bx] = MathF.Max( 0.0f, nearest - margin );\n\t\t}\n\n\t\treturn coarse;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The frame lookup texture: two texels per frame (see \u003Csee cref=\u0022FlatpackMath.EncodeFrameFit\u0022/\u003E), image rectangles in\n\t/// the left half, depth ranges \u003Csee cref=\u0022FlatpackMath.FrameFitColumns\u0022/\u003E texels to the right.\n\t/// \u003C/summary\u003E\n\tstatic string WriteFrames( string path, FlatpackMath.FrameFit[] fits, int n, List\u003Cstring\u003E files )\n\t{\n\t\tconst int width = FlatpackMath.FrameFitColumns * 2;\n\t\tconst int height = FlatpackMath.FrameFitColumns;\n\t\tvar pixels = new Color32[width * height];\n\t\tfor ( var i = 0; i \u003C pixels.Length; i\u002B\u002B )\n\t\t\tpixels[i] = new Color32( 0, 0, 255, 255 );\n\n\t\tfor ( var y = 0; y \u003C n; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( var x = 0; x \u003C n; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar (rect, range) = FlatpackMath.EncodeFrameFit( fits[y * n \u002B x] );\n\t\t\t\tpixels[y * width \u002B x] = rect;\n\t\t\t\tpixels[y * width \u002B x \u002B FlatpackMath.FrameFitColumns] = range;\n\t\t\t}\n\t\t}\n\n\t\treturn WritePng( path, pixels, width, height, files );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Per-mip alpha multipliers that keep the share of covered texels the same as at full resolution.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// Mipmaps average a thin part\u0027s alpha with the empty space around it. Past a mip or two, spindles, wires and leaf\n\t/// tips fall below the 0.5 clip and the card loses them (or they sparkle). For each mip level we find the alpha\n\t/// that the same share of texels exceeds as were above 0.5 at full size, and store the factor that lifts it back to\n\t/// 0.5. A fixed boost instead over-fills foliage and fences into solid blobs. Mips are approximated with a 2x2 box\n\t/// filter, as the texture compiler builds them.\n\t/// \u003C/remarks\u003E\n\tstatic float[] MipAlphaScales( Color32[] albedo, int size )\n\t{\n\t\tconst int levels = 8;\n\t\tvar scales = new float[levels];\n\t\tvar alpha = new float[albedo.Length];\n\t\tvar covered = 0;\n\t\tfor ( var i = 0; i \u003C albedo.Length; i\u002B\u002B )\n\t\t{\n\t\t\talpha[i] = albedo[i].a / 255.0f;\n\t\t\tif ( alpha[i] \u003E 0.5f ) covered\u002B\u002B;\n\t\t}\n\n\t\tvar share = covered / (float)albedo.Length;\n\t\tvar w = size;\n\t\tvar h = size;\n\n\t\tfor ( var level = 0; level \u003C levels; level\u002B\u002B )\n\t\t{\n\t\t\tvar nw = Math.Max( 1, w / 2 );\n\t\t\tvar nh = Math.Max( 1, h / 2 );\n\t\t\tvar next = new float[nw * nh];\n\t\t\tfor ( var y = 0; y \u003C nh; y\u002B\u002B )\n\t\t\t{\n\t\t\t\tfor ( var x = 0; x \u003C nw; x\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tvar x0 = Math.Min( x * 2, w - 1 ); var x1 = Math.Min( x * 2 \u002B 1, w - 1 );\n\t\t\t\t\tvar y0 = Math.Min( y * 2, h - 1 ); var y1 = Math.Min( y * 2 \u002B 1, h - 1 );\n\t\t\t\t\tnext[y * nw \u002B x] = (alpha[y0 * w \u002B x0] \u002B alpha[y0 * w \u002B x1] \u002B alpha[y1 * w \u002B x0] \u002B alpha[y1 * w \u002B x1]) * 0.25f;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\talpha = next; w = nw; h = nh;\n\n\t\t\t// The alpha value with \u0060share\u0060 of this mip\u0027s texels above it.\n\t\t\tvar sorted = (float[])alpha.Clone();\n\t\t\tArray.Sort( sorted );\n\t\t\tvar index = Math.Clamp( (int)((1.0f - share) * sorted.Length), 0, sorted.Length - 1 );\n\t\t\tvar threshold = sorted[index];\n\t\t\tscales[level] = threshold \u003E 0.01f ? Math.Clamp( 0.5f / threshold, 1.0f, 4.0f ) : 1.0f;\n\t\t}\n\n\t\treturn scales;\n\t}\n\n\tstatic string BuildMaterial( string albedo, string normal, string depth, int depthBits, string frames, string coarse, string rao, int n, FlatpackLayout layout, Vector3 center, float radius,\n\t\tbool zLocked, float[] mipAlphaScales, int searchSteps, bool seeThrough, bool bakedMetal )\n\t{\n\t\tstring F( float v ) =\u003E v.ToString( \u00220.000000\u0022, CultureInfo.InvariantCulture );\n\n\t\tvar sb = new StringBuilder();\n\t\tsb.AppendLine( \u0022// Flatpack impostor material. Generated by the Flatpack baker (Anakin Studio); re-baking overwrites it.\u0022 );\n\t\tsb.AppendLine( \u0022Layer0\u0022 );\n\t\tsb.AppendLine( \u0022{\u0022 );\n\t\tsb.AppendLine( $\u0022\\tshader \\\u0022{ImpostorShader}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tTextureAlbedo \\\u0022{albedo}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tTextureNormal \\\u0022{normal}\\\u0022\u0022 );\n\t\tsb.AppendLine( depthBits == 16 ? $\u0022\\tTextureDepth \\\u0022{depth}\\\u0022\u0022 : $\u0022\\tTextureDepthBC4 \\\u0022{depth}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tTextureFrames \\\u0022{frames}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tTextureDepthMin \\\u0022{coarse}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackCoarse \\\u0022{F( 1 )}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackCoarseBlock \\\u0022{F( CoarseBlock )}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackFramed \\\u0022{F( 1 )}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackDepthBits \\\u0022{F( depthBits )}\\\u0022\u0022 );\n\t\tif ( rao is not null )\n\t\t\tsb.AppendLine( $\u0022\\tTextureRoughnessAo \\\u0022{rao}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackGrid \\\u0022{F( n )}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackMode \\\u0022{F( (int)layout )}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_vFlatpackCenter \\\u0022[{F( center.x )} {F( center.y )} {F( center.z )}]\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackRadius \\\u0022{F( radius )}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackZLocked \\\u0022{F( zLocked ? 1 : 0 )}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_vFlatpackMipAlpha0 \\\u0022[{F( mipAlphaScales[0] )} {F( mipAlphaScales[1] )} {F( mipAlphaScales[2] )} {F( mipAlphaScales[3] )}]\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_vFlatpackMipAlpha1 \\\u0022[{F( mipAlphaScales[4] )} {F( mipAlphaScales[5] )} {F( mipAlphaScales[6] )} {F( mipAlphaScales[7] )}]\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackSearchSteps \\\u0022{F( Math.Clamp( searchSteps, 0, 32 ) )}\\\u0022\u0022 );\n\t\t// See-through bakes start from a typical fit; the bake check fits both to the model after baking.\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackSoftCoverage \\\u0022{F( seeThrough ? 1.5f : 0.0f )}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackSoftGrowth \\\u0022{F( seeThrough ? 0.5f : 0.0f )}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\tg_flFlatpackBakedMetal \\\u0022{F( bakedMetal ? 1.0f : 0.0f )}\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022}\u0022 );\n\t\treturn sb.ToString();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Where the outputs go, and the Assets folder that asset paths are relative to.\n\t/// \u003C/summary\u003E\n\tstatic (string Folder, string AssetsRoot) ResolveOutputFolder( Model model, FlatpackBakeSettings settings )\n\t{\n\t\tvar projectAssets = Project.Current.GetAssetsPath();\n\n\t\tif ( !string.IsNullOrWhiteSpace( settings.OutputFolder ) )\n\t\t{\n\t\t\tvar sub = settings.OutputFolder.Trim().Trim( \u0027/\u0027, \u0027\\\\\u0027 );\n\t\t\treturn (Path.GetFullPath( Path.Combine( projectAssets, sub ) ), projectAssets);\n\t\t}\n\n\t\t// Next to the source, but only when the source is a file in this project (or one of its libraries).\n\t\t// Engine, cloud and cached models are read-only to us.\n\t\tvar source = AssetSystem.FindByPath( model.ResourcePath );\n\t\tvar projectRoot = Path.GetFullPath( Project.Current.GetRootPath() );\n\t\tvar absolute = source?.AbsolutePath;\n\n\t\tif ( !string.IsNullOrEmpty( absolute ) )\n\t\t{\n\t\t\tabsolute = Path.GetFullPath( absolute );\n\t\t\tvar inProject = absolute.StartsWith( projectRoot, StringComparison.OrdinalIgnoreCase )\n\t\t\t\t\u0026\u0026 !absolute.Contains( $\u0022{Path.DirectorySeparatorChar}.sbox{Path.DirectorySeparatorChar}\u0022, StringComparison.OrdinalIgnoreCase );\n\n\t\t\tvar assetsRoot = inProject ? FindAssetsRoot( absolute ) : null;\n\t\t\tif ( assetsRoot is not null )\n\t\t\t\treturn (Path.GetDirectoryName( absolute ), assetsRoot);\n\t\t}\n\n\t\treturn (Path.Combine( projectAssets, \u0022flatpack\u0022 ), projectAssets);\n\t}\n\n\tstatic string FindAssetsRoot( string file )\n\t{\n\t\tvar dir = new DirectoryInfo( Path.GetDirectoryName( file ) );\n\t\twhile ( dir is not null )\n\t\t{\n\t\t\tif ( dir.Name.Equals( \u0022Assets\u0022, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\treturn dir.FullName;\n\n\t\t\tdir = dir.Parent;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tstatic string Relative( string assetsRoot, string file )\n\t{\n\t\treturn Path.GetRelativePath( assetsRoot, file ).Replace( \u0027\\\\\u0027, \u0027/\u0027 ).ToLowerInvariant();\n\t}\n\n\tstatic string SafeName( Model model )\n\t{\n\t\tvar name = Path.GetFileNameWithoutExtension( model.ResourcePath ?? model.Name ?? \u0022model\u0022 );\n\t\tvar invalid = Path.GetInvalidFileNameChars();\n\t\treturn new string( name.Select( c =\u003E invalid.Contains( c ) || c == \u0027 \u0027 ? \u0027_\u0027 : char.ToLowerInvariant( c ) ).ToArray() );\n\t}\n}\n"},{"Ident":"anakin.flatpack","Path":"Editor/FlatpackBakeWindow.cs","FileName":"FlatpackBakeWindow.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing Editor;\r\nusing Sandbox;\r\n\r\nnamespace Flatpack.EditorTools;\r\n\r\n/// \u003Csummary\u003E\r\n/// The models queued in the bake window. A class of its own so a control sheet can edit the list.\r\n/// \u003C/summary\u003E\r\npublic sealed class FlatpackBakeQueue\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Models to bake, one impostor each.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public List\u003CModel\u003E Models { get; set; } = new();\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Editor window for baking impostors: a model list, the bake settings, and a bake button with progress and cancel.\r\n/// Open it from the Editor menu (Flatpack, Impostor Baker), or from a model\u0027s right-click menu.\r\n/// \u003C/summary\u003E\r\npublic sealed class FlatpackBakeWindow : Dialog\r\n{\r\n\tconst string SettingsCookie = \u0022Flatpack.BakeSettings\u0022;\r\n\r\n\tstatic FlatpackBakeWindow _current;\r\n\r\n\treadonly FlatpackBakeQueue _queue = new();\r\n\treadonly FlatpackBakeSettings _settings;\r\n\treadonly Label _status;\r\n\treadonly ProgressBar _bar;\r\n\treadonly Button _bake;\r\n\treadonly ControlSheet _models;\r\n\tCancellationTokenSource _cancel;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// True while a bake is running in this window.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool IsBaking =\u003E _cancel is not null;\r\n\r\n\t[Menu( \u0022Editor\u0022, \u0022Flatpack/Impostor Baker\u0022 )]\r\n\tpublic static void OpenFromMenu() =\u003E Open();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Show the bake window (reusing an open one) and queue \u003Cparamref name=\u0022models\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static FlatpackBakeWindow Open( params Model[] models )\r\n\t{\r\n\t\tif ( _current is not { IsValid: true } )\r\n\t\t\t_current = new FlatpackBakeWindow();\r\n\r\n\t\t_current.Enqueue( models );\r\n\t\t_current.Show();\r\n\t\treturn _current;\r\n\t}\r\n\r\n\tFlatpackBakeWindow() : base( EditorWindow )\r\n\t{\r\n\t\t_settings = EditorCookie.Get( SettingsCookie, new FlatpackBakeSettings() ) ?? new FlatpackBakeSettings();\r\n\r\n\t\tWindow.Title = \u0022Flatpack Impostor Baker\u0022;\r\n\t\tWindow.SetWindowIcon( \u0022view_in_ar\u0022 );\r\n\t\tWindow.Size = new Vector2( 460, 640 );\r\n\t\tWindow.StateCookie = \u0022FlatpackBakeWindow\u0022;\r\n\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Margin = 12;\r\n\t\tLayout.Spacing = 8;\r\n\r\n\t\tvar title = Layout.Add( new Label( \u0022Flatpack by Anakin Studio\u0022 ) );\r\n\t\ttitle.SetStyles( \u0022font-weight: bold;\u0022 );\r\n\r\n\t\tLayout.Add( new Label( \u0022Bakes each model into an impostor: atlases, a material and a .fpack resource. Add the .fpack to a Flatpack Renderer.\u0022 ) { WordWrap = true } );\r\n\r\n\t\t_models = new ControlSheet();\r\n\t\t_models.AddObject( _queue.GetSerialized() );\r\n\t\tLayout.Add( _models );\r\n\r\n\t\tvar settings = new ControlSheet();\r\n\t\tvar serializedSettings = _settings.GetSerialized();\r\n\t\tserializedSettings.OnPropertyChanged \u002B= _ =\u003E EditorCookie.Set( SettingsCookie, _settings );\r\n\t\tsettings.AddObject( serializedSettings );\r\n\t\tLayout.Add( settings );\r\n\r\n\t\tLayout.AddStretchCell();\r\n\r\n\t\t_status = Layout.Add( new Label( \u0022Add models, then bake.\u0022 ) { WordWrap = true } );\r\n\t\t_bar = Layout.Add( new ProgressBar( this ) );\r\n\t\t_bar.FixedHeight = 8;\r\n\t\t_bar.Visible = false;\r\n\r\n\t\tvar footer = Layout.AddRow();\r\n\t\tfooter.Spacing = 8;\r\n\t\tfooter.Add( new Button( \u0022Add Selected\u0022, \u0022playlist_add\u0022 )\r\n\t\t{\r\n\t\t\tToolTip = \u0022Queue the models of the selected objects in the open scene.\u0022,\r\n\t\t\tClicked = AddSceneSelection,\r\n\t\t} );\r\n\t\tfooter.Add( new Button( \u0022Clear\u0022, \u0022clear_all\u0022 ) { Clicked = () =\u003E { _queue.Models.Clear(); Rebuild(); } } );\r\n\t\tfooter.Add( new Button( \u0022Apply to Scene\u0022, \u0022forest\u0022 )\r\n\t\t{\r\n\t\t\tToolTip = \u0022Add a Flatpack Renderer to every object in the open scene whose model has a baked impostor.\u0022,\r\n\t\t\tClicked = () =\u003E _status.Text = $\u0022Added Flatpack Renderer to {ApplyToScene()} object(s).\u0022,\r\n\t\t} );\r\n\t\tfooter.AddStretchCell();\r\n\t\t_bake = footer.Add( new Button.Primary( \u0022Bake\u0022, \u0022view_in_ar\u0022 ) { Clicked = OnBakeClicked } );\r\n\r\n\t\tShow();\r\n\t}\r\n\r\n\tpublic override void OnDestroyed()\r\n\t{\r\n\t\t_cancel?.Cancel();\r\n\t\tif ( _current == this )\r\n\t\t\t_current = null;\r\n\r\n\t\tbase.OnDestroyed();\r\n\t}\r\n\r\n\tvoid Enqueue( IEnumerable\u003CModel\u003E models )\r\n\t{\r\n\t\tforeach ( var model in models ?? Enumerable.Empty\u003CModel\u003E() )\r\n\t\t{\r\n\t\t\tif ( model is not null \u0026\u0026 !model.IsError \u0026\u0026 !_queue.Models.Contains( model ) )\r\n\t\t\t\t_queue.Models.Add( model );\r\n\t\t}\r\n\r\n\t\tRebuild();\r\n\t}\r\n\r\n\tvoid Rebuild()\r\n\t{\r\n\t\t// The list control doesn\u0027t watch the list for changes made from code, so rebuild the sheet.\r\n\t\t_models.Clear( true );\r\n\t\t_models.AddObject( _queue.GetSerialized() );\r\n\t}\r\n\r\n\tvoid AddSceneSelection()\r\n\t{\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session is null )\r\n\t\t\treturn;\r\n\r\n\t\tvar found = session.Selection.OfType\u003CGameObject\u003E()\r\n\t\t\t.SelectMany( go =\u003E go.Components.GetAll\u003CModelRenderer\u003E( FindMode.EverythingInSelfAndDescendants ) )\r\n\t\t\t.Select( r =\u003E r.Model )\r\n\t\t\t.Where( m =\u003E m is not null );\r\n\r\n\t\tEnqueue( found.ToArray() );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Give every \u003Csee cref=\u0022ModelRenderer\u0022/\u003E in the open scene that has a baked impostor and no\r\n\t/// \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E one, as a single undo step. Returns how many it added.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static int ApplyToScene()\r\n\t{\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session is null )\r\n\t\t\treturn 0;\r\n\r\n\t\tvar targets = session.Scene.GetAllComponents\u003CModelRenderer\u003E()\r\n\t\t\t.Where( r =\u003E r.Model is not null \u0026\u0026 !r.Components.Get\u003CFlatpackRenderer\u003E().IsValid() )\r\n\t\t\t.Select( r =\u003E (Renderer: r, Impostor: FlatpackRenderer.FindImpostorFor( r.Model )) )\r\n\t\t\t.Where( x =\u003E x.Impostor is not null )\r\n\t\t\t.ToArray();\r\n\r\n\t\tif ( targets.Length == 0 )\r\n\t\t\treturn 0;\r\n\r\n\t\tusing var scene = session.Scene.Push();\r\n\t\tusing ( session.UndoScope( \u0022Apply Flatpack to Scene\u0022 ).WithComponentCreations().Push() )\r\n\t\t{\r\n\t\t\tforeach ( var (renderer, impostor) in targets )\r\n\t\t\t{\r\n\t\t\t\tvar flatpack = renderer.GameObject.Components.Create\u003CFlatpackRenderer\u003E();\r\n\t\t\t\tflatpack.Source = renderer;\r\n\t\t\t\tflatpack.Impostor = impostor;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn targets.Length;\r\n\t}\r\n\r\n\tasync void OnBakeClicked()\r\n\t{\r\n\t\tif ( IsBaking )\r\n\t\t{\r\n\t\t\t_cancel.Cancel();\r\n\t\t\t_bake.Enabled = false;\r\n\t\t\t_status.Text = \u0022Cancelling...\u0022;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tawait BakeQueueAsync();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Bake everything queued. Returns the impostors that baked, in queue order.\r\n\t/// \u003C/summary\u003E\r\n\tpublic async Task\u003CList\u003CFlatpackImpostor\u003E\u003E BakeQueueAsync()\r\n\t{\r\n\t\tvar results = new List\u003CFlatpackImpostor\u003E();\r\n\t\tvar models = _queue.Models.Where( m =\u003E m is not null \u0026\u0026 !m.IsError ).Distinct().ToArray();\r\n\t\tif ( models.Length == 0 )\r\n\t\t{\r\n\t\t\t_status.Text = \u0022Nothing to bake. Add a model first.\u0022;\r\n\t\t\treturn results;\r\n\t\t}\r\n\r\n\t\t_cancel = new CancellationTokenSource();\r\n\t\t_bake.Text = \u0022Cancel\u0022;\r\n\t\t_bake.Icon = \u0022close\u0022;\r\n\t\t_bar.Visible = true;\r\n\r\n\t\tusing var toast = Editor.Application.Editor.ProgressSection();\r\n\t\ttoast.Title = \u0022Baking impostors\u0022;\r\n\t\ttoast.TotalCount = models.Length;\r\n\r\n\t\tvar failed = 0;\r\n\t\ttry\r\n\t\t{\r\n\t\t\tfor ( var i = 0; i \u003C models.Length; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar model = models[i];\r\n\t\t\t\tvar index = i;\r\n\r\n\t\t\t\tvoid Report( float fraction, string text )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( !IsValid )\r\n\t\t\t\t\t\treturn;\r\n\r\n\t\t\t\t\t_bar.Fraction = (index \u002B fraction) / models.Length;\r\n\t\t\t\t\t_status.Text = $\u0022{model.ResourceName} ({index \u002B 1} of {models.Length}): {text}\u0022;\r\n\t\t\t\t}\r\n\r\n\t\t\t\ttry\r\n\t\t\t\t{\r\n\t\t\t\t\ttoast.Current = i;\r\n\t\t\t\t\ttoast.Subtitle = model.ResourceName;\r\n\r\n\t\t\t\t\tvar result = await FlatpackBaker.BakeAsync( model, _settings, Report, _cancel.Token );\r\n\t\t\t\t\tresults.Add( result.Impostor );\r\n\t\t\t\t\tLog.Info( $\u0022Flatpack: baked {model.ResourcePath} to {result.ImpostorPath} in {result.Duration.TotalSeconds:0.0} s\u0022 );\r\n\t\t\t\t}\r\n\t\t\t\tcatch ( OperationCanceledException )\r\n\t\t\t\t{\r\n\t\t\t\t\tthrow;\r\n\t\t\t\t}\r\n\t\t\t\tcatch ( Exception e )\r\n\t\t\t\t{\r\n\t\t\t\t\tfailed\u002B\u002B;\r\n\t\t\t\t\tLog.Warning( $\u0022Flatpack: couldn\u0027t bake {model.ResourcePath}: {e.Message}\u0022 );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( toast.GetCancel().IsCancellationRequested )\r\n\t\t\t\t\t_cancel.Cancel();\r\n\t\t\t}\r\n\r\n\t\t\t_status.Text = failed == 0\r\n\t\t\t\t? $\u0022Baked {results.Count} impostor{(results.Count == 1 ? \u0022\u0022 : \u0022s\u0022)}.\u0022\r\n\t\t\t\t: $\u0022Baked {results.Count}, {failed} failed. See the console.\u0022;\r\n\t\t}\r\n\t\tcatch ( OperationCanceledException )\r\n\t\t{\r\n\t\t\t_status.Text = $\u0022Cancelled after {results.Count} of {models.Length}.\u0022;\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t_cancel = null;\r\n\r\n\t\t\tif ( IsValid )\r\n\t\t\t{\r\n\t\t\t\t_bake.Text = \u0022Bake\u0022;\r\n\t\t\t\t_bake.Icon = \u0022view_in_ar\u0022;\r\n\t\t\t\t_bake.Enabled = true;\r\n\t\t\t\t_bar.Visible = false;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn results;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A thin progress bar.\r\n\t/// \u003C/summary\u003E\r\n\tsealed class ProgressBar : Widget\r\n\t{\r\n\t\tfloat _fraction;\r\n\r\n\t\tpublic float Fraction\r\n\t\t{\r\n\t\t\tget =\u003E _fraction;\r\n\t\t\tset\r\n\t\t\t{\r\n\t\t\t\t_fraction = Math.Clamp( value, 0.0f, 1.0f );\r\n\t\t\t\tUpdate();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tpublic ProgressBar( Widget parent ) : base( parent ) { }\r\n\r\n\t\tprotected override void OnPaint()\r\n\t\t{\r\n\t\t\tPaint.SetPen( Theme.ControlBackground, 1.0f );\r\n\t\t\tPaint.SetBrush( Theme.WidgetBackground.Darken( 0.1f ) );\r\n\t\t\tPaint.DrawRect( LocalRect, 2.0f );\r\n\r\n\t\t\tif ( _fraction \u003C= 0.0f )\r\n\t\t\t\treturn;\r\n\r\n\t\t\tvar filled = LocalRect.Shrink( 1 );\r\n\t\t\tfilled.Width *= _fraction;\r\n\r\n\t\t\tPaint.ClearPen();\r\n\t\t\tPaint.SetBrush( Theme.Primary );\r\n\t\t\tPaint.DrawRect( filled, 2.0f );\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"anakin.flatpack","Path":"Editor/FlatpackQualityCheck.cs","FileName":"FlatpackQualityCheck.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Sandbox;\n\nnamespace Flatpack.EditorTools;\n\n/// \u003Csummary\u003E\n/// Measures how closely a bake matches its model: renders both in a private scene from a few angles at a range of\n/// distances and counts the pixels that differ clearly. The same test as the iteration 3 to 5 benchmarks, cut down to\n/// run in a second or two after every bake.\n/// \u003C/summary\u003E\n/// \u003Cremarks\u003E\n/// Both are rendered through the Albedo debug view, so lighting can\u0027t hide or invent differences: what\u0027s left is shape\n/// (missing thin parts, ghosting, holes) and colour, the failures that matter most for a card. Renders are made at the\n/// pixel density of a 1080p screen at 60 degrees FOV, at twice the resolution and averaged down, like the benchmarks.\n/// \u003C/remarks\u003E\npublic static class FlatpackQualityCheck\n{\n\t/// \u003Csummary\u003EA pixel counts as clearly different past this much average channel difference (of 255).\u003C/summary\u003E\n\tpublic const int BadPixelThreshold = 25;\n\n\t/// \u003Csummary\u003E\n\t/// A bake holds up at a distance when its structural difference from the model (DSSIM, 0..1) is at most this much\n\t/// above its own floor: the difference that\u0027s left far away, where nothing more can be gained (lighting, texture\n\t/// noise). Calibrated by eye in iteration 5: the chair looked right from 6 radii, the tree from 6 to 8, the knight\n\t/// from 8. 0.10 reproduces all three; a fixed cut-off on any single score, or a ratio to the floor, didn\u0027t.\n\t/// \u003C/summary\u003E\n\tpublic const float HoldsUpExcess = 0.10f;\n\n\t/// \u003Csummary\u003E\n\t/// Whatever the structure score says, more than this share of clearly wrong pixels (a wire mesh filled in solid, a\n\t/// missing part) doesn\u0027t hold up.\n\t/// \u003C/summary\u003E\n\tpublic const float HoldsUpBadShare = 0.25f;\n\n\t/// \u003Csummary\u003EDistances checked, in bounding radii, nearest first.\u003C/summary\u003E\n\tpublic static readonly float[] Distances = { 1.5f, 2f, 3f, 4f, 6f, 8f, 12f, 16f, 24f, 30f };\n\n\t/// \u003Csummary\u003EViewing directions averaged at each distance: four sides, a little above.\u003C/summary\u003E\n\tstatic readonly Angles[] Views = { new( 12, 35, 0 ), new( 12, 125, 0 ), new( 12, 215, 0 ), new( 12, 305, 0 ) };\n\n\tpublic sealed class Report\n\t{\n\t\t/// \u003Csummary\u003EShare of the object\u0027s pixels that differ clearly from the model, per entry of \u003Csee cref=\u0022Distances\u0022/\u003E.\u003C/summary\u003E\n\t\tpublic float[] BadShare { get; init; }\n\n\t\t/// \u003Csummary\u003E\n\t\t/// Structural difference from the model (1 - SSIM on luminance, 0..1) per entry of \u003Csee cref=\u0022Distances\u0022/\u003E. Unlike\n\t\t/// \u003Csee cref=\u0022BadShare\u0022/\u003E this sees blur and smearing, which is what makes a card look wrong up close.\n\t\t/// \u003C/summary\u003E\n\t\tpublic float[] Difference { get; init; }\n\n\t\t/// \u003Csummary\u003EThe card\u0027s difference far away (median of 12 to 30 radii): the floor \u003Csee cref=\u0022HoldsUpFrom\u0022/\u003E is judged against.\u003C/summary\u003E\n\t\tpublic float Floor { get; init; }\n\n\t\t/// \u003Csummary\u003E\n\t\t/// Nearest distance (bounding radii) from which every checked distance out to 16 radii holds up (see\n\t\t/// \u003Csee cref=\u0022HoldsUpExcess\u0022/\u003E and \u003Csee cref=\u0022HoldsUpBadShare\u0022/\u003E), or \u003Csee cref=\u0022float.PositiveInfinity\u0022/\u003E when it\n\t\t/// never does.\n\t\t/// \u003C/summary\u003E\n\t\tpublic float HoldsUpFrom { get; init; }\n\n\t\t/// \u003Csummary\u003E\n\t\t/// The worst share of clearly different pixels from \u003Cparamref name=\u0022radii\u0022/\u003E outward (to the furthest checked\n\t\t/// distance): what a card swapped in at that distance shows at its worst.\n\t\t/// \u003C/summary\u003E\n\t\tpublic float WorstFrom( float radii )\n\t\t{\n\t\t\tvar worst = BadShareAt( radii );\n\t\t\tfor ( var i = 0; i \u003C Distances.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( Distances[i] \u003E= radii \u0026\u0026 !float.IsNaN( BadShare[i] ) )\n\t\t\t\t\tworst = MathF.Max( worst, BadShare[i] );\n\t\t\t}\n\t\t\treturn worst;\n\t\t}\n\n\t\t/// \u003Csummary\u003EOne line for status bars and the inspector.\u003C/summary\u003E\n\t\t/// \u003Csummary\u003EThe bake\u0027s bounding radius in world units, for showing distances in metres.\u003C/summary\u003E\n\t\tpublic float Radius { get; init; }\n\n\t\tpublic string Summary =\u003E float.IsPositiveInfinity( HoldsUpFrom )\n\t\t\t? $\u0022Doesn\u0027t match the model closely inside 16 radii ({FlatpackMath.FormatMetres( 16.0f * Radius )}): keep the swap distance generous.\u0022\n\t\t\t: $\u0022Looks like the model from {HoldsUpFrom:0.#} bounding radii ({FlatpackMath.FormatMetres( HoldsUpFrom * Radius )}) outward.\u0022;\n\n\t\t/// \u003Csummary\u003EShare of clearly different pixels at \u003Cparamref name=\u0022radii\u0022/\u003E, interpolated between checked distances.\u003C/summary\u003E\n\t\tpublic float BadShareAt( float radii )\n\t\t{\n\t\t\tif ( radii \u003C= Distances[0] ) return BadShare[0];\n\t\t\tfor ( var i = 1; i \u003C Distances.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( radii \u003C= Distances[i] )\n\t\t\t\t{\n\t\t\t\t\tvar t = (radii - Distances[i - 1]) / (Distances[i] - Distances[i - 1]);\n\t\t\t\t\treturn BadShare[i - 1] \u002B (BadShare[i] - BadShare[i - 1]) * t;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn BadShare[^1];\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// For see-through bakes: find the Soft Coverage strength and growth that make the card cover as much of the screen as\n\t/// the real model does at 8, 16 and 32 bounding radii (where cards show), from two sides, at the pixel density of a\n\t/// 1080p game view. Both go through the normal lit path (the debug views change how alpha-tested meshes resolve) onto\n\t/// a white background in an unlit scene, so a covered pixel is a dark one. Leaves the best values set on the\n\t/// impostor\u0027s material and returns them.\n\t/// \u003C/summary\u003E\n\tpublic static async Task\u003C(float Soft, float Growth)\u003E FitSoftCoverageAsync( FlatpackImpostor impostor, CancellationToken cancel = default )\n\t{\n\t\tvar strengths = new[] { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.5f, 3.0f, 4.0f };\n\t\tvar growths = new[] { 0.0f, 0.25f, 0.5f, 0.75f, 1.0f };\n\t\tvar distances = new[] { 8f, 16f, 32f };\n\t\tvar views = new[] { Views[0], Views[2] };\n\t\tvar material = impostor.Material;\n\n\t\tvar scene = Scene.CreateEditorScene();\n\t\tscene.Name = \u0022Flatpack Coverage Fit\u0022;\n\t\ttry\n\t\t{\n\t\t\tModelRenderer model, card;\n\t\t\tCameraComponent camera;\n\t\t\tusing ( scene.Push() )\n\t\t\t{\n\t\t\t\tmodel = new GameObject( true, \u0022model\u0022 ).Components.Create\u003CModelRenderer\u003E();\n\t\t\t\tmodel.Model = impostor.SourceModel;\n\t\t\t\tmodel.LodOverride = 0;\n\t\t\t\tcard = new GameObject( true, \u0022card\u0022 ).Components.Create\u003CModelRenderer\u003E();\n\t\t\t\tcard.Model = impostor.CardModel;\n\t\t\t\tcamera = new GameObject( true, \u0022camera\u0022 ).Components.Create\u003CCameraComponent\u003E();\n\t\t\t\tcamera.BackgroundColor = Color.White;\n\t\t\t}\n\n\t\t\tawait WarmUpAsync( scene, camera, card, impostor );\n\n\t\t\t// Each shot: framing for one distance and side, and the model\u0027s covered pixel count there.\n\t\t\tvar shots = new List\u003C(Vector3 Position, Rotation Rotation, float Fov, Bitmap Target, long Model)\u003E();\n\t\t\tforeach ( var k in distances )\n\t\t\t{\n\t\t\t\tvar halfAngle = MathF.Atan( 1.15f / k );\n\t\t\t\t// 1920 pixels across a 70 degree view: about 1371 pixels per unit of tangent.\n\t\t\t\tvar size = Math.Clamp( (int)(1371.0f * 2.0f * MathF.Tan( halfAngle )), 24, 1024 );\n\t\t\t\tforeach ( var view in views )\n\t\t\t\t{\n\t\t\t\t\tvar dir = view.ToRotation().Backward;\n\t\t\t\t\tvar shot = ( impostor.Center \u002B dir * impostor.Radius * k, Rotation.LookAt( -dir, Vector3.Up ), halfAngle * 2.0f * (180.0f / MathF.PI), new Bitmap( size, size, false ), 0L );\n\t\t\t\t\tshot.Item5 = Covered( await Render( scene, camera, model, card, impostor, shot.Item1, shot.Item2, shot.Item3, shot.Item4, false ) );\n\t\t\t\t\tshots.Add( shot );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar best = (Soft: 1.0f, Growth: 1.0f, Error: double.MaxValue);\n\t\t\tforeach ( var soft in strengths )\n\t\t\t{\n\t\t\t\tforeach ( var growth in growths )\n\t\t\t\t{\n\t\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\t\tmaterial.Set( \u0022g_flFlatpackSoftCoverage\u0022, soft );\n\t\t\t\t\tmaterial.Set( \u0022g_flFlatpackSoftGrowth\u0022, growth );\n\t\t\t\t\tdouble error = 0;\n\t\t\t\t\tforeach ( var shot in shots )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar c = Covered( await Render( scene, camera, model, card, impostor, shot.Position, shot.Rotation, shot.Fov, shot.Target, true ) );\n\t\t\t\t\t\tvar rel = (c - shot.Model) / (double)Math.Max( shot.Model, 1 );\n\t\t\t\t\t\terror \u002B= rel * rel;\n\t\t\t\t\t}\n\t\t\t\t\tif ( error \u003C best.Error )\n\t\t\t\t\t\tbest = (soft, growth, error);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmaterial.Set( \u0022g_flFlatpackSoftCoverage\u0022, best.Soft );\n\t\t\tmaterial.Set( \u0022g_flFlatpackSoftGrowth\u0022, best.Growth );\n\t\t\treturn (best.Soft, best.Growth);\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\tscene.Destroy();\n\t\t}\n\t}\n\n\tstatic async Task\u003CColor32[]\u003E Render( Scene scene, CameraComponent camera, ModelRenderer model, ModelRenderer card, FlatpackImpostor impostor,\n\t\tVector3 position, Rotation rotation, float fov, Bitmap target, bool showCard )\n\t{\n\t\tusing ( scene.Push() )\n\t\t{\n\t\t\tcamera.FieldOfView = fov;\n\t\t\tcamera.WorldPosition = position;\n\t\t\tcamera.WorldRotation = rotation;\n\t\t\tvar d = (position - impostor.Center).Length;\n\t\t\tcamera.ZNear = MathF.Max( 0.1f, (d - impostor.Radius * 1.2f) * 0.5f );\n\t\t\tcamera.ZFar = d \u002B impostor.Radius * 2.0f;\n\t\t\timpostor.MarkAtlasesUsed();\n\t\t\tmodel.Enabled = !showCard;\n\t\t\tcard.Enabled = showCard;\n\t\t\tscene.EditorTick( 0.0f, 0.0f );\n\t\t\tcamera.RenderToBitmap( target );\n\t\t}\n\t\tawait Task.Delay( 1 );\n\t\treturn target.GetPixels32();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Render the card a few times, a frame apart, before anything is measured. Right after a bake into a new folder the\n\t/// new textures aren\u0027t on the GPU yet and the first renders read blank ones: in iteration 8 a chair measured from 12\n\t/// radii straight after its first bake, and from 4 once its textures had loaded.\n\t/// \u003C/summary\u003E\n\tstatic async Task WarmUpAsync( Scene scene, CameraComponent camera, ModelRenderer card, FlatpackImpostor impostor )\n\t{\n\t\tvar target = new Bitmap( 64, 64, false );\n\t\tvar dir = new Vector3( 1.0f, 0.0f, 0.3f ).Normal;\n\t\tfor ( var i = 0; i \u003C 20; i\u002B\u002B )\n\t\t{\n\t\t\tusing ( scene.Push() )\n\t\t\t{\n\t\t\t\tcamera.FieldOfView = 40.0f;\n\t\t\t\tcamera.WorldPosition = impostor.Center \u002B dir * impostor.Radius * 4.0f;\n\t\t\t\tcamera.WorldRotation = Rotation.LookAt( -dir, Vector3.Up );\n\t\t\t\timpostor.MarkAtlasesUsed();\n\t\t\t\tcard.Enabled = true;\n\t\t\t\tscene.EditorTick( 0.0f, 0.0f );\n\t\t\t\tcamera.RenderToBitmap( target );\n\t\t\t}\n\t\t\tawait Task.Delay( 30 );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EPixels clearly darker than the white background.\u003C/summary\u003E\n\tstatic long Covered( Color32[] pixels )\n\t{\n\t\tlong n = 0;\n\t\tforeach ( var p in pixels )\n\t\t\tif ( p.r \u002B p.g \u002B p.b \u003C 600 ) n\u002B\u002B;\n\t\treturn n;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Compare \u003Cparamref name=\u0022impostor\u0022/\u003E against its source model. \u003Cparamref name=\u0022upTo\u0022/\u003E stops early, for a quick check\n\t/// at one distance (it still checks the distances below it).\n\t/// \u003C/summary\u003E\n\tpublic static async Task\u003CReport\u003E MeasureAsync( FlatpackImpostor impostor, float upTo = float.MaxValue, CancellationToken cancel = default )\n\t{\n\t\tvar scene = Scene.CreateEditorScene();\n\t\tscene.Name = \u0022Flatpack Quality Check\u0022;\n\t\tvar bad = new List\u003Cfloat\u003E();\n\t\tvar structure = new List\u003Cfloat\u003E();\n\n\t\ttry\n\t\t{\n\t\t\tModelRenderer model, card;\n\t\t\tCameraComponent camera;\n\n\t\t\tusing ( scene.Push() )\n\t\t\t{\n\t\t\t\tvar a = new GameObject( true, \u0022model\u0022 );\n\t\t\t\tmodel = a.Components.Create\u003CModelRenderer\u003E();\n\t\t\t\tmodel.Model = impostor.SourceModel;\n\t\t\t\tmodel.LodOverride = 0;\n\n\t\t\t\tvar b = new GameObject( true, \u0022card\u0022 );\n\t\t\t\tcard = b.Components.Create\u003CModelRenderer\u003E();\n\t\t\t\tcard.Model = impostor.CardModel;\n\n\t\t\t\tvar eye = new GameObject( true, \u0022camera\u0022 );\n\t\t\t\tcamera = eye.Components.Create\u003CCameraComponent\u003E();\n\t\t\t\tcamera.BackgroundColor = Color.Transparent;\n\t\t\t\tcamera.DebugMode = SceneCameraDebugMode.Albedo;\n\t\t\t}\n\n\t\t\tawait WarmUpAsync( scene, camera, card, impostor );\n\n\t\t\tvar r = impostor.Radius;\n\t\t\tforeach ( var k in Distances )\n\t\t\t{\n\t\t\t\tif ( k \u003E upTo \u0026\u0026 bad.Count \u003E 0 )\n\t\t\t\t\tbreak;\n\n\t\t\t\t// Frame the bounding sphere with a little margin, at the pixel density a 1080p, 60 degree view would give.\n\t\t\t\tvar halfAngle = MathF.Atan( 1.15f / k );\n\t\t\t\tvar size = Math.Clamp( (int)(1080.0f * MathF.Tan( halfAngle ) / MathF.Tan( MathF.PI / 6.0f )), 24, 1024 );\n\t\t\t\tvar target = new Bitmap( size * 2, size * 2, false );\n\n\t\t\t\tlong badPixels = 0, objectPixels = 0;\n\t\t\t\tdouble dssimSum = 0;\n\t\t\t\tlong dssimCount = 0;\n\t\t\t\tforeach ( var view in Views )\n\t\t\t\t{\n\t\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\t\tvar dir = view.ToRotation().Backward;\n\n\t\t\t\t\tColor32[] m, c;\n\t\t\t\t\tusing ( scene.Push() )\n\t\t\t\t\t{\n\t\t\t\t\t\tcamera.FieldOfView = halfAngle * 2.0f * (180.0f / MathF.PI);\n\t\t\t\t\t\tcamera.WorldPosition = impostor.Center \u002B dir * r * k;\n\t\t\t\t\t\tcamera.WorldRotation = Rotation.LookAt( -dir, Vector3.Up );\n\t\t\t\t\t\tcamera.ZNear = MathF.Max( 0.1f, r * (k - 1.2f) * 0.5f );\n\t\t\t\t\t\tcamera.ZFar = r * (k \u002B 2.0f);\n\n\t\t\t\t\t\timpostor.MarkAtlasesUsed();\n\t\t\t\t\t\tmodel.Enabled = true; card.Enabled = false;\n\t\t\t\t\t\tscene.EditorTick( 0.0f, 0.0f );\n\t\t\t\t\t\tcamera.RenderToBitmap( target );\n\t\t\t\t\t\tm = target.GetPixels32();\n\n\t\t\t\t\t\tmodel.Enabled = false; card.Enabled = true;\n\t\t\t\t\t\tscene.EditorTick( 0.0f, 0.0f );\n\t\t\t\t\t\tcamera.RenderToBitmap( target );\n\t\t\t\t\t\tc = target.GetPixels32();\n\t\t\t\t\t}\n\n\t\t\t\t\tCompare( m, c, size * 2, ref badPixels, ref objectPixels );\n\t\t\t\t\tStructure( m, c, size * 2, ref dssimSum, ref dssimCount );\n\t\t\t\t\tawait Task.Delay( 1 );\n\t\t\t\t}\n\n\t\t\t\tbad.Add( objectPixels \u003E 0 ? badPixels / (float)objectPixels : 0.0f );\n\t\t\t\tstructure.Add( dssimCount \u003E 0 ? (float)(dssimSum / dssimCount) : 0.0f );\n\t\t\t}\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\tscene.Destroy();\n\t\t}\n\n\t\twhile ( bad.Count \u003C Distances.Length )\n\t\t{\n\t\t\tbad.Add( float.NaN );\n\t\t\tstructure.Add( float.NaN );\n\t\t}\n\n\t\tvar far = new List\u003Cfloat\u003E();\n\t\tfor ( var i = 0; i \u003C Distances.Length; i\u002B\u002B )\n\t\t\tif ( Distances[i] \u003E= 12.0f \u0026\u0026 !float.IsNaN( structure[i] ) )\n\t\t\t\tfar.Add( structure[i] );\n\t\tfar.Sort();\n\t\tvar floor = far.Count \u003E 0 ? far[far.Count / 2] : float.NaN;\n\n\t\tvar holds = float.PositiveInfinity;\n\t\tfor ( var i = Distances.Length - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tif ( Distances[i] \u003E 16.0f ) continue;\n\t\t\tif ( float.IsNaN( bad[i] ) || float.IsNaN( floor ) || bad[i] \u003E HoldsUpBadShare || structure[i] \u003E floor \u002B HoldsUpExcess ) break;\n\t\t\tholds = Distances[i];\n\t\t}\n\n\t\treturn new Report { BadShare = bad.ToArray(), Difference = structure.ToArray(), Floor = floor, HoldsUpFrom = holds, Radius = impostor.Radius };\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Average 2 x 2 blocks of both renders (a stand-in for anti-aliasing), then count object pixels that differ clearly:\n\t/// \u003Clist type=\u0022bullet\u0022\u003E\n\t/// \u003Citem\u003EShape: coverage blurred over 3 x 3 pixels differs by more than a quarter. Blurring forgives a thin wire\n\t/// drawn one pixel over, but not a part that\u0027s missing or a see-through mesh that filled in solid.\u003C/item\u003E\n\t/// \u003Citem\u003EColour: both cover the pixel and the colours differ by more than \u003Csee cref=\u0022BadPixelThreshold\u0022/\u003E.\u003C/item\u003E\n\t/// \u003C/list\u003E\n\t/// A pixel belongs to the object when either render covers more than half of it.\n\t/// \u003C/summary\u003E\n\tstatic void Compare( Color32[] m, Color32[] c, int size, ref long badPixels, ref long objectPixels )\n\t{\n\t\tvar half = size / 2;\n\t\tvar count = half * half;\n\t\tvar mCover = new float[count];\n\t\tvar cCover = new float[count];\n\t\tvar mRgb = new int[count * 3];\n\t\tvar cRgb = new int[count * 3];\n\n\t\tfor ( var y = 0; y \u003C half; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( var x = 0; x \u003C half; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar o = y * half \u002B x;\n\t\t\t\tfor ( var dy = 0; dy \u003C 2; dy\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tfor ( var dx = 0; dx \u003C 2; dx\u002B\u002B )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar i = (y * 2 \u002B dy) * size \u002B x * 2 \u002B dx;\n\t\t\t\t\t\tvar pm = m[i]; var pc = c[i];\n\t\t\t\t\t\tmCover[o] \u002B= pm.a / (4.0f * 255.0f);\n\t\t\t\t\t\tcCover[o] \u002B= pc.a / (4.0f * 255.0f);\n\t\t\t\t\t\tmRgb[o * 3] \u002B= pm.r; mRgb[o * 3 \u002B 1] \u002B= pm.g; mRgb[o * 3 \u002B 2] \u002B= pm.b;\n\t\t\t\t\t\tcRgb[o * 3] \u002B= pc.r; cRgb[o * 3 \u002B 1] \u002B= pc.g; cRgb[o * 3 \u002B 2] \u002B= pc.b;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor ( var y = 0; y \u003C half; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( var x = 0; x \u003C half; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar o = y * half \u002B x;\n\t\t\t\tif ( mCover[o] \u003C= 0.5f \u0026\u0026 cCover[o] \u003C= 0.5f )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tobjectPixels\u002B\u002B;\n\n\t\t\t\tfloat mBlur = 0, cBlur = 0, n = 0;\n\t\t\t\tfor ( var dy = -1; dy \u003C= 1; dy\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tfor ( var dx = -1; dx \u003C= 1; dx\u002B\u002B )\n\t\t\t\t\t{\n\t\t\t\t\t\tint yy = y \u002B dy, xx = x \u002B dx;\n\t\t\t\t\t\tif ( yy \u003C 0 || yy \u003E= half || xx \u003C 0 || xx \u003E= half ) continue;\n\t\t\t\t\t\tmBlur \u002B= mCover[yy * half \u002B xx];\n\t\t\t\t\t\tcBlur \u002B= cCover[yy * half \u002B xx];\n\t\t\t\t\t\tn\u002B\u002B;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ( MathF.Abs( mBlur - cBlur ) / n \u003E 0.25f )\n\t\t\t\t{\n\t\t\t\t\tbadPixels\u002B\u002B;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif ( mCover[o] \u003E 0.5f \u0026\u0026 cCover[o] \u003E 0.5f )\n\t\t\t\t{\n\t\t\t\t\tvar diff = (Math.Abs( mRgb[o * 3] - cRgb[o * 3] ) \u002B Math.Abs( mRgb[o * 3 \u002B 1] - cRgb[o * 3 \u002B 1] ) \u002B Math.Abs( mRgb[o * 3 \u002B 2] - cRgb[o * 3 \u002B 2] )) / 12.0f;\n\t\t\t\t\tif ( diff \u003E BadPixelThreshold )\n\t\t\t\t\t\tbadPixels\u002B\u002B;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Structural difference (1 - SSIM, Wang et al. 2004) between the two renders\u0027 luminance, Gaussian window of 1.5\n\t/// pixels, at half resolution like \u003Csee cref=\u0022Compare\u0022/\u003E. Summed over pixels near the object (either render covers\n\t/// something within a few pixels); uncovered pixels count as the benchmarks\u0027 mid grey.\n\t/// \u003C/summary\u003E\n\tstatic void Structure( Color32[] m, Color32[] c, int size, ref double sum, ref long count )\n\t{\n\t\tvar half = size / 2;\n\t\tvar n = half * half;\n\t\tvar a = new float[n];\n\t\tvar b = new float[n];\n\t\tvar cover = new float[n];\n\n\t\tfor ( var y = 0; y \u003C half; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( var x = 0; x \u003C half; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tfloat la = 0, lb = 0, cv = 0;\n\t\t\t\tfor ( var dy = 0; dy \u003C 2; dy\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tfor ( var dx = 0; dx \u003C 2; dx\u002B\u002B )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar i = (y * 2 \u002B dy) * size \u002B x * 2 \u002B dx;\n\t\t\t\t\t\tla \u002B= Luma( m[i] );\n\t\t\t\t\t\tlb \u002B= Luma( c[i] );\n\t\t\t\t\t\tcv \u002B= Math.Max( m[i].a, c[i].a ) / 255.0f;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tvar o = y * half \u002B x;\n\t\t\t\ta[o] = la * 0.25f;\n\t\t\t\tb[o] = lb * 0.25f;\n\t\t\t\tcover[o] = cv * 0.25f;\n\t\t\t}\n\t\t}\n\n\t\tvar muA = Blur( a, half );\n\t\tvar muB = Blur( b, half );\n\t\tvar aa = new float[n];\n\t\tvar bb = new float[n];\n\t\tvar ab = new float[n];\n\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B )\n\t\t{\n\t\t\taa[i] = a[i] * a[i];\n\t\t\tbb[i] = b[i] * b[i];\n\t\t\tab[i] = a[i] * b[i];\n\t\t}\n\n\t\tvar sAA = Blur( aa, half );\n\t\tvar sBB = Blur( bb, half );\n\t\tvar sAB = Blur( ab, half );\n\t\tvar near = Blur( cover, half );\n\n\t\tconst float c1 = (0.01f * 255.0f) * (0.01f * 255.0f);\n\t\tconst float c2 = (0.03f * 255.0f) * (0.03f * 255.0f);\n\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B )\n\t\t{\n\t\t\tif ( near[i] \u003C 0.02f )\n\t\t\t\tcontinue;\n\n\t\t\tvar va = sAA[i] - muA[i] * muA[i];\n\t\t\tvar vb = sBB[i] - muB[i] * muB[i];\n\t\t\tvar cov = sAB[i] - muA[i] * muB[i];\n\t\t\tvar ssim = ((2 * muA[i] * muB[i] \u002B c1) * (2 * cov \u002B c2)) / ((muA[i] * muA[i] \u002B muB[i] * muB[i] \u002B c1) * (va \u002B vb \u002B c2));\n\t\t\tsum \u002B= 1.0 - ssim;\n\t\t\tcount\u002B\u002B;\n\t\t}\n\t}\n\n\tstatic float Luma( Color32 p ) =\u003E p.a \u003E 127 ? p.r * 0.2126f \u002B p.g * 0.7152f \u002B p.b * 0.0722f : 128.0f;\n\n\tstatic readonly float[] Kernel = MakeKernel( 1.5f, 4 );\n\n\tstatic float[] MakeKernel( float sigma, int radius )\n\t{\n\t\tvar k = new float[radius * 2 \u002B 1];\n\t\tvar total = 0.0f;\n\t\tfor ( var i = -radius; i \u003C= radius; i\u002B\u002B )\n\t\t{\n\t\t\tk[i \u002B radius] = MathF.Exp( -i * i / (2 * sigma * sigma) );\n\t\t\ttotal \u002B= k[i \u002B radius];\n\t\t}\n\n\t\tfor ( var i = 0; i \u003C k.Length; i\u002B\u002B )\n\t\t\tk[i] /= total;\n\t\treturn k;\n\t}\n\n\t/// \u003Csummary\u003ESeparable Gaussian blur of a square image, clamped at the edges.\u003C/summary\u003E\n\tstatic float[] Blur( float[] src, int w )\n\t{\n\t\tvar r = Kernel.Length / 2;\n\t\tvar tmp = new float[src.Length];\n\t\tvar dst = new float[src.Length];\n\t\tfor ( var y = 0; y \u003C w; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( var x = 0; x \u003C w; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tfloat v = 0;\n\t\t\t\tfor ( var k = -r; k \u003C= r; k\u002B\u002B )\n\t\t\t\t\tv \u002B= src[y * w \u002B Math.Clamp( x \u002B k, 0, w - 1 )] * Kernel[k \u002B r];\n\t\t\t\ttmp[y * w \u002B x] = v;\n\t\t\t}\n\t\t}\n\n\t\tfor ( var y = 0; y \u003C w; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( var x = 0; x \u003C w; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tfloat v = 0;\n\t\t\t\tfor ( var k = -r; k \u003C= r; k\u002B\u002B )\n\t\t\t\t\tv \u002B= tmp[Math.Clamp( y \u002B k, 0, w - 1 ) * w \u002B x] * Kernel[k \u002B r];\n\t\t\t\tdst[y * w \u002B x] = v;\n\t\t\t}\n\t\t}\n\n\t\treturn dst;\n\t}\n}\n"},{"Ident":"anakin.flatpack","Path":"Code/FlatpackRenderer.cs","FileName":"FlatpackRenderer.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace Flatpack;\r\n\r\n/// \u003Csummary\u003E\r\n/// One mesh LOD in a \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E chain: a model and the camera distance it takes over from.\r\n/// \u003C/summary\u003E\r\npublic sealed class FlatpackLodLevel\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// The model to show in this band. Usually a lower-poly version of the source.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public Model Model { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Camera distance where this level replaces the one before it, in world units.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 0, 20000 )] public float Distance { get; set; } = 1000.0f;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Shows a model up close and its Flatpack impostor card far away.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\r\n/// Put this next to a \u003Csee cref=\u0022ModelRenderer\u0022/\u003E and assign a baked \u003Csee cref=\u0022FlatpackImpostor\u0022/\u003E. The renderer\r\n/// doesn\u0027t tick on its own: \u003Csee cref=\u0022FlatpackSystem\u0022/\u003E checks distances for every renderer in one batched,\r\n/// time-sliced pass, so thousands of instances cost one loop instead of thousands of updates.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// All cards made from the same impostor share one model and material, so the engine can instance them.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// LODs: the source \u003Cc\u003E.vmdl\u003C/c\u003E\u0027s own LODs keep working up close, because this component never touches the\r\n/// \u003Csee cref=\u0022ModelRenderer\u0022/\u003E until the swap. For separate model files per detail level, fill \u003Csee cref=\u0022Levels\u0022/\u003E:\r\n/// the renderer swaps the source\u0027s model by distance (with the same hysteresis), then hands over to the card past\r\n/// the swap distance. With no \u003Csee cref=\u0022ModelRenderer\u0022/\u003E at all the card shows at every distance.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// Alternative without this component: make the card the last LOD of the \u003Cc\u003E.vmdl\u003C/c\u003E (see \u003Csee cref=\u0022FlatpackImpostor\u0022/\u003E).\r\n/// That gives you engine LOD switching for free, but no hysteresis and no cross-fade.\r\n/// \u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\n[Title( \u0022Flatpack Renderer\u0022 ), Category( \u0022Rendering\u0022 ), Icon( \u0022view_in_ar\u0022 )]\r\npublic sealed class FlatpackRenderer : Component, Component.ExecuteInEditor\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// What to draw.\r\n\t/// \u003C/summary\u003E\r\n\tpublic enum DisplayMode\r\n\t{\r\n\t\t/// \u003Csummary\u003EModel inside \u003Csee cref=\u0022EffectiveSwapDistance\u0022/\u003E, card beyond it.\u003C/summary\u003E\r\n\t\tAuto,\r\n\t\t/// \u003Csummary\u003EAlways the real model.\u003C/summary\u003E\r\n\t\tForceModel,\r\n\t\t/// \u003Csummary\u003EAlways the card. Useful for checking a bake.\u003C/summary\u003E\r\n\t\tForceCard,\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The baked impostor to show at distance. Left empty, the renderer uses any baked impostor whose source model\r\n\t/// matches the \u003Csee cref=\u0022Source\u0022/\u003E renderer\u0027s model, so adding the component after a bake needs no setup.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public FlatpackImpostor Impostor { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The renderer for the real model. Found on this GameObject if left empty.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public ModelRenderer Source { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Pick the swap distance from the object\u0027s size: the card takes over once the bounding sphere is about\r\n\t/// 1/\u003Csee cref=\u0022AutoDistanceFactor\u0022/\u003E of the distance across, roughly 5% of a 1080p screen\u0027s height at 70 degrees FOV.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public bool AutoSwapDistance { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Auto mode swaps as soon as the card looks like the model: at the distance the baker measured\r\n\t/// (\u003Csee cref=\u0022FlatpackImpostor.HoldsUpFrom\u0022/\u003E, calibrated by eye), never closer than \u003Csee cref=\u0022MinimumSwapRadii\u0022/\u003E.\r\n\t/// Measured in iteration 6: a 534,000-triangle tree forest drew in 7 to 13 ms swapping at 8 radii against 25 to 28 ms\r\n\t/// at 30. Turn it off to swap at \u003Csee cref=\u0022AutoDistanceFactor\u0022/\u003E instead (bakes that weren\u0027t measured always do).\r\n\t/// \u003C/summary\u003E\r\n\t[Property, ShowIf( nameof( AutoSwapDistance ), true )] public bool SwapWhenItLooksRight { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The closest an early swap may happen, in bounding radii. Up close a card covers many pixels and costs more to\r\n\t/// draw, so very light models can end up slower as cards than as models.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 2, 30 ), ShowIf( nameof( SwapWhenItLooksRight ), true )] public float MinimumSwapRadii { get; set; } = 6.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Auto mode swaps at this many bounding-sphere radii from the camera when it isn\u0027t swapping early (or the bake\r\n\t/// wasn\u0027t measured), or where the bake holds up if that\u0027s further.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 5, 100 ), ShowIf( nameof( AutoSwapDistance ), true )] public float AutoDistanceFactor { get; set; } = 30.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Camera distance where the card takes over, in world units, when \u003Csee cref=\u0022AutoSwapDistance\u0022/\u003E is off.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 0, 20000 ), ShowIf( nameof( AutoSwapDistance ), false )] public float SwapDistance { get; set; } = 2000.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The swap distance in use: \u003Csee cref=\u0022SwapDistance\u0022/\u003E, or the automatic one, then scaled by the global\r\n\t/// \u003Csee cref=\u0022FlatpackSystem.Level\u0022/\u003E (a game\u0027s graphics setting).\r\n\t/// \u003C/summary\u003E\r\n\tpublic float EffectiveSwapDistance\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar (levelScale, levelMinimum) = FlatpackSystem.LevelScaling;\r\n\t\t\tif ( float.IsPositiveInfinity( levelScale ) )\r\n\t\t\t\treturn float.PositiveInfinity;\r\n\r\n\t\t\tif ( !AutoSwapDistance || Impostor is null )\r\n\t\t\t\treturn SwapDistance * levelScale;\r\n\r\n\t\t\tvar scale = WorldScale;\r\n\t\t\tvar radius = Impostor.Radius * MathF.Max( scale.x, MathF.Max( scale.y, scale.z ) );\r\n\t\t\tvar measured = Impostor.HoldsUpFrom \u003E 0.0f;\r\n\t\t\tvar factor = SwapWhenItLooksRight \u0026\u0026 measured\r\n\t\t\t\t? MathF.Max( Impostor.HoldsUpFrom, MinimumSwapRadii )\r\n\t\t\t\t: MathF.Max( AutoDistanceFactor, Impostor.HoldsUpFrom );\r\n\r\n\t\t\t// A lower graphics level swaps earlier, but never closer than its own minimum.\r\n\t\t\tfactor *= levelScale;\r\n\t\t\tif ( levelMinimum \u003E 0.0f )\r\n\t\t\t\tfactor = MathF.Max( factor, levelMinimum );\r\n\t\t\treturn radius * factor;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Width of the dead band around \u003Csee cref=\u0022SwapDistance\u0022/\u003E. The card appears past SwapDistance \u002B Hysteresis / 2\r\n\t/// and the model returns inside SwapDistance - Hysteresis / 2, so a camera hovering at the boundary doesn\u0027t flicker.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 0, 2000 )] public float Hysteresis { get; set; } = 200.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Dither the card in and out over \u003Csee cref=\u0022FadeTime\u0022/\u003E instead of popping. The model stays visible under the\r\n\t/// card until the fade-in finishes, so the swap reads as a dissolve.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public bool CrossFade { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Seconds a cross-fade takes.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 0, 2 ), ShowIf( nameof( CrossFade ), true )] public float FadeTime { get; set; } = 0.35f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Override the distance check.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public DisplayMode Mode { get; set; } = DisplayMode.Auto;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Optional mesh LODs between the source model and the card, nearest first. Each replaces the source renderer\u0027s\r\n\t/// model past its \u003Csee cref=\u0022FlatpackLodLevel.Distance\u0022/\u003E. Distances past \u003Csee cref=\u0022SwapDistance\u0022/\u003E never show.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public List\u003CFlatpackLodLevel\u003E Levels { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Index into \u003Csee cref=\u0022Levels\u0022/\u003E currently on the source renderer, or -1 for the source\u0027s own model.\r\n\t/// \u003C/summary\u003E\r\n\tpublic int CurrentLevel { get; private set; } = -1;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// True while the card is the visible representation (including while it fades in).\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool ShowingCard { get; private set; }\r\n\r\n\tSceneObject _card;\r\n\tModel _baseModel;      // the source renderer\u0027s own model, restored when the chain steps back to level -1\r\n\tFlatpackImpostor _countedImpostor;   // impostor this renderer\u0027s visible card is counted against in the system\r\n\tfloat _fade;           // 0 = card hidden, 1 = card fully shown\r\n\tfloat _fadeTarget;\r\n\tinternal int SystemIndex = -1;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tSource ??= GameObject.Components.Get\u003CModelRenderer\u003E();\r\n\t\t_baseModel = Source.IsValid() ? Source.Model : null;\r\n\t\tImpostor ??= FindImpostorFor( _baseModel );\r\n\t\tCurrentLevel = -1;\r\n\t\tTransform.OnTransformChanged \u002B= SyncTransform;\r\n\r\n\t\tScene.GetSystem\u003CFlatpackSystem\u003E()?.Register( this );\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tTransform.OnTransformChanged -= SyncTransform;\r\n\t\tScene.GetSystem\u003CFlatpackSystem\u003E()?.Unregister( this );\r\n\r\n\t\tDestroyCard();\r\n\t\tSetLevel( -1 );\r\n\t\tSetSourceVisible( true );\r\n\t\tShowingCard = false;\r\n\t\t_fade = _fadeTarget = 0.0f;\r\n\t}\r\n\r\n\tprotected override void OnValidate()\r\n\t{\r\n\t\t// An edited impostor or a changed mode should show up straight away in the editor.\r\n\t\tDestroyCard();\r\n\t\tSetLevel( -1 );\r\n\t\tShowingCard = false;\r\n\t\t_fade = _fadeTarget = 0.0f;\r\n\t\tSetSourceVisible( true );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Called by \u003Csee cref=\u0022FlatpackSystem\u0022/\u003E with the squared distance from the camera to this object.\r\n\t/// Decides which representation should be showing; the fade itself runs in \u003Csee cref=\u0022TickFade\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tinternal bool Evaluate( float distanceSquared )\r\n\t{\r\n\t\tUpdateLevel( distanceSquared );\r\n\r\n\t\tvar wantCard = Mode switch\r\n\t\t{\r\n\t\t\tDisplayMode.ForceCard =\u003E true,\r\n\t\t\tDisplayMode.ForceModel =\u003E false,\r\n\t\t\t_ =\u003E WantCardAt( distanceSquared ),\r\n\t\t};\r\n\r\n\t\t// Card-only setup: nothing else to show.\r\n\t\tif ( !Source.IsValid() \u0026\u0026 Mode != DisplayMode.ForceModel )\r\n\t\t\twantCard = true;\r\n\r\n\t\tif ( wantCard \u0026\u0026 Impostor?.Material is null )\r\n\t\t\twantCard = false;\r\n\r\n\t\tif ( wantCard == ShowingCard )\r\n\t\t\treturn false;\r\n\r\n\t\tShowingCard = wantCard;\r\n\t\t_fadeTarget = wantCard ? 1.0f : 0.0f;\r\n\r\n\t\tif ( wantCard )\r\n\t\t\tEnsureCard();\r\n\r\n\t\tif ( !CrossFade || FadeTime \u003C= 0.0f )\r\n\t\t\t_fade = _fadeTarget;\r\n\r\n\t\tApply();\r\n\r\n\t\t// The system keeps calling TickFade while this returns true.\r\n\t\treturn _fade != _fadeTarget;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Step the mesh LOD chain toward the level for this distance, one band edge at a time, each with the same\r\n\t/// hysteresis band as the card swap.\r\n\t/// \u003C/summary\u003E\r\n\tvoid UpdateLevel( float distanceSquared )\r\n\t{\r\n\t\tif ( Levels is null || Levels.Count == 0 || !Source.IsValid() || Mode != DisplayMode.Auto )\r\n\t\t\treturn;\r\n\r\n\t\tvar level = Math.Min( CurrentLevel, Levels.Count - 1 );\r\n\t\tvar half = Hysteresis * 0.5f;\r\n\r\n\t\twhile ( level \u002B 1 \u003C Levels.Count \u0026\u0026 Beyond( distanceSquared, Levels[level \u002B 1].Distance \u002B half ) )\r\n\t\t\tlevel\u002B\u002B;\r\n\r\n\t\twhile ( level \u003E= 0 \u0026\u0026 !Beyond( distanceSquared, Levels[level].Distance - half ) )\r\n\t\t\tlevel--;\r\n\r\n\t\tSetLevel( level );\r\n\t}\r\n\r\n\tstatic bool Beyond( float distanceSquared, float distance )\r\n\t{\r\n\t\tdistance = MathF.Max( distance, 0.0f );\r\n\t\treturn distanceSquared \u003E distance * distance;\r\n\t}\r\n\r\n\tvoid SetLevel( int level )\r\n\t{\r\n\t\tif ( level == CurrentLevel || !Source.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tCurrentLevel = level;\r\n\t\tvar model = level \u003E= 0 ? Levels[level].Model : _baseModel;\r\n\t\tif ( model is not null )\r\n\t\t\tSource.Model = model;\r\n\r\n\t\t// A new model can mean a new scene object, which starts visible.\r\n\t\tSetSourceVisible( !ShowingCard || _fade \u003C 1.0f );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The first baked impostor whose source is \u003Cparamref name=\u0022model\u0022/\u003E, or null.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static FlatpackImpostor FindImpostorFor( Model model )\r\n\t{\r\n\t\tif ( model is null )\r\n\t\t\treturn null;\r\n\r\n\t\treturn ResourceLibrary.GetAll\u003CFlatpackImpostor\u003E()\r\n\t\t\t.FirstOrDefault( x =\u003E x.SourceModel is not null \u0026\u0026 x.SourceModel.ResourcePath == model.ResourcePath );\r\n\t}\r\n\r\n\tbool WantCardAt( float distanceSquared )\r\n\t{\r\n\t\t// Hysteresis: the threshold depends on which side we\u0027re already on.\r\n\t\tvar swap = EffectiveSwapDistance;\r\n\t\tvar threshold = ShowingCard\r\n\t\t\t? swap - Hysteresis * 0.5f\r\n\t\t\t: swap \u002B Hysteresis * 0.5f;\r\n\r\n\t\tthreshold = MathF.Max( threshold, 0.0f );\r\n\t\treturn distanceSquared \u003E threshold * threshold;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Advances a running cross-fade. Returns false when it has finished.\r\n\t/// \u003C/summary\u003E\r\n\tinternal bool TickFade( float delta )\r\n\t{\r\n\t\tvar step = FadeTime \u003E 0.0f ? delta / FadeTime : 1.0f;\r\n\t\t_fade = _fadeTarget \u003E _fade\r\n\t\t\t? MathF.Min( _fade \u002B step, _fadeTarget )\r\n\t\t\t: MathF.Max( _fade - step, _fadeTarget );\r\n\r\n\t\tApply();\r\n\t\treturn _fade != _fadeTarget;\r\n\t}\r\n\r\n\tvoid Apply()\r\n\t{\r\n\t\tif ( _card.IsValid() )\r\n\t\t{\r\n\t\t\t_card.RenderingEnabled = _fade \u003E 0.0f;\r\n\t\t\tCountCard( _fade \u003E 0.0f ? Impostor : null );\r\n\r\n\t\t\t// 1 draws every pixel; anything lower dithers the card out (see flatpack_impostor.shader).\r\n\t\t\t_card.Attributes.Set( \u0022FlatpackFade\u0022, _fade );\r\n\t\t}\r\n\r\n\t\t// The model stays up until the card has fully faded in, and comes back the moment we head toward it.\r\n\t\tSetSourceVisible( !ShowingCard || _fade \u003C 1.0f );\r\n\t}\r\n\r\n\tvoid SetSourceVisible( bool visible )\r\n\t{\r\n\t\tif ( Source.IsValid() \u0026\u0026 Source.SceneObject.IsValid() )\r\n\t\t\tSource.SceneObject.RenderingEnabled = visible \u0026\u0026 Source.Enabled;\r\n\t}\r\n\r\n\tvoid EnsureCard()\r\n\t{\r\n\t\tif ( _card.IsValid() || Impostor is null )\r\n\t\t\treturn;\r\n\r\n\t\t_card = new SceneObject( Scene.SceneWorld, Impostor.CardModel, WorldTransform );\r\n\t\t_card.Tags.SetFrom( GameObject.Tags );\r\n\t\t_card.RenderingEnabled = false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Keep \u003Csee cref=\u0022FlatpackSystem\u0022/\u003E\u0027s count of visible cards per impostor current, so it streams their atlases in.\r\n\t/// \u003C/summary\u003E\r\n\tvoid CountCard( FlatpackImpostor visibleImpostor )\r\n\t{\r\n\t\tif ( visibleImpostor == _countedImpostor )\r\n\t\t\treturn;\r\n\r\n\t\tvar system = Scene?.GetSystem\u003CFlatpackSystem\u003E();\r\n\t\tsystem?.CardVisibilityChanged( _countedImpostor, false );\r\n\t\tsystem?.CardVisibilityChanged( visibleImpostor, true );\r\n\t\t_countedImpostor = visibleImpostor;\r\n\t}\r\n\r\n\tvoid DestroyCard()\r\n\t{\r\n\t\tCountCard( null );\r\n\t\t_card?.Delete();\r\n\t\t_card = null;\r\n\t}\r\n\r\n\tvoid SyncTransform()\r\n\t{\r\n\t\tif ( _card.IsValid() )\r\n\t\t\t_card.Transform = WorldTransform;\r\n\t}\r\n}\r\n"},{"Ident":"anakin.flatpack","Path":"FlatpackCard.cs","FileName":"FlatpackCard.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\r\nusing Sandbox;\r\n\r\nnamespace Flatpack;\r\n\r\n/// \u003Csummary\u003E\r\n/// Builds the card mesh an impostor is drawn with.\r\n/// \u003C/summary\u003E\r\npublic static class FlatpackCard\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// One quad, two triangles, with the impostor material. The vertex shader moves the corners every frame to\r\n\t/// face the camera, using only the UVs; the positions here are a plausible fallback (a quad facing \u002BX through the\r\n\t/// bounds centre) for anything that reads the mesh on the CPU.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Model Build( FlatpackImpostor impostor )\r\n\t{\r\n\t\treturn Model.Builder\r\n\t\t\t.WithName( $\u0022flatpack_card_{impostor.ResourceName}\u0022 )\r\n\t\t\t.AddMesh( BuildMesh( impostor ) )\r\n\t\t\t.Create();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The card quad as a mesh, for adding to another model (the card as the last LOD of a model).\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Mesh BuildMesh( FlatpackImpostor impostor )\r\n\t{\r\n\t\tvar c = impostor.Center;\r\n\t\tvar r = impostor.Radius;\r\n\r\n\t\tvar normal = Vector3.Forward;\r\n\t\tvar tangent = Vector3.Left;\r\n\r\n\t\t// UV (0,0) is the top-left corner; the shader maps u to the card\u0027s right and v to its down.\r\n\t\tvar vertices = new[]\r\n\t\t{\r\n\t\t\tnew Vertex( c \u002B new Vector3( 0, -r, r ), normal, tangent, new Vector4( 0, 0, 0, 0 ) ),\r\n\t\t\tnew Vertex( c \u002B new Vector3( 0, r, r ), normal, tangent, new Vector4( 1, 0, 0, 0 ) ),\r\n\t\t\tnew Vertex( c \u002B new Vector3( 0, r, -r ), normal, tangent, new Vector4( 1, 1, 0, 0 ) ),\r\n\t\t\tnew Vertex( c \u002B new Vector3( 0, -r, -r ), normal, tangent, new Vector4( 0, 1, 0, 0 ) ),\r\n\t\t};\r\n\r\n\t\tvar indices = new[] { 0, 1, 2, 0, 2, 3 };\r\n\r\n\t\tvar mesh = new Mesh( impostor.Material );\r\n\t\tmesh.CreateVertexBuffer( vertices.Length, vertices.AsSpan() );\r\n\t\tmesh.CreateIndexBuffer( indices.Length, indices.AsSpan() );\r\n\r\n\t\t// Culling has to use the whole bounding sphere: the card can be at any angle around it, and the shader\r\n\t\t// places it on the sphere\u0027s front face.\r\n\t\tmesh.Bounds = BBox.FromPositionAndSize( c, r * 2.0f );\r\n\t\treturn mesh;\r\n\t}\r\n}\r\n"},{"Ident":"anakin.flatpack","Path":"FlatpackMath.cs","FileName":"FlatpackMath.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace Flatpack;\r\n\r\n/// \u003Csummary\u003E\r\n/// How the baked views (\u0022frames\u0022) are spread over the directions an impostor can be seen from.\r\n/// \u003C/summary\u003E\r\npublic enum FlatpackLayout\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Hemi-octahedral: the upper half of the sphere. Best for anything that stands on the ground and is never\r\n\t/// seen from below (trees, rocks, props). All N x N frames go to the half you actually see.\r\n\t/// \u003C/summary\u003E\r\n\tHemisphere = 0,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Full octahedral: every direction. For floating or tumbling objects that can be seen from below.\r\n\t/// \u003C/summary\u003E\r\n\tSphere = 1,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// All N x N frames on the horizon, evenly around Z, and the card only turns around Z. Highest yaw\r\n\t/// resolution, but it can\u0027t show the top of the object. Classic tree billboard behaviour.\r\n\t/// \u003C/summary\u003E\r\n\tCylinder = 2,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The frame layout maths shared by the baker and the runtime. \u003Cc\u003Eflatpack_impostor.shader\u003C/c\u003E has an HLSL\r\n/// copy of the same functions; they must stay identical or the shader samples the wrong frame.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// Coordinates are Source/s\u0026amp;box: \u002BX forward, \u002BY left, \u002BZ up. A frame direction points from the object\r\n/// toward the camera that captured it.\r\n/// \u003C/remarks\u003E\r\npublic static class FlatpackMath\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Maps a unit direction to the [-1, 1] square for the given layout (not used by \u003Csee cref=\u0022FlatpackLayout.Cylinder\u0022/\u003E).\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Vector2 EncodeOct( Vector3 d, FlatpackLayout layout )\r\n\t{\r\n\t\tif ( layout == FlatpackLayout.Hemisphere )\r\n\t\t{\r\n\t\t\t// Nothing below the horizon was baked, so clamp onto the horizon ring.\r\n\t\t\td.z = MathF.Max( d.z, 0.0f );\r\n\t\t\td = (d \u002B new Vector3( 0, 0, 1e-5f )).Normal;\r\n\t\t\tvar px = d.x / (MathF.Abs( d.x ) \u002B MathF.Abs( d.y ) \u002B d.z);\r\n\t\t\tvar py = d.y / (MathF.Abs( d.x ) \u002B MathF.Abs( d.y ) \u002B d.z);\r\n\r\n\t\t\t// Rotate the octahedron\u0027s top half by 45 degrees so it fills the whole square.\r\n\t\t\treturn new Vector2( px \u002B py, px - py );\r\n\t\t}\r\n\r\n\t\tvar sum = MathF.Abs( d.x ) \u002B MathF.Abs( d.y ) \u002B MathF.Abs( d.z );\r\n\t\tvar p = new Vector2( d.x / sum, d.y / sum );\r\n\t\tif ( d.z \u003C 0.0f )\r\n\t\t{\r\n\t\t\t// Fold the lower half out into the square\u0027s corners.\r\n\t\t\tp = new Vector2( (1.0f - MathF.Abs( p.y )) * Sign( p.x ), (1.0f - MathF.Abs( p.x )) * Sign( p.y ) );\r\n\t\t}\r\n\r\n\t\treturn p;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Inverse of \u003Csee cref=\u0022EncodeOct\u0022/\u003E. Returns a unit direction.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Vector3 DecodeOct( Vector2 o, FlatpackLayout layout )\r\n\t{\r\n\t\tif ( layout == FlatpackLayout.Hemisphere )\r\n\t\t{\r\n\t\t\tvar px = (o.x \u002B o.y) * 0.5f;\r\n\t\t\tvar py = (o.x - o.y) * 0.5f;\r\n\t\t\treturn new Vector3( px, py, 1.0f - MathF.Abs( px ) - MathF.Abs( py ) ).Normal;\r\n\t\t}\r\n\r\n\t\tvar d = new Vector3( o.x, o.y, 1.0f - MathF.Abs( o.x ) - MathF.Abs( o.y ) );\r\n\t\tif ( d.z \u003C 0.0f )\r\n\t\t{\r\n\t\t\td.x = (1.0f - MathF.Abs( o.y )) * Sign( o.x );\r\n\t\t\td.y = (1.0f - MathF.Abs( o.x )) * Sign( o.y );\r\n\t\t}\r\n\r\n\t\treturn d.Normal;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The direction frame (\u003Cparamref name=\u0022x\u0022/\u003E, \u003Cparamref name=\u0022y\u0022/\u003E) of an \u003Cparamref name=\u0022n\u0022/\u003E x \u003Cparamref name=\u0022n\u0022/\u003E grid is baked from.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Vector3 FrameDirection( int x, int y, int n, FlatpackLayout layout )\r\n\t{\r\n\t\tif ( layout == FlatpackLayout.Cylinder )\r\n\t\t{\r\n\t\t\tvar index = y * n \u002B x;\r\n\t\t\tvar yaw = index / (float)(n * n) * MathF.Tau;\r\n\t\t\treturn new Vector3( MathF.Cos( yaw ), MathF.Sin( yaw ), 0.0f );\r\n\t\t}\r\n\r\n\t\t// Frame centres sit on the grid vertices (not cell centres), so the outer frames land exactly on the\r\n\t\t// horizon and the shader can blend between them without extrapolating.\r\n\t\tvar o = new Vector2( x, y ) / (n - 1) * 2.0f - 1.0f;\r\n\t\treturn DecodeOct( o, layout );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The camera basis a frame is captured with. Right is image \u002BX, up is image -Y.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void FrameBasis( Vector3 d, out Vector3 right, out Vector3 up )\r\n\t{\r\n\t\tvar refUp = MathF.Abs( d.z ) \u003C 0.999f ? Vector3.Up : Vector3.Forward;\r\n\t\tright = Vector3.Cross( -d, refUp ).Normal;\r\n\t\tup = Vector3.Cross( right, -d );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Camera rotation for a frame. Equivalent to \u003Csee cref=\u0022FrameBasis\u0022/\u003E (checked in the editor: \u003Cc\u003ERotation.LookAt\u003C/c\u003E\r\n\t/// produces the same right and up vectors).\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Rotation FrameRotation( Vector3 d )\r\n\t{\r\n\t\tFrameBasis( d, out _, out var up );\r\n\t\treturn Rotation.LookAt( -d, up );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Smallest sphere-ish bound we bake around: the box centre and its half diagonal. Not the tightest sphere,\r\n\t/// but stable and cheap, and the shader only needs the frames to contain the object.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static (Vector3 Center, float Radius) BoundingSphere( BBox bounds )\r\n\t{\r\n\t\treturn (bounds.Center, MathF.Max( bounds.Size.Length * 0.5f, 0.01f ));\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A bounding sphere fitted to points (Ritter\u0027s method): pick two far-apart points, then grow the sphere for any point\r\n\t/// left outside. Within a few percent of the minimal sphere and never smaller than needed.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static (Vector3 Center, float Radius) BoundingSphere( IReadOnlyList\u003CVector3\u003E points )\r\n\t{\r\n\t\tif ( points is null || points.Count == 0 )\r\n\t\t\treturn (Vector3.Zero, 0.01f);\r\n\r\n\t\tvar a = points[0];\r\n\t\tvar b = Farthest( points, a );\r\n\t\tvar c = Farthest( points, b );\r\n\r\n\t\tvar center = (b \u002B c) * 0.5f;\r\n\t\tvar radius = b.Distance( c ) * 0.5f;\r\n\r\n\t\tforeach ( var p in points )\r\n\t\t{\r\n\t\t\tvar d = p.Distance( center );\r\n\t\t\tif ( d \u003C= radius )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Move the centre toward p just enough that the new sphere still touches the old one\u0027s far side.\r\n\t\t\tvar newRadius = (radius \u002B d) * 0.5f;\r\n\t\t\tcenter \u002B= (p - center) * ((newRadius - radius) / d);\r\n\t\t\tradius = newRadius;\r\n\t\t}\r\n\r\n\t\t// A hair of slack so silhouette texels on the sphere\u0027s rim aren\u0027t clipped by the frame edge.\r\n\t\treturn (center, MathF.Max( radius * 1.01f, 0.01f ));\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Metres in one world unit. s\u0026amp;box units are inches, as in Source; the iteration 6 distance sheet (\u00226 radii (21 m)\u0022\r\n\t/// for a tree 140 units in radius) used the same conversion.\r\n\t/// \u003C/summary\u003E\r\n\tpublic const float MetresPerUnit = 0.0254f;\r\n\r\n\t/// \u003Csummary\u003EA distance in world units, in metres.\u003C/summary\u003E\r\n\tpublic static float ToMetres( float units ) =\u003E units * MetresPerUnit;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A distance in world units as a reader wants it next to bounding radii: one decimal under 10 m, whole metres from\r\n\t/// there (\u00222.5 m\u0022, \u002221 m\u0022). The inspector, the bake check summary and the benchmark sheets all use this.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string FormatMetres( float units )\r\n\t{\r\n\t\tvar metres = ToMetres( units );\r\n\t\treturn metres \u003C 10.0f\r\n\t\t\t? $\u0022{metres.ToString( \u00220.0\u0022, System.Globalization.CultureInfo.InvariantCulture )} m\u0022\r\n\t\t\t: $\u0022{metres.ToString( \u00220\u0022, System.Globalization.CultureInfo.InvariantCulture )} m\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Columns the frame lookup texture reserves for image rectangles; the depth ranges sit this far to the right.\r\n\t/// The shader\u0027s \u003Cc\u003EFlatpackFrameFit\u003C/c\u003E reads texel (x, y) and (x \u002B 32, y).\r\n\t/// \u003C/summary\u003E\r\n\tpublic const int FrameFitColumns = 32;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The bounds one frame was baked with: where the object sits inside the bounding sphere\u0027s frame, and how deep.\r\n\t/// \u003C/summary\u003E\r\n\tpublic struct FrameFit\r\n\t{\r\n\t\t/// \u003Csummary\u003ETop-left corner of the frame\u0027s image rectangle, in the sphere-sized frame\u0027s 0..1 image coordinates (v down).\u003C/summary\u003E\r\n\t\tpublic Vector2 Min;\r\n\r\n\t\t/// \u003Csummary\u003EBottom-right corner of the image rectangle.\u003C/summary\u003E\r\n\t\tpublic Vector2 Max;\r\n\r\n\t\t/// \u003Csummary\u003EHeight of the nearest surface along the frame direction (toward its camera), in bounding radii.\u003C/summary\u003E\r\n\t\tpublic float Near;\r\n\r\n\t\t/// \u003Csummary\u003EHeight of the furthest surface, in bounding radii. Always below \u003Csee cref=\u0022Near\u0022/\u003E.\u003C/summary\u003E\r\n\t\tpublic float Far;\r\n\r\n\t\t/// \u003Csummary\u003EThe whole bounding sphere: what every frame used before iteration 8.\u003C/summary\u003E\r\n\t\tpublic static FrameFit Sphere =\u003E new() { Min = Vector2.Zero, Max = Vector2.One, Near = 1.0f, Far = -1.0f };\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Fit one frame to the points it sees: their image rectangle, grown by \u003Cparamref name=\u0022padTexels\u0022/\u003E of a\r\n\t/// \u003Cparamref name=\u0022cellTexels\u0022/\u003E-wide frame on every side, and their depth range. Rounded outward to the 8-bit steps the\r\n\t/// lookup texture stores, so the baker renders with exactly the bounds the shader reads back.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static FrameFit FitFrame( IReadOnlyList\u003CVector3\u003E points, Vector3 center, float radius, Vector3 dir, int cellTexels, int padTexels )\r\n\t{\r\n\t\tif ( points is null || points.Count == 0 )\r\n\t\t\treturn FrameFit.Sphere;\r\n\r\n\t\tFrameBasis( dir, out var right, out var up );\r\n\t\tfloat minU = float.MaxValue, maxU = float.MinValue, minV = float.MaxValue, maxV = float.MinValue;\r\n\t\tfloat near = float.MinValue, far = float.MaxValue;\r\n\r\n\t\tforeach ( var p in points )\r\n\t\t{\r\n\t\t\tvar rel = p - center;\r\n\t\t\tvar u = 0.5f \u002B Vector3.Dot( rel, right ) / (2.0f * radius);\r\n\t\t\tvar v = 0.5f - Vector3.Dot( rel, up ) / (2.0f * radius);\r\n\t\t\tvar h = Vector3.Dot( rel, dir ) / radius;\r\n\t\t\tminU = MathF.Min( minU, u ); maxU = MathF.Max( maxU, u );\r\n\t\t\tminV = MathF.Min( minV, v ); maxV = MathF.Max( maxV, v );\r\n\t\t\tnear = MathF.Max( near, h ); far = MathF.Min( far, h );\r\n\t\t}\r\n\r\n\t\t// Padding: the rectangle maps onto cellTexels texels, padTexels of which on each side stay outside the object.\r\n\t\tvar grow = cellTexels \u003E 2 * padTexels \u002B 1 ? cellTexels / (float)(cellTexels - 2 * padTexels) : 1.0f;\r\n\t\tvar padU = (maxU - minU) * (grow - 1.0f) * 0.5f;\r\n\t\tvar padV = (maxV - minV) * (grow - 1.0f) * 0.5f;\r\n\r\n\t\tvar fit = new FrameFit\r\n\t\t{\r\n\t\t\tMin = new Vector2( minU - padU, minV - padV ),\r\n\t\t\tMax = new Vector2( maxU \u002B padU, maxV \u002B padV ),\r\n\t\t\tNear = near,\r\n\t\t\tFar = far,\r\n\t\t};\r\n\t\treturn DecodeFrameFit( EncodeFrameFit( fit ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The two lookup texels for a frame: (left, top, right) and (bottom, near, far), each value one byte, rounded outward\r\n\t/// so the stored bounds always contain the real ones. Alpha stays 255 (the PNG path drops colour under low alpha).\r\n\t/// \u003C/summary\u003E\r\n\tpublic static (Color32 Rect, Color32 Depth) EncodeFrameFit( FrameFit fit )\r\n\t{\r\n\t\tint Down( float v ) =\u003E Math.Clamp( (int)MathF.Floor( v * 255.0f \u002B 1e-3f ), 0, 255 );\r\n\t\tint Up( float v ) =\u003E Math.Clamp( (int)MathF.Ceiling( v * 255.0f - 1e-3f ), 0, 255 );\r\n\r\n\t\tint minU = Down( fit.Min.x ), maxU = Up( fit.Max.x );\r\n\t\tint minV = Down( fit.Min.y ), maxV = Up( fit.Max.y );\r\n\t\tint near = Up( (fit.Near \u002B 1.0f) * 0.5f ), far = Down( (fit.Far \u002B 1.0f) * 0.5f );\r\n\r\n\t\t// Never let a frame collapse (a flat panel seen edge-on, a plane seen face-on): keep at least two steps each way.\r\n\t\t(minU, maxU) = Widen( minU, maxU );\r\n\t\t(minV, maxV) = Widen( minV, maxV );\r\n\t\t(far, near) = Widen( far, near );\r\n\r\n\t\treturn (new Color32( (byte)minU, (byte)minV, (byte)maxU, 255 ), new Color32( (byte)maxV, (byte)near, (byte)far, 255 ));\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The bounds two lookup texels describe. Matches the shader\u0027s \u003Cc\u003EFlatpackFrameFit\u003C/c\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static FrameFit DecodeFrameFit( (Color32 Rect, Color32 Depth) texels ) =\u003E new()\r\n\t{\r\n\t\tMin = new Vector2( texels.Rect.r / 255.0f, texels.Rect.g / 255.0f ),\r\n\t\tMax = new Vector2( texels.Rect.b / 255.0f, texels.Depth.r / 255.0f ),\r\n\t\tNear = texels.Depth.g / 255.0f * 2.0f - 1.0f,\r\n\t\tFar = texels.Depth.b / 255.0f * 2.0f - 1.0f,\r\n\t};\r\n\r\n\tstatic (int Low, int High) Widen( int low, int high )\r\n\t{\r\n\t\twhile ( high - low \u003C 2 )\r\n\t\t{\r\n\t\t\tif ( high \u003C 255 ) high\u002B\u002B;\r\n\t\t\tif ( high - low \u003C 2 \u0026\u0026 low \u003E 0 ) low--;\r\n\t\t}\r\n\t\treturn (low, high);\r\n\t}\r\n\r\n\tstatic Vector3 Farthest( IReadOnlyList\u003CVector3\u003E points, Vector3 from )\r\n\t{\r\n\t\tvar best = from;\r\n\t\tvar bestDistance = -1.0f;\r\n\t\tforeach ( var p in points )\r\n\t\t{\r\n\t\t\tvar d = p.DistanceSquared( from );\r\n\t\t\tif ( d \u003E bestDistance )\r\n\t\t\t{\r\n\t\t\t\tbestDistance = d;\r\n\t\t\t\tbest = p;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn best;\r\n\t}\r\n\r\n\tstatic float Sign( float v ) =\u003E v \u003E= 0.0f ? 1.0f : -1.0f;\r\n}\r\n"},{"Ident":"anakin.flatpack","Path":"Code/FlatpackSystem.cs","FileName":"FlatpackSystem.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace Flatpack;\r\n\r\n/// \u003Csummary\u003E\r\n/// How hard Flatpack works, for a game\u0027s graphics settings: high quality presets turn it off, low ones lean on it.\r\n/// Set \u003Csee cref=\u0022FlatpackSystem.Level\u0022/\u003E; every Flatpack Renderer follows it within a few frames, nothing re-bakes.\r\n/// \u003C/summary\u003E\r\npublic enum FlatpackLevel\r\n{\r\n\t/// \u003Csummary\u003EReal models everywhere; cards never show (a game\u0027s High or Ultra preset).\u003C/summary\u003E\r\n\tOff,\r\n\r\n\t/// \u003Csummary\u003ESwap where the bake was measured to look like the model (the default; a game\u0027s Medium).\u003C/summary\u003E\r\n\tBalanced,\r\n\r\n\t/// \u003Csummary\u003ESwap at 0.6 times the distance, from 3 bounding radii (a game\u0027s Low).\u003C/summary\u003E\r\n\tPerformance,\r\n\r\n\t/// \u003Csummary\u003ESwap at 0.35 times the distance, from 1.5 bounding radii (a game\u0027s lowest preset or weak hardware).\u003C/summary\u003E\r\n\tMaximum,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Runs the model/card distance check for every \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E in the scene.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// One loop per frame instead of one Update per renderer. The loop is time-sliced: each frame checks at most\r\n/// \u003Csee cref=\u0022RenderersPerFrame\u0022/\u003E renderers, round-robin, so a forest of 5000 trees spreads its checks over a few\r\n/// frames. Renderers in the middle of a cross-fade are ticked every frame until they finish.\r\n/// \u003C/remarks\u003E\r\npublic sealed class FlatpackSystem : GameObjectSystem\u003CFlatpackSystem\u003E\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Global level for every Flatpack Renderer, meant to be driven by a game\u0027s graphics settings, for example\r\n\t/// \u003Cc\u003EFlatpackSystem.Level = FlatpackLevel.Performance;\u003C/c\u003E on the Low preset. Applies to automatic and manual swap\r\n\t/// distances alike; Force Model and Force Card modes ignore it.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static FlatpackLevel Level { get; set; } = FlatpackLevel.Balanced;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// What \u003Csee cref=\u0022Level\u0022/\u003E does to a swap distance: a multiplier, and the closest swap allowed in bounding radii\r\n\t/// (0 = the renderer\u0027s own minimum). \u003Csee cref=\u0022FlatpackLevel.Off\u0022/\u003E pushes the swap out to infinity.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static (float Scale, float MinimumRadii) LevelScaling =\u003E Level switch\r\n\t{\r\n\t\tFlatpackLevel.Off =\u003E (float.PositiveInfinity, 0.0f),\r\n\t\tFlatpackLevel.Performance =\u003E (0.6f, 3.0f),\r\n\t\tFlatpackLevel.Maximum =\u003E (0.35f, 1.5f),\r\n\t\t_ =\u003E (1.0f, 0.0f),\r\n\t};\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Distance checks per frame. Lower spreads the work thinner but makes swaps lag behind a fast camera.\r\n\t/// \u003C/summary\u003E\r\n\tpublic int RenderersPerFrame { get; set; } = 2048;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Number of registered renderers.\r\n\t/// \u003C/summary\u003E\r\n\tpublic int Count =\u003E _renderers.Count;\r\n\r\n\treadonly List\u003CFlatpackRenderer\u003E _renderers = new();\r\n\treadonly List\u003CFlatpackRenderer\u003E _fading = new();\r\n\treadonly Dictionary\u003CFlatpackImpostor, int\u003E _visibleCards = new();\r\n\tint _cursor;\r\n\r\n\tpublic FlatpackSystem( Scene scene ) : base( scene )\r\n\t{\r\n\t\t// After the scene update, so this frame\u0027s camera and object movement are already in.\r\n\t\tListen( Stage.FinishUpdate, 100, Tick, \u0022Flatpack.DistancePass\u0022 );\r\n\t}\r\n\r\n\tinternal void Register( FlatpackRenderer renderer )\r\n\t{\r\n\t\tif ( renderer.SystemIndex \u003E= 0 )\r\n\t\t\treturn;\r\n\r\n\t\trenderer.SystemIndex = _renderers.Count;\r\n\t\t_renderers.Add( renderer );\r\n\t}\r\n\r\n\tinternal void Unregister( FlatpackRenderer renderer )\r\n\t{\r\n\t\tvar index = renderer.SystemIndex;\r\n\t\tif ( index \u003C 0 || index \u003E= _renderers.Count || _renderers[index] != renderer )\r\n\t\t\treturn;\r\n\r\n\t\t// Swap-remove keeps this O(1); the moved renderer takes over the freed slot.\r\n\t\tvar last = _renderers[^1];\r\n\t\t_renderers[index] = last;\r\n\t\tlast.SystemIndex = index;\r\n\t\t_renderers.RemoveAt( _renderers.Count - 1 );\r\n\r\n\t\trenderer.SystemIndex = -1;\r\n\t\t_fading.Remove( renderer );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Reference count of visible cards per impostor, kept by \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E, so the tick can keep\r\n\t/// those atlases streamed in without walking every renderer.\r\n\t/// \u003C/summary\u003E\r\n\tinternal void CardVisibilityChanged( FlatpackImpostor impostor, bool visible )\r\n\t{\r\n\t\tif ( impostor is null )\r\n\t\t\treturn;\r\n\r\n\t\t_visibleCards.TryGetValue( impostor, out var count );\r\n\t\tcount \u002B= visible ? 1 : -1;\r\n\r\n\t\tif ( count \u003E 0 )\r\n\t\t\t_visibleCards[impostor] = count;\r\n\t\telse\r\n\t\t\t_visibleCards.Remove( impostor );\r\n\t}\r\n\r\n\tvoid Tick()\r\n\t{\r\n\t\tforeach ( var impostor in _visibleCards.Keys )\r\n\t\t\timpostor.MarkAtlasesUsed();\r\n\r\n\t\tif ( _renderers.Count == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tvar camera = Scene.Camera;\r\n\t\tif ( !camera.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar cameraPosition = camera.WorldPosition;\r\n\t\tvar count = Math.Min( RenderersPerFrame, _renderers.Count );\r\n\r\n\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _cursor \u003E= _renderers.Count )\r\n\t\t\t\t_cursor = 0;\r\n\r\n\t\t\tvar renderer = _renderers[_cursor\u002B\u002B];\r\n\t\t\tvar distanceSquared = cameraPosition.DistanceSquared( renderer.WorldPosition );\r\n\r\n\t\t\tif ( renderer.Evaluate( distanceSquared ) \u0026\u0026 !_fading.Contains( renderer ) )\r\n\t\t\t\t_fading.Add( renderer );\r\n\t\t}\r\n\r\n\t\tif ( _fading.Count == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tvar delta = Time.Delta;\r\n\t\tfor ( var i = _fading.Count - 1; i \u003E= 0; i-- )\r\n\t\t{\r\n\t\t\tif ( !_fading[i].TickFade( delta ) )\r\n\t\t\t\t_fading.RemoveAt( i );\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"anakin.flatpack","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":429259,"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, \u0022Flatpack\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022flatpack\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022anakin\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022anakin.flatpack\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002230\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-10-26T15:38:29.8717994Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.112.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.112.0\u0022)]"},{"Ident":"anakin.flatpack","Path":"Editor/FlatpackLodModel.cs","FileName":"FlatpackLodModel.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing System.Threading.Tasks;\r\nusing Editor;\r\nusing Sandbox;\r\n\r\nnamespace Flatpack.EditorTools;\r\n\r\n/// \u003Csummary\u003E\r\n/// Builds a model whose last LOD is the Flatpack card: the source model\u0027s first LODs with their own materials, then the\r\n/// card. The engine then swaps to the card by itself, like any other LOD, with no Flatpack Renderer on the object, and\r\n/// the Clutter tool can paint it.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\r\n/// The engine picks a LOD from the camera distance alone: LOD L shows once \u003Cc\u003Eswitch[L] * scale\u003C/c\u003E is below\r\n/// \u003Cc\u003E100 * distance * tan(fov / 2) / screenWidth\u003C/c\u003E (the native metric, also in \u003Cc\u003Eclutter_cull_cs.shader\u003C/c\u003E). Switch\r\n/// values therefore shift with resolution and field of view; \u003Csee cref=\u0022SwitchValue\u0022/\u003E converts a world distance using a\r\n/// 1920-pixel-wide, 70 degree reference view.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// Clutter draws at most four LODs and only the first mesh of each, so the source keeps at most three LODs here and each\r\n/// LOD is one mesh with a draw call per material.\r\n/// \u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\npublic static class FlatpackLodModel\r\n{\r\n\t/// \u003Csummary\u003ESource LODs kept by default: Clutter draws four LODs at most, and the card takes one.\u003C/summary\u003E\r\n\tpublic const int ClutterSourceLods = 3;\r\n\r\n\tconst float ReferenceWidth = 1920.0f;\r\n\tconst float ReferenceFov = 70.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The LOD switch value that makes the engine swap at \u003Cparamref name=\u0022distance\u0022/\u003E world units for an unscaled object,\r\n\t/// on the reference view.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static float SwitchValue( float distance ) =\u003E\r\n\t\t100.0f * distance * MathF.Tan( ReferenceFov * 0.5f * MathF.PI / 180.0f ) / ReferenceWidth;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Build the combined model. \u003Cparamref name=\u0022swapDistance\u0022/\u003E is where the card takes over, in world units for an\r\n\t/// unscaled object; source LODs that would switch in at or past it are dropped.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Model Build( FlatpackImpostor impostor, float swapDistance, int maxSourceLods = ClutterSourceLods )\r\n\t{\r\n\t\tvar source = impostor.SourceModel ?? throw new ArgumentException( \u0022The impostor has no source model.\u0022 );\r\n\t\tvar info = source.MeshInfo;\r\n\t\tvar switches = info.LodSwitchDistances;\r\n\t\tvar cardSwitch = SwitchValue( swapDistance );\r\n\r\n\t\tvar lods = Math.Clamp( info.LodCount, 1, Math.Max( 1, maxSourceLods ) );\r\n\t\twhile ( lods \u003E 1 \u0026\u0026 switches.Length \u003E lods - 1 \u0026\u0026 switches[lods - 1] \u003E= cardSwitch )\r\n\t\t\tlods--;\r\n\r\n\t\tvar builder = Model.Builder.WithName( $\u0022{source.ResourceName}_flatpack\u0022 );\r\n\t\tvar bounds = BBox.FromPositionAndSize( impostor.Center, impostor.Radius * 2.0f );\r\n\r\n\t\tfor ( var lod = 0; lod \u003C lods; lod\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar (mesh, meshBounds) = BuildSourceLod( source, info, lod );\r\n\t\t\tbuilder.AddMesh( mesh, lod );\r\n\t\t\tbuilder.WithLodDistance( lod, lod \u003C switches.Length ? switches[lod] : lod * 50.0f );\r\n\t\t\tbounds = bounds.AddBBox( meshBounds );\r\n\t\t}\r\n\r\n\t\t// The card from here to the last slot. Every slot gets a switch value; empty slots past the card would be picked\r\n\t\t// at long range and draw nothing.\r\n\t\tfor ( var lod = lods; lod \u003C 8; lod\u002B\u002B )\r\n\t\t{\r\n\t\t\tbuilder.AddMesh( FlatpackCard.BuildMesh( impostor ), lod );\r\n\t\t\tbuilder.WithLodDistance( lod, cardSwitch );\r\n\t\t}\r\n\r\n\t\treturn builder.WithViewBounds( bounds ).Create();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One source LOD as a single mesh: every draw call in that LOD, one after another in shared buffers, each with its\r\n\t/// own material.\r\n\t/// \u003C/summary\u003E\r\n\tstatic (Mesh Mesh, BBox Bounds) BuildSourceLod( Model source, Model.ModelMeshInfo info, int lod )\r\n\t{\r\n\t\tvar vertices = new List\u003CVertex\u003E();\r\n\t\tvar indices = new List\u003Cint\u003E();\r\n\t\tvar ranges = new List\u003C(Material Material, int Start, int Count)\u003E();\r\n\t\tvar bounds = new BBox( Vector3.Zero, Vector3.Zero );\r\n\t\tvar first = true;\r\n\r\n\t\tfor ( var m = 0; m \u003C info.Meshes.Length; m\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar meshInfo = info.Meshes[m];\r\n\t\t\tif ( ((long)meshInfo.LodMask \u0026 (1L \u003C\u003C lod)) == 0 )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar box = new BBox( meshInfo.BoundsMin, meshInfo.BoundsMax );\r\n\t\t\tbounds = first ? box : bounds.AddBBox( box );\r\n\t\t\tfirst = false;\r\n\r\n\t\t\tfor ( var d = 0; d \u003C meshInfo.DrawCalls.Length; d\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( !TryReadDrawCall( source, m, d, out var v, out var ix ) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar start = indices.Count;\r\n\t\t\t\tvar baseVertex = vertices.Count;\r\n\t\t\t\tvertices.AddRange( v );\r\n\t\t\t\tforeach ( var i in ix )\r\n\t\t\t\t\tindices.Add( (int)i \u002B baseVertex );\r\n\r\n\t\t\t\tranges.Add( (meshInfo.DrawCalls[d].Material, start, ix.Length) );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( ranges.Count == 0 )\r\n\t\t\tthrow new InvalidOperationException( $\u0022Couldn\u0027t read LOD {lod} of {source.ResourceName}.\u0022 );\r\n\r\n\t\tvar mesh = new Mesh( ranges[0].Material );\r\n\t\tmesh.CreateVertexBuffer( vertices.Count, vertices );\r\n\t\tmesh.CreateIndexBuffer( indices.Count, indices );\r\n\t\tmesh.SetIndexRange( ranges[0].Start, ranges[0].Count );\r\n\t\tfor ( var r = 1; r \u003C ranges.Count; r\u002B\u002B )\r\n\t\t\tmesh.AddSubMesh( ranges[r].Material, ranges[r].Start, ranges[r].Count, 0, 0 );\r\n\r\n\t\tmesh.Bounds = bounds;\r\n\t\treturn (mesh, bounds);\r\n\t}\r\n\r\n\tstatic MethodInfo _readDrawCall;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The engine\u0027s reader for one draw call\u0027s vertices and indices. It\u0027s internal (the tools\u0027 scene compiler uses it), so\r\n\t/// it\u0027s reached by reflection; the public whole-model readers don\u0027t separate LODs or draw calls.\r\n\t/// \u003C/summary\u003E\r\n\tstatic bool TryReadDrawCall( Model model, int mesh, int drawCall, out Vertex[] vertices, out uint[] indices )\r\n\t{\r\n\t\t_readDrawCall ??= typeof( Model ).GetMethod( \u0022TryReadDrawCall\u0022, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic );\r\n\t\tvertices = null;\r\n\t\tindices = null;\r\n\t\tif ( _readDrawCall is null )\r\n\t\t\treturn false;\r\n\r\n\t\tvar args = new object[] { mesh, drawCall, null, null, null };\r\n\t\tif ( _readDrawCall.Invoke( model, args ) is not true )\r\n\t\t\treturn false;\r\n\r\n\t\tvertices = args[2] as Vertex[];\r\n\t\tindices = args[3] as uint[];\r\n\t\treturn vertices is { Length: \u003E 0 } \u0026\u0026 indices is { Length: \u003E 0 };\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Save \u003Cparamref name=\u0022model\u0022/\u003E as a compiled model next to the impostor\u0027s \u003Cc\u003E.fpack\u003C/c\u003E and load it back from disk,\r\n\t/// so scenes and Clutter can reference it by path. Returns the loaded model.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static async Task\u003CModel\u003E SaveAsync( FlatpackImpostor impostor, Model model )\r\n\t{\r\n\t\tvar fpack = AssetSystem.FindByPath( impostor.ResourcePath ) ?? throw new InvalidOperationException( \u0022Save the impostor first.\u0022 );\r\n\t\tvar folder = Path.GetDirectoryName( fpack.AbsolutePath );\r\n\t\tvar name = $\u0022{impostor.SourceModel.ResourceName}_flatpack\u0022;\r\n\t\tvar file = Path.Combine( folder, $\u0022{name}.vmdl_c\u0022 );\r\n\r\n\t\tvar bytes = await model.SaveToVmdlAsync();\r\n\t\tawait File.WriteAllBytesAsync( file, bytes );\r\n\r\n\t\tAssetSystem.RegisterFile( file );\r\n\t\tvar relative = Path.Combine( Path.GetDirectoryName( impostor.ResourcePath ) ?? \u0022\u0022, $\u0022{name}.vmdl\u0022 ).Replace( \u0027\\\\\u0027, \u0027/\u0027 );\r\n\t\treturn Model.Load( relative );\r\n\t}\r\n}\r\n"},{"Ident":"anakin.flatpack","Path":"Editor/FlatpackMenus.cs","FileName":"FlatpackMenus.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\nusing System.Linq;\nusing Editor;\nusing Sandbox;\n\nnamespace Flatpack.EditorTools;\n\n/// \u003Csummary\u003E\n/// Right-click entry points into the baker: on model assets in the asset browser, and on objects in the scene view.\n/// \u003C/summary\u003E\npublic static class FlatpackMenus\n{\n\t[Event( \u0022asset.contextmenu\u0022, Priority = 60 )]\n\tstatic void OnAssetContextMenu( AssetContextMenu e )\n\t{\n\t\tvar models = e.SelectedList\n\t\t\t.Select( x =\u003E x.Asset )\n\t\t\t.Where( a =\u003E a is not null \u0026\u0026 a.AssetType == AssetType.Model )\n\t\t\t.ToArray();\n\n\t\tif ( models.Length == 0 )\n\t\t\treturn;\n\n\t\tvar label = models.Length == 1 ? \u0022Bake Flatpack Impostor\u0022 : $\u0022Bake {models.Length} Flatpack Impostors\u0022;\n\t\te.Menu.AddOption( label, \u0022view_in_ar\u0022, () =\u003E\n\t\t{\n\t\t\tvar loaded = models.Select( a =\u003E Model.Load( a.Path ) ).Where( m =\u003E m is not null \u0026\u0026 !m.IsError ).ToArray();\n\t\t\tFlatpackBakeWindow.Open( loaded );\n\t\t} );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Bake the model on \u003Cparamref name=\u0022renderer\u0022/\u003E with the window\u0027s current settings, then put a\n\t/// \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E next to it pointing at the result (undoable).\n\t/// \u003C/summary\u003E\n\tpublic static async void BakeAndAttach( ModelRenderer renderer )\n\t{\n\t\tif ( !renderer.IsValid() || renderer.Model is null )\n\t\t\treturn;\n\n\t\tvar window = FlatpackBakeWindow.Open( renderer.Model );\n\t\tif ( window.IsBaking )\n\t\t\treturn;\n\n\t\tvar impostors = await window.BakeQueueAsync();\n\t\tvar impostor = impostors.FirstOrDefault( x =\u003E x.SourceModel == renderer.Model );\n\t\tif ( impostor is null || !renderer.IsValid() )\n\t\t\treturn;\n\n\t\tvar go = renderer.GameObject;\n\t\tvar session = SceneEditorSession.Resolve( go );\n\t\tif ( session is null )\n\t\t\treturn;\n\n\t\tusing var scene = session.Scene.Push();\n\t\tusing ( session.UndoScope( \u0022Add Flatpack Renderer\u0022 ).WithComponentCreations().WithComponentChanges( go.Components.GetAll\u003CFlatpackRenderer\u003E() ).Push() )\n\t\t{\n\t\t\tvar flatpack = go.Components.GetOrCreate\u003CFlatpackRenderer\u003E();\n\t\t\tflatpack.Source = renderer;\n\t\t\tflatpack.Impostor = impostor;\n\t\t}\n\t}\n}\n\n/// \u003Csummary\u003E\n/// Adds \u0022Bake Flatpack Impostor\u0022 to the scene viewport\u0027s right-click menu when you click an object with a\n/// \u003Csee cref=\u0022ModelRenderer\u0022/\u003E. Scene view hooks are instance interfaces, so one listener is registered on first use.\n/// \u003C/summary\u003E\nsealed class FlatpackSceneViewHooks : EditorEvent.ISceneView\n{\n\tstatic FlatpackSceneViewHooks _instance;\n\n\t[EditorEvent.Frame]\n\tstatic void EnsureRegistered()\n\t{\n\t\tif ( _instance is not null )\n\t\t\treturn;\n\n\t\t_instance = new FlatpackSceneViewHooks();\n\t\tEditorEvent.Register( _instance );\n\t}\n\n\tvoid EditorEvent.ISceneView.DrawGizmos( Scene scene ) { }\n\n\tvoid EditorEvent.ISceneView.ShowContextMenu( EditorEvent.ShowContextMenuEvent ev )\n\t{\n\t\tvar target = ev.Trace?.GameObject;\n\t\tvar renderer = target?.Components.Get\u003CModelRenderer\u003E( FindMode.EverythingInSelfAndAncestors );\n\t\tif ( !renderer.IsValid() || renderer.Model is null )\n\t\t\treturn;\n\n\t\tev.Menu.AddSeparator();\n\t\tev.Menu.AddOption( $\u0022Bake Flatpack Impostor ({renderer.Model.ResourceName})\u0022, \u0022view_in_ar\u0022, () =\u003E FlatpackMenus.BakeAndAttach( renderer ) );\n\t}\n}\n"},{"Ident":"anakin.flatpack","Path":"Editor/FlatpackRendererEditor.cs","FileName":"FlatpackRendererEditor.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\nusing System.Linq;\nusing Editor;\nusing Sandbox;\n\nnamespace Flatpack.EditorTools;\n\n/// \u003Csummary\u003E\n/// Inspector for \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E: the usual properties plus a bake button, so giving one model a\n/// Flatpack LOD is \u0022add component, press Bake\u0022.\n/// \u003C/summary\u003E\n[CustomEditor( typeof( FlatpackRenderer ) )]\npublic sealed class FlatpackRendererEditor : ComponentEditorWidget\n{\n\tpublic FlatpackRendererEditor( SerializedObject obj ) : base( obj )\n\t{\n\t\tSetSizeMode( SizeMode.Default, SizeMode.Default );\n\n\t\tLayout = Layout.Column();\n\n\t\tvar sheet = new ControlSheet();\n\t\t// Only the component\u0027s [Property] members; the default sheet hides transform and internals the same way.\n\t\tsheet.AddObject( obj, p =\u003E p.HasAttribute\u003CPropertyAttribute\u003E() );\n\t\tLayout.Add( sheet );\n\n\t\tvar renderers = obj.Targets.OfType\u003CFlatpackRenderer\u003E().ToArray();\n\t\tvar baked = renderers.All( x =\u003E x.Impostor is not null );\n\n\t\t// What the bake is, how close it holds up, and whether the swap distance is inside that.\n\t\tif ( renderers.Length == 1 \u0026\u0026 renderers[0].Impostor is { } impostor )\n\t\t{\n\t\t\tvar r = renderers[0];\n\n\t\t\t// Radii are the bake\u0027s own bounding radius times this object\u0027s scale, so metres follow the object\u0027s real size.\n\t\t\tvar scale = r.WorldScale;\n\t\t\tvar radius = impostor.Radius * MathF.Max( scale.x, MathF.Max( scale.y, scale.z ) );\n\t\t\tvar swapRadii = radius \u003E 0 ? r.EffectiveSwapDistance / radius : 0;\n\t\t\tvar holds = impostor.HoldsUpFrom switch\n\t\t\t{\n\t\t\t\t\u003E 0 =\u003E $\u0022matches the model from {impostor.HoldsUpFrom:0.#} radii ({FlatpackMath.FormatMetres( impostor.HoldsUpFrom * radius )})\u0022,\n\t\t\t\t\u003C 0 =\u003E $\u0022never matches the model closely inside 16 radii ({FlatpackMath.FormatMetres( 16.0f * radius )})\u0022,\n\t\t\t\t_ =\u003E \u0022not measured (re-bake to measure)\u0022,\n\t\t\t};\n\t\t\tvar level = string.IsNullOrEmpty( impostor.QualityLevel ) ? \u0022\u0022 : $\u0022{impostor.QualityLevel} bake, \u0022;\n\t\t\tvar info = Layout.Add( new Label( $\u0022{level}{holds}. Swaps at {swapRadii:0.#} radii ({FlatpackMath.FormatMetres( r.EffectiveSwapDistance )}).\u0022 ) { WordWrap = true } );\n\t\t\tinfo.ContentMargins = new Sandbox.UI.Margin( 8, 4, 8, 0 );\n\t\t\tif ( impostor.HoldsUpFrom \u003C 0 )\n\t\t\t\tinfo.SetStyles( \u0022color: #e0a030;\u0022 );\n\t\t}\n\n\t\tvar row = Layout.AddRow();\n\t\trow.Margin = new Sandbox.UI.Margin( 8, 4, 8, 8 );\n\t\trow.Spacing = 4;\n\n\t\trow.Add( new Button( baked ? \u0022Re-bake Impostor\u0022 : \u0022Bake Impostor\u0022, \u0022view_in_ar\u0022 )\n\t\t{\n\t\t\tToolTip = \u0022Bake this object\u0027s model with the Impostor Baker\u0027s current settings and assign the result.\u0022,\n\t\t\tClicked = () =\u003E\n\t\t\t{\n\t\t\t\tforeach ( var renderer in renderers )\n\t\t\t\t{\n\t\t\t\t\tvar source = renderer.Source ?? renderer.GameObject.Components.Get\u003CModelRenderer\u003E();\n\t\t\t\t\tif ( source.IsValid() )\n\t\t\t\t\t\tFlatpackMenus.BakeAndAttach( source );\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\n\t\trow.Add( new Button( \u0022Bake Settings\u0022, \u0022tune\u0022 )\n\t\t{\n\t\t\tToolTip = \u0022Open the Impostor Baker window to change grid, atlas size and layout.\u0022,\n\t\t\tClicked = () =\u003E FlatpackBakeWindow.Open()\n\t\t} );\n\n\t\trow.AddStretchCell();\n\t}\n}\n"},{"Ident":"anakin.flatpack","Path":"Code/FlatpackCard.cs","FileName":"FlatpackCard.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\r\nusing Sandbox;\r\n\r\nnamespace Flatpack;\r\n\r\n/// \u003Csummary\u003E\r\n/// Builds the card mesh an impostor is drawn with.\r\n/// \u003C/summary\u003E\r\npublic static class FlatpackCard\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// One quad, two triangles, with the impostor material. The vertex shader moves the corners every frame to\r\n\t/// face the camera, using only the UVs; the positions here are a plausible fallback (a quad facing \u002BX through the\r\n\t/// bounds centre) for anything that reads the mesh on the CPU.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Model Build( FlatpackImpostor impostor )\r\n\t{\r\n\t\treturn Model.Builder\r\n\t\t\t.WithName( $\u0022flatpack_card_{impostor.ResourceName}\u0022 )\r\n\t\t\t.AddMesh( BuildMesh( impostor ) )\r\n\t\t\t.Create();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The card quad as a mesh, for adding to another model (the card as the last LOD of a model).\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Mesh BuildMesh( FlatpackImpostor impostor )\r\n\t{\r\n\t\tvar c = impostor.Center;\r\n\t\tvar r = impostor.Radius;\r\n\r\n\t\tvar normal = Vector3.Forward;\r\n\t\tvar tangent = Vector3.Left;\r\n\r\n\t\t// UV (0,0) is the top-left corner; the shader maps u to the card\u0027s right and v to its down.\r\n\t\tvar vertices = new[]\r\n\t\t{\r\n\t\t\tnew Vertex( c \u002B new Vector3( 0, -r, r ), normal, tangent, new Vector4( 0, 0, 0, 0 ) ),\r\n\t\t\tnew Vertex( c \u002B new Vector3( 0, r, r ), normal, tangent, new Vector4( 1, 0, 0, 0 ) ),\r\n\t\t\tnew Vertex( c \u002B new Vector3( 0, r, -r ), normal, tangent, new Vector4( 1, 1, 0, 0 ) ),\r\n\t\t\tnew Vertex( c \u002B new Vector3( 0, -r, -r ), normal, tangent, new Vector4( 0, 1, 0, 0 ) ),\r\n\t\t};\r\n\r\n\t\tvar indices = new[] { 0, 1, 2, 0, 2, 3 };\r\n\r\n\t\tvar mesh = new Mesh( impostor.Material );\r\n\t\tmesh.CreateVertexBuffer( vertices.Length, vertices.AsSpan() );\r\n\t\tmesh.CreateIndexBuffer( indices.Length, indices.AsSpan() );\r\n\r\n\t\t// Culling has to use the whole bounding sphere: the card can be at any angle around it, and the shader\r\n\t\t// places it on the sphere\u0027s front face.\r\n\t\tmesh.Bounds = BBox.FromPositionAndSize( c, r * 2.0f );\r\n\t\treturn mesh;\r\n\t}\r\n}\r\n"},{"Ident":"anakin.flatpack","Path":"Code/FlatpackImpostor.cs","FileName":"FlatpackImpostor.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":429259,"IsPrivate":false,"Code":"using System.Linq;\nusing System.Text.Json.Serialization;\nusing Sandbox;\n\nnamespace Flatpack;\n\n/// \u003Csummary\u003E\n/// A baked impostor: the atlas material plus everything the runtime needs to place the card.\n/// Created by the Flatpack baker (Editor menu, Flatpack, Impostor Baker), one per source model.\n/// \u003C/summary\u003E\n/// \u003Cremarks\u003E\n/// \u003Cpara\u003EUse it with \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E, which swaps between the real model and the card by distance.\u003C/para\u003E\n/// \u003Cpara\u003E\n/// To use the card as the last LOD of a \u003Cc\u003E.vmdl\u003C/c\u003E instead, add a LOD to the model in ModelDoc whose only mesh is a\n/// single quad (any size, centred on the model) with this impostor\u0027s material, UVs (0,0) top-left to (1,1)\n/// bottom-right. The shader ignores the quad\u0027s positions and rebuilds the card from its UVs, the bake bounds and the\n/// instance transform, so the quad\u0027s size and orientation don\u0027t matter. Flatpack doesn\u0027t write \u003Cc\u003E.vmdl\u003C/c\u003E files for you.\n/// \u003C/para\u003E\n/// \u003C/remarks\u003E\n[AssetType( Name = \u0022Flatpack Impostor\u0022, Extension = \u0022fpack\u0022, Category = \u0022Rendering\u0022 )]\npublic sealed class FlatpackImpostor : GameResource\n{\n\t/// \u003Csummary\u003E\n\t/// The model this impostor was baked from.\n\t/// \u003C/summary\u003E\n\t[Property] public Model SourceModel { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Material using \u003Cc\u003Eshaders/flatpack_impostor.shader\u003C/c\u003E with the baked atlases.\n\t/// \u003C/summary\u003E\n\t[Property] public Material Material { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// How the frames are spread over view directions.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public FlatpackLayout Layout { get; set; } = FlatpackLayout.Hemisphere;\n\n\t/// \u003Csummary\u003E\n\t/// Frames per atlas side. The atlas holds GridSize x GridSize frames.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public int GridSize { get; set; } = 8;\n\n\t/// \u003Csummary\u003E\n\t/// Atlas width and height in pixels.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public int AtlasSize { get; set; } = 2048;\n\n\t/// \u003Csummary\u003E\n\t/// Centre of the baked bounding sphere, in the model\u0027s object space (the pivot offset).\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public Vector3 Center { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Radius of the baked bounding sphere, in object space units.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public float Radius { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003E\n\t/// The source model\u0027s render bounds at bake time.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public BBox Bounds { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// How much alpha the baker had to add back at mip level 2 to keep thin parts covered (1 = none). High values mean\n\t/// twigs, spindles or wire mesh that the card keeps only from further away; the scene optimiser warns about them.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public float ThinDetail { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003E\n\t/// Quality level the bake was made at (Low, Medium, High, Ultra or Custom).\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public string QualityLevel { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Nearest distance, in bounding radii, from which the baker measured the card matching its model: at most 15% of\n\t/// pixels clearly different from there out to 16 radii. 0 when not measured, -1 when it never got there.\n\t/// \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E\u0027s automatic swap distance never goes closer than this.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public float HoldsUpFrom { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Share of the covered texels that are only partly covered (0 to 1). Above \u003Cc\u003E0.4\u003C/c\u003E the bake is treated as\n\t/// see-through detail (wire mesh, twigs, nets) and drawn with a lowered alpha cut the baker fits to the model.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public float SeeThrough { get; set; }\n\n\tModel _cardModel;\n\tMaterial _cardMaterial;\n\n\t/// \u003Csummary\u003E\n\t/// The shared card model for this impostor: one quad with the impostor material. Built on first use and reused\n\t/// by every \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E that points here, so identical cards can be instanced by the engine.\n\t/// \u003C/summary\u003E\n\t[JsonIgnore, Hide]\n\tpublic Model CardModel\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( _cardModel is not null \u0026\u0026 _cardMaterial == Material )\n\t\t\t\treturn _cardModel;\n\n\t\t\t_cardMaterial = Material;\n\t\t\t_cardModel = FlatpackCard.Build( this );\n\t\t\treturn _cardModel;\n\t\t}\n\t}\n\n\tTexture[] _atlases;\n\tMaterial _atlasMaterial;\n\n\t/// \u003Csummary\u003E\n\t/// Tell texture streaming this impostor\u0027s atlases are on screen and needed at full size.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// The streamer can\u0027t judge the card\u0027s real texel density: the card shows one frame (1/GridSize of the atlas)\n\t/// across the whole quad, and the shader picks UVs itself. Left alone it kept a 2048 atlas at 512 or 256 and the\n\t/// impostor looked blurred. \u003Csee cref=\u0022FlatpackSystem\u0022/\u003E calls this every frame for impostors with a visible card.\n\t/// \u003C/remarks\u003E\n\tpublic void MarkAtlasesUsed()\n\t{\n\t\tif ( Material is null )\n\t\t\treturn;\n\n\t\tif ( _atlases is null || _atlasMaterial != Material )\n\t\t{\n\t\t\t_atlasMaterial = Material;\n\t\t\t// Bakes from before iteration 8 use the combined normal and depth atlas; later ones the separate pair.\n\t\t\t_atlases = new[] { \u0022g_tFlatpackAlbedo\u0022, \u0022g_tFlatpackNormalDepth\u0022, \u0022g_tFlatpackNormal\u0022, \u0022g_tFlatpackDepth\u0022, \u0022g_tFlatpackDepth8\u0022, \u0022g_tFlatpackDepthMin\u0022, \u0022g_tFlatpackFrames\u0022, \u0022g_tFlatpackRoughnessAo\u0022 }\n\t\t\t\t.Select( Material.GetTexture )\n\t\t\t\t.Where( t =\u003E t is not null )\n\t\t\t\t.ToArray();\n\t\t}\n\n\t\tforeach ( var texture in _atlases )\n\t\t\ttexture.MarkUsed( AtlasSize );\n\t}\n\n\tprotected override void PostReload()\n\t{\n\t\tbase.PostReload();\n\n\t\t// Rebuild the card next time it\u0027s asked for, in case a re-bake changed the bounds.\n\t\t_cardModel = null;\n\t\t_atlases = null;\n\t}\n}\n"},{"Ident":"anakin.flatpack","Path":"FlatpackRenderer.cs","FileName":"FlatpackRenderer.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace Flatpack;\r\n\r\n/// \u003Csummary\u003E\r\n/// One mesh LOD in a \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E chain: a model and the camera distance it takes over from.\r\n/// \u003C/summary\u003E\r\npublic sealed class FlatpackLodLevel\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// The model to show in this band. Usually a lower-poly version of the source.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public Model Model { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Camera distance where this level replaces the one before it, in world units.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 0, 20000 )] public float Distance { get; set; } = 1000.0f;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Shows a model up close and its Flatpack impostor card far away.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\r\n/// Put this next to a \u003Csee cref=\u0022ModelRenderer\u0022/\u003E and assign a baked \u003Csee cref=\u0022FlatpackImpostor\u0022/\u003E. The renderer\r\n/// doesn\u0027t tick on its own: \u003Csee cref=\u0022FlatpackSystem\u0022/\u003E checks distances for every renderer in one batched,\r\n/// time-sliced pass, so thousands of instances cost one loop instead of thousands of updates.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// All cards made from the same impostor share one model and material, so the engine can instance them.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// LODs: the source \u003Cc\u003E.vmdl\u003C/c\u003E\u0027s own LODs keep working up close, because this component never touches the\r\n/// \u003Csee cref=\u0022ModelRenderer\u0022/\u003E until the swap. For separate model files per detail level, fill \u003Csee cref=\u0022Levels\u0022/\u003E:\r\n/// the renderer swaps the source\u0027s model by distance (with the same hysteresis), then hands over to the card past\r\n/// the swap distance. With no \u003Csee cref=\u0022ModelRenderer\u0022/\u003E at all the card shows at every distance.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// Alternative without this component: make the card the last LOD of the \u003Cc\u003E.vmdl\u003C/c\u003E (see \u003Csee cref=\u0022FlatpackImpostor\u0022/\u003E).\r\n/// That gives you engine LOD switching for free, but no hysteresis and no cross-fade.\r\n/// \u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\n[Title( \u0022Flatpack Renderer\u0022 ), Category( \u0022Rendering\u0022 ), Icon( \u0022view_in_ar\u0022 )]\r\npublic sealed class FlatpackRenderer : Component, Component.ExecuteInEditor\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// What to draw.\r\n\t/// \u003C/summary\u003E\r\n\tpublic enum DisplayMode\r\n\t{\r\n\t\t/// \u003Csummary\u003EModel inside \u003Csee cref=\u0022EffectiveSwapDistance\u0022/\u003E, card beyond it.\u003C/summary\u003E\r\n\t\tAuto,\r\n\t\t/// \u003Csummary\u003EAlways the real model.\u003C/summary\u003E\r\n\t\tForceModel,\r\n\t\t/// \u003Csummary\u003EAlways the card. Useful for checking a bake.\u003C/summary\u003E\r\n\t\tForceCard,\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The baked impostor to show at distance. Left empty, the renderer uses any baked impostor whose source model\r\n\t/// matches the \u003Csee cref=\u0022Source\u0022/\u003E renderer\u0027s model, so adding the component after a bake needs no setup.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public FlatpackImpostor Impostor { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The renderer for the real model. Found on this GameObject if left empty.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public ModelRenderer Source { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Pick the swap distance from the object\u0027s size: the card takes over once the bounding sphere is about\r\n\t/// 1/\u003Csee cref=\u0022AutoDistanceFactor\u0022/\u003E of the distance across, roughly 5% of a 1080p screen\u0027s height at 70 degrees FOV.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public bool AutoSwapDistance { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Auto mode swaps as soon as the card looks like the model: at the distance the baker measured\r\n\t/// (\u003Csee cref=\u0022FlatpackImpostor.HoldsUpFrom\u0022/\u003E, calibrated by eye), never closer than \u003Csee cref=\u0022MinimumSwapRadii\u0022/\u003E.\r\n\t/// Measured in iteration 6: a 534,000-triangle tree forest drew in 7 to 13 ms swapping at 8 radii against 25 to 28 ms\r\n\t/// at 30. Turn it off to swap at \u003Csee cref=\u0022AutoDistanceFactor\u0022/\u003E instead (bakes that weren\u0027t measured always do).\r\n\t/// \u003C/summary\u003E\r\n\t[Property, ShowIf( nameof( AutoSwapDistance ), true )] public bool SwapWhenItLooksRight { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The closest an early swap may happen, in bounding radii. Up close a card covers many pixels and costs more to\r\n\t/// draw, so very light models can end up slower as cards than as models.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 2, 30 ), ShowIf( nameof( SwapWhenItLooksRight ), true )] public float MinimumSwapRadii { get; set; } = 6.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Auto mode swaps at this many bounding-sphere radii from the camera when it isn\u0027t swapping early (or the bake\r\n\t/// wasn\u0027t measured), or where the bake holds up if that\u0027s further.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 5, 100 ), ShowIf( nameof( AutoSwapDistance ), true )] public float AutoDistanceFactor { get; set; } = 30.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Camera distance where the card takes over, in world units, when \u003Csee cref=\u0022AutoSwapDistance\u0022/\u003E is off.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 0, 20000 ), ShowIf( nameof( AutoSwapDistance ), false )] public float SwapDistance { get; set; } = 2000.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The swap distance in use: \u003Csee cref=\u0022SwapDistance\u0022/\u003E, or the automatic one, then scaled by the global\r\n\t/// \u003Csee cref=\u0022FlatpackSystem.Level\u0022/\u003E (a game\u0027s graphics setting).\r\n\t/// \u003C/summary\u003E\r\n\tpublic float EffectiveSwapDistance\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar (levelScale, levelMinimum) = FlatpackSystem.LevelScaling;\r\n\t\t\tif ( float.IsPositiveInfinity( levelScale ) )\r\n\t\t\t\treturn float.PositiveInfinity;\r\n\r\n\t\t\tif ( !AutoSwapDistance || Impostor is null )\r\n\t\t\t\treturn SwapDistance * levelScale;\r\n\r\n\t\t\tvar scale = WorldScale;\r\n\t\t\tvar radius = Impostor.Radius * MathF.Max( scale.x, MathF.Max( scale.y, scale.z ) );\r\n\t\t\tvar measured = Impostor.HoldsUpFrom \u003E 0.0f;\r\n\t\t\tvar factor = SwapWhenItLooksRight \u0026\u0026 measured\r\n\t\t\t\t? MathF.Max( Impostor.HoldsUpFrom, MinimumSwapRadii )\r\n\t\t\t\t: MathF.Max( AutoDistanceFactor, Impostor.HoldsUpFrom );\r\n\r\n\t\t\t// A lower graphics level swaps earlier, but never closer than its own minimum.\r\n\t\t\tfactor *= levelScale;\r\n\t\t\tif ( levelMinimum \u003E 0.0f )\r\n\t\t\t\tfactor = MathF.Max( factor, levelMinimum );\r\n\t\t\treturn radius * factor;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Width of the dead band around \u003Csee cref=\u0022SwapDistance\u0022/\u003E. The card appears past SwapDistance \u002B Hysteresis / 2\r\n\t/// and the model returns inside SwapDistance - Hysteresis / 2, so a camera hovering at the boundary doesn\u0027t flicker.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 0, 2000 )] public float Hysteresis { get; set; } = 200.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Dither the card in and out over \u003Csee cref=\u0022FadeTime\u0022/\u003E instead of popping. The model stays visible under the\r\n\t/// card until the fade-in finishes, so the swap reads as a dissolve.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public bool CrossFade { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Seconds a cross-fade takes.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Range( 0, 2 ), ShowIf( nameof( CrossFade ), true )] public float FadeTime { get; set; } = 0.35f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Override the distance check.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public DisplayMode Mode { get; set; } = DisplayMode.Auto;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Optional mesh LODs between the source model and the card, nearest first. Each replaces the source renderer\u0027s\r\n\t/// model past its \u003Csee cref=\u0022FlatpackLodLevel.Distance\u0022/\u003E. Distances past \u003Csee cref=\u0022SwapDistance\u0022/\u003E never show.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public List\u003CFlatpackLodLevel\u003E Levels { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Index into \u003Csee cref=\u0022Levels\u0022/\u003E currently on the source renderer, or -1 for the source\u0027s own model.\r\n\t/// \u003C/summary\u003E\r\n\tpublic int CurrentLevel { get; private set; } = -1;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// True while the card is the visible representation (including while it fades in).\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool ShowingCard { get; private set; }\r\n\r\n\tSceneObject _card;\r\n\tModel _baseModel;      // the source renderer\u0027s own model, restored when the chain steps back to level -1\r\n\tFlatpackImpostor _countedImpostor;   // impostor this renderer\u0027s visible card is counted against in the system\r\n\tfloat _fade;           // 0 = card hidden, 1 = card fully shown\r\n\tfloat _fadeTarget;\r\n\tinternal int SystemIndex = -1;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tSource ??= GameObject.Components.Get\u003CModelRenderer\u003E();\r\n\t\t_baseModel = Source.IsValid() ? Source.Model : null;\r\n\t\tImpostor ??= FindImpostorFor( _baseModel );\r\n\t\tCurrentLevel = -1;\r\n\t\tTransform.OnTransformChanged \u002B= SyncTransform;\r\n\r\n\t\tScene.GetSystem\u003CFlatpackSystem\u003E()?.Register( this );\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tTransform.OnTransformChanged -= SyncTransform;\r\n\t\tScene.GetSystem\u003CFlatpackSystem\u003E()?.Unregister( this );\r\n\r\n\t\tDestroyCard();\r\n\t\tSetLevel( -1 );\r\n\t\tSetSourceVisible( true );\r\n\t\tShowingCard = false;\r\n\t\t_fade = _fadeTarget = 0.0f;\r\n\t}\r\n\r\n\tprotected override void OnValidate()\r\n\t{\r\n\t\t// An edited impostor or a changed mode should show up straight away in the editor.\r\n\t\tDestroyCard();\r\n\t\tSetLevel( -1 );\r\n\t\tShowingCard = false;\r\n\t\t_fade = _fadeTarget = 0.0f;\r\n\t\tSetSourceVisible( true );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Called by \u003Csee cref=\u0022FlatpackSystem\u0022/\u003E with the squared distance from the camera to this object.\r\n\t/// Decides which representation should be showing; the fade itself runs in \u003Csee cref=\u0022TickFade\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tinternal bool Evaluate( float distanceSquared )\r\n\t{\r\n\t\tUpdateLevel( distanceSquared );\r\n\r\n\t\tvar wantCard = Mode switch\r\n\t\t{\r\n\t\t\tDisplayMode.ForceCard =\u003E true,\r\n\t\t\tDisplayMode.ForceModel =\u003E false,\r\n\t\t\t_ =\u003E WantCardAt( distanceSquared ),\r\n\t\t};\r\n\r\n\t\t// Card-only setup: nothing else to show.\r\n\t\tif ( !Source.IsValid() \u0026\u0026 Mode != DisplayMode.ForceModel )\r\n\t\t\twantCard = true;\r\n\r\n\t\tif ( wantCard \u0026\u0026 Impostor?.Material is null )\r\n\t\t\twantCard = false;\r\n\r\n\t\tif ( wantCard == ShowingCard )\r\n\t\t\treturn false;\r\n\r\n\t\tShowingCard = wantCard;\r\n\t\t_fadeTarget = wantCard ? 1.0f : 0.0f;\r\n\r\n\t\tif ( wantCard )\r\n\t\t\tEnsureCard();\r\n\r\n\t\tif ( !CrossFade || FadeTime \u003C= 0.0f )\r\n\t\t\t_fade = _fadeTarget;\r\n\r\n\t\tApply();\r\n\r\n\t\t// The system keeps calling TickFade while this returns true.\r\n\t\treturn _fade != _fadeTarget;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Step the mesh LOD chain toward the level for this distance, one band edge at a time, each with the same\r\n\t/// hysteresis band as the card swap.\r\n\t/// \u003C/summary\u003E\r\n\tvoid UpdateLevel( float distanceSquared )\r\n\t{\r\n\t\tif ( Levels is null || Levels.Count == 0 || !Source.IsValid() || Mode != DisplayMode.Auto )\r\n\t\t\treturn;\r\n\r\n\t\tvar level = Math.Min( CurrentLevel, Levels.Count - 1 );\r\n\t\tvar half = Hysteresis * 0.5f;\r\n\r\n\t\twhile ( level \u002B 1 \u003C Levels.Count \u0026\u0026 Beyond( distanceSquared, Levels[level \u002B 1].Distance \u002B half ) )\r\n\t\t\tlevel\u002B\u002B;\r\n\r\n\t\twhile ( level \u003E= 0 \u0026\u0026 !Beyond( distanceSquared, Levels[level].Distance - half ) )\r\n\t\t\tlevel--;\r\n\r\n\t\tSetLevel( level );\r\n\t}\r\n\r\n\tstatic bool Beyond( float distanceSquared, float distance )\r\n\t{\r\n\t\tdistance = MathF.Max( distance, 0.0f );\r\n\t\treturn distanceSquared \u003E distance * distance;\r\n\t}\r\n\r\n\tvoid SetLevel( int level )\r\n\t{\r\n\t\tif ( level == CurrentLevel || !Source.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tCurrentLevel = level;\r\n\t\tvar model = level \u003E= 0 ? Levels[level].Model : _baseModel;\r\n\t\tif ( model is not null )\r\n\t\t\tSource.Model = model;\r\n\r\n\t\t// A new model can mean a new scene object, which starts visible.\r\n\t\tSetSourceVisible( !ShowingCard || _fade \u003C 1.0f );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The first baked impostor whose source is \u003Cparamref name=\u0022model\u0022/\u003E, or null.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static FlatpackImpostor FindImpostorFor( Model model )\r\n\t{\r\n\t\tif ( model is null )\r\n\t\t\treturn null;\r\n\r\n\t\treturn ResourceLibrary.GetAll\u003CFlatpackImpostor\u003E()\r\n\t\t\t.FirstOrDefault( x =\u003E x.SourceModel is not null \u0026\u0026 x.SourceModel.ResourcePath == model.ResourcePath );\r\n\t}\r\n\r\n\tbool WantCardAt( float distanceSquared )\r\n\t{\r\n\t\t// Hysteresis: the threshold depends on which side we\u0027re already on.\r\n\t\tvar swap = EffectiveSwapDistance;\r\n\t\tvar threshold = ShowingCard\r\n\t\t\t? swap - Hysteresis * 0.5f\r\n\t\t\t: swap \u002B Hysteresis * 0.5f;\r\n\r\n\t\tthreshold = MathF.Max( threshold, 0.0f );\r\n\t\treturn distanceSquared \u003E threshold * threshold;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Advances a running cross-fade. Returns false when it has finished.\r\n\t/// \u003C/summary\u003E\r\n\tinternal bool TickFade( float delta )\r\n\t{\r\n\t\tvar step = FadeTime \u003E 0.0f ? delta / FadeTime : 1.0f;\r\n\t\t_fade = _fadeTarget \u003E _fade\r\n\t\t\t? MathF.Min( _fade \u002B step, _fadeTarget )\r\n\t\t\t: MathF.Max( _fade - step, _fadeTarget );\r\n\r\n\t\tApply();\r\n\t\treturn _fade != _fadeTarget;\r\n\t}\r\n\r\n\tvoid Apply()\r\n\t{\r\n\t\tif ( _card.IsValid() )\r\n\t\t{\r\n\t\t\t_card.RenderingEnabled = _fade \u003E 0.0f;\r\n\t\t\tCountCard( _fade \u003E 0.0f ? Impostor : null );\r\n\r\n\t\t\t// 1 draws every pixel; anything lower dithers the card out (see flatpack_impostor.shader).\r\n\t\t\t_card.Attributes.Set( \u0022FlatpackFade\u0022, _fade );\r\n\t\t}\r\n\r\n\t\t// The model stays up until the card has fully faded in, and comes back the moment we head toward it.\r\n\t\tSetSourceVisible( !ShowingCard || _fade \u003C 1.0f );\r\n\t}\r\n\r\n\tvoid SetSourceVisible( bool visible )\r\n\t{\r\n\t\tif ( Source.IsValid() \u0026\u0026 Source.SceneObject.IsValid() )\r\n\t\t\tSource.SceneObject.RenderingEnabled = visible \u0026\u0026 Source.Enabled;\r\n\t}\r\n\r\n\tvoid EnsureCard()\r\n\t{\r\n\t\tif ( _card.IsValid() || Impostor is null )\r\n\t\t\treturn;\r\n\r\n\t\t_card = new SceneObject( Scene.SceneWorld, Impostor.CardModel, WorldTransform );\r\n\t\t_card.Tags.SetFrom( GameObject.Tags );\r\n\t\t_card.RenderingEnabled = false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Keep \u003Csee cref=\u0022FlatpackSystem\u0022/\u003E\u0027s count of visible cards per impostor current, so it streams their atlases in.\r\n\t/// \u003C/summary\u003E\r\n\tvoid CountCard( FlatpackImpostor visibleImpostor )\r\n\t{\r\n\t\tif ( visibleImpostor == _countedImpostor )\r\n\t\t\treturn;\r\n\r\n\t\tvar system = Scene?.GetSystem\u003CFlatpackSystem\u003E();\r\n\t\tsystem?.CardVisibilityChanged( _countedImpostor, false );\r\n\t\tsystem?.CardVisibilityChanged( visibleImpostor, true );\r\n\t\t_countedImpostor = visibleImpostor;\r\n\t}\r\n\r\n\tvoid DestroyCard()\r\n\t{\r\n\t\tCountCard( null );\r\n\t\t_card?.Delete();\r\n\t\t_card = null;\r\n\t}\r\n\r\n\tvoid SyncTransform()\r\n\t{\r\n\t\tif ( _card.IsValid() )\r\n\t\t\t_card.Transform = WorldTransform;\r\n\t}\r\n}\r\n"},{"Ident":"anakin.flatpack","Path":"Editor/FlatpackBakeSettings.cs","FileName":"FlatpackBakeSettings.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":429259,"IsPrivate":false,"Code":"using Sandbox;\n\nnamespace Flatpack.EditorTools;\n\n/// \u003Csummary\u003E\n/// Atlas sizes the baker offers.\n/// \u003C/summary\u003E\npublic enum FlatpackAtlasSize\n{\n\t[Title( \u0022512\u0022 )] Size512 = 512,\n\t[Title( \u00221024\u0022 )] Size1024 = 1024,\n\t[Title( \u00222048\u0022 )] Size2048 = 2048,\n\t[Title( \u00223072\u0022 )] Size3072 = 3072,\n\t[Title( \u00224096\u0022 )] Size4096 = 4096,\n}\n\n/// \u003Csummary\u003E\n/// Quality levels for a bake. Each one sets the number of views, the atlas size and the depth search steps, picked\n/// from the iteration 4 measurements (see \u003Cc\u003EBenchmarks/iteration-4-quality-levels\u003C/c\u003E).\n/// \u003C/summary\u003E\npublic enum FlatpackQuality\n{\n\t[Title( \u0022Low\u0022 ), Description( \u002212 x 12 views, 1024 atlas, 8 search steps. About 5 MB. For props you only see from far away (past about 16 bounding radii).\u0022 )]\n\tLow,\n\n\t[Title( \u0022Medium\u0022 ), Description( \u002212 x 12 views, 2048 atlas, 16 search steps. About 19 MB. The default: holds up from about 4 to 6 bounding radii.\u0022 )]\n\tMedium,\n\n\t[Title( \u0022High\u0022 ), Description( \u002224 x 24 views, 4096 atlas, 16 search steps. About 75 MB. Same as Medium up close, closer to the real model past 8 bounding radii.\u0022 )]\n\tHigh,\n\n\t[Title( \u0022Ultra\u0022 ), Description( \u002212 x 12 views, 3072 atlas, 32 search steps. About 42 MB and about 1.5x the GPU cost of Medium. The sharpest level up close: for props that swap near the camera.\u0022 )]\n\tUltra,\n\n\t[Title( \u0022Auto\u0022 ), Description( \u0022Bake at Low, measure the card against the model at the target distance, and step up a level until it holds up. Slower to bake; saves memory on models that look fine at a lower level.\u0022 )]\n\tAuto,\n\n\t[Title( \u0022Custom\u0022 ), Description( \u0022Set views, atlas size and search steps yourself.\u0022 )]\n\tCustom,\n}\n\n/// \u003Csummary\u003E\n/// Everything the baker needs to know besides the model. Shown in the bake window with a control sheet.\n/// \u003C/summary\u003E\npublic sealed class FlatpackBakeSettings\n{\n\t/// \u003Csummary\u003E\n\t/// How good the impostor looks, against memory and GPU cost. Pick Custom to set the numbers yourself.\n\t/// \u003C/summary\u003E\n\t[Property] public FlatpackQuality Quality { get; set; } = FlatpackQuality.Medium;\n\n\t/// \u003Csummary\u003E\n\t/// For Auto: the distance, in bounding radii, where the card has to match the model (from there outward). Set it to\n\t/// the swap distance you\u0027ll use; the Flatpack Renderer\u0027s automatic one is 30.\n\t/// \u003C/summary\u003E\n\t[Property, Range( 2, 60 ), Step( 1 ), ShowIf( nameof( Quality ), FlatpackQuality.Auto )] public float AutoTargetRadii { get; set; } = 30.0f;\n\n\t/// \u003Csummary\u003E\n\t/// Measure the finished bake against the model (a second or two) and store where it holds up. The Flatpack Renderer\n\t/// then never swaps closer than that, and the inspector shows it.\n\t/// \u003C/summary\u003E\n\t[Property] public bool MeasureQuality { get; set; } = true;\n\n\t/// \u003Csummary\u003E\n\t/// Frames per atlas side. 12 gives 144 views, 15 degrees apart on the hemisphere. 8 ghosts visibly; past 12, more\n\t/// views only help a little unless the atlas grows with them. Used when Quality is Custom.\n\t/// \u003C/summary\u003E\n\t[Property, Range( 4, 32 ), Step( 1 ), ShowIf( nameof( Quality ), FlatpackQuality.Custom )] public int GridSize { get; set; } = 12;\n\n\t/// \u003Csummary\u003E\n\t/// Atlas width and height. Each frame gets AtlasSize / GridSize pixels. Used when Quality is Custom.\n\t/// \u003C/summary\u003E\n\t[Property, ShowIf( nameof( Quality ), FlatpackQuality.Custom )] public FlatpackAtlasSize AtlasSize { get; set; } = FlatpackAtlasSize.Size2048;\n\n\t/// \u003Csummary\u003E\n\t/// Depth search steps per frame, written into the material (you can change it there later). The biggest single\n\t/// lever on both quality and GPU cost: 8 ghosts thin parts, 32 costs about 1.5x of 16. Used when Quality is Custom.\n\t/// \u003C/summary\u003E\n\t[Property, Range( 0, 32 ), Step( 1 ), ShowIf( nameof( Quality ), FlatpackQuality.Custom )] public int SearchSteps { get; set; } = 16;\n\n\t/// \u003Csummary\u003E\n\t/// The views, atlas size and search steps this bake will use: the quality level\u0027s, or the custom values.\n\t/// \u003C/summary\u003E\n\tpublic (int GridSize, FlatpackAtlasSize AtlasSize, int SearchSteps) Resolve() =\u003E Quality switch\n\t{\n\t\tFlatpackQuality.Low =\u003E (12, FlatpackAtlasSize.Size1024, 8),\n\t\tFlatpackQuality.Medium =\u003E (12, FlatpackAtlasSize.Size2048, 16),\n\t\tFlatpackQuality.High =\u003E (24, FlatpackAtlasSize.Size4096, 16),\n\t\tFlatpackQuality.Ultra =\u003E (12, FlatpackAtlasSize.Size3072, 32),\n\t\tFlatpackQuality.Auto =\u003E (12, FlatpackAtlasSize.Size1024, 8),\n\t\t_ =\u003E (GridSize, AtlasSize, SearchSteps),\n\t};\n\n\t/// \u003Csummary\u003E\n\t/// A copy of these settings at another quality level.\n\t/// \u003C/summary\u003E\n\tpublic FlatpackBakeSettings WithQuality( FlatpackQuality quality )\n\t{\n\t\tvar copy = (FlatpackBakeSettings)MemberwiseClone();\n\t\tcopy.Quality = quality;\n\t\treturn copy;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Which view directions get frames.\n\t/// \u003C/summary\u003E\n\t[Property] public FlatpackLayout Layout { get; set; } = FlatpackLayout.Hemisphere;\n\n\t/// \u003Csummary\u003E\n\t/// Turn the card around the object\u0027s up axis only, instead of facing the camera fully.\n\t/// Always on for \u003Csee cref=\u0022FlatpackLayout.Cylinder\u0022/\u003E.\n\t/// \u003C/summary\u003E\n\t[Property] public bool ZLockedCard { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Also bake roughness and ambient occlusion. Without it the impostor uses roughness 1 and no AO.\n\t/// \u003C/summary\u003E\n\t[Property] public bool BakeRoughnessAo { get; set; } = true;\n\n\t/// \u003Csummary\u003E\n\t/// Also bake how much sky each part of the model sees (lit from eight directions, shadowed against unshadowed).\n\t/// The impostor material\u0027s Sky Occlusion slider then darkens enclosed parts, which helps dense foliage keep its dark\n\t/// interior. Off by default: the real model gets no such occlusion in-engine, so the card matches it better without.\n\t/// Needs \u003Csee cref=\u0022BakeRoughnessAo\u0022/\u003E; roughly triples bake time.\n\t/// \u003C/summary\u003E\n\t[Property, ShowIf( nameof( BakeRoughnessAo ), true )] public bool BakeSelfShadow { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Render each view this many times larger and average it down. Parts thinner than a texel (wire mesh, twigs) then\n\t/// bake as partial coverage instead of filling in or vanishing. 2 measured 2 to 3 points fewer wrong pixels up close\n\t/// for about a third more bake time; 4 adds little. Fine wire mesh (fences) gets worse either way.\n\t/// \u003C/summary\u003E\n\t[Property, Range( 1, 4 ), Step( 1 )] public int Supersample { get; set; } = 2;\n\n\t/// \u003Csummary\u003E\n\t/// How many texels to grow colour out past the silhouette, inside each frame, so mipmaps don\u0027t pull in\n\t/// the transparent background as a dark halo. 0 fills the whole frame.\n\t/// \u003C/summary\u003E\n\t[Property, Range( 0, 64 ), Step( 1 )] public int EdgePadding { get; set; } = 16;\n\n\t/// \u003Csummary\u003E\n\t/// Frame every view to the model\u0027s own outline and depth in that view instead of its whole bounding sphere. A tall\n\t/// statue fills its views instead of a third of them, and the depth spans only the surfaces the view sees, so the\n\t/// same atlas holds more texels and finer depth on the object.\n\t/// \u003C/summary\u003E\n\t[Property] public bool FitEachView { get; set; } = true;\n\n\t/// \u003Csummary\u003E\n\t/// Bits of depth per texel: 8 (BC4, half a byte a texel) or 16 (R16, 2 bytes). Measured in iteration 8 on the test set\n\t/// at Medium: 16 bits had 2.3 points fewer wrong pixels at 2 radii and 1.6 at 4, for more video memory (about\n\t/// 27 MB against 19 at Medium, 60 against 42 at Ultra).\n\t/// \u003C/summary\u003E\n\t[Property, Range( 8, 16 ), Step( 8 )] public int DepthBits { get; set; } = 8;\n\n\t/// \u003Csummary\u003E\n\t/// Folder for the outputs, relative to the project\u0027s Assets folder (for example \u003Cc\u003Eflatpack/trees\u003C/c\u003E).\n\t/// Empty puts them next to the source model, or in \u003Cc\u003Eflatpack/\u003C/c\u003E when the source isn\u0027t in this project.\n\t/// \u003C/summary\u003E\n\t[Property] public string OutputFolder { get; set; } = \u0022\u0022;\n}\n"},{"Ident":"anakin.flatpack","Path":"Code/FlatpackMath.cs","FileName":"FlatpackMath.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace Flatpack;\r\n\r\n/// \u003Csummary\u003E\r\n/// How the baked views (\u0022frames\u0022) are spread over the directions an impostor can be seen from.\r\n/// \u003C/summary\u003E\r\npublic enum FlatpackLayout\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Hemi-octahedral: the upper half of the sphere. Best for anything that stands on the ground and is never\r\n\t/// seen from below (trees, rocks, props). All N x N frames go to the half you actually see.\r\n\t/// \u003C/summary\u003E\r\n\tHemisphere = 0,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Full octahedral: every direction. For floating or tumbling objects that can be seen from below.\r\n\t/// \u003C/summary\u003E\r\n\tSphere = 1,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// All N x N frames on the horizon, evenly around Z, and the card only turns around Z. Highest yaw\r\n\t/// resolution, but it can\u0027t show the top of the object. Classic tree billboard behaviour.\r\n\t/// \u003C/summary\u003E\r\n\tCylinder = 2,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The frame layout maths shared by the baker and the runtime. \u003Cc\u003Eflatpack_impostor.shader\u003C/c\u003E has an HLSL\r\n/// copy of the same functions; they must stay identical or the shader samples the wrong frame.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// Coordinates are Source/s\u0026amp;box: \u002BX forward, \u002BY left, \u002BZ up. A frame direction points from the object\r\n/// toward the camera that captured it.\r\n/// \u003C/remarks\u003E\r\npublic static class FlatpackMath\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Maps a unit direction to the [-1, 1] square for the given layout (not used by \u003Csee cref=\u0022FlatpackLayout.Cylinder\u0022/\u003E).\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Vector2 EncodeOct( Vector3 d, FlatpackLayout layout )\r\n\t{\r\n\t\tif ( layout == FlatpackLayout.Hemisphere )\r\n\t\t{\r\n\t\t\t// Nothing below the horizon was baked, so clamp onto the horizon ring.\r\n\t\t\td.z = MathF.Max( d.z, 0.0f );\r\n\t\t\td = (d \u002B new Vector3( 0, 0, 1e-5f )).Normal;\r\n\t\t\tvar px = d.x / (MathF.Abs( d.x ) \u002B MathF.Abs( d.y ) \u002B d.z);\r\n\t\t\tvar py = d.y / (MathF.Abs( d.x ) \u002B MathF.Abs( d.y ) \u002B d.z);\r\n\r\n\t\t\t// Rotate the octahedron\u0027s top half by 45 degrees so it fills the whole square.\r\n\t\t\treturn new Vector2( px \u002B py, px - py );\r\n\t\t}\r\n\r\n\t\tvar sum = MathF.Abs( d.x ) \u002B MathF.Abs( d.y ) \u002B MathF.Abs( d.z );\r\n\t\tvar p = new Vector2( d.x / sum, d.y / sum );\r\n\t\tif ( d.z \u003C 0.0f )\r\n\t\t{\r\n\t\t\t// Fold the lower half out into the square\u0027s corners.\r\n\t\t\tp = new Vector2( (1.0f - MathF.Abs( p.y )) * Sign( p.x ), (1.0f - MathF.Abs( p.x )) * Sign( p.y ) );\r\n\t\t}\r\n\r\n\t\treturn p;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Inverse of \u003Csee cref=\u0022EncodeOct\u0022/\u003E. Returns a unit direction.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Vector3 DecodeOct( Vector2 o, FlatpackLayout layout )\r\n\t{\r\n\t\tif ( layout == FlatpackLayout.Hemisphere )\r\n\t\t{\r\n\t\t\tvar px = (o.x \u002B o.y) * 0.5f;\r\n\t\t\tvar py = (o.x - o.y) * 0.5f;\r\n\t\t\treturn new Vector3( px, py, 1.0f - MathF.Abs( px ) - MathF.Abs( py ) ).Normal;\r\n\t\t}\r\n\r\n\t\tvar d = new Vector3( o.x, o.y, 1.0f - MathF.Abs( o.x ) - MathF.Abs( o.y ) );\r\n\t\tif ( d.z \u003C 0.0f )\r\n\t\t{\r\n\t\t\td.x = (1.0f - MathF.Abs( o.y )) * Sign( o.x );\r\n\t\t\td.y = (1.0f - MathF.Abs( o.x )) * Sign( o.y );\r\n\t\t}\r\n\r\n\t\treturn d.Normal;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The direction frame (\u003Cparamref name=\u0022x\u0022/\u003E, \u003Cparamref name=\u0022y\u0022/\u003E) of an \u003Cparamref name=\u0022n\u0022/\u003E x \u003Cparamref name=\u0022n\u0022/\u003E grid is baked from.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Vector3 FrameDirection( int x, int y, int n, FlatpackLayout layout )\r\n\t{\r\n\t\tif ( layout == FlatpackLayout.Cylinder )\r\n\t\t{\r\n\t\t\tvar index = y * n \u002B x;\r\n\t\t\tvar yaw = index / (float)(n * n) * MathF.Tau;\r\n\t\t\treturn new Vector3( MathF.Cos( yaw ), MathF.Sin( yaw ), 0.0f );\r\n\t\t}\r\n\r\n\t\t// Frame centres sit on the grid vertices (not cell centres), so the outer frames land exactly on the\r\n\t\t// horizon and the shader can blend between them without extrapolating.\r\n\t\tvar o = new Vector2( x, y ) / (n - 1) * 2.0f - 1.0f;\r\n\t\treturn DecodeOct( o, layout );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The camera basis a frame is captured with. Right is image \u002BX, up is image -Y.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void FrameBasis( Vector3 d, out Vector3 right, out Vector3 up )\r\n\t{\r\n\t\tvar refUp = MathF.Abs( d.z ) \u003C 0.999f ? Vector3.Up : Vector3.Forward;\r\n\t\tright = Vector3.Cross( -d, refUp ).Normal;\r\n\t\tup = Vector3.Cross( right, -d );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Camera rotation for a frame. Equivalent to \u003Csee cref=\u0022FrameBasis\u0022/\u003E (checked in the editor: \u003Cc\u003ERotation.LookAt\u003C/c\u003E\r\n\t/// produces the same right and up vectors).\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Rotation FrameRotation( Vector3 d )\r\n\t{\r\n\t\tFrameBasis( d, out _, out var up );\r\n\t\treturn Rotation.LookAt( -d, up );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Smallest sphere-ish bound we bake around: the box centre and its half diagonal. Not the tightest sphere,\r\n\t/// but stable and cheap, and the shader only needs the frames to contain the object.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static (Vector3 Center, float Radius) BoundingSphere( BBox bounds )\r\n\t{\r\n\t\treturn (bounds.Center, MathF.Max( bounds.Size.Length * 0.5f, 0.01f ));\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A bounding sphere fitted to points (Ritter\u0027s method): pick two far-apart points, then grow the sphere for any point\r\n\t/// left outside. Within a few percent of the minimal sphere and never smaller than needed.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static (Vector3 Center, float Radius) BoundingSphere( IReadOnlyList\u003CVector3\u003E points )\r\n\t{\r\n\t\tif ( points is null || points.Count == 0 )\r\n\t\t\treturn (Vector3.Zero, 0.01f);\r\n\r\n\t\tvar a = points[0];\r\n\t\tvar b = Farthest( points, a );\r\n\t\tvar c = Farthest( points, b );\r\n\r\n\t\tvar center = (b \u002B c) * 0.5f;\r\n\t\tvar radius = b.Distance( c ) * 0.5f;\r\n\r\n\t\tforeach ( var p in points )\r\n\t\t{\r\n\t\t\tvar d = p.Distance( center );\r\n\t\t\tif ( d \u003C= radius )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Move the centre toward p just enough that the new sphere still touches the old one\u0027s far side.\r\n\t\t\tvar newRadius = (radius \u002B d) * 0.5f;\r\n\t\t\tcenter \u002B= (p - center) * ((newRadius - radius) / d);\r\n\t\t\tradius = newRadius;\r\n\t\t}\r\n\r\n\t\t// A hair of slack so silhouette texels on the sphere\u0027s rim aren\u0027t clipped by the frame edge.\r\n\t\treturn (center, MathF.Max( radius * 1.01f, 0.01f ));\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Metres in one world unit. s\u0026amp;box units are inches, as in Source; the iteration 6 distance sheet (\u00226 radii (21 m)\u0022\r\n\t/// for a tree 140 units in radius) used the same conversion.\r\n\t/// \u003C/summary\u003E\r\n\tpublic const float MetresPerUnit = 0.0254f;\r\n\r\n\t/// \u003Csummary\u003EA distance in world units, in metres.\u003C/summary\u003E\r\n\tpublic static float ToMetres( float units ) =\u003E units * MetresPerUnit;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A distance in world units as a reader wants it next to bounding radii: one decimal under 10 m, whole metres from\r\n\t/// there (\u00222.5 m\u0022, \u002221 m\u0022). The inspector, the bake check summary and the benchmark sheets all use this.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string FormatMetres( float units )\r\n\t{\r\n\t\tvar metres = ToMetres( units );\r\n\t\treturn metres \u003C 10.0f\r\n\t\t\t? $\u0022{metres.ToString( \u00220.0\u0022, System.Globalization.CultureInfo.InvariantCulture )} m\u0022\r\n\t\t\t: $\u0022{metres.ToString( \u00220\u0022, System.Globalization.CultureInfo.InvariantCulture )} m\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Columns the frame lookup texture reserves for image rectangles; the depth ranges sit this far to the right.\r\n\t/// The shader\u0027s \u003Cc\u003EFlatpackFrameFit\u003C/c\u003E reads texel (x, y) and (x \u002B 32, y).\r\n\t/// \u003C/summary\u003E\r\n\tpublic const int FrameFitColumns = 32;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The bounds one frame was baked with: where the object sits inside the bounding sphere\u0027s frame, and how deep.\r\n\t/// \u003C/summary\u003E\r\n\tpublic struct FrameFit\r\n\t{\r\n\t\t/// \u003Csummary\u003ETop-left corner of the frame\u0027s image rectangle, in the sphere-sized frame\u0027s 0..1 image coordinates (v down).\u003C/summary\u003E\r\n\t\tpublic Vector2 Min;\r\n\r\n\t\t/// \u003Csummary\u003EBottom-right corner of the image rectangle.\u003C/summary\u003E\r\n\t\tpublic Vector2 Max;\r\n\r\n\t\t/// \u003Csummary\u003EHeight of the nearest surface along the frame direction (toward its camera), in bounding radii.\u003C/summary\u003E\r\n\t\tpublic float Near;\r\n\r\n\t\t/// \u003Csummary\u003EHeight of the furthest surface, in bounding radii. Always below \u003Csee cref=\u0022Near\u0022/\u003E.\u003C/summary\u003E\r\n\t\tpublic float Far;\r\n\r\n\t\t/// \u003Csummary\u003EThe whole bounding sphere: what every frame used before iteration 8.\u003C/summary\u003E\r\n\t\tpublic static FrameFit Sphere =\u003E new() { Min = Vector2.Zero, Max = Vector2.One, Near = 1.0f, Far = -1.0f };\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Fit one frame to the points it sees: their image rectangle, grown by \u003Cparamref name=\u0022padTexels\u0022/\u003E of a\r\n\t/// \u003Cparamref name=\u0022cellTexels\u0022/\u003E-wide frame on every side, and their depth range. Rounded outward to the 8-bit steps the\r\n\t/// lookup texture stores, so the baker renders with exactly the bounds the shader reads back.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static FrameFit FitFrame( IReadOnlyList\u003CVector3\u003E points, Vector3 center, float radius, Vector3 dir, int cellTexels, int padTexels )\r\n\t{\r\n\t\tif ( points is null || points.Count == 0 )\r\n\t\t\treturn FrameFit.Sphere;\r\n\r\n\t\tFrameBasis( dir, out var right, out var up );\r\n\t\tfloat minU = float.MaxValue, maxU = float.MinValue, minV = float.MaxValue, maxV = float.MinValue;\r\n\t\tfloat near = float.MinValue, far = float.MaxValue;\r\n\r\n\t\tforeach ( var p in points )\r\n\t\t{\r\n\t\t\tvar rel = p - center;\r\n\t\t\tvar u = 0.5f \u002B Vector3.Dot( rel, right ) / (2.0f * radius);\r\n\t\t\tvar v = 0.5f - Vector3.Dot( rel, up ) / (2.0f * radius);\r\n\t\t\tvar h = Vector3.Dot( rel, dir ) / radius;\r\n\t\t\tminU = MathF.Min( minU, u ); maxU = MathF.Max( maxU, u );\r\n\t\t\tminV = MathF.Min( minV, v ); maxV = MathF.Max( maxV, v );\r\n\t\t\tnear = MathF.Max( near, h ); far = MathF.Min( far, h );\r\n\t\t}\r\n\r\n\t\t// Padding: the rectangle maps onto cellTexels texels, padTexels of which on each side stay outside the object.\r\n\t\tvar grow = cellTexels \u003E 2 * padTexels \u002B 1 ? cellTexels / (float)(cellTexels - 2 * padTexels) : 1.0f;\r\n\t\tvar padU = (maxU - minU) * (grow - 1.0f) * 0.5f;\r\n\t\tvar padV = (maxV - minV) * (grow - 1.0f) * 0.5f;\r\n\r\n\t\tvar fit = new FrameFit\r\n\t\t{\r\n\t\t\tMin = new Vector2( minU - padU, minV - padV ),\r\n\t\t\tMax = new Vector2( maxU \u002B padU, maxV \u002B padV ),\r\n\t\t\tNear = near,\r\n\t\t\tFar = far,\r\n\t\t};\r\n\t\treturn DecodeFrameFit( EncodeFrameFit( fit ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The two lookup texels for a frame: (left, top, right) and (bottom, near, far), each value one byte, rounded outward\r\n\t/// so the stored bounds always contain the real ones. Alpha stays 255 (the PNG path drops colour under low alpha).\r\n\t/// \u003C/summary\u003E\r\n\tpublic static (Color32 Rect, Color32 Depth) EncodeFrameFit( FrameFit fit )\r\n\t{\r\n\t\tint Down( float v ) =\u003E Math.Clamp( (int)MathF.Floor( v * 255.0f \u002B 1e-3f ), 0, 255 );\r\n\t\tint Up( float v ) =\u003E Math.Clamp( (int)MathF.Ceiling( v * 255.0f - 1e-3f ), 0, 255 );\r\n\r\n\t\tint minU = Down( fit.Min.x ), maxU = Up( fit.Max.x );\r\n\t\tint minV = Down( fit.Min.y ), maxV = Up( fit.Max.y );\r\n\t\tint near = Up( (fit.Near \u002B 1.0f) * 0.5f ), far = Down( (fit.Far \u002B 1.0f) * 0.5f );\r\n\r\n\t\t// Never let a frame collapse (a flat panel seen edge-on, a plane seen face-on): keep at least two steps each way.\r\n\t\t(minU, maxU) = Widen( minU, maxU );\r\n\t\t(minV, maxV) = Widen( minV, maxV );\r\n\t\t(far, near) = Widen( far, near );\r\n\r\n\t\treturn (new Color32( (byte)minU, (byte)minV, (byte)maxU, 255 ), new Color32( (byte)maxV, (byte)near, (byte)far, 255 ));\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The bounds two lookup texels describe. Matches the shader\u0027s \u003Cc\u003EFlatpackFrameFit\u003C/c\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static FrameFit DecodeFrameFit( (Color32 Rect, Color32 Depth) texels ) =\u003E new()\r\n\t{\r\n\t\tMin = new Vector2( texels.Rect.r / 255.0f, texels.Rect.g / 255.0f ),\r\n\t\tMax = new Vector2( texels.Rect.b / 255.0f, texels.Depth.r / 255.0f ),\r\n\t\tNear = texels.Depth.g / 255.0f * 2.0f - 1.0f,\r\n\t\tFar = texels.Depth.b / 255.0f * 2.0f - 1.0f,\r\n\t};\r\n\r\n\tstatic (int Low, int High) Widen( int low, int high )\r\n\t{\r\n\t\twhile ( high - low \u003C 2 )\r\n\t\t{\r\n\t\t\tif ( high \u003C 255 ) high\u002B\u002B;\r\n\t\t\tif ( high - low \u003C 2 \u0026\u0026 low \u003E 0 ) low--;\r\n\t\t}\r\n\t\treturn (low, high);\r\n\t}\r\n\r\n\tstatic Vector3 Farthest( IReadOnlyList\u003CVector3\u003E points, Vector3 from )\r\n\t{\r\n\t\tvar best = from;\r\n\t\tvar bestDistance = -1.0f;\r\n\t\tforeach ( var p in points )\r\n\t\t{\r\n\t\t\tvar d = p.DistanceSquared( from );\r\n\t\t\tif ( d \u003E bestDistance )\r\n\t\t\t{\r\n\t\t\t\tbestDistance = d;\r\n\t\t\t\tbest = p;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn best;\r\n\t}\r\n\r\n\tstatic float Sign( float v ) =\u003E v \u003E= 0.0f ? 1.0f : -1.0f;\r\n}\r\n"},{"Ident":"anakin.flatpack","Path":"Editor/FlatpackSceneOptimizer.cs","FileName":"FlatpackSceneOptimizer.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":429259,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Threading;\r\nusing Editor;\r\nusing Sandbox;\r\n\r\nnamespace Flatpack.EditorTools;\r\n\r\n/// \u003Csummary\u003E\r\n/// One window to Flatpack a whole scene: lists every model in it, recommends which ones are worth an impostor and why,\r\n/// bakes what\u0027s missing and adds a \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E to every instance in one undoable step.\r\n/// Open it from Editor \u003E Flatpack \u003E Optimize Scene.\r\n/// \u003C/summary\u003E\r\npublic sealed class FlatpackSceneOptimizer : Dialog\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// How strongly the window suggests Flatpacking a model.\r\n\t/// \u003C/summary\u003E\r\n\tpublic enum Advice\r\n\t{\r\n\t\tRecommended,\r\n\t\tLowGain,\r\n\t\tSkip,\r\n\t\tDone,\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Everything the window knows about one model used in the scene.\r\n\t/// \u003C/summary\u003E\r\n\tpublic sealed class Entry\r\n\t{\r\n\t\tpublic Model Model { get; init; }\r\n\t\tpublic List\u003CModelRenderer\u003E Renderers { get; } = new();\r\n\t\tpublic int Triangles { get; init; }\r\n\t\tpublic int Flatpacked { get; set; }\r\n\t\tpublic FlatpackImpostor Impostor { get; set; }\r\n\t\tpublic Advice Advice { get; set; }\r\n\t\tpublic string Reason { get; set; }\r\n\t\tpublic bool Selected { get; set; }\r\n\r\n\t\tpublic int Instances =\u003E Renderers.Count;\r\n\t\tpublic long TotalTriangles =\u003E (long)Triangles * Instances;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Below this many triangles a card rarely beats the model: the card\u0027s pixel shader costs about the same as a cheap\r\n\t/// mesh (see Benchmarks/iteration-2: a 1,832-triangle plant broke even).\r\n\t/// \u003C/summary\u003E\r\n\tpublic const int MinTriangles = 1500;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A cheap model can still be worth it in bulk: this many triangles across all its instances.\r\n\t/// \u003C/summary\u003E\r\n\tpublic const long MinTotalTriangles = 200_000;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Above this per-mip alpha correction at mip 2, a bake has a lot of thin detail (twigs, wire mesh) that the card\r\n\t/// keeps only from further away. The model is still recommended, with a warning.\r\n\t/// \u003C/summary\u003E\r\n\tpublic const float ThinDetailWarning = 1.25f;\r\n\r\n\tstatic FlatpackSceneOptimizer _current;\r\n\r\n\treadonly List\u003CEntry\u003E _entries = new();\r\n\treadonly ScrollArea _list;\r\n\treadonly Label _summary;\r\n\treadonly Label _status;\r\n\treadonly Button _apply;\r\n\treadonly Button _remove;\r\n\tCancellationTokenSource _cancel;\r\n\tScene _scannedScene;\r\n\r\n\t[Menu( \u0022Editor\u0022, \u0022Flatpack/Optimize Scene\u0022 )]\r\n\tpublic static void Open()\r\n\t{\r\n\t\tif ( _current is not { IsValid: true } )\r\n\t\t\t_current = new FlatpackSceneOptimizer();\r\n\r\n\t\t_current.Scan();\r\n\t\t_current.Show();\r\n\t}\r\n\r\n\tFlatpackSceneOptimizer() : base( EditorWindow )\r\n\t{\r\n\t\tWindow.Title = \u0022Flatpack: Optimize Scene\u0022;\r\n\t\tWindow.SetWindowIcon( \u0022forest\u0022 );\r\n\t\tWindow.Size = new Vector2( 860, 620 );\r\n\t\tWindow.StateCookie = \u0022FlatpackSceneOptimizer\u0022;\r\n\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Margin = 12;\r\n\t\tLayout.Spacing = 8;\r\n\r\n\t\tvar title = Layout.Add( new Label( \u0022Every model in the open scene, with a recommendation for each.\u0022 ) );\r\n\t\ttitle.SetStyles( \u0022font-weight: bold;\u0022 );\r\n\t\tLayout.Add( new Label( \u0022Ticked models get baked (if needed) and a Flatpack Renderer on every instance, as one step you can undo with Ctrl\u002BZ.\u0022 ) { WordWrap = true } );\r\n\r\n\t\tvar tools = Layout.AddRow();\r\n\t\ttools.Spacing = 6;\r\n\t\ttools.Add( new Button( \u0022Select Recommended\u0022, \u0022auto_awesome\u0022 ) { Clicked = () =\u003E SelectWhere( e =\u003E e.Advice == Advice.Recommended ) } );\r\n\t\ttools.Add( new Button( \u0022Select All\u0022, \u0022select_all\u0022 ) { Clicked = () =\u003E SelectWhere( e =\u003E e.Advice != Advice.Done \u0026\u0026 e.Advice != Advice.Skip ) } );\r\n\t\ttools.Add( new Button( \u0022Select None\u0022, \u0022deselect\u0022 ) { Clicked = () =\u003E SelectWhere( _ =\u003E false ) } );\r\n\t\ttools.AddStretchCell();\r\n\t\ttools.Add( new Button( \u0022Rescan\u0022, \u0022refresh\u0022 ) { Clicked = Scan } );\r\n\r\n\t\t_list = Layout.Add( new ScrollArea( this ), 1 );\r\n\t\t_list.Canvas = new Widget( _list );\r\n\t\t_list.Canvas.Layout = Layout.Column();\r\n\t\t_list.Canvas.Layout.Spacing = 2;\r\n\r\n\t\t_summary = Layout.Add( new Label( \u0022\u0022 ) { WordWrap = true } );\r\n\t\t_status = Layout.Add( new Label( \u0022\u0022 ) { WordWrap = true } );\r\n\r\n\t\t// Quality for the bakes this window makes. Shares the bake window\u0027s saved settings, so both stay in step.\r\n\t\tvar settings = EditorCookie.Get( \u0022Flatpack.BakeSettings\u0022, new FlatpackBakeSettings() ) ?? new FlatpackBakeSettings();\r\n\t\tvar serializedSettings = settings.GetSerialized();\r\n\t\tserializedSettings.OnPropertyChanged \u002B= _ =\u003E\r\n\t\t{\r\n\t\t\tEditorCookie.Set( \u0022Flatpack.BakeSettings\u0022, settings );\r\n\t\t\tUpdateSummary();\r\n\t\t};\r\n\t\tvar quality = new ControlSheet();\r\n\t\tquality.AddObject( serializedSettings, p =\u003E p.Name == nameof( FlatpackBakeSettings.Quality ) );\r\n\t\tLayout.Add( quality );\r\n\r\n\t\tvar footer = Layout.AddRow();\r\n\t\tfooter.Spacing = 8;\r\n\t\t_remove = footer.Add( new Button( \u0022Remove Flatpack from Scene\u0022, \u0022undo\u0022 )\r\n\t\t{\r\n\t\t\tToolTip = \u0022Remove every Flatpack Renderer in the open scene, as one undoable step. Baked files stay.\u0022,\r\n\t\t\tClicked = RemoveAll,\r\n\t\t} );\r\n\t\tfooter.AddStretchCell();\r\n\t\t_apply = footer.Add( new Button.Primary( \u0022Flatpack Selected\u0022, \u0022view_in_ar\u0022 ) { Clicked = OnApplyClicked } );\r\n\t}\r\n\r\n\tpublic override void OnDestroyed()\r\n\t{\r\n\t\t_cancel?.Cancel();\r\n\t\tif ( _current == this )\r\n\t\t\t_current = null;\r\n\r\n\t\tbase.OnDestroyed();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Gather every model in the open scene and decide what to recommend for each.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void Scan()\r\n\t{\r\n\t\t// Keep the user\u0027s ticks across rescans (the scene may have changed under us, e.g. after Ctrl\u002BZ).\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tvar previous = scene == _scannedScene\r\n\t\t\t? _entries.Where( e =\u003E e.Advice != Advice.Done \u0026\u0026 e.Advice != Advice.Skip ).ToDictionary( e =\u003E e.Model, e =\u003E e.Selected )\r\n\t\t\t: new Dictionary\u003CModel, bool\u003E();\r\n\t\t_scannedScene = scene;\r\n\t\t_entries.Clear();\r\n\t\tif ( scene is null )\r\n\t\t{\r\n\t\t\tRebuild();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tforeach ( var renderer in scene.GetAllComponents\u003CModelRenderer\u003E() )\r\n\t\t{\r\n\t\t\tvar model = renderer.Model;\r\n\t\t\tif ( model is null || model.IsError )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar entry = _entries.FirstOrDefault( e =\u003E e.Model == model );\r\n\t\t\tif ( entry is null )\r\n\t\t\t{\r\n\t\t\t\tentry = new Entry { Model = model, Triangles = Lod0Triangles( model ) };\r\n\t\t\t\t_entries.Add( entry );\r\n\t\t\t}\r\n\r\n\t\t\tentry.Renderers.Add( renderer );\r\n\t\t\tif ( renderer.Components.Get\u003CFlatpackRenderer\u003E().IsValid() )\r\n\t\t\t\tentry.Flatpacked\u002B\u002B;\r\n\t\t}\r\n\r\n\t\tforeach ( var entry in _entries )\r\n\t\t{\r\n\t\t\tentry.Impostor = FlatpackRenderer.FindImpostorFor( entry.Model );\r\n\t\t\tRecommend( entry );\r\n\t\t\tentry.Selected = entry.Advice != Advice.Done \u0026\u0026 entry.Advice != Advice.Skip\r\n\t\t\t\t\u0026\u0026 ( previous.TryGetValue( entry.Model, out var was ) ? was : entry.Advice == Advice.Recommended );\r\n\t\t}\r\n\r\n\t\t// Biggest wins first.\r\n\t\t_entries.Sort( ( a, b ) =\u003E b.TotalTriangles.CompareTo( a.TotalTriangles ) );\r\n\t\tRebuild();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The recommendation rules. Kept in one place so the reasons in the list always match the decision.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Csummary\u003E\r\n\t/// Typical time to bake one model (including the quality check after it) and the video memory its atlases use, by\r\n\t/// quality level. Measured on an RTX 3090 in iterations 4 to 6; Auto bakes Low first and climbs, so it\u0027s a range.\r\n\t/// \u003C/summary\u003E\r\n\tstatic (float Seconds, float MaxSeconds, float Megabytes) BakeCost( FlatpackBakeSettings settings )\r\n\t{\r\n\t\t// Supersampling renders each view larger; 2 is the default the times below were measured at.\r\n\t\tvar ss = Math.Clamp( settings.Supersample, 1, 4 );\r\n\t\tvar time = ss switch { 1 =\u003E 0.75f, 2 =\u003E 1.0f, 3 =\u003E 1.3f, _ =\u003E 1.7f };\r\n\t\tif ( settings.BakeSelfShadow )\r\n\t\t\ttime *= 3.0f;\r\n\r\n\t\treturn settings.Quality switch\r\n\t\t{\r\n\t\t\tFlatpackQuality.Low =\u003E (3 * time, 3 * time, 4),\r\n\t\t\tFlatpackQuality.Medium =\u003E (6 * time, 6 * time, 16),\r\n\t\t\tFlatpackQuality.High =\u003E (16 * time, 16 * time, 64),\r\n\t\t\tFlatpackQuality.Ultra =\u003E (12 * time, 12 * time, 36),\r\n\t\t\tFlatpackQuality.Auto =\u003E (4 * time, 40 * time, 16),\r\n\t\t\t_ =\u003E CustomCost( settings, time ),\r\n\t\t};\r\n\t}\r\n\r\n\tstatic (float, float, float) CustomCost( FlatpackBakeSettings settings, float time )\r\n\t{\r\n\t\tvar (grid, atlas, _) = settings.Resolve();\r\n\t\tvar size = (float)(int)atlas;\r\n\t\tvar megabytes = size * size * 3 * 4 / 3 / (1024 * 1024);\r\n\t\tvar seconds = (2.0f \u002B 4.0f * (size / 2048) * (size / 2048) * MathF.Max( 1, grid / 12.0f )) * time;\r\n\t\treturn (seconds, seconds, megabytes);\r\n\t}\r\n\r\n\tstatic string Duration( float seconds ) =\u003E seconds \u003C 90 ? $\u0022{seconds:0} s\u0022 : $\u0022{seconds / 60:0.#} min\u0022;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Triangles in the model\u0027s most detailed LOD, every mesh and draw call. \u003Cc\u003EModel.GetIndexCountForLod\u003C/c\u003E counts only\r\n\t/// the first draw call, which undercounts models with several materials (a tree\u0027s bark and leaves: 293,609 of 534,024).\r\n\t/// \u003C/summary\u003E\r\n\tpublic static int Lod0Triangles( Model model )\r\n\t{\r\n\t\tvar meshes = model.MeshInfo?.Meshes;\r\n\t\tif ( meshes is null || meshes.Length == 0 )\r\n\t\t\treturn model.GetIndexCountForLod( 0 ) / 3;\r\n\r\n\t\tvar total = meshes.Where( m =\u003E ((long)m.LodMask \u0026 1) != 0 ).Sum( m =\u003E (long)m.Triangles );\r\n\t\treturn (int)Math.Min( total \u003E 0 ? total : model.GetIndexCountForLod( 0 ) / 3, int.MaxValue );\r\n\t}\r\n\r\n\tstatic void Recommend( Entry e )\r\n\t{\r\n\t\tif ( e.Flatpacked == e.Instances )\r\n\t\t{\r\n\t\t\te.Advice = Advice.Done;\r\n\t\t\te.Reason = \u0022Already Flatpacked.\u0022;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( e.Renderers.Any( r =\u003E r is SkinnedModelRenderer ) )\r\n\t\t{\r\n\t\t\te.Advice = Advice.Skip;\r\n\t\t\te.Reason = \u0022Animated: a card can\u0027t show animation.\u0022;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( e.Triangles \u003C= 12 )\r\n\t\t{\r\n\t\t\te.Advice = Advice.Skip;\r\n\t\t\te.Reason = \u0022Already as simple as a card.\u0022;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( e.Model.RenderBounds.Size.Length \u003C 8.0f )\r\n\t\t{\r\n\t\t\te.Advice = Advice.Skip;\r\n\t\t\te.Reason = \u0022Tiny: it\u0027s only a few pixels before a card would take over.\u0022;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( e.Triangles \u003C MinTriangles \u0026\u0026 e.TotalTriangles \u003C MinTotalTriangles )\r\n\t\t{\r\n\t\t\te.Advice = Advice.LowGain;\r\n\t\t\te.Reason = $\u0022Cheap already ({e.Triangles:n0} triangles): a card saves little here.\u0022;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\te.Advice = Advice.Recommended;\r\n\t\te.Reason = e.Triangles \u003E= MinTriangles\r\n\t\t\t? $\u0022{e.Triangles:n0} triangles each: big saving at a distance.\u0022\r\n\t\t\t: $\u0022{e.TotalTriangles:n0} triangles across {e.Instances} copies.\u0022;\r\n\r\n\t\tif ( e.Impostor is not null \u0026\u0026 e.Impostor.ThinDetail \u003E ThinDetailWarning )\r\n\t\t\te.Reason \u002B= \u0022 Has thin parts (twigs, wire): check it holds up at your swap distance.\u0022;\r\n\t}\r\n\r\n\tvoid SelectWhere( Func\u003CEntry, bool\u003E predicate )\r\n\t{\r\n\t\tforeach ( var e in _entries )\r\n\t\t\te.Selected = predicate( e );\r\n\r\n\t\tRebuild();\r\n\t}\r\n\r\n\tvoid Rebuild()\r\n\t{\r\n\t\tif ( !IsValid )\r\n\t\t\treturn;\r\n\r\n\t\t_list.Canvas.Layout.Clear( true );\r\n\r\n\t\tif ( _entries.Count == 0 )\r\n\t\t{\r\n\t\t\t_list.Canvas.Layout.Add( new Label( SceneEditorSession.Active is null ? \u0022Open a scene first.\u0022 : \u0022No models in this scene.\u0022 ) );\r\n\t\t}\r\n\r\n\t\tvar header = _list.Canvas.Layout.AddRow();\r\n\t\theader.Spacing = 8;\r\n\t\tforeach ( var (text, width) in new[] { (\u0022\u0022, 24), (\u0022Model\u0022, 230), (\u0022Copies\u0022, 60), (\u0022Triangles\u0022, 90), (\u0022Status\u0022, 90), (\u0022Recommendation\u0022, 330) } )\r\n\t\t{\r\n\t\t\tvar l = header.Add( new Label( text ) { FixedWidth = width } );\r\n\t\t\tl.SetStyles( \u0022font-weight: bold;\u0022 );\r\n\t\t}\r\n\r\n\t\tforeach ( var entry in _entries )\r\n\t\t{\r\n\t\t\tvar row = _list.Canvas.Layout.AddRow();\r\n\t\t\trow.Spacing = 8;\r\n\r\n\t\t\tvar check = row.Add( new Checkbox( \u0022\u0022 ) { FixedWidth = 24, Value = entry.Selected } );\r\n\t\t\tcheck.Enabled = entry.Advice != Advice.Done;\r\n\t\t\tcheck.StateChanged = _ =\u003E { entry.Selected = check.Value; UpdateSummary(); };\r\n\r\n\t\t\trow.Add( new Label( entry.Model.ResourceName ) { FixedWidth = 230, ToolTip = entry.Model.ResourcePath } );\r\n\t\t\trow.Add( new Label( $\u0022{entry.Instances}\u0022 ) { FixedWidth = 60 } );\r\n\t\t\trow.Add( new Label( $\u0022{entry.Triangles:n0}\u0022 ) { FixedWidth = 90 } );\r\n\r\n\t\t\tvar status = entry.Flatpacked == entry.Instances ? \u0022Done\u0022\r\n\t\t\t\t: entry.Flatpacked \u003E 0 ? $\u0022{entry.Flatpacked}/{entry.Instances}\u0022\r\n\t\t\t\t: entry.Impostor is not null ? \u0022Baked\u0022 : \u0022Not baked\u0022;\r\n\t\t\trow.Add( new Label( status ) { FixedWidth = 90 } );\r\n\r\n\t\t\tvar advice = row.Add( new Label( entry.Reason ) { FixedWidth = 330, WordWrap = true } );\r\n\t\t\tadvice.Color = entry.Advice switch\r\n\t\t\t{\r\n\t\t\t\tAdvice.Recommended =\u003E Theme.Green,\r\n\t\t\t\tAdvice.LowGain =\u003E Theme.Yellow,\r\n\t\t\t\tAdvice.Skip =\u003E Theme.TextLight.WithAlpha( 0.6f ),\r\n\t\t\t\t_ =\u003E Theme.TextLight,\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\t_list.Canvas.Layout.AddStretchCell();\r\n\t\tUpdateSummary();\r\n\t}\r\n\r\n\tvoid UpdateSummary()\r\n\t{\r\n\t\tvar picked = _entries.Where( e =\u003E e.Selected \u0026\u0026 e.Advice != Advice.Done ).ToArray();\r\n\t\tvar instances = picked.Sum( e =\u003E e.Instances - e.Flatpacked );\r\n\t\tvar before = picked.Sum( e =\u003E (long)e.Triangles * (e.Instances - e.Flatpacked) );\r\n\t\tvar toBake = picked.Count( e =\u003E e.Impostor is null );\r\n\r\n\t\tif ( picked.Length == 0 )\r\n\t\t{\r\n\t\t\t_summary.Text = \u0022Nothing selected.\u0022;\r\n\t\t\t_apply.Enabled = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar settings = EditorCookie.Get( \u0022Flatpack.BakeSettings\u0022, new FlatpackBakeSettings() ) ?? new FlatpackBakeSettings();\r\n\t\tvar cost = BakeCost( settings );\r\n\t\tvar bakeText = toBake == 0\r\n\t\t\t? \u0022Everything is baked already.\u0022\r\n\t\t\t: cost.MaxSeconds \u003E cost.Seconds\r\n\t\t\t\t? $\u0022{toBake} to bake at {settings.Quality}: about {Duration( toBake * cost.Seconds )} (up to {Duration( toBake * cost.MaxSeconds )}), adding about {toBake * cost.Megabytes:n0} MB of video memory.\u0022\r\n\t\t\t\t: $\u0022{toBake} to bake at {settings.Quality}: about {Duration( toBake * cost.Seconds )}, adding about {toBake * cost.Megabytes:n0} MB of video memory.\u0022;\r\n\r\n\t\t_summary.Text = $\u0022{picked.Length} model(s), {instances:n0} object(s). Far away they draw {instances * 2:n0} triangles instead of {before:n0}. {bakeText} \u0022 \u002B\r\n\t\t\t\u0022Gains are biggest where you can see far (forests, open maps); in closed rooms the cost is usually lights and draw calls.\u0022;\r\n\r\n\t\t_apply.Enabled = picked.Length \u003E 0 \u0026\u0026 _cancel is null;\r\n\t}\r\n\r\n\tasync void OnApplyClicked()\r\n\t{\r\n\t\tif ( _cancel is not null )\r\n\t\t{\r\n\t\t\t_cancel.Cancel();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// The scene may have changed since the list was built (edits, Ctrl\u002BZ): start from what\u0027s there now.\r\n\t\tScan();\r\n\t\tvar picked = _entries.Where( e =\u003E e.Selected \u0026\u0026 e.Advice != Advice.Done ).ToList();\r\n\t\tif ( picked.Count == 0 )\r\n\t\t\treturn;\r\n\r\n\t\t_cancel = new CancellationTokenSource();\r\n\t\t_apply.Text = \u0022Cancel\u0022;\r\n\t\t_apply.Enabled = true;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// Bake what\u0027s missing first; scene changes come after, in one undo step.\r\n\t\t\tvar toBake = picked.Where( e =\u003E e.Impostor is null ).ToList();\r\n\t\t\tvar settings = EditorCookie.Get( \u0022Flatpack.BakeSettings\u0022, new FlatpackBakeSettings() ) ?? new FlatpackBakeSettings();\r\n\r\n\t\t\tfor ( var i = 0; i \u003C toBake.Count; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar entry = toBake[i];\r\n\t\t\t\tvar index = i;\r\n\t\t\t\tvar result = await FlatpackBaker.BakeAsync( entry.Model, settings,\r\n\t\t\t\t\t( f, text ) =\u003E { if ( IsValid ) _status.Text = $\u0022Baking {entry.Model.ResourceName} ({index \u002B 1} of {toBake.Count}): {text}\u0022; },\r\n\t\t\t\t\t_cancel.Token );\r\n\t\t\t\tentry.Impostor = result.Impostor;\r\n\t\t\t}\r\n\r\n\t\t\tvar added = Apply( picked );\r\n\t\t\t_status.Text = $\u0022Added a Flatpack Renderer to {added:n0} object(s). Ctrl\u002BZ undoes it, or use Remove Flatpack from Scene.\u0022;\r\n\t\t}\r\n\t\tcatch ( OperationCanceledException )\r\n\t\t{\r\n\t\t\t_status.Text = \u0022Cancelled. Nothing in the scene changed; finished bakes are kept.\u0022;\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\t_status.Text = $\u0022Stopped: {ex.Message}\u0022;\r\n\t\t\tLog.Warning( $\u0022Flatpack: scene optimise stopped: {ex}\u0022 );\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t_cancel = null;\r\n\t\t\tif ( IsValid )\r\n\t\t\t{\r\n\t\t\t\t_apply.Text = \u0022Flatpack Selected\u0022;\r\n\t\t\t\tScan();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Add a Flatpack Renderer to every instance of the picked models that doesn\u0027t have one, as a single undo step.\r\n\t/// \u003C/summary\u003E\r\n\tstatic int Apply( IEnumerable\u003CEntry\u003E picked )\r\n\t{\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session is null )\r\n\t\t\treturn 0;\r\n\r\n\t\tvar targets = picked\r\n\t\t\t.Where( e =\u003E e.Impostor is not null )\r\n\t\t\t.SelectMany( e =\u003E e.Renderers.Select( r =\u003E (Renderer: r, e.Impostor) ) )\r\n\t\t\t.Where( x =\u003E x.Renderer.IsValid() \u0026\u0026 !x.Renderer.Components.Get\u003CFlatpackRenderer\u003E().IsValid() )\r\n\t\t\t.ToArray();\r\n\r\n\t\tif ( targets.Length == 0 )\r\n\t\t\treturn 0;\r\n\r\n\t\tusing var scene = session.Scene.Push();\r\n\t\tusing ( session.UndoScope( $\u0022Flatpack {targets.Length} Objects\u0022 ).WithComponentCreations().Push() )\r\n\t\t{\r\n\t\t\tforeach ( var (renderer, impostor) in targets )\r\n\t\t\t{\r\n\t\t\t\tvar flatpack = renderer.GameObject.Components.Create\u003CFlatpackRenderer\u003E();\r\n\t\t\t\tflatpack.Source = renderer;\r\n\t\t\t\tflatpack.Impostor = impostor;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tsession.HasUnsavedChanges = true;\r\n\t\treturn targets.Length;\r\n\t}\r\n\r\n\tvoid RemoveAll()\r\n\t{\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session is null )\r\n\t\t\treturn;\r\n\r\n\t\tvar all = session.Scene.GetAllComponents\u003CFlatpackRenderer\u003E().ToArray();\r\n\t\tif ( all.Length == 0 )\r\n\t\t{\r\n\t\t\t_status.Text = \u0022No Flatpack Renderers in this scene.\u0022;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tusing ( session.Scene.Push() )\r\n\t\tusing ( session.UndoScope( $\u0022Remove Flatpack from {all.Length} Objects\u0022 ).WithComponentDestructions( all ).Push() )\r\n\t\t{\r\n\t\t\tforeach ( var c in all )\r\n\t\t\t\tc.Destroy();\r\n\t\t}\r\n\r\n\t\tsession.HasUnsavedChanges = true;\r\n\t\t_status.Text = $\u0022Removed {all.Length:n0} Flatpack Renderer(s). Ctrl\u002BZ puts them back.\u0022;\r\n\t\tScan();\r\n\t}\r\n}\r\n"},{"Ident":"anakin.flatpack","Path":"FlatpackImpostor.cs","FileName":"FlatpackImpostor.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":429259,"IsPrivate":false,"Code":"using System.Linq;\nusing System.Text.Json.Serialization;\nusing Sandbox;\n\nnamespace Flatpack;\n\n/// \u003Csummary\u003E\n/// A baked impostor: the atlas material plus everything the runtime needs to place the card.\n/// Created by the Flatpack baker (Editor menu, Flatpack, Impostor Baker), one per source model.\n/// \u003C/summary\u003E\n/// \u003Cremarks\u003E\n/// \u003Cpara\u003EUse it with \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E, which swaps between the real model and the card by distance.\u003C/para\u003E\n/// \u003Cpara\u003E\n/// To use the card as the last LOD of a \u003Cc\u003E.vmdl\u003C/c\u003E instead, add a LOD to the model in ModelDoc whose only mesh is a\n/// single quad (any size, centred on the model) with this impostor\u0027s material, UVs (0,0) top-left to (1,1)\n/// bottom-right. The shader ignores the quad\u0027s positions and rebuilds the card from its UVs, the bake bounds and the\n/// instance transform, so the quad\u0027s size and orientation don\u0027t matter. Flatpack doesn\u0027t write \u003Cc\u003E.vmdl\u003C/c\u003E files for you.\n/// \u003C/para\u003E\n/// \u003C/remarks\u003E\n[AssetType( Name = \u0022Flatpack Impostor\u0022, Extension = \u0022fpack\u0022, Category = \u0022Rendering\u0022 )]\npublic sealed class FlatpackImpostor : GameResource\n{\n\t/// \u003Csummary\u003E\n\t/// The model this impostor was baked from.\n\t/// \u003C/summary\u003E\n\t[Property] public Model SourceModel { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Material using \u003Cc\u003Eshaders/flatpack_impostor.shader\u003C/c\u003E with the baked atlases.\n\t/// \u003C/summary\u003E\n\t[Property] public Material Material { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// How the frames are spread over view directions.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public FlatpackLayout Layout { get; set; } = FlatpackLayout.Hemisphere;\n\n\t/// \u003Csummary\u003E\n\t/// Frames per atlas side. The atlas holds GridSize x GridSize frames.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public int GridSize { get; set; } = 8;\n\n\t/// \u003Csummary\u003E\n\t/// Atlas width and height in pixels.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public int AtlasSize { get; set; } = 2048;\n\n\t/// \u003Csummary\u003E\n\t/// Centre of the baked bounding sphere, in the model\u0027s object space (the pivot offset).\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public Vector3 Center { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Radius of the baked bounding sphere, in object space units.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public float Radius { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003E\n\t/// The source model\u0027s render bounds at bake time.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public BBox Bounds { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// How much alpha the baker had to add back at mip level 2 to keep thin parts covered (1 = none). High values mean\n\t/// twigs, spindles or wire mesh that the card keeps only from further away; the scene optimiser warns about them.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public float ThinDetail { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003E\n\t/// Quality level the bake was made at (Low, Medium, High, Ultra or Custom).\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public string QualityLevel { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Nearest distance, in bounding radii, from which the baker measured the card matching its model: at most 15% of\n\t/// pixels clearly different from there out to 16 radii. 0 when not measured, -1 when it never got there.\n\t/// \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E\u0027s automatic swap distance never goes closer than this.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public float HoldsUpFrom { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Share of the covered texels that are only partly covered (0 to 1). Above \u003Cc\u003E0.4\u003C/c\u003E the bake is treated as\n\t/// see-through detail (wire mesh, twigs, nets) and drawn with a lowered alpha cut the baker fits to the model.\n\t/// \u003C/summary\u003E\n\t[Property, ReadOnly] public float SeeThrough { get; set; }\n\n\tModel _cardModel;\n\tMaterial _cardMaterial;\n\n\t/// \u003Csummary\u003E\n\t/// The shared card model for this impostor: one quad with the impostor material. Built on first use and reused\n\t/// by every \u003Csee cref=\u0022FlatpackRenderer\u0022/\u003E that points here, so identical cards can be instanced by the engine.\n\t/// \u003C/summary\u003E\n\t[JsonIgnore, Hide]\n\tpublic Model CardModel\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( _cardModel is not null \u0026\u0026 _cardMaterial == Material )\n\t\t\t\treturn _cardModel;\n\n\t\t\t_cardMaterial = Material;\n\t\t\t_cardModel = FlatpackCard.Build( this );\n\t\t\treturn _cardModel;\n\t\t}\n\t}\n\n\tTexture[] _atlases;\n\tMaterial _atlasMaterial;\n\n\t/// \u003Csummary\u003E\n\t/// Tell texture streaming this impostor\u0027s atlases are on screen and needed at full size.\n\t/// \u003C/summary\u003E\n\t/// \u003Cremarks\u003E\n\t/// The streamer can\u0027t judge the card\u0027s real texel density: the card shows one frame (1/GridSize of the atlas)\n\t/// across the whole quad, and the shader picks UVs itself. Left alone it kept a 2048 atlas at 512 or 256 and the\n\t/// impostor looked blurred. \u003Csee cref=\u0022FlatpackSystem\u0022/\u003E calls this every frame for impostors with a visible card.\n\t/// \u003C/remarks\u003E\n\tpublic void MarkAtlasesUsed()\n\t{\n\t\tif ( Material is null )\n\t\t\treturn;\n\n\t\tif ( _atlases is null || _atlasMaterial != Material )\n\t\t{\n\t\t\t_atlasMaterial = Material;\n\t\t\t// Bakes from before iteration 8 use the combined normal and depth atlas; later ones the separate pair.\n\t\t\t_atlases = new[] { \u0022g_tFlatpackAlbedo\u0022, \u0022g_tFlatpackNormalDepth\u0022, \u0022g_tFlatpackNormal\u0022, \u0022g_tFlatpackDepth\u0022, \u0022g_tFlatpackDepth8\u0022, \u0022g_tFlatpackDepthMin\u0022, \u0022g_tFlatpackFrames\u0022, \u0022g_tFlatpackRoughnessAo\u0022 }\n\t\t\t\t.Select( Material.GetTexture )\n\t\t\t\t.Where( t =\u003E t is not null )\n\t\t\t\t.ToArray();\n\t\t}\n\n\t\tforeach ( var texture in _atlases )\n\t\t\ttexture.MarkUsed( AtlasSize );\n\t}\n\n\tprotected override void PostReload()\n\t{\n\t\tbase.PostReload();\n\n\t\t// Rebuild the card next time it\u0027s asked for, in case a re-bake changed the bounds.\n\t\t_cardModel = null;\n\t\t_atlases = null;\n\t}\n}\n"}]}