129 lines
3.6 KiB
C#
129 lines
3.6 KiB
C#
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 <code>GetImage()</code> works directly on
|
|
a DrawableTexture2D, so none of the fallbacks the plan budgeted for
|
|
(RenderingServer.Texture2DGet, a viewport quad) are needed.
|
|
- Writing. <code>Procedural/Baking/Textures.Save</code> 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<Texture2D>( path );
|
|
|
|
if ( texture != null )
|
|
{
|
|
return texture.GetImage();
|
|
}
|
|
}
|
|
|
|
if ( ! FileAccess.FileExists( path ) )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return Image.LoadFromFile( path );
|
|
}
|
|
}
|
|
}
|