Skip to content

Instantly share code, notes, and snippets.

@snowflower
Last active December 13, 2024 14:14
Show Gist options
  • Select an option

  • Save snowflower/4bd788f30e1ca2a0dbe55e454b160b4e to your computer and use it in GitHub Desktop.

Select an option

Save snowflower/4bd788f30e1ca2a0dbe55e454b160b4e to your computer and use it in GitHub Desktop.
Custom class of NoiseTexture2D that gives an option to bake the generated noise texture. See comments for revisions and original source this is based on.
@tool
extends NoiseTexture2D
class_name BakeableNoiseTexture2D
#
# Custom class for bakeable NoiseTexture2D resource
#
# based on code by @fabimakesgames.bsky.social
# https://gist.github.com/nan0m/c280761802c6dfc86533d1de89d27dae
# bsky post: https://bsky.app/profile/fabimakesgames.bsky.social/post/3ld4teamupk2i
#
# original post about the hidden costs of using unbaked noise textures is here:
# https://bsky.app/profile/christinacoffin.bsky.social/post/3ld3gyk323k2m
#
# Version History:
# 13/12/2024 : additions+documenting+verbose output added by christinacoffin.bsky.social
#
#
# you can press 'Pre Bake Check' to get detailed information about
# the bake before actually doing one, showing you what file will get written
# and where it will be located.
#
# 'Do Bake' does what it says. First time users may want to press 'Pre Bake First'
# to see what kind of verbose information is output regarding the bake.
#
# Added support for an export directory, verbose output to inform the user
# and handle cases of the noiseTexture and/or scene not being saved,
# and still allows a bake to a default filename.
#
# If the user resizes the texture to a larger size that takes time to generate,
# and then tries to bake it immediately,
# A warning will be given that the texture is not generated yet
# since the image dimensions, and the dimensions of the noise texture
# will not match since it still points to the old version of the image before resizing.
#
#
##
## Bakeable version of NoiseTexture2D , replace this with a Texture2D after baking!
##
@export_group("Bake Properties") # to group all this custom stuff at the bottom so its not in the way while editing the noise texture.
## Path to folder where the output .png baked noise textures will go.
## Don't forget to copy the .import files if you adjust the texture compression
## or import settings of the out .png files in this folder and move them elsewhere.
##
@export_dir var bake_folder_path = "res://noise/baked"
##
## After baking, remember to replace this node with the baked texture as Texture2D
## or you will still pay the startup/load computation cost of an unbaked texture
##
## choose bake extension
@export_enum("png", "jpg", "webp") var bake_format: String = "png"
## runs a check on baking the texture, creating the bake directory if it doesnt exist, and informs you about the filename, format and dimensions of the texture to be baked.
@export var preBakeCheck: bool = false:
set(value):
preBakeCheck = false
print("")
print("*** preBakeCheck start!")
bake_prep_and_execute(false)
emit_changed()
notify_property_list_changed()
#clickable button to execute the bake (can be exchanged with @tool_button when godot 4.4 is out)
## bakes the noise texture to a static image, creating the bake directory if it doesnt exist, and informs you about the filename, format and dimensions of the texture to be baked.
## NOTE: there will be a pause after clicking this without user feedback until the bake finishes.
@export var doBake: bool = false:
set(value):
doBake = false
print("")
print("*** doBake start!")
bake_prep_and_execute(true)
emit_changed()
notify_property_list_changed()
## name the baked texture will be saved as if the noise texture is not saved
## and/or not part of a saved scene.
## texture will be placed in the Bake Folder Path directory.
##
var defaultBakedName: String = "tmpBakedNoise2D"
##
## debug variable - tracking name that would be written for baked texture, updated when prebake or bake is run.
@export var _dbg_LastBakedName: String = "- unknown -"
##
## debug variable - track resource name , updated when prebake or bake is run.
@export var _dbg_resourceName: String = "- unknown -"
func bake_prep_and_execute( doTheBake = true ):
print("Tool script: ", get_script().resource_path)
print("class is ", get_script().get_global_name() )
print_debug("bake_prep_and_execute() with parameter : doTheBake = ", doTheBake)
#print("self : ", self)
#detect the case of a noisetexture being recently changed, but not
#fully generated yet.
# this can be detected by a size discrepancy between the dimensions of
# the noise texture, and the get_image()
#
#
var image = get_image()
if !image:
print("noise texture image does not yet exist ")
return
var currentNoiseTextureSize = get_size()
print("currentNoiseTextureSize ", get_width() ," " , get_height())
#
# WARNING:
# if the user resizes the noise texture ,
# image.get_size() will give us the last valid and generated image dimensions.
# but get_size() will give us the dimensions of the noise texture which can be different
# if the user resizes the noise texture and immediately presses bake
#
# you can replicate this by setting the noise texture to 1x1 size, which would regenerate
# instantly, but then change the noise texture to a large value (4096x4096)
# that would take several seconds and then press bake to see the mismatch between sizes
# when trying to do the bake.
# To handle this, we'll just abort the bake and tell the user to wait for the noise
# to finish generating, since I can't reliably detect the noise texture being generated
# with await
#
if( (get_width() != image.get_width()) || (get_height() != image.get_height()) ) :
print("noise texture still being generated, dimension mismatch")
print("image() = ", image.get_size() )
print("get_size() = ", get_size() )
print("")
print("*** wait for the noise texture to be loaded and verify its correct before baking. ***")
print("")
return
if image != null:
var imageSize = image.get_size()
var format = image.get_format()
var data = image.get_data()
if data:
print("baked texture output will be ", image.get_size() , " pixels in size. " )
else:
print("image data is not yet ready")
return
# output info about the format of the noise texture
# it always is rgba8 from testing.
#
# Note:We dont export the color ramp texture
#
if(format == Image.Format.FORMAT_RGBA8):
print("noise texture source is FORMAT_RGBA8")
else:
print("noise texture source is unknown image format")
else:
print("image is null - noise texture is still being generated! ")
return
#
# debug code to write something in the inspector panel
# to notify the user this noise texture isnt a saved resorce yet
#
_dbg_resourceName = self.resource_path
if not _dbg_resourceName:
_dbg_resourceName = " - is unsaved resource -"
#
# self.get_path() can be an empty string in the case of an unsaved texture
# (e.g.) ""
# or it will be the path to the asset as as a packed scene item that is not
# saved as its own file, such as
# (e.g.) res://testScene.tscn::NoiseTexture2D_f2lba
# or it will be the path to the asset in the filesystem if its explicitly saved
# (e.g.) res://someNoiseTexture.tres
#
# the following code below handles splitting the name apart if it exists in some form.
#
var split = self.get_path().rsplit("::") # split packed scene path
var splitAmt = split.size() # number of split text entries, will be 1 or 2
var uniqueName = split[max(0,splitAmt-1)] # gives us the rightmost string of the split if it happened to be 2
# handle file extension:
# if the texture file is saved instead of a unique resource with no resource path
# it may have an extension like .tres and an asset prefix such as res://
# remove those to get the base name
uniqueName = uniqueName.trim_suffix(".tres")
uniqueName = uniqueName.trim_prefix("res://")
# handle case where resource path is something like: res://textures/noise/amfield_damage_noise_texture_2d.tres
# and we have a unique string remaining such as 'textures/noise/amfield_damage_noise_texture_2d'
# so split and take the right side, checking for a / in the unique name.
var split2 = uniqueName.split("/")
var splitAmt2 = split2.size() # number of split text entries, will be 1 or 2
uniqueName = split2[max(0,splitAmt2-1)]
print("uniqueName ", uniqueName ) # in the case of unsaved resource, this will be an empty string.
_dbg_LastBakedName = uniqueName;
if not self.get_path().get_file(): # will be an empty string if not saved yet.
print("WARNING:")
print("trying to bake with a texture that is not saved.")
print("resource name, path and id are undefined for unsaved textures.")
print("Texture will be baked using the following placeholder name : ", defaultBakedName)
var outputPathAndFile;
var method: String = "save_" +bake_format
var ext: String = bake_format
if not ext:
print(" no bake extension specified - aborting bake ")
return
# make sure the output bake folder exists
var dirAccess = DirAccess.open("res://")
if ! dirAccess.dir_exists(bake_folder_path):
print("WARNING : ")
print("bake folder does not exist, creating... : ", bake_folder_path )
# some projects dont like make_dir_absolute(), use make_dir_recursive in the case of nested bake folder path.
dirAccess.make_dir_recursive(bake_folder_path)
if ! dirAccess.dir_exists(bake_folder_path):
print("failed to make output directory")
else:
print("output bake folder created : ", bake_folder_path)
var hasResourceName = false
if( self.resource_path == ""):
hasResourceName = false
print("texture has not been saved / has no resource name.")
print("save this texture or save the scene it is in first to save it with a name.")
outputPathAndFile = bake_folder_path + "/" + defaultBakedName + "." + ext
print(" output for baked noise will use this fallback output name and destination : ", outputPathAndFile )
if doTheBake:
self.get_image().call(method, outputPathAndFile )
_notifyUserTextureBakingDone()
else:
_notifyUserTheyPressedPrebake()
_dbg_LastBakedName = defaultBakedName # update panel
else:
hasResourceName = true
print("resource path exists : ", resource_path )
outputPathAndFile = bake_folder_path + "/" + uniqueName + "." + ext
print("output for baked noise will be : ", outputPathAndFile )
if doTheBake:
self.get_image().call(method, outputPathAndFile)
_notifyUserTextureBakingDone()
else:
_notifyUserTheyPressedPrebake()
_dbg_LastBakedName = uniqueName # update panel
_reScanFiles() # rescan files so we see the directory + files appear in the FileSystem panel
return
func _notifyUserTheyPressedPrebake():
print("user pressed 'preBakeCheck' : not writing out bake file.")
print("press 'Do Bake' to bake the texture.")
return
func _notifyUserTextureBakingDone():
print("Texture bake is done, remember to replace BakeableNoiseTexture2D")
print("with the baked Texture2D or have assets use another material with the baked textures.")
return
func _reScanFiles():
#
# Force the editor to rescan folders so we see the exported textures
# in the export folder.
# Otherwise godot wont rescan unless you change application focus to another window
# and then back to the editor, which is not good workflow.
#
var ei_rfs = EditorInterface.get_resource_filesystem()
ei_rfs.scan()
return
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment