khanat-client/addons/zylann.hterrain/tools/packed_textures/stream_texture_importer.gd

434 lines
12 KiB
GDScript

tool
# TODO Godot does not have an API to make custom texture importers easier.
# So we have to re-implement the entire logic of `ResourceImporterTexture`.
# See https://github.com/godotengine/godot/issues/24381
const Result = preload("../util/result.gd")
const Errors = preload("../../util/errors.gd")
const Util = preload("../../util/util.gd")
const COMPRESS_LOSSLESS = 0
const COMPRESS_LOSSY = 1
const COMPRESS_VIDEO_RAM = 2
const COMPRESS_UNCOMPRESSED = 3
const COMPRESS_HINT_STRING = "Lossless,Lossy,VRAM,Uncompressed"
const REPEAT_NONE = 0
const REPEAT_ENABLED = 1
const REPEAT_MIRRORED = 2
const REPEAT_HINT_STRING = "None,Enabled,Mirrored"
# StreamTexture.FormatBits, not exposed to GDScript
const StreamTexture_FORMAT_MASK_IMAGE_FORMAT = (1 << 20) - 1
const StreamTexture_FORMAT_BIT_LOSSLESS = 1 << 20
const StreamTexture_FORMAT_BIT_LOSSY = 1 << 21
const StreamTexture_FORMAT_BIT_STREAM = 1 << 22
const StreamTexture_FORMAT_BIT_HAS_MIPMAPS = 1 << 23
const StreamTexture_FORMAT_BIT_DETECT_3D = 1 << 24
const StreamTexture_FORMAT_BIT_DETECT_SRGB = 1 << 25
const StreamTexture_FORMAT_BIT_DETECT_NORMAL = 1 << 26
static func import(
p_source_path: String,
image: Image,
p_save_path: String,
r_platform_variants: Array,
r_gen_files: Array,
p_contains_albedo: bool,
importer_name: String,
p_compress_mode: int,
p_repeat: int,
p_filter: bool,
p_mipmaps: bool,
p_anisotropic: bool) -> Result:
var compress_mode := p_compress_mode
var lossy := 0.7
var repeat := p_repeat
var filter := p_filter
var mipmaps := p_mipmaps
var anisotropic := p_anisotropic
var srgb := 1 if p_contains_albedo else 2
var fix_alpha_border := false
var premult_alpha := false
var invert_color := false
var stream := false
var size_limit := 0
var hdr_as_srgb := false
var normal := 0
var scale := 1.0
var force_rgbe := false
var bptc_ldr := 0
var detect_3d := false
var formats_imported := []
var tex_flags := 0
if repeat > 0:
tex_flags |= Texture.FLAG_REPEAT
if repeat == 2:
tex_flags |= Texture.FLAG_MIRRORED_REPEAT
if filter:
tex_flags |= Texture.FLAG_FILTER
if mipmaps or compress_mode == COMPRESS_VIDEO_RAM:
tex_flags |= Texture.FLAG_MIPMAPS
if anisotropic:
tex_flags |= Texture.FLAG_ANISOTROPIC_FILTER
if srgb == 1:
tex_flags |= Texture.FLAG_CONVERT_TO_LINEAR
if size_limit > 0 and (image.get_width() > size_limit or image.get_height() > size_limit):
#limit size
if image.get_width() >= image.get_height():
var new_width := size_limit
var new_height := image.get_height() * new_width / image.get_width()
image.resize(new_width, new_height, Image.INTERPOLATE_CUBIC)
else:
var new_height := size_limit
var new_width := image.get_width() * new_height / image.get_height()
image.resize(new_width, new_height, Image.INTERPOLATE_CUBIC)
if normal == 1:
image.normalize()
if fix_alpha_border:
image.fix_alpha_edges()
if premult_alpha:
image.premultiply_alpha()
if invert_color:
var height = image.get_height()
var width = image.get_width()
image.lock()
for i in width:
for j in height:
image.set_pixel(i, j, image.get_pixel(i, j).inverted())
image.unlock()
var detect_srgb := srgb == 2
var detect_normal := normal == 0
var force_normal := normal == 1
if compress_mode == COMPRESS_VIDEO_RAM:
#must import in all formats,
#in order of priority (so platform choses the best supported one. IE, etc2 over etc).
#Android, GLES 2.x
var ok_on_pc := false
var is_hdr := \
(image.get_format() >= Image.FORMAT_RF and image.get_format() <= Image.FORMAT_RGBE9995)
var is_ldr := \
(image.get_format() >= Image.FORMAT_L8 and image.get_format() <= Image.FORMAT_RGBA5551)
var can_bptc : bool = ProjectSettings.get("rendering/vram_compression/import_bptc")
var can_s3tc : bool = ProjectSettings.get("rendering/vram_compression/import_s3tc")
if can_bptc:
# return Result.new(false, "{0} cannot handle BPTC compression on {1}, " +
# "because the required logic is not exposed to the script API. " +
# "If you don't aim to export for a platform requiring BPTC, " +
# "you can turn it off in your ProjectSettings." \
# .format([importer_name, p_source_path])) \
# .with_value(ERR_UNAVAILABLE)
# Can't do this optimization because not exposed to GDScript
# var channels = image.get_detected_channels()
# if is_hdr:
# if channels == Image.DETECTED_LA or channels == Image.DETECTED_RGBA:
# can_bptc = false
# elif is_ldr:
# #handle "RGBA Only" setting
# if bptc_ldr == 1 and channels != Image.DETECTED_LA \
# and channels != Image.DETECTED_RGBA:
# can_bptc = false
#
formats_imported.push_back("bptc")
if not can_bptc and is_hdr and not force_rgbe:
#convert to ldr if this can't be stored hdr
image.convert(Image.FORMAT_RGBA8)
if can_bptc or can_s3tc:
_save_stex(
image,
p_save_path + ".s3tc.stex",
compress_mode,
lossy,
Image.COMPRESS_BPTC if can_bptc else Image.COMPRESS_S3TC,
mipmaps,
tex_flags,
stream,
detect_3d,
detect_srgb,
force_rgbe,
detect_normal,
force_normal,
false)
r_platform_variants.push_back("s3tc")
formats_imported.push_back("s3tc")
ok_on_pc = true
if ProjectSettings.get("rendering/vram_compression/import_etc2"):
_save_stex(
image,
p_save_path + ".etc2.stex",
compress_mode,
lossy,
Image.COMPRESS_ETC2,
mipmaps,
tex_flags,
stream,
detect_3d,
detect_srgb,
force_rgbe,
detect_normal,
force_normal,
true)
r_platform_variants.push_back("etc2")
formats_imported.push_back("etc2")
if ProjectSettings.get("rendering/vram_compression/import_etc"):
_save_stex(
image,
p_save_path + ".etc.stex",
compress_mode,
lossy,
Image.COMPRESS_ETC,
mipmaps,
tex_flags,
stream,
detect_3d,
detect_srgb,
force_rgbe,
detect_normal,
force_normal,
true)
r_platform_variants.push_back("etc")
formats_imported.push_back("etc")
if ProjectSettings.get("rendering/vram_compression/import_pvrtc"):
_save_stex(
image,
p_save_path + ".pvrtc.stex",
compress_mode,
lossy,
Image.COMPRESS_PVRTC4,
mipmaps,
tex_flags,
stream,
detect_3d,
detect_srgb,
force_rgbe,
detect_normal,
force_normal,
true)
r_platform_variants.push_back("pvrtc")
formats_imported.push_back("pvrtc")
if not ok_on_pc:
# TODO This warning is normally printed by `EditorNode::add_io_error`,
# which doesn't seem to be exposed to the script API
return Result.new(false,
"No suitable PC VRAM compression enabled in Project Settings. " +
"The texture {0} will not display correctly on PC.".format([p_source_path])) \
.with_value(ERR_INVALID_PARAMETER)
else:
#import normally
_save_stex(
image,
p_save_path + ".stex",
compress_mode,
lossy,
Image.COMPRESS_S3TC, #this is ignored,
mipmaps,
tex_flags,
stream,
detect_3d,
detect_srgb,
force_rgbe,
detect_normal,
force_normal,
false)
# TODO I have no idea what this part means, but it's not exposed to the script API either.
# if (r_metadata) {
# Dictionary metadata;
# metadata["vram_texture"] = compress_mode == COMPRESS_VIDEO_RAM;
# if (formats_imported.size()) {
# metadata["imported_formats"] = formats_imported;
# }
# *r_metadata = metadata;
# }
return Result.new(true).with_value(OK)
static func _save_stex(
p_image: Image,
p_fpath: String,
p_compress_mode: int, # ResourceImporterTexture.CompressMode
p_lossy_quality: float,
p_vram_compression: int, # Image.CompressMode
p_mipmaps: bool,
p_texture_flags: int,
p_streamable: bool,
p_detect_3d: bool,
p_detect_srgb: bool,
p_force_rgbe: bool,
p_detect_normal: bool,
p_force_normal: bool,
p_force_po2_for_compressed: bool
) -> Result:
# Need to work on a copy because we will modify it,
# but the calling code may have to call this function multiple times
p_image = p_image.duplicate()
var f = File.new()
var err = f.open(p_fpath, File.WRITE)
if err != OK:
return Result.new(false, "Could not open file {0}:\n{1}" \
.format([p_fpath, Errors.get_message(err)]))
f.store_8(ord('G'))
f.store_8(ord('D'))
f.store_8(ord('S'))
f.store_8(ord('T')) # godot streamable texture
var resize_to_po2 := false
if p_compress_mode == COMPRESS_VIDEO_RAM and p_force_po2_for_compressed \
and (p_mipmaps or p_texture_flags & Texture.FLAG_REPEAT):
resize_to_po2 = true
f.store_16(Util.next_power_of_two(p_image.get_width()))
f.store_16(p_image.get_width())
f.store_16(Util.next_power_of_two(p_image.get_height()))
f.store_16(p_image.get_height())
else:
f.store_16(p_image.get_width())
f.store_16(0)
f.store_16(p_image.get_height())
f.store_16(0)
f.store_32(p_texture_flags)
var format := 0
if p_streamable:
format |= StreamTexture_FORMAT_BIT_STREAM
if p_mipmaps:
format |= StreamTexture_FORMAT_BIT_HAS_MIPMAPS # mipmaps bit
if p_detect_3d:
format |= StreamTexture_FORMAT_BIT_DETECT_3D
if p_detect_srgb:
format |= StreamTexture_FORMAT_BIT_DETECT_SRGB
if p_detect_normal:
format |= StreamTexture_FORMAT_BIT_DETECT_NORMAL
if (p_compress_mode == COMPRESS_LOSSLESS or p_compress_mode == COMPRESS_LOSSY) \
and p_image.get_format() > Image.FORMAT_RGBA8:
p_compress_mode = COMPRESS_UNCOMPRESSED # these can't go as lossy
match p_compress_mode:
COMPRESS_LOSSLESS:
# Not required for our use case
# var image : Image = p_image.duplicate()
# if p_mipmaps:
# image.generate_mipmaps()
# else:
# image.clear_mipmaps()
var image := p_image
var mmc := _get_required_mipmap_count(image)
format |= StreamTexture_FORMAT_BIT_LOSSLESS
f.store_32(format)
f.store_32(mmc)
for i in mmc:
if i > 0:
image.shrink_x2()
#var data = Image::lossless_packer(image);
# This is actually PNG...
var data = image.save_png_to_buffer()
f.store_32(data.size() + 4)
f.store_8(ord('P'))
f.store_8(ord('N'))
f.store_8(ord('G'))
f.store_8(ord(' '))
f.store_buffer(data)
COMPRESS_LOSSY:
return Result.new(false,
"Saving a StreamTexture with lossy compression cannot be achieved by scripts.\n"
+ "Godot would need to either allow to save an image as WEBP to a buffer,\n"
+ "or expose `ResourceImporterTexture::_save_stex` so custom importers\n"
+ "would be easier to make.")
COMPRESS_VIDEO_RAM:
var image : Image = p_image.duplicate()
if resize_to_po2:
image.resize_to_po2()
if p_mipmaps:
image.generate_mipmaps(p_force_normal)
if p_force_rgbe \
and image.get_format() >= Image.FORMAT_R8 \
and image.get_format() <= Image.FORMAT_RGBE9995:
image.convert(Image.FORMAT_RGBE9995)
else:
var csource := Image.COMPRESS_SOURCE_GENERIC
if p_force_normal:
csource = Image.COMPRESS_SOURCE_NORMAL
elif p_texture_flags & VisualServer.TEXTURE_FLAG_CONVERT_TO_LINEAR:
csource = Image.COMPRESS_SOURCE_SRGB
image.compress(p_vram_compression, csource, p_lossy_quality)
format |= image.get_format()
f.store_32(format)
var data = image.get_data();
f.store_buffer(data)
COMPRESS_UNCOMPRESSED:
var image := p_image.duplicate()
if p_mipmaps:
image.generate_mipmaps()
else:
image.clear_mipmaps()
format |= image.get_format()
f.store_32(format)
var data = image.get_data()
f.store_buffer(data)
_:
return Result.new(false, "Invalid compress mode specified: {0}" \
.format([p_compress_mode]))
return Result.new(true)
# TODO Godot doesn't expose `Image.get_mipmap_count()`
# And the implementation involves shittons of unexposed code,
# so we have to fallback on a simplified version
static func _get_required_mipmap_count(image: Image) -> int:
var dim := max(image.get_width(), image.get_height())
return int(log(dim) / log(2) + 1)