using Godot; using System; using Rokojori.Extensions; namespace Rokojori { /** Reads a composited TextureStack back off the GPU and writes it to disk. Thin on purpose - the two halves it needs already existed before this system did: - Readback. Spike gate G3 found GetImage() works directly on a DrawableTexture2D, so none of the fallbacks the plan budgeted for (RenderingServer.Texture2DGet, a viewport quad) are needed. - Writing. Procedural/Baking/Textures.Save already handles png / jpg / webp / exr by extension. So baking really is the one-liner the research addendum predicted; what is left here is the editor bookkeeping (telling the filesystem a file appeared) and compositing into a throwaway target so that baking a stack does not need a TextureStackTexture2D to exist first. */ public static class TextureStackBaker { /** Composites a stack into a temporary DrawableTexture2D and reads it back. The target is local to this call, so nothing is cached and nothing holds VRAM afterwards - which is the point for a one-off 4k bake. Incremental redraw is off for the same reason: a checkpoint buffer here would be allocated and dropped immediately. */ public static Image Render( TextureStack stack, Vector2I size, TextureStackFormat format, Color clearColor, bool mipmaps ) { if ( stack == null || size.X <= 0 || size.Y <= 0 ) { return null; } var target = new DrawableTexture2D(); var context = new TextureStackCompositorContext(); context.stack = stack; context.size = size; context.format = format; context.clearColor = clearColor; context.mipmaps = mipmaps; context.fromLayer = 0; context.incrementalRedraw = false; context.targets.Add( target ); new TextureStackCompositor().Composite( context ); context.targets.Clear(); return target.GetImage(); } /** Writes an image to a project path and makes the editor notice it. Without the rescan the file exists but has no .import entry, so nothing can reference it until the editor is refocused - which looks exactly like the bake having failed. */ public static bool Save( Image image, string path, float quality = 1 ) { if ( image == null || string.IsNullOrEmpty( path ) ) { return false; } Textures.Save( image, path, quality ); Rescan(); return true; } public static void Rescan() { #if TOOLS if ( ! Engine.IsEditorHint() ) { return; } EditorInterface.Singleton.GetResourceFilesystem().Scan(); #endif } /** The baked file as an Image, or null when there is none yet. Two paths, because the file is a different thing in the editor than in an export: in the editor it is the png on disk, in an export it is the imported .ctex the png produced and the png itself may not be shipped at all. ResourceLoader finds the imported one; LoadFromFile is the editor and user:// case. */ public static Image Load( string path ) { if ( string.IsNullOrEmpty( path ) ) { return null; } if ( ResourceLoader.Exists( path ) ) { var texture = ResourceLoader.Load( path ); if ( texture != null ) { return texture.GetImage(); } } if ( ! FileAccess.FileExists( path ) ) { return null; } return Image.LoadFromFile( path ); } } }